ferro_cli/commands/
make_seeder.rs1use console::style;
4use std::fs;
5use std::path::Path;
6
7use crate::templates;
8
9pub fn run(name: String) {
10 let struct_name = to_pascal_case(&name);
12
13 let struct_name = if struct_name.ends_with("Seeder") {
15 struct_name
16 } else {
17 format!("{struct_name}Seeder")
18 };
19
20 let file_name = to_snake_case(&struct_name);
22
23 if !is_valid_identifier(&file_name) {
25 eprintln!(
26 "{} '{}' is not a valid seeder name",
27 style("Error:").red().bold(),
28 name
29 );
30 std::process::exit(1);
31 }
32
33 let seeders_dir = Path::new("src/seeders");
34 let seeder_file = seeders_dir.join(format!("{file_name}.rs"));
35 let mod_file = seeders_dir.join("mod.rs");
36
37 if !Path::new("src").exists() {
39 eprintln!(
40 "{} Not in a Ferro project root directory",
41 style("Error:").red().bold()
42 );
43 eprintln!(
44 "{}",
45 style("Make sure you're in a Ferro project directory with a src/ folder.").dim()
46 );
47 std::process::exit(1);
48 }
49
50 if !seeders_dir.exists() {
52 if let Err(e) = fs::create_dir_all(seeders_dir) {
53 eprintln!(
54 "{} Failed to create seeders directory: {}",
55 style("Error:").red().bold(),
56 e
57 );
58 std::process::exit(1);
59 }
60 println!("{} Created src/seeders/", style("✓").green());
61
62 let mod_content = templates::seeders_mod();
64 if let Err(e) = fs::write(&mod_file, mod_content) {
65 eprintln!(
66 "{} Failed to create mod.rs: {}",
67 style("Error:").red().bold(),
68 e
69 );
70 std::process::exit(1);
71 }
72 println!("{} Created src/seeders/mod.rs", style("✓").green());
73 }
74
75 if seeder_file.exists() {
77 eprintln!(
78 "{} Seeder '{}' already exists at {}",
79 style("Info:").yellow().bold(),
80 struct_name,
81 seeder_file.display()
82 );
83 std::process::exit(0);
84 }
85
86 if mod_file.exists() {
88 let mod_content = fs::read_to_string(&mod_file).unwrap_or_default();
89 let mod_decl = format!("mod {file_name};");
90 let pub_mod_decl = format!("pub mod {file_name};");
91 if mod_content.contains(&mod_decl) || mod_content.contains(&pub_mod_decl) {
92 eprintln!(
93 "{} Module '{}' is already declared in src/seeders/mod.rs",
94 style("Info:").yellow().bold(),
95 file_name
96 );
97 std::process::exit(0);
98 }
99 }
100
101 let seeder_content = templates::seeder_template(&file_name, &struct_name);
103
104 if let Err(e) = fs::write(&seeder_file, seeder_content) {
106 eprintln!(
107 "{} Failed to write seeder file: {}",
108 style("Error:").red().bold(),
109 e
110 );
111 std::process::exit(1);
112 }
113 println!("{} Created {}", style("✓").green(), seeder_file.display());
114
115 if let Err(e) = update_mod_file(&mod_file, &file_name, &struct_name) {
117 eprintln!(
118 "{} Failed to update mod.rs: {}",
119 style("Error:").red().bold(),
120 e
121 );
122 std::process::exit(1);
123 }
124 println!("{} Updated src/seeders/mod.rs", style("✓").green());
125
126 println!();
127 println!(
128 "Seeder {} created successfully!",
129 style(&struct_name).cyan().bold()
130 );
131 println!();
132 println!("Next steps:");
133 println!(
134 " {} Implement your seeder logic in {}",
135 style("1.").dim(),
136 seeder_file.display()
137 );
138 println!();
139 println!(
140 " {} Register the seeder in src/seeders/mod.rs:",
141 style("2.").dim()
142 );
143 println!(
144 " {}",
145 style(format!(
146 "SeederRegistry::new().add::<{struct_name}>() // for Default seeders"
147 ))
148 .cyan()
149 );
150 println!();
151 println!(" {} Run the seeders:", style("3.").dim());
152 println!(
153 " ./target/debug/app db:seed {} Run all seeders",
154 style("#").dim()
155 );
156 println!(
157 " ./target/debug/app db:seed --class {} {} Run specific seeder",
158 struct_name,
159 style("#").dim()
160 );
161 println!();
162}
163
164fn is_valid_identifier(name: &str) -> bool {
165 if name.is_empty() {
166 return false;
167 }
168
169 let mut chars = name.chars();
170
171 match chars.next() {
173 Some(c) if c.is_alphabetic() || c == '_' => {}
174 _ => return false,
175 }
176
177 chars.all(|c| c.is_alphanumeric() || c == '_')
179}
180
181fn to_snake_case(s: &str) -> String {
182 let mut result = String::new();
183 for (i, c) in s.chars().enumerate() {
184 if c.is_uppercase() {
185 if i > 0 {
186 result.push('_');
187 }
188 result.push(c.to_lowercase().next().unwrap());
189 } else {
190 result.push(c);
191 }
192 }
193 result
194}
195
196fn to_pascal_case(s: &str) -> String {
197 let mut result = String::new();
198 let mut capitalize_next = true;
199
200 for c in s.chars() {
201 if c == '_' || c == '-' || c == ' ' {
202 capitalize_next = true;
203 } else if capitalize_next {
204 result.push(c.to_uppercase().next().unwrap());
205 capitalize_next = false;
206 } else {
207 result.push(c);
208 }
209 }
210 result
211}
212
213fn update_mod_file(mod_file: &Path, file_name: &str, struct_name: &str) -> Result<(), String> {
214 let content =
215 fs::read_to_string(mod_file).map_err(|e| format!("Failed to read mod.rs: {e}"))?;
216
217 let pub_mod_decl = format!("pub mod {file_name};");
218 let pub_use_decl = format!("pub use {file_name}::{struct_name};");
219
220 let lines: Vec<&str> = content.lines().collect();
222
223 let mut last_pub_mod_idx = None;
225 let mut last_pub_use_idx = None;
226
227 for (i, line) in lines.iter().enumerate() {
228 if line.trim().starts_with("pub mod ") {
229 last_pub_mod_idx = Some(i);
230 }
231 if line.trim().starts_with("pub use ") {
232 last_pub_use_idx = Some(i);
233 }
234 }
235
236 let mut new_lines: Vec<String> = Vec::new();
238
239 if let Some(idx) = last_pub_mod_idx {
241 for (i, line) in lines.iter().enumerate() {
242 new_lines.push(line.to_string());
243 if i == idx {
244 new_lines.push(pub_mod_decl.clone());
245 }
246 }
247 } else {
248 let mut content_end = lines.len();
250 while content_end > 0 && lines[content_end - 1].trim().is_empty() {
251 content_end -= 1;
252 }
253
254 for (i, line) in lines.iter().enumerate() {
255 new_lines.push(line.to_string());
256 if i == content_end.saturating_sub(1) || (content_end == 0 && i == 0) {
257 new_lines.push(pub_mod_decl.clone());
258 }
259 }
260
261 if lines.is_empty() {
263 new_lines.push(pub_mod_decl.clone());
264 }
265 }
266
267 if last_pub_use_idx.is_some() {
269 let mut insert_idx = None;
271 for (i, line) in new_lines.iter().enumerate() {
272 if line.trim().starts_with("pub use ") {
273 insert_idx = Some(i);
274 }
275 }
276 if let Some(idx) = insert_idx {
277 new_lines.insert(idx + 1, pub_use_decl);
278 }
279 }
280
281 let new_content = new_lines.join("\n");
282
283 let new_content = if new_content.ends_with('\n') {
285 new_content
286 } else {
287 format!("{new_content}\n")
288 };
289
290 fs::write(mod_file, new_content).map_err(|e| format!("Failed to write mod.rs: {e}"))?;
291
292 Ok(())
293}