slave 0.2.0

Laravel-inspired CLI for Rust web apps: scaffold Axum projects with built-in auth, users, roles and Turso migrations
//! Embedded file templates and a tiny `{{ placeholder }}` renderer.

/// Version requirement for `slave-framework` written into generated applications.
/// Keep in step with `framework/Cargo.toml`.
pub const FRAMEWORK_VERSION: &str = "0.1";

/// A template file to be written relative to a target directory.
pub struct TemplateFile {
    /// Output path; may contain placeholders such as `{{ timestamp }}`.
    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")),
        }
    };
}

/// Files that make up a freshly generated application.
pub const PROJECT: &[TemplateFile] = &[
    project_file!("Cargo.toml"),
    project_file!(".env.example"),
    project_file!(".gitignore"),
    project_file!("README.md"),
    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/modules/mod.rs"),
    project_file!("src/modules/user/mod.rs"),
    project_file!("src/modules/user/routes.rs"),
    project_file!("src/modules/user/models/mod.rs"),
    project_file!("src/modules/user/models/user.rs"),
    project_file!("src/modules/user/repositories/mod.rs"),
    project_file!("src/modules/user/repositories/user_repository.rs"),
    project_file!("src/modules/user/services/mod.rs"),
    project_file!("src/modules/user/services/user_service.rs"),
    project_file!("src/modules/user/controllers/mod.rs"),
    project_file!("src/modules/user/controllers/user_controller.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/routes.rs"),
    project_file!("src/modules/role/models/mod.rs"),
    project_file!("src/modules/role/models/role.rs"),
    project_file!("src/modules/role/repositories/mod.rs"),
    project_file!("src/modules/role/repositories/role_repository.rs"),
    project_file!("src/modules/role/services/mod.rs"),
    project_file!("src/modules/role/services/role_service.rs"),
    project_file!("src/modules/role/controllers/mod.rs"),
    project_file!("src/modules/role/controllers/role_controller.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/routes.rs"),
    project_file!("src/modules/auth/repositories/mod.rs"),
    project_file!("src/modules/auth/repositories/token_repository.rs"),
    project_file!("src/modules/auth/services/mod.rs"),
    project_file!("src/modules/auth/services/auth_service.rs"),
    project_file!("src/modules/auth/controllers/mod.rs"),
    project_file!("src/modules/auth/controllers/auth_controller.rs"),
    project_file!("src/modules/auth/dto/mod.rs"),
    project_file!("src/modules/auth/dto/auth.rs"),
    project_file!("src/bin/migrate.rs"),
    project_file!("database/migrations/mod.rs"),
    project_file!(
        "database/migrations/{{ ts_users }}_create_users_table.rs",
        "database/migrations/create_users_table.rs"
    ),
    project_file!(
        "database/migrations/{{ ts_roles }}_create_roles_tables.rs",
        "database/migrations/create_roles_tables.rs"
    ),
    project_file!(
        "database/migrations/{{ ts_tokens }}_create_auth_tokens_table.rs",
        "database/migrations/create_auth_tokens_table.rs"
    ),
    project_file!("tests/http.rs"),
];

pub const MODEL: &str = include_str!("../templates/make/model.rs.stub");
pub const CONTROLLER: &str = include_str!("../templates/make/controller.rs.stub");
pub const ROUTES: &str = include_str!("../templates/make/routes.rs.stub");
pub const MIGRATION: &str = include_str!("../templates/make/migration.rs.stub");
pub const MIGRATION_CREATE_TABLE: &str =
    include_str!("../templates/make/migration_create_table.rs.stub");

/// Marker line in `src/modules/mod.rs` above which domain routes are registered.
pub const ROUTES_MARKER: &str = "// slave:routes";

/// Markers in `database/migrations/mod.rs` for attaching and listing migrations.
pub const MIGRATION_MODULES_MARKER: &str = "// slave:migration-modules";
pub const MIGRATIONS_MARKER: &str = "// slave:migrations";

/// Replaces every `{{ key }}` in `template` with its value from `vars`.
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\""),
            ("ts_users", "20260101000000"),
            ("ts_roles", "20260101000001"),
            ("ts_tokens", "20260101000002"),
        ];
        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"),
            ("controller", "post_controller"),
            ("route", "post"),
        ];
        for template in [MODEL, CONTROLLER, ROUTES] {
            assert!(!render(template, &make_vars).contains("{{ "));
        }

        let migration_vars = [("struct", "CreatePostsTable"), ("table", "posts")];
        for template in [MIGRATION, MIGRATION_CREATE_TABLE] {
            assert!(!render(template, &migration_vars).contains("{{ "));
        }
    }

    #[test]
    fn migrations_registry_has_markers() {
        let registry = PROJECT
            .iter()
            .find(|file| file.path == "database/migrations/mod.rs")
            .expect("migrations registry");
        for marker in [MIGRATION_MODULES_MARKER, MIGRATIONS_MARKER] {
            assert!(registry.contents.contains(marker), "{marker}");
        }
    }

    #[test]
    fn modules_template_has_routes_marker() {
        let modules = PROJECT
            .iter()
            .find(|file| file.path == "src/modules/mod.rs")
            .expect("modules template");
        assert!(modules.contents.contains(ROUTES_MARKER));
    }

    #[test]
    fn framework_version_matches_workspace_crate() {
        // The framework manifest only exists in the workspace, not the published package.
        let manifest = concat!(env!("CARGO_MANIFEST_DIR"), "/framework/Cargo.toml");
        let Ok(contents) = std::fs::read_to_string(manifest) else {
            return;
        };
        let expected = format!("version = \"{FRAMEWORK_VERSION}.");
        assert!(contents.contains(&expected), "update FRAMEWORK_VERSION");
    }
}