mod commands;
mod generators;
mod patchers;
mod puerto_toml;
mod scaffold;
mod snippets;
mod validation;
use clap::{CommandFactory, Parser, Subcommand};
use clap_complete::Shell;
use include_dir::{Dir, include_dir};
use std::path::PathBuf;
pub(crate) static TEMPLATE_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/template");
#[derive(Parser)]
#[command(
name = "puerto",
version,
about = "Puerto — Rust full-stack DDD framework"
)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
New {
#[arg(long)]
name: Option<String>,
#[arg(long, conflicts_with = "no_db")]
db: bool,
#[arg(long, conflicts_with = "db")]
no_db: bool,
#[arg(long)]
no_demo: bool,
#[arg(long)]
destination: Option<PathBuf>,
},
Generate {
#[command(subcommand)]
action: GenerateAction,
},
List,
Validate,
Completions {
shell: Shell,
},
}
#[derive(Subcommand)]
enum GenerateAction {
Scaffold {
name: String,
#[arg(long)]
force: bool,
#[arg(raw = true)]
fields: Vec<String>,
},
UseCase {
entity: String,
action: String,
},
Bootstrap {
#[arg(long)]
allow_missing: bool,
},
Migration {
name: String,
},
Snippets {
#[arg(long)]
ide: Option<String>,
},
Domain {
name: String,
#[arg(raw = true)]
fields: Vec<String>,
},
Application {
name: String,
},
Repository {
name: String,
},
Presentation {
name: String,
},
ValueObject {
name: String,
inner_type: String,
},
}
fn parse_cli_fields(
cwd: &std::path::Path,
raw: &[String],
) -> Result<Vec<puerto_toml::Field>, Box<dyn std::error::Error>> {
let mut parsed: Vec<puerto_toml::Field> = raw
.iter()
.map(|s| puerto_toml::parse_field_arg(s))
.collect::<Result<_, _>>()?;
if let Ok(config) = puerto_toml::read(cwd) {
parsed = puerto_toml::apply_shared_vo_inference(parsed, &config.value_object);
}
generators::types::validate_fields(&parsed)?;
validation::validate_unique_field_names(&parsed)?;
Ok(parsed)
}
fn main() {
let cli = Cli::parse();
if let Err(e) = run(cli.command) {
eprintln!("Error: {e}");
std::process::exit(1);
}
}
fn run(command: Commands) -> Result<(), Box<dyn std::error::Error>> {
match command {
Commands::New {
name,
db,
no_db,
no_demo,
destination,
} => commands::new::new_project(name, db, no_db, no_demo, destination).map(|path| {
let project_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
println!("├── business/ (domain logic)");
println!("├── infrastructure/ (adapters)");
println!("├── presentation/ (openapi routes)");
println!("└── Cargo.toml");
println!("✓ Project '{project_name}' created.\n");
println!("Next steps:");
println!(" cd {project_name}");
println!(" make run");
println!();
println!("API docs available at http://localhost:8080");
println!("Add an entity with: puerto generate scaffold <Name>");
}),
Commands::Generate {
action:
GenerateAction::Scaffold {
name,
force,
fields,
},
} => {
let cwd = std::env::current_dir()?;
let policy = if force {
crate::generators::conflict::OverwritePolicy::Force
} else {
crate::generators::conflict::OverwritePolicy::Ask
};
commands::list::require_puerto_project(&cwd)
.and_then(|_| parse_cli_fields(&cwd, &fields))
.and_then(|parsed| scaffold::run_scaffold(&name, &cwd, &parsed, policy))
}
Commands::Generate {
action: GenerateAction::UseCase { entity, action },
} => {
let cwd = std::env::current_dir()?;
commands::list::require_puerto_project(&cwd)
.and_then(|_| scaffold::run_use_case(&entity, &action, &cwd))
}
Commands::Generate {
action: GenerateAction::Bootstrap { allow_missing },
} => {
let cwd = std::env::current_dir()?;
commands::list::require_puerto_project(&cwd)
.and_then(|_| scaffold::run_bootstrap_command(&cwd, allow_missing))
}
Commands::Generate {
action: GenerateAction::Migration { name },
} => {
let cwd = std::env::current_dir()?;
commands::list::require_puerto_project(&cwd)
.and_then(|_| scaffold::run_generate_migration(&name, &cwd))
}
Commands::Generate {
action: GenerateAction::Snippets { ide },
} => {
let cwd = std::env::current_dir()?;
commands::list::require_puerto_project(&cwd)
.and_then(|_| snippets::run(&cwd, ide.as_deref()))
}
Commands::Generate {
action: GenerateAction::Domain { name, fields },
} => {
let cwd = std::env::current_dir()?;
commands::list::require_puerto_project(&cwd)
.and_then(|_| parse_cli_fields(&cwd, &fields))
.and_then(|parsed| scaffold::run_generate_domain(&name, &cwd, &parsed))
}
Commands::Generate {
action: GenerateAction::Application { name },
} => {
let cwd = std::env::current_dir()?;
commands::list::require_puerto_project(&cwd)
.and_then(|_| scaffold::run_generate_application(&name, &cwd))
}
Commands::Generate {
action: GenerateAction::Repository { name },
} => {
let cwd = std::env::current_dir()?;
commands::list::require_puerto_project(&cwd)
.and_then(|_| scaffold::run_generate_repository(&name, &cwd))
}
Commands::Generate {
action: GenerateAction::Presentation { name },
} => {
let cwd = std::env::current_dir()?;
commands::list::require_puerto_project(&cwd)
.and_then(|_| scaffold::run_generate_presentation(&name, &cwd))
}
Commands::Generate {
action: GenerateAction::ValueObject { name, inner_type },
} => {
let cwd = std::env::current_dir()?;
commands::list::require_puerto_project(&cwd)
.and_then(|_| scaffold::run_value_object(&name, &inner_type, &cwd))
}
Commands::List => {
let cwd = std::env::current_dir()?;
commands::list::run_list(&cwd)
}
Commands::Validate => {
let cwd = std::env::current_dir()?;
commands::validate::run_validate(&cwd)
}
Commands::Completions { shell } => {
clap_complete::generate(shell, &mut Cli::command(), "puerto", &mut std::io::stdout());
Ok(())
}
}
}
#[cfg(test)]
mod tests;