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
17fn 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), Array(String), }
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 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 let path_params = extract_path_parameters(openapi, operation, enum_registry)?;
204
205 let query_params = extract_query_parameters(openapi, operation, enum_registry)?;
207
208 let request_body_info = extract_request_body(openapi, operation)?;
210
211 let all_responses = extract_all_responses(openapi, operation)?;
213
214 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 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 let namespace_name = to_pascal_case(&module_name.replace("/", "_"));
236
237 let mut params = Vec::new();
239 let mut path_template = op_info.path.clone();
240 let mut enum_types = Vec::new();
241
242 for param in &path_params {
244 let param_type = match ¶m.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(), };
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 if let Some((body_type, _)) = &request_body_info {
267 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 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 params.push(format!("query?: {}.{}", namespace_name, query_type_name));
289 }
290
291 let params_str = params.join(", ");
292
293 let mut body_lines = Vec::new();
295
296 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 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 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 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 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 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 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 let module_depth = module_name.matches('/').count() + 1; let runtime_import = if let (Some(root), Some(apis)) = (root_dir, apis_dir) {
379 let root_normalized = root.trim_end_matches('/');
382 let apis_normalized = apis
383 .trim_start_matches(root_normalized)
384 .trim_start_matches('/');
385
386 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 format!("{}runtime", "../".repeat(module_depth))
399 };
400
401 let mut type_imports = String::new();
404 let mut needs_common_import = false;
405 let mut needs_namespace_import = false;
406
407 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 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 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 if needs_common_import || needs_namespace_import {
440 let schemas_import_path = if let (Some(apis), Some(schemas)) = (apis_dir, schemas_dir) {
441 let apis_normalized = apis.trim_end_matches('/');
448 let schemas_normalized = schemas.trim_end_matches('/');
449
450 let common_prefix = find_common_prefix(apis_normalized, schemas_normalized);
452
453 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 let schemas_relative = schemas_normalized
466 .strip_prefix(&common_prefix)
467 .unwrap_or(schemas_normalized)
468 .trim_start_matches('/');
469
470 let total_depth = module_depth + apis_depth;
472
473 if schemas_relative.is_empty() {
475 "../".repeat(total_depth)
476 } else {
477 format!("{}{}", "../".repeat(total_depth), schemas_relative)
478 }
479 } else {
480 let schemas_depth = module_depth + 2; format!("{}schemas", "../".repeat(schemas_depth))
483 };
484
485 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 format!("{}/common", schemas_import_path.trim_end_matches('/'))
502 } else {
503 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 format!(
514 "{}/{}",
515 schemas_import_path.trim_end_matches('/'),
516 sanitized_module_name
517 )
518 } else {
519 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 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 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 if !type_imports.is_empty() && !type_imports.ends_with('\n') {
559 type_imports.push('\n');
560 }
561
562 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 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 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 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 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 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; }
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, ¶meter_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 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 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; }
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, ¶meter_data, enum_registry)?
782 {
783 param_info.style = Some(format!("{:?}", style));
785 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 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 param_info.style = Some(format!("{:?}", style));
818 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 let schema = match ¶meter_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 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 let enum_name = format!("{}Enum", to_pascal_case(&name));
869 let context_key = format!("{}:{}", enum_key, name);
870
871 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()), explode: Some(false), 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 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()), explode: Some(true), 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 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 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 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 Ok(Some(("any".to_string(), None)))
1063 }
1064 Err(_) => {
1065 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 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 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 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 Ok("any".to_string())
1140 }
1141 Err(_) => {
1142 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 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 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 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 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 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 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 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 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 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 let qualified_type = if common_schemas.contains(&response.body_type) {
1332 format!("Common.{}", response.body_type)
1333 } else {
1334 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 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 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 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 let resource_name = if path_parts.len() > 1 {
1428 path_parts.last().unwrap_or(&"")
1430 } else {
1431 path_parts.first().unwrap_or(&"")
1432 };
1433
1434 if resource_name.ends_with("s") && path.contains('{') {
1436 let singular = &resource_name[..resource_name.len() - 1];
1438 format!("{}{}ById", method_prefix, to_pascal_case(singular))
1439 } else if path.contains('{') {
1440 format!("{}{}ById", method_prefix, to_pascal_case(resource_name))
1442 } else {
1443 format!("{}{}", method_prefix, to_pascal_case(resource_name))
1445 }
1446 };
1447
1448 to_camel_case(&base_name)
1449}