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