use super::Template;
pub fn cargo_toml(name: &str, template: Template) -> String {
let rustango_dep = template.rustango_features();
format!(
r#"[package]
name = "{name}"
version = "0.1.0"
edition = "2021"
# Empty `[workspace]` table makes this project standalone: if a parent
# directory has its own workspace `Cargo.toml`, cargo would otherwise
# refuse to build (see "current package believes it's in a workspace
# when it's not"). This declaration severs that link without taking on
# any workspace members. Delete it if you intentionally want the
# project to be a member of a parent workspace.
[workspace]
[dependencies]
# `rustango` re-exports every proc-macro (Model / Form / ViewSet /
# Serializer / embed_migrations / main), so you do NOT need to depend
# on `rustango-macros` directly. Use `rustango::Model` etc.
rustango = {rustango_dep}
tokio = {{ version = "1", features = ["macros", "rt-multi-thread", "sync", "signal", "net"] }}
axum = {{ version = "0.8", default-features = false, features = ["tokio", "http1", "json", "form", "query"] }}
tower = {{ version = "0.5", features = ["util"] }}
serde = {{ version = "1", features = ["derive"] }}
serde_json = "1"
chrono = {{ version = "0.4", default-features = false, features = ["serde", "clock"] }}
tracing = "0.1"
tracing-subscriber = {{ version = "0.3", features = ["env-filter"] }}
dotenvy = "0.15"
[dev-dependencies]
tokio = {{ version = "1", features = ["macros", "rt-multi-thread"] }}
"#
)
}
pub fn env_example(name: &str) -> String {
format!(
"# Copy this file to .env and edit the values for your environment.
# `dotenvy::dotenv()` in src/main.rs picks it up at startup.
# Database name matches docker-compose.yml's POSTGRES_DB ({name}_dev).
DATABASE_URL=postgres://rustango:rustango@localhost:5432/{name}_dev
RUSTANGO_BIND=127.0.0.1:8080
# Tenancy template only — apex domain + signing secret.
RUSTANGO_APEX_DOMAIN=localhost
RUSTANGO_SESSION_SECRET=change-me-base64-encoded-32-bytes-or-more
"
)
}
pub const GITIGNORE: &str = "/target
/.env
*.log
";
pub const RUST_TOOLCHAIN: &str = "[toolchain]
channel = \"1.88\"
";
pub fn docker_compose(name: &str) -> String {
format!(
r#"services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: rustango
POSTGRES_PASSWORD: rustango
POSTGRES_DB: {name}_dev
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U rustango -d {name}_dev"]
interval: 2s
timeout: 2s
retries: 20
"#
)
}
pub fn readme(name: &str, template: Template) -> String {
let template_label = match template {
Template::Api => "api (bare ORM + axum, no admin)",
Template::Fullstack => "fullstack (ORM + auto-admin)",
Template::Tenant => "tenant (multi-tenancy + operator console)",
};
format!(
r#"# {name}
Generated with `cargo rustango new {name}` — template `{template_label}`.
## Run locally
```sh
cp .env.example .env
docker compose up -d
cargo run -- migrate # apply pending migrations
cargo run # boot the HTTP server
cargo run -- --help # full verb list (makemigrations, startapp, etc.)
```
`cargo run` (no args) is `runserver`. Every other Django-style verb
flows through the same binary via `rustango::manage::Cli` — see
`src/main.rs`.
## Project layout
```text
src/
main.rs — Cli::new().api(urls::api()).run() boots both server + verbs
models.rs — every #[derive(Model)] lives here
views.rs — request handlers (Django-style "views")
urls.rs — pub fn api() -> Router aggregator
migrations/ — JSON migration files (committed to git)
```
Adding a new model is one struct in `models.rs`; the auto-admin sees
it immediately. See <https://github.com/ujeenet/rustango> for the full
feature list.
"#
)
}
pub fn main_rs(template: Template) -> &'static str {
match template {
Template::Api => MAIN_RS_API,
Template::Fullstack => MAIN_RS_FULLSTACK,
Template::Tenant => MAIN_RS_TENANT,
}
}
const MAIN_RS_API: &str = "//! Project entrypoint — `Cli::run()` is the unified dispatcher
//! that handles `cargo run` (runserver) AND `cargo run -- migrate` /
//! `makemigrations` / `startapp` / etc. from one binary. No
//! `src/bin/manage.rs` needed.
mod models;
mod urls;
mod views;
#[rustango::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let _ = dotenvy::dotenv();
rustango::manage::Cli::new().api(urls::api()).run().await
}
";
const MAIN_RS_FULLSTACK: &str = "//! Project entrypoint — `Cli::run()` is the unified dispatcher
//! that handles `cargo run` (runserver) AND `cargo run -- migrate` /
//! `makemigrations` / `startapp` / etc. from one binary. No
//! `src/bin/manage.rs` needed. Auto-admin is mounted via
//! `urls::api()` which nests `admin_router(pool)` itself.
mod models;
mod urls;
mod views;
#[rustango::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let _ = dotenvy::dotenv();
rustango::manage::Cli::new().api(urls::api()).run().await
}
";
const MAIN_RS_TENANT: &str = r##"//! Tenant project entrypoint — HTTP server serving both the operator
//! console and per-tenant apps via subdomain routing.
mod models;
mod urls;
mod views;
#[rustango::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let _ = dotenvy::dotenv();
rustango::manage::Cli::new()
.tenancy()
.api(urls::api())
.run().await
}
"##;
pub fn models_rs(template: Template) -> String {
let header = "//! Project models — every #[derive(Model)] lives here.
//!
//! Adding a struct here makes it admin-visible automatically: the
//! macro populates the `inventory` registry that
//! `rustango::admin::router(pool)` walks.
use rustango::sql::Auto;
use rustango::Model;
#[derive(Model, Debug, Clone)]
#[rustango(table = \"item\", display = \"name\")]
pub struct Item {
#[rustango(primary_key)]
pub id: Auto<i64>,
#[rustango(max_length = 64)]
pub name: String,
pub active: bool,
}
";
if matches!(template, Template::Tenant) {
format!(
"{header}
// Tenancy registry models (Org, Operator, User) come along
// automatically via the `rustango::tenancy::*` import in
// src/bin/manage.rs — you don't need to redefine them here.
"
)
} else {
header.to_owned()
}
}
pub const VIEWS_RS: &str = "//! Project views — request handlers (Django-style \"views\").
use axum::response::Html;
pub async fn index() -> Html<&'static str> {
Html(
\"<!doctype html>\\n\\
<title>rustango app</title>\\n\\
<h1>Hello from rustango!</h1>\\n\\
<p>The auto-admin (if enabled) is at <a href=\\\"/admin\\\">/admin</a>.</p>\",
)
}
pub async fn healthz() -> &'static str {
\"ok\"
}
";
pub fn urls_rs(template: Template) -> String {
match template {
Template::Api => {
"//! Project URL routing (template: api — no admin).
//!
//! `Router::new()` is the auto-mount anchor — `manage startapp`
//! inserts `.merge(crate::<name>::urls::api())` lines here.
use axum::routing::get;
use axum::Router;
use crate::views;
pub fn api() -> Router<()> {
Router::new()
.route(\"/\", get(views::index))
.route(\"/healthz\", get(views::healthz))
}
"
.to_owned()
}
Template::Fullstack => {
"//! Project URL routing (template: fullstack — ORM + auto-admin).
//!
//! `Router::new()` in `api()` is the auto-mount anchor —
//! `manage startapp` inserts `.merge(crate::<name>::urls::api())`
//! lines here. The auto-admin is built separately via
//! `admin_router(pool)` and nested at `/admin` from `main.rs`.
use axum::routing::get;
use axum::Router;
use rustango::admin;
use rustango::sql::sqlx::PgPool;
use crate::views;
pub fn api() -> Router<()> {
Router::new()
.route(\"/\", get(views::index))
.route(\"/healthz\", get(views::healthz))
}
pub fn admin_router(pool: PgPool) -> Router {
admin::Builder::new(pool).build()
}
"
.to_owned()
}
Template::Tenant => {
"//! Project URL routing (template: tenant).
//!
//! `Builder::api(...)` mounts these routes on every tenant
//! subdomain alongside the auto-admin. Handlers can take
//! `rustango::extractors::Tenant` to resolve the current tenant +
//! get a tenant-scoped `&mut PgConnection`. Example:
//!
//! ```ignore
//! pub async fn list_items(mut t: rustango::extractors::Tenant)
//! -> Result<axum::Json<Vec<crate::models::Item>>, axum::http::StatusCode> {
//! let rows = crate::models::Item::objects()
//! .fetch_on(t.conn()).await
//! .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?;
//! Ok(axum::Json(rows))
//! }
//! ```
use axum::routing::get;
use axum::Router;
use crate::views;
pub fn api() -> Router<()> {
Router::new()
.route(\"/\", get(views::index))
.route(\"/healthz\", get(views::healthz))
}
"
.to_owned()
}
}
}
pub const BOOTSTRAP_REGISTRY_MIGRATION: &str =
include_str!("../templates/0001_rustango_registry_initial.json");
pub const BOOTSTRAP_TENANT_MIGRATION: &str =
include_str!("../templates/0001_rustango_tenant_initial.json");