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::utils::{to_camel_case, to_pascal_case};
7use openapiv3::OpenAPI;
8use openapiv3::{Operation, Parameter, ReferenceOr};
9
10pub struct ApiFunction {
11 pub content: String,
12}
13
14pub fn generate_api_client(
15 openapi: &OpenAPI,
16 operations: &[OperationInfo],
17 module_name: &str,
18 common_schemas: &[String],
19) -> Result<Vec<ApiFunction>> {
20 let mut functions = Vec::new();
21
22 for op_info in operations {
23 let func = generate_function_for_operation(openapi, op_info, module_name, common_schemas)?;
24 functions.push(func);
25 }
26
27 Ok(functions)
28}
29
30fn generate_function_for_operation(
31 openapi: &OpenAPI,
32 op_info: &OperationInfo,
33 module_name: &str,
34 common_schemas: &[String],
35) -> Result<ApiFunction> {
36 let operation = &op_info.operation;
37 let method = op_info.method.to_lowercase();
38
39 let func_name = if let Some(operation_id) = &operation.operation_id {
41 to_camel_case(operation_id)
42 } else {
43 generate_function_name_from_path(&op_info.path, &op_info.method)
44 };
45
46 let path_params = extract_path_parameters(openapi, operation)?;
48
49 let query_params = extract_query_parameters(openapi, operation)?;
51
52 let request_body = extract_request_body(openapi, operation)?;
54
55 let response_type = extract_response_type(openapi, operation)?;
57
58 let namespace_name = to_pascal_case(&module_name.replace("/", "_"));
61
62 let mut params = Vec::new();
64 let mut path_template = op_info.path.clone();
65
66 for param in &path_params {
68 params.push(format!("{}: string", param));
69 path_template =
70 path_template.replace(&format!("{{{}}}", param), &format!("${{{}}}", param));
71 }
72
73 if !query_params.is_empty() {
75 let query_fields: Vec<String> = query_params
76 .iter()
77 .map(|p| format!("{}?: string", p))
78 .collect();
79 let query_type = format!("{{ {} }}", query_fields.join(", "));
80 params.push(format!("query?: {}", query_type));
81 }
82
83 if let Some(body_type) = &request_body {
85 if body_type == "any" {
87 params.push("body: any".to_string());
88 } else {
89 let qualified_body_type = if common_schemas.contains(body_type) {
90 format!("Common.{}", body_type)
91 } else {
92 format!("{}.{}", namespace_name, body_type)
93 };
94 params.push(format!("body: {}", qualified_body_type));
95 }
96 }
97
98 let params_str = params.join(", ");
99
100 let mut body_lines = Vec::new();
102
103 let mut url_template = op_info.path.clone();
105 for param in &path_params {
106 url_template = url_template.replace(&format!("{{{}}}", param), &format!("${{{}}}", param));
107 }
108
109 if !query_params.is_empty() {
111 body_lines.push(" const queryString = new URLSearchParams();".to_string());
112 for param in &query_params {
113 body_lines.push(format!(
114 " if (query?.{}) queryString.append(\"{}\", query.{});",
115 param, param, param
116 ));
117 }
118 body_lines.push(" const queryStr = queryString.toString();".to_string());
119 body_lines.push(format!(
120 " const url = `{}` + (queryStr ? `?${{queryStr}}` : '');",
121 url_template
122 ));
123 } else {
124 body_lines.push(format!(" const url = `{}`;", url_template));
125 }
126
127 let http_method = match method.to_uppercase().as_str() {
129 "GET" => "get",
130 "POST" => "post",
131 "PUT" => "put",
132 "DELETE" => "delete",
133 "PATCH" => "patch",
134 "HEAD" => "head",
135 "OPTIONS" => "options",
136 _ => "get",
137 };
138
139 let qualified_response_type_for_generic = if response_type != "any" {
141 let is_common = common_schemas.contains(&response_type);
142 if is_common {
143 format!("Common.{}", response_type)
144 } else {
145 format!("{}.{}", namespace_name, response_type)
146 }
147 } else {
148 response_type.clone()
149 };
150
151 if let Some(_body_type) = &request_body {
152 body_lines.push(format!(" return http.{}(url, body);", http_method));
153 } else {
154 body_lines.push(format!(
155 " return http.{}<{}>(url);",
156 http_method, qualified_response_type_for_generic
157 ));
158 }
159
160 let http_import = "../http";
163
164 let (type_imports, qualified_type) = if response_type != "any" {
166 let is_common = common_schemas.contains(&response_type);
167 if is_common {
168 let common_import = "../../schemas/common";
170 let common_namespace = "Common";
171 let imports = format!(
172 "import * as {} from \"{}\";\n",
173 common_namespace, common_import
174 );
175 let qualified = format!("{}.{}", common_namespace, response_type);
176 (imports, qualified)
177 } else {
178 let schemas_import = format!("../../schemas/{}", module_name);
180 let imports = format!(
181 "import * as {} from \"{}\";\n",
182 namespace_name, schemas_import
183 );
184 let qualified = format!("{}.{}", namespace_name, response_type);
185 (imports, qualified)
186 }
187 } else {
188 (String::new(), String::new())
189 };
190
191 let return_type = if response_type == "any" {
192 String::new()
193 } else {
194 format!(": Promise<{}>", qualified_type)
195 };
196
197 let function_body = body_lines.join("\n");
198
199 let content = if params_str.is_empty() {
200 format!(
201 "import {{ http }} from \"{}\";\n{}\
202 export const {} = async (){} => {{\n{}\n}};",
203 http_import, type_imports, func_name, return_type, function_body
204 )
205 } else {
206 format!(
207 "import {{ http }} from \"{}\";\n{}\
208 export const {} = async ({}){} => {{\n{}\n}};",
209 http_import, type_imports, func_name, params_str, return_type, function_body
210 )
211 };
212
213 Ok(ApiFunction { content })
214}
215
216fn extract_path_parameters(openapi: &OpenAPI, operation: &Operation) -> Result<Vec<String>> {
217 let mut params = Vec::new();
218
219 for param_ref in &operation.parameters {
220 match param_ref {
221 ReferenceOr::Reference { reference } => {
222 let mut current_ref = Some(reference.clone());
224 let mut depth = 0;
225 while let Some(ref_path) = current_ref.take() {
226 if depth > 3 {
227 break; }
229 match resolve_parameter_ref(openapi, &ref_path) {
230 Ok(ReferenceOr::Item(param)) => {
231 if let Parameter::Path { parameter_data, .. } = param {
232 params.push(parameter_data.name.clone());
233 }
234 break;
235 }
236 Ok(ReferenceOr::Reference {
237 reference: nested_ref,
238 }) => {
239 current_ref = Some(nested_ref);
240 depth += 1;
241 }
242 Err(_) => {
243 break;
245 }
246 }
247 }
248 }
249 ReferenceOr::Item(param) => {
250 if let Parameter::Path { parameter_data, .. } = param {
251 params.push(parameter_data.name.clone());
252 }
253 }
254 }
255 }
256
257 Ok(params)
258}
259
260fn extract_query_parameters(openapi: &OpenAPI, operation: &Operation) -> Result<Vec<String>> {
261 let mut params = Vec::new();
262
263 for param_ref in &operation.parameters {
264 match param_ref {
265 ReferenceOr::Reference { reference } => {
266 let mut current_ref = Some(reference.clone());
268 let mut depth = 0;
269 while let Some(ref_path) = current_ref.take() {
270 if depth > 3 {
271 break; }
273 match resolve_parameter_ref(openapi, &ref_path) {
274 Ok(ReferenceOr::Item(param)) => {
275 if let Parameter::Query { parameter_data, .. } = param {
276 params.push(parameter_data.name.clone());
277 }
278 break;
279 }
280 Ok(ReferenceOr::Reference {
281 reference: nested_ref,
282 }) => {
283 current_ref = Some(nested_ref);
284 depth += 1;
285 }
286 Err(_) => {
287 break;
289 }
290 }
291 }
292 }
293 ReferenceOr::Item(param) => {
294 if let Parameter::Query { parameter_data, .. } = param {
295 params.push(parameter_data.name.clone());
296 }
297 }
298 }
299 }
300
301 Ok(params)
302}
303
304fn extract_request_body(openapi: &OpenAPI, operation: &Operation) -> Result<Option<String>> {
305 if let Some(request_body) = &operation.request_body {
306 match request_body {
307 ReferenceOr::Reference { reference } => {
308 match resolve_request_body_ref(openapi, reference) {
310 Ok(ReferenceOr::Item(body)) => {
311 if let Some(json_media) = body.content.get("application/json") {
312 if let Some(schema_ref) = &json_media.schema {
313 match schema_ref {
314 ReferenceOr::Reference { reference } => {
315 if let Some(ref_name) = get_schema_name_from_ref(reference)
316 {
317 Ok(Some(to_pascal_case(&ref_name)))
318 } else {
319 Ok(Some("any".to_string()))
320 }
321 }
322 ReferenceOr::Item(_schema) => {
323 Ok(Some("any".to_string()))
329 }
330 }
331 } else {
332 Ok(Some("any".to_string()))
333 }
334 } else {
335 Ok(Some("any".to_string()))
336 }
337 }
338 Ok(ReferenceOr::Reference { .. }) => {
339 Ok(Some("any".to_string()))
341 }
342 Err(_) => {
343 Ok(Some("any".to_string()))
345 }
346 }
347 }
348 ReferenceOr::Item(body) => {
349 if let Some(json_media) = body.content.get("application/json") {
350 if let Some(schema_ref) = &json_media.schema {
351 match schema_ref {
352 ReferenceOr::Reference { reference } => {
353 if let Some(ref_name) = get_schema_name_from_ref(reference) {
354 Ok(Some(to_pascal_case(&ref_name)))
355 } else {
356 Ok(Some("any".to_string()))
357 }
358 }
359 ReferenceOr::Item(_schema) => {
360 Ok(Some("any".to_string()))
366 }
367 }
368 } else {
369 Ok(Some("any".to_string()))
370 }
371 } else {
372 Ok(Some("any".to_string()))
373 }
374 }
375 }
376 } else {
377 Ok(None)
378 }
379}
380
381fn extract_response_type(openapi: &OpenAPI, operation: &Operation) -> Result<String> {
382 if let Some(success_response) = operation
384 .responses
385 .responses
386 .get(&openapiv3::StatusCode::Code(200))
387 {
388 match success_response {
389 ReferenceOr::Reference { reference } => {
390 match resolve_response_ref(openapi, reference) {
392 Ok(ReferenceOr::Item(response)) => {
393 if let Some(json_media) = response.content.get("application/json") {
394 if let Some(schema_ref) = &json_media.schema {
395 match schema_ref {
396 ReferenceOr::Reference { reference } => {
397 if let Some(ref_name) = get_schema_name_from_ref(reference)
398 {
399 Ok(to_pascal_case(&ref_name))
400 } else {
401 Ok("any".to_string())
402 }
403 }
404 ReferenceOr::Item(_) => Ok("any".to_string()),
405 }
406 } else {
407 Ok("any".to_string())
408 }
409 } else {
410 Ok("any".to_string())
411 }
412 }
413 Ok(ReferenceOr::Reference { .. }) => {
414 Ok("any".to_string())
416 }
417 Err(_) => {
418 Ok("any".to_string())
420 }
421 }
422 }
423 ReferenceOr::Item(response) => {
424 if let Some(json_media) = response.content.get("application/json") {
425 if let Some(schema_ref) = &json_media.schema {
426 match schema_ref {
427 ReferenceOr::Reference { reference } => {
428 if let Some(ref_name) = get_schema_name_from_ref(reference) {
429 Ok(to_pascal_case(&ref_name))
430 } else {
431 Ok("any".to_string())
432 }
433 }
434 ReferenceOr::Item(_) => Ok("any".to_string()),
435 }
436 } else {
437 Ok("any".to_string())
438 }
439 } else {
440 Ok("any".to_string())
441 }
442 }
443 }
444 } else {
445 Ok("any".to_string())
446 }
447}
448
449fn generate_function_name_from_path(path: &str, method: &str) -> String {
450 let path_parts: Vec<&str> = path
451 .trim_start_matches('/')
452 .split('/')
453 .filter(|p| !p.starts_with('{'))
454 .collect();
455
456 let method_upper = method.to_uppercase();
458 let method_lower = method.to_lowercase();
459 let method_prefix = match method_upper.as_str() {
460 "GET" => "get",
461 "POST" => "create",
462 "PUT" => "update",
463 "DELETE" => "delete",
464 "PATCH" => "patch",
465 _ => method_lower.as_str(),
466 };
467
468 let base_name = if path_parts.is_empty() {
469 method_prefix.to_string()
470 } else {
471 let resource_name = if path_parts.len() > 1 {
473 path_parts.last().unwrap_or(&"")
475 } else {
476 path_parts.first().unwrap_or(&"")
477 };
478
479 if resource_name.ends_with("s") && path.contains('{') {
481 let singular = &resource_name[..resource_name.len() - 1];
483 format!("{}{}ById", method_prefix, to_pascal_case(singular))
484 } else if path.contains('{') {
485 format!("{}{}ById", method_prefix, to_pascal_case(resource_name))
487 } else {
488 format!("{}{}", method_prefix, to_pascal_case(resource_name))
490 }
491 };
492
493 to_camel_case(&base_name)
494}