1use crate::analysis::{
152 OperationInfo, OperationRepresentation, OperationResponseBody, ParameterInfo, SchemaAnalysis,
153};
154use crate::generator::CodeGenerator;
155use heck::{ToPascalCase, ToSnakeCase};
156use proc_macro2::TokenStream;
157use quote::{format_ident, quote};
158use std::collections::BTreeMap;
159
160struct AllocatedOperationParam<'a> {
161 param: &'a ParameterInfo,
162 ident: syn::Ident,
163}
164
165#[derive(Clone)]
166struct BodyFieldPlan {
167 wire_name: String,
168 preferred_method_name: String,
169 value_ident: syn::Ident,
170 value_type: TokenStream,
171 access_path: Vec<syn::Ident>,
172 tri_state: bool,
173}
174
175#[derive(Clone, Copy)]
176enum MultipartClientFieldKind {
177 RawBytes,
178 Base64,
179 Base64UrlUnpadded,
180 Text,
181 Json,
183}
184
185#[derive(Clone, Copy)]
186enum ClientSuccessBody<'a> {
187 Json(&'a str),
188 Text,
189 Binary,
190 EventStream,
191 ParsedSse,
192 Empty,
193}
194
195#[derive(Clone)]
196struct ClientSuccessSelection<'a> {
197 statuses: Vec<&'a str>,
198 excluded_statuses: Vec<&'a str>,
199 body: ClientSuccessBody<'a>,
200 accept: Option<&'a str>,
201}
202
203pub(crate) struct ClientMethodPlan<'a> {
205 pub operation: &'a OperationInfo,
206 pub method_ident: syn::Ident,
207 pub request_parameters: TokenStream,
208 pub generics: TokenStream,
209 pub response_type: TokenStream,
210 pub error_type: TokenStream,
211 pub response: ClientResponsePlan,
212 pub builder_method_ident: Option<syn::Ident>,
213 pub builder_type_ident: Option<syn::Ident>,
214 pub content_type_method_ident: Option<syn::Ident>,
215 success: ClientSuccessSelection<'a>,
216 validate_content_type: bool,
217}
218
219pub(crate) struct ClientResponsePlan {
220 pub statuses: Vec<String>,
221 pub excluded_statuses: Vec<String>,
222 pub media_type: Option<String>,
223 pub kind: String,
224 pub consumption: String,
225}
226
227enum RequiredBodyConstruction {
228 Default,
229 New(Vec<BodyConstructorParam>),
230 Whole,
231}
232
233struct BodyConstructorParam {
234 preferred_ident: syn::Ident,
235 value_type: TokenStream,
236}
237
238struct BodyModelPlan {
239 body_ident: syn::Ident,
240 body_type: TokenStream,
241 required_construction: RequiredBodyConstruction,
242 optional_fields: Vec<BodyFieldPlan>,
243}
244
245impl CodeGenerator {
246 pub fn generate_http_client_struct(&self) -> TokenStream {
248 let has_retry = self.config().retry_config.is_some();
249 let has_tracing = self.config().tracing_enabled;
250
251 let retry_config_struct = if has_retry {
253 quote! {
254 #[derive(Debug, Clone)]
256 pub struct RetryConfig {
257 pub max_retries: u32,
258 pub initial_delay_ms: u64,
259 pub max_delay_ms: u64,
260 }
261
262 impl Default for RetryConfig {
263 fn default() -> Self {
264 Self {
265 max_retries: 3,
266 initial_delay_ms: 500,
267 max_delay_ms: 16000,
268 }
269 }
270 }
271 }
272 } else {
273 quote! {}
274 };
275
276 let client_struct = quote! {
278 use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
279 use std::collections::BTreeMap;
280
281 pub const DEFAULT_MAX_RESPONSE_BODY_BYTES: usize = 8 * 1024 * 1024;
283
284 #[derive(Clone)]
286 pub struct HttpClient {
287 base_url: String,
288 api_key: Option<String>,
289 http_client: ClientWithMiddleware,
290 custom_headers: BTreeMap<String, String>,
291 max_response_body_bytes: usize,
292 }
293
294 async fn __read_bounded_response_body(
301 response: reqwest::Response,
302 limit: usize,
303 ) -> Result<Vec<u8>, HttpError> {
304 use futures_util::StreamExt;
305
306 let mut body = Vec::new();
307 let mut stream = std::pin::pin!(response.bytes_stream());
308 while let Some(chunk) = stream.next().await {
309 let chunk = chunk.map_err(HttpError::Network)?;
310 let next_len = body.len().checked_add(chunk.len());
311 if next_len.is_none_or(|next_len| next_len > limit) {
312 return Err(HttpError::ResponseTooLarge { limit });
313 }
314 body.extend_from_slice(&chunk);
315 }
316 Ok(body)
317 }
318 };
319
320 let constructor = self.generate_constructor(has_retry, has_tracing);
322
323 let builder_methods = self.generate_builder_methods();
325
326 let default_impl = quote! {
328 impl Default for HttpClient {
329 fn default() -> Self {
330 Self::new()
331 }
332 }
333 };
334
335 let path_encoder = quote! {
339 fn __pct_encode_path_segment(s: &str) -> String {
340 let mut out = String::with_capacity(s.len());
341 for &b in s.as_bytes() {
342 match b {
343 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
344 out.push(b as char);
345 }
346 _ => {
347 out.push('%');
348 out.push_str(&format!("{:02X}", b));
349 }
350 }
351 }
352 out
353 }
354
355 };
356
357 quote! {
359 #retry_config_struct
360 #client_struct
361
362 impl HttpClient {
363 #constructor
364 #builder_methods
365 }
366
367 #default_impl
368 #path_encoder
369 }
370 }
371
372 fn generate_constructor(&self, has_retry: bool, has_tracing: bool) -> TokenStream {
374 let configured_base_url = self
381 .config()
382 .http_client_config
383 .as_ref()
384 .and_then(|http| http.base_url.as_deref())
385 .unwrap_or_default();
386 let default_base_url = quote! { #configured_base_url.to_string() };
387 let max_response_body_bytes = self
388 .config()
389 .http_client_config
390 .as_ref()
391 .and_then(|http| http.max_response_body_bytes)
392 .unwrap_or(8 * 1024 * 1024);
393
394 let retry_param = if has_retry {
395 quote! { retry_config: Option<RetryConfig>, }
396 } else {
397 quote! {}
398 };
399
400 let tracing_param = if has_tracing {
401 quote! { enable_tracing: bool, }
402 } else {
403 quote! {}
404 };
405
406 let retry_middleware = if has_retry {
407 quote! {
408 if let Some(config) = retry_config {
409 use reqwest_retry::{RetryTransientMiddleware, policies::ExponentialBackoff};
410
411 let retry_policy = ExponentialBackoff::builder()
412 .retry_bounds(
413 std::time::Duration::from_millis(config.initial_delay_ms),
414 std::time::Duration::from_millis(config.max_delay_ms),
415 )
416 .build_with_max_retries(config.max_retries);
417
418 let retry_middleware = RetryTransientMiddleware::new_with_policy(retry_policy);
419 client_builder = client_builder.with(retry_middleware);
420 }
421 }
422 } else {
423 quote! {}
424 };
425
426 let tracing_middleware = if has_tracing {
427 quote! {
428 if enable_tracing {
429 use reqwest_tracing::TracingMiddleware;
430 client_builder = client_builder.with(TracingMiddleware::default());
431 }
432 }
433 } else {
434 quote! {}
435 };
436
437 let default_constructor = if has_retry && has_tracing {
438 quote! {
439 pub fn new() -> Self {
441 Self::with_config(None, true)
442 }
443 }
444 } else if has_retry {
445 quote! {
446 pub fn new() -> Self {
448 Self::with_config(None)
449 }
450 }
451 } else if has_tracing {
452 quote! {
453 pub fn new() -> Self {
455 Self::with_config(true)
456 }
457 }
458 } else {
459 quote! {
460 pub fn new() -> Self {
462 Self::with_client(reqwest::Client::new())
463 }
464 }
465 };
466
467 if has_retry || has_tracing {
468 let retry_arg = has_retry.then(|| quote! { retry_config, });
469 let tracing_arg = has_tracing.then(|| quote! { enable_tracing, });
470 let default_retry_arg = has_retry.then(|| quote! { None, });
471 let default_tracing_arg = has_tracing.then(|| quote! { true, });
472
473 quote! {
474 #default_constructor
475
476 pub fn with_config(#retry_param #tracing_param) -> Self {
478 Self::with_client_and_config(reqwest::Client::new(), #retry_arg #tracing_arg)
479 }
480
481 pub fn with_client(client: reqwest::Client) -> Self {
487 Self::with_client_and_config(client, #default_retry_arg #default_tracing_arg)
488 }
489
490 pub fn with_client_and_config(client: reqwest::Client, #retry_param #tracing_param) -> Self {
495 let mut client_builder = ClientBuilder::new(client);
496
497 #tracing_middleware
498 #retry_middleware
499
500 let http_client = client_builder.build();
501
502 Self {
503 base_url: #default_base_url,
504 api_key: None,
505 http_client,
506 custom_headers: BTreeMap::new(),
507 max_response_body_bytes: #max_response_body_bytes,
508 }
509 }
510 }
511 } else {
512 quote! {
513 #default_constructor
514
515 pub fn with_client(client: reqwest::Client) -> Self {
520 let http_client = ClientBuilder::new(client).build();
521
522 Self {
523 base_url: #default_base_url,
524 api_key: None,
525 http_client,
526 custom_headers: BTreeMap::new(),
527 max_response_body_bytes: #max_response_body_bytes,
528 }
529 }
530 }
531 }
532 }
533
534 fn generate_builder_methods(&self) -> TokenStream {
536 quote! {
537 pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
539 self.base_url = base_url.into();
540 self
541 }
542
543 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
545 self.api_key = Some(api_key.into());
546 self
547 }
548
549 pub fn with_max_response_body_bytes(mut self, limit: usize) -> Self {
551 self.max_response_body_bytes = limit;
552 self
553 }
554
555 pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
557 self.custom_headers.insert(name.into(), value.into());
558 self
559 }
560
561 pub fn with_headers(mut self, headers: BTreeMap<String, String>) -> Self {
563 self.custom_headers.extend(headers);
564 self
565 }
566 }
567 }
568
569 pub fn generate_operation_methods(&self, analysis: &SchemaAnalysis) -> TokenStream {
581 let operations: Vec<&OperationInfo> = analysis.operations.values().collect();
582 let mut config = self.config().clone();
583 config.enable_sse_client = false;
584 CodeGenerator::new(config).generate_operation_methods_for(analysis, &operations)
585 }
586
587 fn generate_reserved_path_encoder(
590 &self,
591 operations: &[&crate::analysis::OperationInfo],
592 ) -> TokenStream {
593 let used = operations.iter().any(|op| {
594 op.parameters
595 .iter()
596 .any(|p| p.location == "path" && p.allow_reserved)
597 });
598 if !used {
599 return TokenStream::new();
600 }
601 quote! {
602 fn __pct_encode_path_reserved(s: &str) -> String {
607 let bytes = s.as_bytes();
608 let mut out = String::with_capacity(bytes.len());
609 let mut i = 0;
610 while i < bytes.len() {
611 let b = bytes[i];
612 match b {
613 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~'
614 | b':' | b'@' | b'[' | b']' | b'!' | b'$' | b'&' | b'\'' | b'('
615 | b')' | b'*' | b'+' | b',' | b';' | b'=' => out.push(b as char),
616 b'%' if i + 2 < bytes.len()
617 && bytes[i + 1].is_ascii_hexdigit()
618 && bytes[i + 2].is_ascii_hexdigit() =>
619 {
620 out.push_str(&s[i..i + 3]);
621 i += 2;
622 }
623 _ => {
624 out.push('%');
625 out.push_str(&format!("{:02X}", b));
626 }
627 }
628 i += 1;
629 }
630 out
631 }
632 }
633 }
634
635 pub(crate) fn generate_operation_methods_for(
639 &self,
640 analysis: &SchemaAnalysis,
641 operations: &[&OperationInfo],
642 ) -> TokenStream {
643 let param_enums = self.generate_param_enum_types(operations);
644 let reserved_path_encoder = self.generate_reserved_path_encoder(operations);
645
646 let op_error_enums: Vec<TokenStream> = operations
647 .iter()
648 .copied()
649 .filter_map(|op| self.generate_op_error_enum(op))
650 .collect();
651
652 let plans = self.client_method_plans(analysis, operations);
653 let request_content_type_helper = plans
654 .iter()
655 .any(|plan| plan.content_type_method_ident.is_some())
656 .then(Self::generate_request_content_type_helper);
657 let methods: Vec<TokenStream> = plans
658 .iter()
659 .map(|plan| {
660 let method = self.generate_planned_operation_method(analysis, plan, false);
661 let companion = plan
662 .content_type_method_ident
663 .as_ref()
664 .map(|_| self.generate_planned_operation_method(analysis, plan, true));
665 quote! { #method #companion }
666 })
667 .collect();
668 let (operation_builders, builder_entries) =
669 self.generate_operation_builders(analysis, &plans);
670
671 quote! {
672 #reserved_path_encoder
673
674 #param_enums
675
676 #(#op_error_enums)*
677
678 #request_content_type_helper
679
680 #(#operation_builders)*
681
682 impl HttpClient {
683 #(#methods)*
684 #(#builder_entries)*
685 }
686 }
687 }
688
689 pub(crate) fn client_method_plans<'a>(
690 &self,
691 analysis: &'a SchemaAnalysis,
692 operations: &[&'a OperationInfo],
693 ) -> Vec<ClientMethodPlan<'a>> {
694 let mut used: std::collections::HashSet<String> = [
696 "new",
697 "with_config",
698 "with_client",
699 "with_client_and_config",
700 "with_base_url",
701 "with_api_key",
702 "with_max_response_body_bytes",
703 "with_header",
704 "with_headers",
705 "__request_content_type",
706 ]
707 .into_iter()
708 .map(str::to_owned)
709 .collect();
710 let inherent = used.clone();
711 for operation in operations {
712 let ident = self.get_method_name(operation).to_string();
713 used.insert(ident.strip_prefix("r#").unwrap_or(&ident).to_string());
714 }
715 let mut assigned = std::collections::HashSet::new();
716 let mut base_names = BTreeMap::new();
717 for operation in operations {
718 let ident = self.get_method_name(operation).to_string();
719 let plain = ident.strip_prefix("r#").unwrap_or(&ident);
720 let name = if inherent.contains(plain) || !assigned.insert(plain.to_string()) {
721 Self::allocate_name(&ident, &mut used)
722 } else {
723 ident
724 };
725 base_names.insert(operation.operation_id.as_str(), Self::to_field_ident(&name));
726 }
727 let mut plans = Vec::new();
728 for &operation in operations {
729 let method_ident = base_names[operation.operation_id.as_str()].clone();
730 let success = self.get_success_response(analysis, operation);
731 plans.push(self.make_client_method_plan(
732 operation,
733 method_ident.clone(),
734 success.clone(),
735 "buffered",
736 false,
737 ));
738 let Some(responses) = analysis.operation_responses.get(&operation.operation_id) else {
739 continue;
740 };
741 let mut representations: BTreeMap<&str, Vec<(&str, &OperationRepresentation)>> =
742 BTreeMap::new();
743 for (status, response) in responses
744 .iter()
745 .filter(|(status, _)| status.starts_with('2'))
746 {
747 for (media_type, representation) in &response.representations {
748 representations
749 .entry(media_type)
750 .or_default()
751 .push((status, representation));
752 }
753 }
754 let mut kind_counts = BTreeMap::new();
755 for alternatives in representations.values() {
756 let kind = Self::representation_kind(alternatives[0].1);
757 *kind_counts.entry(kind).or_insert(0usize) += 1;
758 }
759 for (media_type, alternatives) in representations {
760 let representation = alternatives[0].1;
761 let incompatible = alternatives
762 .iter()
763 .any(|(_, other)| *other != representation);
764 let groups: Vec<Vec<(&str, &OperationRepresentation)>> = if incompatible {
765 alternatives
766 .into_iter()
767 .map(|alternative| vec![alternative])
768 .collect()
769 } else {
770 vec![alternatives]
771 };
772 for alternatives in groups {
773 let representation = alternatives[0].1;
774 let kind = Self::representation_kind(representation);
775 let suffix = if kind_counts[kind] > 1 {
776 format!("{kind}_{}", media_type.to_snake_case())
777 } else {
778 kind.to_string()
779 };
780 let suffix = if incompatible {
781 format!("{suffix}_status_{}", alternatives[0].0.to_ascii_lowercase())
782 } else {
783 suffix
784 };
785 let body = match representation {
786 OperationRepresentation::Json { schema_name } => {
787 ClientSuccessBody::Json(schema_name)
788 }
789 OperationRepresentation::Text => ClientSuccessBody::Text,
790 OperationRepresentation::Binary { .. } => ClientSuccessBody::Binary,
791 OperationRepresentation::EventStream => ClientSuccessBody::EventStream,
792 };
793 let selection = ClientSuccessSelection {
794 statuses: alternatives.iter().map(|(status, _)| *status).collect(),
795 excluded_statuses: responses
796 .keys()
797 .filter(|status| {
798 status.len() == 3
799 && status.bytes().all(|c| c.is_ascii_digit())
800 && !alternatives
801 .iter()
802 .any(|(selected, _)| selected == &status.as_str())
803 })
804 .map(String::as_str)
805 .collect(),
806 body,
807 accept: Some(media_type),
808 };
809 let default = self.get_success_response(analysis, operation);
810 let equivalent =
811 Self::success_bodies_are_compatible(default.body, selection.body)
812 && default.statuses == selection.statuses
813 && default.accept == selection.accept;
814 if !matches!(representation, OperationRepresentation::EventStream)
815 && !equivalent
816 {
817 let name =
818 Self::allocate_name(&format!("{method_ident}_{suffix}"), &mut used);
819 plans.push(self.make_client_method_plan(
820 operation,
821 Self::to_field_ident(&name),
822 selection.clone(),
823 "buffered",
824 true,
825 ));
826 }
827 if matches!(representation, OperationRepresentation::Binary { .. }) {
828 let name = Self::allocate_name(
829 &format!("{method_ident}_{suffix}_stream"),
830 &mut used,
831 );
832 let live_selection = ClientSuccessSelection {
833 body: ClientSuccessBody::EventStream,
834 ..selection.clone()
835 };
836 let mut plan = self.make_client_method_plan(
837 operation,
838 Self::to_field_ident(&name),
839 live_selection,
840 "binary_stream",
841 true,
842 );
843 plan.response.kind = "binary".to_string();
844 plans.push(plan);
845 }
846 if matches!(representation, OperationRepresentation::EventStream) {
847 if self.config().enable_sse_client {
848 let name =
849 Self::allocate_name(&format!("{method_ident}_sse"), &mut used);
850 let selection = ClientSuccessSelection {
851 body: ClientSuccessBody::ParsedSse,
852 ..selection
853 };
854 plans.push(self.make_client_method_plan(
855 operation,
856 Self::to_field_ident(&name),
857 selection,
858 "parsed_sse",
859 true,
860 ));
861 } else if !equivalent {
862 let name =
863 Self::allocate_name(&format!("{method_ident}_{suffix}"), &mut used);
864 plans.push(self.make_client_method_plan(
865 operation,
866 Self::to_field_ident(&name),
867 selection,
868 "raw_event_stream",
869 true,
870 ));
871 }
872 }
873 }
874 }
875 }
876 for plan in &mut plans {
877 if Self::wildcard_request_media_types(plan.operation).is_some() {
878 let base = plan.method_ident.to_string();
879 let base = base.strip_prefix("r#").unwrap_or(&base);
880 let name = Self::allocate_name(&format!("{base}_with_content_type"), &mut used);
881 plan.content_type_method_ident = Some(Self::to_field_ident(&name));
882 }
883 }
884 self.plan_operation_builders(analysis, &mut plans);
885 plans
886 }
887
888 fn representation_kind(representation: &OperationRepresentation) -> &'static str {
889 match representation {
890 OperationRepresentation::Json { .. } => "json",
891 OperationRepresentation::Text => "text",
892 OperationRepresentation::Binary { .. } => "binary",
893 OperationRepresentation::EventStream => "event_stream",
894 }
895 }
896
897 fn make_client_method_plan<'a>(
898 &self,
899 operation: &'a OperationInfo,
900 method_ident: syn::Ident,
901 success: ClientSuccessSelection<'a>,
902 consumption: &str,
903 validate_content_type: bool,
904 ) -> ClientMethodPlan<'a> {
905 let precise_stream = matches!(success.body, ClientSuccessBody::EventStream);
906 let mut captures = Vec::new();
907 let request = if precise_stream {
908 self.generate_request_param_with_capture(operation, Some(&mut captures))
909 } else {
910 self.generate_request_param(operation)
911 };
912 let generics = if captures.is_empty() {
913 TokenStream::new()
914 } else {
915 quote! { <#(#captures: AsRef<str>),*> }
916 };
917 let request_parameters = request;
918 let response_type = if precise_stream {
919 quote! { impl futures_util::Stream<Item = Result<bytes::Bytes, reqwest::Error>> + 'static + use<#(#captures),*> }
920 } else if consumption == "parsed_sse" {
921 quote! { super::sse::BoxSseStream<Result<super::sse::SseEvent<String>, super::sse::StreamingError>> }
922 } else {
923 self.response_type_for_body(success.body)
924 };
925 let kind = match success.body {
926 ClientSuccessBody::Json(_) => "json",
927 ClientSuccessBody::Text => "text",
928 ClientSuccessBody::Binary => "binary",
929 ClientSuccessBody::EventStream | ClientSuccessBody::ParsedSse => "event_stream",
930 ClientSuccessBody::Empty => "empty",
931 };
932 let consumption = if matches!(success.body, ClientSuccessBody::EventStream)
933 && consumption == "buffered"
934 {
935 "raw_event_stream"
936 } else {
937 consumption
938 };
939 ClientMethodPlan {
940 operation,
941 method_ident,
942 request_parameters,
943 generics,
944 response_type,
945 error_type: self.op_error_type_token(operation),
946 response: ClientResponsePlan {
947 statuses: success.statuses.iter().map(|s| (*s).to_string()).collect(),
948 excluded_statuses: success
949 .excluded_statuses
950 .iter()
951 .map(|s| (*s).to_string())
952 .collect(),
953 media_type: success.accept.map(str::to_owned),
954 kind: kind.to_string(),
955 consumption: consumption.to_string(),
956 },
957 builder_method_ident: None,
958 builder_type_ident: None,
959 content_type_method_ident: None,
960 success,
961 validate_content_type,
962 }
963 }
964
965 fn plan_operation_builders(
966 &self,
967 analysis: &SchemaAnalysis,
968 plans: &mut [ClientMethodPlan<'_>],
969 ) {
970 if !self.config().builders.enabled {
971 return;
972 }
973
974 let mut used_entry_methods: std::collections::HashSet<String> = plans
975 .iter()
976 .flat_map(|plan| {
977 std::iter::once(plan.method_ident.to_string()).chain(
978 plan.content_type_method_ident
979 .as_ref()
980 .map(ToString::to_string),
981 )
982 })
983 .collect();
984 let mut used_type_names = std::collections::HashSet::new();
985 for schema_name in analysis.schemas.keys() {
986 let rust_name = self.to_rust_type_name(schema_name);
987 used_type_names.insert(rust_name.clone());
988 used_type_names.insert(format!("{rust_name}Builder"));
992 }
993 used_type_names.insert("HttpClient".to_string());
994 used_type_names.insert("ApiOpError".to_string());
995 used_type_names.extend(
996 [
997 "ClientBuilder",
998 "ClientWithMiddleware",
999 "RetryConfig",
1000 "HttpError",
1001 "BTreeMap",
1002 ]
1003 .into_iter()
1004 .map(str::to_string),
1005 );
1006 for plan in plans.iter() {
1007 let operation = plan.operation;
1008 used_type_names.insert(self.op_error_enum_ident(operation).to_string());
1009 used_type_names.extend(
1010 operation
1011 .parameters
1012 .iter()
1013 .filter(|parameter| parameter.enum_values.is_some())
1014 .map(|parameter| parameter.rust_type.clone()),
1015 );
1016 }
1017
1018 for plan in plans.iter_mut() {
1019 let operation = plan.operation;
1020 let allocated_params = self.allocated_operation_params(operation);
1021 let body_plan = self.body_model_plan(operation, analysis);
1022 let optional_param_count = allocated_params
1023 .iter()
1024 .filter(|allocated| !Self::builder_param_is_required(allocated.param))
1025 .count();
1026 let optional_body_count =
1027 usize::from(operation.request_body.is_some() && !operation.request_body_required);
1028 let body_field_count = body_plan
1029 .as_ref()
1030 .filter(|plan| {
1031 operation.request_body_required
1032 || matches!(
1033 &plan.required_construction,
1034 RequiredBodyConstruction::Default
1035 )
1036 })
1037 .map_or(0, |plan| plan.optional_fields.len());
1038 let optional_count = optional_param_count
1039 + optional_body_count
1040 + body_field_count
1041 + usize::from(plan.content_type_method_ident.is_some());
1042 if optional_count <= self.config().builders.threshold {
1043 continue;
1044 }
1045
1046 let flat_method = plan.method_ident.clone();
1047 let entry_base = format!("{flat_method}_builder");
1048 let entry_name = Self::allocate_name(&entry_base, &mut used_entry_methods);
1049 let entry_ident = Self::to_field_ident(&entry_name);
1050
1051 let builder_base = format!("{}Builder", flat_method.to_string().to_pascal_case());
1052 let builder_name = Self::allocate_type_name(&builder_base, &mut used_type_names);
1053 let builder_ident = format_ident!("{builder_name}");
1054
1055 plan.builder_method_ident = Some(entry_ident);
1056 plan.builder_type_ident = Some(builder_ident);
1057 }
1058 }
1059
1060 fn generate_operation_builders(
1061 &self,
1062 analysis: &SchemaAnalysis,
1063 plans: &[ClientMethodPlan<'_>],
1064 ) -> (Vec<TokenStream>, Vec<TokenStream>) {
1065 let mut definitions = Vec::new();
1066 let mut entries = Vec::new();
1067 for plan in plans {
1068 let (Some(entry_ident), Some(builder_ident)) =
1069 (&plan.builder_method_ident, &plan.builder_type_ident)
1070 else {
1071 continue;
1072 };
1073 let operation = plan.operation;
1074 let allocated_params = self.allocated_operation_params(operation);
1075 let (definition, entry) = self.generate_single_operation_builder(
1076 analysis,
1077 operation,
1078 &allocated_params,
1079 self.body_model_plan(operation, analysis),
1080 &plan.method_ident,
1081 entry_ident,
1082 builder_ident,
1083 plan,
1084 );
1085 definitions.push(definition);
1086 entries.push(entry);
1087 }
1088 (definitions, entries)
1089 }
1090
1091 #[allow(clippy::too_many_arguments)]
1092 fn generate_single_operation_builder(
1093 &self,
1094 _analysis: &SchemaAnalysis,
1095 operation: &OperationInfo,
1096 allocated_params: &[AllocatedOperationParam<'_>],
1097 body_plan: Option<BodyModelPlan>,
1098 flat_method: &syn::Ident,
1099 entry_ident: &syn::Ident,
1100 builder_ident: &syn::Ident,
1101 plan: &ClientMethodPlan<'_>,
1102 ) -> (TokenStream, TokenStream) {
1103 let mut fields = vec![quote! { client: &'a HttpClient }];
1104 let mut entry_parameters = Vec::new();
1105 let mut initializers = vec![quote! { client: self }];
1106 let mut call_arguments = Vec::new();
1107 let mut setters = Vec::new();
1108 let mut used_entry_params = std::collections::HashSet::new();
1109 let mut used_methods = std::collections::HashSet::from(["send".to_string()]);
1110 let content_type_field = plan.content_type_method_ident.as_ref().map(|_| {
1111 let ident = self.request_content_type_ident(operation, "content_type");
1112 fields.push(quote! { #ident: Option<String> });
1113 initializers.push(quote! { #ident: None });
1114 used_methods.insert("content_type".to_string());
1116 setters.push(quote! {
1117 #[must_use]
1119 pub fn content_type(mut self, value: impl Into<String>) -> Self {
1120 self.#ident = Some(value.into());
1121 self
1122 }
1123 });
1124 ident
1125 });
1126
1127 for allocated in allocated_params {
1128 let field_ident = &allocated.ident;
1129 let storage_type = self.builder_param_storage_type(allocated.param);
1130 if Self::builder_param_is_required(allocated.param) {
1131 fields.push(quote! { #field_ident: #storage_type });
1132 let entry_name =
1133 Self::allocate_name(&field_ident.to_string(), &mut used_entry_params);
1134 let entry_param = Self::to_field_ident(&entry_name);
1135 if Self::param_has_impl_as_ref_type(allocated.param) {
1136 entry_parameters.push(quote! { #entry_param: impl Into<String> });
1137 initializers.push(quote! { #field_ident: #entry_param.into() });
1138 } else {
1139 entry_parameters.push(quote! { #entry_param: #storage_type });
1140 initializers.push(quote! { #field_ident: #entry_param });
1141 }
1142 } else {
1143 fields.push(quote! { #field_ident: Option<#storage_type> });
1144 initializers.push(quote! { #field_ident: None });
1145 let setter_ident =
1146 Self::allocate_builder_method(&field_ident.to_string(), &mut used_methods);
1147 let wire_name = &allocated.param.name;
1148 let assignment = if Self::param_has_impl_as_ref_type(allocated.param) {
1149 quote! { self.#field_ident = Some(#field_ident.into()); }
1150 } else {
1151 quote! { self.#field_ident = Some(#field_ident); }
1152 };
1153 let setter_type = if Self::param_has_impl_as_ref_type(allocated.param) {
1154 quote! { impl Into<String> }
1155 } else {
1156 storage_type.clone()
1157 };
1158 setters.push(quote! {
1159 #[doc = concat!("Set the optional `", #wire_name, "` operation parameter.")]
1160 #[must_use]
1161 pub fn #setter_ident(mut self, #field_ident: #setter_type) -> Self {
1162 #assignment
1163 self
1164 }
1165 });
1166 }
1167 call_arguments.push(quote! { self.#field_ident });
1168 }
1169
1170 if let Some(body_plan) = body_plan {
1171 let BodyModelPlan {
1172 body_ident,
1173 body_type,
1174 required_construction,
1175 optional_fields,
1176 } = body_plan;
1177 let can_initialize_optional_body =
1178 matches!(&required_construction, RequiredBodyConstruction::Default);
1179 if operation.request_body_required {
1180 fields.push(quote! { #body_ident: #body_type });
1181 match required_construction {
1182 RequiredBodyConstruction::Default => {
1183 initializers.push(quote! { #body_ident: Default::default() });
1184 }
1185 RequiredBodyConstruction::New(constructor_params) => {
1186 let mut constructor_args = Vec::new();
1187 for constructor in constructor_params {
1188 let preferred = constructor.preferred_ident.to_string();
1189 let entry_name =
1190 Self::allocate_name(&preferred, &mut used_entry_params);
1191 let entry_param = Self::to_field_ident(&entry_name);
1192 let value_type = constructor.value_type;
1193 entry_parameters.push(quote! { #entry_param: #value_type });
1194 constructor_args.push(entry_param);
1195 }
1196 initializers.push(quote! {
1197 #body_ident: #body_type::new(#(#constructor_args),*)
1198 });
1199 }
1200 RequiredBodyConstruction::Whole => {
1201 let entry_name =
1202 Self::allocate_name(&body_ident.to_string(), &mut used_entry_params);
1203 let entry_param = Self::to_field_ident(&entry_name);
1204 entry_parameters.push(quote! { #entry_param: #body_type });
1205 initializers.push(quote! { #body_ident: #entry_param });
1206 }
1207 }
1208 } else {
1209 fields.push(quote! { #body_ident: Option<#body_type> });
1210 initializers.push(quote! { #body_ident: None });
1211 }
1212
1213 let body_setter =
1214 Self::allocate_builder_method(&body_ident.to_string(), &mut used_methods);
1215 let body_assignment = if operation.request_body_required {
1216 quote! { self.#body_ident = #body_ident; }
1217 } else {
1218 quote! { self.#body_ident = Some(#body_ident); }
1219 };
1220 setters.push(quote! {
1221 #[must_use]
1223 pub fn #body_setter(mut self, #body_ident: #body_type) -> Self {
1224 #body_assignment
1225 self
1226 }
1227 });
1228
1229 if operation.request_body_required || can_initialize_optional_body {
1230 for field in optional_fields {
1231 let setter_ident = Self::allocate_builder_method(
1232 &field.preferred_method_name,
1233 &mut used_methods,
1234 );
1235 let preferred_method_name = field.preferred_method_name.clone();
1236 let value_ident = field.value_ident;
1237 let value_type = field.value_type;
1238 let wire_name = field.wire_name;
1239 let access_path = field.access_path;
1240 let assignment = if operation.request_body_required {
1241 let mut target = quote! { self.#body_ident };
1242 for access in &access_path {
1243 target = quote! { #target.#access };
1244 }
1245 if field.tri_state {
1246 quote! { #target = Some(Some(#value_ident)); }
1247 } else {
1248 quote! { #target = Some(#value_ident); }
1249 }
1250 } else {
1251 let mut target = quote! { request };
1252 for access in &access_path {
1253 target = quote! { #target.#access };
1254 }
1255 if field.tri_state {
1256 quote! {
1257 let request = self.#body_ident.get_or_insert_with(Default::default);
1258 #target = Some(Some(#value_ident));
1259 }
1260 } else {
1261 quote! {
1262 let request = self.#body_ident.get_or_insert_with(Default::default);
1263 #target = Some(#value_ident);
1264 }
1265 }
1266 };
1267 setters.push(quote! {
1268 #[doc = concat!("Set the optional request-body field `", #wire_name, "`.")]
1269 #[must_use]
1270 pub fn #setter_ident(mut self, #value_ident: #value_type) -> Self {
1271 #assignment
1272 self
1273 }
1274 });
1275 if field.tri_state {
1276 let null_ident = Self::allocate_builder_method(
1277 &format!("{preferred_method_name}_null"),
1278 &mut used_methods,
1279 );
1280 let absent_ident = Self::allocate_builder_method(
1281 &format!("{preferred_method_name}_absent"),
1282 &mut used_methods,
1283 );
1284 let null_assignment = if operation.request_body_required {
1285 let mut target = quote! { self.#body_ident };
1286 for access in &access_path {
1287 target = quote! { #target.#access };
1288 }
1289 quote! { #target = Some(None); }
1290 } else {
1291 let mut target = quote! { request };
1292 for access in &access_path {
1293 target = quote! { #target.#access };
1294 }
1295 quote! {
1296 let request = self.#body_ident.get_or_insert_with(Default::default);
1297 #target = Some(None);
1298 }
1299 };
1300 let absent_assignment = if operation.request_body_required {
1301 let mut target = quote! { self.#body_ident };
1302 for access in &access_path {
1303 target = quote! { #target.#access };
1304 }
1305 quote! { #target = None; }
1306 } else {
1307 let mut target = quote! { request };
1308 for access in &access_path {
1309 target = quote! { #target.#access };
1310 }
1311 quote! {
1312 if let Some(request) = self.#body_ident.as_mut() {
1313 #target = None;
1314 }
1315 }
1316 };
1317 setters.push(quote! {
1318 #[doc = concat!("Set the optional nullable request-body field `", #wire_name, "` to JSON null.")]
1319 #[must_use]
1320 pub fn #null_ident(mut self) -> Self {
1321 #null_assignment
1322 self
1323 }
1324
1325 #[doc = concat!("Omit the optional nullable request-body field `", #wire_name, "`.")]
1326 #[must_use]
1327 pub fn #absent_ident(mut self) -> Self {
1328 #absent_assignment
1329 self
1330 }
1331 });
1332 }
1333 }
1334 }
1335 call_arguments.push(quote! { self.#body_ident });
1336 }
1337
1338 let response_type = self.response_type_for_body(plan.success.body);
1339 let error_type = &plan.error_type;
1340 let operation_id = &operation.operation_id;
1341 let send = if let (Some(companion), Some(field)) =
1342 (&plan.content_type_method_ident, content_type_field)
1343 {
1344 let (with_type, without_type) =
1345 if matches!(plan.success.body, ClientSuccessBody::EventStream) {
1346 (
1347 quote! { .map(futures_util::future::Either::Left) },
1348 quote! { .map(futures_util::future::Either::Right) },
1349 )
1350 } else {
1351 (TokenStream::new(), TokenStream::new())
1352 };
1353 quote! {
1354 match self.#field {
1355 Some(content_type) => self.client.#companion(#(#call_arguments,)* &content_type).await #with_type,
1356 None => self.client.#flat_method(#(#call_arguments),*).await #without_type,
1357 }
1358 }
1359 } else {
1360 quote! { self.client.#flat_method(#(#call_arguments),*).await }
1361 };
1362 let deprecated = Self::deprecated_attribute(operation);
1365 let allow_deprecated = if operation.deprecated {
1366 quote! { #[allow(deprecated)] }
1367 } else {
1368 TokenStream::new()
1369 };
1370 let definition = quote! {
1371 #[doc = concat!("Additive request builder for `", #operation_id, "`.")]
1372 #[must_use]
1373 pub struct #builder_ident<'a> {
1374 #(#fields,)*
1375 }
1376
1377 impl<'a> #builder_ident<'a> {
1378 #(#setters)*
1379
1380 #allow_deprecated
1382 pub async fn send(self) -> Result<#response_type, ApiOpError<#error_type>> {
1383 #send
1384 }
1385 }
1386 };
1387 let entry = quote! {
1388 #[doc = concat!("Start an additive builder for `", #operation_id, "`.")]
1389 #deprecated
1390 pub fn #entry_ident(
1391 &self,
1392 #(#entry_parameters),*
1393 ) -> #builder_ident<'_> {
1394 #builder_ident {
1395 #(#initializers,)*
1396 }
1397 }
1398 };
1399 (definition, entry)
1400 }
1401
1402 fn allocate_name(base: &str, used: &mut std::collections::HashSet<String>) -> String {
1403 let raw = base.starts_with("r#");
1404 let base = base.strip_prefix("r#").unwrap_or(base);
1405 let mut candidate = base.to_string();
1406 let mut suffix = 2;
1407 while used.contains(&candidate) || used.contains(&format!("r#{candidate}")) {
1408 candidate = format!("{base}_{suffix}");
1409 suffix += 1;
1410 }
1411 used.insert(candidate.clone());
1412 if raw {
1413 format!("r#{candidate}")
1414 } else {
1415 candidate
1416 }
1417 }
1418
1419 fn allocate_type_name(base: &str, used: &mut std::collections::HashSet<String>) -> String {
1420 if used.insert(base.to_string()) {
1421 return base.to_string();
1422 }
1423
1424 let mut suffix = 2;
1425 loop {
1426 let candidate = format!("{base}{suffix}");
1427 if used.insert(candidate.clone()) {
1428 return candidate;
1429 }
1430 suffix += 1;
1431 }
1432 }
1433
1434 fn allocate_builder_method(
1435 preferred: &str,
1436 used: &mut std::collections::HashSet<String>,
1437 ) -> syn::Ident {
1438 let plain = preferred.strip_prefix("r#").unwrap_or(preferred);
1439 let base = if used.contains(preferred) {
1440 format!("with_{plain}")
1441 } else {
1442 preferred.to_string()
1443 };
1444 let allocated = Self::allocate_name(&base, used);
1445 Self::to_field_ident(&allocated)
1446 }
1447
1448 fn allocated_operation_params<'a>(
1449 &self,
1450 operation: &'a OperationInfo,
1451 ) -> Vec<AllocatedOperationParam<'a>> {
1452 let mut used: std::collections::HashSet<String> =
1455 ["request", "body", "req", "request_url", "client", "form"]
1456 .into_iter()
1457 .map(str::to_owned)
1458 .collect();
1459 let mut allocated = Vec::new();
1460 for location in ["path", "query", "header", "cookie"] {
1461 for parameter in &operation.parameters {
1462 if parameter.location != location {
1463 continue;
1464 }
1465 let raw = self.param_ident_str(parameter);
1466 let chosen = Self::allocate_name(&raw, &mut used);
1467 allocated.push(AllocatedOperationParam {
1468 param: parameter,
1469 ident: Self::to_field_ident(&chosen),
1470 });
1471 }
1472 }
1473 allocated
1474 }
1475
1476 fn operation_param_ident(
1477 &self,
1478 operation: &OperationInfo,
1479 parameter: &ParameterInfo,
1480 ) -> String {
1481 self.allocated_operation_params(operation)
1482 .into_iter()
1483 .find(|allocated| std::ptr::eq(allocated.param, parameter))
1484 .map(|allocated| allocated.ident.to_string())
1485 .unwrap_or_else(|| self.param_ident_str(parameter))
1486 }
1487
1488 fn wildcard_request_media_types(operation: &OperationInfo) -> Option<&[String]> {
1491 match &operation.request_body {
1492 Some(crate::analysis::RequestBodyContent::Unsupported { media_types })
1493 if !media_types.is_empty()
1494 && media_types
1495 .iter()
1496 .all(|media_type| crate::openapi::is_wildcard_media_type(media_type)) =>
1497 {
1498 Some(media_types)
1499 }
1500 Some(
1501 crate::analysis::RequestBodyContent::TextPlain { media_type }
1502 | crate::analysis::RequestBodyContent::Binary { media_type }
1503 | crate::analysis::RequestBodyContent::OctetStream { media_type },
1504 ) if crate::openapi::is_wildcard_media_type(media_type) => {
1505 Some(std::slice::from_ref(media_type))
1506 }
1507 _ => None,
1508 }
1509 }
1510
1511 fn request_content_type_ident(&self, operation: &OperationInfo, preferred: &str) -> syn::Ident {
1512 let mut used: std::collections::HashSet<_> = self
1513 .allocated_operation_params(operation)
1514 .into_iter()
1515 .map(|parameter| parameter.ident.to_string())
1516 .chain([
1517 "body".to_string(),
1518 "request".to_string(),
1519 "client".to_string(),
1520 ])
1521 .collect();
1522 Self::to_field_ident(&Self::allocate_name(preferred, &mut used))
1523 }
1524
1525 fn builder_param_is_required(parameter: &ParameterInfo) -> bool {
1526 parameter.location == "path" || parameter.required
1530 }
1531
1532 fn builder_param_storage_type(&self, parameter: &ParameterInfo) -> TokenStream {
1533 self.get_param_owned_rust_type(parameter)
1534 }
1535
1536 fn param_has_impl_as_ref_type(parameter: &ParameterInfo) -> bool {
1537 !matches!(
1538 ¶meter.query_serialization,
1539 Some(
1540 crate::analysis::QuerySerialization::FormExplodedArray { .. }
1541 | crate::analysis::QuerySerialization::FormArray { .. }
1542 | crate::analysis::QuerySerialization::SimpleHeaderArray { .. },
1543 )
1544 ) && Self::param_uses_as_ref_str(parameter)
1545 }
1546
1547 fn body_model_plan(
1548 &self,
1549 operation: &OperationInfo,
1550 analysis: &SchemaAnalysis,
1551 ) -> Option<BodyModelPlan> {
1552 use crate::analysis::{RequestBodyContent, SchemaType};
1553
1554 let request_body = operation.request_body.as_ref()?;
1555 let (body_name, body_ident) = match request_body {
1556 RequestBodyContent::Json { schema_name, .. }
1557 | RequestBodyContent::FormUrlEncoded { schema_name, .. }
1558 | RequestBodyContent::Multipart { schema_name, .. } => {
1559 (schema_name.as_str(), format_ident!("request"))
1560 }
1561 RequestBodyContent::OctetStream { .. }
1562 | RequestBodyContent::Binary { .. }
1563 | RequestBodyContent::Unsupported { .. } => {
1564 return Some(BodyModelPlan {
1565 body_ident: format_ident!("body"),
1566 body_type: quote! { Vec<u8> },
1567 required_construction: RequiredBodyConstruction::Whole,
1568 optional_fields: Vec::new(),
1569 });
1570 }
1571 RequestBodyContent::TextPlain { .. } => {
1572 return Some(BodyModelPlan {
1573 body_ident: format_ident!("body"),
1574 body_type: quote! { String },
1575 required_construction: RequiredBodyConstruction::Whole,
1576 optional_fields: Vec::new(),
1577 });
1578 }
1579 RequestBodyContent::SchemaLess { .. } => return None,
1580 };
1581 let body_type_name = self.to_rust_type_name(body_name);
1582 let body_type = syn::Ident::new(&body_type_name, proc_macro2::Span::call_site());
1583 let Some((resolved_name, resolved_schema)) =
1584 self.resolve_reference_schema(body_name, analysis)
1585 else {
1586 return Some(BodyModelPlan {
1587 body_ident,
1588 body_type: quote! { #body_type },
1589 required_construction: RequiredBodyConstruction::Whole,
1590 optional_fields: Vec::new(),
1591 });
1592 };
1593
1594 let mut optional_fields = Vec::new();
1595 let mut stack = std::collections::HashSet::new();
1596 self.collect_optional_body_fields(
1597 resolved_name,
1598 Vec::new(),
1599 analysis,
1600 &mut stack,
1601 &mut optional_fields,
1602 );
1603
1604 let required_construction = match &resolved_schema.schema_type {
1605 SchemaType::Object {
1609 variant: Some(_), ..
1610 } => RequiredBodyConstruction::Whole,
1611 SchemaType::Object {
1612 properties,
1613 required,
1614 additional_properties,
1615 ..
1616 } if !self.is_discriminated_variant(resolved_name, analysis) => {
1617 let emitted = self.emitted_object_properties(
1618 resolved_name,
1619 properties,
1620 required,
1621 additional_properties,
1622 analysis,
1623 );
1624 let required_fields: Vec<_> = emitted
1625 .iter()
1626 .filter(|field| field.is_required)
1627 .map(|field| BodyConstructorParam {
1628 preferred_ident: field.ident.clone(),
1629 value_type: field.field_type.clone(),
1630 })
1631 .collect();
1632 if required_fields.is_empty() {
1633 RequiredBodyConstruction::Default
1634 } else if emitted.iter().any(|field| !field.is_required)
1635 || additional_properties.is_open()
1636 {
1637 RequiredBodyConstruction::New(required_fields)
1638 } else {
1639 RequiredBodyConstruction::Whole
1640 }
1641 }
1642 _ => RequiredBodyConstruction::Whole,
1643 };
1644
1645 Some(BodyModelPlan {
1646 body_ident,
1647 body_type: quote! { #body_type },
1648 required_construction,
1649 optional_fields,
1650 })
1651 }
1652
1653 fn resolve_reference_schema<'a>(
1654 &self,
1655 schema_name: &'a str,
1656 analysis: &'a SchemaAnalysis,
1657 ) -> Option<(&'a str, &'a crate::analysis::AnalyzedSchema)> {
1658 let mut current = schema_name;
1659 let mut visited = std::collections::HashSet::new();
1660 loop {
1661 if !visited.insert(current) {
1662 return None;
1663 }
1664 let schema = analysis.schemas.get(current)?;
1665 if let crate::analysis::SchemaType::Reference { target } = &schema.schema_type {
1666 current = target;
1667 } else {
1668 return Some((current, schema));
1669 }
1670 }
1671 }
1672
1673 fn collect_optional_body_fields(
1674 &self,
1675 schema_name: &str,
1676 access_path: Vec<syn::Ident>,
1677 analysis: &SchemaAnalysis,
1678 stack: &mut std::collections::HashSet<String>,
1679 output: &mut Vec<BodyFieldPlan>,
1680 ) {
1681 use crate::analysis::SchemaType;
1682 if !stack.insert(schema_name.to_string()) {
1683 return;
1684 }
1685 let Some(schema) = analysis.schemas.get(schema_name) else {
1686 stack.remove(schema_name);
1687 return;
1688 };
1689 match &schema.schema_type {
1690 SchemaType::Reference { target } => {
1691 self.collect_optional_body_fields(target, access_path, analysis, stack, output);
1692 }
1693 SchemaType::Object {
1694 properties,
1695 required,
1696 additional_properties,
1697 ..
1698 } if !self.is_discriminated_variant(schema_name, analysis) => {
1699 for field in self.emitted_object_properties(
1700 schema_name,
1701 properties,
1702 required,
1703 additional_properties,
1704 analysis,
1705 ) {
1706 if field.is_required {
1707 continue;
1708 }
1709 let mut field_path = access_path.clone();
1710 field_path.push(field.ident.clone());
1711 output.push(BodyFieldPlan {
1712 wire_name: field.wire_name.to_string(),
1713 preferred_method_name: field.ident.to_string(),
1714 value_ident: field.ident.clone(),
1715 value_type: self.generate_property_base_type(
1716 schema_name,
1717 field.wire_name,
1718 field.property,
1719 analysis,
1720 ),
1721 access_path: field_path,
1722 tri_state: self.property_is_tri_state(
1723 schema_name,
1724 field.wire_name,
1725 field.property,
1726 field.is_required,
1727 ),
1728 });
1729 }
1730 }
1731 SchemaType::Composition { schemas } => {
1732 for (index, schema_ref) in schemas.iter().enumerate() {
1733 let mut nested_path = access_path.clone();
1734 nested_path.push(format_ident!("part_{index}"));
1735 self.collect_optional_body_fields(
1736 &schema_ref.target,
1737 nested_path,
1738 analysis,
1739 stack,
1740 output,
1741 );
1742 }
1743 }
1744 _ => {}
1745 }
1746 stack.remove(schema_name);
1747 }
1748
1749 fn is_discriminated_variant(&self, schema_name: &str, analysis: &SchemaAnalysis) -> bool {
1750 analysis.schemas.values().any(|schema| {
1751 matches!(
1752 &schema.schema_type,
1753 crate::analysis::SchemaType::DiscriminatedUnion { variants, .. }
1754 if variants.iter().any(|variant| variant.type_name == schema_name)
1755 )
1756 })
1757 }
1758
1759 fn generate_param_enum_types(&self, operations: &[&OperationInfo]) -> TokenStream {
1764 let mut by_name: BTreeMap<String, &ParameterInfo> = BTreeMap::new();
1765 for op in operations {
1766 for param in &op.parameters {
1767 if param.enum_values.is_some() {
1768 by_name.entry(param.rust_type.clone()).or_insert(param);
1769 }
1770 }
1771 }
1772
1773 if by_name.is_empty() {
1774 return quote! {};
1775 }
1776
1777 let defs: Vec<TokenStream> = by_name
1778 .values()
1779 .map(|param| self.generate_single_param_enum(param))
1780 .collect();
1781
1782 quote! { #(#defs)* }
1783 }
1784
1785 fn generate_single_param_enum(&self, param: &ParameterInfo) -> TokenStream {
1786 let Some(values) = param.enum_values.as_deref() else {
1787 return quote! {};
1788 };
1789
1790 let enum_ident = format_ident!("{}", param.rust_type);
1791
1792 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
1797 let variant_names: Vec<String> = values
1805 .iter()
1806 .enumerate()
1807 .map(|(index, value)| {
1808 let base = param
1809 .enum_varnames
1810 .as_ref()
1811 .and_then(|names| names.get(index))
1812 .map(|name| self.to_rust_enum_variant(name))
1813 .unwrap_or_else(|| self.to_rust_enum_variant(value));
1814 let mut chosen = base.clone();
1815 let mut suffix = 2;
1816 while !used.insert(chosen.clone()) {
1817 chosen = crate::generator::numbered_variant_name(&base, suffix);
1818 suffix += 1;
1819 }
1820 chosen
1821 })
1822 .collect();
1823
1824 let variants: Vec<TokenStream> = values
1825 .iter()
1826 .zip(&variant_names)
1827 .map(|(value, name)| {
1828 let variant_ident = format_ident!("{}", name);
1829 quote! {
1830 #[serde(rename = #value)]
1831 #variant_ident,
1832 }
1833 })
1834 .collect();
1835
1836 let display_arms: Vec<TokenStream> = values
1837 .iter()
1838 .zip(&variant_names)
1839 .map(|(value, name)| {
1840 let variant_ident = format_ident!("{}", name);
1841 quote! { Self::#variant_ident => #value, }
1842 })
1843 .collect();
1844
1845 let doc = format!(
1846 "Allowed values for the `{}` {} parameter.",
1847 param.name, param.location
1848 );
1849
1850 quote! {
1851 #[doc = #doc]
1852 #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
1853 pub enum #enum_ident {
1854 #(#variants)*
1855 }
1856
1857 impl #enum_ident {
1858 pub fn as_str(&self) -> &'static str {
1859 match self {
1860 #(#display_arms)*
1861 }
1862 }
1863 }
1864
1865 impl std::fmt::Display for #enum_ident {
1866 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1867 f.write_str(self.as_str())
1868 }
1869 }
1870
1871 impl AsRef<str> for #enum_ident {
1872 fn as_ref(&self) -> &str {
1873 self.as_str()
1874 }
1875 }
1876 }
1877 }
1878
1879 fn generate_op_error_enum(&self, op: &OperationInfo) -> Option<TokenStream> {
1884 let variants: Vec<(String, String)> = op
1885 .response_schemas
1886 .iter()
1887 .filter(|(code, _)| !code.starts_with('2'))
1888 .map(|(code, schema)| (code.clone(), schema.clone()))
1889 .collect();
1890
1891 if variants.is_empty() {
1892 return None;
1893 }
1894
1895 let enum_ident = self.op_error_enum_ident(op);
1896 let variant_decls: Vec<TokenStream> = variants
1897 .iter()
1898 .map(|(code, schema)| {
1899 let variant_ident = Self::op_error_variant_ident(code);
1900 let payload_ty_name = self.to_rust_type_name(schema);
1901 let payload_ty = syn::Ident::new(&payload_ty_name, proc_macro2::Span::call_site());
1902 quote! { #variant_ident(#payload_ty) }
1903 })
1904 .collect();
1905
1906 let doc = format!(
1907 "Typed error responses for `{}`. One variant per declared non-2xx response.",
1908 op.operation_id
1909 );
1910
1911 Some(quote! {
1912 #[doc = #doc]
1913 #[derive(Debug, Clone)]
1914 pub enum #enum_ident {
1915 #(#variant_decls,)*
1916 }
1917 })
1918 }
1919
1920 fn op_error_enum_ident(&self, op: &OperationInfo) -> syn::Ident {
1922 use heck::ToPascalCase;
1923 let name = format!(
1924 "{}ApiError",
1925 op.operation_id.replace('.', "_").to_pascal_case()
1926 );
1927 syn::Ident::new(&name, proc_macro2::Span::call_site())
1928 }
1929
1930 fn op_error_variant_ident(status_code: &str) -> syn::Ident {
1933 let raw = match status_code {
1934 "default" | "Default" => "Default".to_string(),
1935 other if other.chars().all(|c| c.is_ascii_digit()) => format!("Status{other}"),
1936 other => format!("Status{}", other.to_ascii_lowercase()),
1937 };
1938 syn::Ident::new(&raw, proc_macro2::Span::call_site())
1939 }
1940
1941 fn op_error_type_token(&self, op: &OperationInfo) -> TokenStream {
1945 if op
1946 .response_schemas
1947 .iter()
1948 .any(|(code, _)| !code.starts_with('2'))
1949 {
1950 let ident = self.op_error_enum_ident(op);
1951 quote! { #ident }
1952 } else {
1953 quote! { serde_json::Value }
1954 }
1955 }
1956
1957 fn generate_planned_operation_method(
1959 &self,
1960 analysis: &SchemaAnalysis,
1961 plan: &ClientMethodPlan<'_>,
1962 with_content_type: bool,
1963 ) -> TokenStream {
1964 let op = plan.operation;
1965 let method_name = if with_content_type {
1966 plan.content_type_method_ident
1967 .as_ref()
1968 .unwrap_or(&plan.method_ident)
1969 } else {
1970 &plan.method_ident
1971 };
1972 let http_method_call = self.http_method_call(op);
1973 let path = &op.path;
1974 let content_type_ident = self.request_content_type_ident(op, "content_type");
1975 let content_type_header =
1976 self.request_content_type_ident(op, "__request_content_type_header");
1977 let request_param = if with_content_type {
1978 let parameters = &plan.request_parameters;
1979 quote! { #parameters, #content_type_ident: &str }
1980 } else {
1981 plan.request_parameters.clone()
1982 };
1983 let generics = &plan.generics;
1984 let request_body = self.generate_request_body(op, analysis, with_content_type);
1985 let request_content_type_validation = if with_content_type {
1986 self.generate_request_content_type_validation(
1987 op,
1988 &content_type_ident,
1989 &content_type_header,
1990 )
1991 } else {
1992 TokenStream::new()
1993 };
1994 let skip_content_type_header = if with_content_type {
1995 quote! { && (#content_type_header.is_none() || !name.eq_ignore_ascii_case("content-type")) }
1996 } else {
1997 TokenStream::new()
1998 };
1999 let add_custom_header = if with_content_type {
2000 quote! {
2001 if #content_type_header.is_none() || !name.eq_ignore_ascii_case("content-type") {
2002 req = req.header(name, value);
2003 }
2004 }
2005 } else {
2006 quote! { req = req.header(name, value); }
2007 };
2008 let apply_content_type_header = if with_content_type {
2009 quote! {
2010 if let Some(value) = #content_type_header {
2011 let mut headers = reqwest::header::HeaderMap::new();
2012 headers.insert(reqwest::header::CONTENT_TYPE, value);
2013 req = req.headers(headers);
2014 }
2015 }
2016 } else {
2017 TokenStream::new()
2018 };
2019 let query_params = self.generate_query_params(op);
2020 let header_params = self.generate_header_params(op);
2021 let cookie_params = self.generate_cookie_params(op);
2022 let auth_application = self.generate_auth_application();
2023 let success = plan.success.clone();
2024 let response_type = &plan.response_type;
2025 let op_error_type = &plan.error_type;
2026 let accept = success.accept;
2027 let error_handling = self.generate_error_handling(op, success.clone());
2028 let content_validation = if plan.validate_content_type {
2029 self.generate_response_content_type_validation(op, &success)
2030 } else {
2031 TokenStream::new()
2032 };
2033 let (custom_headers, accept_header) = if let Some(media_type) = accept {
2034 (
2035 quote! {
2036 for (name, value) in &self.custom_headers {
2037 if !name.eq_ignore_ascii_case("accept") #skip_content_type_header {
2038 req = req.header(name, value);
2039 }
2040 }
2041 },
2042 quote! {
2043 req = req.header(reqwest::header::ACCEPT, #media_type);
2044 },
2045 )
2046 } else {
2047 (
2048 quote! {
2049 for (name, value) in &self.custom_headers {
2050 #add_custom_header
2051 }
2052 },
2053 TokenStream::new(),
2054 )
2055 };
2056 let url_construction = self.generate_url_construction(path, op);
2057 let doc_comment = self.generate_operation_doc_comment(op);
2058 let variant_doc = if plan.validate_content_type {
2059 let description = format!(
2060 "Select `{}` as {}; accepts response statuses {}. Other successful statuses and mismatched Content-Type values are returned as inspectable API errors.",
2061 plan.response.media_type.as_deref().unwrap_or("no body"),
2062 plan.response.consumption,
2063 plan.response.statuses.join(", ")
2064 );
2065 quote! { #[doc = #description] }
2066 } else {
2067 TokenStream::new()
2068 };
2069 let stream_doc = if plan.response.consumption == "binary_stream" {
2070 quote! { #[doc = "Success chunks are returned live without a total body-size limit. Error responses use the configured buffered body-size limit."] }
2071 } else {
2072 TokenStream::new()
2073 };
2074 let request_content_type_doc = if with_content_type {
2075 quote! {
2076 #[doc = "Send a raw body with a concrete Content-Type matching a declared request media range. The per-call value overrides configured Content-Type headers. When the optional body is absent, this value is ignored."]
2077 }
2078 } else if let Some(companion) = &plan.content_type_method_ident {
2079 let doc = format!(
2080 "A present request body requires a concrete Content-Type; use [`Self::{companion}`] or the operation builder's `content_type` setter."
2081 );
2082 quote! { #[doc = #doc] }
2083 } else {
2084 TokenStream::new()
2085 };
2086
2087 let deprecated = Self::deprecated_attribute(op);
2088
2089 quote! {
2090 #doc_comment
2091 #variant_doc
2092 #stream_doc
2093 #request_content_type_doc
2094 #deprecated
2095 pub async fn #method_name #generics(
2096 &self,
2097 #request_param
2098 ) -> Result<#response_type, ApiOpError<#op_error_type>> {
2099 #url_construction
2100
2101 let mut req = #http_method_call;
2102 #request_content_type_validation
2103 #request_body
2104
2105 #query_params
2106 #header_params
2107 #cookie_params
2108
2109 #auth_application
2112
2113 #custom_headers
2115
2116 #accept_header
2119 #apply_content_type_header
2120
2121 let response = req.send().await?;
2122 #content_validation
2123 #error_handling
2124 }
2125 }
2126 }
2127
2128 fn generate_auth_application(&self) -> TokenStream {
2132 use crate::http_config::AuthConfig;
2133 match &self.config().auth_config {
2134 Some(AuthConfig::Bearer { header_name }) if header_name == "Authorization" => quote! {
2135 if let Some(api_key) = &self.api_key {
2136 req = req.bearer_auth(api_key);
2137 }
2138 },
2139 Some(AuthConfig::Bearer { header_name }) => {
2140 let h = header_name.clone();
2141 quote! {
2142 if let Some(api_key) = &self.api_key {
2143 req = req.header(#h, format!("Bearer {}", api_key));
2144 }
2145 }
2146 }
2147 Some(AuthConfig::ApiKey { header_name }) => {
2148 let h = header_name.clone();
2149 quote! {
2150 if let Some(api_key) = &self.api_key {
2151 req = req.header(#h, api_key.as_str());
2152 }
2153 }
2154 }
2155 Some(AuthConfig::Custom {
2156 header_name,
2157 header_value_prefix,
2158 }) => {
2159 let h = header_name.clone();
2160 let prefix = header_value_prefix.clone().unwrap_or_default();
2161 if prefix.is_empty() {
2162 quote! {
2163 if let Some(api_key) = &self.api_key {
2164 req = req.header(#h, api_key.as_str());
2165 }
2166 }
2167 } else {
2168 let format_str = format!("{}{{}}", prefix);
2169 quote! {
2170 if let Some(api_key) = &self.api_key {
2171 req = req.header(#h, format!(#format_str, api_key));
2172 }
2173 }
2174 }
2175 }
2176 None => quote! {
2177 if let Some(api_key) = &self.api_key {
2178 req = req.bearer_auth(api_key);
2179 }
2180 },
2181 }
2182 }
2183
2184 fn generate_header_params(&self, op: &OperationInfo) -> TokenStream {
2188 let header_params: Vec<_> = op
2189 .parameters
2190 .iter()
2191 .filter(|p| p.location == "header")
2192 .collect();
2193 if header_params.is_empty() {
2194 return quote! {};
2195 }
2196 let mut emit = Vec::new();
2197 for param in header_params {
2198 let param_name_snake = self.operation_param_ident(op, param);
2199 let param_ident = Self::to_field_ident(¶m_name_snake);
2200 let header_name = ¶m.name;
2201 if matches!(
2202 param.query_serialization,
2203 Some(crate::analysis::QuerySerialization::SimpleHeaderArray { .. })
2204 ) {
2205 let encode = quote! {
2206 v.iter().map(::std::string::ToString::to_string).collect::<Vec<_>>().join(",")
2207 };
2208 if param.required {
2209 emit.push(quote! {
2210 let v = #param_ident;
2211 req = req.header(#header_name, #encode);
2212 });
2213 } else {
2214 emit.push(quote! {
2215 if let Some(v) = #param_ident {
2216 req = req.header(#header_name, #encode);
2217 }
2218 });
2219 }
2220 continue;
2221 }
2222 if param.required {
2223 if Self::param_uses_as_ref_str(param) {
2224 emit.push(quote! {
2225 req = req.header(#header_name, #param_ident.as_ref());
2226 });
2227 } else {
2228 emit.push(quote! {
2229 req = req.header(#header_name, #param_ident.to_string());
2230 });
2231 }
2232 } else if Self::param_uses_as_ref_str(param) {
2233 emit.push(quote! {
2234 if let Some(v) = #param_ident {
2235 req = req.header(#header_name, v.as_ref());
2236 }
2237 });
2238 } else {
2239 emit.push(quote! {
2240 if let Some(v) = #param_ident {
2241 req = req.header(#header_name, v.to_string());
2242 }
2243 });
2244 }
2245 }
2246 quote! {
2247 #(#emit)*
2248 }
2249 }
2250
2251 fn generate_cookie_params(&self, op: &OperationInfo) -> TokenStream {
2252 let cookie_params: Vec<_> = op
2253 .parameters
2254 .iter()
2255 .filter(|parameter| parameter.location == "cookie")
2256 .collect();
2257 if cookie_params.is_empty() {
2258 return quote! {};
2259 }
2260 let mut emit = Vec::new();
2261 for parameter in cookie_params {
2262 let ident = Self::to_field_ident(&self.operation_param_ident(op, parameter));
2263 let wire_name = parameter.name.as_str();
2264 if parameter.required {
2265 emit.push(quote! {
2266 __cookie_fields.push(format!("{}={}", #wire_name, #ident));
2267 });
2268 } else {
2269 emit.push(quote! {
2270 if let Some(value) = #ident {
2271 __cookie_fields.push(format!("{}={}", #wire_name, value));
2272 }
2273 });
2274 }
2275 }
2276 quote! {
2277 let mut __cookie_fields = Vec::new();
2278 #(#emit)*
2279 if !__cookie_fields.is_empty() {
2280 req = req.header(::reqwest::header::COOKIE, __cookie_fields.join("; "));
2281 }
2282 }
2283 }
2284
2285 fn generate_query_params(&self, op: &OperationInfo) -> TokenStream {
2287 let query_params: Vec<_> = op
2288 .parameters
2289 .iter()
2290 .filter(|p| p.location == "query")
2291 .collect();
2292
2293 if query_params.is_empty() {
2294 return quote! {};
2295 }
2296
2297 let mut param_building = Vec::new();
2298 let mut req_appends = Vec::new();
2302
2303 for param in query_params {
2304 use crate::analysis::QuerySerialization;
2305
2306 let param_name_snake = self.operation_param_ident(op, param);
2308 let param_name = Self::to_field_ident(¶m_name_snake);
2309
2310 let param_key = ¶m.name;
2312
2313 match ¶m.query_serialization {
2314 Some(QuerySerialization::FormExplodedNestedObject { properties }) => {
2315 let emit_properties = properties.iter().map(|property| {
2316 let wire_name = property.wire_name.as_str();
2317 let field_ident = CodeGenerator::to_field_ident(
2318 &self.to_rust_field_name(wire_name),
2319 );
2320 match &property.value_type {
2321 crate::analysis::QueryStructPropertyType::Scalar(_) => quote! {
2322 let value = serde_json::to_value(&v.#field_ident)
2323 .map_err(HttpError::serialization_error)?;
2324 if !value.is_null() {
2325 let value = match value {
2326 serde_json::Value::String(value) => value,
2327 serde_json::Value::Bool(value) => value.to_string(),
2328 serde_json::Value::Number(value) => value.to_string(),
2329 _ => return Err(HttpError::serialization_error(
2330 format!("query field `{}` did not serialize as a scalar", #wire_name)
2331 ).into()),
2332 };
2333 nested_params.push((format!("{}.{}", #param_key, #wire_name), value));
2334 }
2335 },
2336 crate::analysis::QueryStructPropertyType::Object { properties } => {
2337 let leaves = properties.iter().map(|property| property.wire_name.clone()).collect::<Vec<_>>();
2338 quote! {
2339 let value = serde_json::to_value(&v.#field_ident)
2340 .map_err(HttpError::serialization_error)?;
2341 if !value.is_null() {
2342 let serde_json::Value::Object(object) = value else {
2343 return Err(HttpError::serialization_error(format!("query field `{}` did not serialize as an object", #wire_name)).into());
2344 };
2345 if object.is_empty() {
2346 nested_params.push((format!("{}.{}[]", #param_key, #wire_name), String::new()));
2347 } else {
2348 for leaf in [#(#leaves),*] {
2349 let Some(value) = object.get(leaf) else { continue };
2350 if value.is_null() { continue; }
2351 let value = match value {
2352 serde_json::Value::String(value) => value.clone(),
2353 serde_json::Value::Bool(value) => value.to_string(),
2354 serde_json::Value::Number(value) => value.to_string(),
2355 _ => return Err(HttpError::serialization_error(format!("query field `{}` contained a non-scalar leaf", #wire_name)).into()),
2356 };
2357 nested_params.push((format!("{}.{}.{}", #param_key, #wire_name, leaf), value));
2358 }
2359 }
2360 }
2361 }
2362 }
2363 crate::analysis::QueryStructPropertyType::Array { item_type } => {
2364 let nested_properties = match item_type {
2365 crate::analysis::ArrayItemType::FlatStructRef { properties, .. } => Some(
2366 properties.iter().map(|property| property.wire_name.clone()).collect::<Vec<_>>()
2367 ),
2368 _ => None,
2369 };
2370 if let Some(nested_properties) = nested_properties {
2371 quote! {
2372 let values = serde_json::to_value(&v.#field_ident)
2373 .map_err(HttpError::serialization_error)?;
2374 if !values.is_null() {
2375 let serde_json::Value::Array(values) = values else {
2376 return Err(HttpError::serialization_error(
2377 format!("query field `{}` did not serialize as an array", #wire_name)
2378 ).into());
2379 };
2380 if values.is_empty() {
2381 nested_params.push((format!("{}.{}[]", #param_key, #wire_name), String::new()));
2382 }
2383 for (index, value) in values.into_iter().enumerate() {
2384 let serde_json::Value::Object(object) = value else {
2385 return Err(HttpError::serialization_error(
2386 format!("query field `{}` contained a non-object item", #wire_name)
2387 ).into());
2388 };
2389 for leaf in [#(#nested_properties),*] {
2390 let Some(value) = object.get(leaf) else { continue };
2391 if value.is_null() { continue; }
2392 let value = match value {
2393 serde_json::Value::String(value) => value.clone(),
2394 serde_json::Value::Bool(value) => value.to_string(),
2395 serde_json::Value::Number(value) => value.to_string(),
2396 _ => return Err(HttpError::serialization_error(
2397 format!("query field `{}` contained a non-scalar leaf", #wire_name)
2398 ).into()),
2399 };
2400 nested_params.push((format!("{}.{}.{}.{}", #param_key, #wire_name, index + 1, leaf), value));
2401 }
2402 }
2403 }
2404 }
2405 } else {
2406 quote! {
2407 let values = serde_json::to_value(&v.#field_ident)
2408 .map_err(HttpError::serialization_error)?;
2409 if !values.is_null() {
2410 let serde_json::Value::Array(values) = values else {
2411 return Err(HttpError::serialization_error(
2412 format!("query field `{}` did not serialize as an array", #wire_name)
2413 ).into());
2414 };
2415 if values.is_empty() {
2416 nested_params.push((format!("{}.{}[]", #param_key, #wire_name), String::new()));
2417 }
2418 for (index, value) in values.into_iter().enumerate() {
2419 let value = match value {
2420 serde_json::Value::String(value) => value,
2421 serde_json::Value::Bool(value) => value.to_string(),
2422 serde_json::Value::Number(value) => value.to_string(),
2423 _ => return Err(HttpError::serialization_error(
2424 format!("query field `{}` contained a non-scalar item", #wire_name)
2425 ).into()),
2426 };
2427 nested_params.push((format!("{}.{}.{}", #param_key, #wire_name, index + 1), value));
2428 }
2429 }
2430 }
2431 }
2432 }
2433 }
2434 }).collect::<Vec<_>>();
2435 let apply = quote! {
2436 let mut nested_params: Vec<(String, String)> = Vec::new();
2437 #(#emit_properties)*
2438 if nested_params.is_empty() {
2439 nested_params.push((format!("{}[]", #param_key), String::new()));
2440 }
2441 req = req.query(&nested_params);
2442 };
2443 if param.required {
2444 req_appends.push(quote! {{ let v = #param_name; #apply }});
2445 } else {
2446 req_appends.push(quote! { if let Some(v) = #param_name { #apply } });
2447 }
2448 continue;
2449 }
2450 Some(QuerySerialization::FormExplodedObject) => {
2451 let apply = quote! {
2458 let __empty = match serde_json::to_value(&v)
2459 .map_err(HttpError::serialization_error)?
2460 {
2461 serde_json::Value::Object(map) => map.is_empty(),
2462 _ => false,
2463 };
2464 if __empty {
2465 req = req.query(&[(format!("{}[]", #param_key), String::new())]);
2466 } else {
2467 req = req.query(&v);
2468 }
2469 };
2470 if param.required {
2471 req_appends.push(quote! {
2472 {
2473 let v = #param_name;
2474 #apply
2475 }
2476 });
2477 } else {
2478 req_appends.push(quote! {
2479 if let Some(v) = #param_name {
2480 #apply
2481 }
2482 });
2483 }
2484 continue;
2485 }
2486 Some(QuerySerialization::DeepObject) => {
2487 let apply = quote! {
2491 let map = match serde_json::to_value(&v)
2492 .map_err(HttpError::serialization_error)?
2493 {
2494 serde_json::Value::Object(map) => map,
2495 _ => return Err(HttpError::serialization_error(
2496 format!("query parameter `{}` did not serialize as an object", #param_key)
2497 ).into()),
2498 };
2499 let mut deep_params: Vec<(String, String)> = Vec::new();
2500 for (k, val) in map {
2501 let s = match val {
2502 serde_json::Value::Null => continue,
2503 serde_json::Value::String(s) => s,
2504 other => other.to_string(),
2505 };
2506 deep_params.push((format!("{}[{}]", #param_key, k), s));
2507 }
2508 if deep_params.is_empty() {
2509 deep_params.push((format!("{}[]", #param_key), String::new()));
2510 }
2511 req = req.query(&deep_params);
2512 };
2513 if param.required {
2514 req_appends.push(quote! {
2515 {
2516 let v = #param_name;
2517 #apply
2518 }
2519 });
2520 } else {
2521 req_appends.push(quote! {
2522 if let Some(v) = #param_name {
2523 #apply
2524 }
2525 });
2526 }
2527 continue;
2528 }
2529 Some(QuerySerialization::FormObject) => {
2530 let apply = quote! {
2534 let map = match serde_json::to_value(&v)
2535 .map_err(HttpError::serialization_error)?
2536 {
2537 serde_json::Value::Object(map) => map,
2538 _ => return Err(HttpError::serialization_error(
2539 format!("query parameter `{}` did not serialize as an object", #param_key)
2540 ).into()),
2541 };
2542 let mut parts: Vec<String> = Vec::new();
2543 for (k, val) in map {
2544 let s = match val {
2545 serde_json::Value::Null => continue,
2546 serde_json::Value::String(s) => s,
2547 other => other.to_string(),
2548 };
2549 if k.contains(',') || s.contains(',') {
2550 return Err(HttpError::serialization_error(
2551 format!(
2552 "query object `{}` contains a comma in key `{}`; use explode=true for lossless string values",
2553 #param_key,
2554 k,
2555 )
2556 ).into());
2557 }
2558 parts.push(k);
2559 parts.push(s);
2560 }
2561 if parts.is_empty() {
2562 query_params.push((
2563 format!("{}[]", #param_key),
2564 String::new(),
2565 ));
2566 } else {
2567 query_params.push((#param_key.to_string(), parts.join(",")));
2568 }
2569 };
2570 if param.required {
2571 param_building.push(quote! {
2572 {
2573 let v = #param_name;
2574 #apply
2575 }
2576 });
2577 } else {
2578 param_building.push(quote! {
2579 if let Some(v) = #param_name {
2580 #apply
2581 }
2582 });
2583 }
2584 continue;
2585 }
2586 Some(QuerySerialization::FormExplodedArray { item_type }) => {
2587 let struct_properties = match item_type {
2590 crate::analysis::ArrayItemType::FlatStructRef { properties, .. }
2591 | crate::analysis::ArrayItemType::NestedStructRef { properties, .. } => {
2592 Some(properties.clone())
2593 }
2594 _ => None,
2595 };
2596 let emit_items = if let Some(properties) = struct_properties {
2597 let pushes = properties
2598 .iter()
2599 .map(|property| {
2600 let wire_name = &property.wire_name;
2601 let field_ident = CodeGenerator::to_field_ident(
2605 &self.to_rust_field_name(wire_name),
2606 );
2607 match &property.value_type {
2608 crate::analysis::QueryStructPropertyType::Scalar(_) => quote! {
2609 let value = serde_json::to_value(&item.#field_ident)
2610 .map_err(HttpError::serialization_error)?;
2611 if !value.is_null() {
2612 let value = match value {
2613 serde_json::Value::String(value) => value,
2614 serde_json::Value::Bool(value) => value.to_string(),
2615 serde_json::Value::Number(value) => value.to_string(),
2616 _ => return Err(HttpError::serialization_error(
2617 format!("query field `{}.{}` did not serialize as a scalar", #param_key, #wire_name)
2618 ).into()),
2619 };
2620 query_params.push((
2621 format!("{}.{}.{}", #param_key, index, #wire_name),
2622 value,
2623 ));
2624 }
2625 },
2626 crate::analysis::QueryStructPropertyType::Object { properties } => {
2627 let leaves = properties.iter().map(|property| property.wire_name.clone()).collect::<Vec<_>>();
2628 quote! {
2629 let value = serde_json::to_value(&item.#field_ident)
2630 .map_err(HttpError::serialization_error)?;
2631 if !value.is_null() {
2632 let serde_json::Value::Object(object) = value else {
2633 return Err(HttpError::serialization_error(format!("query field `{}.{}` did not serialize as an object", #param_key, #wire_name)).into());
2634 };
2635 if object.is_empty() {
2636 query_params.push((format!("{}.{}.{}[]", #param_key, index, #wire_name), String::new()));
2637 }
2638 for leaf in [#(#leaves),*] {
2639 let Some(value) = object.get(leaf) else { continue };
2640 if value.is_null() { continue; }
2641 let value = match value {
2642 serde_json::Value::String(value) => value.clone(),
2643 serde_json::Value::Bool(value) => value.to_string(),
2644 serde_json::Value::Number(value) => value.to_string(),
2645 _ => return Err(HttpError::serialization_error(format!("query field `{}.{}` contained a non-scalar leaf", #param_key, #wire_name)).into()),
2646 };
2647 query_params.push((format!("{}.{}.{}.{}", #param_key, index, #wire_name, leaf), value));
2648 }
2649 }
2650 }
2651 }
2652 crate::analysis::QueryStructPropertyType::Array { item_type } => {
2653 let nested_properties = match item_type {
2654 crate::analysis::ArrayItemType::FlatStructRef { properties, .. } => {
2655 Some(properties.iter().map(|property| property.wire_name.clone()).collect::<Vec<_>>())
2656 }
2657 _ => None,
2658 };
2659 if let Some(nested_properties) = nested_properties {
2660 quote! {
2661 let values = serde_json::to_value(&item.#field_ident)
2662 .map_err(HttpError::serialization_error)?;
2663 if !values.is_null() {
2664 let serde_json::Value::Array(values) = values else {
2665 return Err(HttpError::serialization_error(
2666 format!("query field `{}.{}` did not serialize as an array", #param_key, #wire_name)
2667 ).into());
2668 };
2669 if values.is_empty() {
2670 query_params.push((format!("{}.{}.{}[]", #param_key, index, #wire_name), String::new()));
2671 }
2672 for (nested_index, value) in values.into_iter().enumerate() {
2673 let serde_json::Value::Object(object) = value else {
2674 return Err(HttpError::serialization_error(
2675 format!("query field `{}.{}` contained a non-object item", #param_key, #wire_name)
2676 ).into());
2677 };
2678 if object.is_empty() {
2679 query_params.push((format!("{}.{}.{}.{}[]", #param_key, index, #wire_name, nested_index + 1), String::new()));
2680 }
2681 for nested_wire_name in [#(#nested_properties),*] {
2682 let Some(value) = object.get(nested_wire_name) else { continue };
2683 if value.is_null() { continue; }
2684 let value = match value {
2685 serde_json::Value::String(value) => value.clone(),
2686 serde_json::Value::Bool(value) => value.to_string(),
2687 serde_json::Value::Number(value) => value.to_string(),
2688 _ => return Err(HttpError::serialization_error(
2689 format!("query field `{}.{}` contained a non-scalar leaf", #param_key, #wire_name)
2690 ).into()),
2691 };
2692 query_params.push((
2693 format!("{}.{}.{}.{}.{}", #param_key, index, #wire_name, nested_index + 1, nested_wire_name),
2694 value,
2695 ));
2696 }
2697 }
2698 }
2699 }
2700 } else {
2701 quote! {
2702 let values = serde_json::to_value(&item.#field_ident)
2703 .map_err(HttpError::serialization_error)?;
2704 if !values.is_null() {
2705 let serde_json::Value::Array(values) = values else {
2706 return Err(HttpError::serialization_error(
2707 format!("query field `{}.{}` did not serialize as an array", #param_key, #wire_name)
2708 ).into());
2709 };
2710 if values.is_empty() {
2711 query_params.push((format!("{}.{}.{}[]", #param_key, index, #wire_name), String::new()));
2712 }
2713 for (nested_index, value) in values.into_iter().enumerate() {
2714 let value = match value {
2715 serde_json::Value::String(value) => value,
2716 serde_json::Value::Bool(value) => value.to_string(),
2717 serde_json::Value::Number(value) => value.to_string(),
2718 _ => return Err(HttpError::serialization_error(
2719 format!("query field `{}.{}` contained a non-scalar item", #param_key, #wire_name)
2720 ).into()),
2721 };
2722 query_params.push((
2723 format!("{}.{}.{}.{}", #param_key, index, #wire_name, nested_index + 1),
2724 value,
2725 ));
2726 }
2727 }
2728 }
2729 }
2730 }
2731 }
2732 })
2733 .collect::<Vec<_>>();
2734 quote! {
2735 for (index, item) in v.iter().enumerate() {
2736 let index = index + 1;
2737 let item_start_len = query_params.len();
2738 #(#pushes)*
2739 if query_params.len() == item_start_len {
2740 query_params.push((format!("{}.{}[]", #param_key, index), String::new()));
2741 }
2742 }
2743 }
2744 } else {
2745 quote! {
2746 for item in v {
2747 query_params.push((#param_key.to_string(), item.to_string()));
2748 }
2749 }
2750 };
2751 if param.required {
2752 param_building.push(quote! {
2753 let v = #param_name;
2754 if v.is_empty() {
2755 query_params.push((
2756 format!("{}[]", #param_key),
2757 String::new(),
2758 ));
2759 } else {
2760 #emit_items
2761 }
2762 });
2763 } else {
2764 param_building.push(quote! {
2765 if let Some(v) = #param_name {
2766 if v.is_empty() {
2767 query_params.push((
2768 format!("{}[]", #param_key),
2769 String::new(),
2770 ));
2771 } else {
2772 #emit_items
2773 }
2774 }
2775 });
2776 }
2777 continue;
2778 }
2779 Some(QuerySerialization::FormArray { .. }) => {
2780 let apply = quote! {
2783 if v.is_empty() {
2784 query_params.push((
2785 format!("{}[]", #param_key),
2786 String::new(),
2787 ));
2788 } else {
2789 let mut parts = Vec::with_capacity(v.len());
2790 for item in &v {
2791 let item = item.to_string();
2792 if item.contains(',') {
2793 return Err(HttpError::serialization_error(
2794 format!(
2795 "query array `{}` contains a comma; use explode=true for lossless string values",
2796 #param_key,
2797 )
2798 ).into());
2799 }
2800 parts.push(item);
2801 }
2802 query_params.push((
2803 #param_key.to_string(),
2804 parts.join(","),
2805 ));
2806 }
2807 };
2808 if param.required {
2809 param_building.push(quote! {
2810 {
2811 let v = #param_name;
2812 #apply
2813 }
2814 });
2815 } else {
2816 param_building.push(quote! {
2817 if let Some(v) = #param_name {
2818 #apply
2819 }
2820 });
2821 }
2822 continue;
2823 }
2824 Some(
2825 QuerySerialization::Unsupported { .. }
2826 | QuerySerialization::SimpleHeaderArray { .. },
2827 ) => {}
2828 None => {}
2829 }
2830
2831 if param.required {
2832 if Self::param_uses_as_ref_str(param) {
2834 param_building.push(quote! {
2835 query_params.push((#param_key.to_string(), #param_name.as_ref().to_string()));
2836 });
2837 } else {
2838 param_building.push(quote! {
2839 query_params.push((#param_key.to_string(), #param_name.to_string()));
2840 });
2841 }
2842 } else {
2843 if Self::param_uses_as_ref_str(param) {
2845 param_building.push(quote! {
2846 if let Some(v) = #param_name {
2847 query_params.push((#param_key.to_string(), v.as_ref().to_string()));
2848 }
2849 });
2850 } else {
2851 param_building.push(quote! {
2852 if let Some(v) = #param_name {
2853 query_params.push((#param_key.to_string(), v.to_string()));
2854 }
2855 });
2856 }
2857 }
2858 }
2859
2860 let pairs_block = if param_building.is_empty() {
2863 quote! {}
2864 } else {
2865 quote! {
2866 {
2867 let mut query_params: Vec<(String, String)> = Vec::new();
2868 #(#param_building)*
2869 if !query_params.is_empty() {
2870 req = req.query(&query_params);
2871 }
2872 }
2873 }
2874 };
2875
2876 quote! {
2877 #pairs_block
2879 #(#req_appends)*
2880 }
2881 }
2882
2883 fn deprecated_attribute(op: &OperationInfo) -> TokenStream {
2886 if op.deprecated {
2887 quote! { #[deprecated] }
2888 } else {
2889 TokenStream::new()
2890 }
2891 }
2892
2893 fn generate_operation_doc_comment(&self, op: &OperationInfo) -> TokenStream {
2897 let method = op.method.to_uppercase();
2898 let path = &op.path;
2899 let mut docs: Vec<String> = Vec::new();
2900 if let Some(s) = &op.summary {
2901 if !s.is_empty() {
2902 docs.push(s.clone());
2903 docs.push(String::new());
2904 }
2905 }
2906 if let Some(d) = &op.description {
2907 if !d.is_empty() {
2908 for line in d.lines() {
2909 docs.push(line.to_string());
2910 }
2911 docs.push(String::new());
2912 }
2913 }
2914 docs.push(format!("`{} {}`", method, path));
2915 let doc_attrs: Vec<TokenStream> = docs
2916 .iter()
2917 .map(|line| {
2918 let prefixed = if line.is_empty() {
2919 String::new()
2920 } else {
2921 format!(" {line}")
2922 };
2923 quote! { #[doc = #prefixed] }
2924 })
2925 .collect();
2926 quote! { #(#doc_attrs)* }
2927 }
2928
2929 fn get_method_name(&self, op: &OperationInfo) -> syn::Ident {
2931 let name = if !op.operation_id.is_empty() {
2932 op.operation_id.to_snake_case()
2933 } else {
2934 format!(
2936 "{}_{}",
2937 op.method,
2938 op.path.replace('/', "_").replace(['{', '}'], "")
2939 )
2940 .to_snake_case()
2941 };
2942
2943 Self::to_field_ident(&self.escape_keyword_ident(&name))
2944 }
2945
2946 fn http_method_call(&self, op: &OperationInfo) -> TokenStream {
2951 match op.method.to_uppercase().as_str() {
2952 "GET" => quote! { self.http_client.get(request_url) },
2953 "POST" => quote! { self.http_client.post(request_url) },
2954 "PUT" => quote! { self.http_client.put(request_url) },
2955 "DELETE" => quote! { self.http_client.delete(request_url) },
2956 "PATCH" => quote! { self.http_client.patch(request_url) },
2957 "HEAD" => quote! { self.http_client.head(request_url) },
2958 "OPTIONS" => quote! {
2959 self.http_client.request(reqwest::Method::OPTIONS, request_url)
2960 },
2961 "TRACE" => quote! {
2962 self.http_client.request(reqwest::Method::TRACE, request_url)
2963 },
2964 other => {
2969 let upper = other.to_string();
2970 quote! {
2971 self.http_client.request(
2972 reqwest::Method::from_bytes(#upper.as_bytes())
2973 .expect("invalid HTTP method"),
2974 request_url,
2975 )
2976 }
2977 }
2978 }
2979 }
2980
2981 fn generate_request_param(&self, op: &OperationInfo) -> TokenStream {
2983 self.generate_request_param_with_capture(op, None)
2984 }
2985
2986 fn generate_request_param_with_capture(
2987 &self,
2988 op: &OperationInfo,
2989 mut captures: Option<&mut Vec<syn::Ident>>,
2990 ) -> TokenStream {
2991 let mut params = Vec::new();
2992 for allocated in self.allocated_operation_params(op) {
2993 let param_name = allocated.ident;
2994 let param_type = if Self::param_has_impl_as_ref_type(allocated.param) {
2995 if let Some(captures) = captures.as_deref_mut() {
2996 let ident = format_ident!("__Param{}", captures.len());
2997 captures.push(ident.clone());
2998 quote! { #ident }
2999 } else {
3000 self.get_param_rust_type(allocated.param)
3001 }
3002 } else {
3003 self.get_param_rust_type(allocated.param)
3004 };
3005 if Self::builder_param_is_required(allocated.param) {
3006 params.push(quote! { #param_name: #param_type });
3007 } else {
3008 params.push(quote! { #param_name: Option<#param_type> });
3009 }
3010 }
3011
3012 if let Some(ref rb) = op.request_body {
3015 use crate::analysis::RequestBodyContent;
3016 if matches!(rb, RequestBodyContent::SchemaLess { .. }) {
3017 return if params.is_empty() {
3018 quote! {}
3019 } else {
3020 quote! { #(#params),* }
3021 };
3022 }
3023 let required = op.request_body_required;
3024 let body_type = match rb {
3025 RequestBodyContent::Json { schema_name, .. }
3026 | RequestBodyContent::FormUrlEncoded { schema_name, .. }
3027 | RequestBodyContent::Multipart { schema_name, .. } => {
3028 let rust_type_name = self.to_rust_type_name(schema_name);
3029 let request_ident =
3030 syn::Ident::new(&rust_type_name, proc_macro2::Span::call_site());
3031 quote! { #request_ident }
3032 }
3033
3034 RequestBodyContent::OctetStream { .. } | RequestBodyContent::Binary { .. } => {
3035 quote! { Vec<u8> }
3036 }
3037 RequestBodyContent::TextPlain { .. } => quote! { String },
3038 RequestBodyContent::Unsupported { .. } => quote! { Vec<u8> },
3039 RequestBodyContent::SchemaLess { .. } => unreachable!(
3040 "schema-less request bodies preserve the historical client signature"
3041 ),
3042 };
3043 let body_ident = match rb {
3044 RequestBodyContent::OctetStream { .. }
3045 | RequestBodyContent::Binary { .. }
3046 | RequestBodyContent::TextPlain { .. }
3047 | RequestBodyContent::Unsupported { .. } => quote! { body },
3048 RequestBodyContent::SchemaLess { .. } => unreachable!(
3049 "schema-less request bodies preserve the historical client signature"
3050 ),
3051 _ => quote! { request },
3052 };
3053 if required {
3054 params.push(quote! { #body_ident: #body_type });
3055 } else {
3056 params.push(quote! { #body_ident: Option<#body_type> });
3057 }
3058 }
3059
3060 if params.is_empty() {
3061 quote! {}
3062 } else {
3063 quote! { #(#params),* }
3064 }
3065 }
3066
3067 fn get_param_rust_type(&self, param: &crate::analysis::ParameterInfo) -> TokenStream {
3069 if Self::param_has_impl_as_ref_type(param) {
3070 quote! { impl AsRef<str> }
3071 } else {
3072 self.get_param_owned_rust_type(param)
3073 }
3074 }
3075
3076 pub(crate) fn get_param_owned_rust_type(
3080 &self,
3081 param: &crate::analysis::ParameterInfo,
3082 ) -> TokenStream {
3083 use crate::analysis::QuerySerialization;
3084 if let Some(
3090 QuerySerialization::FormExplodedArray { item_type }
3091 | QuerySerialization::FormArray { item_type }
3092 | QuerySerialization::SimpleHeaderArray { item_type },
3093 ) = ¶m.query_serialization
3094 {
3095 use crate::analysis::ArrayItemType;
3096 let item_ty: syn::Type = match item_type {
3097 ArrayItemType::Scalar(rust_type) => syn::parse_str(rust_type)
3098 .unwrap_or_else(|_| panic!("invalid scalar item type `{rust_type}`")),
3099 ArrayItemType::SchemaRef(schema_name) => {
3100 let rust_name = self.to_rust_type_name(schema_name);
3101 syn::parse_str(&rust_name)
3102 .unwrap_or_else(|_| panic!("invalid schema item type `{rust_name}`"))
3103 }
3104 ArrayItemType::FlatStructRef { schema_name, .. } => {
3105 let rust_name = self.to_rust_type_name(schema_name);
3106 syn::parse_str(&rust_name)
3107 .unwrap_or_else(|_| panic!("invalid struct item type `{rust_name}`"))
3108 }
3109 ArrayItemType::NestedStructRef { schema_name, .. } => {
3110 let rust_name = self.to_rust_type_name(schema_name);
3111 syn::parse_str(&rust_name)
3112 .unwrap_or_else(|_| panic!("invalid nested struct item type `{rust_name}`"))
3113 }
3114 };
3115 return quote! { Vec<#item_ty> };
3116 }
3117 if let Some(ref schema_name) = param.schema_ref {
3121 let rust_name = self.to_rust_type_name(schema_name);
3122 let ident = syn::Ident::new(&rust_name, proc_macro2::Span::call_site());
3123 return quote! { #ident };
3124 }
3125 syn::parse_str::<syn::Type>(¶m.rust_type)
3126 .map(|ty| quote! { #ty })
3127 .unwrap_or_else(|_| {
3128 let type_ident = syn::Ident::new(¶m.rust_type, proc_macro2::Span::call_site());
3129 quote! { #type_ident }
3130 })
3131 }
3132
3133 fn param_uses_as_ref_str(param: &crate::analysis::ParameterInfo) -> bool {
3138 param.schema_ref.is_none() && param.rust_type == "String"
3139 }
3140
3141 fn multipart_client_field_kind(
3142 schema_type: &crate::analysis::SchemaType,
3143 analysis: &SchemaAnalysis,
3144 visited: &mut std::collections::HashSet<String>,
3145 ) -> Option<MultipartClientFieldKind> {
3146 match schema_type {
3147 crate::analysis::SchemaType::Primitive {
3148 rust_type,
3149 serde_with,
3150 } => {
3151 let rust_type = rust_type.replace(' ', "");
3152 if rust_type == "bytes::Bytes" {
3153 Some(MultipartClientFieldKind::RawBytes)
3154 } else if serde_with
3155 .as_deref()
3156 .is_some_and(|codec| codec.contains("base64_url"))
3157 {
3158 Some(MultipartClientFieldKind::Base64UrlUnpadded)
3159 } else if serde_with
3160 .as_deref()
3161 .is_some_and(|codec| codec.contains("base64"))
3162 || rust_type == "Vec<u8>"
3163 {
3164 Some(MultipartClientFieldKind::Base64)
3165 } else {
3166 Some(MultipartClientFieldKind::Text)
3167 }
3168 }
3169 crate::analysis::SchemaType::StringEnum { .. }
3170 | crate::analysis::SchemaType::ExtensibleEnum { .. } => {
3171 Some(MultipartClientFieldKind::Text)
3172 }
3173 crate::analysis::SchemaType::Object { .. }
3177 | crate::analysis::SchemaType::DiscriminatedUnion { .. }
3178 | crate::analysis::SchemaType::Union { .. }
3179 | crate::analysis::SchemaType::Composition { .. }
3180 | crate::analysis::SchemaType::Untyped {
3181 shape:
3182 crate::analysis::UntypedShape::Value | crate::analysis::UntypedShape::ValueMap,
3183 ..
3184 } => Some(MultipartClientFieldKind::Json),
3185 crate::analysis::SchemaType::Reference { target } => {
3186 if !visited.insert(target.clone()) {
3187 return None;
3188 }
3189 analysis.schemas.get(target).and_then(|schema| {
3190 Self::multipart_client_field_kind(&schema.schema_type, analysis, visited)
3191 })
3192 }
3193 _ => None,
3194 }
3195 }
3196
3197 fn generate_response_content_type_validation(
3198 &self,
3199 operation: &OperationInfo,
3200 success: &ClientSuccessSelection<'_>,
3201 ) -> TokenStream {
3202 let Some(expected) = success.accept else {
3203 return TokenStream::new();
3204 };
3205 let guard = self.success_selection_guard(operation, success);
3206 let errors = self.generate_error_match_arms(operation);
3207 let error_type = self.op_error_type_token(operation);
3208 let raw_body = if matches!(
3209 success.body,
3210 ClientSuccessBody::EventStream | ClientSuccessBody::ParsedSse
3211 ) {
3212 quote! { Vec::new() }
3213 } else {
3214 quote! { __read_bounded_response_body(response, self.max_response_body_bytes).await? }
3215 };
3216 quote! {
3217 let status = response.status();
3218 let status_code = status.as_u16();
3219 if #guard {
3220 let actual = response.headers().get(reqwest::header::CONTENT_TYPE).and_then(|value| value.to_str().ok()).unwrap_or("").to_string();
3221 let expected = #expected;
3222 let parse_media = |value: &str| {
3223 let mut parts = Vec::new();
3224 let mut start = 0;
3225 let mut quoted = false;
3226 let mut escaped = false;
3227 for (index, character) in value.char_indices() {
3228 if escaped { escaped = false; continue }
3229 if character == '\\' && quoted { escaped = true; continue }
3230 if character == '"' { quoted = !quoted; }
3231 if character == ';' && !quoted { parts.push(&value[start..index]); start = index + 1; }
3232 }
3233 if quoted || escaped { return None }
3234 parts.push(&value[start..]);
3235 let essence = parts.first().copied().unwrap_or("").trim().to_ascii_lowercase();
3236 let mut parameters = std::collections::BTreeMap::new();
3237 for part in parts.into_iter().skip(1) {
3238 let (name, value) = part.split_once('=')?;
3239 let name = name.trim().to_ascii_lowercase();
3240 if name.is_empty() { return None }
3241 let value = value.trim();
3242 let value = if let Some(quoted) = value.strip_prefix('"') {
3243 let inner = quoted.strip_suffix('"')?;
3244 let mut decoded = String::new();
3245 let mut escaped = false;
3246 for character in inner.chars() {
3247 if escaped { decoded.push(character); escaped = false; }
3248 else if character == '\\' { escaped = true; }
3249 else if character == '"' { return None }
3250 else { decoded.push(character); }
3251 }
3252 if escaped { return None }
3253 decoded
3254 } else {
3255 if value.contains('"') { return None }
3256 value.to_string()
3257 };
3258 if parameters.insert(name, value).is_some() { return None }
3259 }
3260 Some((essence, parameters))
3261 };
3262 let matches = match (parse_media(&actual), parse_media(expected)) {
3263 (Some((actual_essence, actual_parameters)), Some((expected_essence, expected_parameters))) => {
3264 let essence_matches = if expected_essence == "*/*" { actual_essence.contains('/') }
3265 else if let Some(prefix) = expected_essence.strip_suffix("/*") { actual_essence.split('/').next() == Some(prefix) }
3266 else { actual_essence == expected_essence };
3267 essence_matches && expected_parameters.iter().all(|(name, value)| actual_parameters.get(name).is_some_and(|actual|
3268 if name == "charset" { actual.eq_ignore_ascii_case(value) } else { actual == value }))
3269 }
3270 _ => false,
3271 };
3272 if !matches {
3273 let headers = response.headers().clone();
3274 let raw_body = #raw_body;
3275 let body_text = String::from_utf8_lossy(&raw_body).into_owned();
3276 let typed: Option<#error_type>;
3277 let parse_error: Option<String>;
3278 #errors
3279 let _ = parse_error;
3280 return Err(ApiOpError::Api(ApiError {
3281 status: status_code, headers, body: body_text, raw_body, typed,
3282 parse_error: Some(format!("unexpected response Content-Type `{}`; expected `{}`", actual, expected)),
3283 }));
3284 }
3285 }
3286 }
3287 }
3288
3289 fn resolve_multipart_list<'a>(
3292 schema_type: &'a crate::analysis::SchemaType,
3293 analysis: &'a SchemaAnalysis,
3294 ) -> Option<&'a crate::analysis::SchemaType> {
3295 let mut current = schema_type;
3296 let mut visited = std::collections::HashSet::new();
3297 loop {
3298 match current {
3299 crate::analysis::SchemaType::Array { item_type } => return Some(item_type),
3300 crate::analysis::SchemaType::Reference { target } if visited.insert(target) => {
3301 current = &analysis.schemas.get(target)?.schema_type;
3302 }
3303 _ => return None,
3304 }
3305 }
3306 }
3307
3308 fn is_multipart_file_type(
3311 schema_type: &crate::analysis::SchemaType,
3312 analysis: &SchemaAnalysis,
3313 ) -> bool {
3314 match schema_type {
3315 crate::analysis::SchemaType::Primitive { rust_type, .. } => {
3316 analysis.multipart_file_type.as_deref() == Some(rust_type.as_str())
3317 }
3318 crate::analysis::SchemaType::Nullable { inner_type } => {
3319 Self::is_multipart_file_type(inner_type, analysis)
3320 }
3321 _ => false,
3322 }
3323 }
3324
3325 fn single_media_type(content_type: &str) -> Option<&str> {
3329 let content_type = content_type.trim();
3330 let essence = content_type.split(';').next().unwrap_or_default();
3331 (!content_type.contains(',') && !essence.contains('*') && essence.contains('/'))
3332 .then_some(content_type)
3333 }
3334
3335 fn generate_typed_multipart_form(
3336 &self,
3337 schema_name: &str,
3338 encoding: &BTreeMap<String, String>,
3339 analysis: &SchemaAnalysis,
3340 ) -> TokenStream {
3341 use crate::analysis::{ObjectAdditionalProperties, SchemaType};
3342
3343 let Some((resolved_name, resolved_schema)) =
3344 self.resolve_reference_schema(schema_name, analysis)
3345 else {
3346 let message = format!(
3347 "multipart request schema `{schema_name}` could not be resolved during generation"
3348 );
3349 return quote! {
3350 return Err(HttpError::Config(#message.to_string()).into());
3351 };
3352 };
3353 let SchemaType::Object {
3354 properties,
3355 required,
3356 additional_properties,
3357 ..
3358 } = &resolved_schema.schema_type
3359 else {
3360 let message =
3361 format!("multipart request schema `{schema_name}` must resolve to an object");
3362 return quote! {
3363 return Err(HttpError::Config(#message.to_string()).into());
3364 };
3365 };
3366 let fields = self.emitted_object_properties(
3367 resolved_name,
3368 properties,
3369 required,
3370 additional_properties,
3371 analysis,
3372 );
3373 if matches!(
3374 additional_properties,
3375 ObjectAdditionalProperties::Typed { .. }
3376 ) {
3377 let message = format!(
3378 "multipart request schema `{schema_name}` cannot contain typed additional properties"
3379 );
3380 return quote! {
3381 return Err(HttpError::Config(#message.to_string()).into());
3382 };
3383 }
3384
3385 let mut parts = Vec::new();
3386 for field in fields {
3387 let wire_name = field.wire_name;
3388 let is_nullable = self.property_is_nullable(resolved_name, wire_name, field.property);
3389 let is_tri_state = self.property_is_tri_state(
3390 resolved_name,
3391 wire_name,
3392 field.property,
3393 field.is_required,
3394 );
3395 let ident = field.ident;
3396 let field_type = match &field.property.schema_type {
3398 crate::analysis::SchemaType::Nullable { inner_type } => &**inner_type,
3399 other => other,
3400 };
3401 let (is_array, item_type) = match Self::resolve_multipart_list(field_type, analysis) {
3402 Some(item_type) => (true, item_type),
3403 None => (false, field_type),
3404 };
3405 let (nullable_items, item_type) = match item_type {
3407 crate::analysis::SchemaType::Nullable { inner_type } if is_array => {
3408 (true, &**inner_type)
3409 }
3410 other => (false, other),
3411 };
3412 let declared_content_type = encoding
3416 .get(wire_name)
3417 .and_then(|content_type| Self::single_media_type(content_type));
3418 let add_item = if Self::is_multipart_file_type(item_type, analysis) {
3419 let part = match self.config().types.binary {
3421 crate::type_mapping::BinaryStrategy::Bytes => {
3422 quote! { reqwest::multipart::Part::bytes(value.content.to_vec()) }
3423 }
3424 crate::type_mapping::BinaryStrategy::VecU8 => {
3425 quote! { reqwest::multipart::Part::bytes(value.content.clone()) }
3426 }
3427 crate::type_mapping::BinaryStrategy::String => {
3428 quote! { reqwest::multipart::Part::text(value.content.clone()) }
3429 }
3430 };
3431 let invalid = format!("multipart field `{wire_name}` has an invalid content type");
3432 let set_content_type = quote! {
3433 part = part.mime_str(content_type).map_err(|error| {
3434 HttpError::Config(format!("{} `{}`: {}", #invalid, content_type, error))
3435 })?;
3436 };
3437 let content_type = match declared_content_type {
3438 Some(declared) => quote! {
3439 let content_type = value.content_type.as_deref().unwrap_or(#declared);
3440 #set_content_type
3441 },
3442 None => quote! {
3443 if let Some(content_type) = value.content_type.as_deref() {
3444 #set_content_type
3445 }
3446 },
3447 };
3448 quote! {
3449 let mut part = #part;
3450 if let Some(file_name) = &value.file_name {
3451 part = part.file_name(file_name.clone());
3452 }
3453 #content_type
3454 form = form.part(#wire_name, part);
3455 }
3456 } else {
3457 let Some(kind) = Self::multipart_client_field_kind(
3458 item_type,
3459 analysis,
3460 &mut std::collections::HashSet::new(),
3461 ) else {
3462 let message = format!(
3463 "multipart field `{wire_name}` must be a file, an object, a scalar, or a list of them"
3464 );
3465 return quote! {
3466 return Err(HttpError::Config(#message.to_string()).into());
3467 };
3468 };
3469 let content_type =
3470 declared_content_type.or(matches!(kind, MultipartClientFieldKind::Json)
3471 .then_some("application/json"));
3472 let shorthand = match kind {
3475 MultipartClientFieldKind::RawBytes => Some(quote! {
3476 form = form.part(#wire_name, reqwest::multipart::Part::bytes(value.to_vec()));
3477 }),
3478 MultipartClientFieldKind::Base64 => Some(quote! {
3479 use base64::Engine as _;
3480 form = form.text(#wire_name, base64::engine::general_purpose::STANDARD.encode(value));
3481 }),
3482 MultipartClientFieldKind::Base64UrlUnpadded => Some(quote! {
3483 use base64::Engine as _;
3484 form = form.text(#wire_name, base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(value));
3485 }),
3486 MultipartClientFieldKind::Text => {
3487 Some(quote! { form = form.text(#wire_name, value.to_string()); })
3488 }
3489 MultipartClientFieldKind::Json => None,
3490 }
3491 .filter(|_| content_type.is_none());
3492 if let Some(shorthand) = shorthand {
3493 shorthand
3494 } else {
3495 let part = match kind {
3496 MultipartClientFieldKind::RawBytes => {
3497 quote! { reqwest::multipart::Part::bytes(value.to_vec()) }
3498 }
3499 MultipartClientFieldKind::Base64 => quote! { {
3500 use base64::Engine as _;
3501 reqwest::multipart::Part::text(base64::engine::general_purpose::STANDARD.encode(value))
3502 } },
3503 MultipartClientFieldKind::Base64UrlUnpadded => quote! { {
3504 use base64::Engine as _;
3505 reqwest::multipart::Part::text(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(value))
3506 } },
3507 MultipartClientFieldKind::Text => {
3508 quote! { reqwest::multipart::Part::text(value.to_string()) }
3509 }
3510 MultipartClientFieldKind::Json => quote! {
3511 reqwest::multipart::Part::bytes(serde_json::to_vec(value).map_err(HttpError::serialization_error)?)
3512 },
3513 };
3514 let content_type = content_type.map(|content_type| {
3515 let message = format!(
3516 "multipart field `{wire_name}` has an invalid content type `{content_type}`"
3517 );
3518 quote! {
3519 let part = part
3520 .mime_str(#content_type)
3521 .map_err(|error| HttpError::Config(format!("{}: {}", #message, error)))?;
3522 }
3523 });
3524 quote! {
3525 let part = #part;
3526 #content_type
3527 form = form.part(#wire_name, part);
3528 }
3529 }
3530 };
3531 let add_value = if nullable_items {
3532 quote! {
3533 for value in value.iter().flatten() {
3534 #add_item
3535 }
3536 }
3537 } else if is_array {
3538 quote! {
3539 for value in value {
3540 #add_item
3541 }
3542 }
3543 } else {
3544 add_item
3545 };
3546 parts.push(if is_tri_state {
3547 quote! {
3551 if let Some(Some(value)) = &request.#ident {
3552 #add_value
3553 }
3554 }
3555 } else if field.is_required && is_nullable {
3556 quote! {
3557 if let Some(value) = &request.#ident {
3558 #add_value
3559 }
3560 }
3561 } else if field.is_required {
3562 quote! {
3563 let value = &request.#ident;
3564 #add_value
3565 }
3566 } else {
3567 quote! {
3568 if let Some(value) = &request.#ident {
3569 #add_value
3570 }
3571 }
3572 });
3573 }
3574
3575 quote! {
3576 let mut form = reqwest::multipart::Form::new();
3577 #(#parts)*
3578 req = req.multipart(form);
3579 }
3580 }
3581
3582 fn generate_request_content_type_validation(
3583 &self,
3584 operation: &OperationInfo,
3585 input: &syn::Ident,
3586 header: &syn::Ident,
3587 ) -> TokenStream {
3588 let ranges = Self::wildcard_request_media_types(operation).unwrap_or_default();
3589 let operation_id = &operation.operation_id;
3590 let validate =
3591 quote! { Self::__request_content_type(#input, &[#(#ranges),*], #operation_id)? };
3592 if operation.request_body_required {
3593 quote! { let #header = Some(#validate); }
3594 } else {
3595 quote! {
3596 let #header = if body.is_some() { Some(#validate) } else { None };
3597 }
3598 }
3599 }
3600
3601 fn generate_request_content_type_helper() -> TokenStream {
3602 quote! {
3603 impl HttpClient {
3604 fn __request_content_type(input: &str, ranges: &[&str], operation_id: &str) -> Result<reqwest::header::HeaderValue, HttpError> {
3605 let actual = input.parse::<::mime::Mime>().map_err(|error| {
3606 HttpError::Config(format!("invalid request Content-Type `{}` for operation `{}`: {}", input, operation_id, error))
3607 })?;
3608 if actual.type_().as_str().contains('*') || actual.subtype().as_str().contains('*') {
3609 return Err(HttpError::Config(format!("request Content-Type `{}` for operation `{}` must be concrete", input, operation_id)));
3610 }
3611 let matches = ranges.iter().any(|range| {
3612 let Ok(expected) = range.parse::<::mime::Mime>() else { return false };
3613 (expected.type_() == ::mime::STAR || expected.type_() == actual.type_())
3614 && (expected.subtype() == ::mime::STAR
3615 || (expected.subtype() == actual.subtype() && expected.suffix() == actual.suffix()))
3616 && expected.params().all(|(name, value)| {
3617 actual.get_param(name).is_some_and(|actual| actual == value)
3618 })
3619 });
3620 if !matches {
3621 return Err(HttpError::Config(format!("request Content-Type `{}` for operation `{}` does not match declared media ranges ({})", input, operation_id, ranges.join(", "))));
3622 }
3623 input.parse::<reqwest::header::HeaderValue>().map_err(|error| {
3624 HttpError::Config(format!("invalid request Content-Type header for operation `{}`: {}", operation_id, error))
3625 })
3626 }
3627 }
3628 }
3629 }
3630
3631 fn generate_request_body(
3637 &self,
3638 op: &OperationInfo,
3639 analysis: &SchemaAnalysis,
3640 with_content_type: bool,
3641 ) -> TokenStream {
3642 let empty_request_framing = Self::generate_empty_request_framing(op);
3643 let Some(rb) = op.request_body.as_ref() else {
3644 return empty_request_framing;
3645 };
3646 use crate::analysis::RequestBodyContent;
3647 let required = op.request_body_required;
3648 let (ident, apply): (TokenStream, TokenStream) = match rb {
3649 _ if with_content_type => (quote! { body }, quote! { req = req.body(body); }),
3650 _ if Self::wildcard_request_media_types(op).is_some() => {
3651 return Self::generate_missing_request_content_type(
3652 op,
3653 Self::wildcard_request_media_types(op).unwrap_or_default(),
3654 );
3655 }
3656 RequestBodyContent::Json { media_type, .. } => (
3657 quote! { request },
3658 quote! {
3659 req = req
3660 .body(serde_json::to_vec(&request).map_err(HttpError::serialization_error)?)
3661 .header("content-type", #media_type);
3662 },
3663 ),
3664 RequestBodyContent::FormUrlEncoded { media_type, .. } => (
3665 quote! { request },
3666 quote! {
3667 req = req
3668 .body(serde_urlencoded::to_string(&request).map_err(HttpError::serialization_error)?)
3669 .header("content-type", #media_type);
3670 },
3671 ),
3672 RequestBodyContent::Multipart {
3673 schema_name,
3674 encoding,
3675 ..
3676 } => (
3677 quote! { request },
3678 self.generate_typed_multipart_form(schema_name, encoding, analysis),
3679 ),
3680 RequestBodyContent::OctetStream { media_type } => (
3681 quote! { body },
3682 quote! {
3683 req = req
3684 .body(body)
3685 .header("content-type", #media_type);
3686 },
3687 ),
3688 RequestBodyContent::Binary { media_type } => (
3689 quote! { body },
3690 quote! {
3691 req = req
3692 .body(body)
3693 .header("content-type", #media_type);
3694 },
3695 ),
3696 RequestBodyContent::TextPlain { media_type } => (
3697 quote! { body },
3698 quote! {
3699 req = req
3700 .body(body)
3701 .header("content-type", #media_type);
3702 },
3703 ),
3704 RequestBodyContent::Unsupported { media_types } => {
3705 let media_type = media_types
3706 .iter()
3707 .find(|media_type| !crate::openapi::is_wildcard_media_type(media_type))
3708 .map(String::as_str);
3709 let Some(media_type) = media_type else {
3710 return Self::generate_missing_request_content_type(op, media_types);
3711 };
3712 (
3713 quote! { body },
3714 quote! {
3715 req = req
3716 .body(body)
3717 .header("content-type", #media_type);
3718 },
3719 )
3720 }
3721 RequestBodyContent::SchemaLess { .. } => return empty_request_framing,
3722 };
3723 if required {
3724 apply
3725 } else {
3726 quote! {
3727 if let Some(#ident) = #ident {
3728 #apply
3729 } else {
3730 #empty_request_framing
3731 }
3732 }
3733 }
3734 }
3735
3736 fn generate_missing_request_content_type(
3737 op: &OperationInfo,
3738 media_types: &[String],
3739 ) -> TokenStream {
3740 let ranges = media_types.join(", ");
3741 let message = format!(
3742 "request body for operation `{}` declares only wildcard media ranges ({ranges}); a concrete Content-Type is required",
3743 op.operation_id,
3744 );
3745 if op.request_body_required {
3746 quote! {
3747 let _ = body;
3748 return Err(HttpError::Config(#message.to_string()).into());
3749 }
3750 } else {
3751 let empty_request_framing = Self::generate_empty_request_framing(op);
3752 quote! {
3753 if body.is_some() {
3754 return Err(HttpError::Config(#message.to_string()).into());
3755 }
3756 #empty_request_framing
3757 }
3758 }
3759 }
3760
3761 fn generate_empty_request_framing(op: &OperationInfo) -> TokenStream {
3766 if ["POST", "PUT", "PATCH"]
3767 .iter()
3768 .any(|method| op.method.eq_ignore_ascii_case(method))
3769 {
3770 quote! {
3771 req = req.header(reqwest::header::CONTENT_LENGTH, "0");
3772 }
3773 } else {
3774 quote! {}
3775 }
3776 }
3777
3778 fn get_success_response_schema<'a>(
3784 &self,
3785 op: &'a OperationInfo,
3786 ) -> Option<(&'a str, &'a String)> {
3787 op.response_schemas
3788 .get_key_value("200")
3789 .or_else(|| op.response_schemas.get_key_value("201"))
3790 .or_else(|| {
3791 op.response_schemas
3792 .iter()
3793 .find(|(code, _)| code.starts_with('2'))
3794 })
3795 .map(|(status, schema)| (status.as_str(), schema))
3796 }
3797
3798 fn get_success_response<'a>(
3799 &self,
3800 analysis: &'a SchemaAnalysis,
3801 op: &'a OperationInfo,
3802 ) -> ClientSuccessSelection<'a> {
3803 if let Some(responses) = analysis.operation_responses.get(&op.operation_id) {
3804 let mut candidates = Vec::new();
3805 for preferred in ["200", "201"] {
3806 if let Some((status, response)) = responses.get_key_value(preferred) {
3807 candidates.push((status.as_str(), response));
3808 }
3809 }
3810 candidates.extend(
3811 responses
3812 .iter()
3813 .filter(|(status, _)| {
3814 status.starts_with('2')
3815 && status.as_str() != "200"
3816 && status.as_str() != "201"
3817 })
3818 .map(|(status, response)| (status.as_str(), response)),
3819 );
3820
3821 let selected = candidates
3822 .iter()
3823 .copied()
3824 .find(|(_, response)| {
3825 matches!(
3826 Self::response_body(response),
3827 ClientSuccessBody::Json(_)
3828 | ClientSuccessBody::Text
3829 | ClientSuccessBody::Binary
3830 )
3831 })
3832 .or_else(|| {
3833 candidates.iter().copied().find(|(_, response)| {
3834 matches!(
3835 Self::response_body(response),
3836 ClientSuccessBody::EventStream
3837 )
3838 })
3839 })
3840 .or_else(|| candidates.first().copied());
3841
3842 if let Some((_, response)) = selected {
3843 let body = Self::response_body(response);
3844 let statuses: Vec<&str> = candidates
3845 .iter()
3846 .filter_map(|(status, candidate)| {
3847 Self::success_bodies_are_compatible(body, Self::response_body(candidate))
3848 .then_some(*status)
3849 })
3850 .collect();
3851 let accept = match &response.body {
3852 Some(OperationResponseBody::Json { media_type, .. })
3853 | Some(OperationResponseBody::Text { media_type }) => Some(media_type.as_str()),
3854 Some(OperationResponseBody::Binary {
3855 media_type,
3856 wildcard,
3857 }) => (!wildcard).then_some(media_type.as_str()),
3858 None if response.schema_name.is_some() => {
3859 response.media_type.as_deref().or(Some("application/json"))
3860 }
3861 None if response.supports_streaming => Some("text/event-stream"),
3862 None => None,
3863 };
3864 let excluded_statuses = responses
3865 .keys()
3866 .filter(|status| {
3867 status.len() == 3
3868 && status.bytes().all(|c| c.is_ascii_digit())
3869 && !statuses.contains(&status.as_str())
3870 })
3871 .map(String::as_str)
3872 .collect();
3873 return ClientSuccessSelection {
3874 statuses,
3875 excluded_statuses,
3876 body,
3877 accept,
3878 };
3879 }
3880 }
3881
3882 if let Some((_status, schema_name)) = self.get_success_response_schema(op) {
3883 let statuses = op
3884 .response_schemas
3885 .iter()
3886 .filter_map(|(candidate_status, candidate_schema)| {
3887 (candidate_status.starts_with('2') && candidate_schema == schema_name)
3888 .then_some(candidate_status.as_str())
3889 })
3890 .collect();
3891 ClientSuccessSelection {
3892 statuses,
3893 excluded_statuses: Vec::new(),
3894 body: ClientSuccessBody::Json(schema_name),
3895 accept: Some("application/json"),
3896 }
3897 } else if Self::returns_raw_event_stream(op) {
3898 ClientSuccessSelection {
3899 statuses: Vec::new(),
3900 excluded_statuses: Vec::new(),
3901 body: ClientSuccessBody::EventStream,
3902 accept: Some("text/event-stream"),
3903 }
3904 } else {
3905 ClientSuccessSelection {
3906 statuses: Vec::new(),
3907 excluded_statuses: Vec::new(),
3908 body: ClientSuccessBody::Empty,
3909 accept: None,
3910 }
3911 }
3912 }
3913
3914 fn response_body(response: &crate::analysis::OperationResponse) -> ClientSuccessBody<'_> {
3915 match &response.body {
3916 Some(OperationResponseBody::Json { schema_name, .. }) => {
3917 ClientSuccessBody::Json(schema_name)
3918 }
3919 Some(OperationResponseBody::Text { .. }) => ClientSuccessBody::Text,
3920 Some(OperationResponseBody::Binary { .. }) => ClientSuccessBody::Binary,
3921 None if response.schema_name.is_some() => {
3922 ClientSuccessBody::Json(response.schema_name.as_deref().unwrap_or_default())
3923 }
3924 None if response.supports_streaming => ClientSuccessBody::EventStream,
3925 None => ClientSuccessBody::Empty,
3926 }
3927 }
3928
3929 fn success_bodies_are_compatible(
3930 selected: ClientSuccessBody<'_>,
3931 candidate: ClientSuccessBody<'_>,
3932 ) -> bool {
3933 match (selected, candidate) {
3934 (ClientSuccessBody::Json(selected), ClientSuccessBody::Json(candidate)) => {
3935 selected == candidate
3936 }
3937 (ClientSuccessBody::Text, ClientSuccessBody::Text)
3938 | (ClientSuccessBody::Binary, ClientSuccessBody::Binary)
3939 | (ClientSuccessBody::EventStream, ClientSuccessBody::EventStream)
3940 | (ClientSuccessBody::Empty, ClientSuccessBody::Empty) => true,
3941 _ => false,
3942 }
3943 }
3944
3945 fn response_type_for_body(&self, body: ClientSuccessBody<'_>) -> TokenStream {
3946 match body {
3947 ClientSuccessBody::Json(response_type) => {
3948 let rust_type_name = self.to_rust_type_name(response_type);
3950 let response_ident =
3951 syn::Ident::new(&rust_type_name, proc_macro2::Span::call_site());
3952 quote! { #response_ident }
3953 }
3954 ClientSuccessBody::Text => quote! { String },
3955 ClientSuccessBody::Binary => quote! { bytes::Bytes },
3956 ClientSuccessBody::EventStream => {
3957 quote! { impl futures_util::Stream<Item = Result<bytes::Bytes, reqwest::Error>> }
3958 }
3959 ClientSuccessBody::ParsedSse => {
3960 quote! { super::sse::BoxSseStream<Result<super::sse::SseEvent<String>, super::sse::StreamingError>> }
3961 }
3962 ClientSuccessBody::Empty => quote! { () },
3963 }
3964 }
3965
3966 fn success_status_guard(statuses: &[&str]) -> TokenStream {
3967 if statuses.is_empty() {
3968 return quote! { status.is_success() };
3969 }
3970 let guards = statuses
3971 .iter()
3972 .map(|status| Self::single_status_guard(status));
3973 quote! { false #( || #guards )* }
3974 }
3975
3976 fn success_selection_guard(
3977 &self,
3978 _operation: &OperationInfo,
3979 success: &ClientSuccessSelection<'_>,
3980 ) -> TokenStream {
3981 let guard = Self::success_status_guard(&success.statuses);
3982 if !success
3983 .statuses
3984 .iter()
3985 .any(|status| status.eq_ignore_ascii_case("2XX"))
3986 {
3987 return guard;
3988 }
3989 let exact: Vec<_> = success
3990 .excluded_statuses
3991 .iter()
3992 .map(|status| Self::single_status_guard(status))
3993 .collect();
3994 quote! { (#guard) && !(false #( || #exact )*) }
3995 }
3996
3997 fn single_status_guard(status: &str) -> TokenStream {
3998 match status {
3999 status if status.chars().all(|character| character.is_ascii_digit()) => {
4000 let status: u16 = status.parse().unwrap_or_default();
4001 quote! { status_code == #status }
4002 }
4003 status if matches!(status.as_bytes(), [b'1'..=b'5', b'X' | b'x', b'X' | b'x']) => {
4004 let class = u16::from(status.as_bytes()[0] - b'0');
4005 quote! { status_code / 100 == #class }
4006 }
4007 _ => quote! { status.is_success() },
4008 }
4009 }
4010
4011 fn returns_raw_event_stream(op: &OperationInfo) -> bool {
4025 op.supports_streaming
4026 }
4027
4028 fn generate_error_handling(
4035 &self,
4036 op: &OperationInfo,
4037 success: ClientSuccessSelection<'_>,
4038 ) -> TokenStream {
4039 let op_error_type = self.op_error_type_token(op);
4040 let success_body = success.body;
4041 let success_status_guard = self.success_selection_guard(op, &success);
4042 let selected_status = if success.statuses.is_empty() {
4043 "any declared 2xx response".to_string()
4044 } else {
4045 success.statuses.join(", ")
4046 };
4047 let rejects_unexpected_success = !success.statuses.is_empty();
4051
4052 let success_branch = match success_body {
4053 ClientSuccessBody::Json(_) => quote! {
4054 match serde_json::from_str(&body_text) {
4055 Ok(body) => Ok(body),
4056 Err(e) => Err(ApiOpError::Api(ApiError {
4057 status: status_code,
4058 headers: headers,
4059 body: body_text,
4060 raw_body,
4061 typed: None,
4062 parse_error: Some(format!(
4063 "failed to deserialize 2xx response body: {}",
4064 e
4065 )),
4066 })),
4067 }
4068 },
4069 ClientSuccessBody::Text => quote! {
4070 let _ = raw_body;
4071 Ok(body_text)
4072 },
4073 ClientSuccessBody::Empty => quote! {
4074 let _ = body_text;
4075 let _ = raw_body;
4076 let _ = headers;
4077 Ok(())
4078 },
4079 ClientSuccessBody::Binary
4080 | ClientSuccessBody::EventStream
4081 | ClientSuccessBody::ParsedSse => quote! {},
4082 };
4083
4084 let error_match_arms = self.generate_error_match_arms(op);
4085
4086 if matches!(
4091 success_body,
4092 ClientSuccessBody::EventStream | ClientSuccessBody::ParsedSse
4093 ) {
4094 let stream = if matches!(success_body, ClientSuccessBody::ParsedSse) {
4095 quote! { super::sse::parse_sse_response(response) }
4096 } else {
4097 quote! { response.bytes_stream() }
4098 };
4099 let reject_unexpected_success = if rejects_unexpected_success {
4100 quote! {
4101 if status.is_success() {
4102 return Err(ApiOpError::Api(ApiError {
4103 status: status_code,
4104 headers,
4105 body: String::new(),
4106 raw_body: Vec::new(),
4107 typed: None,
4108 parse_error: Some(format!(
4109 "unexpected successful status {}; generated return type selects `{}`; live response body was not buffered",
4110 status_code,
4111 #selected_status,
4112 )),
4113 }));
4114 }
4115 }
4116 } else {
4117 quote! {}
4118 };
4119 return quote! {
4120 let status = response.status();
4121 let status_code = status.as_u16();
4122 let headers = response.headers().clone();
4123
4124 if #success_status_guard {
4125 Ok(#stream)
4126 } else {
4127 #reject_unexpected_success
4128 let body_bytes = __read_bounded_response_body(
4129 response,
4130 self.max_response_body_bytes,
4131 ).await?;
4132 let raw_body = body_bytes;
4133 let body_text = String::from_utf8_lossy(&raw_body).into_owned();
4134 let typed: Option<#op_error_type>;
4135 let parse_error: Option<String>;
4136 #error_match_arms
4137 Err(ApiOpError::Api(ApiError {
4138 status: status_code,
4139 headers,
4140 body: body_text,
4141 raw_body,
4142 typed,
4143 parse_error,
4144 }))
4145 }
4146 };
4147 }
4148
4149 if matches!(success_body, ClientSuccessBody::Binary) {
4150 let reject_unexpected_success = if rejects_unexpected_success {
4151 quote! {
4152 if status.is_success() {
4153 return Err(ApiOpError::Api(ApiError {
4154 status: status_code,
4155 headers,
4156 body: body_text,
4157 raw_body,
4158 typed: None,
4159 parse_error: Some(format!(
4160 "unexpected successful status {}; generated return type selects `{}`",
4161 status_code,
4162 #selected_status,
4163 )),
4164 }));
4165 }
4166 }
4167 } else {
4168 quote! {}
4169 };
4170 return quote! {
4171 let status = response.status();
4172 let status_code = status.as_u16();
4173 let headers = response.headers().clone();
4174
4175 let body_bytes = __read_bounded_response_body(
4176 response,
4177 self.max_response_body_bytes,
4178 ).await?;
4179 if #success_status_guard {
4180 Ok(bytes::Bytes::from(body_bytes))
4181 } else {
4182 let raw_body = body_bytes;
4183 let body_text = String::from_utf8_lossy(&raw_body).into_owned();
4184 #reject_unexpected_success
4185 let typed: Option<#op_error_type>;
4186 let parse_error: Option<String>;
4187 #error_match_arms
4188 Err(ApiOpError::Api(ApiError {
4189 status: status_code,
4190 headers,
4191 body: body_text,
4192 raw_body,
4193 typed,
4194 parse_error,
4195 }))
4196 }
4197 };
4198 }
4199
4200 let reject_unexpected_success = if rejects_unexpected_success {
4201 quote! {
4202 else if status.is_success() {
4203 Err(ApiOpError::Api(ApiError {
4204 status: status_code,
4205 headers,
4206 body: body_text,
4207 raw_body,
4208 typed: None,
4209 parse_error: Some(format!(
4210 "unexpected successful status {}; generated return type selects `{}`",
4211 status_code,
4212 #selected_status,
4213 )),
4214 }))
4215 }
4216 }
4217 } else {
4218 quote! {}
4219 };
4220
4221 quote! {
4222 let status = response.status();
4223 let status_code = status.as_u16();
4224 let headers = response.headers().clone();
4225 let body_bytes = __read_bounded_response_body(
4226 response,
4227 self.max_response_body_bytes,
4228 ).await?;
4229 let raw_body = body_bytes;
4230 let body_text = String::from_utf8_lossy(&raw_body).into_owned();
4231
4232 if #success_status_guard {
4233 #success_branch
4234 } #reject_unexpected_success else {
4235 let typed: Option<#op_error_type>;
4236 let parse_error: Option<String>;
4237 #error_match_arms
4238 Err(ApiOpError::Api(ApiError {
4239 status: status_code,
4240 headers,
4241 body: body_text,
4242 raw_body,
4243 typed,
4244 parse_error,
4245 }))
4246 }
4247 }
4248 }
4249
4250 fn generate_error_match_arms(&self, op: &OperationInfo) -> TokenStream {
4253 let arms: Vec<TokenStream> = op
4254 .response_schemas
4255 .iter()
4256 .filter(|(code, _)| !code.starts_with('2'))
4257 .filter_map(|(code, schema)| {
4258 let variant_ident = Self::op_error_variant_ident(code);
4259 let payload_ty_name = self.to_rust_type_name(schema);
4260 let payload_ty = syn::Ident::new(&payload_ty_name, proc_macro2::Span::call_site());
4261 let enum_ident = self.op_error_enum_ident(op);
4262
4263 let pattern = match code.as_str() {
4268 "default" | "Default" => return None, other if other.chars().all(|c| c.is_ascii_digit()) => {
4270 let n: u16 = other.parse().ok()?;
4271 quote! { #n }
4272 }
4273 "1XX" | "1xx" => quote! { code if (100..=199).contains(&code) },
4274 "2XX" | "2xx" => quote! { code if (200..=299).contains(&code) },
4275 "3XX" | "3xx" => quote! { code if (300..=399).contains(&code) },
4276 "4XX" | "4xx" => quote! { code if (400..=499).contains(&code) },
4277 "5XX" | "5xx" => quote! { code if (500..=599).contains(&code) },
4278 _ => return None,
4279 };
4280
4281 Some(quote! {
4282 #pattern => {
4283 match serde_json::from_str::<#payload_ty>(&body_text) {
4284 Ok(v) => {
4285 typed = Some(#enum_ident::#variant_ident(v));
4286 parse_error = None;
4287 }
4288 Err(e) => {
4289 typed = None;
4290 parse_error = Some(e.to_string());
4291 }
4292 }
4293 }
4294 })
4295 })
4296 .collect();
4297
4298 let has_typed_enum = op
4306 .response_schemas
4307 .iter()
4308 .any(|(code, _)| !code.starts_with('2'));
4309
4310 let default_payload = op
4317 .response_schemas
4318 .iter()
4319 .find(|(code, _)| matches!(code.as_str(), "default" | "Default"))
4320 .map(|(_, schema)| self.to_rust_type_name(schema));
4321
4322 let default_arm = if let Some(payload_ty_name) = default_payload {
4323 let payload_ty = syn::Ident::new(&payload_ty_name, proc_macro2::Span::call_site());
4324 let enum_ident = self.op_error_enum_ident(op);
4325 quote! {
4326 _ => {
4327 match serde_json::from_str::<#payload_ty>(&body_text) {
4328 Ok(v) => {
4329 typed = Some(#enum_ident::Default(v));
4330 parse_error = None;
4331 }
4332 Err(e) => {
4333 typed = None;
4334 parse_error = Some(e.to_string());
4335 }
4336 }
4337 }
4338 }
4339 } else if has_typed_enum {
4340 quote! {
4341 _ => {
4342 typed = None;
4343 parse_error = None;
4344 }
4345 }
4346 } else {
4347 quote! {
4349 _ => {
4350 match serde_json::from_str::<serde_json::Value>(&body_text) {
4351 Ok(v) => {
4352 typed = Some(v);
4353 parse_error = None;
4354 }
4355 Err(e) => {
4356 typed = None;
4357 parse_error = Some(e.to_string());
4358 }
4359 }
4360 }
4361 }
4362 };
4363
4364 if arms.is_empty() {
4365 quote! {
4367 match status_code {
4368 #default_arm
4369 }
4370 }
4371 } else {
4372 quote! {
4373 match status_code {
4374 #(#arms)*
4375 #default_arm
4376 }
4377 }
4378 }
4379 }
4380
4381 fn generate_url_construction(&self, path: &str, op: &OperationInfo) -> TokenStream {
4383 if path.contains('{') {
4385 self.generate_url_with_params(path, op)
4386 } else {
4387 quote! {
4388 let request_url = format!("{}{}", self.base_url, #path);
4389 }
4390 }
4391 }
4392
4393 fn generate_url_with_params(&self, path: &str, op: &OperationInfo) -> TokenStream {
4395 let path_params: Vec<_> = op
4397 .parameters
4398 .iter()
4399 .filter(|p| p.location == "path")
4400 .collect();
4401
4402 let mut format_string = String::with_capacity(path.len());
4411 let mut format_args: Vec<TokenStream> = Vec::new();
4412 let mut chars = path.chars().peekable();
4413 while let Some(c) = chars.next() {
4414 if c != '{' {
4415 format_string.push(c);
4416 continue;
4417 }
4418 let mut name = String::new();
4420 while let Some(&n) = chars.peek() {
4421 chars.next();
4422 if n == '}' {
4423 break;
4424 }
4425 name.push(n);
4426 }
4427 let param = path_params.iter().find(|p| p.name == name);
4431 let Some(param) = param else {
4432 format_string.push('{');
4433 format_string.push_str(&name);
4434 format_string.push('}');
4435 continue;
4436 };
4437 format_string.push_str("{}");
4438 let param_name_snake = self.operation_param_ident(op, param);
4439 let param_ident = Self::to_field_ident(¶m_name_snake);
4440 let encoder = if param.allow_reserved {
4441 quote! { __pct_encode_path_reserved }
4442 } else {
4443 quote! { __pct_encode_path_segment }
4444 };
4445 if Self::param_uses_as_ref_str(param) {
4446 format_args.push(quote! {
4447 #encoder(#param_ident.as_ref())
4448 });
4449 } else {
4450 format_args.push(quote! {
4451 #encoder(&#param_ident.to_string())
4452 });
4453 }
4454 }
4455
4456 if format_args.is_empty() {
4457 quote! {
4458 let request_url = format!("{}{}", self.base_url, #path);
4459 }
4460 } else {
4461 quote! {
4462 let request_url = format!("{}{}", self.base_url, format!(#format_string, #(#format_args),*));
4463 }
4464 }
4465 }
4466
4467 pub(crate) fn param_ident_str(&self, param: &crate::analysis::ParameterInfo) -> String {
4472 if let Some(ident) = ¶m.rust_ident {
4473 return self.escape_keyword_ident(ident);
4477 }
4478 self.sanitize_param_name(¶m.name)
4479 }
4480
4481 fn escape_keyword_ident(&self, snake_case: &str) -> String {
4482 if matches!(snake_case, "self" | "super" | "crate" | "Self") {
4483 return format!("{snake_case}_param");
4484 }
4485 if Self::is_rust_keyword(snake_case) {
4486 format!("r#{snake_case}")
4487 } else {
4488 snake_case.to_string()
4489 }
4490 }
4491
4492 fn sanitize_param_name(&self, name: &str) -> String {
4497 let suffix = if name.ends_with("<=") {
4502 "_lte"
4503 } else if name.ends_with(">=") {
4504 "_gte"
4505 } else if name.ends_with('<') {
4506 "_lt"
4507 } else if name.ends_with('>') {
4508 "_gt"
4509 } else {
4510 ""
4511 };
4512 let stripped = name.trim_end_matches(['<', '>', '=']);
4513 let mut snake_case = stripped.to_snake_case();
4514 if snake_case.is_empty() {
4515 snake_case.push_str("parameter");
4516 } else if snake_case.starts_with(|character: char| character.is_ascii_digit()) {
4517 snake_case.insert(0, '_');
4518 }
4519 snake_case.push_str(suffix);
4520
4521 if matches!(snake_case.as_str(), "self" | "super" | "crate" | "Self") {
4522 return format!("{snake_case}_param");
4523 }
4524 if Self::is_rust_keyword(&snake_case) {
4525 format!("r#{snake_case}")
4526 } else {
4527 snake_case
4528 }
4529 }
4530}