1use crate::error::Result;
2use crate::generator::swagger_parser::{
3 get_schema_name_from_ref, resolve_parameter_ref, resolve_request_body_ref,
4 resolve_response_ref, OperationInfo,
5};
6use crate::generator::swagger_parser::resolve_ref;
7use crate::generator::ts_typings::TypeScriptType;
8use crate::generator::utils::{to_camel_case, to_pascal_case, sanitize_module_name};
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(openapi, operations, module_name, common_schemas, &mut std::collections::HashMap::new())
63}
64
65pub fn generate_api_client_with_registry(
66 openapi: &OpenAPI,
67 operations: &[OperationInfo],
68 module_name: &str,
69 common_schemas: &[String],
70 enum_registry: &mut std::collections::HashMap<String, String>,
71) -> Result<ApiGenerationResult> {
72 let mut functions = Vec::new();
73 let mut response_types = Vec::new();
74
75 for op_info in operations {
76 let result = generate_function_for_operation(openapi, op_info, module_name, common_schemas, enum_registry)?;
77 functions.push(result.function);
78 response_types.extend(result.response_types);
79 }
80
81 Ok(ApiGenerationResult {
82 functions,
83 response_types,
84 })
85}
86
87struct FunctionGenerationResult {
88 function: ApiFunction,
89 response_types: Vec<TypeScriptType>,
90}
91
92fn generate_function_for_operation(
93 openapi: &OpenAPI,
94 op_info: &OperationInfo,
95 module_name: &str,
96 common_schemas: &[String],
97 enum_registry: &mut std::collections::HashMap<String, String>,
98) -> Result<FunctionGenerationResult> {
99 let operation = &op_info.operation;
100 let method = op_info.method.to_lowercase();
101
102 let func_name = if let Some(operation_id) = &operation.operation_id {
104 to_camel_case(operation_id)
105 } else {
106 generate_function_name_from_path(&op_info.path, &op_info.method)
107 };
108
109 let path_params = extract_path_parameters(openapi, operation, enum_registry)?;
111
112 let query_params = extract_query_parameters(openapi, operation, enum_registry)?;
114
115 let request_body = extract_request_body(openapi, operation)?;
117
118 let all_responses = extract_all_responses(openapi, operation)?;
120
121 let success_responses: Vec<ResponseInfo> = all_responses
123 .iter()
124 .filter(|r| r.status_code >= 200 && r.status_code < 300)
125 .cloned()
126 .collect();
127 let error_responses: Vec<ResponseInfo> = all_responses
128 .iter()
129 .filter(|r| r.status_code < 200 || r.status_code >= 300)
130 .cloned()
131 .collect();
132
133 let response_type = success_responses
135 .iter()
136 .find(|r| r.status_code == 200)
137 .map(|r| r.body_type.clone())
138 .unwrap_or_else(|| "any".to_string());
139
140 let namespace_name = to_pascal_case(&module_name.replace("/", "_"));
143
144 let mut params = Vec::new();
146 let mut path_template = op_info.path.clone();
147 let mut enum_types = Vec::new();
148
149 for param in &path_params {
151 let param_type = match ¶m.param_type {
152 ParameterType::Enum(enum_name) => {
153 enum_types.push((enum_name.clone(), param.enum_values.clone().unwrap_or_default()));
154 enum_name.clone()
155 }
156 ParameterType::String => "string".to_string(),
157 ParameterType::Number => "number".to_string(),
158 ParameterType::Integer => "number".to_string(),
159 ParameterType::Boolean => "boolean".to_string(),
160 ParameterType::Array(_) => "string".to_string(), };
162 params.push(format!("{}: {}", param.name, param_type));
163 path_template =
164 path_template.replace(&format!("{{{}}}", param.name), &format!("${{{}}}", param.name));
165 }
166
167 if let Some(body_type) = &request_body {
169 if body_type == "any" {
171 params.push("body: any".to_string());
172 } else {
173 let qualified_body_type = if common_schemas.contains(body_type) {
174 format!("Common.{}", body_type)
175 } else {
176 format!("{}.{}", namespace_name, body_type)
177 };
178 params.push(format!("body: {}", qualified_body_type));
179 }
180 }
181
182 if !query_params.is_empty() {
185 let mut query_fields = Vec::new();
186 for param in &query_params {
187 let param_type = match ¶m.param_type {
188 ParameterType::Enum(enum_name) => {
189 enum_types
190 .push((enum_name.clone(), param.enum_values.clone().unwrap_or_default()));
191 enum_name.clone()
192 }
193 ParameterType::Array(item_type) => {
194 format!("{}[]", item_type)
195 }
196 ParameterType::String => "string".to_string(),
197 ParameterType::Number => "number".to_string(),
198 ParameterType::Integer => "number".to_string(),
199 ParameterType::Boolean => "boolean".to_string(),
200 };
201 query_fields.push(format!("{}?: {}", param.name, param_type));
202 }
203 let query_type = format!("{{ {} }}", query_fields.join(", "));
204 params.push(format!("query?: {}", query_type));
205 }
206
207 let params_str = params.join(", ");
208
209 let mut body_lines = Vec::new();
211
212 let mut url_template = op_info.path.clone();
214 for param in &path_params {
215 url_template = url_template.replace(&format!("{{{}}}", param.name), &format!("${{{}}}", param.name));
216 }
217
218 if !query_params.is_empty() {
220 body_lines.push(" const queryString = new URLSearchParams();".to_string());
221 for param in &query_params {
222 if param.is_array {
223 let explode = param.explode.unwrap_or(true);
224 if explode {
225 body_lines.push(format!(
227 " if (query?.{}) {{",
228 param.name
229 ));
230 body_lines.push(format!(
231 " query.{}.forEach((item) => queryString.append(\"{}\", String(item)));",
232 param.name, param.name
233 ));
234 body_lines.push(" }".to_string());
235 } else {
236 body_lines.push(format!(
238 " if (query?.{}) queryString.append(\"{}\", query.{}.join(\",\"));",
239 param.name, param.name, param.name
240 ));
241 }
242 } else {
243 body_lines.push(format!(
244 " if (query?.{}) queryString.append(\"{}\", String(query.{}));",
245 param.name, param.name, param.name
246 ));
247 }
248 }
249 body_lines.push(" const queryStr = queryString.toString();".to_string());
250 body_lines.push(format!(
251 " const url = `{}` + (queryStr ? `?${{queryStr}}` : '');",
252 url_template
253 ));
254 } else {
255 body_lines.push(format!(" const url = `{}`;", url_template));
256 }
257
258 let http_method = match method.to_uppercase().as_str() {
260 "GET" => "get",
261 "POST" => "post",
262 "PUT" => "put",
263 "DELETE" => "delete",
264 "PATCH" => "patch",
265 "HEAD" => "head",
266 "OPTIONS" => "options",
267 _ => "get",
268 };
269
270 let qualified_response_type_for_generic = if response_type != "any" {
272 let is_common = common_schemas.contains(&response_type);
273 if is_common {
274 format!("Common.{}", response_type)
275 } else {
276 format!("{}.{}", namespace_name, response_type)
277 }
278 } else {
279 response_type.clone()
280 };
281
282 if let Some(_body_type) = &request_body {
283 body_lines.push(format!(" return http.{}(url, body);", http_method));
284 } else {
285 body_lines.push(format!(
286 " return http.{}<{}>(url);",
287 http_method, qualified_response_type_for_generic
288 ));
289 }
290
291 let depth = module_name.matches('/').count();
294 let http_relative_path = if depth == 0 {
295 "../http"
296 } else {
297 &format!("{}../http", "../".repeat(depth))
298 };
299 let http_import = http_relative_path;
300
301 let mut type_imports = String::new();
304 let mut needs_common_import = false;
305 let mut needs_namespace_import = false;
306
307 if response_type != "any" {
309 let is_common = common_schemas.contains(&response_type);
310 if is_common {
311 needs_common_import = true;
312 } else {
313 needs_namespace_import = true;
314 }
315 }
316
317 if let Some(body_type) = &request_body {
319 if body_type != "any" {
320 if common_schemas.contains(body_type) {
321 needs_common_import = true;
322 } else {
323 needs_namespace_import = true;
324 }
325 }
326 }
327
328 if needs_common_import {
330 let schemas_depth = depth + 1; let common_import = format!("{}../schemas/common", "../".repeat(schemas_depth));
332 type_imports.push_str(&format!(
333 "import * as Common from \"{}\";\n",
334 common_import
335 ));
336 }
337 if needs_namespace_import {
338 let schemas_depth = depth + 1; let sanitized_module_name = sanitize_module_name(module_name);
340 let schemas_import = format!("{}../schemas/{}", "../".repeat(schemas_depth), sanitized_module_name);
341 type_imports.push_str(&format!(
342 "import * as {} from \"{}\";\n",
343 namespace_name, schemas_import
344 ));
345 }
346
347 let response_types = generate_response_types(
349 &func_name,
350 &success_responses,
351 &error_responses,
352 &namespace_name,
353 common_schemas,
354 &enum_types,
355 );
356
357 let type_name_base = to_pascal_case(&func_name);
359 let mut response_type_imports = Vec::new();
360
361 let errors_with_schemas: Vec<&ResponseInfo> = error_responses
363 .iter()
364 .filter(|r| r.status_code > 0)
365 .collect();
366 if !errors_with_schemas.is_empty() {
367 response_type_imports.push(format!("{}Errors", type_name_base));
368 response_type_imports.push(format!("{}Error", type_name_base));
369 }
370
371 let success_with_schemas: Vec<&ResponseInfo> = success_responses
373 .iter()
374 .filter(|r| r.status_code >= 200 && r.status_code < 300 && r.body_type != "any")
375 .collect();
376 if !success_with_schemas.is_empty() {
377 response_type_imports.push(format!("{}Responses", type_name_base));
378 }
379
380 if !response_type_imports.is_empty() {
382 let schemas_depth = depth + 1; let sanitized_module_name = sanitize_module_name(module_name);
385 let schemas_import = format!("{}../schemas/{}", "../".repeat(schemas_depth), sanitized_module_name);
386 let type_import_line = format!(
387 "import type {{ {} }} from \"{}\";",
388 response_type_imports.join(", "),
389 schemas_import
390 );
391 if type_imports.is_empty() {
392 type_imports = format!("{}\n", type_import_line);
393 } else {
394 type_imports = format!("{}\n{}", type_imports.trim_end(), type_import_line);
395 }
396 }
397
398 if !enum_types.is_empty() {
400 let schemas_depth = depth + 1; let sanitized_module_name = sanitize_module_name(module_name);
402 let schemas_import = format!("{}../schemas/{}", "../".repeat(schemas_depth), sanitized_module_name);
403 let enum_names: Vec<String> = enum_types.iter().map(|(name, _)| name.clone()).collect();
404 let enum_import_line = format!(
405 "import type {{ {} }} from \"{}\";",
406 enum_names.join(", "),
407 schemas_import
408 );
409 if type_imports.is_empty() {
410 type_imports = format!("{}\n", enum_import_line);
411 } else {
412 type_imports = format!("{}\n{}", type_imports.trim_end(), enum_import_line);
413 }
414 }
415
416 if !type_imports.is_empty() && !type_imports.ends_with('\n') {
418 type_imports.push('\n');
419 }
420
421 let has_responses_type = response_type_imports.iter().any(|imp| imp.contains("Responses"));
423 let return_type = if has_responses_type {
424 if let Some(primary_response) = success_responses.iter().find(|r| r.status_code == 200 && r.body_type != "any") {
426 format!(": Promise<{}Responses[200]>", type_name_base)
427 } else if let Some(first_success) = success_responses.iter().find(|r| r.status_code >= 200 && r.status_code < 300 && r.body_type != "any") {
428 format!(": Promise<{}Responses[{}]>", type_name_base, first_success.status_code)
429 } else {
430 String::new()
431 }
432 } else if !success_responses.is_empty() {
433 if let Some(primary_response) = success_responses.iter().find(|r| r.status_code == 200) {
435 if primary_response.body_type != "any" {
436 let qualified = if common_schemas.contains(&primary_response.body_type) {
437 format!("Common.{}", primary_response.body_type)
438 } else {
439 format!("{}.{}", namespace_name, primary_response.body_type)
440 };
441 format!(": Promise<{}>", qualified)
442 } else {
443 String::new()
444 }
445 } else {
446 String::new()
447 }
448 } else {
449 String::new()
450 };
451
452 let function_body = body_lines.join("\n");
453
454 let content = if params_str.is_empty() {
457 format!(
458 "import {{ http }} from \"{}\";\n{}{}export const {} = async (){} => {{\n{}\n}};",
459 http_import, type_imports, if !type_imports.is_empty() { "\n" } else { "" }, func_name, return_type, function_body
460 )
461 } else {
462 format!(
463 "import {{ http }} from \"{}\";\n{}{}export const {} = async ({}){} => {{\n{}\n}};",
464 http_import, type_imports, if !type_imports.is_empty() { "\n" } else { "" }, func_name, params_str, return_type, function_body
465 )
466 };
467
468 Ok(FunctionGenerationResult {
469 function: ApiFunction { content },
470 response_types,
471 })
472}
473
474fn extract_path_parameters(
475 openapi: &OpenAPI,
476 operation: &Operation,
477 enum_registry: &mut std::collections::HashMap<String, String>,
478) -> Result<Vec<ParameterInfo>> {
479 let mut params = Vec::new();
480
481 for param_ref in &operation.parameters {
482 match param_ref {
483 ReferenceOr::Reference { reference } => {
484 let mut current_ref = Some(reference.clone());
486 let mut depth = 0;
487 while let Some(ref_path) = current_ref.take() {
488 if depth > 3 {
489 break; }
491 match resolve_parameter_ref(openapi, &ref_path) {
492 Ok(ReferenceOr::Item(param)) => {
493 if let Parameter::Path { parameter_data, .. } = param {
494 if let Some(param_info) = extract_parameter_info(openapi, ¶meter_data, enum_registry)? {
495 params.push(param_info);
496 }
497 }
498 break;
499 }
500 Ok(ReferenceOr::Reference {
501 reference: nested_ref,
502 }) => {
503 current_ref = Some(nested_ref);
504 depth += 1;
505 }
506 Err(_) => {
507 break;
509 }
510 }
511 }
512 }
513 ReferenceOr::Item(param) => {
514 if let Parameter::Path { parameter_data, .. } = param {
515 if let Some(param_info) = extract_parameter_info(openapi, ¶meter_data, enum_registry)? {
516 params.push(param_info);
517 }
518 }
519 }
520 }
521 }
522
523 Ok(params)
524}
525
526fn extract_query_parameters(
527 openapi: &OpenAPI,
528 operation: &Operation,
529 enum_registry: &mut std::collections::HashMap<String, String>,
530) -> Result<Vec<ParameterInfo>> {
531 let mut params = Vec::new();
532
533 for param_ref in &operation.parameters {
534 match param_ref {
535 ReferenceOr::Reference { reference } => {
536 let mut current_ref = Some(reference.clone());
538 let mut depth = 0;
539 while let Some(ref_path) = current_ref.take() {
540 if depth > 3 {
541 break; }
543 match resolve_parameter_ref(openapi, &ref_path) {
544 Ok(ReferenceOr::Item(param)) => {
545 if let Parameter::Query { parameter_data, style, .. } = param {
546 if let Some(mut param_info) = extract_parameter_info(openapi, ¶meter_data, enum_registry)? {
547 param_info.style = Some(format!("{:?}", style));
549 param_info.explode = Some(parameter_data.explode.unwrap_or(false));
551 params.push(param_info);
552 }
553 }
554 break;
555 }
556 Ok(ReferenceOr::Reference {
557 reference: nested_ref,
558 }) => {
559 current_ref = Some(nested_ref);
560 depth += 1;
561 }
562 Err(_) => {
563 break;
565 }
566 }
567 }
568 }
569 ReferenceOr::Item(param) => {
570 if let Parameter::Query { parameter_data, style, .. } = param {
571 if let Some(mut param_info) = extract_parameter_info(openapi, ¶meter_data, enum_registry)? {
572 param_info.style = Some(format!("{:?}", style));
574 param_info.explode = Some(parameter_data.explode.unwrap_or(false));
576 params.push(param_info);
577 }
578 }
579 }
580 }
581 }
582
583 Ok(params)
584}
585
586fn extract_parameter_info(
587 openapi: &OpenAPI,
588 parameter_data: &openapiv3::ParameterData,
589 enum_registry: &mut std::collections::HashMap<String, String>,
590) -> Result<Option<ParameterInfo>> {
591 let name = parameter_data.name.clone();
592
593 let schema = match ¶meter_data.format {
595 openapiv3::ParameterSchemaOrContent::Schema(schema_ref) => {
596 match schema_ref {
597 ReferenceOr::Reference { reference } => {
598 resolve_ref(openapi, reference).ok().and_then(|r| match r {
599 ReferenceOr::Item(s) => Some(s),
600 _ => None,
601 })
602 }
603 ReferenceOr::Item(s) => Some(s.clone()),
604 }
605 }
606 _ => None,
607 };
608
609 if let Some(schema) = schema {
610 match &schema.schema_kind {
611 SchemaKind::Type(type_) => {
612 match type_ {
613 Type::String(string_type) => {
614 if !string_type.enumeration.is_empty() {
616 let mut enum_values: Vec<String> = string_type
617 .enumeration
618 .iter()
619 .filter_map(|v| v.as_ref().cloned())
620 .collect();
621 enum_values.sort();
622 let enum_key = enum_values.join(",");
623
624 let enum_name = format!("{}Enum", to_pascal_case(&name));
626 let context_key = format!("{}:{}", enum_key, name);
627
628 let final_enum_name = if let Some(existing) = enum_registry.get(&context_key).or_else(|| enum_registry.get(&enum_key)) {
630 existing.clone()
631 } else {
632 enum_registry.insert(context_key.clone(), enum_name.clone());
633 enum_registry.insert(enum_key.clone(), enum_name.clone());
634 enum_name
635 };
636
637 Ok(Some(ParameterInfo {
638 name,
639 param_type: ParameterType::Enum(final_enum_name.clone()),
640 enum_values: Some(enum_values),
641 enum_type_name: Some(final_enum_name),
642 is_array: false,
643 array_item_type: None,
644 style: Some("simple".to_string()), explode: Some(false), }))
647 } else {
648 Ok(Some(ParameterInfo {
649 name,
650 param_type: ParameterType::String,
651 enum_values: None,
652 enum_type_name: None,
653 is_array: false,
654 array_item_type: None,
655 style: Some("simple".to_string()),
656 explode: Some(false),
657 }))
658 }
659 }
660 Type::Number(_) => Ok(Some(ParameterInfo {
661 name,
662 param_type: ParameterType::Number,
663 enum_values: None,
664 enum_type_name: None,
665 is_array: false,
666 array_item_type: None,
667 style: Some("simple".to_string()),
668 explode: Some(false),
669 })),
670 Type::Integer(_) => Ok(Some(ParameterInfo {
671 name,
672 param_type: ParameterType::Integer,
673 enum_values: None,
674 enum_type_name: None,
675 is_array: false,
676 array_item_type: None,
677 style: Some("simple".to_string()),
678 explode: Some(false),
679 })),
680 Type::Boolean(_) => Ok(Some(ParameterInfo {
681 name,
682 param_type: ParameterType::Boolean,
683 enum_values: None,
684 enum_type_name: None,
685 is_array: false,
686 array_item_type: None,
687 style: Some("simple".to_string()),
688 explode: Some(false),
689 })),
690 Type::Object(_) => Ok(Some(ParameterInfo {
691 name,
692 param_type: ParameterType::String,
693 enum_values: None,
694 enum_type_name: None,
695 is_array: false,
696 array_item_type: None,
697 style: Some("simple".to_string()),
698 explode: Some(false),
699 })),
700 Type::Array(array) => {
701 let item_type = if let Some(items) = &array.items {
702 match items {
703 ReferenceOr::Reference { reference } => {
704 if let Some(ref_name) = get_schema_name_from_ref(reference) {
705 to_pascal_case(&ref_name)
706 } else {
707 "string".to_string()
708 }
709 }
710 ReferenceOr::Item(item_schema) => {
711 match &item_schema.schema_kind {
713 SchemaKind::Type(item_type) => {
714 match item_type {
715 Type::String(_) => "string".to_string(),
716 Type::Number(_) => "number".to_string(),
717 Type::Integer(_) => "number".to_string(),
718 Type::Boolean(_) => "boolean".to_string(),
719 _ => "string".to_string(),
720 }
721 }
722 _ => "string".to_string(),
723 }
724 }
725 }
726 } else {
727 "string".to_string()
728 };
729
730 Ok(Some(ParameterInfo {
731 name,
732 param_type: ParameterType::Array(item_type.clone()),
733 enum_values: None,
734 enum_type_name: None,
735 is_array: true,
736 array_item_type: Some(item_type),
737 style: Some("form".to_string()), explode: Some(true), }))
740 }
741 }
742 }
743 _ => Ok(Some(ParameterInfo {
744 name,
745 param_type: ParameterType::String,
746 enum_values: None,
747 enum_type_name: None,
748 is_array: false,
749 array_item_type: None,
750 style: Some("simple".to_string()),
751 explode: Some(false),
752 })),
753 }
754 } else {
755 Ok(Some(ParameterInfo {
757 name,
758 param_type: ParameterType::String,
759 enum_values: None,
760 enum_type_name: None,
761 is_array: false,
762 array_item_type: None,
763 style: Some("simple".to_string()),
764 explode: Some(false),
765 }))
766 }
767}
768
769fn extract_request_body(openapi: &OpenAPI, operation: &Operation) -> Result<Option<String>> {
770 if let Some(request_body) = &operation.request_body {
771 match request_body {
772 ReferenceOr::Reference { reference } => {
773 match resolve_request_body_ref(openapi, reference) {
775 Ok(ReferenceOr::Item(body)) => {
776 if let Some(json_media) = body.content.get("application/json") {
777 if let Some(schema_ref) = &json_media.schema {
778 match schema_ref {
779 ReferenceOr::Reference { reference } => {
780 if let Some(ref_name) = get_schema_name_from_ref(reference)
781 {
782 Ok(Some(to_pascal_case(&ref_name)))
783 } else {
784 Ok(Some("any".to_string()))
785 }
786 }
787 ReferenceOr::Item(_schema) => {
788 Ok(Some("any".to_string()))
794 }
795 }
796 } else {
797 Ok(Some("any".to_string()))
798 }
799 } else {
800 Ok(Some("any".to_string()))
801 }
802 }
803 Ok(ReferenceOr::Reference { .. }) => {
804 Ok(Some("any".to_string()))
806 }
807 Err(_) => {
808 Ok(Some("any".to_string()))
810 }
811 }
812 }
813 ReferenceOr::Item(body) => {
814 if let Some(json_media) = body.content.get("application/json") {
815 if let Some(schema_ref) = &json_media.schema {
816 match schema_ref {
817 ReferenceOr::Reference { reference } => {
818 if let Some(ref_name) = get_schema_name_from_ref(reference) {
819 Ok(Some(to_pascal_case(&ref_name)))
820 } else {
821 Ok(Some("any".to_string()))
822 }
823 }
824 ReferenceOr::Item(_schema) => {
825 Ok(Some("any".to_string()))
831 }
832 }
833 } else {
834 Ok(Some("any".to_string()))
835 }
836 } else {
837 Ok(Some("any".to_string()))
838 }
839 }
840 }
841 } else {
842 Ok(None)
843 }
844}
845
846fn extract_response_type(openapi: &OpenAPI, operation: &Operation) -> Result<String> {
847 if let Some(success_response) = operation
849 .responses
850 .responses
851 .get(&openapiv3::StatusCode::Code(200))
852 {
853 match success_response {
854 ReferenceOr::Reference { reference } => {
855 match resolve_response_ref(openapi, reference) {
857 Ok(ReferenceOr::Item(response)) => {
858 if let Some(json_media) = response.content.get("application/json") {
859 if let Some(schema_ref) = &json_media.schema {
860 match schema_ref {
861 ReferenceOr::Reference { reference } => {
862 if let Some(ref_name) = get_schema_name_from_ref(reference)
863 {
864 Ok(to_pascal_case(&ref_name))
865 } else {
866 Ok("any".to_string())
867 }
868 }
869 ReferenceOr::Item(_) => Ok("any".to_string()),
870 }
871 } else {
872 Ok("any".to_string())
873 }
874 } else {
875 Ok("any".to_string())
876 }
877 }
878 Ok(ReferenceOr::Reference { .. }) => {
879 Ok("any".to_string())
881 }
882 Err(_) => {
883 Ok("any".to_string())
885 }
886 }
887 }
888 ReferenceOr::Item(response) => {
889 if let Some(json_media) = response.content.get("application/json") {
890 if let Some(schema_ref) = &json_media.schema {
891 match schema_ref {
892 ReferenceOr::Reference { reference } => {
893 if let Some(ref_name) = get_schema_name_from_ref(reference) {
894 Ok(to_pascal_case(&ref_name))
895 } else {
896 Ok("any".to_string())
897 }
898 }
899 ReferenceOr::Item(_) => Ok("any".to_string()),
900 }
901 } else {
902 Ok("any".to_string())
903 }
904 } else {
905 Ok("any".to_string())
906 }
907 }
908 }
909 } else {
910 Ok("any".to_string())
911 }
912}
913
914fn extract_all_responses(openapi: &OpenAPI, operation: &Operation) -> Result<Vec<ResponseInfo>> {
915 let mut responses = Vec::new();
916
917 for (status_code, response_ref) in &operation.responses.responses {
918 let status_num = match status_code {
919 openapiv3::StatusCode::Code(code) => *code,
920 openapiv3::StatusCode::Range(range) => {
921 match format!("{:?}", range).as_str() {
924 s if s.contains("4") => 400,
925 s if s.contains("5") => 500,
926 _ => 0,
927 }
928 }
929 };
930
931 let (description, body_type) = match response_ref {
933 ReferenceOr::Reference { reference } => {
934 match resolve_response_ref(openapi, reference) {
935 Ok(ReferenceOr::Item(response)) => {
936 let desc = response.description.clone();
937 let body = extract_response_body_type(openapi, &response);
938 (Some(desc), body)
939 }
940 _ => (None, "any".to_string()),
941 }
942 }
943 ReferenceOr::Item(response) => {
944 let desc = response.description.clone();
945 let body = extract_response_body_type(openapi, response);
946 (Some(desc), body)
947 }
948 };
949
950 responses.push(ResponseInfo {
951 status_code: status_num,
952 body_type,
953 description,
954 });
955 }
956
957 Ok(responses)
958}
959
960fn extract_error_responses(openapi: &OpenAPI, operation: &Operation) -> Result<Vec<ErrorResponse>> {
961 let all_responses = extract_all_responses(openapi, operation)?;
962 let errors: Vec<ErrorResponse> = all_responses
963 .iter()
964 .filter(|r| r.status_code < 200 || r.status_code >= 300)
965 .map(|r| ErrorResponse {
966 status_code: r.status_code,
967 body_type: r.body_type.clone(),
968 })
969 .collect();
970 Ok(errors)
971}
972
973fn generate_response_types(
974 func_name: &str,
975 success_responses: &[ResponseInfo],
976 error_responses: &[ResponseInfo],
977 namespace_name: &str,
978 common_schemas: &[String],
979 enum_types: &[(String, Vec<String>)],
980) -> Vec<TypeScriptType> {
981 let mut types = Vec::new();
982 let type_name_base = to_pascal_case(func_name);
983
984 for (enum_name, enum_values) in enum_types {
986 let variants = enum_values
987 .iter()
988 .map(|v| format!("\"{}\"", v))
989 .collect::<Vec<_>>()
990 .join(" |\n");
991 let enum_type = format!("export type {} =\n{};", enum_name, variants);
992 types.push(TypeScriptType { content: enum_type });
993 }
994
995 if !error_responses.is_empty() {
997 let mut error_fields = Vec::new();
998 for error in error_responses {
999 if error.status_code > 0 {
1000 let qualified_type = if error.body_type != "any" {
1003 if common_schemas.contains(&error.body_type) {
1004 format!("Common.{}", error.body_type)
1005 } else {
1006 error.body_type.clone()
1008 }
1009 } else {
1010 "any".to_string()
1011 };
1012
1013 let description = error.description.as_ref()
1014 .map(|d| format!(" /**\n * {}\n */", d))
1015 .unwrap_or_else(|| String::new());
1016
1017 error_fields.push(format!("{}\n {}: {};", description, error.status_code, qualified_type));
1018 }
1019 }
1020
1021 if !error_fields.is_empty() {
1022 let errors_type = format!("export type {}Errors = {{\n{}\n}};", type_name_base, error_fields.join("\n"));
1023 types.push(TypeScriptType { content: errors_type });
1024
1025 let error_union_type = format!("export type {}Error = {}Errors[keyof {}Errors];",
1027 type_name_base, type_name_base, type_name_base);
1028 types.push(TypeScriptType { content: error_union_type });
1029 }
1030 }
1031
1032 let success_with_schemas: Vec<&ResponseInfo> = success_responses
1034 .iter()
1035 .filter(|r| r.status_code >= 200 && r.status_code < 300 && r.body_type != "any")
1036 .collect();
1037
1038 if !success_with_schemas.is_empty() {
1039 let mut response_fields = Vec::new();
1040 for response in success_with_schemas {
1041 let qualified_type = if common_schemas.contains(&response.body_type) {
1044 format!("Common.{}", response.body_type)
1045 } else {
1046 response.body_type.clone()
1048 };
1049
1050 let description = response.description.as_ref()
1051 .map(|d| format!(" /**\n * {}\n */", d))
1052 .unwrap_or_else(|| String::new());
1053
1054 response_fields.push(format!("{}\n {}: {};", description, response.status_code, qualified_type));
1055 }
1056
1057 if !response_fields.is_empty() {
1058 let responses_type = format!("export type {}Responses = {{\n{}\n}};", type_name_base, response_fields.join("\n"));
1059 types.push(TypeScriptType { content: responses_type });
1060 }
1061 }
1062
1063 types
1064}
1065
1066fn extract_response_body_type(_openapi: &OpenAPI, response: &openapiv3::Response) -> String {
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 to_pascal_case(&ref_name)
1073 } else {
1074 "any".to_string()
1075 }
1076 }
1077 ReferenceOr::Item(_) => "any".to_string(),
1078 }
1079 } else {
1080 "any".to_string()
1081 }
1082 } else {
1083 "any".to_string()
1084 }
1085}
1086
1087fn generate_function_name_from_path(path: &str, method: &str) -> String {
1088 let path_parts: Vec<&str> = path
1089 .trim_start_matches('/')
1090 .split('/')
1091 .filter(|p| !p.starts_with('{'))
1092 .collect();
1093
1094 let method_upper = method.to_uppercase();
1096 let method_lower = method.to_lowercase();
1097 let method_prefix = match method_upper.as_str() {
1098 "GET" => "get",
1099 "POST" => "create",
1100 "PUT" => "update",
1101 "DELETE" => "delete",
1102 "PATCH" => "patch",
1103 _ => method_lower.as_str(),
1104 };
1105
1106 let base_name = if path_parts.is_empty() {
1107 method_prefix.to_string()
1108 } else {
1109 let resource_name = if path_parts.len() > 1 {
1111 path_parts.last().unwrap_or(&"")
1113 } else {
1114 path_parts.first().unwrap_or(&"")
1115 };
1116
1117 if resource_name.ends_with("s") && path.contains('{') {
1119 let singular = &resource_name[..resource_name.len() - 1];
1121 format!("{}{}ById", method_prefix, to_pascal_case(singular))
1122 } else if path.contains('{') {
1123 format!("{}{}ById", method_prefix, to_pascal_case(resource_name))
1125 } else {
1126 format!("{}{}", method_prefix, to_pascal_case(resource_name))
1128 }
1129 };
1130
1131 to_camel_case(&base_name)
1132}
1133