Skip to main content

ferro_cli/commands/
make_controller.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    // Validate the resulting name is a valid Rust identifier
12    if !is_valid_identifier(&file_name) {
13        eprintln!(
14            "{} '{}' is not a valid controller name",
15            style("Error:").red().bold(),
16            name
17        );
18        std::process::exit(1);
19    }
20
21    let controllers_dir = Path::new("src/controllers");
22    let controller_file = controllers_dir.join(format!("{file_name}.rs"));
23    let mod_file = controllers_dir.join("mod.rs");
24
25    // Check if controllers directory exists
26    if !controllers_dir.exists() {
27        eprintln!(
28            "{} Controllers directory not found at src/controllers",
29            style("Error:").red().bold()
30        );
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    // Check if controller file already exists
39    if controller_file.exists() {
40        eprintln!(
41            "{} Controller '{}' already exists at {}",
42            style("Info:").yellow().bold(),
43            file_name,
44            controller_file.display()
45        );
46        std::process::exit(0);
47    }
48
49    // Check if module is already declared in mod.rs
50    if mod_file.exists() {
51        let mod_content = fs::read_to_string(&mod_file).unwrap_or_default();
52        let mod_decl = format!("mod {file_name};");
53        let pub_mod_decl = format!("pub mod {file_name};");
54        if mod_content.contains(&mod_decl) || mod_content.contains(&pub_mod_decl) {
55            eprintln!(
56                "{} Module '{}' is already declared in src/controllers/mod.rs",
57                style("Info:").yellow().bold(),
58                file_name
59            );
60            std::process::exit(0);
61        }
62    }
63
64    // Generate controller file content
65    let controller_content = templates::controller_template(&file_name);
66
67    // Write controller file
68    if let Err(e) = fs::write(&controller_file, controller_content) {
69        eprintln!(
70            "{} Failed to write controller file: {}",
71            style("Error:").red().bold(),
72            e
73        );
74        std::process::exit(1);
75    }
76    println!(
77        "{} Created {}",
78        style("✓").green(),
79        controller_file.display()
80    );
81
82    // Update mod.rs
83    if mod_file.exists() {
84        if let Err(e) = update_mod_file(&mod_file, &file_name) {
85            eprintln!(
86                "{} Failed to update mod.rs: {}",
87                style("Error:").red().bold(),
88                e
89            );
90            std::process::exit(1);
91        }
92        println!("{} Updated src/controllers/mod.rs", style("✓").green());
93    } else {
94        // Create mod.rs if it doesn't exist
95        let mod_content = format!("pub mod {file_name};\n");
96        if let Err(e) = fs::write(&mod_file, mod_content) {
97            eprintln!(
98                "{} Failed to create mod.rs: {}",
99                style("Error:").red().bold(),
100                e
101            );
102            std::process::exit(1);
103        }
104        println!("{} Created src/controllers/mod.rs", style("✓").green());
105    }
106
107    println!();
108    println!(
109        "Controller {} created successfully!",
110        style(&file_name).cyan().bold()
111    );
112    println!();
113    println!("Usage:");
114    println!("  {} Add a route in src/routes.rs:", style("1.").dim());
115    println!("     .get(\"/{file_name}\", controllers::{file_name}::invoke)");
116    println!();
117}
118
119fn is_valid_identifier(name: &str) -> bool {
120    if name.is_empty() {
121        return false;
122    }
123
124    let mut chars = name.chars();
125
126    // First character must be letter or underscore
127    match chars.next() {
128        Some(c) if c.is_alphabetic() || c == '_' => {}
129        _ => return false,
130    }
131
132    // Rest must be alphanumeric or underscore
133    chars.all(|c| c.is_alphanumeric() || c == '_')
134}
135
136fn to_snake_case(s: &str) -> String {
137    let mut result = String::new();
138    for (i, c) in s.chars().enumerate() {
139        if c.is_uppercase() {
140            if i > 0 {
141                result.push('_');
142            }
143            result.push(c.to_lowercase().next().unwrap());
144        } else {
145            result.push(c);
146        }
147    }
148    result
149}
150
151fn update_mod_file(mod_file: &Path, file_name: &str) -> Result<(), String> {
152    let content =
153        fs::read_to_string(mod_file).map_err(|e| format!("Failed to read mod.rs: {e}"))?;
154
155    let pub_mod_decl = format!("pub mod {file_name};");
156
157    // Find position to insert pub mod declaration (after other pub mod declarations)
158    let mut lines: Vec<&str> = content.lines().collect();
159
160    // Find the last pub mod declaration line
161    let mut last_pub_mod_idx = None;
162    for (i, line) in lines.iter().enumerate() {
163        if line.trim().starts_with("pub mod ") {
164            last_pub_mod_idx = Some(i);
165        }
166    }
167
168    // Insert pub mod declaration
169    let insert_idx = match last_pub_mod_idx {
170        Some(idx) => idx + 1,
171        None => {
172            // If no pub mod declarations, insert at the beginning (after any doc comments)
173            let mut insert_idx = 0;
174            for (i, line) in lines.iter().enumerate() {
175                if line.starts_with("//!") || line.is_empty() {
176                    insert_idx = i + 1;
177                } else {
178                    break;
179                }
180            }
181            insert_idx
182        }
183    };
184    lines.insert(insert_idx, &pub_mod_decl);
185
186    let new_content = lines.join("\n");
187    fs::write(mod_file, new_content).map_err(|e| format!("Failed to write mod.rs: {e}"))?;
188
189    Ok(())
190}