1use crate::analysis::{OperationInfo, OperationResponseBody, ParameterInfo, SchemaAnalysis};
151use crate::generator::CodeGenerator;
152use heck::{ToPascalCase, ToSnakeCase};
153use proc_macro2::TokenStream;
154use quote::{format_ident, quote};
155use std::collections::BTreeMap;
156
157struct AllocatedOperationParam<'a> {
158 param: &'a ParameterInfo,
159 ident: syn::Ident,
160}
161
162#[derive(Clone)]
163struct BodyFieldPlan {
164 wire_name: String,
165 preferred_method_name: String,
166 value_ident: syn::Ident,
167 value_type: TokenStream,
168 access_path: Vec<syn::Ident>,
169 tri_state: bool,
170}
171
172#[derive(Clone, Copy)]
173enum MultipartClientFieldKind {
174 RawBytes,
175 Base64,
176 Base64UrlUnpadded,
177 Text,
178}
179
180#[derive(Clone, Copy)]
181enum ClientSuccessBody<'a> {
182 Json(&'a str),
183 Text,
184 Binary,
185 EventStream,
186 Empty,
187}
188
189#[derive(Clone)]
190struct ClientSuccessSelection<'a> {
191 statuses: Vec<&'a str>,
192 body: ClientSuccessBody<'a>,
193 accept: Option<&'a str>,
194}
195
196enum RequiredBodyConstruction {
197 Default,
198 New(Vec<BodyConstructorParam>),
199 Whole,
200}
201
202struct BodyConstructorParam {
203 preferred_ident: syn::Ident,
204 value_type: TokenStream,
205}
206
207struct BodyModelPlan {
208 body_ident: syn::Ident,
209 body_type: TokenStream,
210 required_construction: RequiredBodyConstruction,
211 optional_fields: Vec<BodyFieldPlan>,
212}
213
214impl CodeGenerator {
215 pub fn generate_http_client_struct(&self) -> TokenStream {
217 let has_retry = self.config().retry_config.is_some();
218 let has_tracing = self.config().tracing_enabled;
219
220 let retry_config_struct = if has_retry {
222 quote! {
223 #[derive(Debug, Clone)]
225 pub struct RetryConfig {
226 pub max_retries: u32,
227 pub initial_delay_ms: u64,
228 pub max_delay_ms: u64,
229 }
230
231 impl Default for RetryConfig {
232 fn default() -> Self {
233 Self {
234 max_retries: 3,
235 initial_delay_ms: 500,
236 max_delay_ms: 16000,
237 }
238 }
239 }
240 }
241 } else {
242 quote! {}
243 };
244
245 let client_struct = quote! {
247 use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
248 use std::collections::BTreeMap;
249
250 pub const DEFAULT_MAX_RESPONSE_BODY_BYTES: usize = 8 * 1024 * 1024;
252
253 #[derive(Clone)]
255 pub struct HttpClient {
256 base_url: String,
257 api_key: Option<String>,
258 http_client: ClientWithMiddleware,
259 custom_headers: BTreeMap<String, String>,
260 max_response_body_bytes: usize,
261 }
262
263 async fn __read_bounded_response_body(
270 response: reqwest::Response,
271 limit: usize,
272 ) -> Result<Vec<u8>, HttpError> {
273 use futures_util::StreamExt;
274
275 let mut body = Vec::new();
276 let mut stream = std::pin::pin!(response.bytes_stream());
277 while let Some(chunk) = stream.next().await {
278 let chunk = chunk.map_err(HttpError::Network)?;
279 let next_len = body.len().checked_add(chunk.len());
280 if next_len.is_none_or(|next_len| next_len > limit) {
281 return Err(HttpError::ResponseTooLarge { limit });
282 }
283 body.extend_from_slice(&chunk);
284 }
285 Ok(body)
286 }
287 };
288
289 let constructor = self.generate_constructor(has_retry, has_tracing);
291
292 let builder_methods = self.generate_builder_methods();
294
295 let default_impl = quote! {
297 impl Default for HttpClient {
298 fn default() -> Self {
299 Self::new()
300 }
301 }
302 };
303
304 let path_encoder = quote! {
308 fn __pct_encode_path_segment(s: &str) -> String {
309 let mut out = String::with_capacity(s.len());
310 for &b in s.as_bytes() {
311 match b {
312 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
313 out.push(b as char);
314 }
315 _ => {
316 out.push('%');
317 out.push_str(&format!("{:02X}", b));
318 }
319 }
320 }
321 out
322 }
323 };
324
325 quote! {
327 #retry_config_struct
328 #client_struct
329
330 impl HttpClient {
331 #constructor
332 #builder_methods
333 }
334
335 #default_impl
336 #path_encoder
337 }
338 }
339
340 fn generate_constructor(&self, has_retry: bool, has_tracing: bool) -> TokenStream {
342 let configured_base_url = self
349 .config()
350 .http_client_config
351 .as_ref()
352 .and_then(|http| http.base_url.as_deref())
353 .unwrap_or_default();
354 let default_base_url = quote! { #configured_base_url.to_string() };
355 let max_response_body_bytes = self
356 .config()
357 .http_client_config
358 .as_ref()
359 .and_then(|http| http.max_response_body_bytes)
360 .unwrap_or(8 * 1024 * 1024);
361
362 let retry_param = if has_retry {
363 quote! { retry_config: Option<RetryConfig>, }
364 } else {
365 quote! {}
366 };
367
368 let tracing_param = if has_tracing {
369 quote! { enable_tracing: bool, }
370 } else {
371 quote! {}
372 };
373
374 let retry_middleware = if has_retry {
375 quote! {
376 if let Some(config) = retry_config {
377 use reqwest_retry::{RetryTransientMiddleware, policies::ExponentialBackoff};
378
379 let retry_policy = ExponentialBackoff::builder()
380 .retry_bounds(
381 std::time::Duration::from_millis(config.initial_delay_ms),
382 std::time::Duration::from_millis(config.max_delay_ms),
383 )
384 .build_with_max_retries(config.max_retries);
385
386 let retry_middleware = RetryTransientMiddleware::new_with_policy(retry_policy);
387 client_builder = client_builder.with(retry_middleware);
388 }
389 }
390 } else {
391 quote! {}
392 };
393
394 let tracing_middleware = if has_tracing {
395 quote! {
396 if enable_tracing {
397 use reqwest_tracing::TracingMiddleware;
398 client_builder = client_builder.with(TracingMiddleware::default());
399 }
400 }
401 } else {
402 quote! {}
403 };
404
405 let default_constructor = if has_retry && has_tracing {
406 quote! {
407 pub fn new() -> Self {
409 Self::with_config(None, true)
410 }
411 }
412 } else if has_retry {
413 quote! {
414 pub fn new() -> Self {
416 Self::with_config(None)
417 }
418 }
419 } else if has_tracing {
420 quote! {
421 pub fn new() -> Self {
423 Self::with_config(true)
424 }
425 }
426 } else {
427 quote! {
428 pub fn new() -> Self {
430 let reqwest_client = reqwest::Client::new();
431 let client_builder = ClientBuilder::new(reqwest_client);
432 let http_client = client_builder.build();
433
434 Self {
435 base_url: #default_base_url,
436 api_key: None,
437 http_client,
438 custom_headers: BTreeMap::new(),
439 max_response_body_bytes: #max_response_body_bytes,
440 }
441 }
442 }
443 };
444
445 if has_retry || has_tracing {
446 quote! {
447 #default_constructor
448
449 pub fn with_config(#retry_param #tracing_param) -> Self {
451 let reqwest_client = reqwest::Client::new();
452 let mut client_builder = ClientBuilder::new(reqwest_client);
453
454 #tracing_middleware
455 #retry_middleware
456
457 let http_client = client_builder.build();
458
459 Self {
460 base_url: #default_base_url,
461 api_key: None,
462 http_client,
463 custom_headers: BTreeMap::new(),
464 max_response_body_bytes: #max_response_body_bytes,
465 }
466 }
467 }
468 } else {
469 default_constructor
470 }
471 }
472
473 fn generate_builder_methods(&self) -> TokenStream {
475 quote! {
476 pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
478 self.base_url = base_url.into();
479 self
480 }
481
482 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
484 self.api_key = Some(api_key.into());
485 self
486 }
487
488 pub fn with_max_response_body_bytes(mut self, limit: usize) -> Self {
490 self.max_response_body_bytes = limit;
491 self
492 }
493
494 pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
496 self.custom_headers.insert(name.into(), value.into());
497 self
498 }
499
500 pub fn with_headers(mut self, headers: BTreeMap<String, String>) -> Self {
502 self.custom_headers.extend(headers);
503 self
504 }
505 }
506 }
507
508 pub fn generate_operation_methods(&self, analysis: &SchemaAnalysis) -> TokenStream {
517 let operations: Vec<&OperationInfo> = analysis.operations.values().collect();
518 self.generate_operation_methods_for(analysis, &operations)
519 }
520
521 pub(crate) fn generate_operation_methods_for(
525 &self,
526 analysis: &SchemaAnalysis,
527 operations: &[&OperationInfo],
528 ) -> TokenStream {
529 let param_enums = self.generate_param_enum_types(operations);
530
531 let op_error_enums: Vec<TokenStream> = operations
532 .iter()
533 .copied()
534 .filter_map(|op| self.generate_op_error_enum(op))
535 .collect();
536
537 let methods: Vec<TokenStream> = operations
538 .iter()
539 .copied()
540 .map(|op| self.generate_single_operation_method(analysis, op))
541 .collect();
542
543 let (operation_builders, builder_entries) =
544 self.generate_operation_builders(analysis, operations);
545
546 quote! {
547 #param_enums
548
549 #(#op_error_enums)*
550
551 #(#operation_builders)*
552
553 impl HttpClient {
554 #(#methods)*
555 #(#builder_entries)*
556 }
557 }
558 }
559
560 fn generate_operation_builders(
561 &self,
562 analysis: &SchemaAnalysis,
563 operations: &[&OperationInfo],
564 ) -> (Vec<TokenStream>, Vec<TokenStream>) {
565 if !self.config().builders.enabled {
566 return (Vec::new(), Vec::new());
567 }
568
569 let mut used_entry_methods: std::collections::HashSet<String> = operations
570 .iter()
571 .map(|operation| self.get_method_name(operation).to_string())
572 .collect();
573 let mut used_type_names = std::collections::HashSet::new();
574 for schema_name in analysis.schemas.keys() {
575 let rust_name = self.to_rust_type_name(schema_name);
576 used_type_names.insert(rust_name.clone());
577 used_type_names.insert(format!("{rust_name}Builder"));
581 }
582 used_type_names.insert("HttpClient".to_string());
583 used_type_names.insert("ApiOpError".to_string());
584 used_type_names.extend(
585 [
586 "ClientBuilder",
587 "ClientWithMiddleware",
588 "RetryConfig",
589 "HttpError",
590 "BTreeMap",
591 ]
592 .into_iter()
593 .map(str::to_string),
594 );
595 for operation in operations {
596 used_type_names.insert(self.op_error_enum_ident(operation).to_string());
597 used_type_names.extend(
598 operation
599 .parameters
600 .iter()
601 .filter(|parameter| parameter.enum_values.is_some())
602 .map(|parameter| parameter.rust_type.clone()),
603 );
604 }
605
606 let mut definitions = Vec::new();
607 let mut entries = Vec::new();
608 for operation in operations {
609 let allocated_params = self.allocated_operation_params(operation);
610 let body_plan = self.body_model_plan(operation, analysis);
611 let optional_param_count = allocated_params
612 .iter()
613 .filter(|allocated| !Self::builder_param_is_required(allocated.param))
614 .count();
615 let optional_body_count =
616 usize::from(operation.request_body.is_some() && !operation.request_body_required);
617 let body_field_count = body_plan
618 .as_ref()
619 .filter(|plan| {
620 operation.request_body_required
621 || matches!(
622 &plan.required_construction,
623 RequiredBodyConstruction::Default
624 )
625 })
626 .map_or(0, |plan| plan.optional_fields.len());
627 let optional_count = optional_param_count + optional_body_count + body_field_count;
628 if optional_count <= self.config().builders.threshold {
629 continue;
630 }
631
632 let flat_method = self.get_method_name(operation);
633 let entry_base = format!("{flat_method}_builder");
634 let entry_name = Self::allocate_name(&entry_base, &mut used_entry_methods);
635 let entry_ident = Self::to_field_ident(&entry_name);
636
637 let builder_base = format!("{}Builder", flat_method.to_string().to_pascal_case());
638 let builder_name = Self::allocate_type_name(&builder_base, &mut used_type_names);
639 let builder_ident = format_ident!("{builder_name}");
640
641 let (definition, entry) = self.generate_single_operation_builder(
642 analysis,
643 operation,
644 &allocated_params,
645 body_plan,
646 &flat_method,
647 &entry_ident,
648 &builder_ident,
649 );
650 definitions.push(definition);
651 entries.push(entry);
652 }
653
654 (definitions, entries)
655 }
656
657 #[allow(clippy::too_many_arguments)]
658 fn generate_single_operation_builder(
659 &self,
660 analysis: &SchemaAnalysis,
661 operation: &OperationInfo,
662 allocated_params: &[AllocatedOperationParam<'_>],
663 body_plan: Option<BodyModelPlan>,
664 flat_method: &syn::Ident,
665 entry_ident: &syn::Ident,
666 builder_ident: &syn::Ident,
667 ) -> (TokenStream, TokenStream) {
668 let mut fields = vec![quote! { client: &'a HttpClient }];
669 let mut entry_parameters = Vec::new();
670 let mut initializers = vec![quote! { client: self }];
671 let mut call_arguments = Vec::new();
672 let mut setters = Vec::new();
673 let mut used_entry_params = std::collections::HashSet::new();
674 let mut used_methods = std::collections::HashSet::from(["send".to_string()]);
675
676 for allocated in allocated_params {
677 let field_ident = &allocated.ident;
678 let storage_type = self.builder_param_storage_type(allocated.param);
679 if Self::builder_param_is_required(allocated.param) {
680 fields.push(quote! { #field_ident: #storage_type });
681 let entry_name =
682 Self::allocate_name(&field_ident.to_string(), &mut used_entry_params);
683 let entry_param = Self::to_field_ident(&entry_name);
684 if Self::param_has_impl_as_ref_type(allocated.param) {
685 entry_parameters.push(quote! { #entry_param: impl Into<String> });
686 initializers.push(quote! { #field_ident: #entry_param.into() });
687 } else {
688 entry_parameters.push(quote! { #entry_param: #storage_type });
689 initializers.push(quote! { #field_ident: #entry_param });
690 }
691 } else {
692 fields.push(quote! { #field_ident: Option<#storage_type> });
693 initializers.push(quote! { #field_ident: None });
694 let setter_ident =
695 Self::allocate_builder_method(&field_ident.to_string(), &mut used_methods);
696 let wire_name = &allocated.param.name;
697 let assignment = if Self::param_has_impl_as_ref_type(allocated.param) {
698 quote! { self.#field_ident = Some(#field_ident.into()); }
699 } else {
700 quote! { self.#field_ident = Some(#field_ident); }
701 };
702 let setter_type = if Self::param_has_impl_as_ref_type(allocated.param) {
703 quote! { impl Into<String> }
704 } else {
705 storage_type.clone()
706 };
707 setters.push(quote! {
708 #[doc = concat!("Set the optional `", #wire_name, "` operation parameter.")]
709 #[must_use]
710 pub fn #setter_ident(mut self, #field_ident: #setter_type) -> Self {
711 #assignment
712 self
713 }
714 });
715 }
716 call_arguments.push(quote! { self.#field_ident });
717 }
718
719 if let Some(body_plan) = body_plan {
720 let BodyModelPlan {
721 body_ident,
722 body_type,
723 required_construction,
724 optional_fields,
725 } = body_plan;
726 let can_initialize_optional_body =
727 matches!(&required_construction, RequiredBodyConstruction::Default);
728 if operation.request_body_required {
729 fields.push(quote! { #body_ident: #body_type });
730 match required_construction {
731 RequiredBodyConstruction::Default => {
732 initializers.push(quote! { #body_ident: Default::default() });
733 }
734 RequiredBodyConstruction::New(constructor_params) => {
735 let mut constructor_args = Vec::new();
736 for constructor in constructor_params {
737 let preferred = constructor.preferred_ident.to_string();
738 let entry_name =
739 Self::allocate_name(&preferred, &mut used_entry_params);
740 let entry_param = Self::to_field_ident(&entry_name);
741 let value_type = constructor.value_type;
742 entry_parameters.push(quote! { #entry_param: #value_type });
743 constructor_args.push(entry_param);
744 }
745 initializers.push(quote! {
746 #body_ident: #body_type::new(#(#constructor_args),*)
747 });
748 }
749 RequiredBodyConstruction::Whole => {
750 let entry_name =
751 Self::allocate_name(&body_ident.to_string(), &mut used_entry_params);
752 let entry_param = Self::to_field_ident(&entry_name);
753 entry_parameters.push(quote! { #entry_param: #body_type });
754 initializers.push(quote! { #body_ident: #entry_param });
755 }
756 }
757 } else {
758 fields.push(quote! { #body_ident: Option<#body_type> });
759 initializers.push(quote! { #body_ident: None });
760 }
761
762 let body_setter =
763 Self::allocate_builder_method(&body_ident.to_string(), &mut used_methods);
764 let body_assignment = if operation.request_body_required {
765 quote! { self.#body_ident = #body_ident; }
766 } else {
767 quote! { self.#body_ident = Some(#body_ident); }
768 };
769 setters.push(quote! {
770 #[must_use]
772 pub fn #body_setter(mut self, #body_ident: #body_type) -> Self {
773 #body_assignment
774 self
775 }
776 });
777
778 if operation.request_body_required || can_initialize_optional_body {
779 for field in optional_fields {
780 let setter_ident = Self::allocate_builder_method(
781 &field.preferred_method_name,
782 &mut used_methods,
783 );
784 let preferred_method_name = field.preferred_method_name.clone();
785 let value_ident = field.value_ident;
786 let value_type = field.value_type;
787 let wire_name = field.wire_name;
788 let access_path = field.access_path;
789 let assignment = if operation.request_body_required {
790 let mut target = quote! { self.#body_ident };
791 for access in &access_path {
792 target = quote! { #target.#access };
793 }
794 if field.tri_state {
795 quote! { #target = Some(Some(#value_ident)); }
796 } else {
797 quote! { #target = Some(#value_ident); }
798 }
799 } else {
800 let mut target = quote! { request };
801 for access in &access_path {
802 target = quote! { #target.#access };
803 }
804 if field.tri_state {
805 quote! {
806 let request = self.#body_ident.get_or_insert_with(Default::default);
807 #target = Some(Some(#value_ident));
808 }
809 } else {
810 quote! {
811 let request = self.#body_ident.get_or_insert_with(Default::default);
812 #target = Some(#value_ident);
813 }
814 }
815 };
816 setters.push(quote! {
817 #[doc = concat!("Set the optional request-body field `", #wire_name, "`.")]
818 #[must_use]
819 pub fn #setter_ident(mut self, #value_ident: #value_type) -> Self {
820 #assignment
821 self
822 }
823 });
824 if field.tri_state {
825 let null_ident = Self::allocate_builder_method(
826 &format!("{preferred_method_name}_null"),
827 &mut used_methods,
828 );
829 let absent_ident = Self::allocate_builder_method(
830 &format!("{preferred_method_name}_absent"),
831 &mut used_methods,
832 );
833 let null_assignment = if operation.request_body_required {
834 let mut target = quote! { self.#body_ident };
835 for access in &access_path {
836 target = quote! { #target.#access };
837 }
838 quote! { #target = Some(None); }
839 } else {
840 let mut target = quote! { request };
841 for access in &access_path {
842 target = quote! { #target.#access };
843 }
844 quote! {
845 let request = self.#body_ident.get_or_insert_with(Default::default);
846 #target = Some(None);
847 }
848 };
849 let absent_assignment = if operation.request_body_required {
850 let mut target = quote! { self.#body_ident };
851 for access in &access_path {
852 target = quote! { #target.#access };
853 }
854 quote! { #target = None; }
855 } else {
856 let mut target = quote! { request };
857 for access in &access_path {
858 target = quote! { #target.#access };
859 }
860 quote! {
861 if let Some(request) = self.#body_ident.as_mut() {
862 #target = None;
863 }
864 }
865 };
866 setters.push(quote! {
867 #[doc = concat!("Set the optional nullable request-body field `", #wire_name, "` to JSON null.")]
868 #[must_use]
869 pub fn #null_ident(mut self) -> Self {
870 #null_assignment
871 self
872 }
873
874 #[doc = concat!("Omit the optional nullable request-body field `", #wire_name, "`.")]
875 #[must_use]
876 pub fn #absent_ident(mut self) -> Self {
877 #absent_assignment
878 self
879 }
880 });
881 }
882 }
883 }
884 call_arguments.push(quote! { self.#body_ident });
885 }
886
887 let response_type = self.get_response_type(analysis, operation);
888 let error_type = self.op_error_type_token(operation);
889 let operation_id = &operation.operation_id;
890 let definition = quote! {
891 #[doc = concat!("Additive request builder for `", #operation_id, "`.")]
892 #[must_use]
893 pub struct #builder_ident<'a> {
894 #(#fields,)*
895 }
896
897 impl<'a> #builder_ident<'a> {
898 #(#setters)*
899
900 pub async fn send(self) -> Result<#response_type, ApiOpError<#error_type>> {
902 self.client.#flat_method(#(#call_arguments),*).await
903 }
904 }
905 };
906 let entry = quote! {
907 #[doc = concat!("Start an additive builder for `", #operation_id, "`.")]
908 pub fn #entry_ident(
909 &self,
910 #(#entry_parameters),*
911 ) -> #builder_ident<'_> {
912 #builder_ident {
913 #(#initializers,)*
914 }
915 }
916 };
917 (definition, entry)
918 }
919
920 fn allocate_name(base: &str, used: &mut std::collections::HashSet<String>) -> String {
921 let mut candidate = base.to_string();
922 let mut suffix = 2;
923 while !used.insert(candidate.clone()) {
924 candidate = format!("{base}_{suffix}");
925 suffix += 1;
926 }
927 candidate
928 }
929
930 fn allocate_type_name(base: &str, used: &mut std::collections::HashSet<String>) -> String {
931 if used.insert(base.to_string()) {
932 return base.to_string();
933 }
934
935 let mut suffix = 2;
936 loop {
937 let candidate = format!("{base}{suffix}");
938 if used.insert(candidate.clone()) {
939 return candidate;
940 }
941 suffix += 1;
942 }
943 }
944
945 fn allocate_builder_method(
946 preferred: &str,
947 used: &mut std::collections::HashSet<String>,
948 ) -> syn::Ident {
949 let plain = preferred.strip_prefix("r#").unwrap_or(preferred);
950 let base = if used.contains(preferred) {
951 format!("with_{plain}")
952 } else {
953 preferred.to_string()
954 };
955 let allocated = Self::allocate_name(&base, used);
956 Self::to_field_ident(&allocated)
957 }
958
959 fn allocated_operation_params<'a>(
960 &self,
961 operation: &'a OperationInfo,
962 ) -> Vec<AllocatedOperationParam<'a>> {
963 let mut used = std::collections::HashSet::from([
967 "client".to_string(),
968 "request".to_string(),
969 "form".to_string(),
970 "body".to_string(),
971 ]);
972 let mut allocated = Vec::new();
973 for location in ["path", "query", "header", "cookie"] {
974 for parameter in &operation.parameters {
975 if parameter.location != location {
976 continue;
977 }
978 let raw = self.param_ident_str(parameter);
979 let chosen = Self::allocate_name(&raw, &mut used);
980 allocated.push(AllocatedOperationParam {
981 param: parameter,
982 ident: Self::to_field_ident(&chosen),
983 });
984 }
985 }
986 allocated
987 }
988
989 fn builder_param_is_required(parameter: &ParameterInfo) -> bool {
990 parameter.location == "path" || parameter.required
994 }
995
996 fn builder_param_storage_type(&self, parameter: &ParameterInfo) -> TokenStream {
997 self.get_param_owned_rust_type(parameter)
998 }
999
1000 fn param_has_impl_as_ref_type(parameter: &ParameterInfo) -> bool {
1001 !matches!(
1002 ¶meter.query_serialization,
1003 Some(
1004 crate::analysis::QuerySerialization::FormExplodedArray { .. }
1005 | crate::analysis::QuerySerialization::FormArray { .. }
1006 | crate::analysis::QuerySerialization::SimpleHeaderArray { .. },
1007 )
1008 ) && Self::param_uses_as_ref_str(parameter)
1009 }
1010
1011 fn body_model_plan(
1012 &self,
1013 operation: &OperationInfo,
1014 analysis: &SchemaAnalysis,
1015 ) -> Option<BodyModelPlan> {
1016 use crate::analysis::{RequestBodyContent, SchemaType};
1017
1018 let request_body = operation.request_body.as_ref()?;
1019 let (body_name, body_ident) = match request_body {
1020 RequestBodyContent::Json { schema_name, .. }
1021 | RequestBodyContent::FormUrlEncoded { schema_name, .. }
1022 | RequestBodyContent::Multipart { schema_name, .. } => {
1023 (schema_name.as_str(), format_ident!("request"))
1024 }
1025 RequestBodyContent::OctetStream { .. }
1026 | RequestBodyContent::Binary { .. }
1027 | RequestBodyContent::Unsupported { .. } => {
1028 return Some(BodyModelPlan {
1029 body_ident: format_ident!("body"),
1030 body_type: quote! { Vec<u8> },
1031 required_construction: RequiredBodyConstruction::Whole,
1032 optional_fields: Vec::new(),
1033 });
1034 }
1035 RequestBodyContent::TextPlain { .. } => {
1036 return Some(BodyModelPlan {
1037 body_ident: format_ident!("body"),
1038 body_type: quote! { String },
1039 required_construction: RequiredBodyConstruction::Whole,
1040 optional_fields: Vec::new(),
1041 });
1042 }
1043 RequestBodyContent::SchemaLess { .. } => return None,
1044 };
1045 let body_type_name = self.to_rust_type_name(body_name);
1046 let body_type = syn::Ident::new(&body_type_name, proc_macro2::Span::call_site());
1047 let Some((resolved_name, resolved_schema)) =
1048 self.resolve_reference_schema(body_name, analysis)
1049 else {
1050 return Some(BodyModelPlan {
1051 body_ident,
1052 body_type: quote! { #body_type },
1053 required_construction: RequiredBodyConstruction::Whole,
1054 optional_fields: Vec::new(),
1055 });
1056 };
1057
1058 let mut optional_fields = Vec::new();
1059 let mut stack = std::collections::HashSet::new();
1060 self.collect_optional_body_fields(
1061 resolved_name,
1062 Vec::new(),
1063 analysis,
1064 &mut stack,
1065 &mut optional_fields,
1066 );
1067
1068 let required_construction = match &resolved_schema.schema_type {
1069 SchemaType::Object {
1070 properties,
1071 required,
1072 additional_properties,
1073 ..
1074 } if !self.is_discriminated_variant(resolved_name, analysis) => {
1075 let emitted = self.emitted_object_properties(
1076 resolved_name,
1077 properties,
1078 required,
1079 additional_properties,
1080 analysis,
1081 );
1082 let required_fields: Vec<_> = emitted
1083 .iter()
1084 .filter(|field| field.is_required)
1085 .map(|field| BodyConstructorParam {
1086 preferred_ident: field.ident.clone(),
1087 value_type: field.field_type.clone(),
1088 })
1089 .collect();
1090 if required_fields.is_empty() {
1091 RequiredBodyConstruction::Default
1092 } else if emitted.iter().any(|field| !field.is_required)
1093 || additional_properties.is_open()
1094 {
1095 RequiredBodyConstruction::New(required_fields)
1096 } else {
1097 RequiredBodyConstruction::Whole
1098 }
1099 }
1100 _ => RequiredBodyConstruction::Whole,
1101 };
1102
1103 Some(BodyModelPlan {
1104 body_ident,
1105 body_type: quote! { #body_type },
1106 required_construction,
1107 optional_fields,
1108 })
1109 }
1110
1111 fn resolve_reference_schema<'a>(
1112 &self,
1113 schema_name: &'a str,
1114 analysis: &'a SchemaAnalysis,
1115 ) -> Option<(&'a str, &'a crate::analysis::AnalyzedSchema)> {
1116 let mut current = schema_name;
1117 let mut visited = std::collections::HashSet::new();
1118 loop {
1119 if !visited.insert(current) {
1120 return None;
1121 }
1122 let schema = analysis.schemas.get(current)?;
1123 if let crate::analysis::SchemaType::Reference { target } = &schema.schema_type {
1124 current = target;
1125 } else {
1126 return Some((current, schema));
1127 }
1128 }
1129 }
1130
1131 fn collect_optional_body_fields(
1132 &self,
1133 schema_name: &str,
1134 access_path: Vec<syn::Ident>,
1135 analysis: &SchemaAnalysis,
1136 stack: &mut std::collections::HashSet<String>,
1137 output: &mut Vec<BodyFieldPlan>,
1138 ) {
1139 use crate::analysis::SchemaType;
1140 if !stack.insert(schema_name.to_string()) {
1141 return;
1142 }
1143 let Some(schema) = analysis.schemas.get(schema_name) else {
1144 stack.remove(schema_name);
1145 return;
1146 };
1147 match &schema.schema_type {
1148 SchemaType::Reference { target } => {
1149 self.collect_optional_body_fields(target, access_path, analysis, stack, output);
1150 }
1151 SchemaType::Object {
1152 properties,
1153 required,
1154 additional_properties,
1155 ..
1156 } if !self.is_discriminated_variant(schema_name, analysis) => {
1157 for field in self.emitted_object_properties(
1158 schema_name,
1159 properties,
1160 required,
1161 additional_properties,
1162 analysis,
1163 ) {
1164 if field.is_required {
1165 continue;
1166 }
1167 let mut field_path = access_path.clone();
1168 field_path.push(field.ident.clone());
1169 output.push(BodyFieldPlan {
1170 wire_name: field.wire_name.to_string(),
1171 preferred_method_name: field.ident.to_string(),
1172 value_ident: field.ident.clone(),
1173 value_type: self.generate_property_base_type(
1174 schema_name,
1175 field.wire_name,
1176 field.property,
1177 analysis,
1178 ),
1179 access_path: field_path,
1180 tri_state: self.property_is_tri_state(
1181 schema_name,
1182 field.wire_name,
1183 field.property,
1184 field.is_required,
1185 ),
1186 });
1187 }
1188 }
1189 SchemaType::Composition { schemas } => {
1190 for (index, schema_ref) in schemas.iter().enumerate() {
1191 let mut nested_path = access_path.clone();
1192 nested_path.push(format_ident!("part_{index}"));
1193 self.collect_optional_body_fields(
1194 &schema_ref.target,
1195 nested_path,
1196 analysis,
1197 stack,
1198 output,
1199 );
1200 }
1201 }
1202 _ => {}
1203 }
1204 stack.remove(schema_name);
1205 }
1206
1207 fn is_discriminated_variant(&self, schema_name: &str, analysis: &SchemaAnalysis) -> bool {
1208 analysis.schemas.values().any(|schema| {
1209 matches!(
1210 &schema.schema_type,
1211 crate::analysis::SchemaType::DiscriminatedUnion { variants, .. }
1212 if variants.iter().any(|variant| variant.type_name == schema_name)
1213 )
1214 })
1215 }
1216
1217 fn generate_param_enum_types(&self, operations: &[&OperationInfo]) -> TokenStream {
1222 let mut by_name: BTreeMap<String, &ParameterInfo> = BTreeMap::new();
1223 for op in operations {
1224 for param in &op.parameters {
1225 if param.enum_values.is_some() {
1226 by_name.entry(param.rust_type.clone()).or_insert(param);
1227 }
1228 }
1229 }
1230
1231 if by_name.is_empty() {
1232 return quote! {};
1233 }
1234
1235 let defs: Vec<TokenStream> = by_name
1236 .values()
1237 .map(|param| self.generate_single_param_enum(param))
1238 .collect();
1239
1240 quote! { #(#defs)* }
1241 }
1242
1243 fn generate_single_param_enum(&self, param: &ParameterInfo) -> TokenStream {
1244 let Some(values) = param.enum_values.as_deref() else {
1245 return quote! {};
1246 };
1247
1248 let enum_ident = format_ident!("{}", param.rust_type);
1249
1250 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
1256 let variant_names: Vec<String> = values
1264 .iter()
1265 .enumerate()
1266 .map(|(index, value)| {
1267 let base = param
1268 .enum_varnames
1269 .as_ref()
1270 .and_then(|names| names.get(index))
1271 .map(|name| self.to_rust_enum_variant(name))
1272 .unwrap_or_else(|| self.to_rust_enum_variant(value));
1273 let mut chosen = base.clone();
1274 let mut suffix = 2;
1275 while !used.insert(chosen.clone()) {
1276 chosen = format!("{base}_{suffix}");
1277 suffix += 1;
1278 }
1279 chosen
1280 })
1281 .collect();
1282
1283 let variants: Vec<TokenStream> = values
1284 .iter()
1285 .zip(&variant_names)
1286 .map(|(value, name)| {
1287 let variant_ident = format_ident!("{}", name);
1288 quote! {
1289 #[serde(rename = #value)]
1290 #variant_ident,
1291 }
1292 })
1293 .collect();
1294
1295 let display_arms: Vec<TokenStream> = values
1296 .iter()
1297 .zip(&variant_names)
1298 .map(|(value, name)| {
1299 let variant_ident = format_ident!("{}", name);
1300 quote! { Self::#variant_ident => #value, }
1301 })
1302 .collect();
1303
1304 let doc = format!(
1305 "Allowed values for the `{}` {} parameter.",
1306 param.name, param.location
1307 );
1308
1309 quote! {
1310 #[doc = #doc]
1311 #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
1312 pub enum #enum_ident {
1313 #(#variants)*
1314 }
1315
1316 impl #enum_ident {
1317 pub fn as_str(&self) -> &'static str {
1318 match self {
1319 #(#display_arms)*
1320 }
1321 }
1322 }
1323
1324 impl std::fmt::Display for #enum_ident {
1325 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1326 f.write_str(self.as_str())
1327 }
1328 }
1329
1330 impl AsRef<str> for #enum_ident {
1331 fn as_ref(&self) -> &str {
1332 self.as_str()
1333 }
1334 }
1335 }
1336 }
1337
1338 fn generate_op_error_enum(&self, op: &OperationInfo) -> Option<TokenStream> {
1343 let variants: Vec<(String, String)> = op
1344 .response_schemas
1345 .iter()
1346 .filter(|(code, _)| !code.starts_with('2'))
1347 .map(|(code, schema)| (code.clone(), schema.clone()))
1348 .collect();
1349
1350 if variants.is_empty() {
1351 return None;
1352 }
1353
1354 let enum_ident = self.op_error_enum_ident(op);
1355 let variant_decls: Vec<TokenStream> = variants
1356 .iter()
1357 .map(|(code, schema)| {
1358 let variant_ident = Self::op_error_variant_ident(code);
1359 let payload_ty_name = self.to_rust_type_name(schema);
1360 let payload_ty = syn::Ident::new(&payload_ty_name, proc_macro2::Span::call_site());
1361 quote! { #variant_ident(#payload_ty) }
1362 })
1363 .collect();
1364
1365 let doc = format!(
1366 "Typed error responses for `{}`. One variant per declared non-2xx response.",
1367 op.operation_id
1368 );
1369
1370 Some(quote! {
1371 #[doc = #doc]
1372 #[derive(Debug, Clone)]
1373 pub enum #enum_ident {
1374 #(#variant_decls,)*
1375 }
1376 })
1377 }
1378
1379 fn op_error_enum_ident(&self, op: &OperationInfo) -> syn::Ident {
1381 use heck::ToPascalCase;
1382 let name = format!(
1383 "{}ApiError",
1384 op.operation_id.replace('.', "_").to_pascal_case()
1385 );
1386 syn::Ident::new(&name, proc_macro2::Span::call_site())
1387 }
1388
1389 fn op_error_variant_ident(status_code: &str) -> syn::Ident {
1392 let raw = match status_code {
1393 "default" | "Default" => "Default".to_string(),
1394 other if other.chars().all(|c| c.is_ascii_digit()) => format!("Status{other}"),
1395 other => format!("Status{}", other.to_ascii_lowercase()),
1396 };
1397 syn::Ident::new(&raw, proc_macro2::Span::call_site())
1398 }
1399
1400 fn op_error_type_token(&self, op: &OperationInfo) -> TokenStream {
1404 if op
1405 .response_schemas
1406 .iter()
1407 .any(|(code, _)| !code.starts_with('2'))
1408 {
1409 let ident = self.op_error_enum_ident(op);
1410 quote! { #ident }
1411 } else {
1412 quote! { serde_json::Value }
1413 }
1414 }
1415
1416 fn generate_single_operation_method(
1418 &self,
1419 analysis: &SchemaAnalysis,
1420 op: &OperationInfo,
1421 ) -> TokenStream {
1422 let method_name = self.get_method_name(op);
1423 let http_method_call = self.http_method_call(op);
1424 let path = &op.path;
1425 let request_param = self.generate_request_param(op);
1426 let request_body = self.generate_request_body(op, analysis);
1427 let query_params = self.generate_query_params(op);
1428 let header_params = self.generate_header_params(op);
1429 let cookie_params = self.generate_cookie_params(op);
1430 let auth_application = self.generate_auth_application();
1431 let success = self.get_success_response(analysis, op);
1432 let response_type = self.get_response_type(analysis, op);
1433 let op_error_type = self.op_error_type_token(op);
1434 let accept = success.accept;
1435 let error_handling = self.generate_error_handling(op, success);
1436 let (custom_headers, accept_header) = if let Some(media_type) = accept {
1437 (
1438 quote! {
1439 for (name, value) in &self.custom_headers {
1440 if !name.eq_ignore_ascii_case("accept") {
1441 req = req.header(name, value);
1442 }
1443 }
1444 },
1445 quote! {
1446 req = req.header(reqwest::header::ACCEPT, #media_type);
1447 },
1448 )
1449 } else {
1450 (
1451 quote! {
1452 for (name, value) in &self.custom_headers {
1453 req = req.header(name, value);
1454 }
1455 },
1456 TokenStream::new(),
1457 )
1458 };
1459 let url_construction = self.generate_url_construction(path, op);
1460 let doc_comment = self.generate_operation_doc_comment(op);
1461
1462 quote! {
1463 #doc_comment
1464 pub async fn #method_name(
1465 &self,
1466 #request_param
1467 ) -> Result<#response_type, ApiOpError<#op_error_type>> {
1468 #url_construction
1469
1470 let mut req = #http_method_call;
1471 #request_body
1472
1473 #query_params
1474 #header_params
1475 #cookie_params
1476
1477 #auth_application
1480
1481 #custom_headers
1483
1484 #accept_header
1487
1488 let response = req.send().await?;
1489 #error_handling
1490 }
1491 }
1492 }
1493
1494 fn generate_auth_application(&self) -> TokenStream {
1498 use crate::http_config::AuthConfig;
1499 match &self.config().auth_config {
1500 Some(AuthConfig::Bearer { header_name }) if header_name == "Authorization" => quote! {
1501 if let Some(api_key) = &self.api_key {
1502 req = req.bearer_auth(api_key);
1503 }
1504 },
1505 Some(AuthConfig::Bearer { header_name }) => {
1506 let h = header_name.clone();
1507 quote! {
1508 if let Some(api_key) = &self.api_key {
1509 req = req.header(#h, format!("Bearer {}", api_key));
1510 }
1511 }
1512 }
1513 Some(AuthConfig::ApiKey { header_name }) => {
1514 let h = header_name.clone();
1515 quote! {
1516 if let Some(api_key) = &self.api_key {
1517 req = req.header(#h, api_key.as_str());
1518 }
1519 }
1520 }
1521 Some(AuthConfig::Custom {
1522 header_name,
1523 header_value_prefix,
1524 }) => {
1525 let h = header_name.clone();
1526 let prefix = header_value_prefix.clone().unwrap_or_default();
1527 if prefix.is_empty() {
1528 quote! {
1529 if let Some(api_key) = &self.api_key {
1530 req = req.header(#h, api_key.as_str());
1531 }
1532 }
1533 } else {
1534 let format_str = format!("{}{{}}", prefix);
1535 quote! {
1536 if let Some(api_key) = &self.api_key {
1537 req = req.header(#h, format!(#format_str, api_key));
1538 }
1539 }
1540 }
1541 }
1542 None => quote! {
1543 if let Some(api_key) = &self.api_key {
1544 req = req.bearer_auth(api_key);
1545 }
1546 },
1547 }
1548 }
1549
1550 fn generate_header_params(&self, op: &OperationInfo) -> TokenStream {
1554 let header_params: Vec<_> = op
1555 .parameters
1556 .iter()
1557 .filter(|p| p.location == "header")
1558 .collect();
1559 if header_params.is_empty() {
1560 return quote! {};
1561 }
1562 let mut emit = Vec::new();
1563 for param in header_params {
1564 let param_name_snake = self.param_ident_str(param);
1565 let param_ident = Self::to_field_ident(¶m_name_snake);
1566 let header_name = ¶m.name;
1567 if matches!(
1568 param.query_serialization,
1569 Some(crate::analysis::QuerySerialization::SimpleHeaderArray { .. })
1570 ) {
1571 let encode = quote! {
1572 v.iter().map(::std::string::ToString::to_string).collect::<Vec<_>>().join(",")
1573 };
1574 if param.required {
1575 emit.push(quote! {
1576 let v = #param_ident;
1577 req = req.header(#header_name, #encode);
1578 });
1579 } else {
1580 emit.push(quote! {
1581 if let Some(v) = #param_ident {
1582 req = req.header(#header_name, #encode);
1583 }
1584 });
1585 }
1586 continue;
1587 }
1588 if param.required {
1589 if Self::param_uses_as_ref_str(param) {
1590 emit.push(quote! {
1591 req = req.header(#header_name, #param_ident.as_ref());
1592 });
1593 } else {
1594 emit.push(quote! {
1595 req = req.header(#header_name, #param_ident.to_string());
1596 });
1597 }
1598 } else if Self::param_uses_as_ref_str(param) {
1599 emit.push(quote! {
1600 if let Some(v) = #param_ident {
1601 req = req.header(#header_name, v.as_ref());
1602 }
1603 });
1604 } else {
1605 emit.push(quote! {
1606 if let Some(v) = #param_ident {
1607 req = req.header(#header_name, v.to_string());
1608 }
1609 });
1610 }
1611 }
1612 quote! {
1613 #(#emit)*
1614 }
1615 }
1616
1617 fn generate_cookie_params(&self, op: &OperationInfo) -> TokenStream {
1618 let cookie_params: Vec<_> = op
1619 .parameters
1620 .iter()
1621 .filter(|parameter| parameter.location == "cookie")
1622 .collect();
1623 if cookie_params.is_empty() {
1624 return quote! {};
1625 }
1626 let mut emit = Vec::new();
1627 for parameter in cookie_params {
1628 let ident = Self::to_field_ident(&self.param_ident_str(parameter));
1629 let wire_name = parameter.name.as_str();
1630 if parameter.required {
1631 emit.push(quote! {
1632 __cookie_fields.push(format!("{}={}", #wire_name, #ident));
1633 });
1634 } else {
1635 emit.push(quote! {
1636 if let Some(value) = #ident {
1637 __cookie_fields.push(format!("{}={}", #wire_name, value));
1638 }
1639 });
1640 }
1641 }
1642 quote! {
1643 let mut __cookie_fields = Vec::new();
1644 #(#emit)*
1645 if !__cookie_fields.is_empty() {
1646 req = req.header(::reqwest::header::COOKIE, __cookie_fields.join("; "));
1647 }
1648 }
1649 }
1650
1651 fn generate_query_params(&self, op: &OperationInfo) -> TokenStream {
1653 let query_params: Vec<_> = op
1654 .parameters
1655 .iter()
1656 .filter(|p| p.location == "query")
1657 .collect();
1658
1659 if query_params.is_empty() {
1660 return quote! {};
1661 }
1662
1663 let mut param_building = Vec::new();
1664 let mut req_appends = Vec::new();
1668
1669 for param in query_params {
1670 use crate::analysis::QuerySerialization;
1671
1672 let param_name_snake = self.param_ident_str(param);
1674 let param_name = Self::to_field_ident(¶m_name_snake);
1675
1676 let param_key = ¶m.name;
1678
1679 match ¶m.query_serialization {
1680 Some(QuerySerialization::FormExplodedNestedObject { properties }) => {
1681 let emit_properties = properties.iter().map(|property| {
1682 let wire_name = property.wire_name.as_str();
1683 let field_ident = CodeGenerator::to_field_ident(
1684 &self.to_rust_field_name(wire_name),
1685 );
1686 match &property.value_type {
1687 crate::analysis::QueryStructPropertyType::Scalar(_) => quote! {
1688 let value = serde_json::to_value(&v.#field_ident)
1689 .map_err(HttpError::serialization_error)?;
1690 if !value.is_null() {
1691 let value = match value {
1692 serde_json::Value::String(value) => value,
1693 serde_json::Value::Bool(value) => value.to_string(),
1694 serde_json::Value::Number(value) => value.to_string(),
1695 _ => return Err(HttpError::serialization_error(
1696 format!("query field `{}` did not serialize as a scalar", #wire_name)
1697 ).into()),
1698 };
1699 nested_params.push((format!("{}.{}", #param_key, #wire_name), value));
1700 }
1701 },
1702 crate::analysis::QueryStructPropertyType::Object { properties } => {
1703 let leaves = properties.iter().map(|property| property.wire_name.clone()).collect::<Vec<_>>();
1704 quote! {
1705 let value = serde_json::to_value(&v.#field_ident)
1706 .map_err(HttpError::serialization_error)?;
1707 if !value.is_null() {
1708 let serde_json::Value::Object(object) = value else {
1709 return Err(HttpError::serialization_error(format!("query field `{}` did not serialize as an object", #wire_name)).into());
1710 };
1711 if object.is_empty() {
1712 nested_params.push((format!("{}.{}[]", #param_key, #wire_name), String::new()));
1713 } else {
1714 for leaf in [#(#leaves),*] {
1715 let Some(value) = object.get(leaf) else { continue };
1716 if value.is_null() { continue; }
1717 let value = match value {
1718 serde_json::Value::String(value) => value.clone(),
1719 serde_json::Value::Bool(value) => value.to_string(),
1720 serde_json::Value::Number(value) => value.to_string(),
1721 _ => return Err(HttpError::serialization_error(format!("query field `{}` contained a non-scalar leaf", #wire_name)).into()),
1722 };
1723 nested_params.push((format!("{}.{}.{}", #param_key, #wire_name, leaf), value));
1724 }
1725 }
1726 }
1727 }
1728 }
1729 crate::analysis::QueryStructPropertyType::Array { item_type } => {
1730 let nested_properties = match item_type {
1731 crate::analysis::ArrayItemType::FlatStructRef { properties, .. } => Some(
1732 properties.iter().map(|property| property.wire_name.clone()).collect::<Vec<_>>()
1733 ),
1734 _ => None,
1735 };
1736 if let Some(nested_properties) = nested_properties {
1737 quote! {
1738 let values = serde_json::to_value(&v.#field_ident)
1739 .map_err(HttpError::serialization_error)?;
1740 if !values.is_null() {
1741 let serde_json::Value::Array(values) = values else {
1742 return Err(HttpError::serialization_error(
1743 format!("query field `{}` did not serialize as an array", #wire_name)
1744 ).into());
1745 };
1746 if values.is_empty() {
1747 nested_params.push((format!("{}.{}[]", #param_key, #wire_name), String::new()));
1748 }
1749 for (index, value) in values.into_iter().enumerate() {
1750 let serde_json::Value::Object(object) = value else {
1751 return Err(HttpError::serialization_error(
1752 format!("query field `{}` contained a non-object item", #wire_name)
1753 ).into());
1754 };
1755 for leaf in [#(#nested_properties),*] {
1756 let Some(value) = object.get(leaf) else { continue };
1757 if value.is_null() { continue; }
1758 let value = match value {
1759 serde_json::Value::String(value) => value.clone(),
1760 serde_json::Value::Bool(value) => value.to_string(),
1761 serde_json::Value::Number(value) => value.to_string(),
1762 _ => return Err(HttpError::serialization_error(
1763 format!("query field `{}` contained a non-scalar leaf", #wire_name)
1764 ).into()),
1765 };
1766 nested_params.push((format!("{}.{}.{}.{}", #param_key, #wire_name, index + 1, leaf), value));
1767 }
1768 }
1769 }
1770 }
1771 } else {
1772 quote! {
1773 let values = serde_json::to_value(&v.#field_ident)
1774 .map_err(HttpError::serialization_error)?;
1775 if !values.is_null() {
1776 let serde_json::Value::Array(values) = values else {
1777 return Err(HttpError::serialization_error(
1778 format!("query field `{}` did not serialize as an array", #wire_name)
1779 ).into());
1780 };
1781 if values.is_empty() {
1782 nested_params.push((format!("{}.{}[]", #param_key, #wire_name), String::new()));
1783 }
1784 for (index, value) in values.into_iter().enumerate() {
1785 let value = match value {
1786 serde_json::Value::String(value) => value,
1787 serde_json::Value::Bool(value) => value.to_string(),
1788 serde_json::Value::Number(value) => value.to_string(),
1789 _ => return Err(HttpError::serialization_error(
1790 format!("query field `{}` contained a non-scalar item", #wire_name)
1791 ).into()),
1792 };
1793 nested_params.push((format!("{}.{}.{}", #param_key, #wire_name, index + 1), value));
1794 }
1795 }
1796 }
1797 }
1798 }
1799 }
1800 }).collect::<Vec<_>>();
1801 let apply = quote! {
1802 let mut nested_params: Vec<(String, String)> = Vec::new();
1803 #(#emit_properties)*
1804 if nested_params.is_empty() {
1805 nested_params.push((format!("{}[]", #param_key), String::new()));
1806 }
1807 req = req.query(&nested_params);
1808 };
1809 if param.required {
1810 req_appends.push(quote! {{ let v = #param_name; #apply }});
1811 } else {
1812 req_appends.push(quote! { if let Some(v) = #param_name { #apply } });
1813 }
1814 continue;
1815 }
1816 Some(QuerySerialization::FormExplodedObject) => {
1817 let apply = quote! {
1824 let __empty = match serde_json::to_value(&v)
1825 .map_err(HttpError::serialization_error)?
1826 {
1827 serde_json::Value::Object(map) => map.is_empty(),
1828 _ => false,
1829 };
1830 if __empty {
1831 req = req.query(&[(format!("{}[]", #param_key), String::new())]);
1832 } else {
1833 req = req.query(&v);
1834 }
1835 };
1836 if param.required {
1837 req_appends.push(quote! {
1838 {
1839 let v = #param_name;
1840 #apply
1841 }
1842 });
1843 } else {
1844 req_appends.push(quote! {
1845 if let Some(v) = #param_name {
1846 #apply
1847 }
1848 });
1849 }
1850 continue;
1851 }
1852 Some(QuerySerialization::DeepObject) => {
1853 let apply = quote! {
1857 let map = match serde_json::to_value(&v)
1858 .map_err(HttpError::serialization_error)?
1859 {
1860 serde_json::Value::Object(map) => map,
1861 _ => return Err(HttpError::serialization_error(
1862 format!("query parameter `{}` did not serialize as an object", #param_key)
1863 ).into()),
1864 };
1865 let mut deep_params: Vec<(String, String)> = Vec::new();
1866 for (k, val) in map {
1867 let s = match val {
1868 serde_json::Value::Null => continue,
1869 serde_json::Value::String(s) => s,
1870 other => other.to_string(),
1871 };
1872 deep_params.push((format!("{}[{}]", #param_key, k), s));
1873 }
1874 if deep_params.is_empty() {
1875 deep_params.push((format!("{}[]", #param_key), String::new()));
1876 }
1877 req = req.query(&deep_params);
1878 };
1879 if param.required {
1880 req_appends.push(quote! {
1881 {
1882 let v = #param_name;
1883 #apply
1884 }
1885 });
1886 } else {
1887 req_appends.push(quote! {
1888 if let Some(v) = #param_name {
1889 #apply
1890 }
1891 });
1892 }
1893 continue;
1894 }
1895 Some(QuerySerialization::FormObject) => {
1896 let apply = quote! {
1900 let map = match serde_json::to_value(&v)
1901 .map_err(HttpError::serialization_error)?
1902 {
1903 serde_json::Value::Object(map) => map,
1904 _ => return Err(HttpError::serialization_error(
1905 format!("query parameter `{}` did not serialize as an object", #param_key)
1906 ).into()),
1907 };
1908 let mut parts: Vec<String> = Vec::new();
1909 for (k, val) in map {
1910 let s = match val {
1911 serde_json::Value::Null => continue,
1912 serde_json::Value::String(s) => s,
1913 other => other.to_string(),
1914 };
1915 if k.contains(',') || s.contains(',') {
1916 return Err(HttpError::serialization_error(
1917 format!(
1918 "query object `{}` contains a comma in key `{}`; use explode=true for lossless string values",
1919 #param_key,
1920 k,
1921 )
1922 ).into());
1923 }
1924 parts.push(k);
1925 parts.push(s);
1926 }
1927 if parts.is_empty() {
1928 query_params.push((
1929 format!("{}[]", #param_key),
1930 String::new(),
1931 ));
1932 } else {
1933 query_params.push((#param_key.to_string(), parts.join(",")));
1934 }
1935 };
1936 if param.required {
1937 param_building.push(quote! {
1938 {
1939 let v = #param_name;
1940 #apply
1941 }
1942 });
1943 } else {
1944 param_building.push(quote! {
1945 if let Some(v) = #param_name {
1946 #apply
1947 }
1948 });
1949 }
1950 continue;
1951 }
1952 Some(QuerySerialization::FormExplodedArray { item_type }) => {
1953 let struct_properties = match item_type {
1956 crate::analysis::ArrayItemType::FlatStructRef { properties, .. }
1957 | crate::analysis::ArrayItemType::NestedStructRef { properties, .. } => {
1958 Some(properties.clone())
1959 }
1960 _ => None,
1961 };
1962 let emit_items = if let Some(properties) = struct_properties {
1963 let pushes = properties
1964 .iter()
1965 .map(|property| {
1966 let wire_name = &property.wire_name;
1967 let field_ident = CodeGenerator::to_field_ident(
1971 &self.to_rust_field_name(wire_name),
1972 );
1973 match &property.value_type {
1974 crate::analysis::QueryStructPropertyType::Scalar(_) => quote! {
1975 let value = serde_json::to_value(&item.#field_ident)
1976 .map_err(HttpError::serialization_error)?;
1977 if !value.is_null() {
1978 let value = match value {
1979 serde_json::Value::String(value) => value,
1980 serde_json::Value::Bool(value) => value.to_string(),
1981 serde_json::Value::Number(value) => value.to_string(),
1982 _ => return Err(HttpError::serialization_error(
1983 format!("query field `{}.{}` did not serialize as a scalar", #param_key, #wire_name)
1984 ).into()),
1985 };
1986 query_params.push((
1987 format!("{}.{}.{}", #param_key, index, #wire_name),
1988 value,
1989 ));
1990 }
1991 },
1992 crate::analysis::QueryStructPropertyType::Object { properties } => {
1993 let leaves = properties.iter().map(|property| property.wire_name.clone()).collect::<Vec<_>>();
1994 quote! {
1995 let value = serde_json::to_value(&item.#field_ident)
1996 .map_err(HttpError::serialization_error)?;
1997 if !value.is_null() {
1998 let serde_json::Value::Object(object) = value else {
1999 return Err(HttpError::serialization_error(format!("query field `{}.{}` did not serialize as an object", #param_key, #wire_name)).into());
2000 };
2001 if object.is_empty() {
2002 query_params.push((format!("{}.{}.{}[]", #param_key, index, #wire_name), String::new()));
2003 }
2004 for leaf in [#(#leaves),*] {
2005 let Some(value) = object.get(leaf) else { continue };
2006 if value.is_null() { continue; }
2007 let value = match value {
2008 serde_json::Value::String(value) => value.clone(),
2009 serde_json::Value::Bool(value) => value.to_string(),
2010 serde_json::Value::Number(value) => value.to_string(),
2011 _ => return Err(HttpError::serialization_error(format!("query field `{}.{}` contained a non-scalar leaf", #param_key, #wire_name)).into()),
2012 };
2013 query_params.push((format!("{}.{}.{}.{}", #param_key, index, #wire_name, leaf), value));
2014 }
2015 }
2016 }
2017 }
2018 crate::analysis::QueryStructPropertyType::Array { item_type } => {
2019 let nested_properties = match item_type {
2020 crate::analysis::ArrayItemType::FlatStructRef { properties, .. } => {
2021 Some(properties.iter().map(|property| property.wire_name.clone()).collect::<Vec<_>>())
2022 }
2023 _ => None,
2024 };
2025 if let Some(nested_properties) = nested_properties {
2026 quote! {
2027 let values = serde_json::to_value(&item.#field_ident)
2028 .map_err(HttpError::serialization_error)?;
2029 if !values.is_null() {
2030 let serde_json::Value::Array(values) = values else {
2031 return Err(HttpError::serialization_error(
2032 format!("query field `{}.{}` did not serialize as an array", #param_key, #wire_name)
2033 ).into());
2034 };
2035 if values.is_empty() {
2036 query_params.push((format!("{}.{}.{}[]", #param_key, index, #wire_name), String::new()));
2037 }
2038 for (nested_index, value) in values.into_iter().enumerate() {
2039 let serde_json::Value::Object(object) = value else {
2040 return Err(HttpError::serialization_error(
2041 format!("query field `{}.{}` contained a non-object item", #param_key, #wire_name)
2042 ).into());
2043 };
2044 if object.is_empty() {
2045 query_params.push((format!("{}.{}.{}.{}[]", #param_key, index, #wire_name, nested_index + 1), String::new()));
2046 }
2047 for nested_wire_name in [#(#nested_properties),*] {
2048 let Some(value) = object.get(nested_wire_name) else { continue };
2049 if value.is_null() { continue; }
2050 let value = match value {
2051 serde_json::Value::String(value) => value.clone(),
2052 serde_json::Value::Bool(value) => value.to_string(),
2053 serde_json::Value::Number(value) => value.to_string(),
2054 _ => return Err(HttpError::serialization_error(
2055 format!("query field `{}.{}` contained a non-scalar leaf", #param_key, #wire_name)
2056 ).into()),
2057 };
2058 query_params.push((
2059 format!("{}.{}.{}.{}.{}", #param_key, index, #wire_name, nested_index + 1, nested_wire_name),
2060 value,
2061 ));
2062 }
2063 }
2064 }
2065 }
2066 } else {
2067 quote! {
2068 let values = serde_json::to_value(&item.#field_ident)
2069 .map_err(HttpError::serialization_error)?;
2070 if !values.is_null() {
2071 let serde_json::Value::Array(values) = values else {
2072 return Err(HttpError::serialization_error(
2073 format!("query field `{}.{}` did not serialize as an array", #param_key, #wire_name)
2074 ).into());
2075 };
2076 if values.is_empty() {
2077 query_params.push((format!("{}.{}.{}[]", #param_key, index, #wire_name), String::new()));
2078 }
2079 for (nested_index, value) in values.into_iter().enumerate() {
2080 let value = match value {
2081 serde_json::Value::String(value) => value,
2082 serde_json::Value::Bool(value) => value.to_string(),
2083 serde_json::Value::Number(value) => value.to_string(),
2084 _ => return Err(HttpError::serialization_error(
2085 format!("query field `{}.{}` contained a non-scalar item", #param_key, #wire_name)
2086 ).into()),
2087 };
2088 query_params.push((
2089 format!("{}.{}.{}.{}", #param_key, index, #wire_name, nested_index + 1),
2090 value,
2091 ));
2092 }
2093 }
2094 }
2095 }
2096 }
2097 }
2098 })
2099 .collect::<Vec<_>>();
2100 quote! {
2101 for (index, item) in v.iter().enumerate() {
2102 let index = index + 1;
2103 let item_start_len = query_params.len();
2104 #(#pushes)*
2105 if query_params.len() == item_start_len {
2106 query_params.push((format!("{}.{}[]", #param_key, index), String::new()));
2107 }
2108 }
2109 }
2110 } else {
2111 quote! {
2112 for item in v {
2113 query_params.push((#param_key.to_string(), item.to_string()));
2114 }
2115 }
2116 };
2117 if param.required {
2118 param_building.push(quote! {
2119 let v = #param_name;
2120 if v.is_empty() {
2121 query_params.push((
2122 format!("{}[]", #param_key),
2123 String::new(),
2124 ));
2125 } else {
2126 #emit_items
2127 }
2128 });
2129 } else {
2130 param_building.push(quote! {
2131 if let Some(v) = #param_name {
2132 if v.is_empty() {
2133 query_params.push((
2134 format!("{}[]", #param_key),
2135 String::new(),
2136 ));
2137 } else {
2138 #emit_items
2139 }
2140 }
2141 });
2142 }
2143 continue;
2144 }
2145 Some(QuerySerialization::FormArray { .. }) => {
2146 let apply = quote! {
2149 if v.is_empty() {
2150 query_params.push((
2151 format!("{}[]", #param_key),
2152 String::new(),
2153 ));
2154 } else {
2155 let mut parts = Vec::with_capacity(v.len());
2156 for item in &v {
2157 let item = item.to_string();
2158 if item.contains(',') {
2159 return Err(HttpError::serialization_error(
2160 format!(
2161 "query array `{}` contains a comma; use explode=true for lossless string values",
2162 #param_key,
2163 )
2164 ).into());
2165 }
2166 parts.push(item);
2167 }
2168 query_params.push((
2169 #param_key.to_string(),
2170 parts.join(","),
2171 ));
2172 }
2173 };
2174 if param.required {
2175 param_building.push(quote! {
2176 {
2177 let v = #param_name;
2178 #apply
2179 }
2180 });
2181 } else {
2182 param_building.push(quote! {
2183 if let Some(v) = #param_name {
2184 #apply
2185 }
2186 });
2187 }
2188 continue;
2189 }
2190 Some(
2191 QuerySerialization::Unsupported { .. }
2192 | QuerySerialization::SimpleHeaderArray { .. },
2193 ) => {}
2194 None => {}
2195 }
2196
2197 if param.required {
2198 if Self::param_uses_as_ref_str(param) {
2200 param_building.push(quote! {
2201 query_params.push((#param_key.to_string(), #param_name.as_ref().to_string()));
2202 });
2203 } else {
2204 param_building.push(quote! {
2205 query_params.push((#param_key.to_string(), #param_name.to_string()));
2206 });
2207 }
2208 } else {
2209 if Self::param_uses_as_ref_str(param) {
2211 param_building.push(quote! {
2212 if let Some(v) = #param_name {
2213 query_params.push((#param_key.to_string(), v.as_ref().to_string()));
2214 }
2215 });
2216 } else {
2217 param_building.push(quote! {
2218 if let Some(v) = #param_name {
2219 query_params.push((#param_key.to_string(), v.to_string()));
2220 }
2221 });
2222 }
2223 }
2224 }
2225
2226 let pairs_block = if param_building.is_empty() {
2229 quote! {}
2230 } else {
2231 quote! {
2232 {
2233 let mut query_params: Vec<(String, String)> = Vec::new();
2234 #(#param_building)*
2235 if !query_params.is_empty() {
2236 req = req.query(&query_params);
2237 }
2238 }
2239 }
2240 };
2241
2242 quote! {
2243 #pairs_block
2245 #(#req_appends)*
2246 }
2247 }
2248
2249 fn generate_operation_doc_comment(&self, op: &OperationInfo) -> TokenStream {
2253 let method = op.method.to_uppercase();
2254 let path = &op.path;
2255 let mut docs: Vec<String> = Vec::new();
2256 if let Some(s) = &op.summary {
2257 if !s.is_empty() {
2258 docs.push(s.clone());
2259 docs.push(String::new());
2260 }
2261 }
2262 if let Some(d) = &op.description {
2263 if !d.is_empty() {
2264 for line in d.lines() {
2265 docs.push(line.to_string());
2266 }
2267 docs.push(String::new());
2268 }
2269 }
2270 docs.push(format!("`{} {}`", method, path));
2271 let doc_attrs: Vec<TokenStream> = docs
2272 .iter()
2273 .map(|line| {
2274 let prefixed = if line.is_empty() {
2275 String::new()
2276 } else {
2277 format!(" {line}")
2278 };
2279 quote! { #[doc = #prefixed] }
2280 })
2281 .collect();
2282 quote! { #(#doc_attrs)* }
2283 }
2284
2285 fn get_method_name(&self, op: &OperationInfo) -> syn::Ident {
2287 let name = if !op.operation_id.is_empty() {
2288 op.operation_id.to_snake_case()
2289 } else {
2290 format!(
2292 "{}_{}",
2293 op.method,
2294 op.path.replace('/', "_").replace(['{', '}'], "")
2295 )
2296 .to_snake_case()
2297 };
2298
2299 syn::Ident::new(&name, proc_macro2::Span::call_site())
2300 }
2301
2302 fn http_method_call(&self, op: &OperationInfo) -> TokenStream {
2307 match op.method.to_uppercase().as_str() {
2308 "GET" => quote! { self.http_client.get(request_url) },
2309 "POST" => quote! { self.http_client.post(request_url) },
2310 "PUT" => quote! { self.http_client.put(request_url) },
2311 "DELETE" => quote! { self.http_client.delete(request_url) },
2312 "PATCH" => quote! { self.http_client.patch(request_url) },
2313 "HEAD" => quote! { self.http_client.head(request_url) },
2314 "OPTIONS" => quote! {
2315 self.http_client.request(reqwest::Method::OPTIONS, request_url)
2316 },
2317 "TRACE" => quote! {
2318 self.http_client.request(reqwest::Method::TRACE, request_url)
2319 },
2320 other => {
2325 let upper = other.to_string();
2326 quote! {
2327 self.http_client.request(
2328 reqwest::Method::from_bytes(#upper.as_bytes())
2329 .expect("invalid HTTP method"),
2330 request_url,
2331 )
2332 }
2333 }
2334 }
2335 }
2336
2337 fn generate_request_param(&self, op: &OperationInfo) -> TokenStream {
2339 let mut params = Vec::new();
2340 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
2347 let mut unique_param_ident = |raw: String| -> syn::Ident {
2348 let mut chosen = raw.clone();
2349 let mut suffix = 2;
2350 while !used.insert(chosen.clone()) {
2351 chosen = format!("{raw}_{suffix}");
2352 suffix += 1;
2353 }
2354 Self::to_field_ident(&chosen)
2355 };
2356
2357 for param in &op.parameters {
2359 if param.location == "path" {
2360 let param_name_snake = self.param_ident_str(param);
2361 let param_name = unique_param_ident(param_name_snake);
2362 let param_type = self.get_param_rust_type(param);
2363 params.push(quote! { #param_name: #param_type });
2364 }
2365 }
2366
2367 for param in &op.parameters {
2369 if param.location == "query" {
2370 let param_name_snake = self.param_ident_str(param);
2371 let param_name = unique_param_ident(param_name_snake);
2372 let param_type = self.get_param_rust_type(param);
2373
2374 if param.required {
2376 params.push(quote! { #param_name: #param_type });
2377 } else {
2378 params.push(quote! { #param_name: Option<#param_type> });
2379 }
2380 }
2381 }
2382
2383 for param in &op.parameters {
2389 if param.location == "header" {
2390 let param_name_snake = self.param_ident_str(param);
2391 let param_name = unique_param_ident(param_name_snake);
2392 let param_type = self.get_param_rust_type(param);
2393 if param.required {
2394 params.push(quote! { #param_name: #param_type });
2395 } else {
2396 params.push(quote! { #param_name: Option<#param_type> });
2397 }
2398 }
2399 }
2400
2401 for param in &op.parameters {
2402 if param.location == "cookie" {
2403 let param_name_snake = self.param_ident_str(param);
2404 let param_name = unique_param_ident(param_name_snake);
2405 let param_type = self.get_param_rust_type(param);
2406 if param.required {
2407 params.push(quote! { #param_name: #param_type });
2408 } else {
2409 params.push(quote! { #param_name: Option<#param_type> });
2410 }
2411 }
2412 }
2413
2414 if let Some(ref rb) = op.request_body {
2417 use crate::analysis::RequestBodyContent;
2418 if matches!(rb, RequestBodyContent::SchemaLess { .. }) {
2419 return if params.is_empty() {
2420 quote! {}
2421 } else {
2422 quote! { #(#params),* }
2423 };
2424 }
2425 let required = op.request_body_required;
2426 let body_type = match rb {
2427 RequestBodyContent::Json { schema_name, .. }
2428 | RequestBodyContent::FormUrlEncoded { schema_name, .. }
2429 | RequestBodyContent::Multipart { schema_name, .. } => {
2430 let rust_type_name = self.to_rust_type_name(schema_name);
2431 let request_ident =
2432 syn::Ident::new(&rust_type_name, proc_macro2::Span::call_site());
2433 quote! { #request_ident }
2434 }
2435
2436 RequestBodyContent::OctetStream { .. } | RequestBodyContent::Binary { .. } => {
2437 quote! { Vec<u8> }
2438 }
2439 RequestBodyContent::TextPlain { .. } => quote! { String },
2440 RequestBodyContent::Unsupported { .. } => quote! { Vec<u8> },
2441 RequestBodyContent::SchemaLess { .. } => unreachable!(
2442 "schema-less request bodies preserve the historical client signature"
2443 ),
2444 };
2445 let body_ident = match rb {
2446 RequestBodyContent::OctetStream { .. }
2447 | RequestBodyContent::Binary { .. }
2448 | RequestBodyContent::TextPlain { .. }
2449 | RequestBodyContent::Unsupported { .. } => quote! { body },
2450 RequestBodyContent::SchemaLess { .. } => unreachable!(
2451 "schema-less request bodies preserve the historical client signature"
2452 ),
2453 _ => quote! { request },
2454 };
2455 if required {
2456 params.push(quote! { #body_ident: #body_type });
2457 } else {
2458 params.push(quote! { #body_ident: Option<#body_type> });
2459 }
2460 }
2461
2462 if params.is_empty() {
2463 quote! {}
2464 } else {
2465 quote! { #(#params),* }
2466 }
2467 }
2468
2469 fn get_param_rust_type(&self, param: &crate::analysis::ParameterInfo) -> TokenStream {
2471 if Self::param_has_impl_as_ref_type(param) {
2472 quote! { impl AsRef<str> }
2473 } else {
2474 self.get_param_owned_rust_type(param)
2475 }
2476 }
2477
2478 pub(crate) fn get_param_owned_rust_type(
2482 &self,
2483 param: &crate::analysis::ParameterInfo,
2484 ) -> TokenStream {
2485 use crate::analysis::QuerySerialization;
2486 if let Some(
2492 QuerySerialization::FormExplodedArray { item_type }
2493 | QuerySerialization::FormArray { item_type }
2494 | QuerySerialization::SimpleHeaderArray { item_type },
2495 ) = ¶m.query_serialization
2496 {
2497 use crate::analysis::ArrayItemType;
2498 let item_ty: syn::Type = match item_type {
2499 ArrayItemType::Scalar(rust_type) => syn::parse_str(rust_type)
2500 .unwrap_or_else(|_| panic!("invalid scalar item type `{rust_type}`")),
2501 ArrayItemType::SchemaRef(schema_name) => {
2502 let rust_name = self.to_rust_type_name(schema_name);
2503 syn::parse_str(&rust_name)
2504 .unwrap_or_else(|_| panic!("invalid schema item type `{rust_name}`"))
2505 }
2506 ArrayItemType::FlatStructRef { schema_name, .. } => {
2507 let rust_name = self.to_rust_type_name(schema_name);
2508 syn::parse_str(&rust_name)
2509 .unwrap_or_else(|_| panic!("invalid struct item type `{rust_name}`"))
2510 }
2511 ArrayItemType::NestedStructRef { schema_name, .. } => {
2512 let rust_name = self.to_rust_type_name(schema_name);
2513 syn::parse_str(&rust_name)
2514 .unwrap_or_else(|_| panic!("invalid nested struct item type `{rust_name}`"))
2515 }
2516 };
2517 return quote! { Vec<#item_ty> };
2518 }
2519 if let Some(ref schema_name) = param.schema_ref {
2523 let rust_name = self.to_rust_type_name(schema_name);
2524 let ident = syn::Ident::new(&rust_name, proc_macro2::Span::call_site());
2525 return quote! { #ident };
2526 }
2527 syn::parse_str::<syn::Type>(¶m.rust_type)
2528 .map(|ty| quote! { #ty })
2529 .unwrap_or_else(|_| {
2530 let type_ident = syn::Ident::new(¶m.rust_type, proc_macro2::Span::call_site());
2531 quote! { #type_ident }
2532 })
2533 }
2534
2535 fn param_uses_as_ref_str(param: &crate::analysis::ParameterInfo) -> bool {
2540 param.schema_ref.is_none() && param.rust_type == "String"
2541 }
2542
2543 fn resolve_multipart_wire_schema<'a>(
2544 schema: &'a serde_json::Value,
2545 analysis: &'a SchemaAnalysis,
2546 visited: &mut std::collections::HashSet<String>,
2547 ) -> Option<&'a serde_json::Value> {
2548 let Some(reference) = schema.get("$ref").and_then(serde_json::Value::as_str) else {
2549 return Some(schema);
2550 };
2551 let name = reference.strip_prefix("#/components/schemas/")?;
2552 if !visited.insert(name.to_string()) {
2553 return None;
2554 }
2555 let resolved = analysis.validation_context.component_schemas.get(name)?;
2556 Self::resolve_multipart_wire_schema(resolved, analysis, visited)
2557 }
2558
2559 fn multipart_client_field_kind(
2560 schema_type: &crate::analysis::SchemaType,
2561 analysis: &SchemaAnalysis,
2562 visited: &mut std::collections::HashSet<String>,
2563 ) -> Option<MultipartClientFieldKind> {
2564 match schema_type {
2565 crate::analysis::SchemaType::Primitive {
2566 rust_type,
2567 serde_with,
2568 } => {
2569 let rust_type = rust_type.replace(' ', "");
2570 if rust_type == "bytes::Bytes" {
2571 Some(MultipartClientFieldKind::RawBytes)
2572 } else if serde_with
2573 .as_deref()
2574 .is_some_and(|codec| codec.contains("base64_url"))
2575 {
2576 Some(MultipartClientFieldKind::Base64UrlUnpadded)
2577 } else if serde_with
2578 .as_deref()
2579 .is_some_and(|codec| codec.contains("base64"))
2580 || rust_type == "Vec<u8>"
2581 {
2582 Some(MultipartClientFieldKind::Base64)
2583 } else {
2584 Some(MultipartClientFieldKind::Text)
2585 }
2586 }
2587 crate::analysis::SchemaType::StringEnum { .. }
2588 | crate::analysis::SchemaType::ExtensibleEnum { .. } => {
2589 Some(MultipartClientFieldKind::Text)
2590 }
2591 crate::analysis::SchemaType::Reference { target } => {
2592 if !visited.insert(target.clone()) {
2593 return None;
2594 }
2595 analysis.schemas.get(target).and_then(|schema| {
2596 Self::multipart_client_field_kind(&schema.schema_type, analysis, visited)
2597 })
2598 }
2599 _ => None,
2600 }
2601 }
2602
2603 fn generate_typed_multipart_form(
2604 &self,
2605 schema_name: &str,
2606 validation_schema: &serde_json::Value,
2607 analysis: &SchemaAnalysis,
2608 ) -> TokenStream {
2609 use crate::analysis::{ObjectAdditionalProperties, SchemaType};
2610
2611 let Some((resolved_name, resolved_schema)) =
2612 self.resolve_reference_schema(schema_name, analysis)
2613 else {
2614 let message = format!(
2615 "multipart request schema `{schema_name}` could not be resolved during generation"
2616 );
2617 return quote! {
2618 return Err(HttpError::Config(#message.to_string()).into());
2619 };
2620 };
2621 let SchemaType::Object {
2622 properties,
2623 required,
2624 additional_properties,
2625 ..
2626 } = &resolved_schema.schema_type
2627 else {
2628 let message =
2629 format!("multipart request schema `{schema_name}` must resolve to an object");
2630 return quote! {
2631 return Err(HttpError::Config(#message.to_string()).into());
2632 };
2633 };
2634 let wire_schema = Self::resolve_multipart_wire_schema(
2635 validation_schema,
2636 analysis,
2637 &mut std::collections::HashSet::new(),
2638 );
2639 let wire_properties = wire_schema
2640 .and_then(|schema| schema.get("properties"))
2641 .and_then(serde_json::Value::as_object);
2642 let fields = self.emitted_object_properties(
2643 resolved_name,
2644 properties,
2645 required,
2646 additional_properties,
2647 analysis,
2648 );
2649 if matches!(
2650 additional_properties,
2651 ObjectAdditionalProperties::Typed { .. }
2652 ) {
2653 let message = format!(
2654 "multipart request schema `{schema_name}` cannot contain typed additional properties"
2655 );
2656 return quote! {
2657 return Err(HttpError::Config(#message.to_string()).into());
2658 };
2659 }
2660
2661 let mut parts = Vec::new();
2662 for field in fields {
2663 let wire_name = field.wire_name;
2664 let is_nullable = self.property_is_nullable(resolved_name, wire_name, field.property);
2665 let is_tri_state = self.property_is_tri_state(
2666 resolved_name,
2667 wire_name,
2668 field.property,
2669 field.is_required,
2670 );
2671 let ident = field.ident;
2672 let wire_format = wire_properties
2673 .and_then(|properties| properties.get(wire_name))
2674 .and_then(|schema| {
2675 Self::resolve_multipart_wire_schema(
2676 schema,
2677 analysis,
2678 &mut std::collections::HashSet::new(),
2679 )
2680 })
2681 .and_then(|schema| schema.get("format"))
2682 .and_then(serde_json::Value::as_str);
2683 let kind = if wire_format == Some("binary") {
2684 match self.config().types.binary {
2685 crate::type_mapping::BinaryStrategy::String => MultipartClientFieldKind::Text,
2686 crate::type_mapping::BinaryStrategy::Bytes
2687 | crate::type_mapping::BinaryStrategy::VecU8 => {
2688 MultipartClientFieldKind::RawBytes
2689 }
2690 }
2691 } else if let Some(kind) = Self::multipart_client_field_kind(
2692 &field.property.schema_type,
2693 analysis,
2694 &mut std::collections::HashSet::new(),
2695 ) {
2696 kind
2697 } else {
2698 let message =
2699 format!("multipart field `{wire_name}` must be binary or a scalar text field");
2700 return quote! {
2701 return Err(HttpError::Config(#message.to_string()).into());
2702 };
2703 };
2704 let add_value = match kind {
2705 MultipartClientFieldKind::RawBytes => quote! {
2706 form = form.part(
2707 #wire_name,
2708 reqwest::multipart::Part::bytes(value.to_vec()),
2709 );
2710 },
2711 MultipartClientFieldKind::Base64 => quote! {
2712 use base64::Engine as _;
2713 form = form.text(
2714 #wire_name,
2715 base64::engine::general_purpose::STANDARD.encode(value),
2716 );
2717 },
2718 MultipartClientFieldKind::Base64UrlUnpadded => quote! {
2719 use base64::Engine as _;
2720 form = form.text(
2721 #wire_name,
2722 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(value),
2723 );
2724 },
2725 MultipartClientFieldKind::Text => quote! {
2726 form = form.text(#wire_name, value.to_string());
2727 },
2728 };
2729 parts.push(if is_tri_state {
2730 quote! {
2734 if let Some(Some(value)) = &request.#ident {
2735 #add_value
2736 }
2737 }
2738 } else if field.is_required && is_nullable {
2739 quote! {
2740 if let Some(value) = &request.#ident {
2741 #add_value
2742 }
2743 }
2744 } else if field.is_required {
2745 quote! {
2746 let value = &request.#ident;
2747 #add_value
2748 }
2749 } else {
2750 quote! {
2751 if let Some(value) = &request.#ident {
2752 #add_value
2753 }
2754 }
2755 });
2756 }
2757
2758 quote! {
2759 let mut form = reqwest::multipart::Form::new();
2760 #(#parts)*
2761 req = req.multipart(form);
2762 }
2763 }
2764
2765 fn generate_request_body(&self, op: &OperationInfo, analysis: &SchemaAnalysis) -> TokenStream {
2771 let empty_request_framing = Self::generate_empty_request_framing(op);
2772 let Some(rb) = op.request_body.as_ref() else {
2773 return empty_request_framing;
2774 };
2775 use crate::analysis::RequestBodyContent;
2776 let required = op.request_body_required;
2777 let (ident, apply): (TokenStream, TokenStream) = match rb {
2778 RequestBodyContent::Json { media_type, .. } => (
2779 quote! { request },
2780 quote! {
2781 req = req
2782 .body(serde_json::to_vec(&request).map_err(HttpError::serialization_error)?)
2783 .header("content-type", #media_type);
2784 },
2785 ),
2786 RequestBodyContent::FormUrlEncoded { media_type, .. } => (
2787 quote! { request },
2788 quote! {
2789 req = req
2790 .body(serde_urlencoded::to_string(&request).map_err(HttpError::serialization_error)?)
2791 .header("content-type", #media_type);
2792 },
2793 ),
2794 RequestBodyContent::Multipart {
2795 schema_name,
2796 validation_schema,
2797 ..
2798 } => (
2799 quote! { request },
2800 self.generate_typed_multipart_form(schema_name, validation_schema, analysis),
2801 ),
2802 RequestBodyContent::OctetStream { media_type } => (
2803 quote! { body },
2804 quote! {
2805 req = req
2806 .body(body)
2807 .header("content-type", #media_type);
2808 },
2809 ),
2810 RequestBodyContent::Binary { media_type } => (
2811 quote! { body },
2812 quote! {
2813 req = req
2814 .body(body)
2815 .header("content-type", #media_type);
2816 },
2817 ),
2818 RequestBodyContent::TextPlain { media_type } => (
2819 quote! { body },
2820 quote! {
2821 req = req
2822 .body(body)
2823 .header("content-type", #media_type);
2824 },
2825 ),
2826 RequestBodyContent::Unsupported { media_types } => {
2827 let media_type = media_types
2828 .iter()
2829 .find(|media_type| !crate::openapi::is_wildcard_media_type(media_type))
2830 .map(String::as_str);
2831 let Some(media_type) = media_type else {
2832 let ranges = media_types.join(", ");
2833 let message = format!(
2834 "request body for operation `{}` declares only wildcard media ranges ({ranges}); a concrete Content-Type is required",
2835 op.operation_id,
2836 );
2837 return if required {
2838 quote! {
2839 let _ = body;
2840 return Err(HttpError::Config(#message.to_string()).into());
2841 }
2842 } else {
2843 quote! {
2844 if body.is_some() {
2845 return Err(HttpError::Config(#message.to_string()).into());
2846 }
2847 #empty_request_framing
2848 }
2849 };
2850 };
2851 (
2852 quote! { body },
2853 quote! {
2854 req = req
2855 .body(body)
2856 .header("content-type", #media_type);
2857 },
2858 )
2859 }
2860 RequestBodyContent::SchemaLess { .. } => return empty_request_framing,
2861 };
2862 if required {
2863 apply
2864 } else {
2865 quote! {
2866 if let Some(#ident) = #ident {
2867 #apply
2868 } else {
2869 #empty_request_framing
2870 }
2871 }
2872 }
2873 }
2874
2875 fn generate_empty_request_framing(op: &OperationInfo) -> TokenStream {
2880 if ["POST", "PUT", "PATCH"]
2881 .iter()
2882 .any(|method| op.method.eq_ignore_ascii_case(method))
2883 {
2884 quote! {
2885 req = req.header(reqwest::header::CONTENT_LENGTH, "0");
2886 }
2887 } else {
2888 quote! {}
2889 }
2890 }
2891
2892 fn get_success_response_schema<'a>(
2898 &self,
2899 op: &'a OperationInfo,
2900 ) -> Option<(&'a str, &'a String)> {
2901 op.response_schemas
2902 .get_key_value("200")
2903 .or_else(|| op.response_schemas.get_key_value("201"))
2904 .or_else(|| {
2905 op.response_schemas
2906 .iter()
2907 .find(|(code, _)| code.starts_with('2'))
2908 })
2909 .map(|(status, schema)| (status.as_str(), schema))
2910 }
2911
2912 fn get_success_response<'a>(
2913 &self,
2914 analysis: &'a SchemaAnalysis,
2915 op: &'a OperationInfo,
2916 ) -> ClientSuccessSelection<'a> {
2917 if let Some(responses) = analysis.operation_responses.get(&op.operation_id) {
2918 let mut candidates = Vec::new();
2919 for preferred in ["200", "201"] {
2920 if let Some((status, response)) = responses.get_key_value(preferred) {
2921 candidates.push((status.as_str(), response));
2922 }
2923 }
2924 candidates.extend(
2925 responses
2926 .iter()
2927 .filter(|(status, _)| {
2928 status.starts_with('2')
2929 && status.as_str() != "200"
2930 && status.as_str() != "201"
2931 })
2932 .map(|(status, response)| (status.as_str(), response)),
2933 );
2934
2935 let selected = candidates
2936 .iter()
2937 .copied()
2938 .find(|(_, response)| {
2939 matches!(
2940 Self::response_body(response),
2941 ClientSuccessBody::Json(_)
2942 | ClientSuccessBody::Text
2943 | ClientSuccessBody::Binary
2944 )
2945 })
2946 .or_else(|| {
2947 candidates.iter().copied().find(|(_, response)| {
2948 matches!(
2949 Self::response_body(response),
2950 ClientSuccessBody::EventStream
2951 )
2952 })
2953 })
2954 .or_else(|| candidates.first().copied());
2955
2956 if let Some((_, response)) = selected {
2957 let body = Self::response_body(response);
2958 let statuses = candidates
2959 .iter()
2960 .filter_map(|(status, candidate)| {
2961 Self::success_bodies_are_compatible(body, Self::response_body(candidate))
2962 .then_some(*status)
2963 })
2964 .collect();
2965 let accept = match &response.body {
2966 Some(OperationResponseBody::Json { media_type, .. })
2967 | Some(OperationResponseBody::Text { media_type }) => Some(media_type.as_str()),
2968 Some(OperationResponseBody::Binary {
2969 media_type,
2970 wildcard,
2971 }) => (!wildcard).then_some(media_type.as_str()),
2972 None if response.schema_name.is_some() => {
2973 response.media_type.as_deref().or(Some("application/json"))
2974 }
2975 None if response.supports_streaming => Some("text/event-stream"),
2976 None => None,
2977 };
2978 return ClientSuccessSelection {
2979 statuses,
2980 body,
2981 accept,
2982 };
2983 }
2984 }
2985
2986 if let Some((_status, schema_name)) = self.get_success_response_schema(op) {
2987 let statuses = op
2988 .response_schemas
2989 .iter()
2990 .filter_map(|(candidate_status, candidate_schema)| {
2991 (candidate_status.starts_with('2') && candidate_schema == schema_name)
2992 .then_some(candidate_status.as_str())
2993 })
2994 .collect();
2995 ClientSuccessSelection {
2996 statuses,
2997 body: ClientSuccessBody::Json(schema_name),
2998 accept: Some("application/json"),
2999 }
3000 } else if Self::returns_raw_event_stream(op) {
3001 ClientSuccessSelection {
3002 statuses: Vec::new(),
3003 body: ClientSuccessBody::EventStream,
3004 accept: Some("text/event-stream"),
3005 }
3006 } else {
3007 ClientSuccessSelection {
3008 statuses: Vec::new(),
3009 body: ClientSuccessBody::Empty,
3010 accept: None,
3011 }
3012 }
3013 }
3014
3015 fn response_body(response: &crate::analysis::OperationResponse) -> ClientSuccessBody<'_> {
3016 match &response.body {
3017 Some(OperationResponseBody::Json { schema_name, .. }) => {
3018 ClientSuccessBody::Json(schema_name)
3019 }
3020 Some(OperationResponseBody::Text { .. }) => ClientSuccessBody::Text,
3021 Some(OperationResponseBody::Binary { .. }) => ClientSuccessBody::Binary,
3022 None if response.schema_name.is_some() => {
3023 ClientSuccessBody::Json(response.schema_name.as_deref().unwrap_or_default())
3024 }
3025 None if response.supports_streaming => ClientSuccessBody::EventStream,
3026 None => ClientSuccessBody::Empty,
3027 }
3028 }
3029
3030 fn success_bodies_are_compatible(
3031 selected: ClientSuccessBody<'_>,
3032 candidate: ClientSuccessBody<'_>,
3033 ) -> bool {
3034 match (selected, candidate) {
3035 (ClientSuccessBody::Json(selected), ClientSuccessBody::Json(candidate)) => {
3036 selected == candidate
3037 }
3038 (ClientSuccessBody::Text, ClientSuccessBody::Text)
3039 | (ClientSuccessBody::Binary, ClientSuccessBody::Binary)
3040 | (ClientSuccessBody::EventStream, ClientSuccessBody::EventStream)
3041 | (ClientSuccessBody::Empty, ClientSuccessBody::Empty) => true,
3042 _ => false,
3043 }
3044 }
3045
3046 fn get_response_type(&self, analysis: &SchemaAnalysis, op: &OperationInfo) -> TokenStream {
3048 match self.get_success_response(analysis, op).body {
3049 ClientSuccessBody::Json(response_type) => {
3050 let rust_type_name = self.to_rust_type_name(response_type);
3052 let response_ident =
3053 syn::Ident::new(&rust_type_name, proc_macro2::Span::call_site());
3054 quote! { #response_ident }
3055 }
3056 ClientSuccessBody::Text => quote! { String },
3057 ClientSuccessBody::Binary => quote! { bytes::Bytes },
3058 ClientSuccessBody::EventStream => {
3059 quote! { impl futures_util::Stream<Item = Result<bytes::Bytes, reqwest::Error>> }
3060 }
3061 ClientSuccessBody::Empty => quote! { () },
3062 }
3063 }
3064
3065 fn success_status_guard(statuses: &[&str]) -> TokenStream {
3066 if statuses.is_empty() {
3067 return quote! { status.is_success() };
3068 }
3069 let guards = statuses
3070 .iter()
3071 .map(|status| Self::single_status_guard(status));
3072 quote! { false #( || #guards )* }
3073 }
3074
3075 fn single_status_guard(status: &str) -> TokenStream {
3076 match status {
3077 status if status.chars().all(|character| character.is_ascii_digit()) => {
3078 let status: u16 = status.parse().unwrap_or_default();
3079 quote! { status_code == #status }
3080 }
3081 status if matches!(status.as_bytes(), [b'1'..=b'5', b'X' | b'x', b'X' | b'x']) => {
3082 let class = u16::from(status.as_bytes()[0] - b'0');
3083 quote! { status_code / 100 == #class }
3084 }
3085 _ => quote! { status.is_success() },
3086 }
3087 }
3088
3089 fn returns_raw_event_stream(op: &OperationInfo) -> bool {
3103 op.supports_streaming
3104 }
3105
3106 fn generate_error_handling(
3113 &self,
3114 op: &OperationInfo,
3115 success: ClientSuccessSelection<'_>,
3116 ) -> TokenStream {
3117 let op_error_type = self.op_error_type_token(op);
3118 let success_body = success.body;
3119 let success_status_guard = Self::success_status_guard(&success.statuses);
3120 let selected_status = if success.statuses.is_empty() {
3121 "any declared 2xx response".to_string()
3122 } else {
3123 success.statuses.join(", ")
3124 };
3125
3126 let success_branch = match success_body {
3127 ClientSuccessBody::Json(_) => quote! {
3128 match serde_json::from_str(&body_text) {
3129 Ok(body) => Ok(body),
3130 Err(e) => Err(ApiOpError::Api(ApiError {
3131 status: status_code,
3132 headers: headers,
3133 body: body_text,
3134 raw_body,
3135 typed: None,
3136 parse_error: Some(format!(
3137 "failed to deserialize 2xx response body: {}",
3138 e
3139 )),
3140 })),
3141 }
3142 },
3143 ClientSuccessBody::Text => quote! {
3144 let _ = raw_body;
3145 Ok(body_text)
3146 },
3147 ClientSuccessBody::Empty => quote! {
3148 let _ = body_text;
3149 let _ = raw_body;
3150 let _ = headers;
3151 Ok(())
3152 },
3153 ClientSuccessBody::Binary | ClientSuccessBody::EventStream => quote! {},
3154 };
3155
3156 let error_match_arms = self.generate_error_match_arms(op);
3157
3158 if matches!(success_body, ClientSuccessBody::EventStream) {
3163 return quote! {
3164 let status = response.status();
3165 let status_code = status.as_u16();
3166 let headers = response.headers().clone();
3167
3168 if #success_status_guard {
3169 Ok(response.bytes_stream())
3170 } else {
3171 if status.is_success() {
3172 return Err(ApiOpError::Api(ApiError {
3173 status: status_code,
3174 headers,
3175 body: String::new(),
3176 raw_body: Vec::new(),
3177 typed: None,
3178 parse_error: Some(format!(
3179 "unexpected successful status {}; generated return type selects `{}`; live response body was not buffered",
3180 status_code,
3181 #selected_status,
3182 )),
3183 }));
3184 }
3185 let body_bytes = __read_bounded_response_body(
3186 response,
3187 self.max_response_body_bytes,
3188 ).await?;
3189 let raw_body = body_bytes;
3190 let body_text = String::from_utf8_lossy(&raw_body).into_owned();
3191 let typed: Option<#op_error_type>;
3192 let parse_error: Option<String>;
3193 #error_match_arms
3194 Err(ApiOpError::Api(ApiError {
3195 status: status_code,
3196 headers,
3197 body: body_text,
3198 raw_body,
3199 typed,
3200 parse_error,
3201 }))
3202 }
3203 };
3204 }
3205
3206 if matches!(success_body, ClientSuccessBody::Binary) {
3207 return quote! {
3208 let status = response.status();
3209 let status_code = status.as_u16();
3210 let headers = response.headers().clone();
3211
3212 let body_bytes = __read_bounded_response_body(
3213 response,
3214 self.max_response_body_bytes,
3215 ).await?;
3216 if #success_status_guard {
3217 Ok(bytes::Bytes::from(body_bytes))
3218 } else {
3219 let raw_body = body_bytes;
3220 let body_text = String::from_utf8_lossy(&raw_body).into_owned();
3221 if status.is_success() {
3222 return Err(ApiOpError::Api(ApiError {
3223 status: status_code,
3224 headers,
3225 body: body_text,
3226 raw_body,
3227 typed: None,
3228 parse_error: Some(format!(
3229 "unexpected successful status {}; generated return type selects `{}`",
3230 status_code,
3231 #selected_status,
3232 )),
3233 }));
3234 }
3235 let typed: Option<#op_error_type>;
3236 let parse_error: Option<String>;
3237 #error_match_arms
3238 Err(ApiOpError::Api(ApiError {
3239 status: status_code,
3240 headers,
3241 body: body_text,
3242 raw_body,
3243 typed,
3244 parse_error,
3245 }))
3246 }
3247 };
3248 }
3249
3250 quote! {
3251 let status = response.status();
3252 let status_code = status.as_u16();
3253 let headers = response.headers().clone();
3254 let body_bytes = __read_bounded_response_body(
3255 response,
3256 self.max_response_body_bytes,
3257 ).await?;
3258 let raw_body = body_bytes;
3259 let body_text = String::from_utf8_lossy(&raw_body).into_owned();
3260
3261 if #success_status_guard {
3262 #success_branch
3263 } else if status.is_success() {
3264 Err(ApiOpError::Api(ApiError {
3265 status: status_code,
3266 headers,
3267 body: body_text,
3268 raw_body,
3269 typed: None,
3270 parse_error: Some(format!(
3271 "unexpected successful status {}; generated return type selects `{}`",
3272 status_code,
3273 #selected_status,
3274 )),
3275 }))
3276 } else {
3277 let typed: Option<#op_error_type>;
3278 let parse_error: Option<String>;
3279 #error_match_arms
3280 Err(ApiOpError::Api(ApiError {
3281 status: status_code,
3282 headers,
3283 body: body_text,
3284 raw_body,
3285 typed,
3286 parse_error,
3287 }))
3288 }
3289 }
3290 }
3291
3292 fn generate_error_match_arms(&self, op: &OperationInfo) -> TokenStream {
3295 let arms: Vec<TokenStream> = op
3296 .response_schemas
3297 .iter()
3298 .filter(|(code, _)| !code.starts_with('2'))
3299 .filter_map(|(code, schema)| {
3300 let variant_ident = Self::op_error_variant_ident(code);
3301 let payload_ty_name = self.to_rust_type_name(schema);
3302 let payload_ty = syn::Ident::new(&payload_ty_name, proc_macro2::Span::call_site());
3303 let enum_ident = self.op_error_enum_ident(op);
3304
3305 let pattern = match code.as_str() {
3310 "default" | "Default" => return None, other if other.chars().all(|c| c.is_ascii_digit()) => {
3312 let n: u16 = other.parse().ok()?;
3313 quote! { #n }
3314 }
3315 "1XX" | "1xx" => quote! { code if (100..=199).contains(&code) },
3316 "2XX" | "2xx" => quote! { code if (200..=299).contains(&code) },
3317 "3XX" | "3xx" => quote! { code if (300..=399).contains(&code) },
3318 "4XX" | "4xx" => quote! { code if (400..=499).contains(&code) },
3319 "5XX" | "5xx" => quote! { code if (500..=599).contains(&code) },
3320 _ => return None,
3321 };
3322
3323 Some(quote! {
3324 #pattern => {
3325 match serde_json::from_str::<#payload_ty>(&body_text) {
3326 Ok(v) => {
3327 typed = Some(#enum_ident::#variant_ident(v));
3328 parse_error = None;
3329 }
3330 Err(e) => {
3331 typed = None;
3332 parse_error = Some(e.to_string());
3333 }
3334 }
3335 }
3336 })
3337 })
3338 .collect();
3339
3340 let has_typed_enum = op
3348 .response_schemas
3349 .iter()
3350 .any(|(code, _)| !code.starts_with('2'));
3351
3352 let default_payload = op
3359 .response_schemas
3360 .iter()
3361 .find(|(code, _)| matches!(code.as_str(), "default" | "Default"))
3362 .map(|(_, schema)| self.to_rust_type_name(schema));
3363
3364 let default_arm = if let Some(payload_ty_name) = default_payload {
3365 let payload_ty = syn::Ident::new(&payload_ty_name, proc_macro2::Span::call_site());
3366 let enum_ident = self.op_error_enum_ident(op);
3367 quote! {
3368 _ => {
3369 match serde_json::from_str::<#payload_ty>(&body_text) {
3370 Ok(v) => {
3371 typed = Some(#enum_ident::Default(v));
3372 parse_error = None;
3373 }
3374 Err(e) => {
3375 typed = None;
3376 parse_error = Some(e.to_string());
3377 }
3378 }
3379 }
3380 }
3381 } else if has_typed_enum {
3382 quote! {
3383 _ => {
3384 typed = None;
3385 parse_error = None;
3386 }
3387 }
3388 } else {
3389 quote! {
3391 _ => {
3392 match serde_json::from_str::<serde_json::Value>(&body_text) {
3393 Ok(v) => {
3394 typed = Some(v);
3395 parse_error = None;
3396 }
3397 Err(e) => {
3398 typed = None;
3399 parse_error = Some(e.to_string());
3400 }
3401 }
3402 }
3403 }
3404 };
3405
3406 if arms.is_empty() {
3407 quote! {
3409 match status_code {
3410 #default_arm
3411 }
3412 }
3413 } else {
3414 quote! {
3415 match status_code {
3416 #(#arms)*
3417 #default_arm
3418 }
3419 }
3420 }
3421 }
3422
3423 fn generate_url_construction(&self, path: &str, op: &OperationInfo) -> TokenStream {
3425 if path.contains('{') {
3427 self.generate_url_with_params(path, op)
3428 } else {
3429 quote! {
3430 let request_url = format!("{}{}", self.base_url, #path);
3431 }
3432 }
3433 }
3434
3435 fn generate_url_with_params(&self, path: &str, op: &OperationInfo) -> TokenStream {
3437 let path_params: Vec<_> = op
3439 .parameters
3440 .iter()
3441 .filter(|p| p.location == "path")
3442 .collect();
3443
3444 let mut format_string = String::with_capacity(path.len());
3453 let mut format_args: Vec<TokenStream> = Vec::new();
3454 let mut chars = path.chars().peekable();
3455 while let Some(c) = chars.next() {
3456 if c != '{' {
3457 format_string.push(c);
3458 continue;
3459 }
3460 let mut name = String::new();
3462 while let Some(&n) = chars.peek() {
3463 chars.next();
3464 if n == '}' {
3465 break;
3466 }
3467 name.push(n);
3468 }
3469 let param = path_params.iter().find(|p| p.name == name);
3473 let Some(param) = param else {
3474 format_string.push('{');
3475 format_string.push_str(&name);
3476 format_string.push('}');
3477 continue;
3478 };
3479 format_string.push_str("{}");
3480 let param_name_snake = self.param_ident_str(param);
3481 let param_ident = Self::to_field_ident(¶m_name_snake);
3482 if Self::param_uses_as_ref_str(param) {
3483 format_args.push(quote! {
3484 __pct_encode_path_segment(#param_ident.as_ref())
3485 });
3486 } else {
3487 format_args.push(quote! {
3488 __pct_encode_path_segment(&#param_ident.to_string())
3489 });
3490 }
3491 }
3492
3493 if format_args.is_empty() {
3494 quote! {
3495 let request_url = format!("{}{}", self.base_url, #path);
3496 }
3497 } else {
3498 quote! {
3499 let request_url = format!("{}{}", self.base_url, format!(#format_string, #(#format_args),*));
3500 }
3501 }
3502 }
3503
3504 pub(crate) fn param_ident_str(&self, param: &crate::analysis::ParameterInfo) -> String {
3509 if let Some(ident) = ¶m.rust_ident {
3510 return self.escape_keyword_ident(ident);
3514 }
3515 self.sanitize_param_name(¶m.name)
3516 }
3517
3518 fn escape_keyword_ident(&self, snake_case: &str) -> String {
3519 if matches!(snake_case, "self" | "super" | "crate" | "Self") {
3520 return format!("{snake_case}_param");
3521 }
3522 if Self::is_rust_keyword(snake_case) {
3523 format!("r#{snake_case}")
3524 } else {
3525 snake_case.to_string()
3526 }
3527 }
3528
3529 fn sanitize_param_name(&self, name: &str) -> String {
3534 let suffix = if name.ends_with("<=") {
3539 "_lte"
3540 } else if name.ends_with(">=") {
3541 "_gte"
3542 } else if name.ends_with('<') {
3543 "_lt"
3544 } else if name.ends_with('>') {
3545 "_gt"
3546 } else {
3547 ""
3548 };
3549 let stripped = name.trim_end_matches(['<', '>', '=']);
3550 let mut snake_case = stripped.to_snake_case();
3551 if snake_case.is_empty() {
3552 snake_case.push_str("parameter");
3553 } else if snake_case.starts_with(|character: char| character.is_ascii_digit()) {
3554 snake_case.insert(0, '_');
3555 }
3556 snake_case.push_str(suffix);
3557
3558 if matches!(snake_case.as_str(), "self" | "super" | "crate" | "Self") {
3559 return format!("{snake_case}_param");
3560 }
3561 if Self::is_rust_keyword(&snake_case) {
3562 format!("r#{snake_case}")
3563 } else {
3564 snake_case
3565 }
3566 }
3567}