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