ferro_cli/commands/
make_factory.rs1use console::style;
2use std::fs;
3use std::path::Path;
4
5use crate::templates;
6
7pub fn run(name: String) {
8 let struct_name = to_pascal_case(&name);
10
11 let struct_name = if struct_name.ends_with("Factory") {
13 struct_name
14 } else {
15 format!("{struct_name}Factory")
16 };
17
18 let model_name = struct_name
20 .strip_suffix("Factory")
21 .unwrap_or(&struct_name)
22 .to_string();
23
24 let file_name = to_snake_case(&struct_name);
26
27 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 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 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 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 let factory_content = templates::factory_template(&file_name, &struct_name, &model_name);
82
83 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 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 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 match chars.next() {
154 Some(c) if c.is_alphabetic() || c == '_' => {}
155 _ => return false,
156 }
157
158 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 let mut lines: Vec<&str> = content.lines().collect();
202
203 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 let insert_idx = match last_pub_mod_idx {
213 Some(idx) => idx + 1,
214 None => {
215 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}