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