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
105 let http_file = apis_dir.join("http.ts");
107 if !http_file.exists() {
108 use crate::generator::writer::write_http_client_template;
109 write_http_client_template(&http_file)?;
110 if options.verbose {
111 progress.success(&format!("Created {}", http_file.display()));
112 }
113 }
114
115 let mut total_files = 0;
116
117 if !common_schemas.is_empty() {
119 progress.start_spinner("Generating common schemas...");
120
121 let mut shared_enum_registry = std::collections::HashMap::new();
123
124 let project_root = std::env::current_dir().ok();
126 let template_engine =
127 crate::templates::engine::TemplateEngine::new(project_root.as_deref())?;
128
129 let common_types = generate_typings_with_registry_and_engine_and_spec(
132 &parsed.openapi,
133 &parsed.schemas,
134 &common_schemas,
135 &mut shared_enum_registry,
136 &[], Some(&template_engine),
138 spec_name,
139 )?;
140
141 let common_zod_schemas = generate_zod_schemas_with_registry_and_engine_and_spec(
144 &parsed.openapi,
145 &parsed.schemas,
146 &common_schemas,
147 &mut shared_enum_registry,
148 &[], Some(&template_engine),
150 spec_name,
151 )?;
152
153 use crate::generator::writer::write_schemas_with_module_mapping;
155 let common_files = write_schemas_with_module_mapping(
156 &schemas_dir,
157 "common",
158 &common_types,
159 &common_zod_schemas,
160 spec_name,
161 options.use_backup,
162 options.use_force,
163 Some(&filtered_module_schemas),
164 &common_schemas,
165 )?;
166 total_files += common_files.len();
167 progress.finish_spinner(&format!(
168 "Generated {} common schema files",
169 common_files.len()
170 ));
171 }
172
173 for module in &selected_modules {
174 progress.start_spinner(&format!("Generating code for module: {}", module));
175
176 let operations = parsed
178 .operations_by_tag
179 .get(module)
180 .cloned()
181 .unwrap_or_default();
182
183 if operations.is_empty() {
184 progress.warning(&format!("No operations found for module: {}", module));
185 continue;
186 }
187
188 let module_schema_names = filtered_module_schemas
190 .get(module)
191 .cloned()
192 .unwrap_or_default();
193
194 let project_root = std::env::current_dir().ok();
196 let template_engine =
197 crate::templates::engine::TemplateEngine::new(project_root.as_deref())?;
198
199 let mut shared_enum_registry = std::collections::HashMap::new();
201
202 let types = if !module_schema_names.is_empty() {
204 generate_typings_with_registry_and_engine_and_spec(
205 &parsed.openapi,
206 &parsed.schemas,
207 &module_schema_names,
208 &mut shared_enum_registry,
209 &common_schemas,
210 Some(&template_engine),
211 spec_name,
212 )?
213 } else {
214 Vec::new()
215 };
216
217 let zod_schemas = if !module_schema_names.is_empty() {
219 generate_zod_schemas_with_registry_and_engine_and_spec(
220 &parsed.openapi,
221 &parsed.schemas,
222 &module_schema_names,
223 &mut shared_enum_registry,
224 &common_schemas,
225 Some(&template_engine),
226 spec_name,
227 )?
228 } else {
229 Vec::new()
230 };
231
232 let api_result = generate_api_client_with_registry_and_engine_and_spec(
234 &parsed.openapi,
235 &operations,
236 module,
237 &common_schemas,
238 &mut shared_enum_registry,
239 Some(&template_engine),
240 spec_name,
241 )?;
242
243 let mut all_types = types;
245 all_types.extend(api_result.response_types);
246
247 use crate::generator::writer::write_schemas_with_module_mapping;
250 let schema_files = write_schemas_with_module_mapping(
251 &schemas_dir,
252 module,
253 &all_types,
254 &zod_schemas,
255 spec_name,
256 options.use_backup,
257 options.use_force,
258 Some(&filtered_module_schemas),
259 &common_schemas,
260 )?;
261 total_files += schema_files.len();
262
263 let api_files = write_api_client_with_options(
265 &apis_dir,
266 module,
267 &api_result.functions,
268 spec_name,
269 options.use_backup,
270 options.use_force,
271 )?;
272 total_files += api_files.len();
273
274 if let Some(hook_type) = options.hook_type {
276 progress.start_spinner(&format!("Generating hooks for module: {}", module));
277
278 use crate::generator::query_keys::generate_query_keys;
280 let query_keys_context = generate_query_keys(&operations, module, spec_name);
281
282 let query_keys_content = template_engine.render(
284 crate::templates::registry::TemplateId::QueryKeys,
285 &query_keys_context,
286 )?;
287
288 let root_dir = std::env::current_dir().ok();
291 let query_keys_output = if let Some(ref root) = root_dir {
292 if let Some(spec) = spec_name {
293 root.join("src").join("query-keys").join(spec)
294 } else {
295 root.join("src").join("query-keys")
296 }
297 } else {
298 PathBuf::from("src/query-keys")
299 };
300
301 use crate::generator::writer::write_query_keys_with_options;
302 write_query_keys_with_options(
303 &query_keys_output,
304 module,
305 &query_keys_content,
306 spec_name,
307 options.use_backup,
308 options.use_force,
309 )?;
310 total_files += 1;
311
312 let hooks = match hook_type {
314 HookType::ReactQuery => {
315 use crate::generator::hooks::react_query::generate_react_query_hooks;
316 generate_react_query_hooks(
317 &parsed.openapi,
318 &operations,
319 module,
320 spec_name,
321 &common_schemas,
322 &mut shared_enum_registry,
323 &template_engine,
324 )?
325 }
326 HookType::Swr => {
327 use crate::generator::hooks::swr::generate_swr_hooks;
328 generate_swr_hooks(
329 &parsed.openapi,
330 &operations,
331 module,
332 spec_name,
333 &common_schemas,
334 &mut shared_enum_registry,
335 &template_engine,
336 )?
337 }
338 };
339
340 let hooks_output = if let Some(ref root) = root_dir {
343 if let Some(spec) = spec_name {
344 root.join("src").join("hooks").join(spec)
345 } else {
346 root.join("src").join("hooks")
347 }
348 } else {
349 PathBuf::from("src/hooks")
350 };
351
352 use crate::generator::writer::write_hooks_with_options;
353 let hook_files = write_hooks_with_options(
354 &hooks_output,
355 module,
356 &hooks,
357 spec_name,
358 options.use_backup,
359 options.use_force,
360 )?;
361 total_files += hook_files.len();
362
363 progress.finish_spinner(&format!(
364 "Generated {} hook files for module: {}",
365 hook_files.len(),
366 module
367 ));
368 }
369
370 progress.finish_spinner(&format!(
371 "Generated {} files for module: {}",
372 schema_files.len() + api_files.len(),
373 module
374 ));
375 }
376
377 let mut all_generated_files = Vec::new();
379
380 if schemas_dir.exists() {
382 collect_ts_files(&schemas_dir, &mut all_generated_files)?;
383 }
384
385 if apis_dir.exists() {
387 collect_ts_files(&apis_dir, &mut all_generated_files)?;
388 }
389
390 if options.hook_type.is_some() {
392 let root_dir = std::env::current_dir().ok();
393 let hooks_dir = if let Some(ref root) = root_dir {
394 if let Some(spec) = spec_name {
395 root.join("src").join("hooks").join(spec)
396 } else {
397 root.join("src").join("hooks")
398 }
399 } else {
400 PathBuf::from("src/hooks")
401 };
402 if hooks_dir.exists() {
403 collect_ts_files(&hooks_dir, &mut all_generated_files)?;
404 }
405
406 let query_keys_dir = if let Some(ref root) = root_dir {
408 if let Some(spec) = spec_name {
409 root.join("src").join("query-keys").join(spec)
410 } else {
411 root.join("src").join("query-keys")
412 }
413 } else {
414 PathBuf::from("src/query-keys")
415 };
416 if query_keys_dir.exists() {
417 collect_ts_files(&query_keys_dir, &mut all_generated_files)?;
418 }
419 }
420
421 if !all_generated_files.is_empty() {
423 let current_dir =
425 std::env::current_dir().map_err(|e| crate::error::FileSystemError::ReadFileFailed {
426 path: ".".to_string(),
427 source: e,
428 })?;
429
430 let schemas_dir_abs = if schemas_dir.is_absolute() {
432 schemas_dir.clone()
433 } else {
434 current_dir.join(&schemas_dir)
435 };
436 let apis_dir_abs = if apis_dir.is_absolute() {
437 apis_dir.clone()
438 } else {
439 current_dir.join(&apis_dir)
440 };
441
442 let output_base = schemas_dir_abs
443 .parent()
444 .and_then(|p| p.parent())
445 .or_else(|| apis_dir_abs.parent().and_then(|p| p.parent()));
446
447 let formatter = if let Some(base_dir) = output_base {
448 FormatterManager::detect_formatter_from_dir(base_dir)
449 .or_else(FormatterManager::detect_formatter)
450 } else {
451 FormatterManager::detect_formatter()
452 };
453
454 if let Some(formatter) = formatter {
455 progress.start_spinner("Formatting generated files...");
456 let original_dir = std::env::current_dir().map_err(|e| {
457 crate::error::FileSystemError::ReadFileFailed {
458 path: ".".to_string(),
459 source: e,
460 }
461 })?;
462
463 if let Some(output_base) = output_base {
464 if output_base.as_os_str().is_empty() {
466 FormatterManager::format_files(&all_generated_files, formatter)?;
468 } else {
469 std::env::set_current_dir(output_base).map_err(|e| {
470 crate::error::FileSystemError::ReadFileFailed {
471 path: output_base.display().to_string(),
472 source: e,
473 }
474 })?;
475
476 let relative_files: Vec<PathBuf> = all_generated_files
477 .iter()
478 .filter_map(|p| {
479 p.strip_prefix(output_base)
480 .ok()
481 .map(|p| p.to_path_buf())
482 .filter(|p| !p.as_os_str().is_empty())
483 })
484 .collect();
485
486 if !relative_files.is_empty() {
487 let result = FormatterManager::format_files(&relative_files, formatter);
488
489 std::env::set_current_dir(&original_dir).map_err(|e| {
490 crate::error::FileSystemError::ReadFileFailed {
491 path: original_dir.display().to_string(),
492 source: e,
493 }
494 })?;
495
496 result?;
497
498 use crate::generator::writer::batch_update_file_metadata_from_disk;
500 if let Err(e) = batch_update_file_metadata_from_disk(&all_generated_files) {
501 progress.warning(&format!("Failed to update metadata: {}", e));
503 }
504 } else {
505 std::env::set_current_dir(&original_dir).map_err(|e| {
506 crate::error::FileSystemError::ReadFileFailed {
507 path: original_dir.display().to_string(),
508 source: e,
509 }
510 })?;
511 }
512 }
513 } else {
514 FormatterManager::format_files(&all_generated_files, formatter)?;
515
516 use crate::generator::writer::batch_update_file_metadata_from_disk;
518 if let Err(e) = batch_update_file_metadata_from_disk(&all_generated_files) {
519 progress.warning(&format!("Failed to update metadata: {}", e));
521 }
522 }
523 progress.finish_spinner("Files formatted");
524 }
525 }
526
527 Ok(GenerationStats {
528 spec_name: spec.name.clone(),
529 modules_generated: selected_modules.len(),
530 files_generated: total_files,
531 modules: selected_modules,
532 })
533}
534
535pub async fn run_all_specs(
537 specs: &[SpecEntry],
538 config: &Config,
539 options: &GenerateOptions,
540) -> Result<Vec<GenerationStats>> {
541 let mut stats = Vec::new();
542 for spec in specs {
543 let result = run_single_spec(spec, config, options).await?;
544 stats.push(result);
545 }
546 Ok(stats)
547}
548
549fn collect_ts_files(dir: &std::path::Path, files: &mut Vec<PathBuf>) -> Result<()> {
550 if dir.is_dir() {
551 for entry in
552 std::fs::read_dir(dir).map_err(|e| crate::error::FileSystemError::ReadFileFailed {
553 path: dir.display().to_string(),
554 source: e,
555 })?
556 {
557 let entry = entry.map_err(|e| crate::error::FileSystemError::ReadFileFailed {
558 path: dir.display().to_string(),
559 source: e,
560 })?;
561 let path = entry.path();
562 if path.as_os_str().is_empty() {
564 continue;
565 }
566 if path.is_dir() {
567 collect_ts_files(&path, files)?;
568 } else if path.extension().and_then(|s| s.to_str()) == Some("ts") {
569 files.push(path);
570 }
571 }
572 }
573 Ok(())
574}