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 } if !self.is_discriminated_variant(resolved_name, analysis) => {
993 let emitted = self.emitted_object_properties(
994 resolved_name,
995 properties,
996 required,
997 additional_properties,
998 analysis,
999 None,
1000 );
1001 let required_fields: Vec<_> = emitted
1002 .iter()
1003 .filter(|field| field.is_required)
1004 .map(|field| BodyConstructorParam {
1005 preferred_ident: field.ident.clone(),
1006 value_type: field.field_type.clone(),
1007 })
1008 .collect();
1009 if required_fields.is_empty() {
1010 RequiredBodyConstruction::Default
1011 } else if emitted.iter().any(|field| !field.is_required)
1012 || !matches!(additional_properties, ObjectAdditionalProperties::Forbidden)
1013 {
1014 RequiredBodyConstruction::New(required_fields)
1015 } else {
1016 RequiredBodyConstruction::Whole
1017 }
1018 }
1019 _ => RequiredBodyConstruction::Whole,
1020 };
1021
1022 Some(BodyModelPlan {
1023 body_ident,
1024 body_type: quote! { #body_type },
1025 required_construction,
1026 optional_fields,
1027 })
1028 }
1029
1030 fn resolve_reference_schema<'a>(
1031 &self,
1032 schema_name: &'a str,
1033 analysis: &'a SchemaAnalysis,
1034 ) -> Option<(&'a str, &'a crate::analysis::AnalyzedSchema)> {
1035 let mut current = schema_name;
1036 let mut visited = std::collections::HashSet::new();
1037 loop {
1038 if !visited.insert(current) {
1039 return None;
1040 }
1041 let schema = analysis.schemas.get(current)?;
1042 if let crate::analysis::SchemaType::Reference { target } = &schema.schema_type {
1043 current = target;
1044 } else {
1045 return Some((current, schema));
1046 }
1047 }
1048 }
1049
1050 fn collect_optional_body_fields(
1051 &self,
1052 schema_name: &str,
1053 access_path: Vec<syn::Ident>,
1054 analysis: &SchemaAnalysis,
1055 stack: &mut std::collections::HashSet<String>,
1056 output: &mut Vec<BodyFieldPlan>,
1057 ) {
1058 use crate::analysis::SchemaType;
1059 if !stack.insert(schema_name.to_string()) {
1060 return;
1061 }
1062 let Some(schema) = analysis.schemas.get(schema_name) else {
1063 stack.remove(schema_name);
1064 return;
1065 };
1066 match &schema.schema_type {
1067 SchemaType::Reference { target } => {
1068 self.collect_optional_body_fields(target, access_path, analysis, stack, output);
1069 }
1070 SchemaType::Object {
1071 properties,
1072 required,
1073 additional_properties,
1074 } if !self.is_discriminated_variant(schema_name, analysis) => {
1075 for field in self.emitted_object_properties(
1076 schema_name,
1077 properties,
1078 required,
1079 additional_properties,
1080 analysis,
1081 None,
1082 ) {
1083 if field.is_required {
1084 continue;
1085 }
1086 let mut field_path = access_path.clone();
1087 field_path.push(field.ident.clone());
1088 output.push(BodyFieldPlan {
1089 wire_name: field.wire_name.to_string(),
1090 preferred_method_name: field.ident.to_string(),
1091 value_ident: field.ident.clone(),
1092 value_type: self.generate_property_base_type(
1093 schema_name,
1094 field.wire_name,
1095 field.property,
1096 analysis,
1097 ),
1098 access_path: field_path,
1099 });
1100 }
1101 }
1102 SchemaType::Composition { schemas } => {
1103 for (index, schema_ref) in schemas.iter().enumerate() {
1104 let mut nested_path = access_path.clone();
1105 nested_path.push(format_ident!("part_{index}"));
1106 self.collect_optional_body_fields(
1107 &schema_ref.target,
1108 nested_path,
1109 analysis,
1110 stack,
1111 output,
1112 );
1113 }
1114 }
1115 _ => {}
1116 }
1117 stack.remove(schema_name);
1118 }
1119
1120 fn is_discriminated_variant(&self, schema_name: &str, analysis: &SchemaAnalysis) -> bool {
1121 analysis.schemas.values().any(|schema| {
1122 matches!(
1123 &schema.schema_type,
1124 crate::analysis::SchemaType::DiscriminatedUnion { variants, .. }
1125 if variants.iter().any(|variant| variant.type_name == schema_name)
1126 )
1127 })
1128 }
1129
1130 fn generate_param_enum_types(&self, operations: &[&OperationInfo]) -> TokenStream {
1135 let mut by_name: BTreeMap<String, &ParameterInfo> = BTreeMap::new();
1136 for op in operations {
1137 for param in &op.parameters {
1138 if param.enum_values.is_some() {
1139 by_name.entry(param.rust_type.clone()).or_insert(param);
1140 }
1141 }
1142 }
1143
1144 if by_name.is_empty() {
1145 return quote! {};
1146 }
1147
1148 let defs: Vec<TokenStream> = by_name
1149 .values()
1150 .map(|param| self.generate_single_param_enum(param))
1151 .collect();
1152
1153 quote! { #(#defs)* }
1154 }
1155
1156 fn generate_single_param_enum(&self, param: &ParameterInfo) -> TokenStream {
1157 let Some(values) = param.enum_values.as_deref() else {
1158 return quote! {};
1159 };
1160
1161 let enum_ident = format_ident!("{}", param.rust_type);
1162
1163 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
1169 let variant_names: Vec<String> = values
1177 .iter()
1178 .enumerate()
1179 .map(|(index, value)| {
1180 let base = param
1181 .enum_varnames
1182 .as_ref()
1183 .and_then(|names| names.get(index))
1184 .map(|name| self.to_rust_enum_variant(name))
1185 .unwrap_or_else(|| self.to_rust_enum_variant(value));
1186 let mut chosen = base.clone();
1187 let mut suffix = 2;
1188 while !used.insert(chosen.clone()) {
1189 chosen = format!("{base}_{suffix}");
1190 suffix += 1;
1191 }
1192 chosen
1193 })
1194 .collect();
1195
1196 let variants: Vec<TokenStream> = values
1197 .iter()
1198 .zip(&variant_names)
1199 .map(|(value, name)| {
1200 let variant_ident = format_ident!("{}", name);
1201 quote! {
1202 #[serde(rename = #value)]
1203 #variant_ident,
1204 }
1205 })
1206 .collect();
1207
1208 let display_arms: Vec<TokenStream> = values
1209 .iter()
1210 .zip(&variant_names)
1211 .map(|(value, name)| {
1212 let variant_ident = format_ident!("{}", name);
1213 quote! { Self::#variant_ident => #value, }
1214 })
1215 .collect();
1216
1217 let doc = format!(
1218 "Allowed values for the `{}` {} parameter.",
1219 param.name, param.location
1220 );
1221
1222 quote! {
1223 #[doc = #doc]
1224 #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
1225 pub enum #enum_ident {
1226 #(#variants)*
1227 }
1228
1229 impl #enum_ident {
1230 pub fn as_str(&self) -> &'static str {
1231 match self {
1232 #(#display_arms)*
1233 }
1234 }
1235 }
1236
1237 impl std::fmt::Display for #enum_ident {
1238 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1239 f.write_str(self.as_str())
1240 }
1241 }
1242
1243 impl AsRef<str> for #enum_ident {
1244 fn as_ref(&self) -> &str {
1245 self.as_str()
1246 }
1247 }
1248 }
1249 }
1250
1251 fn generate_op_error_enum(&self, op: &OperationInfo) -> Option<TokenStream> {
1256 let variants: Vec<(String, String)> = op
1257 .response_schemas
1258 .iter()
1259 .filter(|(code, _)| !code.starts_with('2'))
1260 .map(|(code, schema)| (code.clone(), schema.clone()))
1261 .collect();
1262
1263 if variants.is_empty() {
1264 return None;
1265 }
1266
1267 let enum_ident = self.op_error_enum_ident(op);
1268 let variant_decls: Vec<TokenStream> = variants
1269 .iter()
1270 .map(|(code, schema)| {
1271 let variant_ident = Self::op_error_variant_ident(code);
1272 let payload_ty_name = self.to_rust_type_name(schema);
1273 let payload_ty = syn::Ident::new(&payload_ty_name, proc_macro2::Span::call_site());
1274 quote! { #variant_ident(#payload_ty) }
1275 })
1276 .collect();
1277
1278 let doc = format!(
1279 "Typed error responses for `{}`. One variant per declared non-2xx response.",
1280 op.operation_id
1281 );
1282
1283 Some(quote! {
1284 #[doc = #doc]
1285 #[derive(Debug, Clone)]
1286 pub enum #enum_ident {
1287 #(#variant_decls,)*
1288 }
1289 })
1290 }
1291
1292 fn op_error_enum_ident(&self, op: &OperationInfo) -> syn::Ident {
1294 use heck::ToPascalCase;
1295 let name = format!(
1296 "{}ApiError",
1297 op.operation_id.replace('.', "_").to_pascal_case()
1298 );
1299 syn::Ident::new(&name, proc_macro2::Span::call_site())
1300 }
1301
1302 fn op_error_variant_ident(status_code: &str) -> syn::Ident {
1305 let raw = match status_code {
1306 "default" | "Default" => "Default".to_string(),
1307 other if other.chars().all(|c| c.is_ascii_digit()) => format!("Status{other}"),
1308 other => format!("Status{}", other.to_ascii_lowercase()),
1309 };
1310 syn::Ident::new(&raw, proc_macro2::Span::call_site())
1311 }
1312
1313 fn op_error_type_token(&self, op: &OperationInfo) -> TokenStream {
1317 if op
1318 .response_schemas
1319 .iter()
1320 .any(|(code, _)| !code.starts_with('2'))
1321 {
1322 let ident = self.op_error_enum_ident(op);
1323 quote! { #ident }
1324 } else {
1325 quote! { serde_json::Value }
1326 }
1327 }
1328
1329 fn generate_single_operation_method(
1331 &self,
1332 analysis: &SchemaAnalysis,
1333 op: &OperationInfo,
1334 ) -> TokenStream {
1335 let method_name = self.get_method_name(op);
1336 let http_method_call = self.http_method_call(op);
1337 let path = &op.path;
1338 let request_param = self.generate_request_param(op);
1339 let request_body = self.generate_request_body(op, analysis);
1340 let query_params = self.generate_query_params(op);
1341 let header_params = self.generate_header_params(op);
1342 let cookie_params = self.generate_cookie_params(op);
1343 let auth_application = self.generate_auth_application();
1344 let success = self.get_success_response(analysis, op);
1345 let response_type = self.get_response_type(analysis, op);
1346 let op_error_type = self.op_error_type_token(op);
1347 let accept = success.accept;
1348 let error_handling = self.generate_error_handling(op, success);
1349 let (custom_headers, accept_header) = if let Some(media_type) = accept {
1350 (
1351 quote! {
1352 for (name, value) in &self.custom_headers {
1353 if !name.eq_ignore_ascii_case("accept") {
1354 req = req.header(name, value);
1355 }
1356 }
1357 },
1358 quote! {
1359 req = req.header(reqwest::header::ACCEPT, #media_type);
1360 },
1361 )
1362 } else {
1363 (
1364 quote! {
1365 for (name, value) in &self.custom_headers {
1366 req = req.header(name, value);
1367 }
1368 },
1369 TokenStream::new(),
1370 )
1371 };
1372 let url_construction = self.generate_url_construction(path, op);
1373 let doc_comment = self.generate_operation_doc_comment(op);
1374
1375 quote! {
1376 #doc_comment
1377 pub async fn #method_name(
1378 &self,
1379 #request_param
1380 ) -> Result<#response_type, ApiOpError<#op_error_type>> {
1381 #url_construction
1382
1383 let mut req = #http_method_call;
1384 #request_body
1385
1386 #query_params
1387 #header_params
1388 #cookie_params
1389
1390 #auth_application
1393
1394 #custom_headers
1396
1397 #accept_header
1400
1401 let response = req.send().await?;
1402 #error_handling
1403 }
1404 }
1405 }
1406
1407 fn generate_auth_application(&self) -> TokenStream {
1411 use crate::http_config::AuthConfig;
1412 match &self.config().auth_config {
1413 Some(AuthConfig::Bearer { header_name }) if header_name == "Authorization" => quote! {
1414 if let Some(api_key) = &self.api_key {
1415 req = req.bearer_auth(api_key);
1416 }
1417 },
1418 Some(AuthConfig::Bearer { header_name }) => {
1419 let h = header_name.clone();
1420 quote! {
1421 if let Some(api_key) = &self.api_key {
1422 req = req.header(#h, format!("Bearer {}", api_key));
1423 }
1424 }
1425 }
1426 Some(AuthConfig::ApiKey { header_name }) => {
1427 let h = header_name.clone();
1428 quote! {
1429 if let Some(api_key) = &self.api_key {
1430 req = req.header(#h, api_key.as_str());
1431 }
1432 }
1433 }
1434 Some(AuthConfig::Custom {
1435 header_name,
1436 header_value_prefix,
1437 }) => {
1438 let h = header_name.clone();
1439 let prefix = header_value_prefix.clone().unwrap_or_default();
1440 if prefix.is_empty() {
1441 quote! {
1442 if let Some(api_key) = &self.api_key {
1443 req = req.header(#h, api_key.as_str());
1444 }
1445 }
1446 } else {
1447 let format_str = format!("{}{{}}", prefix);
1448 quote! {
1449 if let Some(api_key) = &self.api_key {
1450 req = req.header(#h, format!(#format_str, api_key));
1451 }
1452 }
1453 }
1454 }
1455 None => quote! {
1456 if let Some(api_key) = &self.api_key {
1457 req = req.bearer_auth(api_key);
1458 }
1459 },
1460 }
1461 }
1462
1463 fn generate_header_params(&self, op: &OperationInfo) -> TokenStream {
1467 let header_params: Vec<_> = op
1468 .parameters
1469 .iter()
1470 .filter(|p| p.location == "header")
1471 .collect();
1472 if header_params.is_empty() {
1473 return quote! {};
1474 }
1475 let mut emit = Vec::new();
1476 for param in header_params {
1477 let param_name_snake = self.param_ident_str(param);
1478 let param_ident = Self::to_field_ident(¶m_name_snake);
1479 let header_name = ¶m.name;
1480 if matches!(
1481 param.query_serialization,
1482 Some(crate::analysis::QuerySerialization::SimpleHeaderArray { .. })
1483 ) {
1484 let encode = quote! {
1485 v.iter().map(::std::string::ToString::to_string).collect::<Vec<_>>().join(",")
1486 };
1487 if param.required {
1488 emit.push(quote! {
1489 let v = #param_ident;
1490 req = req.header(#header_name, #encode);
1491 });
1492 } else {
1493 emit.push(quote! {
1494 if let Some(v) = #param_ident {
1495 req = req.header(#header_name, #encode);
1496 }
1497 });
1498 }
1499 continue;
1500 }
1501 if param.required {
1502 if Self::param_uses_as_ref_str(param) {
1503 emit.push(quote! {
1504 req = req.header(#header_name, #param_ident.as_ref());
1505 });
1506 } else {
1507 emit.push(quote! {
1508 req = req.header(#header_name, #param_ident.to_string());
1509 });
1510 }
1511 } else if Self::param_uses_as_ref_str(param) {
1512 emit.push(quote! {
1513 if let Some(v) = #param_ident {
1514 req = req.header(#header_name, v.as_ref());
1515 }
1516 });
1517 } else {
1518 emit.push(quote! {
1519 if let Some(v) = #param_ident {
1520 req = req.header(#header_name, v.to_string());
1521 }
1522 });
1523 }
1524 }
1525 quote! {
1526 #(#emit)*
1527 }
1528 }
1529
1530 fn generate_cookie_params(&self, op: &OperationInfo) -> TokenStream {
1531 let cookie_params: Vec<_> = op
1532 .parameters
1533 .iter()
1534 .filter(|parameter| parameter.location == "cookie")
1535 .collect();
1536 if cookie_params.is_empty() {
1537 return quote! {};
1538 }
1539 let mut emit = Vec::new();
1540 for parameter in cookie_params {
1541 let ident = Self::to_field_ident(&self.param_ident_str(parameter));
1542 let wire_name = parameter.name.as_str();
1543 if parameter.required {
1544 emit.push(quote! {
1545 __cookie_fields.push(format!("{}={}", #wire_name, #ident));
1546 });
1547 } else {
1548 emit.push(quote! {
1549 if let Some(value) = #ident {
1550 __cookie_fields.push(format!("{}={}", #wire_name, value));
1551 }
1552 });
1553 }
1554 }
1555 quote! {
1556 let mut __cookie_fields = Vec::new();
1557 #(#emit)*
1558 if !__cookie_fields.is_empty() {
1559 req = req.header(::reqwest::header::COOKIE, __cookie_fields.join("; "));
1560 }
1561 }
1562 }
1563
1564 fn generate_query_params(&self, op: &OperationInfo) -> TokenStream {
1566 let query_params: Vec<_> = op
1567 .parameters
1568 .iter()
1569 .filter(|p| p.location == "query")
1570 .collect();
1571
1572 if query_params.is_empty() {
1573 return quote! {};
1574 }
1575
1576 let mut param_building = Vec::new();
1577 let mut req_appends = Vec::new();
1581
1582 for param in query_params {
1583 use crate::analysis::QuerySerialization;
1584
1585 let param_name_snake = self.param_ident_str(param);
1587 let param_name = Self::to_field_ident(¶m_name_snake);
1588
1589 let param_key = ¶m.name;
1591
1592 match ¶m.query_serialization {
1593 Some(QuerySerialization::FormExplodedNestedObject { properties }) => {
1594 let emit_properties = properties.iter().map(|property| {
1595 let wire_name = property.wire_name.as_str();
1596 let field_ident = CodeGenerator::to_field_ident(
1597 &self.to_rust_field_name(wire_name),
1598 );
1599 match &property.value_type {
1600 crate::analysis::QueryStructPropertyType::Scalar(_) => quote! {
1601 let value = serde_json::to_value(&v.#field_ident)
1602 .map_err(HttpError::serialization_error)?;
1603 if !value.is_null() {
1604 let value = match value {
1605 serde_json::Value::String(value) => value,
1606 serde_json::Value::Bool(value) => value.to_string(),
1607 serde_json::Value::Number(value) => value.to_string(),
1608 _ => return Err(HttpError::serialization_error(
1609 format!("query field `{}` did not serialize as a scalar", #wire_name)
1610 ).into()),
1611 };
1612 nested_params.push((format!("{}.{}", #param_key, #wire_name), value));
1613 }
1614 },
1615 crate::analysis::QueryStructPropertyType::Object { properties } => {
1616 let leaves = properties.iter().map(|property| property.wire_name.clone()).collect::<Vec<_>>();
1617 quote! {
1618 let value = serde_json::to_value(&v.#field_ident)
1619 .map_err(HttpError::serialization_error)?;
1620 if !value.is_null() {
1621 let serde_json::Value::Object(object) = value else {
1622 return Err(HttpError::serialization_error(format!("query field `{}` did not serialize as an object", #wire_name)).into());
1623 };
1624 if object.is_empty() {
1625 nested_params.push((format!("{}.{}[]", #param_key, #wire_name), String::new()));
1626 } else {
1627 for leaf in [#(#leaves),*] {
1628 let Some(value) = object.get(leaf) else { continue };
1629 if value.is_null() { continue; }
1630 let value = match value {
1631 serde_json::Value::String(value) => value.clone(),
1632 serde_json::Value::Bool(value) => value.to_string(),
1633 serde_json::Value::Number(value) => value.to_string(),
1634 _ => return Err(HttpError::serialization_error(format!("query field `{}` contained a non-scalar leaf", #wire_name)).into()),
1635 };
1636 nested_params.push((format!("{}.{}.{}", #param_key, #wire_name, leaf), value));
1637 }
1638 }
1639 }
1640 }
1641 }
1642 crate::analysis::QueryStructPropertyType::Array { item_type } => {
1643 let nested_properties = match item_type {
1644 crate::analysis::ArrayItemType::FlatStructRef { properties, .. } => Some(
1645 properties.iter().map(|property| property.wire_name.clone()).collect::<Vec<_>>()
1646 ),
1647 _ => None,
1648 };
1649 if let Some(nested_properties) = nested_properties {
1650 quote! {
1651 let values = serde_json::to_value(&v.#field_ident)
1652 .map_err(HttpError::serialization_error)?;
1653 if !values.is_null() {
1654 let serde_json::Value::Array(values) = values else {
1655 return Err(HttpError::serialization_error(
1656 format!("query field `{}` did not serialize as an array", #wire_name)
1657 ).into());
1658 };
1659 if values.is_empty() {
1660 nested_params.push((format!("{}.{}[]", #param_key, #wire_name), String::new()));
1661 }
1662 for (index, value) in values.into_iter().enumerate() {
1663 let serde_json::Value::Object(object) = value else {
1664 return Err(HttpError::serialization_error(
1665 format!("query field `{}` contained a non-object item", #wire_name)
1666 ).into());
1667 };
1668 for leaf in [#(#nested_properties),*] {
1669 let Some(value) = object.get(leaf) else { continue };
1670 if value.is_null() { continue; }
1671 let value = match value {
1672 serde_json::Value::String(value) => value.clone(),
1673 serde_json::Value::Bool(value) => value.to_string(),
1674 serde_json::Value::Number(value) => value.to_string(),
1675 _ => return Err(HttpError::serialization_error(
1676 format!("query field `{}` contained a non-scalar leaf", #wire_name)
1677 ).into()),
1678 };
1679 nested_params.push((format!("{}.{}.{}.{}", #param_key, #wire_name, index + 1, leaf), value));
1680 }
1681 }
1682 }
1683 }
1684 } else {
1685 quote! {
1686 let values = serde_json::to_value(&v.#field_ident)
1687 .map_err(HttpError::serialization_error)?;
1688 if !values.is_null() {
1689 let serde_json::Value::Array(values) = values else {
1690 return Err(HttpError::serialization_error(
1691 format!("query field `{}` did not serialize as an array", #wire_name)
1692 ).into());
1693 };
1694 if values.is_empty() {
1695 nested_params.push((format!("{}.{}[]", #param_key, #wire_name), String::new()));
1696 }
1697 for (index, value) in values.into_iter().enumerate() {
1698 let value = match value {
1699 serde_json::Value::String(value) => value,
1700 serde_json::Value::Bool(value) => value.to_string(),
1701 serde_json::Value::Number(value) => value.to_string(),
1702 _ => return Err(HttpError::serialization_error(
1703 format!("query field `{}` contained a non-scalar item", #wire_name)
1704 ).into()),
1705 };
1706 nested_params.push((format!("{}.{}.{}", #param_key, #wire_name, index + 1), value));
1707 }
1708 }
1709 }
1710 }
1711 }
1712 }
1713 }).collect::<Vec<_>>();
1714 let apply = quote! {
1715 let mut nested_params: Vec<(String, String)> = Vec::new();
1716 #(#emit_properties)*
1717 if nested_params.is_empty() {
1718 nested_params.push((format!("{}[]", #param_key), String::new()));
1719 }
1720 req = req.query(&nested_params);
1721 };
1722 if param.required {
1723 req_appends.push(quote! {{ let v = #param_name; #apply }});
1724 } else {
1725 req_appends.push(quote! { if let Some(v) = #param_name { #apply } });
1726 }
1727 continue;
1728 }
1729 Some(QuerySerialization::FormExplodedObject) => {
1730 let apply = quote! {
1737 let __empty = match serde_json::to_value(&v)
1738 .map_err(HttpError::serialization_error)?
1739 {
1740 serde_json::Value::Object(map) => map.is_empty(),
1741 _ => false,
1742 };
1743 if __empty {
1744 req = req.query(&[(format!("{}[]", #param_key), String::new())]);
1745 } else {
1746 req = req.query(&v);
1747 }
1748 };
1749 if param.required {
1750 req_appends.push(quote! {
1751 {
1752 let v = #param_name;
1753 #apply
1754 }
1755 });
1756 } else {
1757 req_appends.push(quote! {
1758 if let Some(v) = #param_name {
1759 #apply
1760 }
1761 });
1762 }
1763 continue;
1764 }
1765 Some(QuerySerialization::DeepObject) => {
1766 let apply = quote! {
1770 let map = match serde_json::to_value(&v)
1771 .map_err(HttpError::serialization_error)?
1772 {
1773 serde_json::Value::Object(map) => map,
1774 _ => return Err(HttpError::serialization_error(
1775 format!("query parameter `{}` did not serialize as an object", #param_key)
1776 ).into()),
1777 };
1778 let mut deep_params: Vec<(String, String)> = Vec::new();
1779 for (k, val) in map {
1780 let s = match val {
1781 serde_json::Value::Null => continue,
1782 serde_json::Value::String(s) => s,
1783 other => other.to_string(),
1784 };
1785 deep_params.push((format!("{}[{}]", #param_key, k), s));
1786 }
1787 if deep_params.is_empty() {
1788 deep_params.push((format!("{}[]", #param_key), String::new()));
1789 }
1790 req = req.query(&deep_params);
1791 };
1792 if param.required {
1793 req_appends.push(quote! {
1794 {
1795 let v = #param_name;
1796 #apply
1797 }
1798 });
1799 } else {
1800 req_appends.push(quote! {
1801 if let Some(v) = #param_name {
1802 #apply
1803 }
1804 });
1805 }
1806 continue;
1807 }
1808 Some(QuerySerialization::FormObject) => {
1809 let apply = quote! {
1813 let map = match serde_json::to_value(&v)
1814 .map_err(HttpError::serialization_error)?
1815 {
1816 serde_json::Value::Object(map) => map,
1817 _ => return Err(HttpError::serialization_error(
1818 format!("query parameter `{}` did not serialize as an object", #param_key)
1819 ).into()),
1820 };
1821 let mut parts: Vec<String> = Vec::new();
1822 for (k, val) in map {
1823 let s = match val {
1824 serde_json::Value::Null => continue,
1825 serde_json::Value::String(s) => s,
1826 other => other.to_string(),
1827 };
1828 if k.contains(',') || s.contains(',') {
1829 return Err(HttpError::serialization_error(
1830 format!(
1831 "query object `{}` contains a comma in key `{}`; use explode=true for lossless string values",
1832 #param_key,
1833 k,
1834 )
1835 ).into());
1836 }
1837 parts.push(k);
1838 parts.push(s);
1839 }
1840 if parts.is_empty() {
1841 query_params.push((
1842 format!("{}[]", #param_key),
1843 String::new(),
1844 ));
1845 } else {
1846 query_params.push((#param_key.to_string(), parts.join(",")));
1847 }
1848 };
1849 if param.required {
1850 param_building.push(quote! {
1851 {
1852 let v = #param_name;
1853 #apply
1854 }
1855 });
1856 } else {
1857 param_building.push(quote! {
1858 if let Some(v) = #param_name {
1859 #apply
1860 }
1861 });
1862 }
1863 continue;
1864 }
1865 Some(QuerySerialization::FormExplodedArray { item_type }) => {
1866 let struct_properties = match item_type {
1869 crate::analysis::ArrayItemType::FlatStructRef { properties, .. }
1870 | crate::analysis::ArrayItemType::NestedStructRef { properties, .. } => {
1871 Some(properties.clone())
1872 }
1873 _ => None,
1874 };
1875 let emit_items = if let Some(properties) = struct_properties {
1876 let pushes = properties
1877 .iter()
1878 .map(|property| {
1879 let wire_name = &property.wire_name;
1880 let field_ident = CodeGenerator::to_field_ident(
1884 &self.to_rust_field_name(wire_name),
1885 );
1886 match &property.value_type {
1887 crate::analysis::QueryStructPropertyType::Scalar(_) => quote! {
1888 let value = serde_json::to_value(&item.#field_ident)
1889 .map_err(HttpError::serialization_error)?;
1890 if !value.is_null() {
1891 let value = match value {
1892 serde_json::Value::String(value) => value,
1893 serde_json::Value::Bool(value) => value.to_string(),
1894 serde_json::Value::Number(value) => value.to_string(),
1895 _ => return Err(HttpError::serialization_error(
1896 format!("query field `{}.{}` did not serialize as a scalar", #param_key, #wire_name)
1897 ).into()),
1898 };
1899 query_params.push((
1900 format!("{}.{}.{}", #param_key, index, #wire_name),
1901 value,
1902 ));
1903 }
1904 },
1905 crate::analysis::QueryStructPropertyType::Object { properties } => {
1906 let leaves = properties.iter().map(|property| property.wire_name.clone()).collect::<Vec<_>>();
1907 quote! {
1908 let value = serde_json::to_value(&item.#field_ident)
1909 .map_err(HttpError::serialization_error)?;
1910 if !value.is_null() {
1911 let serde_json::Value::Object(object) = value else {
1912 return Err(HttpError::serialization_error(format!("query field `{}.{}` did not serialize as an object", #param_key, #wire_name)).into());
1913 };
1914 if object.is_empty() {
1915 query_params.push((format!("{}.{}.{}[]", #param_key, index, #wire_name), String::new()));
1916 }
1917 for leaf in [#(#leaves),*] {
1918 let Some(value) = object.get(leaf) else { continue };
1919 if value.is_null() { continue; }
1920 let value = match value {
1921 serde_json::Value::String(value) => value.clone(),
1922 serde_json::Value::Bool(value) => value.to_string(),
1923 serde_json::Value::Number(value) => value.to_string(),
1924 _ => return Err(HttpError::serialization_error(format!("query field `{}.{}` contained a non-scalar leaf", #param_key, #wire_name)).into()),
1925 };
1926 query_params.push((format!("{}.{}.{}.{}", #param_key, index, #wire_name, leaf), value));
1927 }
1928 }
1929 }
1930 }
1931 crate::analysis::QueryStructPropertyType::Array { item_type } => {
1932 let nested_properties = match item_type {
1933 crate::analysis::ArrayItemType::FlatStructRef { properties, .. } => {
1934 Some(properties.iter().map(|property| property.wire_name.clone()).collect::<Vec<_>>())
1935 }
1936 _ => None,
1937 };
1938 if let Some(nested_properties) = nested_properties {
1939 quote! {
1940 let values = serde_json::to_value(&item.#field_ident)
1941 .map_err(HttpError::serialization_error)?;
1942 if !values.is_null() {
1943 let serde_json::Value::Array(values) = values else {
1944 return Err(HttpError::serialization_error(
1945 format!("query field `{}.{}` did not serialize as an array", #param_key, #wire_name)
1946 ).into());
1947 };
1948 if values.is_empty() {
1949 query_params.push((format!("{}.{}.{}[]", #param_key, index, #wire_name), String::new()));
1950 }
1951 for (nested_index, value) in values.into_iter().enumerate() {
1952 let serde_json::Value::Object(object) = value else {
1953 return Err(HttpError::serialization_error(
1954 format!("query field `{}.{}` contained a non-object item", #param_key, #wire_name)
1955 ).into());
1956 };
1957 if object.is_empty() {
1958 query_params.push((format!("{}.{}.{}.{}[]", #param_key, index, #wire_name, nested_index + 1), String::new()));
1959 }
1960 for nested_wire_name in [#(#nested_properties),*] {
1961 let Some(value) = object.get(nested_wire_name) else { continue };
1962 if value.is_null() { continue; }
1963 let value = match value {
1964 serde_json::Value::String(value) => value.clone(),
1965 serde_json::Value::Bool(value) => value.to_string(),
1966 serde_json::Value::Number(value) => value.to_string(),
1967 _ => return Err(HttpError::serialization_error(
1968 format!("query field `{}.{}` contained a non-scalar leaf", #param_key, #wire_name)
1969 ).into()),
1970 };
1971 query_params.push((
1972 format!("{}.{}.{}.{}.{}", #param_key, index, #wire_name, nested_index + 1, nested_wire_name),
1973 value,
1974 ));
1975 }
1976 }
1977 }
1978 }
1979 } else {
1980 quote! {
1981 let values = serde_json::to_value(&item.#field_ident)
1982 .map_err(HttpError::serialization_error)?;
1983 if !values.is_null() {
1984 let serde_json::Value::Array(values) = values else {
1985 return Err(HttpError::serialization_error(
1986 format!("query field `{}.{}` did not serialize as an array", #param_key, #wire_name)
1987 ).into());
1988 };
1989 if values.is_empty() {
1990 query_params.push((format!("{}.{}.{}[]", #param_key, index, #wire_name), String::new()));
1991 }
1992 for (nested_index, value) in values.into_iter().enumerate() {
1993 let value = match value {
1994 serde_json::Value::String(value) => value,
1995 serde_json::Value::Bool(value) => value.to_string(),
1996 serde_json::Value::Number(value) => value.to_string(),
1997 _ => return Err(HttpError::serialization_error(
1998 format!("query field `{}.{}` contained a non-scalar item", #param_key, #wire_name)
1999 ).into()),
2000 };
2001 query_params.push((
2002 format!("{}.{}.{}.{}", #param_key, index, #wire_name, nested_index + 1),
2003 value,
2004 ));
2005 }
2006 }
2007 }
2008 }
2009 }
2010 }
2011 })
2012 .collect::<Vec<_>>();
2013 quote! {
2014 for (index, item) in v.iter().enumerate() {
2015 let index = index + 1;
2016 let item_start_len = query_params.len();
2017 #(#pushes)*
2018 if query_params.len() == item_start_len {
2019 query_params.push((format!("{}.{}[]", #param_key, index), String::new()));
2020 }
2021 }
2022 }
2023 } else {
2024 quote! {
2025 for item in v {
2026 query_params.push((#param_key.to_string(), item.to_string()));
2027 }
2028 }
2029 };
2030 if param.required {
2031 param_building.push(quote! {
2032 let v = #param_name;
2033 if v.is_empty() {
2034 query_params.push((
2035 format!("{}[]", #param_key),
2036 String::new(),
2037 ));
2038 } else {
2039 #emit_items
2040 }
2041 });
2042 } else {
2043 param_building.push(quote! {
2044 if let Some(v) = #param_name {
2045 if v.is_empty() {
2046 query_params.push((
2047 format!("{}[]", #param_key),
2048 String::new(),
2049 ));
2050 } else {
2051 #emit_items
2052 }
2053 }
2054 });
2055 }
2056 continue;
2057 }
2058 Some(QuerySerialization::FormArray { .. }) => {
2059 let apply = quote! {
2062 if v.is_empty() {
2063 query_params.push((
2064 format!("{}[]", #param_key),
2065 String::new(),
2066 ));
2067 } else {
2068 let mut parts = Vec::with_capacity(v.len());
2069 for item in &v {
2070 let item = item.to_string();
2071 if item.contains(',') {
2072 return Err(HttpError::serialization_error(
2073 format!(
2074 "query array `{}` contains a comma; use explode=true for lossless string values",
2075 #param_key,
2076 )
2077 ).into());
2078 }
2079 parts.push(item);
2080 }
2081 query_params.push((
2082 #param_key.to_string(),
2083 parts.join(","),
2084 ));
2085 }
2086 };
2087 if param.required {
2088 param_building.push(quote! {
2089 {
2090 let v = #param_name;
2091 #apply
2092 }
2093 });
2094 } else {
2095 param_building.push(quote! {
2096 if let Some(v) = #param_name {
2097 #apply
2098 }
2099 });
2100 }
2101 continue;
2102 }
2103 Some(
2104 QuerySerialization::Unsupported { .. }
2105 | QuerySerialization::SimpleHeaderArray { .. },
2106 ) => {}
2107 None => {}
2108 }
2109
2110 if param.required {
2111 if Self::param_uses_as_ref_str(param) {
2113 param_building.push(quote! {
2114 query_params.push((#param_key.to_string(), #param_name.as_ref().to_string()));
2115 });
2116 } else {
2117 param_building.push(quote! {
2118 query_params.push((#param_key.to_string(), #param_name.to_string()));
2119 });
2120 }
2121 } else {
2122 if Self::param_uses_as_ref_str(param) {
2124 param_building.push(quote! {
2125 if let Some(v) = #param_name {
2126 query_params.push((#param_key.to_string(), v.as_ref().to_string()));
2127 }
2128 });
2129 } else {
2130 param_building.push(quote! {
2131 if let Some(v) = #param_name {
2132 query_params.push((#param_key.to_string(), v.to_string()));
2133 }
2134 });
2135 }
2136 }
2137 }
2138
2139 let pairs_block = if param_building.is_empty() {
2142 quote! {}
2143 } else {
2144 quote! {
2145 {
2146 let mut query_params: Vec<(String, String)> = Vec::new();
2147 #(#param_building)*
2148 if !query_params.is_empty() {
2149 req = req.query(&query_params);
2150 }
2151 }
2152 }
2153 };
2154
2155 quote! {
2156 #pairs_block
2158 #(#req_appends)*
2159 }
2160 }
2161
2162 fn generate_operation_doc_comment(&self, op: &OperationInfo) -> TokenStream {
2166 let method = op.method.to_uppercase();
2167 let path = &op.path;
2168 let mut docs: Vec<String> = Vec::new();
2169 if let Some(s) = &op.summary {
2170 if !s.is_empty() {
2171 docs.push(s.clone());
2172 docs.push(String::new());
2173 }
2174 }
2175 if let Some(d) = &op.description {
2176 if !d.is_empty() {
2177 for line in d.lines() {
2178 docs.push(line.to_string());
2179 }
2180 docs.push(String::new());
2181 }
2182 }
2183 docs.push(format!("`{} {}`", method, path));
2184 let doc_attrs: Vec<TokenStream> = docs
2185 .iter()
2186 .map(|line| {
2187 let prefixed = if line.is_empty() {
2188 String::new()
2189 } else {
2190 format!(" {line}")
2191 };
2192 quote! { #[doc = #prefixed] }
2193 })
2194 .collect();
2195 quote! { #(#doc_attrs)* }
2196 }
2197
2198 fn get_method_name(&self, op: &OperationInfo) -> syn::Ident {
2200 let name = if !op.operation_id.is_empty() {
2201 op.operation_id.to_snake_case()
2202 } else {
2203 format!(
2205 "{}_{}",
2206 op.method,
2207 op.path.replace('/', "_").replace(['{', '}'], "")
2208 )
2209 .to_snake_case()
2210 };
2211
2212 syn::Ident::new(&name, proc_macro2::Span::call_site())
2213 }
2214
2215 fn http_method_call(&self, op: &OperationInfo) -> TokenStream {
2220 match op.method.to_uppercase().as_str() {
2221 "GET" => quote! { self.http_client.get(request_url) },
2222 "POST" => quote! { self.http_client.post(request_url) },
2223 "PUT" => quote! { self.http_client.put(request_url) },
2224 "DELETE" => quote! { self.http_client.delete(request_url) },
2225 "PATCH" => quote! { self.http_client.patch(request_url) },
2226 "HEAD" => quote! { self.http_client.head(request_url) },
2227 "OPTIONS" => quote! {
2228 self.http_client.request(reqwest::Method::OPTIONS, request_url)
2229 },
2230 "TRACE" => quote! {
2231 self.http_client.request(reqwest::Method::TRACE, request_url)
2232 },
2233 other => {
2238 let upper = other.to_string();
2239 quote! {
2240 self.http_client.request(
2241 reqwest::Method::from_bytes(#upper.as_bytes())
2242 .expect("invalid HTTP method"),
2243 request_url,
2244 )
2245 }
2246 }
2247 }
2248 }
2249
2250 fn generate_request_param(&self, op: &OperationInfo) -> TokenStream {
2252 let mut params = Vec::new();
2253 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
2260 let mut unique_param_ident = |raw: String| -> syn::Ident {
2261 let mut chosen = raw.clone();
2262 let mut suffix = 2;
2263 while !used.insert(chosen.clone()) {
2264 chosen = format!("{raw}_{suffix}");
2265 suffix += 1;
2266 }
2267 Self::to_field_ident(&chosen)
2268 };
2269
2270 for param in &op.parameters {
2272 if param.location == "path" {
2273 let param_name_snake = self.param_ident_str(param);
2274 let param_name = unique_param_ident(param_name_snake);
2275 let param_type = self.get_param_rust_type(param);
2276 params.push(quote! { #param_name: #param_type });
2277 }
2278 }
2279
2280 for param in &op.parameters {
2282 if param.location == "query" {
2283 let param_name_snake = self.param_ident_str(param);
2284 let param_name = unique_param_ident(param_name_snake);
2285 let param_type = self.get_param_rust_type(param);
2286
2287 if param.required {
2289 params.push(quote! { #param_name: #param_type });
2290 } else {
2291 params.push(quote! { #param_name: Option<#param_type> });
2292 }
2293 }
2294 }
2295
2296 for param in &op.parameters {
2302 if param.location == "header" {
2303 let param_name_snake = self.param_ident_str(param);
2304 let param_name = unique_param_ident(param_name_snake);
2305 let param_type = self.get_param_rust_type(param);
2306 if param.required {
2307 params.push(quote! { #param_name: #param_type });
2308 } else {
2309 params.push(quote! { #param_name: Option<#param_type> });
2310 }
2311 }
2312 }
2313
2314 for param in &op.parameters {
2315 if param.location == "cookie" {
2316 let param_name_snake = self.param_ident_str(param);
2317 let param_name = unique_param_ident(param_name_snake);
2318 let param_type = self.get_param_rust_type(param);
2319 if param.required {
2320 params.push(quote! { #param_name: #param_type });
2321 } else {
2322 params.push(quote! { #param_name: Option<#param_type> });
2323 }
2324 }
2325 }
2326
2327 if let Some(ref rb) = op.request_body {
2330 use crate::analysis::RequestBodyContent;
2331 if matches!(rb, RequestBodyContent::SchemaLess { .. }) {
2332 return if params.is_empty() {
2333 quote! {}
2334 } else {
2335 quote! { #(#params),* }
2336 };
2337 }
2338 let required = op.request_body_required;
2339 let body_type = match rb {
2340 RequestBodyContent::Json { schema_name, .. }
2341 | RequestBodyContent::FormUrlEncoded { schema_name, .. }
2342 | RequestBodyContent::Multipart { schema_name, .. } => {
2343 let rust_type_name = self.to_rust_type_name(schema_name);
2344 let request_ident =
2345 syn::Ident::new(&rust_type_name, proc_macro2::Span::call_site());
2346 quote! { #request_ident }
2347 }
2348
2349 RequestBodyContent::OctetStream { .. } | RequestBodyContent::Binary { .. } => {
2350 quote! { Vec<u8> }
2351 }
2352 RequestBodyContent::TextPlain { .. } => quote! { String },
2353 RequestBodyContent::Unsupported { .. } => quote! { Vec<u8> },
2354 RequestBodyContent::SchemaLess { .. } => unreachable!(
2355 "schema-less request bodies preserve the historical client signature"
2356 ),
2357 };
2358 let body_ident = match rb {
2359 RequestBodyContent::OctetStream { .. }
2360 | RequestBodyContent::Binary { .. }
2361 | RequestBodyContent::TextPlain { .. }
2362 | RequestBodyContent::Unsupported { .. } => quote! { body },
2363 RequestBodyContent::SchemaLess { .. } => unreachable!(
2364 "schema-less request bodies preserve the historical client signature"
2365 ),
2366 _ => quote! { request },
2367 };
2368 if required {
2369 params.push(quote! { #body_ident: #body_type });
2370 } else {
2371 params.push(quote! { #body_ident: Option<#body_type> });
2372 }
2373 }
2374
2375 if params.is_empty() {
2376 quote! {}
2377 } else {
2378 quote! { #(#params),* }
2379 }
2380 }
2381
2382 fn get_param_rust_type(&self, param: &crate::analysis::ParameterInfo) -> TokenStream {
2384 if Self::param_has_impl_as_ref_type(param) {
2385 quote! { impl AsRef<str> }
2386 } else {
2387 self.get_param_owned_rust_type(param)
2388 }
2389 }
2390
2391 pub(crate) fn get_param_owned_rust_type(
2395 &self,
2396 param: &crate::analysis::ParameterInfo,
2397 ) -> TokenStream {
2398 use crate::analysis::QuerySerialization;
2399 if let Some(
2405 QuerySerialization::FormExplodedArray { item_type }
2406 | QuerySerialization::FormArray { item_type }
2407 | QuerySerialization::SimpleHeaderArray { item_type },
2408 ) = ¶m.query_serialization
2409 {
2410 use crate::analysis::ArrayItemType;
2411 let item_ty: syn::Type = match item_type {
2412 ArrayItemType::Scalar(rust_type) => syn::parse_str(rust_type)
2413 .unwrap_or_else(|_| panic!("invalid scalar item type `{rust_type}`")),
2414 ArrayItemType::SchemaRef(schema_name) => {
2415 let rust_name = self.to_rust_type_name(schema_name);
2416 syn::parse_str(&rust_name)
2417 .unwrap_or_else(|_| panic!("invalid schema item type `{rust_name}`"))
2418 }
2419 ArrayItemType::FlatStructRef { schema_name, .. } => {
2420 let rust_name = self.to_rust_type_name(schema_name);
2421 syn::parse_str(&rust_name)
2422 .unwrap_or_else(|_| panic!("invalid struct item type `{rust_name}`"))
2423 }
2424 ArrayItemType::NestedStructRef { schema_name, .. } => {
2425 let rust_name = self.to_rust_type_name(schema_name);
2426 syn::parse_str(&rust_name)
2427 .unwrap_or_else(|_| panic!("invalid nested struct item type `{rust_name}`"))
2428 }
2429 };
2430 return quote! { Vec<#item_ty> };
2431 }
2432 if let Some(ref schema_name) = param.schema_ref {
2436 let rust_name = self.to_rust_type_name(schema_name);
2437 let ident = syn::Ident::new(&rust_name, proc_macro2::Span::call_site());
2438 return quote! { #ident };
2439 }
2440 syn::parse_str::<syn::Type>(¶m.rust_type)
2441 .map(|ty| quote! { #ty })
2442 .unwrap_or_else(|_| {
2443 let type_ident = syn::Ident::new(¶m.rust_type, proc_macro2::Span::call_site());
2444 quote! { #type_ident }
2445 })
2446 }
2447
2448 fn param_uses_as_ref_str(param: &crate::analysis::ParameterInfo) -> bool {
2453 param.schema_ref.is_none() && param.rust_type == "String"
2454 }
2455
2456 fn resolve_multipart_wire_schema<'a>(
2457 schema: &'a serde_json::Value,
2458 analysis: &'a SchemaAnalysis,
2459 visited: &mut std::collections::HashSet<String>,
2460 ) -> Option<&'a serde_json::Value> {
2461 let Some(reference) = schema.get("$ref").and_then(serde_json::Value::as_str) else {
2462 return Some(schema);
2463 };
2464 let name = reference.strip_prefix("#/components/schemas/")?;
2465 if !visited.insert(name.to_string()) {
2466 return None;
2467 }
2468 let resolved = analysis.validation_context.component_schemas.get(name)?;
2469 Self::resolve_multipart_wire_schema(resolved, analysis, visited)
2470 }
2471
2472 fn multipart_client_field_kind(
2473 schema_type: &crate::analysis::SchemaType,
2474 analysis: &SchemaAnalysis,
2475 visited: &mut std::collections::HashSet<String>,
2476 ) -> Option<MultipartClientFieldKind> {
2477 match schema_type {
2478 crate::analysis::SchemaType::Primitive {
2479 rust_type,
2480 serde_with,
2481 } => {
2482 let rust_type = rust_type.replace(' ', "");
2483 if rust_type == "bytes::Bytes" {
2484 Some(MultipartClientFieldKind::RawBytes)
2485 } else if serde_with
2486 .as_deref()
2487 .is_some_and(|codec| codec.contains("base64_url"))
2488 {
2489 Some(MultipartClientFieldKind::Base64UrlUnpadded)
2490 } else if serde_with
2491 .as_deref()
2492 .is_some_and(|codec| codec.contains("base64"))
2493 || rust_type == "Vec<u8>"
2494 {
2495 Some(MultipartClientFieldKind::Base64)
2496 } else {
2497 Some(MultipartClientFieldKind::Text)
2498 }
2499 }
2500 crate::analysis::SchemaType::StringEnum { .. }
2501 | crate::analysis::SchemaType::ExtensibleEnum { .. } => {
2502 Some(MultipartClientFieldKind::Text)
2503 }
2504 crate::analysis::SchemaType::Reference { target } => {
2505 if !visited.insert(target.clone()) {
2506 return None;
2507 }
2508 analysis.schemas.get(target).and_then(|schema| {
2509 Self::multipart_client_field_kind(&schema.schema_type, analysis, visited)
2510 })
2511 }
2512 _ => None,
2513 }
2514 }
2515
2516 fn generate_typed_multipart_form(
2517 &self,
2518 schema_name: &str,
2519 validation_schema: &serde_json::Value,
2520 analysis: &SchemaAnalysis,
2521 ) -> TokenStream {
2522 use crate::analysis::{ObjectAdditionalProperties, SchemaType};
2523
2524 let Some((resolved_name, resolved_schema)) =
2525 self.resolve_reference_schema(schema_name, analysis)
2526 else {
2527 let message = format!(
2528 "multipart request schema `{schema_name}` could not be resolved during generation"
2529 );
2530 return quote! {
2531 return Err(HttpError::Config(#message.to_string()).into());
2532 };
2533 };
2534 let SchemaType::Object {
2535 properties,
2536 required,
2537 additional_properties,
2538 } = &resolved_schema.schema_type
2539 else {
2540 let message =
2541 format!("multipart request schema `{schema_name}` must resolve to an object");
2542 return quote! {
2543 return Err(HttpError::Config(#message.to_string()).into());
2544 };
2545 };
2546 let wire_schema = Self::resolve_multipart_wire_schema(
2547 validation_schema,
2548 analysis,
2549 &mut std::collections::HashSet::new(),
2550 );
2551 let wire_properties = wire_schema
2552 .and_then(|schema| schema.get("properties"))
2553 .and_then(serde_json::Value::as_object);
2554 let fields = self.emitted_object_properties(
2555 resolved_name,
2556 properties,
2557 required,
2558 additional_properties,
2559 analysis,
2560 None,
2561 );
2562 if matches!(
2563 additional_properties,
2564 ObjectAdditionalProperties::Typed { .. }
2565 ) {
2566 let message = format!(
2567 "multipart request schema `{schema_name}` cannot contain typed additional properties"
2568 );
2569 return quote! {
2570 return Err(HttpError::Config(#message.to_string()).into());
2571 };
2572 }
2573
2574 let mut parts = Vec::new();
2575 for field in fields {
2576 let wire_name = field.wire_name;
2577 let ident = field.ident;
2578 let wire_format = wire_properties
2579 .and_then(|properties| properties.get(wire_name))
2580 .and_then(|schema| {
2581 Self::resolve_multipart_wire_schema(
2582 schema,
2583 analysis,
2584 &mut std::collections::HashSet::new(),
2585 )
2586 })
2587 .and_then(|schema| schema.get("format"))
2588 .and_then(serde_json::Value::as_str);
2589 let kind = if wire_format == Some("binary") {
2590 match self.config().types.binary {
2591 crate::type_mapping::BinaryStrategy::String => MultipartClientFieldKind::Text,
2592 crate::type_mapping::BinaryStrategy::Bytes
2593 | crate::type_mapping::BinaryStrategy::VecU8 => {
2594 MultipartClientFieldKind::RawBytes
2595 }
2596 }
2597 } else if let Some(kind) = Self::multipart_client_field_kind(
2598 &field.property.schema_type,
2599 analysis,
2600 &mut std::collections::HashSet::new(),
2601 ) {
2602 kind
2603 } else {
2604 let message =
2605 format!("multipart field `{wire_name}` must be binary or a scalar text field");
2606 return quote! {
2607 return Err(HttpError::Config(#message.to_string()).into());
2608 };
2609 };
2610 let add_value = match kind {
2611 MultipartClientFieldKind::RawBytes => quote! {
2612 form = form.part(
2613 #wire_name,
2614 reqwest::multipart::Part::bytes(value.to_vec()),
2615 );
2616 },
2617 MultipartClientFieldKind::Base64 => quote! {
2618 use base64::Engine as _;
2619 form = form.text(
2620 #wire_name,
2621 base64::engine::general_purpose::STANDARD.encode(value),
2622 );
2623 },
2624 MultipartClientFieldKind::Base64UrlUnpadded => quote! {
2625 use base64::Engine as _;
2626 form = form.text(
2627 #wire_name,
2628 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(value),
2629 );
2630 },
2631 MultipartClientFieldKind::Text => quote! {
2632 form = form.text(#wire_name, value.to_string());
2633 },
2634 };
2635 parts.push(if field.is_required {
2636 quote! {
2637 let value = &request.#ident;
2638 #add_value
2639 }
2640 } else {
2641 quote! {
2642 if let Some(value) = &request.#ident {
2643 #add_value
2644 }
2645 }
2646 });
2647 }
2648
2649 quote! {
2650 let mut form = reqwest::multipart::Form::new();
2651 #(#parts)*
2652 req = req.multipart(form);
2653 }
2654 }
2655
2656 fn generate_request_body(&self, op: &OperationInfo, analysis: &SchemaAnalysis) -> TokenStream {
2662 let empty_request_framing = Self::generate_empty_request_framing(op);
2663 let Some(rb) = op.request_body.as_ref() else {
2664 return empty_request_framing;
2665 };
2666 use crate::analysis::RequestBodyContent;
2667 let required = op.request_body_required;
2668 let (ident, apply): (TokenStream, TokenStream) = match rb {
2669 RequestBodyContent::Json { media_type, .. } => (
2670 quote! { request },
2671 quote! {
2672 req = req
2673 .body(serde_json::to_vec(&request).map_err(HttpError::serialization_error)?)
2674 .header("content-type", #media_type);
2675 },
2676 ),
2677 RequestBodyContent::FormUrlEncoded { media_type, .. } => (
2678 quote! { request },
2679 quote! {
2680 req = req
2681 .body(serde_urlencoded::to_string(&request).map_err(HttpError::serialization_error)?)
2682 .header("content-type", #media_type);
2683 },
2684 ),
2685 RequestBodyContent::Multipart {
2686 schema_name,
2687 validation_schema,
2688 ..
2689 } => (
2690 quote! { request },
2691 self.generate_typed_multipart_form(schema_name, validation_schema, analysis),
2692 ),
2693 RequestBodyContent::OctetStream { media_type } => (
2694 quote! { body },
2695 quote! {
2696 req = req
2697 .body(body)
2698 .header("content-type", #media_type);
2699 },
2700 ),
2701 RequestBodyContent::Binary { media_type } => (
2702 quote! { body },
2703 quote! {
2704 req = req
2705 .body(body)
2706 .header("content-type", #media_type);
2707 },
2708 ),
2709 RequestBodyContent::TextPlain { media_type } => (
2710 quote! { body },
2711 quote! {
2712 req = req
2713 .body(body)
2714 .header("content-type", #media_type);
2715 },
2716 ),
2717 RequestBodyContent::Unsupported { media_types } => {
2718 let media_type = media_types
2719 .iter()
2720 .find(|media_type| !crate::openapi::is_wildcard_media_type(media_type))
2721 .map(String::as_str);
2722 let Some(media_type) = media_type else {
2723 let ranges = media_types.join(", ");
2724 let message = format!(
2725 "request body for operation `{}` declares only wildcard media ranges ({ranges}); a concrete Content-Type is required",
2726 op.operation_id,
2727 );
2728 return if required {
2729 quote! {
2730 let _ = body;
2731 return Err(HttpError::Config(#message.to_string()).into());
2732 }
2733 } else {
2734 quote! {
2735 if body.is_some() {
2736 return Err(HttpError::Config(#message.to_string()).into());
2737 }
2738 #empty_request_framing
2739 }
2740 };
2741 };
2742 (
2743 quote! { body },
2744 quote! {
2745 req = req
2746 .body(body)
2747 .header("content-type", #media_type);
2748 },
2749 )
2750 }
2751 RequestBodyContent::SchemaLess { .. } => return empty_request_framing,
2752 };
2753 if required {
2754 apply
2755 } else {
2756 quote! {
2757 if let Some(#ident) = #ident {
2758 #apply
2759 } else {
2760 #empty_request_framing
2761 }
2762 }
2763 }
2764 }
2765
2766 fn generate_empty_request_framing(op: &OperationInfo) -> TokenStream {
2771 if ["POST", "PUT", "PATCH"]
2772 .iter()
2773 .any(|method| op.method.eq_ignore_ascii_case(method))
2774 {
2775 quote! {
2776 req = req.header(reqwest::header::CONTENT_LENGTH, "0");
2777 }
2778 } else {
2779 quote! {}
2780 }
2781 }
2782
2783 fn get_success_response_schema<'a>(
2789 &self,
2790 op: &'a OperationInfo,
2791 ) -> Option<(&'a str, &'a String)> {
2792 op.response_schemas
2793 .get_key_value("200")
2794 .or_else(|| op.response_schemas.get_key_value("201"))
2795 .or_else(|| {
2796 op.response_schemas
2797 .iter()
2798 .find(|(code, _)| code.starts_with('2'))
2799 })
2800 .map(|(status, schema)| (status.as_str(), schema))
2801 }
2802
2803 fn get_success_response<'a>(
2804 &self,
2805 analysis: &'a SchemaAnalysis,
2806 op: &'a OperationInfo,
2807 ) -> ClientSuccessSelection<'a> {
2808 if let Some(responses) = analysis.operation_responses.get(&op.operation_id) {
2809 let mut candidates = Vec::new();
2810 for preferred in ["200", "201"] {
2811 if let Some((status, response)) = responses.get_key_value(preferred) {
2812 candidates.push((status.as_str(), response));
2813 }
2814 }
2815 candidates.extend(
2816 responses
2817 .iter()
2818 .filter(|(status, _)| {
2819 status.starts_with('2')
2820 && status.as_str() != "200"
2821 && status.as_str() != "201"
2822 })
2823 .map(|(status, response)| (status.as_str(), response)),
2824 );
2825
2826 let selected = candidates
2827 .iter()
2828 .copied()
2829 .find(|(_, response)| {
2830 matches!(
2831 Self::response_body(response),
2832 ClientSuccessBody::Json(_)
2833 | ClientSuccessBody::Text
2834 | ClientSuccessBody::Binary
2835 )
2836 })
2837 .or_else(|| {
2838 candidates.iter().copied().find(|(_, response)| {
2839 matches!(
2840 Self::response_body(response),
2841 ClientSuccessBody::EventStream
2842 )
2843 })
2844 })
2845 .or_else(|| candidates.first().copied());
2846
2847 if let Some((_, response)) = selected {
2848 let body = Self::response_body(response);
2849 let statuses = candidates
2850 .iter()
2851 .filter_map(|(status, candidate)| {
2852 Self::success_bodies_are_compatible(body, Self::response_body(candidate))
2853 .then_some(*status)
2854 })
2855 .collect();
2856 let accept = match &response.body {
2857 Some(OperationResponseBody::Json { media_type, .. })
2858 | Some(OperationResponseBody::Text { media_type }) => Some(media_type.as_str()),
2859 Some(OperationResponseBody::Binary {
2860 media_type,
2861 wildcard,
2862 }) => (!wildcard).then_some(media_type.as_str()),
2863 None if response.schema_name.is_some() => {
2864 response.media_type.as_deref().or(Some("application/json"))
2865 }
2866 None if response.supports_streaming => Some("text/event-stream"),
2867 None => None,
2868 };
2869 return ClientSuccessSelection {
2870 statuses,
2871 body,
2872 accept,
2873 };
2874 }
2875 }
2876
2877 if let Some((_status, schema_name)) = self.get_success_response_schema(op) {
2878 let statuses = op
2879 .response_schemas
2880 .iter()
2881 .filter_map(|(candidate_status, candidate_schema)| {
2882 (candidate_status.starts_with('2') && candidate_schema == schema_name)
2883 .then_some(candidate_status.as_str())
2884 })
2885 .collect();
2886 ClientSuccessSelection {
2887 statuses,
2888 body: ClientSuccessBody::Json(schema_name),
2889 accept: Some("application/json"),
2890 }
2891 } else if Self::returns_raw_event_stream(op) {
2892 ClientSuccessSelection {
2893 statuses: Vec::new(),
2894 body: ClientSuccessBody::EventStream,
2895 accept: Some("text/event-stream"),
2896 }
2897 } else {
2898 ClientSuccessSelection {
2899 statuses: Vec::new(),
2900 body: ClientSuccessBody::Empty,
2901 accept: None,
2902 }
2903 }
2904 }
2905
2906 fn response_body(response: &crate::analysis::OperationResponse) -> ClientSuccessBody<'_> {
2907 match &response.body {
2908 Some(OperationResponseBody::Json { schema_name, .. }) => {
2909 ClientSuccessBody::Json(schema_name)
2910 }
2911 Some(OperationResponseBody::Text { .. }) => ClientSuccessBody::Text,
2912 Some(OperationResponseBody::Binary { .. }) => ClientSuccessBody::Binary,
2913 None if response.schema_name.is_some() => {
2914 ClientSuccessBody::Json(response.schema_name.as_deref().unwrap_or_default())
2915 }
2916 None if response.supports_streaming => ClientSuccessBody::EventStream,
2917 None => ClientSuccessBody::Empty,
2918 }
2919 }
2920
2921 fn success_bodies_are_compatible(
2922 selected: ClientSuccessBody<'_>,
2923 candidate: ClientSuccessBody<'_>,
2924 ) -> bool {
2925 match (selected, candidate) {
2926 (ClientSuccessBody::Json(selected), ClientSuccessBody::Json(candidate)) => {
2927 selected == candidate
2928 }
2929 (ClientSuccessBody::Text, ClientSuccessBody::Text)
2930 | (ClientSuccessBody::Binary, ClientSuccessBody::Binary)
2931 | (ClientSuccessBody::EventStream, ClientSuccessBody::EventStream)
2932 | (ClientSuccessBody::Empty, ClientSuccessBody::Empty) => true,
2933 _ => false,
2934 }
2935 }
2936
2937 fn get_response_type(&self, analysis: &SchemaAnalysis, op: &OperationInfo) -> TokenStream {
2939 match self.get_success_response(analysis, op).body {
2940 ClientSuccessBody::Json(response_type) => {
2941 let rust_type_name = self.to_rust_type_name(response_type);
2943 let response_ident =
2944 syn::Ident::new(&rust_type_name, proc_macro2::Span::call_site());
2945 quote! { #response_ident }
2946 }
2947 ClientSuccessBody::Text => quote! { String },
2948 ClientSuccessBody::Binary => quote! { bytes::Bytes },
2949 ClientSuccessBody::EventStream => {
2950 quote! { impl futures_util::Stream<Item = Result<bytes::Bytes, reqwest::Error>> }
2951 }
2952 ClientSuccessBody::Empty => quote! { () },
2953 }
2954 }
2955
2956 fn success_status_guard(statuses: &[&str]) -> TokenStream {
2957 if statuses.is_empty() {
2958 return quote! { status.is_success() };
2959 }
2960 let guards = statuses
2961 .iter()
2962 .map(|status| Self::single_status_guard(status));
2963 quote! { false #( || #guards )* }
2964 }
2965
2966 fn single_status_guard(status: &str) -> TokenStream {
2967 match status {
2968 status if status.chars().all(|character| character.is_ascii_digit()) => {
2969 let status: u16 = status.parse().unwrap_or_default();
2970 quote! { status_code == #status }
2971 }
2972 status if matches!(status.as_bytes(), [b'1'..=b'5', b'X' | b'x', b'X' | b'x']) => {
2973 let class = u16::from(status.as_bytes()[0] - b'0');
2974 quote! { status_code / 100 == #class }
2975 }
2976 _ => quote! { status.is_success() },
2977 }
2978 }
2979
2980 fn returns_raw_event_stream(op: &OperationInfo) -> bool {
2994 op.supports_streaming
2995 }
2996
2997 fn generate_error_handling(
3004 &self,
3005 op: &OperationInfo,
3006 success: ClientSuccessSelection<'_>,
3007 ) -> TokenStream {
3008 let op_error_type = self.op_error_type_token(op);
3009 let success_body = success.body;
3010 let success_status_guard = Self::success_status_guard(&success.statuses);
3011 let selected_status = if success.statuses.is_empty() {
3012 "any declared 2xx response".to_string()
3013 } else {
3014 success.statuses.join(", ")
3015 };
3016
3017 let success_branch = match success_body {
3018 ClientSuccessBody::Json(_) => quote! {
3019 match serde_json::from_str(&body_text) {
3020 Ok(body) => Ok(body),
3021 Err(e) => Err(ApiOpError::Api(ApiError {
3022 status: status_code,
3023 headers: headers,
3024 body: body_text,
3025 raw_body,
3026 typed: None,
3027 parse_error: Some(format!(
3028 "failed to deserialize 2xx response body: {}",
3029 e
3030 )),
3031 })),
3032 }
3033 },
3034 ClientSuccessBody::Text => quote! {
3035 let _ = raw_body;
3036 Ok(body_text)
3037 },
3038 ClientSuccessBody::Empty => quote! {
3039 let _ = body_text;
3040 let _ = raw_body;
3041 let _ = headers;
3042 Ok(())
3043 },
3044 ClientSuccessBody::Binary | ClientSuccessBody::EventStream => quote! {},
3045 };
3046
3047 let error_match_arms = self.generate_error_match_arms(op);
3048
3049 if matches!(success_body, ClientSuccessBody::EventStream) {
3054 return quote! {
3055 let status = response.status();
3056 let status_code = status.as_u16();
3057 let headers = response.headers().clone();
3058
3059 if #success_status_guard {
3060 Ok(response.bytes_stream())
3061 } else {
3062 if status.is_success() {
3063 return Err(ApiOpError::Api(ApiError {
3064 status: status_code,
3065 headers,
3066 body: String::new(),
3067 raw_body: Vec::new(),
3068 typed: None,
3069 parse_error: Some(format!(
3070 "unexpected successful status {}; generated return type selects `{}`; live response body was not buffered",
3071 status_code,
3072 #selected_status,
3073 )),
3074 }));
3075 }
3076 let body_bytes = __read_bounded_response_body(
3077 response,
3078 self.max_response_body_bytes,
3079 ).await?;
3080 let raw_body = body_bytes;
3081 let body_text = String::from_utf8_lossy(&raw_body).into_owned();
3082 let typed: Option<#op_error_type>;
3083 let parse_error: Option<String>;
3084 #error_match_arms
3085 Err(ApiOpError::Api(ApiError {
3086 status: status_code,
3087 headers,
3088 body: body_text,
3089 raw_body,
3090 typed,
3091 parse_error,
3092 }))
3093 }
3094 };
3095 }
3096
3097 if matches!(success_body, ClientSuccessBody::Binary) {
3098 return quote! {
3099 let status = response.status();
3100 let status_code = status.as_u16();
3101 let headers = response.headers().clone();
3102
3103 let body_bytes = __read_bounded_response_body(
3104 response,
3105 self.max_response_body_bytes,
3106 ).await?;
3107 if #success_status_guard {
3108 Ok(bytes::Bytes::from(body_bytes))
3109 } else {
3110 let raw_body = body_bytes;
3111 let body_text = String::from_utf8_lossy(&raw_body).into_owned();
3112 if status.is_success() {
3113 return Err(ApiOpError::Api(ApiError {
3114 status: status_code,
3115 headers,
3116 body: body_text,
3117 raw_body,
3118 typed: None,
3119 parse_error: Some(format!(
3120 "unexpected successful status {}; generated return type selects `{}`",
3121 status_code,
3122 #selected_status,
3123 )),
3124 }));
3125 }
3126 let typed: Option<#op_error_type>;
3127 let parse_error: Option<String>;
3128 #error_match_arms
3129 Err(ApiOpError::Api(ApiError {
3130 status: status_code,
3131 headers,
3132 body: body_text,
3133 raw_body,
3134 typed,
3135 parse_error,
3136 }))
3137 }
3138 };
3139 }
3140
3141 quote! {
3142 let status = response.status();
3143 let status_code = status.as_u16();
3144 let headers = response.headers().clone();
3145 let body_bytes = __read_bounded_response_body(
3146 response,
3147 self.max_response_body_bytes,
3148 ).await?;
3149 let raw_body = body_bytes;
3150 let body_text = String::from_utf8_lossy(&raw_body).into_owned();
3151
3152 if #success_status_guard {
3153 #success_branch
3154 } else if status.is_success() {
3155 Err(ApiOpError::Api(ApiError {
3156 status: status_code,
3157 headers,
3158 body: body_text,
3159 raw_body,
3160 typed: None,
3161 parse_error: Some(format!(
3162 "unexpected successful status {}; generated return type selects `{}`",
3163 status_code,
3164 #selected_status,
3165 )),
3166 }))
3167 } else {
3168 let typed: Option<#op_error_type>;
3169 let parse_error: Option<String>;
3170 #error_match_arms
3171 Err(ApiOpError::Api(ApiError {
3172 status: status_code,
3173 headers,
3174 body: body_text,
3175 raw_body,
3176 typed,
3177 parse_error,
3178 }))
3179 }
3180 }
3181 }
3182
3183 fn generate_error_match_arms(&self, op: &OperationInfo) -> TokenStream {
3186 let arms: Vec<TokenStream> = op
3187 .response_schemas
3188 .iter()
3189 .filter(|(code, _)| !code.starts_with('2'))
3190 .filter_map(|(code, schema)| {
3191 let variant_ident = Self::op_error_variant_ident(code);
3192 let payload_ty_name = self.to_rust_type_name(schema);
3193 let payload_ty = syn::Ident::new(&payload_ty_name, proc_macro2::Span::call_site());
3194 let enum_ident = self.op_error_enum_ident(op);
3195
3196 let pattern = match code.as_str() {
3201 "default" | "Default" => return None, other if other.chars().all(|c| c.is_ascii_digit()) => {
3203 let n: u16 = other.parse().ok()?;
3204 quote! { #n }
3205 }
3206 "1XX" | "1xx" => quote! { code if (100..=199).contains(&code) },
3207 "2XX" | "2xx" => quote! { code if (200..=299).contains(&code) },
3208 "3XX" | "3xx" => quote! { code if (300..=399).contains(&code) },
3209 "4XX" | "4xx" => quote! { code if (400..=499).contains(&code) },
3210 "5XX" | "5xx" => quote! { code if (500..=599).contains(&code) },
3211 _ => return None,
3212 };
3213
3214 Some(quote! {
3215 #pattern => {
3216 match serde_json::from_str::<#payload_ty>(&body_text) {
3217 Ok(v) => {
3218 typed = Some(#enum_ident::#variant_ident(v));
3219 parse_error = None;
3220 }
3221 Err(e) => {
3222 typed = None;
3223 parse_error = Some(e.to_string());
3224 }
3225 }
3226 }
3227 })
3228 })
3229 .collect();
3230
3231 let has_typed_enum = op
3239 .response_schemas
3240 .iter()
3241 .any(|(code, _)| !code.starts_with('2'));
3242
3243 let default_payload = op
3250 .response_schemas
3251 .iter()
3252 .find(|(code, _)| matches!(code.as_str(), "default" | "Default"))
3253 .map(|(_, schema)| self.to_rust_type_name(schema));
3254
3255 let default_arm = if let Some(payload_ty_name) = default_payload {
3256 let payload_ty = syn::Ident::new(&payload_ty_name, proc_macro2::Span::call_site());
3257 let enum_ident = self.op_error_enum_ident(op);
3258 quote! {
3259 _ => {
3260 match serde_json::from_str::<#payload_ty>(&body_text) {
3261 Ok(v) => {
3262 typed = Some(#enum_ident::Default(v));
3263 parse_error = None;
3264 }
3265 Err(e) => {
3266 typed = None;
3267 parse_error = Some(e.to_string());
3268 }
3269 }
3270 }
3271 }
3272 } else if has_typed_enum {
3273 quote! {
3274 _ => {
3275 typed = None;
3276 parse_error = None;
3277 }
3278 }
3279 } else {
3280 quote! {
3282 _ => {
3283 match serde_json::from_str::<serde_json::Value>(&body_text) {
3284 Ok(v) => {
3285 typed = Some(v);
3286 parse_error = None;
3287 }
3288 Err(e) => {
3289 typed = None;
3290 parse_error = Some(e.to_string());
3291 }
3292 }
3293 }
3294 }
3295 };
3296
3297 if arms.is_empty() {
3298 quote! {
3300 match status_code {
3301 #default_arm
3302 }
3303 }
3304 } else {
3305 quote! {
3306 match status_code {
3307 #(#arms)*
3308 #default_arm
3309 }
3310 }
3311 }
3312 }
3313
3314 fn generate_url_construction(&self, path: &str, op: &OperationInfo) -> TokenStream {
3316 if path.contains('{') {
3318 self.generate_url_with_params(path, op)
3319 } else {
3320 quote! {
3321 let request_url = format!("{}{}", self.base_url, #path);
3322 }
3323 }
3324 }
3325
3326 fn generate_url_with_params(&self, path: &str, op: &OperationInfo) -> TokenStream {
3328 let path_params: Vec<_> = op
3330 .parameters
3331 .iter()
3332 .filter(|p| p.location == "path")
3333 .collect();
3334
3335 let mut format_string = String::with_capacity(path.len());
3344 let mut format_args: Vec<TokenStream> = Vec::new();
3345 let mut chars = path.chars().peekable();
3346 while let Some(c) = chars.next() {
3347 if c != '{' {
3348 format_string.push(c);
3349 continue;
3350 }
3351 let mut name = String::new();
3353 while let Some(&n) = chars.peek() {
3354 chars.next();
3355 if n == '}' {
3356 break;
3357 }
3358 name.push(n);
3359 }
3360 let param = path_params.iter().find(|p| p.name == name);
3364 let Some(param) = param else {
3365 format_string.push('{');
3366 format_string.push_str(&name);
3367 format_string.push('}');
3368 continue;
3369 };
3370 format_string.push_str("{}");
3371 let param_name_snake = self.param_ident_str(param);
3372 let param_ident = Self::to_field_ident(¶m_name_snake);
3373 if Self::param_uses_as_ref_str(param) {
3374 format_args.push(quote! {
3375 __pct_encode_path_segment(#param_ident.as_ref())
3376 });
3377 } else {
3378 format_args.push(quote! {
3379 __pct_encode_path_segment(&#param_ident.to_string())
3380 });
3381 }
3382 }
3383
3384 if format_args.is_empty() {
3385 quote! {
3386 let request_url = format!("{}{}", self.base_url, #path);
3387 }
3388 } else {
3389 quote! {
3390 let request_url = format!("{}{}", self.base_url, format!(#format_string, #(#format_args),*));
3391 }
3392 }
3393 }
3394
3395 pub(crate) fn param_ident_str(&self, param: &crate::analysis::ParameterInfo) -> String {
3400 if let Some(ident) = ¶m.rust_ident {
3401 return self.escape_keyword_ident(ident);
3405 }
3406 self.sanitize_param_name(¶m.name)
3407 }
3408
3409 fn escape_keyword_ident(&self, snake_case: &str) -> String {
3410 if matches!(snake_case, "self" | "super" | "crate" | "Self") {
3411 return format!("{snake_case}_param");
3412 }
3413 if Self::is_rust_keyword(snake_case) {
3414 format!("r#{snake_case}")
3415 } else {
3416 snake_case.to_string()
3417 }
3418 }
3419
3420 fn sanitize_param_name(&self, name: &str) -> String {
3425 let suffix = if name.ends_with("<=") {
3430 "_lte"
3431 } else if name.ends_with(">=") {
3432 "_gte"
3433 } else if name.ends_with('<') {
3434 "_lt"
3435 } else if name.ends_with('>') {
3436 "_gt"
3437 } else {
3438 ""
3439 };
3440 let stripped = name.trim_end_matches(['<', '>', '=']);
3441 let mut snake_case = stripped.to_snake_case();
3442 if snake_case.is_empty() {
3443 snake_case.push_str("parameter");
3444 } else if snake_case.starts_with(|character: char| character.is_ascii_digit()) {
3445 snake_case.insert(0, '_');
3446 }
3447 snake_case.push_str(suffix);
3448
3449 if matches!(snake_case.as_str(), "self" | "super" | "crate" | "Self") {
3450 return format!("{snake_case}_param");
3451 }
3452 if Self::is_rust_keyword(&snake_case) {
3453 format!("r#{snake_case}")
3454 } else {
3455 snake_case
3456 }
3457 }
3458}