use std::path::Path;
use crate::generators::effective_fields;
use crate::generators::naming::{
crud_use_case, entity_ctx, pascal_to_snake, to_pascal_case, write_file,
};
use crate::generators::render::{RenderError, render};
use crate::generators::types::{
FieldCtx, field_ctx, is_value_object, literal_import, literal_names_vo, vo_import_path,
};
use crate::patchers::lib_rs::patch_business_lib_application_crud;
use crate::puerto_toml::{Field, ValueObjectDefinition};
#[derive(Clone, Copy)]
enum TestImports {
ParamsOnly,
Props,
}
fn use_case_context(
pascal: &str,
snake: &str,
fields: &[Field],
shared_vos: &[ValueObjectDefinition],
test_imports: TestImports,
) -> tera::Context {
let eff = effective_fields(fields);
let mut vo_imports: Vec<String> = vec![];
for import in eff
.iter()
.filter(|f| is_value_object(f))
.map(|f| vo_import_path(f, snake, shared_vos))
{
if !vo_imports.contains(&import) {
vo_imports.push(import);
}
}
let mut tests: Vec<String> = vec![];
let literal_imports = eff.iter().filter_map(literal_import).map(str::to_string);
let named_vos = eff
.iter()
.filter(|f| !matches!(test_imports, TestImports::ParamsOnly) && literal_names_vo(f))
.map(|f| vo_import_path(f, snake, shared_vos));
for import in literal_imports.chain(named_vos) {
if !tests.contains(&import) {
tests.push(import);
}
}
let field_ctxs: Vec<FieldCtx> = eff
.iter()
.map(|f| field_ctx(f, pascal, shared_vos))
.collect();
let mut ctx = entity_ctx(pascal, snake);
ctx.insert("fields", &field_ctxs);
ctx.insert("vo_imports", &vo_imports);
ctx.insert("test_imports", &tests);
ctx
}
fn render_use_case(
action: &str,
test_imports: TestImports,
pascal: &str,
snake: &str,
fields: &[Field],
shared_vos: &[ValueObjectDefinition],
) -> Result<String, RenderError> {
let ctx = use_case_context(pascal, snake, fields, shared_vos, test_imports);
render(&format!("application/{action}.tera"), &ctx)
}
pub fn generate_create_use_case_impl(
pascal: &str,
snake: &str,
fields: &[Field],
shared_vos: &[ValueObjectDefinition],
) -> Result<String, RenderError> {
render_use_case(
"create",
TestImports::ParamsOnly,
pascal,
snake,
fields,
shared_vos,
)
}
pub fn generate_get_use_case_impl(
pascal: &str,
snake: &str,
fields: &[Field],
shared_vos: &[ValueObjectDefinition],
) -> Result<String, RenderError> {
render_use_case("get", TestImports::Props, pascal, snake, fields, shared_vos)
}
pub fn generate_list_use_case_impl(
pascal: &str,
snake: &str,
fields: &[Field],
shared_vos: &[ValueObjectDefinition],
) -> Result<String, RenderError> {
render_use_case(
"list",
TestImports::Props,
pascal,
snake,
fields,
shared_vos,
)
}
pub fn generate_update_use_case_impl(
pascal: &str,
snake: &str,
fields: &[Field],
shared_vos: &[ValueObjectDefinition],
) -> Result<String, RenderError> {
render_use_case(
"update",
TestImports::Props,
pascal,
snake,
fields,
shared_vos,
)
}
pub fn generate_delete_use_case_impl(
pascal: &str,
snake: &str,
fields: &[Field],
shared_vos: &[ValueObjectDefinition],
) -> Result<String, RenderError> {
render_use_case(
"delete",
TestImports::Props,
pascal,
snake,
fields,
shared_vos,
)
}
pub fn write_application_files(
pascal: &str,
snake: &str,
base: &Path,
fields: &[Field],
shared_vos: &[ValueObjectDefinition],
) -> Result<(), Box<dyn std::error::Error>> {
let dir = base.join(format!("business/src/application/{snake}"));
let write = |action: &str, content: String| {
write_file(
&dir.join(format!("{}.rs", crud_use_case(action, snake))),
&content,
)
};
write(
"create",
generate_create_use_case_impl(pascal, snake, fields, shared_vos)?,
)?;
write(
"get",
generate_get_use_case_impl(pascal, snake, fields, shared_vos)?,
)?;
write(
"list",
generate_list_use_case_impl(pascal, snake, fields, shared_vos)?,
)?;
write(
"update",
generate_update_use_case_impl(pascal, snake, fields, shared_vos)?,
)?;
write(
"delete",
generate_delete_use_case_impl(pascal, snake, fields, shared_vos)?,
)?;
Ok(())
}
pub fn run_generate_application(name: &str, base: &Path) -> Result<(), Box<dyn std::error::Error>> {
let config = crate::puerto_toml::read(base)?;
let pascal = to_pascal_case(name);
let snake = pascal_to_snake(&pascal);
if !config.entity.iter().any(|e| e.name == pascal) {
return Err(format!(
"{pascal} not found in puerto.toml. Run `puerto generate domain {pascal}` first."
)
.into());
}
let fields: Vec<Field> = config
.entity
.iter()
.find(|e| e.name == pascal)
.map(|e| e.fields.clone())
.unwrap_or_default();
let shared_vos = config.value_object.clone();
write_application_files(&pascal, &snake, base, &fields, &shared_vos)?;
patch_business_lib_application_crud(base, &snake)?;
println!("✓ business/application/ — 5 use case impls (create, get, list, update, delete)");
println!();
println!(" Next: puerto generate repository {pascal}");
Ok(())
}