doido_generators/generators/
model.rs1use crate::generator::{GeneratedFile, Generator};
2use crate::generators::field::Field;
3use crate::generators::{to_pascal, to_snake, to_table_name};
4use chrono::Utc;
5use doido_core::Result;
6
7const MIGRATION_LIB_BASE: &str = include_str!("../../templates/new/db/migration/src/lib.rs");
10const MODELS_MOD_BASE: &str = include_str!("../../templates/new/app/models/mod.rs");
12
13const MIGRATION_SRC_DIR: &str = "db/migration/src";
15const MODELS_MOD_PATH: &str = "app/models/mod.rs";
17
18pub struct ModelGenerator;
19
20impl Generator for ModelGenerator {
21 fn name(&self) -> &str {
22 "model"
23 }
24
25 fn generate(&self, args: &[&str]) -> Result<Vec<GeneratedFile>> {
26 let name = args.first().copied().ok_or_else(|| {
27 doido_core::anyhow::anyhow!("model generator requires a name argument")
28 })?;
29 let snake = to_snake(name);
30 let table_name = to_table_name(name);
33
34 let fields = Field::parse_all(&args[1..])?;
36
37 let model = crate::templates::get("models/model.rs.template")
39 .replace("{table_name}", &table_name)
40 .replace("{fields}", &model_fields(&fields));
41
42 let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
45 let migration_module = format!("m{timestamp}_create_{table_name}_table");
46 let migration = crate::templates::get("models/migration.rs.template")
47 .replace("{migration_imports}", &migration_imports(&fields))
48 .replace("{up_body}", &migration_up_body(&table_name, &fields))
49 .replace("{table_name}", &table_name);
50
51 let lib_path = format!("{MIGRATION_SRC_DIR}/lib.rs");
54 let existing =
55 std::fs::read_to_string(&lib_path).unwrap_or_else(|_| MIGRATION_LIB_BASE.to_string());
56 let lib = register_migration(&existing, &migration_module);
57
58 let models_mod_existing = std::fs::read_to_string(MODELS_MOD_PATH)
61 .unwrap_or_else(|_| MODELS_MOD_BASE.to_string());
62 let models_mod = register_model_module(&models_mod_existing, &snake);
63
64 let model_test = crate::templates::get("models/model_test.rs.template")
67 .replace("{Model}", &to_pascal(name))
68 .replace("{singular}", &snake);
69
70 Ok(vec![
71 GeneratedFile {
72 path: format!("app/models/{snake}.rs"),
73 content: model,
74 },
75 GeneratedFile {
76 path: format!("{MIGRATION_SRC_DIR}/{migration_module}.rs"),
77 content: migration,
78 },
79 GeneratedFile {
80 path: lib_path,
81 content: lib,
82 },
83 GeneratedFile {
84 path: MODELS_MOD_PATH.to_string(),
85 content: models_mod,
86 },
87 GeneratedFile {
88 path: format!("tests/{snake}_model_test.rs"),
89 content: model_test,
90 },
91 ])
92 }
93}
94
95fn model_fields(fields: &[Field]) -> String {
98 fields
99 .iter()
100 .map(|f| format!(" {}\n", f.model_field()))
101 .collect()
102}
103
104fn migration_imports(fields: &[Field]) -> String {
107 if fields.iter().any(Field::wants_index) {
108 "use doido_model::migration::{add_index, create_table, drop_table};".to_string()
109 } else {
110 "use doido_model::migration::{create_table, drop_table};".to_string()
111 }
112}
113
114fn migration_up_body(table_name: &str, fields: &[Field]) -> String {
117 if fields.is_empty() {
119 return format!(
120 " // `create_table` adds an auto-incrementing `id` primary key for you.\n\
121 \x20 // Add columns with the builder, e.g. `t.string(\"name\").not_null();`.\n\
122 \x20 create_table(manager, \"{table_name}\", |_t| {{}}).await\n"
123 );
124 }
125
126 let columns: String = fields
127 .iter()
128 .map(|f| format!(" {}\n", f.migration_line()))
129 .collect();
130
131 let indexes: Vec<&Field> = fields.iter().filter(|f| f.wants_index()).collect();
132
133 let mut body = String::new();
134 body.push_str(
135 " // `create_table` adds an auto-incrementing `id` primary key for you.\n",
136 );
137 body.push_str(&format!(
138 " create_table(manager, \"{table_name}\", |t| {{\n{columns} }})\n"
139 ));
140
141 if indexes.is_empty() {
142 body.push_str(" .await\n");
144 } else {
145 body.push_str(" .await?;\n");
146 for f in indexes {
147 body.push_str(&format!(
148 " add_index(manager, \"{table_name}\", &[\"{}\"]).await?;\n",
149 f.column_name()
150 ));
151 }
152 body.push_str(" Ok(())\n");
153 }
154
155 body
156}
157
158fn register_model_module(models_mod: &str, module: &str) -> String {
162 let decl = format!("pub mod {module};");
163 if models_mod.lines().any(|l| l.trim() == decl) {
164 return models_mod.to_string();
165 }
166
167 let mut lines: Vec<String> = models_mod.lines().map(String::from).collect();
168 if let Some(i) = lines.iter().position(|l| l.contains("@generated-models")) {
169 lines.insert(i, decl);
170 } else {
171 lines.push(decl);
172 }
173 let mut out = lines.join("\n");
174 out.push('\n');
175 out
176}
177
178fn register_migration(lib: &str, module: &str) -> String {
182 let mut lines: Vec<String> = lib.lines().map(String::from).collect();
183
184 if let Some(i) = lines
185 .iter()
186 .position(|l| l.contains("@generated-migrations-mod"))
187 {
188 lines.insert(i, format!("mod {module};"));
189 }
190
191 if let Some(i) = lines
192 .iter()
193 .position(|l| l.contains("@generated-migrations-list"))
194 {
195 let indent: String = lines[i].chars().take_while(|c| c.is_whitespace()).collect();
196 lines.insert(i, format!("{indent}Box::new({module}::Migration),"));
197 }
198
199 let mut out = lines.join("\n");
200 out.push('\n');
201 out
202}