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