rustango 0.61.13

Batteries-included web framework for Rust: ORM + migrations + auto-admin + multi-tenancy + audit log + auth (sessions, JWT, OAuth2/OIDC, HMAC) + APIs (ViewSet, OpenAPI auto-derive, JSON:API) + jobs (in-mem + Postgres) + email + media (S3 / R2 / B2 / MinIO + presigned uploads + collections + tags) + production middleware (CSRF, CSP, rate-limiting, compression, idempotency, etc.).
Documentation
//! Tenancy variant of `startapp` — same file layout as
//! `rustango::migrate::manage startapp`, with one twist: when
//! `--with-manage-bin` is passed, the generated `src/bin/manage.rs`
//! runs `Cli::new().tenancy()` instead of the single-tenant `Cli`.
//! Useful for bootstrapping multi-tenant projects.
//!
//! Falls through to [`rustango::migrate::scaffold::startapp`] for the
//! actual file writes — this module just owns the tenancy template
//! and the argv parser.

use std::io::Write;

use crate::tenancy::error::TenancyError;
use crate::tenancy::manage::args::next_value;

/// Tenancy-aware `manage.rs` template. `Cli::tenancy()` makes the binary
/// recognize `create-tenant` / `migrate-tenants` / `run-server` / etc. on
/// top of the standard `migrate` / `makemigrations` set.
pub const TENANCY_MANAGE_BIN: &str =
    "//! Generated by `manage startapp --with-manage-bin` (tenancy-aware).
//!
//! UX: `cargo run --bin manage -- create-tenant <slug>`, `-- run-server`,
//! plus every single-tenant verb. `Cli` resolves the registry URL
//! (`DATABASE_URL` or `DB_*`) and serves `register_command!` verbs too.

// A bin is its own crate: name your library so `inventory` sees its models.
//   use my_app as _;

