use super::*;
pub(super) struct Scenario<'a> {
pub(super) name: &'a str,
db: bool,
no_demo: bool,
no_web: bool,
shared_vos: &'a [(&'a str, &'a str)],
entities: &'a [(&'a str, &'a [&'a str])],
use_cases: &'a [(&'a str, &'a str)],
}
pub(super) const NO_DEMO: Scenario = Scenario {
name: "no_demo",
db: false,
no_demo: true,
no_web: false,
shared_vos: &[],
use_cases: &[],
entities: &[("Persona", &["name:String"])],
};
pub(super) const NO_WEB: Scenario = Scenario {
name: "no_web",
db: false,
no_demo: false,
no_web: true,
shared_vos: &[],
use_cases: &[],
entities: &[],
};
pub(super) const PRIMITIVES: Scenario = Scenario {
name: "primitives",
db: false,
no_demo: false,
no_web: false,
shared_vos: &[],
use_cases: &[],
entities: &[(
"Product",
&[
"title:String",
"price:i64",
"in_stock:bool",
"rate:f64",
"released_at:DateTime",
"meta:map",
"category_id:Uuid",
],
)],
};
pub(super) const UUID_VO: Scenario = Scenario {
name: "uuid_vo",
db: false,
no_demo: false,
no_web: false,
shared_vos: &[],
use_cases: &[],
entities: &[("Booking", &["reference:String", "tenant_id:TenantId:Uuid"])],
};
pub(super) const OPT_VEC_PRIM: Scenario = Scenario {
name: "opt_vec_prim",
db: false,
no_demo: false,
no_web: false,
shared_vos: &[],
use_cases: &[],
entities: &[(
"Article",
&[
"title:String",
"summary:opt:String",
"views:opt:i64",
"tags:vec:String",
"scores:vec:i64",
],
)],
};
pub(super) const PLAIN_VO: Scenario = Scenario {
name: "plain_vo",
db: false,
no_demo: false,
no_web: false,
shared_vos: &[],
use_cases: &[],
entities: &[(
"Item",
&["name:Name:String", "sku:Sku:String!", "weight:Weight:i64"],
)],
};
pub(super) const OPT_VEC_VO: Scenario = Scenario {
name: "opt_vec_vo",
db: false,
no_demo: false,
no_web: false,
shared_vos: &[],
use_cases: &[],
entities: &[(
"Persona",
&[
"name:String",
"middle_name:MiddleName:opt:String",
"tags:Tag:vec:String",
],
)],
};
pub(super) const ENUM_VO: Scenario = Scenario {
name: "enum_vo",
db: false,
no_demo: false,
no_web: false,
shared_vos: &[],
use_cases: &[],
entities: &[(
"Order",
&[
"reference:String",
"status:Status:enum:Pending/Confirmed/Cancelled",
],
)],
};
pub(super) const SHARED_VO: Scenario = Scenario {
name: "shared_vo",
db: false,
no_demo: false,
no_web: false,
shared_vos: &[("Email", "String"), ("Money", "i64")],
use_cases: &[],
entities: &[("Customer", &["name:String", "email:Email", "credit:Money"])],
};
pub(super) const MULTI_ENTITY: Scenario = Scenario {
name: "multi_entity",
db: false,
no_demo: false,
no_web: false,
shared_vos: &[],
use_cases: &[],
entities: &[
("Product", &["title:String", "price:i64"]),
("Category", &["name:Name:String"]),
],
};
pub(super) const VO_KITCHEN_SINK: Scenario = Scenario {
name: "vo_kitchen_sink",
db: false,
no_demo: false,
no_web: false,
shared_vos: &[("Email", "String")],
use_cases: &[],
entities: &[(
"Account",
&[
"title:String",
"price:i64",
"name:Name:String",
"sku:Sku:String!",
"note:opt:String",
"middle_name:MiddleName:opt:String",
"tags:Tag:vec:String",
"status:Status:enum:Active/Archived",
"email:Email",
],
)],
};
pub(super) const ENUM_ONLY: Scenario = Scenario {
name: "enum_only",
db: false,
no_demo: false,
no_web: false,
shared_vos: &[],
use_cases: &[],
entities: &[("Ticket", &["state:State:enum:Open/Closed"])],
};
pub(super) const CUSTOM_USE_CASE: Scenario = Scenario {
name: "custom_use_case",
db: false,
no_demo: false,
no_web: false,
shared_vos: &[],
use_cases: &[("Product", "archive_product")],
entities: &[("Product", &["title:String", "price:i64"])],
};
pub(super) const WEB_CRUD: Scenario = Scenario {
name: "web_crud",
db: false,
no_demo: false,
no_web: false,
shared_vos: &[],
use_cases: &[],
entities: &[(
"OrderItem",
&[
"title:String",
"quantity:i64",
"rate:f64",
"in_stock:bool",
"ref_id:Uuid",
"sku:Sku:String",
],
)],
};
pub(super) const WEB_EVERY_TYPE: Scenario = Scenario {
name: "web_every_type",
db: false,
no_demo: false,
no_web: false,
shared_vos: &[],
use_cases: &[],
entities: &[(
"Case",
&[
"title:String",
"summary:opt:String",
"views:opt:i64",
"ratio:opt:f64",
"urgent:opt:bool",
"due_at:opt:DateTime",
"opened_at:DateTime",
"tags:vec:String",
"scores:vec:i64",
"meta:map",
"status:Status:enum:Open/Closed",
"middle_name:MiddleName:opt:String",
"labels:Label:vec:String",
],
)],
};
pub(super) const POSTGRES_EVERY_TYPE: Scenario = Scenario {
name: "postgres_every_type",
db: true,
no_demo: false,
no_web: false,
shared_vos: &[("Email", "String")],
use_cases: &[("Product", "archive_product")],
entities: &[
(
"Product",
&[
"title:String",
"sku:String!",
"price:i64!",
"rating:f64",
"active:bool",
"ref_id:Uuid",
"published_at:DateTime",
"summary:opt:String",
"views:opt:i64",
"expires_at:opt:DateTime",
"tags:vec:String",
"scores:vec:i64",
"meta:map",
],
),
(
"Customer",
&[
"email:Email",
"name:Name:String",
"code:Code:String!",
"age:Age:i64",
"joined:Joined:DateTime",
"middle:MiddleName:opt:String",
"labels:Label:vec:String",
"status:Status:enum:Active/Inactive",
],
),
("Ticket", &["state:State:enum:Open/Closed"]),
],
};
fn assert_scenario_compiles(scenario: &Scenario) {
let dir = scenario_workspace(scenario.name);
cleanup(&dir);
fs::create_dir_all(&dir).unwrap();
let output = generate_project(&format!("compile-{}", scenario.name), &dir).unwrap();
apply_scenario(scenario, &output);
assert_generated_project_is_clean(&output, &dir, scenario.name, &[]);
cleanup(&dir);
}
pub(super) fn apply_scenario(scenario: &Scenario, output: &Path) {
if scenario.db {
crate::generators::project::apply_db_to_new_project(output).unwrap();
}
if scenario.no_demo {
scaffold::apply_no_demo(output).unwrap();
}
if scenario.no_web {
scaffold::apply_no_web(output).unwrap();
}
for (vo_name, inner) in scenario.shared_vos {
puerto_toml::add_value_object(output, vo_name, inner).unwrap();
}
let shared_vos = puerto_toml::read(output).unwrap().value_object;
for (entity, field_args) in scenario.entities {
let parsed: Vec<puerto_toml::Field> = field_args
.iter()
.map(|arg| puerto_toml::parse_field_arg(arg).unwrap())
.collect();
let fields = puerto_toml::apply_shared_vo_inference(parsed, &shared_vos);
crate::generators::types::validate_fields(&fields).unwrap();
scaffold::run_scaffold(entity, output, &fields, OverwritePolicy::Ask).unwrap();
}
for (entity, action) in scenario.use_cases {
scaffold::run_use_case(entity, action, output).unwrap();
}
}
pub(super) fn assert_generated_project_is_clean(
output: &Path,
dir: &Path,
name: &str,
envs: &[(&str, &str)],
) {
for args in [
&[
"clippy",
"--workspace",
"--all-targets",
"--",
"-D",
"warnings",
][..],
&["test", "--workspace"][..],
] {
let result = std::process::Command::new("cargo")
.args(args)
.env("CARGO_TARGET_DIR", dir.join("target"))
.envs(envs.iter().copied())
.current_dir(output)
.output()
.expect("failed to run cargo");
if !result.status.success() {
eprintln!("stdout:\n{}", String::from_utf8_lossy(&result.stdout));
eprintln!("stderr:\n{}", String::from_utf8_lossy(&result.stderr));
panic!(
"scenario '{name}': generated project failed `cargo {}`",
args.join(" ")
);
}
}
if output.join("web").is_dir() {
assert_spec_export_is_stable(output, dir, name, envs);
assert_web_is_clean(output, name);
}
}
fn assert_spec_export_is_stable(output: &Path, dir: &Path, name: &str, envs: &[(&str, &str)]) {
assert!(
output.join("docs/api/openapi.json").exists(),
"scenario '{name}': `cargo test` did not export docs/api/openapi.json"
);
let result = std::process::Command::new("cargo")
.args(["test", "-p", "presentation", "openapi"])
.env("CARGO_TARGET_DIR", dir.join("target"))
.envs(envs.iter().copied())
.current_dir(output)
.output()
.expect("failed to run cargo");
if !result.status.success() {
eprintln!("stdout:\n{}", String::from_utf8_lossy(&result.stdout));
eprintln!("stderr:\n{}", String::from_utf8_lossy(&result.stderr));
panic!("scenario '{name}': the exported spec differs from the one just written");
}
}
pub(super) fn scenario_workspace(scenario: &str) -> PathBuf {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../target/compile-matrix")
.join(scenario);
let _ = fs::create_dir_all(&root);
root
}
#[test]
#[ignore = "slow: compiles and tests a generated project"]
fn compiles_with_no_demo_then_scaffold() {
assert_scenario_compiles(&NO_DEMO);
}
#[test]
#[ignore = "slow: compiles and tests a generated project"]
fn compiles_with_all_primitive_field_types() {
assert_scenario_compiles(&PRIMITIVES);
}
#[test]
#[ignore = "slow: compiles and tests a generated project"]
fn compiles_with_uuid_value_object() {
assert_scenario_compiles(&UUID_VO);
}
#[test]
#[ignore = "slow: compiles and tests a generated project"]
fn compiles_with_option_and_vec_primitives() {
assert_scenario_compiles(&OPT_VEC_PRIM);
}
#[test]
#[ignore = "slow: compiles and tests a generated project"]
fn compiles_with_plain_and_unique_value_objects() {
assert_scenario_compiles(&PLAIN_VO);
}
#[test]
#[ignore = "slow: compiles and tests a generated project"]
fn compiles_with_option_and_vec_value_objects() {
assert_scenario_compiles(&OPT_VEC_VO);
}
#[test]
#[ignore = "slow: compiles and tests a generated project"]
fn compiles_with_enum_value_object() {
assert_scenario_compiles(&ENUM_VO);
}
#[test]
#[ignore = "slow: compiles and tests a generated project"]
fn compiles_with_shared_value_objects() {
assert_scenario_compiles(&SHARED_VO);
}
#[test]
#[ignore = "slow: compiles and tests a generated project"]
fn compiles_with_multiple_entities() {
assert_scenario_compiles(&MULTI_ENTITY);
}
#[test]
#[ignore = "slow: compiles and tests a generated project"]
fn compiles_with_every_value_object_kind_at_once() {
assert_scenario_compiles(&VO_KITCHEN_SINK);
}
#[test]
#[ignore = "slow: compiles and tests a generated project"]
fn compiles_with_domain_first_layered_flow_and_fields() {
let dir = scenario_workspace("layered_fields");
cleanup(&dir);
fs::create_dir_all(&dir).unwrap();
let output = generate_project("compile-layered-fields", &dir).unwrap();
let fields: Vec<puerto_toml::Field> = [
"amount:i64",
"reference:Reference:String",
"note:opt:String",
]
.iter()
.map(|arg| puerto_toml::parse_field_arg(arg).unwrap())
.collect();
scaffold::run_generate_domain("Invoice", &output, &fields).unwrap();
scaffold::run_generate_application("Invoice", &output).unwrap();
scaffold::run_generate_repository("Invoice", &output).unwrap();
scaffold::run_generate_presentation("Invoice", &output).unwrap();
scaffold::run_generate_web("Invoice", &output, OverwritePolicy::Ask).unwrap();
assert_generated_project_is_clean(&output, &dir, "layered_fields", &[]);
cleanup(&dir);
}
#[test]
#[ignore = "slow: compiles and tests a generated project"]
fn compiles_with_enum_vo_as_only_string_field() {
assert_scenario_compiles(&ENUM_ONLY);
}
#[test]
#[ignore = "slow: compiles and tests a generated project"]
fn compiles_with_custom_use_case_after_scaffold() {
assert_scenario_compiles(&CUSTOM_USE_CASE);
}
fn assert_web_is_clean(output: &Path, name: &str) {
let web = output.join("web");
for args in [
&["install"][..],
&["run", "openapi:types"][..],
&["run", "typecheck"][..],
&["run", "lint"][..],
&["run", "lint:arch"][..],
&["test"][..],
&["run", "build"][..],
] {
let result = std::process::Command::new("bun")
.args(args)
.current_dir(&web)
.output()
.expect("bun not found — the web scenarios need it on PATH (https://bun.sh)");
if !result.status.success() {
eprintln!("stdout:\n{}", String::from_utf8_lossy(&result.stdout));
eprintln!("stderr:\n{}", String::from_utf8_lossy(&result.stderr));
panic!(
"scenario '{name}': generated frontend failed `bun {}`",
args.join(" ")
);
}
}
}
#[test]
#[ignore = "slow: compiles a generated project, then installs, lints and tests its frontend"]
fn web_shell_passes_its_own_checks() {
let dir = scenario_workspace("web_shell");
cleanup(&dir);
fs::create_dir_all(&dir).unwrap();
let output = generate_project("compile-web-shell", &dir).unwrap();
assert_generated_project_is_clean(&output, &dir, "web_shell", &[]);
assert!(
output
.join("web/src/infrastructure/http/schema.d.ts")
.exists()
);
cleanup(&dir);
}
#[test]
#[ignore = "slow: compiles a generated project, then installs, lints and tests its frontend"]
fn web_crud_slice_passes_its_own_checks() {
assert_scenario_compiles(&WEB_CRUD);
}
#[test]
#[ignore = "slow: compiles a generated project, then installs, lints and tests its frontend"]
fn web_every_type_slice_passes_its_own_checks() {
assert_scenario_compiles(&WEB_EVERY_TYPE);
}
#[test]
#[ignore = "slow: compiles and tests a generated project"]
fn compiles_without_the_web_frontend() {
assert_scenario_compiles(&NO_WEB);
}
pub(super) fn test_database_server() -> Option<String> {
std::env::var("PUERTO_TEST_DATABASE_URL")
.ok()
.map(|u| u.trim_end_matches('/').to_string())
.filter(|u| !u.is_empty())
}
#[test]
#[ignore = "slow: needs Postgres — run via `make test/db`"]
fn postgres_project_with_every_type_compiles_lints_and_passes_integration_tests() {
let Some(server) = test_database_server() else {
eprintln!("PUERTO_TEST_DATABASE_URL not set — skipping (run `make test/db`)");
return;
};
let dir = scenario_workspace("postgres_every_type");
cleanup(&dir);
fs::create_dir_all(&dir).unwrap();
let output = generate_project("compile-postgres", &dir).unwrap();
apply_scenario(&POSTGRES_EVERY_TYPE, &output);
let database_url = format!("{server}/puerto_matrix");
for args in [
&["database", "drop", "-y"][..],
&["database", "create"][..],
&["migrate", "run", "--source", "infrastructure/migrations"][..],
] {
let status = std::process::Command::new("sqlx")
.args(args)
.env("DATABASE_URL", &database_url)
.current_dir(&output)
.status()
.expect("sqlx CLI not found — cargo install sqlx-cli");
assert!(status.success(), "`sqlx {}` failed", args.join(" "));
}
assert_generated_project_is_clean(
&output,
&dir,
"postgres_every_type",
&[("DATABASE_URL", &database_url), ("SQLX_OFFLINE", "false")],
);
cleanup(&dir);
}