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::engine::TemplateEngine;
10use crate::templates::registry::TemplateId;
11use crate::templates::context::{ApiContext, Parameter as ApiParameter, RequestBody, Response as ApiResponse};
12use openapiv3::OpenAPI;
13use openapiv3::{Operation, Parameter, ReferenceOr, SchemaKind, Type};
14
15pub struct ApiFunction {
16 pub content: String,
17}
18
19pub struct ApiGenerationResult {
20 pub functions: Vec<ApiFunction>,
21 pub response_types: Vec<TypeScriptType>,
22}
23
24#[derive(Clone, Debug)]
25pub struct ParameterInfo {
26 pub name: String,
27 pub param_type: ParameterType,
28 pub enum_values: Option<Vec<String>>,
29 pub enum_type_name: Option<String>,
30 pub is_array: bool,
31 pub array_item_type: Option<String>,
32 pub style: Option<String>,
33 pub explode: Option<bool>,
34 pub description: Option<String>,
35}
36
37#[derive(Clone, Debug)]
38pub enum ParameterType {
39 String,
40 Number,
41 Integer,
42 Boolean,
43 Enum(String), Array(String), }
46
47#[derive(Clone, Debug)]
48pub struct ResponseInfo {
49 pub status_code: u16,
50 pub body_type: String,
51 pub description: Option<String>,
52}
53
54#[derive(Clone, Debug)]
55pub struct ErrorResponse {
56 pub status_code: u16,
57 pub body_type: String,
58}
59
60pub fn generate_api_client(
61 openapi: &OpenAPI,
62 operations: &[OperationInfo],
63 module_name: &str,
64 common_schemas: &[String],
65) -> Result<ApiGenerationResult> {
66 generate_api_client_with_registry(
67 openapi,
68 operations,
69 module_name,
70 common_schemas,
71 &mut std::collections::HashMap::new(),
72 )
73}
74
75pub fn generate_api_client_with_registry(
76 openapi: &OpenAPI,
77 operations: &[OperationInfo],
78 module_name: &str,
79 common_schemas: &[String],
80 enum_registry: &mut std::collections::HashMap<String, String>,
81) -> Result<ApiGenerationResult> {
82 generate_api_client_with_registry_and_engine(
83 openapi,
84 operations,
85 module_name,
86 common_schemas,
87 enum_registry,
88 None,
89 )
90}
91
92pub fn generate_api_client_with_registry_and_engine(
93 openapi: &OpenAPI,
94 operations: &[OperationInfo],
95 module_name: &str,
96 common_schemas: &[String],
97 enum_registry: &mut std::collections::HashMap<String, String>,
98 template_engine: Option<&TemplateEngine>,
99) -> Result<ApiGenerationResult> {
100 let mut functions = Vec::new();
101 let mut response_types = Vec::new();
102
103 for op_info in operations {
104 let result = generate_function_for_operation(
105 openapi,
106 op_info,
107 module_name,
108 common_schemas,
109 enum_registry,
110 template_engine,
111 )?;
112 functions.push(result.function);
113 response_types.extend(result.response_types);
114 }
115
116 Ok(ApiGenerationResult {
117 functions,
118 response_types,
119 })
120}
121
122struct FunctionGenerationResult {
123 function: ApiFunction,
124 response_types: Vec<TypeScriptType>,
125}
126
127fn generate_function_for_operation(
128 openapi: &OpenAPI,
129 op_info: &OperationInfo,
130 module_name: &str,
131 common_schemas: &[String],
132 enum_registry: &mut std::collections::HashMap<String, String>,
133 template_engine: Option<&TemplateEngine>,
134) -> Result<FunctionGenerationResult> {
135 let operation = &op_info.operation;
136 let method = op_info.method.to_lowercase();
137
138 let func_name = if let Some(operation_id) = &operation.operation_id {
140 to_camel_case(operation_id)
141 } else {
142 generate_function_name_from_path(&op_info.path, &op_info.method)
143 };
144
145 let path_params = extract_path_parameters(openapi, operation, enum_registry)?;
147
148 let query_params = extract_query_parameters(openapi, operation, enum_registry)?;
150
151 let request_body_info = extract_request_body(openapi, operation)?;
153
154 let all_responses = extract_all_responses(openapi, operation)?;
156
157 let success_responses: Vec<ResponseInfo> = all_responses
159 .iter()
160 .filter(|r| r.status_code >= 200 && r.status_code < 300)
161 .cloned()
162 .collect();
163 let error_responses: Vec<ResponseInfo> = all_responses
164 .iter()
165 .filter(|r| r.status_code < 200 || r.status_code >= 300)
166 .cloned()
167 .collect();
168
169 let response_type = success_responses
171 .iter()
172 .find(|r| r.status_code == 200)
173 .map(|r| r.body_type.clone())
174 .unwrap_or_else(|| "any".to_string());
175
176 let namespace_name = to_pascal_case(&module_name.replace("/", "_"));
179
180 let mut params = Vec::new();
182 let mut path_template = op_info.path.clone();
183 let mut enum_types = Vec::new();
184
185 for param in &path_params {
187 let param_type = match ¶m.param_type {
188 ParameterType::Enum(enum_name) => {
189 enum_types.push((
190 enum_name.clone(),
191 param.enum_values.clone().unwrap_or_default(),
192 ));
193 enum_name.clone()
194 }
195 ParameterType::String => "string".to_string(),
196 ParameterType::Number => "number".to_string(),
197 ParameterType::Integer => "number".to_string(),
198 ParameterType::Boolean => "boolean".to_string(),
199 ParameterType::Array(_) => "string".to_string(), };
201 params.push(format!("{}: {}", param.name, param_type));
202 path_template = path_template.replace(
203 &format!("{{{}}}", param.name),
204 &format!("${{{}}}", param.name),
205 );
206 }
207
208 if let Some((body_type, _)) = &request_body_info {
210 if body_type == "any" {
212 params.push("body: any".to_string());
213 } else {
214 let qualified_body_type = if common_schemas.contains(body_type) {
215 format!("Common.{}", body_type)
216 } else {
217 format!("{}.{}", namespace_name, body_type)
218 };
219 params.push(format!("body: {}", qualified_body_type));
220 }
221 }
222
223 if !query_params.is_empty() {
226 let mut query_fields = Vec::new();
227 for param in &query_params {
228 let param_type = match ¶m.param_type {
229 ParameterType::Enum(enum_name) => {
230 enum_types.push((
231 enum_name.clone(),
232 param.enum_values.clone().unwrap_or_default(),
233 ));
234 enum_name.clone()
235 }
236 ParameterType::Array(item_type) => {
237 format!("{}[]", item_type)
238 }
239 ParameterType::String => "string".to_string(),
240 ParameterType::Number => "number".to_string(),
241 ParameterType::Integer => "number".to_string(),
242 ParameterType::Boolean => "boolean".to_string(),
243 };
244 query_fields.push(format!("{}?: {}", param.name, param_type));
245 }
246 let query_type = format!("{{ {} }}", query_fields.join(", "));
247 params.push(format!("query?: {}", query_type));
248 }
249
250 let params_str = params.join(", ");
251
252 let mut body_lines = Vec::new();
254
255 let mut url_template = op_info.path.clone();
257 for param in &path_params {
258 url_template = url_template.replace(
259 &format!("{{{}}}", param.name),
260 &format!("${{{}}}", param.name),
261 );
262 }
263
264 if !query_params.is_empty() {
266 body_lines.push(" const queryString = new URLSearchParams();".to_string());
267 for param in &query_params {
268 if param.is_array {
269 let explode = param.explode.unwrap_or(true);
270 if explode {
271 body_lines.push(format!(" if (query?.{}) {{", param.name));
273 body_lines.push(format!(
274 " query.{}.forEach((item) => queryString.append(\"{}\", String(item)));",
275 param.name, param.name
276 ));
277 body_lines.push(" }".to_string());
278 } else {
279 body_lines.push(format!(
281 " if (query?.{}) queryString.append(\"{}\", query.{}.join(\",\"));",
282 param.name, param.name, param.name
283 ));
284 }
285 } else {
286 body_lines.push(format!(
287 " if (query?.{}) queryString.append(\"{}\", String(query.{}));",
288 param.name, param.name, param.name
289 ));
290 }
291 }
292 body_lines.push(" const queryStr = queryString.toString();".to_string());
293 body_lines.push(format!(
294 " const url = `{}` + (queryStr ? `?${{queryStr}}` : '');",
295 url_template
296 ));
297 } else {
298 body_lines.push(format!(" const url = `{}`;", url_template));
299 }
300
301 let http_method = match method.to_uppercase().as_str() {
303 "GET" => "get",
304 "POST" => "post",
305 "PUT" => "put",
306 "DELETE" => "delete",
307 "PATCH" => "patch",
308 "HEAD" => "head",
309 "OPTIONS" => "options",
310 _ => "get",
311 };
312
313 let qualified_response_type_for_generic = if response_type != "any" {
315 let is_common = common_schemas.contains(&response_type);
316 if is_common {
317 format!("Common.{}", response_type)
318 } else {
319 format!("{}.{}", namespace_name, response_type)
320 }
321 } else {
322 response_type.clone()
323 };
324
325 if let Some((_body_type, _)) = &request_body_info {
326 body_lines.push(format!(" return http.{}(url, body);", http_method));
327 } else {
328 body_lines.push(format!(
329 " return http.{}<{}>(url);",
330 http_method, qualified_response_type_for_generic
331 ));
332 }
333
334 let depth = module_name.matches('/').count();
337 let http_relative_path = if depth == 0 {
338 "../http"
339 } else {
340 &format!("{}../http", "../".repeat(depth))
341 };
342 let http_import = http_relative_path;
343
344 let mut type_imports = String::new();
347 let mut needs_common_import = false;
348 let mut needs_namespace_import = false;
349
350 if response_type != "any" {
352 let is_common = common_schemas.contains(&response_type);
353 if is_common {
354 needs_common_import = true;
355 } else {
356 needs_namespace_import = true;
357 }
358 }
359
360 if let Some((body_type, _)) = &request_body_info {
362 if body_type != "any" {
363 if common_schemas.contains(body_type) {
364 needs_common_import = true;
365 } else {
366 needs_namespace_import = true;
367 }
368 }
369 }
370
371 if needs_common_import {
373 let schemas_depth = depth + 1; let common_import = format!("{}../schemas/common", "../".repeat(schemas_depth));
375 type_imports.push_str(&format!("import * as Common from \"{}\";\n", common_import));
376 }
377 if needs_namespace_import {
378 let schemas_depth = depth + 1; let sanitized_module_name = sanitize_module_name(module_name);
380 let schemas_import = format!(
381 "{}../schemas/{}",
382 "../".repeat(schemas_depth),
383 sanitized_module_name
384 );
385 type_imports.push_str(&format!(
386 "import * as {} from \"{}\";\n",
387 namespace_name, schemas_import
388 ));
389 }
390
391 let response_types = generate_response_types(
393 &func_name,
394 &success_responses,
395 &error_responses,
396 &namespace_name,
397 common_schemas,
398 &enum_types,
399 );
400
401 let type_name_base = to_pascal_case(&func_name);
403 let mut response_type_imports = Vec::new();
404
405 let errors_with_schemas: Vec<&ResponseInfo> = error_responses
407 .iter()
408 .filter(|r| r.status_code > 0)
409 .collect();
410 if !errors_with_schemas.is_empty() {
411 response_type_imports.push(format!("{}Errors", type_name_base));
412 response_type_imports.push(format!("{}Error", type_name_base));
413 }
414
415 let success_with_schemas: Vec<&ResponseInfo> = success_responses
417 .iter()
418 .filter(|r| r.status_code >= 200 && r.status_code < 300 && r.body_type != "any")
419 .collect();
420 if !success_with_schemas.is_empty() {
421 response_type_imports.push(format!("{}Responses", type_name_base));
422 }
423
424 if !response_type_imports.is_empty() {
426 let schemas_depth = depth + 1; let sanitized_module_name = sanitize_module_name(module_name);
429 let schemas_import = format!(
430 "{}../schemas/{}",
431 "../".repeat(schemas_depth),
432 sanitized_module_name
433 );
434 let type_import_line = format!(
435 "import type {{ {} }} from \"{}\";",
436 response_type_imports.join(", "),
437 schemas_import
438 );
439 if type_imports.is_empty() {
440 type_imports = format!("{}\n", type_import_line);
441 } else {
442 type_imports = format!("{}\n{}", type_imports.trim_end(), type_import_line);
443 }
444 }
445
446 if !enum_types.is_empty() {
448 let schemas_depth = depth + 1; let sanitized_module_name = sanitize_module_name(module_name);
450 let schemas_import = format!(
451 "{}../schemas/{}",
452 "../".repeat(schemas_depth),
453 sanitized_module_name
454 );
455 let enum_names: Vec<String> = enum_types.iter().map(|(name, _)| name.clone()).collect();
456 let enum_import_line = format!(
457 "import type {{ {} }} from \"{}\";",
458 enum_names.join(", "),
459 schemas_import
460 );
461 if type_imports.is_empty() {
462 type_imports = format!("{}\n", enum_import_line);
463 } else {
464 type_imports = format!("{}\n{}", type_imports.trim_end(), enum_import_line);
465 }
466 }
467
468 if !type_imports.is_empty() && !type_imports.ends_with('\n') {
470 type_imports.push('\n');
471 }
472
473 let has_responses_type = response_type_imports
475 .iter()
476 .any(|imp| imp.contains("Responses"));
477 let return_type = if has_responses_type {
478 if let Some(_primary_response) = success_responses
480 .iter()
481 .find(|r| r.status_code == 200 && r.body_type != "any")
482 {
483 format!(": Promise<{}Responses[200]>", type_name_base)
484 } else if let Some(first_success) = success_responses
485 .iter()
486 .find(|r| r.status_code >= 200 && r.status_code < 300 && r.body_type != "any")
487 {
488 format!(
489 ": Promise<{}Responses[{}]>",
490 type_name_base, first_success.status_code
491 )
492 } else {
493 String::new()
494 }
495 } else if !success_responses.is_empty() {
496 if let Some(primary_response) = success_responses.iter().find(|r| r.status_code == 200) {
498 if primary_response.body_type != "any" {
499 let qualified = if common_schemas.contains(&primary_response.body_type) {
500 format!("Common.{}", primary_response.body_type)
501 } else {
502 format!("{}.{}", namespace_name, primary_response.body_type)
503 };
504 format!(": Promise<{}>", qualified)
505 } else {
506 String::new()
507 }
508 } else {
509 String::new()
510 }
511 } else {
512 String::new()
513 };
514
515 let function_body = body_lines.join("\n");
516
517 let api_path_params: Vec<ApiParameter> = path_params
519 .iter()
520 .map(|p| {
521 let param_type = match &p.param_type {
522 ParameterType::Enum(enum_name) => enum_name.clone(),
523 ParameterType::Array(item_type) => format!("{}[]", item_type),
524 ParameterType::String => "string".to_string(),
525 ParameterType::Number => "number".to_string(),
526 ParameterType::Integer => "number".to_string(),
527 ParameterType::Boolean => "boolean".to_string(),
528 };
529 ApiParameter::new(p.name.clone(), param_type, false, p.description.clone())
530 })
531 .collect();
532
533 let api_query_params: Vec<ApiParameter> = query_params
534 .iter()
535 .map(|p| {
536 let param_type = match &p.param_type {
537 ParameterType::Enum(enum_name) => enum_name.clone(),
538 ParameterType::Array(item_type) => format!("{}[]", item_type),
539 ParameterType::String => "string".to_string(),
540 ParameterType::Number => "number".to_string(),
541 ParameterType::Integer => "number".to_string(),
542 ParameterType::Boolean => "boolean".to_string(),
543 };
544 ApiParameter::new(p.name.clone(), param_type, true, p.description.clone())
545 })
546 .collect();
547
548 let api_request_body = request_body_info.as_ref().map(|(rb_type, rb_desc)| RequestBody::new(rb_type.clone(), rb_desc.clone()));
549 let api_responses: Vec<ApiResponse> = all_responses
550 .iter()
551 .map(|r| ApiResponse::new(r.status_code, r.body_type.clone()))
552 .collect();
553
554 let operation_description = operation.description.clone()
558 .or_else(|| operation.summary.clone())
559 .filter(|s| !s.is_empty())
560 .unwrap_or_default();
561
562 let content = if let Some(engine) = template_engine {
563 let context = ApiContext::new(
564 func_name.clone(),
565 operation.operation_id.clone(),
566 method.clone(),
567 op_info.path.clone(),
568 api_path_params,
569 api_query_params,
570 api_request_body,
571 api_responses,
572 type_imports.clone(),
573 http_import.to_string(),
574 return_type.clone(),
575 function_body.clone(),
576 module_name.to_string(),
577 params_str.clone(),
578 operation_description.clone(),
579 );
580
581 engine.render(TemplateId::ApiClientFetch, &context)?
582 } else {
583 let jsdoc = if !operation_description.is_empty() {
585 format!("/**\n * {}\n */\n", operation_description)
586 } else {
587 String::new()
588 };
589 if params_str.is_empty() {
590 format!(
591 "import {{ http }} from \"{}\";\n{}{}{}export const {} = async (){} => {{\n{}\n}};",
592 http_import,
593 type_imports,
594 if !type_imports.is_empty() { "\n" } else { "" },
595 jsdoc,
596 func_name,
597 return_type,
598 function_body
599 )
600 } else {
601 format!(
602 "import {{ http }} from \"{}\";\n{}{}{}export const {} = async ({}){} => {{\n{}\n}};",
603 http_import,
604 type_imports,
605 if !type_imports.is_empty() { "\n" } else { "" },
606 jsdoc,
607 func_name,
608 params_str,
609 return_type,
610 function_body
611 )
612 }
613 };
614
615 Ok(FunctionGenerationResult {
616 function: ApiFunction { content },
617 response_types,
618 })
619}
620
621fn extract_path_parameters(
622 openapi: &OpenAPI,
623 operation: &Operation,
624 enum_registry: &mut std::collections::HashMap<String, String>,
625) -> Result<Vec<ParameterInfo>> {
626 let mut params = Vec::new();
627
628 for param_ref in &operation.parameters {
629 match param_ref {
630 ReferenceOr::Reference { reference } => {
631 let mut current_ref = Some(reference.clone());
633 let mut depth = 0;
634 while let Some(ref_path) = current_ref.take() {
635 if depth > 3 {
636 break; }
638 match resolve_parameter_ref(openapi, &ref_path) {
639 Ok(ReferenceOr::Item(param)) => {
640 if let Parameter::Path { parameter_data, .. } = param {
641 if let Some(param_info) =
642 extract_parameter_info(openapi, ¶meter_data, enum_registry)?
643 {
644 params.push(param_info);
645 }
646 }
647 break;
648 }
649 Ok(ReferenceOr::Reference {
650 reference: nested_ref,
651 }) => {
652 current_ref = Some(nested_ref);
653 depth += 1;
654 }
655 Err(_) => {
656 break;
658 }
659 }
660 }
661 }
662 ReferenceOr::Item(param) => {
663 if let Parameter::Path { parameter_data, .. } = param {
664 if let Some(param_info) =
665 extract_parameter_info(openapi, parameter_data, enum_registry)?
666 {
667 params.push(param_info);
668 }
669 }
670 }
671 }
672 }
673
674 Ok(params)
675}
676
677fn extract_query_parameters(
678 openapi: &OpenAPI,
679 operation: &Operation,
680 enum_registry: &mut std::collections::HashMap<String, String>,
681) -> Result<Vec<ParameterInfo>> {
682 let mut params = Vec::new();
683
684 for param_ref in &operation.parameters {
685 match param_ref {
686 ReferenceOr::Reference { reference } => {
687 let mut current_ref = Some(reference.clone());
689 let mut depth = 0;
690 while let Some(ref_path) = current_ref.take() {
691 if depth > 3 {
692 break; }
694 match resolve_parameter_ref(openapi, &ref_path) {
695 Ok(ReferenceOr::Item(param)) => {
696 if let Parameter::Query {
697 parameter_data,
698 style,
699 ..
700 } = param
701 {
702 if let Some(mut param_info) =
703 extract_parameter_info(openapi, ¶meter_data, enum_registry)?
704 {
705 param_info.style = Some(format!("{:?}", style));
707 param_info.explode =
709 Some(parameter_data.explode.unwrap_or(false));
710 params.push(param_info);
711 }
712 }
713 break;
714 }
715 Ok(ReferenceOr::Reference {
716 reference: nested_ref,
717 }) => {
718 current_ref = Some(nested_ref);
719 depth += 1;
720 }
721 Err(_) => {
722 break;
724 }
725 }
726 }
727 }
728 ReferenceOr::Item(param) => {
729 if let Parameter::Query {
730 parameter_data,
731 style,
732 ..
733 } = param
734 {
735 if let Some(mut param_info) =
736 extract_parameter_info(openapi, parameter_data, enum_registry)?
737 {
738 param_info.style = Some(format!("{:?}", style));
740 param_info.explode = Some(parameter_data.explode.unwrap_or(false));
742 params.push(param_info);
743 }
744 }
745 }
746 }
747 }
748
749 Ok(params)
750}
751
752fn extract_parameter_info(
753 openapi: &OpenAPI,
754 parameter_data: &openapiv3::ParameterData,
755 enum_registry: &mut std::collections::HashMap<String, String>,
756) -> Result<Option<ParameterInfo>> {
757 let name = parameter_data.name.clone();
758 let description = parameter_data.description.clone();
759
760 let schema = match ¶meter_data.format {
762 openapiv3::ParameterSchemaOrContent::Schema(schema_ref) => match schema_ref {
763 ReferenceOr::Reference { reference } => {
764 resolve_ref(openapi, reference).ok().and_then(|r| match r {
765 ReferenceOr::Item(s) => Some(s),
766 _ => None,
767 })
768 }
769 ReferenceOr::Item(s) => Some(s.clone()),
770 },
771 _ => None,
772 };
773
774 if let Some(schema) = schema {
775 match &schema.schema_kind {
776 SchemaKind::Type(type_) => {
777 match type_ {
778 Type::String(string_type) => {
779 if !string_type.enumeration.is_empty() {
781 let mut enum_values: Vec<String> = string_type
782 .enumeration
783 .iter()
784 .filter_map(|v| v.as_ref().cloned())
785 .collect();
786 enum_values.sort();
787 let enum_key = enum_values.join(",");
788
789 let enum_name = format!("{}Enum", to_pascal_case(&name));
791 let context_key = format!("{}:{}", enum_key, name);
792
793 let final_enum_name = if let Some(existing) = enum_registry
795 .get(&context_key)
796 .or_else(|| enum_registry.get(&enum_key))
797 {
798 existing.clone()
799 } else {
800 enum_registry.insert(context_key.clone(), enum_name.clone());
801 enum_registry.insert(enum_key.clone(), enum_name.clone());
802 enum_name
803 };
804
805 Ok(Some(ParameterInfo {
806 name,
807 param_type: ParameterType::Enum(final_enum_name.clone()),
808 enum_values: Some(enum_values),
809 enum_type_name: Some(final_enum_name),
810 is_array: false,
811 array_item_type: None,
812 style: Some("simple".to_string()), explode: Some(false), description: description.clone(),
815 }))
816 } else {
817 Ok(Some(ParameterInfo {
818 name,
819 param_type: ParameterType::String,
820 enum_values: None,
821 enum_type_name: None,
822 is_array: false,
823 array_item_type: None,
824 style: Some("simple".to_string()),
825 explode: Some(false),
826 description: description.clone(),
827 }))
828 }
829 }
830 Type::Number(_) => Ok(Some(ParameterInfo {
831 name,
832 param_type: ParameterType::Number,
833 enum_values: None,
834 enum_type_name: None,
835 is_array: false,
836 array_item_type: None,
837 style: Some("simple".to_string()),
838 explode: Some(false),
839 description: description.clone(),
840 })),
841 Type::Integer(_) => Ok(Some(ParameterInfo {
842 name,
843 param_type: ParameterType::Integer,
844 enum_values: None,
845 enum_type_name: None,
846 is_array: false,
847 array_item_type: None,
848 style: Some("simple".to_string()),
849 explode: Some(false),
850 description: description.clone(),
851 })),
852 Type::Boolean(_) => Ok(Some(ParameterInfo {
853 name,
854 param_type: ParameterType::Boolean,
855 enum_values: None,
856 enum_type_name: None,
857 is_array: false,
858 array_item_type: None,
859 style: Some("simple".to_string()),
860 explode: Some(false),
861 description: description.clone(),
862 })),
863 Type::Object(_) => Ok(Some(ParameterInfo {
864 name,
865 param_type: ParameterType::String,
866 enum_values: None,
867 enum_type_name: None,
868 is_array: false,
869 array_item_type: None,
870 style: Some("simple".to_string()),
871 explode: Some(false),
872 description: description.clone(),
873 })),
874 Type::Array(array) => {
875 let item_type = if let Some(items) = &array.items {
876 match items {
877 ReferenceOr::Reference { reference } => {
878 if let Some(ref_name) = get_schema_name_from_ref(reference) {
879 to_pascal_case(&ref_name)
880 } else {
881 "string".to_string()
882 }
883 }
884 ReferenceOr::Item(item_schema) => {
885 match &item_schema.schema_kind {
887 SchemaKind::Type(item_type) => match item_type {
888 Type::String(_) => "string".to_string(),
889 Type::Number(_) => "number".to_string(),
890 Type::Integer(_) => "number".to_string(),
891 Type::Boolean(_) => "boolean".to_string(),
892 _ => "string".to_string(),
893 },
894 _ => "string".to_string(),
895 }
896 }
897 }
898 } else {
899 "string".to_string()
900 };
901
902 Ok(Some(ParameterInfo {
903 name,
904 param_type: ParameterType::Array(item_type.clone()),
905 enum_values: None,
906 enum_type_name: None,
907 is_array: true,
908 array_item_type: Some(item_type),
909 style: Some("form".to_string()), explode: Some(true), description: description.clone(),
912 }))
913 }
914 }
915 }
916 _ => Ok(Some(ParameterInfo {
917 name,
918 param_type: ParameterType::String,
919 enum_values: None,
920 enum_type_name: None,
921 is_array: false,
922 array_item_type: None,
923 style: Some("simple".to_string()),
924 explode: Some(false),
925 description: description.clone(),
926 })),
927 }
928 } else {
929 Ok(Some(ParameterInfo {
931 name,
932 param_type: ParameterType::String,
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 }
942}
943
944fn extract_request_body(openapi: &OpenAPI, operation: &Operation) -> Result<Option<(String, Option<String>)>> {
945 if let Some(request_body) = &operation.request_body {
946 match request_body {
947 ReferenceOr::Reference { reference } => {
948 match resolve_request_body_ref(openapi, reference) {
950 Ok(ReferenceOr::Item(body)) => {
951 let description = body.description.clone();
952 if let Some(json_media) = body.content.get("application/json") {
953 if let Some(schema_ref) = &json_media.schema {
954 match schema_ref {
955 ReferenceOr::Reference { reference } => {
956 if let Some(ref_name) = get_schema_name_from_ref(reference)
957 {
958 Ok(Some((to_pascal_case(&ref_name), description)))
959 } else {
960 Ok(Some(("any".to_string(), description)))
961 }
962 }
963 ReferenceOr::Item(_schema) => {
964 Ok(Some(("any".to_string(), description)))
970 }
971 }
972 } else {
973 Ok(Some(("any".to_string(), description)))
974 }
975 } else {
976 Ok(Some(("any".to_string(), description)))
977 }
978 }
979 Ok(ReferenceOr::Reference { .. }) => {
980 Ok(Some(("any".to_string(), None)))
982 }
983 Err(_) => {
984 Ok(Some(("any".to_string(), None)))
986 }
987 }
988 }
989 ReferenceOr::Item(body) => {
990 let description = body.description.clone();
991 if let Some(json_media) = body.content.get("application/json") {
992 if let Some(schema_ref) = &json_media.schema {
993 match schema_ref {
994 ReferenceOr::Reference { reference } => {
995 if let Some(ref_name) = get_schema_name_from_ref(reference) {
996 Ok(Some((to_pascal_case(&ref_name), description)))
997 } else {
998 Ok(Some(("any".to_string(), description)))
999 }
1000 }
1001 ReferenceOr::Item(_schema) => {
1002 Ok(Some(("any".to_string(), description)))
1008 }
1009 }
1010 } else {
1011 Ok(Some(("any".to_string(), description)))
1012 }
1013 } else {
1014 Ok(Some(("any".to_string(), description)))
1015 }
1016 }
1017 }
1018 } else {
1019 Ok(None)
1020 }
1021}
1022
1023#[allow(dead_code)]
1024fn extract_response_type(openapi: &OpenAPI, operation: &Operation) -> Result<String> {
1025 if let Some(success_response) = operation
1027 .responses
1028 .responses
1029 .get(&openapiv3::StatusCode::Code(200))
1030 {
1031 match success_response {
1032 ReferenceOr::Reference { reference } => {
1033 match resolve_response_ref(openapi, reference) {
1035 Ok(ReferenceOr::Item(response)) => {
1036 if let Some(json_media) = response.content.get("application/json") {
1037 if let Some(schema_ref) = &json_media.schema {
1038 match schema_ref {
1039 ReferenceOr::Reference { reference } => {
1040 if let Some(ref_name) = get_schema_name_from_ref(reference)
1041 {
1042 Ok(to_pascal_case(&ref_name))
1043 } else {
1044 Ok("any".to_string())
1045 }
1046 }
1047 ReferenceOr::Item(_) => Ok("any".to_string()),
1048 }
1049 } else {
1050 Ok("any".to_string())
1051 }
1052 } else {
1053 Ok("any".to_string())
1054 }
1055 }
1056 Ok(ReferenceOr::Reference { .. }) => {
1057 Ok("any".to_string())
1059 }
1060 Err(_) => {
1061 Ok("any".to_string())
1063 }
1064 }
1065 }
1066 ReferenceOr::Item(response) => {
1067 if let Some(json_media) = response.content.get("application/json") {
1068 if let Some(schema_ref) = &json_media.schema {
1069 match schema_ref {
1070 ReferenceOr::Reference { reference } => {
1071 if let Some(ref_name) = get_schema_name_from_ref(reference) {
1072 Ok(to_pascal_case(&ref_name))
1073 } else {
1074 Ok("any".to_string())
1075 }
1076 }
1077 ReferenceOr::Item(_) => Ok("any".to_string()),
1078 }
1079 } else {
1080 Ok("any".to_string())
1081 }
1082 } else {
1083 Ok("any".to_string())
1084 }
1085 }
1086 }
1087 } else {
1088 Ok("any".to_string())
1089 }
1090}
1091
1092fn extract_all_responses(openapi: &OpenAPI, operation: &Operation) -> Result<Vec<ResponseInfo>> {
1093 let mut responses = Vec::new();
1094
1095 for (status_code, response_ref) in &operation.responses.responses {
1096 let status_num = match status_code {
1097 openapiv3::StatusCode::Code(code) => *code,
1098 openapiv3::StatusCode::Range(range) => {
1099 match format!("{:?}", range).as_str() {
1102 s if s.contains("4") => 400,
1103 s if s.contains("5") => 500,
1104 _ => 0,
1105 }
1106 }
1107 };
1108
1109 let (description, body_type) = match response_ref {
1111 ReferenceOr::Reference { reference } => {
1112 match resolve_response_ref(openapi, reference) {
1113 Ok(ReferenceOr::Item(response)) => {
1114 let desc = response.description.clone();
1115 let body = extract_response_body_type(openapi, &response);
1116 (Some(desc), body)
1117 }
1118 _ => (None, "any".to_string()),
1119 }
1120 }
1121 ReferenceOr::Item(response) => {
1122 let desc = response.description.clone();
1123 let body = extract_response_body_type(openapi, response);
1124 (Some(desc), body)
1125 }
1126 };
1127
1128 responses.push(ResponseInfo {
1129 status_code: status_num,
1130 body_type,
1131 description,
1132 });
1133 }
1134
1135 Ok(responses)
1136}
1137
1138#[allow(dead_code)]
1139fn extract_error_responses(openapi: &OpenAPI, operation: &Operation) -> Result<Vec<ErrorResponse>> {
1140 let all_responses = extract_all_responses(openapi, operation)?;
1141 let errors: Vec<ErrorResponse> = all_responses
1142 .iter()
1143 .filter(|r| r.status_code < 200 || r.status_code >= 300)
1144 .map(|r| ErrorResponse {
1145 status_code: r.status_code,
1146 body_type: r.body_type.clone(),
1147 })
1148 .collect();
1149 Ok(errors)
1150}
1151
1152fn generate_response_types(
1153 func_name: &str,
1154 success_responses: &[ResponseInfo],
1155 error_responses: &[ResponseInfo],
1156 _namespace_name: &str,
1157 common_schemas: &[String],
1158 enum_types: &[(String, Vec<String>)],
1159) -> Vec<TypeScriptType> {
1160 let mut types = Vec::new();
1161 let type_name_base = to_pascal_case(func_name);
1162
1163 for (enum_name, enum_values) in enum_types {
1165 let variants = enum_values
1166 .iter()
1167 .map(|v| format!("\"{}\"", v))
1168 .collect::<Vec<_>>()
1169 .join(" |\n");
1170 let enum_type = format!("export type {} =\n{};", enum_name, variants);
1171 types.push(TypeScriptType { content: enum_type });
1172 }
1173
1174 if !error_responses.is_empty() {
1176 let mut error_fields = Vec::new();
1177 for error in error_responses {
1178 if error.status_code > 0 {
1179 let qualified_type = if error.body_type != "any" {
1182 if common_schemas.contains(&error.body_type) {
1183 format!("Common.{}", error.body_type)
1184 } else {
1185 error.body_type.clone()
1187 }
1188 } else {
1189 "any".to_string()
1190 };
1191
1192 let description = error
1193 .description
1194 .as_ref()
1195 .map(|d| format!(" /**\n * {}\n */", d))
1196 .unwrap_or_default();
1197
1198 error_fields.push(format!(
1199 "{}\n {}: {};",
1200 description, error.status_code, qualified_type
1201 ));
1202 }
1203 }
1204
1205 if !error_fields.is_empty() {
1206 let errors_type = format!(
1207 "export type {}Errors = {{\n{}\n}};",
1208 type_name_base,
1209 error_fields.join("\n")
1210 );
1211 types.push(TypeScriptType {
1212 content: errors_type,
1213 });
1214
1215 let error_union_type = format!(
1217 "export type {}Error = {}Errors[keyof {}Errors];",
1218 type_name_base, type_name_base, type_name_base
1219 );
1220 types.push(TypeScriptType {
1221 content: error_union_type,
1222 });
1223 }
1224 }
1225
1226 let success_with_schemas: Vec<&ResponseInfo> = success_responses
1228 .iter()
1229 .filter(|r| r.status_code >= 200 && r.status_code < 300 && r.body_type != "any")
1230 .collect();
1231
1232 if !success_with_schemas.is_empty() {
1233 let mut response_fields = Vec::new();
1234 for response in success_with_schemas {
1235 let qualified_type = if common_schemas.contains(&response.body_type) {
1238 format!("Common.{}", response.body_type)
1239 } else {
1240 response.body_type.clone()
1242 };
1243
1244 let description = response
1245 .description
1246 .as_ref()
1247 .map(|d| format!(" /**\n * {}\n */", d))
1248 .unwrap_or_default();
1249
1250 response_fields.push(format!(
1251 "{}\n {}: {};",
1252 description, response.status_code, qualified_type
1253 ));
1254 }
1255
1256 if !response_fields.is_empty() {
1257 let responses_type = format!(
1258 "export type {}Responses = {{\n{}\n}};",
1259 type_name_base,
1260 response_fields.join("\n")
1261 );
1262 types.push(TypeScriptType {
1263 content: responses_type,
1264 });
1265 }
1266 }
1267
1268 types
1269}
1270
1271fn extract_response_body_type(_openapi: &OpenAPI, response: &openapiv3::Response) -> String {
1272 if let Some(json_media) = response.content.get("application/json") {
1273 if let Some(schema_ref) = &json_media.schema {
1274 match schema_ref {
1275 ReferenceOr::Reference { reference } => {
1276 if let Some(ref_name) = get_schema_name_from_ref(reference) {
1277 to_pascal_case(&ref_name)
1278 } else {
1279 "any".to_string()
1280 }
1281 }
1282 ReferenceOr::Item(_) => "any".to_string(),
1283 }
1284 } else {
1285 "any".to_string()
1286 }
1287 } else {
1288 "any".to_string()
1289 }
1290}
1291
1292fn generate_function_name_from_path(path: &str, method: &str) -> String {
1293 let path_parts: Vec<&str> = path
1294 .trim_start_matches('/')
1295 .split('/')
1296 .filter(|p| !p.starts_with('{'))
1297 .collect();
1298
1299 let method_upper = method.to_uppercase();
1301 let method_lower = method.to_lowercase();
1302 let method_prefix = match method_upper.as_str() {
1303 "GET" => "get",
1304 "POST" => "create",
1305 "PUT" => "update",
1306 "DELETE" => "delete",
1307 "PATCH" => "patch",
1308 _ => method_lower.as_str(),
1309 };
1310
1311 let base_name = if path_parts.is_empty() {
1312 method_prefix.to_string()
1313 } else {
1314 let resource_name = if path_parts.len() > 1 {
1316 path_parts.last().unwrap_or(&"")
1318 } else {
1319 path_parts.first().unwrap_or(&"")
1320 };
1321
1322 if resource_name.ends_with("s") && path.contains('{') {
1324 let singular = &resource_name[..resource_name.len() - 1];
1326 format!("{}{}ById", method_prefix, to_pascal_case(singular))
1327 } else if path.contains('{') {
1328 format!("{}{}ById", method_prefix, to_pascal_case(resource_name))
1330 } else {
1331 format!("{}{}", method_prefix, to_pascal_case(resource_name))
1333 }
1334 };
1335
1336 to_camel_case(&base_name)
1337}