vika_cli/generator/
api_client.rs

1use crate::error::Result;
2use crate::generator::swagger_parser::resolve_ref;
3use crate::generator::swagger_parser::{
4    get_schema_name_from_ref, resolve_parameter_ref, resolve_request_body_ref,
5    resolve_response_ref, OperationInfo,
6};
7use crate::generator::ts_typings::TypeScriptType;
8use crate::generator::utils::{sanitize_module_name, to_camel_case, to_pascal_case};
9use crate::templates::context::{
10    ApiContext, Parameter as ApiParameter, RequestBody, Response as ApiResponse,
11};
12use crate::templates::engine::TemplateEngine;
13use crate::templates::registry::TemplateId;
14use openapiv3::OpenAPI;
15use openapiv3::{Operation, Parameter, ReferenceOr, SchemaKind, Type};
16
17pub struct ApiFunction {
18    pub content: String,
19}
20
21pub struct ApiGenerationResult {
22    pub functions: Vec<ApiFunction>,
23    pub response_types: Vec<TypeScriptType>,
24}
25
26#[derive(Clone, Debug)]
27pub struct ParameterInfo {
28    pub name: String,
29    pub param_type: ParameterType,
30    pub enum_values: Option<Vec<String>>,
31    pub enum_type_name: Option<String>,
32    pub is_array: bool,
33    pub array_item_type: Option<String>,
34    pub style: Option<String>,
35    pub explode: Option<bool>,
36    pub description: Option<String>,
37}
38
39#[derive(Clone, Debug)]
40pub enum ParameterType {
41    String,
42    Number,
43    Integer,
44    Boolean,
45    Enum(String),  // enum type name
46    Array(String), // array item type
47}
48
49#[derive(Clone, Debug)]
50pub struct ResponseInfo {
51    pub status_code: u16,
52    pub body_type: String,
53    pub description: Option<String>,
54}
55
56#[derive(Clone, Debug)]
57pub struct ErrorResponse {
58    pub status_code: u16,
59    pub body_type: String,
60}
61
62pub fn generate_api_client(
63    openapi: &OpenAPI,
64    operations: &[OperationInfo],
65    module_name: &str,
66    common_schemas: &[String],
67) -> Result<ApiGenerationResult> {
68    generate_api_client_with_registry(
69        openapi,
70        operations,
71        module_name,
72        common_schemas,
73        &mut std::collections::HashMap::new(),
74    )
75}
76
77pub fn generate_api_client_with_registry(
78    openapi: &OpenAPI,
79    operations: &[OperationInfo],
80    module_name: &str,
81    common_schemas: &[String],
82    enum_registry: &mut std::collections::HashMap<String, String>,
83) -> Result<ApiGenerationResult> {
84    generate_api_client_with_registry_and_engine(
85        openapi,
86        operations,
87        module_name,
88        common_schemas,
89        enum_registry,
90        None,
91    )
92}
93
94pub fn generate_api_client_with_registry_and_engine(
95    openapi: &OpenAPI,
96    operations: &[OperationInfo],
97    module_name: &str,
98    common_schemas: &[String],
99    enum_registry: &mut std::collections::HashMap<String, String>,
100    template_engine: Option<&TemplateEngine>,
101) -> Result<ApiGenerationResult> {
102    generate_api_client_with_registry_and_engine_and_spec(
103        openapi,
104        operations,
105        module_name,
106        common_schemas,
107        enum_registry,
108        template_engine,
109        None,
110    )
111}
112
113pub fn generate_api_client_with_registry_and_engine_and_spec(
114    openapi: &OpenAPI,
115    operations: &[OperationInfo],
116    module_name: &str,
117    common_schemas: &[String],
118    enum_registry: &mut std::collections::HashMap<String, String>,
119    template_engine: Option<&TemplateEngine>,
120    spec_name: Option<&str>,
121) -> Result<ApiGenerationResult> {
122    let mut functions = Vec::new();
123    let mut response_types = Vec::new();
124
125    for op_info in operations {
126        let result = generate_function_for_operation(
127            openapi,
128            op_info,
129            module_name,
130            common_schemas,
131            enum_registry,
132            template_engine,
133            spec_name,
134        )?;
135        functions.push(result.function);
136        response_types.extend(result.response_types);
137    }
138
139    Ok(ApiGenerationResult {
140        functions,
141        response_types,
142    })
143}
144
145struct FunctionGenerationResult {
146    function: ApiFunction,
147    response_types: Vec<TypeScriptType>,
148}
149
150fn generate_function_for_operation(
151    openapi: &OpenAPI,
152    op_info: &OperationInfo,
153    module_name: &str,
154    common_schemas: &[String],
155    enum_registry: &mut std::collections::HashMap<String, String>,
156    template_engine: Option<&TemplateEngine>,
157    spec_name: Option<&str>,
158) -> Result<FunctionGenerationResult> {
159    let operation = &op_info.operation;
160    let method = op_info.method.to_lowercase();
161
162    // Generate function name from operation ID or path
163    let func_name = if let Some(operation_id) = &operation.operation_id {
164        to_camel_case(operation_id)
165    } else {
166        generate_function_name_from_path(&op_info.path, &op_info.method)
167    };
168
169    // Extract path parameters
170    let path_params = extract_path_parameters(openapi, operation, enum_registry)?;
171
172    // Extract query parameters
173    let query_params = extract_query_parameters(openapi, operation, enum_registry)?;
174
175    // Extract request body
176    let request_body_info = extract_request_body(openapi, operation)?;
177
178    // Extract all responses (success + error)
179    let all_responses = extract_all_responses(openapi, operation)?;
180
181    // Separate success and error responses
182    let success_responses: Vec<ResponseInfo> = all_responses
183        .iter()
184        .filter(|r| r.status_code >= 200 && r.status_code < 300)
185        .cloned()
186        .collect();
187    let error_responses: Vec<ResponseInfo> = all_responses
188        .iter()
189        .filter(|r| r.status_code < 200 || r.status_code >= 300)
190        .cloned()
191        .collect();
192
193    // Get primary success response type (for backward compatibility)
194    let response_type = success_responses
195        .iter()
196        .find(|r| r.status_code == 200)
197        .map(|r| r.body_type.clone())
198        .unwrap_or_else(|| "any".to_string());
199
200    // Calculate namespace name for qualified type access
201    // Replace slashes with underscore and convert to PascalCase (e.g., "tenant/auth" -> "TenantAuth")
202    let namespace_name = to_pascal_case(&module_name.replace("/", "_"));
203
204    // Build function signature
205    let mut params = Vec::new();
206    let mut path_template = op_info.path.clone();
207    let mut enum_types = Vec::new();
208
209    // Add path parameters
210    for param in &path_params {
211        let param_type = match &param.param_type {
212            ParameterType::Enum(enum_name) => {
213                enum_types.push((
214                    enum_name.clone(),
215                    param.enum_values.clone().unwrap_or_default(),
216                ));
217                enum_name.clone()
218            }
219            ParameterType::String => "string".to_string(),
220            ParameterType::Number => "number".to_string(),
221            ParameterType::Integer => "number".to_string(),
222            ParameterType::Boolean => "boolean".to_string(),
223            ParameterType::Array(_) => "string".to_string(), // Arrays in path are serialized as strings
224        };
225        params.push(format!("{}: {}", param.name, param_type));
226        path_template = path_template.replace(
227            &format!("{{{}}}", param.name),
228            &format!("${{{}}}", param.name),
229        );
230    }
231
232    // Add request body (check if it's in common schemas)
233    if let Some((body_type, _)) = &request_body_info {
234        // Don't qualify "any" type with namespace
235        if body_type == "any" {
236            params.push("body: any".to_string());
237        } else {
238            let qualified_body_type = if common_schemas.contains(body_type) {
239                format!("Common.{}", body_type)
240            } else {
241                format!("{}.{}", namespace_name, body_type)
242            };
243            params.push(format!("body: {}", qualified_body_type));
244        }
245    }
246
247    // Add query parameters (optional) AFTER any required parameters like body,
248    // to satisfy TypeScript's \"required parameter cannot follow an optional parameter\" rule.
249    if !query_params.is_empty() {
250        let mut query_fields = Vec::new();
251        for param in &query_params {
252            let param_type = match &param.param_type {
253                ParameterType::Enum(enum_name) => {
254                    enum_types.push((
255                        enum_name.clone(),
256                        param.enum_values.clone().unwrap_or_default(),
257                    ));
258                    enum_name.clone()
259                }
260                ParameterType::Array(item_type) => {
261                    format!("{}[]", item_type)
262                }
263                ParameterType::String => "string".to_string(),
264                ParameterType::Number => "number".to_string(),
265                ParameterType::Integer => "number".to_string(),
266                ParameterType::Boolean => "boolean".to_string(),
267            };
268            query_fields.push(format!("{}?: {}", param.name, param_type));
269        }
270        let query_type = format!("{{ {} }}", query_fields.join(", "));
271        params.push(format!("query?: {}", query_type));
272    }
273
274    let params_str = params.join(", ");
275
276    // Build function body
277    let mut body_lines = Vec::new();
278
279    // Build URL with path parameters
280    let mut url_template = op_info.path.clone();
281    for param in &path_params {
282        url_template = url_template.replace(
283            &format!("{{{}}}", param.name),
284            &format!("${{{}}}", param.name),
285        );
286    }
287
288    // Build URL with query parameters
289    if !query_params.is_empty() {
290        body_lines.push("    const queryString = new URLSearchParams();".to_string());
291        for param in &query_params {
292            if param.is_array {
293                let explode = param.explode.unwrap_or(true);
294                if explode {
295                    // explode: true -> tags=one&tags=two
296                    body_lines.push(format!("    if (query?.{}) {{", param.name));
297                    body_lines.push(format!(
298                        "      query.{}.forEach((item) => queryString.append(\"{}\", String(item)));",
299                        param.name, param.name
300                    ));
301                    body_lines.push("    }".to_string());
302                } else {
303                    // explode: false -> tags=one,two
304                    body_lines.push(format!(
305                        "    if (query?.{}) queryString.append(\"{}\", query.{}.join(\",\"));",
306                        param.name, param.name, param.name
307                    ));
308                }
309            } else {
310                body_lines.push(format!(
311                    "    if (query?.{}) queryString.append(\"{}\", String(query.{}));",
312                    param.name, param.name, param.name
313                ));
314            }
315        }
316        body_lines.push("    const queryStr = queryString.toString();".to_string());
317        body_lines.push(format!(
318            "    const url = `{}` + (queryStr ? `?${{queryStr}}` : '');",
319            url_template
320        ));
321    } else {
322        body_lines.push(format!("    const url = `{}`;", url_template));
323    }
324
325    // Build HTTP call
326    let http_method = match method.to_uppercase().as_str() {
327        "GET" => "get",
328        "POST" => "post",
329        "PUT" => "put",
330        "DELETE" => "delete",
331        "PATCH" => "patch",
332        "HEAD" => "head",
333        "OPTIONS" => "options",
334        _ => "get",
335    };
336
337    // Use qualified type for generic parameter (check if it's common or module-specific)
338    let qualified_response_type_for_generic = if response_type != "any" {
339        let is_common = common_schemas.contains(&response_type);
340        if is_common {
341            format!("Common.{}", response_type)
342        } else {
343            format!("{}.{}", namespace_name, response_type)
344        }
345    } else {
346        response_type.clone()
347    };
348
349    if let Some((_body_type, _)) = &request_body_info {
350        body_lines.push(format!("    return http.{}(url, body);", http_method));
351    } else {
352        body_lines.push(format!(
353            "    return http.{}<{}>(url);",
354            http_method, qualified_response_type_for_generic
355        ));
356    }
357
358    // HTTP client is at {output_dir}/http.ts
359    // Note: output_dir already includes spec_name if needed (from config)
360    // So the file structure is: {output_dir}/{module}/index.ts and {output_dir}/http.ts
361    // For example: src/apis/ecommerce/addresses/index.ts and src/apis/ecommerce/http.ts
362    // From addresses/index.ts: go up 1 level to ecommerce/, then http.ts is at the same level
363    // So depth = 1 (just the module directory)
364    let module_depth = module_name.matches('/').count() + 1; // +1 for the module directory itself
365    let http_relative_path = format!("{}http", "../".repeat(module_depth));
366    let http_import = http_relative_path;
367
368    // Determine if response type is in common schemas or module-specific
369    // We still need schema imports for request/response body types
370    let mut type_imports = String::new();
371    let mut needs_common_import = false;
372    let mut needs_namespace_import = false;
373
374    // Check if response type needs import
375    if response_type != "any" {
376        let is_common = common_schemas.contains(&response_type);
377        if is_common {
378            needs_common_import = true;
379        } else {
380            needs_namespace_import = true;
381        }
382    }
383
384    // Check if request body type needs import
385    if let Some((body_type, _)) = &request_body_info {
386        if body_type != "any" {
387            if common_schemas.contains(body_type) {
388                needs_common_import = true;
389            } else {
390                needs_namespace_import = true;
391            }
392        }
393    }
394
395    // Add imports
396    // File is at: {output_dir}/{module}/index.ts
397    // output_dir already includes spec_name (e.g., src/apis/tenant)
398    // To get to schemas: go up from module to output_dir, then up to apis/, then up to src/, then into schemas/
399    // From auth/index.ts: go up 1 to tenant/, up 1 more to apis/, up 1 more to src/, then into schemas/
400    // So depth = module_depth (1) + 1 (to apis/) + 1 (to src/) = module_depth + 2
401    if needs_common_import {
402        let schemas_depth = module_depth + 2; // +1 to apis/, +1 to src/
403        let common_import = if let Some(spec) = spec_name {
404            format!(
405                "{}schemas/{}/common",
406                "../".repeat(schemas_depth),
407                sanitize_module_name(spec)
408            )
409        } else {
410            format!("{}schemas/common", "../".repeat(schemas_depth))
411        };
412        type_imports.push_str(&format!("import * as Common from \"{}\";\n", common_import));
413    }
414    if needs_namespace_import {
415        let schemas_depth = module_depth + 2; // +1 to apis/, +1 to src/
416        let sanitized_module_name = sanitize_module_name(module_name);
417        let schemas_import = if let Some(spec) = spec_name {
418            format!(
419                "{}schemas/{}/{}",
420                "../".repeat(schemas_depth),
421                sanitize_module_name(spec),
422                sanitized_module_name
423            )
424        } else {
425            format!(
426                "{}schemas/{}",
427                "../".repeat(schemas_depth),
428                sanitized_module_name
429            )
430        };
431        type_imports.push_str(&format!(
432            "import * as {} from \"{}\";\n",
433            namespace_name, schemas_import
434        ));
435    }
436
437    // Generate response types (Errors, Error union, Responses)
438    let response_types = generate_response_types(
439        &func_name,
440        &success_responses,
441        &error_responses,
442        &namespace_name,
443        common_schemas,
444        &enum_types,
445    );
446
447    // Add imports for response types if we have any
448    let type_name_base = to_pascal_case(&func_name);
449    let mut response_type_imports = Vec::new();
450
451    // Only add error types if we have errors with schemas
452    let errors_with_schemas: Vec<&ResponseInfo> = error_responses
453        .iter()
454        .filter(|r| r.status_code > 0)
455        .collect();
456    if !errors_with_schemas.is_empty() {
457        response_type_imports.push(format!("{}Errors", type_name_base));
458        response_type_imports.push(format!("{}Error", type_name_base));
459    }
460
461    // Only add Responses type if we have success responses with schemas
462    let success_with_schemas: Vec<&ResponseInfo> = success_responses
463        .iter()
464        .filter(|r| r.status_code >= 200 && r.status_code < 300 && r.body_type != "any")
465        .collect();
466    if !success_with_schemas.is_empty() {
467        response_type_imports.push(format!("{}Responses", type_name_base));
468    }
469
470    // Add type import if we have response types (separate line)
471    if !response_type_imports.is_empty() {
472        // Calculate relative path based on module depth
473        // module_depth already includes +1 for the module directory itself
474        // +1 to apis/, +1 to src/ = module_depth + 2
475        let schemas_depth = module_depth + 2;
476        let sanitized_module_name = sanitize_module_name(module_name);
477        let schemas_import = if let Some(spec) = spec_name {
478            format!(
479                "{}schemas/{}/{}",
480                "../".repeat(schemas_depth),
481                sanitize_module_name(spec),
482                sanitized_module_name
483            )
484        } else {
485            format!(
486                "{}schemas/{}",
487                "../".repeat(schemas_depth),
488                sanitized_module_name
489            )
490        };
491        let type_import_line = format!(
492            "import type {{ {} }} from \"{}\";",
493            response_type_imports.join(", "),
494            schemas_import
495        );
496        if type_imports.is_empty() {
497            type_imports = format!("{}\n", type_import_line);
498        } else {
499            type_imports = format!("{}\n{}", type_imports.trim_end(), type_import_line);
500        }
501    }
502
503    // Add enum type imports if we have any
504    if !enum_types.is_empty() {
505        // module_depth already includes +1 for the module directory itself
506        // +1 to apis/, +1 to src/ = module_depth + 2
507        let schemas_depth = module_depth + 2;
508        let sanitized_module_name = sanitize_module_name(module_name);
509        let schemas_import = if let Some(spec) = spec_name {
510            format!(
511                "{}schemas/{}/{}",
512                "../".repeat(schemas_depth),
513                sanitize_module_name(spec),
514                sanitized_module_name
515            )
516        } else {
517            format!(
518                "{}schemas/{}",
519                "../".repeat(schemas_depth),
520                sanitized_module_name
521            )
522        };
523        let enum_names: Vec<String> = enum_types.iter().map(|(name, _)| name.clone()).collect();
524        let enum_import_line = format!(
525            "import type {{ {} }} from \"{}\";",
526            enum_names.join(", "),
527            schemas_import
528        );
529        if type_imports.is_empty() {
530            type_imports = format!("{}\n", enum_import_line);
531        } else {
532            type_imports = format!("{}\n{}", type_imports.trim_end(), enum_import_line);
533        }
534    }
535
536    // Ensure type_imports ends with newline for proper separation
537    if !type_imports.is_empty() && !type_imports.ends_with('\n') {
538        type_imports.push('\n');
539    }
540
541    // Determine return type - use Responses type if available, otherwise fallback to direct type
542    let has_responses_type = response_type_imports
543        .iter()
544        .any(|imp| imp.contains("Responses"));
545    let return_type = if has_responses_type {
546        // Use Responses type with primary status code
547        if let Some(_primary_response) = success_responses
548            .iter()
549            .find(|r| r.status_code == 200 && r.body_type != "any")
550        {
551            format!(": Promise<{}Responses[200]>", type_name_base)
552        } else if let Some(first_success) = success_responses
553            .iter()
554            .find(|r| r.status_code >= 200 && r.status_code < 300 && r.body_type != "any")
555        {
556            format!(
557                ": Promise<{}Responses[{}]>",
558                type_name_base, first_success.status_code
559            )
560        } else {
561            String::new()
562        }
563    } else if !success_responses.is_empty() {
564        // Fallback to direct type if no Responses type generated
565        if let Some(primary_response) = success_responses.iter().find(|r| r.status_code == 200) {
566            if primary_response.body_type != "any" {
567                let qualified = if common_schemas.contains(&primary_response.body_type) {
568                    format!("Common.{}", primary_response.body_type)
569                } else {
570                    format!("{}.{}", namespace_name, primary_response.body_type)
571                };
572                format!(": Promise<{}>", qualified)
573            } else {
574                String::new()
575            }
576        } else {
577            String::new()
578        }
579    } else {
580        String::new()
581    };
582
583    let function_body = body_lines.join("\n");
584
585    // Build API context for template
586    let api_path_params: Vec<ApiParameter> = path_params
587        .iter()
588        .map(|p| {
589            let param_type = match &p.param_type {
590                ParameterType::Enum(enum_name) => enum_name.clone(),
591                ParameterType::Array(item_type) => format!("{}[]", item_type),
592                ParameterType::String => "string".to_string(),
593                ParameterType::Number => "number".to_string(),
594                ParameterType::Integer => "number".to_string(),
595                ParameterType::Boolean => "boolean".to_string(),
596            };
597            ApiParameter::new(p.name.clone(), param_type, false, p.description.clone())
598        })
599        .collect();
600
601    let api_query_params: Vec<ApiParameter> = query_params
602        .iter()
603        .map(|p| {
604            let param_type = match &p.param_type {
605                ParameterType::Enum(enum_name) => enum_name.clone(),
606                ParameterType::Array(item_type) => format!("{}[]", item_type),
607                ParameterType::String => "string".to_string(),
608                ParameterType::Number => "number".to_string(),
609                ParameterType::Integer => "number".to_string(),
610                ParameterType::Boolean => "boolean".to_string(),
611            };
612            ApiParameter::new(p.name.clone(), param_type, true, p.description.clone())
613        })
614        .collect();
615
616    let api_request_body = request_body_info
617        .as_ref()
618        .map(|(rb_type, rb_desc)| RequestBody::new(rb_type.clone(), rb_desc.clone()));
619    let api_responses: Vec<ApiResponse> = all_responses
620        .iter()
621        .map(|r| ApiResponse::new(r.status_code, r.body_type.clone()))
622        .collect();
623
624    // Generate content using template or fallback to string formatting
625    // Use description if available and non-empty, otherwise fall back to summary
626    // Note: Both description and summary are Option<String> in the OpenAPI crate
627    let operation_description = operation
628        .description
629        .clone()
630        .or_else(|| operation.summary.clone())
631        .filter(|s| !s.is_empty())
632        .unwrap_or_default();
633
634    let content = if let Some(engine) = template_engine {
635        let context = ApiContext::new(
636            func_name.clone(),
637            operation.operation_id.clone(),
638            method.clone(),
639            op_info.path.clone(),
640            api_path_params,
641            api_query_params,
642            api_request_body,
643            api_responses,
644            type_imports.clone(),
645            http_import.to_string(),
646            return_type.clone(),
647            function_body.clone(),
648            module_name.to_string(),
649            params_str.clone(),
650            operation_description.clone(),
651            spec_name.map(|s| s.to_string()),
652        );
653
654        engine.render(TemplateId::ApiClientFetch, &context)?
655    } else {
656        // Fallback to string formatting
657        let jsdoc = if !operation_description.is_empty() {
658            format!("/**\n * {}\n */\n", operation_description)
659        } else {
660            String::new()
661        };
662        if params_str.is_empty() {
663            format!(
664                "import {{ http }} from \"{}\";\n{}{}{}export const {} = async (){} => {{\n{}\n}};",
665                http_import,
666                type_imports,
667                if !type_imports.is_empty() { "\n" } else { "" },
668                jsdoc,
669                func_name,
670                return_type,
671                function_body
672            )
673        } else {
674            format!(
675                "import {{ http }} from \"{}\";\n{}{}{}export const {} = async ({}){} => {{\n{}\n}};",
676                http_import,
677                type_imports,
678                if !type_imports.is_empty() { "\n" } else { "" },
679                jsdoc,
680                func_name,
681                params_str,
682                return_type,
683                function_body
684            )
685        }
686    };
687
688    Ok(FunctionGenerationResult {
689        function: ApiFunction { content },
690        response_types,
691    })
692}
693
694fn extract_path_parameters(
695    openapi: &OpenAPI,
696    operation: &Operation,
697    enum_registry: &mut std::collections::HashMap<String, String>,
698) -> Result<Vec<ParameterInfo>> {
699    let mut params = Vec::new();
700
701    for param_ref in &operation.parameters {
702        match param_ref {
703            ReferenceOr::Reference { reference } => {
704                // Resolve parameter reference (with support for nested references up to 3 levels)
705                let mut current_ref = Some(reference.clone());
706                let mut depth = 0;
707                while let Some(ref_path) = current_ref.take() {
708                    if depth > 3 {
709                        break; // Prevent infinite loops
710                    }
711                    match resolve_parameter_ref(openapi, &ref_path) {
712                        Ok(ReferenceOr::Item(param)) => {
713                            if let Parameter::Path { parameter_data, .. } = param {
714                                if let Some(param_info) =
715                                    extract_parameter_info(openapi, &parameter_data, enum_registry)?
716                                {
717                                    params.push(param_info);
718                                }
719                            }
720                            break;
721                        }
722                        Ok(ReferenceOr::Reference {
723                            reference: nested_ref,
724                        }) => {
725                            current_ref = Some(nested_ref);
726                            depth += 1;
727                        }
728                        Err(_) => {
729                            // Reference resolution failed - skip
730                            break;
731                        }
732                    }
733                }
734            }
735            ReferenceOr::Item(param) => {
736                if let Parameter::Path { parameter_data, .. } = param {
737                    if let Some(param_info) =
738                        extract_parameter_info(openapi, parameter_data, enum_registry)?
739                    {
740                        params.push(param_info);
741                    }
742                }
743            }
744        }
745    }
746
747    Ok(params)
748}
749
750fn extract_query_parameters(
751    openapi: &OpenAPI,
752    operation: &Operation,
753    enum_registry: &mut std::collections::HashMap<String, String>,
754) -> Result<Vec<ParameterInfo>> {
755    let mut params = Vec::new();
756
757    for param_ref in &operation.parameters {
758        match param_ref {
759            ReferenceOr::Reference { reference } => {
760                // Resolve parameter reference (with support for nested references up to 3 levels)
761                let mut current_ref = Some(reference.clone());
762                let mut depth = 0;
763                while let Some(ref_path) = current_ref.take() {
764                    if depth > 3 {
765                        break; // Prevent infinite loops
766                    }
767                    match resolve_parameter_ref(openapi, &ref_path) {
768                        Ok(ReferenceOr::Item(param)) => {
769                            if let Parameter::Query {
770                                parameter_data,
771                                style,
772                                ..
773                            } = param
774                            {
775                                if let Some(mut param_info) =
776                                    extract_parameter_info(openapi, &parameter_data, enum_registry)?
777                                {
778                                    // Override style and explode for query parameters
779                                    param_info.style = Some(format!("{:?}", style));
780                                    // explode defaults to true for arrays, false otherwise
781                                    param_info.explode =
782                                        Some(parameter_data.explode.unwrap_or(false));
783                                    params.push(param_info);
784                                }
785                            }
786                            break;
787                        }
788                        Ok(ReferenceOr::Reference {
789                            reference: nested_ref,
790                        }) => {
791                            current_ref = Some(nested_ref);
792                            depth += 1;
793                        }
794                        Err(_) => {
795                            // Reference resolution failed - skip
796                            break;
797                        }
798                    }
799                }
800            }
801            ReferenceOr::Item(param) => {
802                if let Parameter::Query {
803                    parameter_data,
804                    style,
805                    ..
806                } = param
807                {
808                    if let Some(mut param_info) =
809                        extract_parameter_info(openapi, parameter_data, enum_registry)?
810                    {
811                        // Override style and explode for query parameters
812                        param_info.style = Some(format!("{:?}", style));
813                        // explode defaults to true for arrays, false otherwise
814                        param_info.explode = Some(parameter_data.explode.unwrap_or(false));
815                        params.push(param_info);
816                    }
817                }
818            }
819        }
820    }
821
822    Ok(params)
823}
824
825fn extract_parameter_info(
826    openapi: &OpenAPI,
827    parameter_data: &openapiv3::ParameterData,
828    enum_registry: &mut std::collections::HashMap<String, String>,
829) -> Result<Option<ParameterInfo>> {
830    let name = parameter_data.name.clone();
831    let description = parameter_data.description.clone();
832
833    // Get schema from parameter
834    let schema = match &parameter_data.format {
835        openapiv3::ParameterSchemaOrContent::Schema(schema_ref) => match schema_ref {
836            ReferenceOr::Reference { reference } => {
837                resolve_ref(openapi, reference).ok().and_then(|r| match r {
838                    ReferenceOr::Item(s) => Some(s),
839                    _ => None,
840                })
841            }
842            ReferenceOr::Item(s) => Some(s.clone()),
843        },
844        _ => None,
845    };
846
847    if let Some(schema) = schema {
848        match &schema.schema_kind {
849            SchemaKind::Type(type_) => {
850                match type_ {
851                    Type::String(string_type) => {
852                        // Check for enum
853                        if !string_type.enumeration.is_empty() {
854                            let mut enum_values: Vec<String> = string_type
855                                .enumeration
856                                .iter()
857                                .filter_map(|v| v.as_ref().cloned())
858                                .collect();
859                            enum_values.sort();
860                            let enum_key = enum_values.join(",");
861
862                            // Generate enum name
863                            let enum_name = format!("{}Enum", to_pascal_case(&name));
864                            let context_key = format!("{}:{}", enum_key, name);
865
866                            // Check registry
867                            let final_enum_name = if let Some(existing) = enum_registry
868                                .get(&context_key)
869                                .or_else(|| enum_registry.get(&enum_key))
870                            {
871                                existing.clone()
872                            } else {
873                                enum_registry.insert(context_key.clone(), enum_name.clone());
874                                enum_registry.insert(enum_key.clone(), enum_name.clone());
875                                enum_name
876                            };
877
878                            Ok(Some(ParameterInfo {
879                                name,
880                                param_type: ParameterType::Enum(final_enum_name.clone()),
881                                enum_values: Some(enum_values),
882                                enum_type_name: Some(final_enum_name),
883                                is_array: false,
884                                array_item_type: None,
885                                style: Some("simple".to_string()), // default for path
886                                explode: Some(false),              // default for path
887                                description: description.clone(),
888                            }))
889                        } else {
890                            Ok(Some(ParameterInfo {
891                                name,
892                                param_type: ParameterType::String,
893                                enum_values: None,
894                                enum_type_name: None,
895                                is_array: false,
896                                array_item_type: None,
897                                style: Some("simple".to_string()),
898                                explode: Some(false),
899                                description: description.clone(),
900                            }))
901                        }
902                    }
903                    Type::Number(_) => Ok(Some(ParameterInfo {
904                        name,
905                        param_type: ParameterType::Number,
906                        enum_values: None,
907                        enum_type_name: None,
908                        is_array: false,
909                        array_item_type: None,
910                        style: Some("simple".to_string()),
911                        explode: Some(false),
912                        description: description.clone(),
913                    })),
914                    Type::Integer(_) => Ok(Some(ParameterInfo {
915                        name,
916                        param_type: ParameterType::Integer,
917                        enum_values: None,
918                        enum_type_name: None,
919                        is_array: false,
920                        array_item_type: None,
921                        style: Some("simple".to_string()),
922                        explode: Some(false),
923                        description: description.clone(),
924                    })),
925                    Type::Boolean(_) => Ok(Some(ParameterInfo {
926                        name,
927                        param_type: ParameterType::Boolean,
928                        enum_values: None,
929                        enum_type_name: None,
930                        is_array: false,
931                        array_item_type: None,
932                        style: Some("simple".to_string()),
933                        explode: Some(false),
934                        description: description.clone(),
935                    })),
936                    Type::Object(_) => Ok(Some(ParameterInfo {
937                        name,
938                        param_type: ParameterType::String,
939                        enum_values: None,
940                        enum_type_name: None,
941                        is_array: false,
942                        array_item_type: None,
943                        style: Some("simple".to_string()),
944                        explode: Some(false),
945                        description: description.clone(),
946                    })),
947                    Type::Array(array) => {
948                        let item_type = if let Some(items) = &array.items {
949                            match items {
950                                ReferenceOr::Reference { reference } => {
951                                    if let Some(ref_name) = get_schema_name_from_ref(reference) {
952                                        to_pascal_case(&ref_name)
953                                    } else {
954                                        "string".to_string()
955                                    }
956                                }
957                                ReferenceOr::Item(item_schema) => {
958                                    // Extract type from item schema
959                                    match &item_schema.schema_kind {
960                                        SchemaKind::Type(item_type) => match item_type {
961                                            Type::String(_) => "string".to_string(),
962                                            Type::Number(_) => "number".to_string(),
963                                            Type::Integer(_) => "number".to_string(),
964                                            Type::Boolean(_) => "boolean".to_string(),
965                                            _ => "string".to_string(),
966                                        },
967                                        _ => "string".to_string(),
968                                    }
969                                }
970                            }
971                        } else {
972                            "string".to_string()
973                        };
974
975                        Ok(Some(ParameterInfo {
976                            name,
977                            param_type: ParameterType::Array(item_type.clone()),
978                            enum_values: None,
979                            enum_type_name: None,
980                            is_array: true,
981                            array_item_type: Some(item_type),
982                            style: Some("form".to_string()), // default for query arrays
983                            explode: Some(true),             // default for query arrays
984                            description: description.clone(),
985                        }))
986                    }
987                }
988            }
989            _ => Ok(Some(ParameterInfo {
990                name,
991                param_type: ParameterType::String,
992                enum_values: None,
993                enum_type_name: None,
994                is_array: false,
995                array_item_type: None,
996                style: Some("simple".to_string()),
997                explode: Some(false),
998                description: description.clone(),
999            })),
1000        }
1001    } else {
1002        // No schema, default to string
1003        Ok(Some(ParameterInfo {
1004            name,
1005            param_type: ParameterType::String,
1006            enum_values: None,
1007            enum_type_name: None,
1008            is_array: false,
1009            array_item_type: None,
1010            style: Some("simple".to_string()),
1011            explode: Some(false),
1012            description: description.clone(),
1013        }))
1014    }
1015}
1016
1017fn extract_request_body(
1018    openapi: &OpenAPI,
1019    operation: &Operation,
1020) -> Result<Option<(String, Option<String>)>> {
1021    if let Some(request_body) = &operation.request_body {
1022        match request_body {
1023            ReferenceOr::Reference { reference } => {
1024                // Resolve request body reference
1025                match resolve_request_body_ref(openapi, reference) {
1026                    Ok(ReferenceOr::Item(body)) => {
1027                        let description = body.description.clone();
1028                        if let Some(json_media) = body.content.get("application/json") {
1029                            if let Some(schema_ref) = &json_media.schema {
1030                                match schema_ref {
1031                                    ReferenceOr::Reference { reference } => {
1032                                        if let Some(ref_name) = get_schema_name_from_ref(reference)
1033                                        {
1034                                            Ok(Some((to_pascal_case(&ref_name), description)))
1035                                        } else {
1036                                            Ok(Some(("any".to_string(), description)))
1037                                        }
1038                                    }
1039                                    ReferenceOr::Item(_schema) => {
1040                                        // Inline schemas: These are schema definitions embedded directly
1041                                        // in the request body. Generating proper types would require
1042                                        // recursive type generation at this point, which is complex.
1043                                        // For now, we use 'any' as a fallback. This can be enhanced
1044                                        // to generate inline types if needed.
1045                                        Ok(Some(("any".to_string(), description)))
1046                                    }
1047                                }
1048                            } else {
1049                                Ok(Some(("any".to_string(), description)))
1050                            }
1051                        } else {
1052                            Ok(Some(("any".to_string(), description)))
1053                        }
1054                    }
1055                    Ok(ReferenceOr::Reference { .. }) => {
1056                        // Nested reference - return any
1057                        Ok(Some(("any".to_string(), None)))
1058                    }
1059                    Err(_) => {
1060                        // Reference resolution failed - return any
1061                        Ok(Some(("any".to_string(), None)))
1062                    }
1063                }
1064            }
1065            ReferenceOr::Item(body) => {
1066                let description = body.description.clone();
1067                if let Some(json_media) = body.content.get("application/json") {
1068                    if let Some(schema_ref) = &json_media.schema {
1069                        match schema_ref {
1070                            ReferenceOr::Reference { reference } => {
1071                                if let Some(ref_name) = get_schema_name_from_ref(reference) {
1072                                    Ok(Some((to_pascal_case(&ref_name), description)))
1073                                } else {
1074                                    Ok(Some(("any".to_string(), description)))
1075                                }
1076                            }
1077                            ReferenceOr::Item(_schema) => {
1078                                // Inline schemas: These are schema definitions embedded directly
1079                                // in the request body. Generating proper types would require
1080                                // recursive type generation at this point, which is complex.
1081                                // For now, we use 'any' as a fallback. This can be enhanced
1082                                // to generate inline types if needed.
1083                                Ok(Some(("any".to_string(), description)))
1084                            }
1085                        }
1086                    } else {
1087                        Ok(Some(("any".to_string(), description)))
1088                    }
1089                } else {
1090                    Ok(Some(("any".to_string(), description)))
1091                }
1092            }
1093        }
1094    } else {
1095        Ok(None)
1096    }
1097}
1098
1099#[allow(dead_code)]
1100fn extract_response_type(openapi: &OpenAPI, operation: &Operation) -> Result<String> {
1101    // Try to get 200 response
1102    if let Some(success_response) = operation
1103        .responses
1104        .responses
1105        .get(&openapiv3::StatusCode::Code(200))
1106    {
1107        match success_response {
1108            ReferenceOr::Reference { reference } => {
1109                // Resolve response reference
1110                match resolve_response_ref(openapi, reference) {
1111                    Ok(ReferenceOr::Item(response)) => {
1112                        if let Some(json_media) = response.content.get("application/json") {
1113                            if let Some(schema_ref) = &json_media.schema {
1114                                match schema_ref {
1115                                    ReferenceOr::Reference { reference } => {
1116                                        if let Some(ref_name) = get_schema_name_from_ref(reference)
1117                                        {
1118                                            Ok(to_pascal_case(&ref_name))
1119                                        } else {
1120                                            Ok("any".to_string())
1121                                        }
1122                                    }
1123                                    ReferenceOr::Item(_) => Ok("any".to_string()),
1124                                }
1125                            } else {
1126                                Ok("any".to_string())
1127                            }
1128                        } else {
1129                            Ok("any".to_string())
1130                        }
1131                    }
1132                    Ok(ReferenceOr::Reference { .. }) => {
1133                        // Nested reference - return any
1134                        Ok("any".to_string())
1135                    }
1136                    Err(_) => {
1137                        // Reference resolution failed - return any
1138                        Ok("any".to_string())
1139                    }
1140                }
1141            }
1142            ReferenceOr::Item(response) => {
1143                if let Some(json_media) = response.content.get("application/json") {
1144                    if let Some(schema_ref) = &json_media.schema {
1145                        match schema_ref {
1146                            ReferenceOr::Reference { reference } => {
1147                                if let Some(ref_name) = get_schema_name_from_ref(reference) {
1148                                    Ok(to_pascal_case(&ref_name))
1149                                } else {
1150                                    Ok("any".to_string())
1151                                }
1152                            }
1153                            ReferenceOr::Item(_) => Ok("any".to_string()),
1154                        }
1155                    } else {
1156                        Ok("any".to_string())
1157                    }
1158                } else {
1159                    Ok("any".to_string())
1160                }
1161            }
1162        }
1163    } else {
1164        Ok("any".to_string())
1165    }
1166}
1167
1168fn extract_all_responses(openapi: &OpenAPI, operation: &Operation) -> Result<Vec<ResponseInfo>> {
1169    let mut responses = Vec::new();
1170
1171    for (status_code, response_ref) in &operation.responses.responses {
1172        let status_num = match status_code {
1173            openapiv3::StatusCode::Code(code) => *code,
1174            openapiv3::StatusCode::Range(range) => {
1175                // For ranges like 4xx, 5xx, extract the range value
1176                // Range is an enum, check its variant
1177                match format!("{:?}", range).as_str() {
1178                    s if s.contains("4") => 400,
1179                    s if s.contains("5") => 500,
1180                    _ => 0,
1181                }
1182            }
1183        };
1184
1185        // Extract response info (description and body type)
1186        let (description, body_type) = match response_ref {
1187            ReferenceOr::Reference { reference } => {
1188                match resolve_response_ref(openapi, reference) {
1189                    Ok(ReferenceOr::Item(response)) => {
1190                        let desc = response.description.clone();
1191                        let body = extract_response_body_type(openapi, &response);
1192                        (Some(desc), body)
1193                    }
1194                    _ => (None, "any".to_string()),
1195                }
1196            }
1197            ReferenceOr::Item(response) => {
1198                let desc = response.description.clone();
1199                let body = extract_response_body_type(openapi, response);
1200                (Some(desc), body)
1201            }
1202        };
1203
1204        responses.push(ResponseInfo {
1205            status_code: status_num,
1206            body_type,
1207            description,
1208        });
1209    }
1210
1211    Ok(responses)
1212}
1213
1214#[allow(dead_code)]
1215fn extract_error_responses(openapi: &OpenAPI, operation: &Operation) -> Result<Vec<ErrorResponse>> {
1216    let all_responses = extract_all_responses(openapi, operation)?;
1217    let errors: Vec<ErrorResponse> = all_responses
1218        .iter()
1219        .filter(|r| r.status_code < 200 || r.status_code >= 300)
1220        .map(|r| ErrorResponse {
1221            status_code: r.status_code,
1222            body_type: r.body_type.clone(),
1223        })
1224        .collect();
1225    Ok(errors)
1226}
1227
1228fn generate_response_types(
1229    func_name: &str,
1230    success_responses: &[ResponseInfo],
1231    error_responses: &[ResponseInfo],
1232    _namespace_name: &str,
1233    common_schemas: &[String],
1234    enum_types: &[(String, Vec<String>)],
1235) -> Vec<TypeScriptType> {
1236    let mut types = Vec::new();
1237    let type_name_base = to_pascal_case(func_name);
1238
1239    // Generate enum types for parameters
1240    for (enum_name, enum_values) in enum_types {
1241        let variants = enum_values
1242            .iter()
1243            .map(|v| format!("\"{}\"", v))
1244            .collect::<Vec<_>>()
1245            .join(" |\n");
1246        let enum_type = format!("export type {} =\n{};", enum_name, variants);
1247        types.push(TypeScriptType { content: enum_type });
1248    }
1249
1250    // Generate Errors type
1251    if !error_responses.is_empty() {
1252        let mut error_fields = Vec::new();
1253        for error in error_responses {
1254            if error.status_code > 0 {
1255                // For types in the same file (not common), use unqualified name
1256                // For common types, use Common.TypeName
1257                let qualified_type = if error.body_type != "any" {
1258                    if common_schemas.contains(&error.body_type) {
1259                        format!("Common.{}", error.body_type)
1260                    } else {
1261                        // Type is in the same file, use unqualified name
1262                        error.body_type.clone()
1263                    }
1264                } else {
1265                    "any".to_string()
1266                };
1267
1268                let description = error
1269                    .description
1270                    .as_ref()
1271                    .map(|d| format!("    /**\n     * {}\n     */", d))
1272                    .unwrap_or_default();
1273
1274                error_fields.push(format!(
1275                    "{}\n    {}: {};",
1276                    description, error.status_code, qualified_type
1277                ));
1278            }
1279        }
1280
1281        if !error_fields.is_empty() {
1282            let errors_type = format!(
1283                "export type {}Errors = {{\n{}\n}};",
1284                type_name_base,
1285                error_fields.join("\n")
1286            );
1287            types.push(TypeScriptType {
1288                content: errors_type,
1289            });
1290
1291            // Generate Error union type
1292            let error_union_type = format!(
1293                "export type {}Error = {}Errors[keyof {}Errors];",
1294                type_name_base, type_name_base, type_name_base
1295            );
1296            types.push(TypeScriptType {
1297                content: error_union_type,
1298            });
1299        }
1300    }
1301
1302    // Generate Responses type (only if we have success responses with schemas)
1303    let success_with_schemas: Vec<&ResponseInfo> = success_responses
1304        .iter()
1305        .filter(|r| r.status_code >= 200 && r.status_code < 300 && r.body_type != "any")
1306        .collect();
1307
1308    if !success_with_schemas.is_empty() {
1309        let mut response_fields = Vec::new();
1310        for response in success_with_schemas {
1311            // For types in the same file (not common), use unqualified name
1312            // For common types, use Common.TypeName
1313            let qualified_type = if common_schemas.contains(&response.body_type) {
1314                format!("Common.{}", response.body_type)
1315            } else {
1316                // Type is in the same file, use unqualified name
1317                response.body_type.clone()
1318            };
1319
1320            let description = response
1321                .description
1322                .as_ref()
1323                .map(|d| format!("    /**\n     * {}\n     */", d))
1324                .unwrap_or_default();
1325
1326            response_fields.push(format!(
1327                "{}\n    {}: {};",
1328                description, response.status_code, qualified_type
1329            ));
1330        }
1331
1332        if !response_fields.is_empty() {
1333            let responses_type = format!(
1334                "export type {}Responses = {{\n{}\n}};",
1335                type_name_base,
1336                response_fields.join("\n")
1337            );
1338            types.push(TypeScriptType {
1339                content: responses_type,
1340            });
1341        }
1342    }
1343
1344    types
1345}
1346
1347fn extract_response_body_type(_openapi: &OpenAPI, response: &openapiv3::Response) -> String {
1348    if let Some(json_media) = response.content.get("application/json") {
1349        if let Some(schema_ref) = &json_media.schema {
1350            match schema_ref {
1351                ReferenceOr::Reference { reference } => {
1352                    if let Some(ref_name) = get_schema_name_from_ref(reference) {
1353                        to_pascal_case(&ref_name)
1354                    } else {
1355                        "any".to_string()
1356                    }
1357                }
1358                ReferenceOr::Item(_) => "any".to_string(),
1359            }
1360        } else {
1361            "any".to_string()
1362        }
1363    } else {
1364        "any".to_string()
1365    }
1366}
1367
1368fn generate_function_name_from_path(path: &str, method: &str) -> String {
1369    let path_parts: Vec<&str> = path
1370        .trim_start_matches('/')
1371        .split('/')
1372        .filter(|p| !p.starts_with('{'))
1373        .collect();
1374
1375    // Map HTTP methods to common prefixes
1376    let method_upper = method.to_uppercase();
1377    let method_lower = method.to_lowercase();
1378    let method_prefix = match method_upper.as_str() {
1379        "GET" => "get",
1380        "POST" => "create",
1381        "PUT" => "update",
1382        "DELETE" => "delete",
1383        "PATCH" => "patch",
1384        _ => method_lower.as_str(),
1385    };
1386
1387    let base_name = if path_parts.is_empty() {
1388        method_prefix.to_string()
1389    } else {
1390        // Extract resource name from path (usually the first or last part)
1391        let resource_name = if path_parts.len() > 1 {
1392            // For nested paths like /users/{id}/posts, use the last resource
1393            path_parts.last().unwrap_or(&"")
1394        } else {
1395            path_parts.first().unwrap_or(&"")
1396        };
1397
1398        // Handle common patterns
1399        if resource_name.ends_with("s") && path.contains('{') {
1400            // Plural resource with ID: /products/{id} -> getProductById
1401            let singular = &resource_name[..resource_name.len() - 1];
1402            format!("{}{}ById", method_prefix, to_pascal_case(singular))
1403        } else if path.contains('{') {
1404            // Resource with ID: /user/{id} -> getUserById
1405            format!("{}{}ById", method_prefix, to_pascal_case(resource_name))
1406        } else {
1407            // No ID: /products -> getProducts
1408            format!("{}{}", method_prefix, to_pascal_case(resource_name))
1409        }
1410    };
1411
1412    to_camel_case(&base_name)
1413}