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