Skip to main content

ferro_cli/commands/
make_resource.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    // Ensure name ends with "Resource"
9    let name = if name.ends_with("Resource") {
10        name
11    } else {
12        format!("{name}Resource")
13    };
14
15    // Convert to snake_case for file name
16    let file_name = to_snake_case(&name);
17
18    // Validate the resulting name is a valid Rust identifier
19    if !is_valid_identifier(&file_name) {
20        eprintln!(
21            "{} '{}' is not a valid resource name",
22            style("Error:").red().bold(),
23            name
24        );
25        std::process::exit(1);
26    }
27
28    let resources_dir = Path::new("src/resources");
29    let resource_file = resources_dir.join(format!("{file_name}.rs"));
30
31    // Check if resources directory exists; create if not
32    if !resources_dir.exists() {
33        if let Err(e) = fs::create_dir_all(resources_dir) {
34            eprintln!(
35                "{} Failed to create resources directory: {}",
36                style("Error:").red().bold(),
37                e
38            );
39            std::process::exit(1);
40        }
41        println!("{} Created src/resources/", style("✓").green());
42    }
43
44    // Check if resource file already exists
45    if resource_file.exists() {
46        eprintln!(
47            "{} Resource '{}' already exists at {}",
48            style("Info:").yellow().bold(),
49            file_name,
50            resource_file.display()
51        );
52        std::process::exit(0);
53    }
54
55    // Generate resource file content
56    let content = templates::resource_template(&name, model.as_deref());
57
58    // Write resource file
59    if let Err(e) = fs::write(&resource_file, content) {
60        eprintln!(
61            "{} Failed to write resource file: {}",
62            style("Error:").red().bold(),
63            e
64        );
65        std::process::exit(1);
66    }
67    println!("{} Created {}", style("✓").green(), resource_file.display());
68
69    println!();
70    println!(
71        "Resource {} created successfully!",
72        style(&name).cyan().bold()
73    );
74    println!();
75    println!(
76        "  {} Add `pub mod {};` to src/resources/mod.rs",
77        style("→").dim(),
78        file_name
79    );
80    println!();
81}
82
83fn is_valid_identifier(name: &str) -> bool {
84    if name.is_empty() {
85        return false;
86    }
87
88    let mut chars = name.chars();
89
90    // First character must be letter or underscore
91    match chars.next() {
92        Some(c) if c.is_alphabetic() || c == '_' => {}
93        _ => return false,
94    }
95
96    // Rest must be alphanumeric or underscore
97    chars.all(|c| c.is_alphanumeric() || c == '_')
98}
99
100fn to_snake_case(s: &str) -> String {
101    let mut result = String::new();
102    for (i, c) in s.chars().enumerate() {
103        if c.is_uppercase() {
104            if i > 0 {
105                result.push('_');
106            }
107            result.push(c.to_lowercase().next().unwrap());
108        } else {
109            result.push(c);
110        }
111    }
112    result
113}