use super::feature::{Endpoint, Feature, Key};
use super::framework::Status;
use super::layout::{Uses, push_call, push_signature};
use super::paths;
use super::relation_routes;
use super::scope;
use crate::ApiFramework;
use crate::generator::{FN_CALL_WIDTH, HEADER, MAX_WIDTH, push_doc};
use crate::schema::{BaseType, Column};
pub fn render_routes(feature: &Feature<'_>) -> String {
match feature.framework {
ApiFramework::Actix => render_actix_routes(feature),
ApiFramework::Axum => super::axum::render_routes(feature),
}
}
pub fn route_list(feature: &Feature<'_>) -> Vec<(String, &'static str, String)> {
let key_path = feature.key_path();
let mut routes: Vec<(String, &'static str, String)> = Vec::new();
for endpoint in &feature.endpoints {
let path = endpoint.path(&key_path);
routes.push((path, endpoint.method(), endpoint.name().to_owned()));
}
for lookup in &feature.lookups {
routes.push((lookup.path(), "get", lookup.function.clone()));
}
routes.extend(relation_routes::routes(feature));
routes.sort_by_key(|(path, _, _)| path.matches('{').count());
routes
}
fn render_actix_routes(feature: &Feature<'_>) -> String {
let mut output = format!(
"{HEADER}\n//! The `{}` routes, under `{}`.\n\nuse actix_web::web;\n\nuse super::handlers;\nuse crate::shared::validation::{{path_config, query_config}};\n\n",
feature.table.display_name(),
feature.url
);
push_doc(
&mut output,
0,
&[&format!(
"Mounts every `{}` endpoint under `{}`.",
feature.table.display_name(),
feature.url
)],
);
output.push_str("pub fn routes(cfg: &mut web::ServiceConfig) {\n cfg.service(\n");
output.push_str(&format!(
" web::scope({})\n .app_data(path_config())\n .app_data(query_config())",
crate::generator::sql::str_literal(&feature.url)
));
for (path, method, handler) in &route_list(feature) {
let to = format!("web::{method}().to(handlers::{handler})");
let one_line = format!(" .route(\"{path}\", {to})");
let args = format!("\"{path}\", {to}");
if one_line.len() <= MAX_WIDTH && args.len() <= FN_CALL_WIDTH {
output.push('\n');
output.push_str(&one_line);
} else {
output.push_str(&format!(
"\n .route(\n \"{path}\",\n {to},\n )"
));
}
}
output.push_str(",\n );\n}\n");
output
}
pub fn render_handlers(feature: &Feature<'_>) -> String {
let mut uses = Uses::default();
feature.framework.handler_uses(&mut uses);
uses.add(&format!("super::dto::{}", feature.response()));
uses.add(&format!("super::service::{}", feature.service()));
uses.add(&format!("super::service_api::{}", feature.service_api()));
uses.add("crate::error::AppError");
let mut body = String::new();
for endpoint in &feature.endpoints {
body.push('\n');
render_handler(&mut body, &mut uses, feature, *endpoint);
}
for lookup in &feature.lookups {
body.push('\n');
render_lookup(&mut body, &mut uses, feature, lookup);
}
relation_routes::render_handlers(&mut body, &mut uses, feature);
let mut output = format!(
"{HEADER}\n//! The `{}` handlers: each parses the request, calls one\n//! [`{}`] method, and answers.\n\n",
feature.table.display_name(),
feature.service_api()
);
output.push_str(&uses.render());
output.push_str(body.trim_start_matches('\n'));
output
}
pub fn key_columns<'a>(feature: &Feature<'a>) -> Vec<&'a Column> {
match &feature.key {
None => Vec::new(),
Some(Key::Single(column)) => vec![column],
Some(Key::Composite(columns)) => columns.clone(),
}
}
fn needs_encoding(column: &Column) -> bool {
!matches!(column.base_type(), BaseType::Integer | BaseType::Uuid)
}
pub fn own_scope(feature: &Feature<'_>, uses: &mut Uses) -> scope::HandlerScope {
scope::handler(feature, uses, feature.tenant, feature.caller())
}
fn render_handler(output: &mut String, uses: &mut Uses, feature: &Feature<'_>, endpoint: Endpoint) {
let name = endpoint.name();
let path = endpoint.path(&feature.key_path());
let what = match endpoint {
Endpoint::List => "one page of rows, `200`.",
Endpoint::Search => "one page of the rows the body's filter matches, `200`.",
Endpoint::Create if feature.key.is_some() => "creates a row, `201` with its `Location`.",
Endpoint::Create => "creates a row, `201`.",
Endpoint::GetOne => "the row, `200`.",
Endpoint::Update => "changes the row, `200` with it.",
Endpoint::Remove if feature.soft_delete => {
"soft-deletes the row (deletes it with `?permanent=true`), `204`."
}
Endpoint::Remove => "deletes the row, `204`.",
Endpoint::Restore => "restores the soft-deleted row, `200` with it.",
};
let mut doc = vec![format!(
"`{} {}{path}`: {what}",
endpoint.http_method(),
feature.url
)];
if endpoint == Endpoint::Update && feature.version.is_some() {
doc.push("`If-Match` must hold the row's current `ETag`.".to_owned());
}
let includes = endpoint == Endpoint::GetOne && !feature.includes().is_empty();
if includes {
doc.push("`?include=` embeds a to-one relationship.".to_owned());
}
let doc: Vec<&str> = doc.iter().map(String::as_str).collect();
push_doc(output, 0, &doc);
paths::push_endpoint(output, uses, feature, endpoint);
if includes {
relation_routes::render_get_one(output, uses, feature);
return;
}
let framework = feature.framework;
let tenant = feature.tenant_of(endpoint);
let scope = scope::handler(feature, uses, tenant, feature.caller_of(endpoint));
let mut params = vec![framework.service_param(&feature.service())];
params.extend(scope.params);
let key = feature.key_type().unwrap_or_default();
let path = framework.path("key", &key);
if endpoint.takes_key() {
uses.add(&format!("super::model::{key}"));
params.push(path.param(uses));
}
let mut query = None;
match endpoint {
Endpoint::List => {
uses.add(&format!("super::dto::{}", feature.list_query()));
query = Some(framework.query(&feature.list_query()));
}
Endpoint::Search => {
uses.add("crate::shared::validation::ValidatedJson");
uses.add(&format!("super::dto::{}", feature.search_body()));
params.push(format!("body: ValidatedJson<{}>", feature.search_body()));
}
Endpoint::Create => {
uses.add("crate::shared::validation::ValidatedJson");
uses.add(&format!("super::dto::{}", feature.create_dto()));
params.push(format!("body: ValidatedJson<{}>", feature.create_dto()));
}
Endpoint::Update => {
if let Some(version) = feature.version {
uses.add("crate::shared::validation::IfMatch");
params.push(format!("version: IfMatch<{}>", version.value_type()));
}
uses.add("crate::shared::validation::ValidatedJson");
uses.add(&format!("super::dto::{}", feature.update_dto()));
params.push(format!("body: ValidatedJson<{}>", feature.update_dto()));
}
Endpoint::Remove if feature.soft_delete => {
uses.add(&format!("super::dto::{}", feature.remove_query()));
query = Some(framework.query(&feature.remove_query()));
}
Endpoint::GetOne | Endpoint::Remove | Endpoint::Restore => {}
}
params.extend(query.as_ref().map(|query| query.param(uses)));
push_signature(
output,
0,
&format!("pub async fn {name}"),
¶ms,
framework.returns(),
);
let mut args = scope.args;
match endpoint {
Endpoint::List | Endpoint::Search => {
let input = match &query {
Some(query) => query.value().to_owned(),
None => "body.into_inner()".to_owned(),
};
args.push(input);
let callee = format!("service.{name}");
push_call(output, 4, "let page = ", &callee, &args, ".await?;");
output.push_str(&format!(
" let response = page.map({}::from);\n",
feature.response()
));
framework.push_answer(output, uses, Status::Ok, &[], "response");
}
Endpoint::Remove => {
path.push_let(output, "key");
args.push("key".to_owned());
if feature.soft_delete {
output.push_str(" if query.permanent {\n");
let callee = "service.remove_permanently";
push_call(output, 8, "", callee, &args, ".await?;");
output.push_str(" } else {\n");
push_call(output, 8, "", "service.remove", &args, ".await?;");
output.push_str(" }\n");
} else {
push_call(output, 4, "", "service.remove", &args, ".await?;");
}
framework.push_no_content(output, uses);
}
_ => {
match endpoint {
Endpoint::Create => args.push("body.into_inner()".to_owned()),
Endpoint::Update => {
path.push_let(output, "key");
args.push("key".to_owned());
if feature.version.is_some() {
args.push("version.into_inner()".to_owned());
}
args.push("body.into_inner()".to_owned());
}
_ => args.push(path.value().to_owned()),
}
let callee = format!("service.{name}");
push_call(output, 4, "let row = ", &callee, &args, ".await?;");
let status = if endpoint == Endpoint::Create {
Status::Created
} else {
Status::Ok
};
push_row_answer(output, uses, feature, status);
}
}
output.push_str("}\n");
}
fn render_lookup(
output: &mut String,
uses: &mut Uses,
feature: &Feature<'_>,
lookup: &super::feature::Lookup<'_>,
) {
push_doc(
output,
0,
&[&format!(
"`GET {}{}`: the row whose `{}` is the value, `200`.",
feature.url,
lookup.path(),
lookup.column.sql_name
)],
);
paths::push_lookup(output, uses, feature, lookup);
let framework = feature.framework;
let scope = own_scope(feature, uses);
let mut params = vec![framework.service_param(&feature.service())];
params.extend(scope.params);
scope::import_type(feature, uses, lookup.column);
let path = framework.path("value", lookup.column.value_type());
params.push(path.param(uses));
push_signature(
output,
0,
&format!("pub async fn {}", lookup.function),
¶ms,
framework.returns(),
);
let mut args = scope.args;
args.push(path.value().to_owned());
let callee = format!("service.{}", lookup.function);
push_call(output, 4, "let row = ", &callee, &args, ".await?;");
push_row_answer(output, uses, feature, Status::Ok);
output.push_str("}\n");
}
pub fn push_row_answer(
output: &mut String,
uses: &mut Uses,
feature: &Feature<'_>,
status: Status,
) {
let mut headers = Vec::new();
if status == Status::Created && feature.key.is_some() {
push_location(output, uses, feature);
headers.push("(header::LOCATION, location)");
}
if let Some(version) = feature.version {
uses.add("crate::shared::response::etag");
output.push_str(&format!(
" let etag = etag(&row.{});\n",
version.rust_name
));
headers.push("(header::ETAG, etag)");
}
output.push_str(&format!(
" let response = {}::from(row);\n",
feature.response()
));
feature
.framework
.push_answer(output, uses, status, &headers, "response");
}
fn push_location(output: &mut String, uses: &mut Uses, feature: &Feature<'_>) {
let columns = key_columns(feature);
let template: String = columns.iter().map(|_| "/{}").collect();
let args: Vec<String> = columns
.iter()
.map(|column| {
if needs_encoding(column) {
uses.add("crate::shared::response::path_segment");
format!("path_segment(&row.{})", column.rust_name)
} else {
format!("row.{}", column.rust_name)
}
})
.collect();
let mut all = vec![format!("\"{}{template}\"", feature.url)];
all.extend(args);
let joined = all.join(", ");
let one_line = format!(" let location = format!({joined});");
if joined.len() <= FN_CALL_WIDTH && one_line.len() <= MAX_WIDTH {
output.push_str(&one_line);
output.push('\n');
return;
}
output.push_str(&format!(
" let location = format!(\n {}\n );\n",
all.join(",\n ")
));
}