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