ferro_cli/commands/
make_event.rs1use console::style;
4use std::fs;
5use std::path::Path;
6
7use crate::templates;
8
9pub fn run(name: String) {
10 let struct_name = to_pascal_case(&name);
12
13 let struct_name = if struct_name.ends_with("Event") {
15 struct_name
16 } else {
17 format!("{struct_name}Event")
18 };
19
20 let file_name = to_snake_case(&struct_name);
22
23 if !is_valid_identifier(&file_name) {
25 eprintln!(
26 "{} '{}' is not a valid event name",
27 style("Error:").red().bold(),
28 name
29 );
30 std::process::exit(1);
31 }
32
33 let events_dir = Path::new("src/events");
34 let event_file = events_dir.join(format!("{file_name}.rs"));
35 let mod_file = events_dir.join("mod.rs");
36
37 if !Path::new("src").exists() {
39 eprintln!(
40 "{} Not in a Ferro project root directory",
41 style("Error:").red().bold()
42 );
43 eprintln!(
44 "{}",
45 style("Make sure you're in a Ferro project directory with a src/ folder.").dim()
46 );
47 std::process::exit(1);
48 }
49
50 if !events_dir.exists() {
52 if let Err(e) = fs::create_dir_all(events_dir) {
53 eprintln!(
54 "{} Failed to create events directory: {}",
55 style("Error:").red().bold(),
56 e
57 );
58 std::process::exit(1);
59 }
60 println!("{} Created src/events/", style("✓").green());
61
62 let mod_content = templates::events_mod();
64 if let Err(e) = fs::write(&mod_file, mod_content) {
65 eprintln!(
66 "{} Failed to create mod.rs: {}",
67 style("Error:").red().bold(),
68 e
69 );
70 std::process::exit(1);
71 }
72 println!("{} Created src/events/mod.rs", style("✓").green());
73 }
74
75 if event_file.exists() {
77 eprintln!(
78 "{} Event '{}' already exists at {}",
79 style("Info:").yellow().bold(),
80 struct_name,
81 event_file.display()
82 );
83 std::process::exit(0);
84 }
85
86 if mod_file.exists() {
88 let mod_content = fs::read_to_string(&mod_file).unwrap_or_default();
89 let mod_decl = format!("mod {file_name};");
90 let pub_mod_decl = format!("pub mod {file_name};");
91 if mod_content.contains(&mod_decl) || mod_content.contains(&pub_mod_decl) {
92 eprintln!(
93 "{} Module '{}' is already declared in src/events/mod.rs",
94 style("Info:").yellow().bold(),
95 file_name
96 );
97 std::process::exit(0);
98 }
99 }
100
101 let event_content = templates::event_template(&file_name, &struct_name);
103
104 if let Err(e) = fs::write(&event_file, event_content) {
106 eprintln!(
107 "{} Failed to write event file: {}",
108 style("Error:").red().bold(),
109 e
110 );
111 std::process::exit(1);
112 }
113 println!("{} Created {}", style("✓").green(), event_file.display());
114
115 if let Err(e) = update_mod_file(&mod_file, &file_name, &struct_name) {
117 eprintln!(
118 "{} Failed to update mod.rs: {}",
119 style("Error:").red().bold(),
120 e
121 );
122 std::process::exit(1);
123 }
124 println!("{} Updated src/events/mod.rs", style("✓").green());
125
126 println!();
127 println!(
128 "Event {} created successfully!",
129 style(&struct_name).cyan().bold()
130 );
131 println!();
132 println!("Next steps:");
133 println!(
134 " {} Add event data fields in {}",
135 style("1.").dim(),
136 event_file.display()
137 );
138 println!();
139 println!(
140 " {} Add the events module to src/lib.rs or src/main.rs:",
141 style("2.").dim()
142 );
143 println!(" {}", style("mod events;").cyan());
144 println!();
145 println!(" {} Dispatch the event in your code:", style("3.").dim());
146 println!(
147 " {}",
148 style(format!("use crate::events::{file_name}::{struct_name};")).cyan()
149 );
150 println!(
151 " {}",
152 style(format!(
153 "{struct_name} {{ /* fields */ }}.dispatch().await?;"
154 ))
155 .cyan()
156 );
157 println!();
158}
159
160fn is_valid_identifier(name: &str) -> bool {
161 if name.is_empty() {
162 return false;
163 }
164
165 let mut chars = name.chars();
166
167 match chars.next() {
169 Some(c) if c.is_alphabetic() || c == '_' => {}
170 _ => return false,
171 }
172
173 chars.all(|c| c.is_alphanumeric() || c == '_')
175}
176
177fn to_snake_case(s: &str) -> String {
178 let mut result = String::new();
179 for (i, c) in s.chars().enumerate() {
180 if c.is_uppercase() {
181 if i > 0 {
182 result.push('_');
183 }
184 result.push(c.to_lowercase().next().unwrap());
185 } else {
186 result.push(c);
187 }
188 }
189 result
190}
191
192fn to_pascal_case(s: &str) -> String {
193 let mut result = String::new();
194 let mut capitalize_next = true;
195
196 for c in s.chars() {
197 if c == '_' || c == '-' || c == ' ' {
198 capitalize_next = true;
199 } else if capitalize_next {
200 result.push(c.to_uppercase().next().unwrap());
201 capitalize_next = false;
202 } else {
203 result.push(c);
204 }
205 }
206 result
207}
208
209fn update_mod_file(mod_file: &Path, file_name: &str, struct_name: &str) -> Result<(), String> {
210 let content =
211 fs::read_to_string(mod_file).map_err(|e| format!("Failed to read mod.rs: {e}"))?;
212
213 let pub_mod_decl = format!("pub mod {file_name};");
214 let pub_use_decl = format!("pub use {file_name}::{struct_name};");
215
216 let lines: Vec<&str> = content.lines().collect();
218
219 let mut last_pub_mod_idx = None;
221 let mut last_pub_use_idx = None;
222
223 for (i, line) in lines.iter().enumerate() {
224 if line.trim().starts_with("pub mod ") {
225 last_pub_mod_idx = Some(i);
226 }
227 if line.trim().starts_with("pub use ") {
228 last_pub_use_idx = Some(i);
229 }
230 }
231
232 let mut new_lines: Vec<String> = Vec::new();
234
235 if let Some(idx) = last_pub_mod_idx {
237 for (i, line) in lines.iter().enumerate() {
238 new_lines.push(line.to_string());
239 if i == idx {
240 new_lines.push(pub_mod_decl.clone());
241 }
242 }
243 } else {
244 let mut content_end = lines.len();
246 while content_end > 0 && lines[content_end - 1].trim().is_empty() {
247 content_end -= 1;
248 }
249
250 for (i, line) in lines.iter().enumerate() {
251 new_lines.push(line.to_string());
252 if i == content_end.saturating_sub(1) || (content_end == 0 && i == 0) {
253 new_lines.push(pub_mod_decl.clone());
254 }
255 }
256
257 if lines.is_empty() {
259 new_lines.push(pub_mod_decl.clone());
260 }
261 }
262
263 if last_pub_use_idx.is_some() {
265 let mut insert_idx = None;
267 for (i, line) in new_lines.iter().enumerate() {
268 if line.trim().starts_with("pub use ") {
269 insert_idx = Some(i);
270 }
271 }
272 if let Some(idx) = insert_idx {
273 new_lines.insert(idx + 1, pub_use_decl);
274 }
275 }
276
277 let new_content = new_lines.join("\n");
278
279 let new_content = if new_content.ends_with('\n') {
281 new_content
282 } else {
283 format!("{new_content}\n")
284 };
285
286 fs::write(mod_file, new_content).map_err(|e| format!("Failed to write mod.rs: {e}"))?;
287
288 Ok(())
289}