pub const FRAMEWORK_VERSION: &str = "0.9";
pub const GATEWAY_VERSION: &str = "0.9";
pub struct TemplateFile {
pub path: &'static str,
pub contents: &'static str,
}
macro_rules! project_file {
($path:literal) => {
project_file!($path, $path)
};
($path:literal, $stub:literal) => {
TemplateFile {
path: $path,
contents: include_str!(concat!("../templates/project/", $stub, ".stub")),
}
};
}
pub const PROJECT: &[TemplateFile] = &[
project_file!("Cargo.toml"),
project_file!(".env.example"),
project_file!(".gitignore"),
project_file!("README.md"),
project_file!("AGENTS.md"),
project_file!("CLAUDE.md"),
project_file!("Toasty.toml"),
project_file!("src/lib.rs"),
project_file!("src/main.rs"),
project_file!("src/app.rs"),
project_file!("src/base/mod.rs"),
project_file!("src/base/config.rs"),
project_file!("src/base/error.rs"),
project_file!("src/base/state.rs"),
project_file!("src/base/auth.rs"),
project_file!("src/base/mcp.rs"),
project_file!("src/modules/mod.rs"),
project_file!("src/modules/user/mod.rs"),
project_file!("src/modules/user/models/mod.rs"),
project_file!("src/modules/user/models/user.rs"),
project_file!("src/modules/user/services/mod.rs"),
project_file!("src/modules/user/services/user_service.rs"),
project_file!("src/modules/user/tools.rs"),
project_file!("src/modules/user/dto/mod.rs"),
project_file!("src/modules/user/dto/user.rs"),
project_file!("src/modules/role/mod.rs"),
project_file!("src/modules/role/models/mod.rs"),
project_file!("src/modules/role/models/role.rs"),
project_file!("src/modules/role/models/user_role.rs"),
project_file!("src/modules/role/services/mod.rs"),
project_file!("src/modules/role/services/role_service.rs"),
project_file!("src/modules/role/tools.rs"),
project_file!("src/modules/role/dto/mod.rs"),
project_file!("src/modules/role/dto/role.rs"),
project_file!("src/modules/auth/mod.rs"),
project_file!("src/modules/auth/services/mod.rs"),
project_file!("src/modules/auth/services/auth_service.rs"),
project_file!("src/modules/auth/tools.rs"),
project_file!("src/modules/auth/dto/mod.rs"),
project_file!("src/modules/auth/dto/auth.rs"),
project_file!("src/modules/app/mod.rs"),
project_file!("src/modules/app/models/mod.rs"),
project_file!("src/modules/app/models/app.rs"),
project_file!("src/modules/app/services/mod.rs"),
project_file!("src/modules/app/services/app_service.rs"),
project_file!("src/modules/app/tools.rs"),
project_file!("src/modules/app/dto/mod.rs"),
project_file!("src/modules/app/dto/app.rs"),
project_file!("src/modules/team/mod.rs"),
project_file!("src/modules/team/models/mod.rs"),
project_file!("src/modules/team/models/team.rs"),
project_file!("src/modules/team/models/membership.rs"),
project_file!("src/modules/team/models/invitation.rs"),
project_file!("src/modules/team/services/mod.rs"),
project_file!("src/modules/team/services/access.rs"),
project_file!("src/modules/team/services/team_service.rs"),
project_file!("src/modules/team/tools.rs"),
project_file!("src/modules/team/dto/mod.rs"),
project_file!("src/modules/team/dto/team.rs"),
project_file!("src/bin/migrate.rs"),
project_file!("runtime/Cargo.toml"),
project_file!("runtime/src/main.rs"),
project_file!("runtime/src/apps.rs"),
project_file!("config/apps.toml"),
project_file!("apps/README.md"),
project_file!("database/history.toml"),
project_file!(
"database/migrations/{{ migration_ts }}_create_users_and_roles.sql",
"database/migrations/create_users_and_roles.sql"
),
project_file!(
"database/snapshots/{{ migration_ts }}_snapshot.toml",
"database/snapshots/snapshot.toml"
),
project_file!("tests/mcp.rs"),
];
macro_rules! app_file {
($stack:literal, $path:literal) => {
TemplateFile {
path: $path,
contents: include_str!(concat!("../templates/apps/", $stack, "/", $path, ".stub")),
}
};
}
pub const THEME_APP: &[TemplateFile] = &[
app_file!("theme", "Cargo.toml"),
app_file!("theme", "src/lib.rs"),
app_file!("theme", "src/main.rs"),
app_file!("theme", "src/mcp.rs"),
app_file!("theme", "src/pages.rs"),
];
pub struct Engine {
pub name: &'static str,
pub dependency: &'static str,
pub files: &'static [TemplateFile],
}
macro_rules! engine_file {
($engine:literal, $path:literal) => {
TemplateFile {
path: $path,
contents: include_str!(concat!("../templates/apps/engines/", $engine, "/", $path)),
}
};
}
pub const ADMIN_APP: &[TemplateFile] = &[
TemplateFile {
path: "Cargo.toml",
contents: include_str!("../templates/apps/admin/Cargo.toml.stub"),
},
app_file!("theme", "src/lib.rs"),
app_file!("theme", "src/main.rs"),
app_file!("theme", "src/mcp.rs"),
TemplateFile {
path: "src/pages.rs",
contents: include_str!("../templates/apps/admin/src/pages.rs.stub"),
},
];
pub const ADMIN_RAW: &[TemplateFile] = &[
engine_file!("minijinja", "src/views.rs"),
TemplateFile {
path: "templates/layout.html",
contents: include_str!("../templates/apps/admin/templates/layout.html"),
},
TemplateFile {
path: "templates/login.html",
contents: include_str!("../templates/apps/admin/templates/login.html"),
},
TemplateFile {
path: "templates/dashboard.html",
contents: include_str!("../templates/apps/admin/templates/dashboard.html"),
},
TemplateFile {
path: "templates/team.html",
contents: include_str!("../templates/apps/admin/templates/team.html"),
},
TemplateFile {
path: "templates/message.html",
contents: include_str!("../templates/apps/admin/templates/message.html"),
},
];
pub const MINIJINJA: Engine = Engine {
name: "MiniJinja",
dependency: "minijinja = { version = \"3\", features = [\"serde\"] }",
files: &[
engine_file!("minijinja", "src/views.rs"),
engine_file!("minijinja", "templates/layout.html"),
engine_file!("minijinja", "templates/home.html"),
engine_file!("minijinja", "templates/message.html"),
],
};
pub const TERA: Engine = Engine {
name: "Tera",
dependency: "tera = \"2\"",
files: &[
engine_file!("tera", "src/views.rs"),
engine_file!("tera", "templates/layout.html"),
engine_file!("tera", "templates/home.html"),
engine_file!("tera", "templates/message.html"),
],
};
pub const HANDLEBARS: Engine = Engine {
name: "Handlebars",
dependency: "handlebars = \"6\"",
files: &[
engine_file!("handlebars", "src/views.rs"),
engine_file!("handlebars", "templates/layout.hbs"),
engine_file!("handlebars", "templates/home.hbs"),
engine_file!("handlebars", "templates/message.hbs"),
],
};
pub const LIQUID: Engine = Engine {
name: "Liquid",
dependency: "liquid = \"0.26\"",
files: &[
engine_file!("liquid", "src/views.rs"),
engine_file!("liquid", "templates/layout_top.liquid"),
engine_file!("liquid", "templates/layout_bottom.liquid"),
engine_file!("liquid", "templates/home.liquid"),
engine_file!("liquid", "templates/message.liquid"),
],
};
#[cfg(test)]
pub const ENGINES: [&Engine; 4] = [&MINIJINJA, &TERA, &HANDLEBARS, &LIQUID];
pub const FLY: &[TemplateFile] = &[
TemplateFile {
path: "Dockerfile",
contents: include_str!("../templates/fly/Dockerfile.stub"),
},
TemplateFile {
path: ".dockerignore",
contents: include_str!("../templates/fly/dockerignore.stub"),
},
TemplateFile {
path: "fly.toml",
contents: include_str!("../templates/fly/fly.toml.stub"),
},
];
pub const MODEL: &str = include_str!("../templates/make/model.rs.stub");
pub const TOOLS: &str = include_str!("../templates/make/tools.rs.stub");
pub const APPS_MARKER: &str = "// slave:apps";
pub const TOOLS_MARKER: &str = "// slave:tools";
pub fn render(template: &str, vars: &[(&str, &str)]) -> String {
vars.iter().fold(template.to_owned(), |acc, (key, value)| {
acc.replace(&format!("{{{{ {key} }}}}"), value)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn renders_placeholders() {
let out = render(
"struct {{ name }}; // {{ name }} {{ other }}",
&[("name", "User")],
);
assert_eq!(out, "struct User; // User {{ other }}");
}
#[test]
fn templates_have_no_unknown_placeholders() {
let project_vars = [
("name", "demo"),
("slave_version", "0.0.0"),
("framework_dependency", "slave-framework = \"0.1\""),
("gateway_dependency", "slave-gateway = \"0.1\""),
("crate_name", "demo"),
("migration_ts", "20260101_000000"),
];
for file in PROJECT {
let path = render(file.path, &project_vars);
assert!(!path.contains("{{ "), "{path}");
assert!(
!render(file.contents, &project_vars).contains("{{ "),
"{path}"
);
}
let make_vars = [("name", "Post"), ("module", "post"), ("prefix", "post")];
for template in [MODEL, TOOLS] {
assert!(!render(template, &make_vars).contains("{{ "));
}
let app_vars = [
("app", "dashboard"),
("crate_name", "dashboard"),
("title", "Dashboard"),
("port", "4000"),
("env", "DASHBOARD"),
("engine_dependency", MINIJINJA.dependency),
("engine_name", MINIJINJA.name),
];
for file in THEME_APP.iter().chain(ADMIN_APP) {
assert!(
!render(file.contents, &app_vars).contains("{{ "),
"{}",
file.path
);
}
}
#[test]
fn every_engine_has_views_and_the_pages_rendered_by_pages_rs() {
for engine in ENGINES {
let paths: Vec<&str> = engine.files.iter().map(|f| f.path).collect();
assert!(paths.contains(&"src/views.rs"), "{}", engine.name);
for view in ["home", "message"] {
assert!(
paths
.iter()
.any(|p| p.starts_with(&format!("templates/{view}."))),
"{} lacks the `{view}` view",
engine.name
);
}
let views = engine.files[0].contents;
assert!(views.contains("pub fn load(dir: &Path)"), "{}", engine.name);
assert!(views.contains("pub fn render("), "{}", engine.name);
}
for file in LIQUID.files.iter().filter(|f| f.path.ends_with(".liquid")) {
for output in file.contents.split("{{").skip(1) {
let expression = output.split("}}").next().unwrap_or_default();
assert!(
expression.contains("| escape"),
"{}: {{{{{expression}}}}}",
file.path
);
}
}
}
#[test]
fn modules_template_has_tools_marker() {
let modules = PROJECT
.iter()
.find(|file| file.path == "src/modules/mod.rs")
.expect("modules template");
assert!(modules.contents.contains(TOOLS_MARKER));
}
#[test]
fn versions_match_workspace_crates() {
for (dir, version) in [
("framework", FRAMEWORK_VERSION),
("gateway", GATEWAY_VERSION),
] {
let manifest = format!("{}/{dir}/Cargo.toml", env!("CARGO_MANIFEST_DIR"));
let Ok(contents) = std::fs::read_to_string(manifest) else {
continue;
};
let expected = format!("version = \"{version}.");
assert!(
contents.contains(&expected),
"update the {dir} version constant"
);
}
}
}