vika_cli/generator/
api_client.rs

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