slave 0.4.0

Laravel-inspired CLI for Rust MCP servers: scaffold apps whose business operations are MCP tools over Streamable HTTP, 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";

/// Version requirement for `slave-gateway` written into generated applications.
/// Keep in step with `gateway/Cargo.toml`.
pub const GATEWAY_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/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/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/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/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/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/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/tools.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!("src/bin/gateway.rs"),
    project_file!("config/apps.toml"),
    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/mcp.rs"),
];

macro_rules! app_file {
    ($stack:literal, $path:literal) => {
        TemplateFile {
            path: $path,
            contents: include_str!(concat!("../templates/apps/", $stack, "/", $path, ".stub")),
        }
    };
}

/// Files of an Axum web app (`slave make:app --stack axum`), relative to `apps/<name>`.
pub const AXUM_APP: &[TemplateFile] = &[
    app_file!("axum", "Cargo.toml"),
    app_file!("axum", "src/main.rs"),
    app_file!("axum", "src/mcp.rs"),
    app_file!("axum", "src/html.rs"),
    app_file!("axum", "src/pages.rs"),
];

/// Files of a Dioxus single-page app (`slave make:app --stack dioxus`).
pub const DIOXUS_APP: &[TemplateFile] = &[
    app_file!("dioxus", "Cargo.toml"),
    app_file!("dioxus", "Dioxus.toml"),
    app_file!("dioxus", "src/main.rs"),
    app_file!("dioxus", "src/app.rs"),
    app_file!("dioxus", "src/mcp.rs"),
    app_file!("dioxus", "src/server.rs"),
];

pub const MODEL: &str = include_str!("../templates/make/model.rs.stub");
pub const TOOLS: &str = include_str!("../templates/make/tools.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 MCP tools are registered.
pub const TOOLS_MARKER: &str = "// slave:tools";

/// 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\""),
            ("gateway_dependency", "slave-gateway = \"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"), ("prefix", "post")];
        for template in [MODEL, TOOLS] {
            assert!(!render(template, &make_vars).contains("{{ "));
        }

        let app_vars = [
            ("app", "dashboard"),
            ("title", "Dashboard"),
            ("port", "4000"),
        ];
        for file in AXUM_APP.iter().chain(DIOXUS_APP) {
            assert!(
                !render(file.contents, &app_vars).contains("{{ "),
                "{}",
                file.path
            );
        }

        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_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() {
        // These manifests only exist in the workspace, not the published package.
        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"
            );
        }
    }
}