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
694pub fn 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
750pub fn 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
1017pub fn 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
1168pub fn extract_all_responses(
1169    openapi: &OpenAPI,
1170    operation: &Operation,
1171) -> Result<Vec<ResponseInfo>> {
1172    let mut responses = Vec::new();
1173
1174    for (status_code, response_ref) in &operation.responses.responses {
1175        let status_num = match status_code {
1176            openapiv3::StatusCode::Code(code) => *code,
1177            openapiv3::StatusCode::Range(range) => {
1178                // For ranges like 4xx, 5xx, extract the range value
1179                // Range is an enum, check its variant
1180                match format!("{:?}", range).as_str() {
1181                    s if s.contains("4") => 400,
1182                    s if s.contains("5") => 500,
1183                    _ => 0,
1184                }
1185            }
1186        };
1187
1188        // Extract response info (description and body type)
1189        let (description, body_type) = match response_ref {
1190            ReferenceOr::Reference { reference } => {
1191                match resolve_response_ref(openapi, reference) {
1192                    Ok(ReferenceOr::Item(response)) => {
1193                        let desc = response.description.clone();
1194                        let body = extract_response_body_type(openapi, &response);
1195                        (Some(desc), body)
1196                    }
1197                    _ => (None, "any".to_string()),
1198                }
1199            }
1200            ReferenceOr::Item(response) => {
1201                let desc = response.description.clone();
1202                let body = extract_response_body_type(openapi, response);
1203                (Some(desc), body)
1204            }
1205        };
1206
1207        responses.push(ResponseInfo {
1208            status_code: status_num,
1209            body_type,
1210            description,
1211        });
1212    }
1213
1214    Ok(responses)
1215}
1216
1217#[allow(dead_code)]
1218fn extract_error_responses(openapi: &OpenAPI, operation: &Operation) -> Result<Vec<ErrorResponse>> {
1219    let all_responses = extract_all_responses(openapi, operation)?;
1220    let errors: Vec<ErrorResponse> = all_responses
1221        .iter()
1222        .filter(|r| r.status_code < 200 || r.status_code >= 300)
1223        .map(|r| ErrorResponse {
1224            status_code: r.status_code,
1225            body_type: r.body_type.clone(),
1226        })
1227        .collect();
1228    Ok(errors)
1229}
1230
1231fn generate_response_types(
1232    func_name: &str,
1233    success_responses: &[ResponseInfo],
1234    error_responses: &[ResponseInfo],
1235    _namespace_name: &str,
1236    common_schemas: &[String],
1237    enum_types: &[(String, Vec<String>)],
1238) -> Vec<TypeScriptType> {
1239    let mut types = Vec::new();
1240    let type_name_base = to_pascal_case(func_name);
1241
1242    // Generate enum types for parameters
1243    for (enum_name, enum_values) in enum_types {
1244        let variants = enum_values
1245            .iter()
1246            .map(|v| format!("\"{}\"", v))
1247            .collect::<Vec<_>>()
1248            .join(" |\n");
1249        let enum_type = format!("export type {} =\n{};", enum_name, variants);
1250        types.push(TypeScriptType { content: enum_type });
1251    }
1252
1253    // Generate Errors type
1254    if !error_responses.is_empty() {
1255        let mut error_fields = Vec::new();
1256        for error in error_responses {
1257            if error.status_code > 0 {
1258                // For types in the same file (not common), use unqualified name
1259                // For common types, use Common.TypeName
1260                let qualified_type = if error.body_type != "any" {
1261                    if common_schemas.contains(&error.body_type) {
1262                        format!("Common.{}", error.body_type)
1263                    } else {
1264                        // Type is in the same file, use unqualified name
1265                        error.body_type.clone()
1266                    }
1267                } else {
1268                    "any".to_string()
1269                };
1270
1271                let description = error
1272                    .description
1273                    .as_ref()
1274                    .map(|d| format!("    /**\n     * {}\n     */", d))
1275                    .unwrap_or_default();
1276
1277                error_fields.push(format!(
1278                    "{}\n    {}: {};",
1279                    description, error.status_code, qualified_type
1280                ));
1281            }
1282        }
1283
1284        if !error_fields.is_empty() {
1285            let errors_type = format!(
1286                "export type {}Errors = {{\n{}\n}};",
1287                type_name_base,
1288                error_fields.join("\n")
1289            );
1290            types.push(TypeScriptType {
1291                content: errors_type,
1292            });
1293
1294            // Generate Error union type
1295            let error_union_type = format!(
1296                "export type {}Error = {}Errors[keyof {}Errors];",
1297                type_name_base, type_name_base, type_name_base
1298            );
1299            types.push(TypeScriptType {
1300                content: error_union_type,
1301            });
1302        }
1303    }
1304
1305    // Generate Responses type (only if we have success responses with schemas)
1306    let success_with_schemas: Vec<&ResponseInfo> = success_responses
1307        .iter()
1308        .filter(|r| r.status_code >= 200 && r.status_code < 300 && r.body_type != "any")
1309        .collect();
1310
1311    if !success_with_schemas.is_empty() {
1312        let mut response_fields = Vec::new();
1313        for response in success_with_schemas {
1314            // For types in the same file (not common), use unqualified name
1315            // For common types, use Common.TypeName
1316            let qualified_type = if common_schemas.contains(&response.body_type) {
1317                format!("Common.{}", response.body_type)
1318            } else {
1319                // Type is in the same file, use unqualified name
1320                response.body_type.clone()
1321            };
1322
1323            let description = response
1324                .description
1325                .as_ref()
1326                .map(|d| format!("    /**\n     * {}\n     */", d))
1327                .unwrap_or_default();
1328
1329            response_fields.push(format!(
1330                "{}\n    {}: {};",
1331                description, response.status_code, qualified_type
1332            ));
1333        }
1334
1335        if !response_fields.is_empty() {
1336            let responses_type = format!(
1337                "export type {}Responses = {{\n{}\n}};",
1338                type_name_base,
1339                response_fields.join("\n")
1340            );
1341            types.push(TypeScriptType {
1342                content: responses_type,
1343            });
1344        }
1345    }
1346
1347    types
1348}
1349
1350fn extract_response_body_type(_openapi: &OpenAPI, response: &openapiv3::Response) -> String {
1351    if let Some(json_media) = response.content.get("application/json") {
1352        if let Some(schema_ref) = &json_media.schema {
1353            match schema_ref {
1354                ReferenceOr::Reference { reference } => {
1355                    if let Some(ref_name) = get_schema_name_from_ref(reference) {
1356                        to_pascal_case(&ref_name)
1357                    } else {
1358                        "any".to_string()
1359                    }
1360                }
1361                ReferenceOr::Item(_) => "any".to_string(),
1362            }
1363        } else {
1364            "any".to_string()
1365        }
1366    } else {
1367        "any".to_string()
1368    }
1369}
1370
1371fn generate_function_name_from_path(path: &str, method: &str) -> String {
1372    let path_parts: Vec<&str> = path
1373        .trim_start_matches('/')
1374        .split('/')
1375        .filter(|p| !p.starts_with('{'))
1376        .collect();
1377
1378    // Map HTTP methods to common prefixes
1379    let method_upper = method.to_uppercase();
1380    let method_lower = method.to_lowercase();
1381    let method_prefix = match method_upper.as_str() {
1382        "GET" => "get",
1383        "POST" => "create",
1384        "PUT" => "update",
1385        "DELETE" => "delete",
1386        "PATCH" => "patch",
1387        _ => method_lower.as_str(),
1388    };
1389
1390    let base_name = if path_parts.is_empty() {
1391        method_prefix.to_string()
1392    } else {
1393        // Extract resource name from path (usually the first or last part)
1394        let resource_name = if path_parts.len() > 1 {
1395            // For nested paths like /users/{id}/posts, use the last resource
1396            path_parts.last().unwrap_or(&"")
1397        } else {
1398            path_parts.first().unwrap_or(&"")
1399        };
1400
1401        // Handle common patterns
1402        if resource_name.ends_with("s") && path.contains('{') {
1403            // Plural resource with ID: /products/{id} -> getProductById
1404            let singular = &resource_name[..resource_name.len() - 1];
1405            format!("{}{}ById", method_prefix, to_pascal_case(singular))
1406        } else if path.contains('{') {
1407            // Resource with ID: /user/{id} -> getUserById
1408            format!("{}{}ById", method_prefix, to_pascal_case(resource_name))
1409        } else {
1410            // No ID: /products -> getProducts
1411            format!("{}{}", method_prefix, to_pascal_case(resource_name))
1412        }
1413    };
1414
1415    to_camel_case(&base_name)
1416}