1use crate::analysis::{OperationInfo, 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
171enum RequiredBodyConstruction {
172 Default,
173 New(Vec<BodyConstructorParam>),
174 Whole,
175}
176
177struct BodyConstructorParam {
178 preferred_ident: syn::Ident,
179 value_type: TokenStream,
180}
181
182struct BodyModelPlan {
183 body_ident: syn::Ident,
184 body_type: TokenStream,
185 required_construction: RequiredBodyConstruction,
186 optional_fields: Vec<BodyFieldPlan>,
187}
188
189impl CodeGenerator {
190 pub fn generate_http_client_struct(&self) -> TokenStream {
192 let has_retry = self.config().retry_config.is_some();
193 let has_tracing = self.config().tracing_enabled;
194
195 let retry_config_struct = if has_retry {
197 quote! {
198 #[derive(Debug, Clone)]
200 pub struct RetryConfig {
201 pub max_retries: u32,
202 pub initial_delay_ms: u64,
203 pub max_delay_ms: u64,
204 }
205
206 impl Default for RetryConfig {
207 fn default() -> Self {
208 Self {
209 max_retries: 3,
210 initial_delay_ms: 500,
211 max_delay_ms: 16000,
212 }
213 }
214 }
215 }
216 } else {
217 quote! {}
218 };
219
220 let client_struct = quote! {
222 use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
223 use std::collections::BTreeMap;
224
225 #[derive(Clone)]
227 pub struct HttpClient {
228 base_url: String,
229 api_key: Option<String>,
230 http_client: ClientWithMiddleware,
231 custom_headers: BTreeMap<String, String>,
232 }
233 };
234
235 let constructor = self.generate_constructor(has_retry, has_tracing);
237
238 let builder_methods = self.generate_builder_methods();
240
241 let default_impl = quote! {
243 impl Default for HttpClient {
244 fn default() -> Self {
245 Self::new()
246 }
247 }
248 };
249
250 let path_encoder = quote! {
254 fn __pct_encode_path_segment(s: &str) -> String {
255 let mut out = String::with_capacity(s.len());
256 for &b in s.as_bytes() {
257 match b {
258 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
259 out.push(b as char);
260 }
261 _ => {
262 out.push('%');
263 out.push_str(&format!("{:02X}", b));
264 }
265 }
266 }
267 out
268 }
269 };
270
271 quote! {
273 #retry_config_struct
274 #client_struct
275
276 impl HttpClient {
277 #constructor
278 #builder_methods
279 }
280
281 #default_impl
282 #path_encoder
283 }
284 }
285
286 fn generate_constructor(&self, has_retry: bool, has_tracing: bool) -> TokenStream {
288 let configured_base_url = self
295 .config()
296 .http_client_config
297 .as_ref()
298 .and_then(|http| http.base_url.as_deref())
299 .unwrap_or_default();
300 let default_base_url = quote! { #configured_base_url.to_string() };
301
302 let retry_param = if has_retry {
303 quote! { retry_config: Option<RetryConfig>, }
304 } else {
305 quote! {}
306 };
307
308 let tracing_param = if has_tracing {
309 quote! { enable_tracing: bool, }
310 } else {
311 quote! {}
312 };
313
314 let retry_middleware = if has_retry {
315 quote! {
316 if let Some(config) = retry_config {
317 use reqwest_retry::{RetryTransientMiddleware, policies::ExponentialBackoff};
318
319 let retry_policy = ExponentialBackoff::builder()
320 .retry_bounds(
321 std::time::Duration::from_millis(config.initial_delay_ms),
322 std::time::Duration::from_millis(config.max_delay_ms),
323 )
324 .build_with_max_retries(config.max_retries);
325
326 let retry_middleware = RetryTransientMiddleware::new_with_policy(retry_policy);
327 client_builder = client_builder.with(retry_middleware);
328 }
329 }
330 } else {
331 quote! {}
332 };
333
334 let tracing_middleware = if has_tracing {
335 quote! {
336 if enable_tracing {
337 use reqwest_tracing::TracingMiddleware;
338 client_builder = client_builder.with(TracingMiddleware::default());
339 }
340 }
341 } else {
342 quote! {}
343 };
344
345 let default_constructor = if has_retry && has_tracing {
346 quote! {
347 pub fn new() -> Self {
349 Self::with_config(None, true)
350 }
351 }
352 } else if has_retry {
353 quote! {
354 pub fn new() -> Self {
356 Self::with_config(None)
357 }
358 }
359 } else if has_tracing {
360 quote! {
361 pub fn new() -> Self {
363 Self::with_config(true)
364 }
365 }
366 } else {
367 quote! {
368 pub fn new() -> Self {
370 let reqwest_client = reqwest::Client::new();
371 let client_builder = ClientBuilder::new(reqwest_client);
372 let http_client = client_builder.build();
373
374 Self {
375 base_url: #default_base_url,
376 api_key: None,
377 http_client,
378 custom_headers: BTreeMap::new(),
379 }
380 }
381 }
382 };
383
384 if has_retry || has_tracing {
385 quote! {
386 #default_constructor
387
388 pub fn with_config(#retry_param #tracing_param) -> Self {
390 let reqwest_client = reqwest::Client::new();
391 let mut client_builder = ClientBuilder::new(reqwest_client);
392
393 #tracing_middleware
394 #retry_middleware
395
396 let http_client = client_builder.build();
397
398 Self {
399 base_url: #default_base_url,
400 api_key: None,
401 http_client,
402 custom_headers: BTreeMap::new(),
403 }
404 }
405 }
406 } else {
407 default_constructor
408 }
409 }
410
411 fn generate_builder_methods(&self) -> TokenStream {
413 quote! {
414 pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
416 self.base_url = base_url.into();
417 self
418 }
419
420 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
422 self.api_key = Some(api_key.into());
423 self
424 }
425
426 pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
428 self.custom_headers.insert(name.into(), value.into());
429 self
430 }
431
432 pub fn with_headers(mut self, headers: BTreeMap<String, String>) -> Self {
434 self.custom_headers.extend(headers);
435 self
436 }
437 }
438 }
439
440 pub fn generate_operation_methods(&self, analysis: &SchemaAnalysis) -> TokenStream {
449 let operations: Vec<&OperationInfo> = analysis.operations.values().collect();
450 self.generate_operation_methods_for(analysis, &operations)
451 }
452
453 pub(crate) fn generate_operation_methods_for(
457 &self,
458 analysis: &SchemaAnalysis,
459 operations: &[&OperationInfo],
460 ) -> TokenStream {
461 let param_enums = self.generate_param_enum_types(operations);
462
463 let op_error_enums: Vec<TokenStream> = operations
464 .iter()
465 .copied()
466 .filter_map(|op| self.generate_op_error_enum(op))
467 .collect();
468
469 let methods: Vec<TokenStream> = operations
470 .iter()
471 .copied()
472 .map(|op| self.generate_single_operation_method(op))
473 .collect();
474
475 let (operation_builders, builder_entries) =
476 self.generate_operation_builders(analysis, operations);
477
478 quote! {
479 #param_enums
480
481 #(#op_error_enums)*
482
483 #(#operation_builders)*
484
485 impl HttpClient {
486 #(#methods)*
487 #(#builder_entries)*
488 }
489 }
490 }
491
492 fn generate_operation_builders(
493 &self,
494 analysis: &SchemaAnalysis,
495 operations: &[&OperationInfo],
496 ) -> (Vec<TokenStream>, Vec<TokenStream>) {
497 if !self.config().builders.enabled {
498 return (Vec::new(), Vec::new());
499 }
500
501 let mut used_entry_methods: std::collections::HashSet<String> = operations
502 .iter()
503 .map(|operation| self.get_method_name(operation).to_string())
504 .collect();
505 let mut used_type_names = std::collections::HashSet::new();
506 for schema_name in analysis.schemas.keys() {
507 let rust_name = self.to_rust_type_name(schema_name);
508 used_type_names.insert(rust_name.clone());
509 used_type_names.insert(format!("{rust_name}Builder"));
513 }
514 used_type_names.insert("HttpClient".to_string());
515 used_type_names.insert("ApiOpError".to_string());
516 used_type_names.extend(
517 [
518 "ClientBuilder",
519 "ClientWithMiddleware",
520 "RetryConfig",
521 "HttpError",
522 "BTreeMap",
523 ]
524 .into_iter()
525 .map(str::to_string),
526 );
527 for operation in operations {
528 used_type_names.insert(self.op_error_enum_ident(operation).to_string());
529 used_type_names.extend(
530 operation
531 .parameters
532 .iter()
533 .filter(|parameter| parameter.enum_values.is_some())
534 .map(|parameter| parameter.rust_type.clone()),
535 );
536 }
537
538 let mut definitions = Vec::new();
539 let mut entries = Vec::new();
540 for operation in operations {
541 let allocated_params = self.allocated_operation_params(operation);
542 let body_plan = self.body_model_plan(operation, analysis);
543 let optional_param_count = allocated_params
544 .iter()
545 .filter(|allocated| !Self::builder_param_is_required(allocated.param))
546 .count();
547 let optional_body_count =
548 usize::from(operation.request_body.is_some() && !operation.request_body_required);
549 let body_field_count = body_plan
550 .as_ref()
551 .filter(|plan| {
552 operation.request_body_required
553 || matches!(
554 &plan.required_construction,
555 RequiredBodyConstruction::Default
556 )
557 })
558 .map_or(0, |plan| plan.optional_fields.len());
559 let optional_count = optional_param_count + optional_body_count + body_field_count;
560 if optional_count <= self.config().builders.threshold {
561 continue;
562 }
563
564 let flat_method = self.get_method_name(operation);
565 let entry_base = format!("{flat_method}_builder");
566 let entry_name = Self::allocate_name(&entry_base, &mut used_entry_methods);
567 let entry_ident = Self::to_field_ident(&entry_name);
568
569 let builder_base = format!("{}Builder", flat_method.to_string().to_pascal_case());
570 let builder_name = Self::allocate_type_name(&builder_base, &mut used_type_names);
571 let builder_ident = format_ident!("{builder_name}");
572
573 let (definition, entry) = self.generate_single_operation_builder(
574 operation,
575 &allocated_params,
576 body_plan,
577 &flat_method,
578 &entry_ident,
579 &builder_ident,
580 );
581 definitions.push(definition);
582 entries.push(entry);
583 }
584
585 (definitions, entries)
586 }
587
588 fn generate_single_operation_builder(
589 &self,
590 operation: &OperationInfo,
591 allocated_params: &[AllocatedOperationParam<'_>],
592 body_plan: Option<BodyModelPlan>,
593 flat_method: &syn::Ident,
594 entry_ident: &syn::Ident,
595 builder_ident: &syn::Ident,
596 ) -> (TokenStream, TokenStream) {
597 let mut fields = vec![quote! { client: &'a HttpClient }];
598 let mut entry_parameters = Vec::new();
599 let mut initializers = vec![quote! { client: self }];
600 let mut call_arguments = Vec::new();
601 let mut setters = Vec::new();
602 let mut used_entry_params = std::collections::HashSet::new();
603 let mut used_methods = std::collections::HashSet::from(["send".to_string()]);
604
605 for allocated in allocated_params {
606 let field_ident = &allocated.ident;
607 let storage_type = self.builder_param_storage_type(allocated.param);
608 if Self::builder_param_is_required(allocated.param) {
609 fields.push(quote! { #field_ident: #storage_type });
610 let entry_name =
611 Self::allocate_name(&field_ident.to_string(), &mut used_entry_params);
612 let entry_param = Self::to_field_ident(&entry_name);
613 if Self::param_has_impl_as_ref_type(allocated.param) {
614 entry_parameters.push(quote! { #entry_param: impl Into<String> });
615 initializers.push(quote! { #field_ident: #entry_param.into() });
616 } else {
617 entry_parameters.push(quote! { #entry_param: #storage_type });
618 initializers.push(quote! { #field_ident: #entry_param });
619 }
620 } else {
621 fields.push(quote! { #field_ident: Option<#storage_type> });
622 initializers.push(quote! { #field_ident: None });
623 let setter_ident =
624 Self::allocate_builder_method(&field_ident.to_string(), &mut used_methods);
625 let wire_name = &allocated.param.name;
626 let assignment = if Self::param_has_impl_as_ref_type(allocated.param) {
627 quote! { self.#field_ident = Some(#field_ident.into()); }
628 } else {
629 quote! { self.#field_ident = Some(#field_ident); }
630 };
631 let setter_type = if Self::param_has_impl_as_ref_type(allocated.param) {
632 quote! { impl Into<String> }
633 } else {
634 storage_type.clone()
635 };
636 setters.push(quote! {
637 #[doc = concat!("Set the optional `", #wire_name, "` operation parameter.")]
638 #[must_use]
639 pub fn #setter_ident(mut self, #field_ident: #setter_type) -> Self {
640 #assignment
641 self
642 }
643 });
644 }
645 call_arguments.push(quote! { self.#field_ident });
646 }
647
648 if let Some(body_plan) = body_plan {
649 let BodyModelPlan {
650 body_ident,
651 body_type,
652 required_construction,
653 optional_fields,
654 } = body_plan;
655 let can_initialize_optional_body =
656 matches!(&required_construction, RequiredBodyConstruction::Default);
657 if operation.request_body_required {
658 fields.push(quote! { #body_ident: #body_type });
659 match required_construction {
660 RequiredBodyConstruction::Default => {
661 initializers.push(quote! { #body_ident: Default::default() });
662 }
663 RequiredBodyConstruction::New(constructor_params) => {
664 let mut constructor_args = Vec::new();
665 for constructor in constructor_params {
666 let preferred = constructor.preferred_ident.to_string();
667 let entry_name =
668 Self::allocate_name(&preferred, &mut used_entry_params);
669 let entry_param = Self::to_field_ident(&entry_name);
670 let value_type = constructor.value_type;
671 entry_parameters.push(quote! { #entry_param: #value_type });
672 constructor_args.push(entry_param);
673 }
674 initializers.push(quote! {
675 #body_ident: #body_type::new(#(#constructor_args),*)
676 });
677 }
678 RequiredBodyConstruction::Whole => {
679 let entry_name =
680 Self::allocate_name(&body_ident.to_string(), &mut used_entry_params);
681 let entry_param = Self::to_field_ident(&entry_name);
682 entry_parameters.push(quote! { #entry_param: #body_type });
683 initializers.push(quote! { #body_ident: #entry_param });
684 }
685 }
686 } else {
687 fields.push(quote! { #body_ident: Option<#body_type> });
688 initializers.push(quote! { #body_ident: None });
689 }
690
691 let body_setter =
692 Self::allocate_builder_method(&body_ident.to_string(), &mut used_methods);
693 let body_assignment = if operation.request_body_required {
694 quote! { self.#body_ident = #body_ident; }
695 } else {
696 quote! { self.#body_ident = Some(#body_ident); }
697 };
698 setters.push(quote! {
699 #[must_use]
701 pub fn #body_setter(mut self, #body_ident: #body_type) -> Self {
702 #body_assignment
703 self
704 }
705 });
706
707 if operation.request_body_required || can_initialize_optional_body {
708 for field in optional_fields {
709 let setter_ident = Self::allocate_builder_method(
710 &field.preferred_method_name,
711 &mut used_methods,
712 );
713 let value_ident = field.value_ident;
714 let value_type = field.value_type;
715 let wire_name = field.wire_name;
716 let access_path = field.access_path;
717 let assignment = if operation.request_body_required {
718 let mut target = quote! { self.#body_ident };
719 for access in &access_path {
720 target = quote! { #target.#access };
721 }
722 quote! { #target = Some(#value_ident); }
723 } else {
724 let mut target = quote! { request };
725 for access in &access_path {
726 target = quote! { #target.#access };
727 }
728 quote! {
729 let request = self.#body_ident.get_or_insert_with(Default::default);
730 #target = Some(#value_ident);
731 }
732 };
733 setters.push(quote! {
734 #[doc = concat!("Set the optional request-body field `", #wire_name, "`.")]
735 #[must_use]
736 pub fn #setter_ident(mut self, #value_ident: #value_type) -> Self {
737 #assignment
738 self
739 }
740 });
741 }
742 }
743 call_arguments.push(quote! { self.#body_ident });
744 }
745
746 let response_type = self.get_response_type(operation);
747 let error_type = self.op_error_type_token(operation);
748 let operation_id = &operation.operation_id;
749 let definition = quote! {
750 #[doc = concat!("Additive request builder for `", #operation_id, "`.")]
751 #[must_use]
752 pub struct #builder_ident<'a> {
753 #(#fields,)*
754 }
755
756 impl<'a> #builder_ident<'a> {
757 #(#setters)*
758
759 pub async fn send(self) -> Result<#response_type, ApiOpError<#error_type>> {
761 self.client.#flat_method(#(#call_arguments),*).await
762 }
763 }
764 };
765 let entry = quote! {
766 #[doc = concat!("Start an additive builder for `", #operation_id, "`.")]
767 pub fn #entry_ident(
768 &self,
769 #(#entry_parameters),*
770 ) -> #builder_ident<'_> {
771 #builder_ident {
772 #(#initializers,)*
773 }
774 }
775 };
776 (definition, entry)
777 }
778
779 fn allocate_name(base: &str, used: &mut std::collections::HashSet<String>) -> String {
780 let mut candidate = base.to_string();
781 let mut suffix = 2;
782 while !used.insert(candidate.clone()) {
783 candidate = format!("{base}_{suffix}");
784 suffix += 1;
785 }
786 candidate
787 }
788
789 fn allocate_type_name(base: &str, used: &mut std::collections::HashSet<String>) -> String {
790 if used.insert(base.to_string()) {
791 return base.to_string();
792 }
793
794 let mut suffix = 2;
795 loop {
796 let candidate = format!("{base}{suffix}");
797 if used.insert(candidate.clone()) {
798 return candidate;
799 }
800 suffix += 1;
801 }
802 }
803
804 fn allocate_builder_method(
805 preferred: &str,
806 used: &mut std::collections::HashSet<String>,
807 ) -> syn::Ident {
808 let plain = preferred.strip_prefix("r#").unwrap_or(preferred);
809 let base = if used.contains(preferred) {
810 format!("with_{plain}")
811 } else {
812 preferred.to_string()
813 };
814 let allocated = Self::allocate_name(&base, used);
815 Self::to_field_ident(&allocated)
816 }
817
818 fn allocated_operation_params<'a>(
819 &self,
820 operation: &'a OperationInfo,
821 ) -> Vec<AllocatedOperationParam<'a>> {
822 let mut used = std::collections::HashSet::from([
826 "client".to_string(),
827 "request".to_string(),
828 "form".to_string(),
829 "body".to_string(),
830 ]);
831 let mut allocated = Vec::new();
832 for location in ["path", "query", "header", "cookie"] {
833 for parameter in &operation.parameters {
834 if parameter.location != location {
835 continue;
836 }
837 let raw = self.param_ident_str(parameter);
838 let chosen = Self::allocate_name(&raw, &mut used);
839 allocated.push(AllocatedOperationParam {
840 param: parameter,
841 ident: Self::to_field_ident(&chosen),
842 });
843 }
844 }
845 allocated
846 }
847
848 fn builder_param_is_required(parameter: &ParameterInfo) -> bool {
849 parameter.location == "path" || parameter.required
853 }
854
855 fn builder_param_storage_type(&self, parameter: &ParameterInfo) -> TokenStream {
856 self.get_param_owned_rust_type(parameter)
857 }
858
859 fn param_has_impl_as_ref_type(parameter: &ParameterInfo) -> bool {
860 !matches!(
861 ¶meter.query_serialization,
862 Some(
863 crate::analysis::QuerySerialization::FormExplodedArray { .. }
864 | crate::analysis::QuerySerialization::FormArray { .. }
865 )
866 ) && Self::param_uses_as_ref_str(parameter)
867 }
868
869 fn body_model_plan(
870 &self,
871 operation: &OperationInfo,
872 analysis: &SchemaAnalysis,
873 ) -> Option<BodyModelPlan> {
874 use crate::analysis::{ObjectAdditionalProperties, RequestBodyContent, SchemaType};
875
876 let request_body = operation.request_body.as_ref()?;
877 let (body_name, body_ident) = match request_body {
878 RequestBodyContent::Json { schema_name, .. }
879 | RequestBodyContent::FormUrlEncoded { schema_name, .. } => {
880 (schema_name.as_str(), format_ident!("request"))
881 }
882 RequestBodyContent::Multipart => {
883 return Some(BodyModelPlan {
884 body_ident: format_ident!("form"),
885 body_type: quote! { reqwest::multipart::Form },
886 required_construction: RequiredBodyConstruction::Whole,
887 optional_fields: Vec::new(),
888 });
889 }
890 RequestBodyContent::OctetStream | RequestBodyContent::Unsupported { .. } => {
891 return Some(BodyModelPlan {
892 body_ident: format_ident!("body"),
893 body_type: quote! { Vec<u8> },
894 required_construction: RequiredBodyConstruction::Whole,
895 optional_fields: Vec::new(),
896 });
897 }
898 RequestBodyContent::TextPlain => {
899 return Some(BodyModelPlan {
900 body_ident: format_ident!("body"),
901 body_type: quote! { String },
902 required_construction: RequiredBodyConstruction::Whole,
903 optional_fields: Vec::new(),
904 });
905 }
906 RequestBodyContent::SchemaLess { .. } => return None,
907 };
908 let body_type_name = self.to_rust_type_name(body_name);
909 let body_type = syn::Ident::new(&body_type_name, proc_macro2::Span::call_site());
910 let Some((resolved_name, resolved_schema)) =
911 self.resolve_reference_schema(body_name, analysis)
912 else {
913 return Some(BodyModelPlan {
914 body_ident,
915 body_type: quote! { #body_type },
916 required_construction: RequiredBodyConstruction::Whole,
917 optional_fields: Vec::new(),
918 });
919 };
920
921 let mut optional_fields = Vec::new();
922 let mut stack = std::collections::HashSet::new();
923 self.collect_optional_body_fields(
924 resolved_name,
925 Vec::new(),
926 analysis,
927 &mut stack,
928 &mut optional_fields,
929 );
930
931 let required_construction = match &resolved_schema.schema_type {
932 SchemaType::Object {
933 properties,
934 required,
935 additional_properties,
936 } if !self.is_discriminated_variant(resolved_name, analysis) => {
937 let emitted = self.emitted_object_properties(
938 resolved_name,
939 properties,
940 required,
941 additional_properties,
942 analysis,
943 None,
944 );
945 let required_fields: Vec<_> = emitted
946 .iter()
947 .filter(|field| field.is_required)
948 .map(|field| BodyConstructorParam {
949 preferred_ident: field.ident.clone(),
950 value_type: field.field_type.clone(),
951 })
952 .collect();
953 if required_fields.is_empty() {
954 RequiredBodyConstruction::Default
955 } else if emitted.iter().any(|field| !field.is_required)
956 || !matches!(additional_properties, ObjectAdditionalProperties::Forbidden)
957 {
958 RequiredBodyConstruction::New(required_fields)
959 } else {
960 RequiredBodyConstruction::Whole
961 }
962 }
963 _ => RequiredBodyConstruction::Whole,
964 };
965
966 Some(BodyModelPlan {
967 body_ident,
968 body_type: quote! { #body_type },
969 required_construction,
970 optional_fields,
971 })
972 }
973
974 fn resolve_reference_schema<'a>(
975 &self,
976 schema_name: &'a str,
977 analysis: &'a SchemaAnalysis,
978 ) -> Option<(&'a str, &'a crate::analysis::AnalyzedSchema)> {
979 let mut current = schema_name;
980 let mut visited = std::collections::HashSet::new();
981 loop {
982 if !visited.insert(current) {
983 return None;
984 }
985 let schema = analysis.schemas.get(current)?;
986 if let crate::analysis::SchemaType::Reference { target } = &schema.schema_type {
987 current = target;
988 } else {
989 return Some((current, schema));
990 }
991 }
992 }
993
994 fn collect_optional_body_fields(
995 &self,
996 schema_name: &str,
997 access_path: Vec<syn::Ident>,
998 analysis: &SchemaAnalysis,
999 stack: &mut std::collections::HashSet<String>,
1000 output: &mut Vec<BodyFieldPlan>,
1001 ) {
1002 use crate::analysis::SchemaType;
1003 if !stack.insert(schema_name.to_string()) {
1004 return;
1005 }
1006 let Some(schema) = analysis.schemas.get(schema_name) else {
1007 stack.remove(schema_name);
1008 return;
1009 };
1010 match &schema.schema_type {
1011 SchemaType::Reference { target } => {
1012 self.collect_optional_body_fields(target, access_path, analysis, stack, output);
1013 }
1014 SchemaType::Object {
1015 properties,
1016 required,
1017 additional_properties,
1018 } if !self.is_discriminated_variant(schema_name, analysis) => {
1019 for field in self.emitted_object_properties(
1020 schema_name,
1021 properties,
1022 required,
1023 additional_properties,
1024 analysis,
1025 None,
1026 ) {
1027 if field.is_required {
1028 continue;
1029 }
1030 let mut field_path = access_path.clone();
1031 field_path.push(field.ident.clone());
1032 output.push(BodyFieldPlan {
1033 wire_name: field.wire_name.to_string(),
1034 preferred_method_name: field.ident.to_string(),
1035 value_ident: field.ident.clone(),
1036 value_type: self.generate_property_base_type(
1037 schema_name,
1038 field.wire_name,
1039 field.property,
1040 analysis,
1041 ),
1042 access_path: field_path,
1043 });
1044 }
1045 }
1046 SchemaType::Composition { schemas } => {
1047 for (index, schema_ref) in schemas.iter().enumerate() {
1048 let mut nested_path = access_path.clone();
1049 nested_path.push(format_ident!("part_{index}"));
1050 self.collect_optional_body_fields(
1051 &schema_ref.target,
1052 nested_path,
1053 analysis,
1054 stack,
1055 output,
1056 );
1057 }
1058 }
1059 _ => {}
1060 }
1061 stack.remove(schema_name);
1062 }
1063
1064 fn is_discriminated_variant(&self, schema_name: &str, analysis: &SchemaAnalysis) -> bool {
1065 analysis.schemas.values().any(|schema| {
1066 matches!(
1067 &schema.schema_type,
1068 crate::analysis::SchemaType::DiscriminatedUnion { variants, .. }
1069 if variants.iter().any(|variant| variant.type_name == schema_name)
1070 )
1071 })
1072 }
1073
1074 fn generate_param_enum_types(&self, operations: &[&OperationInfo]) -> TokenStream {
1079 let mut by_name: BTreeMap<String, &ParameterInfo> = BTreeMap::new();
1080 for op in operations {
1081 for param in &op.parameters {
1082 if param.enum_values.is_some() {
1083 by_name.entry(param.rust_type.clone()).or_insert(param);
1084 }
1085 }
1086 }
1087
1088 if by_name.is_empty() {
1089 return quote! {};
1090 }
1091
1092 let defs: Vec<TokenStream> = by_name
1093 .values()
1094 .map(|param| self.generate_single_param_enum(param))
1095 .collect();
1096
1097 quote! { #(#defs)* }
1098 }
1099
1100 fn generate_single_param_enum(&self, param: &ParameterInfo) -> TokenStream {
1101 let Some(values) = param.enum_values.as_deref() else {
1102 return quote! {};
1103 };
1104
1105 let enum_ident = format_ident!("{}", param.rust_type);
1106
1107 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
1113 let variant_names: Vec<String> = values
1121 .iter()
1122 .enumerate()
1123 .map(|(index, value)| {
1124 let base = param
1125 .enum_varnames
1126 .as_ref()
1127 .and_then(|names| names.get(index))
1128 .map(|name| self.to_rust_enum_variant(name))
1129 .unwrap_or_else(|| self.to_rust_enum_variant(value));
1130 let mut chosen = base.clone();
1131 let mut suffix = 2;
1132 while !used.insert(chosen.clone()) {
1133 chosen = format!("{base}_{suffix}");
1134 suffix += 1;
1135 }
1136 chosen
1137 })
1138 .collect();
1139
1140 let variants: Vec<TokenStream> = values
1141 .iter()
1142 .zip(&variant_names)
1143 .map(|(value, name)| {
1144 let variant_ident = format_ident!("{}", name);
1145 quote! {
1146 #[serde(rename = #value)]
1147 #variant_ident,
1148 }
1149 })
1150 .collect();
1151
1152 let display_arms: Vec<TokenStream> = values
1153 .iter()
1154 .zip(&variant_names)
1155 .map(|(value, name)| {
1156 let variant_ident = format_ident!("{}", name);
1157 quote! { Self::#variant_ident => #value, }
1158 })
1159 .collect();
1160
1161 let doc = format!(
1162 "Allowed values for the `{}` {} parameter.",
1163 param.name, param.location
1164 );
1165
1166 quote! {
1167 #[doc = #doc]
1168 #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
1169 pub enum #enum_ident {
1170 #(#variants)*
1171 }
1172
1173 impl #enum_ident {
1174 pub fn as_str(&self) -> &'static str {
1175 match self {
1176 #(#display_arms)*
1177 }
1178 }
1179 }
1180
1181 impl std::fmt::Display for #enum_ident {
1182 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1183 f.write_str(self.as_str())
1184 }
1185 }
1186
1187 impl AsRef<str> for #enum_ident {
1188 fn as_ref(&self) -> &str {
1189 self.as_str()
1190 }
1191 }
1192 }
1193 }
1194
1195 fn generate_op_error_enum(&self, op: &OperationInfo) -> Option<TokenStream> {
1200 let variants: Vec<(String, String)> = op
1201 .response_schemas
1202 .iter()
1203 .filter(|(code, _)| !code.starts_with('2'))
1204 .map(|(code, schema)| (code.clone(), schema.clone()))
1205 .collect();
1206
1207 if variants.is_empty() {
1208 return None;
1209 }
1210
1211 let enum_ident = self.op_error_enum_ident(op);
1212 let variant_decls: Vec<TokenStream> = variants
1213 .iter()
1214 .map(|(code, schema)| {
1215 let variant_ident = Self::op_error_variant_ident(code);
1216 let payload_ty_name = self.to_rust_type_name(schema);
1217 let payload_ty = syn::Ident::new(&payload_ty_name, proc_macro2::Span::call_site());
1218 quote! { #variant_ident(#payload_ty) }
1219 })
1220 .collect();
1221
1222 let doc = format!(
1223 "Typed error responses for `{}`. One variant per declared non-2xx response.",
1224 op.operation_id
1225 );
1226
1227 Some(quote! {
1228 #[doc = #doc]
1229 #[derive(Debug, Clone)]
1230 pub enum #enum_ident {
1231 #(#variant_decls,)*
1232 }
1233 })
1234 }
1235
1236 fn op_error_enum_ident(&self, op: &OperationInfo) -> syn::Ident {
1238 use heck::ToPascalCase;
1239 let name = format!(
1240 "{}ApiError",
1241 op.operation_id.replace('.', "_").to_pascal_case()
1242 );
1243 syn::Ident::new(&name, proc_macro2::Span::call_site())
1244 }
1245
1246 fn op_error_variant_ident(status_code: &str) -> syn::Ident {
1249 let raw = match status_code {
1250 "default" | "Default" => "Default".to_string(),
1251 other if other.chars().all(|c| c.is_ascii_digit()) => format!("Status{other}"),
1252 other => format!("Status{}", other.to_ascii_lowercase()),
1253 };
1254 syn::Ident::new(&raw, proc_macro2::Span::call_site())
1255 }
1256
1257 fn op_error_type_token(&self, op: &OperationInfo) -> TokenStream {
1261 if op
1262 .response_schemas
1263 .iter()
1264 .any(|(code, _)| !code.starts_with('2'))
1265 {
1266 let ident = self.op_error_enum_ident(op);
1267 quote! { #ident }
1268 } else {
1269 quote! { serde_json::Value }
1270 }
1271 }
1272
1273 fn generate_single_operation_method(&self, op: &OperationInfo) -> TokenStream {
1275 let method_name = self.get_method_name(op);
1276 let http_method_call = self.http_method_call(op);
1277 let path = &op.path;
1278 let request_param = self.generate_request_param(op);
1279 let request_body = self.generate_request_body(op);
1280 let query_params = self.generate_query_params(op);
1281 let header_params = self.generate_header_params(op);
1282 let cookie_params = self.generate_cookie_params(op);
1283 let auth_application = self.generate_auth_application();
1284 let response_type = self.get_response_type(op);
1285 let has_response_body = self.get_success_response_schema(op).is_some();
1286 let op_error_type = self.op_error_type_token(op);
1287 let error_handling = self.generate_error_handling(op, has_response_body);
1288 let url_construction = self.generate_url_construction(path, op);
1289 let doc_comment = self.generate_operation_doc_comment(op);
1290
1291 quote! {
1292 #doc_comment
1293 pub async fn #method_name(
1294 &self,
1295 #request_param
1296 ) -> Result<#response_type, ApiOpError<#op_error_type>> {
1297 #url_construction
1298
1299 let mut req = #http_method_call;
1300 #request_body
1301
1302 #query_params
1303 #header_params
1304 #cookie_params
1305
1306 #auth_application
1309
1310 for (name, value) in &self.custom_headers {
1312 req = req.header(name, value);
1313 }
1314
1315 let response = req.send().await?;
1316 #error_handling
1317 }
1318 }
1319 }
1320
1321 fn generate_auth_application(&self) -> TokenStream {
1325 use crate::http_config::AuthConfig;
1326 match &self.config().auth_config {
1327 Some(AuthConfig::Bearer { header_name }) if header_name == "Authorization" => quote! {
1328 if let Some(api_key) = &self.api_key {
1329 req = req.bearer_auth(api_key);
1330 }
1331 },
1332 Some(AuthConfig::Bearer { header_name }) => {
1333 let h = header_name.clone();
1334 quote! {
1335 if let Some(api_key) = &self.api_key {
1336 req = req.header(#h, format!("Bearer {}", api_key));
1337 }
1338 }
1339 }
1340 Some(AuthConfig::ApiKey { header_name }) => {
1341 let h = header_name.clone();
1342 quote! {
1343 if let Some(api_key) = &self.api_key {
1344 req = req.header(#h, api_key.as_str());
1345 }
1346 }
1347 }
1348 Some(AuthConfig::Custom {
1349 header_name,
1350 header_value_prefix,
1351 }) => {
1352 let h = header_name.clone();
1353 let prefix = header_value_prefix.clone().unwrap_or_default();
1354 if prefix.is_empty() {
1355 quote! {
1356 if let Some(api_key) = &self.api_key {
1357 req = req.header(#h, api_key.as_str());
1358 }
1359 }
1360 } else {
1361 let format_str = format!("{}{{}}", prefix);
1362 quote! {
1363 if let Some(api_key) = &self.api_key {
1364 req = req.header(#h, format!(#format_str, api_key));
1365 }
1366 }
1367 }
1368 }
1369 None => quote! {
1370 if let Some(api_key) = &self.api_key {
1371 req = req.bearer_auth(api_key);
1372 }
1373 },
1374 }
1375 }
1376
1377 fn generate_header_params(&self, op: &OperationInfo) -> TokenStream {
1381 let header_params: Vec<_> = op
1382 .parameters
1383 .iter()
1384 .filter(|p| p.location == "header")
1385 .collect();
1386 if header_params.is_empty() {
1387 return quote! {};
1388 }
1389 let mut emit = Vec::new();
1390 for param in header_params {
1391 let param_name_snake = self.param_ident_str(param);
1392 let param_ident = Self::to_field_ident(¶m_name_snake);
1393 let header_name = ¶m.name;
1394 if param.required {
1395 if Self::param_uses_as_ref_str(param) {
1396 emit.push(quote! {
1397 req = req.header(#header_name, #param_ident.as_ref());
1398 });
1399 } else {
1400 emit.push(quote! {
1401 req = req.header(#header_name, #param_ident.to_string());
1402 });
1403 }
1404 } else if Self::param_uses_as_ref_str(param) {
1405 emit.push(quote! {
1406 if let Some(v) = #param_ident {
1407 req = req.header(#header_name, v.as_ref());
1408 }
1409 });
1410 } else {
1411 emit.push(quote! {
1412 if let Some(v) = #param_ident {
1413 req = req.header(#header_name, v.to_string());
1414 }
1415 });
1416 }
1417 }
1418 quote! {
1419 #(#emit)*
1420 }
1421 }
1422
1423 fn generate_cookie_params(&self, op: &OperationInfo) -> TokenStream {
1424 let cookie_params: Vec<_> = op
1425 .parameters
1426 .iter()
1427 .filter(|parameter| parameter.location == "cookie")
1428 .collect();
1429 if cookie_params.is_empty() {
1430 return quote! {};
1431 }
1432 let mut emit = Vec::new();
1433 for parameter in cookie_params {
1434 let ident = Self::to_field_ident(&self.param_ident_str(parameter));
1435 let wire_name = parameter.name.as_str();
1436 if parameter.required {
1437 emit.push(quote! {
1438 __cookie_fields.push(format!("{}={}", #wire_name, #ident));
1439 });
1440 } else {
1441 emit.push(quote! {
1442 if let Some(value) = #ident {
1443 __cookie_fields.push(format!("{}={}", #wire_name, value));
1444 }
1445 });
1446 }
1447 }
1448 quote! {
1449 let mut __cookie_fields = Vec::new();
1450 #(#emit)*
1451 if !__cookie_fields.is_empty() {
1452 req = req.header(::reqwest::header::COOKIE, __cookie_fields.join("; "));
1453 }
1454 }
1455 }
1456
1457 fn generate_query_params(&self, op: &OperationInfo) -> TokenStream {
1459 let query_params: Vec<_> = op
1460 .parameters
1461 .iter()
1462 .filter(|p| p.location == "query")
1463 .collect();
1464
1465 if query_params.is_empty() {
1466 return quote! {};
1467 }
1468
1469 let mut param_building = Vec::new();
1470 let mut req_appends = Vec::new();
1474
1475 for param in query_params {
1476 use crate::analysis::QuerySerialization;
1477
1478 let param_name_snake = self.param_ident_str(param);
1480 let param_name = Self::to_field_ident(¶m_name_snake);
1481
1482 let param_key = ¶m.name;
1484
1485 match ¶m.query_serialization {
1486 Some(QuerySerialization::FormExplodedObject) => {
1487 let apply = quote! {
1494 let __empty = match serde_json::to_value(&v)
1495 .map_err(HttpError::serialization_error)?
1496 {
1497 serde_json::Value::Object(map) => map.is_empty(),
1498 _ => false,
1499 };
1500 if __empty {
1501 req = req.query(&[(format!("{}[]", #param_key), String::new())]);
1502 } else {
1503 req = req.query(&v);
1504 }
1505 };
1506 if param.required {
1507 req_appends.push(quote! {
1508 {
1509 let v = #param_name;
1510 #apply
1511 }
1512 });
1513 } else {
1514 req_appends.push(quote! {
1515 if let Some(v) = #param_name {
1516 #apply
1517 }
1518 });
1519 }
1520 continue;
1521 }
1522 Some(QuerySerialization::DeepObject) => {
1523 let apply = quote! {
1527 let map = match serde_json::to_value(&v)
1528 .map_err(HttpError::serialization_error)?
1529 {
1530 serde_json::Value::Object(map) => map,
1531 _ => return Err(HttpError::serialization_error(
1532 format!("query parameter `{}` did not serialize as an object", #param_key)
1533 ).into()),
1534 };
1535 let mut deep_params: Vec<(String, String)> = Vec::new();
1536 for (k, val) in map {
1537 let s = match val {
1538 serde_json::Value::Null => continue,
1539 serde_json::Value::String(s) => s,
1540 other => other.to_string(),
1541 };
1542 deep_params.push((format!("{}[{}]", #param_key, k), s));
1543 }
1544 if deep_params.is_empty() {
1545 deep_params.push((format!("{}[]", #param_key), String::new()));
1546 }
1547 req = req.query(&deep_params);
1548 };
1549 if param.required {
1550 req_appends.push(quote! {
1551 {
1552 let v = #param_name;
1553 #apply
1554 }
1555 });
1556 } else {
1557 req_appends.push(quote! {
1558 if let Some(v) = #param_name {
1559 #apply
1560 }
1561 });
1562 }
1563 continue;
1564 }
1565 Some(QuerySerialization::FormObject) => {
1566 let apply = quote! {
1570 let map = match serde_json::to_value(&v)
1571 .map_err(HttpError::serialization_error)?
1572 {
1573 serde_json::Value::Object(map) => map,
1574 _ => return Err(HttpError::serialization_error(
1575 format!("query parameter `{}` did not serialize as an object", #param_key)
1576 ).into()),
1577 };
1578 let mut parts: Vec<String> = Vec::new();
1579 for (k, val) in map {
1580 let s = match val {
1581 serde_json::Value::Null => continue,
1582 serde_json::Value::String(s) => s,
1583 other => other.to_string(),
1584 };
1585 if k.contains(',') || s.contains(',') {
1586 return Err(HttpError::serialization_error(
1587 format!(
1588 "query object `{}` contains a comma in key `{}`; use explode=true for lossless string values",
1589 #param_key,
1590 k,
1591 )
1592 ).into());
1593 }
1594 parts.push(k);
1595 parts.push(s);
1596 }
1597 if parts.is_empty() {
1598 query_params.push((
1599 format!("{}[]", #param_key),
1600 String::new(),
1601 ));
1602 } else {
1603 query_params.push((#param_key.to_string(), parts.join(",")));
1604 }
1605 };
1606 if param.required {
1607 param_building.push(quote! {
1608 {
1609 let v = #param_name;
1610 #apply
1611 }
1612 });
1613 } else {
1614 param_building.push(quote! {
1615 if let Some(v) = #param_name {
1616 #apply
1617 }
1618 });
1619 }
1620 continue;
1621 }
1622 Some(QuerySerialization::FormExplodedArray { .. }) => {
1623 if param.required {
1625 param_building.push(quote! {
1626 if #param_name.is_empty() {
1627 query_params.push((
1628 format!("{}[]", #param_key),
1629 String::new(),
1630 ));
1631 } else {
1632 for item in #param_name {
1633 query_params.push((#param_key.to_string(), item.to_string()));
1634 }
1635 }
1636 });
1637 } else {
1638 param_building.push(quote! {
1639 if let Some(v) = #param_name {
1640 if v.is_empty() {
1641 query_params.push((
1642 format!("{}[]", #param_key),
1643 String::new(),
1644 ));
1645 } else {
1646 for item in v {
1647 query_params.push((#param_key.to_string(), item.to_string()));
1648 }
1649 }
1650 }
1651 });
1652 }
1653 continue;
1654 }
1655 Some(QuerySerialization::FormArray { .. }) => {
1656 let apply = quote! {
1659 if v.is_empty() {
1660 query_params.push((
1661 format!("{}[]", #param_key),
1662 String::new(),
1663 ));
1664 } else {
1665 let mut parts = Vec::with_capacity(v.len());
1666 for item in &v {
1667 let item = item.to_string();
1668 if item.contains(',') {
1669 return Err(HttpError::serialization_error(
1670 format!(
1671 "query array `{}` contains a comma; use explode=true for lossless string values",
1672 #param_key,
1673 )
1674 ).into());
1675 }
1676 parts.push(item);
1677 }
1678 query_params.push((
1679 #param_key.to_string(),
1680 parts.join(","),
1681 ));
1682 }
1683 };
1684 if param.required {
1685 param_building.push(quote! {
1686 {
1687 let v = #param_name;
1688 #apply
1689 }
1690 });
1691 } else {
1692 param_building.push(quote! {
1693 if let Some(v) = #param_name {
1694 #apply
1695 }
1696 });
1697 }
1698 continue;
1699 }
1700 Some(QuerySerialization::Unsupported { .. }) => {}
1701 None => {}
1702 }
1703
1704 if param.required {
1705 if Self::param_uses_as_ref_str(param) {
1707 param_building.push(quote! {
1708 query_params.push((#param_key.to_string(), #param_name.as_ref().to_string()));
1709 });
1710 } else {
1711 param_building.push(quote! {
1712 query_params.push((#param_key.to_string(), #param_name.to_string()));
1713 });
1714 }
1715 } else {
1716 if Self::param_uses_as_ref_str(param) {
1718 param_building.push(quote! {
1719 if let Some(v) = #param_name {
1720 query_params.push((#param_key.to_string(), v.as_ref().to_string()));
1721 }
1722 });
1723 } else {
1724 param_building.push(quote! {
1725 if let Some(v) = #param_name {
1726 query_params.push((#param_key.to_string(), v.to_string()));
1727 }
1728 });
1729 }
1730 }
1731 }
1732
1733 let pairs_block = if param_building.is_empty() {
1736 quote! {}
1737 } else {
1738 quote! {
1739 {
1740 let mut query_params: Vec<(String, String)> = Vec::new();
1741 #(#param_building)*
1742 if !query_params.is_empty() {
1743 req = req.query(&query_params);
1744 }
1745 }
1746 }
1747 };
1748
1749 quote! {
1750 #pairs_block
1752 #(#req_appends)*
1753 }
1754 }
1755
1756 fn generate_operation_doc_comment(&self, op: &OperationInfo) -> TokenStream {
1760 let method = op.method.to_uppercase();
1761 let path = &op.path;
1762 let mut docs: Vec<String> = Vec::new();
1763 if let Some(s) = &op.summary {
1764 if !s.is_empty() {
1765 docs.push(s.clone());
1766 docs.push(String::new());
1767 }
1768 }
1769 if let Some(d) = &op.description {
1770 if !d.is_empty() {
1771 for line in d.lines() {
1772 docs.push(line.to_string());
1773 }
1774 docs.push(String::new());
1775 }
1776 }
1777 docs.push(format!("`{} {}`", method, path));
1778 let doc_attrs: Vec<TokenStream> = docs
1779 .iter()
1780 .map(|line| {
1781 let prefixed = if line.is_empty() {
1782 String::new()
1783 } else {
1784 format!(" {line}")
1785 };
1786 quote! { #[doc = #prefixed] }
1787 })
1788 .collect();
1789 quote! { #(#doc_attrs)* }
1790 }
1791
1792 fn get_method_name(&self, op: &OperationInfo) -> syn::Ident {
1794 let name = if !op.operation_id.is_empty() {
1795 op.operation_id.to_snake_case()
1796 } else {
1797 format!(
1799 "{}_{}",
1800 op.method,
1801 op.path.replace('/', "_").replace(['{', '}'], "")
1802 )
1803 .to_snake_case()
1804 };
1805
1806 syn::Ident::new(&name, proc_macro2::Span::call_site())
1807 }
1808
1809 fn http_method_call(&self, op: &OperationInfo) -> TokenStream {
1814 match op.method.to_uppercase().as_str() {
1815 "GET" => quote! { self.http_client.get(request_url) },
1816 "POST" => quote! { self.http_client.post(request_url) },
1817 "PUT" => quote! { self.http_client.put(request_url) },
1818 "DELETE" => quote! { self.http_client.delete(request_url) },
1819 "PATCH" => quote! { self.http_client.patch(request_url) },
1820 "HEAD" => quote! { self.http_client.head(request_url) },
1821 "OPTIONS" => quote! {
1822 self.http_client.request(reqwest::Method::OPTIONS, request_url)
1823 },
1824 "TRACE" => quote! {
1825 self.http_client.request(reqwest::Method::TRACE, request_url)
1826 },
1827 other => {
1832 let upper = other.to_string();
1833 quote! {
1834 self.http_client.request(
1835 reqwest::Method::from_bytes(#upper.as_bytes())
1836 .expect("invalid HTTP method"),
1837 request_url,
1838 )
1839 }
1840 }
1841 }
1842 }
1843
1844 fn generate_request_param(&self, op: &OperationInfo) -> TokenStream {
1846 let mut params = Vec::new();
1847 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
1854 let mut unique_param_ident = |raw: String| -> syn::Ident {
1855 let mut chosen = raw.clone();
1856 let mut suffix = 2;
1857 while !used.insert(chosen.clone()) {
1858 chosen = format!("{raw}_{suffix}");
1859 suffix += 1;
1860 }
1861 Self::to_field_ident(&chosen)
1862 };
1863
1864 for param in &op.parameters {
1866 if param.location == "path" {
1867 let param_name_snake = self.param_ident_str(param);
1868 let param_name = unique_param_ident(param_name_snake);
1869 let param_type = self.get_param_rust_type(param);
1870 params.push(quote! { #param_name: #param_type });
1871 }
1872 }
1873
1874 for param in &op.parameters {
1876 if param.location == "query" {
1877 let param_name_snake = self.param_ident_str(param);
1878 let param_name = unique_param_ident(param_name_snake);
1879 let param_type = self.get_param_rust_type(param);
1880
1881 if param.required {
1883 params.push(quote! { #param_name: #param_type });
1884 } else {
1885 params.push(quote! { #param_name: Option<#param_type> });
1886 }
1887 }
1888 }
1889
1890 for param in &op.parameters {
1896 if param.location == "header" {
1897 let param_name_snake = self.param_ident_str(param);
1898 let param_name = unique_param_ident(param_name_snake);
1899 let param_type = self.get_param_rust_type(param);
1900 if param.required {
1901 params.push(quote! { #param_name: #param_type });
1902 } else {
1903 params.push(quote! { #param_name: Option<#param_type> });
1904 }
1905 }
1906 }
1907
1908 for param in &op.parameters {
1909 if param.location == "cookie" {
1910 let param_name_snake = self.param_ident_str(param);
1911 let param_name = unique_param_ident(param_name_snake);
1912 let param_type = self.get_param_rust_type(param);
1913 if param.required {
1914 params.push(quote! { #param_name: #param_type });
1915 } else {
1916 params.push(quote! { #param_name: Option<#param_type> });
1917 }
1918 }
1919 }
1920
1921 if let Some(ref rb) = op.request_body {
1924 use crate::analysis::RequestBodyContent;
1925 if matches!(rb, RequestBodyContent::SchemaLess { .. }) {
1926 return if params.is_empty() {
1927 quote! {}
1928 } else {
1929 quote! { #(#params),* }
1930 };
1931 }
1932 let required = op.request_body_required;
1933 let body_type = match rb {
1934 RequestBodyContent::Json { schema_name, .. }
1935 | RequestBodyContent::FormUrlEncoded { schema_name, .. } => {
1936 let rust_type_name = self.to_rust_type_name(schema_name);
1937 let request_ident =
1938 syn::Ident::new(&rust_type_name, proc_macro2::Span::call_site());
1939 quote! { #request_ident }
1940 }
1941 RequestBodyContent::Multipart => quote! { reqwest::multipart::Form },
1942 RequestBodyContent::OctetStream => quote! { Vec<u8> },
1943 RequestBodyContent::TextPlain => quote! { String },
1944 RequestBodyContent::Unsupported { .. } => quote! { Vec<u8> },
1945 RequestBodyContent::SchemaLess { .. } => unreachable!(
1946 "schema-less request bodies preserve the historical client signature"
1947 ),
1948 };
1949 let body_ident = match rb {
1950 RequestBodyContent::Multipart => quote! { form },
1951 RequestBodyContent::OctetStream
1952 | RequestBodyContent::TextPlain
1953 | RequestBodyContent::Unsupported { .. } => quote! { body },
1954 RequestBodyContent::SchemaLess { .. } => unreachable!(
1955 "schema-less request bodies preserve the historical client signature"
1956 ),
1957 _ => quote! { request },
1958 };
1959 if required {
1960 params.push(quote! { #body_ident: #body_type });
1961 } else {
1962 params.push(quote! { #body_ident: Option<#body_type> });
1963 }
1964 }
1965
1966 if params.is_empty() {
1967 quote! {}
1968 } else {
1969 quote! { #(#params),* }
1970 }
1971 }
1972
1973 fn get_param_rust_type(&self, param: &crate::analysis::ParameterInfo) -> TokenStream {
1975 if Self::param_has_impl_as_ref_type(param) {
1976 quote! { impl AsRef<str> }
1977 } else {
1978 self.get_param_owned_rust_type(param)
1979 }
1980 }
1981
1982 pub(crate) fn get_param_owned_rust_type(
1986 &self,
1987 param: &crate::analysis::ParameterInfo,
1988 ) -> TokenStream {
1989 use crate::analysis::QuerySerialization;
1990 if let Some(
1996 QuerySerialization::FormExplodedArray { item_type }
1997 | QuerySerialization::FormArray { item_type },
1998 ) = ¶m.query_serialization
1999 {
2000 use crate::analysis::ArrayItemType;
2001 let item_ty: syn::Type = match item_type {
2002 ArrayItemType::Scalar(rust_type) => syn::parse_str(rust_type)
2003 .unwrap_or_else(|_| panic!("invalid scalar item type `{rust_type}`")),
2004 ArrayItemType::EnumRef(schema_name) => {
2005 let rust_name = self.to_rust_type_name(schema_name);
2006 syn::parse_str(&rust_name)
2007 .unwrap_or_else(|_| panic!("invalid enum item type `{rust_name}`"))
2008 }
2009 };
2010 return quote! { Vec<#item_ty> };
2011 }
2012 if let Some(ref schema_name) = param.schema_ref {
2016 let rust_name = self.to_rust_type_name(schema_name);
2017 let ident = syn::Ident::new(&rust_name, proc_macro2::Span::call_site());
2018 return quote! { #ident };
2019 }
2020 syn::parse_str::<syn::Type>(¶m.rust_type)
2021 .map(|ty| quote! { #ty })
2022 .unwrap_or_else(|_| {
2023 let type_ident = syn::Ident::new(¶m.rust_type, proc_macro2::Span::call_site());
2024 quote! { #type_ident }
2025 })
2026 }
2027
2028 fn param_uses_as_ref_str(param: &crate::analysis::ParameterInfo) -> bool {
2033 param.schema_ref.is_none() && param.rust_type == "String"
2034 }
2035
2036 fn generate_request_body(&self, op: &OperationInfo) -> TokenStream {
2041 let Some(rb) = op.request_body.as_ref() else {
2042 return quote! {};
2043 };
2044 use crate::analysis::RequestBodyContent;
2045 let required = op.request_body_required;
2046 let (ident, apply): (TokenStream, TokenStream) = match rb {
2047 RequestBodyContent::Json { media_type, .. } => (
2048 quote! { request },
2049 quote! {
2050 req = req
2051 .body(serde_json::to_vec(&request).map_err(HttpError::serialization_error)?)
2052 .header("content-type", #media_type);
2053 },
2054 ),
2055 RequestBodyContent::FormUrlEncoded { media_type, .. } => (
2056 quote! { request },
2057 quote! {
2058 req = req
2059 .body(serde_urlencoded::to_string(&request).map_err(HttpError::serialization_error)?)
2060 .header("content-type", #media_type);
2061 },
2062 ),
2063 RequestBodyContent::Multipart => (
2064 quote! { form },
2065 quote! {
2066 req = req.multipart(form);
2067 },
2068 ),
2069 RequestBodyContent::OctetStream => (
2070 quote! { body },
2071 quote! {
2072 req = req
2073 .body(body)
2074 .header("content-type", "application/octet-stream");
2075 },
2076 ),
2077 RequestBodyContent::TextPlain => (
2078 quote! { body },
2079 quote! {
2080 req = req
2081 .body(body)
2082 .header("content-type", "text/plain");
2083 },
2084 ),
2085 RequestBodyContent::Unsupported { media_types } => {
2086 let media_type = media_types
2087 .first()
2088 .map(String::as_str)
2089 .unwrap_or("application/octet-stream");
2090 (
2091 quote! { body },
2092 quote! {
2093 req = req
2094 .body(body)
2095 .header("content-type", #media_type);
2096 },
2097 )
2098 }
2099 RequestBodyContent::SchemaLess { .. } => return quote! {},
2100 };
2101 if required {
2102 apply
2103 } else {
2104 quote! {
2105 if let Some(#ident) = #ident {
2106 #apply
2107 }
2108 }
2109 }
2110 }
2111
2112 fn get_success_response_schema<'a>(&self, op: &'a OperationInfo) -> Option<&'a String> {
2118 op.response_schemas
2119 .get("200")
2120 .or_else(|| op.response_schemas.get("201"))
2121 .or_else(|| {
2122 op.response_schemas
2123 .iter()
2124 .find(|(code, _)| code.starts_with('2'))
2125 .map(|(_, v)| v)
2126 })
2127 }
2128
2129 fn get_response_type(&self, op: &OperationInfo) -> TokenStream {
2131 if let Some(response_type) = self.get_success_response_schema(op) {
2132 let rust_type_name = self.to_rust_type_name(response_type);
2134 let response_ident = syn::Ident::new(&rust_type_name, proc_macro2::Span::call_site());
2135 quote! { #response_ident }
2136 } else if Self::returns_raw_event_stream(op) {
2137 quote! { impl futures_util::Stream<Item = Result<bytes::Bytes, reqwest::Error>> }
2138 } else {
2139 quote! { () }
2140 }
2141 }
2142
2143 fn returns_raw_event_stream(op: &OperationInfo) -> bool {
2157 op.supports_streaming
2158 }
2159
2160 fn generate_error_handling(&self, op: &OperationInfo, has_response_body: bool) -> TokenStream {
2169 let op_error_type = self.op_error_type_token(op);
2170
2171 let success_branch = if has_response_body {
2172 quote! {
2173 match serde_json::from_str(&body_text) {
2174 Ok(body) => Ok(body),
2175 Err(e) => Err(ApiOpError::Api(ApiError {
2176 status: status_code,
2177 headers: headers,
2178 body: body_text,
2179 typed: None,
2180 parse_error: Some(format!(
2181 "failed to deserialize 2xx response body: {}",
2182 e
2183 )),
2184 })),
2185 }
2186 }
2187 } else {
2188 quote! {
2189 let _ = body_text;
2190 let _ = headers;
2191 Ok(())
2192 }
2193 };
2194
2195 let error_match_arms = self.generate_error_match_arms(op);
2196
2197 if !has_response_body && Self::returns_raw_event_stream(op) {
2202 return quote! {
2203 let status = response.status();
2204 let status_code = status.as_u16();
2205 let headers = response.headers().clone();
2206
2207 if status.is_success() {
2208 Ok(response.bytes_stream())
2209 } else {
2210 let body_text = response.text().await
2211 .map_err(|e| ApiOpError::Transport(HttpError::Network(e)))?;
2212 let typed: Option<#op_error_type>;
2213 let parse_error: Option<String>;
2214 #error_match_arms
2215 Err(ApiOpError::Api(ApiError {
2216 status: status_code,
2217 headers,
2218 body: body_text,
2219 typed,
2220 parse_error,
2221 }))
2222 }
2223 };
2224 }
2225
2226 quote! {
2227 let status = response.status();
2228 let status_code = status.as_u16();
2229 let headers = response.headers().clone();
2230 let body_text = response.text().await
2231 .map_err(|e| ApiOpError::Transport(HttpError::Network(e)))?;
2232
2233 if status.is_success() {
2234 #success_branch
2235 } else {
2236 let typed: Option<#op_error_type>;
2237 let parse_error: Option<String>;
2238 #error_match_arms
2239 Err(ApiOpError::Api(ApiError {
2240 status: status_code,
2241 headers,
2242 body: body_text,
2243 typed,
2244 parse_error,
2245 }))
2246 }
2247 }
2248 }
2249
2250 fn generate_error_match_arms(&self, op: &OperationInfo) -> TokenStream {
2253 let arms: Vec<TokenStream> = op
2254 .response_schemas
2255 .iter()
2256 .filter(|(code, _)| !code.starts_with('2'))
2257 .filter_map(|(code, schema)| {
2258 let variant_ident = Self::op_error_variant_ident(code);
2259 let payload_ty_name = self.to_rust_type_name(schema);
2260 let payload_ty = syn::Ident::new(&payload_ty_name, proc_macro2::Span::call_site());
2261 let enum_ident = self.op_error_enum_ident(op);
2262
2263 let pattern = match code.as_str() {
2268 "default" | "Default" => return None, other if other.chars().all(|c| c.is_ascii_digit()) => {
2270 let n: u16 = other.parse().ok()?;
2271 quote! { #n }
2272 }
2273 "1XX" | "1xx" => quote! { code if (100..=199).contains(&code) },
2274 "2XX" | "2xx" => quote! { code if (200..=299).contains(&code) },
2275 "3XX" | "3xx" => quote! { code if (300..=399).contains(&code) },
2276 "4XX" | "4xx" => quote! { code if (400..=499).contains(&code) },
2277 "5XX" | "5xx" => quote! { code if (500..=599).contains(&code) },
2278 _ => return None,
2279 };
2280
2281 Some(quote! {
2282 #pattern => {
2283 match serde_json::from_str::<#payload_ty>(&body_text) {
2284 Ok(v) => {
2285 typed = Some(#enum_ident::#variant_ident(v));
2286 parse_error = None;
2287 }
2288 Err(e) => {
2289 typed = None;
2290 parse_error = Some(e.to_string());
2291 }
2292 }
2293 }
2294 })
2295 })
2296 .collect();
2297
2298 let has_typed_enum = op
2306 .response_schemas
2307 .iter()
2308 .any(|(code, _)| !code.starts_with('2'));
2309
2310 let default_payload = op
2317 .response_schemas
2318 .iter()
2319 .find(|(code, _)| matches!(code.as_str(), "default" | "Default"))
2320 .map(|(_, schema)| self.to_rust_type_name(schema));
2321
2322 let default_arm = if let Some(payload_ty_name) = default_payload {
2323 let payload_ty = syn::Ident::new(&payload_ty_name, proc_macro2::Span::call_site());
2324 let enum_ident = self.op_error_enum_ident(op);
2325 quote! {
2326 _ => {
2327 match serde_json::from_str::<#payload_ty>(&body_text) {
2328 Ok(v) => {
2329 typed = Some(#enum_ident::Default(v));
2330 parse_error = None;
2331 }
2332 Err(e) => {
2333 typed = None;
2334 parse_error = Some(e.to_string());
2335 }
2336 }
2337 }
2338 }
2339 } else if has_typed_enum {
2340 quote! {
2341 _ => {
2342 typed = None;
2343 parse_error = None;
2344 }
2345 }
2346 } else {
2347 quote! {
2349 _ => {
2350 match serde_json::from_str::<serde_json::Value>(&body_text) {
2351 Ok(v) => {
2352 typed = Some(v);
2353 parse_error = None;
2354 }
2355 Err(e) => {
2356 typed = None;
2357 parse_error = Some(e.to_string());
2358 }
2359 }
2360 }
2361 }
2362 };
2363
2364 if arms.is_empty() {
2365 quote! {
2367 match status_code {
2368 #default_arm
2369 }
2370 }
2371 } else {
2372 quote! {
2373 match status_code {
2374 #(#arms)*
2375 #default_arm
2376 }
2377 }
2378 }
2379 }
2380
2381 fn generate_url_construction(&self, path: &str, op: &OperationInfo) -> TokenStream {
2383 if path.contains('{') {
2385 self.generate_url_with_params(path, op)
2386 } else {
2387 quote! {
2388 let request_url = format!("{}{}", self.base_url, #path);
2389 }
2390 }
2391 }
2392
2393 fn generate_url_with_params(&self, path: &str, op: &OperationInfo) -> TokenStream {
2395 let path_params: Vec<_> = op
2397 .parameters
2398 .iter()
2399 .filter(|p| p.location == "path")
2400 .collect();
2401
2402 let mut format_string = String::with_capacity(path.len());
2411 let mut format_args: Vec<TokenStream> = Vec::new();
2412 let mut chars = path.chars().peekable();
2413 while let Some(c) = chars.next() {
2414 if c != '{' {
2415 format_string.push(c);
2416 continue;
2417 }
2418 let mut name = String::new();
2420 while let Some(&n) = chars.peek() {
2421 chars.next();
2422 if n == '}' {
2423 break;
2424 }
2425 name.push(n);
2426 }
2427 let param = path_params.iter().find(|p| p.name == name);
2431 let Some(param) = param else {
2432 format_string.push('{');
2433 format_string.push_str(&name);
2434 format_string.push('}');
2435 continue;
2436 };
2437 format_string.push_str("{}");
2438 let param_name_snake = self.param_ident_str(param);
2439 let param_ident = Self::to_field_ident(¶m_name_snake);
2440 if Self::param_uses_as_ref_str(param) {
2441 format_args.push(quote! {
2442 __pct_encode_path_segment(#param_ident.as_ref())
2443 });
2444 } else {
2445 format_args.push(quote! {
2446 __pct_encode_path_segment(&#param_ident.to_string())
2447 });
2448 }
2449 }
2450
2451 if format_args.is_empty() {
2452 quote! {
2453 let request_url = format!("{}{}", self.base_url, #path);
2454 }
2455 } else {
2456 quote! {
2457 let request_url = format!("{}{}", self.base_url, format!(#format_string, #(#format_args),*));
2458 }
2459 }
2460 }
2461
2462 pub(crate) fn param_ident_str(&self, param: &crate::analysis::ParameterInfo) -> String {
2467 if let Some(ident) = ¶m.rust_ident {
2468 return self.escape_keyword_ident(ident);
2472 }
2473 self.sanitize_param_name(¶m.name)
2474 }
2475
2476 fn escape_keyword_ident(&self, snake_case: &str) -> String {
2477 if matches!(snake_case, "self" | "super" | "crate" | "Self") {
2478 return format!("{snake_case}_param");
2479 }
2480 if Self::is_rust_keyword(snake_case) {
2481 format!("r#{snake_case}")
2482 } else {
2483 snake_case.to_string()
2484 }
2485 }
2486
2487 fn sanitize_param_name(&self, name: &str) -> String {
2492 let suffix = if name.ends_with("<=") {
2497 "_lte"
2498 } else if name.ends_with(">=") {
2499 "_gte"
2500 } else if name.ends_with('<') {
2501 "_lt"
2502 } else if name.ends_with('>') {
2503 "_gt"
2504 } else {
2505 ""
2506 };
2507 let stripped = name.trim_end_matches(['<', '>', '=']);
2508 let mut snake_case = stripped.to_snake_case();
2509 if snake_case.is_empty() {
2510 snake_case.push_str("parameter");
2511 } else if snake_case.starts_with(|character: char| character.is_ascii_digit()) {
2512 snake_case.insert(0, '_');
2513 }
2514 snake_case.push_str(suffix);
2515
2516 if matches!(snake_case.as_str(), "self" | "super" | "crate" | "Self") {
2517 return format!("{snake_case}_param");
2518 }
2519 if Self::is_rust_keyword(&snake_case) {
2520 format!("r#{snake_case}")
2521 } else {
2522 snake_case
2523 }
2524 }
2525}