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"] {
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 => {
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 };
893 let body_type_name = self.to_rust_type_name(body_name);
894 let body_type = syn::Ident::new(&body_type_name, proc_macro2::Span::call_site());
895 let Some((resolved_name, resolved_schema)) =
896 self.resolve_reference_schema(body_name, analysis)
897 else {
898 return Some(BodyModelPlan {
899 body_ident,
900 body_type: quote! { #body_type },
901 required_construction: RequiredBodyConstruction::Whole,
902 optional_fields: Vec::new(),
903 });
904 };
905
906 let mut optional_fields = Vec::new();
907 let mut stack = std::collections::HashSet::new();
908 self.collect_optional_body_fields(
909 resolved_name,
910 Vec::new(),
911 analysis,
912 &mut stack,
913 &mut optional_fields,
914 );
915
916 let required_construction = match &resolved_schema.schema_type {
917 SchemaType::Object {
918 properties,
919 required,
920 additional_properties,
921 } if !self.is_discriminated_variant(resolved_name, analysis) => {
922 let emitted = self.emitted_object_properties(
923 resolved_name,
924 properties,
925 required,
926 additional_properties,
927 analysis,
928 None,
929 );
930 let required_fields: Vec<_> = emitted
931 .iter()
932 .filter(|field| field.is_required)
933 .map(|field| BodyConstructorParam {
934 preferred_ident: field.ident.clone(),
935 value_type: field.field_type.clone(),
936 })
937 .collect();
938 if required_fields.is_empty() {
939 RequiredBodyConstruction::Default
940 } else if emitted.iter().any(|field| !field.is_required)
941 || !matches!(additional_properties, ObjectAdditionalProperties::Forbidden)
942 {
943 RequiredBodyConstruction::New(required_fields)
944 } else {
945 RequiredBodyConstruction::Whole
946 }
947 }
948 _ => RequiredBodyConstruction::Whole,
949 };
950
951 Some(BodyModelPlan {
952 body_ident,
953 body_type: quote! { #body_type },
954 required_construction,
955 optional_fields,
956 })
957 }
958
959 fn resolve_reference_schema<'a>(
960 &self,
961 schema_name: &'a str,
962 analysis: &'a SchemaAnalysis,
963 ) -> Option<(&'a str, &'a crate::analysis::AnalyzedSchema)> {
964 let mut current = schema_name;
965 let mut visited = std::collections::HashSet::new();
966 loop {
967 if !visited.insert(current) {
968 return None;
969 }
970 let schema = analysis.schemas.get(current)?;
971 if let crate::analysis::SchemaType::Reference { target } = &schema.schema_type {
972 current = target;
973 } else {
974 return Some((current, schema));
975 }
976 }
977 }
978
979 fn collect_optional_body_fields(
980 &self,
981 schema_name: &str,
982 access_path: Vec<syn::Ident>,
983 analysis: &SchemaAnalysis,
984 stack: &mut std::collections::HashSet<String>,
985 output: &mut Vec<BodyFieldPlan>,
986 ) {
987 use crate::analysis::SchemaType;
988 if !stack.insert(schema_name.to_string()) {
989 return;
990 }
991 let Some(schema) = analysis.schemas.get(schema_name) else {
992 stack.remove(schema_name);
993 return;
994 };
995 match &schema.schema_type {
996 SchemaType::Reference { target } => {
997 self.collect_optional_body_fields(target, access_path, analysis, stack, output);
998 }
999 SchemaType::Object {
1000 properties,
1001 required,
1002 additional_properties,
1003 } if !self.is_discriminated_variant(schema_name, analysis) => {
1004 for field in self.emitted_object_properties(
1005 schema_name,
1006 properties,
1007 required,
1008 additional_properties,
1009 analysis,
1010 None,
1011 ) {
1012 if field.is_required {
1013 continue;
1014 }
1015 let mut field_path = access_path.clone();
1016 field_path.push(field.ident.clone());
1017 output.push(BodyFieldPlan {
1018 wire_name: field.wire_name.to_string(),
1019 preferred_method_name: field.ident.to_string(),
1020 value_ident: field.ident.clone(),
1021 value_type: self.generate_property_base_type(
1022 schema_name,
1023 field.wire_name,
1024 field.property,
1025 analysis,
1026 ),
1027 access_path: field_path,
1028 });
1029 }
1030 }
1031 SchemaType::Composition { schemas } => {
1032 for (index, schema_ref) in schemas.iter().enumerate() {
1033 let mut nested_path = access_path.clone();
1034 nested_path.push(format_ident!("part_{index}"));
1035 self.collect_optional_body_fields(
1036 &schema_ref.target,
1037 nested_path,
1038 analysis,
1039 stack,
1040 output,
1041 );
1042 }
1043 }
1044 _ => {}
1045 }
1046 stack.remove(schema_name);
1047 }
1048
1049 fn is_discriminated_variant(&self, schema_name: &str, analysis: &SchemaAnalysis) -> bool {
1050 analysis.schemas.values().any(|schema| {
1051 matches!(
1052 &schema.schema_type,
1053 crate::analysis::SchemaType::DiscriminatedUnion { variants, .. }
1054 if variants.iter().any(|variant| variant.type_name == schema_name)
1055 )
1056 })
1057 }
1058
1059 fn generate_param_enum_types(&self, operations: &[&OperationInfo]) -> TokenStream {
1064 let mut by_name: BTreeMap<String, &ParameterInfo> = BTreeMap::new();
1065 for op in operations {
1066 for param in &op.parameters {
1067 if param.enum_values.is_some() {
1068 by_name.entry(param.rust_type.clone()).or_insert(param);
1069 }
1070 }
1071 }
1072
1073 if by_name.is_empty() {
1074 return quote! {};
1075 }
1076
1077 let defs: Vec<TokenStream> = by_name
1078 .values()
1079 .map(|param| self.generate_single_param_enum(param))
1080 .collect();
1081
1082 quote! { #(#defs)* }
1083 }
1084
1085 fn generate_single_param_enum(&self, param: &ParameterInfo) -> TokenStream {
1086 let Some(values) = param.enum_values.as_deref() else {
1087 return quote! {};
1088 };
1089
1090 let enum_ident = format_ident!("{}", param.rust_type);
1091
1092 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
1098 let variant_names: Vec<String> = values
1099 .iter()
1100 .map(|value| {
1101 let base = self.to_rust_enum_variant(value);
1102 let mut chosen = base.clone();
1103 let mut suffix = 2;
1104 while !used.insert(chosen.clone()) {
1105 chosen = format!("{base}_{suffix}");
1106 suffix += 1;
1107 }
1108 chosen
1109 })
1110 .collect();
1111
1112 let variants: Vec<TokenStream> = values
1113 .iter()
1114 .zip(&variant_names)
1115 .map(|(value, name)| {
1116 let variant_ident = format_ident!("{}", name);
1117 quote! {
1118 #[serde(rename = #value)]
1119 #variant_ident,
1120 }
1121 })
1122 .collect();
1123
1124 let display_arms: Vec<TokenStream> = values
1125 .iter()
1126 .zip(&variant_names)
1127 .map(|(value, name)| {
1128 let variant_ident = format_ident!("{}", name);
1129 quote! { Self::#variant_ident => #value, }
1130 })
1131 .collect();
1132
1133 let doc = format!(
1134 "Allowed values for the `{}` {} parameter.",
1135 param.name, param.location
1136 );
1137
1138 quote! {
1139 #[doc = #doc]
1140 #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
1141 pub enum #enum_ident {
1142 #(#variants)*
1143 }
1144
1145 impl #enum_ident {
1146 pub fn as_str(&self) -> &'static str {
1147 match self {
1148 #(#display_arms)*
1149 }
1150 }
1151 }
1152
1153 impl std::fmt::Display for #enum_ident {
1154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1155 f.write_str(self.as_str())
1156 }
1157 }
1158
1159 impl AsRef<str> for #enum_ident {
1160 fn as_ref(&self) -> &str {
1161 self.as_str()
1162 }
1163 }
1164 }
1165 }
1166
1167 fn generate_op_error_enum(&self, op: &OperationInfo) -> Option<TokenStream> {
1172 let variants: Vec<(String, String)> = op
1173 .response_schemas
1174 .iter()
1175 .filter(|(code, _)| !code.starts_with('2'))
1176 .map(|(code, schema)| (code.clone(), schema.clone()))
1177 .collect();
1178
1179 if variants.is_empty() {
1180 return None;
1181 }
1182
1183 let enum_ident = self.op_error_enum_ident(op);
1184 let variant_decls: Vec<TokenStream> = variants
1185 .iter()
1186 .map(|(code, schema)| {
1187 let variant_ident = Self::op_error_variant_ident(code);
1188 let payload_ty_name = self.to_rust_type_name(schema);
1189 let payload_ty = syn::Ident::new(&payload_ty_name, proc_macro2::Span::call_site());
1190 quote! { #variant_ident(#payload_ty) }
1191 })
1192 .collect();
1193
1194 let doc = format!(
1195 "Typed error responses for `{}`. One variant per declared non-2xx response.",
1196 op.operation_id
1197 );
1198
1199 Some(quote! {
1200 #[doc = #doc]
1201 #[derive(Debug, Clone)]
1202 pub enum #enum_ident {
1203 #(#variant_decls,)*
1204 }
1205 })
1206 }
1207
1208 fn op_error_enum_ident(&self, op: &OperationInfo) -> syn::Ident {
1210 use heck::ToPascalCase;
1211 let name = format!(
1212 "{}ApiError",
1213 op.operation_id.replace('.', "_").to_pascal_case()
1214 );
1215 syn::Ident::new(&name, proc_macro2::Span::call_site())
1216 }
1217
1218 fn op_error_variant_ident(status_code: &str) -> syn::Ident {
1221 let raw = match status_code {
1222 "default" | "Default" => "Default".to_string(),
1223 other if other.chars().all(|c| c.is_ascii_digit()) => format!("Status{other}"),
1224 other => format!("Status{}", other.to_ascii_lowercase()),
1225 };
1226 syn::Ident::new(&raw, proc_macro2::Span::call_site())
1227 }
1228
1229 fn op_error_type_token(&self, op: &OperationInfo) -> TokenStream {
1233 if op
1234 .response_schemas
1235 .iter()
1236 .any(|(code, _)| !code.starts_with('2'))
1237 {
1238 let ident = self.op_error_enum_ident(op);
1239 quote! { #ident }
1240 } else {
1241 quote! { serde_json::Value }
1242 }
1243 }
1244
1245 fn generate_single_operation_method(&self, op: &OperationInfo) -> TokenStream {
1247 let method_name = self.get_method_name(op);
1248 let http_method_call = self.http_method_call(op);
1249 let path = &op.path;
1250 let request_param = self.generate_request_param(op);
1251 let request_body = self.generate_request_body(op);
1252 let query_params = self.generate_query_params(op);
1253 let header_params = self.generate_header_params(op);
1254 let auth_application = self.generate_auth_application();
1255 let response_type = self.get_response_type(op);
1256 let has_response_body = self.get_success_response_schema(op).is_some();
1257 let op_error_type = self.op_error_type_token(op);
1258 let error_handling = self.generate_error_handling(op, has_response_body);
1259 let url_construction = self.generate_url_construction(path, op);
1260 let doc_comment = self.generate_operation_doc_comment(op);
1261
1262 quote! {
1263 #doc_comment
1264 pub async fn #method_name(
1265 &self,
1266 #request_param
1267 ) -> Result<#response_type, ApiOpError<#op_error_type>> {
1268 #url_construction
1269
1270 let mut req = #http_method_call;
1271 #request_body
1272
1273 #query_params
1274 #header_params
1275
1276 #auth_application
1279
1280 for (name, value) in &self.custom_headers {
1282 req = req.header(name, value);
1283 }
1284
1285 let response = req.send().await?;
1286 #error_handling
1287 }
1288 }
1289 }
1290
1291 fn generate_auth_application(&self) -> TokenStream {
1295 use crate::http_config::AuthConfig;
1296 match &self.config().auth_config {
1297 Some(AuthConfig::Bearer { header_name }) if header_name == "Authorization" => quote! {
1298 if let Some(api_key) = &self.api_key {
1299 req = req.bearer_auth(api_key);
1300 }
1301 },
1302 Some(AuthConfig::Bearer { header_name }) => {
1303 let h = header_name.clone();
1304 quote! {
1305 if let Some(api_key) = &self.api_key {
1306 req = req.header(#h, format!("Bearer {}", api_key));
1307 }
1308 }
1309 }
1310 Some(AuthConfig::ApiKey { header_name }) => {
1311 let h = header_name.clone();
1312 quote! {
1313 if let Some(api_key) = &self.api_key {
1314 req = req.header(#h, api_key.as_str());
1315 }
1316 }
1317 }
1318 Some(AuthConfig::Custom {
1319 header_name,
1320 header_value_prefix,
1321 }) => {
1322 let h = header_name.clone();
1323 let prefix = header_value_prefix.clone().unwrap_or_default();
1324 if prefix.is_empty() {
1325 quote! {
1326 if let Some(api_key) = &self.api_key {
1327 req = req.header(#h, api_key.as_str());
1328 }
1329 }
1330 } else {
1331 let format_str = format!("{}{{}}", prefix);
1332 quote! {
1333 if let Some(api_key) = &self.api_key {
1334 req = req.header(#h, format!(#format_str, api_key));
1335 }
1336 }
1337 }
1338 }
1339 None => quote! {
1340 if let Some(api_key) = &self.api_key {
1341 req = req.bearer_auth(api_key);
1342 }
1343 },
1344 }
1345 }
1346
1347 fn generate_header_params(&self, op: &OperationInfo) -> TokenStream {
1351 let header_params: Vec<_> = op
1352 .parameters
1353 .iter()
1354 .filter(|p| p.location == "header")
1355 .collect();
1356 if header_params.is_empty() {
1357 return quote! {};
1358 }
1359 let mut emit = Vec::new();
1360 for param in header_params {
1361 let param_name_snake = self.param_ident_str(param);
1362 let param_ident = Self::to_field_ident(¶m_name_snake);
1363 let header_name = ¶m.name;
1364 if param.required {
1365 if Self::param_uses_as_ref_str(param) {
1366 emit.push(quote! {
1367 req = req.header(#header_name, #param_ident.as_ref());
1368 });
1369 } else {
1370 emit.push(quote! {
1371 req = req.header(#header_name, #param_ident.to_string());
1372 });
1373 }
1374 } else if Self::param_uses_as_ref_str(param) {
1375 emit.push(quote! {
1376 if let Some(v) = #param_ident {
1377 req = req.header(#header_name, v.as_ref());
1378 }
1379 });
1380 } else {
1381 emit.push(quote! {
1382 if let Some(v) = #param_ident {
1383 req = req.header(#header_name, v.to_string());
1384 }
1385 });
1386 }
1387 }
1388 quote! {
1389 #(#emit)*
1390 }
1391 }
1392
1393 fn generate_query_params(&self, op: &OperationInfo) -> TokenStream {
1395 let query_params: Vec<_> = op
1396 .parameters
1397 .iter()
1398 .filter(|p| p.location == "query")
1399 .collect();
1400
1401 if query_params.is_empty() {
1402 return quote! {};
1403 }
1404
1405 let mut param_building = Vec::new();
1406 let mut req_appends = Vec::new();
1410
1411 for param in query_params {
1412 use crate::analysis::QuerySerialization;
1413
1414 let param_name_snake = self.param_ident_str(param);
1416 let param_name = Self::to_field_ident(¶m_name_snake);
1417
1418 let param_key = ¶m.name;
1420
1421 match ¶m.query_serialization {
1422 Some(QuerySerialization::FormExplodedObject) => {
1423 let apply = quote! {
1430 let __empty = match serde_json::to_value(&v)
1431 .map_err(HttpError::serialization_error)?
1432 {
1433 serde_json::Value::Object(map) => map.is_empty(),
1434 _ => false,
1435 };
1436 if __empty {
1437 req = req.query(&[(format!("{}[]", #param_key), String::new())]);
1438 } else {
1439 req = req.query(&v);
1440 }
1441 };
1442 if param.required {
1443 req_appends.push(quote! {
1444 {
1445 let v = #param_name;
1446 #apply
1447 }
1448 });
1449 } else {
1450 req_appends.push(quote! {
1451 if let Some(v) = #param_name {
1452 #apply
1453 }
1454 });
1455 }
1456 continue;
1457 }
1458 Some(QuerySerialization::DeepObject) => {
1459 let apply = quote! {
1463 let map = match serde_json::to_value(&v)
1464 .map_err(HttpError::serialization_error)?
1465 {
1466 serde_json::Value::Object(map) => map,
1467 _ => return Err(HttpError::serialization_error(
1468 format!("query parameter `{}` did not serialize as an object", #param_key)
1469 ).into()),
1470 };
1471 let mut deep_params: Vec<(String, String)> = Vec::new();
1472 for (k, val) in map {
1473 let s = match val {
1474 serde_json::Value::Null => continue,
1475 serde_json::Value::String(s) => s,
1476 other => other.to_string(),
1477 };
1478 deep_params.push((format!("{}[{}]", #param_key, k), s));
1479 }
1480 if deep_params.is_empty() {
1481 deep_params.push((format!("{}[]", #param_key), String::new()));
1482 }
1483 req = req.query(&deep_params);
1484 };
1485 if param.required {
1486 req_appends.push(quote! {
1487 {
1488 let v = #param_name;
1489 #apply
1490 }
1491 });
1492 } else {
1493 req_appends.push(quote! {
1494 if let Some(v) = #param_name {
1495 #apply
1496 }
1497 });
1498 }
1499 continue;
1500 }
1501 Some(QuerySerialization::FormObject) => {
1502 let apply = quote! {
1506 let map = match serde_json::to_value(&v)
1507 .map_err(HttpError::serialization_error)?
1508 {
1509 serde_json::Value::Object(map) => map,
1510 _ => return Err(HttpError::serialization_error(
1511 format!("query parameter `{}` did not serialize as an object", #param_key)
1512 ).into()),
1513 };
1514 let mut parts: Vec<String> = Vec::new();
1515 for (k, val) in map {
1516 let s = match val {
1517 serde_json::Value::Null => continue,
1518 serde_json::Value::String(s) => s,
1519 other => other.to_string(),
1520 };
1521 if k.contains(',') || s.contains(',') {
1522 return Err(HttpError::serialization_error(
1523 format!(
1524 "query object `{}` contains a comma in key `{}`; use explode=true for lossless string values",
1525 #param_key,
1526 k,
1527 )
1528 ).into());
1529 }
1530 parts.push(k);
1531 parts.push(s);
1532 }
1533 if parts.is_empty() {
1534 query_params.push((
1535 format!("{}[]", #param_key),
1536 String::new(),
1537 ));
1538 } else {
1539 query_params.push((#param_key.to_string(), parts.join(",")));
1540 }
1541 };
1542 if param.required {
1543 param_building.push(quote! {
1544 {
1545 let v = #param_name;
1546 #apply
1547 }
1548 });
1549 } else {
1550 param_building.push(quote! {
1551 if let Some(v) = #param_name {
1552 #apply
1553 }
1554 });
1555 }
1556 continue;
1557 }
1558 Some(QuerySerialization::FormExplodedArray { .. }) => {
1559 if param.required {
1561 param_building.push(quote! {
1562 if #param_name.is_empty() {
1563 query_params.push((
1564 format!("{}[]", #param_key),
1565 String::new(),
1566 ));
1567 } else {
1568 for item in #param_name {
1569 query_params.push((#param_key.to_string(), item.to_string()));
1570 }
1571 }
1572 });
1573 } else {
1574 param_building.push(quote! {
1575 if let Some(v) = #param_name {
1576 if v.is_empty() {
1577 query_params.push((
1578 format!("{}[]", #param_key),
1579 String::new(),
1580 ));
1581 } else {
1582 for item in v {
1583 query_params.push((#param_key.to_string(), item.to_string()));
1584 }
1585 }
1586 }
1587 });
1588 }
1589 continue;
1590 }
1591 Some(QuerySerialization::FormArray { .. }) => {
1592 let apply = quote! {
1595 if v.is_empty() {
1596 query_params.push((
1597 format!("{}[]", #param_key),
1598 String::new(),
1599 ));
1600 } else {
1601 let mut parts = Vec::with_capacity(v.len());
1602 for item in &v {
1603 let item = item.to_string();
1604 if item.contains(',') {
1605 return Err(HttpError::serialization_error(
1606 format!(
1607 "query array `{}` contains a comma; use explode=true for lossless string values",
1608 #param_key,
1609 )
1610 ).into());
1611 }
1612 parts.push(item);
1613 }
1614 query_params.push((
1615 #param_key.to_string(),
1616 parts.join(","),
1617 ));
1618 }
1619 };
1620 if param.required {
1621 param_building.push(quote! {
1622 {
1623 let v = #param_name;
1624 #apply
1625 }
1626 });
1627 } else {
1628 param_building.push(quote! {
1629 if let Some(v) = #param_name {
1630 #apply
1631 }
1632 });
1633 }
1634 continue;
1635 }
1636 Some(QuerySerialization::Unsupported { .. }) => {}
1637 None => {}
1638 }
1639
1640 if param.required {
1641 if Self::param_uses_as_ref_str(param) {
1643 param_building.push(quote! {
1644 query_params.push((#param_key.to_string(), #param_name.as_ref().to_string()));
1645 });
1646 } else {
1647 param_building.push(quote! {
1648 query_params.push((#param_key.to_string(), #param_name.to_string()));
1649 });
1650 }
1651 } else {
1652 if Self::param_uses_as_ref_str(param) {
1654 param_building.push(quote! {
1655 if let Some(v) = #param_name {
1656 query_params.push((#param_key.to_string(), v.as_ref().to_string()));
1657 }
1658 });
1659 } else {
1660 param_building.push(quote! {
1661 if let Some(v) = #param_name {
1662 query_params.push((#param_key.to_string(), v.to_string()));
1663 }
1664 });
1665 }
1666 }
1667 }
1668
1669 let pairs_block = if param_building.is_empty() {
1672 quote! {}
1673 } else {
1674 quote! {
1675 {
1676 let mut query_params: Vec<(String, String)> = Vec::new();
1677 #(#param_building)*
1678 if !query_params.is_empty() {
1679 req = req.query(&query_params);
1680 }
1681 }
1682 }
1683 };
1684
1685 quote! {
1686 #pairs_block
1688 #(#req_appends)*
1689 }
1690 }
1691
1692 fn generate_operation_doc_comment(&self, op: &OperationInfo) -> TokenStream {
1696 let method = op.method.to_uppercase();
1697 let path = &op.path;
1698 let mut docs: Vec<String> = Vec::new();
1699 if let Some(s) = &op.summary {
1700 if !s.is_empty() {
1701 docs.push(s.clone());
1702 docs.push(String::new());
1703 }
1704 }
1705 if let Some(d) = &op.description {
1706 if !d.is_empty() {
1707 for line in d.lines() {
1708 docs.push(line.to_string());
1709 }
1710 docs.push(String::new());
1711 }
1712 }
1713 docs.push(format!("`{} {}`", method, path));
1714 let doc_attrs: Vec<TokenStream> = docs
1715 .iter()
1716 .map(|line| {
1717 let prefixed = if line.is_empty() {
1718 String::new()
1719 } else {
1720 format!(" {line}")
1721 };
1722 quote! { #[doc = #prefixed] }
1723 })
1724 .collect();
1725 quote! { #(#doc_attrs)* }
1726 }
1727
1728 fn get_method_name(&self, op: &OperationInfo) -> syn::Ident {
1730 let name = if !op.operation_id.is_empty() {
1731 op.operation_id.to_snake_case()
1732 } else {
1733 format!(
1735 "{}_{}",
1736 op.method,
1737 op.path.replace('/', "_").replace(['{', '}'], "")
1738 )
1739 .to_snake_case()
1740 };
1741
1742 syn::Ident::new(&name, proc_macro2::Span::call_site())
1743 }
1744
1745 fn http_method_call(&self, op: &OperationInfo) -> TokenStream {
1750 match op.method.to_uppercase().as_str() {
1751 "GET" => quote! { self.http_client.get(request_url) },
1752 "POST" => quote! { self.http_client.post(request_url) },
1753 "PUT" => quote! { self.http_client.put(request_url) },
1754 "DELETE" => quote! { self.http_client.delete(request_url) },
1755 "PATCH" => quote! { self.http_client.patch(request_url) },
1756 "HEAD" => quote! { self.http_client.head(request_url) },
1757 "OPTIONS" => quote! {
1758 self.http_client.request(reqwest::Method::OPTIONS, request_url)
1759 },
1760 "TRACE" => quote! {
1761 self.http_client.request(reqwest::Method::TRACE, request_url)
1762 },
1763 other => {
1768 let upper = other.to_string();
1769 quote! {
1770 self.http_client.request(
1771 reqwest::Method::from_bytes(#upper.as_bytes())
1772 .expect("invalid HTTP method"),
1773 request_url,
1774 )
1775 }
1776 }
1777 }
1778 }
1779
1780 fn generate_request_param(&self, op: &OperationInfo) -> TokenStream {
1782 let mut params = Vec::new();
1783 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
1790 let mut unique_param_ident = |raw: String| -> syn::Ident {
1791 let mut chosen = raw.clone();
1792 let mut suffix = 2;
1793 while !used.insert(chosen.clone()) {
1794 chosen = format!("{raw}_{suffix}");
1795 suffix += 1;
1796 }
1797 Self::to_field_ident(&chosen)
1798 };
1799
1800 for param in &op.parameters {
1802 if param.location == "path" {
1803 let param_name_snake = self.param_ident_str(param);
1804 let param_name = unique_param_ident(param_name_snake);
1805 let param_type = self.get_param_rust_type(param);
1806 params.push(quote! { #param_name: #param_type });
1807 }
1808 }
1809
1810 for param in &op.parameters {
1812 if param.location == "query" {
1813 let param_name_snake = self.param_ident_str(param);
1814 let param_name = unique_param_ident(param_name_snake);
1815 let param_type = self.get_param_rust_type(param);
1816
1817 if param.required {
1819 params.push(quote! { #param_name: #param_type });
1820 } else {
1821 params.push(quote! { #param_name: Option<#param_type> });
1822 }
1823 }
1824 }
1825
1826 for param in &op.parameters {
1832 if param.location == "header" {
1833 let param_name_snake = self.param_ident_str(param);
1834 let param_name = unique_param_ident(param_name_snake);
1835 let param_type = self.get_param_rust_type(param);
1836 if param.required {
1837 params.push(quote! { #param_name: #param_type });
1838 } else {
1839 params.push(quote! { #param_name: Option<#param_type> });
1840 }
1841 }
1842 }
1843
1844 if let Some(ref rb) = op.request_body {
1847 use crate::analysis::RequestBodyContent;
1848 let required = op.request_body_required;
1849 let body_type = match rb {
1850 RequestBodyContent::Json { schema_name }
1851 | RequestBodyContent::FormUrlEncoded { schema_name } => {
1852 let rust_type_name = self.to_rust_type_name(schema_name);
1853 let request_ident =
1854 syn::Ident::new(&rust_type_name, proc_macro2::Span::call_site());
1855 quote! { #request_ident }
1856 }
1857 RequestBodyContent::Multipart => quote! { reqwest::multipart::Form },
1858 RequestBodyContent::OctetStream => quote! { Vec<u8> },
1859 RequestBodyContent::TextPlain => quote! { String },
1860 };
1861 let body_ident = match rb {
1862 RequestBodyContent::Multipart => quote! { form },
1863 RequestBodyContent::OctetStream | RequestBodyContent::TextPlain => quote! { body },
1864 _ => quote! { request },
1865 };
1866 if required {
1867 params.push(quote! { #body_ident: #body_type });
1868 } else {
1869 params.push(quote! { #body_ident: Option<#body_type> });
1870 }
1871 }
1872
1873 if params.is_empty() {
1874 quote! {}
1875 } else {
1876 quote! { #(#params),* }
1877 }
1878 }
1879
1880 fn get_param_rust_type(&self, param: &crate::analysis::ParameterInfo) -> TokenStream {
1882 if Self::param_has_impl_as_ref_type(param) {
1883 quote! { impl AsRef<str> }
1884 } else {
1885 self.get_param_owned_rust_type(param)
1886 }
1887 }
1888
1889 pub(crate) fn get_param_owned_rust_type(
1893 &self,
1894 param: &crate::analysis::ParameterInfo,
1895 ) -> TokenStream {
1896 use crate::analysis::QuerySerialization;
1897 if let Some(
1903 QuerySerialization::FormExplodedArray { item_type }
1904 | QuerySerialization::FormArray { item_type },
1905 ) = ¶m.query_serialization
1906 {
1907 use crate::analysis::ArrayItemType;
1908 let item_ty: syn::Type = match item_type {
1909 ArrayItemType::Scalar(rust_type) => syn::parse_str(rust_type)
1910 .unwrap_or_else(|_| panic!("invalid scalar item type `{rust_type}`")),
1911 ArrayItemType::EnumRef(schema_name) => {
1912 let rust_name = self.to_rust_type_name(schema_name);
1913 syn::parse_str(&rust_name)
1914 .unwrap_or_else(|_| panic!("invalid enum item type `{rust_name}`"))
1915 }
1916 };
1917 return quote! { Vec<#item_ty> };
1918 }
1919 if let Some(ref schema_name) = param.schema_ref {
1923 let rust_name = self.to_rust_type_name(schema_name);
1924 let ident = syn::Ident::new(&rust_name, proc_macro2::Span::call_site());
1925 return quote! { #ident };
1926 }
1927 syn::parse_str::<syn::Type>(¶m.rust_type)
1928 .map(|ty| quote! { #ty })
1929 .unwrap_or_else(|_| {
1930 let type_ident = syn::Ident::new(¶m.rust_type, proc_macro2::Span::call_site());
1931 quote! { #type_ident }
1932 })
1933 }
1934
1935 fn param_uses_as_ref_str(param: &crate::analysis::ParameterInfo) -> bool {
1940 param.schema_ref.is_none() && param.rust_type == "String"
1941 }
1942
1943 fn generate_request_body(&self, op: &OperationInfo) -> TokenStream {
1948 let Some(rb) = op.request_body.as_ref() else {
1949 return quote! {};
1950 };
1951 use crate::analysis::RequestBodyContent;
1952 let required = op.request_body_required;
1953 let (ident, apply): (TokenStream, TokenStream) = match rb {
1954 RequestBodyContent::Json { .. } => (
1955 quote! { request },
1956 quote! {
1957 req = req
1958 .body(serde_json::to_vec(&request).map_err(HttpError::serialization_error)?)
1959 .header("content-type", "application/json");
1960 },
1961 ),
1962 RequestBodyContent::FormUrlEncoded { .. } => (
1963 quote! { request },
1964 quote! {
1965 req = req
1966 .body(serde_urlencoded::to_string(&request).map_err(HttpError::serialization_error)?)
1967 .header("content-type", "application/x-www-form-urlencoded");
1968 },
1969 ),
1970 RequestBodyContent::Multipart => (
1971 quote! { form },
1972 quote! {
1973 req = req.multipart(form);
1974 },
1975 ),
1976 RequestBodyContent::OctetStream => (
1977 quote! { body },
1978 quote! {
1979 req = req
1980 .body(body)
1981 .header("content-type", "application/octet-stream");
1982 },
1983 ),
1984 RequestBodyContent::TextPlain => (
1985 quote! { body },
1986 quote! {
1987 req = req
1988 .body(body)
1989 .header("content-type", "text/plain");
1990 },
1991 ),
1992 };
1993 if required {
1994 apply
1995 } else {
1996 quote! {
1997 if let Some(#ident) = #ident {
1998 #apply
1999 }
2000 }
2001 }
2002 }
2003
2004 fn get_success_response_schema<'a>(&self, op: &'a OperationInfo) -> Option<&'a String> {
2010 op.response_schemas
2011 .get("200")
2012 .or_else(|| op.response_schemas.get("201"))
2013 .or_else(|| {
2014 op.response_schemas
2015 .iter()
2016 .find(|(code, _)| code.starts_with('2'))
2017 .map(|(_, v)| v)
2018 })
2019 }
2020
2021 fn get_response_type(&self, op: &OperationInfo) -> TokenStream {
2023 if let Some(response_type) = self.get_success_response_schema(op) {
2024 let rust_type_name = self.to_rust_type_name(response_type);
2026 let response_ident = syn::Ident::new(&rust_type_name, proc_macro2::Span::call_site());
2027 quote! { #response_ident }
2028 } else {
2029 quote! { () }
2030 }
2031 }
2032
2033 fn generate_error_handling(&self, op: &OperationInfo, has_response_body: bool) -> TokenStream {
2042 let op_error_type = self.op_error_type_token(op);
2043
2044 let success_branch = if has_response_body {
2045 quote! {
2046 match serde_json::from_str(&body_text) {
2047 Ok(body) => Ok(body),
2048 Err(e) => Err(ApiOpError::Api(ApiError {
2049 status: status_code,
2050 headers: headers,
2051 body: body_text,
2052 typed: None,
2053 parse_error: Some(format!(
2054 "failed to deserialize 2xx response body: {}",
2055 e
2056 )),
2057 })),
2058 }
2059 }
2060 } else {
2061 quote! {
2062 let _ = body_text;
2063 let _ = headers;
2064 Ok(())
2065 }
2066 };
2067
2068 let error_match_arms = self.generate_error_match_arms(op);
2069
2070 quote! {
2071 let status = response.status();
2072 let status_code = status.as_u16();
2073 let headers = response.headers().clone();
2074 let body_text = response.text().await
2075 .map_err(|e| ApiOpError::Transport(HttpError::Network(e)))?;
2076
2077 if status.is_success() {
2078 #success_branch
2079 } else {
2080 let typed: Option<#op_error_type>;
2081 let parse_error: Option<String>;
2082 #error_match_arms
2083 Err(ApiOpError::Api(ApiError {
2084 status: status_code,
2085 headers,
2086 body: body_text,
2087 typed,
2088 parse_error,
2089 }))
2090 }
2091 }
2092 }
2093
2094 fn generate_error_match_arms(&self, op: &OperationInfo) -> TokenStream {
2097 let arms: Vec<TokenStream> = op
2098 .response_schemas
2099 .iter()
2100 .filter(|(code, _)| !code.starts_with('2'))
2101 .filter_map(|(code, schema)| {
2102 let variant_ident = Self::op_error_variant_ident(code);
2103 let payload_ty_name = self.to_rust_type_name(schema);
2104 let payload_ty = syn::Ident::new(&payload_ty_name, proc_macro2::Span::call_site());
2105 let enum_ident = self.op_error_enum_ident(op);
2106
2107 let pattern = match code.as_str() {
2112 "default" | "Default" => return None, other if other.chars().all(|c| c.is_ascii_digit()) => {
2114 let n: u16 = other.parse().ok()?;
2115 quote! { #n }
2116 }
2117 "1XX" | "1xx" => quote! { code if (100..=199).contains(&code) },
2118 "2XX" | "2xx" => quote! { code if (200..=299).contains(&code) },
2119 "3XX" | "3xx" => quote! { code if (300..=399).contains(&code) },
2120 "4XX" | "4xx" => quote! { code if (400..=499).contains(&code) },
2121 "5XX" | "5xx" => quote! { code if (500..=599).contains(&code) },
2122 _ => return None,
2123 };
2124
2125 Some(quote! {
2126 #pattern => {
2127 match serde_json::from_str::<#payload_ty>(&body_text) {
2128 Ok(v) => {
2129 typed = Some(#enum_ident::#variant_ident(v));
2130 parse_error = None;
2131 }
2132 Err(e) => {
2133 typed = None;
2134 parse_error = Some(e.to_string());
2135 }
2136 }
2137 }
2138 })
2139 })
2140 .collect();
2141
2142 let has_typed_enum = op
2150 .response_schemas
2151 .iter()
2152 .any(|(code, _)| !code.starts_with('2'));
2153
2154 let default_arm = if has_typed_enum {
2155 quote! {
2156 _ => {
2157 typed = None;
2158 parse_error = None;
2159 }
2160 }
2161 } else {
2162 quote! {
2164 _ => {
2165 match serde_json::from_str::<serde_json::Value>(&body_text) {
2166 Ok(v) => {
2167 typed = Some(v);
2168 parse_error = None;
2169 }
2170 Err(e) => {
2171 typed = None;
2172 parse_error = Some(e.to_string());
2173 }
2174 }
2175 }
2176 }
2177 };
2178
2179 if arms.is_empty() {
2180 quote! {
2182 match status_code {
2183 #default_arm
2184 }
2185 }
2186 } else {
2187 quote! {
2188 match status_code {
2189 #(#arms)*
2190 #default_arm
2191 }
2192 }
2193 }
2194 }
2195
2196 fn generate_url_construction(&self, path: &str, op: &OperationInfo) -> TokenStream {
2198 if path.contains('{') {
2200 self.generate_url_with_params(path, op)
2201 } else {
2202 quote! {
2203 let request_url = format!("{}{}", self.base_url, #path);
2204 }
2205 }
2206 }
2207
2208 fn generate_url_with_params(&self, path: &str, op: &OperationInfo) -> TokenStream {
2210 let path_params: Vec<_> = op
2212 .parameters
2213 .iter()
2214 .filter(|p| p.location == "path")
2215 .collect();
2216
2217 let mut format_string = String::with_capacity(path.len());
2226 let mut format_args: Vec<TokenStream> = Vec::new();
2227 let mut chars = path.chars().peekable();
2228 while let Some(c) = chars.next() {
2229 if c != '{' {
2230 format_string.push(c);
2231 continue;
2232 }
2233 let mut name = String::new();
2235 while let Some(&n) = chars.peek() {
2236 chars.next();
2237 if n == '}' {
2238 break;
2239 }
2240 name.push(n);
2241 }
2242 let param = path_params.iter().find(|p| p.name == name);
2246 let Some(param) = param else {
2247 format_string.push('{');
2248 format_string.push_str(&name);
2249 format_string.push('}');
2250 continue;
2251 };
2252 format_string.push_str("{}");
2253 let param_name_snake = self.param_ident_str(param);
2254 let param_ident = Self::to_field_ident(¶m_name_snake);
2255 if Self::param_uses_as_ref_str(param) {
2256 format_args.push(quote! {
2257 __pct_encode_path_segment(#param_ident.as_ref())
2258 });
2259 } else {
2260 format_args.push(quote! {
2261 __pct_encode_path_segment(&#param_ident.to_string())
2262 });
2263 }
2264 }
2265
2266 if format_args.is_empty() {
2267 quote! {
2268 let request_url = format!("{}{}", self.base_url, #path);
2269 }
2270 } else {
2271 quote! {
2272 let request_url = format!("{}{}", self.base_url, format!(#format_string, #(#format_args),*));
2273 }
2274 }
2275 }
2276
2277 pub(crate) fn param_ident_str(&self, param: &crate::analysis::ParameterInfo) -> String {
2282 if let Some(ident) = ¶m.rust_ident {
2283 return self.escape_keyword_ident(ident);
2287 }
2288 self.sanitize_param_name(¶m.name)
2289 }
2290
2291 fn escape_keyword_ident(&self, snake_case: &str) -> String {
2292 if matches!(snake_case, "self" | "super" | "crate" | "Self") {
2293 return format!("{snake_case}_param");
2294 }
2295 if Self::is_rust_keyword(snake_case) {
2296 format!("r#{snake_case}")
2297 } else {
2298 snake_case.to_string()
2299 }
2300 }
2301
2302 fn sanitize_param_name(&self, name: &str) -> String {
2307 let suffix = if name.ends_with("<=") {
2312 "_lte"
2313 } else if name.ends_with(">=") {
2314 "_gte"
2315 } else if name.ends_with('<') {
2316 "_lt"
2317 } else if name.ends_with('>') {
2318 "_gt"
2319 } else {
2320 ""
2321 };
2322 let stripped = name.trim_end_matches(['<', '>', '=']);
2323 let mut snake_case = stripped.to_snake_case();
2324 snake_case.push_str(suffix);
2325
2326 if matches!(snake_case.as_str(), "self" | "super" | "crate" | "Self") {
2327 return format!("{snake_case}_param");
2328 }
2329 if Self::is_rust_keyword(&snake_case) {
2330 format!("r#{snake_case}")
2331 } else {
2332 snake_case
2333 }
2334 }
2335}