rok-sqlc 0.7.0

Generate SQLx Rust database modules from SQL schema files
Documentation
//! The files at the root of `api.output`: `generated.rs`, which declares
//! every feature and mounts them, and `README.md`, which maps the files.

use super::feature::Feature;
use crate::generator::{FN_CALL_WIDTH, HEADER, MAX_WIDTH};
use crate::{ApiConfig, ApiFramework};

/// The generated marker of a Markdown file, which the writer recognizes as
/// it does [`HEADER`].
const MARKDOWN_HEADER: &str =
    "<!-- @generated by rok-sqlc. Do not edit by hand. Regenerated by `rok-sqlc g`. -->\n";

/// Renders `generated.rs` for the features' framework, with
/// `GeneratedApiDoc` under `[api] openapi`.
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),
    }
}

/// actix-web's `generated.rs`.
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 {
            // A long name: built first, so each statement wraps simply.
            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
}

/// `generated.rs`' items before its functions: each feature's module
/// declaration, then the responses other features' relationship routes
/// answer with.
pub fn push_modules(output: &mut String, features: &[Feature<'_>]) {
    // rustfmt sorts module declarations by name.
    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');
    }
    // The responses relationship routes answer related rows with: a
    // feature names another's response only through these, never its
    // module.
    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"));
        }
    }
}

/// Renders `README.md`.
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
}