Skip to main content

ferro_cli/commands/
make_event.rs

1//! make:event command - Generate a new domain event
2
3use console::style;
4use std::fs;
5use std::path::Path;
6
7use crate::templates;
8
9pub fn run(name: String) {
10    // Convert to PascalCase for struct name
11    let struct_name = to_pascal_case(&name);
12
13    // Append "Event" suffix if not already present
14    let struct_name = if struct_name.ends_with("Event") {
15        struct_name
16    } else {
17        format!("{struct_name}Event")
18    };
19
20    // Convert to snake_case for file name
21    let file_name = to_snake_case(&struct_name);
22
23    // Validate the resulting name is a valid Rust identifier
24    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    // Ensure we're in a Ferro project (check for src directory)
38    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    // Create events directory if it doesn't exist
51    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        // Create mod.rs
63        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    // Check if event file already exists
76    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    // Check if module is already declared in mod.rs
87    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    // Generate event file content
102    let event_content = templates::event_template(&file_name, &struct_name);
103
104    // Write event file
105    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    // Update mod.rs
116    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    // First character must be letter or underscore
168    match chars.next() {
169        Some(c) if c.is_alphabetic() || c == '_' => {}
170        _ => return false,
171    }
172
173    // Rest must be alphanumeric or underscore
174    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    // Find position to insert declarations
217    let lines: Vec<&str> = content.lines().collect();
218
219    // Find the last pub mod declaration line
220    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    // Build new content
233    let mut new_lines: Vec<String> = Vec::new();
234
235    // If we found existing pub mod declarations, insert after them
236    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        // No existing pub mod declarations, add at the end (before empty lines)
245        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 file was empty
258        if lines.is_empty() {
259            new_lines.push(pub_mod_decl.clone());
260        }
261    }
262
263    // Now add pub use declaration if there are existing pub use declarations
264    if last_pub_use_idx.is_some() {
265        // Find the new position of the last pub use after our modification
266        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    // Ensure file ends with newline
280    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}