1use crate::config::loader::load_config;
2use crate::config::validator::validate_config;
3use crate::error::{FileSystemError, Result};
4use crate::formatter::FormatterManager;
5use crate::generator::swagger_parser::filter_common_schemas;
6use crate::generator::writer::write_api_client_with_options;
7use colored::*;
8use std::path::{Path, PathBuf};
9
10pub async fn run() -> Result<()> {
11 println!("{}", "🔄 Updating generated code...".bright_cyan());
12 println!();
13
14 let config = load_config()?;
16 validate_config(&config)?;
17
18 use crate::error::{FileSystemError, GenerationError};
19 use crate::specs::manager::list_specs;
20
21 let specs = list_specs(&config);
23 if specs.is_empty() {
24 return Err(GenerationError::SpecPathRequired.into());
25 }
26
27 type SpecSummary = (String, usize, Vec<(String, usize)>);
29 let mut all_specs_summary: Vec<SpecSummary> = Vec::new();
30 let mut all_generated_files = Vec::new();
31 let mut printed_urls: std::collections::HashSet<String> = std::collections::HashSet::new();
33
34 for spec in &specs {
35 println!();
36 println!(
37 "{}",
38 format!("🔄 Updating spec: {}", spec.name).bright_cyan()
39 );
40 println!();
41
42 let spec_path = &spec.path;
43
44 if !spec_path.starts_with("http://")
46 && !spec_path.starts_with("https://")
47 && !std::path::Path::new(spec_path).exists()
48 {
49 println!(
50 "{}",
51 format!(
52 "⚠️ Skipping spec '{}': spec file no longer exists at {}",
53 spec.name, spec_path
54 )
55 .yellow()
56 );
57 continue;
58 }
59
60 let schemas_config = &spec.schemas;
62 let apis_config = &spec.apis;
63 let modules_config = &spec.modules;
64
65 use crate::generator::writer::{ensure_directory, write_http_client_template};
67 let apis_dir = PathBuf::from(&apis_config.output);
68 ensure_directory(&apis_dir)?;
69 let http_file = apis_dir.join("http.ts");
70 if !http_file.exists() {
71 write_http_client_template(&http_file)?;
72 }
73
74 if !printed_urls.contains(spec_path) {
76 println!(
77 "{}",
78 format!("📥 Fetching spec from: {}", spec_path).bright_blue()
79 );
80 printed_urls.insert(spec_path.clone());
81 }
82
83 let use_cache = config.generation.enable_cache;
85 let parsed = crate::generator::swagger_parser::fetch_and_parse_spec_with_cache_and_name(
86 spec_path,
87 use_cache,
88 Some(&spec.name),
89 )
90 .await?;
91
92 let selected_modules = if modules_config.selected.is_empty() {
94 println!(
96 "{}",
97 format!(
98 "No modules selected for spec '{}'. Please select modules to update:",
99 spec.name
100 )
101 .bright_yellow()
102 );
103 println!();
104
105 let available_modules: Vec<String> = parsed
107 .modules
108 .iter()
109 .filter(|m| !modules_config.ignore.contains(m))
110 .cloned()
111 .collect();
112
113 if available_modules.is_empty() {
114 println!(
115 "{}",
116 format!("⚠️ Skipping spec '{}': No modules available", spec.name).yellow()
117 );
118 continue;
119 }
120
121 use crate::generator::module_selector::select_modules;
123 let selected = select_modules(&available_modules, &modules_config.ignore)?;
124
125 use crate::config::loader::load_config;
127 let mut config = load_config()?;
128 if let Some(spec_entry) = config.specs.iter_mut().find(|s| s.name == spec.name) {
129 spec_entry.modules.selected = selected.clone();
130 }
131 use crate::config::loader::save_config;
132 save_config(&config)?;
133
134 selected
135 } else {
136 modules_config.selected.clone()
137 };
138 println!(
139 "{}",
140 format!("✅ Parsed spec with {} modules", parsed.modules.len()).green()
141 );
142 println!();
143 println!(
144 "{}",
145 format!(
146 "📦 Updating {} module(s): {}",
147 selected_modules.len(),
148 selected_modules.join(", ")
149 )
150 .bright_green()
151 );
152 println!();
153
154 let (filtered_module_schemas, common_schemas) =
156 filter_common_schemas(&parsed.module_schemas, &selected_modules);
157
158 let project_root = std::env::current_dir().ok();
160 let template_engine =
161 crate::templates::engine::TemplateEngine::new(project_root.as_deref())?;
162
163 let schemas_dir = PathBuf::from(&schemas_config.output);
165 let apis_dir = PathBuf::from(&apis_config.output);
166
167 let mut total_files = 0;
168 let mut module_summary: Vec<(String, usize)> = Vec::new();
169
170 let use_force = config.generation.conflict_strategy == "force";
172 let use_backup = config.generation.enable_backup;
173
174 if !common_schemas.is_empty() {
176 println!("{}", "🔨 Regenerating common schemas...".bright_cyan());
177
178 let mut shared_enum_registry = std::collections::HashMap::new();
180
181 let common_types =
184 crate::generator::ts_typings::generate_typings_with_registry_and_engine_and_spec(
185 &parsed.openapi,
186 &parsed.schemas,
187 &common_schemas,
188 &mut shared_enum_registry,
189 &[], Some(&template_engine),
191 Some(&spec.name),
192 )?;
193
194 let common_zod_schemas =
197 crate::generator::zod_schema::generate_zod_schemas_with_registry_and_engine_and_spec(
198 &parsed.openapi,
199 &parsed.schemas,
200 &common_schemas,
201 &mut shared_enum_registry,
202 &[], Some(&template_engine),
204 Some(&spec.name),
205 )?;
206
207 use crate::generator::writer::write_schemas_with_module_mapping;
209 let common_files = write_schemas_with_module_mapping(
210 &schemas_dir,
211 "common",
212 &common_types,
213 &common_zod_schemas,
214 Some(&spec.name), use_backup,
216 use_force,
217 Some(&filtered_module_schemas),
218 &common_schemas,
219 )?;
220 total_files += common_files.len();
221 module_summary.push(("common".to_string(), common_files.len()));
222 }
223
224 for module in &selected_modules {
225 println!(
226 "{}",
227 format!("🔨 Regenerating code for module: {}", module).bright_cyan()
228 );
229
230 let operations = parsed
232 .operations_by_tag
233 .get(module)
234 .cloned()
235 .unwrap_or_default();
236
237 if operations.is_empty() {
238 println!(
239 "{}",
240 format!("⚠️ No operations found for module: {}", module).yellow()
241 );
242 continue;
243 }
244
245 let module_schema_names = filtered_module_schemas
247 .get(module)
248 .cloned()
249 .unwrap_or_default();
250
251 let mut shared_enum_registry = std::collections::HashMap::new();
253
254 let types = if !module_schema_names.is_empty() {
256 crate::generator::ts_typings::generate_typings_with_registry_and_engine_and_spec(
257 &parsed.openapi,
258 &parsed.schemas,
259 &module_schema_names,
260 &mut shared_enum_registry,
261 &common_schemas,
262 Some(&template_engine),
263 Some(&spec.name),
264 )?
265 } else {
266 Vec::new()
267 };
268
269 let zod_schemas = if !module_schema_names.is_empty() {
271 crate::generator::zod_schema::generate_zod_schemas_with_registry_and_engine_and_spec(
272 &parsed.openapi,
273 &parsed.schemas,
274 &module_schema_names,
275 &mut shared_enum_registry,
276 &common_schemas,
277 Some(&template_engine),
278 Some(&spec.name),
279 )?
280 } else {
281 Vec::new()
282 };
283
284 let api_result =
286 crate::generator::api_client::generate_api_client_with_registry_and_engine_and_spec(
287 &parsed.openapi,
288 &operations,
289 module,
290 &common_schemas,
291 &mut shared_enum_registry,
292 Some(&template_engine),
293 Some(&spec.name),
294 )?;
295
296 let mut all_types = types;
298 all_types.extend(api_result.response_types);
299
300 use crate::generator::writer::write_schemas_with_module_mapping;
302 let schema_files = write_schemas_with_module_mapping(
303 &schemas_dir,
304 module,
305 &all_types,
306 &zod_schemas,
307 Some(&spec.name), use_backup,
309 use_force,
310 Some(&filtered_module_schemas),
311 &common_schemas,
312 )?;
313 total_files += schema_files.len();
314
315 let api_files = write_api_client_with_options(
317 &apis_dir,
318 module,
319 &api_result.functions,
320 Some(&spec.name), use_backup,
322 use_force,
323 )?;
324 total_files += api_files.len();
325
326 let module_file_count = schema_files.len() + api_files.len();
327 module_summary.push((module.clone(), module_file_count));
328 println!(
329 "{}",
330 format!(
331 "✅ Regenerated {} files for module: {}",
332 module_file_count, module
333 )
334 .green()
335 );
336 }
337
338 println!();
339 println!(
340 "{}",
341 format!(
342 "✨ Successfully updated {} files for spec '{}'!",
343 total_files, spec.name
344 )
345 .bright_green()
346 );
347 println!();
348 println!(
349 "{}",
350 format!("Updated files for '{}':", spec.name).bright_cyan()
351 );
352 println!(" 📁 Schemas: {}", schemas_config.output);
353 println!(" 📁 APIs: {}", apis_config.output);
354
355 all_specs_summary.push((spec.name.clone(), total_files, module_summary.clone()));
357
358 let current_dir = std::env::current_dir().map_err(|e| FileSystemError::ReadFileFailed {
360 path: ".".to_string(),
361 source: e,
362 })?;
363
364 let schemas_dir_abs = if schemas_dir.is_absolute() {
365 schemas_dir.clone()
366 } else {
367 current_dir.join(&schemas_dir)
368 };
369 let apis_dir_abs = if apis_dir.is_absolute() {
370 apis_dir.clone()
371 } else {
372 current_dir.join(&apis_dir)
373 };
374
375 if schemas_dir_abs.exists() {
377 collect_ts_files(&schemas_dir_abs, &mut all_generated_files)?;
378 }
379
380 if apis_dir_abs.exists() {
382 collect_ts_files(&apis_dir_abs, &mut all_generated_files)?;
383 }
384 }
385
386 println!();
388 println!("{}", "=".repeat(60).bright_black());
389 println!();
390 let total_all_files: usize = all_specs_summary.iter().map(|(_, count, _)| count).sum();
391 println!(
392 "{}",
393 format!(
394 "✨ Successfully updated {} files across {} spec(s)!",
395 total_all_files,
396 all_specs_summary.len()
397 )
398 .bright_green()
399 );
400 println!();
401 println!("{}", "Summary by spec:".bright_cyan());
402 for (spec_name, file_count, module_summary) in &all_specs_summary {
403 println!(" 📦 {}: {} files", spec_name, file_count);
404 if !module_summary.is_empty() {
405 for (module, count) in module_summary {
406 println!(" • {}: {} files", module, count);
407 }
408 }
409 }
410 println!();
411
412 if !all_generated_files.is_empty() {
414 let output_base = all_generated_files.first().and_then(|first_file| {
417 first_file
418 .parent()
419 .and_then(|p| p.parent())
420 .and_then(|p| p.parent())
421 });
422
423 let formatter = if let Some(base_dir) = output_base {
424 FormatterManager::detect_formatter_from_dir(base_dir)
425 .or_else(FormatterManager::detect_formatter)
426 } else {
427 FormatterManager::detect_formatter()
428 };
429
430 if let Some(formatter) = formatter {
431 println!("{}", "Formatting generated files...".bright_cyan());
432 let original_dir =
433 std::env::current_dir().map_err(|e| FileSystemError::ReadFileFailed {
434 path: ".".to_string(),
435 source: e,
436 })?;
437
438 if let Some(output_base) = output_base {
439 if output_base.as_os_str().is_empty() {
441 FormatterManager::format_files(&all_generated_files, formatter)?;
443 } else {
444 std::env::set_current_dir(output_base).map_err(|e| {
445 FileSystemError::ReadFileFailed {
446 path: output_base.display().to_string(),
447 source: e,
448 }
449 })?;
450
451 let relative_files: Vec<PathBuf> = all_generated_files
453 .iter()
454 .filter_map(|p| {
455 p.strip_prefix(output_base)
456 .ok()
457 .map(|p| p.to_path_buf())
458 .filter(|p| !p.as_os_str().is_empty())
459 })
460 .collect();
461
462 if !relative_files.is_empty() {
463 let result = FormatterManager::format_files(&relative_files, formatter);
464
465 std::env::set_current_dir(&original_dir).map_err(|e| {
467 FileSystemError::ReadFileFailed {
468 path: original_dir.display().to_string(),
469 source: e,
470 }
471 })?;
472
473 result?;
474
475 use crate::generator::writer::batch_update_file_metadata_from_disk;
477 if let Err(e) = batch_update_file_metadata_from_disk(&all_generated_files) {
478 eprintln!("Warning: Failed to update metadata: {}", e);
480 }
481 } else {
482 std::env::set_current_dir(&original_dir).map_err(|e| {
484 FileSystemError::ReadFileFailed {
485 path: original_dir.display().to_string(),
486 source: e,
487 }
488 })?;
489 }
490 }
491 } else {
492 FormatterManager::format_files(&all_generated_files, formatter)?;
493
494 use crate::generator::writer::batch_update_file_metadata_from_disk;
496 if let Err(e) = batch_update_file_metadata_from_disk(&all_generated_files) {
497 eprintln!("Warning: Failed to update metadata: {}", e);
499 }
500 }
501 println!("{}", "✅ Files formatted".green());
502 }
503 }
504
505 Ok(())
506}
507
508fn collect_ts_files(dir: &Path, files: &mut Vec<PathBuf>) -> Result<()> {
509 if dir.is_dir() {
510 for entry in std::fs::read_dir(dir).map_err(|e| FileSystemError::ReadFileFailed {
511 path: dir.display().to_string(),
512 source: e,
513 })? {
514 let entry = entry.map_err(|e| FileSystemError::ReadFileFailed {
515 path: dir.display().to_string(),
516 source: e,
517 })?;
518 let path = entry.path();
519 if path.as_os_str().is_empty() {
521 continue;
522 }
523 if path.is_dir() {
524 collect_ts_files(&path, files)?;
525 } else if path.extension().and_then(|s| s.to_str()) == Some("ts") {
526 files.push(path);
527 }
528 }
529 }
530 Ok(())
531}