Skip to main content

ferro_cli/commands/
make_notification.rs

1//! make:notification command - Generate a new notification
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 "Notification" suffix if not already present
14    let struct_name = if struct_name.ends_with("Notification") {
15        struct_name
16    } else {
17        format!("{struct_name}Notification")
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 notification name",
27            style("Error:").red().bold(),
28            name
29        );
30        std::process::exit(1);
31    }
32
33    let notifications_dir = Path::new("src/notifications");
34    let notification_file = notifications_dir.join(format!("{file_name}.rs"));
35    let mod_file = notifications_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 notifications directory if it doesn't exist
51    if !notifications_dir.exists() {
52        if let Err(e) = fs::create_dir_all(notifications_dir) {
53            eprintln!(
54                "{} Failed to create notifications directory: {}",
55                style("Error:").red().bold(),
56                e
57            );
58            std::process::exit(1);
59        }
60        println!("{} Created src/notifications/", style("✓").green());
61
62        // Create mod.rs
63        let mod_content = templates::notifications_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/notifications/mod.rs", style("✓").green());
73    }
74
75    // Check if notification file already exists
76    if notification_file.exists() {
77        eprintln!(
78            "{} Notification '{}' already exists at {}",
79            style("Info:").yellow().bold(),
80            struct_name,
81            notification_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/notifications/mod.rs",
94                style("Info:").yellow().bold(),
95                file_name
96            );
97            std::process::exit(0);
98        }
99    }
100
101    // Generate notification file content
102    let notification_content = templates::notification_template(&file_name, &struct_name);
103
104    // Write notification file
105    if let Err(e) = fs::write(&notification_file, notification_content) {
106        eprintln!(
107            "{} Failed to write notification file: {}",
108            style("Error:").red().bold(),
109            e
110        );
111        std::process::exit(1);
112    }
113    println!(
114        "{} Created {}",
115        style("✓").green(),
116        notification_file.display()
117    );
118
119    // Update mod.rs
120    if let Err(e) = update_mod_file(&mod_file, &file_name, &struct_name) {
121        eprintln!(
122            "{} Failed to update mod.rs: {}",
123            style("Error:").red().bold(),
124            e
125        );
126        std::process::exit(1);
127    }
128    println!("{} Updated src/notifications/mod.rs", style("✓").green());
129
130    println!();
131    println!(
132        "Notification {} created successfully!",
133        style(&struct_name).cyan().bold()
134    );
135    println!();
136    println!("Next steps:");
137    println!(
138        "  {} Add notification data fields in {}",
139        style("1.").dim(),
140        notification_file.display()
141    );
142    println!();
143    println!(
144        "  {} Add the notifications module to src/lib.rs or src/main.rs:",
145        style("2.").dim()
146    );
147    println!("     {}", style("mod notifications;").cyan());
148    println!();
149    println!(
150        "  {} Send the notification in your code:",
151        style("3.").dim()
152    );
153    println!(
154        "     {}",
155        style(format!(
156            "use crate::notifications::{file_name}::{struct_name};"
157        ))
158        .cyan()
159    );
160    println!(
161        "     {}",
162        style(format!(
163            "user.notify({struct_name} {{ /* fields */ }}).await?;"
164        ))
165        .cyan()
166    );
167    println!();
168}
169
170fn is_valid_identifier(name: &str) -> bool {
171    if name.is_empty() {
172        return false;
173    }
174
175    let mut chars = name.chars();
176
177    // First character must be letter or underscore
178    match chars.next() {
179        Some(c) if c.is_alphabetic() || c == '_' => {}
180        _ => return false,
181    }
182
183    // Rest must be alphanumeric or underscore
184    chars.all(|c| c.is_alphanumeric() || c == '_')
185}
186
187fn to_snake_case(s: &str) -> String {
188    let mut result = String::new();
189    for (i, c) in s.chars().enumerate() {
190        if c.is_uppercase() {
191            if i > 0 {
192                result.push('_');
193            }
194            result.push(c.to_lowercase().next().unwrap());
195        } else {
196            result.push(c);
197        }
198    }
199    result
200}
201
202fn to_pascal_case(s: &str) -> String {
203    let mut result = String::new();
204    let mut capitalize_next = true;
205
206    for c in s.chars() {
207        if c == '_' || c == '-' || c == ' ' {
208            capitalize_next = true;
209        } else if capitalize_next {
210            result.push(c.to_uppercase().next().unwrap());
211            capitalize_next = false;
212        } else {
213            result.push(c);
214        }
215    }
216    result
217}
218
219fn update_mod_file(mod_file: &Path, file_name: &str, struct_name: &str) -> Result<(), String> {
220    let content =
221        fs::read_to_string(mod_file).map_err(|e| format!("Failed to read mod.rs: {e}"))?;
222
223    let pub_mod_decl = format!("pub mod {file_name};");
224    let pub_use_decl = format!("pub use {file_name}::{struct_name};");
225
226    // Find position to insert declarations
227    let lines: Vec<&str> = content.lines().collect();
228
229    // Find the last pub mod declaration line
230    let mut last_pub_mod_idx = None;
231    let mut last_pub_use_idx = None;
232
233    for (i, line) in lines.iter().enumerate() {
234        if line.trim().starts_with("pub mod ") {
235            last_pub_mod_idx = Some(i);
236        }
237        if line.trim().starts_with("pub use ") {
238            last_pub_use_idx = Some(i);
239        }
240    }
241
242    // Build new content
243    let mut new_lines: Vec<String> = Vec::new();
244
245    // If we found existing pub mod declarations, insert after them
246    if let Some(idx) = last_pub_mod_idx {
247        for (i, line) in lines.iter().enumerate() {
248            new_lines.push(line.to_string());
249            if i == idx {
250                new_lines.push(pub_mod_decl.clone());
251            }
252        }
253    } else {
254        // No existing pub mod declarations, add at the end (before empty lines)
255        let mut content_end = lines.len();
256        while content_end > 0 && lines[content_end - 1].trim().is_empty() {
257            content_end -= 1;
258        }
259
260        for (i, line) in lines.iter().enumerate() {
261            new_lines.push(line.to_string());
262            if i == content_end.saturating_sub(1) || (content_end == 0 && i == 0) {
263                new_lines.push(pub_mod_decl.clone());
264            }
265        }
266
267        // If file was empty
268        if lines.is_empty() {
269            new_lines.push(pub_mod_decl.clone());
270        }
271    }
272
273    // Now add pub use declaration if there are existing pub use declarations
274    if last_pub_use_idx.is_some() {
275        // Find the new position of the last pub use after our modification
276        let mut insert_idx = None;
277        for (i, line) in new_lines.iter().enumerate() {
278            if line.trim().starts_with("pub use ") {
279                insert_idx = Some(i);
280            }
281        }
282        if let Some(idx) = insert_idx {
283            new_lines.insert(idx + 1, pub_use_decl);
284        }
285    }
286
287    let new_content = new_lines.join("\n");
288
289    // Ensure file ends with newline
290    let new_content = if new_content.ends_with('\n') {
291        new_content
292    } else {
293        format!("{new_content}\n")
294    };
295
296    fs::write(mod_file, new_content).map_err(|e| format!("Failed to write mod.rs: {e}"))?;
297
298    Ok(())
299}