// Errors print with Display, so a multi-line CLI error reads as lines.
#[tokio::main]
async fn main() -> std::process::ExitCode {
    if let Err(e) = rustango::manage::Cli::new().tenancy().run().await {
        eprintln!(\"Error: {e}\");
        return std::process::ExitCode::FAILURE;
    }
    std::process::ExitCode::SUCCESS
}
";

pub(crate) fn startapp_cmd<W: Write>(args: &[String], w: &mut W) -> Result<(), TenancyError> {
    let mut iter = args.iter();
    let app_name = iter
        .next()
        .cloned()
        .ok_or_else(|| TenancyError::Validation(usage()))?;
    let mut with_manage_bin = false;
    let mut with_bootstrap = false;
    let mut into: Option<String> = None;
    while let Some(flag) = iter.next() {
        match flag.as_str() {
            "--with-manage-bin" => with_manage_bin = true,
            "--with-bootstrap-migration" => with_bootstrap = true,
            "--into" => {
                into = Some(next_value(&mut iter, "--into")?);
            }
            "--help" | "-h" => {
                writeln!(w, "{}", usage())?;
                return Ok(());
            }
            "--app-name" => {
                // Belt-and-braces: accept --app-name <X> as an alias
                // even though the positional form is canonical.
                let _ = next_value(&mut iter, "--app-name")?;
            }
            other => {
                return Err(TenancyError::Validation(format!(
                    "startapp: unknown argument `{other}` (run --help for usage)"
                )));
            }
        }
    }
    let base_label = into.clone().unwrap_or_else(|| "src".into());
    let opts = rustango::migrate::scaffold::StartAppOptions {
        app_name: app_name.clone(),
        manage_bin: with_manage_bin.then_some(TENANCY_MANAGE_BIN),
        base_dir: into.map(std::path::PathBuf::from),
        crate_root: None,
    };
    let cwd = std::env::current_dir().map_err(TenancyError::Io)?;
    let report =
        rustango::migrate::scaffold::startapp(&cwd, &opts).map_err(TenancyError::Migrate)?;

    // Optional: drop the framework's registry+tenant bootstrap
    // migrations into the new app's `migrations/` subdirectory so a
    // fresh project is `cargo run`-ready in one command — no separate
    // `init-tenancy` step.
    let bootstrap_report = if with_bootstrap {
        let migrations_dir = cwd
            .join(
                opts.base_dir
                    .clone()
                    .unwrap_or_else(|| std::path::PathBuf::from("src")),
            )
            .join(&app_name)
            .join("migrations");
        Some(crate::tenancy::init_tenancy(&migrations_dir)?)
    } else {
        None
    };

    write_report(
        w,
        &app_name,
        &base_label,
        &report,
        bootstrap_report.as_ref(),
    )
}

fn write_report<W: Write>(
    w: &mut W,
    app_name: &str,
    base_label: &str,
    report: &rustango::migrate::scaffold::StartAppReport,
    bootstrap: Option<&crate::tenancy::InitTenancyReport>,
) -> Result<(), TenancyError> {
    if report.written.is_empty()
        && report.skipped.is_empty()
        && bootstrap.is_none_or(|b| b.written.is_empty() && b.skipped.is_empty())
    {
        writeln!(w, "startapp: nothing to do")?;
        return Ok(());
    }
    writeln!(w, "startapp `{app_name}` (tenancy-aware)")?;
    for path in &report.written {
        writeln!(w, "  + wrote {path}")?;
    }
    for path in &report.skipped {
        writeln!(w, "  · {path} already exists — left untouched")?;
    }
    for path in &report.patched {
        writeln!(w, "  ~ patched {path} (auto-mounted new app)")?;
    }
    for hint in &report.manual_steps {
        writeln!(w, "  ! manual: {hint}")?;
    }
    if let Some(b) = bootstrap {
        let migrations_rel = format!("{base_label}/{app_name}/migrations");
        for name in &b.written {
            writeln!(w, "  + wrote {migrations_rel}/{name}.json")?;
        }
        for name in &b.skipped {
            writeln!(
                w,
                "  · {migrations_rel}/{name}.json already exists — left untouched"
            )?;
        }
    }
    if !report.written.is_empty() {
        writeln!(w, "next:")?;
        writeln!(w, "  {}", crate::migrate::scaffold::STARTAPP_NEXT)?;
    }
    Ok(())
}

fn usage() -> String {
    "startapp <name> [--into <dir>] [--with-manage-bin] [--with-bootstrap-migration]\n  \
     Scaffold an app module under <dir>/<name>/ (mod.rs +\n  \
     models.rs + views.rs + urls.rs). Idempotent: existing files are\n  \
     left untouched. <name> must be a valid Rust identifier.\n\n  \
     --into <dir>\n  \
     Override the default `src/` directory the app lands in. Use\n  \
     for non-standard layouts — e.g. `--into examples/blog_demo`\n  \
     for an in-tree example dir, or `--into crates/web` for a\n  \
     workspace member with no `src/` parent. Default: `src`.\n\n  \
     --with-manage-bin\n  \
     Also write <dir>/bin/manage.rs with the **tenancy-aware**\n  \
     dispatcher boilerplate (crate::tenancy::manage::run). Skipped\n  \
     if the file already exists.\n\n  \
     --with-bootstrap-migration\n  \
     Also drop the framework's registry+tenant bootstrap migrations\n  \
     into <dir>/<name>/migrations/. Pair with\n  \
     `Builder::migrate(\"<dir>/<name>/migrations\")` and the project\n  \
     is `cargo run`-ready out of the gate — no separate `init-tenancy`\n  \
     step. Idempotent."
        .to_owned()
}

#[cfg(test)]
mod tests {
    /// The bin is its own crate, and a `Result` main prints its error with Debug.
    #[test]
    fn manage_bins_print_errors_with_display() {
        for bin in [
            super::TENANCY_MANAGE_BIN,
            crate::migrate::scaffold::SINGLE_TENANT_MANAGE_BIN,
        ] {
            assert!(
                bin.contains("async fn main() -> std::process::ExitCode"),
                "{bin}"
            );
            assert!(bin.contains("eprintln!(\"Error: {e}\")"), "{bin}");
            assert!(
                !bin.contains("crate::"),
                "a bin can't name the library as crate:\n{bin}"
            );
        }
    }

    /// No `mod` line for a patched entry, and no no-op `init-tenancy` step.
    #[test]
    fn report_hints_only_the_steps_left_to_do() {
        let report = crate::migrate::scaffold::StartAppReport {
            written: vec!["src/shop/mod.rs".into()],
            patched: vec!["src/lib.rs".into()],
            ..Default::default()
        };
        let mut out = Vec::new();
        super::write_report(&mut out, "shop", "src", &report, None).unwrap();
        let out = String::from_utf8(out).unwrap();
        assert!(!out.contains("add `mod"), "{out}");
        assert!(!out.contains("init-tenancy"), "{out}");
        assert!(out.contains("makemigrations"), "{out}");
    }

    /// Like the single-tenant bin: `Cli` resolves the URL and the verbs.
    #[test]
    fn tenancy_manage_bin_goes_through_cli() {
        let bin = super::TENANCY_MANAGE_BIN;
        assert!(bin.contains("rustango::manage::Cli::new()"), "{bin}");
        assert!(bin.contains(".tenancy()"), "{bin}");
        assert!(!bin.contains("env::var("), "{bin}");
        assert!(!bin.contains("tenancy::manage::run"), "{bin}");
    }
}