Skip to main content

ferro_cli/commands/
make_json_view.rs

1//! `ferro make:json-view` command implementation.
2//!
3//! Generates a JSON-UI view file, optionally using the Anthropic API
4//! for AI-powered generation from a natural language description.
5
6use console::style;
7use std::fs;
8use std::path::Path;
9
10use crate::ai;
11use crate::templates;
12
13pub fn run(name: String, description: Option<String>, no_ai: bool, layout: Option<String>) {
14    let file_name = to_snake_case(&name);
15
16    if !is_valid_identifier(&file_name) {
17        eprintln!(
18            "{} '{}' is not a valid view name",
19            style("Error:").red().bold(),
20            name
21        );
22        std::process::exit(1);
23    }
24
25    let views_dir = Path::new("src/views");
26    let view_file = views_dir.join(format!("{file_name}.rs"));
27    let mod_file = views_dir.join("mod.rs");
28
29    // Create views directory if it doesn't exist
30    if !views_dir.exists() {
31        if let Err(e) = fs::create_dir_all(views_dir) {
32            eprintln!(
33                "{} Failed to create src/views directory: {}",
34                style("Error:").red().bold(),
35                e
36            );
37            std::process::exit(1);
38        }
39        println!("{} Created src/views/", style("✓").green());
40    }
41
42    // Check if view file already exists
43    if view_file.exists() {
44        eprintln!(
45            "{} View '{}' already exists at {}",
46            style("Info:").yellow().bold(),
47            file_name,
48            view_file.display()
49        );
50        std::process::exit(0);
51    }
52
53    // Check if module is already declared in mod.rs
54    if mod_file.exists() {
55        let mod_content = fs::read_to_string(&mod_file).unwrap_or_default();
56        let mod_decl = format!("mod {file_name};");
57        let pub_mod_decl = format!("pub mod {file_name};");
58        if mod_content.contains(&mod_decl) || mod_content.contains(&pub_mod_decl) {
59            eprintln!(
60                "{} Module '{}' is already declared in src/views/mod.rs",
61                style("Info:").yellow().bold(),
62                file_name
63            );
64            std::process::exit(0);
65        }
66    }
67
68    let layout_name = layout.as_deref().unwrap_or("app");
69    let title = to_title_case(&file_name);
70
71    // Determine content: AI or static template
72    let content = if no_ai {
73        templates::json_view_template(&file_name, &title, layout_name)
74    } else {
75        match std::env::var("ANTHROPIC_API_KEY") {
76            Ok(_) => {
77                let desc = description.as_deref().unwrap_or(&title);
78                println!("{} Generating view with AI...", style("⏳").cyan());
79
80                let (system, user_prompt) = ai::build_view_context(&file_name, desc);
81
82                match ai::call_anthropic(&system, &user_prompt) {
83                    Ok(code) => code,
84                    Err(e) => {
85                        eprintln!(
86                            "{} AI generation failed: {}",
87                            style("Warning:").yellow().bold(),
88                            e
89                        );
90                        eprintln!("{}", style("Falling back to static template.").dim());
91                        templates::json_view_template(&file_name, &title, layout_name)
92                    }
93                }
94            }
95            Err(_) => {
96                if description.is_some() {
97                    eprintln!(
98                        "{} No ANTHROPIC_API_KEY found, using static template. \
99                         Set the key or use --no-ai to suppress this message.",
100                        style("Info:").yellow().bold(),
101                    );
102                }
103                templates::json_view_template(&file_name, &title, layout_name)
104            }
105        }
106    };
107
108    // Write view file
109    if let Err(e) = fs::write(&view_file, content) {
110        eprintln!(
111            "{} Failed to write view file: {}",
112            style("Error:").red().bold(),
113            e
114        );
115        std::process::exit(1);
116    }
117    println!("{} Created {}", style("✓").green(), view_file.display());
118
119    // Update mod.rs
120    if mod_file.exists() {
121        if let Err(e) = update_mod_file(&mod_file, &file_name) {
122            eprintln!(
123                "{} Failed to update mod.rs: {}",
124                style("Error:").red().bold(),
125                e
126            );
127            std::process::exit(1);
128        }
129        println!("{} Updated src/views/mod.rs", style("✓").green());
130    } else {
131        let mod_content = format!("pub mod {file_name};\n");
132        if let Err(e) = fs::write(&mod_file, mod_content) {
133            eprintln!(
134                "{} Failed to create mod.rs: {}",
135                style("Error:").red().bold(),
136                e
137            );
138            std::process::exit(1);
139        }
140        println!("{} Created src/views/mod.rs", style("✓").green());
141    }
142
143    println!();
144    println!(
145        "View {} created successfully!",
146        style(&file_name).cyan().bold()
147    );
148    println!();
149    println!("Usage:");
150    println!("  {} Use the view in a handler:", style("1.").dim());
151    println!("     use crate::views::{file_name};");
152    println!();
153    println!("     pub async fn index() -> Response {{");
154    println!("         JsonUi::render(&{file_name}::view(), &json!({{}}))");
155    println!("     }}");
156    println!();
157}
158
159fn is_valid_identifier(name: &str) -> bool {
160    if name.is_empty() {
161        return false;
162    }
163
164    let mut chars = name.chars();
165
166    match chars.next() {
167        Some(c) if c.is_alphabetic() || c == '_' => {}
168        _ => return false,
169    }
170
171    chars.all(|c| c.is_alphanumeric() || c == '_')
172}
173
174fn to_snake_case(s: &str) -> String {
175    let mut result = String::new();
176    for (i, c) in s.chars().enumerate() {
177        if c.is_uppercase() {
178            if i > 0 {
179                result.push('_');
180            }
181            result.push(c.to_lowercase().next().unwrap());
182        } else {
183            result.push(c);
184        }
185    }
186    result
187}
188
189fn to_title_case(s: &str) -> String {
190    s.split('_')
191        .map(|word| {
192            let mut chars = word.chars();
193            match chars.next() {
194                None => String::new(),
195                Some(first) => {
196                    let mut result = first.to_uppercase().to_string();
197                    result.extend(chars);
198                    result
199                }
200            }
201        })
202        .collect::<Vec<_>>()
203        .join(" ")
204}
205
206fn update_mod_file(mod_file: &Path, file_name: &str) -> Result<(), String> {
207    let content =
208        fs::read_to_string(mod_file).map_err(|e| format!("Failed to read mod.rs: {e}"))?;
209
210    let pub_mod_decl = format!("pub mod {file_name};");
211
212    let mut lines: Vec<&str> = content.lines().collect();
213
214    // Find the last pub mod declaration line
215    let mut last_pub_mod_idx = None;
216    for (i, line) in lines.iter().enumerate() {
217        if line.trim().starts_with("pub mod ") {
218            last_pub_mod_idx = Some(i);
219        }
220    }
221
222    let insert_idx = match last_pub_mod_idx {
223        Some(idx) => idx + 1,
224        None => {
225            let mut insert_idx = 0;
226            for (i, line) in lines.iter().enumerate() {
227                if line.starts_with("//!") || line.is_empty() {
228                    insert_idx = i + 1;
229                } else {
230                    break;
231                }
232            }
233            insert_idx
234        }
235    };
236    lines.insert(insert_idx, &pub_mod_decl);
237
238    let new_content = lines.join("\n");
239    fs::write(mod_file, new_content).map_err(|e| format!("Failed to write mod.rs: {e}"))?;
240
241    Ok(())
242}