1use crate::analysis::{OperationInfo, ParameterInfo, SchemaAnalysis};
151use crate::generator::CodeGenerator;
152use heck::ToSnakeCase;
153use proc_macro2::TokenStream;
154use quote::{format_ident, quote};
155use std::collections::BTreeMap;
156
157impl CodeGenerator {
158 pub fn generate_http_client_struct(&self) -> TokenStream {
160 let has_retry = self.config().retry_config.is_some();
161 let has_tracing = self.config().tracing_enabled;
162
163 let retry_config_struct = if has_retry {
165 quote! {
166 #[derive(Debug, Clone)]
168 pub struct RetryConfig {
169 pub max_retries: u32,
170 pub initial_delay_ms: u64,
171 pub max_delay_ms: u64,
172 }
173
174 impl Default for RetryConfig {
175 fn default() -> Self {
176 Self {
177 max_retries: 3,
178 initial_delay_ms: 500,
179 max_delay_ms: 16000,
180 }
181 }
182 }
183 }
184 } else {
185 quote! {}
186 };
187
188 let client_struct = quote! {
190 use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
191 use std::collections::BTreeMap;
192
193 #[derive(Clone)]
195 pub struct HttpClient {
196 base_url: String,
197 api_key: Option<String>,
198 http_client: ClientWithMiddleware,
199 custom_headers: BTreeMap<String, String>,
200 }
201 };
202
203 let constructor = self.generate_constructor(has_retry, has_tracing);
205
206 let builder_methods = self.generate_builder_methods();
208
209 let default_impl = quote! {
211 impl Default for HttpClient {
212 fn default() -> Self {
213 Self::new()
214 }
215 }
216 };
217
218 let path_encoder = quote! {
222 fn __pct_encode_path_segment(s: &str) -> String {
223 let mut out = String::with_capacity(s.len());
224 for &b in s.as_bytes() {
225 match b {
226 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
227 out.push(b as char);
228 }
229 _ => {
230 out.push('%');
231 out.push_str(&format!("{:02X}", b));
232 }
233 }
234 }
235 out
236 }
237 };
238
239 quote! {
241 #retry_config_struct
242 #client_struct
243
244 impl HttpClient {
245 #constructor
246 #builder_methods
247 }
248
249 #default_impl
250 #path_encoder
251 }
252 }
253
254 fn generate_constructor(&self, has_retry: bool, has_tracing: bool) -> TokenStream {
256 let retry_param = if has_retry {
257 quote! { retry_config: Option<RetryConfig>, }
258 } else {
259 quote! {}
260 };
261
262 let tracing_param = if has_tracing {
263 quote! { enable_tracing: bool, }
264 } else {
265 quote! {}
266 };
267
268 let retry_middleware = if has_retry {
269 quote! {
270 if let Some(config) = retry_config {
271 use reqwest_retry::{RetryTransientMiddleware, policies::ExponentialBackoff};
272
273 let retry_policy = ExponentialBackoff::builder()
274 .retry_bounds(
275 std::time::Duration::from_millis(config.initial_delay_ms),
276 std::time::Duration::from_millis(config.max_delay_ms),
277 )
278 .build_with_max_retries(config.max_retries);
279
280 let retry_middleware = RetryTransientMiddleware::new_with_policy(retry_policy);
281 client_builder = client_builder.with(retry_middleware);
282 }
283 }
284 } else {
285 quote! {}
286 };
287
288 let tracing_middleware = if has_tracing {
289 quote! {
290 if enable_tracing {
291 use reqwest_tracing::TracingMiddleware;
292 client_builder = client_builder.with(TracingMiddleware::default());
293 }
294 }
295 } else {
296 quote! {}
297 };
298
299 let default_constructor = if has_retry && has_tracing {
300 quote! {
301 pub fn new() -> Self {
303 Self::with_config(None, true)
304 }
305 }
306 } else if has_retry {
307 quote! {
308 pub fn new() -> Self {
310 Self::with_config(None)
311 }
312 }
313 } else if has_tracing {
314 quote! {
315 pub fn new() -> Self {
317 Self::with_config(true)
318 }
319 }
320 } else {
321 quote! {
322 pub fn new() -> Self {
324 let reqwest_client = reqwest::Client::new();
325 let client_builder = ClientBuilder::new(reqwest_client);
326 let http_client = client_builder.build();
327
328 Self {
329 base_url: String::new(),
330 api_key: None,
331 http_client,
332 custom_headers: BTreeMap::new(),
333 }
334 }
335 }
336 };
337
338 if has_retry || has_tracing {
339 quote! {
340 #default_constructor
341
342 pub fn with_config(#retry_param #tracing_param) -> Self {
344 let reqwest_client = reqwest::Client::new();
345 let mut client_builder = ClientBuilder::new(reqwest_client);
346
347 #tracing_middleware
348 #retry_middleware
349
350 let http_client = client_builder.build();
351
352 Self {
353 base_url: String::new(),
354 api_key: None,
355 http_client,
356 custom_headers: BTreeMap::new(),
357 }
358 }
359 }
360 } else {
361 default_constructor
362 }
363 }
364
365 fn generate_builder_methods(&self) -> TokenStream {
367 quote! {
368 pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
370 self.base_url = base_url.into();
371 self
372 }
373
374 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
376 self.api_key = Some(api_key.into());
377 self
378 }
379
380 pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
382 self.custom_headers.insert(name.into(), value.into());
383 self
384 }
385
386 pub fn with_headers(mut self, headers: BTreeMap<String, String>) -> Self {
388 self.custom_headers.extend(headers);
389 self
390 }
391 }
392 }
393
394 pub fn generate_operation_methods(&self, analysis: &SchemaAnalysis) -> TokenStream {
400 let param_enums = self.generate_param_enum_types(analysis);
401
402 let op_error_enums: Vec<TokenStream> = analysis
403 .operations
404 .values()
405 .filter_map(|op| self.generate_op_error_enum(op))
406 .collect();
407
408 let methods: Vec<TokenStream> = analysis
409 .operations
410 .values()
411 .map(|op| self.generate_single_operation_method(op))
412 .collect();
413
414 quote! {
415 #param_enums
416
417 #(#op_error_enums)*
418
419 impl HttpClient {
420 #(#methods)*
421 }
422 }
423 }
424
425 fn generate_param_enum_types(&self, analysis: &SchemaAnalysis) -> TokenStream {
430 let mut by_name: BTreeMap<String, &ParameterInfo> = BTreeMap::new();
431 for op in analysis.operations.values() {
432 for param in &op.parameters {
433 if param.enum_values.is_some() {
434 by_name.entry(param.rust_type.clone()).or_insert(param);
435 }
436 }
437 }
438
439 if by_name.is_empty() {
440 return quote! {};
441 }
442
443 let defs: Vec<TokenStream> = by_name
444 .values()
445 .map(|param| self.generate_single_param_enum(param))
446 .collect();
447
448 quote! { #(#defs)* }
449 }
450
451 fn generate_single_param_enum(&self, param: &ParameterInfo) -> TokenStream {
452 let Some(values) = param.enum_values.as_deref() else {
453 return quote! {};
454 };
455
456 let enum_ident = format_ident!("{}", param.rust_type);
457
458 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
464 let variant_names: Vec<String> = values
465 .iter()
466 .map(|value| {
467 let base = self.to_rust_enum_variant(value);
468 let mut chosen = base.clone();
469 let mut suffix = 2;
470 while !used.insert(chosen.clone()) {
471 chosen = format!("{base}_{suffix}");
472 suffix += 1;
473 }
474 chosen
475 })
476 .collect();
477
478 let variants: Vec<TokenStream> = values
479 .iter()
480 .zip(&variant_names)
481 .map(|(value, name)| {
482 let variant_ident = format_ident!("{}", name);
483 quote! {
484 #[serde(rename = #value)]
485 #variant_ident,
486 }
487 })
488 .collect();
489
490 let display_arms: Vec<TokenStream> = values
491 .iter()
492 .zip(&variant_names)
493 .map(|(value, name)| {
494 let variant_ident = format_ident!("{}", name);
495 quote! { Self::#variant_ident => #value, }
496 })
497 .collect();
498
499 let doc = format!(
500 "Allowed values for the `{}` {} parameter.",
501 param.name, param.location
502 );
503
504 quote! {
505 #[doc = #doc]
506 #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
507 pub enum #enum_ident {
508 #(#variants)*
509 }
510
511 impl #enum_ident {
512 pub fn as_str(&self) -> &'static str {
513 match self {
514 #(#display_arms)*
515 }
516 }
517 }
518
519 impl std::fmt::Display for #enum_ident {
520 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
521 f.write_str(self.as_str())
522 }
523 }
524
525 impl AsRef<str> for #enum_ident {
526 fn as_ref(&self) -> &str {
527 self.as_str()
528 }
529 }
530 }
531 }
532
533 fn generate_op_error_enum(&self, op: &OperationInfo) -> Option<TokenStream> {
538 let variants: Vec<(String, String)> = op
539 .response_schemas
540 .iter()
541 .filter(|(code, _)| !code.starts_with('2'))
542 .map(|(code, schema)| (code.clone(), schema.clone()))
543 .collect();
544
545 if variants.is_empty() {
546 return None;
547 }
548
549 let enum_ident = self.op_error_enum_ident(op);
550 let variant_decls: Vec<TokenStream> = variants
551 .iter()
552 .map(|(code, schema)| {
553 let variant_ident = Self::op_error_variant_ident(code);
554 let payload_ty_name = self.to_rust_type_name(schema);
555 let payload_ty = syn::Ident::new(&payload_ty_name, proc_macro2::Span::call_site());
556 quote! { #variant_ident(#payload_ty) }
557 })
558 .collect();
559
560 let doc = format!(
561 "Typed error responses for `{}`. One variant per declared non-2xx response.",
562 op.operation_id
563 );
564
565 Some(quote! {
566 #[doc = #doc]
567 #[derive(Debug, Clone)]
568 pub enum #enum_ident {
569 #(#variant_decls,)*
570 }
571 })
572 }
573
574 fn op_error_enum_ident(&self, op: &OperationInfo) -> syn::Ident {
576 use heck::ToPascalCase;
577 let name = format!(
578 "{}ApiError",
579 op.operation_id.replace('.', "_").to_pascal_case()
580 );
581 syn::Ident::new(&name, proc_macro2::Span::call_site())
582 }
583
584 fn op_error_variant_ident(status_code: &str) -> syn::Ident {
587 let raw = match status_code {
588 "default" | "Default" => "Default".to_string(),
589 other if other.chars().all(|c| c.is_ascii_digit()) => format!("Status{other}"),
590 other => format!("Status{}", other.to_ascii_lowercase()),
591 };
592 syn::Ident::new(&raw, proc_macro2::Span::call_site())
593 }
594
595 fn op_error_type_token(&self, op: &OperationInfo) -> TokenStream {
599 if op
600 .response_schemas
601 .iter()
602 .any(|(code, _)| !code.starts_with('2'))
603 {
604 let ident = self.op_error_enum_ident(op);
605 quote! { #ident }
606 } else {
607 quote! { serde_json::Value }
608 }
609 }
610
611 fn generate_single_operation_method(&self, op: &OperationInfo) -> TokenStream {
613 let method_name = self.get_method_name(op);
614 let http_method_call = self.http_method_call(op);
615 let path = &op.path;
616 let request_param = self.generate_request_param(op);
617 let request_body = self.generate_request_body(op);
618 let query_params = self.generate_query_params(op);
619 let header_params = self.generate_header_params(op);
620 let auth_application = self.generate_auth_application();
621 let response_type = self.get_response_type(op);
622 let has_response_body = self.get_success_response_schema(op).is_some();
623 let op_error_type = self.op_error_type_token(op);
624 let error_handling = self.generate_error_handling(op, has_response_body);
625 let url_construction = self.generate_url_construction(path, op);
626 let doc_comment = self.generate_operation_doc_comment(op);
627
628 quote! {
629 #doc_comment
630 pub async fn #method_name(
631 &self,
632 #request_param
633 ) -> Result<#response_type, ApiOpError<#op_error_type>> {
634 #url_construction
635
636 let mut req = #http_method_call;
637 #request_body
638
639 #query_params
640 #header_params
641
642 #auth_application
645
646 for (name, value) in &self.custom_headers {
648 req = req.header(name, value);
649 }
650
651 let response = req.send().await?;
652 #error_handling
653 }
654 }
655 }
656
657 fn generate_auth_application(&self) -> TokenStream {
661 use crate::http_config::AuthConfig;
662 match &self.config().auth_config {
663 Some(AuthConfig::Bearer { header_name }) if header_name == "Authorization" => quote! {
664 if let Some(api_key) = &self.api_key {
665 req = req.bearer_auth(api_key);
666 }
667 },
668 Some(AuthConfig::Bearer { header_name }) => {
669 let h = header_name.clone();
670 quote! {
671 if let Some(api_key) = &self.api_key {
672 req = req.header(#h, format!("Bearer {}", api_key));
673 }
674 }
675 }
676 Some(AuthConfig::ApiKey { header_name }) => {
677 let h = header_name.clone();
678 quote! {
679 if let Some(api_key) = &self.api_key {
680 req = req.header(#h, api_key.as_str());
681 }
682 }
683 }
684 Some(AuthConfig::Custom {
685 header_name,
686 header_value_prefix,
687 }) => {
688 let h = header_name.clone();
689 let prefix = header_value_prefix.clone().unwrap_or_default();
690 if prefix.is_empty() {
691 quote! {
692 if let Some(api_key) = &self.api_key {
693 req = req.header(#h, api_key.as_str());
694 }
695 }
696 } else {
697 let format_str = format!("{}{{}}", prefix);
698 quote! {
699 if let Some(api_key) = &self.api_key {
700 req = req.header(#h, format!(#format_str, api_key));
701 }
702 }
703 }
704 }
705 None => quote! {
706 if let Some(api_key) = &self.api_key {
707 req = req.bearer_auth(api_key);
708 }
709 },
710 }
711 }
712
713 fn generate_header_params(&self, op: &OperationInfo) -> TokenStream {
717 let header_params: Vec<_> = op
718 .parameters
719 .iter()
720 .filter(|p| p.location == "header")
721 .collect();
722 if header_params.is_empty() {
723 return quote! {};
724 }
725 let mut emit = Vec::new();
726 for param in header_params {
727 let param_name_snake = self.param_ident_str(param);
728 let param_ident = Self::to_field_ident(¶m_name_snake);
729 let header_name = ¶m.name;
730 if param.required {
731 if Self::param_uses_as_ref_str(param) {
732 emit.push(quote! {
733 req = req.header(#header_name, #param_ident.as_ref());
734 });
735 } else {
736 emit.push(quote! {
737 req = req.header(#header_name, #param_ident.to_string());
738 });
739 }
740 } else if Self::param_uses_as_ref_str(param) {
741 emit.push(quote! {
742 if let Some(v) = #param_ident {
743 req = req.header(#header_name, v.as_ref());
744 }
745 });
746 } else {
747 emit.push(quote! {
748 if let Some(v) = #param_ident {
749 req = req.header(#header_name, v.to_string());
750 }
751 });
752 }
753 }
754 quote! {
755 #(#emit)*
756 }
757 }
758
759 fn generate_query_params(&self, op: &OperationInfo) -> TokenStream {
761 let query_params: Vec<_> = op
762 .parameters
763 .iter()
764 .filter(|p| p.location == "query")
765 .collect();
766
767 if query_params.is_empty() {
768 return quote! {};
769 }
770
771 let mut param_building = Vec::new();
772
773 for param in query_params {
774 let param_name_snake = self.param_ident_str(param);
776 let param_name = Self::to_field_ident(¶m_name_snake);
777
778 let param_key = ¶m.name;
780
781 if param.required {
782 if Self::param_uses_as_ref_str(param) {
784 param_building.push(quote! {
785 query_params.push((#param_key, #param_name.as_ref().to_string()));
786 });
787 } else {
788 param_building.push(quote! {
789 query_params.push((#param_key, #param_name.to_string()));
790 });
791 }
792 } else {
793 if Self::param_uses_as_ref_str(param) {
795 param_building.push(quote! {
796 if let Some(v) = #param_name {
797 query_params.push((#param_key, v.as_ref().to_string()));
798 }
799 });
800 } else {
801 param_building.push(quote! {
802 if let Some(v) = #param_name {
803 query_params.push((#param_key, v.to_string()));
804 }
805 });
806 }
807 }
808 }
809
810 quote! {
811 {
813 let mut query_params: Vec<(&str, String)> = Vec::new();
814 #(#param_building)*
815 if !query_params.is_empty() {
816 req = req.query(&query_params);
817 }
818 }
819 }
820 }
821
822 fn generate_operation_doc_comment(&self, op: &OperationInfo) -> TokenStream {
826 let method = op.method.to_uppercase();
827 let path = &op.path;
828 let mut docs: Vec<String> = Vec::new();
829 if let Some(s) = &op.summary {
830 if !s.is_empty() {
831 docs.push(s.clone());
832 docs.push(String::new());
833 }
834 }
835 if let Some(d) = &op.description {
836 if !d.is_empty() {
837 for line in d.lines() {
838 docs.push(line.to_string());
839 }
840 docs.push(String::new());
841 }
842 }
843 docs.push(format!("`{} {}`", method, path));
844 let doc_attrs: Vec<TokenStream> = docs
845 .iter()
846 .map(|line| {
847 let prefixed = if line.is_empty() {
848 String::new()
849 } else {
850 format!(" {line}")
851 };
852 quote! { #[doc = #prefixed] }
853 })
854 .collect();
855 quote! { #(#doc_attrs)* }
856 }
857
858 fn get_method_name(&self, op: &OperationInfo) -> syn::Ident {
860 let name = if !op.operation_id.is_empty() {
861 op.operation_id.to_snake_case()
862 } else {
863 format!(
865 "{}_{}",
866 op.method,
867 op.path.replace('/', "_").replace(['{', '}'], "")
868 )
869 .to_snake_case()
870 };
871
872 syn::Ident::new(&name, proc_macro2::Span::call_site())
873 }
874
875 fn http_method_call(&self, op: &OperationInfo) -> TokenStream {
880 match op.method.to_uppercase().as_str() {
881 "GET" => quote! { self.http_client.get(request_url) },
882 "POST" => quote! { self.http_client.post(request_url) },
883 "PUT" => quote! { self.http_client.put(request_url) },
884 "DELETE" => quote! { self.http_client.delete(request_url) },
885 "PATCH" => quote! { self.http_client.patch(request_url) },
886 "HEAD" => quote! { self.http_client.head(request_url) },
887 "OPTIONS" => quote! {
888 self.http_client.request(reqwest::Method::OPTIONS, request_url)
889 },
890 "TRACE" => quote! {
891 self.http_client.request(reqwest::Method::TRACE, request_url)
892 },
893 other => {
898 let upper = other.to_string();
899 quote! {
900 self.http_client.request(
901 reqwest::Method::from_bytes(#upper.as_bytes())
902 .expect("invalid HTTP method"),
903 request_url,
904 )
905 }
906 }
907 }
908 }
909
910 fn generate_request_param(&self, op: &OperationInfo) -> TokenStream {
912 let mut params = Vec::new();
913 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
920 let mut unique_param_ident = |raw: String| -> syn::Ident {
921 let mut chosen = raw.clone();
922 let mut suffix = 2;
923 while !used.insert(chosen.clone()) {
924 chosen = format!("{raw}_{suffix}");
925 suffix += 1;
926 }
927 Self::to_field_ident(&chosen)
928 };
929
930 for param in &op.parameters {
932 if param.location == "path" {
933 let param_name_snake = self.param_ident_str(param);
934 let param_name = unique_param_ident(param_name_snake);
935 let param_type = self.get_param_rust_type(param);
936 params.push(quote! { #param_name: #param_type });
937 }
938 }
939
940 for param in &op.parameters {
942 if param.location == "query" {
943 let param_name_snake = self.param_ident_str(param);
944 let param_name = unique_param_ident(param_name_snake);
945 let param_type = self.get_param_rust_type(param);
946
947 if param.required {
949 params.push(quote! { #param_name: #param_type });
950 } else {
951 params.push(quote! { #param_name: Option<#param_type> });
952 }
953 }
954 }
955
956 for param in &op.parameters {
962 if param.location == "header" {
963 let param_name_snake = self.param_ident_str(param);
964 let param_name = unique_param_ident(param_name_snake);
965 let param_type = self.get_param_rust_type(param);
966 if param.required {
967 params.push(quote! { #param_name: #param_type });
968 } else {
969 params.push(quote! { #param_name: Option<#param_type> });
970 }
971 }
972 }
973
974 if let Some(ref rb) = op.request_body {
977 use crate::analysis::RequestBodyContent;
978 let required = op.request_body_required;
979 let body_type = match rb {
980 RequestBodyContent::Json { schema_name }
981 | RequestBodyContent::FormUrlEncoded { schema_name } => {
982 let rust_type_name = self.to_rust_type_name(schema_name);
983 let request_ident =
984 syn::Ident::new(&rust_type_name, proc_macro2::Span::call_site());
985 quote! { #request_ident }
986 }
987 RequestBodyContent::Multipart => quote! { reqwest::multipart::Form },
988 RequestBodyContent::OctetStream => quote! { Vec<u8> },
989 RequestBodyContent::TextPlain => quote! { String },
990 };
991 let body_ident = match rb {
992 RequestBodyContent::Multipart => quote! { form },
993 RequestBodyContent::OctetStream | RequestBodyContent::TextPlain => quote! { body },
994 _ => quote! { request },
995 };
996 if required {
997 params.push(quote! { #body_ident: #body_type });
998 } else {
999 params.push(quote! { #body_ident: Option<#body_type> });
1000 }
1001 }
1002
1003 if params.is_empty() {
1004 quote! {}
1005 } else {
1006 quote! { #(#params),* }
1007 }
1008 }
1009
1010 fn get_param_rust_type(&self, param: &crate::analysis::ParameterInfo) -> TokenStream {
1012 if let Some(ref schema_name) = param.schema_ref {
1016 let rust_name = self.to_rust_type_name(schema_name);
1017 let ident = syn::Ident::new(&rust_name, proc_macro2::Span::call_site());
1018 return quote! { #ident };
1019 }
1020 let type_str = ¶m.rust_type;
1021 match type_str.as_str() {
1022 "String" => quote! { impl AsRef<str> },
1023 "i64" => quote! { i64 },
1024 "i32" => quote! { i32 },
1025 "f64" => quote! { f64 },
1026 "bool" => quote! { bool },
1027 _ => {
1028 let type_ident = syn::Ident::new(type_str, proc_macro2::Span::call_site());
1029 quote! { #type_ident }
1030 }
1031 }
1032 }
1033
1034 fn param_uses_as_ref_str(param: &crate::analysis::ParameterInfo) -> bool {
1039 param.schema_ref.is_none() && param.rust_type == "String"
1040 }
1041
1042 fn generate_request_body(&self, op: &OperationInfo) -> TokenStream {
1047 let Some(rb) = op.request_body.as_ref() else {
1048 return quote! {};
1049 };
1050 use crate::analysis::RequestBodyContent;
1051 let required = op.request_body_required;
1052 let (ident, apply): (TokenStream, TokenStream) = match rb {
1053 RequestBodyContent::Json { .. } => (
1054 quote! { request },
1055 quote! {
1056 req = req
1057 .body(serde_json::to_vec(&request).map_err(HttpError::serialization_error)?)
1058 .header("content-type", "application/json");
1059 },
1060 ),
1061 RequestBodyContent::FormUrlEncoded { .. } => (
1062 quote! { request },
1063 quote! {
1064 req = req
1065 .body(serde_urlencoded::to_string(&request).map_err(HttpError::serialization_error)?)
1066 .header("content-type", "application/x-www-form-urlencoded");
1067 },
1068 ),
1069 RequestBodyContent::Multipart => (
1070 quote! { form },
1071 quote! {
1072 req = req.multipart(form);
1073 },
1074 ),
1075 RequestBodyContent::OctetStream => (
1076 quote! { body },
1077 quote! {
1078 req = req
1079 .body(body)
1080 .header("content-type", "application/octet-stream");
1081 },
1082 ),
1083 RequestBodyContent::TextPlain => (
1084 quote! { body },
1085 quote! {
1086 req = req
1087 .body(body)
1088 .header("content-type", "text/plain");
1089 },
1090 ),
1091 };
1092 if required {
1093 apply
1094 } else {
1095 quote! {
1096 if let Some(#ident) = #ident {
1097 #apply
1098 }
1099 }
1100 }
1101 }
1102
1103 fn get_success_response_schema<'a>(&self, op: &'a OperationInfo) -> Option<&'a String> {
1109 op.response_schemas
1110 .get("200")
1111 .or_else(|| op.response_schemas.get("201"))
1112 .or_else(|| {
1113 op.response_schemas
1114 .iter()
1115 .find(|(code, _)| code.starts_with('2'))
1116 .map(|(_, v)| v)
1117 })
1118 }
1119
1120 fn get_response_type(&self, op: &OperationInfo) -> TokenStream {
1122 if let Some(response_type) = self.get_success_response_schema(op) {
1123 let rust_type_name = self.to_rust_type_name(response_type);
1125 let response_ident = syn::Ident::new(&rust_type_name, proc_macro2::Span::call_site());
1126 quote! { #response_ident }
1127 } else {
1128 quote! { () }
1129 }
1130 }
1131
1132 fn generate_error_handling(&self, op: &OperationInfo, has_response_body: bool) -> TokenStream {
1141 let op_error_type = self.op_error_type_token(op);
1142
1143 let success_branch = if has_response_body {
1144 quote! {
1145 match serde_json::from_str(&body_text) {
1146 Ok(body) => Ok(body),
1147 Err(e) => Err(ApiOpError::Api(ApiError {
1148 status: status_code,
1149 headers: headers,
1150 body: body_text,
1151 typed: None,
1152 parse_error: Some(format!(
1153 "failed to deserialize 2xx response body: {}",
1154 e
1155 )),
1156 })),
1157 }
1158 }
1159 } else {
1160 quote! {
1161 let _ = body_text;
1162 let _ = headers;
1163 Ok(())
1164 }
1165 };
1166
1167 let error_match_arms = self.generate_error_match_arms(op);
1168
1169 quote! {
1170 let status = response.status();
1171 let status_code = status.as_u16();
1172 let headers = response.headers().clone();
1173 let body_text = response.text().await
1174 .map_err(|e| ApiOpError::Transport(HttpError::Network(e)))?;
1175
1176 if status.is_success() {
1177 #success_branch
1178 } else {
1179 let typed: Option<#op_error_type>;
1180 let parse_error: Option<String>;
1181 #error_match_arms
1182 Err(ApiOpError::Api(ApiError {
1183 status: status_code,
1184 headers,
1185 body: body_text,
1186 typed,
1187 parse_error,
1188 }))
1189 }
1190 }
1191 }
1192
1193 fn generate_error_match_arms(&self, op: &OperationInfo) -> TokenStream {
1196 let arms: Vec<TokenStream> = op
1197 .response_schemas
1198 .iter()
1199 .filter(|(code, _)| !code.starts_with('2'))
1200 .filter_map(|(code, schema)| {
1201 let variant_ident = Self::op_error_variant_ident(code);
1202 let payload_ty_name = self.to_rust_type_name(schema);
1203 let payload_ty = syn::Ident::new(&payload_ty_name, proc_macro2::Span::call_site());
1204 let enum_ident = self.op_error_enum_ident(op);
1205
1206 let pattern = match code.as_str() {
1211 "default" | "Default" => return None, other if other.chars().all(|c| c.is_ascii_digit()) => {
1213 let n: u16 = other.parse().ok()?;
1214 quote! { #n }
1215 }
1216 "1XX" | "1xx" => quote! { code if (100..=199).contains(&code) },
1217 "2XX" | "2xx" => quote! { code if (200..=299).contains(&code) },
1218 "3XX" | "3xx" => quote! { code if (300..=399).contains(&code) },
1219 "4XX" | "4xx" => quote! { code if (400..=499).contains(&code) },
1220 "5XX" | "5xx" => quote! { code if (500..=599).contains(&code) },
1221 _ => return None,
1222 };
1223
1224 Some(quote! {
1225 #pattern => {
1226 match serde_json::from_str::<#payload_ty>(&body_text) {
1227 Ok(v) => {
1228 typed = Some(#enum_ident::#variant_ident(v));
1229 parse_error = None;
1230 }
1231 Err(e) => {
1232 typed = None;
1233 parse_error = Some(e.to_string());
1234 }
1235 }
1236 }
1237 })
1238 })
1239 .collect();
1240
1241 let has_typed_enum = op
1249 .response_schemas
1250 .iter()
1251 .any(|(code, _)| !code.starts_with('2'));
1252
1253 let default_arm = if has_typed_enum {
1254 quote! {
1255 _ => {
1256 typed = None;
1257 parse_error = None;
1258 }
1259 }
1260 } else {
1261 quote! {
1263 _ => {
1264 match serde_json::from_str::<serde_json::Value>(&body_text) {
1265 Ok(v) => {
1266 typed = Some(v);
1267 parse_error = None;
1268 }
1269 Err(e) => {
1270 typed = None;
1271 parse_error = Some(e.to_string());
1272 }
1273 }
1274 }
1275 }
1276 };
1277
1278 if arms.is_empty() {
1279 quote! {
1281 match status_code {
1282 #default_arm
1283 }
1284 }
1285 } else {
1286 quote! {
1287 match status_code {
1288 #(#arms)*
1289 #default_arm
1290 }
1291 }
1292 }
1293 }
1294
1295 fn generate_url_construction(&self, path: &str, op: &OperationInfo) -> TokenStream {
1297 if path.contains('{') {
1299 self.generate_url_with_params(path, op)
1300 } else {
1301 quote! {
1302 let request_url = format!("{}{}", self.base_url, #path);
1303 }
1304 }
1305 }
1306
1307 fn generate_url_with_params(&self, path: &str, op: &OperationInfo) -> TokenStream {
1309 let path_params: Vec<_> = op
1311 .parameters
1312 .iter()
1313 .filter(|p| p.location == "path")
1314 .collect();
1315
1316 let mut format_string = String::with_capacity(path.len());
1325 let mut format_args: Vec<TokenStream> = Vec::new();
1326 let mut chars = path.chars().peekable();
1327 while let Some(c) = chars.next() {
1328 if c != '{' {
1329 format_string.push(c);
1330 continue;
1331 }
1332 let mut name = String::new();
1334 while let Some(&n) = chars.peek() {
1335 chars.next();
1336 if n == '}' {
1337 break;
1338 }
1339 name.push(n);
1340 }
1341 let param = path_params.iter().find(|p| p.name == name);
1345 let Some(param) = param else {
1346 format_string.push('{');
1347 format_string.push_str(&name);
1348 format_string.push('}');
1349 continue;
1350 };
1351 format_string.push_str("{}");
1352 let param_name_snake = self.param_ident_str(param);
1353 let param_ident = Self::to_field_ident(¶m_name_snake);
1354 if Self::param_uses_as_ref_str(param) {
1355 format_args.push(quote! {
1356 __pct_encode_path_segment(#param_ident.as_ref())
1357 });
1358 } else {
1359 format_args.push(quote! {
1360 __pct_encode_path_segment(&#param_ident.to_string())
1361 });
1362 }
1363 }
1364
1365 if format_args.is_empty() {
1366 quote! {
1367 let request_url = format!("{}{}", self.base_url, #path);
1368 }
1369 } else {
1370 quote! {
1371 let request_url = format!("{}{}", self.base_url, format!(#format_string, #(#format_args),*));
1372 }
1373 }
1374 }
1375
1376 fn param_ident_str(&self, param: &crate::analysis::ParameterInfo) -> String {
1381 if let Some(ident) = ¶m.rust_ident {
1382 return self.escape_keyword_ident(ident);
1386 }
1387 self.sanitize_param_name(¶m.name)
1388 }
1389
1390 fn escape_keyword_ident(&self, snake_case: &str) -> String {
1391 if matches!(snake_case, "self" | "super" | "crate" | "Self") {
1392 return format!("{snake_case}_param");
1393 }
1394 if Self::is_rust_keyword(snake_case) {
1395 format!("r#{snake_case}")
1396 } else {
1397 snake_case.to_string()
1398 }
1399 }
1400
1401 fn sanitize_param_name(&self, name: &str) -> String {
1406 let suffix = if name.ends_with("<=") {
1411 "_lte"
1412 } else if name.ends_with(">=") {
1413 "_gte"
1414 } else if name.ends_with('<') {
1415 "_lt"
1416 } else if name.ends_with('>') {
1417 "_gt"
1418 } else {
1419 ""
1420 };
1421 let stripped = name.trim_end_matches(['<', '>', '=']);
1422 let mut snake_case = stripped.to_snake_case();
1423 snake_case.push_str(suffix);
1424
1425 if matches!(snake_case.as_str(), "self" | "super" | "crate" | "Self") {
1426 return format!("{snake_case}_param");
1427 }
1428 if Self::is_rust_keyword(&snake_case) {
1429 format!("r#{snake_case}")
1430 } else {
1431 snake_case
1432 }
1433 }
1434}