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