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