ferro_cli/commands/
make_json_view.rs1use 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 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 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 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 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 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 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 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}