1use crate::{GeneratorError, Result, analysis::SchemaAnalysis, streaming::StreamingConfig};
2use proc_macro2::TokenStream;
3use quote::{format_ident, quote};
4use std::collections::BTreeMap;
5use std::path::PathBuf;
6
7fn parse_rust_type(rust_type: &str) -> Result<TokenStream> {
16 let parsed: syn::Type = syn::parse_str(rust_type).map_err(|e| {
17 GeneratorError::CodeGenError(format!(
18 "TypeMapper produced un-parseable type `{rust_type}`: {e}"
19 ))
20 })?;
21 Ok(quote! { #parsed })
22}
23
24fn format_constraints_doc(c: &crate::analysis::PropertyConstraints) -> String {
33 let mut parts: Vec<String> = Vec::new();
34
35 if let Some(v) = c.minimum {
36 parts.push(format!("minimum={}", strip_trailing_zero(v)));
37 }
38 if let Some(v) = c.maximum {
39 parts.push(format!("maximum={}", strip_trailing_zero(v)));
40 }
41 if let Some(v) = c.exclusive_minimum {
42 parts.push(format!("exclusiveMinimum={}", strip_trailing_zero(v)));
43 }
44 if let Some(v) = c.exclusive_maximum {
45 parts.push(format!("exclusiveMaximum={}", strip_trailing_zero(v)));
46 }
47 if let Some(v) = c.multiple_of {
48 parts.push(format!("multipleOf={}", strip_trailing_zero(v)));
49 }
50 if let Some(v) = c.min_length {
51 parts.push(format!("minLength={v}"));
52 }
53 if let Some(v) = c.max_length {
54 parts.push(format!("maxLength={v}"));
55 }
56 if let Some(v) = c.min_items {
57 parts.push(format!("minItems={v}"));
58 }
59 if let Some(v) = c.max_items {
60 parts.push(format!("maxItems={v}"));
61 }
62 if c.unique_items == Some(true) {
63 parts.push("uniqueItems=true".to_string());
64 }
65 if let Some(p) = &c.pattern {
66 let safe = p.replace("///", "/\u{200B}//").replace("*/", "*\u{200B}/");
71 parts.push(format!("pattern=`{safe}`"));
72 }
73
74 format!("Constraint: {}", parts.join(", "))
75}
76
77fn strip_trailing_zero(v: f64) -> String {
80 if v.fract() == 0.0 && v.is_finite() {
81 format!("{}", v as i64)
82 } else {
83 format!("{v}")
84 }
85}
86
87#[derive(Clone)]
89pub(crate) struct DiscriminatedVariantInfo {
90 pub(crate) discriminator_field: String,
92 pub(crate) discriminator_value: String,
94 pub(crate) is_parent_untagged: bool,
96}
97
98pub(crate) struct EmittedObjectProperty<'a> {
102 pub(crate) wire_name: &'a str,
103 pub(crate) property: &'a crate::analysis::PropertyInfo,
104 pub(crate) ident: syn::Ident,
105 pub(crate) is_required: bool,
106 pub(crate) field_type: TokenStream,
107}
108
109struct TypeGenerationIndex {
113 request_body_roots: std::collections::HashSet<String>,
114 reserved_type_names: std::collections::HashSet<String>,
115}
116
117struct TypeGenerationContext<'a> {
118 discriminated_variants: &'a BTreeMap<String, DiscriminatedVariantInfo>,
119 index: &'a TypeGenerationIndex,
120}
121
122#[derive(Debug, Clone)]
123pub struct GeneratorConfig {
124 pub spec_path: PathBuf,
126 pub output_dir: PathBuf,
128 pub module_name: String,
135 pub enable_sse_client: bool,
137 pub enable_async_client: bool,
139 pub enable_specta: bool,
141 pub type_mappings: BTreeMap<String, String>,
143 pub streaming_config: Option<StreamingConfig>,
145 pub nullable_field_overrides: BTreeMap<String, bool>,
148 pub extensible_enum_overrides: BTreeMap<String, bool>,
155 pub schema_extensions: Vec<PathBuf>,
158 pub http_client_config: Option<crate::http_config::HttpClientConfig>,
160 pub retry_config: Option<crate::http_config::RetryConfig>,
162 pub tracing_enabled: bool,
164 pub auth_config: Option<crate::http_config::AuthConfig>,
166 pub enable_registry: bool,
168 pub registry_only: bool,
170 pub types: crate::type_mapping::TypeMappingConfig,
174 pub builders: crate::config::BuildersSection,
176 pub server: Option<crate::config::ServerSection>,
179 pub client: Option<crate::config::ClientSection>,
182}
183
184impl Default for GeneratorConfig {
185 fn default() -> Self {
186 Self {
187 spec_path: "openapi.json".into(),
188 output_dir: "src/gen".into(),
189 module_name: "api_types".to_string(),
190 enable_sse_client: true,
191 enable_async_client: true,
192 enable_specta: false,
193 type_mappings: default_type_mappings(),
194 streaming_config: None,
195 nullable_field_overrides: BTreeMap::new(),
196 extensible_enum_overrides: BTreeMap::new(),
197 schema_extensions: Vec::new(),
198 http_client_config: None,
199 retry_config: None,
200 tracing_enabled: true,
201 auth_config: None,
202 enable_registry: false,
203 registry_only: false,
204 types: crate::type_mapping::TypeMappingConfig::default(),
205 builders: crate::config::BuildersSection::default(),
206 server: None,
207 client: None,
208 }
209 }
210}
211
212impl GeneratorConfig {
213 pub fn apply_spec_server_default(&mut self, spec: &serde_json::Value) {
224 let already_configured = self
225 .http_client_config
226 .as_ref()
227 .and_then(|http| http.base_url.as_deref())
228 .is_some_and(|url| !url.is_empty());
229 if already_configured {
230 return;
231 }
232
233 let Some(url) = spec
234 .pointer("/servers/0/url")
235 .and_then(serde_json::Value::as_str)
236 .map(str::trim)
237 .filter(|url| !url.is_empty())
238 .filter(|url| !url.starts_with('/'))
239 .filter(|url| !url.contains('{'))
240 else {
241 return;
242 };
243
244 match self.http_client_config.as_mut() {
245 Some(http) => http.base_url = Some(url.to_string()),
246 None => {
247 self.http_client_config = Some(crate::http_config::HttpClientConfig {
248 base_url: Some(url.to_string()),
249 timeout_seconds: None,
250 max_response_body_bytes: None,
251 default_headers: Default::default(),
252 })
253 }
254 }
255 }
256}
257
258pub fn default_type_mappings() -> BTreeMap<String, String> {
259 let mut mappings = BTreeMap::new();
260 mappings.insert("integer".to_string(), "i64".to_string());
261 mappings.insert("number".to_string(), "f64".to_string());
262 mappings.insert("string".to_string(), "String".to_string());
263 mappings.insert("boolean".to_string(), "bool".to_string());
264 mappings
265}
266
267pub(crate) fn rust_type_name(s: &str) -> String {
273 let mut result = String::new();
274 let mut next_upper = true;
275
276 for c in s.chars() {
277 match c {
278 'a'..='z' => {
279 result.push(if next_upper {
280 c.to_ascii_uppercase()
281 } else {
282 c
283 });
284 next_upper = false;
285 }
286 'A'..='Z' | '0'..='9' => {
287 result.push(c);
288 next_upper = false;
289 }
290 _ => next_upper = true,
291 }
292 }
293
294 if result.is_empty() {
295 result = "Type".to_string();
296 }
297 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
298 result = format!("Type{result}");
299 }
300 if matches!(
301 result.as_str(),
302 "Result"
303 | "Option"
304 | "Box"
305 | "Vec"
306 | "String"
307 | "Some"
308 | "None"
309 | "Ok"
310 | "Err"
311 | "Default"
312 | "Clone"
313 | "Debug"
314 | "Send"
315 | "Sync"
316 | "Sized"
317 | "Iterator"
318 | "From"
319 | "Into"
320 | "TryFrom"
321 | "TryInto"
322 | "AsRef"
323 | "AsMut"
324 ) {
325 result.push_str("Type");
326 }
327 result
328}
329
330#[derive(Debug, Clone)]
332pub struct GeneratedFile {
333 pub path: PathBuf,
335 pub content: String,
337}
338
339#[derive(Debug, Clone)]
341pub struct GenerationResult {
342 pub files: Vec<GeneratedFile>,
344 pub mod_file: GeneratedFile,
346 pub required_deps: Vec<crate::type_mapping::DepRequirement>,
350 pub pruned_schemas: usize,
352}
353
354#[derive(Debug)]
355struct OperationScopes {
356 client_ids: Option<std::collections::BTreeSet<String>>,
358 server_ids: std::collections::BTreeSet<String>,
359 streaming_ids: std::collections::BTreeSet<String>,
360 prune_models: bool,
361 extra_schema_roots: Vec<String>,
362}
363
364pub struct CodeGenerator {
365 config: GeneratorConfig,
366 source_provenance: Option<String>,
367}
368
369struct ObjectShape<'a> {
372 properties: &'a BTreeMap<String, crate::analysis::PropertyInfo>,
373 required: &'a std::collections::HashSet<String>,
374 additional_properties: &'a crate::analysis::ObjectAdditionalProperties,
375 variant: Option<&'a crate::analysis::SchemaRef>,
377}
378
379fn untyped_rust_type(shape: crate::analysis::UntypedShape) -> &'static str {
381 use crate::analysis::UntypedShape;
382 match shape {
383 UntypedShape::Value => "serde_json::Value",
384 UntypedShape::ValueArray => "Vec<serde_json::Value>",
385 UntypedShape::ValueMap => "std::collections::BTreeMap<String, serde_json::Value>",
386 }
387}
388
389fn untyped_tokens(shape: crate::analysis::UntypedShape) -> TokenStream {
390 use crate::analysis::UntypedShape;
391 match shape {
392 UntypedShape::Value => quote! { serde_json::Value },
393 UntypedShape::ValueArray => quote! { Vec<serde_json::Value> },
394 UntypedShape::ValueMap => quote! { std::collections::BTreeMap<String, serde_json::Value> },
395 }
396}
397
398impl CodeGenerator {
399 pub fn new(config: GeneratorConfig) -> Self {
400 Self {
401 config,
402 source_provenance: None,
403 }
404 }
405
406 pub fn with_source_provenance(mut self, source: impl Into<String>) -> Self {
408 self.source_provenance = Some(source.into());
409 self
410 }
411
412 pub fn config(&self) -> &GeneratorConfig {
414 &self.config
415 }
416
417 pub(crate) fn provenance_attribute(&self) -> TokenStream {
418 self.source_provenance
419 .as_ref()
420 .map(|source| {
421 let provenance = format!(
422 " Generated by openapi-to-rust v{}. Source OpenAPI document: {source}",
423 env!("CARGO_PKG_VERSION")
424 );
425 quote! { #![doc = #provenance] }
426 })
427 .unwrap_or_default()
428 }
429
430 pub fn generate_all(&self, analysis: &mut SchemaAnalysis) -> Result<GenerationResult> {
432 let scopes = self.resolve_operation_scopes(analysis)?;
435 let pruned_schemas = self.prune_models_to_scopes(analysis, &scopes);
436 let mut files = Vec::new();
437
438 if !self.config.registry_only {
439 let types_content = self.generate_types(analysis)?;
441 files.push(GeneratedFile {
442 path: "types.rs".into(),
443 content: types_content,
444 });
445
446 if self.config.enable_sse_client
448 && let Some(ref streaming_config) = self.config.streaming_config
449 {
450 if streaming_config.generate_client && !streaming_config.event_parser_helpers {
451 return Err(GeneratorError::ValidationError(
452 "streaming generate_client=true requires event_parser_helpers=true"
453 .to_string(),
454 ));
455 }
456 if streaming_config.event_parser_helpers {
457 files.push(GeneratedFile {
458 path: "sse.rs".into(),
459 content: self.generate_sse_runtime()?,
460 });
461 }
462 let streaming_content =
463 self.generate_streaming_client(streaming_config, analysis)?;
464 files.push(GeneratedFile {
465 path: "streaming.rs".into(),
466 content: streaming_content,
467 });
468 }
469
470 if self.config.enable_async_client {
472 let operations = self.client_operations(analysis, scopes.client_ids.as_ref());
473 let http_content =
474 self.generate_http_client_for_operations(analysis, &operations)?;
475 files.push(GeneratedFile {
476 path: "client.rs".into(),
477 content: http_content,
478 });
479 }
480 }
481
482 if self.config.enable_registry || self.config.registry_only {
484 let registry_content = self.generate_registry(analysis)?;
485 files.push(GeneratedFile {
486 path: "registry.rs".into(),
487 content: registry_content,
488 });
489 }
490
491 if !self.config.registry_only
495 && let Some(server) = self
496 .config
497 .server
498 .as_ref()
499 .filter(|server| !server.operations.is_empty())
500 {
501 let server_files =
502 crate::server::codegen::ServerCodegen::new(&self.config, analysis, server)
503 .with_source_provenance(self.source_provenance.as_deref())
504 .generate()
505 .map_err(|error| {
506 GeneratorError::CodeGenError(format!(
507 "server code generation failed: {error}"
508 ))
509 })?;
510 files.extend(server_files);
511 }
512
513 let mod_content = self.generate_mod_file(&files)?;
515 let mod_file = GeneratedFile {
516 path: "mod.rs".into(),
517 content: mod_content,
518 };
519
520 let required_deps = crate::type_mapping::collect_generated_dep_requirements(
521 files.iter().map(|file| file.content.as_str()),
522 self.config.enable_specta,
523 );
524
525 Ok(GenerationResult {
526 files,
527 mod_file,
528 required_deps,
529 pruned_schemas,
530 })
531 }
532
533 pub fn generate(&self, analysis: &mut SchemaAnalysis) -> Result<String> {
535 self.generate_types(analysis)
536 }
537
538 fn generate_types(&self, analysis: &mut SchemaAnalysis) -> Result<String> {
540 self.validate_schema_type_names(analysis)?;
541
542 let provenance_attribute = self.provenance_attribute();
543 let mut type_definitions = TokenStream::new();
544
545 let mut discriminated_variant_info: BTreeMap<String, DiscriminatedVariantInfo> =
548 BTreeMap::new();
549
550 let mut sorted_schemas: Vec<_> = analysis.schemas.iter().collect();
552 sorted_schemas.sort_by_key(|(name, _)| name.as_str());
553
554 for (_parent_name, schema) in sorted_schemas {
555 if let crate::analysis::SchemaType::DiscriminatedUnion {
556 variants,
557 discriminator_field,
558 } = &schema.schema_type
559 {
560 let is_parent_untagged =
562 self.should_use_untagged_discriminated_union(schema, analysis);
563
564 for variant in variants {
565 if let Some(variant_schema) = analysis.schemas.get(&variant.type_name) {
568 if let crate::analysis::SchemaType::Object { properties, .. } =
569 &variant_schema.schema_type
570 {
571 if properties.contains_key(discriminator_field) {
572 discriminated_variant_info.insert(
573 variant.type_name.clone(),
574 DiscriminatedVariantInfo {
575 discriminator_field: discriminator_field.clone(),
576 discriminator_value: variant.discriminator_value.clone(),
577 is_parent_untagged,
578 },
579 );
580 }
581 }
582 }
583 }
584 }
585 }
586
587 let type_index = self.type_generation_index(analysis);
588 let type_context = TypeGenerationContext {
589 discriminated_variants: &discriminated_variant_info,
590 index: &type_index,
591 };
592
593 let generation_order = analysis.dependencies.topological_sort()?;
595
596 let mut processed = std::collections::HashSet::new();
597
598 for schema_name in generation_order {
600 if let Some(schema) = analysis.schemas.get(&schema_name) {
601 let type_def = self.generate_type_definition(schema, analysis, &type_context)?;
602 if !type_def.is_empty() {
603 type_definitions.extend(type_def);
604 }
605 processed.insert(schema_name);
606 }
607 }
608
609 let mut remaining_schemas: Vec<_> = analysis
611 .schemas
612 .iter()
613 .filter(|(name, _)| !processed.contains(*name))
614 .collect();
615 remaining_schemas.sort_by_key(|(name, _)| name.as_str());
616
617 for (_schema_name, schema) in remaining_schemas {
618 let type_def = self.generate_type_definition(schema, analysis, &type_context)?;
619 if !type_def.is_empty() {
620 type_definitions.extend(type_def);
621 }
622 }
623
624 let base64_helper = if analysis
629 .used_type_features
630 .contains(crate::type_mapping::TypeFeature::Base64)
631 {
632 let engine = match self.config.types.byte {
633 crate::type_mapping::ByteStrategy::Base64UrlUnpadded => {
634 quote::format_ident!("URL_SAFE_NO_PAD")
635 }
636 _ => quote::format_ident!("STANDARD"),
637 };
638 quote! {
639 mod base64_serde {
644 use base64::{Engine as _, engine::general_purpose::#engine as ENGINE};
645 use serde::{Deserialize, Deserializer, Serializer};
646
647 pub fn serialize<S: Serializer>(
648 bytes: &Vec<u8>,
649 ser: S,
650 ) -> Result<S::Ok, S::Error> {
651 ser.serialize_str(&ENGINE.encode(bytes))
652 }
653
654 pub fn deserialize<'de, D: Deserializer<'de>>(
655 de: D,
656 ) -> Result<Vec<u8>, D::Error> {
657 let s = String::deserialize(de)?;
658 ENGINE
659 .decode(s.as_bytes())
660 .map_err(serde::de::Error::custom)
661 }
662
663 pub mod option {
669 use super::*;
670 use serde::{Deserialize, Deserializer, Serializer};
671
672 pub fn serialize<S: Serializer>(
673 opt: &Option<Vec<u8>>,
674 ser: S,
675 ) -> Result<S::Ok, S::Error> {
676 match opt {
677 Some(bytes) => super::serialize(bytes, ser),
678 None => ser.serialize_none(),
679 }
680 }
681
682 pub fn deserialize<'de, D: Deserializer<'de>>(
683 de: D,
684 ) -> Result<Option<Vec<u8>>, D::Error> {
685 let opt = Option::<String>::deserialize(de)?;
686 opt.map(|s| {
687 ENGINE
688 .decode(s.as_bytes())
689 .map_err(serde::de::Error::custom)
690 })
691 .transpose()
692 }
693 }
694 }
695 }
696 } else {
697 TokenStream::new()
698 };
699
700 let time_date_helper = if analysis
707 .used_type_features
708 .contains(crate::type_mapping::TypeFeature::TimeDate)
709 {
710 quote! {
711 time::serde::format_description!(
712 time_date_format,
713 Date,
714 "[year]-[month]-[day]"
715 );
716 }
717 } else {
718 TokenStream::new()
719 };
720
721 let time_time_helper = if analysis
726 .used_type_features
727 .contains(crate::type_mapping::TypeFeature::TimeTime)
728 {
729 quote! {
730 time::serde::format_description!(
731 version = 2,
732 time_time_format,
733 Time,
734 "[hour]:[minute]:[second][optional [.[subsecond]]]"
735 );
736 }
737 } else {
738 TokenStream::new()
739 };
740
741 let generated = quote! {
743 #provenance_attribute
749
750 #![allow(clippy::large_enum_variant)]
751 #![allow(clippy::format_in_format_args)]
752 #![allow(clippy::let_unit_value)]
753 #![allow(unreachable_patterns)]
754
755 use serde::{Deserialize, Serialize};
756
757 #base64_helper
758
759 #time_date_helper
760
761 #time_time_helper
762
763 #type_definitions
764 };
765
766 let syntax_tree = syn::parse2::<syn::File>(generated).map_err(|e| {
768 GeneratorError::CodeGenError(format!("Failed to parse generated code: {e}"))
769 })?;
770
771 let formatted = prettyplease::unparse(&syntax_tree);
772
773 Ok(formatted)
774 }
775
776 fn generate_streaming_client(
778 &self,
779 streaming_config: &StreamingConfig,
780 analysis: &SchemaAnalysis,
781 ) -> Result<String> {
782 let mut client_code = TokenStream::new();
783 let provenance_attribute = self.provenance_attribute();
784 let duration_import = streaming_config
785 .reconnection_config
786 .as_ref()
787 .map(|_| quote! { use std::time::Duration; });
788
789 let imports = quote! {
791 #provenance_attribute
796 #![allow(clippy::format_in_format_args)]
797 #![allow(clippy::let_unit_value)]
798 #![allow(unused_mut)]
799
800 use super::types::*;
801 use async_trait::async_trait;
802 use futures_util::Stream;
803 use std::pin::Pin;
804 use reqwest::header::{HeaderMap, HeaderValue};
805 use tracing::{debug, info, instrument};
806 #duration_import
807 };
808 client_code.extend(imports);
809
810 if streaming_config.generate_client {
811 if streaming_config.reconnection_config.is_some() {
812 client_code.extend(quote! {
813 use super::sse::{SseClient, SseReconnectOptions};
814 pub use super::sse::StreamingError;
815 });
816 } else {
817 client_code.extend(quote! {
818 use super::sse::SseClient;
819 pub use super::sse::StreamingError;
820 });
821 }
822 }
823
824 for endpoint in &streaming_config.endpoints {
826 let trait_code = self.generate_endpoint_trait(endpoint, analysis)?;
827 client_code.extend(trait_code);
828 }
829
830 if streaming_config.generate_client {
832 let client_impl = self.generate_streaming_client_impl(streaming_config, analysis)?;
833 client_code.extend(client_impl);
834 }
835
836 if let Some(reconnect_config) = &streaming_config.reconnection_config {
838 let reconnect_code = self.generate_reconnection_utilities(reconnect_config)?;
839 client_code.extend(reconnect_code);
840 }
841
842 let syntax_tree = syn::parse2::<syn::File>(client_code).map_err(|e| {
843 GeneratorError::CodeGenError(format!("Failed to parse streaming client code: {e}"))
844 })?;
845
846 Ok(prettyplease::unparse(&syntax_tree))
847 }
848
849 fn validate_schema_type_names(&self, analysis: &SchemaAnalysis) -> Result<()> {
850 let mut source_by_rust_name = BTreeMap::<String, String>::new();
851
852 for schema in analysis.schemas.values() {
853 let rust_name = self.to_rust_type_name(&schema.name);
854 if let Some(first) = source_by_rust_name.insert(rust_name.clone(), schema.name.clone())
855 {
856 return Err(GeneratorError::InvalidSchema(format!(
857 "schema names `{first}` and `{}` both map to Rust type `{rust_name}`",
858 schema.name
859 )));
860 }
861 }
862
863 Ok(())
864 }
865
866 pub fn generate_http_client(&self, analysis: &SchemaAnalysis) -> Result<String> {
872 let client_ids = self.resolve_client_operation_ids(analysis)?;
873 let operations = self.client_operations(analysis, client_ids.as_ref());
874 self.generate_http_client_for_operations(analysis, &operations)
875 }
876
877 fn generate_http_client_for_operations(
878 &self,
879 analysis: &SchemaAnalysis,
880 operations: &[&crate::analysis::OperationInfo],
881 ) -> Result<String> {
882 let provenance_attribute = self.provenance_attribute();
883 let error_types = self.generate_http_error_types();
884 let client_struct = self.generate_http_client_struct();
885 let operation_methods = self.generate_operation_methods_for(analysis, operations);
886
887 let generated = quote! {
888 #provenance_attribute
893 #![allow(clippy::format_in_format_args)]
894 #![allow(clippy::let_unit_value)]
895
896 use super::types::*;
897
898 #error_types
899
900 #client_struct
901
902 #operation_methods
903 };
904
905 let syntax_tree = syn::parse2::<syn::File>(generated.clone()).map_err(|e| {
906 if let Ok(dump) = std::env::var("OATR_DUMP_TOKENS_ON_PARSE_ERROR") {
907 let _ = std::fs::write(&dump, generated.to_string());
908 }
909 GeneratorError::CodeGenError(format!("Failed to parse HTTP client code: {e}"))
910 })?;
911
912 Ok(prettyplease::unparse(&syntax_tree))
913 }
914
915 fn resolve_operation_scopes(&self, analysis: &SchemaAnalysis) -> Result<OperationScopes> {
916 let client_ids = if self.config.enable_async_client && !self.config.registry_only {
917 self.resolve_client_operation_ids(analysis)?
918 } else {
919 None
920 };
921
922 let server_ids = match &self.config.server {
923 Some(server) if !server.operations.is_empty() => {
924 crate::server::resolve_operation_selectors(&server.operations, analysis)
925 .map_err(|error| {
926 GeneratorError::ValidationError(format!(
927 "Invalid [server].operations: {error}"
928 ))
929 })?
930 .operations
931 .into_iter()
932 .map(|operation| operation.operation_id)
933 .collect()
934 }
935 _ => Default::default(),
936 };
937
938 let streaming_ids = if self.config.registry_only || !self.config.enable_sse_client {
939 Default::default()
940 } else if let Some(streaming) = &self.config.streaming_config {
941 let mut ids = std::collections::BTreeSet::new();
942 for (index, endpoint) in streaming.endpoints.iter().enumerate() {
943 let resolution =
944 crate::server::resolve_operation_id(&endpoint.operation_id, analysis).map_err(
945 |error| {
946 GeneratorError::ValidationError(format!(
947 "Invalid [streaming].endpoints[{index}].operation_id: {error}"
948 ))
949 },
950 )?;
951 ids.extend(
952 resolution
953 .operations
954 .into_iter()
955 .map(|operation| operation.operation_id),
956 );
957 }
958 ids
959 } else {
960 Default::default()
961 };
962
963 let client_prunes = self.config.enable_async_client
964 && !self.config.registry_only
965 && self
966 .config
967 .client
968 .as_ref()
969 .is_some_and(|client| client.prune_models);
970 let server_prunes = self
971 .config
972 .server
973 .as_ref()
974 .is_some_and(|server| server.prune_models && !server.operations.is_empty());
975 let extra_schema_roots = if self.config.registry_only || !self.config.enable_sse_client {
976 Vec::new()
977 } else {
978 self.config
979 .streaming_config
980 .as_ref()
981 .map(|streaming| {
982 streaming
983 .endpoints
984 .iter()
985 .map(|endpoint| endpoint.event_union_type.clone())
986 .collect()
987 })
988 .unwrap_or_default()
989 };
990
991 Ok(OperationScopes {
992 client_ids,
993 server_ids,
994 streaming_ids,
995 prune_models: client_prunes || server_prunes,
996 extra_schema_roots,
997 })
998 }
999
1000 fn resolve_client_operation_ids(
1001 &self,
1002 analysis: &SchemaAnalysis,
1003 ) -> Result<Option<std::collections::BTreeSet<String>>> {
1004 match &self.config.client {
1005 Some(client) if !client.operations.is_empty() => {
1006 let resolution =
1007 crate::server::resolve_operation_selectors(&client.operations, analysis)
1008 .map_err(|error| {
1009 GeneratorError::ValidationError(format!(
1010 "Invalid [client].operations: {error}"
1011 ))
1012 })?;
1013 Ok(Some(
1014 resolution
1015 .operations
1016 .into_iter()
1017 .map(|operation| operation.operation_id)
1018 .collect(),
1019 ))
1020 }
1021 _ => Ok(None),
1022 }
1023 }
1024
1025 fn client_operations<'a>(
1026 &self,
1027 analysis: &'a SchemaAnalysis,
1028 selected: Option<&std::collections::BTreeSet<String>>,
1029 ) -> Vec<&'a crate::analysis::OperationInfo> {
1030 analysis
1031 .operations
1032 .iter()
1033 .filter(|(operation_id, _)| selected.is_none_or(|ids| ids.contains(*operation_id)))
1034 .map(|(_, operation)| operation)
1035 .collect()
1036 }
1037
1038 fn prune_models_to_scopes(
1039 &self,
1040 analysis: &mut SchemaAnalysis,
1041 scopes: &OperationScopes,
1042 ) -> usize {
1043 if !scopes.prune_models {
1044 return 0;
1045 }
1046
1047 let mut consumer_ids = scopes.server_ids.clone();
1048 if self.config.enable_async_client && !self.config.registry_only {
1049 match &scopes.client_ids {
1050 Some(ids) => consumer_ids.extend(ids.iter().cloned()),
1051 None => consumer_ids.extend(analysis.operations.keys().cloned()),
1052 }
1053 }
1054 consumer_ids.extend(scopes.streaming_ids.iter().cloned());
1055
1056 let operations: Vec<&crate::analysis::OperationInfo> = consumer_ids
1057 .iter()
1058 .filter_map(|operation_id| analysis.operations.get(operation_id))
1059 .collect();
1060 let keep = crate::server::codegen::reachable_schemas_with_roots(
1061 analysis,
1062 &operations,
1063 &scopes.extra_schema_roots,
1064 );
1065 let before = analysis.schemas.len();
1066 analysis.schemas.retain(|name, _| keep.contains(name));
1067 before - analysis.schemas.len()
1068 }
1069
1070 fn generate_http_error_types(&self) -> TokenStream {
1072 quote! {
1073 use thiserror::Error;
1074
1075 pub mod openapi_to_rust_problem {
1078 #[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
1079 pub struct ProblemDetails {
1080 #[serde(rename = "type")]
1081 pub type_uri: String,
1082 pub title: String,
1083 pub status: u16,
1084 pub code: String,
1085 #[serde(default)]
1086 pub errors: Vec<InvalidParameter>,
1087 #[serde(default, skip_serializing_if = "Option::is_none")]
1088 pub detail: Option<String>,
1089 #[serde(default, skip_serializing_if = "Option::is_none")]
1090 pub instance: Option<String>,
1091 }
1092
1093 #[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
1094 pub struct InvalidParameter {
1095 pub code: String,
1096 pub location: String,
1097 pub message: String,
1098 }
1099 }
1100
1101 #[derive(Error, Debug)]
1109 pub enum HttpError {
1110 #[error("Network error: {0}")]
1112 Network(#[from] reqwest::Error),
1113
1114 #[error("Middleware error: {0}")]
1116 Middleware(#[from] reqwest_middleware::Error),
1117
1118 #[error("Failed to serialize request: {0}")]
1120 Serialization(String),
1121
1122 #[error("Authentication error: {0}")]
1124 Auth(String),
1125
1126 #[error("Request timeout")]
1128 Timeout,
1129
1130 #[error("Response body exceeded configured limit of {limit} bytes")]
1132 ResponseTooLarge { limit: usize },
1133
1134 #[error("Configuration error: {0}")]
1136 Config(String),
1137
1138 #[error("{0}")]
1140 Other(String),
1141 }
1142
1143 impl HttpError {
1144 pub fn serialization_error(error: impl std::fmt::Display) -> Self {
1146 Self::Serialization(error.to_string())
1147 }
1148
1149 pub fn is_retryable(&self) -> bool {
1151 matches!(self, Self::Network(_) | Self::Middleware(_) | Self::Timeout)
1152 }
1153 }
1154
1155 #[derive(Debug, Clone)]
1167 pub struct ApiError<E> {
1168 pub status: u16,
1169 pub headers: reqwest::header::HeaderMap,
1170 pub body: String,
1171 pub raw_body: Vec<u8>,
1173 pub typed: Option<E>,
1174 pub parse_error: Option<String>,
1175 }
1176
1177 const API_ERROR_BODY_DISPLAY_LIMIT: usize = 500;
1178 const API_ERROR_BODY_TRUNCATION_MARKER: &str = "... [truncated]";
1179
1180 fn display_api_error_body(body: &str) -> std::borrow::Cow<'_, str> {
1181 let Some((end, _)) = body.char_indices().nth(API_ERROR_BODY_DISPLAY_LIMIT) else {
1182 return std::borrow::Cow::Borrowed(body);
1183 };
1184
1185 let mut displayed =
1186 String::with_capacity(end + API_ERROR_BODY_TRUNCATION_MARKER.len());
1187 displayed.push_str(&body[..end]);
1188 displayed.push_str(API_ERROR_BODY_TRUNCATION_MARKER);
1189 std::borrow::Cow::Owned(displayed)
1190 }
1191
1192 impl<E> ApiError<E> {
1193 pub fn is_client_error(&self) -> bool {
1194 (400..500).contains(&self.status)
1195 }
1196
1197 pub fn is_server_error(&self) -> bool {
1198 (500..600).contains(&self.status)
1199 }
1200
1201 pub fn is_retryable(&self) -> bool {
1204 matches!(self.status, 429 | 500 | 502 | 503 | 504)
1205 }
1206
1207 pub fn problem_details(
1219 &self,
1220 ) -> Option<openapi_to_rust_problem::ProblemDetails> {
1221 let content_type = self
1222 .headers
1223 .get(reqwest::header::CONTENT_TYPE)?
1224 .to_str()
1225 .ok()?;
1226 let media_type = content_type.split(';').next()?.trim();
1227 if !media_type.eq_ignore_ascii_case("application/problem+json") {
1228 return None;
1229 }
1230 serde_json::from_str(&self.body).ok()
1231 }
1232 }
1233
1234 impl<E: std::fmt::Debug> std::fmt::Display for ApiError<E> {
1235 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1236 write!(
1237 f,
1238 "API error {}: {}",
1239 self.status,
1240 display_api_error_body(&self.body)
1241 )?;
1242
1243 if let Some(typed) = &self.typed {
1244 write!(f, "; typed: {typed:?}")?;
1245 }
1246
1247 if let Some(parse_error) = &self.parse_error {
1248 write!(f, "; parse error: {parse_error}")?;
1249 }
1250
1251 Ok(())
1252 }
1253 }
1254
1255 impl<E: std::fmt::Debug> std::error::Error for ApiError<E> {}
1256
1257 #[derive(Debug, Error)]
1265 pub enum ApiOpError<E: std::fmt::Debug> {
1266 #[error(transparent)]
1267 Transport(#[from] HttpError),
1268
1269 #[error(transparent)]
1270 Api(ApiError<E>),
1271 }
1272
1273 impl<E: std::fmt::Debug> ApiOpError<E> {
1274 pub fn api(&self) -> Option<&ApiError<E>> {
1276 match self {
1277 Self::Api(e) => Some(e),
1278 Self::Transport(_) => None,
1279 }
1280 }
1281
1282 pub fn is_api_error(&self) -> bool {
1285 matches!(self, Self::Api(_))
1286 }
1287 }
1288
1289 impl<E: std::fmt::Debug> From<reqwest::Error> for ApiOpError<E> {
1292 fn from(e: reqwest::Error) -> Self {
1293 Self::Transport(HttpError::Network(e))
1294 }
1295 }
1296
1297 impl<E: std::fmt::Debug> From<reqwest_middleware::Error> for ApiOpError<E> {
1298 fn from(e: reqwest_middleware::Error) -> Self {
1299 Self::Transport(HttpError::Middleware(e))
1300 }
1301 }
1302
1303 pub type HttpResult<T> = Result<T, HttpError>;
1307 }
1308 }
1309
1310 fn generate_mod_file(&self, files: &[GeneratedFile]) -> Result<String> {
1312 let mut module_names = std::collections::BTreeSet::new();
1313
1314 for file in files {
1315 let module_name = if file.path.components().count() > 1 {
1316 file.path.iter().next().and_then(|part| part.to_str())
1317 } else {
1318 file.path.file_stem().and_then(|stem| stem.to_str())
1319 };
1320 if let Some(module_name) = module_name.filter(|name| *name != "mod") {
1321 module_names.insert(module_name.to_string());
1322 }
1323 }
1324 let module_declarations = module_names
1325 .iter()
1326 .map(|name| format!("pub mod {name};"))
1327 .collect::<Vec<_>>();
1328 let pub_uses = module_names
1329 .iter()
1330 .filter(|name| name.as_str() != "sse")
1333 .map(|name| format!("pub use {name}::*;"))
1334 .collect::<Vec<_>>();
1335
1336 let mount_hint = format!(
1343 "//! Configured `module_name` = `{name}`. Mount this tree under your\n\
1344 //! preferred path, e.g. `pub mod {name};` in your crate root.\n",
1345 name = self.config.module_name,
1346 );
1347 let source_hint = self
1348 .source_provenance
1349 .as_ref()
1350 .map(|source| {
1351 format!(
1352 "//! Generated by openapi-to-rust v{}. Source OpenAPI document: {source}\n",
1353 env!("CARGO_PKG_VERSION")
1354 )
1355 })
1356 .unwrap_or_default();
1357
1358 let content = format!(
1359 r#"//! Generated API modules
1360//!
1361//! This module exports all generated API types and clients.
1362//! Do not edit manually - regenerate using the appropriate script.
1363//!
1364{source_hint}
1365{mount_hint}
1366#![allow(unused_imports)]
1367
1368{decls}
1369
1370{uses}
1371"#,
1372 mount_hint = mount_hint,
1373 source_hint = source_hint,
1374 decls = module_declarations.join("\n"),
1375 uses = pub_uses.join("\n"),
1376 );
1377
1378 Ok(content)
1379 }
1380
1381 pub fn output_artifacts(
1383 &self,
1384 result: &GenerationResult,
1385 ) -> std::collections::BTreeMap<PathBuf, String> {
1386 let mut artifacts = std::collections::BTreeMap::new();
1387 for file in &result.files {
1388 artifacts.insert(file.path.clone(), file.content.clone());
1389 }
1390 artifacts.insert(
1391 result.mod_file.path.clone(),
1392 result.mod_file.content.clone(),
1393 );
1394 if let Some(mut fragment) =
1395 crate::type_mapping::render_required_deps_toml(&result.required_deps)
1396 {
1397 if let Some(source) = &self.source_provenance {
1398 let header = format!(
1399 "# Generated by openapi-to-rust v{}. Source OpenAPI document: {source}",
1400 env!("CARGO_PKG_VERSION")
1401 );
1402 fragment = fragment.replacen("# Generated by openapi-to-rust.", &header, 1);
1403 }
1404 artifacts.insert(PathBuf::from("REQUIRED_DEPS.toml"), fragment);
1405 }
1406 artifacts
1407 }
1408
1409 pub fn write_files(&self, result: &GenerationResult) -> Result<()> {
1412 use std::fs;
1413
1414 fs::create_dir_all(&self.config.output_dir)?;
1416
1417 let artifacts = self.output_artifacts(result);
1418 for (relative, content) in &artifacts {
1419 let file_path = self.config.output_dir.join(relative);
1420 if let Some(parent) = file_path.parent() {
1421 fs::create_dir_all(parent)?;
1422 }
1423 fs::write(&file_path, content)?;
1424 }
1425
1426 let deps_path = self.config.output_dir.join("REQUIRED_DEPS.toml");
1427 if !artifacts.contains_key(std::path::Path::new("REQUIRED_DEPS.toml")) && deps_path.exists()
1428 {
1429 fs::remove_file(&deps_path)?;
1430 }
1431
1432 Ok(())
1433 }
1434
1435 fn generate_type_definition(
1436 &self,
1437 schema: &crate::analysis::AnalyzedSchema,
1438 analysis: &crate::analysis::SchemaAnalysis,
1439 type_context: &TypeGenerationContext<'_>,
1440 ) -> Result<TokenStream> {
1441 use crate::analysis::SchemaType;
1442
1443 match &schema.schema_type {
1444 SchemaType::Primitive { rust_type, .. } => {
1445 self.generate_type_alias(schema, rust_type)
1447 }
1448 SchemaType::StringEnum { values } => {
1449 let ext = analysis.enum_extensions.get(&schema.name);
1450 let rust_name = self.to_rust_type_name(&schema.name);
1457 let force_extensible = self
1458 .config
1459 .extensible_enum_overrides
1460 .get(&schema.name)
1461 .or_else(|| self.config.extensible_enum_overrides.get(&rust_name))
1462 .copied()
1463 .unwrap_or(false);
1464 if force_extensible {
1465 self.generate_extensible_enum(schema, values, ext)
1466 } else {
1467 self.generate_string_enum(schema, values, ext)
1468 }
1469 }
1470 SchemaType::ExtensibleEnum { known_values } => {
1471 let ext = analysis.enum_extensions.get(&schema.name);
1472 self.generate_extensible_enum(schema, known_values, ext)
1473 }
1474 SchemaType::Object {
1475 properties,
1476 required,
1477 additional_properties,
1478 variant,
1479 } => self.generate_struct(
1480 schema,
1481 ObjectShape {
1482 properties,
1483 required,
1484 additional_properties,
1485 variant: variant.as_ref(),
1486 },
1487 analysis,
1488 type_context,
1489 ),
1490 SchemaType::DiscriminatedUnion {
1491 discriminator_field,
1492 variants,
1493 } => {
1494 if self.should_use_untagged_discriminated_union(schema, analysis) {
1496 let schema_refs: Vec<crate::analysis::SchemaRef> = variants
1498 .iter()
1499 .map(|v| crate::analysis::SchemaRef {
1500 target: v.type_name.clone(),
1501 nullable: false,
1502 })
1503 .collect();
1504 self.generate_union_enum(schema, &schema_refs, analysis)
1505 } else {
1506 self.generate_discriminated_enum(
1507 schema,
1508 discriminator_field,
1509 variants,
1510 analysis,
1511 )
1512 }
1513 }
1514 SchemaType::Union { variants } => self.generate_union_enum(schema, variants, analysis),
1515 SchemaType::Reference { target } => {
1516 if schema.name != *target {
1519 let alias_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1521 let target_type = format_ident!("{}", self.to_rust_type_name(target));
1522
1523 let doc_comment = if let Some(desc) = &schema.description {
1524 quote! { #[doc = #desc] }
1525 } else {
1526 TokenStream::new()
1527 };
1528
1529 Ok(quote! {
1530 #doc_comment
1531 pub type #alias_name = #target_type;
1532 })
1533 } else {
1534 Ok(TokenStream::new())
1536 }
1537 }
1538 SchemaType::Untyped { shape, .. } => {
1539 self.generate_type_alias(schema, untyped_rust_type(*shape))
1540 }
1541 SchemaType::Tuple { element_types } => {
1542 let tuple_type = self.generate_tuple_type(element_types, analysis);
1543 let type_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1544 let doc_comment = if let Some(description) = &schema.description {
1545 let sanitized = self.sanitize_doc_comment(description);
1546 quote! { #[doc = #sanitized] }
1547 } else {
1548 TokenStream::new()
1549 };
1550 Ok(quote! {
1551 #doc_comment
1552 pub type #type_name = #tuple_type;
1553 })
1554 }
1555 SchemaType::Array { item_type } => {
1556 let array_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1564
1565 if let SchemaType::Reference { target } = item_type.as_ref() {
1567 if let Some(info) = type_context.discriminated_variants.get(target) {
1568 if !info.is_parent_untagged {
1569 let wrapper_name =
1571 format_ident!("{}Item", self.to_rust_type_name(&schema.name));
1572 let variant_type = format_ident!("{}", self.to_rust_type_name(target));
1573 let disc_field = &info.discriminator_field;
1574 let disc_value = &info.discriminator_value;
1575
1576 let doc_comment = if let Some(desc) = &schema.description {
1577 quote! { #[doc = #desc] }
1578 } else {
1579 TokenStream::new()
1580 };
1581
1582 return Ok(quote! {
1583 #[derive(Debug, Clone, Deserialize, Serialize)]
1587 #[serde(tag = #disc_field)]
1588 pub enum #wrapper_name {
1589 #[serde(rename = #disc_value)]
1590 #variant_type(#variant_type),
1591 }
1592 #doc_comment
1593 pub type #array_name = Vec<#wrapper_name>;
1594 });
1595 }
1596 }
1597 }
1598
1599 let inner_type = self.generate_array_item_type(item_type, analysis);
1600
1601 let doc_comment = if let Some(desc) = &schema.description {
1602 quote! { #[doc = #desc] }
1603 } else {
1604 TokenStream::new()
1605 };
1606
1607 Ok(quote! {
1608 #doc_comment
1609 pub type #array_name = Vec<#inner_type>;
1610 })
1611 }
1612 SchemaType::Composition { schemas } => {
1613 self.generate_composition_struct(schema, schemas)
1614 }
1615 }
1616 }
1617
1618 fn generate_type_alias(
1619 &self,
1620 schema: &crate::analysis::AnalyzedSchema,
1621 rust_type: &str,
1622 ) -> Result<TokenStream> {
1623 let type_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1624 let base_type = parse_rust_type(rust_type)?;
1628
1629 let doc_comment = if let Some(desc) = &schema.description {
1630 let sanitized_desc = self.sanitize_doc_comment(desc);
1631 quote! { #[doc = #sanitized_desc] }
1632 } else {
1633 TokenStream::new()
1634 };
1635
1636 Ok(quote! {
1637 #doc_comment
1638 pub type #type_name = #base_type;
1639 })
1640 }
1641
1642 fn generate_extensible_enum(
1643 &self,
1644 schema: &crate::analysis::AnalyzedSchema,
1645 known_values: &[String],
1646 ext: Option<&crate::analysis::EnumExtensions>,
1647 ) -> Result<TokenStream> {
1648 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1649
1650 let doc_comment = if let Some(desc) = &schema.description {
1651 quote! { #[doc = #desc] }
1652 } else {
1653 TokenStream::new()
1654 };
1655
1656 let varnames_override: Option<&Vec<String>> = ext
1660 .filter(|_| self.config.types.x_enum_varnames_enabled())
1661 .map(|e| &e.varnames)
1662 .filter(|v| !v.is_empty() && v.len() == known_values.len());
1663 let descriptions_override: Option<&Vec<String>> = ext
1664 .filter(|_| self.config.types.x_enum_descriptions_enabled())
1665 .map(|e| &e.descriptions)
1666 .filter(|v| !v.is_empty() && v.len() == known_values.len());
1667
1668 let variant_ident_for = |index: usize, value: &str| -> proc_macro2::Ident {
1669 let name = match varnames_override {
1670 Some(v) => v[index].clone(),
1671 None => self.to_rust_enum_variant(value),
1672 };
1673 format_ident!("{}", name)
1674 };
1675
1676 let known_variants = known_values.iter().enumerate().map(|(i, value)| {
1681 let variant_ident = variant_ident_for(i, value);
1682 let doc = descriptions_override
1683 .map(|d| {
1684 let s = self.sanitize_doc_comment(&d[i]);
1685 quote! { #[doc = #s] }
1686 })
1687 .unwrap_or_default();
1688 quote! {
1689 #doc
1690 #variant_ident,
1691 }
1692 });
1693
1694 let match_arms_de = known_values.iter().enumerate().map(|(i, value)| {
1695 let variant_ident = variant_ident_for(i, value);
1696 quote! {
1697 #value => Ok(#enum_name::#variant_ident),
1698 }
1699 });
1700
1701 let match_arms_ser = known_values.iter().enumerate().map(|(i, value)| {
1702 let variant_ident = variant_ident_for(i, value);
1703 quote! {
1704 #enum_name::#variant_ident => #value,
1705 }
1706 });
1707
1708 let derives = if self.config.enable_specta {
1709 quote! {
1710 #[derive(Debug, Clone, PartialEq, Eq)]
1711 #[cfg_attr(feature = "specta", derive(specta::Type))]
1712 }
1713 } else {
1714 quote! {
1715 #[derive(Debug, Clone, PartialEq, Eq)]
1716 }
1717 };
1718
1719 Ok(quote! {
1720 #doc_comment
1721 #derives
1722 pub enum #enum_name {
1723 #(#known_variants)*
1724 Custom(String),
1726 }
1727
1728 impl<'de> serde::Deserialize<'de> for #enum_name {
1729 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1730 where
1731 D: serde::Deserializer<'de>,
1732 {
1733 let value = String::deserialize(deserializer)?;
1734 match value.as_str() {
1735 #(#match_arms_de)*
1736 _ => Ok(#enum_name::Custom(value)),
1737 }
1738 }
1739 }
1740
1741 impl serde::Serialize for #enum_name {
1742 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1743 where
1744 S: serde::Serializer,
1745 {
1746 serializer.serialize_str(self.as_str())
1747 }
1748 }
1749
1750 impl #enum_name {
1751 pub fn as_str(&self) -> &str {
1752 match self {
1753 #(#match_arms_ser)*
1754 #enum_name::Custom(s) => s.as_str(),
1755 }
1756 }
1757 }
1758
1759 impl ::std::fmt::Display for #enum_name {
1760 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
1761 f.write_str(self.as_str())
1762 }
1763 }
1764
1765 impl AsRef<str> for #enum_name {
1766 fn as_ref(&self) -> &str {
1767 self.as_str()
1768 }
1769 }
1770 })
1771 }
1772
1773 fn generate_string_enum(
1774 &self,
1775 schema: &crate::analysis::AnalyzedSchema,
1776 values: &[String],
1777 ext: Option<&crate::analysis::EnumExtensions>,
1778 ) -> Result<TokenStream> {
1779 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1780
1781 let default_value = schema
1788 .default
1789 .as_ref()
1790 .and_then(|v| v.as_str())
1791 .map(|s| s.to_string());
1792 let has_default_match = match &default_value {
1793 Some(d) => values.iter().any(|v| v == d),
1794 None => !values.is_empty(),
1795 };
1796
1797 let varnames_override: Option<&Vec<String>> = ext
1801 .filter(|_| self.config.types.x_enum_varnames_enabled())
1802 .map(|e| &e.varnames)
1803 .filter(|v| !v.is_empty() && v.len() == values.len());
1804 let descriptions_override: Option<&Vec<String>> = ext
1805 .filter(|_| self.config.types.x_enum_descriptions_enabled())
1806 .map(|e| &e.descriptions)
1807 .filter(|v| !v.is_empty() && v.len() == values.len());
1808
1809 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
1816 let variant_pairs: Vec<(syn::Ident, &String, bool, Option<String>)> = values
1817 .iter()
1818 .enumerate()
1819 .map(|(i, value)| {
1820 let base = match varnames_override {
1821 Some(v) => v[i].clone(),
1822 None => self.to_rust_enum_variant(value),
1823 };
1824 let mut variant_name = base.clone();
1825 let mut suffix = 2;
1826 while !used.insert(variant_name.clone()) {
1827 variant_name = format!("{base}_{suffix}");
1828 suffix += 1;
1829 }
1830 let variant_ident = format_ident!("{}", variant_name);
1831 let is_default = if let Some(ref default) = default_value {
1832 value == default
1833 } else {
1834 i == 0
1835 };
1836 let description = descriptions_override.map(|d| d[i].clone());
1837 (variant_ident, value, is_default, description)
1838 })
1839 .collect();
1840
1841 let variants =
1842 variant_pairs
1843 .iter()
1844 .map(|(variant_ident, value, is_default, description)| {
1845 let doc = description
1846 .as_ref()
1847 .map(|d| {
1848 let s = self.sanitize_doc_comment(d);
1849 quote! { #[doc = #s] }
1850 })
1851 .unwrap_or_default();
1852 if *is_default {
1853 quote! {
1854 #doc
1855 #[default]
1856 #[serde(rename = #value)]
1857 #variant_ident,
1858 }
1859 } else {
1860 quote! {
1861 #doc
1862 #[serde(rename = #value)]
1863 #variant_ident,
1864 }
1865 }
1866 });
1867
1868 let as_str_arms = variant_pairs.iter().map(|(variant_ident, value, _, _)| {
1872 quote! { Self::#variant_ident => #value, }
1873 });
1874
1875 let doc_comment = if let Some(desc) = &schema.description {
1876 quote! { #[doc = #desc] }
1877 } else {
1878 TokenStream::new()
1879 };
1880
1881 let derives = match (self.config.enable_specta, has_default_match) {
1884 (true, true) => quote! {
1885 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
1886 #[cfg_attr(feature = "specta", derive(specta::Type))]
1887 },
1888 (true, false) => quote! {
1889 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1890 #[cfg_attr(feature = "specta", derive(specta::Type))]
1891 },
1892 (false, true) => quote! {
1893 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
1894 },
1895 (false, false) => quote! {
1896 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1897 },
1898 };
1899
1900 Ok(quote! {
1901 #doc_comment
1902 #derives
1903 pub enum #enum_name {
1904 #(#variants)*
1905 }
1906
1907 impl #enum_name {
1908 pub fn as_str(&self) -> &'static str {
1909 match self {
1910 #(#as_str_arms)*
1911 }
1912 }
1913 }
1914
1915 impl ::std::fmt::Display for #enum_name {
1916 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
1917 f.write_str(self.as_str())
1918 }
1919 }
1920
1921 impl AsRef<str> for #enum_name {
1922 fn as_ref(&self) -> &str {
1923 self.as_str()
1924 }
1925 }
1926 })
1927 }
1928
1929 fn variant_field_name(
1932 &self,
1933 properties: &BTreeMap<String, crate::analysis::PropertyInfo>,
1934 ) -> String {
1935 let taken = |candidate: &str| {
1936 properties
1937 .keys()
1938 .any(|name| self.to_rust_field_name(name) == candidate)
1939 };
1940 if !taken("variant") {
1941 return "variant".to_string();
1942 }
1943 let mut suffix = 2;
1944 loop {
1945 let candidate = format!("variant{suffix}");
1946 if !taken(&candidate) {
1947 return candidate;
1948 }
1949 suffix += 1;
1950 }
1951 }
1952
1953 fn generate_struct(
1954 &self,
1955 schema: &crate::analysis::AnalyzedSchema,
1956 object: ObjectShape<'_>,
1957 analysis: &crate::analysis::SchemaAnalysis,
1958 type_context: &TypeGenerationContext<'_>,
1959 ) -> Result<TokenStream> {
1960 let ObjectShape {
1961 properties,
1962 required,
1963 additional_properties,
1964 variant,
1965 } = object;
1966 let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1967 let emitted_properties = self.emitted_object_properties(
1968 &schema.name,
1969 properties,
1970 required,
1971 additional_properties,
1972 analysis,
1973 type_context.discriminated_variants.get(&schema.name),
1974 );
1975
1976 let mut fields: Vec<TokenStream> = emitted_properties
1977 .iter()
1978 .map(|emitted| {
1979 let field_name = emitted.wire_name;
1980 let property = emitted.property;
1981 let field_ident = &emitted.ident;
1982 let field_type = &emitted.field_type;
1983 let serde_attrs = self.generate_serde_field_attrs(
1984 &schema.name,
1985 field_name,
1986 field_ident,
1987 property,
1988 emitted.is_required,
1989 analysis,
1990 );
1991 let specta_attrs = self.generate_specta_field_attrs(field_name);
1992
1993 let doc_comment = if let Some(desc) = &property.description {
1994 let sanitized_desc = self.sanitize_doc_comment(desc);
1995 quote! { #[doc = #sanitized_desc] }
1996 } else {
1997 TokenStream::new()
1998 };
1999 let constraint_doc = self.generate_constraint_doc(&property.constraints);
2000
2001 quote! {
2002 #doc_comment
2003 #constraint_doc
2004 #serde_attrs
2005 #specta_attrs
2006 pub #field_ident: #field_type,
2007 }
2008 })
2009 .collect();
2010
2011 match additional_properties {
2017 crate::analysis::ObjectAdditionalProperties::Forbidden => {}
2018 crate::analysis::ObjectAdditionalProperties::Untyped => {
2019 fields.push(quote! {
2020 #[serde(flatten)]
2022 pub additional_properties:
2023 std::collections::BTreeMap<String, serde_json::Value>,
2024 });
2025 }
2026 crate::analysis::ObjectAdditionalProperties::Typed { value_type } => {
2027 let value_tokens = self.generate_array_item_type(value_type, analysis);
2028 fields.push(quote! {
2029 #[serde(flatten)]
2032 pub additional_properties:
2033 std::collections::BTreeMap<String, #value_tokens>,
2034 });
2035 }
2036 }
2037
2038 if let Some(variant) = variant {
2043 let variant_type = format_ident!("{}", self.to_rust_type_name(&variant.target));
2044 let variant_field = format_ident!("{}", self.variant_field_name(properties));
2045 fields.push(quote! {
2046 #[serde(flatten)]
2048 pub #variant_field: #variant_type,
2049 });
2050 }
2051
2052 let doc_comment = if let Some(desc) = &schema.description {
2053 quote! { #[doc = #desc] }
2054 } else {
2055 TokenStream::new()
2056 };
2057
2058 let can_derive_default = variant.is_none()
2066 && emitted_properties
2067 .iter()
2068 .all(|property| !property.is_required);
2069
2070 let derives = match (self.config.enable_specta, can_derive_default) {
2074 (true, true) => quote! {
2075 #[derive(Debug, Clone, Deserialize, Serialize, Default)]
2076 #[cfg_attr(feature = "specta", derive(specta::Type))]
2077 },
2078 (true, false) => quote! {
2079 #[derive(Debug, Clone, Deserialize, Serialize)]
2080 #[cfg_attr(feature = "specta", derive(specta::Type))]
2081 },
2082 (false, true) => quote! {
2083 #[derive(Debug, Clone, Deserialize, Serialize, Default)]
2084 },
2085 (false, false) => quote! {
2086 #[derive(Debug, Clone, Deserialize, Serialize)]
2087 },
2088 };
2089
2090 let builder = if type_context.index.request_body_roots.contains(&schema.name)
2091 && variant.is_none()
2092 && emitted_properties
2093 .iter()
2094 .any(|property| property.is_required)
2095 && (emitted_properties
2096 .iter()
2097 .any(|property| !property.is_required)
2098 || !matches!(
2099 additional_properties,
2100 crate::analysis::ObjectAdditionalProperties::Forbidden
2101 )) {
2102 self.generate_request_model_builder(
2103 schema,
2104 &emitted_properties,
2105 additional_properties,
2106 analysis,
2107 type_context.index,
2108 )
2109 } else {
2110 TokenStream::new()
2111 };
2112
2113 Ok(quote! {
2114 #doc_comment
2115 #derives
2116 pub struct #struct_name {
2117 #(#fields)*
2118 }
2119
2120 #builder
2121 })
2122 }
2123
2124 pub(crate) fn emitted_object_properties<'a>(
2129 &self,
2130 schema_name: &str,
2131 properties: &'a BTreeMap<String, crate::analysis::PropertyInfo>,
2132 required: &std::collections::HashSet<String>,
2133 additional_properties: &crate::analysis::ObjectAdditionalProperties,
2134 analysis: &crate::analysis::SchemaAnalysis,
2135 discriminator_info: Option<&DiscriminatedVariantInfo>,
2136 ) -> Vec<EmittedObjectProperty<'a>> {
2137 let mut sorted_properties: Vec<_> = properties.iter().collect();
2138 sorted_properties.sort_by_key(|(name, _)| name.as_str());
2139
2140 let mut used_field_idents = std::collections::HashSet::new();
2141 if !matches!(
2142 additional_properties,
2143 crate::analysis::ObjectAdditionalProperties::Forbidden
2144 ) {
2145 used_field_idents.insert("additional_properties".to_string());
2146 }
2147
2148 let mut emitted = Vec::new();
2149 for (field_name, property) in sorted_properties {
2150 if discriminator_info.is_some_and(|info| {
2151 !info.is_parent_untagged && field_name.as_str() == info.discriminator_field.as_str()
2152 }) {
2153 continue;
2154 }
2155
2156 let raw = self.to_rust_field_name(field_name);
2157 let mut chosen = raw.clone();
2158 let mut suffix = 2;
2159 while !used_field_idents.insert(chosen.clone()) {
2160 chosen = format!("{raw}_{suffix}");
2161 suffix += 1;
2162 }
2163 let is_required = required.contains(field_name);
2164 emitted.push(EmittedObjectProperty {
2165 wire_name: field_name,
2166 property,
2167 ident: Self::to_field_ident(&chosen),
2168 is_required,
2169 field_type: self.generate_field_type(
2170 schema_name,
2171 field_name,
2172 property,
2173 is_required,
2174 analysis,
2175 ),
2176 });
2177 }
2178 emitted
2179 }
2180
2181 fn type_generation_index(
2182 &self,
2183 analysis: &crate::analysis::SchemaAnalysis,
2184 ) -> TypeGenerationIndex {
2185 let reserved_type_names = analysis
2186 .schemas
2187 .keys()
2188 .map(|name| self.to_rust_type_name(name))
2189 .collect();
2190 let mut request_body_roots = std::collections::HashSet::new();
2191 for operation in analysis.operations.values() {
2192 let Some(mut current) = operation
2193 .request_body
2194 .as_ref()
2195 .and_then(crate::analysis::RequestBodyContent::schema_name)
2196 else {
2197 continue;
2198 };
2199 while request_body_roots.insert(current.to_string()) {
2200 let Some(crate::analysis::AnalyzedSchema {
2201 schema_type: crate::analysis::SchemaType::Reference { target },
2202 ..
2203 }) = analysis.schemas.get(current)
2204 else {
2205 break;
2206 };
2207 current = target;
2208 }
2209 }
2210 TypeGenerationIndex {
2211 request_body_roots,
2212 reserved_type_names,
2213 }
2214 }
2215
2216 fn generate_request_model_builder(
2217 &self,
2218 schema: &crate::analysis::AnalyzedSchema,
2219 properties: &[EmittedObjectProperty<'_>],
2220 additional_properties: &crate::analysis::ObjectAdditionalProperties,
2221 analysis: &crate::analysis::SchemaAnalysis,
2222 type_index: &TypeGenerationIndex,
2223 ) -> TokenStream {
2224 let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2225 let builder_base = format!("{}Builder", struct_name);
2226 let mut builder_name = builder_base.clone();
2227 let mut suffix = 2;
2228 while type_index.reserved_type_names.contains(&builder_name) {
2229 builder_name = format!("{builder_base}{suffix}");
2230 suffix += 1;
2231 }
2232 let builder_name = format_ident!("{builder_name}");
2233
2234 let required_parameters: Vec<TokenStream> = properties
2235 .iter()
2236 .filter(|property| property.is_required)
2237 .map(|property| {
2238 let ident = &property.ident;
2239 let field_type = &property.field_type;
2240 quote! { #ident: #field_type }
2241 })
2242 .collect();
2243 let required_idents: Vec<&syn::Ident> = properties
2244 .iter()
2245 .filter(|property| property.is_required)
2246 .map(|property| &property.ident)
2247 .collect();
2248 let optional_initializers: Vec<TokenStream> = properties
2249 .iter()
2250 .filter(|property| !property.is_required)
2251 .map(|property| {
2252 let ident = &property.ident;
2253 quote! { #ident: None }
2254 })
2255 .collect();
2256
2257 let additional_initializer = match additional_properties {
2258 crate::analysis::ObjectAdditionalProperties::Forbidden => TokenStream::new(),
2259 crate::analysis::ObjectAdditionalProperties::Untyped
2260 | crate::analysis::ObjectAdditionalProperties::Typed { .. } => quote! {
2261 additional_properties: ::std::collections::BTreeMap::new(),
2262 },
2263 };
2264
2265 let mut used_builder_methods =
2266 std::collections::HashSet::from(["new".to_string(), "build".to_string()]);
2267 if !matches!(
2268 additional_properties,
2269 crate::analysis::ObjectAdditionalProperties::Forbidden
2270 ) {
2271 used_builder_methods.insert("additional_properties".to_string());
2272 }
2273 let optional_setters: Vec<TokenStream> = properties
2274 .iter()
2275 .filter(|property| !property.is_required)
2276 .map(|property| {
2277 let field_ident = &property.ident;
2278 let field_type = self.generate_property_base_type(
2279 &schema.name,
2280 property.wire_name,
2281 property.property,
2282 analysis,
2283 );
2284 let field_name = field_ident.to_string();
2288 let plain_field_name = field_name.strip_prefix("r#").unwrap_or(&field_name);
2289 let mut setter_name = if matches!(plain_field_name, "new" | "build") {
2290 format!("with_{plain_field_name}")
2291 } else {
2292 field_name.clone()
2293 };
2294 let setter_base = setter_name.clone();
2295 let mut suffix = 2;
2296 while !used_builder_methods.insert(setter_name.clone()) {
2297 setter_name = format!("{setter_base}_{suffix}");
2298 suffix += 1;
2299 }
2300 let setter_ident = Self::to_field_ident(&setter_name);
2301 let wire_name = property.wire_name;
2302 quote! {
2303 #[doc = concat!("Set the optional `", #wire_name, "` request field.")]
2304 #[must_use]
2305 pub fn #setter_ident(mut self, #field_ident: #field_type) -> Self {
2306 self.value.#field_ident = Some(#field_ident);
2307 self
2308 }
2309 }
2310 })
2311 .collect();
2312
2313 let additional_setter = match additional_properties {
2314 crate::analysis::ObjectAdditionalProperties::Forbidden => TokenStream::new(),
2315 crate::analysis::ObjectAdditionalProperties::Untyped => quote! {
2316 #[must_use]
2318 pub fn additional_properties(
2319 mut self,
2320 additional_properties: ::std::collections::BTreeMap<
2321 String,
2322 serde_json::Value,
2323 >,
2324 ) -> Self {
2325 self.value.additional_properties = additional_properties;
2326 self
2327 }
2328 },
2329 crate::analysis::ObjectAdditionalProperties::Typed { value_type } => {
2330 let value_type = self.generate_array_item_type(value_type, analysis);
2331 quote! {
2332 #[must_use]
2334 pub fn additional_properties(
2335 mut self,
2336 additional_properties: ::std::collections::BTreeMap<
2337 String,
2338 #value_type,
2339 >,
2340 ) -> Self {
2341 self.value.additional_properties = additional_properties;
2342 self
2343 }
2344 }
2345 }
2346 };
2347
2348 quote! {
2349 impl #struct_name {
2350 pub fn new(#(#required_parameters),*) -> Self {
2352 Self {
2353 #(#required_idents,)*
2354 #(#optional_initializers,)*
2355 #additional_initializer
2356 }
2357 }
2358
2359 pub fn builder(#(#required_parameters),*) -> #builder_name {
2361 #builder_name::new(#(#required_idents),*)
2362 }
2363 }
2364
2365 #[derive(Debug, Clone)]
2367 #[must_use]
2368 pub struct #builder_name {
2369 value: #struct_name,
2370 }
2371
2372 impl #builder_name {
2373 pub fn new(#(#required_parameters),*) -> Self {
2375 Self {
2376 value: #struct_name::new(#(#required_idents),*),
2377 }
2378 }
2379
2380 #(#optional_setters)*
2381 #additional_setter
2382
2383 pub fn build(self) -> #struct_name {
2385 self.value
2386 }
2387 }
2388 }
2389 }
2390
2391 fn generate_discriminated_enum(
2392 &self,
2393 schema: &crate::analysis::AnalyzedSchema,
2394 discriminator_field: &str,
2395 variants: &[crate::analysis::UnionVariant],
2396 analysis: &crate::analysis::SchemaAnalysis,
2397 ) -> Result<TokenStream> {
2398 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2399
2400 let has_nested_discriminated_union = variants.iter().any(|variant| {
2402 if let Some(variant_schema) = analysis.schemas.get(&variant.type_name) {
2403 matches!(
2404 variant_schema.schema_type,
2405 crate::analysis::SchemaType::DiscriminatedUnion { .. }
2406 )
2407 } else {
2408 false
2409 }
2410 });
2411
2412 if has_nested_discriminated_union {
2414 let schema_refs: Vec<crate::analysis::SchemaRef> = variants
2416 .iter()
2417 .map(|v| crate::analysis::SchemaRef {
2418 target: v.type_name.clone(),
2419 nullable: false,
2420 })
2421 .collect();
2422 return self.generate_union_enum(schema, &schema_refs, analysis);
2423 }
2424
2425 let enclosing = self.to_rust_type_name(&schema.name);
2426 let enum_variants = variants.iter().map(|variant| {
2427 let variant_name = format_ident!("{}", variant.rust_name);
2428 let variant_value = &variant.discriminator_value;
2429
2430 let variant_type = format_ident!("{}", self.to_rust_type_name(&variant.type_name));
2431 let payload = if self.to_rust_type_name(&variant.type_name) == enclosing
2435 || analysis
2436 .dependencies
2437 .recursive_schemas
2438 .contains(&variant.type_name)
2439 {
2440 quote! { Box<#variant_type> }
2441 } else {
2442 quote! { #variant_type }
2443 };
2444 quote! {
2445 #[serde(rename = #variant_value)]
2446 #variant_name(#payload),
2447 }
2448 });
2449
2450 let doc_comment = if let Some(desc) = &schema.description {
2451 quote! { #[doc = #desc] }
2452 } else {
2453 TokenStream::new()
2454 };
2455
2456 let derives = if self.config.enable_specta {
2458 quote! {
2459 #[derive(Debug, Clone, Deserialize, Serialize)]
2460 #[cfg_attr(feature = "specta", derive(specta::Type))]
2461 #[serde(tag = #discriminator_field)]
2462 }
2463 } else {
2464 quote! {
2465 #[derive(Debug, Clone, Deserialize, Serialize)]
2466 #[serde(tag = #discriminator_field)]
2467 }
2468 };
2469
2470 Ok(quote! {
2471 #doc_comment
2472 #derives
2473 pub enum #enum_name {
2474 #(#enum_variants)*
2475 }
2476 })
2477 }
2478
2479 fn should_use_untagged_discriminated_union(
2481 &self,
2482 schema: &crate::analysis::AnalyzedSchema,
2483 analysis: &crate::analysis::SchemaAnalysis,
2484 ) -> bool {
2485 for other_schema in analysis.schemas.values() {
2490 if let crate::analysis::SchemaType::DiscriminatedUnion {
2491 variants,
2492 discriminator_field: _,
2493 } = &other_schema.schema_type
2494 {
2495 for variant in variants {
2496 if variant.type_name == schema.name {
2497 if let crate::analysis::SchemaType::DiscriminatedUnion {
2502 discriminator_field: current_discriminator,
2503 variants: current_variants,
2504 ..
2505 } = &schema.schema_type
2506 {
2507 for current_variant in current_variants {
2509 if let Some(variant_schema) =
2510 analysis.schemas.get(¤t_variant.type_name)
2511 {
2512 if let crate::analysis::SchemaType::Object {
2513 properties, ..
2514 } = &variant_schema.schema_type
2515 {
2516 if properties.contains_key(current_discriminator) {
2517 return false;
2520 }
2521 }
2522 }
2523 }
2524 }
2525
2526 return true;
2528 }
2529 }
2530 }
2531 }
2532 false
2533 }
2534
2535 fn generate_union_enum(
2536 &self,
2537 schema: &crate::analysis::AnalyzedSchema,
2538 variants: &[crate::analysis::SchemaRef],
2539 analysis: &crate::analysis::SchemaAnalysis,
2540 ) -> Result<TokenStream> {
2541 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2542
2543 let mut used_variant_names = std::collections::HashSet::new();
2545 let enum_variants = variants.iter().enumerate().map(|(i, variant)| {
2546 let base_variant_name = self.type_name_to_variant_name(&variant.target);
2548 let variant_name = self.ensure_unique_variant_name_generator(
2549 base_variant_name,
2550 &mut used_variant_names,
2551 i,
2552 );
2553 let variant_name_ident = format_ident!("{}", variant_name);
2554
2555 let variant_type_tokens = if matches!(
2557 variant.target.as_str(),
2558 "bool"
2559 | "i8"
2560 | "i16"
2561 | "i32"
2562 | "i64"
2563 | "i128"
2564 | "u8"
2565 | "u16"
2566 | "u32"
2567 | "u64"
2568 | "u128"
2569 | "f32"
2570 | "f64"
2571 | "String"
2572 ) {
2573 let type_ident = format_ident!("{}", variant.target);
2574 quote! { #type_ident }
2575 } else if variant.target == "serde_json::Value" {
2576 quote! { serde_json::Value }
2579 } else if variant.target.starts_with("Vec<") && variant.target.ends_with(">") {
2580 let inner = &variant.target[4..variant.target.len() - 1];
2582
2583 if inner.starts_with("Vec<") && inner.ends_with(">") {
2585 let inner_inner = &inner[4..inner.len() - 1];
2586 if inner_inner == "serde_json::Value" {
2587 quote! { Vec<Vec<serde_json::Value>> }
2588 } else {
2589 let inner_inner_type = if matches!(
2590 inner_inner,
2591 "bool"
2592 | "i8"
2593 | "i16"
2594 | "i32"
2595 | "i64"
2596 | "i128"
2597 | "u8"
2598 | "u16"
2599 | "u32"
2600 | "u64"
2601 | "u128"
2602 | "f32"
2603 | "f64"
2604 | "String"
2605 ) {
2606 format_ident!("{}", inner_inner)
2607 } else {
2608 format_ident!("{}", self.to_rust_type_name(inner_inner))
2609 };
2610 quote! { Vec<Vec<#inner_inner_type>> }
2611 }
2612 } else if inner == "serde_json::Value" {
2613 quote! { Vec<serde_json::Value> }
2614 } else {
2615 let inner_type = if matches!(
2616 inner,
2617 "bool"
2618 | "i8"
2619 | "i16"
2620 | "i32"
2621 | "i64"
2622 | "i128"
2623 | "u8"
2624 | "u16"
2625 | "u32"
2626 | "u64"
2627 | "u128"
2628 | "f32"
2629 | "f64"
2630 | "String"
2631 ) {
2632 format_ident!("{}", inner)
2633 } else {
2634 format_ident!("{}", self.to_rust_type_name(inner))
2635 };
2636 quote! { Vec<#inner_type> }
2637 }
2638 } else if variant.target.contains("::") || variant.target.contains('<') {
2639 parse_rust_type(&variant.target).unwrap_or_else(|_| {
2644 let fallback = format_ident!("{}", self.to_rust_type_name(&variant.target));
2645 quote! { #fallback }
2646 })
2647 } else {
2648 let type_ident = format_ident!("{}", self.to_rust_type_name(&variant.target));
2649 quote! { #type_ident }
2650 };
2651
2652 let target_rust_name = self.to_rust_type_name(&variant.target);
2656 let enclosing_name = self.to_rust_type_name(&schema.name);
2657 let is_self_ref = target_rust_name == enclosing_name;
2658 let is_recursive_target = analysis
2662 .dependencies
2663 .recursive_schemas
2664 .contains(&variant.target);
2665 let variant_type_tokens = if is_self_ref || is_recursive_target {
2666 quote! { Box<#variant_type_tokens> }
2667 } else {
2668 variant_type_tokens
2669 };
2670
2671 quote! {
2672 #variant_name_ident(#variant_type_tokens),
2673 }
2674 });
2675
2676 let doc_comment = if let Some(desc) = &schema.description {
2677 quote! { #[doc = #desc] }
2678 } else {
2679 TokenStream::new()
2680 };
2681
2682 let derives = if self.config.enable_specta {
2684 quote! {
2685 #[derive(Debug, Clone, Deserialize, Serialize)]
2686 #[cfg_attr(feature = "specta", derive(specta::Type))]
2687 #[serde(untagged)]
2688 }
2689 } else {
2690 quote! {
2691 #[derive(Debug, Clone, Deserialize, Serialize)]
2692 #[serde(untagged)]
2693 }
2694 };
2695
2696 Ok(quote! {
2697 #doc_comment
2698 #derives
2699 pub enum #enum_name {
2700 #(#enum_variants)*
2701 }
2702 })
2703 }
2704
2705 fn target_aliases_back_to(
2710 &self,
2711 target: &str,
2712 enclosing_rust_name: &str,
2713 analysis: &crate::analysis::SchemaAnalysis,
2714 ) -> bool {
2715 let mut current = target.to_string();
2716 let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
2717 for _ in 0..16 {
2718 if !visited.insert(current.clone()) {
2719 return true;
2720 }
2721 let Some(schema) = analysis.schemas.get(¤t) else {
2722 return false;
2723 };
2724 if let crate::analysis::SchemaType::Reference { target: next } = &schema.schema_type {
2725 if self.to_rust_type_name(next) == enclosing_rust_name {
2726 return true;
2727 }
2728 current = next.clone();
2729 continue;
2730 }
2731 return false;
2732 }
2733 false
2734 }
2735
2736 fn generate_field_type(
2737 &self,
2738 schema_name: &str,
2739 field_name: &str,
2740 prop: &crate::analysis::PropertyInfo,
2741 is_required: bool,
2742 analysis: &crate::analysis::SchemaAnalysis,
2743 ) -> TokenStream {
2744 let base_type = self.generate_property_base_type(schema_name, field_name, prop, analysis);
2745
2746 if self.property_is_option_wrapped(schema_name, field_name, prop, is_required, analysis) {
2747 quote! { Option<#base_type> }
2748 } else {
2749 base_type
2750 }
2751 }
2752
2753 fn property_is_option_wrapped(
2754 &self,
2755 schema_name: &str,
2756 field_name: &str,
2757 prop: &crate::analysis::PropertyInfo,
2758 is_required: bool,
2759 analysis: &crate::analysis::SchemaAnalysis,
2760 ) -> bool {
2761 let override_key = format!("{schema_name}.{field_name}");
2762 let is_nullable_override = self
2763 .config
2764 .nullable_field_overrides
2765 .get(&override_key)
2766 .copied()
2767 .unwrap_or(false);
2768
2769 !is_required
2770 || prop.nullable
2771 || is_nullable_override
2772 || (prop.default.is_some() && self.type_lacks_default(&prop.schema_type, analysis))
2773 }
2774
2775 pub(crate) fn generate_property_base_type(
2776 &self,
2777 schema_name: &str,
2778 _field_name: &str,
2779 prop: &crate::analysis::PropertyInfo,
2780 analysis: &crate::analysis::SchemaAnalysis,
2781 ) -> TokenStream {
2782 use crate::analysis::SchemaType;
2783
2784 match &prop.schema_type {
2785 SchemaType::Primitive { rust_type, .. } => {
2786 parse_rust_type(rust_type).unwrap_or_else(|_| {
2789 eprintln!(
2794 "⚠️ TypeMapper produced un-parseable type `{rust_type}`; \
2795 falling back to String"
2796 );
2797 quote! { String }
2798 })
2799 }
2800 SchemaType::Reference { target } => {
2801 let target_rust_name = self.to_rust_type_name(target);
2802 let target_type = format_ident!("{}", target_rust_name);
2803 let enclosing_rust_name = self.to_rust_type_name(schema_name);
2816 let is_self_via_rust_name = target_rust_name == enclosing_rust_name;
2817 let is_alias_chain_self =
2818 self.target_aliases_back_to(target, &enclosing_rust_name, analysis);
2819 if analysis.dependencies.recursive_schemas.contains(target)
2820 || is_self_via_rust_name
2821 || is_alias_chain_self
2822 {
2823 quote! { Box<#target_type> }
2824 } else {
2825 quote! { #target_type }
2826 }
2827 }
2828 SchemaType::Array { item_type } => {
2829 let inner_type = self.generate_array_item_type(item_type, analysis);
2830 quote! { Vec<#inner_type> }
2831 }
2832 SchemaType::Tuple { element_types } => {
2833 self.generate_tuple_type(element_types, analysis)
2834 }
2835 SchemaType::Untyped { shape, .. } => untyped_tokens(*shape),
2836 _ => {
2837 quote! { serde_json::Value }
2839 }
2840 }
2841 }
2842
2843 fn generate_tuple_type(
2847 &self,
2848 element_types: &[crate::analysis::SchemaType],
2849 analysis: &crate::analysis::SchemaAnalysis,
2850 ) -> TokenStream {
2851 let elements = element_types
2852 .iter()
2853 .map(|element_type| self.generate_array_item_type(element_type, analysis))
2854 .collect::<Vec<_>>();
2855 if let [only] = elements.as_slice() {
2859 return quote! { (#only,) };
2860 }
2861 quote! { (#(#elements),*) }
2862 }
2863
2864 fn generate_serde_field_attrs(
2865 &self,
2866 schema_name: &str,
2867 field_name: &str,
2868 field_ident: &syn::Ident,
2869 prop: &crate::analysis::PropertyInfo,
2870 is_required: bool,
2871 analysis: &crate::analysis::SchemaAnalysis,
2872 ) -> TokenStream {
2873 let mut attrs = Vec::new();
2874
2875 let rust_field_name = field_ident.to_string();
2878 let comparison_name = rust_field_name
2879 .strip_prefix("r#")
2880 .unwrap_or(&rust_field_name);
2881 if comparison_name != field_name {
2882 attrs.push(quote! { rename = #field_name });
2883 }
2884
2885 if !is_required || prop.nullable {
2887 attrs.push(quote! { skip_serializing_if = "Option::is_none" });
2888 }
2889
2890 if prop.default.is_some()
2894 && (is_required && !prop.nullable)
2895 && !self.type_lacks_default(&prop.schema_type, analysis)
2896 {
2897 attrs.push(quote! { default });
2898 }
2899
2900 if let crate::analysis::SchemaType::Primitive {
2908 serde_with: Some(codec),
2909 ..
2910 } = &prop.schema_type
2911 {
2912 let is_option_wrapped = self.property_is_option_wrapped(
2913 schema_name,
2914 field_name,
2915 prop,
2916 is_required,
2917 analysis,
2918 );
2919 let codec_path = if is_option_wrapped {
2920 format!("{codec}::option")
2921 } else {
2922 codec.clone()
2923 };
2924 attrs.push(quote! { with = #codec_path });
2925 if is_option_wrapped {
2930 attrs.push(quote! { default });
2931 }
2932 }
2933
2934 if attrs.is_empty() {
2935 TokenStream::new()
2936 } else {
2937 quote! { #[serde(#(#attrs),*)] }
2938 }
2939 }
2940
2941 fn type_lacks_default(
2945 &self,
2946 schema_type: &crate::analysis::SchemaType,
2947 analysis: &crate::analysis::SchemaAnalysis,
2948 ) -> bool {
2949 use crate::analysis::SchemaType;
2950 match schema_type {
2951 SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => true,
2952 SchemaType::Primitive { rust_type, .. } => matches!(
2956 rust_type.as_str(),
2957 "chrono::DateTime<chrono::Utc>"
2958 | "chrono::NaiveDate"
2959 | "chrono::NaiveTime"
2960 | "chrono::Duration"
2961 | "url::Url"
2962 | "time::OffsetDateTime"
2963 | "time::Date"
2964 | "time::Time"
2965 | "iso8601::Duration"
2966 | "email_address::EmailAddress"
2967 ),
2968 SchemaType::Reference { target } => {
2969 if let Some(schema) = analysis.schemas.get(target) {
2970 self.type_lacks_default(&schema.schema_type, analysis)
2971 } else {
2972 false
2973 }
2974 }
2975 _ => false,
2976 }
2977 }
2978
2979 fn generate_specta_field_attrs(&self, field_name: &str) -> TokenStream {
2980 if !self.config.enable_specta {
2981 return TokenStream::new();
2982 }
2983
2984 let camel_case_name = self.to_camel_case(field_name);
2986
2987 if camel_case_name != field_name {
2989 quote! { #[cfg_attr(feature = "specta", specta(rename = #camel_case_name))] }
2990 } else {
2991 TokenStream::new()
2992 }
2993 }
2994
2995 pub(crate) fn to_rust_enum_variant(&self, s: &str) -> String {
2996 let neg_prefix =
3000 if s.starts_with('-') && s.chars().skip(1).all(|c| c.is_ascii_digit() || c == '.') {
3001 "Neg"
3002 } else {
3003 ""
3004 };
3005
3006 let mut result = String::new();
3008 let mut next_upper = true;
3009 let mut prev_was_upper = false;
3010
3011 for (i, c) in s.chars().enumerate() {
3012 match c {
3013 'a'..='z' => {
3014 if next_upper {
3015 result.push(c.to_ascii_uppercase());
3016 next_upper = false;
3017 } else {
3018 result.push(c);
3019 }
3020 prev_was_upper = false;
3021 }
3022 'A'..='Z' => {
3023 if next_upper || (!prev_was_upper && i > 0) {
3024 result.push(c);
3026 next_upper = false;
3027 } else {
3028 result.push(c.to_ascii_lowercase());
3030 }
3031 prev_was_upper = true;
3032 }
3033 '0'..='9' => {
3034 result.push(c);
3035 next_upper = false;
3036 prev_was_upper = false;
3037 }
3038 '.' | '-' | '_' | ' ' | '@' | '#' | '$' | '/' | '\\' => {
3039 next_upper = true;
3041 prev_was_upper = false;
3042 }
3043 _ => {
3044 next_upper = true;
3046 prev_was_upper = false;
3047 }
3048 }
3049 }
3050
3051 if result.is_empty() {
3053 result = "Value".to_string();
3054 }
3055
3056 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
3058 result = format!("Variant{neg_prefix}{result}");
3059 } else if !neg_prefix.is_empty() {
3060 result = format!("{neg_prefix}{result}");
3063 }
3064
3065 match result.as_str() {
3067 "Null" => "NullValue".to_string(),
3068 "True" => "TrueValue".to_string(),
3069 "False" => "FalseValue".to_string(),
3070 "Type" => "Type_".to_string(),
3071 "Match" => "Match_".to_string(),
3072 "Fn" => "Fn_".to_string(),
3073 "Impl" => "Impl_".to_string(),
3074 "Trait" => "Trait_".to_string(),
3075 "Struct" => "Struct_".to_string(),
3076 "Enum" => "Enum_".to_string(),
3077 "Mod" => "Mod_".to_string(),
3078 "Use" => "Use_".to_string(),
3079 "Pub" => "Pub_".to_string(),
3080 "Const" => "Const_".to_string(),
3081 "Static" => "Static_".to_string(),
3082 "Let" => "Let_".to_string(),
3083 "Mut" => "Mut_".to_string(),
3084 "Ref" => "Ref_".to_string(),
3085 "Move" => "Move_".to_string(),
3086 "Return" => "Return_".to_string(),
3087 "If" => "If_".to_string(),
3088 "Else" => "Else_".to_string(),
3089 "While" => "While_".to_string(),
3090 "For" => "For_".to_string(),
3091 "Loop" => "Loop_".to_string(),
3092 "Break" => "Break_".to_string(),
3093 "Continue" => "Continue_".to_string(),
3094 "Self" => "Self_".to_string(),
3095 "Super" => "Super_".to_string(),
3096 "Crate" => "Crate_".to_string(),
3097 "Async" => "Async_".to_string(),
3098 "Await" => "Await_".to_string(),
3099 _ => result,
3100 }
3101 }
3102
3103 #[allow(dead_code)]
3104 fn to_rust_identifier(&self, s: &str) -> String {
3105 let mut result = s
3107 .chars()
3108 .map(|c| match c {
3109 'a'..='z' | 'A'..='Z' | '0'..='9' => c,
3110 '.' | '-' | '_' | ' ' | '@' | '#' | '$' | '/' | '\\' => '_',
3111 _ => '_',
3112 })
3113 .collect::<String>();
3114
3115 result = result.trim_matches('_').to_string();
3117
3118 if result.is_empty() {
3120 result = "value".to_string();
3121 }
3122
3123 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
3125 result = format!("variant_{result}");
3126 }
3127
3128 match result.as_str() {
3130 "null" => "null_value".to_string(),
3131 "true" => "true_value".to_string(),
3132 "false" => "false_value".to_string(),
3133 "type" => "type_".to_string(),
3134 "match" => "match_".to_string(),
3135 "fn" => "fn_".to_string(),
3136 "impl" => "impl_".to_string(),
3137 "trait" => "trait_".to_string(),
3138 "struct" => "struct_".to_string(),
3139 "enum" => "enum_".to_string(),
3140 "mod" => "mod_".to_string(),
3141 "use" => "use_".to_string(),
3142 "pub" => "pub_".to_string(),
3143 "const" => "const_".to_string(),
3144 "static" => "static_".to_string(),
3145 "let" => "let_".to_string(),
3146 "mut" => "mut_".to_string(),
3147 "ref" => "ref_".to_string(),
3148 "move" => "move_".to_string(),
3149 "return" => "return_".to_string(),
3150 "if" => "if_".to_string(),
3151 "else" => "else_".to_string(),
3152 "while" => "while_".to_string(),
3153 "for" => "for_".to_string(),
3154 "loop" => "loop_".to_string(),
3155 "break" => "break_".to_string(),
3156 "continue" => "continue_".to_string(),
3157 "self" => "self_".to_string(),
3158 "super" => "super_".to_string(),
3159 "crate" => "crate_".to_string(),
3160 "async" => "async_".to_string(),
3161 "await" => "await_".to_string(),
3162 "override" => "override_".to_string(),
3164 "box" => "box_".to_string(),
3165 "dyn" => "dyn_".to_string(),
3166 "where" => "where_".to_string(),
3167 "in" => "in_".to_string(),
3168 "abstract" => "abstract_".to_string(),
3170 "become" => "become_".to_string(),
3171 "do" => "do_".to_string(),
3172 "final" => "final_".to_string(),
3173 "macro" => "macro_".to_string(),
3174 "priv" => "priv_".to_string(),
3175 "try" => "try_".to_string(),
3176 "typeof" => "typeof_".to_string(),
3177 "unsized" => "unsized_".to_string(),
3178 "virtual" => "virtual_".to_string(),
3179 "yield" => "yield_".to_string(),
3180 _ => result,
3181 }
3182 }
3183
3184 fn generate_constraint_doc(
3192 &self,
3193 constraints: &crate::analysis::PropertyConstraints,
3194 ) -> TokenStream {
3195 use crate::type_mapping::ConstraintMode;
3196
3197 if constraints.is_empty() {
3198 return TokenStream::new();
3199 }
3200 match self.config.types.constraint_mode() {
3201 ConstraintMode::Off => TokenStream::new(),
3202 ConstraintMode::Doc => {
3203 let formatted = format_constraints_doc(constraints);
3204 quote! { #[doc = #formatted] }
3205 }
3206 }
3207 }
3208
3209 fn sanitize_doc_comment(&self, desc: &str) -> String {
3210 let mut result = desc.to_string();
3212
3213 if result.contains('\n')
3221 && (result.contains('{')
3222 || result.contains("```")
3223 || result.contains("Human:")
3224 || result.contains("Assistant:")
3225 || result
3226 .lines()
3227 .any(|line| line.trim().starts_with('"') && line.trim().ends_with('"')))
3228 {
3229 if result.contains("```") {
3231 result = result.replace("```", "```ignore");
3232 } else {
3233 if result.lines().any(|line| {
3235 let trimmed = line.trim();
3236 trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() > 2
3237 }) {
3238 result = format!("```ignore\n{result}\n```");
3239 }
3240 }
3241 }
3242
3243 result
3244 }
3245
3246 pub(crate) fn to_rust_type_name(&self, s: &str) -> String {
3247 rust_type_name(s)
3248 }
3249
3250 pub(crate) fn to_rust_field_name(&self, s: &str) -> String {
3251 let leading_marker = match s.chars().next() {
3255 Some('-') if s.len() > 1 => "neg_",
3256 Some('+') if s.len() > 1 => "pos_",
3257 _ => "",
3258 };
3259
3260 let mut result = String::new();
3262 let mut prev_was_upper = false;
3263 let mut prev_was_underscore = false;
3264
3265 for (i, c) in s.chars().enumerate() {
3266 match c {
3267 'A'..='Z' => {
3268 if i > 0 && !prev_was_upper && !prev_was_underscore {
3270 result.push('_');
3271 }
3272 result.push(c.to_ascii_lowercase());
3273 prev_was_upper = true;
3274 prev_was_underscore = false;
3275 }
3276 'a'..='z' | '0'..='9' => {
3277 result.push(c);
3278 prev_was_upper = false;
3279 prev_was_underscore = false;
3280 }
3281 '-' | '.' | '_' | '@' | '#' | '$' | ' ' => {
3282 if !prev_was_underscore && !result.is_empty() {
3283 result.push('_');
3284 prev_was_underscore = true;
3285 }
3286 prev_was_upper = false;
3287 }
3288 _ => {
3289 if !prev_was_underscore && !result.is_empty() {
3291 result.push('_');
3292 }
3293 prev_was_upper = false;
3294 prev_was_underscore = true;
3295 }
3296 }
3297 }
3298
3299 let mut result = result.trim_matches('_').to_string();
3301 if result.is_empty() {
3302 return "field".to_string();
3303 }
3304
3305 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
3307 result = format!("field_{leading_marker}{result}");
3308 } else if !leading_marker.is_empty() {
3309 result = format!("{leading_marker}{result}");
3310 }
3311
3312 if matches!(result.as_str(), "self" | "super" | "crate" | "Self") {
3316 return format!("{result}_field");
3317 }
3318 if Self::is_rust_keyword(&result) {
3320 format!("r#{result}")
3321 } else {
3322 result
3323 }
3324 }
3325
3326 pub fn is_rust_keyword(s: &str) -> bool {
3328 matches!(
3329 s,
3330 "type"
3331 | "match"
3332 | "fn"
3333 | "struct"
3334 | "enum"
3335 | "impl"
3336 | "trait"
3337 | "mod"
3338 | "use"
3339 | "pub"
3340 | "const"
3341 | "static"
3342 | "let"
3343 | "mut"
3344 | "ref"
3345 | "move"
3346 | "return"
3347 | "if"
3348 | "else"
3349 | "while"
3350 | "for"
3351 | "loop"
3352 | "break"
3353 | "continue"
3354 | "self"
3355 | "super"
3356 | "crate"
3357 | "async"
3358 | "await"
3359 | "override"
3360 | "box"
3361 | "dyn"
3362 | "where"
3363 | "in"
3364 | "abstract"
3365 | "become"
3366 | "do"
3367 | "final"
3368 | "macro"
3369 | "priv"
3370 | "try"
3371 | "typeof"
3372 | "unsized"
3373 | "virtual"
3374 | "yield"
3375 | "gen"
3377 )
3378 }
3379
3380 pub fn to_field_ident(name: &str) -> proc_macro2::Ident {
3382 if let Some(raw) = name.strip_prefix("r#") {
3383 proc_macro2::Ident::new_raw(raw, proc_macro2::Span::call_site())
3384 } else {
3385 proc_macro2::Ident::new(name, proc_macro2::Span::call_site())
3386 }
3387 }
3388
3389 fn to_camel_case(&self, s: &str) -> String {
3390 let mut result = String::new();
3392 let mut capitalize_next = false;
3393
3394 for (i, c) in s.chars().enumerate() {
3395 match c {
3396 '_' | '-' | '.' | ' ' => {
3397 capitalize_next = true;
3399 }
3400 'A'..='Z' => {
3401 if i == 0 {
3402 result.push(c.to_ascii_lowercase());
3404 } else if capitalize_next {
3405 result.push(c);
3406 capitalize_next = false;
3407 } else {
3408 result.push(c.to_ascii_lowercase());
3409 }
3410 }
3411 'a'..='z' | '0'..='9' => {
3412 if capitalize_next {
3413 result.push(c.to_ascii_uppercase());
3414 capitalize_next = false;
3415 } else {
3416 result.push(c);
3417 }
3418 }
3419 _ => {
3420 capitalize_next = true;
3422 }
3423 }
3424 }
3425
3426 if result.is_empty() {
3427 return "field".to_string();
3428 }
3429
3430 result
3431 }
3432
3433 fn generate_composition_struct(
3434 &self,
3435 schema: &crate::analysis::AnalyzedSchema,
3436 schemas: &[crate::analysis::SchemaRef],
3437 ) -> Result<TokenStream> {
3438 let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
3439
3440 let fields = schemas.iter().enumerate().map(|(i, schema_ref)| {
3446 let field_name = format_ident!("part_{}", i);
3447 let field_type = format_ident!("{}", self.to_rust_type_name(&schema_ref.target));
3448
3449 quote! {
3450 #[serde(flatten)]
3451 pub #field_name: #field_type,
3452 }
3453 });
3454
3455 let doc_comment = if let Some(desc) = &schema.description {
3456 quote! { #[doc = #desc] }
3457 } else {
3458 TokenStream::new()
3459 };
3460
3461 let derives = if self.config.enable_specta {
3463 quote! {
3464 #[derive(Debug, Clone, Deserialize, Serialize)]
3465 #[cfg_attr(feature = "specta", derive(specta::Type))]
3466 }
3467 } else {
3468 quote! {
3469 #[derive(Debug, Clone, Deserialize, Serialize)]
3470 }
3471 };
3472
3473 Ok(quote! {
3474 #doc_comment
3475 #derives
3476 pub struct #struct_name {
3477 #(#fields)*
3478 }
3479 })
3480 }
3481
3482 #[allow(dead_code)]
3483 fn find_missing_types(&self, analysis: &SchemaAnalysis) -> std::collections::HashSet<String> {
3484 let mut missing = std::collections::HashSet::new();
3485 let defined_types: std::collections::HashSet<String> =
3486 analysis.schemas.keys().cloned().collect();
3487
3488 for schema in analysis.schemas.values() {
3490 match &schema.schema_type {
3491 crate::analysis::SchemaType::Union { variants } => {
3492 for variant in variants {
3493 if !defined_types.contains(&variant.target) {
3494 missing.insert(variant.target.clone());
3495 }
3496 }
3497 }
3498 crate::analysis::SchemaType::DiscriminatedUnion { variants, .. } => {
3499 for variant in variants {
3500 if !defined_types.contains(&variant.type_name) {
3501 missing.insert(variant.type_name.clone());
3502 }
3503 }
3504 }
3505 crate::analysis::SchemaType::Object { properties, .. } => {
3506 let mut sorted_props: Vec<_> = properties.iter().collect();
3508 sorted_props.sort_by_key(|(name, _)| name.as_str());
3509 for (_, prop) in sorted_props {
3510 if let crate::analysis::SchemaType::Reference { target } = &prop.schema_type
3511 {
3512 if !defined_types.contains(target) {
3513 missing.insert(target.clone());
3514 }
3515 }
3516 }
3517 }
3518 crate::analysis::SchemaType::Reference { target }
3519 if !defined_types.contains(target) =>
3520 {
3521 missing.insert(target.clone());
3522 }
3523 _ => {}
3524 }
3525 }
3526
3527 missing
3528 }
3529
3530 #[allow(clippy::only_used_in_recursion)]
3531 fn generate_array_item_type(
3532 &self,
3533 item_type: &crate::analysis::SchemaType,
3534 analysis: &crate::analysis::SchemaAnalysis,
3535 ) -> TokenStream {
3536 use crate::analysis::SchemaType;
3537
3538 match item_type {
3539 SchemaType::Primitive { rust_type, .. } => {
3540 if let Ok(parsed) = syn::parse_str::<syn::Type>(rust_type) {
3545 quote! { #parsed }
3546 } else if rust_type.contains("::") {
3547 let parts: Vec<_> = rust_type
3548 .split("::")
3549 .map(|p| format_ident!("{}", p))
3550 .collect();
3551 quote! { #(#parts)::* }
3552 } else {
3553 let type_ident = format_ident!("{}", rust_type);
3554 quote! { #type_ident }
3555 }
3556 }
3557 SchemaType::Reference { target } => {
3558 let target_type = format_ident!("{}", self.to_rust_type_name(target));
3559 if analysis.dependencies.recursive_schemas.contains(target) {
3561 quote! { Box<#target_type> }
3562 } else {
3563 quote! { #target_type }
3564 }
3565 }
3566 SchemaType::Array { item_type } => {
3567 let inner_type = self.generate_array_item_type(item_type, analysis);
3569 quote! { Vec<#inner_type> }
3570 }
3571 SchemaType::Tuple { element_types } => {
3572 self.generate_tuple_type(element_types, analysis)
3573 }
3574 SchemaType::Untyped { shape, .. } => untyped_tokens(*shape),
3575 _ => {
3576 quote! { serde_json::Value }
3578 }
3579 }
3580 }
3581
3582 fn type_name_to_variant_name(&self, type_name: &str) -> String {
3584 match type_name {
3586 "bool" => return "Boolean".to_string(),
3587 "i8" | "i16" | "i32" | "i64" | "i128" => return "Integer".to_string(),
3588 "u8" | "u16" | "u32" | "u64" | "u128" => return "UnsignedInteger".to_string(),
3589 "f32" | "f64" => return "Number".to_string(),
3590 "String" => return "String".to_string(),
3591 "serde_json::Value" => return "Value".to_string(),
3592 "bytes::Bytes" => return "Binary".to_string(),
3596 "chrono::DateTime<chrono::Utc>" => return "DateTime".to_string(),
3597 "chrono::NaiveDate" => return "Date".to_string(),
3598 "chrono::NaiveTime" => return "Time".to_string(),
3599 "uuid::Uuid" => return "Uuid".to_string(),
3600 "url::Url" => return "Url".to_string(),
3601 "std::net::Ipv4Addr" => return "Ipv4".to_string(),
3602 "std::net::Ipv6Addr" => return "Ipv6".to_string(),
3603 _ => {}
3604 }
3605
3606 if type_name.starts_with("Vec<") && type_name.ends_with(">") {
3608 let inner = &type_name[4..type_name.len() - 1];
3609 if inner.starts_with("Vec<") && inner.ends_with(">") {
3611 let inner_inner = &inner[4..inner.len() - 1];
3612 return format!("{}ArrayArray", self.type_name_to_variant_name(inner_inner));
3613 }
3614 return format!("{}Array", self.type_name_to_variant_name(inner));
3615 }
3616
3617 let clean_name = type_name
3623 .trim_end_matches("Type")
3624 .trim_end_matches("Schema")
3625 .trim_end_matches("Item");
3626
3627 self.to_rust_type_name(clean_name)
3629 }
3630
3631 fn ensure_unique_variant_name_generator(
3633 &self,
3634 base_name: String,
3635 used_names: &mut std::collections::HashSet<String>,
3636 fallback_index: usize,
3637 ) -> String {
3638 if used_names.insert(base_name.clone()) {
3639 return base_name;
3640 }
3641
3642 for i in 2..100 {
3644 let numbered_name = format!("{base_name}{i}");
3645 if used_names.insert(numbered_name.clone()) {
3646 return numbered_name;
3647 }
3648 }
3649
3650 let fallback = format!("Variant{fallback_index}");
3652 used_names.insert(fallback.clone());
3653 fallback
3654 }
3655
3656 fn find_request_type_for_operation(
3658 &self,
3659 operation_id: &str,
3660 analysis: &SchemaAnalysis,
3661 ) -> Option<String> {
3662 analysis.operations.get(operation_id).and_then(|op| {
3664 op.request_body
3665 .as_ref()
3666 .and_then(|rb| rb.schema_name().map(|s| s.to_string()))
3667 })
3668 }
3669
3670 fn resolve_streaming_event_type(
3672 &self,
3673 endpoint: &crate::streaming::StreamingEndpoint,
3674 analysis: &SchemaAnalysis,
3675 ) -> Result<String> {
3676 match &endpoint.event_flow {
3677 crate::streaming::EventFlow::Simple => {
3678 if analysis.schemas.contains_key(&endpoint.event_union_type) {
3681 Ok(endpoint.event_union_type.to_string())
3682 } else {
3683 Err(crate::error::GeneratorError::ValidationError(format!(
3684 "Streaming response type '{}' not found in schema for simple streaming endpoint '{}'",
3685 endpoint.event_union_type, endpoint.operation_id
3686 )))
3687 }
3688 }
3689 crate::streaming::EventFlow::StartDeltaStop { .. } => {
3690 if analysis.schemas.contains_key(&endpoint.event_union_type) {
3693 Ok(endpoint.event_union_type.to_string())
3694 } else {
3695 Err(crate::error::GeneratorError::ValidationError(format!(
3696 "Event union type '{}' not found in schema for complex streaming endpoint '{}'",
3697 endpoint.event_union_type, endpoint.operation_id
3698 )))
3699 }
3700 }
3701 }
3702 }
3703
3704 fn generate_streaming_error_types(&self) -> Result<TokenStream> {
3706 Ok(quote! {
3707 #[derive(Debug, thiserror::Error)]
3709 pub enum StreamingError {
3710 #[error("Connection error: {0}")]
3711 Connection(String),
3712 #[error("HTTP error: {status}")]
3713 Http { status: u16 },
3714 #[error("SSE parsing error: {0}")]
3715 Parsing(String),
3716 #[error("Authentication error: {0}")]
3717 Authentication(String),
3718 #[error("Rate limit error: {0}")]
3719 RateLimit(String),
3720 #[error("API error: {0}")]
3721 Api(String),
3722 #[error("Timeout error: {0}")]
3723 Timeout(String),
3724 #[error("Response body exceeded configured limit of {limit} bytes")]
3725 ResponseTooLarge { limit: usize },
3726 #[error("JSON serialization/deserialization error: {0}")]
3727 Json(#[from] serde_json::Error),
3728 #[error("Request error: {0}")]
3729 Request(reqwest::Error),
3730 }
3731
3732 impl From<reqwest::header::InvalidHeaderValue> for StreamingError {
3733 fn from(err: reqwest::header::InvalidHeaderValue) -> Self {
3734 StreamingError::Api(format!("Invalid header value: {}", err))
3735 }
3736 }
3737
3738 impl From<reqwest::Error> for StreamingError {
3739 fn from(err: reqwest::Error) -> Self {
3740 if err.is_timeout() {
3741 StreamingError::Timeout(err.to_string())
3742 } else if err.is_status() {
3743 if let Some(status) = err.status() {
3744 StreamingError::Http { status: status.as_u16() }
3745 } else {
3746 StreamingError::Connection(err.to_string())
3747 }
3748 } else {
3749 StreamingError::Request(err)
3750 }
3751 }
3752 }
3753 })
3754 }
3755
3756 fn generate_endpoint_trait(
3758 &self,
3759 endpoint: &crate::streaming::StreamingEndpoint,
3760 analysis: &SchemaAnalysis,
3761 ) -> Result<TokenStream> {
3762 use crate::streaming::HttpMethod;
3763
3764 let trait_name = format_ident!(
3765 "{}StreamingClient",
3766 self.to_rust_type_name(&endpoint.operation_id)
3767 );
3768 let method_name =
3769 format_ident!("stream_{}", self.to_rust_field_name(&endpoint.operation_id));
3770 let event_type =
3771 format_ident!("{}", self.resolve_streaming_event_type(endpoint, analysis)?);
3772
3773 let method_signature = match endpoint.http_method {
3775 HttpMethod::Get => {
3776 let mut param_defs = Vec::new();
3778 for qp in &endpoint.query_parameters {
3779 let param_name = format_ident!("{}", self.to_rust_field_name(&qp.name));
3780 if qp.required {
3781 param_defs.push(quote! { #param_name: &str });
3782 } else {
3783 param_defs.push(quote! { #param_name: Option<&str> });
3784 }
3785 }
3786 quote! {
3787 async fn #method_name(
3788 &self,
3789 #(#param_defs),*
3790 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error>;
3791 }
3792 }
3793 HttpMethod::Post => {
3794 let request_type = self
3796 .find_request_type_for_operation(&endpoint.operation_id, analysis)
3797 .unwrap_or_else(|| "serde_json::Value".to_string());
3798 let request_type_ident = if request_type.contains("::") {
3799 let parts: Vec<&str> = request_type.split("::").collect();
3800 let path_parts: Vec<_> = parts.iter().map(|p| format_ident!("{}", p)).collect();
3801 quote! { #(#path_parts)::* }
3802 } else {
3803 let ident = format_ident!("{}", request_type);
3804 quote! { #ident }
3805 };
3806 quote! {
3807 async fn #method_name(
3808 &self,
3809 request: #request_type_ident,
3810 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error>;
3811 }
3812 }
3813 };
3814
3815 Ok(quote! {
3816 #[async_trait]
3818 pub trait #trait_name {
3819 type Error: std::error::Error + Send + Sync + 'static;
3820
3821 #method_signature
3823 }
3824 })
3825 }
3826
3827 fn generate_streaming_client_impl(
3829 &self,
3830 streaming_config: &crate::streaming::StreamingConfig,
3831 analysis: &SchemaAnalysis,
3832 ) -> Result<TokenStream> {
3833 let client_name = format_ident!(
3834 "{}Client",
3835 self.to_rust_type_name(&streaming_config.client_module_name)
3836 );
3837
3838 let mut struct_fields = vec![
3841 quote! { base_url: String },
3842 quote! { api_key: Option<String> },
3843 quote! { sse_client: SseClient },
3844 quote! { custom_headers: std::collections::BTreeMap<String, String> },
3845 ];
3846
3847 let has_optional_headers = !streaming_config
3848 .endpoints
3849 .iter()
3850 .all(|e| e.optional_headers.is_empty());
3851
3852 if has_optional_headers {
3853 struct_fields
3854 .push(quote! { optional_headers: std::collections::BTreeMap<String, String> });
3855 }
3856
3857 let default_base_url = if let Some(ref streaming_config) = self.config.streaming_config {
3860 streaming_config
3861 .endpoints
3862 .first()
3863 .and_then(|e| e.base_url.as_deref())
3864 .unwrap_or("https://api.example.com")
3865 } else {
3866 "https://api.example.com"
3867 };
3868 let max_response_body_bytes = self
3869 .config()
3870 .http_client_config
3871 .as_ref()
3872 .and_then(|http| http.max_response_body_bytes)
3873 .unwrap_or(8 * 1024 * 1024);
3874 let sse_client_initializer = if let Some(reconnect) = &streaming_config.reconnection_config
3875 {
3876 let max_retries = reconnect.max_retries;
3877 let initial_delay_ms = reconnect.initial_delay_ms;
3878 let max_delay_ms = reconnect.max_delay_ms;
3879 let backoff_multiplier = reconnect.backoff_multiplier;
3880 quote! {
3881 SseClient::new()
3882 .with_max_error_body_bytes(#max_response_body_bytes)
3883 .with_reconnect_options(SseReconnectOptions {
3884 max_retries: #max_retries,
3885 initial_retry_delay: std::time::Duration::from_millis(#initial_delay_ms),
3886 max_retry_delay: std::time::Duration::from_millis(#max_delay_ms),
3887 backoff_multiplier: #backoff_multiplier,
3888 })
3889 }
3890 } else {
3891 quote! {
3892 SseClient::new()
3893 .with_max_error_body_bytes(#max_response_body_bytes)
3894 }
3895 };
3896
3897 let constructor_fields = if has_optional_headers {
3899 quote! {
3900 base_url: #default_base_url.to_string(),
3901 api_key: None,
3902 sse_client: #sse_client_initializer,
3903 custom_headers: std::collections::BTreeMap::new(),
3904 optional_headers: std::collections::BTreeMap::new(),
3905 }
3906 } else {
3907 quote! {
3908 base_url: #default_base_url.to_string(),
3909 api_key: None,
3910 sse_client: #sse_client_initializer,
3911 custom_headers: std::collections::BTreeMap::new(),
3912 }
3913 };
3914
3915 let optional_headers_method = if has_optional_headers {
3917 quote! {
3918 pub fn set_optional_headers(&mut self, headers: std::collections::BTreeMap<String, String>) {
3920 self.optional_headers = headers;
3921 }
3922 }
3923 } else {
3924 TokenStream::new()
3925 };
3926
3927 let constructor = quote! {
3928 impl #client_name {
3929 pub fn new() -> Self {
3931 Self {
3932 #constructor_fields
3933 }
3934 }
3935
3936 pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
3938 self.base_url = base_url.into();
3939 self
3940 }
3941
3942 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
3944 self.api_key = Some(api_key.into());
3945 self
3946 }
3947
3948 pub fn with_max_response_body_bytes(mut self, limit: usize) -> Self {
3950 self.sse_client = self.sse_client.with_max_error_body_bytes(limit);
3951 self
3952 }
3953
3954 pub fn with_header(
3956 mut self,
3957 name: impl Into<String>,
3958 value: impl Into<String>,
3959 ) -> Self {
3960 self.custom_headers.insert(name.into(), value.into());
3961 self
3962 }
3963
3964 pub fn with_http_client(mut self, client: reqwest::Client) -> Self {
3966 self.sse_client = self.sse_client.with_http_client(client);
3967 self
3968 }
3969
3970 #optional_headers_method
3971 }
3972 };
3973
3974 let mut trait_impls = Vec::new();
3976 for endpoint in &streaming_config.endpoints {
3977 let trait_impl = self.generate_endpoint_trait_impl(endpoint, &client_name, analysis)?;
3978 trait_impls.push(trait_impl);
3979 }
3980
3981 let default_impl = quote! {
3983 impl Default for #client_name {
3984 fn default() -> Self {
3985 Self::new()
3986 }
3987 }
3988 };
3989
3990 Ok(quote! {
3991 #[derive(Debug, Clone)]
3993 pub struct #client_name {
3994 #(#struct_fields,)*
3995 }
3996
3997 #constructor
3998
3999 #default_impl
4000
4001 #(#trait_impls)*
4002 })
4003 }
4004
4005 fn generate_endpoint_trait_impl(
4007 &self,
4008 endpoint: &crate::streaming::StreamingEndpoint,
4009 client_name: &proc_macro2::Ident,
4010 analysis: &SchemaAnalysis,
4011 ) -> Result<TokenStream> {
4012 use crate::streaming::HttpMethod;
4013
4014 let trait_name = format_ident!(
4015 "{}StreamingClient",
4016 self.to_rust_type_name(&endpoint.operation_id)
4017 );
4018 let method_name =
4019 format_ident!("stream_{}", self.to_rust_field_name(&endpoint.operation_id));
4020 let event_type =
4021 format_ident!("{}", self.resolve_streaming_event_type(endpoint, analysis)?);
4022
4023 let mut header_setup = Vec::new();
4025 for (name, value) in &endpoint.required_headers {
4026 header_setup.push(quote! {
4027 headers.insert(#name, HeaderValue::from_static(#value));
4028 });
4029 }
4030
4031 if let Some(auth_header) = &endpoint.auth_header {
4034 match auth_header {
4035 crate::streaming::AuthHeader::Bearer(header_name) => {
4036 header_setup.push(quote! {
4037 if let Some(ref api_key) = self.api_key {
4038 headers.insert(#header_name, HeaderValue::from_str(&format!("Bearer {}", api_key))?);
4039 }
4040 });
4041 }
4042 crate::streaming::AuthHeader::ApiKey(header_name) => {
4043 header_setup.push(quote! {
4044 if let Some(ref api_key) = self.api_key {
4045 headers.insert(#header_name, HeaderValue::from_str(api_key)?);
4046 }
4047 });
4048 }
4049 }
4050 } else {
4051 header_setup.push(quote! {
4053 if let Some(ref api_key) = self.api_key {
4054 headers.insert("Authorization", HeaderValue::from_str(&format!("Bearer {}", api_key))?);
4055 }
4056 });
4057 }
4058
4059 header_setup.push(quote! {
4061 for (name, value) in &self.custom_headers {
4062 if let (Ok(header_name), Ok(header_value)) = (reqwest::header::HeaderName::from_bytes(name.as_bytes()), HeaderValue::from_str(value)) {
4063 headers.insert(header_name, header_value);
4064 }
4065 }
4066 });
4067
4068 if !endpoint.optional_headers.is_empty() {
4070 header_setup.push(quote! {
4071 for (key, value) in &self.optional_headers {
4072 if let (Ok(header_name), Ok(header_value)) = (reqwest::header::HeaderName::from_bytes(key.as_bytes()), HeaderValue::from_str(value)) {
4073 headers.insert(header_name, header_value);
4074 }
4075 }
4076 });
4077 }
4078
4079 match endpoint.http_method {
4081 HttpMethod::Get => self.generate_get_streaming_impl(
4082 endpoint,
4083 client_name,
4084 &trait_name,
4085 &method_name,
4086 &event_type,
4087 &header_setup,
4088 ),
4089 HttpMethod::Post => self.generate_post_streaming_impl(
4090 endpoint,
4091 client_name,
4092 &trait_name,
4093 &method_name,
4094 &event_type,
4095 &header_setup,
4096 analysis,
4097 ),
4098 }
4099 }
4100
4101 fn generate_get_streaming_impl(
4103 &self,
4104 endpoint: &crate::streaming::StreamingEndpoint,
4105 client_name: &proc_macro2::Ident,
4106 trait_name: &proc_macro2::Ident,
4107 method_name: &proc_macro2::Ident,
4108 event_type: &proc_macro2::Ident,
4109 header_setup: &[TokenStream],
4110 ) -> Result<TokenStream> {
4111 let path = &endpoint.path;
4112
4113 let mut param_defs = Vec::new();
4115 let mut query_params = Vec::new();
4116
4117 for qp in &endpoint.query_parameters {
4118 let param_name = format_ident!("{}", self.to_rust_field_name(&qp.name));
4119 let param_name_str = &qp.name;
4120
4121 if qp.required {
4122 param_defs.push(quote! { #param_name: &str });
4123 query_params.push(quote! {
4124 url.query_pairs_mut().append_pair(#param_name_str, #param_name);
4125 });
4126 } else {
4127 param_defs.push(quote! { #param_name: Option<&str> });
4128 query_params.push(quote! {
4129 if let Some(v) = #param_name {
4130 url.query_pairs_mut().append_pair(#param_name_str, v);
4131 }
4132 });
4133 }
4134 }
4135
4136 let url_construction = quote! {
4138 let base_url = url::Url::parse(&self.base_url)
4139 .map_err(|e| StreamingError::Connection(format!("Invalid base URL: {}", e)))?;
4140 let path_to_join = #path.trim_start_matches('/');
4141 let mut url = base_url.join(path_to_join)
4142 .map_err(|e| StreamingError::Connection(format!("URL join error: {}", e)))?;
4143 #(#query_params)*
4144 };
4145
4146 let instrument_skip = quote! { #[instrument(skip(self), name = "streaming_get_request")] };
4147
4148 Ok(quote! {
4149 #[async_trait]
4150 impl #trait_name for #client_name {
4151 type Error = StreamingError;
4152
4153 #instrument_skip
4154 async fn #method_name(
4155 &self,
4156 #(#param_defs),*
4157 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error> {
4158 debug!("Starting streaming GET request");
4159
4160 let mut headers = HeaderMap::new();
4161 #(#header_setup)*
4162
4163 #url_construction
4164 let url_str = url.to_string();
4165 debug!("Making streaming GET request to: {}", url_str);
4166
4167 let request_builder = self.sse_client
4168 .get(url_str)
4169 .headers(headers);
4170
4171 debug!("Creating SSE stream from request");
4172 let stream = self
4173 .sse_client
4174 .stream::<#event_type>(request_builder)
4175 .await?;
4176 info!("SSE stream created successfully");
4177 Ok(stream)
4178 }
4179 }
4180 })
4181 }
4182
4183 #[allow(clippy::too_many_arguments)]
4185 fn generate_post_streaming_impl(
4186 &self,
4187 endpoint: &crate::streaming::StreamingEndpoint,
4188 client_name: &proc_macro2::Ident,
4189 trait_name: &proc_macro2::Ident,
4190 method_name: &proc_macro2::Ident,
4191 event_type: &proc_macro2::Ident,
4192 header_setup: &[TokenStream],
4193 analysis: &SchemaAnalysis,
4194 ) -> Result<TokenStream> {
4195 let path = &endpoint.path;
4196
4197 let request_type = self
4199 .find_request_type_for_operation(&endpoint.operation_id, analysis)
4200 .unwrap_or_else(|| "serde_json::Value".to_string());
4201 let request_type_ident = if request_type.contains("::") {
4202 let parts: Vec<&str> = request_type.split("::").collect();
4203 let path_parts: Vec<_> = parts.iter().map(|p| format_ident!("{}", p)).collect();
4204 quote! { #(#path_parts)::* }
4205 } else {
4206 let ident = format_ident!("{}", request_type);
4207 quote! { #ident }
4208 };
4209
4210 let url_construction = quote! {
4212 let base_url = url::Url::parse(&self.base_url)
4213 .map_err(|e| StreamingError::Connection(format!("Invalid base URL: {}", e)))?;
4214 let path_to_join = #path.trim_start_matches('/');
4215 let url = base_url.join(path_to_join)
4216 .map_err(|e| StreamingError::Connection(format!("URL join error: {}", e)))?
4217 .to_string();
4218 };
4219
4220 let stream_param = &endpoint.stream_parameter;
4222 let stream_setup = if stream_param.is_empty() {
4223 quote! {
4224 let streaming_request = request;
4225 }
4226 } else {
4227 quote! {
4228 let mut streaming_request = request;
4230 if let Ok(mut request_value) = serde_json::to_value(&streaming_request) {
4231 if let Some(obj) = request_value.as_object_mut() {
4232 obj.insert(#stream_param.to_string(), serde_json::Value::Bool(true));
4233 }
4234 streaming_request = serde_json::from_value(request_value)?;
4235 }
4236 }
4237 };
4238
4239 Ok(quote! {
4240 #[async_trait]
4241 impl #trait_name for #client_name {
4242 type Error = StreamingError;
4243
4244 #[instrument(skip(self, request), name = "streaming_post_request")]
4245 async fn #method_name(
4246 &self,
4247 request: #request_type_ident,
4248 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error> {
4249 debug!("Starting streaming POST request");
4250
4251 #stream_setup
4252
4253 let mut headers = HeaderMap::new();
4254 #(#header_setup)*
4255
4256 #url_construction
4257 debug!("Making streaming POST request to: {}", url);
4258
4259 let request_builder = self.sse_client
4260 .post(&url)
4261 .headers(headers)
4262 .json(&streaming_request);
4263
4264 debug!("Creating SSE stream from request");
4265 let stream = self
4266 .sse_client
4267 .stream::<#event_type>(request_builder)
4268 .await?;
4269 info!("SSE stream created successfully");
4270 Ok(stream)
4271 }
4272 }
4273 })
4274 }
4275
4276 fn generate_sse_runtime(&self) -> Result<String> {
4278 let provenance_attribute = self.provenance_attribute();
4279 let error_types = self.generate_streaming_error_types()?;
4280 let parser = self.generate_sse_parser_utilities()?;
4281 let tokens = quote! {
4282 #provenance_attribute
4286 #![allow(clippy::format_in_format_args)]
4287
4288 use futures_util::{Stream, StreamExt};
4289 use std::pin::Pin;
4290 use std::time::Duration;
4291 use tracing::debug;
4292
4293 #error_types
4294
4295 #[derive(Debug, Clone)]
4297 pub struct SseClient {
4298 http_client: reqwest::Client,
4299 max_error_body_bytes: usize,
4300 reconnect_options: Option<SseReconnectOptions>,
4301 }
4302
4303 impl SseClient {
4304 pub fn new() -> Self {
4305 Self {
4306 http_client: reqwest::Client::new(),
4307 max_error_body_bytes: DEFAULT_MAX_SSE_ERROR_BODY_BYTES,
4308 reconnect_options: None,
4309 }
4310 }
4311
4312 pub fn with_http_client(mut self, client: reqwest::Client) -> Self {
4313 self.http_client = client;
4314 self
4315 }
4316
4317 pub fn with_max_error_body_bytes(mut self, limit: usize) -> Self {
4318 self.max_error_body_bytes = limit;
4319 self
4320 }
4321
4322 pub fn with_reconnect_options(mut self, options: SseReconnectOptions) -> Self {
4324 self.reconnect_options = Some(options);
4325 self
4326 }
4327
4328 pub fn get(&self, url: impl reqwest::IntoUrl) -> reqwest::RequestBuilder {
4329 self.http_client.get(url)
4330 }
4331
4332 pub fn post(&self, url: impl reqwest::IntoUrl) -> reqwest::RequestBuilder {
4333 self.http_client.post(url)
4334 }
4335
4336 pub async fn stream<T>(
4337 &self,
4338 request_builder: reqwest::RequestBuilder,
4339 ) -> Result<Pin<Box<dyn Stream<Item = Result<T, StreamingError>> + Send>>, StreamingError>
4340 where
4341 T: serde::de::DeserializeOwned + Send + 'static,
4342 {
4343 if let Some(options) = self.reconnect_options.clone() {
4344 parse_sse_json_reconnecting_with_limit(
4345 request_builder,
4346 self.max_error_body_bytes,
4347 options,
4348 ).await
4349 } else {
4350 parse_sse_json_stream_with_limit(
4351 request_builder,
4352 self.max_error_body_bytes,
4353 ).await
4354 }
4355 }
4356
4357 pub async fn stream_raw(
4359 &self,
4360 request_builder: reqwest::RequestBuilder,
4361 ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<String>, StreamingError>> + Send>>, StreamingError> {
4362 parse_sse_raw_stream_with_limit(request_builder, self.max_error_body_bytes).await
4363 }
4364
4365 pub async fn stream_json<T>(
4367 &self,
4368 request_builder: reqwest::RequestBuilder,
4369 ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<T>, StreamingError>> + Send>>, StreamingError>
4370 where
4371 T: serde::de::DeserializeOwned + Send + 'static,
4372 {
4373 parse_sse_json_events_with_limit(request_builder, self.max_error_body_bytes).await
4374 }
4375
4376 pub async fn stream_raw_reconnecting(
4378 &self,
4379 request_builder: reqwest::RequestBuilder,
4380 ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<String>, StreamingError>> + Send>>, StreamingError> {
4381 parse_sse_raw_reconnecting_with_limit(
4382 request_builder,
4383 self.max_error_body_bytes,
4384 self.reconnect_options.clone().unwrap_or_default(),
4385 ).await
4386 }
4387
4388 pub async fn stream_json_reconnecting<T>(
4390 &self,
4391 request_builder: reqwest::RequestBuilder,
4392 ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<T>, StreamingError>> + Send>>, StreamingError>
4393 where
4394 T: serde::de::DeserializeOwned + Send + 'static,
4395 {
4396 parse_sse_json_reconnecting_events_with_limit(
4397 request_builder,
4398 self.max_error_body_bytes,
4399 self.reconnect_options.clone().unwrap_or_default(),
4400 ).await
4401 }
4402 }
4403
4404 impl Default for SseClient {
4405 fn default() -> Self {
4406 Self::new()
4407 }
4408 }
4409
4410 #parser
4411 };
4412 let syntax_tree = syn::parse2::<syn::File>(tokens).map_err(|error| {
4413 GeneratorError::CodeGenError(format!("Failed to parse generated sse.rs: {error}"))
4414 })?;
4415 Ok(prettyplease::unparse(&syntax_tree))
4416 }
4417
4418 fn generate_sse_parser_utilities(&self) -> Result<TokenStream> {
4420 Ok(quote! {
4421 pub const DEFAULT_MAX_SSE_ERROR_BODY_BYTES: usize = 8 * 1024 * 1024;
4423
4424 async fn __read_bounded_streaming_error_body(
4425 mut response: reqwest::Response,
4426 limit: usize,
4427 ) -> Result<Vec<u8>, StreamingError> {
4428 let mut body = Vec::new();
4429 while let Some(chunk) = response.chunk().await? {
4430 let next_len = body.len().checked_add(chunk.len());
4431 if next_len.is_none_or(|next_len| next_len > limit) {
4432 return Err(StreamingError::ResponseTooLarge { limit });
4433 }
4434 body.extend_from_slice(&chunk);
4435 }
4436 Ok(body)
4437 }
4438
4439 #[derive(Debug, Clone, PartialEq, Eq)]
4441 pub struct SseEvent<T> {
4442 pub event: String,
4444 pub data: T,
4446 pub id: Option<String>,
4448 pub retry: Option<Duration>,
4450 }
4451
4452 #[derive(Debug, Clone)]
4454 pub struct SseReconnectOptions {
4455 pub max_retries: u32,
4457 pub initial_retry_delay: Duration,
4459 pub max_retry_delay: Duration,
4461 pub backoff_multiplier: f64,
4463 }
4464
4465 impl Default for SseReconnectOptions {
4466 fn default() -> Self {
4467 Self {
4468 max_retries: 3,
4469 initial_retry_delay: Duration::from_secs(3),
4470 max_retry_delay: Duration::from_secs(30),
4471 backoff_multiplier: 2.0,
4472 }
4473 }
4474 }
4475
4476 impl SseReconnectOptions {
4477 fn delay(&self, attempt: u32, server_retry: Option<Duration>) -> Duration {
4478 if let Some(delay) = server_retry {
4479 return delay.min(self.max_retry_delay);
4480 }
4481 let multiplier = self.backoff_multiplier.max(1.0);
4482 let millis = self.initial_retry_delay.as_millis() as f64
4483 * multiplier.powi(attempt.min(63) as i32);
4484 Duration::from_millis(
4485 millis.min(self.max_retry_delay.as_millis() as f64) as u64,
4486 )
4487 }
4488 }
4489
4490 #[derive(Default)]
4491 struct __SseDecoder {
4492 line: Vec<u8>,
4493 event: String,
4494 data: Vec<String>,
4495 last_event_id: Option<String>,
4496 retry_delay: Option<Duration>,
4497 event_retry: Option<Duration>,
4498 saw_carriage_return: bool,
4499 }
4500
4501 impl __SseDecoder {
4502 fn feed(
4503 &mut self,
4504 chunk: &[u8],
4505 ) -> Vec<Result<SseEvent<String>, StreamingError>> {
4506 let mut messages = Vec::new();
4507 for &byte in chunk {
4508 if self.saw_carriage_return {
4509 self.saw_carriage_return = false;
4510 if byte == b'\n' {
4511 continue;
4512 }
4513 }
4514
4515 match byte {
4516 b'\n' => self.finish_line(&mut messages),
4517 b'\r' => {
4518 self.finish_line(&mut messages);
4519 self.saw_carriage_return = true;
4520 }
4521 _ => self.line.push(byte),
4522 }
4523 }
4524 messages
4525 }
4526
4527 fn finish(&mut self) -> Vec<Result<SseEvent<String>, StreamingError>> {
4528 let mut messages = Vec::new();
4529 if !self.line.is_empty() {
4530 self.finish_line(&mut messages);
4531 }
4532 self.dispatch(&mut messages);
4533 messages
4534 }
4535
4536 fn finish_line(
4537 &mut self,
4538 messages: &mut Vec<Result<SseEvent<String>, StreamingError>>,
4539 ) {
4540 let line = std::mem::take(&mut self.line);
4541 let line = match String::from_utf8(line) {
4542 Ok(line) => line,
4543 Err(error) => {
4544 messages.push(Err(StreamingError::Parsing(format!(
4545 "SSE line is not valid UTF-8: {}",
4546 error
4547 ))));
4548 return;
4549 }
4550 };
4551
4552 if line.is_empty() {
4553 self.dispatch(messages);
4554 return;
4555 }
4556 if line.starts_with(':') {
4557 return;
4558 }
4559
4560 let (field, value) = line
4561 .split_once(':')
4562 .map_or((line.as_str(), ""), |(field, value)| {
4563 (field, value.strip_prefix(' ').unwrap_or(value))
4564 });
4565 match field {
4566 "event" => self.event = value.to_string(),
4567 "data" => self.data.push(value.to_string()),
4568 "id" if !value.contains('\0') => {
4569 self.last_event_id = (!value.is_empty()).then(|| value.to_string());
4570 }
4571 "retry" if value.bytes().all(|byte| byte.is_ascii_digit()) => {
4572 if let Ok(milliseconds) = value.parse::<u64>() {
4573 let delay = Duration::from_millis(milliseconds);
4574 self.retry_delay = Some(delay);
4575 self.event_retry = Some(delay);
4576 }
4577 }
4578 _ => {}
4579 }
4580 }
4581
4582 fn dispatch(
4583 &mut self,
4584 messages: &mut Vec<Result<SseEvent<String>, StreamingError>>,
4585 ) {
4586 if self.data.is_empty() {
4587 self.event.clear();
4588 self.event_retry = None;
4589 return;
4590 }
4591 messages.push(Ok(SseEvent {
4592 event: if self.event.is_empty() {
4593 "message".to_string()
4594 } else {
4595 std::mem::take(&mut self.event)
4596 },
4597 data: std::mem::take(&mut self.data).join("\n"),
4598 id: self.last_event_id.clone(),
4599 retry: self.event_retry.take(),
4600 }));
4601 self.event.clear();
4602 }
4603
4604 fn reset_for_reconnect(&mut self) {
4605 self.line.clear();
4606 self.event.clear();
4607 self.data.clear();
4608 self.event_retry = None;
4609 self.saw_carriage_return = false;
4610 }
4611 }
4612
4613 fn __deserialize_sse_event<T>(
4614 event: SseEvent<String>,
4615 ) -> Option<Result<SseEvent<T>, StreamingError>>
4616 where
4617 T: serde::de::DeserializeOwned,
4618 {
4619 if event.data.trim() == "[DONE]" {
4620 return None;
4621 }
4622 if event.event == "ping" {
4623 debug!("Received SSE ping event, skipping");
4624 return None;
4625 }
4626 if event.data.trim().is_empty() {
4627 debug!("Empty SSE data, skipping");
4628 return None;
4629 }
4630
4631 let json_value = match serde_json::from_str::<serde_json::Value>(&event.data) {
4632 Ok(value) => value,
4633 Err(error) => {
4634 return Some(Err(StreamingError::Parsing(format!(
4635 "SSE event is not valid JSON: {} ({})",
4636 event.data, error
4637 ))));
4638 }
4639 };
4640 let is_ping = json_value
4641 .get("event")
4642 .or_else(|| json_value.get("type"))
4643 .and_then(serde_json::Value::as_str)
4644 .is_some_and(|event| event == "ping");
4645 if is_ping {
4646 debug!("Received ping event in JSON data, skipping");
4647 return None;
4648 }
4649
4650 Some(
4651 serde_json::from_value::<T>(json_value)
4652 .map(|data| SseEvent {
4653 event: event.event.clone(),
4654 data,
4655 id: event.id.clone(),
4656 retry: event.retry,
4657 })
4658 .map_err(|error| StreamingError::Parsing(format!(
4659 "Failed to parse SSE event: {} (raw: {}, event: {})",
4660 error, event.data, event.event
4661 ))),
4662 )
4663 }
4664
4665 pub async fn parse_sse_stream<T>(
4667 request_builder: reqwest::RequestBuilder
4668 ) -> Result<Pin<Box<dyn Stream<Item = Result<T, StreamingError>> + Send>>, StreamingError>
4669 where
4670 T: serde::de::DeserializeOwned + Send + 'static,
4671 {
4672 parse_sse_json_stream_with_limit(
4673 request_builder,
4674 DEFAULT_MAX_SSE_ERROR_BODY_BYTES,
4675 ).await
4676 }
4677
4678 struct __SseOpenError {
4679 error: StreamingError,
4680 retryable: bool,
4681 }
4682
4683 async fn __open_sse_response(
4684 request_builder: reqwest::RequestBuilder,
4685 max_response_body_bytes: usize,
4686 ) -> Result<reqwest::Response, __SseOpenError> {
4687 let response = request_builder.send().await.map_err(|error| __SseOpenError {
4688 error: error.into(),
4689 retryable: true,
4690 })?;
4691 if !response.status().is_success() {
4692 let status = response.status();
4693 let retryable = status.as_u16() == 429 || status.is_server_error();
4694 let error = match __read_bounded_streaming_error_body(
4695 response,
4696 max_response_body_bytes,
4697 ).await {
4698 Ok(body) => StreamingError::Connection(format!(
4699 "HTTP {} error: {}",
4700 status.as_u16(),
4701 String::from_utf8_lossy(&body)
4702 )),
4703 Err(error) => error,
4704 };
4705 return Err(__SseOpenError { error, retryable });
4706 }
4707
4708 let content_type = response
4709 .headers()
4710 .get(reqwest::header::CONTENT_TYPE)
4711 .and_then(|value| value.to_str().ok())
4712 .unwrap_or_default();
4713 if !content_type
4714 .split(';')
4715 .next()
4716 .is_some_and(|value| value.trim().eq_ignore_ascii_case("text/event-stream"))
4717 {
4718 let error = StreamingError::Parsing(format!(
4719 "Expected text/event-stream response, received {}",
4720 if content_type.is_empty() { "no Content-Type" } else { content_type }
4721 ));
4722 return Err(__SseOpenError { error, retryable: false });
4723 }
4724
4725 debug!("SSE connection opened");
4726 Ok(response)
4727 }
4728
4729 fn __raw_response_stream(
4730 response: reqwest::Response,
4731 ) -> Pin<Box<dyn Stream<Item = Result<SseEvent<String>, StreamingError>> + Send>> {
4732 let stream = futures_util::stream::unfold(
4733 (
4734 response,
4735 __SseDecoder::default(),
4736 std::collections::VecDeque::<Result<SseEvent<String>, StreamingError>>::new(),
4737 false,
4738 ),
4739 |(mut response, mut decoder, mut pending, mut done)| async move {
4740 loop {
4741 if let Some(item) = pending.pop_front() {
4742 return Some((item, (response, decoder, pending, done)));
4743 }
4744 if done {
4745 debug!("SSE stream completed normally");
4746 return None;
4747 }
4748
4749 match response.chunk().await {
4750 Ok(Some(chunk)) => {
4751 for event in decoder.feed(&chunk) {
4752 let is_done = event
4753 .as_ref()
4754 .is_ok_and(|event| event.data.trim() == "[DONE]");
4755 pending.push_back(event);
4756 if is_done {
4757 done = true;
4758 break;
4759 }
4760 }
4761 }
4762 Err(error) => {
4763 done = true;
4764 pending.push_back(Err(error.into()));
4765 }
4766 Ok(None) => {
4767 done = true;
4768 for event in decoder.finish() {
4769 pending.push_back(event);
4770 }
4771 }
4772 }
4773 }
4774 }
4775 );
4776
4777 Box::pin(stream)
4778 }
4779
4780 async fn parse_sse_raw_stream_with_limit(
4781 request_builder: reqwest::RequestBuilder,
4782 max_response_body_bytes: usize,
4783 ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<String>, StreamingError>> + Send>>, StreamingError> {
4784 Ok(match __open_sse_response(request_builder, max_response_body_bytes).await {
4785 Ok(response) => __raw_response_stream(response),
4786 Err(error) => Box::pin(futures_util::stream::once(async move { Err(error.error) })),
4787 })
4788 }
4789
4790 fn __json_event_stream<T>(
4791 raw: Pin<Box<dyn Stream<Item = Result<SseEvent<String>, StreamingError>> + Send>>,
4792 ) -> Pin<Box<dyn Stream<Item = Result<SseEvent<T>, StreamingError>> + Send>>
4793 where
4794 T: serde::de::DeserializeOwned + Send + 'static,
4795 {
4796 Box::pin(raw.filter_map(|event| async move {
4797 match event {
4798 Ok(event) => __deserialize_sse_event(event),
4799 Err(error) => Some(Err(error)),
4800 }
4801 }))
4802 }
4803
4804 async fn parse_sse_json_events_with_limit<T>(
4805 request_builder: reqwest::RequestBuilder,
4806 max_response_body_bytes: usize,
4807 ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<T>, StreamingError>> + Send>>, StreamingError>
4808 where
4809 T: serde::de::DeserializeOwned + Send + 'static,
4810 {
4811 Ok(__json_event_stream(
4812 parse_sse_raw_stream_with_limit(request_builder, max_response_body_bytes).await?,
4813 ))
4814 }
4815
4816 async fn parse_sse_json_stream_with_limit<T>(
4817 request_builder: reqwest::RequestBuilder,
4818 max_response_body_bytes: usize,
4819 ) -> Result<Pin<Box<dyn Stream<Item = Result<T, StreamingError>> + Send>>, StreamingError>
4820 where
4821 T: serde::de::DeserializeOwned + Send + 'static,
4822 {
4823 let events = parse_sse_json_events_with_limit(request_builder, max_response_body_bytes).await?;
4824 Ok(Box::pin(events.map(|event| event.map(|event| event.data))))
4825 }
4826
4827 struct __ReconnectState {
4828 request: reqwest::RequestBuilder,
4829 response: Option<reqwest::Response>,
4830 decoder: __SseDecoder,
4831 pending: std::collections::VecDeque<Result<SseEvent<String>, StreamingError>>,
4832 options: SseReconnectOptions,
4833 max_response_body_bytes: usize,
4834 attempts: u32,
4835 wait_before_open: bool,
4836 done: bool,
4837 }
4838
4839 async fn parse_sse_raw_reconnecting_with_limit(
4840 request_builder: reqwest::RequestBuilder,
4841 max_response_body_bytes: usize,
4842 options: SseReconnectOptions,
4843 ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<String>, StreamingError>> + Send>>, StreamingError> {
4844 if request_builder.try_clone().is_none() {
4845 return Err(StreamingError::Connection(
4846 "SSE reconnection requires a cloneable request body".to_string(),
4847 ));
4848 }
4849
4850 let stream = futures_util::stream::unfold(
4851 __ReconnectState {
4852 request: request_builder,
4853 response: None,
4854 decoder: __SseDecoder::default(),
4855 pending: std::collections::VecDeque::new(),
4856 options,
4857 max_response_body_bytes,
4858 attempts: 0,
4859 wait_before_open: false,
4860 done: false,
4861 },
4862 |mut state| async move {
4863 loop {
4864 if let Some(item) = state.pending.pop_front() {
4865 return Some((item, state));
4866 }
4867 if state.done {
4868 return None;
4869 }
4870
4871 if state.response.is_none() {
4872 if state.wait_before_open {
4873 let delay = state.options.delay(
4874 state.attempts.saturating_sub(1),
4875 state.decoder.retry_delay,
4876 );
4877 debug!(?delay, attempt = state.attempts, "Reconnecting SSE stream");
4878 futures_timer::Delay::new(delay).await;
4879 state.wait_before_open = false;
4880 }
4881
4882 let mut request = state.request.try_clone().expect("request clone checked");
4883 if let Some(last_event_id) = state.decoder.last_event_id.as_deref() {
4884 request = request.header("Last-Event-ID", last_event_id);
4885 }
4886 match __open_sse_response(request, state.max_response_body_bytes).await {
4887 Ok(response) => state.response = Some(response),
4888 Err(error) if error.retryable && state.attempts < state.options.max_retries => {
4889 state.attempts += 1;
4890 state.wait_before_open = true;
4891 continue;
4892 }
4893 Err(error) => {
4894 state.done = true;
4895 state.pending.push_back(Err(error.error));
4896 continue;
4897 }
4898 }
4899 }
4900
4901 let next = state.response.as_mut().expect("response opened").chunk().await;
4902 match next {
4903 Ok(Some(chunk)) => {
4904 let events = state.decoder.feed(&chunk);
4905 if !events.is_empty() {
4906 state.attempts = 0;
4907 }
4908 for event in events {
4909 let is_done = event
4910 .as_ref()
4911 .is_ok_and(|event| event.data.trim() == "[DONE]");
4912 state.pending.push_back(event);
4913 if is_done {
4914 state.done = true;
4915 state.response = None;
4916 break;
4917 }
4918 }
4919 }
4920 Ok(None) => {
4921 let events = state.decoder.finish();
4922 if !events.is_empty() {
4923 state.attempts = 0;
4924 }
4925 for event in events {
4926 let is_done = event
4927 .as_ref()
4928 .is_ok_and(|event| event.data.trim() == "[DONE]");
4929 state.pending.push_back(event);
4930 if is_done {
4931 state.done = true;
4932 break;
4933 }
4934 }
4935 state.response = None;
4936 state.decoder.reset_for_reconnect();
4937 if !state.done {
4938 if state.attempts < state.options.max_retries {
4939 state.attempts += 1;
4940 state.wait_before_open = true;
4941 } else {
4942 state.done = true;
4943 }
4944 }
4945 }
4946 Err(error) => {
4947 state.response = None;
4948 state.decoder.reset_for_reconnect();
4949 if state.attempts < state.options.max_retries {
4950 state.attempts += 1;
4951 state.wait_before_open = true;
4952 } else {
4953 state.done = true;
4954 state.pending.push_back(Err(error.into()));
4955 }
4956 }
4957 }
4958 }
4959 },
4960 );
4961 Ok(Box::pin(stream))
4962 }
4963
4964 async fn parse_sse_json_reconnecting_events_with_limit<T>(
4965 request_builder: reqwest::RequestBuilder,
4966 max_response_body_bytes: usize,
4967 options: SseReconnectOptions,
4968 ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<T>, StreamingError>> + Send>>, StreamingError>
4969 where
4970 T: serde::de::DeserializeOwned + Send + 'static,
4971 {
4972 Ok(__json_event_stream(
4973 parse_sse_raw_reconnecting_with_limit(
4974 request_builder,
4975 max_response_body_bytes,
4976 options,
4977 ).await?,
4978 ))
4979 }
4980
4981 async fn parse_sse_json_reconnecting_with_limit<T>(
4982 request_builder: reqwest::RequestBuilder,
4983 max_response_body_bytes: usize,
4984 options: SseReconnectOptions,
4985 ) -> Result<Pin<Box<dyn Stream<Item = Result<T, StreamingError>> + Send>>, StreamingError>
4986 where
4987 T: serde::de::DeserializeOwned + Send + 'static,
4988 {
4989 let events = parse_sse_json_reconnecting_events_with_limit(
4990 request_builder,
4991 max_response_body_bytes,
4992 options,
4993 ).await?;
4994 Ok(Box::pin(events.map(|event| event.map(|event| event.data))))
4995 }
4996 })
4997 }
4998
4999 fn generate_reconnection_utilities(
5001 &self,
5002 reconnect_config: &crate::streaming::ReconnectionConfig,
5003 ) -> Result<TokenStream> {
5004 let max_retries = reconnect_config.max_retries;
5005 let initial_delay = reconnect_config.initial_delay_ms;
5006 let max_delay = reconnect_config.max_delay_ms;
5007 let backoff_multiplier = reconnect_config.backoff_multiplier;
5008
5009 Ok(quote! {
5010 #[derive(Debug, Clone)]
5012 pub struct ReconnectionManager {
5013 max_retries: u32,
5014 initial_delay_ms: u64,
5015 max_delay_ms: u64,
5016 backoff_multiplier: f64,
5017 current_attempt: u32,
5018 }
5019
5020 impl ReconnectionManager {
5021 pub fn new() -> Self {
5023 Self {
5024 max_retries: #max_retries,
5025 initial_delay_ms: #initial_delay,
5026 max_delay_ms: #max_delay,
5027 backoff_multiplier: #backoff_multiplier,
5028 current_attempt: 0,
5029 }
5030 }
5031
5032 pub fn should_retry(&self) -> bool {
5034 self.current_attempt < self.max_retries
5035 }
5036
5037 pub fn next_retry_delay(&mut self) -> Duration {
5039 if !self.should_retry() {
5040 return Duration::from_secs(0);
5041 }
5042
5043 let delay_ms = (self.initial_delay_ms as f64
5044 * self.backoff_multiplier.powi(self.current_attempt as i32)) as u64;
5045 let delay_ms = delay_ms.min(self.max_delay_ms);
5046
5047 self.current_attempt += 1;
5048 Duration::from_millis(delay_ms)
5049 }
5050
5051 pub fn reset(&mut self) {
5053 self.current_attempt = 0;
5054 }
5055
5056 pub fn current_attempt(&self) -> u32 {
5058 self.current_attempt
5059 }
5060 }
5061
5062 impl Default for ReconnectionManager {
5063 fn default() -> Self {
5064 Self::new()
5065 }
5066 }
5067 })
5068 }
5069}