Skip to main content

ferro_cli/commands/
make_action.rs

1use console::style;
2use std::fs;
3use std::path::Path;
4
5use crate::templates;
6
7pub fn run(name: String) {
8    // Convert to PascalCase for struct name
9    let struct_name = to_pascal_case(&name);
10
11    // Append "Action" suffix if not already present
12    let struct_name = if struct_name.ends_with("Action") {
13        struct_name
14    } else {
15        format!("{struct_name}Action")
16    };
17
18    // Convert to snake_case for file name
19    let file_name = to_snake_case(&struct_name);
20
21    // Validate the resulting name is a valid Rust identifier
22    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    // Check if actions directory exists
36    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    // Check if action file already exists
53    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    // Check if module is already declared in mod.rs
64    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    // Generate action file content
79    let action_content = templates::action_template(&file_name, &struct_name);
80
81    // Write action file
82    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    // Update mod.rs
93    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        // Create mod.rs if it doesn't exist
105        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    // First character must be letter or underscore
141    match chars.next() {
142        Some(c) if c.is_alphabetic() || c == '_' => {}
143        _ => return false,
144    }
145
146    // Rest must be alphanumeric or underscore
147    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    // Find position to insert pub mod declaration (after other pub mod declarations)
189    let mut lines: Vec<&str> = content.lines().collect();
190
191    // Find the last pub mod declaration line
192    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    // Insert pub mod declaration
200    let insert_idx = match last_pub_mod_idx {
201        Some(idx) => idx + 1,
202        None => {
203            // If no pub mod declarations, insert at the beginning (after any doc comments)
204            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}