1use crate::config::model::{Config, SpecEntry};
2use crate::error::Result;
3use crate::formatter::FormatterManager;
4use crate::generator::api_client::generate_api_client_with_registry_and_engine_and_spec;
5use crate::generator::module_selector::select_modules;
6use crate::generator::swagger_parser::filter_common_schemas;
7use crate::generator::ts_typings::generate_typings_with_registry_and_engine_and_spec;
8use crate::generator::writer::write_api_client_with_options;
9use crate::generator::zod_schema::generate_zod_schemas_with_registry_and_engine_and_spec;
10use crate::progress::ProgressReporter;
11use std::path::PathBuf;
12
13#[derive(Debug, Clone)]
15pub struct GenerationStats {
16 pub spec_name: String,
17 pub modules_generated: usize,
18 pub files_generated: usize,
19 pub modules: Vec<String>,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum HookType {
25 ReactQuery,
26 Swr,
27}
28
29pub struct GenerateOptions {
31 pub use_cache: bool,
32 pub use_backup: bool,
33 pub use_force: bool,
34 pub verbose: bool,
35 pub hook_type: Option<HookType>,
36}
37
38pub async fn run_single_spec(
40 spec: &SpecEntry,
41 config: &Config,
42 options: &GenerateOptions,
43) -> Result<GenerationStats> {
44 let mut progress = ProgressReporter::new(options.verbose);
45 let spec_name = Some(spec.name.as_str());
47
48 progress.start_spinner(&format!("Fetching spec from: {}", spec.path));
49 let parsed = crate::generator::swagger_parser::fetch_and_parse_spec_with_cache_and_name(
50 &spec.path,
51 options.use_cache,
52 Some(&spec.name),
53 )
54 .await?;
55 progress.finish_spinner(&format!(
56 "Parsed spec with {} modules",
57 parsed.modules.len()
58 ));
59
60 let schemas_config = &spec.schemas;
62 let apis_config = &spec.apis;
63 let modules_config = &spec.modules;
64
65 let available_modules: Vec<String> = parsed
67 .modules
68 .iter()
69 .filter(|m| !modules_config.ignore.contains(m))
70 .cloned()
71 .collect();
72
73 if available_modules.is_empty() {
74 return Err(crate::error::GenerationError::NoModulesAvailable.into());
75 }
76
77 let selected_modules = if !modules_config.selected.is_empty() {
79 let valid_selected: Vec<String> = modules_config
81 .selected
82 .iter()
83 .filter(|m| available_modules.contains(m))
84 .cloned()
85 .collect();
86
87 if valid_selected.is_empty() {
88 return Err(crate::error::GenerationError::NoModulesSelected.into());
89 }
90
91 valid_selected
92 } else {
93 select_modules(&available_modules, &modules_config.ignore)?
95 };
96
97 let (filtered_module_schemas, common_schemas) =
99 filter_common_schemas(&parsed.module_schemas, &selected_modules);
100
101 let schemas_dir = PathBuf::from(&schemas_config.output);
103 let apis_dir = PathBuf::from(&apis_config.output);
104 let _root_dir = PathBuf::from(&config.root_dir);
105
106 let hooks_config = spec.hooks.clone().unwrap_or_default();
108
109 let mut total_files = 0;
112
113 if !common_schemas.is_empty() {
115 progress.start_spinner("Generating common schemas...");
116
117 let mut shared_enum_registry = std::collections::HashMap::new();
119
120 let project_root = std::env::current_dir().ok();
122 let template_engine =
123 crate::templates::engine::TemplateEngine::new(project_root.as_deref())?;
124
125 let common_types = generate_typings_with_registry_and_engine_and_spec(
128 &parsed.openapi,
129 &parsed.schemas,
130 &common_schemas,
131 &mut shared_enum_registry,
132 &[], Some(&template_engine),
134 spec_name,
135 )?;
136
137 let common_zod_schemas = generate_zod_schemas_with_registry_and_engine_and_spec(
140 &parsed.openapi,
141 &parsed.schemas,
142 &common_schemas,
143 &mut shared_enum_registry,
144 &[], Some(&template_engine),
146 spec_name,
147 )?;
148
149 use crate::generator::writer::write_schemas_with_module_mapping;
151 let common_files = write_schemas_with_module_mapping(
152 &schemas_dir,
153 "common",
154 &common_types,
155 &common_zod_schemas,
156 spec_name,
157 options.use_backup,
158 options.use_force,
159 Some(&filtered_module_schemas),
160 &common_schemas,
161 )?;
162 total_files += common_files.len();
163 progress.finish_spinner(&format!(
164 "Generated {} common schema files",
165 common_files.len()
166 ));
167 }
168
169 for module in &selected_modules {
170 progress.start_spinner(&format!("Generating code for module: {}", module));
171
172 let operations = parsed
174 .operations_by_tag
175 .get(module)
176 .cloned()
177 .unwrap_or_default();
178
179 if operations.is_empty() {
180 progress.warning(&format!("No operations found for module: {}", module));
181 continue;
182 }
183
184 let module_schema_names = filtered_module_schemas
186 .get(module)
187 .cloned()
188 .unwrap_or_default();
189
190 let project_root = std::env::current_dir().ok();
192 let template_engine =
193 crate::templates::engine::TemplateEngine::new(project_root.as_deref())?;
194
195 let mut shared_enum_registry = std::collections::HashMap::new();
197
198 let types = if !module_schema_names.is_empty() {
200 generate_typings_with_registry_and_engine_and_spec(
201 &parsed.openapi,
202 &parsed.schemas,
203 &module_schema_names,
204 &mut shared_enum_registry,
205 &common_schemas,
206 Some(&template_engine),
207 spec_name,
208 )?
209 } else {
210 Vec::new()
211 };
212
213 let zod_schemas = if !module_schema_names.is_empty() {
215 generate_zod_schemas_with_registry_and_engine_and_spec(
216 &parsed.openapi,
217 &parsed.schemas,
218 &module_schema_names,
219 &mut shared_enum_registry,
220 &common_schemas,
221 Some(&template_engine),
222 spec_name,
223 )?
224 } else {
225 Vec::new()
226 };
227
228 use crate::generator::query_params::{
231 generate_query_params_for_module, QueryParamsContext,
232 };
233 let query_params_result = generate_query_params_for_module(QueryParamsContext {
234 openapi: &parsed.openapi,
235 operations: &operations,
236 enum_registry: &mut shared_enum_registry,
237 template_engine: Some(&template_engine),
238 spec_name,
239 existing_types: &types,
240 existing_zod_schemas: &zod_schemas,
241 })?;
242
243 let api_result = generate_api_client_with_registry_and_engine_and_spec(
245 &parsed.openapi,
246 &operations,
247 module,
248 &common_schemas,
249 &mut shared_enum_registry,
250 Some(&template_engine),
251 spec_name,
252 Some(&config.root_dir),
253 Some(&apis_config.output),
254 Some(&schemas_config.output),
255 )?;
256
257 let mut all_types = types;
260 all_types.extend(query_params_result.types);
261
262 let mut all_zod_schemas = zod_schemas;
264 all_zod_schemas.extend(query_params_result.zod_schemas);
265
266 use crate::generator::writer::write_schemas_with_module_mapping;
269 let schema_files = write_schemas_with_module_mapping(
270 &schemas_dir,
271 module,
272 &all_types,
273 &all_zod_schemas,
274 spec_name,
275 options.use_backup,
276 options.use_force,
277 Some(&filtered_module_schemas),
278 &common_schemas,
279 )?;
280 total_files += schema_files.len();
281
282 let api_files = write_api_client_with_options(
284 &apis_dir,
285 module,
286 &api_result.functions,
287 spec_name,
288 options.use_backup,
289 options.use_force,
290 )?;
291 total_files += api_files.len();
292
293 let hook_type = options.hook_type.or_else(|| {
296 hooks_config
297 .library
298 .as_ref()
299 .and_then(|lib| match lib.as_str() {
300 "react-query" => Some(HookType::ReactQuery),
301 "swr" => Some(HookType::Swr),
302 _ => None,
303 })
304 });
305
306 if let Some(hook_type) = hook_type {
308 progress.start_spinner(&format!("Generating hooks for module: {}", module));
309
310 use crate::generator::query_keys::generate_query_keys;
312 let query_keys_context = generate_query_keys(&operations, module, spec_name);
313
314 let query_keys_content = template_engine.render(
316 crate::templates::registry::TemplateId::QueryKeys,
317 &query_keys_context,
318 )?;
319
320 let query_keys_output = PathBuf::from(&hooks_config.query_keys_output);
323
324 use crate::generator::writer::write_query_keys_with_options;
325 write_query_keys_with_options(
326 &query_keys_output,
327 module,
328 &query_keys_content,
329 spec_name,
330 options.use_backup,
331 options.use_force,
332 )?;
333 total_files += 1;
334
335 let hooks = match hook_type {
337 HookType::ReactQuery => {
338 use crate::generator::hooks::react_query::generate_react_query_hooks;
339 generate_react_query_hooks(
340 &parsed.openapi,
341 &operations,
342 module,
343 spec_name,
344 &common_schemas,
345 &mut shared_enum_registry,
346 &template_engine,
347 Some(&apis_config.output),
348 Some(&schemas_config.output),
349 Some(&hooks_config.output),
350 Some(&hooks_config.query_keys_output),
351 )?
352 }
353 HookType::Swr => {
354 use crate::generator::hooks::swr::generate_swr_hooks;
355 generate_swr_hooks(
356 &parsed.openapi,
357 &operations,
358 module,
359 spec_name,
360 &common_schemas,
361 &mut shared_enum_registry,
362 &template_engine,
363 Some(&apis_config.output),
364 Some(&schemas_config.output),
365 Some(&hooks_config.output),
366 Some(&hooks_config.query_keys_output),
367 )?
368 }
369 };
370
371 let hooks_output = PathBuf::from(&hooks_config.output);
374
375 use crate::generator::writer::write_hooks_with_options;
376 let hook_files = write_hooks_with_options(
377 &hooks_output,
378 module,
379 &hooks,
380 spec_name,
381 options.use_backup,
382 options.use_force,
383 )?;
384 total_files += hook_files.len();
385
386 progress.finish_spinner(&format!(
387 "Generated {} hook files for module: {}",
388 hook_files.len(),
389 module
390 ));
391 }
392
393 progress.finish_spinner(&format!(
394 "Generated {} files for module: {}",
395 schema_files.len() + api_files.len(),
396 module
397 ));
398 }
399
400 let mut all_generated_files = Vec::new();
402
403 if schemas_dir.exists() {
405 collect_ts_files(&schemas_dir, &mut all_generated_files)?;
406 }
407
408 if apis_dir.exists() {
410 collect_ts_files(&apis_dir, &mut all_generated_files)?;
411 }
412
413 if options.hook_type.is_some() {
415 let root_dir = std::env::current_dir().ok();
416 let hooks_dir = if let Some(ref root) = root_dir {
417 if let Some(spec) = spec_name {
418 root.join("src").join("hooks").join(spec)
419 } else {
420 root.join("src").join("hooks")
421 }
422 } else {
423 PathBuf::from("src/hooks")
424 };
425 if hooks_dir.exists() {
426 collect_ts_files(&hooks_dir, &mut all_generated_files)?;
427 }
428
429 let query_keys_dir = if let Some(ref root) = root_dir {
431 if let Some(spec) = spec_name {
432 root.join("src").join("query-keys").join(spec)
433 } else {
434 root.join("src").join("query-keys")
435 }
436 } else {
437 PathBuf::from("src/query-keys")
438 };
439 if query_keys_dir.exists() {
440 collect_ts_files(&query_keys_dir, &mut all_generated_files)?;
441 }
442 }
443
444 if !all_generated_files.is_empty() {
446 let current_dir =
448 std::env::current_dir().map_err(|e| crate::error::FileSystemError::ReadFileFailed {
449 path: ".".to_string(),
450 source: e,
451 })?;
452
453 let schemas_dir_abs = if schemas_dir.is_absolute() {
455 schemas_dir.clone()
456 } else {
457 current_dir.join(&schemas_dir)
458 };
459 let apis_dir_abs = if apis_dir.is_absolute() {
460 apis_dir.clone()
461 } else {
462 current_dir.join(&apis_dir)
463 };
464
465 let output_base = schemas_dir_abs
466 .parent()
467 .and_then(|p| p.parent())
468 .or_else(|| apis_dir_abs.parent().and_then(|p| p.parent()));
469
470 let formatter = if let Some(base_dir) = output_base {
471 FormatterManager::detect_formatter_from_dir(base_dir)
472 .or_else(FormatterManager::detect_formatter)
473 } else {
474 FormatterManager::detect_formatter()
475 };
476
477 if let Some(formatter) = formatter {
478 progress.start_spinner("Formatting generated files...");
479 let original_dir = std::env::current_dir().map_err(|e| {
480 crate::error::FileSystemError::ReadFileFailed {
481 path: ".".to_string(),
482 source: e,
483 }
484 })?;
485
486 if let Some(output_base) = output_base {
487 if output_base.as_os_str().is_empty() {
489 FormatterManager::format_files(&all_generated_files, formatter)?;
491 } else {
492 std::env::set_current_dir(output_base).map_err(|e| {
493 crate::error::FileSystemError::ReadFileFailed {
494 path: output_base.display().to_string(),
495 source: e,
496 }
497 })?;
498
499 let relative_files: Vec<PathBuf> = all_generated_files
500 .iter()
501 .filter_map(|p| {
502 p.strip_prefix(output_base)
503 .ok()
504 .map(|p| p.to_path_buf())
505 .filter(|p| !p.as_os_str().is_empty())
506 })
507 .collect();
508
509 if !relative_files.is_empty() {
510 let result = FormatterManager::format_files(&relative_files, formatter);
511
512 std::env::set_current_dir(&original_dir).map_err(|e| {
513 crate::error::FileSystemError::ReadFileFailed {
514 path: original_dir.display().to_string(),
515 source: e,
516 }
517 })?;
518
519 result?;
520
521 use crate::generator::writer::batch_update_file_metadata_from_disk;
523 if let Err(e) = batch_update_file_metadata_from_disk(&all_generated_files) {
524 progress.warning(&format!("Failed to update metadata: {}", e));
526 }
527 } else {
528 std::env::set_current_dir(&original_dir).map_err(|e| {
529 crate::error::FileSystemError::ReadFileFailed {
530 path: original_dir.display().to_string(),
531 source: e,
532 }
533 })?;
534 }
535 }
536 } else {
537 FormatterManager::format_files(&all_generated_files, formatter)?;
538
539 use crate::generator::writer::batch_update_file_metadata_from_disk;
541 if let Err(e) = batch_update_file_metadata_from_disk(&all_generated_files) {
542 progress.warning(&format!("Failed to update metadata: {}", e));
544 }
545 }
546 progress.finish_spinner("Files formatted");
547 }
548 }
549
550 Ok(GenerationStats {
551 spec_name: spec.name.clone(),
552 modules_generated: selected_modules.len(),
553 files_generated: total_files,
554 modules: selected_modules,
555 })
556}
557
558pub async fn run_all_specs(
560 specs: &[SpecEntry],
561 config: &Config,
562 options: &GenerateOptions,
563) -> Result<Vec<GenerationStats>> {
564 let mut stats = Vec::new();
565 for spec in specs {
566 let result = run_single_spec(spec, config, options).await?;
567 stats.push(result);
568 }
569 Ok(stats)
570}
571
572fn collect_ts_files(dir: &std::path::Path, files: &mut Vec<PathBuf>) -> Result<()> {
573 if dir.is_dir() {
574 for entry in
575 std::fs::read_dir(dir).map_err(|e| crate::error::FileSystemError::ReadFileFailed {
576 path: dir.display().to_string(),
577 source: e,
578 })?
579 {
580 let entry = entry.map_err(|e| crate::error::FileSystemError::ReadFileFailed {
581 path: dir.display().to_string(),
582 source: e,
583 })?;
584 let path = entry.path();
585 if path.as_os_str().is_empty() {
587 continue;
588 }
589 if path.is_dir() {
590 collect_ts_files(&path, files)?;
591 } else if path.extension().and_then(|s| s.to_str()) == Some("ts") {
592 files.push(path);
593 }
594 }
595 }
596 Ok(())
597}