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
22pub struct GenerateOptions {
24 pub use_cache: bool,
25 pub use_backup: bool,
26 pub use_force: bool,
27 pub verbose: bool,
28}
29
30pub async fn run_single_spec(
32 spec: &SpecEntry,
33 _config: &Config,
34 options: &GenerateOptions,
35) -> Result<GenerationStats> {
36 let mut progress = ProgressReporter::new(options.verbose);
37 let spec_name = Some(spec.name.as_str());
39
40 progress.start_spinner(&format!("Fetching spec from: {}", spec.path));
41 let parsed = crate::generator::swagger_parser::fetch_and_parse_spec_with_cache_and_name(
42 &spec.path,
43 options.use_cache,
44 Some(&spec.name),
45 )
46 .await?;
47 progress.finish_spinner(&format!(
48 "Parsed spec with {} modules",
49 parsed.modules.len()
50 ));
51
52 let schemas_config = &spec.schemas;
54 let apis_config = &spec.apis;
55 let modules_config = &spec.modules;
56
57 let available_modules: Vec<String> = parsed
59 .modules
60 .iter()
61 .filter(|m| !modules_config.ignore.contains(m))
62 .cloned()
63 .collect();
64
65 if available_modules.is_empty() {
66 return Err(crate::error::GenerationError::NoModulesAvailable.into());
67 }
68
69 let selected_modules = select_modules(&available_modules, &modules_config.ignore)?;
71
72 let (filtered_module_schemas, common_schemas) =
74 filter_common_schemas(&parsed.module_schemas, &selected_modules);
75
76 let schemas_dir = PathBuf::from(&schemas_config.output);
78 let apis_dir = PathBuf::from(&apis_config.output);
79
80 let http_file = apis_dir.join("http.ts");
82 if !http_file.exists() {
83 use crate::generator::writer::write_http_client_template;
84 write_http_client_template(&http_file)?;
85 if options.verbose {
86 progress.success(&format!("Created {}", http_file.display()));
87 }
88 }
89
90 let mut total_files = 0;
91
92 if !common_schemas.is_empty() {
94 progress.start_spinner("Generating common schemas...");
95
96 let mut shared_enum_registry = std::collections::HashMap::new();
98
99 let project_root = std::env::current_dir().ok();
101 let template_engine =
102 crate::templates::engine::TemplateEngine::new(project_root.as_deref())?;
103
104 let common_types = generate_typings_with_registry_and_engine_and_spec(
107 &parsed.openapi,
108 &parsed.schemas,
109 &common_schemas,
110 &mut shared_enum_registry,
111 &[], Some(&template_engine),
113 spec_name,
114 )?;
115
116 let common_zod_schemas = generate_zod_schemas_with_registry_and_engine_and_spec(
119 &parsed.openapi,
120 &parsed.schemas,
121 &common_schemas,
122 &mut shared_enum_registry,
123 &[], Some(&template_engine),
125 spec_name,
126 )?;
127
128 use crate::generator::writer::write_schemas_with_module_mapping;
130 let common_files = write_schemas_with_module_mapping(
131 &schemas_dir,
132 "common",
133 &common_types,
134 &common_zod_schemas,
135 spec_name,
136 options.use_backup,
137 options.use_force,
138 Some(&filtered_module_schemas),
139 &common_schemas,
140 )?;
141 total_files += common_files.len();
142 progress.finish_spinner(&format!(
143 "Generated {} common schema files",
144 common_files.len()
145 ));
146 }
147
148 for module in &selected_modules {
149 progress.start_spinner(&format!("Generating code for module: {}", module));
150
151 let operations = parsed
153 .operations_by_tag
154 .get(module)
155 .cloned()
156 .unwrap_or_default();
157
158 if operations.is_empty() {
159 progress.warning(&format!("No operations found for module: {}", module));
160 continue;
161 }
162
163 let module_schema_names = filtered_module_schemas
165 .get(module)
166 .cloned()
167 .unwrap_or_default();
168
169 let project_root = std::env::current_dir().ok();
171 let template_engine =
172 crate::templates::engine::TemplateEngine::new(project_root.as_deref())?;
173
174 let mut shared_enum_registry = std::collections::HashMap::new();
176
177 let types = if !module_schema_names.is_empty() {
179 generate_typings_with_registry_and_engine_and_spec(
180 &parsed.openapi,
181 &parsed.schemas,
182 &module_schema_names,
183 &mut shared_enum_registry,
184 &common_schemas,
185 Some(&template_engine),
186 spec_name,
187 )?
188 } else {
189 Vec::new()
190 };
191
192 let zod_schemas = if !module_schema_names.is_empty() {
194 generate_zod_schemas_with_registry_and_engine_and_spec(
195 &parsed.openapi,
196 &parsed.schemas,
197 &module_schema_names,
198 &mut shared_enum_registry,
199 &common_schemas,
200 Some(&template_engine),
201 spec_name,
202 )?
203 } else {
204 Vec::new()
205 };
206
207 let api_result = generate_api_client_with_registry_and_engine_and_spec(
209 &parsed.openapi,
210 &operations,
211 module,
212 &common_schemas,
213 &mut shared_enum_registry,
214 Some(&template_engine),
215 spec_name,
216 )?;
217
218 let mut all_types = types;
220 all_types.extend(api_result.response_types);
221
222 use crate::generator::writer::write_schemas_with_module_mapping;
225 let schema_files = write_schemas_with_module_mapping(
226 &schemas_dir,
227 module,
228 &all_types,
229 &zod_schemas,
230 spec_name,
231 options.use_backup,
232 options.use_force,
233 Some(&filtered_module_schemas),
234 &common_schemas,
235 )?;
236 total_files += schema_files.len();
237
238 let api_files = write_api_client_with_options(
240 &apis_dir,
241 module,
242 &api_result.functions,
243 spec_name,
244 options.use_backup,
245 options.use_force,
246 )?;
247 total_files += api_files.len();
248
249 progress.finish_spinner(&format!(
250 "Generated {} files for module: {}",
251 schema_files.len() + api_files.len(),
252 module
253 ));
254 }
255
256 let mut all_generated_files = Vec::new();
258
259 if schemas_dir.exists() {
261 collect_ts_files(&schemas_dir, &mut all_generated_files)?;
262 }
263
264 if apis_dir.exists() {
266 collect_ts_files(&apis_dir, &mut all_generated_files)?;
267 }
268
269 if !all_generated_files.is_empty() {
271 let current_dir =
273 std::env::current_dir().map_err(|e| crate::error::FileSystemError::ReadFileFailed {
274 path: ".".to_string(),
275 source: e,
276 })?;
277
278 let schemas_dir_abs = if schemas_dir.is_absolute() {
280 schemas_dir.clone()
281 } else {
282 current_dir.join(&schemas_dir)
283 };
284 let apis_dir_abs = if apis_dir.is_absolute() {
285 apis_dir.clone()
286 } else {
287 current_dir.join(&apis_dir)
288 };
289
290 let output_base = schemas_dir_abs
291 .parent()
292 .and_then(|p| p.parent())
293 .or_else(|| apis_dir_abs.parent().and_then(|p| p.parent()));
294
295 let formatter = if let Some(base_dir) = output_base {
296 FormatterManager::detect_formatter_from_dir(base_dir)
297 .or_else(FormatterManager::detect_formatter)
298 } else {
299 FormatterManager::detect_formatter()
300 };
301
302 if let Some(formatter) = formatter {
303 progress.start_spinner("Formatting generated files...");
304 let original_dir = std::env::current_dir().map_err(|e| {
305 crate::error::FileSystemError::ReadFileFailed {
306 path: ".".to_string(),
307 source: e,
308 }
309 })?;
310
311 if let Some(output_base) = output_base {
312 if output_base.as_os_str().is_empty() {
314 FormatterManager::format_files(&all_generated_files, formatter)?;
316 } else {
317 std::env::set_current_dir(output_base).map_err(|e| {
318 crate::error::FileSystemError::ReadFileFailed {
319 path: output_base.display().to_string(),
320 source: e,
321 }
322 })?;
323
324 let relative_files: Vec<PathBuf> = all_generated_files
325 .iter()
326 .filter_map(|p| {
327 p.strip_prefix(output_base)
328 .ok()
329 .map(|p| p.to_path_buf())
330 .filter(|p| !p.as_os_str().is_empty())
331 })
332 .collect();
333
334 if !relative_files.is_empty() {
335 let result = FormatterManager::format_files(&relative_files, formatter);
336
337 std::env::set_current_dir(&original_dir).map_err(|e| {
338 crate::error::FileSystemError::ReadFileFailed {
339 path: original_dir.display().to_string(),
340 source: e,
341 }
342 })?;
343
344 result?;
345
346 use crate::generator::writer::batch_update_file_metadata_from_disk;
348 if let Err(e) = batch_update_file_metadata_from_disk(&all_generated_files) {
349 progress.warning(&format!("Failed to update metadata: {}", e));
351 }
352 } else {
353 std::env::set_current_dir(&original_dir).map_err(|e| {
354 crate::error::FileSystemError::ReadFileFailed {
355 path: original_dir.display().to_string(),
356 source: e,
357 }
358 })?;
359 }
360 }
361 } else {
362 FormatterManager::format_files(&all_generated_files, formatter)?;
363
364 use crate::generator::writer::batch_update_file_metadata_from_disk;
366 if let Err(e) = batch_update_file_metadata_from_disk(&all_generated_files) {
367 progress.warning(&format!("Failed to update metadata: {}", e));
369 }
370 }
371 progress.finish_spinner("Files formatted");
372 }
373 }
374
375 Ok(GenerationStats {
376 spec_name: spec.name.clone(),
377 modules_generated: selected_modules.len(),
378 files_generated: total_files,
379 modules: selected_modules,
380 })
381}
382
383pub async fn run_all_specs(
385 specs: &[SpecEntry],
386 config: &Config,
387 options: &GenerateOptions,
388) -> Result<Vec<GenerationStats>> {
389 let mut stats = Vec::new();
390 for spec in specs {
391 let result = run_single_spec(spec, config, options).await?;
392 stats.push(result);
393 }
394 Ok(stats)
395}
396
397fn collect_ts_files(dir: &std::path::Path, files: &mut Vec<PathBuf>) -> Result<()> {
398 if dir.is_dir() {
399 for entry in
400 std::fs::read_dir(dir).map_err(|e| crate::error::FileSystemError::ReadFileFailed {
401 path: dir.display().to_string(),
402 source: e,
403 })?
404 {
405 let entry = entry.map_err(|e| crate::error::FileSystemError::ReadFileFailed {
406 path: dir.display().to_string(),
407 source: e,
408 })?;
409 let path = entry.path();
410 if path.as_os_str().is_empty() {
412 continue;
413 }
414 if path.is_dir() {
415 collect_ts_files(&path, files)?;
416 } else if path.extension().and_then(|s| s.to_str()) == Some("ts") {
417 files.push(path);
418 }
419 }
420 }
421 Ok(())
422}