use super::feature::Feature;
use crate::generator::{FN_CALL_WIDTH, HEADER, MAX_WIDTH};
use crate::{ApiConfig, ApiFramework};
const MARKDOWN_HEADER: &str =
"<!-- @generated by rok-sqlc. Do not edit by hand. Regenerated by `rok-sqlc g`. -->\n";
pub fn render_generated(features: &[Feature<'_>], api: &ApiConfig) -> String {
match api.framework {
ApiFramework::Actix => render_actix_generated(features, api),
ApiFramework::Axum => super::axum::render_generated(features, api),
}
}
fn render_actix_generated(features: &[Feature<'_>], api: &ApiConfig) -> String {
let mut output = format!(
"{HEADER}
//! Every generated API feature, declared from its own folder, and the two
//! functions `app.rs` calls: [`register_services`] and [`configure_all`].
use actix_web::web;
use sqlx::PgPool;
"
);
if api.openapi {
output.push_str(super::api_doc::USES);
}
push_modules(&mut output, features);
let (cfg, pool) = if features.is_empty() {
("_cfg", "_pool")
} else {
("cfg", "pool")
};
output.push_str(&format!(
"\n/// Registers every feature's service as app data, each over `pool`.\npub fn register_services({cfg}: &mut web::ServiceConfig, {pool}: &PgPool) {{\n"
));
for feature in features {
let service = format!("{}::{}", feature.module(), feature.service());
let data = format!("web::Data::new({service}::new(pool.clone()))");
let line = format!(" cfg.app_data({data});");
if data.len() <= FN_CALL_WIDTH && line.len() <= MAX_WIDTH {
output.push_str(&line);
output.push('\n');
} else {
let call = format!("{service}::new(pool.clone());");
if format!(" let service = {call}").len() <= MAX_WIDTH {
output.push_str(&format!(" let service = {call}\n"));
} else if format!(" {call}").len() <= MAX_WIDTH {
output.push_str(&format!(" let service =\n {call}\n"));
} else {
output.push_str(&format!(
" let service =\n {service}::new(\n pool.clone(),\n );\n"
));
}
output.push_str(" cfg.app_data(web::Data::new(service));\n");
}
}
output.push_str(&format!(
"}}\n\n/// Mounts every feature's routes.\npub fn configure_all({cfg}: &mut web::ServiceConfig) {{\n"
));
for feature in features {
output.push_str(&format!(" {}::routes(cfg);\n", feature.module()));
}
output.push_str("}\n");
if api.openapi {
output.push_str(&super::api_doc::render(features));
}
output
}
pub fn push_modules(output: &mut String, features: &[Feature<'_>]) {
let mut modules: Vec<&str> = features.iter().map(Feature::module).collect();
modules.sort_unstable();
for module in modules {
output.push_str(&format!(
"\n#[path = \"{module}/mod.rs\"]\npub mod {module};"
));
}
if !features.is_empty() {
output.push('\n');
}
let mut related: Vec<(&str, String)> = features
.iter()
.flat_map(|feature| {
feature
.relations
.iter()
.filter(|relation| relation.target.module_name != feature.module())
.map(|relation| (relation.target.module_name.as_str(), relation.response()))
})
.collect();
related.sort();
related.dedup();
if !related.is_empty() {
output.push_str("\n// The responses other features' relationship routes answer with.\n");
for (module, response) in related {
output.push_str(&format!("pub use {module}::dto::{response};\n"));
}
}
}
pub fn render_readme(features: &[Feature<'_>], framework: ApiFramework) -> String {
let wiring = framework.wiring();
let mut output = format!(
"{MARKDOWN_HEADER}
# API features
`rok-sqlc g` generates one folder per table here, from the schema and the
`[api]` section of `rok-sqlc.toml`. Every file starting with
`// @generated by rok-sqlc` is rewritten on each run, and removed once its
table is gone: don't edit it. `service.rs` starts with
`// Scaffolded once by rok-sqlc`: it is written once, then it is yours, and
rok-sqlc never touches it again.
| File | Owner | Holds |
|---|---|---|
| `generated.rs` | generated | declares the features; {wiring} |
| `<feature>/mod.rs` | generated | the module list; re-exports `routes`, the service and its trait |
| `<feature>/routes.rs` | generated | URL to handler |
| `<feature>/handlers.rs` | generated | parse, call one service method, answer |
| `<feature>/dto.rs` | generated | request and response bodies |
| `<feature>/model.rs` | generated | the rows and typed ids, from `db` |
| `<feature>/repo.rs` | generated | the table's `db` functions |
| `<feature>/service_api.rs` | generated | the service trait, a default per endpoint |
| `<feature>/service.rs` | **yours** | the service and your overrides: business rules go here |
## Features
"
);
if features.is_empty() {
output.push_str("\nNone: every table is excluded.\n");
} else {
output.push_str("\n| Feature | URL | Endpoints |\n|---|---|---|\n");
}
for feature in features {
let endpoints: Vec<String> = feature
.endpoints
.iter()
.map(|endpoint| format!("`{}`", endpoint.name()))
.collect();
output.push_str(&format!(
"| `{}` | `{}` | {} |\n",
feature.module(),
feature.url,
endpoints.join(", ")
));
}
output
}