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 let hooks_config = spec.hooks.clone().unwrap_or_default();
67
68 use crate::generator::writer::{ensure_directory, write_runtime_client};
70 let root_dir_path = PathBuf::from(&config.root_dir);
71 ensure_directory(&root_dir_path)?;
72 let runtime_dir = root_dir_path.join("runtime");
73 if !runtime_dir.exists() {
74 write_runtime_client(&root_dir_path, None, Some(apis_config))?;
75 }
76
77 if !printed_urls.contains(spec_path) {
79 println!(
80 "{}",
81 format!("📥 Fetching spec from: {}", spec_path).bright_blue()
82 );
83 printed_urls.insert(spec_path.clone());
84 }
85
86 let use_cache = config.generation.enable_cache;
88 let parsed = crate::generator::swagger_parser::fetch_and_parse_spec_with_cache_and_name(
89 spec_path,
90 use_cache,
91 Some(&spec.name),
92 )
93 .await?;
94
95 let selected_modules = if modules_config.selected.is_empty() {
97 println!(
99 "{}",
100 format!(
101 "No modules selected for spec '{}'. Please select modules to update:",
102 spec.name
103 )
104 .bright_yellow()
105 );
106 println!();
107
108 let available_modules: Vec<String> = parsed
110 .modules
111 .iter()
112 .filter(|m| !modules_config.ignore.contains(m))
113 .cloned()
114 .collect();
115
116 if available_modules.is_empty() {
117 println!(
118 "{}",
119 format!("⚠️ Skipping spec '{}': No modules available", spec.name).yellow()
120 );
121 continue;
122 }
123
124 use crate::generator::module_selector::select_modules;
126 let selected = select_modules(&available_modules, &modules_config.ignore)?;
127
128 use crate::config::loader::load_config;
130 let mut config = load_config()?;
131 if let Some(spec_entry) = config.specs.iter_mut().find(|s| s.name == spec.name) {
132 spec_entry.modules.selected = selected.clone();
133 }
134 use crate::config::loader::save_config;
135 save_config(&config)?;
136
137 selected
138 } else {
139 modules_config.selected.clone()
140 };
141 println!(
142 "{}",
143 format!("✅ Parsed spec with {} modules", parsed.modules.len()).green()
144 );
145 println!();
146 println!(
147 "{}",
148 format!(
149 "📦 Updating {} module(s): {}",
150 selected_modules.len(),
151 selected_modules.join(", ")
152 )
153 .bright_green()
154 );
155 println!();
156
157 let (filtered_module_schemas, common_schemas) =
159 filter_common_schemas(&parsed.module_schemas, &selected_modules);
160
161 let project_root = std::env::current_dir().ok();
163 let template_engine =
164 crate::templates::engine::TemplateEngine::new(project_root.as_deref())?;
165
166 let schemas_dir = PathBuf::from(&schemas_config.output);
168 let apis_dir = PathBuf::from(&apis_config.output);
169
170 let mut total_files = 0;
171 let mut module_summary: Vec<(String, usize)> = Vec::new();
172
173 let use_force = config.generation.conflict_strategy == "force";
175 let use_backup = config.generation.enable_backup;
176
177 if !common_schemas.is_empty() {
179 println!("{}", "🔨 Regenerating common schemas...".bright_cyan());
180
181 let mut shared_enum_registry = std::collections::HashMap::new();
183
184 let common_types =
187 crate::generator::ts_typings::generate_typings_with_registry_and_engine_and_spec(
188 &parsed.openapi,
189 &parsed.schemas,
190 &common_schemas,
191 &mut shared_enum_registry,
192 &[], Some(&template_engine),
194 Some(&spec.name),
195 )?;
196
197 let common_zod_schemas =
200 crate::generator::zod_schema::generate_zod_schemas_with_registry_and_engine_and_spec(
201 &parsed.openapi,
202 &parsed.schemas,
203 &common_schemas,
204 &mut shared_enum_registry,
205 &[], Some(&template_engine),
207 Some(&spec.name),
208 )?;
209
210 use crate::generator::writer::write_schemas_with_module_mapping;
212 let common_files = write_schemas_with_module_mapping(
213 &schemas_dir,
214 "common",
215 &common_types,
216 &common_zod_schemas,
217 Some(&spec.name), use_backup,
219 use_force,
220 Some(&filtered_module_schemas),
221 &common_schemas,
222 )?;
223 total_files += common_files.len();
224 module_summary.push(("common".to_string(), common_files.len()));
225 }
226
227 for module in &selected_modules {
228 println!(
229 "{}",
230 format!("🔨 Regenerating code for module: {}", module).bright_cyan()
231 );
232
233 let operations = parsed
235 .operations_by_tag
236 .get(module)
237 .cloned()
238 .unwrap_or_default();
239
240 if operations.is_empty() {
241 println!(
242 "{}",
243 format!("⚠️ No operations found for module: {}", module).yellow()
244 );
245 continue;
246 }
247
248 let module_schema_names = filtered_module_schemas
250 .get(module)
251 .cloned()
252 .unwrap_or_default();
253
254 let mut shared_enum_registry = std::collections::HashMap::new();
256
257 let types = if !module_schema_names.is_empty() {
259 crate::generator::ts_typings::generate_typings_with_registry_and_engine_and_spec(
260 &parsed.openapi,
261 &parsed.schemas,
262 &module_schema_names,
263 &mut shared_enum_registry,
264 &common_schemas,
265 Some(&template_engine),
266 Some(&spec.name),
267 )?
268 } else {
269 Vec::new()
270 };
271
272 let zod_schemas = if !module_schema_names.is_empty() {
274 crate::generator::zod_schema::generate_zod_schemas_with_registry_and_engine_and_spec(
275 &parsed.openapi,
276 &parsed.schemas,
277 &module_schema_names,
278 &mut shared_enum_registry,
279 &common_schemas,
280 Some(&template_engine),
281 Some(&spec.name),
282 )?
283 } else {
284 Vec::new()
285 };
286
287 use crate::generator::query_params::{
289 generate_query_params_for_module, QueryParamsContext,
290 };
291 let query_params_result = generate_query_params_for_module(QueryParamsContext {
292 openapi: &parsed.openapi,
293 operations: &operations,
294 enum_registry: &mut shared_enum_registry,
295 template_engine: Some(&template_engine),
296 spec_name: Some(&spec.name),
297 existing_types: &types,
298 existing_zod_schemas: &zod_schemas,
299 })?;
300
301 let api_result =
303 crate::generator::api_client::generate_api_client_with_registry_and_engine_and_spec(
304 &parsed.openapi,
305 &operations,
306 module,
307 &common_schemas,
308 &mut shared_enum_registry,
309 Some(&template_engine),
310 Some(&spec.name),
311 Some(&config.root_dir),
312 Some(&apis_config.output),
313 Some(&schemas_config.output),
314 )?;
315
316 let mut all_types = types;
319 all_types.extend(query_params_result.types);
320
321 let mut all_zod_schemas = zod_schemas;
323 all_zod_schemas.extend(query_params_result.zod_schemas);
324
325 use crate::generator::writer::write_schemas_with_module_mapping;
327 let schema_files = write_schemas_with_module_mapping(
328 &schemas_dir,
329 module,
330 &all_types,
331 &all_zod_schemas,
332 Some(&spec.name), use_backup,
334 use_force,
335 Some(&filtered_module_schemas),
336 &common_schemas,
337 )?;
338 total_files += schema_files.len();
339
340 let api_files = write_api_client_with_options(
342 &apis_dir,
343 module,
344 &api_result.functions,
345 Some(&spec.name), use_backup,
347 use_force,
348 )?;
349 total_files += api_files.len();
350
351 use crate::specs::runner::HookType;
353 let hook_type = hooks_config
354 .library
355 .as_ref()
356 .and_then(|lib| match lib.as_str() {
357 "react-query" => Some(HookType::ReactQuery),
358 "swr" => Some(HookType::Swr),
359 _ => None,
360 });
361
362 let mut hook_files_count = 0;
364 if let Some(hook_type) = hook_type {
365 println!(
366 "{}",
367 format!("🔨 Generating hooks for module: {}", module).bright_cyan()
368 );
369
370 use crate::generator::query_keys::generate_query_keys;
372 let query_keys_context = generate_query_keys(&operations, module, Some(&spec.name));
373
374 let query_keys_content = template_engine.render(
376 crate::templates::registry::TemplateId::QueryKeys,
377 &query_keys_context,
378 )?;
379
380 let query_keys_output = PathBuf::from(&hooks_config.query_keys_output);
383
384 use crate::generator::writer::write_query_keys_with_options;
385 write_query_keys_with_options(
386 &query_keys_output,
387 module,
388 &query_keys_content,
389 Some(&spec.name),
390 use_backup,
391 use_force,
392 )?;
393 total_files += 1;
394
395 let hooks = match hook_type {
397 HookType::ReactQuery => {
398 use crate::generator::hooks::react_query::generate_react_query_hooks;
399 generate_react_query_hooks(
400 &parsed.openapi,
401 &operations,
402 module,
403 Some(&spec.name),
404 &common_schemas,
405 &mut shared_enum_registry,
406 &template_engine,
407 Some(&apis_config.output),
408 Some(&schemas_config.output),
409 Some(&hooks_config.output),
410 Some(&hooks_config.query_keys_output),
411 )?
412 }
413 HookType::Swr => {
414 use crate::generator::hooks::swr::generate_swr_hooks;
415 generate_swr_hooks(
416 &parsed.openapi,
417 &operations,
418 module,
419 Some(&spec.name),
420 &common_schemas,
421 &mut shared_enum_registry,
422 &template_engine,
423 Some(&apis_config.output),
424 Some(&schemas_config.output),
425 Some(&hooks_config.output),
426 Some(&hooks_config.query_keys_output),
427 )?
428 }
429 };
430
431 let hooks_output = PathBuf::from(&hooks_config.output);
434
435 use crate::generator::writer::write_hooks_with_options;
436 let hook_files = write_hooks_with_options(
437 &hooks_output,
438 module,
439 &hooks,
440 Some(&spec.name),
441 use_backup,
442 use_force,
443 )?;
444 hook_files_count = hook_files.len();
445 total_files += hook_files_count;
446 }
447
448 let module_file_count = schema_files.len()
449 + api_files.len()
450 + if hook_type.is_some() {
451 1 + hook_files_count
452 } else {
453 0
454 };
455 module_summary.push((module.clone(), module_file_count));
456 println!(
457 "{}",
458 format!(
459 "✅ Regenerated {} files for module: {}",
460 module_file_count, module
461 )
462 .green()
463 );
464 }
465
466 println!();
467 println!(
468 "{}",
469 format!(
470 "✨ Successfully updated {} files for spec '{}'!",
471 total_files, spec.name
472 )
473 .bright_green()
474 );
475 println!();
476 println!(
477 "{}",
478 format!("Updated files for '{}':", spec.name).bright_cyan()
479 );
480 println!(" 📁 Schemas: {}", schemas_config.output);
481 println!(" 📁 APIs: {}", apis_config.output);
482 if hooks_config.library.is_some() {
483 println!(" 📁 Hooks: {}", hooks_config.output);
484 println!(" 📁 Query Keys: {}", hooks_config.query_keys_output);
485 }
486
487 all_specs_summary.push((spec.name.clone(), total_files, module_summary.clone()));
489
490 let current_dir = std::env::current_dir().map_err(|e| FileSystemError::ReadFileFailed {
492 path: ".".to_string(),
493 source: e,
494 })?;
495
496 let schemas_dir_abs = if schemas_dir.is_absolute() {
497 schemas_dir.clone()
498 } else {
499 current_dir.join(&schemas_dir)
500 };
501 let apis_dir_abs = if apis_dir.is_absolute() {
502 apis_dir.clone()
503 } else {
504 current_dir.join(&apis_dir)
505 };
506
507 if schemas_dir_abs.exists() {
509 collect_ts_files(&schemas_dir_abs, &mut all_generated_files)?;
510 }
511
512 if apis_dir_abs.exists() {
514 collect_ts_files(&apis_dir_abs, &mut all_generated_files)?;
515 }
516 }
517
518 println!();
520 println!("{}", "=".repeat(60).bright_black());
521 println!();
522 let total_all_files: usize = all_specs_summary.iter().map(|(_, count, _)| count).sum();
523 println!(
524 "{}",
525 format!(
526 "✨ Successfully updated {} files across {} spec(s)!",
527 total_all_files,
528 all_specs_summary.len()
529 )
530 .bright_green()
531 );
532 println!();
533 println!("{}", "Summary by spec:".bright_cyan());
534 for (spec_name, file_count, module_summary) in &all_specs_summary {
535 println!(" 📦 {}: {} files", spec_name, file_count);
536 if !module_summary.is_empty() {
537 for (module, count) in module_summary {
538 println!(" • {}: {} files", module, count);
539 }
540 }
541 }
542 println!();
543
544 if !all_generated_files.is_empty() {
546 let output_base = all_generated_files.first().and_then(|first_file| {
549 first_file
550 .parent()
551 .and_then(|p| p.parent())
552 .and_then(|p| p.parent())
553 });
554
555 let formatter = if let Some(base_dir) = output_base {
556 FormatterManager::detect_formatter_from_dir(base_dir)
557 .or_else(FormatterManager::detect_formatter)
558 } else {
559 FormatterManager::detect_formatter()
560 };
561
562 if let Some(formatter) = formatter {
563 println!("{}", "Formatting generated files...".bright_cyan());
564 let original_dir =
565 std::env::current_dir().map_err(|e| FileSystemError::ReadFileFailed {
566 path: ".".to_string(),
567 source: e,
568 })?;
569
570 if let Some(output_base) = output_base {
571 if output_base.as_os_str().is_empty() {
573 FormatterManager::format_files(&all_generated_files, formatter)?;
575 } else {
576 std::env::set_current_dir(output_base).map_err(|e| {
577 FileSystemError::ReadFileFailed {
578 path: output_base.display().to_string(),
579 source: e,
580 }
581 })?;
582
583 let relative_files: Vec<PathBuf> = all_generated_files
585 .iter()
586 .filter_map(|p| {
587 p.strip_prefix(output_base)
588 .ok()
589 .map(|p| p.to_path_buf())
590 .filter(|p| !p.as_os_str().is_empty())
591 })
592 .collect();
593
594 if !relative_files.is_empty() {
595 let result = FormatterManager::format_files(&relative_files, formatter);
596
597 std::env::set_current_dir(&original_dir).map_err(|e| {
599 FileSystemError::ReadFileFailed {
600 path: original_dir.display().to_string(),
601 source: e,
602 }
603 })?;
604
605 result?;
606
607 use crate::generator::writer::batch_update_file_metadata_from_disk;
609 if let Err(e) = batch_update_file_metadata_from_disk(&all_generated_files) {
610 eprintln!("Warning: Failed to update metadata: {}", e);
612 }
613 } else {
614 std::env::set_current_dir(&original_dir).map_err(|e| {
616 FileSystemError::ReadFileFailed {
617 path: original_dir.display().to_string(),
618 source: e,
619 }
620 })?;
621 }
622 }
623 } else {
624 FormatterManager::format_files(&all_generated_files, formatter)?;
625
626 use crate::generator::writer::batch_update_file_metadata_from_disk;
628 if let Err(e) = batch_update_file_metadata_from_disk(&all_generated_files) {
629 eprintln!("Warning: Failed to update metadata: {}", e);
631 }
632 }
633 println!("{}", "✅ Files formatted".green());
634 }
635 }
636
637 Ok(())
638}
639
640fn collect_ts_files(dir: &Path, files: &mut Vec<PathBuf>) -> Result<()> {
641 if dir.is_dir() {
642 for entry in std::fs::read_dir(dir).map_err(|e| FileSystemError::ReadFileFailed {
643 path: dir.display().to_string(),
644 source: e,
645 })? {
646 let entry = entry.map_err(|e| FileSystemError::ReadFileFailed {
647 path: dir.display().to_string(),
648 source: e,
649 })?;
650 let path = entry.path();
651 if path.as_os_str().is_empty() {
653 continue;
654 }
655 if path.is_dir() {
656 collect_ts_files(&path, files)?;
657 } else if path.extension().and_then(|s| s.to_str()) == Some("ts") {
658 files.push(path);
659 }
660 }
661 }
662 Ok(())
663}