Skip to main content

ferro_cli/commands/
make_seeder.rs

1//! make:seeder command - Generate a new database seeder
2
3use console::style;
4use std::fs;
5use std::path::Path;
6
7use crate::templates;
8
9pub fn run(name: String) {
10    // Convert to PascalCase for struct name
11    let struct_name = to_pascal_case(&name);
12
13    // Append "Seeder" suffix if not already present
14    let struct_name = if struct_name.ends_with("Seeder") {
15        struct_name
16    } else {
17        format!("{struct_name}Seeder")
18    };
19
20    // Convert to snake_case for file name
21    let file_name = to_snake_case(&struct_name);
22
23    // Validate the resulting name is a valid Rust identifier
24    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    // Ensure we're in a Ferro project (check for src directory)
38    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    // Create seeders directory if it doesn't exist
51    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        // Create mod.rs
63        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    // Check if seeder file already exists
76    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    // Check if module is already declared in mod.rs
87    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    // Generate seeder file content
102    let seeder_content = templates::seeder_template(&file_name, &struct_name);
103
104    // Write seeder file
105    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    // Update mod.rs
116    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    // First character must be letter or underscore
172    match chars.next() {
173        Some(c) if c.is_alphabetic() || c == '_' => {}
174        _ => return false,
175    }
176
177    // Rest must be alphanumeric or underscore
178    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    // Find position to insert declarations
221    let lines: Vec<&str> = content.lines().collect();
222
223    // Find the last pub mod declaration line
224    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    // Build new content
237    let mut new_lines: Vec<String> = Vec::new();
238
239    // If we found existing pub mod declarations, insert after them
240    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        // No existing pub mod declarations, add at the end (before empty lines)
249        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 file was empty
262        if lines.is_empty() {
263            new_lines.push(pub_mod_decl.clone());
264        }
265    }
266
267    // Now add pub use declaration if there are existing pub use declarations
268    if last_pub_use_idx.is_some() {
269        // Find the new position of the last pub use after our modification
270        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    // Ensure file ends with newline
284    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}