ferro_cli/commands/
make_notification.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("Notification") {
15 struct_name
16 } else {
17 format!("{struct_name}Notification")
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 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 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 !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 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 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 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 let notification_content = templates::notification_template(&file_name, &struct_name);
103
104 if let Err(e) = fs::write(¬ification_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 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 match chars.next() {
179 Some(c) if c.is_alphabetic() || c == '_' => {}
180 _ => return false,
181 }
182
183 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 let lines: Vec<&str> = content.lines().collect();
228
229 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 let mut new_lines: Vec<String> = Vec::new();
244
245 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 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 lines.is_empty() {
269 new_lines.push(pub_mod_decl.clone());
270 }
271 }
272
273 if last_pub_use_idx.is_some() {
275 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 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}