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::utils::{to_camel_case, to_pascal_case};
7use openapiv3::OpenAPI;
8use openapiv3::{Operation, Parameter, ReferenceOr};
9
10pub struct ApiFunction {
11    pub content: String,
12}
13
14pub fn generate_api_client(
15    openapi: &OpenAPI,
16    operations: &[OperationInfo],
17    module_name: &str,
18    common_schemas: &[String],
19) -> Result<Vec<ApiFunction>> {
20    let mut functions = Vec::new();
21
22    for op_info in operations {
23        let func = generate_function_for_operation(openapi, op_info, module_name, common_schemas)?;
24        functions.push(func);
25    }
26
27    Ok(functions)
28}
29
30fn generate_function_for_operation(
31    openapi: &OpenAPI,
32    op_info: &OperationInfo,
33    module_name: &str,
34    common_schemas: &[String],
35) -> Result<ApiFunction> {
36    let operation = &op_info.operation;
37    let method = op_info.method.to_lowercase();
38
39    // Generate function name from operation ID or path
40    let func_name = if let Some(operation_id) = &operation.operation_id {
41        to_camel_case(operation_id)
42    } else {
43        generate_function_name_from_path(&op_info.path, &op_info.method)
44    };
45
46    // Extract path parameters
47    let path_params = extract_path_parameters(openapi, operation)?;
48
49    // Extract query parameters
50    let query_params = extract_query_parameters(openapi, operation)?;
51
52    // Extract request body
53    let request_body = extract_request_body(openapi, operation)?;
54
55    // Extract response type
56    let response_type = extract_response_type(openapi, operation)?;
57
58    // Calculate namespace name for qualified type access
59    // Replace slashes with underscore and convert to PascalCase (e.g., "tenant/auth" -> "TenantAuth")
60    let namespace_name = to_pascal_case(&module_name.replace("/", "_"));
61
62    // Build function signature
63    let mut params = Vec::new();
64    let mut path_template = op_info.path.clone();
65
66    // Add path parameters
67    for param in &path_params {
68        params.push(format!("{}: string", param));
69        path_template =
70            path_template.replace(&format!("{{{}}}", param), &format!("${{{}}}", param));
71    }
72
73    // Add query parameters
74    if !query_params.is_empty() {
75        let query_fields: Vec<String> = query_params
76            .iter()
77            .map(|p| format!("{}?: string", p))
78            .collect();
79        let query_type = format!("{{ {} }}", query_fields.join(", "));
80        params.push(format!("query?: {}", query_type));
81    }
82
83    // Add request body (check if it's in common schemas)
84    if let Some(body_type) = &request_body {
85        // Don't qualify "any" type with namespace
86        if body_type == "any" {
87            params.push("body: any".to_string());
88        } else {
89            let qualified_body_type = if common_schemas.contains(body_type) {
90                format!("Common.{}", body_type)
91            } else {
92                format!("{}.{}", namespace_name, body_type)
93            };
94            params.push(format!("body: {}", qualified_body_type));
95        }
96    }
97
98    let params_str = params.join(", ");
99
100    // Build function body
101    let mut body_lines = Vec::new();
102
103    // Build URL with path parameters
104    let mut url_template = op_info.path.clone();
105    for param in &path_params {
106        url_template = url_template.replace(&format!("{{{}}}", param), &format!("${{{}}}", param));
107    }
108
109    // Build URL with query parameters
110    if !query_params.is_empty() {
111        body_lines.push("    const queryString = new URLSearchParams();".to_string());
112        for param in &query_params {
113            body_lines.push(format!(
114                "    if (query?.{}) queryString.append(\"{}\", query.{});",
115                param, param, param
116            ));
117        }
118        body_lines.push("    const queryStr = queryString.toString();".to_string());
119        body_lines.push(format!(
120            "    const url = `{}` + (queryStr ? `?${{queryStr}}` : '');",
121            url_template
122        ));
123    } else {
124        body_lines.push(format!("    const url = `{}`;", url_template));
125    }
126
127    // Build HTTP call
128    let http_method = match method.to_uppercase().as_str() {
129        "GET" => "get",
130        "POST" => "post",
131        "PUT" => "put",
132        "DELETE" => "delete",
133        "PATCH" => "patch",
134        "HEAD" => "head",
135        "OPTIONS" => "options",
136        _ => "get",
137    };
138
139    // Use qualified type for generic parameter (check if it's common or module-specific)
140    let qualified_response_type_for_generic = if response_type != "any" {
141        let is_common = common_schemas.contains(&response_type);
142        if is_common {
143            format!("Common.{}", response_type)
144        } else {
145            format!("{}.{}", namespace_name, response_type)
146        }
147    } else {
148        response_type.clone()
149    };
150
151    if let Some(_body_type) = &request_body {
152        body_lines.push(format!("    return http.{}(url, body);", http_method));
153    } else {
154        body_lines.push(format!(
155            "    return http.{}<{}>(url);",
156            http_method, qualified_response_type_for_generic
157        ));
158    }
159
160    // HTTP client is at apis/http.ts, and we're generating apis/<module>/index.ts
161    // So the relative path is ../http
162    let http_import = "../http";
163
164    // Determine if response type is in common schemas or module-specific
165    let (type_imports, qualified_type) = if response_type != "any" {
166        let is_common = common_schemas.contains(&response_type);
167        if is_common {
168            // Import from common module
169            let common_import = "../../schemas/common";
170            let common_namespace = "Common";
171            let imports = format!(
172                "import * as {} from \"{}\";\n",
173                common_namespace, common_import
174            );
175            let qualified = format!("{}.{}", common_namespace, response_type);
176            (imports, qualified)
177        } else {
178            // Import from module-specific schemas
179            let schemas_import = format!("../../schemas/{}", module_name);
180            let imports = format!(
181                "import * as {} from \"{}\";\n",
182                namespace_name, schemas_import
183            );
184            let qualified = format!("{}.{}", namespace_name, response_type);
185            (imports, qualified)
186        }
187    } else {
188        (String::new(), String::new())
189    };
190
191    let return_type = if response_type == "any" {
192        String::new()
193    } else {
194        format!(": Promise<{}>", qualified_type)
195    };
196
197    let function_body = body_lines.join("\n");
198
199    let content = if params_str.is_empty() {
200        format!(
201            "import {{ http }} from \"{}\";\n{}\
202            export const {} = async (){} => {{\n{}\n}};",
203            http_import, type_imports, func_name, return_type, function_body
204        )
205    } else {
206        format!(
207            "import {{ http }} from \"{}\";\n{}\
208            export const {} = async ({}){} => {{\n{}\n}};",
209            http_import, type_imports, func_name, params_str, return_type, function_body
210        )
211    };
212
213    Ok(ApiFunction { content })
214}
215
216fn extract_path_parameters(openapi: &OpenAPI, operation: &Operation) -> Result<Vec<String>> {
217    let mut params = Vec::new();
218
219    for param_ref in &operation.parameters {
220        match param_ref {
221            ReferenceOr::Reference { reference } => {
222                // Resolve parameter reference (with support for nested references up to 3 levels)
223                let mut current_ref = Some(reference.clone());
224                let mut depth = 0;
225                while let Some(ref_path) = current_ref.take() {
226                    if depth > 3 {
227                        break; // Prevent infinite loops
228                    }
229                    match resolve_parameter_ref(openapi, &ref_path) {
230                        Ok(ReferenceOr::Item(param)) => {
231                            if let Parameter::Path { parameter_data, .. } = param {
232                                params.push(parameter_data.name.clone());
233                            }
234                            break;
235                        }
236                        Ok(ReferenceOr::Reference {
237                            reference: nested_ref,
238                        }) => {
239                            current_ref = Some(nested_ref);
240                            depth += 1;
241                        }
242                        Err(_) => {
243                            // Reference resolution failed - skip
244                            break;
245                        }
246                    }
247                }
248            }
249            ReferenceOr::Item(param) => {
250                if let Parameter::Path { parameter_data, .. } = param {
251                    params.push(parameter_data.name.clone());
252                }
253            }
254        }
255    }
256
257    Ok(params)
258}
259
260fn extract_query_parameters(openapi: &OpenAPI, operation: &Operation) -> Result<Vec<String>> {
261    let mut params = Vec::new();
262
263    for param_ref in &operation.parameters {
264        match param_ref {
265            ReferenceOr::Reference { reference } => {
266                // Resolve parameter reference (with support for nested references up to 3 levels)
267                let mut current_ref = Some(reference.clone());
268                let mut depth = 0;
269                while let Some(ref_path) = current_ref.take() {
270                    if depth > 3 {
271                        break; // Prevent infinite loops
272                    }
273                    match resolve_parameter_ref(openapi, &ref_path) {
274                        Ok(ReferenceOr::Item(param)) => {
275                            if let Parameter::Query { parameter_data, .. } = param {
276                                params.push(parameter_data.name.clone());
277                            }
278                            break;
279                        }
280                        Ok(ReferenceOr::Reference {
281                            reference: nested_ref,
282                        }) => {
283                            current_ref = Some(nested_ref);
284                            depth += 1;
285                        }
286                        Err(_) => {
287                            // Reference resolution failed - skip
288                            break;
289                        }
290                    }
291                }
292            }
293            ReferenceOr::Item(param) => {
294                if let Parameter::Query { parameter_data, .. } = param {
295                    params.push(parameter_data.name.clone());
296                }
297            }
298        }
299    }
300
301    Ok(params)
302}
303
304fn extract_request_body(openapi: &OpenAPI, operation: &Operation) -> Result<Option<String>> {
305    if let Some(request_body) = &operation.request_body {
306        match request_body {
307            ReferenceOr::Reference { reference } => {
308                // Resolve request body reference
309                match resolve_request_body_ref(openapi, reference) {
310                    Ok(ReferenceOr::Item(body)) => {
311                        if let Some(json_media) = body.content.get("application/json") {
312                            if let Some(schema_ref) = &json_media.schema {
313                                match schema_ref {
314                                    ReferenceOr::Reference { reference } => {
315                                        if let Some(ref_name) = get_schema_name_from_ref(reference)
316                                        {
317                                            Ok(Some(to_pascal_case(&ref_name)))
318                                        } else {
319                                            Ok(Some("any".to_string()))
320                                        }
321                                    }
322                                    ReferenceOr::Item(_schema) => {
323                                        // Inline schemas: These are schema definitions embedded directly
324                                        // in the request body. Generating proper types would require
325                                        // recursive type generation at this point, which is complex.
326                                        // For now, we use 'any' as a fallback. This can be enhanced
327                                        // to generate inline types if needed.
328                                        Ok(Some("any".to_string()))
329                                    }
330                                }
331                            } else {
332                                Ok(Some("any".to_string()))
333                            }
334                        } else {
335                            Ok(Some("any".to_string()))
336                        }
337                    }
338                    Ok(ReferenceOr::Reference { .. }) => {
339                        // Nested reference - return any
340                        Ok(Some("any".to_string()))
341                    }
342                    Err(_) => {
343                        // Reference resolution failed - return any
344                        Ok(Some("any".to_string()))
345                    }
346                }
347            }
348            ReferenceOr::Item(body) => {
349                if let Some(json_media) = body.content.get("application/json") {
350                    if let Some(schema_ref) = &json_media.schema {
351                        match schema_ref {
352                            ReferenceOr::Reference { reference } => {
353                                if let Some(ref_name) = get_schema_name_from_ref(reference) {
354                                    Ok(Some(to_pascal_case(&ref_name)))
355                                } else {
356                                    Ok(Some("any".to_string()))
357                                }
358                            }
359                            ReferenceOr::Item(_schema) => {
360                                // Inline schemas: These are schema definitions embedded directly
361                                // in the request body. Generating proper types would require
362                                // recursive type generation at this point, which is complex.
363                                // For now, we use 'any' as a fallback. This can be enhanced
364                                // to generate inline types if needed.
365                                Ok(Some("any".to_string()))
366                            }
367                        }
368                    } else {
369                        Ok(Some("any".to_string()))
370                    }
371                } else {
372                    Ok(Some("any".to_string()))
373                }
374            }
375        }
376    } else {
377        Ok(None)
378    }
379}
380
381fn extract_response_type(openapi: &OpenAPI, operation: &Operation) -> Result<String> {
382    // Try to get 200 response
383    if let Some(success_response) = operation
384        .responses
385        .responses
386        .get(&openapiv3::StatusCode::Code(200))
387    {
388        match success_response {
389            ReferenceOr::Reference { reference } => {
390                // Resolve response reference
391                match resolve_response_ref(openapi, reference) {
392                    Ok(ReferenceOr::Item(response)) => {
393                        if let Some(json_media) = response.content.get("application/json") {
394                            if let Some(schema_ref) = &json_media.schema {
395                                match schema_ref {
396                                    ReferenceOr::Reference { reference } => {
397                                        if let Some(ref_name) = get_schema_name_from_ref(reference)
398                                        {
399                                            Ok(to_pascal_case(&ref_name))
400                                        } else {
401                                            Ok("any".to_string())
402                                        }
403                                    }
404                                    ReferenceOr::Item(_) => Ok("any".to_string()),
405                                }
406                            } else {
407                                Ok("any".to_string())
408                            }
409                        } else {
410                            Ok("any".to_string())
411                        }
412                    }
413                    Ok(ReferenceOr::Reference { .. }) => {
414                        // Nested reference - return any
415                        Ok("any".to_string())
416                    }
417                    Err(_) => {
418                        // Reference resolution failed - return any
419                        Ok("any".to_string())
420                    }
421                }
422            }
423            ReferenceOr::Item(response) => {
424                if let Some(json_media) = response.content.get("application/json") {
425                    if let Some(schema_ref) = &json_media.schema {
426                        match schema_ref {
427                            ReferenceOr::Reference { reference } => {
428                                if let Some(ref_name) = get_schema_name_from_ref(reference) {
429                                    Ok(to_pascal_case(&ref_name))
430                                } else {
431                                    Ok("any".to_string())
432                                }
433                            }
434                            ReferenceOr::Item(_) => Ok("any".to_string()),
435                        }
436                    } else {
437                        Ok("any".to_string())
438                    }
439                } else {
440                    Ok("any".to_string())
441                }
442            }
443        }
444    } else {
445        Ok("any".to_string())
446    }
447}
448
449fn generate_function_name_from_path(path: &str, method: &str) -> String {
450    let path_parts: Vec<&str> = path
451        .trim_start_matches('/')
452        .split('/')
453        .filter(|p| !p.starts_with('{'))
454        .collect();
455
456    // Map HTTP methods to common prefixes
457    let method_upper = method.to_uppercase();
458    let method_lower = method.to_lowercase();
459    let method_prefix = match method_upper.as_str() {
460        "GET" => "get",
461        "POST" => "create",
462        "PUT" => "update",
463        "DELETE" => "delete",
464        "PATCH" => "patch",
465        _ => method_lower.as_str(),
466    };
467
468    let base_name = if path_parts.is_empty() {
469        method_prefix.to_string()
470    } else {
471        // Extract resource name from path (usually the first or last part)
472        let resource_name = if path_parts.len() > 1 {
473            // For nested paths like /users/{id}/posts, use the last resource
474            path_parts.last().unwrap_or(&"")
475        } else {
476            path_parts.first().unwrap_or(&"")
477        };
478
479        // Handle common patterns
480        if resource_name.ends_with("s") && path.contains('{') {
481            // Plural resource with ID: /products/{id} -> getProductById
482            let singular = &resource_name[..resource_name.len() - 1];
483            format!("{}{}ById", method_prefix, to_pascal_case(singular))
484        } else if path.contains('{') {
485            // Resource with ID: /user/{id} -> getUserById
486            format!("{}{}ById", method_prefix, to_pascal_case(resource_name))
487        } else {
488            // No ID: /products -> getProducts
489            format!("{}{}", method_prefix, to_pascal_case(resource_name))
490        }
491    };
492
493    to_camel_case(&base_name)
494}