doido_generators/generators/
scaffold.rs1use crate::generator::{GeneratedFile, Generator};
2use crate::generators::field::Field;
3use crate::generators::model::ModelGenerator;
4use crate::generators::{to_pascal, to_snake, to_table_name};
5use doido_core::Result;
6
7const CONTROLLERS_MOD_BASE: &str = include_str!("../../templates/new/app/controllers/mod.rs");
10const ROUTES_BASE: &str = include_str!("../../templates/new/config/routes.rs");
11
12const CONTROLLERS_MOD_PATH: &str = "app/controllers/mod.rs";
13const ROUTES_PATH: &str = "config/routes.rs";
14
15pub struct ScaffoldGenerator;
16
17impl Generator for ScaffoldGenerator {
18 fn name(&self) -> &str {
19 "scaffold"
20 }
21
22 fn generate(&self, args: &[&str]) -> Result<Vec<GeneratedFile>> {
23 let api = args.contains(&"--api");
24 let positional: Vec<&str> = args
26 .iter()
27 .copied()
28 .filter(|a| !a.starts_with("--"))
29 .collect();
30 let name = positional.first().copied().ok_or_else(|| {
31 doido_core::anyhow::anyhow!("scaffold generator requires a name argument")
32 })?;
33 let fields = Field::parse_all(&positional[1..])?;
34
35 let singular = to_snake(name); let plural = to_table_name(name); let model = to_pascal(name); let controller = format!("{}Controller", to_pascal(&plural)); let mut files = Vec::new();
41
42 files.extend(ModelGenerator.generate(&positional)?);
44
45 let controller_template = if api {
47 crate::templates::get("scaffold/controller_api.rs.template")
48 } else {
49 crate::templates::get("scaffold/controller_html.rs.template")
50 };
51 files.push(GeneratedFile {
52 path: format!("app/controllers/{plural}_controller.rs"),
53 content: render_controller(
54 &controller_template,
55 &singular,
56 &plural,
57 &model,
58 &controller,
59 &fields,
60 ),
61 });
62
63 let mod_existing = std::fs::read_to_string(CONTROLLERS_MOD_PATH)
65 .unwrap_or_else(|_| CONTROLLERS_MOD_BASE.to_string());
66 files.push(GeneratedFile {
67 path: CONTROLLERS_MOD_PATH.to_string(),
68 content: register_controller(&mod_existing, &plural, &controller),
69 });
70
71 if !api {
73 for (file, rel) in [
74 ("index", "scaffold/views/index.html.tera"),
75 ("show", "scaffold/views/show.html.tera"),
76 ("new", "scaffold/views/new.html.tera"),
77 ("edit", "scaffold/views/edit.html.tera"),
78 ("_form", "scaffold/views/_form.html.tera"),
79 ] {
80 let template = crate::templates::get(rel);
81 files.push(GeneratedFile {
82 path: format!("app/views/{plural}/{file}.html.tera"),
83 content: render_view(&template, &singular, &plural, &model, &fields),
84 });
85 }
86
87 let test_template = crate::templates::get("scaffold/controller_test.rs.template");
89 files.push(GeneratedFile {
90 path: format!("tests/{plural}_controller_test.rs"),
91 content: render_controller(
92 &test_template,
93 &singular,
94 &plural,
95 &model,
96 &controller,
97 &fields,
98 ),
99 });
100 }
101
102 let routes_existing =
104 std::fs::read_to_string(ROUTES_PATH).unwrap_or_else(|_| ROUTES_BASE.to_string());
105 files.push(GeneratedFile {
106 path: ROUTES_PATH.to_string(),
107 content: inject_route(&routes_existing, &plural, &controller, api),
108 });
109
110 Ok(files)
111 }
112}
113
114fn render_controller(
116 template: &str,
117 singular: &str,
118 plural: &str,
119 model: &str,
120 controller: &str,
121 fields: &[Field],
122) -> String {
123 let params_fields: String = fields
124 .iter()
125 .map(|f| format!(" {}\n", f.params_struct_field()))
126 .collect();
127 let active_model_sets: String = fields
128 .iter()
129 .map(|f| format!(" {}\n", f.active_model_set()))
130 .collect();
131 let active_model_assigns: String = fields
132 .iter()
133 .map(|f| format!(" {}\n", f.active_model_assign()))
134 .collect();
135 let form_body: String = fields
137 .iter()
138 .filter_map(Field::sample_form_pair)
139 .collect::<Vec<_>>()
140 .join("&");
141
142 template
143 .replace("{params_fields}", ¶ms_fields)
144 .replace("{active_model_sets}", &active_model_sets)
145 .replace("{active_model_assigns}", &active_model_assigns)
146 .replace("{form_body}", &form_body)
147 .replace("{Controller}", controller)
148 .replace("{Model}", model)
149 .replace("{singular}", singular)
150 .replace("{plural}", plural)
151}
152
153fn render_view(
155 template: &str,
156 singular: &str,
157 plural: &str,
158 model: &str,
159 fields: &[Field],
160) -> String {
161 let table_headers: String = fields
162 .iter()
163 .map(|f| format!(" <th>{}</th>\n", f.column_name()))
164 .collect();
165 let table_cells: String = fields
166 .iter()
167 .map(|f| format!(" <td>{{{{ {singular}.{} }}}}</td>\n", f.column_name()))
168 .collect();
169 let show_fields: String = fields
170 .iter()
171 .map(|f| {
172 let col = f.column_name();
173 format!("<p><strong>{col}:</strong> {{{{ {singular}.{col} }}}}</p>\n")
174 })
175 .collect();
176 let form_fields: String = fields.iter().map(form_field).collect();
177
178 template
179 .replace("{table_headers}", &table_headers)
180 .replace("{table_cells}", &table_cells)
181 .replace("{show_fields}", &show_fields)
182 .replace("{form_fields}", &form_fields)
183 .replace("{Model}", model)
184 .replace("{singular}", singular)
185 .replace("{plural}", plural)
186}
187
188fn form_field(f: &Field) -> String {
190 let col = f.column_name();
191 match f.html_input_type() {
192 "textarea" => format!(" <label>{col}<br><textarea name=\"{col}\"></textarea></label>\n"),
193 "checkbox" => format!(" <label>{col} <input type=\"checkbox\" name=\"{col}\"></label>\n"),
194 input => format!(" <label>{col}<br><input type=\"{input}\" name=\"{col}\"></label>\n"),
195 }
196}
197
198fn register_controller(controllers_mod: &str, plural: &str, controller: &str) -> String {
201 let module = format!("{plural}_controller");
202 let decl = format!("mod {module};");
203 if controllers_mod.lines().any(|l| l.trim() == decl) {
204 return controllers_mod.to_string();
205 }
206 let mut out = controllers_mod.trim_end().to_string();
207 out.push('\n');
208 out.push_str(&format!("mod {module};\n"));
209 out.push_str(&format!("pub use {module}::{controller};\n"));
210 out
211}
212
213fn inject_route(routes: &str, plural: &str, controller: &str, api: bool) -> String {
221 let resources = if api {
222 format!("resources!({plural}, {controller}, except: [new, edit]);")
223 } else {
224 format!("resources!({plural}, {controller});")
225 };
226 if routes.contains(&resources) {
227 return routes.to_string();
228 }
229
230 let use_line = format!("use crate::controllers::{controller};");
231 let mut lines: Vec<String> = routes.lines().map(String::from).collect();
232
233 if !routes.contains(&use_line) {
236 let pos = lines
237 .iter()
238 .rposition(|l| l.contains("use crate::controllers"))
239 .map(|i| i + 1)
240 .unwrap_or(0);
241 lines.insert(pos, use_line);
242 }
243
244 if let Some(open) = lines.iter().position(|l| l.contains("routes!")) {
246 if let Some(close_rel) = lines[open..].iter().position(|l| l.trim() == "}") {
248 let close = open + close_rel;
249 lines.insert(close, format!(" {resources}"));
250 }
251 }
252
253 let mut out = lines.join("\n");
254 out.push('\n');
255 out
256}