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"
[dependencies]
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"] }}
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 const ENV_EXAMPLE: &str = "# Copy this file to .env and edit the values for your environment.
# `dotenvy::dotenv()` in src/bin/manage.rs picks it up at startup.
DATABASE_URL=postgres://rustango:rustango@localhost:5432/rustango_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 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 --bin manage -- migrate # apply pending migrations
cargo run # boot the HTTP server
```
## Project layout
```text
src/
main.rs — boots the binary, wires the router
models.rs — every #[derive(Model)] lives here
views.rs — request handlers (Django-style "views")
urls.rs — pub fn router(pool) -> Router mapping paths → views
bin/manage.rs — Django-style migration / scaffolding CLI
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 const MAIN_RS: &str = "//! Project entrypoint — boots the HTTP server.
//!
//! `manage.rs` (under src/bin/) handles migrations + scaffolding;
//! this binary is just the runtime web server.
mod models;
mod urls;
mod views;
use rustango::sql::sqlx::PgPool;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Load .env (or any ancestor) so DATABASE_URL et al. are set
// without re-exporting each session.
let _ = dotenvy::dotenv();
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(\"info\")),
)
.init();
let url = std::env::var(\"DATABASE_URL\")?;
let pool = PgPool::connect(&url).await?;
let app = urls::router(pool);
let bind = std::env::var(\"RUSTANGO_BIND\").unwrap_or_else(|_| \"127.0.0.1:8080\".into());
let listener = tokio::net::TcpListener::bind(&bind).await?;
eprintln!(\"server listening on http://{}\", listener.local_addr()?);
axum::serve(listener, app).await?;
Ok(())
}
";
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).
use axum::routing::get;
use axum::Router;
use rustango::sql::sqlx::PgPool;
use crate::views;
pub fn router(_pool: PgPool) -> Router {
Router::new()
.route(\"/\", get(views::index))
.route(\"/healthz\", get(views::healthz))
}
"
.to_owned()
}
Template::Fullstack => {
"//! Project URL routing (template: fullstack — ORM + auto-admin).
use axum::routing::get;
use axum::Router;
use rustango::admin;
use rustango::sql::sqlx::PgPool;
use crate::views;
pub fn router(pool: PgPool) -> Router {
let admin = admin::Builder::new(pool.clone()).build();
Router::new()
.route(\"/\", get(views::index))
.route(\"/healthz\", get(views::healthz))
.with_state(pool)
.nest(\"/admin\", admin)
}
"
.to_owned()
}
Template::Tenant => {
"//! Project URL routing (template: tenant — multi-tenancy enabled).
//!
//! For production wiring, the recommended entrypoint is
//! `cargo run --bin manage -- run-server` which boots the operator
//! console at the apex + tenant admin at every subdomain via
//! host-based dispatch. This file is the simpler dev-server form.
use axum::routing::get;
use axum::Router;
use rustango::sql::sqlx::PgPool;
use crate::views;
pub fn router(_pool: PgPool) -> Router {
Router::new()
.route(\"/\", get(views::index))
.route(\"/healthz\", get(views::healthz))
}
"
.to_owned()
}
}
}
pub fn manage_rs(template: Template) -> String {
match template {
Template::Api | Template::Fullstack => {
"//! Generated by `cargo rustango new`. Edit freely.
//!
//! UX: `cargo run --bin manage -- migrate`,
//! `cargo run --bin manage -- makemigrations`,
//! `cargo run --bin manage -- startapp <name>`, etc.
use rustango::sql::sqlx::PgPool;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Pull your models into this binary so `inventory` registers
// them. Keep this in sync with src/main.rs's `mod models;`.
#[allow(unused_imports)]
use crate::models::*;
let _ = dotenvy::dotenv();
let pool = PgPool::connect(&std::env::var(\"DATABASE_URL\")?).await?;
let dir: &std::path::Path = \"./migrations\".as_ref();
rustango::migrate::manage::run(&pool, dir, std::env::args().skip(1)).await?;
Ok(())
}
// `cargo run --bin manage` is a separate binary; pull the project's
// own crate root into scope as a dependency for the model imports
// above. Cargo gives every `[[bin]]` access to the lib via the crate
// name; for binary-only projects we use a path module instead.
mod models {
include!(\"../models.rs\");
}
"
.to_owned()
}
Template::Tenant => {
"//! Generated by `cargo rustango new --template tenant`. Edit freely.
//!
//! Tenancy-aware dispatcher: `create-tenant`, `migrate-tenants`,
//! `run-server`, `create-operator`, `create-user`, plus everything
//! the single-tenant `manage` offers.
use rustango::sql::sqlx::PgPool;
use rustango::tenancy::TenantPools;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let _ = dotenvy::dotenv();
let registry_url = std::env::var(\"DATABASE_URL\")?;
let pool = PgPool::connect(®istry_url).await?;
let pools = TenantPools::new(pool);
let dir: &std::path::Path = \"./migrations\".as_ref();
rustango::tenancy::manage::run(&pools, ®istry_url, dir, std::env::args().skip(1)).await?;
Ok(())
}
"
.to_owned()
}
}
}