ferro_cli/commands/
make_policy.rs1use console::style;
2use std::fs;
3use std::path::Path;
4
5use crate::templates;
6
7pub fn run(name: String, model: Option<String>) {
8 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 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 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 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 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 let policy_content = templates::policy_template(&file_name, &struct_name, &model_name);
65
66 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 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 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 match chars.next() {
145 Some(c) if c.is_alphabetic() || c == '_' => {}
146 _ => return false,
147 }
148
149 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 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 let mut lines: Vec<&str> = content.lines().collect();
197
198 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 let mod_insert_idx = match last_mod_idx {
208 Some(idx) => idx + 1,
209 None => {
210 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 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 match last_pub_use_idx {
235 Some(idx) => {
236 lines.insert(idx + 1, &pub_use_decl);
237 }
238 None => {
239 let mut insert_idx = mod_insert_idx + 1;
241 while insert_idx < lines.len() && lines[insert_idx].trim().starts_with("mod ") {
243 insert_idx += 1;
244 }
245 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}