1use crate::analysis::{OperationInfo, OperationResponseBody, ParameterInfo, SchemaAnalysis};
151use crate::generator::CodeGenerator;
152use heck::{ToPascalCase, ToSnakeCase};
153use proc_macro2::TokenStream;
154use quote::{format_ident, quote};
155use std::collections::BTreeMap;
156
157struct AllocatedOperationParam<'a> {
158 param: &'a ParameterInfo,
159 ident: syn::Ident,
160}
161
162#[derive(Clone)]
163struct BodyFieldPlan {
164 wire_name: String,
165 preferred_method_name: String,
166 value_ident: syn::Ident,
167 value_type: TokenStream,
168 access_path: Vec<syn::Ident>,
169}
170
171#[derive(Clone, Copy)]
172enum MultipartClientFieldKind {
173 RawBytes,
174 Base64,
175 Base64UrlUnpadded,
176 Text,
177}
178
179#[derive(Clone, Copy)]
180enum ClientSuccessBody<'a> {
181 Json(&'a str),
182 Text,
183 Binary,
184 EventStream,
185 Empty,
186}
187
188#[derive(Clone)]
189struct ClientSuccessSelection<'a> {
190 statuses: Vec<&'a str>,
191 body: ClientSuccessBody<'a>,
192 accept: Option<&'a str>,
193}
194
195enum RequiredBodyConstruction {
196 Default,
197 New(Vec<BodyConstructorParam>),
198 Whole,
199}
200
201struct BodyConstructorParam {
202 preferred_ident: syn::Ident,
203 value_type: TokenStream,
204}
205
206struct BodyModelPlan {
207 body_ident: syn::Ident,
208 body_type: TokenStream,
209 required_construction: RequiredBodyConstruction,
210 optional_fields: Vec<BodyFieldPlan>,
211}
212
213impl CodeGenerator {
214 pub fn generate_http_client_struct(&self) -> TokenStream {
216 let has_retry = self.config().retry_config.is_some();
217 let has_tracing = self.config().tracing_enabled;
218
219 let retry_config_struct = if has_retry {
221 quote! {
222 #[derive(Debug, Clone)]
224 pub struct RetryConfig {
225 pub max_retries: u32,
226 pub initial_delay_ms: u64,
227 pub max_delay_ms: u64,
228 }
229
230 impl Default for RetryConfig {
231 fn default() -> Self {
232 Self {
233 max_retries: 3,
234 initial_delay_ms: 500,
235 max_delay_ms: 16000,
236 }
237 }
238 }
239 }
240 } else {
241 quote! {}
242 };
243
244 let client_struct = quote! {
246 use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
247 use std::collections::BTreeMap;
248
249 pub const DEFAULT_MAX_RESPONSE_BODY_BYTES: usize = 8 * 1024 * 1024;
251
252 #[derive(Clone)]
254 pub struct HttpClient {
255 base_url: String,
256 api_key: Option<String>,
257 http_client: ClientWithMiddleware,
258 custom_headers: BTreeMap<String, String>,
259 max_response_body_bytes: usize,
260 }
261
262 async fn __read_bounded_response_body(
263 mut response: reqwest::Response,
264 limit: usize,
265 ) -> Result<Vec<u8>, HttpError> {
266 let mut body = Vec::new();
267 while let Some(chunk) = response.chunk().await.map_err(HttpError::Network)? {
268 let next_len = body.len().checked_add(chunk.len());
269 if next_len.is_none_or(|next_len| next_len > limit) {
270 return Err(HttpError::ResponseTooLarge { limit });
271 }
272 body.extend_from_slice(&chunk);
273 }
274 Ok(body)
275 }
276 };
277
278 let constructor = self.generate_constructor(has_retry, has_tracing);
280
281 let builder_methods = self.generate_builder_methods();
283
284 let default_impl = quote! {
286 impl Default for HttpClient {
287 fn default() -> Self {
288 Self::new()
289 }
290 }
291 };
292
293 let path_encoder = quote! {
297 fn __pct_encode_path_segment(s: &str) -> String {
298 let mut out = String::with_capacity(s.len());
299 for &b in s.as_bytes() {
300 match b {
301 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
302 out.push(b as char);
303 }
304 _ => {
305 out.push('%');
306 out.push_str(&format!("{:02X}", b));
307 }
308 }
309 }
310 out
311 }
312 };
313
314 quote! {
316 #retry_config_struct
317 #client_struct
318
319 impl HttpClient {
320 #constructor
321 #builder_methods
322 }
323
324 #default_impl
325 #path_encoder
326 }
327 }
328
329 fn generate_constructor(&self, has_retry: bool, has_tracing: bool) -> TokenStream {
331 let configured_base_url = self
338 .config()
339 .http_client_config
340 .as_ref()
341 .and_then(|http| http.base_url.as_deref())
342 .unwrap_or_default();
343 let default_base_url = quote! { #configured_base_url.to_string() };
344 let max_response_body_bytes = self
345 .config()
346 .http_client_config
347 .as_ref()
348 .and_then(|http| http.max_response_body_bytes)
349 .unwrap_or(8 * 1024 * 1024);
350
351 let retry_param = if has_retry {
352 quote! { retry_config: Option<RetryConfig>, }
353 } else {
354 quote! {}
355 };
356
357 let tracing_param = if has_tracing {
358 quote! { enable_tracing: bool, }
359 } else {
360 quote! {}
361 };
362
363 let retry_middleware = if has_retry {
364 quote! {
365 if let Some(config) = retry_config {
366 use reqwest_retry::{RetryTransientMiddleware, policies::ExponentialBackoff};
367
368 let retry_policy = ExponentialBackoff::builder()
369 .retry_bounds(
370 std::time::Duration::from_millis(config.initial_delay_ms),
371 std::time::Duration::from_millis(config.max_delay_ms),
372 )
373 .build_with_max_retries(config.max_retries);
374
375 let retry_middleware = RetryTransientMiddleware::new_with_policy(retry_policy);
376 client_builder = client_builder.with(retry_middleware);
377 }
378 }
379 } else {
380 quote! {}
381 };
382
383 let tracing_middleware = if has_tracing {
384 quote! {
385 if enable_tracing {
386 use reqwest_tracing::TracingMiddleware;
387 client_builder = client_builder.with(TracingMiddleware::default());
388 }
389 }
390 } else {
391 quote! {}
392 };
393
394 let default_constructor = if has_retry && has_tracing {
395 quote! {
396 pub fn new() -> Self {
398 Self::with_config(None, true)
399 }
400 }
401 } else if has_retry {
402 quote! {
403 pub fn new() -> Self {
405 Self::with_config(None)
406 }
407 }
408 } else if has_tracing {
409 quote! {
410 pub fn new() -> Self {
412 Self::with_config(true)
413 }
414 }
415 } else {
416 quote! {
417 pub fn new() -> Self {
419 let reqwest_client = reqwest::Client::new();
420 let client_builder = ClientBuilder::new(reqwest_client);
421 let http_client = client_builder.build();
422
423 Self {
424 base_url: #default_base_url,
425 api_key: None,
426 http_client,
427 custom_headers: BTreeMap::new(),
428 max_response_body_bytes: #max_response_body_bytes,
429 }
430 }
431 }
432 };
433
434 if has_retry || has_tracing {
435 quote! {
436 #default_constructor
437
438 pub fn with_config(#retry_param #tracing_param) -> Self {
440 let reqwest_client = reqwest::Client::new();
441 let mut client_builder = ClientBuilder::new(reqwest_client);
442
443 #tracing_middleware
444 #retry_middleware
445
446 let http_client = client_builder.build();
447
448 Self {
449 base_url: #default_base_url,
450 api_key: None,
451 http_client,
452 custom_headers: BTreeMap::new(),
453 max_response_body_bytes: #max_response_body_bytes,
454 }
455 }
456 }
457 } else {
458 default_constructor
459 }
460 }
461
462 fn generate_builder_methods(&self) -> TokenStream {
464 quote! {
465 pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
467 self.base_url = base_url.into();
468 self
469 }
470
471 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
473 self.api_key = Some(api_key.into());
474 self
475 }
476
477 pub fn with_max_response_body_bytes(mut self, limit: usize) -> Self {
479 self.max_response_body_bytes = limit;
480 self
481 }
482
483 pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
485 self.custom_headers.insert(name.into(), value.into());
486 self
487 }
488
489 pub fn with_headers(mut self, headers: BTreeMap<String, String>) -> Self {
491 self.custom_headers.extend(headers);
492 self
493 }
494 }
495 }
496
497 pub fn generate_operation_methods(&self, analysis: &SchemaAnalysis) -> TokenStream {
506 let operations: Vec<&OperationInfo> = analysis.operations.values().collect();
507 self.generate_operation_methods_for(analysis, &operations)
508 }
509
510 pub(crate) fn generate_operation_methods_for(
514 &self,
515 analysis: &SchemaAnalysis,
516 operations: &[&OperationInfo],
517 ) -> TokenStream {
518 let param_enums = self.generate_param_enum_types(operations);
519
520 let op_error_enums: Vec<TokenStream> = operations
521 .iter()
522 .copied()
523 .filter_map(|op| self.generate_op_error_enum(op))
524 .collect();
525
526 let methods: Vec<TokenStream> = operations
527 .iter()
528 .copied()
529 .map(|op| self.generate_single_operation_method(analysis, op))
530 .collect();
531
532 let (operation_builders, builder_entries) =
533 self.generate_operation_builders(analysis, operations);
534
535 quote! {
536 #param_enums
537
538 #(#op_error_enums)*
539
540 #(#operation_builders)*
541
542 impl HttpClient {
543 #(#methods)*
544 #(#builder_entries)*
545 }
546 }
547 }
548
549 fn generate_operation_builders(
550 &self,
551 analysis: &SchemaAnalysis,
552 operations: &[&OperationInfo],
553 ) -> (Vec<TokenStream>, Vec<TokenStream>) {
554 if !self.config().builders.enabled {
555 return (Vec::new(), Vec::new());
556 }
557
558 let mut used_entry_methods: std::collections::HashSet<String> = operations
559 .iter()
560 .map(|operation| self.get_method_name(operation).to_string())
561 .collect();
562 let mut used_type_names = std::collections::HashSet::new();
563 for schema_name in analysis.schemas.keys() {
564 let rust_name = self.to_rust_type_name(schema_name);
565 used_type_names.insert(rust_name.clone());
566 used_type_names.insert(format!("{rust_name}Builder"));
570 }
571 used_type_names.insert("HttpClient".to_string());
572 used_type_names.insert("ApiOpError".to_string());
573 used_type_names.extend(
574 [
575 "ClientBuilder",
576 "ClientWithMiddleware",
577 "RetryConfig",
578 "HttpError",
579 "BTreeMap",
580 ]
581 .into_iter()
582 .map(str::to_string),
583 );
584 for operation in operations {
585 used_type_names.insert(self.op_error_enum_ident(operation).to_string());
586 used_type_names.extend(
587 operation
588 .parameters
589 .iter()
590 .filter(|parameter| parameter.enum_values.is_some())
591 .map(|parameter| parameter.rust_type.clone()),
592 );
593 }
594
595 let mut definitions = Vec::new();
596 let mut entries = Vec::new();
597 for operation in operations {
598 let allocated_params = self.allocated_operation_params(operation);
599 let body_plan = self.body_model_plan(operation, analysis);
600 let optional_param_count = allocated_params
601 .iter()
602 .filter(|allocated| !Self::builder_param_is_required(allocated.param))
603 .count();
604 let optional_body_count =
605 usize::from(operation.request_body.is_some() && !operation.request_body_required);
606 let body_field_count = body_plan
607 .as_ref()
608 .filter(|plan| {
609 operation.request_body_required
610 || matches!(
611 &plan.required_construction,
612 RequiredBodyConstruction::Default
613 )
614 })
615 .map_or(0, |plan| plan.optional_fields.len());
616 let optional_count = optional_param_count + optional_body_count + body_field_count;
617 if optional_count <= self.config().builders.threshold {
618 continue;
619 }
620
621 let flat_method = self.get_method_name(operation);
622 let entry_base = format!("{flat_method}_builder");
623 let entry_name = Self::allocate_name(&entry_base, &mut used_entry_methods);
624 let entry_ident = Self::to_field_ident(&entry_name);
625
626 let builder_base = format!("{}Builder", flat_method.to_string().to_pascal_case());
627 let builder_name = Self::allocate_type_name(&builder_base, &mut used_type_names);
628 let builder_ident = format_ident!("{builder_name}");
629
630 let (definition, entry) = self.generate_single_operation_builder(
631 analysis,
632 operation,
633 &allocated_params,
634 body_plan,
635 &flat_method,
636 &entry_ident,
637 &builder_ident,
638 );
639 definitions.push(definition);
640 entries.push(entry);
641 }
642
643 (definitions, entries)
644 }
645
646 #[allow(clippy::too_many_arguments)]
647 fn generate_single_operation_builder(
648 &self,
649 analysis: &SchemaAnalysis,
650 operation: &OperationInfo,
651 allocated_params: &[AllocatedOperationParam<'_>],
652 body_plan: Option<BodyModelPlan>,
653 flat_method: &syn::Ident,
654 entry_ident: &syn::Ident,
655 builder_ident: &syn::Ident,
656 ) -> (TokenStream, TokenStream) {
657 let mut fields = vec![quote! { client: &'a HttpClient }];
658 let mut entry_parameters = Vec::new();
659 let mut initializers = vec![quote! { client: self }];
660 let mut call_arguments = Vec::new();
661 let mut setters = Vec::new();
662 let mut used_entry_params = std::collections::HashSet::new();
663 let mut used_methods = std::collections::HashSet::from(["send".to_string()]);
664
665 for allocated in allocated_params {
666 let field_ident = &allocated.ident;
667 let storage_type = self.builder_param_storage_type(allocated.param);
668 if Self::builder_param_is_required(allocated.param) {
669 fields.push(quote! { #field_ident: #storage_type });
670 let entry_name =
671 Self::allocate_name(&field_ident.to_string(), &mut used_entry_params);
672 let entry_param = Self::to_field_ident(&entry_name);
673 if Self::param_has_impl_as_ref_type(allocated.param) {
674 entry_parameters.push(quote! { #entry_param: impl Into<String> });
675 initializers.push(quote! { #field_ident: #entry_param.into() });
676 } else {
677 entry_parameters.push(quote! { #entry_param: #storage_type });
678 initializers.push(quote! { #field_ident: #entry_param });
679 }
680 } else {
681 fields.push(quote! { #field_ident: Option<#storage_type> });
682 initializers.push(quote! { #field_ident: None });
683 let setter_ident =
684 Self::allocate_builder_method(&field_ident.to_string(), &mut used_methods);
685 let wire_name = &allocated.param.name;
686 let assignment = if Self::param_has_impl_as_ref_type(allocated.param) {
687 quote! { self.#field_ident = Some(#field_ident.into()); }
688 } else {
689 quote! { self.#field_ident = Some(#field_ident); }
690 };
691 let setter_type = if Self::param_has_impl_as_ref_type(allocated.param) {
692 quote! { impl Into<String> }
693 } else {
694 storage_type.clone()
695 };
696 setters.push(quote! {
697 #[doc = concat!("Set the optional `", #wire_name, "` operation parameter.")]
698 #[must_use]
699 pub fn #setter_ident(mut self, #field_ident: #setter_type) -> Self {
700 #assignment
701 self
702 }
703 });
704 }
705 call_arguments.push(quote! { self.#field_ident });
706 }
707
708 if let Some(body_plan) = body_plan {
709 let BodyModelPlan {
710 body_ident,
711 body_type,
712 required_construction,
713 optional_fields,
714 } = body_plan;
715 let can_initialize_optional_body =
716 matches!(&required_construction, RequiredBodyConstruction::Default);
717 if operation.request_body_required {
718 fields.push(quote! { #body_ident: #body_type });
719 match required_construction {
720 RequiredBodyConstruction::Default => {
721 initializers.push(quote! { #body_ident: Default::default() });
722 }
723 RequiredBodyConstruction::New(constructor_params) => {
724 let mut constructor_args = Vec::new();
725 for constructor in constructor_params {
726 let preferred = constructor.preferred_ident.to_string();
727 let entry_name =
728 Self::allocate_name(&preferred, &mut used_entry_params);
729 let entry_param = Self::to_field_ident(&entry_name);
730 let value_type = constructor.value_type;
731 entry_parameters.push(quote! { #entry_param: #value_type });
732 constructor_args.push(entry_param);
733 }
734 initializers.push(quote! {
735 #body_ident: #body_type::new(#(#constructor_args),*)
736 });
737 }
738 RequiredBodyConstruction::Whole => {
739 let entry_name =
740 Self::allocate_name(&body_ident.to_string(), &mut used_entry_params);
741 let entry_param = Self::to_field_ident(&entry_name);
742 entry_parameters.push(quote! { #entry_param: #body_type });
743 initializers.push(quote! { #body_ident: #entry_param });
744 }
745 }
746 } else {
747 fields.push(quote! { #body_ident: Option<#body_type> });
748 initializers.push(quote! { #body_ident: None });
749 }
750
751 let body_setter =
752 Self::allocate_builder_method(&body_ident.to_string(), &mut used_methods);
753 let body_assignment = if operation.request_body_required {
754 quote! { self.#body_ident = #body_ident; }
755 } else {
756 quote! { self.#body_ident = Some(#body_ident); }
757 };
758 setters.push(quote! {
759 #[must_use]
761 pub fn #body_setter(mut self, #body_ident: #body_type) -> Self {
762 #body_assignment
763 self
764 }
765 });
766
767 if operation.request_body_required || can_initialize_optional_body {
768 for field in optional_fields {
769 let setter_ident = Self::allocate_builder_method(
770 &field.preferred_method_name,
771 &mut used_methods,
772 );
773 let value_ident = field.value_ident;
774 let value_type = field.value_type;
775 let wire_name = field.wire_name;
776 let access_path = field.access_path;
777 let assignment = if operation.request_body_required {
778 let mut target = quote! { self.#body_ident };
779 for access in &access_path {
780 target = quote! { #target.#access };
781 }
782 quote! { #target = Some(#value_ident); }
783 } else {
784 let mut target = quote! { request };
785 for access in &access_path {
786 target = quote! { #target.#access };
787 }
788 quote! {
789 let request = self.#body_ident.get_or_insert_with(Default::default);
790 #target = Some(#value_ident);
791 }
792 };
793 setters.push(quote! {
794 #[doc = concat!("Set the optional request-body field `", #wire_name, "`.")]
795 #[must_use]
796 pub fn #setter_ident(mut self, #value_ident: #value_type) -> Self {
797 #assignment
798 self
799 }
800 });
801 }
802 }
803 call_arguments.push(quote! { self.#body_ident });
804 }
805
806 let response_type = self.get_response_type(analysis, operation);
807 let error_type = self.op_error_type_token(operation);
808 let operation_id = &operation.operation_id;
809 let definition = quote! {
810 #[doc = concat!("Additive request builder for `", #operation_id, "`.")]
811 #[must_use]
812 pub struct #builder_ident<'a> {
813 #(#fields,)*
814 }
815
816 impl<'a> #builder_ident<'a> {
817 #(#setters)*
818
819 pub async fn send(self) -> Result<#response_type, ApiOpError<#error_type>> {
821 self.client.#flat_method(#(#call_arguments),*).await
822 }
823 }
824 };
825 let entry = quote! {
826 #[doc = concat!("Start an additive builder for `", #operation_id, "`.")]
827 pub fn #entry_ident(
828 &self,
829 #(#entry_parameters),*
830 ) -> #builder_ident<'_> {
831 #builder_ident {
832 #(#initializers,)*
833 }
834 }
835 };
836 (definition, entry)
837 }
838
839 fn allocate_name(base: &str, used: &mut std::collections::HashSet<String>) -> String {
840 let mut candidate = base.to_string();
841 let mut suffix = 2;
842 while !used.insert(candidate.clone()) {
843 candidate = format!("{base}_{suffix}");
844 suffix += 1;
845 }
846 candidate
847 }
848
849 fn allocate_type_name(base: &str, used: &mut std::collections::HashSet<String>) -> String {
850 if used.insert(base.to_string()) {
851 return base.to_string();
852 }
853
854 let mut suffix = 2;
855 loop {
856 let candidate = format!("{base}{suffix}");
857 if used.insert(candidate.clone()) {
858 return candidate;
859 }
860 suffix += 1;
861 }
862 }
863
864 fn allocate_builder_method(
865 preferred: &str,
866 used: &mut std::collections::HashSet<String>,
867 ) -> syn::Ident {
868 let plain = preferred.strip_prefix("r#").unwrap_or(preferred);
869 let base = if used.contains(preferred) {
870 format!("with_{plain}")
871 } else {
872 preferred.to_string()
873 };
874 let allocated = Self::allocate_name(&base, used);
875 Self::to_field_ident(&allocated)
876 }
877
878 fn allocated_operation_params<'a>(
879 &self,
880 operation: &'a OperationInfo,
881 ) -> Vec<AllocatedOperationParam<'a>> {
882 let mut used = std::collections::HashSet::from([
886 "client".to_string(),
887 "request".to_string(),
888 "form".to_string(),
889 "body".to_string(),
890 ]);
891 let mut allocated = Vec::new();
892 for location in ["path", "query", "header", "cookie"] {
893 for parameter in &operation.parameters {
894 if parameter.location != location {
895 continue;
896 }
897 let raw = self.param_ident_str(parameter);
898 let chosen = Self::allocate_name(&raw, &mut used);
899 allocated.push(AllocatedOperationParam {
900 param: parameter,
901 ident: Self::to_field_ident(&chosen),
902 });
903 }
904 }
905 allocated
906 }
907
908 fn builder_param_is_required(parameter: &ParameterInfo) -> bool {
909 parameter.location == "path" || parameter.required
913 }
914
915 fn builder_param_storage_type(&self, parameter: &ParameterInfo) -> TokenStream {
916 self.get_param_owned_rust_type(parameter)
917 }
918
919 fn param_has_impl_as_ref_type(parameter: &ParameterInfo) -> bool {
920 !matches!(
921 ¶meter.query_serialization,
922 Some(
923 crate::analysis::QuerySerialization::FormExplodedArray { .. }
924 | crate::analysis::QuerySerialization::FormArray { .. }
925 | crate::analysis::QuerySerialization::SimpleHeaderArray { .. },
926 )
927 ) && Self::param_uses_as_ref_str(parameter)
928 }
929
930 fn body_model_plan(
931 &self,
932 operation: &OperationInfo,
933 analysis: &SchemaAnalysis,
934 ) -> Option<BodyModelPlan> {
935 use crate::analysis::{ObjectAdditionalProperties, RequestBodyContent, SchemaType};
936
937 let request_body = operation.request_body.as_ref()?;
938 let (body_name, body_ident) = match request_body {
939 RequestBodyContent::Json { schema_name, .. }
940 | RequestBodyContent::FormUrlEncoded { schema_name, .. }
941 | RequestBodyContent::Multipart { schema_name, .. } => {
942 (schema_name.as_str(), format_ident!("request"))
943 }
944 RequestBodyContent::OctetStream { .. }
945 | RequestBodyContent::Binary { .. }
946 | RequestBodyContent::Unsupported { .. } => {
947 return Some(BodyModelPlan {
948 body_ident: format_ident!("body"),
949 body_type: quote! { Vec<u8> },
950 required_construction: RequiredBodyConstruction::Whole,
951 optional_fields: Vec::new(),
952 });
953 }
954 RequestBodyContent::TextPlain { .. } => {
955 return Some(BodyModelPlan {
956 body_ident: format_ident!("body"),
957 body_type: quote! { String },
958 required_construction: RequiredBodyConstruction::Whole,
959 optional_fields: Vec::new(),
960 });
961 }
962 RequestBodyContent::SchemaLess { .. } => return None,
963 };
964 let body_type_name = self.to_rust_type_name(body_name);
965 let body_type = syn::Ident::new(&body_type_name, proc_macro2::Span::call_site());
966 let Some((resolved_name, resolved_schema)) =
967 self.resolve_reference_schema(body_name, analysis)
968 else {
969 return Some(BodyModelPlan {
970 body_ident,
971 body_type: quote! { #body_type },
972 required_construction: RequiredBodyConstruction::Whole,
973 optional_fields: Vec::new(),
974 });
975 };
976
977 let mut optional_fields = Vec::new();
978 let mut stack = std::collections::HashSet::new();
979 self.collect_optional_body_fields(
980 resolved_name,
981 Vec::new(),
982 analysis,
983 &mut stack,
984 &mut optional_fields,
985 );
986
987 let required_construction = match &resolved_schema.schema_type {
988 SchemaType::Object {
989 properties,
990 required,
991 additional_properties,
992 ..
993 } if !self.is_discriminated_variant(resolved_name, analysis) => {
994 let emitted = self.emitted_object_properties(
995 resolved_name,
996 properties,
997 required,
998 additional_properties,
999 analysis,
1000 None,
1001 );
1002 let required_fields: Vec<_> = emitted
1003 .iter()
1004 .filter(|field| field.is_required)
1005 .map(|field| BodyConstructorParam {
1006 preferred_ident: field.ident.clone(),
1007 value_type: field.field_type.clone(),
1008 })
1009 .collect();
1010 if required_fields.is_empty() {
1011 RequiredBodyConstruction::Default
1012 } else if emitted.iter().any(|field| !field.is_required)
1013 || !matches!(additional_properties, ObjectAdditionalProperties::Forbidden)
1014 {
1015 RequiredBodyConstruction::New(required_fields)
1016 } else {
1017 RequiredBodyConstruction::Whole
1018 }
1019 }
1020 _ => RequiredBodyConstruction::Whole,
1021 };
1022
1023 Some(BodyModelPlan {
1024 body_ident,
1025 body_type: quote! { #body_type },
1026 required_construction,
1027 optional_fields,
1028 })
1029 }
1030
1031 fn resolve_reference_schema<'a>(
1032 &self,
1033 schema_name: &'a str,
1034 analysis: &'a SchemaAnalysis,
1035 ) -> Option<(&'a str, &'a crate::analysis::AnalyzedSchema)> {
1036 let mut current = schema_name;
1037 let mut visited = std::collections::HashSet::new();
1038 loop {
1039 if !visited.insert(current) {
1040 return None;
1041 }
1042 let schema = analysis.schemas.get(current)?;
1043 if let crate::analysis::SchemaType::Reference { target } = &schema.schema_type {
1044 current = target;
1045 } else {
1046 return Some((current, schema));
1047 }
1048 }
1049 }
1050
1051 fn collect_optional_body_fields(
1052 &self,
1053 schema_name: &str,
1054 access_path: Vec<syn::Ident>,
1055 analysis: &SchemaAnalysis,
1056 stack: &mut std::collections::HashSet<String>,
1057 output: &mut Vec<BodyFieldPlan>,
1058 ) {
1059 use crate::analysis::SchemaType;
1060 if !stack.insert(schema_name.to_string()) {
1061 return;
1062 }
1063 let Some(schema) = analysis.schemas.get(schema_name) else {
1064 stack.remove(schema_name);
1065 return;
1066 };
1067 match &schema.schema_type {
1068 SchemaType::Reference { target } => {
1069 self.collect_optional_body_fields(target, access_path, analysis, stack, output);
1070 }
1071 SchemaType::Object {
1072 properties,
1073 required,
1074 additional_properties,
1075 ..
1076 } if !self.is_discriminated_variant(schema_name, analysis) => {
1077 for field in self.emitted_object_properties(
1078 schema_name,
1079 properties,
1080 required,
1081 additional_properties,
1082 analysis,
1083 None,
1084 ) {
1085 if field.is_required {
1086 continue;
1087 }
1088 let mut field_path = access_path.clone();
1089 field_path.push(field.ident.clone());
1090 output.push(BodyFieldPlan {
1091 wire_name: field.wire_name.to_string(),
1092 preferred_method_name: field.ident.to_string(),
1093 value_ident: field.ident.clone(),
1094 value_type: self.generate_property_base_type(
1095 schema_name,
1096 field.wire_name,
1097 field.property,
1098 analysis,
1099 ),
1100 access_path: field_path,
1101 });
1102 }
1103 }
1104 SchemaType::Composition { schemas } => {
1105 for (index, schema_ref) in schemas.iter().enumerate() {
1106 let mut nested_path = access_path.clone();
1107 nested_path.push(format_ident!("part_{index}"));
1108 self.collect_optional_body_fields(
1109 &schema_ref.target,
1110 nested_path,
1111 analysis,
1112 stack,
1113 output,
1114 );
1115 }
1116 }
1117 _ => {}
1118 }
1119 stack.remove(schema_name);
1120 }
1121
1122 fn is_discriminated_variant(&self, schema_name: &str, analysis: &SchemaAnalysis) -> bool {
1123 analysis.schemas.values().any(|schema| {
1124 matches!(
1125 &schema.schema_type,
1126 crate::analysis::SchemaType::DiscriminatedUnion { variants, .. }
1127 if variants.iter().any(|variant| variant.type_name == schema_name)
1128 )
1129 })
1130 }
1131
1132 fn generate_param_enum_types(&self, operations: &[&OperationInfo]) -> TokenStream {
1137 let mut by_name: BTreeMap<String, &ParameterInfo> = BTreeMap::new();
1138 for op in operations {
1139 for param in &op.parameters {
1140 if param.enum_values.is_some() {
1141 by_name.entry(param.rust_type.clone()).or_insert(param);
1142 }
1143 }
1144 }
1145
1146 if by_name.is_empty() {
1147 return quote! {};
1148 }
1149
1150 let defs: Vec<TokenStream> = by_name
1151 .values()
1152 .map(|param| self.generate_single_param_enum(param))
1153 .collect();
1154
1155 quote! { #(#defs)* }
1156 }
1157
1158 fn generate_single_param_enum(&self, param: &ParameterInfo) -> TokenStream {
1159 let Some(values) = param.enum_values.as_deref() else {
1160 return quote! {};
1161 };
1162
1163 let enum_ident = format_ident!("{}", param.rust_type);
1164
1165 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
1171 let variant_names: Vec<String> = values
1179 .iter()
1180 .enumerate()
1181 .map(|(index, value)| {
1182 let base = param
1183 .enum_varnames
1184 .as_ref()
1185 .and_then(|names| names.get(index))
1186 .map(|name| self.to_rust_enum_variant(name))
1187 .unwrap_or_else(|| self.to_rust_enum_variant(value));
1188 let mut chosen = base.clone();
1189 let mut suffix = 2;
1190 while !used.insert(chosen.clone()) {
1191 chosen = format!("{base}_{suffix}");
1192 suffix += 1;
1193 }
1194 chosen
1195 })
1196 .collect();
1197
1198 let variants: Vec<TokenStream> = values
1199 .iter()
1200 .zip(&variant_names)
1201 .map(|(value, name)| {
1202 let variant_ident = format_ident!("{}", name);
1203 quote! {
1204 #[serde(rename = #value)]
1205 #variant_ident,
1206 }
1207 })
1208 .collect();
1209
1210 let display_arms: Vec<TokenStream> = values
1211 .iter()
1212 .zip(&variant_names)
1213 .map(|(value, name)| {
1214 let variant_ident = format_ident!("{}", name);
1215 quote! { Self::#variant_ident => #value, }
1216 })
1217 .collect();
1218
1219 let doc = format!(
1220 "Allowed values for the `{}` {} parameter.",
1221 param.name, param.location
1222 );
1223
1224 quote! {
1225 #[doc = #doc]
1226 #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
1227 pub enum #enum_ident {
1228 #(#variants)*
1229 }
1230
1231 impl #enum_ident {
1232 pub fn as_str(&self) -> &'static str {
1233 match self {
1234 #(#display_arms)*
1235 }
1236 }
1237 }
1238
1239 impl std::fmt::Display for #enum_ident {
1240 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1241 f.write_str(self.as_str())
1242 }
1243 }
1244
1245 impl AsRef<str> for #enum_ident {
1246 fn as_ref(&self) -> &str {
1247 self.as_str()
1248 }
1249 }
1250 }
1251 }
1252
1253 fn generate_op_error_enum(&self, op: &OperationInfo) -> Option<TokenStream> {
1258 let variants: Vec<(String, String)> = op
1259 .response_schemas
1260 .iter()
1261 .filter(|(code, _)| !code.starts_with('2'))
1262 .map(|(code, schema)| (code.clone(), schema.clone()))
1263 .collect();
1264
1265 if variants.is_empty() {
1266 return None;
1267 }
1268
1269 let enum_ident = self.op_error_enum_ident(op);
1270 let variant_decls: Vec<TokenStream> = variants
1271 .iter()
1272 .map(|(code, schema)| {
1273 let variant_ident = Self::op_error_variant_ident(code);
1274 let payload_ty_name = self.to_rust_type_name(schema);
1275 let payload_ty = syn::Ident::new(&payload_ty_name, proc_macro2::Span::call_site());
1276 quote! { #variant_ident(#payload_ty) }
1277 })
1278 .collect();
1279
1280 let doc = format!(
1281 "Typed error responses for `{}`. One variant per declared non-2xx response.",
1282 op.operation_id
1283 );
1284
1285 Some(quote! {
1286 #[doc = #doc]
1287 #[derive(Debug, Clone)]
1288 pub enum #enum_ident {
1289 #(#variant_decls,)*
1290 }
1291 })
1292 }
1293
1294 fn op_error_enum_ident(&self, op: &OperationInfo) -> syn::Ident {
1296 use heck::ToPascalCase;
1297 let name = format!(
1298 "{}ApiError",
1299 op.operation_id.replace('.', "_").to_pascal_case()
1300 );
1301 syn::Ident::new(&name, proc_macro2::Span::call_site())
1302 }
1303
1304 fn op_error_variant_ident(status_code: &str) -> syn::Ident {
1307 let raw = match status_code {
1308 "default" | "Default" => "Default".to_string(),
1309 other if other.chars().all(|c| c.is_ascii_digit()) => format!("Status{other}"),
1310 other => format!("Status{}", other.to_ascii_lowercase()),
1311 };
1312 syn::Ident::new(&raw, proc_macro2::Span::call_site())
1313 }
1314
1315 fn op_error_type_token(&self, op: &OperationInfo) -> TokenStream {
1319 if op
1320 .response_schemas
1321 .iter()
1322 .any(|(code, _)| !code.starts_with('2'))
1323 {
1324 let ident = self.op_error_enum_ident(op);
1325 quote! { #ident }
1326 } else {
1327 quote! { serde_json::Value }
1328 }
1329 }
1330
1331 fn generate_single_operation_method(
1333 &self,
1334 analysis: &SchemaAnalysis,
1335 op: &OperationInfo,
1336 ) -> TokenStream {
1337 let method_name = self.get_method_name(op);
1338 let http_method_call = self.http_method_call(op);
1339 let path = &op.path;
1340 let request_param = self.generate_request_param(op);
1341 let request_body = self.generate_request_body(op, analysis);
1342 let query_params = self.generate_query_params(op);
1343 let header_params = self.generate_header_params(op);
1344 let cookie_params = self.generate_cookie_params(op);
1345 let auth_application = self.generate_auth_application();
1346 let success = self.get_success_response(analysis, op);
1347 let response_type = self.get_response_type(analysis, op);
1348 let op_error_type = self.op_error_type_token(op);
1349 let accept = success.accept;
1350 let error_handling = self.generate_error_handling(op, success);
1351 let (custom_headers, accept_header) = if let Some(media_type) = accept {
1352 (
1353 quote! {
1354 for (name, value) in &self.custom_headers {
1355 if !name.eq_ignore_ascii_case("accept") {
1356 req = req.header(name, value);
1357 }
1358 }
1359 },
1360 quote! {
1361 req = req.header(reqwest::header::ACCEPT, #media_type);
1362 },
1363 )
1364 } else {
1365 (
1366 quote! {
1367 for (name, value) in &self.custom_headers {
1368 req = req.header(name, value);
1369 }
1370 },
1371 TokenStream::new(),
1372 )
1373 };
1374 let url_construction = self.generate_url_construction(path, op);
1375 let doc_comment = self.generate_operation_doc_comment(op);
1376
1377 quote! {
1378 #doc_comment
1379 pub async fn #method_name(
1380 &self,
1381 #request_param
1382 ) -> Result<#response_type, ApiOpError<#op_error_type>> {
1383 #url_construction
1384
1385 let mut req = #http_method_call;
1386 #request_body
1387
1388 #query_params
1389 #header_params
1390 #cookie_params
1391
1392 #auth_application
1395
1396 #custom_headers
1398
1399 #accept_header
1402
1403 let response = req.send().await?;
1404 #error_handling
1405 }
1406 }
1407 }
1408
1409 fn generate_auth_application(&self) -> TokenStream {
1413 use crate::http_config::AuthConfig;
1414 match &self.config().auth_config {
1415 Some(AuthConfig::Bearer { header_name }) if header_name == "Authorization" => quote! {
1416 if let Some(api_key) = &self.api_key {
1417 req = req.bearer_auth(api_key);
1418 }
1419 },
1420 Some(AuthConfig::Bearer { header_name }) => {
1421 let h = header_name.clone();
1422 quote! {
1423 if let Some(api_key) = &self.api_key {
1424 req = req.header(#h, format!("Bearer {}", api_key));
1425 }
1426 }
1427 }
1428 Some(AuthConfig::ApiKey { header_name }) => {
1429 let h = header_name.clone();
1430 quote! {
1431 if let Some(api_key) = &self.api_key {
1432 req = req.header(#h, api_key.as_str());
1433 }
1434 }
1435 }
1436 Some(AuthConfig::Custom {
1437 header_name,
1438 header_value_prefix,
1439 }) => {
1440 let h = header_name.clone();
1441 let prefix = header_value_prefix.clone().unwrap_or_default();
1442 if prefix.is_empty() {
1443 quote! {
1444 if let Some(api_key) = &self.api_key {
1445 req = req.header(#h, api_key.as_str());
1446 }
1447 }
1448 } else {
1449 let format_str = format!("{}{{}}", prefix);
1450 quote! {
1451 if let Some(api_key) = &self.api_key {
1452 req = req.header(#h, format!(#format_str, api_key));
1453 }
1454 }
1455 }
1456 }
1457 None => quote! {
1458 if let Some(api_key) = &self.api_key {
1459 req = req.bearer_auth(api_key);
1460 }
1461 },
1462 }
1463 }
1464
1465 fn generate_header_params(&self, op: &OperationInfo) -> TokenStream {
1469 let header_params: Vec<_> = op
1470 .parameters
1471 .iter()
1472 .filter(|p| p.location == "header")
1473 .collect();
1474 if header_params.is_empty() {
1475 return quote! {};
1476 }
1477 let mut emit = Vec::new();
1478 for param in header_params {
1479 let param_name_snake = self.param_ident_str(param);
1480 let param_ident = Self::to_field_ident(¶m_name_snake);
1481 let header_name = ¶m.name;
1482 if matches!(
1483 param.query_serialization,
1484 Some(crate::analysis::QuerySerialization::SimpleHeaderArray { .. })
1485 ) {
1486 let encode = quote! {
1487 v.iter().map(::std::string::ToString::to_string).collect::<Vec<_>>().join(",")
1488 };
1489 if param.required {
1490 emit.push(quote! {
1491 let v = #param_ident;
1492 req = req.header(#header_name, #encode);
1493 });
1494 } else {
1495 emit.push(quote! {
1496 if let Some(v) = #param_ident {
1497 req = req.header(#header_name, #encode);
1498 }
1499 });
1500 }
1501 continue;
1502 }
1503 if param.required {
1504 if Self::param_uses_as_ref_str(param) {
1505 emit.push(quote! {
1506 req = req.header(#header_name, #param_ident.as_ref());
1507 });
1508 } else {
1509 emit.push(quote! {
1510 req = req.header(#header_name, #param_ident.to_string());
1511 });
1512 }
1513 } else if Self::param_uses_as_ref_str(param) {
1514 emit.push(quote! {
1515 if let Some(v) = #param_ident {
1516 req = req.header(#header_name, v.as_ref());
1517 }
1518 });
1519 } else {
1520 emit.push(quote! {
1521 if let Some(v) = #param_ident {
1522 req = req.header(#header_name, v.to_string());
1523 }
1524 });
1525 }
1526 }
1527 quote! {
1528 #(#emit)*
1529 }
1530 }
1531
1532 fn generate_cookie_params(&self, op: &OperationInfo) -> TokenStream {
1533 let cookie_params: Vec<_> = op
1534 .parameters
1535 .iter()
1536 .filter(|parameter| parameter.location == "cookie")
1537 .collect();
1538 if cookie_params.is_empty() {
1539 return quote! {};
1540 }
1541 let mut emit = Vec::new();
1542 for parameter in cookie_params {
1543 let ident = Self::to_field_ident(&self.param_ident_str(parameter));
1544 let wire_name = parameter.name.as_str();
1545 if parameter.required {
1546 emit.push(quote! {
1547 __cookie_fields.push(format!("{}={}", #wire_name, #ident));
1548 });
1549 } else {
1550 emit.push(quote! {
1551 if let Some(value) = #ident {
1552 __cookie_fields.push(format!("{}={}", #wire_name, value));
1553 }
1554 });
1555 }
1556 }
1557 quote! {
1558 let mut __cookie_fields = Vec::new();
1559 #(#emit)*
1560 if !__cookie_fields.is_empty() {
1561 req = req.header(::reqwest::header::COOKIE, __cookie_fields.join("; "));
1562 }
1563 }
1564 }
1565
1566 fn generate_query_params(&self, op: &OperationInfo) -> TokenStream {
1568 let query_params: Vec<_> = op
1569 .parameters
1570 .iter()
1571 .filter(|p| p.location == "query")
1572 .collect();
1573
1574 if query_params.is_empty() {
1575 return quote! {};
1576 }
1577
1578 let mut param_building = Vec::new();
1579 let mut req_appends = Vec::new();
1583
1584 for param in query_params {
1585 use crate::analysis::QuerySerialization;
1586
1587 let param_name_snake = self.param_ident_str(param);
1589 let param_name = Self::to_field_ident(¶m_name_snake);
1590
1591 let param_key = ¶m.name;
1593
1594 match ¶m.query_serialization {
1595 Some(QuerySerialization::FormExplodedNestedObject { properties }) => {
1596 let emit_properties = properties.iter().map(|property| {
1597 let wire_name = property.wire_name.as_str();
1598 let field_ident = CodeGenerator::to_field_ident(
1599 &self.to_rust_field_name(wire_name),
1600 );
1601 match &property.value_type {
1602 crate::analysis::QueryStructPropertyType::Scalar(_) => quote! {
1603 let value = serde_json::to_value(&v.#field_ident)
1604 .map_err(HttpError::serialization_error)?;
1605 if !value.is_null() {
1606 let value = match value {
1607 serde_json::Value::String(value) => value,
1608 serde_json::Value::Bool(value) => value.to_string(),
1609 serde_json::Value::Number(value) => value.to_string(),
1610 _ => return Err(HttpError::serialization_error(
1611 format!("query field `{}` did not serialize as a scalar", #wire_name)
1612 ).into()),
1613 };
1614 nested_params.push((format!("{}.{}", #param_key, #wire_name), value));
1615 }
1616 },
1617 crate::analysis::QueryStructPropertyType::Object { properties } => {
1618 let leaves = properties.iter().map(|property| property.wire_name.clone()).collect::<Vec<_>>();
1619 quote! {
1620 let value = serde_json::to_value(&v.#field_ident)
1621 .map_err(HttpError::serialization_error)?;
1622 if !value.is_null() {
1623 let serde_json::Value::Object(object) = value else {
1624 return Err(HttpError::serialization_error(format!("query field `{}` did not serialize as an object", #wire_name)).into());
1625 };
1626 if object.is_empty() {
1627 nested_params.push((format!("{}.{}[]", #param_key, #wire_name), String::new()));
1628 } else {
1629 for leaf in [#(#leaves),*] {
1630 let Some(value) = object.get(leaf) else { continue };
1631 if value.is_null() { continue; }
1632 let value = match value {
1633 serde_json::Value::String(value) => value.clone(),
1634 serde_json::Value::Bool(value) => value.to_string(),
1635 serde_json::Value::Number(value) => value.to_string(),
1636 _ => return Err(HttpError::serialization_error(format!("query field `{}` contained a non-scalar leaf", #wire_name)).into()),
1637 };
1638 nested_params.push((format!("{}.{}.{}", #param_key, #wire_name, leaf), value));
1639 }
1640 }
1641 }
1642 }
1643 }
1644 crate::analysis::QueryStructPropertyType::Array { item_type } => {
1645 let nested_properties = match item_type {
1646 crate::analysis::ArrayItemType::FlatStructRef { properties, .. } => Some(
1647 properties.iter().map(|property| property.wire_name.clone()).collect::<Vec<_>>()
1648 ),
1649 _ => None,
1650 };
1651 if let Some(nested_properties) = nested_properties {
1652 quote! {
1653 let values = serde_json::to_value(&v.#field_ident)
1654 .map_err(HttpError::serialization_error)?;
1655 if !values.is_null() {
1656 let serde_json::Value::Array(values) = values else {
1657 return Err(HttpError::serialization_error(
1658 format!("query field `{}` did not serialize as an array", #wire_name)
1659 ).into());
1660 };
1661 if values.is_empty() {
1662 nested_params.push((format!("{}.{}[]", #param_key, #wire_name), String::new()));
1663 }
1664 for (index, value) in values.into_iter().enumerate() {
1665 let serde_json::Value::Object(object) = value else {
1666 return Err(HttpError::serialization_error(
1667 format!("query field `{}` contained a non-object item", #wire_name)
1668 ).into());
1669 };
1670 for leaf in [#(#nested_properties),*] {
1671 let Some(value) = object.get(leaf) else { continue };
1672 if value.is_null() { continue; }
1673 let value = match value {
1674 serde_json::Value::String(value) => value.clone(),
1675 serde_json::Value::Bool(value) => value.to_string(),
1676 serde_json::Value::Number(value) => value.to_string(),
1677 _ => return Err(HttpError::serialization_error(
1678 format!("query field `{}` contained a non-scalar leaf", #wire_name)
1679 ).into()),
1680 };
1681 nested_params.push((format!("{}.{}.{}.{}", #param_key, #wire_name, index + 1, leaf), value));
1682 }
1683 }
1684 }
1685 }
1686 } else {
1687 quote! {
1688 let values = serde_json::to_value(&v.#field_ident)
1689 .map_err(HttpError::serialization_error)?;
1690 if !values.is_null() {
1691 let serde_json::Value::Array(values) = values else {
1692 return Err(HttpError::serialization_error(
1693 format!("query field `{}` did not serialize as an array", #wire_name)
1694 ).into());
1695 };
1696 if values.is_empty() {
1697 nested_params.push((format!("{}.{}[]", #param_key, #wire_name), String::new()));
1698 }
1699 for (index, value) in values.into_iter().enumerate() {
1700 let value = match value {
1701 serde_json::Value::String(value) => value,
1702 serde_json::Value::Bool(value) => value.to_string(),
1703 serde_json::Value::Number(value) => value.to_string(),
1704 _ => return Err(HttpError::serialization_error(
1705 format!("query field `{}` contained a non-scalar item", #wire_name)
1706 ).into()),
1707 };
1708 nested_params.push((format!("{}.{}.{}", #param_key, #wire_name, index + 1), value));
1709 }
1710 }
1711 }
1712 }
1713 }
1714 }
1715 }).collect::<Vec<_>>();
1716 let apply = quote! {
1717 let mut nested_params: Vec<(String, String)> = Vec::new();
1718 #(#emit_properties)*
1719 if nested_params.is_empty() {
1720 nested_params.push((format!("{}[]", #param_key), String::new()));
1721 }
1722 req = req.query(&nested_params);
1723 };
1724 if param.required {
1725 req_appends.push(quote! {{ let v = #param_name; #apply }});
1726 } else {
1727 req_appends.push(quote! { if let Some(v) = #param_name { #apply } });
1728 }
1729 continue;
1730 }
1731 Some(QuerySerialization::FormExplodedObject) => {
1732 let apply = quote! {
1739 let __empty = match serde_json::to_value(&v)
1740 .map_err(HttpError::serialization_error)?
1741 {
1742 serde_json::Value::Object(map) => map.is_empty(),
1743 _ => false,
1744 };
1745 if __empty {
1746 req = req.query(&[(format!("{}[]", #param_key), String::new())]);
1747 } else {
1748 req = req.query(&v);
1749 }
1750 };
1751 if param.required {
1752 req_appends.push(quote! {
1753 {
1754 let v = #param_name;
1755 #apply
1756 }
1757 });
1758 } else {
1759 req_appends.push(quote! {
1760 if let Some(v) = #param_name {
1761 #apply
1762 }
1763 });
1764 }
1765 continue;
1766 }
1767 Some(QuerySerialization::DeepObject) => {
1768 let apply = quote! {
1772 let map = match serde_json::to_value(&v)
1773 .map_err(HttpError::serialization_error)?
1774 {
1775 serde_json::Value::Object(map) => map,
1776 _ => return Err(HttpError::serialization_error(
1777 format!("query parameter `{}` did not serialize as an object", #param_key)
1778 ).into()),
1779 };
1780 let mut deep_params: Vec<(String, String)> = Vec::new();
1781 for (k, val) in map {
1782 let s = match val {
1783 serde_json::Value::Null => continue,
1784 serde_json::Value::String(s) => s,
1785 other => other.to_string(),
1786 };
1787 deep_params.push((format!("{}[{}]", #param_key, k), s));
1788 }
1789 if deep_params.is_empty() {
1790 deep_params.push((format!("{}[]", #param_key), String::new()));
1791 }
1792 req = req.query(&deep_params);
1793 };
1794 if param.required {
1795 req_appends.push(quote! {
1796 {
1797 let v = #param_name;
1798 #apply
1799 }
1800 });
1801 } else {
1802 req_appends.push(quote! {
1803 if let Some(v) = #param_name {
1804 #apply
1805 }
1806 });
1807 }
1808 continue;
1809 }
1810 Some(QuerySerialization::FormObject) => {
1811 let apply = quote! {
1815 let map = match serde_json::to_value(&v)
1816 .map_err(HttpError::serialization_error)?
1817 {
1818 serde_json::Value::Object(map) => map,
1819 _ => return Err(HttpError::serialization_error(
1820 format!("query parameter `{}` did not serialize as an object", #param_key)
1821 ).into()),
1822 };
1823 let mut parts: Vec<String> = Vec::new();
1824 for (k, val) in map {
1825 let s = match val {
1826 serde_json::Value::Null => continue,
1827 serde_json::Value::String(s) => s,
1828 other => other.to_string(),
1829 };
1830 if k.contains(',') || s.contains(',') {
1831 return Err(HttpError::serialization_error(
1832 format!(
1833 "query object `{}` contains a comma in key `{}`; use explode=true for lossless string values",
1834 #param_key,
1835 k,
1836 )
1837 ).into());
1838 }
1839 parts.push(k);
1840 parts.push(s);
1841 }
1842 if parts.is_empty() {
1843 query_params.push((
1844 format!("{}[]", #param_key),
1845 String::new(),
1846 ));
1847 } else {
1848 query_params.push((#param_key.to_string(), parts.join(",")));
1849 }
1850 };
1851 if param.required {
1852 param_building.push(quote! {
1853 {
1854 let v = #param_name;
1855 #apply
1856 }
1857 });
1858 } else {
1859 param_building.push(quote! {
1860 if let Some(v) = #param_name {
1861 #apply
1862 }
1863 });
1864 }
1865 continue;
1866 }
1867 Some(QuerySerialization::FormExplodedArray { item_type }) => {
1868 let struct_properties = match item_type {
1871 crate::analysis::ArrayItemType::FlatStructRef { properties, .. }
1872 | crate::analysis::ArrayItemType::NestedStructRef { properties, .. } => {
1873 Some(properties.clone())
1874 }
1875 _ => None,
1876 };
1877 let emit_items = if let Some(properties) = struct_properties {
1878 let pushes = properties
1879 .iter()
1880 .map(|property| {
1881 let wire_name = &property.wire_name;
1882 let field_ident = CodeGenerator::to_field_ident(
1886 &self.to_rust_field_name(wire_name),
1887 );
1888 match &property.value_type {
1889 crate::analysis::QueryStructPropertyType::Scalar(_) => quote! {
1890 let value = serde_json::to_value(&item.#field_ident)
1891 .map_err(HttpError::serialization_error)?;
1892 if !value.is_null() {
1893 let value = match value {
1894 serde_json::Value::String(value) => value,
1895 serde_json::Value::Bool(value) => value.to_string(),
1896 serde_json::Value::Number(value) => value.to_string(),
1897 _ => return Err(HttpError::serialization_error(
1898 format!("query field `{}.{}` did not serialize as a scalar", #param_key, #wire_name)
1899 ).into()),
1900 };
1901 query_params.push((
1902 format!("{}.{}.{}", #param_key, index, #wire_name),
1903 value,
1904 ));
1905 }
1906 },
1907 crate::analysis::QueryStructPropertyType::Object { properties } => {
1908 let leaves = properties.iter().map(|property| property.wire_name.clone()).collect::<Vec<_>>();
1909 quote! {
1910 let value = serde_json::to_value(&item.#field_ident)
1911 .map_err(HttpError::serialization_error)?;
1912 if !value.is_null() {
1913 let serde_json::Value::Object(object) = value else {
1914 return Err(HttpError::serialization_error(format!("query field `{}.{}` did not serialize as an object", #param_key, #wire_name)).into());
1915 };
1916 if object.is_empty() {
1917 query_params.push((format!("{}.{}.{}[]", #param_key, index, #wire_name), String::new()));
1918 }
1919 for leaf in [#(#leaves),*] {
1920 let Some(value) = object.get(leaf) else { continue };
1921 if value.is_null() { continue; }
1922 let value = match value {
1923 serde_json::Value::String(value) => value.clone(),
1924 serde_json::Value::Bool(value) => value.to_string(),
1925 serde_json::Value::Number(value) => value.to_string(),
1926 _ => return Err(HttpError::serialization_error(format!("query field `{}.{}` contained a non-scalar leaf", #param_key, #wire_name)).into()),
1927 };
1928 query_params.push((format!("{}.{}.{}.{}", #param_key, index, #wire_name, leaf), value));
1929 }
1930 }
1931 }
1932 }
1933 crate::analysis::QueryStructPropertyType::Array { item_type } => {
1934 let nested_properties = match item_type {
1935 crate::analysis::ArrayItemType::FlatStructRef { properties, .. } => {
1936 Some(properties.iter().map(|property| property.wire_name.clone()).collect::<Vec<_>>())
1937 }
1938 _ => None,
1939 };
1940 if let Some(nested_properties) = nested_properties {
1941 quote! {
1942 let values = serde_json::to_value(&item.#field_ident)
1943 .map_err(HttpError::serialization_error)?;
1944 if !values.is_null() {
1945 let serde_json::Value::Array(values) = values else {
1946 return Err(HttpError::serialization_error(
1947 format!("query field `{}.{}` did not serialize as an array", #param_key, #wire_name)
1948 ).into());
1949 };
1950 if values.is_empty() {
1951 query_params.push((format!("{}.{}.{}[]", #param_key, index, #wire_name), String::new()));
1952 }
1953 for (nested_index, value) in values.into_iter().enumerate() {
1954 let serde_json::Value::Object(object) = value else {
1955 return Err(HttpError::serialization_error(
1956 format!("query field `{}.{}` contained a non-object item", #param_key, #wire_name)
1957 ).into());
1958 };
1959 if object.is_empty() {
1960 query_params.push((format!("{}.{}.{}.{}[]", #param_key, index, #wire_name, nested_index + 1), String::new()));
1961 }
1962 for nested_wire_name in [#(#nested_properties),*] {
1963 let Some(value) = object.get(nested_wire_name) else { continue };
1964 if value.is_null() { continue; }
1965 let value = match value {
1966 serde_json::Value::String(value) => value.clone(),
1967 serde_json::Value::Bool(value) => value.to_string(),
1968 serde_json::Value::Number(value) => value.to_string(),
1969 _ => return Err(HttpError::serialization_error(
1970 format!("query field `{}.{}` contained a non-scalar leaf", #param_key, #wire_name)
1971 ).into()),
1972 };
1973 query_params.push((
1974 format!("{}.{}.{}.{}.{}", #param_key, index, #wire_name, nested_index + 1, nested_wire_name),
1975 value,
1976 ));
1977 }
1978 }
1979 }
1980 }
1981 } else {
1982 quote! {
1983 let values = serde_json::to_value(&item.#field_ident)
1984 .map_err(HttpError::serialization_error)?;
1985 if !values.is_null() {
1986 let serde_json::Value::Array(values) = values else {
1987 return Err(HttpError::serialization_error(
1988 format!("query field `{}.{}` did not serialize as an array", #param_key, #wire_name)
1989 ).into());
1990 };
1991 if values.is_empty() {
1992 query_params.push((format!("{}.{}.{}[]", #param_key, index, #wire_name), String::new()));
1993 }
1994 for (nested_index, value) in values.into_iter().enumerate() {
1995 let value = match value {
1996 serde_json::Value::String(value) => value,
1997 serde_json::Value::Bool(value) => value.to_string(),
1998 serde_json::Value::Number(value) => value.to_string(),
1999 _ => return Err(HttpError::serialization_error(
2000 format!("query field `{}.{}` contained a non-scalar item", #param_key, #wire_name)
2001 ).into()),
2002 };
2003 query_params.push((
2004 format!("{}.{}.{}.{}", #param_key, index, #wire_name, nested_index + 1),
2005 value,
2006 ));
2007 }
2008 }
2009 }
2010 }
2011 }
2012 }
2013 })
2014 .collect::<Vec<_>>();
2015 quote! {
2016 for (index, item) in v.iter().enumerate() {
2017 let index = index + 1;
2018 let item_start_len = query_params.len();
2019 #(#pushes)*
2020 if query_params.len() == item_start_len {
2021 query_params.push((format!("{}.{}[]", #param_key, index), String::new()));
2022 }
2023 }
2024 }
2025 } else {
2026 quote! {
2027 for item in v {
2028 query_params.push((#param_key.to_string(), item.to_string()));
2029 }
2030 }
2031 };
2032 if param.required {
2033 param_building.push(quote! {
2034 let v = #param_name;
2035 if v.is_empty() {
2036 query_params.push((
2037 format!("{}[]", #param_key),
2038 String::new(),
2039 ));
2040 } else {
2041 #emit_items
2042 }
2043 });
2044 } else {
2045 param_building.push(quote! {
2046 if let Some(v) = #param_name {
2047 if v.is_empty() {
2048 query_params.push((
2049 format!("{}[]", #param_key),
2050 String::new(),
2051 ));
2052 } else {
2053 #emit_items
2054 }
2055 }
2056 });
2057 }
2058 continue;
2059 }
2060 Some(QuerySerialization::FormArray { .. }) => {
2061 let apply = quote! {
2064 if v.is_empty() {
2065 query_params.push((
2066 format!("{}[]", #param_key),
2067 String::new(),
2068 ));
2069 } else {
2070 let mut parts = Vec::with_capacity(v.len());
2071 for item in &v {
2072 let item = item.to_string();
2073 if item.contains(',') {
2074 return Err(HttpError::serialization_error(
2075 format!(
2076 "query array `{}` contains a comma; use explode=true for lossless string values",
2077 #param_key,
2078 )
2079 ).into());
2080 }
2081 parts.push(item);
2082 }
2083 query_params.push((
2084 #param_key.to_string(),
2085 parts.join(","),
2086 ));
2087 }
2088 };
2089 if param.required {
2090 param_building.push(quote! {
2091 {
2092 let v = #param_name;
2093 #apply
2094 }
2095 });
2096 } else {
2097 param_building.push(quote! {
2098 if let Some(v) = #param_name {
2099 #apply
2100 }
2101 });
2102 }
2103 continue;
2104 }
2105 Some(
2106 QuerySerialization::Unsupported { .. }
2107 | QuerySerialization::SimpleHeaderArray { .. },
2108 ) => {}
2109 None => {}
2110 }
2111
2112 if param.required {
2113 if Self::param_uses_as_ref_str(param) {
2115 param_building.push(quote! {
2116 query_params.push((#param_key.to_string(), #param_name.as_ref().to_string()));
2117 });
2118 } else {
2119 param_building.push(quote! {
2120 query_params.push((#param_key.to_string(), #param_name.to_string()));
2121 });
2122 }
2123 } else {
2124 if Self::param_uses_as_ref_str(param) {
2126 param_building.push(quote! {
2127 if let Some(v) = #param_name {
2128 query_params.push((#param_key.to_string(), v.as_ref().to_string()));
2129 }
2130 });
2131 } else {
2132 param_building.push(quote! {
2133 if let Some(v) = #param_name {
2134 query_params.push((#param_key.to_string(), v.to_string()));
2135 }
2136 });
2137 }
2138 }
2139 }
2140
2141 let pairs_block = if param_building.is_empty() {
2144 quote! {}
2145 } else {
2146 quote! {
2147 {
2148 let mut query_params: Vec<(String, String)> = Vec::new();
2149 #(#param_building)*
2150 if !query_params.is_empty() {
2151 req = req.query(&query_params);
2152 }
2153 }
2154 }
2155 };
2156
2157 quote! {
2158 #pairs_block
2160 #(#req_appends)*
2161 }
2162 }
2163
2164 fn generate_operation_doc_comment(&self, op: &OperationInfo) -> TokenStream {
2168 let method = op.method.to_uppercase();
2169 let path = &op.path;
2170 let mut docs: Vec<String> = Vec::new();
2171 if let Some(s) = &op.summary {
2172 if !s.is_empty() {
2173 docs.push(s.clone());
2174 docs.push(String::new());
2175 }
2176 }
2177 if let Some(d) = &op.description {
2178 if !d.is_empty() {
2179 for line in d.lines() {
2180 docs.push(line.to_string());
2181 }
2182 docs.push(String::new());
2183 }
2184 }
2185 docs.push(format!("`{} {}`", method, path));
2186 let doc_attrs: Vec<TokenStream> = docs
2187 .iter()
2188 .map(|line| {
2189 let prefixed = if line.is_empty() {
2190 String::new()
2191 } else {
2192 format!(" {line}")
2193 };
2194 quote! { #[doc = #prefixed] }
2195 })
2196 .collect();
2197 quote! { #(#doc_attrs)* }
2198 }
2199
2200 fn get_method_name(&self, op: &OperationInfo) -> syn::Ident {
2202 let name = if !op.operation_id.is_empty() {
2203 op.operation_id.to_snake_case()
2204 } else {
2205 format!(
2207 "{}_{}",
2208 op.method,
2209 op.path.replace('/', "_").replace(['{', '}'], "")
2210 )
2211 .to_snake_case()
2212 };
2213
2214 syn::Ident::new(&name, proc_macro2::Span::call_site())
2215 }
2216
2217 fn http_method_call(&self, op: &OperationInfo) -> TokenStream {
2222 match op.method.to_uppercase().as_str() {
2223 "GET" => quote! { self.http_client.get(request_url) },
2224 "POST" => quote! { self.http_client.post(request_url) },
2225 "PUT" => quote! { self.http_client.put(request_url) },
2226 "DELETE" => quote! { self.http_client.delete(request_url) },
2227 "PATCH" => quote! { self.http_client.patch(request_url) },
2228 "HEAD" => quote! { self.http_client.head(request_url) },
2229 "OPTIONS" => quote! {
2230 self.http_client.request(reqwest::Method::OPTIONS, request_url)
2231 },
2232 "TRACE" => quote! {
2233 self.http_client.request(reqwest::Method::TRACE, request_url)
2234 },
2235 other => {
2240 let upper = other.to_string();
2241 quote! {
2242 self.http_client.request(
2243 reqwest::Method::from_bytes(#upper.as_bytes())
2244 .expect("invalid HTTP method"),
2245 request_url,
2246 )
2247 }
2248 }
2249 }
2250 }
2251
2252 fn generate_request_param(&self, op: &OperationInfo) -> TokenStream {
2254 let mut params = Vec::new();
2255 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
2262 let mut unique_param_ident = |raw: String| -> syn::Ident {
2263 let mut chosen = raw.clone();
2264 let mut suffix = 2;
2265 while !used.insert(chosen.clone()) {
2266 chosen = format!("{raw}_{suffix}");
2267 suffix += 1;
2268 }
2269 Self::to_field_ident(&chosen)
2270 };
2271
2272 for param in &op.parameters {
2274 if param.location == "path" {
2275 let param_name_snake = self.param_ident_str(param);
2276 let param_name = unique_param_ident(param_name_snake);
2277 let param_type = self.get_param_rust_type(param);
2278 params.push(quote! { #param_name: #param_type });
2279 }
2280 }
2281
2282 for param in &op.parameters {
2284 if param.location == "query" {
2285 let param_name_snake = self.param_ident_str(param);
2286 let param_name = unique_param_ident(param_name_snake);
2287 let param_type = self.get_param_rust_type(param);
2288
2289 if param.required {
2291 params.push(quote! { #param_name: #param_type });
2292 } else {
2293 params.push(quote! { #param_name: Option<#param_type> });
2294 }
2295 }
2296 }
2297
2298 for param in &op.parameters {
2304 if param.location == "header" {
2305 let param_name_snake = self.param_ident_str(param);
2306 let param_name = unique_param_ident(param_name_snake);
2307 let param_type = self.get_param_rust_type(param);
2308 if param.required {
2309 params.push(quote! { #param_name: #param_type });
2310 } else {
2311 params.push(quote! { #param_name: Option<#param_type> });
2312 }
2313 }
2314 }
2315
2316 for param in &op.parameters {
2317 if param.location == "cookie" {
2318 let param_name_snake = self.param_ident_str(param);
2319 let param_name = unique_param_ident(param_name_snake);
2320 let param_type = self.get_param_rust_type(param);
2321 if param.required {
2322 params.push(quote! { #param_name: #param_type });
2323 } else {
2324 params.push(quote! { #param_name: Option<#param_type> });
2325 }
2326 }
2327 }
2328
2329 if let Some(ref rb) = op.request_body {
2332 use crate::analysis::RequestBodyContent;
2333 if matches!(rb, RequestBodyContent::SchemaLess { .. }) {
2334 return if params.is_empty() {
2335 quote! {}
2336 } else {
2337 quote! { #(#params),* }
2338 };
2339 }
2340 let required = op.request_body_required;
2341 let body_type = match rb {
2342 RequestBodyContent::Json { schema_name, .. }
2343 | RequestBodyContent::FormUrlEncoded { schema_name, .. }
2344 | RequestBodyContent::Multipart { schema_name, .. } => {
2345 let rust_type_name = self.to_rust_type_name(schema_name);
2346 let request_ident =
2347 syn::Ident::new(&rust_type_name, proc_macro2::Span::call_site());
2348 quote! { #request_ident }
2349 }
2350
2351 RequestBodyContent::OctetStream { .. } | RequestBodyContent::Binary { .. } => {
2352 quote! { Vec<u8> }
2353 }
2354 RequestBodyContent::TextPlain { .. } => quote! { String },
2355 RequestBodyContent::Unsupported { .. } => quote! { Vec<u8> },
2356 RequestBodyContent::SchemaLess { .. } => unreachable!(
2357 "schema-less request bodies preserve the historical client signature"
2358 ),
2359 };
2360 let body_ident = match rb {
2361 RequestBodyContent::OctetStream { .. }
2362 | RequestBodyContent::Binary { .. }
2363 | RequestBodyContent::TextPlain { .. }
2364 | RequestBodyContent::Unsupported { .. } => quote! { body },
2365 RequestBodyContent::SchemaLess { .. } => unreachable!(
2366 "schema-less request bodies preserve the historical client signature"
2367 ),
2368 _ => quote! { request },
2369 };
2370 if required {
2371 params.push(quote! { #body_ident: #body_type });
2372 } else {
2373 params.push(quote! { #body_ident: Option<#body_type> });
2374 }
2375 }
2376
2377 if params.is_empty() {
2378 quote! {}
2379 } else {
2380 quote! { #(#params),* }
2381 }
2382 }
2383
2384 fn get_param_rust_type(&self, param: &crate::analysis::ParameterInfo) -> TokenStream {
2386 if Self::param_has_impl_as_ref_type(param) {
2387 quote! { impl AsRef<str> }
2388 } else {
2389 self.get_param_owned_rust_type(param)
2390 }
2391 }
2392
2393 pub(crate) fn get_param_owned_rust_type(
2397 &self,
2398 param: &crate::analysis::ParameterInfo,
2399 ) -> TokenStream {
2400 use crate::analysis::QuerySerialization;
2401 if let Some(
2407 QuerySerialization::FormExplodedArray { item_type }
2408 | QuerySerialization::FormArray { item_type }
2409 | QuerySerialization::SimpleHeaderArray { item_type },
2410 ) = ¶m.query_serialization
2411 {
2412 use crate::analysis::ArrayItemType;
2413 let item_ty: syn::Type = match item_type {
2414 ArrayItemType::Scalar(rust_type) => syn::parse_str(rust_type)
2415 .unwrap_or_else(|_| panic!("invalid scalar item type `{rust_type}`")),
2416 ArrayItemType::SchemaRef(schema_name) => {
2417 let rust_name = self.to_rust_type_name(schema_name);
2418 syn::parse_str(&rust_name)
2419 .unwrap_or_else(|_| panic!("invalid schema item type `{rust_name}`"))
2420 }
2421 ArrayItemType::FlatStructRef { schema_name, .. } => {
2422 let rust_name = self.to_rust_type_name(schema_name);
2423 syn::parse_str(&rust_name)
2424 .unwrap_or_else(|_| panic!("invalid struct item type `{rust_name}`"))
2425 }
2426 ArrayItemType::NestedStructRef { schema_name, .. } => {
2427 let rust_name = self.to_rust_type_name(schema_name);
2428 syn::parse_str(&rust_name)
2429 .unwrap_or_else(|_| panic!("invalid nested struct item type `{rust_name}`"))
2430 }
2431 };
2432 return quote! { Vec<#item_ty> };
2433 }
2434 if let Some(ref schema_name) = param.schema_ref {
2438 let rust_name = self.to_rust_type_name(schema_name);
2439 let ident = syn::Ident::new(&rust_name, proc_macro2::Span::call_site());
2440 return quote! { #ident };
2441 }
2442 syn::parse_str::<syn::Type>(¶m.rust_type)
2443 .map(|ty| quote! { #ty })
2444 .unwrap_or_else(|_| {
2445 let type_ident = syn::Ident::new(¶m.rust_type, proc_macro2::Span::call_site());
2446 quote! { #type_ident }
2447 })
2448 }
2449
2450 fn param_uses_as_ref_str(param: &crate::analysis::ParameterInfo) -> bool {
2455 param.schema_ref.is_none() && param.rust_type == "String"
2456 }
2457
2458 fn resolve_multipart_wire_schema<'a>(
2459 schema: &'a serde_json::Value,
2460 analysis: &'a SchemaAnalysis,
2461 visited: &mut std::collections::HashSet<String>,
2462 ) -> Option<&'a serde_json::Value> {
2463 let Some(reference) = schema.get("$ref").and_then(serde_json::Value::as_str) else {
2464 return Some(schema);
2465 };
2466 let name = reference.strip_prefix("#/components/schemas/")?;
2467 if !visited.insert(name.to_string()) {
2468 return None;
2469 }
2470 let resolved = analysis.validation_context.component_schemas.get(name)?;
2471 Self::resolve_multipart_wire_schema(resolved, analysis, visited)
2472 }
2473
2474 fn multipart_client_field_kind(
2475 schema_type: &crate::analysis::SchemaType,
2476 analysis: &SchemaAnalysis,
2477 visited: &mut std::collections::HashSet<String>,
2478 ) -> Option<MultipartClientFieldKind> {
2479 match schema_type {
2480 crate::analysis::SchemaType::Primitive {
2481 rust_type,
2482 serde_with,
2483 } => {
2484 let rust_type = rust_type.replace(' ', "");
2485 if rust_type == "bytes::Bytes" {
2486 Some(MultipartClientFieldKind::RawBytes)
2487 } else if serde_with
2488 .as_deref()
2489 .is_some_and(|codec| codec.contains("base64_url"))
2490 {
2491 Some(MultipartClientFieldKind::Base64UrlUnpadded)
2492 } else if serde_with
2493 .as_deref()
2494 .is_some_and(|codec| codec.contains("base64"))
2495 || rust_type == "Vec<u8>"
2496 {
2497 Some(MultipartClientFieldKind::Base64)
2498 } else {
2499 Some(MultipartClientFieldKind::Text)
2500 }
2501 }
2502 crate::analysis::SchemaType::StringEnum { .. }
2503 | crate::analysis::SchemaType::ExtensibleEnum { .. } => {
2504 Some(MultipartClientFieldKind::Text)
2505 }
2506 crate::analysis::SchemaType::Reference { target } => {
2507 if !visited.insert(target.clone()) {
2508 return None;
2509 }
2510 analysis.schemas.get(target).and_then(|schema| {
2511 Self::multipart_client_field_kind(&schema.schema_type, analysis, visited)
2512 })
2513 }
2514 _ => None,
2515 }
2516 }
2517
2518 fn generate_typed_multipart_form(
2519 &self,
2520 schema_name: &str,
2521 validation_schema: &serde_json::Value,
2522 analysis: &SchemaAnalysis,
2523 ) -> TokenStream {
2524 use crate::analysis::{ObjectAdditionalProperties, SchemaType};
2525
2526 let Some((resolved_name, resolved_schema)) =
2527 self.resolve_reference_schema(schema_name, analysis)
2528 else {
2529 let message = format!(
2530 "multipart request schema `{schema_name}` could not be resolved during generation"
2531 );
2532 return quote! {
2533 return Err(HttpError::Config(#message.to_string()).into());
2534 };
2535 };
2536 let SchemaType::Object {
2537 properties,
2538 required,
2539 additional_properties,
2540 ..
2541 } = &resolved_schema.schema_type
2542 else {
2543 let message =
2544 format!("multipart request schema `{schema_name}` must resolve to an object");
2545 return quote! {
2546 return Err(HttpError::Config(#message.to_string()).into());
2547 };
2548 };
2549 let wire_schema = Self::resolve_multipart_wire_schema(
2550 validation_schema,
2551 analysis,
2552 &mut std::collections::HashSet::new(),
2553 );
2554 let wire_properties = wire_schema
2555 .and_then(|schema| schema.get("properties"))
2556 .and_then(serde_json::Value::as_object);
2557 let fields = self.emitted_object_properties(
2558 resolved_name,
2559 properties,
2560 required,
2561 additional_properties,
2562 analysis,
2563 None,
2564 );
2565 if matches!(
2566 additional_properties,
2567 ObjectAdditionalProperties::Typed { .. }
2568 ) {
2569 let message = format!(
2570 "multipart request schema `{schema_name}` cannot contain typed additional properties"
2571 );
2572 return quote! {
2573 return Err(HttpError::Config(#message.to_string()).into());
2574 };
2575 }
2576
2577 let mut parts = Vec::new();
2578 for field in fields {
2579 let wire_name = field.wire_name;
2580 let ident = field.ident;
2581 let wire_format = wire_properties
2582 .and_then(|properties| properties.get(wire_name))
2583 .and_then(|schema| {
2584 Self::resolve_multipart_wire_schema(
2585 schema,
2586 analysis,
2587 &mut std::collections::HashSet::new(),
2588 )
2589 })
2590 .and_then(|schema| schema.get("format"))
2591 .and_then(serde_json::Value::as_str);
2592 let kind = if wire_format == Some("binary") {
2593 match self.config().types.binary {
2594 crate::type_mapping::BinaryStrategy::String => MultipartClientFieldKind::Text,
2595 crate::type_mapping::BinaryStrategy::Bytes
2596 | crate::type_mapping::BinaryStrategy::VecU8 => {
2597 MultipartClientFieldKind::RawBytes
2598 }
2599 }
2600 } else if let Some(kind) = Self::multipart_client_field_kind(
2601 &field.property.schema_type,
2602 analysis,
2603 &mut std::collections::HashSet::new(),
2604 ) {
2605 kind
2606 } else {
2607 let message =
2608 format!("multipart field `{wire_name}` must be binary or a scalar text field");
2609 return quote! {
2610 return Err(HttpError::Config(#message.to_string()).into());
2611 };
2612 };
2613 let add_value = match kind {
2614 MultipartClientFieldKind::RawBytes => quote! {
2615 form = form.part(
2616 #wire_name,
2617 reqwest::multipart::Part::bytes(value.to_vec()),
2618 );
2619 },
2620 MultipartClientFieldKind::Base64 => quote! {
2621 use base64::Engine as _;
2622 form = form.text(
2623 #wire_name,
2624 base64::engine::general_purpose::STANDARD.encode(value),
2625 );
2626 },
2627 MultipartClientFieldKind::Base64UrlUnpadded => quote! {
2628 use base64::Engine as _;
2629 form = form.text(
2630 #wire_name,
2631 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(value),
2632 );
2633 },
2634 MultipartClientFieldKind::Text => quote! {
2635 form = form.text(#wire_name, value.to_string());
2636 },
2637 };
2638 parts.push(if field.is_required {
2639 quote! {
2640 let value = &request.#ident;
2641 #add_value
2642 }
2643 } else {
2644 quote! {
2645 if let Some(value) = &request.#ident {
2646 #add_value
2647 }
2648 }
2649 });
2650 }
2651
2652 quote! {
2653 let mut form = reqwest::multipart::Form::new();
2654 #(#parts)*
2655 req = req.multipart(form);
2656 }
2657 }
2658
2659 fn generate_request_body(&self, op: &OperationInfo, analysis: &SchemaAnalysis) -> TokenStream {
2665 let empty_request_framing = Self::generate_empty_request_framing(op);
2666 let Some(rb) = op.request_body.as_ref() else {
2667 return empty_request_framing;
2668 };
2669 use crate::analysis::RequestBodyContent;
2670 let required = op.request_body_required;
2671 let (ident, apply): (TokenStream, TokenStream) = match rb {
2672 RequestBodyContent::Json { media_type, .. } => (
2673 quote! { request },
2674 quote! {
2675 req = req
2676 .body(serde_json::to_vec(&request).map_err(HttpError::serialization_error)?)
2677 .header("content-type", #media_type);
2678 },
2679 ),
2680 RequestBodyContent::FormUrlEncoded { media_type, .. } => (
2681 quote! { request },
2682 quote! {
2683 req = req
2684 .body(serde_urlencoded::to_string(&request).map_err(HttpError::serialization_error)?)
2685 .header("content-type", #media_type);
2686 },
2687 ),
2688 RequestBodyContent::Multipart {
2689 schema_name,
2690 validation_schema,
2691 ..
2692 } => (
2693 quote! { request },
2694 self.generate_typed_multipart_form(schema_name, validation_schema, analysis),
2695 ),
2696 RequestBodyContent::OctetStream { media_type } => (
2697 quote! { body },
2698 quote! {
2699 req = req
2700 .body(body)
2701 .header("content-type", #media_type);
2702 },
2703 ),
2704 RequestBodyContent::Binary { media_type } => (
2705 quote! { body },
2706 quote! {
2707 req = req
2708 .body(body)
2709 .header("content-type", #media_type);
2710 },
2711 ),
2712 RequestBodyContent::TextPlain { media_type } => (
2713 quote! { body },
2714 quote! {
2715 req = req
2716 .body(body)
2717 .header("content-type", #media_type);
2718 },
2719 ),
2720 RequestBodyContent::Unsupported { media_types } => {
2721 let media_type = media_types
2722 .iter()
2723 .find(|media_type| !crate::openapi::is_wildcard_media_type(media_type))
2724 .map(String::as_str);
2725 let Some(media_type) = media_type else {
2726 let ranges = media_types.join(", ");
2727 let message = format!(
2728 "request body for operation `{}` declares only wildcard media ranges ({ranges}); a concrete Content-Type is required",
2729 op.operation_id,
2730 );
2731 return if required {
2732 quote! {
2733 let _ = body;
2734 return Err(HttpError::Config(#message.to_string()).into());
2735 }
2736 } else {
2737 quote! {
2738 if body.is_some() {
2739 return Err(HttpError::Config(#message.to_string()).into());
2740 }
2741 #empty_request_framing
2742 }
2743 };
2744 };
2745 (
2746 quote! { body },
2747 quote! {
2748 req = req
2749 .body(body)
2750 .header("content-type", #media_type);
2751 },
2752 )
2753 }
2754 RequestBodyContent::SchemaLess { .. } => return empty_request_framing,
2755 };
2756 if required {
2757 apply
2758 } else {
2759 quote! {
2760 if let Some(#ident) = #ident {
2761 #apply
2762 } else {
2763 #empty_request_framing
2764 }
2765 }
2766 }
2767 }
2768
2769 fn generate_empty_request_framing(op: &OperationInfo) -> TokenStream {
2774 if ["POST", "PUT", "PATCH"]
2775 .iter()
2776 .any(|method| op.method.eq_ignore_ascii_case(method))
2777 {
2778 quote! {
2779 req = req.header(reqwest::header::CONTENT_LENGTH, "0");
2780 }
2781 } else {
2782 quote! {}
2783 }
2784 }
2785
2786 fn get_success_response_schema<'a>(
2792 &self,
2793 op: &'a OperationInfo,
2794 ) -> Option<(&'a str, &'a String)> {
2795 op.response_schemas
2796 .get_key_value("200")
2797 .or_else(|| op.response_schemas.get_key_value("201"))
2798 .or_else(|| {
2799 op.response_schemas
2800 .iter()
2801 .find(|(code, _)| code.starts_with('2'))
2802 })
2803 .map(|(status, schema)| (status.as_str(), schema))
2804 }
2805
2806 fn get_success_response<'a>(
2807 &self,
2808 analysis: &'a SchemaAnalysis,
2809 op: &'a OperationInfo,
2810 ) -> ClientSuccessSelection<'a> {
2811 if let Some(responses) = analysis.operation_responses.get(&op.operation_id) {
2812 let mut candidates = Vec::new();
2813 for preferred in ["200", "201"] {
2814 if let Some((status, response)) = responses.get_key_value(preferred) {
2815 candidates.push((status.as_str(), response));
2816 }
2817 }
2818 candidates.extend(
2819 responses
2820 .iter()
2821 .filter(|(status, _)| {
2822 status.starts_with('2')
2823 && status.as_str() != "200"
2824 && status.as_str() != "201"
2825 })
2826 .map(|(status, response)| (status.as_str(), response)),
2827 );
2828
2829 let selected = candidates
2830 .iter()
2831 .copied()
2832 .find(|(_, response)| {
2833 matches!(
2834 Self::response_body(response),
2835 ClientSuccessBody::Json(_)
2836 | ClientSuccessBody::Text
2837 | ClientSuccessBody::Binary
2838 )
2839 })
2840 .or_else(|| {
2841 candidates.iter().copied().find(|(_, response)| {
2842 matches!(
2843 Self::response_body(response),
2844 ClientSuccessBody::EventStream
2845 )
2846 })
2847 })
2848 .or_else(|| candidates.first().copied());
2849
2850 if let Some((_, response)) = selected {
2851 let body = Self::response_body(response);
2852 let statuses = candidates
2853 .iter()
2854 .filter_map(|(status, candidate)| {
2855 Self::success_bodies_are_compatible(body, Self::response_body(candidate))
2856 .then_some(*status)
2857 })
2858 .collect();
2859 let accept = match &response.body {
2860 Some(OperationResponseBody::Json { media_type, .. })
2861 | Some(OperationResponseBody::Text { media_type }) => Some(media_type.as_str()),
2862 Some(OperationResponseBody::Binary {
2863 media_type,
2864 wildcard,
2865 }) => (!wildcard).then_some(media_type.as_str()),
2866 None if response.schema_name.is_some() => {
2867 response.media_type.as_deref().or(Some("application/json"))
2868 }
2869 None if response.supports_streaming => Some("text/event-stream"),
2870 None => None,
2871 };
2872 return ClientSuccessSelection {
2873 statuses,
2874 body,
2875 accept,
2876 };
2877 }
2878 }
2879
2880 if let Some((_status, schema_name)) = self.get_success_response_schema(op) {
2881 let statuses = op
2882 .response_schemas
2883 .iter()
2884 .filter_map(|(candidate_status, candidate_schema)| {
2885 (candidate_status.starts_with('2') && candidate_schema == schema_name)
2886 .then_some(candidate_status.as_str())
2887 })
2888 .collect();
2889 ClientSuccessSelection {
2890 statuses,
2891 body: ClientSuccessBody::Json(schema_name),
2892 accept: Some("application/json"),
2893 }
2894 } else if Self::returns_raw_event_stream(op) {
2895 ClientSuccessSelection {
2896 statuses: Vec::new(),
2897 body: ClientSuccessBody::EventStream,
2898 accept: Some("text/event-stream"),
2899 }
2900 } else {
2901 ClientSuccessSelection {
2902 statuses: Vec::new(),
2903 body: ClientSuccessBody::Empty,
2904 accept: None,
2905 }
2906 }
2907 }
2908
2909 fn response_body(response: &crate::analysis::OperationResponse) -> ClientSuccessBody<'_> {
2910 match &response.body {
2911 Some(OperationResponseBody::Json { schema_name, .. }) => {
2912 ClientSuccessBody::Json(schema_name)
2913 }
2914 Some(OperationResponseBody::Text { .. }) => ClientSuccessBody::Text,
2915 Some(OperationResponseBody::Binary { .. }) => ClientSuccessBody::Binary,
2916 None if response.schema_name.is_some() => {
2917 ClientSuccessBody::Json(response.schema_name.as_deref().unwrap_or_default())
2918 }
2919 None if response.supports_streaming => ClientSuccessBody::EventStream,
2920 None => ClientSuccessBody::Empty,
2921 }
2922 }
2923
2924 fn success_bodies_are_compatible(
2925 selected: ClientSuccessBody<'_>,
2926 candidate: ClientSuccessBody<'_>,
2927 ) -> bool {
2928 match (selected, candidate) {
2929 (ClientSuccessBody::Json(selected), ClientSuccessBody::Json(candidate)) => {
2930 selected == candidate
2931 }
2932 (ClientSuccessBody::Text, ClientSuccessBody::Text)
2933 | (ClientSuccessBody::Binary, ClientSuccessBody::Binary)
2934 | (ClientSuccessBody::EventStream, ClientSuccessBody::EventStream)
2935 | (ClientSuccessBody::Empty, ClientSuccessBody::Empty) => true,
2936 _ => false,
2937 }
2938 }
2939
2940 fn get_response_type(&self, analysis: &SchemaAnalysis, op: &OperationInfo) -> TokenStream {
2942 match self.get_success_response(analysis, op).body {
2943 ClientSuccessBody::Json(response_type) => {
2944 let rust_type_name = self.to_rust_type_name(response_type);
2946 let response_ident =
2947 syn::Ident::new(&rust_type_name, proc_macro2::Span::call_site());
2948 quote! { #response_ident }
2949 }
2950 ClientSuccessBody::Text => quote! { String },
2951 ClientSuccessBody::Binary => quote! { bytes::Bytes },
2952 ClientSuccessBody::EventStream => {
2953 quote! { impl futures_util::Stream<Item = Result<bytes::Bytes, reqwest::Error>> }
2954 }
2955 ClientSuccessBody::Empty => quote! { () },
2956 }
2957 }
2958
2959 fn success_status_guard(statuses: &[&str]) -> TokenStream {
2960 if statuses.is_empty() {
2961 return quote! { status.is_success() };
2962 }
2963 let guards = statuses
2964 .iter()
2965 .map(|status| Self::single_status_guard(status));
2966 quote! { false #( || #guards )* }
2967 }
2968
2969 fn single_status_guard(status: &str) -> TokenStream {
2970 match status {
2971 status if status.chars().all(|character| character.is_ascii_digit()) => {
2972 let status: u16 = status.parse().unwrap_or_default();
2973 quote! { status_code == #status }
2974 }
2975 status if matches!(status.as_bytes(), [b'1'..=b'5', b'X' | b'x', b'X' | b'x']) => {
2976 let class = u16::from(status.as_bytes()[0] - b'0');
2977 quote! { status_code / 100 == #class }
2978 }
2979 _ => quote! { status.is_success() },
2980 }
2981 }
2982
2983 fn returns_raw_event_stream(op: &OperationInfo) -> bool {
2997 op.supports_streaming
2998 }
2999
3000 fn generate_error_handling(
3007 &self,
3008 op: &OperationInfo,
3009 success: ClientSuccessSelection<'_>,
3010 ) -> TokenStream {
3011 let op_error_type = self.op_error_type_token(op);
3012 let success_body = success.body;
3013 let success_status_guard = Self::success_status_guard(&success.statuses);
3014 let selected_status = if success.statuses.is_empty() {
3015 "any declared 2xx response".to_string()
3016 } else {
3017 success.statuses.join(", ")
3018 };
3019
3020 let success_branch = match success_body {
3021 ClientSuccessBody::Json(_) => quote! {
3022 match serde_json::from_str(&body_text) {
3023 Ok(body) => Ok(body),
3024 Err(e) => Err(ApiOpError::Api(ApiError {
3025 status: status_code,
3026 headers: headers,
3027 body: body_text,
3028 raw_body,
3029 typed: None,
3030 parse_error: Some(format!(
3031 "failed to deserialize 2xx response body: {}",
3032 e
3033 )),
3034 })),
3035 }
3036 },
3037 ClientSuccessBody::Text => quote! {
3038 let _ = raw_body;
3039 Ok(body_text)
3040 },
3041 ClientSuccessBody::Empty => quote! {
3042 let _ = body_text;
3043 let _ = raw_body;
3044 let _ = headers;
3045 Ok(())
3046 },
3047 ClientSuccessBody::Binary | ClientSuccessBody::EventStream => quote! {},
3048 };
3049
3050 let error_match_arms = self.generate_error_match_arms(op);
3051
3052 if matches!(success_body, ClientSuccessBody::EventStream) {
3057 return quote! {
3058 let status = response.status();
3059 let status_code = status.as_u16();
3060 let headers = response.headers().clone();
3061
3062 if #success_status_guard {
3063 Ok(response.bytes_stream())
3064 } else {
3065 if status.is_success() {
3066 return Err(ApiOpError::Api(ApiError {
3067 status: status_code,
3068 headers,
3069 body: String::new(),
3070 raw_body: Vec::new(),
3071 typed: None,
3072 parse_error: Some(format!(
3073 "unexpected successful status {}; generated return type selects `{}`; live response body was not buffered",
3074 status_code,
3075 #selected_status,
3076 )),
3077 }));
3078 }
3079 let body_bytes = __read_bounded_response_body(
3080 response,
3081 self.max_response_body_bytes,
3082 ).await?;
3083 let raw_body = body_bytes;
3084 let body_text = String::from_utf8_lossy(&raw_body).into_owned();
3085 let typed: Option<#op_error_type>;
3086 let parse_error: Option<String>;
3087 #error_match_arms
3088 Err(ApiOpError::Api(ApiError {
3089 status: status_code,
3090 headers,
3091 body: body_text,
3092 raw_body,
3093 typed,
3094 parse_error,
3095 }))
3096 }
3097 };
3098 }
3099
3100 if matches!(success_body, ClientSuccessBody::Binary) {
3101 return quote! {
3102 let status = response.status();
3103 let status_code = status.as_u16();
3104 let headers = response.headers().clone();
3105
3106 let body_bytes = __read_bounded_response_body(
3107 response,
3108 self.max_response_body_bytes,
3109 ).await?;
3110 if #success_status_guard {
3111 Ok(bytes::Bytes::from(body_bytes))
3112 } else {
3113 let raw_body = body_bytes;
3114 let body_text = String::from_utf8_lossy(&raw_body).into_owned();
3115 if status.is_success() {
3116 return Err(ApiOpError::Api(ApiError {
3117 status: status_code,
3118 headers,
3119 body: body_text,
3120 raw_body,
3121 typed: None,
3122 parse_error: Some(format!(
3123 "unexpected successful status {}; generated return type selects `{}`",
3124 status_code,
3125 #selected_status,
3126 )),
3127 }));
3128 }
3129 let typed: Option<#op_error_type>;
3130 let parse_error: Option<String>;
3131 #error_match_arms
3132 Err(ApiOpError::Api(ApiError {
3133 status: status_code,
3134 headers,
3135 body: body_text,
3136 raw_body,
3137 typed,
3138 parse_error,
3139 }))
3140 }
3141 };
3142 }
3143
3144 quote! {
3145 let status = response.status();
3146 let status_code = status.as_u16();
3147 let headers = response.headers().clone();
3148 let body_bytes = __read_bounded_response_body(
3149 response,
3150 self.max_response_body_bytes,
3151 ).await?;
3152 let raw_body = body_bytes;
3153 let body_text = String::from_utf8_lossy(&raw_body).into_owned();
3154
3155 if #success_status_guard {
3156 #success_branch
3157 } else if status.is_success() {
3158 Err(ApiOpError::Api(ApiError {
3159 status: status_code,
3160 headers,
3161 body: body_text,
3162 raw_body,
3163 typed: None,
3164 parse_error: Some(format!(
3165 "unexpected successful status {}; generated return type selects `{}`",
3166 status_code,
3167 #selected_status,
3168 )),
3169 }))
3170 } else {
3171 let typed: Option<#op_error_type>;
3172 let parse_error: Option<String>;
3173 #error_match_arms
3174 Err(ApiOpError::Api(ApiError {
3175 status: status_code,
3176 headers,
3177 body: body_text,
3178 raw_body,
3179 typed,
3180 parse_error,
3181 }))
3182 }
3183 }
3184 }
3185
3186 fn generate_error_match_arms(&self, op: &OperationInfo) -> TokenStream {
3189 let arms: Vec<TokenStream> = op
3190 .response_schemas
3191 .iter()
3192 .filter(|(code, _)| !code.starts_with('2'))
3193 .filter_map(|(code, schema)| {
3194 let variant_ident = Self::op_error_variant_ident(code);
3195 let payload_ty_name = self.to_rust_type_name(schema);
3196 let payload_ty = syn::Ident::new(&payload_ty_name, proc_macro2::Span::call_site());
3197 let enum_ident = self.op_error_enum_ident(op);
3198
3199 let pattern = match code.as_str() {
3204 "default" | "Default" => return None, other if other.chars().all(|c| c.is_ascii_digit()) => {
3206 let n: u16 = other.parse().ok()?;
3207 quote! { #n }
3208 }
3209 "1XX" | "1xx" => quote! { code if (100..=199).contains(&code) },
3210 "2XX" | "2xx" => quote! { code if (200..=299).contains(&code) },
3211 "3XX" | "3xx" => quote! { code if (300..=399).contains(&code) },
3212 "4XX" | "4xx" => quote! { code if (400..=499).contains(&code) },
3213 "5XX" | "5xx" => quote! { code if (500..=599).contains(&code) },
3214 _ => return None,
3215 };
3216
3217 Some(quote! {
3218 #pattern => {
3219 match serde_json::from_str::<#payload_ty>(&body_text) {
3220 Ok(v) => {
3221 typed = Some(#enum_ident::#variant_ident(v));
3222 parse_error = None;
3223 }
3224 Err(e) => {
3225 typed = None;
3226 parse_error = Some(e.to_string());
3227 }
3228 }
3229 }
3230 })
3231 })
3232 .collect();
3233
3234 let has_typed_enum = op
3242 .response_schemas
3243 .iter()
3244 .any(|(code, _)| !code.starts_with('2'));
3245
3246 let default_payload = op
3253 .response_schemas
3254 .iter()
3255 .find(|(code, _)| matches!(code.as_str(), "default" | "Default"))
3256 .map(|(_, schema)| self.to_rust_type_name(schema));
3257
3258 let default_arm = if let Some(payload_ty_name) = default_payload {
3259 let payload_ty = syn::Ident::new(&payload_ty_name, proc_macro2::Span::call_site());
3260 let enum_ident = self.op_error_enum_ident(op);
3261 quote! {
3262 _ => {
3263 match serde_json::from_str::<#payload_ty>(&body_text) {
3264 Ok(v) => {
3265 typed = Some(#enum_ident::Default(v));
3266 parse_error = None;
3267 }
3268 Err(e) => {
3269 typed = None;
3270 parse_error = Some(e.to_string());
3271 }
3272 }
3273 }
3274 }
3275 } else if has_typed_enum {
3276 quote! {
3277 _ => {
3278 typed = None;
3279 parse_error = None;
3280 }
3281 }
3282 } else {
3283 quote! {
3285 _ => {
3286 match serde_json::from_str::<serde_json::Value>(&body_text) {
3287 Ok(v) => {
3288 typed = Some(v);
3289 parse_error = None;
3290 }
3291 Err(e) => {
3292 typed = None;
3293 parse_error = Some(e.to_string());
3294 }
3295 }
3296 }
3297 }
3298 };
3299
3300 if arms.is_empty() {
3301 quote! {
3303 match status_code {
3304 #default_arm
3305 }
3306 }
3307 } else {
3308 quote! {
3309 match status_code {
3310 #(#arms)*
3311 #default_arm
3312 }
3313 }
3314 }
3315 }
3316
3317 fn generate_url_construction(&self, path: &str, op: &OperationInfo) -> TokenStream {
3319 if path.contains('{') {
3321 self.generate_url_with_params(path, op)
3322 } else {
3323 quote! {
3324 let request_url = format!("{}{}", self.base_url, #path);
3325 }
3326 }
3327 }
3328
3329 fn generate_url_with_params(&self, path: &str, op: &OperationInfo) -> TokenStream {
3331 let path_params: Vec<_> = op
3333 .parameters
3334 .iter()
3335 .filter(|p| p.location == "path")
3336 .collect();
3337
3338 let mut format_string = String::with_capacity(path.len());
3347 let mut format_args: Vec<TokenStream> = Vec::new();
3348 let mut chars = path.chars().peekable();
3349 while let Some(c) = chars.next() {
3350 if c != '{' {
3351 format_string.push(c);
3352 continue;
3353 }
3354 let mut name = String::new();
3356 while let Some(&n) = chars.peek() {
3357 chars.next();
3358 if n == '}' {
3359 break;
3360 }
3361 name.push(n);
3362 }
3363 let param = path_params.iter().find(|p| p.name == name);
3367 let Some(param) = param else {
3368 format_string.push('{');
3369 format_string.push_str(&name);
3370 format_string.push('}');
3371 continue;
3372 };
3373 format_string.push_str("{}");
3374 let param_name_snake = self.param_ident_str(param);
3375 let param_ident = Self::to_field_ident(¶m_name_snake);
3376 if Self::param_uses_as_ref_str(param) {
3377 format_args.push(quote! {
3378 __pct_encode_path_segment(#param_ident.as_ref())
3379 });
3380 } else {
3381 format_args.push(quote! {
3382 __pct_encode_path_segment(&#param_ident.to_string())
3383 });
3384 }
3385 }
3386
3387 if format_args.is_empty() {
3388 quote! {
3389 let request_url = format!("{}{}", self.base_url, #path);
3390 }
3391 } else {
3392 quote! {
3393 let request_url = format!("{}{}", self.base_url, format!(#format_string, #(#format_args),*));
3394 }
3395 }
3396 }
3397
3398 pub(crate) fn param_ident_str(&self, param: &crate::analysis::ParameterInfo) -> String {
3403 if let Some(ident) = ¶m.rust_ident {
3404 return self.escape_keyword_ident(ident);
3408 }
3409 self.sanitize_param_name(¶m.name)
3410 }
3411
3412 fn escape_keyword_ident(&self, snake_case: &str) -> String {
3413 if matches!(snake_case, "self" | "super" | "crate" | "Self") {
3414 return format!("{snake_case}_param");
3415 }
3416 if Self::is_rust_keyword(snake_case) {
3417 format!("r#{snake_case}")
3418 } else {
3419 snake_case.to_string()
3420 }
3421 }
3422
3423 fn sanitize_param_name(&self, name: &str) -> String {
3428 let suffix = if name.ends_with("<=") {
3433 "_lte"
3434 } else if name.ends_with(">=") {
3435 "_gte"
3436 } else if name.ends_with('<') {
3437 "_lt"
3438 } else if name.ends_with('>') {
3439 "_gt"
3440 } else {
3441 ""
3442 };
3443 let stripped = name.trim_end_matches(['<', '>', '=']);
3444 let mut snake_case = stripped.to_snake_case();
3445 if snake_case.is_empty() {
3446 snake_case.push_str("parameter");
3447 } else if snake_case.starts_with(|character: char| character.is_ascii_digit()) {
3448 snake_case.insert(0, '_');
3449 }
3450 snake_case.push_str(suffix);
3451
3452 if matches!(snake_case.as_str(), "self" | "super" | "crate" | "Self") {
3453 return format!("{snake_case}_param");
3454 }
3455 if Self::is_rust_keyword(&snake_case) {
3456 format!("r#{snake_case}")
3457 } else {
3458 snake_case
3459 }
3460 }
3461}