use std::path::Path;
use crate::generators::conflict::{OverwritePolicy, guard_entity_overwrite};
use crate::generators::naming::entity_ctx;
use crate::generators::types::{is_shared_vo, is_value_object};
use crate::generators::{
application::{generate_create_use_case_impl, write_application_files},
domain::{generate_create_use_case_trait, generate_model, write_domain_files, write_mother},
infrastructure::{generate_infra_entity, write_repository_files},
migration::run_migration,
naming::{crud_use_cases, pascal_to_snake, to_pascal_case, write_file},
presentation::write_presentation_files,
render::render,
web::write_web_files,
};
use crate::patchers::lib_rs::{
patch_business_lib_shared, patch_business_lib_value_objects, patch_libs, patch_mothers_lib,
unpatch_business_lib_value_objects,
};
use crate::puerto_toml::{Field, ValueObjectDefinition};
fn write_files(
pascal: &str,
snake: &str,
base: &Path,
db: bool,
) -> Result<(), Box<dyn std::error::Error>> {
write_file(
&base.join(format!("business/src/domain/{snake}/model.rs")),
&generate_model(pascal, snake, &[], &[])?,
)?;
let mut errors_ctx = entity_ctx(pascal, snake);
errors_ctx.insert("vo_fields", &Vec::<String>::new());
write_file(
&base.join(format!("business/src/domain/{snake}/errors.rs")),
&render("domain/errors.tera", &errors_ctx)?,
)?;
{
let repo_ctx = entity_ctx(pascal, snake);
write_file(
&base.join(format!("business/src/domain/{snake}/repository.rs")),
&render("domain/repository_simple.tera", &repo_ctx)?,
)?;
}
write_file(
&base.join(format!(
"business/src/domain/{snake}/use_cases/create_{snake}.rs"
)),
&generate_create_use_case_trait(pascal, snake, &[])?,
)?;
write_file(
&base.join(format!(
"business/src/application/{snake}/create_{snake}.rs"
)),
&generate_create_use_case_impl(pascal, snake, &[], &[])?,
)?;
let infra_ctx = entity_ctx(pascal, snake);
if db {
write_file(
&base.join(format!("infrastructure/src/{snake}/entity.rs")),
&generate_infra_entity(pascal, snake, &[], &[])?,
)?;
write_file(
&base.join(format!("infrastructure/src/{snake}/repository.rs")),
&render("infrastructure/repository_pg_simple.tera", &infra_ctx)?,
)?;
} else {
write_file(
&base.join(format!("infrastructure/src/{snake}/repository.rs")),
&render("infrastructure/repository_inmemory_simple.tera", &infra_ctx)?,
)?;
}
write_file(
&base.join(format!("presentation/src/api/{snake}.rs")),
"pub mod dto;\npub mod error_mapper;\npub mod responses;\npub mod routes;\n",
)?;
{
use crate::generators::presentation::{
build_dto_ctx, build_error_mapper_ctx, build_simple_ctx,
};
write_file(
&base.join(format!("presentation/src/api/{snake}/dto.rs")),
&render(
"presentation/dto.tera",
&build_dto_ctx(pascal, snake, &[], false),
)?,
)?;
write_file(
&base.join(format!("presentation/src/api/{snake}/responses.rs")),
&render(
"presentation/responses_simple.tera",
&build_simple_ctx(pascal, snake),
)?,
)?;
write_file(
&base.join(format!("presentation/src/api/{snake}/error_mapper.rs")),
&render(
"presentation/error_mapper.tera",
&build_error_mapper_ctx(pascal, snake, &[]),
)?,
)?;
write_file(
&base.join(format!("presentation/src/api/{snake}/routes.rs")),
&render(
"presentation/routes_simple.tera",
&build_simple_ctx(pascal, snake),
)?,
)?;
}
Ok(())
}
fn write_files_crud(
pascal: &str,
snake: &str,
base: &Path,
db: bool,
fields: &[Field],
shared_vos: &[ValueObjectDefinition],
) -> Result<(), Box<dyn std::error::Error>> {
write_domain_files(pascal, snake, base, fields, shared_vos)?;
write_application_files(pascal, snake, base, fields, shared_vos)?;
write_repository_files(pascal, snake, base, db, fields, shared_vos)?;
write_presentation_files(pascal, snake, base, fields)?;
Ok(())
}
pub fn run(
name: &str,
base: &Path,
db: bool,
crud: bool,
fields: &[Field],
shared_vos: &[ValueObjectDefinition],
) -> Result<(), Box<dyn std::error::Error>> {
let pascal = to_pascal_case(name);
let snake = pascal_to_snake(&pascal);
let web_project = crate::puerto_toml::read(base).is_ok_and(|c| c.project.web);
let web_slice = web_project && crud;
if crud {
write_files_crud(&pascal, &snake, base, db, fields, shared_vos)?;
} else {
write_files(&pascal, &snake, base, db)?;
}
if web_slice {
write_web_files(&pascal, base, fields)?;
}
patch_libs(&snake, base, db, crud)?;
if !fields.is_empty()
&& fields
.iter()
.any(|f| is_value_object(f) && !is_shared_vo(f, shared_vos))
{
patch_business_lib_value_objects(base, &snake)?;
} else {
unpatch_business_lib_value_objects(base, &snake)?;
}
if !shared_vos.is_empty() {
use crate::generators::domain::write_shared_vo_files;
write_shared_vo_files(base, shared_vos)?;
patch_business_lib_shared(base)?;
}
write_mother(&pascal, &snake, base, fields, shared_vos)?;
patch_mothers_lib(base, &snake)?;
let use_cases = if crud {
crud_use_cases(&snake)
} else {
vec![format!("create_{snake}")]
};
crate::puerto_toml::add_entity(base, &pascal, use_cases, db, fields.to_vec())?;
crate::patchers::routes_rs::patch_routes_from_manifest(base, &pascal, &snake)?;
crate::generators::bootstrap::regenerate_bootstrap(base)?;
let repo_name = if db {
format!("Pg{pascal}Repository")
} else {
format!("InMemory{pascal}Repository")
};
let uc_label = if crud {
"create, get, list, update, delete"
} else {
"create"
};
println!("✓ business/ — model, errors, repository trait, use cases ({uc_label})");
println!("✓ infrastructure/ — {repo_name}");
println!("✓ presentation/ — routes, dto, responses, error mapper");
if web_slice {
println!("✓ web/ — list, detail and form screens, HTTP repository");
}
println!("✓ Done. Zero manual wiring.");
if web_project {
println!(
"\nThe API changed. Next: make web/sync (exports the spec, regenerates the web types)"
);
}
Ok(())
}
pub fn run_scaffold(
name: &str,
base: &Path,
cli_fields: &[Field],
overwrite: OverwritePolicy,
) -> Result<(), Box<dyn std::error::Error>> {
let config = crate::puerto_toml::read(base)?;
let db = config.project.db;
let shared_vos = config.value_object.clone();
let pascal = to_pascal_case(name);
let fields = if !cli_fields.is_empty() {
cli_fields.to_vec()
} else {
config
.entity
.iter()
.find(|e| e.name == pascal)
.map(|e| e.fields.clone())
.unwrap_or_default()
};
crate::validation::validate_entity_name(&pascal)?;
crate::validation::validate_unique_field_names(&fields)?;
if db {
for f in &fields {
crate::validation::validate_sql_field_name(&f.name)?;
}
}
let vo_conflicts = crate::validation::validate_vo_coherence(&fields, &shared_vos);
if !vo_conflicts.is_empty() {
return Err(vo_conflicts.join("\n ").into());
}
guard_entity_overwrite(
base,
&pascal_to_snake(&pascal),
db,
config.project.web,
overwrite,
)?;
run(name, base, db, true, &fields, &shared_vos)?;
if db {
let snake = pascal_to_snake(&pascal);
let migration_sql = crate::generators::infrastructure::create_table_sql(&snake, &fields);
run_migration(&format!("create_{snake}_table"), base, Some(&migration_sql))?;
}
Ok(())
}
pub fn run_value_object(
name: &str,
inner_type: &str,
base: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let inner_type = &crate::puerto_toml::expand_type(inner_type);
crate::validation::validate_vo_name(name)?;
crate::validation::validate_shared_vo_inner_type(inner_type)?;
let config = crate::puerto_toml::read(base)?;
if config.value_object.iter().any(|vo| vo.name == name) {
return Err(
format!("shared value object '{name}' is already declared in puerto.toml").into(),
);
}
crate::puerto_toml::add_value_object(base, name, inner_type)?;
println!("✓ Shared value object '{name}' added to puerto.toml");
Ok(())
}