use std::io::Write;
use crate::tenancy::error::TenancyError;
use crate::tenancy::manage::args::next_value;
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" => {
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)?;
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 {
#[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}"
);
}
}
#[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}");
}
#[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}");
}
}