use gize_core::naming::table_name;
use gize_core::{Manifest, ModelSpec};
use gize_templates::{crud, model, module, project, user};
use crate::plan::Plan;
use crate::registry;
pub fn new_project(name: &str, with_user: bool, timestamp: &str) -> Plan {
let mut manifest = Manifest::new(name);
if with_user {
manifest.add_module("users");
}
let app_mod = if with_user {
registry::register_module(&project::app_mod_rs(), "users")
.expect("app_mod_rs template carries the gize markers")
.content
} else {
project::app_mod_rs()
};
let plan = Plan::new()
.create("Cargo.toml", project::cargo_toml(name))
.create("gize.toml", project::gize_toml(&manifest))
.create(".env.example", project::env_example(name))
.create(".gitignore", "/target\n.env\n")
.create("src/main.rs", project::main_rs())
.create("src/state.rs", project::state_rs())
.create("src/router.rs", project::router_rs())
.create("src/config/mod.rs", project::config_mod_rs())
.create("src/app/mod.rs", app_mod)
.mkdir("src/database")
.mkdir("src/middleware")
.mkdir("src/shared")
.mkdir("migrations");
if with_user {
plan.extend(user_slice(timestamp))
} else {
plan
}
}
fn user_slice(timestamp: &str) -> Plan {
let model = user::spec();
let dir = "src/app/users";
Plan::new()
.create(format!("{dir}/mod.rs"), crud::mod_rs(&model))
.create(format!("{dir}/model.rs"), user::model_rs())
.create(format!("{dir}/dto.rs"), crud::dto_rs(&model))
.create(format!("{dir}/error.rs"), crud::error_rs(&model))
.create(format!("{dir}/repository.rs"), crud::repository_rs(&model))
.create(format!("{dir}/service.rs"), crud::service_rs(&model))
.create(format!("{dir}/handler.rs"), crud::handler_rs(&model))
.create(format!("{dir}/routes.rs"), crud::routes_rs(&model))
.create(format!("{dir}/tests.rs"), crud::tests_rs(&model))
.create(
format!("migrations/{timestamp}_create_users.sql"),
user::migration_sql(),
)
}
pub fn make_app(module: &str) -> Plan {
let dir = format!("src/app/{module}");
Plan::new()
.create(format!("{dir}/mod.rs"), module::mod_rs(module))
.create(
format!("{dir}/model.rs"),
module::model_placeholder_rs(module),
)
.create(format!("{dir}/dto.rs"), module::dto_rs(module))
.create(
format!("{dir}/repository.rs"),
module::repository_rs(module),
)
.create(format!("{dir}/service.rs"), module::service_rs(module))
.create(format!("{dir}/error.rs"), module::error_rs(module))
.create(format!("{dir}/handler.rs"), module::handler_rs(module))
.create(format!("{dir}/routes.rs"), module::routes_rs(module))
.create(format!("{dir}/tests.rs"), module::tests_rs(module))
}
pub fn make_model(model: &ModelSpec, timestamp: &str) -> Plan {
let module = table_name(&model.name);
let table = &module;
Plan::new()
.create(format!("src/app/{module}/model.rs"), model::model_rs(model))
.create(
format!("migrations/{timestamp}_create_{table}.sql"),
model::migration_sql(model),
)
}
pub fn make_crud(model: &ModelSpec, timestamp: &str) -> Plan {
let table = table_name(&model.name);
let dir = format!("src/app/{table}");
Plan::new()
.create(format!("{dir}/mod.rs"), crud::mod_rs(model))
.create(format!("{dir}/model.rs"), model::model_rs(model))
.create(format!("{dir}/dto.rs"), crud::dto_rs(model))
.create(format!("{dir}/error.rs"), crud::error_rs(model))
.create(format!("{dir}/repository.rs"), crud::repository_rs(model))
.create(format!("{dir}/service.rs"), crud::service_rs(model))
.create(format!("{dir}/handler.rs"), crud::handler_rs(model))
.create(format!("{dir}/routes.rs"), crud::routes_rs(model))
.create(format!("{dir}/tests.rs"), crud::tests_rs(model))
.create(
format!("migrations/{timestamp}_create_{table}.sql"),
model::migration_sql(model),
)
}
pub fn make_migration(name: &str, timestamp: &str) -> Plan {
Plan::new().create(
format!("migrations/{timestamp}_{name}.sql"),
model::blank_migration_sql(name),
)
}
#[cfg(test)]
mod tests {
use super::*;
fn paths_of(plan: &Plan) -> Vec<String> {
plan.ops
.iter()
.map(|o| o.path.display().to_string())
.collect()
}
fn content_of<'a>(plan: &'a Plan, path: &str) -> &'a str {
plan.ops
.iter()
.find(|o| o.path.display().to_string() == path)
.map(|o| o.contents.as_str())
.unwrap_or_else(|| panic!("plan has no op for {path}"))
}
#[test]
fn new_project_plan_includes_core_files() {
let plan = new_project("shop", false, "20260704120000");
let paths = paths_of(&plan);
assert!(paths.contains(&"Cargo.toml".to_string()));
assert!(paths.contains(&"gize.toml".to_string()));
assert!(paths.contains(&"src/main.rs".to_string()));
assert!(paths.contains(&"src/app/mod.rs".to_string()));
}
#[test]
fn new_project_without_user_omits_users_slice() {
let plan = new_project("shop", false, "20260704120000");
let paths = paths_of(&plan);
assert!(!paths.iter().any(|p| p.starts_with("src/app/users/")));
assert!(!paths.iter().any(|p| p.ends_with("_create_users.sql")));
assert!(!content_of(&plan, "src/app/mod.rs").contains("mod users;"));
assert!(!content_of(&plan, "gize.toml").contains("users"));
}
#[test]
fn new_project_scaffolds_and_wires_users_by_default() {
let plan = new_project("shop", true, "20260704120000");
let paths = paths_of(&plan);
for file in ["mod.rs", "model.rs", "dto.rs", "repository.rs", "routes.rs"] {
assert!(
paths.contains(&format!("src/app/users/{file}")),
"missing users/{file}"
);
}
assert!(paths.contains(&"migrations/20260704120000_create_users.sql".to_string()));
let app_mod = content_of(&plan, "src/app/mod.rs");
assert!(app_mod.contains("mod users;"));
assert!(app_mod.contains(".merge(users::routes())"));
assert!(content_of(&plan, "gize.toml").contains("users"));
let migration = content_of(&plan, "migrations/20260704120000_create_users.sql");
assert!(migration.contains("is_admin BOOLEAN NOT NULL DEFAULT false"));
}
#[test]
fn make_model_plan_has_model_and_migration() {
let model = ModelSpec::parse("User", &["name:String".to_string()]).unwrap();
let plan = make_model(&model, "20260704120000");
let paths: Vec<_> = plan
.ops
.iter()
.map(|o| o.path.display().to_string())
.collect();
assert!(paths.contains(&"src/app/users/model.rs".to_string()));
assert!(paths.contains(&"migrations/20260704120000_create_users.sql".to_string()));
}
#[test]
fn make_crud_plan_has_slice_and_migration() {
let model = ModelSpec::parse("Product", &["name:String".to_string()]).unwrap();
let plan = make_crud(&model, "20260704120000");
let paths: Vec<_> = plan
.ops
.iter()
.map(|o| o.path.display().to_string())
.collect();
assert!(paths.contains(&"src/app/products/repository.rs".to_string()));
assert!(paths.contains(&"src/app/products/handler.rs".to_string()));
assert!(paths.contains(&"src/app/products/dto.rs".to_string()));
assert!(paths.contains(&"migrations/20260704120000_create_products.sql".to_string()));
}
#[test]
fn make_migration_plan_is_single_timestamped_file() {
let plan = make_migration("add_index_to_users", "20260704120000");
let paths: Vec<_> = plan
.ops
.iter()
.map(|o| o.path.display().to_string())
.collect();
assert_eq!(
paths,
vec!["migrations/20260704120000_add_index_to_users.sql".to_string()]
);
}
#[test]
fn make_app_plan_has_full_module() {
let plan = make_app("users");
let paths: Vec<_> = plan
.ops
.iter()
.map(|o| o.path.display().to_string())
.collect();
for file in [
"mod.rs",
"model.rs",
"dto.rs",
"repository.rs",
"service.rs",
"error.rs",
"handler.rs",
"routes.rs",
"tests.rs",
] {
assert!(
paths.contains(&format!("src/app/users/{file}")),
"missing {file}"
);
}
}
}