vika_cli/generator/
api_client.rs

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