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 retry_param = if has_retry {
289 quote! { retry_config: Option<RetryConfig>, }
290 } else {
291 quote! {}
292 };
293
294 let tracing_param = if has_tracing {
295 quote! { enable_tracing: bool, }
296 } else {
297 quote! {}
298 };
299
300 let retry_middleware = if has_retry {
301 quote! {
302 if let Some(config) = retry_config {
303 use reqwest_retry::{RetryTransientMiddleware, policies::ExponentialBackoff};
304
305 let retry_policy = ExponentialBackoff::builder()
306 .retry_bounds(
307 std::time::Duration::from_millis(config.initial_delay_ms),
308 std::time::Duration::from_millis(config.max_delay_ms),
309 )
310 .build_with_max_retries(config.max_retries);
311
312 let retry_middleware = RetryTransientMiddleware::new_with_policy(retry_policy);
313 client_builder = client_builder.with(retry_middleware);
314 }
315 }
316 } else {
317 quote! {}
318 };
319
320 let tracing_middleware = if has_tracing {
321 quote! {
322 if enable_tracing {
323 use reqwest_tracing::TracingMiddleware;
324 client_builder = client_builder.with(TracingMiddleware::default());
325 }
326 }
327 } else {
328 quote! {}
329 };
330
331 let default_constructor = if has_retry && has_tracing {
332 quote! {
333 pub fn new() -> Self {
335 Self::with_config(None, true)
336 }
337 }
338 } else if has_retry {
339 quote! {
340 pub fn new() -> Self {
342 Self::with_config(None)
343 }
344 }
345 } else if has_tracing {
346 quote! {
347 pub fn new() -> Self {
349 Self::with_config(true)
350 }
351 }
352 } else {
353 quote! {
354 pub fn new() -> Self {
356 let reqwest_client = reqwest::Client::new();
357 let client_builder = ClientBuilder::new(reqwest_client);
358 let http_client = client_builder.build();
359
360 Self {
361 base_url: String::new(),
362 api_key: None,
363 http_client,
364 custom_headers: BTreeMap::new(),
365 }
366 }
367 }
368 };
369
370 if has_retry || has_tracing {
371 quote! {
372 #default_constructor
373
374 pub fn with_config(#retry_param #tracing_param) -> Self {
376 let reqwest_client = reqwest::Client::new();
377 let mut client_builder = ClientBuilder::new(reqwest_client);
378
379 #tracing_middleware
380 #retry_middleware
381
382 let http_client = client_builder.build();
383
384 Self {
385 base_url: String::new(),
386 api_key: None,
387 http_client,
388 custom_headers: BTreeMap::new(),
389 }
390 }
391 }
392 } else {
393 default_constructor
394 }
395 }
396
397 fn generate_builder_methods(&self) -> TokenStream {
399 quote! {
400 pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
402 self.base_url = base_url.into();
403 self
404 }
405
406 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
408 self.api_key = Some(api_key.into());
409 self
410 }
411
412 pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
414 self.custom_headers.insert(name.into(), value.into());
415 self
416 }
417
418 pub fn with_headers(mut self, headers: BTreeMap<String, String>) -> Self {
420 self.custom_headers.extend(headers);
421 self
422 }
423 }
424 }
425
426 pub fn generate_operation_methods(&self, analysis: &SchemaAnalysis) -> TokenStream {
435 let operations: Vec<&OperationInfo> = analysis.operations.values().collect();
436 self.generate_operation_methods_for(analysis, &operations)
437 }
438
439 pub(crate) fn generate_operation_methods_for(
443 &self,
444 analysis: &SchemaAnalysis,
445 operations: &[&OperationInfo],
446 ) -> TokenStream {
447 let param_enums = self.generate_param_enum_types(operations);
448
449 let op_error_enums: Vec<TokenStream> = operations
450 .iter()
451 .copied()
452 .filter_map(|op| self.generate_op_error_enum(op))
453 .collect();
454
455 let methods: Vec<TokenStream> = operations
456 .iter()
457 .copied()
458 .map(|op| self.generate_single_operation_method(op))
459 .collect();
460
461 let (operation_builders, builder_entries) =
462 self.generate_operation_builders(analysis, operations);
463
464 quote! {
465 #param_enums
466
467 #(#op_error_enums)*
468
469 #(#operation_builders)*
470
471 impl HttpClient {
472 #(#methods)*
473 #(#builder_entries)*
474 }
475 }
476 }
477
478 fn generate_operation_builders(
479 &self,
480 analysis: &SchemaAnalysis,
481 operations: &[&OperationInfo],
482 ) -> (Vec<TokenStream>, Vec<TokenStream>) {
483 if !self.config().builders.enabled {
484 return (Vec::new(), Vec::new());
485 }
486
487 let mut used_entry_methods: std::collections::HashSet<String> = operations
488 .iter()
489 .map(|operation| self.get_method_name(operation).to_string())
490 .collect();
491 let mut used_type_names = std::collections::HashSet::new();
492 for schema_name in analysis.schemas.keys() {
493 let rust_name = self.to_rust_type_name(schema_name);
494 used_type_names.insert(rust_name.clone());
495 used_type_names.insert(format!("{rust_name}Builder"));
499 }
500 used_type_names.insert("HttpClient".to_string());
501 used_type_names.insert("ApiOpError".to_string());
502 used_type_names.extend(
503 [
504 "ClientBuilder",
505 "ClientWithMiddleware",
506 "RetryConfig",
507 "HttpError",
508 "BTreeMap",
509 ]
510 .into_iter()
511 .map(str::to_string),
512 );
513 for operation in operations {
514 used_type_names.insert(self.op_error_enum_ident(operation).to_string());
515 used_type_names.extend(
516 operation
517 .parameters
518 .iter()
519 .filter(|parameter| parameter.enum_values.is_some())
520 .map(|parameter| parameter.rust_type.clone()),
521 );
522 }
523
524 let mut definitions = Vec::new();
525 let mut entries = Vec::new();
526 for operation in operations {
527 let allocated_params = self.allocated_operation_params(operation);
528 let body_plan = self.body_model_plan(operation, analysis);
529 let optional_param_count = allocated_params
530 .iter()
531 .filter(|allocated| !Self::builder_param_is_required(allocated.param))
532 .count();
533 let optional_body_count =
534 usize::from(operation.request_body.is_some() && !operation.request_body_required);
535 let body_field_count = body_plan
536 .as_ref()
537 .filter(|plan| {
538 operation.request_body_required
539 || matches!(
540 &plan.required_construction,
541 RequiredBodyConstruction::Default
542 )
543 })
544 .map_or(0, |plan| plan.optional_fields.len());
545 let optional_count = optional_param_count + optional_body_count + body_field_count;
546 if optional_count <= self.config().builders.threshold {
547 continue;
548 }
549
550 let flat_method = self.get_method_name(operation);
551 let entry_base = format!("{flat_method}_builder");
552 let entry_name = Self::allocate_name(&entry_base, &mut used_entry_methods);
553 let entry_ident = Self::to_field_ident(&entry_name);
554
555 let builder_base = format!("{}Builder", flat_method.to_string().to_pascal_case());
556 let builder_name = Self::allocate_type_name(&builder_base, &mut used_type_names);
557 let builder_ident = format_ident!("{builder_name}");
558
559 let (definition, entry) = self.generate_single_operation_builder(
560 operation,
561 &allocated_params,
562 body_plan,
563 &flat_method,
564 &entry_ident,
565 &builder_ident,
566 );
567 definitions.push(definition);
568 entries.push(entry);
569 }
570
571 (definitions, entries)
572 }
573
574 fn generate_single_operation_builder(
575 &self,
576 operation: &OperationInfo,
577 allocated_params: &[AllocatedOperationParam<'_>],
578 body_plan: Option<BodyModelPlan>,
579 flat_method: &syn::Ident,
580 entry_ident: &syn::Ident,
581 builder_ident: &syn::Ident,
582 ) -> (TokenStream, TokenStream) {
583 let mut fields = vec![quote! { client: &'a HttpClient }];
584 let mut entry_parameters = Vec::new();
585 let mut initializers = vec![quote! { client: self }];
586 let mut call_arguments = Vec::new();
587 let mut setters = Vec::new();
588 let mut used_entry_params = std::collections::HashSet::new();
589 let mut used_methods = std::collections::HashSet::from(["send".to_string()]);
590
591 for allocated in allocated_params {
592 let field_ident = &allocated.ident;
593 let storage_type = self.builder_param_storage_type(allocated.param);
594 if Self::builder_param_is_required(allocated.param) {
595 fields.push(quote! { #field_ident: #storage_type });
596 let entry_name =
597 Self::allocate_name(&field_ident.to_string(), &mut used_entry_params);
598 let entry_param = Self::to_field_ident(&entry_name);
599 if Self::param_has_impl_as_ref_type(allocated.param) {
600 entry_parameters.push(quote! { #entry_param: impl Into<String> });
601 initializers.push(quote! { #field_ident: #entry_param.into() });
602 } else {
603 entry_parameters.push(quote! { #entry_param: #storage_type });
604 initializers.push(quote! { #field_ident: #entry_param });
605 }
606 } else {
607 fields.push(quote! { #field_ident: Option<#storage_type> });
608 initializers.push(quote! { #field_ident: None });
609 let setter_ident =
610 Self::allocate_builder_method(&field_ident.to_string(), &mut used_methods);
611 let wire_name = &allocated.param.name;
612 let assignment = if Self::param_has_impl_as_ref_type(allocated.param) {
613 quote! { self.#field_ident = Some(#field_ident.into()); }
614 } else {
615 quote! { self.#field_ident = Some(#field_ident); }
616 };
617 let setter_type = if Self::param_has_impl_as_ref_type(allocated.param) {
618 quote! { impl Into<String> }
619 } else {
620 storage_type.clone()
621 };
622 setters.push(quote! {
623 #[doc = concat!("Set the optional `", #wire_name, "` operation parameter.")]
624 #[must_use]
625 pub fn #setter_ident(mut self, #field_ident: #setter_type) -> Self {
626 #assignment
627 self
628 }
629 });
630 }
631 call_arguments.push(quote! { self.#field_ident });
632 }
633
634 if let Some(body_plan) = body_plan {
635 let BodyModelPlan {
636 body_ident,
637 body_type,
638 required_construction,
639 optional_fields,
640 } = body_plan;
641 let can_initialize_optional_body =
642 matches!(&required_construction, RequiredBodyConstruction::Default);
643 if operation.request_body_required {
644 fields.push(quote! { #body_ident: #body_type });
645 match required_construction {
646 RequiredBodyConstruction::Default => {
647 initializers.push(quote! { #body_ident: Default::default() });
648 }
649 RequiredBodyConstruction::New(constructor_params) => {
650 let mut constructor_args = Vec::new();
651 for constructor in constructor_params {
652 let preferred = constructor.preferred_ident.to_string();
653 let entry_name =
654 Self::allocate_name(&preferred, &mut used_entry_params);
655 let entry_param = Self::to_field_ident(&entry_name);
656 let value_type = constructor.value_type;
657 entry_parameters.push(quote! { #entry_param: #value_type });
658 constructor_args.push(entry_param);
659 }
660 initializers.push(quote! {
661 #body_ident: #body_type::new(#(#constructor_args),*)
662 });
663 }
664 RequiredBodyConstruction::Whole => {
665 let entry_name =
666 Self::allocate_name(&body_ident.to_string(), &mut used_entry_params);
667 let entry_param = Self::to_field_ident(&entry_name);
668 entry_parameters.push(quote! { #entry_param: #body_type });
669 initializers.push(quote! { #body_ident: #entry_param });
670 }
671 }
672 } else {
673 fields.push(quote! { #body_ident: Option<#body_type> });
674 initializers.push(quote! { #body_ident: None });
675 }
676
677 let body_setter =
678 Self::allocate_builder_method(&body_ident.to_string(), &mut used_methods);
679 let body_assignment = if operation.request_body_required {
680 quote! { self.#body_ident = #body_ident; }
681 } else {
682 quote! { self.#body_ident = Some(#body_ident); }
683 };
684 setters.push(quote! {
685 #[must_use]
687 pub fn #body_setter(mut self, #body_ident: #body_type) -> Self {
688 #body_assignment
689 self
690 }
691 });
692
693 if operation.request_body_required || can_initialize_optional_body {
694 for field in optional_fields {
695 let setter_ident = Self::allocate_builder_method(
696 &field.preferred_method_name,
697 &mut used_methods,
698 );
699 let value_ident = field.value_ident;
700 let value_type = field.value_type;
701 let wire_name = field.wire_name;
702 let access_path = field.access_path;
703 let assignment = if operation.request_body_required {
704 let mut target = quote! { self.#body_ident };
705 for access in &access_path {
706 target = quote! { #target.#access };
707 }
708 quote! { #target = Some(#value_ident); }
709 } else {
710 let mut target = quote! { request };
711 for access in &access_path {
712 target = quote! { #target.#access };
713 }
714 quote! {
715 let request = self.#body_ident.get_or_insert_with(Default::default);
716 #target = Some(#value_ident);
717 }
718 };
719 setters.push(quote! {
720 #[doc = concat!("Set the optional request-body field `", #wire_name, "`.")]
721 #[must_use]
722 pub fn #setter_ident(mut self, #value_ident: #value_type) -> Self {
723 #assignment
724 self
725 }
726 });
727 }
728 }
729 call_arguments.push(quote! { self.#body_ident });
730 }
731
732 let response_type = self.get_response_type(operation);
733 let error_type = self.op_error_type_token(operation);
734 let operation_id = &operation.operation_id;
735 let definition = quote! {
736 #[doc = concat!("Additive request builder for `", #operation_id, "`.")]
737 #[must_use]
738 pub struct #builder_ident<'a> {
739 #(#fields,)*
740 }
741
742 impl<'a> #builder_ident<'a> {
743 #(#setters)*
744
745 pub async fn send(self) -> Result<#response_type, ApiOpError<#error_type>> {
747 self.client.#flat_method(#(#call_arguments),*).await
748 }
749 }
750 };
751 let entry = quote! {
752 #[doc = concat!("Start an additive builder for `", #operation_id, "`.")]
753 pub fn #entry_ident(
754 &self,
755 #(#entry_parameters),*
756 ) -> #builder_ident<'_> {
757 #builder_ident {
758 #(#initializers,)*
759 }
760 }
761 };
762 (definition, entry)
763 }
764
765 fn allocate_name(base: &str, used: &mut std::collections::HashSet<String>) -> String {
766 let mut candidate = base.to_string();
767 let mut suffix = 2;
768 while !used.insert(candidate.clone()) {
769 candidate = format!("{base}_{suffix}");
770 suffix += 1;
771 }
772 candidate
773 }
774
775 fn allocate_type_name(base: &str, used: &mut std::collections::HashSet<String>) -> String {
776 if used.insert(base.to_string()) {
777 return base.to_string();
778 }
779
780 let mut suffix = 2;
781 loop {
782 let candidate = format!("{base}{suffix}");
783 if used.insert(candidate.clone()) {
784 return candidate;
785 }
786 suffix += 1;
787 }
788 }
789
790 fn allocate_builder_method(
791 preferred: &str,
792 used: &mut std::collections::HashSet<String>,
793 ) -> syn::Ident {
794 let plain = preferred.strip_prefix("r#").unwrap_or(preferred);
795 let base = if used.contains(preferred) {
796 format!("with_{plain}")
797 } else {
798 preferred.to_string()
799 };
800 let allocated = Self::allocate_name(&base, used);
801 Self::to_field_ident(&allocated)
802 }
803
804 fn allocated_operation_params<'a>(
805 &self,
806 operation: &'a OperationInfo,
807 ) -> Vec<AllocatedOperationParam<'a>> {
808 let mut used = std::collections::HashSet::from([
812 "client".to_string(),
813 "request".to_string(),
814 "form".to_string(),
815 "body".to_string(),
816 ]);
817 let mut allocated = Vec::new();
818 for location in ["path", "query", "header", "cookie"] {
819 for parameter in &operation.parameters {
820 if parameter.location != location {
821 continue;
822 }
823 let raw = self.param_ident_str(parameter);
824 let chosen = Self::allocate_name(&raw, &mut used);
825 allocated.push(AllocatedOperationParam {
826 param: parameter,
827 ident: Self::to_field_ident(&chosen),
828 });
829 }
830 }
831 allocated
832 }
833
834 fn builder_param_is_required(parameter: &ParameterInfo) -> bool {
835 parameter.location == "path" || parameter.required
839 }
840
841 fn builder_param_storage_type(&self, parameter: &ParameterInfo) -> TokenStream {
842 self.get_param_owned_rust_type(parameter)
843 }
844
845 fn param_has_impl_as_ref_type(parameter: &ParameterInfo) -> bool {
846 !matches!(
847 ¶meter.query_serialization,
848 Some(
849 crate::analysis::QuerySerialization::FormExplodedArray { .. }
850 | crate::analysis::QuerySerialization::FormArray { .. }
851 )
852 ) && Self::param_uses_as_ref_str(parameter)
853 }
854
855 fn body_model_plan(
856 &self,
857 operation: &OperationInfo,
858 analysis: &SchemaAnalysis,
859 ) -> Option<BodyModelPlan> {
860 use crate::analysis::{ObjectAdditionalProperties, RequestBodyContent, SchemaType};
861
862 let request_body = operation.request_body.as_ref()?;
863 let (body_name, body_ident) = match request_body {
864 RequestBodyContent::Json { schema_name, .. }
865 | RequestBodyContent::FormUrlEncoded { schema_name, .. } => {
866 (schema_name.as_str(), format_ident!("request"))
867 }
868 RequestBodyContent::Multipart => {
869 return Some(BodyModelPlan {
870 body_ident: format_ident!("form"),
871 body_type: quote! { reqwest::multipart::Form },
872 required_construction: RequiredBodyConstruction::Whole,
873 optional_fields: Vec::new(),
874 });
875 }
876 RequestBodyContent::OctetStream | RequestBodyContent::Unsupported { .. } => {
877 return Some(BodyModelPlan {
878 body_ident: format_ident!("body"),
879 body_type: quote! { Vec<u8> },
880 required_construction: RequiredBodyConstruction::Whole,
881 optional_fields: Vec::new(),
882 });
883 }
884 RequestBodyContent::TextPlain => {
885 return Some(BodyModelPlan {
886 body_ident: format_ident!("body"),
887 body_type: quote! { String },
888 required_construction: RequiredBodyConstruction::Whole,
889 optional_fields: Vec::new(),
890 });
891 }
892 RequestBodyContent::SchemaLess { .. } => return None,
893 };
894 let body_type_name = self.to_rust_type_name(body_name);
895 let body_type = syn::Ident::new(&body_type_name, proc_macro2::Span::call_site());
896 let Some((resolved_name, resolved_schema)) =
897 self.resolve_reference_schema(body_name, analysis)
898 else {
899 return Some(BodyModelPlan {
900 body_ident,
901 body_type: quote! { #body_type },
902 required_construction: RequiredBodyConstruction::Whole,
903 optional_fields: Vec::new(),
904 });
905 };
906
907 let mut optional_fields = Vec::new();
908 let mut stack = std::collections::HashSet::new();
909 self.collect_optional_body_fields(
910 resolved_name,
911 Vec::new(),
912 analysis,
913 &mut stack,
914 &mut optional_fields,
915 );
916
917 let required_construction = match &resolved_schema.schema_type {
918 SchemaType::Object {
919 properties,
920 required,
921 additional_properties,
922 } if !self.is_discriminated_variant(resolved_name, analysis) => {
923 let emitted = self.emitted_object_properties(
924 resolved_name,
925 properties,
926 required,
927 additional_properties,
928 analysis,
929 None,
930 );
931 let required_fields: Vec<_> = emitted
932 .iter()
933 .filter(|field| field.is_required)
934 .map(|field| BodyConstructorParam {
935 preferred_ident: field.ident.clone(),
936 value_type: field.field_type.clone(),
937 })
938 .collect();
939 if required_fields.is_empty() {
940 RequiredBodyConstruction::Default
941 } else if emitted.iter().any(|field| !field.is_required)
942 || !matches!(additional_properties, ObjectAdditionalProperties::Forbidden)
943 {
944 RequiredBodyConstruction::New(required_fields)
945 } else {
946 RequiredBodyConstruction::Whole
947 }
948 }
949 _ => RequiredBodyConstruction::Whole,
950 };
951
952 Some(BodyModelPlan {
953 body_ident,
954 body_type: quote! { #body_type },
955 required_construction,
956 optional_fields,
957 })
958 }
959
960 fn resolve_reference_schema<'a>(
961 &self,
962 schema_name: &'a str,
963 analysis: &'a SchemaAnalysis,
964 ) -> Option<(&'a str, &'a crate::analysis::AnalyzedSchema)> {
965 let mut current = schema_name;
966 let mut visited = std::collections::HashSet::new();
967 loop {
968 if !visited.insert(current) {
969 return None;
970 }
971 let schema = analysis.schemas.get(current)?;
972 if let crate::analysis::SchemaType::Reference { target } = &schema.schema_type {
973 current = target;
974 } else {
975 return Some((current, schema));
976 }
977 }
978 }
979
980 fn collect_optional_body_fields(
981 &self,
982 schema_name: &str,
983 access_path: Vec<syn::Ident>,
984 analysis: &SchemaAnalysis,
985 stack: &mut std::collections::HashSet<String>,
986 output: &mut Vec<BodyFieldPlan>,
987 ) {
988 use crate::analysis::SchemaType;
989 if !stack.insert(schema_name.to_string()) {
990 return;
991 }
992 let Some(schema) = analysis.schemas.get(schema_name) else {
993 stack.remove(schema_name);
994 return;
995 };
996 match &schema.schema_type {
997 SchemaType::Reference { target } => {
998 self.collect_optional_body_fields(target, access_path, analysis, stack, output);
999 }
1000 SchemaType::Object {
1001 properties,
1002 required,
1003 additional_properties,
1004 } if !self.is_discriminated_variant(schema_name, analysis) => {
1005 for field in self.emitted_object_properties(
1006 schema_name,
1007 properties,
1008 required,
1009 additional_properties,
1010 analysis,
1011 None,
1012 ) {
1013 if field.is_required {
1014 continue;
1015 }
1016 let mut field_path = access_path.clone();
1017 field_path.push(field.ident.clone());
1018 output.push(BodyFieldPlan {
1019 wire_name: field.wire_name.to_string(),
1020 preferred_method_name: field.ident.to_string(),
1021 value_ident: field.ident.clone(),
1022 value_type: self.generate_property_base_type(
1023 schema_name,
1024 field.wire_name,
1025 field.property,
1026 analysis,
1027 ),
1028 access_path: field_path,
1029 });
1030 }
1031 }
1032 SchemaType::Composition { schemas } => {
1033 for (index, schema_ref) in schemas.iter().enumerate() {
1034 let mut nested_path = access_path.clone();
1035 nested_path.push(format_ident!("part_{index}"));
1036 self.collect_optional_body_fields(
1037 &schema_ref.target,
1038 nested_path,
1039 analysis,
1040 stack,
1041 output,
1042 );
1043 }
1044 }
1045 _ => {}
1046 }
1047 stack.remove(schema_name);
1048 }
1049
1050 fn is_discriminated_variant(&self, schema_name: &str, analysis: &SchemaAnalysis) -> bool {
1051 analysis.schemas.values().any(|schema| {
1052 matches!(
1053 &schema.schema_type,
1054 crate::analysis::SchemaType::DiscriminatedUnion { variants, .. }
1055 if variants.iter().any(|variant| variant.type_name == schema_name)
1056 )
1057 })
1058 }
1059
1060 fn generate_param_enum_types(&self, operations: &[&OperationInfo]) -> TokenStream {
1065 let mut by_name: BTreeMap<String, &ParameterInfo> = BTreeMap::new();
1066 for op in operations {
1067 for param in &op.parameters {
1068 if param.enum_values.is_some() {
1069 by_name.entry(param.rust_type.clone()).or_insert(param);
1070 }
1071 }
1072 }
1073
1074 if by_name.is_empty() {
1075 return quote! {};
1076 }
1077
1078 let defs: Vec<TokenStream> = by_name
1079 .values()
1080 .map(|param| self.generate_single_param_enum(param))
1081 .collect();
1082
1083 quote! { #(#defs)* }
1084 }
1085
1086 fn generate_single_param_enum(&self, param: &ParameterInfo) -> TokenStream {
1087 let Some(values) = param.enum_values.as_deref() else {
1088 return quote! {};
1089 };
1090
1091 let enum_ident = format_ident!("{}", param.rust_type);
1092
1093 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
1099 let variant_names: Vec<String> = values
1100 .iter()
1101 .map(|value| {
1102 let base = self.to_rust_enum_variant(value);
1103 let mut chosen = base.clone();
1104 let mut suffix = 2;
1105 while !used.insert(chosen.clone()) {
1106 chosen = format!("{base}_{suffix}");
1107 suffix += 1;
1108 }
1109 chosen
1110 })
1111 .collect();
1112
1113 let variants: Vec<TokenStream> = values
1114 .iter()
1115 .zip(&variant_names)
1116 .map(|(value, name)| {
1117 let variant_ident = format_ident!("{}", name);
1118 quote! {
1119 #[serde(rename = #value)]
1120 #variant_ident,
1121 }
1122 })
1123 .collect();
1124
1125 let display_arms: Vec<TokenStream> = values
1126 .iter()
1127 .zip(&variant_names)
1128 .map(|(value, name)| {
1129 let variant_ident = format_ident!("{}", name);
1130 quote! { Self::#variant_ident => #value, }
1131 })
1132 .collect();
1133
1134 let doc = format!(
1135 "Allowed values for the `{}` {} parameter.",
1136 param.name, param.location
1137 );
1138
1139 quote! {
1140 #[doc = #doc]
1141 #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
1142 pub enum #enum_ident {
1143 #(#variants)*
1144 }
1145
1146 impl #enum_ident {
1147 pub fn as_str(&self) -> &'static str {
1148 match self {
1149 #(#display_arms)*
1150 }
1151 }
1152 }
1153
1154 impl std::fmt::Display for #enum_ident {
1155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1156 f.write_str(self.as_str())
1157 }
1158 }
1159
1160 impl AsRef<str> for #enum_ident {
1161 fn as_ref(&self) -> &str {
1162 self.as_str()
1163 }
1164 }
1165 }
1166 }
1167
1168 fn generate_op_error_enum(&self, op: &OperationInfo) -> Option<TokenStream> {
1173 let variants: Vec<(String, String)> = op
1174 .response_schemas
1175 .iter()
1176 .filter(|(code, _)| !code.starts_with('2'))
1177 .map(|(code, schema)| (code.clone(), schema.clone()))
1178 .collect();
1179
1180 if variants.is_empty() {
1181 return None;
1182 }
1183
1184 let enum_ident = self.op_error_enum_ident(op);
1185 let variant_decls: Vec<TokenStream> = variants
1186 .iter()
1187 .map(|(code, schema)| {
1188 let variant_ident = Self::op_error_variant_ident(code);
1189 let payload_ty_name = self.to_rust_type_name(schema);
1190 let payload_ty = syn::Ident::new(&payload_ty_name, proc_macro2::Span::call_site());
1191 quote! { #variant_ident(#payload_ty) }
1192 })
1193 .collect();
1194
1195 let doc = format!(
1196 "Typed error responses for `{}`. One variant per declared non-2xx response.",
1197 op.operation_id
1198 );
1199
1200 Some(quote! {
1201 #[doc = #doc]
1202 #[derive(Debug, Clone)]
1203 pub enum #enum_ident {
1204 #(#variant_decls,)*
1205 }
1206 })
1207 }
1208
1209 fn op_error_enum_ident(&self, op: &OperationInfo) -> syn::Ident {
1211 use heck::ToPascalCase;
1212 let name = format!(
1213 "{}ApiError",
1214 op.operation_id.replace('.', "_").to_pascal_case()
1215 );
1216 syn::Ident::new(&name, proc_macro2::Span::call_site())
1217 }
1218
1219 fn op_error_variant_ident(status_code: &str) -> syn::Ident {
1222 let raw = match status_code {
1223 "default" | "Default" => "Default".to_string(),
1224 other if other.chars().all(|c| c.is_ascii_digit()) => format!("Status{other}"),
1225 other => format!("Status{}", other.to_ascii_lowercase()),
1226 };
1227 syn::Ident::new(&raw, proc_macro2::Span::call_site())
1228 }
1229
1230 fn op_error_type_token(&self, op: &OperationInfo) -> TokenStream {
1234 if op
1235 .response_schemas
1236 .iter()
1237 .any(|(code, _)| !code.starts_with('2'))
1238 {
1239 let ident = self.op_error_enum_ident(op);
1240 quote! { #ident }
1241 } else {
1242 quote! { serde_json::Value }
1243 }
1244 }
1245
1246 fn generate_single_operation_method(&self, op: &OperationInfo) -> TokenStream {
1248 let method_name = self.get_method_name(op);
1249 let http_method_call = self.http_method_call(op);
1250 let path = &op.path;
1251 let request_param = self.generate_request_param(op);
1252 let request_body = self.generate_request_body(op);
1253 let query_params = self.generate_query_params(op);
1254 let header_params = self.generate_header_params(op);
1255 let cookie_params = self.generate_cookie_params(op);
1256 let auth_application = self.generate_auth_application();
1257 let response_type = self.get_response_type(op);
1258 let has_response_body = self.get_success_response_schema(op).is_some();
1259 let op_error_type = self.op_error_type_token(op);
1260 let error_handling = self.generate_error_handling(op, has_response_body);
1261 let url_construction = self.generate_url_construction(path, op);
1262 let doc_comment = self.generate_operation_doc_comment(op);
1263
1264 quote! {
1265 #doc_comment
1266 pub async fn #method_name(
1267 &self,
1268 #request_param
1269 ) -> Result<#response_type, ApiOpError<#op_error_type>> {
1270 #url_construction
1271
1272 let mut req = #http_method_call;
1273 #request_body
1274
1275 #query_params
1276 #header_params
1277 #cookie_params
1278
1279 #auth_application
1282
1283 for (name, value) in &self.custom_headers {
1285 req = req.header(name, value);
1286 }
1287
1288 let response = req.send().await?;
1289 #error_handling
1290 }
1291 }
1292 }
1293
1294 fn generate_auth_application(&self) -> TokenStream {
1298 use crate::http_config::AuthConfig;
1299 match &self.config().auth_config {
1300 Some(AuthConfig::Bearer { header_name }) if header_name == "Authorization" => quote! {
1301 if let Some(api_key) = &self.api_key {
1302 req = req.bearer_auth(api_key);
1303 }
1304 },
1305 Some(AuthConfig::Bearer { header_name }) => {
1306 let h = header_name.clone();
1307 quote! {
1308 if let Some(api_key) = &self.api_key {
1309 req = req.header(#h, format!("Bearer {}", api_key));
1310 }
1311 }
1312 }
1313 Some(AuthConfig::ApiKey { header_name }) => {
1314 let h = header_name.clone();
1315 quote! {
1316 if let Some(api_key) = &self.api_key {
1317 req = req.header(#h, api_key.as_str());
1318 }
1319 }
1320 }
1321 Some(AuthConfig::Custom {
1322 header_name,
1323 header_value_prefix,
1324 }) => {
1325 let h = header_name.clone();
1326 let prefix = header_value_prefix.clone().unwrap_or_default();
1327 if prefix.is_empty() {
1328 quote! {
1329 if let Some(api_key) = &self.api_key {
1330 req = req.header(#h, api_key.as_str());
1331 }
1332 }
1333 } else {
1334 let format_str = format!("{}{{}}", prefix);
1335 quote! {
1336 if let Some(api_key) = &self.api_key {
1337 req = req.header(#h, format!(#format_str, api_key));
1338 }
1339 }
1340 }
1341 }
1342 None => quote! {
1343 if let Some(api_key) = &self.api_key {
1344 req = req.bearer_auth(api_key);
1345 }
1346 },
1347 }
1348 }
1349
1350 fn generate_header_params(&self, op: &OperationInfo) -> TokenStream {
1354 let header_params: Vec<_> = op
1355 .parameters
1356 .iter()
1357 .filter(|p| p.location == "header")
1358 .collect();
1359 if header_params.is_empty() {
1360 return quote! {};
1361 }
1362 let mut emit = Vec::new();
1363 for param in header_params {
1364 let param_name_snake = self.param_ident_str(param);
1365 let param_ident = Self::to_field_ident(¶m_name_snake);
1366 let header_name = ¶m.name;
1367 if param.required {
1368 if Self::param_uses_as_ref_str(param) {
1369 emit.push(quote! {
1370 req = req.header(#header_name, #param_ident.as_ref());
1371 });
1372 } else {
1373 emit.push(quote! {
1374 req = req.header(#header_name, #param_ident.to_string());
1375 });
1376 }
1377 } else if Self::param_uses_as_ref_str(param) {
1378 emit.push(quote! {
1379 if let Some(v) = #param_ident {
1380 req = req.header(#header_name, v.as_ref());
1381 }
1382 });
1383 } else {
1384 emit.push(quote! {
1385 if let Some(v) = #param_ident {
1386 req = req.header(#header_name, v.to_string());
1387 }
1388 });
1389 }
1390 }
1391 quote! {
1392 #(#emit)*
1393 }
1394 }
1395
1396 fn generate_cookie_params(&self, op: &OperationInfo) -> TokenStream {
1397 let cookie_params: Vec<_> = op
1398 .parameters
1399 .iter()
1400 .filter(|parameter| parameter.location == "cookie")
1401 .collect();
1402 if cookie_params.is_empty() {
1403 return quote! {};
1404 }
1405 let mut emit = Vec::new();
1406 for parameter in cookie_params {
1407 let ident = Self::to_field_ident(&self.param_ident_str(parameter));
1408 let wire_name = parameter.name.as_str();
1409 if parameter.required {
1410 emit.push(quote! {
1411 __cookie_fields.push(format!("{}={}", #wire_name, #ident));
1412 });
1413 } else {
1414 emit.push(quote! {
1415 if let Some(value) = #ident {
1416 __cookie_fields.push(format!("{}={}", #wire_name, value));
1417 }
1418 });
1419 }
1420 }
1421 quote! {
1422 let mut __cookie_fields = Vec::new();
1423 #(#emit)*
1424 if !__cookie_fields.is_empty() {
1425 req = req.header(::reqwest::header::COOKIE, __cookie_fields.join("; "));
1426 }
1427 }
1428 }
1429
1430 fn generate_query_params(&self, op: &OperationInfo) -> TokenStream {
1432 let query_params: Vec<_> = op
1433 .parameters
1434 .iter()
1435 .filter(|p| p.location == "query")
1436 .collect();
1437
1438 if query_params.is_empty() {
1439 return quote! {};
1440 }
1441
1442 let mut param_building = Vec::new();
1443 let mut req_appends = Vec::new();
1447
1448 for param in query_params {
1449 use crate::analysis::QuerySerialization;
1450
1451 let param_name_snake = self.param_ident_str(param);
1453 let param_name = Self::to_field_ident(¶m_name_snake);
1454
1455 let param_key = ¶m.name;
1457
1458 match ¶m.query_serialization {
1459 Some(QuerySerialization::FormExplodedObject) => {
1460 let apply = quote! {
1467 let __empty = match serde_json::to_value(&v)
1468 .map_err(HttpError::serialization_error)?
1469 {
1470 serde_json::Value::Object(map) => map.is_empty(),
1471 _ => false,
1472 };
1473 if __empty {
1474 req = req.query(&[(format!("{}[]", #param_key), String::new())]);
1475 } else {
1476 req = req.query(&v);
1477 }
1478 };
1479 if param.required {
1480 req_appends.push(quote! {
1481 {
1482 let v = #param_name;
1483 #apply
1484 }
1485 });
1486 } else {
1487 req_appends.push(quote! {
1488 if let Some(v) = #param_name {
1489 #apply
1490 }
1491 });
1492 }
1493 continue;
1494 }
1495 Some(QuerySerialization::DeepObject) => {
1496 let apply = quote! {
1500 let map = match serde_json::to_value(&v)
1501 .map_err(HttpError::serialization_error)?
1502 {
1503 serde_json::Value::Object(map) => map,
1504 _ => return Err(HttpError::serialization_error(
1505 format!("query parameter `{}` did not serialize as an object", #param_key)
1506 ).into()),
1507 };
1508 let mut deep_params: Vec<(String, String)> = Vec::new();
1509 for (k, val) in map {
1510 let s = match val {
1511 serde_json::Value::Null => continue,
1512 serde_json::Value::String(s) => s,
1513 other => other.to_string(),
1514 };
1515 deep_params.push((format!("{}[{}]", #param_key, k), s));
1516 }
1517 if deep_params.is_empty() {
1518 deep_params.push((format!("{}[]", #param_key), String::new()));
1519 }
1520 req = req.query(&deep_params);
1521 };
1522 if param.required {
1523 req_appends.push(quote! {
1524 {
1525 let v = #param_name;
1526 #apply
1527 }
1528 });
1529 } else {
1530 req_appends.push(quote! {
1531 if let Some(v) = #param_name {
1532 #apply
1533 }
1534 });
1535 }
1536 continue;
1537 }
1538 Some(QuerySerialization::FormObject) => {
1539 let apply = quote! {
1543 let map = match serde_json::to_value(&v)
1544 .map_err(HttpError::serialization_error)?
1545 {
1546 serde_json::Value::Object(map) => map,
1547 _ => return Err(HttpError::serialization_error(
1548 format!("query parameter `{}` did not serialize as an object", #param_key)
1549 ).into()),
1550 };
1551 let mut parts: Vec<String> = Vec::new();
1552 for (k, val) in map {
1553 let s = match val {
1554 serde_json::Value::Null => continue,
1555 serde_json::Value::String(s) => s,
1556 other => other.to_string(),
1557 };
1558 if k.contains(',') || s.contains(',') {
1559 return Err(HttpError::serialization_error(
1560 format!(
1561 "query object `{}` contains a comma in key `{}`; use explode=true for lossless string values",
1562 #param_key,
1563 k,
1564 )
1565 ).into());
1566 }
1567 parts.push(k);
1568 parts.push(s);
1569 }
1570 if parts.is_empty() {
1571 query_params.push((
1572 format!("{}[]", #param_key),
1573 String::new(),
1574 ));
1575 } else {
1576 query_params.push((#param_key.to_string(), parts.join(",")));
1577 }
1578 };
1579 if param.required {
1580 param_building.push(quote! {
1581 {
1582 let v = #param_name;
1583 #apply
1584 }
1585 });
1586 } else {
1587 param_building.push(quote! {
1588 if let Some(v) = #param_name {
1589 #apply
1590 }
1591 });
1592 }
1593 continue;
1594 }
1595 Some(QuerySerialization::FormExplodedArray { .. }) => {
1596 if param.required {
1598 param_building.push(quote! {
1599 if #param_name.is_empty() {
1600 query_params.push((
1601 format!("{}[]", #param_key),
1602 String::new(),
1603 ));
1604 } else {
1605 for item in #param_name {
1606 query_params.push((#param_key.to_string(), item.to_string()));
1607 }
1608 }
1609 });
1610 } else {
1611 param_building.push(quote! {
1612 if let Some(v) = #param_name {
1613 if v.is_empty() {
1614 query_params.push((
1615 format!("{}[]", #param_key),
1616 String::new(),
1617 ));
1618 } else {
1619 for item in v {
1620 query_params.push((#param_key.to_string(), item.to_string()));
1621 }
1622 }
1623 }
1624 });
1625 }
1626 continue;
1627 }
1628 Some(QuerySerialization::FormArray { .. }) => {
1629 let apply = quote! {
1632 if v.is_empty() {
1633 query_params.push((
1634 format!("{}[]", #param_key),
1635 String::new(),
1636 ));
1637 } else {
1638 let mut parts = Vec::with_capacity(v.len());
1639 for item in &v {
1640 let item = item.to_string();
1641 if item.contains(',') {
1642 return Err(HttpError::serialization_error(
1643 format!(
1644 "query array `{}` contains a comma; use explode=true for lossless string values",
1645 #param_key,
1646 )
1647 ).into());
1648 }
1649 parts.push(item);
1650 }
1651 query_params.push((
1652 #param_key.to_string(),
1653 parts.join(","),
1654 ));
1655 }
1656 };
1657 if param.required {
1658 param_building.push(quote! {
1659 {
1660 let v = #param_name;
1661 #apply
1662 }
1663 });
1664 } else {
1665 param_building.push(quote! {
1666 if let Some(v) = #param_name {
1667 #apply
1668 }
1669 });
1670 }
1671 continue;
1672 }
1673 Some(QuerySerialization::Unsupported { .. }) => {}
1674 None => {}
1675 }
1676
1677 if param.required {
1678 if Self::param_uses_as_ref_str(param) {
1680 param_building.push(quote! {
1681 query_params.push((#param_key.to_string(), #param_name.as_ref().to_string()));
1682 });
1683 } else {
1684 param_building.push(quote! {
1685 query_params.push((#param_key.to_string(), #param_name.to_string()));
1686 });
1687 }
1688 } else {
1689 if Self::param_uses_as_ref_str(param) {
1691 param_building.push(quote! {
1692 if let Some(v) = #param_name {
1693 query_params.push((#param_key.to_string(), v.as_ref().to_string()));
1694 }
1695 });
1696 } else {
1697 param_building.push(quote! {
1698 if let Some(v) = #param_name {
1699 query_params.push((#param_key.to_string(), v.to_string()));
1700 }
1701 });
1702 }
1703 }
1704 }
1705
1706 let pairs_block = if param_building.is_empty() {
1709 quote! {}
1710 } else {
1711 quote! {
1712 {
1713 let mut query_params: Vec<(String, String)> = Vec::new();
1714 #(#param_building)*
1715 if !query_params.is_empty() {
1716 req = req.query(&query_params);
1717 }
1718 }
1719 }
1720 };
1721
1722 quote! {
1723 #pairs_block
1725 #(#req_appends)*
1726 }
1727 }
1728
1729 fn generate_operation_doc_comment(&self, op: &OperationInfo) -> TokenStream {
1733 let method = op.method.to_uppercase();
1734 let path = &op.path;
1735 let mut docs: Vec<String> = Vec::new();
1736 if let Some(s) = &op.summary {
1737 if !s.is_empty() {
1738 docs.push(s.clone());
1739 docs.push(String::new());
1740 }
1741 }
1742 if let Some(d) = &op.description {
1743 if !d.is_empty() {
1744 for line in d.lines() {
1745 docs.push(line.to_string());
1746 }
1747 docs.push(String::new());
1748 }
1749 }
1750 docs.push(format!("`{} {}`", method, path));
1751 let doc_attrs: Vec<TokenStream> = docs
1752 .iter()
1753 .map(|line| {
1754 let prefixed = if line.is_empty() {
1755 String::new()
1756 } else {
1757 format!(" {line}")
1758 };
1759 quote! { #[doc = #prefixed] }
1760 })
1761 .collect();
1762 quote! { #(#doc_attrs)* }
1763 }
1764
1765 fn get_method_name(&self, op: &OperationInfo) -> syn::Ident {
1767 let name = if !op.operation_id.is_empty() {
1768 op.operation_id.to_snake_case()
1769 } else {
1770 format!(
1772 "{}_{}",
1773 op.method,
1774 op.path.replace('/', "_").replace(['{', '}'], "")
1775 )
1776 .to_snake_case()
1777 };
1778
1779 syn::Ident::new(&name, proc_macro2::Span::call_site())
1780 }
1781
1782 fn http_method_call(&self, op: &OperationInfo) -> TokenStream {
1787 match op.method.to_uppercase().as_str() {
1788 "GET" => quote! { self.http_client.get(request_url) },
1789 "POST" => quote! { self.http_client.post(request_url) },
1790 "PUT" => quote! { self.http_client.put(request_url) },
1791 "DELETE" => quote! { self.http_client.delete(request_url) },
1792 "PATCH" => quote! { self.http_client.patch(request_url) },
1793 "HEAD" => quote! { self.http_client.head(request_url) },
1794 "OPTIONS" => quote! {
1795 self.http_client.request(reqwest::Method::OPTIONS, request_url)
1796 },
1797 "TRACE" => quote! {
1798 self.http_client.request(reqwest::Method::TRACE, request_url)
1799 },
1800 other => {
1805 let upper = other.to_string();
1806 quote! {
1807 self.http_client.request(
1808 reqwest::Method::from_bytes(#upper.as_bytes())
1809 .expect("invalid HTTP method"),
1810 request_url,
1811 )
1812 }
1813 }
1814 }
1815 }
1816
1817 fn generate_request_param(&self, op: &OperationInfo) -> TokenStream {
1819 let mut params = Vec::new();
1820 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
1827 let mut unique_param_ident = |raw: String| -> syn::Ident {
1828 let mut chosen = raw.clone();
1829 let mut suffix = 2;
1830 while !used.insert(chosen.clone()) {
1831 chosen = format!("{raw}_{suffix}");
1832 suffix += 1;
1833 }
1834 Self::to_field_ident(&chosen)
1835 };
1836
1837 for param in &op.parameters {
1839 if param.location == "path" {
1840 let param_name_snake = self.param_ident_str(param);
1841 let param_name = unique_param_ident(param_name_snake);
1842 let param_type = self.get_param_rust_type(param);
1843 params.push(quote! { #param_name: #param_type });
1844 }
1845 }
1846
1847 for param in &op.parameters {
1849 if param.location == "query" {
1850 let param_name_snake = self.param_ident_str(param);
1851 let param_name = unique_param_ident(param_name_snake);
1852 let param_type = self.get_param_rust_type(param);
1853
1854 if param.required {
1856 params.push(quote! { #param_name: #param_type });
1857 } else {
1858 params.push(quote! { #param_name: Option<#param_type> });
1859 }
1860 }
1861 }
1862
1863 for param in &op.parameters {
1869 if param.location == "header" {
1870 let param_name_snake = self.param_ident_str(param);
1871 let param_name = unique_param_ident(param_name_snake);
1872 let param_type = self.get_param_rust_type(param);
1873 if param.required {
1874 params.push(quote! { #param_name: #param_type });
1875 } else {
1876 params.push(quote! { #param_name: Option<#param_type> });
1877 }
1878 }
1879 }
1880
1881 for param in &op.parameters {
1882 if param.location == "cookie" {
1883 let param_name_snake = self.param_ident_str(param);
1884 let param_name = unique_param_ident(param_name_snake);
1885 let param_type = self.get_param_rust_type(param);
1886 if param.required {
1887 params.push(quote! { #param_name: #param_type });
1888 } else {
1889 params.push(quote! { #param_name: Option<#param_type> });
1890 }
1891 }
1892 }
1893
1894 if let Some(ref rb) = op.request_body {
1897 use crate::analysis::RequestBodyContent;
1898 if matches!(rb, RequestBodyContent::SchemaLess { .. }) {
1899 return if params.is_empty() {
1900 quote! {}
1901 } else {
1902 quote! { #(#params),* }
1903 };
1904 }
1905 let required = op.request_body_required;
1906 let body_type = match rb {
1907 RequestBodyContent::Json { schema_name, .. }
1908 | RequestBodyContent::FormUrlEncoded { schema_name, .. } => {
1909 let rust_type_name = self.to_rust_type_name(schema_name);
1910 let request_ident =
1911 syn::Ident::new(&rust_type_name, proc_macro2::Span::call_site());
1912 quote! { #request_ident }
1913 }
1914 RequestBodyContent::Multipart => quote! { reqwest::multipart::Form },
1915 RequestBodyContent::OctetStream => quote! { Vec<u8> },
1916 RequestBodyContent::TextPlain => quote! { String },
1917 RequestBodyContent::Unsupported { .. } => quote! { Vec<u8> },
1918 RequestBodyContent::SchemaLess { .. } => unreachable!(
1919 "schema-less request bodies preserve the historical client signature"
1920 ),
1921 };
1922 let body_ident = match rb {
1923 RequestBodyContent::Multipart => quote! { form },
1924 RequestBodyContent::OctetStream
1925 | RequestBodyContent::TextPlain
1926 | RequestBodyContent::Unsupported { .. } => quote! { body },
1927 RequestBodyContent::SchemaLess { .. } => unreachable!(
1928 "schema-less request bodies preserve the historical client signature"
1929 ),
1930 _ => quote! { request },
1931 };
1932 if required {
1933 params.push(quote! { #body_ident: #body_type });
1934 } else {
1935 params.push(quote! { #body_ident: Option<#body_type> });
1936 }
1937 }
1938
1939 if params.is_empty() {
1940 quote! {}
1941 } else {
1942 quote! { #(#params),* }
1943 }
1944 }
1945
1946 fn get_param_rust_type(&self, param: &crate::analysis::ParameterInfo) -> TokenStream {
1948 if Self::param_has_impl_as_ref_type(param) {
1949 quote! { impl AsRef<str> }
1950 } else {
1951 self.get_param_owned_rust_type(param)
1952 }
1953 }
1954
1955 pub(crate) fn get_param_owned_rust_type(
1959 &self,
1960 param: &crate::analysis::ParameterInfo,
1961 ) -> TokenStream {
1962 use crate::analysis::QuerySerialization;
1963 if let Some(
1969 QuerySerialization::FormExplodedArray { item_type }
1970 | QuerySerialization::FormArray { item_type },
1971 ) = ¶m.query_serialization
1972 {
1973 use crate::analysis::ArrayItemType;
1974 let item_ty: syn::Type = match item_type {
1975 ArrayItemType::Scalar(rust_type) => syn::parse_str(rust_type)
1976 .unwrap_or_else(|_| panic!("invalid scalar item type `{rust_type}`")),
1977 ArrayItemType::EnumRef(schema_name) => {
1978 let rust_name = self.to_rust_type_name(schema_name);
1979 syn::parse_str(&rust_name)
1980 .unwrap_or_else(|_| panic!("invalid enum item type `{rust_name}`"))
1981 }
1982 };
1983 return quote! { Vec<#item_ty> };
1984 }
1985 if let Some(ref schema_name) = param.schema_ref {
1989 let rust_name = self.to_rust_type_name(schema_name);
1990 let ident = syn::Ident::new(&rust_name, proc_macro2::Span::call_site());
1991 return quote! { #ident };
1992 }
1993 syn::parse_str::<syn::Type>(¶m.rust_type)
1994 .map(|ty| quote! { #ty })
1995 .unwrap_or_else(|_| {
1996 let type_ident = syn::Ident::new(¶m.rust_type, proc_macro2::Span::call_site());
1997 quote! { #type_ident }
1998 })
1999 }
2000
2001 fn param_uses_as_ref_str(param: &crate::analysis::ParameterInfo) -> bool {
2006 param.schema_ref.is_none() && param.rust_type == "String"
2007 }
2008
2009 fn generate_request_body(&self, op: &OperationInfo) -> TokenStream {
2014 let Some(rb) = op.request_body.as_ref() else {
2015 return quote! {};
2016 };
2017 use crate::analysis::RequestBodyContent;
2018 let required = op.request_body_required;
2019 let (ident, apply): (TokenStream, TokenStream) = match rb {
2020 RequestBodyContent::Json { media_type, .. } => (
2021 quote! { request },
2022 quote! {
2023 req = req
2024 .body(serde_json::to_vec(&request).map_err(HttpError::serialization_error)?)
2025 .header("content-type", #media_type);
2026 },
2027 ),
2028 RequestBodyContent::FormUrlEncoded { media_type, .. } => (
2029 quote! { request },
2030 quote! {
2031 req = req
2032 .body(serde_urlencoded::to_string(&request).map_err(HttpError::serialization_error)?)
2033 .header("content-type", #media_type);
2034 },
2035 ),
2036 RequestBodyContent::Multipart => (
2037 quote! { form },
2038 quote! {
2039 req = req.multipart(form);
2040 },
2041 ),
2042 RequestBodyContent::OctetStream => (
2043 quote! { body },
2044 quote! {
2045 req = req
2046 .body(body)
2047 .header("content-type", "application/octet-stream");
2048 },
2049 ),
2050 RequestBodyContent::TextPlain => (
2051 quote! { body },
2052 quote! {
2053 req = req
2054 .body(body)
2055 .header("content-type", "text/plain");
2056 },
2057 ),
2058 RequestBodyContent::Unsupported { media_types } => {
2059 let media_type = media_types
2060 .first()
2061 .map(String::as_str)
2062 .unwrap_or("application/octet-stream");
2063 (
2064 quote! { body },
2065 quote! {
2066 req = req
2067 .body(body)
2068 .header("content-type", #media_type);
2069 },
2070 )
2071 }
2072 RequestBodyContent::SchemaLess { .. } => return quote! {},
2073 };
2074 if required {
2075 apply
2076 } else {
2077 quote! {
2078 if let Some(#ident) = #ident {
2079 #apply
2080 }
2081 }
2082 }
2083 }
2084
2085 fn get_success_response_schema<'a>(&self, op: &'a OperationInfo) -> Option<&'a String> {
2091 op.response_schemas
2092 .get("200")
2093 .or_else(|| op.response_schemas.get("201"))
2094 .or_else(|| {
2095 op.response_schemas
2096 .iter()
2097 .find(|(code, _)| code.starts_with('2'))
2098 .map(|(_, v)| v)
2099 })
2100 }
2101
2102 fn get_response_type(&self, op: &OperationInfo) -> TokenStream {
2104 if let Some(response_type) = self.get_success_response_schema(op) {
2105 let rust_type_name = self.to_rust_type_name(response_type);
2107 let response_ident = syn::Ident::new(&rust_type_name, proc_macro2::Span::call_site());
2108 quote! { #response_ident }
2109 } else {
2110 quote! { () }
2111 }
2112 }
2113
2114 fn generate_error_handling(&self, op: &OperationInfo, has_response_body: bool) -> TokenStream {
2123 let op_error_type = self.op_error_type_token(op);
2124
2125 let success_branch = if has_response_body {
2126 quote! {
2127 match serde_json::from_str(&body_text) {
2128 Ok(body) => Ok(body),
2129 Err(e) => Err(ApiOpError::Api(ApiError {
2130 status: status_code,
2131 headers: headers,
2132 body: body_text,
2133 typed: None,
2134 parse_error: Some(format!(
2135 "failed to deserialize 2xx response body: {}",
2136 e
2137 )),
2138 })),
2139 }
2140 }
2141 } else {
2142 quote! {
2143 let _ = body_text;
2144 let _ = headers;
2145 Ok(())
2146 }
2147 };
2148
2149 let error_match_arms = self.generate_error_match_arms(op);
2150
2151 quote! {
2152 let status = response.status();
2153 let status_code = status.as_u16();
2154 let headers = response.headers().clone();
2155 let body_text = response.text().await
2156 .map_err(|e| ApiOpError::Transport(HttpError::Network(e)))?;
2157
2158 if status.is_success() {
2159 #success_branch
2160 } else {
2161 let typed: Option<#op_error_type>;
2162 let parse_error: Option<String>;
2163 #error_match_arms
2164 Err(ApiOpError::Api(ApiError {
2165 status: status_code,
2166 headers,
2167 body: body_text,
2168 typed,
2169 parse_error,
2170 }))
2171 }
2172 }
2173 }
2174
2175 fn generate_error_match_arms(&self, op: &OperationInfo) -> TokenStream {
2178 let arms: Vec<TokenStream> = op
2179 .response_schemas
2180 .iter()
2181 .filter(|(code, _)| !code.starts_with('2'))
2182 .filter_map(|(code, schema)| {
2183 let variant_ident = Self::op_error_variant_ident(code);
2184 let payload_ty_name = self.to_rust_type_name(schema);
2185 let payload_ty = syn::Ident::new(&payload_ty_name, proc_macro2::Span::call_site());
2186 let enum_ident = self.op_error_enum_ident(op);
2187
2188 let pattern = match code.as_str() {
2193 "default" | "Default" => return None, other if other.chars().all(|c| c.is_ascii_digit()) => {
2195 let n: u16 = other.parse().ok()?;
2196 quote! { #n }
2197 }
2198 "1XX" | "1xx" => quote! { code if (100..=199).contains(&code) },
2199 "2XX" | "2xx" => quote! { code if (200..=299).contains(&code) },
2200 "3XX" | "3xx" => quote! { code if (300..=399).contains(&code) },
2201 "4XX" | "4xx" => quote! { code if (400..=499).contains(&code) },
2202 "5XX" | "5xx" => quote! { code if (500..=599).contains(&code) },
2203 _ => return None,
2204 };
2205
2206 Some(quote! {
2207 #pattern => {
2208 match serde_json::from_str::<#payload_ty>(&body_text) {
2209 Ok(v) => {
2210 typed = Some(#enum_ident::#variant_ident(v));
2211 parse_error = None;
2212 }
2213 Err(e) => {
2214 typed = None;
2215 parse_error = Some(e.to_string());
2216 }
2217 }
2218 }
2219 })
2220 })
2221 .collect();
2222
2223 let has_typed_enum = op
2231 .response_schemas
2232 .iter()
2233 .any(|(code, _)| !code.starts_with('2'));
2234
2235 let default_arm = if has_typed_enum {
2236 quote! {
2237 _ => {
2238 typed = None;
2239 parse_error = None;
2240 }
2241 }
2242 } else {
2243 quote! {
2245 _ => {
2246 match serde_json::from_str::<serde_json::Value>(&body_text) {
2247 Ok(v) => {
2248 typed = Some(v);
2249 parse_error = None;
2250 }
2251 Err(e) => {
2252 typed = None;
2253 parse_error = Some(e.to_string());
2254 }
2255 }
2256 }
2257 }
2258 };
2259
2260 if arms.is_empty() {
2261 quote! {
2263 match status_code {
2264 #default_arm
2265 }
2266 }
2267 } else {
2268 quote! {
2269 match status_code {
2270 #(#arms)*
2271 #default_arm
2272 }
2273 }
2274 }
2275 }
2276
2277 fn generate_url_construction(&self, path: &str, op: &OperationInfo) -> TokenStream {
2279 if path.contains('{') {
2281 self.generate_url_with_params(path, op)
2282 } else {
2283 quote! {
2284 let request_url = format!("{}{}", self.base_url, #path);
2285 }
2286 }
2287 }
2288
2289 fn generate_url_with_params(&self, path: &str, op: &OperationInfo) -> TokenStream {
2291 let path_params: Vec<_> = op
2293 .parameters
2294 .iter()
2295 .filter(|p| p.location == "path")
2296 .collect();
2297
2298 let mut format_string = String::with_capacity(path.len());
2307 let mut format_args: Vec<TokenStream> = Vec::new();
2308 let mut chars = path.chars().peekable();
2309 while let Some(c) = chars.next() {
2310 if c != '{' {
2311 format_string.push(c);
2312 continue;
2313 }
2314 let mut name = String::new();
2316 while let Some(&n) = chars.peek() {
2317 chars.next();
2318 if n == '}' {
2319 break;
2320 }
2321 name.push(n);
2322 }
2323 let param = path_params.iter().find(|p| p.name == name);
2327 let Some(param) = param else {
2328 format_string.push('{');
2329 format_string.push_str(&name);
2330 format_string.push('}');
2331 continue;
2332 };
2333 format_string.push_str("{}");
2334 let param_name_snake = self.param_ident_str(param);
2335 let param_ident = Self::to_field_ident(¶m_name_snake);
2336 if Self::param_uses_as_ref_str(param) {
2337 format_args.push(quote! {
2338 __pct_encode_path_segment(#param_ident.as_ref())
2339 });
2340 } else {
2341 format_args.push(quote! {
2342 __pct_encode_path_segment(&#param_ident.to_string())
2343 });
2344 }
2345 }
2346
2347 if format_args.is_empty() {
2348 quote! {
2349 let request_url = format!("{}{}", self.base_url, #path);
2350 }
2351 } else {
2352 quote! {
2353 let request_url = format!("{}{}", self.base_url, format!(#format_string, #(#format_args),*));
2354 }
2355 }
2356 }
2357
2358 pub(crate) fn param_ident_str(&self, param: &crate::analysis::ParameterInfo) -> String {
2363 if let Some(ident) = ¶m.rust_ident {
2364 return self.escape_keyword_ident(ident);
2368 }
2369 self.sanitize_param_name(¶m.name)
2370 }
2371
2372 fn escape_keyword_ident(&self, snake_case: &str) -> String {
2373 if matches!(snake_case, "self" | "super" | "crate" | "Self") {
2374 return format!("{snake_case}_param");
2375 }
2376 if Self::is_rust_keyword(snake_case) {
2377 format!("r#{snake_case}")
2378 } else {
2379 snake_case.to_string()
2380 }
2381 }
2382
2383 fn sanitize_param_name(&self, name: &str) -> String {
2388 let suffix = if name.ends_with("<=") {
2393 "_lte"
2394 } else if name.ends_with(">=") {
2395 "_gte"
2396 } else if name.ends_with('<') {
2397 "_lt"
2398 } else if name.ends_with('>') {
2399 "_gt"
2400 } else {
2401 ""
2402 };
2403 let stripped = name.trim_end_matches(['<', '>', '=']);
2404 let mut snake_case = stripped.to_snake_case();
2405 if snake_case.is_empty() {
2406 snake_case.push_str("parameter");
2407 } else if snake_case.starts_with(|character: char| character.is_ascii_digit()) {
2408 snake_case.insert(0, '_');
2409 }
2410 snake_case.push_str(suffix);
2411
2412 if matches!(snake_case.as_str(), "self" | "super" | "crate" | "Self") {
2413 return format!("{snake_case}_param");
2414 }
2415 if Self::is_rust_keyword(&snake_case) {
2416 format!("r#{snake_case}")
2417 } else {
2418 snake_case
2419 }
2420 }
2421}