Skip to main content

ferro_cli/commands/
make_factory.rs

1use console::style;
2use std::fs;
3use std::path::Path;
4
5use crate::templates;
6
7pub fn run(name: String) {
8    // Convert to PascalCase for struct name
9    let struct_name = to_pascal_case(&name);
10
11    // Append "Factory" suffix if not already present
12    let struct_name = if struct_name.ends_with("Factory") {
13        struct_name
14    } else {
15        format!("{struct_name}Factory")
16    };
17
18    // Extract model name (remove Factory suffix)
19    let model_name = struct_name
20        .strip_suffix("Factory")
21        .unwrap_or(&struct_name)
22        .to_string();
23
24    // Convert to snake_case for file name
25    let file_name = to_snake_case(&struct_name);
26
27    // Validate the resulting name is a valid Rust identifier
28    if !is_valid_identifier(&file_name) {
29        eprintln!(
30            "{} '{}' is not a valid factory name",
31            style("Error:").red().bold(),
32            name
33        );
34        std::process::exit(1);
35    }
36
37    let factories_dir = Path::new("src/factories");
38    let factory_file = factories_dir.join(format!("{file_name}.rs"));
39    let mod_file = factories_dir.join("mod.rs");
40
41    // Create factories directory if it doesn't exist
42    if !factories_dir.exists() {
43        if let Err(e) = fs::create_dir_all(factories_dir) {
44            eprintln!(
45                "{} Failed to create factories directory: {}",
46                style("Error:").red().bold(),
47                e
48            );
49            std::process::exit(1);
50        }
51        println!("{} Created src/factories directory", style("✓").green());
52    }
53
54    // Check if factory file already exists
55    if factory_file.exists() {
56        eprintln!(
57            "{} Factory '{}' already exists at {}",
58            style("Info:").yellow().bold(),
59            struct_name,
60            factory_file.display()
61        );
62        std::process::exit(0);
63    }
64
65    // Check if module is already declared in mod.rs
66    if mod_file.exists() {
67        let mod_content = fs::read_to_string(&mod_file).unwrap_or_default();
68        let mod_decl = format!("mod {file_name};");
69        let pub_mod_decl = format!("pub mod {file_name};");
70        if mod_content.contains(&mod_decl) || mod_content.contains(&pub_mod_decl) {
71            eprintln!(
72                "{} Module '{}' is already declared in src/factories/mod.rs",
73                style("Info:").yellow().bold(),
74                file_name
75            );
76            std::process::exit(0);
77        }
78    }
79
80    // Generate factory file content
81    let factory_content = templates::factory_template(&file_name, &struct_name, &model_name);
82
83    // Write factory file
84    if let Err(e) = fs::write(&factory_file, factory_content) {
85        eprintln!(
86            "{} Failed to write factory file: {}",
87            style("Error:").red().bold(),
88            e
89        );
90        std::process::exit(1);
91    }
92    println!("{} Created {}", style("✓").green(), factory_file.display());
93
94    // Update or create mod.rs
95    if mod_file.exists() {
96        if let Err(e) = update_mod_file(&mod_file, &file_name) {
97            eprintln!(
98                "{} Failed to update mod.rs: {}",
99                style("Error:").red().bold(),
100                e
101            );
102            std::process::exit(1);
103        }
104        println!("{} Updated src/factories/mod.rs", style("✓").green());
105    } else {
106        // Create mod.rs with template content
107        let mut mod_content = templates::factories_mod().to_string();
108        mod_content.push_str(&format!("pub mod {file_name};\n"));
109        if let Err(e) = fs::write(&mod_file, mod_content) {
110            eprintln!(
111                "{} Failed to create mod.rs: {}",
112                style("Error:").red().bold(),
113                e
114            );
115            std::process::exit(1);
116        }
117        println!("{} Created src/factories/mod.rs", style("✓").green());
118    }
119
120    println!();
121    println!(
122        "Factory {} created successfully!",
123        style(&struct_name).cyan().bold()
124    );
125    println!();
126    println!("Usage:");
127    println!(
128        "  {} Make without persisting (in tests):",
129        style("1.").dim()
130    );
131    println!("     let model = {struct_name}::factory().make();");
132    println!();
133    println!("  {} Create with database persistence:", style("2.").dim());
134    println!("     let model = {struct_name}::factory().create().await?;");
135    println!();
136    println!("  {} Apply named traits:", style("3.").dim());
137    println!("     let admin = {struct_name}::factory().trait_(\"admin\").create().await?;");
138    println!();
139    println!("{}", style("Note:").yellow().bold());
140    println!("  Update the factory struct to match your {model_name} model fields,");
141    println!("  then uncomment the DatabaseFactory impl for database persistence.");
142    println!();
143}
144
145fn is_valid_identifier(name: &str) -> bool {
146    if name.is_empty() {
147        return false;
148    }
149
150    let mut chars = name.chars();
151
152    // First character must be letter or underscore
153    match chars.next() {
154        Some(c) if c.is_alphabetic() || c == '_' => {}
155        _ => return false,
156    }
157
158    // Rest must be alphanumeric or underscore
159    chars.all(|c| c.is_alphanumeric() || c == '_')
160}
161
162fn to_snake_case(s: &str) -> String {
163    let mut result = String::new();
164    for (i, c) in s.chars().enumerate() {
165        if c.is_uppercase() {
166            if i > 0 {
167                result.push('_');
168            }
169            result.push(c.to_lowercase().next().unwrap());
170        } else {
171            result.push(c);
172        }
173    }
174    result
175}
176
177fn to_pascal_case(s: &str) -> String {
178    let mut result = String::new();
179    let mut capitalize_next = true;
180
181    for c in s.chars() {
182        if c == '_' || c == '-' || c == ' ' {
183            capitalize_next = true;
184        } else if capitalize_next {
185            result.push(c.to_uppercase().next().unwrap());
186            capitalize_next = false;
187        } else {
188            result.push(c);
189        }
190    }
191    result
192}
193
194fn update_mod_file(mod_file: &Path, file_name: &str) -> Result<(), String> {
195    let content =
196        fs::read_to_string(mod_file).map_err(|e| format!("Failed to read mod.rs: {e}"))?;
197
198    let pub_mod_decl = format!("pub mod {file_name};");
199
200    // Find position to insert pub mod declaration (after other pub mod declarations)
201    let mut lines: Vec<&str> = content.lines().collect();
202
203    // Find the last pub mod declaration line
204    let mut last_pub_mod_idx = None;
205    for (i, line) in lines.iter().enumerate() {
206        if line.trim().starts_with("pub mod ") {
207            last_pub_mod_idx = Some(i);
208        }
209    }
210
211    // Insert pub mod declaration
212    let insert_idx = match last_pub_mod_idx {
213        Some(idx) => idx + 1,
214        None => {
215            // If no pub mod declarations, insert at the end (after any doc comments)
216            let mut insert_idx = 0;
217            for (i, line) in lines.iter().enumerate() {
218                if line.starts_with("//!") || line.is_empty() {
219                    insert_idx = i + 1;
220                } else {
221                    break;
222                }
223            }
224            insert_idx
225        }
226    };
227    lines.insert(insert_idx, &pub_mod_decl);
228
229    let new_content = lines.join("\n");
230    fs::write(mod_file, new_content).map_err(|e| format!("Failed to write mod.rs: {e}"))?;
231
232    Ok(())
233}