Skip to main content

ferro_cli/commands/
make_middleware.rs

1use console::style;
2use std::fs;
3use std::path::Path;
4
5use crate::templates;
6
7pub fn run(name: 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., "Auth" -> "AuthMiddleware", "auth"
20    // e.g., "RateLimit" -> "RateLimitMiddleware", "rate_limit"
21    let struct_name = if name.ends_with("Middleware") {
22        name.clone()
23    } else {
24        format!("{name}Middleware")
25    };
26    let file_name = to_snake_case(name.trim_end_matches("Middleware"));
27
28    let middleware_dir = Path::new("src/middleware");
29    let middleware_file = middleware_dir.join(format!("{file_name}.rs"));
30    let mod_file = middleware_dir.join("mod.rs");
31
32    // Check if middleware directory exists
33    if !middleware_dir.exists() {
34        if let Err(e) = fs::create_dir_all(middleware_dir) {
35            eprintln!(
36                "{} Failed to create middleware directory: {}",
37                style("Error:").red().bold(),
38                e
39            );
40            std::process::exit(1);
41        }
42        println!("{} Created src/middleware directory", style("✓").green());
43    }
44
45    // Check if middleware file already exists
46    if middleware_file.exists() {
47        eprintln!(
48            "{} Middleware '{}' already exists at {}",
49            style("Error:").red().bold(),
50            struct_name,
51            middleware_file.display()
52        );
53        std::process::exit(1);
54    }
55
56    // Generate middleware file content
57    let middleware_content = templates::middleware_template(&name, &struct_name);
58
59    // Write middleware file
60    if let Err(e) = fs::write(&middleware_file, middleware_content) {
61        eprintln!(
62            "{} Failed to write middleware file: {}",
63            style("Error:").red().bold(),
64            e
65        );
66        std::process::exit(1);
67    }
68    println!(
69        "{} Created {}",
70        style("✓").green(),
71        middleware_file.display()
72    );
73
74    // Update mod.rs
75    if mod_file.exists() {
76        if let Err(e) = update_mod_file(&mod_file, &file_name, &struct_name) {
77            eprintln!(
78                "{} Failed to update mod.rs: {}",
79                style("Error:").red().bold(),
80                e
81            );
82            std::process::exit(1);
83        }
84        println!("{} Updated src/middleware/mod.rs", style("✓").green());
85    } else {
86        // Create mod.rs if it doesn't exist
87        let mod_content = format!(
88            "//! Application middleware\n\nmod {file_name};\n\npub use {file_name}::{struct_name};\n"
89        );
90        if let Err(e) = fs::write(&mod_file, mod_content) {
91            eprintln!(
92                "{} Failed to create mod.rs: {}",
93                style("Error:").red().bold(),
94                e
95            );
96            std::process::exit(1);
97        }
98        println!("{} Created src/middleware/mod.rs", style("✓").green());
99    }
100
101    println!();
102    println!(
103        "Middleware {} created successfully!",
104        style(&struct_name).cyan().bold()
105    );
106    println!();
107    println!("Usage:");
108    println!("  {} Import and use in routes:", style("1.").dim());
109    println!("     use crate::middleware::{struct_name};");
110    println!("     .get(\"/path\", handler).middleware({struct_name})");
111    println!();
112    println!("  {} Or apply globally in main.rs:", style("2.").dim());
113    println!("     .middleware(middleware::{struct_name})");
114    println!();
115}
116
117fn is_valid_identifier(name: &str) -> bool {
118    if name.is_empty() {
119        return false;
120    }
121
122    let mut chars = name.chars();
123
124    // First character must be letter or underscore
125    match chars.next() {
126        Some(c) if c.is_alphabetic() || c == '_' => {}
127        _ => return false,
128    }
129
130    // Rest must be alphanumeric or underscore
131    chars.all(|c| c.is_alphanumeric() || c == '_')
132}
133
134fn to_snake_case(s: &str) -> String {
135    let mut result = String::new();
136    for (i, c) in s.chars().enumerate() {
137        if c.is_uppercase() {
138            if i > 0 {
139                result.push('_');
140            }
141            result.push(c.to_lowercase().next().unwrap());
142        } else {
143            result.push(c);
144        }
145    }
146    result
147}
148
149fn update_mod_file(mod_file: &Path, file_name: &str, struct_name: &str) -> Result<(), String> {
150    let content =
151        fs::read_to_string(mod_file).map_err(|e| format!("Failed to read mod.rs: {e}"))?;
152
153    // Check if module already declared
154    let mod_decl = format!("mod {file_name};");
155    if content.contains(&mod_decl) {
156        return Err(format!("Module '{file_name}' already declared in mod.rs"));
157    }
158
159    // Find position to insert mod declaration (after other mod declarations)
160    let mut lines: Vec<&str> = content.lines().collect();
161
162    // Find the last mod declaration line
163    let mut last_mod_idx = None;
164    for (i, line) in lines.iter().enumerate() {
165        if line.trim().starts_with("mod ") {
166            last_mod_idx = Some(i);
167        }
168    }
169
170    // Insert mod declaration
171    let mod_insert_idx = match last_mod_idx {
172        Some(idx) => idx + 1,
173        None => {
174            // If no mod declarations, insert after doc comments
175            let mut insert_idx = 0;
176            for (i, line) in lines.iter().enumerate() {
177                if line.starts_with("//!") || line.is_empty() {
178                    insert_idx = i + 1;
179                } else {
180                    break;
181                }
182            }
183            insert_idx
184        }
185    };
186    lines.insert(mod_insert_idx, &mod_decl);
187
188    // Find position to insert pub use (after other pub use declarations)
189    let pub_use_decl = format!("pub use {file_name}::{struct_name};");
190    let mut last_pub_use_idx = None;
191    for (i, line) in lines.iter().enumerate() {
192        if line.trim().starts_with("pub use ") {
193            last_pub_use_idx = Some(i);
194        }
195    }
196
197    // Insert pub use declaration
198    match last_pub_use_idx {
199        Some(idx) => {
200            lines.insert(idx + 1, &pub_use_decl);
201        }
202        None => {
203            // If no pub use declarations, add after mod declarations with empty line
204            let mut insert_idx = mod_insert_idx + 1;
205            // Skip past remaining mod declarations
206            while insert_idx < lines.len() && lines[insert_idx].trim().starts_with("mod ") {
207                insert_idx += 1;
208            }
209            // Add empty line if needed
210            if insert_idx < lines.len() && !lines[insert_idx].is_empty() {
211                lines.insert(insert_idx, "");
212                insert_idx += 1;
213            }
214            lines.insert(insert_idx, &pub_use_decl);
215        }
216    }
217
218    let new_content = lines.join("\n");
219    fs::write(mod_file, new_content).map_err(|e| format!("Failed to write mod.rs: {e}"))?;
220
221    Ok(())
222}