ferro_cli/commands/
make_controller.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 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 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 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 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 let controller_content = templates::controller_template(&file_name);
66
67 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 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 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 match chars.next() {
128 Some(c) if c.is_alphabetic() || c == '_' => {}
129 _ => return false,
130 }
131
132 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 let mut lines: Vec<&str> = content.lines().collect();
159
160 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 let insert_idx = match last_pub_mod_idx {
170 Some(idx) => idx + 1,
171 None => {
172 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}