Skip to main content

ferro_cli/commands/
make_policy.rs

1use console::style;
2use std::fs;
3use std::path::Path;
4
5use crate::templates;
6
7pub fn run(name: String, model: Option<String>) {
8    // Validate name is a valid Rust identifier
9    if !is_valid_identifier(&name) {
10        eprintln!(
11            "{} '{}' is not a valid Rust identifier",
12            style("Error:").red().bold(),
13            name
14        );
15        std::process::exit(1);
16    }
17
18    // Convert name to struct name and file name
19    // e.g., "Post" -> "PostPolicy", "post_policy"
20    // e.g., "PostPolicy" -> "PostPolicy", "post_policy"
21    let struct_name = if name.ends_with("Policy") {
22        name.clone()
23    } else {
24        format!("{name}Policy")
25    };
26    let file_name = to_snake_case(name.trim_end_matches("Policy"));
27
28    // Derive model name from policy name if not provided
29    // e.g., "PostPolicy" -> "Post"
30    let model_name = model.unwrap_or_else(|| {
31        let base = name.trim_end_matches("Policy");
32        to_pascal_case(base)
33    });
34
35    let policies_dir = Path::new("src/policies");
36    let policy_file = policies_dir.join(format!("{file_name}_policy.rs"));
37    let mod_file = policies_dir.join("mod.rs");
38
39    // Check if policies directory exists
40    if !policies_dir.exists() {
41        if let Err(e) = fs::create_dir_all(policies_dir) {
42            eprintln!(
43                "{} Failed to create policies directory: {}",
44                style("Error:").red().bold(),
45                e
46            );
47            std::process::exit(1);
48        }
49        println!("{} Created src/policies directory", style("✓").green());
50    }
51
52    // Check if policy file already exists
53    if policy_file.exists() {
54        eprintln!(
55            "{} Policy '{}' already exists at {}",
56            style("Error:").red().bold(),
57            struct_name,
58            policy_file.display()
59        );
60        std::process::exit(1);
61    }
62
63    // Generate policy file content
64    let policy_content = templates::policy_template(&file_name, &struct_name, &model_name);
65
66    // Write policy file
67    if let Err(e) = fs::write(&policy_file, policy_content) {
68        eprintln!(
69            "{} Failed to write policy file: {}",
70            style("Error:").red().bold(),
71            e
72        );
73        std::process::exit(1);
74    }
75    println!("{} Created {}", style("✓").green(), policy_file.display());
76
77    // Update mod.rs
78    let module_name = format!("{file_name}_policy");
79    if mod_file.exists() {
80        if let Err(e) = update_mod_file(&mod_file, &module_name, &struct_name) {
81            eprintln!(
82                "{} Failed to update mod.rs: {}",
83                style("Error:").red().bold(),
84                e
85            );
86            std::process::exit(1);
87        }
88        println!("{} Updated src/policies/mod.rs", style("✓").green());
89    } else {
90        // Create mod.rs if it doesn't exist
91        let mod_content = format!(
92            "{}mod {};\n\npub use {}::{};\n",
93            templates::policies_mod(),
94            module_name,
95            module_name,
96            struct_name
97        );
98        if let Err(e) = fs::write(&mod_file, mod_content) {
99            eprintln!(
100                "{} Failed to create mod.rs: {}",
101                style("Error:").red().bold(),
102                e
103            );
104            std::process::exit(1);
105        }
106        println!("{} Created src/policies/mod.rs", style("✓").green());
107    }
108
109    println!();
110    println!(
111        "Policy {} created successfully!",
112        style(&struct_name).cyan().bold()
113    );
114    println!();
115    println!("Usage:");
116    println!(
117        "  {} Import your model and user types in the policy file",
118        style("1.").dim()
119    );
120    println!(
121        "  {} Implement the authorization logic in each method",
122        style("2.").dim()
123    );
124    println!();
125    println!("Example:");
126    println!("  use crate::policies::{struct_name};");
127    println!("  use ferro::authorization::Policy;");
128    println!();
129    println!("  let policy = {struct_name};");
130    println!("  if policy.update(&user, &model).allowed() {{");
131    println!("      // Proceed with update");
132    println!("  }}");
133    println!();
134}
135
136fn is_valid_identifier(name: &str) -> bool {
137    if name.is_empty() {
138        return false;
139    }
140
141    let mut chars = name.chars();
142
143    // First character must be letter or underscore
144    match chars.next() {
145        Some(c) if c.is_alphabetic() || c == '_' => {}
146        _ => return false,
147    }
148
149    // Rest must be alphanumeric or underscore
150    chars.all(|c| c.is_alphanumeric() || c == '_')
151}
152
153fn to_snake_case(s: &str) -> String {
154    let mut result = String::new();
155    for (i, c) in s.chars().enumerate() {
156        if c.is_uppercase() {
157            if i > 0 {
158                result.push('_');
159            }
160            result.push(c.to_lowercase().next().unwrap());
161        } else {
162            result.push(c);
163        }
164    }
165    result
166}
167
168fn to_pascal_case(s: &str) -> String {
169    let mut result = String::new();
170    let mut capitalize_next = true;
171
172    for c in s.chars() {
173        if c == '_' || c == '-' || c == ' ' {
174            capitalize_next = true;
175        } else if capitalize_next {
176            result.push(c.to_uppercase().next().unwrap());
177            capitalize_next = false;
178        } else {
179            result.push(c);
180        }
181    }
182    result
183}
184
185fn update_mod_file(mod_file: &Path, file_name: &str, struct_name: &str) -> Result<(), String> {
186    let content =
187        fs::read_to_string(mod_file).map_err(|e| format!("Failed to read mod.rs: {e}"))?;
188
189    // Check if module already declared
190    let mod_decl = format!("mod {file_name};");
191    if content.contains(&mod_decl) {
192        return Err(format!("Module '{file_name}' already declared in mod.rs"));
193    }
194
195    // Find position to insert mod declaration (after other mod declarations)
196    let mut lines: Vec<&str> = content.lines().collect();
197
198    // Find the last mod declaration line
199    let mut last_mod_idx = None;
200    for (i, line) in lines.iter().enumerate() {
201        if line.trim().starts_with("mod ") {
202            last_mod_idx = Some(i);
203        }
204    }
205
206    // Insert mod declaration
207    let mod_insert_idx = match last_mod_idx {
208        Some(idx) => idx + 1,
209        None => {
210            // If no mod declarations, insert after doc comments
211            let mut insert_idx = 0;
212            for (i, line) in lines.iter().enumerate() {
213                if line.starts_with("//!") || line.is_empty() {
214                    insert_idx = i + 1;
215                } else {
216                    break;
217                }
218            }
219            insert_idx
220        }
221    };
222    lines.insert(mod_insert_idx, &mod_decl);
223
224    // Find position to insert pub use (after other pub use declarations)
225    let pub_use_decl = format!("pub use {file_name}::{struct_name};");
226    let mut last_pub_use_idx = None;
227    for (i, line) in lines.iter().enumerate() {
228        if line.trim().starts_with("pub use ") {
229            last_pub_use_idx = Some(i);
230        }
231    }
232
233    // Insert pub use declaration
234    match last_pub_use_idx {
235        Some(idx) => {
236            lines.insert(idx + 1, &pub_use_decl);
237        }
238        None => {
239            // If no pub use declarations, add after mod declarations with empty line
240            let mut insert_idx = mod_insert_idx + 1;
241            // Skip past remaining mod declarations
242            while insert_idx < lines.len() && lines[insert_idx].trim().starts_with("mod ") {
243                insert_idx += 1;
244            }
245            // Add empty line if needed
246            if insert_idx < lines.len() && !lines[insert_idx].is_empty() {
247                lines.insert(insert_idx, "");
248                insert_idx += 1;
249            }
250            lines.insert(insert_idx, &pub_use_decl);
251        }
252    }
253
254    let new_content = lines.join("\n");
255    fs::write(mod_file, new_content).map_err(|e| format!("Failed to write mod.rs: {e}"))?;
256
257    Ok(())
258}