puerto 0.7.2

Puerto — Rust full-stack DDD framework CLI. Scaffolds Clean Architecture projects with auto-wired DI, SQLx support, and zero manual wiring. Formerly puerto-framework.
use std::{fs, path::Path};

use crate::generators::naming::{pascal_to_snake, to_pascal_case};

/// Generate the full content of `presentation/src/generated/bootstrap.rs`
/// from a list of entities read from puerto.toml.
pub fn generate_bootstrap_content(entities: &[crate::puerto_toml::Entity]) -> String {
    let mut out = String::new();

    out.push_str(
        "// AUTO-GENERATED by puerto. Edit puerto.toml, then run `puerto generate bootstrap`.\n",
    );
    out.push_str("use std::sync::Arc;\n\n");

    if entities.is_empty() {
        out.push_str("use infrastructure::logger::TracingLogger;\n");
        out.push_str("use business::domain::logger::LoggerTrait;\n\n");
        out.push_str("use poem::{EndpointExt, Route, middleware::Tracing};\n");
        out.push_str("use poem_openapi::OpenApiService;\n\n");
        out.push_str("pub async fn build_app() -> impl poem::Endpoint {\n");
        out.push_str("    let _logger: Arc<dyn LoggerTrait> = Arc::new(TracingLogger);\n\n");
        out.push_str("    let api_service = OpenApiService::new(\n");
        out.push_str("        (),\n");
        out.push_str("        env!(\"CARGO_PKG_NAME\"),\n");
        out.push_str("        env!(\"CARGO_PKG_VERSION\"),\n");
        out.push_str("    )\n");
        out.push_str("    .server(\"http://localhost:8080/api\");\n");
        out.push_str("    let ui = api_service.swagger_ui();\n\n");
        out.push_str(
            "    Route::new().nest(\"/api\", api_service).nest(\"/\", ui).with(Tracing)\n",
        );
        out.push_str("}\n");
        return out;
    }

    // Use case impl imports
    for entity in entities {
        let snake = pascal_to_snake(&entity.name);
        for uc in &entity.use_cases {
            let uc_pascal = to_pascal_case(uc);
            out.push_str(&format!(
                "use business::application::{snake}::{uc}::{uc_pascal}UseCaseImpl;\n"
            ));
        }
    }
    out.push('\n');

    // Repo + trait imports
    let has_db = entities.iter().any(|e| e.db);
    for entity in entities {
        let snake = pascal_to_snake(&entity.name);
        out.push_str(&format!(
            "use business::domain::{snake}::repository::{}RepositoryTrait;\n",
            entity.name
        ));
        if entity.db {
            out.push_str(&format!(
                "use infrastructure::{snake}::repository::Pg{}Repository;\n",
                entity.name
            ));
        } else {
            out.push_str(&format!(
                "use infrastructure::{snake}::repository::InMemory{}Repository;\n",
                entity.name
            ));
        }
    }
    out.push_str("use infrastructure::logger::TracingLogger;\n");
    out.push_str("use business::domain::logger::LoggerTrait;\n");
    out.push('\n');

    out.push_str("use poem::{EndpointExt, Route, middleware::Tracing};\n");
    out.push_str("use poem_openapi::OpenApiService;\n\n");

    // API imports
    for entity in entities {
        let snake = pascal_to_snake(&entity.name);
        out.push_str(&format!(
            "use crate::api::{snake}::routes::{}Api;\n",
            entity.name
        ));
    }
    out.push('\n');

    // build_app signature: async when any entity needs a pool
    if has_db {
        out.push_str("pub async fn build_app() -> impl poem::Endpoint {\n");
        // (always async — main.rs calls build_app().await in both cases)
        out.push_str("    dotenvy::dotenv().ok();\n");
        out.push_str(
            "    let database_url = std::env::var(\"DATABASE_URL\").expect(\"DATABASE_URL must be set\");\n",
        );
        out.push_str(
            "    let pool = infrastructure::db::create_postgres_pool(&database_url)\n        .await\n        .expect(\"Failed to connect to database\");\n",
        );
        out.push_str(
            "    infrastructure::db::run_migrations(&pool)\n        .await\n        .expect(\"Failed to run migrations\");\n\n",
        );
    } else {
        out.push_str("pub async fn build_app() -> impl poem::Endpoint {\n");
    }
    out.push_str("    let logger: Arc<dyn LoggerTrait> = Arc::new(TracingLogger);\n\n");

    // Wire each entity
    for entity in entities {
        let pascal = &entity.name;
        let snake = pascal_to_snake(pascal);
        let uc_count = entity.use_cases.len();
        let repo_type = if entity.db {
            format!("Pg{pascal}Repository::new(pool.clone(), Arc::clone(&logger))")
        } else {
            format!("InMemory{pascal}Repository::new(Arc::clone(&logger))")
        };

        for (i, uc) in entity.use_cases.iter().enumerate() {
            let uc_pascal = to_pascal_case(uc);
            // Single use case: inline the repo. Multiple: bind repo once then clone.
            let repo_expr = if uc_count == 1 {
                format!("Arc::new({repo_type}) as Arc<dyn {pascal}RepositoryTrait>")
            } else if i == 0 {
                out.push_str(&format!(
                    "    let {snake}_repo: Arc<dyn {pascal}RepositoryTrait> = Arc::new({repo_type});\n"
                ));
                format!("Arc::clone(&{snake}_repo)")
            } else if i < uc_count - 1 {
                format!("Arc::clone(&{snake}_repo)")
            } else {
                format!("{snake}_repo")
            };
            out.push_str(&format!(
                "    let {uc} = Arc::new({uc_pascal}UseCaseImpl {{ repository: {repo_expr}, logger: Arc::clone(&logger) }});\n"
            ));
        }

        let fields = entity.use_cases.join(", ");
        out.push_str(&format!(
            "    let {snake}_api = {pascal}Api {{ {fields}, logger: Arc::clone(&logger) }};\n\n"
        ));
    }

    // OpenApiService — single entity: direct; multiple: tuple
    let api_args = if entities.len() == 1 {
        format!("{}_api", pascal_to_snake(&entities[0].name))
    } else {
        let apis = entities
            .iter()
            .map(|e| format!("{}_api", pascal_to_snake(&e.name)))
            .collect::<Vec<_>>()
            .join(", ");
        format!("({apis})")
    };

    out.push_str("    let api_service = OpenApiService::new(\n");
    out.push_str(&format!("        {api_args},\n"));
    out.push_str("        env!(\"CARGO_PKG_NAME\"),\n");
    out.push_str("        env!(\"CARGO_PKG_VERSION\"),\n");
    out.push_str("    )\n");
    out.push_str("    .server(\"http://localhost:8080/api\");\n");
    out.push_str("    let ui = api_service.swagger_ui();\n\n");
    out.push_str("    Route::new().nest(\"/api\", api_service).nest(\"/\", ui).with(Tracing)\n");
    out.push_str("}\n");

    out
}

/// Read puerto.toml and overwrite `presentation/src/generated/bootstrap.rs`.
pub fn regenerate_bootstrap(base: &Path) -> Result<(), Box<dyn std::error::Error>> {
    let config = crate::puerto_toml::read(base)?;
    let content = generate_bootstrap_content(&config.entity);
    let path = base.join("presentation/src/generated/bootstrap.rs");
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
    }
    fs::write(path, content)?;
    Ok(())
}