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