use std::{fs, path::Path};
use crate::generators::naming::{pascal_to_snake, to_pascal_case};
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;
}
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');
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");
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');
if has_db {
out.push_str("pub async fn build_app() -> impl poem::Endpoint {\n");
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");
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);
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"
));
}
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
}
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(())
}