ferro_cli/commands/
make_action.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("Action") {
13 struct_name
14 } else {
15 format!("{struct_name}Action")
16 };
17
18 let file_name = to_snake_case(&struct_name);
20
21 if !is_valid_identifier(&file_name) {
23 eprintln!(
24 "{} '{}' is not a valid action name",
25 style("Error:").red().bold(),
26 name
27 );
28 std::process::exit(1);
29 }
30
31 let actions_dir = Path::new("src/actions");
32 let action_file = actions_dir.join(format!("{file_name}.rs"));
33 let mod_file = actions_dir.join("mod.rs");
34
35 if !actions_dir.exists() {
37 eprintln!(
38 "{} Actions directory not found at src/actions",
39 style("Error:").red().bold()
40 );
41 eprintln!(
42 "{}",
43 style("Make sure you're in a Ferro project root directory.").dim()
44 );
45 eprintln!(
46 "{}",
47 style("You can create the directory with: mkdir -p src/actions").dim()
48 );
49 std::process::exit(1);
50 }
51
52 if action_file.exists() {
54 eprintln!(
55 "{} Action '{}' already exists at {}",
56 style("Info:").yellow().bold(),
57 struct_name,
58 action_file.display()
59 );
60 std::process::exit(0);
61 }
62
63 if mod_file.exists() {
65 let mod_content = fs::read_to_string(&mod_file).unwrap_or_default();
66 let mod_decl = format!("mod {file_name};");
67 let pub_mod_decl = format!("pub mod {file_name};");
68 if mod_content.contains(&mod_decl) || mod_content.contains(&pub_mod_decl) {
69 eprintln!(
70 "{} Module '{}' is already declared in src/actions/mod.rs",
71 style("Info:").yellow().bold(),
72 file_name
73 );
74 std::process::exit(0);
75 }
76 }
77
78 let action_content = templates::action_template(&file_name, &struct_name);
80
81 if let Err(e) = fs::write(&action_file, action_content) {
83 eprintln!(
84 "{} Failed to write action file: {}",
85 style("Error:").red().bold(),
86 e
87 );
88 std::process::exit(1);
89 }
90 println!("{} Created {}", style("✓").green(), action_file.display());
91
92 if mod_file.exists() {
94 if let Err(e) = update_mod_file(&mod_file, &file_name) {
95 eprintln!(
96 "{} Failed to update mod.rs: {}",
97 style("Error:").red().bold(),
98 e
99 );
100 std::process::exit(1);
101 }
102 println!("{} Updated src/actions/mod.rs", style("✓").green());
103 } else {
104 let mod_content = format!("pub mod {file_name};\n");
106 if let Err(e) = fs::write(&mod_file, mod_content) {
107 eprintln!(
108 "{} Failed to create mod.rs: {}",
109 style("Error:").red().bold(),
110 e
111 );
112 std::process::exit(1);
113 }
114 println!("{} Created src/actions/mod.rs", style("✓").green());
115 }
116
117 println!();
118 println!(
119 "Action {} created successfully!",
120 style(&struct_name).cyan().bold()
121 );
122 println!();
123 println!("Usage:");
124 println!(
125 " {} Resolve from container in your controller:",
126 style("1.").dim()
127 );
128 println!(" let action: {struct_name} = App::get().unwrap();");
129 println!(" action.execute();");
130 println!();
131}
132
133fn is_valid_identifier(name: &str) -> bool {
134 if name.is_empty() {
135 return false;
136 }
137
138 let mut chars = name.chars();
139
140 match chars.next() {
142 Some(c) if c.is_alphabetic() || c == '_' => {}
143 _ => return false,
144 }
145
146 chars.all(|c| c.is_alphanumeric() || c == '_')
148}
149
150fn to_snake_case(s: &str) -> String {
151 let mut result = String::new();
152 for (i, c) in s.chars().enumerate() {
153 if c.is_uppercase() {
154 if i > 0 {
155 result.push('_');
156 }
157 result.push(c.to_lowercase().next().unwrap());
158 } else {
159 result.push(c);
160 }
161 }
162 result
163}
164
165fn to_pascal_case(s: &str) -> String {
166 let mut result = String::new();
167 let mut capitalize_next = true;
168
169 for c in s.chars() {
170 if c == '_' || c == '-' || c == ' ' {
171 capitalize_next = true;
172 } else if capitalize_next {
173 result.push(c.to_uppercase().next().unwrap());
174 capitalize_next = false;
175 } else {
176 result.push(c);
177 }
178 }
179 result
180}
181
182fn update_mod_file(mod_file: &Path, file_name: &str) -> Result<(), String> {
183 let content =
184 fs::read_to_string(mod_file).map_err(|e| format!("Failed to read mod.rs: {e}"))?;
185
186 let pub_mod_decl = format!("pub mod {file_name};");
187
188 let mut lines: Vec<&str> = content.lines().collect();
190
191 let mut last_pub_mod_idx = None;
193 for (i, line) in lines.iter().enumerate() {
194 if line.trim().starts_with("pub mod ") {
195 last_pub_mod_idx = Some(i);
196 }
197 }
198
199 let insert_idx = match last_pub_mod_idx {
201 Some(idx) => idx + 1,
202 None => {
203 let mut insert_idx = 0;
205 for (i, line) in lines.iter().enumerate() {
206 if line.starts_with("//!") || line.is_empty() {
207 insert_idx = i + 1;
208 } else {
209 break;
210 }
211 }
212 insert_idx
213 }
214 };
215 lines.insert(insert_idx, &pub_mod_decl);
216
217 let new_content = lines.join("\n");
218 fs::write(mod_file, new_content).map_err(|e| format!("Failed to write mod.rs: {e}"))?;
219
220 Ok(())
221}