Skip to main content

ferro_cli/commands/
make_error.rs

1use console::style;
2use std::fs;
3use std::path::Path;
4
5use crate::templates;
6
7pub fn run(name: String) {
8    // Convert to snake_case for file name
9    let file_name = to_snake_case(&name);
10
11    // Convert to PascalCase for struct name
12    let struct_name = to_pascal_case(&name);
13
14    // Validate the resulting name is a valid Rust identifier
15    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    // Check if we're in a Ferro project (src directory should exist)
29    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    // Create errors directory if it doesn't exist
39    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    // Check if error file already exists
55    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    // 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/errors/mod.rs",
73                style("Info:").yellow().bold(),
74                file_name
75            );
76            std::process::exit(0);
77        }
78    }
79
80    // Generate error file content
81    let error_content = templates::error_template(&struct_name);
82
83    // Write error file
84    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    // 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/errors/mod.rs", style("✓").green());
105    } else {
106        // Create mod.rs if it doesn't exist
107        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 we created the directory, remind user to add module declaration
134    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    // First character must be letter or underscore
151    match chars.next() {
152        Some(c) if c.is_alphabetic() || c == '_' => {}
153        _ => return false,
154    }
155
156    // Rest must be alphanumeric or underscore
157    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    // Find position to insert pub mod declaration (after other pub mod declarations)
199    let mut lines: Vec<&str> = content.lines().collect();
200
201    // Find the last pub mod declaration line
202    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    // Insert pub mod declaration
210    let insert_idx = match last_pub_mod_idx {
211        Some(idx) => idx + 1,
212        None => {
213            // If no pub mod declarations, insert at the beginning (after any doc comments)
214            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}