use std::{fs, path::Path};
use serde::Serialize;
use crate::generators::naming::{crud_use_case, is_crud_entity, pascal_to_snake, to_pascal_case};
use crate::generators::render::{RenderError, render};
#[derive(Serialize)]
struct BootstrapUseCase {
action: String,
uc_pascal: String,
repository: bool,
uow: bool,
}
#[derive(Serialize)]
struct BootstrapEntity {
pascal: String,
snake: String,
db: bool,
uow: bool,
use_cases: Vec<BootstrapUseCase>,
}
pub fn uses_unit_of_work(entity: &crate::puerto_toml::Entity, events: bool) -> bool {
events && entity.db && is_crud_entity(&pascal_to_snake(&entity.name), &entity.use_cases)
}
fn build_context(
entities: &[crate::puerto_toml::Entity],
web: bool,
events: bool,
) -> tera::Context {
let ctx_entities: Vec<BootstrapEntity> = entities
.iter()
.map(|entity| {
let snake = pascal_to_snake(&entity.name);
let create = crud_use_case("create", &snake);
let reads = [crud_use_case("get", &snake), crud_use_case("list", &snake)];
BootstrapEntity {
pascal: entity.name.clone(),
db: entity.db,
uow: uses_unit_of_work(entity, events),
use_cases: entity
.use_cases
.iter()
.map(|uc| BootstrapUseCase {
action: uc.clone(),
uc_pascal: to_pascal_case(uc),
repository: *uc != create,
uow: !reads.contains(uc),
})
.collect(),
snake,
}
})
.collect();
let mut ctx = tera::Context::new();
ctx.insert("entities", &ctx_entities);
ctx.insert("has_db", &entities.iter().any(|e| e.db));
ctx.insert("web", &web);
ctx.insert("apis", &api_expression(entities));
ctx
}
fn api_expression(entities: &[crate::puerto_toml::Entity]) -> String {
let apis: Vec<String> = entities
.iter()
.map(|e| format!("{}_api", pascal_to_snake(&e.name)))
.collect();
match apis.as_slice() {
[single] => single.clone(),
_ => format!("({})", apis.join(", ")),
}
}
#[cfg(test)]
pub fn generate_bootstrap_content(
entities: &[crate::puerto_toml::Entity],
web: bool,
) -> Result<String, RenderError> {
generate_bootstrap_content_with_events(entities, web, false)
}
pub fn generate_bootstrap_content_with_events(
entities: &[crate::puerto_toml::Entity],
web: bool,
events: bool,
) -> Result<String, RenderError> {
let ctx = build_context(entities, web, events);
render("bootstrap/bootstrap.tera", &ctx)
}
pub fn generate_tags_content(
entities: &[crate::puerto_toml::Entity],
) -> Result<String, RenderError> {
let tags: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
let mut ctx = tera::Context::new();
ctx.insert("tags_entities", &tags);
render("bootstrap/tags.tera", &ctx)
}
pub fn run_bootstrap_command(
base: &Path,
allow_missing: bool,
) -> Result<(), Box<dyn std::error::Error>> {
if !allow_missing {
let config = crate::puerto_toml::read(base)?;
let mut lines = crate::generators::consistency::drift_lines(
&crate::generators::consistency::missing_entity_files(base, &config),
);
lines.extend(crate::generators::consistency::missing_consumer_files(
base, &config,
));
if !lines.is_empty() {
return Err(format!(
"refusing to write generated wiring that will not compile:\n {}\n\nPass \
--allow-missing to write it anyway.",
lines.join("\n ")
)
.into());
}
}
regenerate_bootstrap(base)?;
println!("✓ bootstrap.rs regenerated.");
if crate::puerto_toml::read(base).is_ok_and(|c| c.project.web) {
println!("✓ web/src/presentation/core/generated/ regenerated.");
}
Ok(())
}
pub fn regenerate_bootstrap(base: &Path) -> Result<(), Box<dyn std::error::Error>> {
let config = crate::puerto_toml::read(base)?;
for line in crate::generators::consistency::drift_lines(
&crate::generators::consistency::missing_entity_files(base, &config),
)
.into_iter()
.chain(crate::generators::consistency::missing_consumer_files(
base, &config,
)) {
eprintln!("⚠ {line}");
}
let bootstrap_path = base.join("presentation/src/generated/bootstrap.rs");
if let Some(parent) = bootstrap_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(
bootstrap_path,
generate_bootstrap_content_with_events(
&config.entity,
config.project.web,
config.project.has_events(),
)?,
)?;
let tags_path = base.join("presentation/src/api/tags.rs");
if let Some(parent) = tags_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(tags_path, generate_tags_content(&config.entity)?)?;
crate::generators::events::regenerate_events(base, &config)?;
crate::generators::worker::regenerate_registry(base, &config)?;
crate::generators::web::regenerate_web(base, &config)?;
Ok(())
}