ferro_cli/commands/
make_error.rs1use console::style;
2use std::fs;
3use std::path::Path;
4
5use crate::templates;
6
7pub fn run(name: String) {
8 let file_name = to_snake_case(&name);
10
11 let struct_name = to_pascal_case(&name);
13
14 if !is_valid_identifier(&file_name) {
16 eprintln!(
17 "{} '{}' is not a valid error name",
18 style("Error:").red().bold(),
19 name
20 );
21 std::process::exit(1);
22 }
23
24 let errors_dir = Path::new("src/errors");
25 let error_file = errors_dir.join(format!("{file_name}.rs"));
26 let mod_file = errors_dir.join("mod.rs");
27
28 if !Path::new("src").exists() {
30 eprintln!("{} src directory not found", style("Error:").red().bold());
31 eprintln!(
32 "{}",
33 style("Make sure you're in a Ferro project root directory.").dim()
34 );
35 std::process::exit(1);
36 }
37
38 let created_dir = if !errors_dir.exists() {
40 if let Err(e) = fs::create_dir_all(errors_dir) {
41 eprintln!(
42 "{} Failed to create errors directory: {}",
43 style("Error:").red().bold(),
44 e
45 );
46 std::process::exit(1);
47 }
48 println!("{} Created src/errors/", style("✓").green());
49 true
50 } else {
51 false
52 };
53
54 if error_file.exists() {
56 eprintln!(
57 "{} Error '{}' already exists at {}",
58 style("Info:").yellow().bold(),
59 struct_name,
60 error_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/errors/mod.rs",
73 style("Info:").yellow().bold(),
74 file_name
75 );
76 std::process::exit(0);
77 }
78 }
79
80 let error_content = templates::error_template(&struct_name);
82
83 if let Err(e) = fs::write(&error_file, error_content) {
85 eprintln!(
86 "{} Failed to write error file: {}",
87 style("Error:").red().bold(),
88 e
89 );
90 std::process::exit(1);
91 }
92 println!("{} Created {}", style("✓").green(), error_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/errors/mod.rs", style("✓").green());
105 } else {
106 let mod_content = format!("pub mod {file_name};\n");
108 if let Err(e) = fs::write(&mod_file, mod_content) {
109 eprintln!(
110 "{} Failed to create mod.rs: {}",
111 style("Error:").red().bold(),
112 e
113 );
114 std::process::exit(1);
115 }
116 println!("{} Created src/errors/mod.rs", style("✓").green());
117 }
118
119 println!();
120 println!(
121 "Error {} created successfully!",
122 style(&struct_name).cyan().bold()
123 );
124 println!();
125 println!("Usage:");
126 println!(" {} Import in your controller:", style("1.").dim());
127 println!(" use crate::errors::{file_name}::{struct_name};");
128 println!();
129 println!(" {} Return as error:", style("2.").dim());
130 println!(" Err({struct_name})?");
131 println!();
132
133 if created_dir {
135 println!(
136 "{}",
137 style("Note: Make sure to add `mod errors;` to your src/main.rs").yellow()
138 );
139 println!();
140 }
141}
142
143fn is_valid_identifier(name: &str) -> bool {
144 if name.is_empty() {
145 return false;
146 }
147
148 let mut chars = name.chars();
149
150 match chars.next() {
152 Some(c) if c.is_alphabetic() || c == '_' => {}
153 _ => return false,
154 }
155
156 chars.all(|c| c.is_alphanumeric() || c == '_')
158}
159
160fn to_snake_case(s: &str) -> String {
161 let mut result = String::new();
162 for (i, c) in s.chars().enumerate() {
163 if c.is_uppercase() {
164 if i > 0 {
165 result.push('_');
166 }
167 result.push(c.to_lowercase().next().unwrap());
168 } else {
169 result.push(c);
170 }
171 }
172 result
173}
174
175fn to_pascal_case(s: &str) -> String {
176 let mut result = String::new();
177 let mut capitalize_next = true;
178
179 for c in s.chars() {
180 if c == '_' || c == '-' || c == ' ' {
181 capitalize_next = true;
182 } else if capitalize_next {
183 result.push(c.to_uppercase().next().unwrap());
184 capitalize_next = false;
185 } else {
186 result.push(c);
187 }
188 }
189 result
190}
191
192fn update_mod_file(mod_file: &Path, file_name: &str) -> Result<(), String> {
193 let content =
194 fs::read_to_string(mod_file).map_err(|e| format!("Failed to read mod.rs: {e}"))?;
195
196 let pub_mod_decl = format!("pub mod {file_name};");
197
198 let mut lines: Vec<&str> = content.lines().collect();
200
201 let mut last_pub_mod_idx = None;
203 for (i, line) in lines.iter().enumerate() {
204 if line.trim().starts_with("pub mod ") {
205 last_pub_mod_idx = Some(i);
206 }
207 }
208
209 let insert_idx = match last_pub_mod_idx {
211 Some(idx) => idx + 1,
212 None => {
213 let mut insert_idx = 0;
215 for (i, line) in lines.iter().enumerate() {
216 if line.starts_with("//!") || line.is_empty() {
217 insert_idx = i + 1;
218 } else {
219 break;
220 }
221 }
222 insert_idx
223 }
224 };
225 lines.insert(insert_idx, &pub_mod_decl);
226
227 let new_content = lines.join("\n");
228 fs::write(mod_file, new_content).map_err(|e| format!("Failed to write mod.rs: {e}"))?;
229
230 Ok(())
231}