doido_generators/generators/
model.rs1use crate::generator::{GeneratedFile, Generator};
2use crate::generators::field::Field;
3use crate::generators::migration_support::{
4 create_table_imports, create_table_up, register_migration, MIGRATION_LIB_BASE,
5 MIGRATION_SRC_DIR,
6};
7use crate::generators::{to_pascal, to_snake, to_table_name};
8use chrono::Utc;
9use doido_core::Result;
10
11const MODELS_MOD_BASE: &str = include_str!("../../templates/new/app/models/mod.rs");
13const MODELS_MOD_PATH: &str = "app/models/mod.rs";
15
16pub struct ModelGenerator;
17
18impl Generator for ModelGenerator {
19 fn name(&self) -> &str {
20 "model"
21 }
22
23 fn generate(&self, args: &[&str]) -> Result<Vec<GeneratedFile>> {
24 let name = args.first().copied().ok_or_else(|| {
25 doido_core::anyhow::anyhow!("model generator requires a name argument")
26 })?;
27 let snake = to_snake(name);
28 let table_name = to_table_name(name);
31
32 let fields = Field::parse_all(&args[1..])?;
34
35 let model = crate::templates::get("models/model.rs.template")
37 .replace("{table_name}", &table_name)
38 .replace("{fields}", &model_fields(&fields));
39
40 let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
42 let migration_module = format!("m{timestamp}_create_{table_name}_table");
43 let migration = crate::templates::get("models/migration.rs.template")
44 .replace("{migration_name}", &migration_module)
45 .replace("{migration_imports}", &create_table_imports(&fields))
46 .replace("{up_body}", &create_table_up(&table_name, &fields))
47 .replace("{table_name}", &table_name);
48
49 let lib_path = format!("{MIGRATION_SRC_DIR}/lib.rs");
52 let existing =
53 std::fs::read_to_string(&lib_path).unwrap_or_else(|_| MIGRATION_LIB_BASE.to_string());
54 let lib = register_migration(&existing, &migration_module);
55
56 let models_mod_existing = std::fs::read_to_string(MODELS_MOD_PATH)
59 .unwrap_or_else(|_| MODELS_MOD_BASE.to_string());
60 let models_mod = register_model_module(&models_mod_existing, &snake);
61
62 let model_test = crate::templates::get("models/model_test.rs.template")
65 .replace("{Model}", &to_pascal(name))
66 .replace("{singular}", &snake);
67
68 Ok(vec![
69 GeneratedFile {
70 path: format!("app/models/{snake}.rs"),
71 content: model,
72 },
73 GeneratedFile {
74 path: format!("{MIGRATION_SRC_DIR}/{migration_module}.rs"),
75 content: migration,
76 },
77 GeneratedFile {
78 path: lib_path,
79 content: lib,
80 },
81 GeneratedFile {
82 path: MODELS_MOD_PATH.to_string(),
83 content: models_mod,
84 },
85 GeneratedFile {
86 path: format!("tests/{snake}_model_test.rs"),
87 content: model_test,
88 },
89 ])
90 }
91}
92
93fn model_fields(fields: &[Field]) -> String {
96 fields
97 .iter()
98 .map(|f| format!(" {}\n", f.model_field()))
99 .collect()
100}
101
102fn register_model_module(models_mod: &str, module: &str) -> String {
106 let decl = format!("pub mod {module};");
107 if models_mod.lines().any(|l| l.trim() == decl) {
108 return models_mod.to_string();
109 }
110
111 let mut lines: Vec<String> = models_mod.lines().map(String::from).collect();
112 if let Some(i) = lines.iter().position(|l| l.contains("@generated-models")) {
113 lines.insert(i, decl);
114 } else {
115 lines.push(decl);
116 }
117 let mut out = lines.join("\n");
118 out.push('\n');
119 out
120}