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