1use crate::{
2 GeneratorError, Result,
3 analysis::{SchemaAnalysis, SchemaType},
4 streaming::StreamingConfig,
5};
6use proc_macro2::TokenStream;
7use quote::{format_ident, quote};
8use std::collections::BTreeMap;
9use std::path::PathBuf;
10
11fn parse_rust_type(rust_type: &str) -> Result<TokenStream> {
20 let parsed: syn::Type = syn::parse_str(rust_type).map_err(|e| {
21 GeneratorError::CodeGenError(format!(
22 "TypeMapper produced un-parseable type `{rust_type}`: {e}"
23 ))
24 })?;
25 Ok(quote! { #parsed })
26}
27
28fn format_constraints_doc(c: &crate::analysis::PropertyConstraints) -> String {
37 let mut parts: Vec<String> = Vec::new();
38
39 if let Some(v) = c.minimum {
40 parts.push(format!("minimum={}", strip_trailing_zero(v)));
41 }
42 if let Some(v) = c.maximum {
43 parts.push(format!("maximum={}", strip_trailing_zero(v)));
44 }
45 if let Some(v) = c.exclusive_minimum {
46 parts.push(format!("exclusiveMinimum={}", strip_trailing_zero(v)));
47 }
48 if let Some(v) = c.exclusive_maximum {
49 parts.push(format!("exclusiveMaximum={}", strip_trailing_zero(v)));
50 }
51 if let Some(v) = c.multiple_of {
52 parts.push(format!("multipleOf={}", strip_trailing_zero(v)));
53 }
54 if let Some(v) = c.min_length {
55 parts.push(format!("minLength={v}"));
56 }
57 if let Some(v) = c.max_length {
58 parts.push(format!("maxLength={v}"));
59 }
60 if let Some(v) = c.min_items {
61 parts.push(format!("minItems={v}"));
62 }
63 if let Some(v) = c.max_items {
64 parts.push(format!("maxItems={v}"));
65 }
66 if c.unique_items == Some(true) {
67 parts.push("uniqueItems=true".to_string());
68 }
69 if let Some(p) = &c.pattern {
70 let safe = p.replace("///", "/\u{200B}//").replace("*/", "*\u{200B}/");
75 parts.push(format!("pattern=`{safe}`"));
76 }
77
78 format!("Constraint: {}", parts.join(", "))
79}
80
81fn strip_trailing_zero(v: f64) -> String {
84 if v.fract() == 0.0 && v.is_finite() {
85 format!("{}", v as i64)
86 } else {
87 format!("{v}")
88 }
89}
90
91pub(crate) struct EmittedObjectProperty<'a> {
95 pub(crate) wire_name: &'a str,
96 pub(crate) property: &'a crate::analysis::PropertyInfo,
97 pub(crate) ident: syn::Ident,
98 pub(crate) is_required: bool,
99 pub(crate) field_type: TokenStream,
100}
101
102struct TypeGenerationIndex {
106 request_body_roots: std::collections::HashSet<String>,
107 reserved_type_names: std::collections::HashSet<String>,
108}
109
110struct TypeGenerationContext<'a> {
111 index: &'a TypeGenerationIndex,
112 enum_wire_names: Option<&'a std::cell::RefCell<EnumWireNames>>,
113}
114
115type EnumWireNames = BTreeMap<String, BTreeMap<String, Option<String>>>;
116
117#[derive(Debug, Clone)]
118pub struct GeneratorConfig {
119 pub spec_path: PathBuf,
121 pub output_dir: PathBuf,
123 pub module_name: String,
130 pub enable_sse_client: bool,
132 pub enable_async_client: bool,
134 pub enable_specta: bool,
136 pub type_mappings: BTreeMap<String, String>,
138 pub streaming_config: Option<StreamingConfig>,
140 pub nullable_field_overrides: BTreeMap<String, bool>,
143 pub extensible_enum_overrides: BTreeMap<String, bool>,
150 pub schema_extensions: Vec<PathBuf>,
153 pub overlays: Vec<PathBuf>,
155 pub effective_spec: Option<PathBuf>,
157 pub bindings_metadata: Option<String>,
159 pub http_client_config: Option<crate::http_config::HttpClientConfig>,
161 pub retry_config: Option<crate::http_config::RetryConfig>,
163 pub tracing_enabled: bool,
165 pub auth_config: Option<crate::http_config::AuthConfig>,
167 pub enable_registry: bool,
169 pub registry_only: bool,
171 pub types: crate::type_mapping::TypeMappingConfig,
175 pub builders: crate::config::BuildersSection,
177 pub server: Option<crate::config::ServerSection>,
180 pub client: Option<crate::config::ClientSection>,
183}
184
185impl Default for GeneratorConfig {
186 fn default() -> Self {
187 Self {
188 spec_path: "openapi.json".into(),
189 output_dir: "src/gen".into(),
190 module_name: "api_types".to_string(),
191 enable_sse_client: true,
192 enable_async_client: true,
193 enable_specta: false,
194 type_mappings: default_type_mappings(),
195 streaming_config: None,
196 nullable_field_overrides: BTreeMap::new(),
197 extensible_enum_overrides: BTreeMap::new(),
198 schema_extensions: Vec::new(),
199 overlays: Vec::new(),
200 effective_spec: None,
201 bindings_metadata: None,
202 http_client_config: None,
203 retry_config: None,
204 tracing_enabled: true,
205 auth_config: None,
206 enable_registry: false,
207 registry_only: false,
208 types: crate::type_mapping::TypeMappingConfig::default(),
209 builders: crate::config::BuildersSection::default(),
210 server: None,
211 client: None,
212 }
213 }
214}
215
216impl GeneratorConfig {
217 pub fn apply_spec_server_default(&mut self, spec: &serde_json::Value) {
228 let already_configured = self
229 .http_client_config
230 .as_ref()
231 .and_then(|http| http.base_url.as_deref())
232 .is_some_and(|url| !url.is_empty());
233 if already_configured {
234 return;
235 }
236
237 let Some(url) = spec
238 .pointer("/servers/0/url")
239 .and_then(serde_json::Value::as_str)
240 .map(str::trim)
241 .filter(|url| !url.is_empty())
242 .filter(|url| !url.starts_with('/'))
243 .filter(|url| !url.contains('{'))
244 else {
245 return;
246 };
247
248 match self.http_client_config.as_mut() {
249 Some(http) => http.base_url = Some(url.to_string()),
250 None => {
251 self.http_client_config = Some(crate::http_config::HttpClientConfig {
252 base_url: Some(url.to_string()),
253 timeout_seconds: None,
254 max_response_body_bytes: None,
255 default_headers: Default::default(),
256 })
257 }
258 }
259 }
260}
261
262pub fn default_type_mappings() -> BTreeMap<String, String> {
263 let mut mappings = BTreeMap::new();
264 mappings.insert("integer".to_string(), "i64".to_string());
265 mappings.insert("number".to_string(), "f64".to_string());
266 mappings.insert("string".to_string(), "String".to_string());
267 mappings.insert("boolean".to_string(), "bool".to_string());
268 mappings
269}
270
271pub(crate) fn rust_type_name(s: &str) -> String {
277 let mut result = String::new();
278 let mut next_upper = true;
279
280 for c in s.chars() {
281 match c {
282 'a'..='z' => {
283 result.push(if next_upper {
284 c.to_ascii_uppercase()
285 } else {
286 c
287 });
288 next_upper = false;
289 }
290 'A'..='Z' | '0'..='9' => {
291 result.push(c);
292 next_upper = false;
293 }
294 _ => next_upper = true,
295 }
296 }
297
298 if result.is_empty() {
299 result = "Type".to_string();
300 }
301 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
302 result = format!("Type{result}");
303 }
304 if matches!(
305 result.as_str(),
306 "Result"
307 | "Option"
308 | "Box"
309 | "Vec"
310 | "String"
311 | "Some"
312 | "None"
313 | "Ok"
314 | "Err"
315 | "Default"
316 | "Clone"
317 | "Debug"
318 | "Send"
319 | "Sync"
320 | "Sized"
321 | "Iterator"
322 | "From"
323 | "Into"
324 | "TryFrom"
325 | "TryInto"
326 | "AsRef"
327 | "AsMut"
328 ) {
329 result.push_str("Type");
330 }
331 result
332}
333
334#[derive(Debug, Clone)]
336pub struct GeneratedFile {
337 pub path: PathBuf,
339 pub content: String,
341}
342
343#[derive(Debug, Clone)]
345pub struct GenerationResult {
346 pub files: Vec<GeneratedFile>,
348 pub mod_file: GeneratedFile,
350 pub required_deps: Vec<crate::type_mapping::DepRequirement>,
354 pub pruned_schemas: usize,
356}
357
358#[derive(Debug)]
359struct OperationScopes {
360 client_ids: Option<std::collections::BTreeSet<String>>,
362 server_ids: std::collections::BTreeSet<String>,
363 streaming_ids: std::collections::BTreeSet<String>,
364 prune_models: bool,
365 extra_schema_roots: Vec<String>,
366}
367
368pub struct CodeGenerator {
369 config: GeneratorConfig,
370 source_provenance: Option<String>,
371}
372
373struct ObjectShape<'a> {
376 properties: &'a BTreeMap<String, crate::analysis::PropertyInfo>,
377 required: &'a std::collections::HashSet<String>,
378 additional_properties: &'a crate::analysis::ObjectAdditionalProperties,
379 variant: Option<&'a crate::analysis::SchemaRef>,
381}
382
383fn untyped_rust_type(shape: crate::analysis::UntypedShape) -> &'static str {
385 use crate::analysis::UntypedShape;
386 match shape {
387 UntypedShape::Value => "serde_json::Value",
388 UntypedShape::ValueArray => "Vec<serde_json::Value>",
389 UntypedShape::ValueMap => "std::collections::BTreeMap<String, serde_json::Value>",
390 }
391}
392
393fn untyped_tokens(shape: crate::analysis::UntypedShape) -> TokenStream {
394 use crate::analysis::UntypedShape;
395 match shape {
396 UntypedShape::Value => quote! { serde_json::Value },
397 UntypedShape::ValueArray => quote! { Vec<serde_json::Value> },
398 UntypedShape::ValueMap => quote! { std::collections::BTreeMap<String, serde_json::Value> },
399 }
400}
401
402fn schema_type_uses_serde_codec(schema_type: &SchemaType, codec: &str) -> bool {
403 match schema_type {
404 SchemaType::Primitive {
405 serde_with: Some(actual),
406 ..
407 } => actual == codec,
408 SchemaType::Object {
409 properties,
410 additional_properties,
411 ..
412 } => {
413 properties
414 .values()
415 .any(|property| schema_type_uses_serde_codec(&property.schema_type, codec))
416 || matches!(
417 additional_properties,
418 crate::analysis::ObjectAdditionalProperties::Typed { value_type }
419 if schema_type_uses_serde_codec(value_type, codec)
420 )
421 }
422 SchemaType::Array { item_type }
423 | SchemaType::Nullable {
424 inner_type: item_type,
425 } => schema_type_uses_serde_codec(item_type, codec),
426 SchemaType::Tuple { element_types } => element_types
427 .iter()
428 .any(|element| schema_type_uses_serde_codec(element, codec)),
429 _ => false,
430 }
431}
432
433impl CodeGenerator {
434 pub fn new(config: GeneratorConfig) -> Self {
435 Self {
436 config,
437 source_provenance: None,
438 }
439 }
440
441 pub fn with_source_provenance(mut self, source: impl Into<String>) -> Self {
443 self.source_provenance = Some(source.into());
444 self
445 }
446
447 pub fn config(&self) -> &GeneratorConfig {
449 &self.config
450 }
451
452 pub(crate) fn provenance_attribute(&self) -> TokenStream {
453 self.source_provenance
454 .as_ref()
455 .map(|source| {
456 let provenance = format!(
457 " Generated by openapi-to-rust v{}. Source OpenAPI document: {source}",
458 env!("CARGO_PKG_VERSION")
459 );
460 quote! { #![doc = #provenance] }
461 })
462 .unwrap_or_default()
463 }
464
465 pub fn generate_all(&self, analysis: &mut SchemaAnalysis) -> Result<GenerationResult> {
467 Ok(self
468 .generate_all_internal(analysis, self.config.bindings_metadata.is_some())?
469 .generation)
470 }
471
472 pub fn generate_all_with_bindings(
475 &self,
476 analysis: &mut SchemaAnalysis,
477 ) -> Result<crate::bindings::GenerationWithBindings> {
478 self.generate_all_internal(analysis, true)
479 }
480
481 fn generate_all_internal(
482 &self,
483 analysis: &mut SchemaAnalysis,
484 collect_bindings: bool,
485 ) -> Result<crate::bindings::GenerationWithBindings> {
486 if collect_bindings
487 && (self.config.registry_only
488 || self.config.enable_registry
489 || self
490 .config
491 .server
492 .as_ref()
493 .is_some_and(|server| !server.operations.is_empty())
494 || (self.config.enable_sse_client && self.config.streaming_config.is_some()))
495 {
496 return Err(GeneratorError::ValidationError(
497 "bindings metadata v1 supports models and HTTP client output; registry, server, and configured streaming output are unsupported".to_string(),
498 ));
499 }
500 let mut bindings = crate::bindings::BindingsMetadata::new(
501 &self.config.module_name,
502 self.config.enable_async_client,
503 );
504 let scopes = self.resolve_operation_scopes(analysis)?;
507 let pruned_schemas = self.prune_models_to_scopes(analysis, &scopes);
508 let mut files = Vec::new();
509
510 if !self.config.registry_only {
511 let types_content = self.generate_types_with_bindings(
513 analysis,
514 collect_bindings.then_some(&mut bindings),
515 )?;
516 files.push(GeneratedFile {
517 path: "types.rs".into(),
518 content: types_content,
519 });
520
521 if self.config.enable_sse_client
523 && let Some(ref streaming_config) = self.config.streaming_config
524 {
525 if streaming_config.generate_client && !streaming_config.event_parser_helpers {
526 return Err(GeneratorError::ValidationError(
527 "streaming generate_client=true requires event_parser_helpers=true"
528 .to_string(),
529 ));
530 }
531 if streaming_config.event_parser_helpers {
532 files.push(GeneratedFile {
533 path: "sse.rs".into(),
534 content: self.generate_sse_runtime()?,
535 });
536 }
537 let streaming_content =
538 self.generate_streaming_client(streaming_config, analysis)?;
539 files.push(GeneratedFile {
540 path: "streaming.rs".into(),
541 content: streaming_content,
542 });
543 }
544
545 if self.config.enable_async_client {
547 let operations = self.client_operations(analysis, scopes.client_ids.as_ref());
548 let needs_sse_runtime = self
549 .client_method_plans(analysis, &operations)
550 .iter()
551 .any(|plan| plan.response.consumption == "parsed_sse");
552 if needs_sse_runtime
553 && !files
554 .iter()
555 .any(|file| file.path == std::path::Path::new("sse.rs"))
556 {
557 files.push(GeneratedFile {
558 path: "sse.rs".into(),
559 content: self.generate_sse_runtime_with_bindings(
560 collect_bindings.then_some(&mut bindings),
561 )?,
562 });
563 }
564 let http_content = self.generate_http_client_for_operations_with_bindings(
565 analysis,
566 &operations,
567 collect_bindings.then_some(&mut bindings),
568 )?;
569 files.push(GeneratedFile {
570 path: "client.rs".into(),
571 content: http_content,
572 });
573 }
574 }
575
576 if self.config.enable_registry || self.config.registry_only {
578 let registry_content = self.generate_registry(analysis)?;
579 files.push(GeneratedFile {
580 path: "registry.rs".into(),
581 content: registry_content,
582 });
583 }
584
585 if !self.config.registry_only
589 && let Some(server) = self
590 .config
591 .server
592 .as_ref()
593 .filter(|server| !server.operations.is_empty())
594 {
595 let server_files =
596 crate::server::codegen::ServerCodegen::new(&self.config, analysis, server)
597 .with_source_provenance(self.source_provenance.as_deref())
598 .generate()
599 .map_err(|error| {
600 GeneratorError::CodeGenError(format!(
601 "server code generation failed: {error}"
602 ))
603 })?;
604 files.extend(server_files);
605 }
606
607 let mod_content = self.generate_mod_file(&files)?;
609 let mod_file = GeneratedFile {
610 path: "mod.rs".into(),
611 content: mod_content,
612 };
613
614 let required_deps = crate::type_mapping::collect_generated_dep_requirements(
615 files.iter().map(|file| file.content.as_str()),
616 self.config.enable_specta,
617 );
618
619 if collect_bindings {
620 let top_level_modules: Vec<_> = files
621 .iter()
622 .filter_map(|file| {
623 file.path
624 .file_stem()
625 .and_then(|stem| stem.to_str())
626 .map(str::to_string)
627 })
628 .collect();
629 bindings.modules.extend(top_level_modules.iter().cloned());
630 bindings.reexports.extend(
631 top_level_modules
632 .iter()
633 .filter(|module| module.as_str() != "sse")
634 .map(|module| format!("{module}::*")),
635 );
636 bindings.finish();
637 }
638 if let Some(path) = &self.config.bindings_metadata {
639 let artifact_path = std::path::Path::new(path);
640 crate::overlay::validate_artifact_path(artifact_path)?;
641 let overlaps = |other: &std::path::Path| {
642 artifact_path.starts_with(other) || other.starts_with(artifact_path)
643 };
644 if files.iter().any(|file| overlaps(&file.path))
645 || overlaps(std::path::Path::new("mod.rs"))
646 || overlaps(std::path::Path::new("REQUIRED_DEPS.toml"))
647 || self.config.effective_spec.as_deref().is_some_and(overlaps)
648 {
649 return Err(GeneratorError::ValidationError(format!(
650 "bindings metadata path conflicts with generated artifact: {path}"
651 )));
652 }
653 files.push(GeneratedFile {
654 path: path.into(),
655 content: bindings.to_json()?,
656 });
657 }
658 Ok(crate::bindings::GenerationWithBindings {
659 generation: GenerationResult {
660 files,
661 mod_file,
662 required_deps,
663 pruned_schemas,
664 },
665 bindings,
666 })
667 }
668
669 pub fn generate(&self, analysis: &mut SchemaAnalysis) -> Result<String> {
671 self.generate_types(analysis)
672 }
673
674 fn generate_types(&self, analysis: &mut SchemaAnalysis) -> Result<String> {
676 self.generate_types_with_bindings(analysis, None)
677 }
678
679 fn generate_types_with_bindings(
680 &self,
681 analysis: &mut SchemaAnalysis,
682 bindings: Option<&mut crate::bindings::BindingsMetadata>,
683 ) -> Result<String> {
684 self.validate_schema_type_names(analysis)?;
685
686 let provenance_attribute = self.provenance_attribute();
687 let mut type_definitions = TokenStream::new();
688
689 let type_index = self.type_generation_index(analysis);
690 let enum_wire_names = std::cell::RefCell::new(BTreeMap::new());
691 let type_context = TypeGenerationContext {
692 index: &type_index,
693 enum_wire_names: bindings.as_ref().map(|_| &enum_wire_names),
694 };
695
696 let generation_order = analysis.dependencies.topological_sort()?;
698
699 let mut processed = std::collections::HashSet::new();
700
701 for schema_name in generation_order {
703 if let Some(schema) = analysis.schemas.get(&schema_name) {
704 let type_def = self.generate_type_definition(schema, analysis, &type_context)?;
705 if !type_def.is_empty() {
706 type_definitions.extend(type_def);
707 }
708 processed.insert(schema_name);
709 }
710 }
711
712 let mut remaining_schemas: Vec<_> = analysis
714 .schemas
715 .iter()
716 .filter(|(name, _)| !processed.contains(*name))
717 .collect();
718 remaining_schemas.sort_by_key(|(name, _)| name.as_str());
719
720 for (_schema_name, schema) in remaining_schemas {
721 let type_def = self.generate_type_definition(schema, analysis, &type_context)?;
722 if !type_def.is_empty() {
723 type_definitions.extend(type_def);
724 }
725 }
726
727 let mut uses_plain_tri_state = false;
728 let mut tri_state_codecs = std::collections::HashSet::new();
729 for (schema_name, schema) in &analysis.schemas {
730 let crate::analysis::SchemaType::Object {
731 properties,
732 required,
733 ..
734 } = &schema.schema_type
735 else {
736 continue;
737 };
738 for (field_name, property) in properties {
739 if !self.property_is_tri_state(
740 schema_name,
741 field_name,
742 property,
743 required.contains(field_name),
744 ) {
745 continue;
746 }
747 if let Some(codec) = self.schema_type_serde_codec(&property.schema_type, analysis) {
748 tri_state_codecs.insert(codec);
749 } else {
750 uses_plain_tri_state = true;
751 }
752 }
753 }
754
755 let base64_double_option = if tri_state_codecs.contains("base64_serde") {
760 quote! {
761 pub mod double_option {
762 use serde::{Deserializer, Serializer};
763
764 pub fn serialize<S: Serializer>(
765 value: &Option<Option<Vec<u8>>>,
766 ser: S,
767 ) -> Result<S::Ok, S::Error> {
768 match value {
769 Some(value) => super::option::serialize(value, ser),
770 None => ser.serialize_none(),
771 }
772 }
773
774 pub fn deserialize<'de, D: Deserializer<'de>>(
775 de: D,
776 ) -> Result<Option<Option<Vec<u8>>>, D::Error> {
777 super::option::deserialize(de).map(Some)
778 }
779 }
780 }
781 } else {
782 TokenStream::new()
783 };
784 let base64_helper = if analysis
785 .used_type_features
786 .contains(crate::type_mapping::TypeFeature::Base64)
787 {
788 let engine = match self.config.types.byte {
789 crate::type_mapping::ByteStrategy::Base64UrlUnpadded => {
790 quote::format_ident!("URL_SAFE_NO_PAD")
791 }
792 _ => quote::format_ident!("STANDARD"),
793 };
794 quote! {
795 mod base64_serde {
800 use base64::{Engine as _, engine::general_purpose::#engine as ENGINE};
801 use serde::{Deserialize, Deserializer, Serializer};
802
803 pub fn serialize<S: Serializer>(
804 bytes: &Vec<u8>,
805 ser: S,
806 ) -> Result<S::Ok, S::Error> {
807 ser.serialize_str(&ENGINE.encode(bytes))
808 }
809
810 pub fn deserialize<'de, D: Deserializer<'de>>(
811 de: D,
812 ) -> Result<Vec<u8>, D::Error> {
813 let s = String::deserialize(de)?;
814 ENGINE
815 .decode(s.as_bytes())
816 .map_err(serde::de::Error::custom)
817 }
818
819 pub mod option {
825 use super::*;
826 use serde::{Deserialize, Deserializer, Serializer};
827
828 pub fn serialize<S: Serializer>(
829 opt: &Option<Vec<u8>>,
830 ser: S,
831 ) -> Result<S::Ok, S::Error> {
832 match opt {
833 Some(bytes) => super::serialize(bytes, ser),
834 None => ser.serialize_none(),
835 }
836 }
837
838 pub fn deserialize<'de, D: Deserializer<'de>>(
839 de: D,
840 ) -> Result<Option<Vec<u8>>, D::Error> {
841 let opt = Option::<String>::deserialize(de)?;
842 opt.map(|s| {
843 ENGINE
844 .decode(s.as_bytes())
845 .map_err(serde::de::Error::custom)
846 })
847 .transpose()
848 }
849 }
850
851 #base64_double_option
852 }
853 }
854 } else {
855 TokenStream::new()
856 };
857
858 let uses_binary_bytes_codec = analysis
859 .schemas
860 .values()
861 .any(|schema| schema_type_uses_serde_codec(&schema.schema_type, "binary_bytes_serde"));
862 let binary_bytes_double_option = if tri_state_codecs.contains("binary_bytes_serde") {
863 quote! {
864 pub mod double_option {
865 use serde::{Deserializer, Serializer};
866
867 pub fn serialize<S: Serializer>(
868 value: &Option<Option<bytes::Bytes>>,
869 ser: S,
870 ) -> Result<S::Ok, S::Error> {
871 match value {
872 Some(value) => super::option::serialize(value, ser),
873 None => ser.serialize_none(),
874 }
875 }
876
877 pub fn deserialize<'de, D: Deserializer<'de>>(
878 de: D,
879 ) -> Result<Option<Option<bytes::Bytes>>, D::Error> {
880 super::option::deserialize(de).map(Some)
881 }
882 }
883 }
884 } else {
885 TokenStream::new()
886 };
887 let binary_bytes_helper = if uses_binary_bytes_codec {
888 quote! {
889 mod binary_bytes_serde {
893 use serde::{Deserialize, Deserializer, Serializer};
894
895 pub fn serialize<S: Serializer>(
896 bytes: &bytes::Bytes,
897 ser: S,
898 ) -> Result<S::Ok, S::Error> {
899 let value = std::str::from_utf8(bytes.as_ref())
900 .map_err(serde::ser::Error::custom)?;
901 ser.serialize_str(value)
902 }
903
904 pub fn deserialize<'de, D: Deserializer<'de>>(
905 de: D,
906 ) -> Result<bytes::Bytes, D::Error> {
907 String::deserialize(de).map(bytes::Bytes::from)
908 }
909
910 pub mod option {
911 use serde::{Deserialize, Deserializer, Serializer};
912
913 pub fn serialize<S: Serializer>(
914 value: &Option<bytes::Bytes>,
915 ser: S,
916 ) -> Result<S::Ok, S::Error> {
917 match value {
918 Some(bytes) => super::serialize(bytes, ser),
919 None => ser.serialize_none(),
920 }
921 }
922
923 pub fn deserialize<'de, D: Deserializer<'de>>(
924 de: D,
925 ) -> Result<Option<bytes::Bytes>, D::Error> {
926 Option::<String>::deserialize(de)
927 .map(|value| value.map(bytes::Bytes::from))
928 }
929 }
930
931 #binary_bytes_double_option
932 }
933 }
934 } else {
935 TokenStream::new()
936 };
937
938 let uses_binary_vec_codec = analysis
939 .schemas
940 .values()
941 .any(|schema| schema_type_uses_serde_codec(&schema.schema_type, "binary_vec_serde"));
942 let binary_vec_double_option = if tri_state_codecs.contains("binary_vec_serde") {
943 quote! {
944 pub mod double_option {
947 use serde::{Deserializer, Serializer};
948
949 pub fn serialize<S: Serializer>(
950 value: &Option<Option<Vec<u8>>>,
951 ser: S,
952 ) -> Result<S::Ok, S::Error> {
953 match value {
954 Some(value) => super::option::serialize(value, ser),
955 None => ser.serialize_none(),
956 }
957 }
958
959 pub fn deserialize<'de, D: Deserializer<'de>>(
960 de: D,
961 ) -> Result<Option<Option<Vec<u8>>>, D::Error> {
962 super::option::deserialize(de).map(Some)
963 }
964 }
965 }
966 } else {
967 TokenStream::new()
968 };
969 let binary_vec_helper = if uses_binary_vec_codec {
970 quote! {
971 mod binary_vec_serde {
974 use serde::{Deserialize, Deserializer, Serializer};
975
976 pub fn serialize<S: Serializer>(
977 bytes: &Vec<u8>,
978 ser: S,
979 ) -> Result<S::Ok, S::Error> {
980 let value = std::str::from_utf8(bytes)
981 .map_err(serde::ser::Error::custom)?;
982 ser.serialize_str(value)
983 }
984
985 pub fn deserialize<'de, D: Deserializer<'de>>(
986 de: D,
987 ) -> Result<Vec<u8>, D::Error> {
988 String::deserialize(de).map(String::into_bytes)
989 }
990
991 pub mod option {
992 use serde::{Deserialize, Deserializer, Serializer};
993
994 pub fn serialize<S: Serializer>(
995 value: &Option<Vec<u8>>,
996 ser: S,
997 ) -> Result<S::Ok, S::Error> {
998 match value {
999 Some(bytes) => super::serialize(bytes, ser),
1000 None => ser.serialize_none(),
1001 }
1002 }
1003
1004 pub fn deserialize<'de, D: Deserializer<'de>>(
1005 de: D,
1006 ) -> Result<Option<Vec<u8>>, D::Error> {
1007 Option::<String>::deserialize(de)
1008 .map(|value| value.map(String::into_bytes))
1009 }
1010 }
1011
1012 #binary_vec_double_option
1013 }
1014 }
1015 } else {
1016 TokenStream::new()
1017 };
1018
1019 let tri_state_helper = if uses_plain_tri_state {
1020 quote! {
1021 mod tri_state_serde {
1025 use serde::{Deserialize, Deserializer};
1026
1027 pub fn deserialize<'de, D, T>(de: D) -> Result<Option<T>, D::Error>
1028 where
1029 D: Deserializer<'de>,
1030 T: Deserialize<'de>,
1031 {
1032 T::deserialize(de).map(Some)
1033 }
1034 }
1035 }
1036 } else {
1037 TokenStream::new()
1038 };
1039
1040 let time_date_double_option = if tri_state_codecs.contains("time_date_format") {
1047 quote! {
1048 mod time_date_double_option {
1049 use serde::{Deserializer, Serializer};
1050
1051 pub fn serialize<S: Serializer>(
1052 value: &Option<Option<time::Date>>,
1053 ser: S,
1054 ) -> Result<S::Ok, S::Error> {
1055 match value {
1056 Some(value) => time_date_format::option::serialize(value, ser),
1057 None => ser.serialize_none(),
1058 }
1059 }
1060
1061 pub fn deserialize<'de, D: Deserializer<'de>>(
1062 de: D,
1063 ) -> Result<Option<Option<time::Date>>, D::Error> {
1064 time_date_format::option::deserialize(de).map(Some)
1065 }
1066 }
1067 }
1068 } else {
1069 TokenStream::new()
1070 };
1071 let time_date_helper = if analysis
1072 .used_type_features
1073 .contains(crate::type_mapping::TypeFeature::TimeDate)
1074 {
1075 quote! {
1076 time::serde::format_description!(
1077 time_date_format,
1078 Date,
1079 "[year]-[month]-[day]"
1080 );
1081
1082 #time_date_double_option
1083 }
1084 } else {
1085 TokenStream::new()
1086 };
1087
1088 let time_time_double_option = if tri_state_codecs.contains("time_time_format") {
1093 quote! {
1094 mod time_time_double_option {
1095 use serde::{Deserializer, Serializer};
1096
1097 pub fn serialize<S: Serializer>(
1098 value: &Option<Option<time::Time>>,
1099 ser: S,
1100 ) -> Result<S::Ok, S::Error> {
1101 match value {
1102 Some(value) => time_time_format::option::serialize(value, ser),
1103 None => ser.serialize_none(),
1104 }
1105 }
1106
1107 pub fn deserialize<'de, D: Deserializer<'de>>(
1108 de: D,
1109 ) -> Result<Option<Option<time::Time>>, D::Error> {
1110 time_time_format::option::deserialize(de).map(Some)
1111 }
1112 }
1113 }
1114 } else {
1115 TokenStream::new()
1116 };
1117 let time_time_helper = if analysis
1118 .used_type_features
1119 .contains(crate::type_mapping::TypeFeature::TimeTime)
1120 {
1121 quote! {
1122 time::serde::format_description!(
1123 version = 2,
1124 time_time_format,
1125 Time,
1126 "[hour]:[minute]:[second][optional [.[subsecond]]]"
1127 );
1128
1129 #time_time_double_option
1130 }
1131 } else {
1132 TokenStream::new()
1133 };
1134
1135 let time_rfc3339_double_option_helper = if tri_state_codecs.contains("time::serde::rfc3339")
1136 {
1137 quote! {
1138 mod time_rfc3339_double_option {
1139 use serde::{Deserializer, Serializer};
1140
1141 pub fn serialize<S: Serializer>(
1142 value: &Option<Option<time::OffsetDateTime>>,
1143 ser: S,
1144 ) -> Result<S::Ok, S::Error> {
1145 match value {
1146 Some(value) => time::serde::rfc3339::option::serialize(value, ser),
1147 None => ser.serialize_none(),
1148 }
1149 }
1150
1151 pub fn deserialize<'de, D: Deserializer<'de>>(
1152 de: D,
1153 ) -> Result<Option<Option<time::OffsetDateTime>>, D::Error> {
1154 time::serde::rfc3339::option::deserialize(de).map(Some)
1155 }
1156 }
1157 }
1158 } else {
1159 TokenStream::new()
1160 };
1161
1162 let generated = quote! {
1164 #provenance_attribute
1170
1171 #![allow(clippy::large_enum_variant)]
1172 #![allow(clippy::format_in_format_args)]
1173 #![allow(clippy::let_unit_value)]
1174 #![allow(unreachable_patterns)]
1175
1176 use serde::{Deserialize, Serialize};
1177
1178 #base64_helper
1179
1180 #binary_bytes_helper
1181
1182 #binary_vec_helper
1183
1184 #tri_state_helper
1185
1186 #time_date_helper
1187
1188 #time_time_helper
1189
1190 #time_rfc3339_double_option_helper
1191
1192 #type_definitions
1193 };
1194
1195 let syntax_tree = syn::parse2::<syn::File>(generated).map_err(|e| {
1197 GeneratorError::CodeGenError(format!("Failed to parse generated code: {e}"))
1198 })?;
1199
1200 if let Some(bindings) = bindings {
1201 bindings.collect("types", &syntax_tree);
1202 for (name, variants) in enum_wire_names.into_inner() {
1203 if let Some(symbol) = bindings
1204 .symbols
1205 .iter_mut()
1206 .find(|symbol| symbol.path == format!("types::{name}"))
1207 {
1208 for variant in &mut symbol.variants {
1209 variant.wire_name = variants.get(&variant.name).cloned().flatten();
1210 }
1211 }
1212 }
1213 }
1214
1215 let formatted = prettyplease::unparse(&syntax_tree);
1216
1217 Ok(formatted)
1218 }
1219
1220 fn generate_streaming_client(
1222 &self,
1223 streaming_config: &StreamingConfig,
1224 analysis: &SchemaAnalysis,
1225 ) -> Result<String> {
1226 let mut client_code = TokenStream::new();
1227 let provenance_attribute = self.provenance_attribute();
1228 let duration_import = streaming_config
1229 .reconnection_config
1230 .as_ref()
1231 .map(|_| quote! { use std::time::Duration; });
1232
1233 let imports = quote! {
1235 #provenance_attribute
1240 #![allow(clippy::format_in_format_args)]
1241 #![allow(clippy::let_unit_value)]
1242 #![allow(unused_mut)]
1243
1244 use super::types::*;
1245 use async_trait::async_trait;
1246 use futures_util::Stream;
1247 use std::pin::Pin;
1248 use reqwest::header::{HeaderMap, HeaderValue};
1249 use tracing::{debug, info, instrument};
1250 #duration_import
1251 };
1252 client_code.extend(imports);
1253
1254 if streaming_config.generate_client {
1255 if streaming_config.reconnection_config.is_some() {
1256 client_code.extend(quote! {
1257 use super::sse::{BoxSseStream, SseClient, SseReconnectOptions};
1258 pub use super::sse::StreamingError;
1259 });
1260 } else {
1261 client_code.extend(quote! {
1262 use super::sse::{BoxSseStream, SseClient};
1263 pub use super::sse::StreamingError;
1264 });
1265 }
1266 }
1267
1268 for endpoint in &streaming_config.endpoints {
1270 let trait_code = self.generate_endpoint_trait(endpoint, analysis)?;
1271 client_code.extend(trait_code);
1272 }
1273
1274 if streaming_config.generate_client {
1276 let client_impl = self.generate_streaming_client_impl(streaming_config, analysis)?;
1277 client_code.extend(client_impl);
1278 }
1279
1280 if let Some(reconnect_config) = &streaming_config.reconnection_config {
1282 let reconnect_code = self.generate_reconnection_utilities(reconnect_config)?;
1283 client_code.extend(reconnect_code);
1284 }
1285
1286 let syntax_tree = syn::parse2::<syn::File>(client_code).map_err(|e| {
1287 GeneratorError::CodeGenError(format!("Failed to parse streaming client code: {e}"))
1288 })?;
1289
1290 Ok(prettyplease::unparse(&syntax_tree))
1291 }
1292
1293 fn validate_schema_type_names(&self, analysis: &SchemaAnalysis) -> Result<()> {
1294 let mut source_by_rust_name = BTreeMap::<String, String>::new();
1295
1296 for schema in analysis.schemas.values() {
1297 let rust_name = self.to_rust_type_name(&schema.name);
1298 if let Some(first) = source_by_rust_name.insert(rust_name.clone(), schema.name.clone())
1299 {
1300 return Err(GeneratorError::InvalidSchema(format!(
1301 "schema names `{first}` and `{}` both map to Rust type `{rust_name}`",
1302 schema.name
1303 )));
1304 }
1305 }
1306
1307 Ok(())
1308 }
1309
1310 pub fn generate_http_client(&self, analysis: &SchemaAnalysis) -> Result<String> {
1318 let client_ids = self.resolve_client_operation_ids(analysis)?;
1319 let operations = self.client_operations(analysis, client_ids.as_ref());
1320 let mut config = self.config.clone();
1321 config.enable_sse_client = false;
1322 let standalone = Self {
1323 config,
1324 source_provenance: self.source_provenance.clone(),
1325 };
1326 standalone.generate_http_client_for_operations(analysis, &operations)
1327 }
1328
1329 fn generate_http_client_for_operations(
1330 &self,
1331 analysis: &SchemaAnalysis,
1332 operations: &[&crate::analysis::OperationInfo],
1333 ) -> Result<String> {
1334 self.generate_http_client_for_operations_with_bindings(analysis, operations, None)
1335 }
1336
1337 fn generate_http_client_for_operations_with_bindings(
1338 &self,
1339 analysis: &SchemaAnalysis,
1340 operations: &[&crate::analysis::OperationInfo],
1341 bindings: Option<&mut crate::bindings::BindingsMetadata>,
1342 ) -> Result<String> {
1343 let provenance_attribute = self.provenance_attribute();
1344 let error_types = self.generate_http_error_types();
1345 let client_struct = self.generate_http_client_struct();
1346 let operation_methods = self.generate_operation_methods_for(analysis, operations);
1347
1348 let generated = quote! {
1349 #provenance_attribute
1354 #![allow(clippy::format_in_format_args)]
1355 #![allow(clippy::let_unit_value)]
1356
1357 use super::types::*;
1358
1359 #error_types
1360
1361 #client_struct
1362
1363 #operation_methods
1364 };
1365
1366 let syntax_tree = syn::parse2::<syn::File>(generated.clone()).map_err(|e| {
1367 if let Ok(dump) = std::env::var("OATR_DUMP_TOKENS_ON_PARSE_ERROR") {
1368 let _ = std::fs::write(&dump, generated.to_string());
1369 }
1370 GeneratorError::CodeGenError(format!("Failed to parse HTTP client code: {e}"))
1371 })?;
1372
1373 if let Some(bindings) = bindings {
1374 bindings.collect("client", &syntax_tree);
1375 self.attach_operation_bindings(analysis, operations, bindings)?;
1376 }
1377
1378 Ok(prettyplease::unparse(&syntax_tree))
1379 }
1380
1381 fn attach_operation_bindings(
1382 &self,
1383 analysis: &SchemaAnalysis,
1384 operations: &[&crate::analysis::OperationInfo],
1385 bindings: &mut crate::bindings::BindingsMetadata,
1386 ) -> Result<()> {
1387 for plan in self.client_method_plans(analysis, operations) {
1388 let source = analysis
1389 .operation_sources
1390 .get(&plan.operation.operation_id)
1391 .ok_or_else(|| {
1392 GeneratorError::ValidationError(format!(
1393 "bindings metadata requires effective-source identity for operation '{}'",
1394 plan.operation.operation_id
1395 ))
1396 })?;
1397 let path = format!("client::HttpClient::{}", plan.method_ident);
1398 let builder_entry = plan
1399 .builder_method_ident
1400 .as_ref()
1401 .map(|ident| format!("client::HttpClient::{ident}"));
1402 let builder_type = plan
1403 .builder_type_ident
1404 .as_ref()
1405 .map(|ident| format!("client::{ident}"));
1406 let operation_binding = crate::bindings::BindingOperation {
1407 operation_id: plan.operation.operation_id.clone(),
1408 source_json_pointer: source.json_pointer.clone(),
1409 source_method: source.method.clone(),
1410 source_path: source.path.clone(),
1411 source_operation_id: source.operation_id.clone(),
1412 webhook: source.webhook,
1413 response_statuses: plan.response.statuses,
1414 response_excluded_statuses: plan.response.excluded_statuses,
1415 response_media_type: plan.response.media_type,
1416 response_kind: plan.response.kind,
1417 consumption: plan.response.consumption,
1418 multipart_filenames: plan.multipart_filenames.is_some(),
1419 };
1420 for symbol in &mut bindings.symbols {
1421 if symbol.path == path
1422 || builder_entry.as_ref() == Some(&symbol.path)
1423 || builder_type.as_ref().is_some_and(|owner| {
1424 symbol.path == *owner || symbol.path.starts_with(&format!("{owner}::"))
1425 })
1426 {
1427 symbol.operation = Some(operation_binding.clone());
1428 }
1429 }
1430 }
1431 Ok(())
1432 }
1433
1434 fn resolve_operation_scopes(&self, analysis: &SchemaAnalysis) -> Result<OperationScopes> {
1435 let client_ids = if self.config.enable_async_client && !self.config.registry_only {
1436 self.resolve_client_operation_ids(analysis)?
1437 } else {
1438 None
1439 };
1440
1441 let server_ids = match &self.config.server {
1442 Some(server) if !server.operations.is_empty() => {
1443 crate::server::resolve_operation_selectors(&server.operations, analysis)
1444 .map_err(|error| {
1445 GeneratorError::ValidationError(format!(
1446 "Invalid [server].operations: {error}"
1447 ))
1448 })?
1449 .operations
1450 .into_iter()
1451 .map(|operation| operation.operation_id)
1452 .collect()
1453 }
1454 _ => Default::default(),
1455 };
1456
1457 let streaming_ids = if self.config.registry_only || !self.config.enable_sse_client {
1458 Default::default()
1459 } else if let Some(streaming) = &self.config.streaming_config {
1460 let mut ids = std::collections::BTreeSet::new();
1461 for (index, endpoint) in streaming.endpoints.iter().enumerate() {
1462 let resolution =
1463 crate::server::resolve_operation_id(&endpoint.operation_id, analysis).map_err(
1464 |error| {
1465 GeneratorError::ValidationError(format!(
1466 "Invalid [streaming].endpoints[{index}].operation_id: {error}"
1467 ))
1468 },
1469 )?;
1470 ids.extend(
1471 resolution
1472 .operations
1473 .into_iter()
1474 .map(|operation| operation.operation_id),
1475 );
1476 }
1477 ids
1478 } else {
1479 Default::default()
1480 };
1481
1482 let client_prunes = self.config.enable_async_client
1483 && !self.config.registry_only
1484 && self
1485 .config
1486 .client
1487 .as_ref()
1488 .is_some_and(|client| client.prune_models);
1489 let server_prunes = self
1490 .config
1491 .server
1492 .as_ref()
1493 .is_some_and(|server| server.prune_models && !server.operations.is_empty());
1494 let extra_schema_roots = if self.config.registry_only || !self.config.enable_sse_client {
1495 Vec::new()
1496 } else {
1497 self.config
1498 .streaming_config
1499 .as_ref()
1500 .map(|streaming| {
1501 streaming
1502 .endpoints
1503 .iter()
1504 .map(|endpoint| endpoint.event_union_type.clone())
1505 .collect()
1506 })
1507 .unwrap_or_default()
1508 };
1509
1510 Ok(OperationScopes {
1511 client_ids,
1512 server_ids,
1513 streaming_ids,
1514 prune_models: client_prunes || server_prunes,
1515 extra_schema_roots,
1516 })
1517 }
1518
1519 fn resolve_client_operation_ids(
1520 &self,
1521 analysis: &SchemaAnalysis,
1522 ) -> Result<Option<std::collections::BTreeSet<String>>> {
1523 match &self.config.client {
1524 Some(client) if !client.operations.is_empty() => {
1525 let resolution =
1526 crate::server::resolve_operation_selectors(&client.operations, analysis)
1527 .map_err(|error| {
1528 GeneratorError::ValidationError(format!(
1529 "Invalid [client].operations: {error}"
1530 ))
1531 })?;
1532 Ok(Some(
1533 resolution
1534 .operations
1535 .into_iter()
1536 .map(|operation| operation.operation_id)
1537 .collect(),
1538 ))
1539 }
1540 _ => Ok(None),
1541 }
1542 }
1543
1544 fn client_operations<'a>(
1545 &self,
1546 analysis: &'a SchemaAnalysis,
1547 selected: Option<&std::collections::BTreeSet<String>>,
1548 ) -> Vec<&'a crate::analysis::OperationInfo> {
1549 analysis
1550 .operations
1551 .iter()
1552 .filter(|(operation_id, _)| selected.is_none_or(|ids| ids.contains(*operation_id)))
1553 .map(|(_, operation)| operation)
1554 .collect()
1555 }
1556
1557 fn prune_models_to_scopes(
1558 &self,
1559 analysis: &mut SchemaAnalysis,
1560 scopes: &OperationScopes,
1561 ) -> usize {
1562 if !scopes.prune_models {
1563 return 0;
1564 }
1565
1566 let mut consumer_ids = scopes.server_ids.clone();
1567 if self.config.enable_async_client && !self.config.registry_only {
1568 match &scopes.client_ids {
1569 Some(ids) => consumer_ids.extend(ids.iter().cloned()),
1570 None => consumer_ids.extend(analysis.operations.keys().cloned()),
1571 }
1572 }
1573 consumer_ids.extend(scopes.streaming_ids.iter().cloned());
1574
1575 let operations: Vec<&crate::analysis::OperationInfo> = consumer_ids
1576 .iter()
1577 .filter_map(|operation_id| analysis.operations.get(operation_id))
1578 .collect();
1579 let keep = crate::server::codegen::reachable_schemas_with_roots(
1580 analysis,
1581 &operations,
1582 &scopes.extra_schema_roots,
1583 );
1584 let before = analysis.schemas.len();
1585 analysis.schemas.retain(|name, _| keep.contains(name));
1586 before - analysis.schemas.len()
1587 }
1588
1589 fn generate_http_error_types(&self) -> TokenStream {
1591 quote! {
1592 use thiserror::Error;
1593
1594 pub mod openapi_to_rust_problem {
1597 #[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
1598 pub struct ProblemDetails {
1599 #[serde(rename = "type")]
1600 pub type_uri: String,
1601 pub title: String,
1602 pub status: u16,
1603 pub code: String,
1604 #[serde(default)]
1605 pub errors: Vec<InvalidParameter>,
1606 #[serde(default, skip_serializing_if = "Option::is_none")]
1607 pub detail: Option<String>,
1608 #[serde(default, skip_serializing_if = "Option::is_none")]
1609 pub instance: Option<String>,
1610 }
1611
1612 #[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
1613 pub struct InvalidParameter {
1614 pub code: String,
1615 pub location: String,
1616 pub message: String,
1617 }
1618 }
1619
1620 #[derive(Error, Debug)]
1628 pub enum HttpError {
1629 #[error("Network error: {0}")]
1631 Network(#[from] reqwest::Error),
1632
1633 #[error("Middleware error: {0}")]
1635 Middleware(#[from] reqwest_middleware::Error),
1636
1637 #[error("Failed to serialize request: {0}")]
1639 Serialization(String),
1640
1641 #[error("Authentication error: {0}")]
1643 Auth(String),
1644
1645 #[error("Request timeout")]
1647 Timeout,
1648
1649 #[error("Response body exceeded configured limit of {limit} bytes")]
1651 ResponseTooLarge { limit: usize },
1652
1653 #[error("Configuration error: {0}")]
1655 Config(String),
1656
1657 #[error("{0}")]
1659 Other(String),
1660 }
1661
1662 impl HttpError {
1663 pub fn serialization_error(error: impl std::fmt::Display) -> Self {
1665 Self::Serialization(error.to_string())
1666 }
1667
1668 pub fn is_retryable(&self) -> bool {
1670 matches!(self, Self::Network(_) | Self::Middleware(_) | Self::Timeout)
1671 }
1672 }
1673
1674 #[derive(Debug, Clone)]
1686 pub struct ApiError<E> {
1687 pub status: u16,
1688 pub headers: reqwest::header::HeaderMap,
1689 pub body: String,
1690 pub raw_body: Vec<u8>,
1692 pub typed: Option<E>,
1693 pub parse_error: Option<String>,
1694 }
1695
1696 const API_ERROR_BODY_DISPLAY_LIMIT: usize = 500;
1697 const API_ERROR_BODY_TRUNCATION_MARKER: &str = "... [truncated]";
1698
1699 fn display_api_error_body(body: &str) -> std::borrow::Cow<'_, str> {
1700 let Some((end, _)) = body.char_indices().nth(API_ERROR_BODY_DISPLAY_LIMIT) else {
1701 return std::borrow::Cow::Borrowed(body);
1702 };
1703
1704 let mut displayed =
1705 String::with_capacity(end + API_ERROR_BODY_TRUNCATION_MARKER.len());
1706 displayed.push_str(&body[..end]);
1707 displayed.push_str(API_ERROR_BODY_TRUNCATION_MARKER);
1708 std::borrow::Cow::Owned(displayed)
1709 }
1710
1711 impl<E> ApiError<E> {
1712 pub fn is_client_error(&self) -> bool {
1713 (400..500).contains(&self.status)
1714 }
1715
1716 pub fn is_server_error(&self) -> bool {
1717 (500..600).contains(&self.status)
1718 }
1719
1720 pub fn is_retryable(&self) -> bool {
1723 matches!(self.status, 429 | 500 | 502 | 503 | 504)
1724 }
1725
1726 pub fn problem_details(
1738 &self,
1739 ) -> Option<openapi_to_rust_problem::ProblemDetails> {
1740 let content_type = self
1741 .headers
1742 .get(reqwest::header::CONTENT_TYPE)?
1743 .to_str()
1744 .ok()?;
1745 let media_type = content_type.split(';').next()?.trim();
1746 if !media_type.eq_ignore_ascii_case("application/problem+json") {
1747 return None;
1748 }
1749 serde_json::from_str(&self.body).ok()
1750 }
1751 }
1752
1753 impl<E: std::fmt::Debug> std::fmt::Display for ApiError<E> {
1754 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1755 write!(
1756 f,
1757 "API error {}: {}",
1758 self.status,
1759 display_api_error_body(&self.body)
1760 )?;
1761
1762 if let Some(typed) = &self.typed {
1763 write!(f, "; typed: {typed:?}")?;
1764 }
1765
1766 if let Some(parse_error) = &self.parse_error {
1767 write!(f, "; parse error: {parse_error}")?;
1768 }
1769
1770 Ok(())
1771 }
1772 }
1773
1774 impl<E: std::fmt::Debug> std::error::Error for ApiError<E> {}
1775
1776 #[derive(Debug, Error)]
1784 pub enum ApiOpError<E: std::fmt::Debug> {
1785 #[error(transparent)]
1786 Transport(#[from] HttpError),
1787
1788 #[error(transparent)]
1789 Api(ApiError<E>),
1790 }
1791
1792 impl<E: std::fmt::Debug> ApiOpError<E> {
1793 pub fn api(&self) -> Option<&ApiError<E>> {
1795 match self {
1796 Self::Api(e) => Some(e),
1797 Self::Transport(_) => None,
1798 }
1799 }
1800
1801 pub fn is_api_error(&self) -> bool {
1804 matches!(self, Self::Api(_))
1805 }
1806 }
1807
1808 impl<E: std::fmt::Debug> From<reqwest::Error> for ApiOpError<E> {
1811 fn from(e: reqwest::Error) -> Self {
1812 Self::Transport(HttpError::Network(e))
1813 }
1814 }
1815
1816 impl<E: std::fmt::Debug> From<reqwest_middleware::Error> for ApiOpError<E> {
1817 fn from(e: reqwest_middleware::Error) -> Self {
1818 Self::Transport(HttpError::Middleware(e))
1819 }
1820 }
1821
1822 pub type HttpResult<T> = Result<T, HttpError>;
1826 }
1827 }
1828
1829 fn generate_mod_file(&self, files: &[GeneratedFile]) -> Result<String> {
1831 let mut module_names = std::collections::BTreeSet::new();
1832
1833 for file in files {
1834 let module_name = if file.path.components().count() > 1 {
1835 file.path.iter().next().and_then(|part| part.to_str())
1836 } else {
1837 file.path.file_stem().and_then(|stem| stem.to_str())
1838 };
1839 if let Some(module_name) = module_name.filter(|name| *name != "mod") {
1840 module_names.insert(module_name.to_string());
1841 }
1842 }
1843 let module_declarations = module_names
1844 .iter()
1845 .map(|name| format!("pub mod {name};"))
1846 .collect::<Vec<_>>();
1847 let pub_uses = module_names
1848 .iter()
1849 .filter(|name| name.as_str() != "sse")
1852 .map(|name| format!("pub use {name}::*;"))
1853 .collect::<Vec<_>>();
1854
1855 let mount_hint = format!(
1862 "//! Configured `module_name` = `{name}`. Mount this tree under your\n\
1863 //! preferred path, e.g. `pub mod {name};` in your crate root.\n",
1864 name = self.config.module_name,
1865 );
1866 let source_hint = self
1867 .source_provenance
1868 .as_ref()
1869 .map(|source| {
1870 format!(
1871 "//! Generated by openapi-to-rust v{}. Source OpenAPI document: {source}\n",
1872 env!("CARGO_PKG_VERSION")
1873 )
1874 })
1875 .unwrap_or_default();
1876
1877 let content = format!(
1878 r#"//! Generated API modules
1879//!
1880//! This module exports all generated API types and clients.
1881//! Do not edit manually - regenerate using the appropriate script.
1882//!
1883{source_hint}
1884{mount_hint}
1885#![allow(unused_imports)]
1886
1887{decls}
1888
1889{uses}
1890"#,
1891 mount_hint = mount_hint,
1892 source_hint = source_hint,
1893 decls = module_declarations.join("\n"),
1894 uses = pub_uses.join("\n"),
1895 );
1896
1897 Ok(content)
1898 }
1899
1900 pub fn output_artifacts(
1902 &self,
1903 result: &GenerationResult,
1904 ) -> std::collections::BTreeMap<PathBuf, String> {
1905 let mut artifacts = std::collections::BTreeMap::new();
1906 for file in &result.files {
1907 artifacts.insert(file.path.clone(), file.content.clone());
1908 }
1909 artifacts.insert(
1910 result.mod_file.path.clone(),
1911 result.mod_file.content.clone(),
1912 );
1913 if let Some(mut fragment) =
1914 crate::type_mapping::render_required_deps_toml(&result.required_deps)
1915 {
1916 if let Some(source) = &self.source_provenance {
1917 let header = format!(
1918 "# Generated by openapi-to-rust v{}. Source OpenAPI document: {source}",
1919 env!("CARGO_PKG_VERSION")
1920 );
1921 fragment = fragment.replacen("# Generated by openapi-to-rust.", &header, 1);
1922 }
1923 artifacts.insert(PathBuf::from("REQUIRED_DEPS.toml"), fragment);
1924 }
1925 artifacts
1926 }
1927
1928 pub fn write_files(&self, result: &GenerationResult) -> Result<()> {
1931 use std::fs;
1932
1933 fs::create_dir_all(&self.config.output_dir)?;
1935
1936 let artifacts = self.output_artifacts(result);
1937 for (relative, content) in &artifacts {
1938 let file_path = self.config.output_dir.join(relative);
1939 if let Some(parent) = file_path.parent() {
1940 fs::create_dir_all(parent)?;
1941 }
1942 fs::write(&file_path, content)?;
1943 }
1944
1945 let deps_path = self.config.output_dir.join("REQUIRED_DEPS.toml");
1946 if !artifacts.contains_key(std::path::Path::new("REQUIRED_DEPS.toml")) && deps_path.exists()
1947 {
1948 fs::remove_file(&deps_path)?;
1949 }
1950
1951 Ok(())
1952 }
1953
1954 fn generate_type_definition(
1955 &self,
1956 schema: &crate::analysis::AnalyzedSchema,
1957 analysis: &crate::analysis::SchemaAnalysis,
1958 type_context: &TypeGenerationContext<'_>,
1959 ) -> Result<TokenStream> {
1960 use crate::analysis::SchemaType;
1961
1962 match &schema.schema_type {
1963 SchemaType::Primitive { rust_type, .. } => {
1964 self.generate_type_alias(schema, rust_type)
1966 }
1967 SchemaType::StringEnum { values } => {
1968 let ext = analysis.enum_extensions.get(&schema.name);
1969 let rust_name = self.to_rust_type_name(&schema.name);
1976 let force_extensible = self
1977 .config
1978 .extensible_enum_overrides
1979 .get(&schema.name)
1980 .or_else(|| self.config.extensible_enum_overrides.get(&rust_name))
1981 .copied()
1982 .unwrap_or(false);
1983 if force_extensible {
1984 self.generate_extensible_enum(schema, values, ext, type_context)
1985 } else {
1986 self.generate_string_enum(schema, values, ext)
1987 }
1988 }
1989 SchemaType::ExtensibleEnum { known_values } => {
1990 let ext = analysis.enum_extensions.get(&schema.name);
1991 self.generate_extensible_enum(schema, known_values, ext, type_context)
1992 }
1993 SchemaType::Object {
1994 properties,
1995 required,
1996 additional_properties,
1997 variant,
1998 } => self.generate_struct(
1999 schema,
2000 ObjectShape {
2001 properties,
2002 required,
2003 additional_properties,
2004 variant: variant.as_ref(),
2005 },
2006 analysis,
2007 type_context,
2008 ),
2009 SchemaType::DiscriminatedUnion {
2010 discriminator_field,
2011 variants,
2012 exclusive,
2013 } => {
2014 if self.should_use_untagged_discriminated_union(schema, analysis) {
2016 let schema_refs: Vec<crate::analysis::SchemaRef> = variants
2018 .iter()
2019 .map(|v| crate::analysis::SchemaRef {
2020 target: v.type_name.clone(),
2021 nullable: false,
2022 })
2023 .collect();
2024 self.generate_union_enum(schema, &schema_refs, *exclusive, analysis)
2025 } else {
2026 self.generate_discriminated_enum(
2027 schema,
2028 discriminator_field,
2029 variants,
2030 *exclusive,
2031 analysis,
2032 )
2033 }
2034 }
2035 SchemaType::Union {
2036 variants,
2037 exclusive,
2038 } => self.generate_union_enum(schema, variants, *exclusive, analysis),
2039 SchemaType::Reference { target } => {
2040 if schema.name != *target {
2043 let alias_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2045 let target_type = format_ident!("{}", self.to_rust_type_name(target));
2046
2047 let doc_comment = if let Some(desc) = &schema.description {
2048 quote! { #[doc = #desc] }
2049 } else {
2050 TokenStream::new()
2051 };
2052
2053 Ok(quote! {
2054 #doc_comment
2055 pub type #alias_name = #target_type;
2056 })
2057 } else {
2058 Ok(TokenStream::new())
2060 }
2061 }
2062 SchemaType::Untyped { shape, .. } => {
2063 self.generate_type_alias(schema, untyped_rust_type(*shape))
2064 }
2065 SchemaType::Tuple { element_types } => {
2066 let tuple_type = self.generate_tuple_type(element_types, analysis);
2067 let type_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2068 let doc_comment = if let Some(description) = &schema.description {
2069 let sanitized = self.sanitize_doc_comment(description);
2070 quote! { #[doc = #sanitized] }
2071 } else {
2072 TokenStream::new()
2073 };
2074 Ok(quote! {
2075 #doc_comment
2076 pub type #type_name = #tuple_type;
2077 })
2078 }
2079 SchemaType::Array { item_type } => {
2080 let array_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2083
2084 let inner_type = self.generate_array_item_type(item_type, analysis);
2085
2086 let doc_comment = if let Some(desc) = &schema.description {
2087 quote! { #[doc = #desc] }
2088 } else {
2089 TokenStream::new()
2090 };
2091
2092 Ok(quote! {
2093 #doc_comment
2094 pub type #array_name = Vec<#inner_type>;
2095 })
2096 }
2097 SchemaType::Nullable { inner_type } => {
2098 let nullable_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2099 let inner_type = self.generate_array_item_type(inner_type, analysis);
2100 Ok(quote! {
2101 pub type #nullable_name = Option<#inner_type>;
2102 })
2103 }
2104 SchemaType::Composition { schemas } => {
2105 self.generate_composition_struct(schema, schemas)
2106 }
2107 }
2108 }
2109
2110 fn generate_type_alias(
2111 &self,
2112 schema: &crate::analysis::AnalyzedSchema,
2113 rust_type: &str,
2114 ) -> Result<TokenStream> {
2115 let type_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2116 let base_type = parse_rust_type(rust_type)?;
2120
2121 let doc_comment = if let Some(desc) = &schema.description {
2122 let sanitized_desc = self.sanitize_doc_comment(desc);
2123 quote! { #[doc = #sanitized_desc] }
2124 } else {
2125 TokenStream::new()
2126 };
2127
2128 Ok(quote! {
2129 #doc_comment
2130 pub type #type_name = #base_type;
2131 })
2132 }
2133
2134 fn generate_extensible_enum(
2135 &self,
2136 schema: &crate::analysis::AnalyzedSchema,
2137 known_values: &[String],
2138 ext: Option<&crate::analysis::EnumExtensions>,
2139 type_context: &TypeGenerationContext<'_>,
2140 ) -> Result<TokenStream> {
2141 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2142
2143 let doc_comment = if let Some(desc) = &schema.description {
2144 quote! { #[doc = #desc] }
2145 } else {
2146 TokenStream::new()
2147 };
2148
2149 let varnames_override: Option<&Vec<String>> = ext
2153 .filter(|_| self.config.types.x_enum_varnames_enabled())
2154 .map(|e| &e.varnames)
2155 .filter(|v| !v.is_empty() && v.len() == known_values.len());
2156 let descriptions_override: Option<&Vec<String>> = ext
2157 .filter(|_| self.config.types.x_enum_descriptions_enabled())
2158 .map(|e| &e.descriptions)
2159 .filter(|v| !v.is_empty() && v.len() == known_values.len());
2160
2161 let variant_ident_for = |index: usize, value: &str| -> proc_macro2::Ident {
2162 let name = match varnames_override {
2163 Some(v) => v[index].clone(),
2164 None => self.to_rust_enum_variant(value),
2165 };
2166 format_ident!("{}", name)
2167 };
2168
2169 if let Some(metadata) = type_context.enum_wire_names {
2170 let mut variants: BTreeMap<_, _> = known_values
2171 .iter()
2172 .enumerate()
2173 .map(|(index, value)| {
2174 (
2175 variant_ident_for(index, value).to_string(),
2176 Some(value.clone()),
2177 )
2178 })
2179 .collect();
2180 variants.insert("Custom".to_string(), None);
2181 metadata
2182 .borrow_mut()
2183 .insert(enum_name.to_string(), variants);
2184 }
2185
2186 let known_variants = known_values.iter().enumerate().map(|(i, value)| {
2191 let variant_ident = variant_ident_for(i, value);
2192 let doc = descriptions_override
2193 .map(|d| {
2194 let s = self.sanitize_doc_comment(&d[i]);
2195 quote! { #[doc = #s] }
2196 })
2197 .unwrap_or_default();
2198 quote! {
2199 #doc
2200 #variant_ident,
2201 }
2202 });
2203
2204 let match_arms_de = known_values.iter().enumerate().map(|(i, value)| {
2205 let variant_ident = variant_ident_for(i, value);
2206 quote! {
2207 #value => Ok(#enum_name::#variant_ident),
2208 }
2209 });
2210
2211 let match_arms_ser = known_values.iter().enumerate().map(|(i, value)| {
2212 let variant_ident = variant_ident_for(i, value);
2213 quote! {
2214 #enum_name::#variant_ident => #value,
2215 }
2216 });
2217
2218 let derives = if self.config.enable_specta {
2219 quote! {
2220 #[derive(Debug, Clone, PartialEq, Eq)]
2221 #[cfg_attr(feature = "specta", derive(specta::Type))]
2222 }
2223 } else {
2224 quote! {
2225 #[derive(Debug, Clone, PartialEq, Eq)]
2226 }
2227 };
2228
2229 Ok(quote! {
2230 #doc_comment
2231 #derives
2232 pub enum #enum_name {
2233 #(#known_variants)*
2234 Custom(String),
2236 }
2237
2238 impl<'de> serde::Deserialize<'de> for #enum_name {
2239 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2240 where
2241 D: serde::Deserializer<'de>,
2242 {
2243 let value = String::deserialize(deserializer)?;
2244 match value.as_str() {
2245 #(#match_arms_de)*
2246 _ => Ok(#enum_name::Custom(value)),
2247 }
2248 }
2249 }
2250
2251 impl serde::Serialize for #enum_name {
2252 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2253 where
2254 S: serde::Serializer,
2255 {
2256 serializer.serialize_str(self.as_str())
2257 }
2258 }
2259
2260 impl #enum_name {
2261 pub fn as_str(&self) -> &str {
2262 match self {
2263 #(#match_arms_ser)*
2264 #enum_name::Custom(s) => s.as_str(),
2265 }
2266 }
2267 }
2268
2269 impl ::std::fmt::Display for #enum_name {
2270 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
2271 f.write_str(self.as_str())
2272 }
2273 }
2274
2275 impl AsRef<str> for #enum_name {
2276 fn as_ref(&self) -> &str {
2277 self.as_str()
2278 }
2279 }
2280 })
2281 }
2282
2283 fn generate_string_enum(
2284 &self,
2285 schema: &crate::analysis::AnalyzedSchema,
2286 values: &[String],
2287 ext: Option<&crate::analysis::EnumExtensions>,
2288 ) -> Result<TokenStream> {
2289 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2290
2291 let default_value = schema
2298 .default
2299 .as_ref()
2300 .and_then(|v| v.as_str())
2301 .map(|s| s.to_string());
2302 let has_default_match = match &default_value {
2303 Some(d) => values.iter().any(|v| v == d),
2304 None => !values.is_empty(),
2305 };
2306
2307 let varnames_override: Option<&Vec<String>> = ext
2311 .filter(|_| self.config.types.x_enum_varnames_enabled())
2312 .map(|e| &e.varnames)
2313 .filter(|v| !v.is_empty() && v.len() == values.len());
2314 let descriptions_override: Option<&Vec<String>> = ext
2315 .filter(|_| self.config.types.x_enum_descriptions_enabled())
2316 .map(|e| &e.descriptions)
2317 .filter(|v| !v.is_empty() && v.len() == values.len());
2318
2319 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
2326 let variant_pairs: Vec<(syn::Ident, &String, bool, Option<String>)> = values
2327 .iter()
2328 .enumerate()
2329 .map(|(i, value)| {
2330 let base = match varnames_override {
2331 Some(v) => v[i].clone(),
2332 None => self.to_rust_enum_variant(value),
2333 };
2334 let mut variant_name = base.clone();
2335 let mut suffix = 2;
2336 while !used.insert(variant_name.clone()) {
2337 variant_name = format!("{base}_{suffix}");
2338 suffix += 1;
2339 }
2340 let variant_ident = format_ident!("{}", variant_name);
2341 let is_default = if let Some(ref default) = default_value {
2342 value == default
2343 } else {
2344 i == 0
2345 };
2346 let description = descriptions_override.map(|d| d[i].clone());
2347 (variant_ident, value, is_default, description)
2348 })
2349 .collect();
2350
2351 let variants =
2352 variant_pairs
2353 .iter()
2354 .map(|(variant_ident, value, is_default, description)| {
2355 let doc = description
2356 .as_ref()
2357 .map(|d| {
2358 let s = self.sanitize_doc_comment(d);
2359 quote! { #[doc = #s] }
2360 })
2361 .unwrap_or_default();
2362 if *is_default {
2363 quote! {
2364 #doc
2365 #[default]
2366 #[serde(rename = #value)]
2367 #variant_ident,
2368 }
2369 } else {
2370 quote! {
2371 #doc
2372 #[serde(rename = #value)]
2373 #variant_ident,
2374 }
2375 }
2376 });
2377
2378 let as_str_arms = variant_pairs.iter().map(|(variant_ident, value, _, _)| {
2382 quote! { Self::#variant_ident => #value, }
2383 });
2384
2385 let doc_comment = if let Some(desc) = &schema.description {
2386 quote! { #[doc = #desc] }
2387 } else {
2388 TokenStream::new()
2389 };
2390
2391 let derives = match (self.config.enable_specta, has_default_match) {
2394 (true, true) => quote! {
2395 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
2396 #[cfg_attr(feature = "specta", derive(specta::Type))]
2397 },
2398 (true, false) => quote! {
2399 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
2400 #[cfg_attr(feature = "specta", derive(specta::Type))]
2401 },
2402 (false, true) => quote! {
2403 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
2404 },
2405 (false, false) => quote! {
2406 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
2407 },
2408 };
2409
2410 Ok(quote! {
2411 #doc_comment
2412 #derives
2413 pub enum #enum_name {
2414 #(#variants)*
2415 }
2416
2417 impl #enum_name {
2418 pub fn as_str(&self) -> &'static str {
2419 match self {
2420 #(#as_str_arms)*
2421 }
2422 }
2423 }
2424
2425 impl ::std::fmt::Display for #enum_name {
2426 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
2427 f.write_str(self.as_str())
2428 }
2429 }
2430
2431 impl AsRef<str> for #enum_name {
2432 fn as_ref(&self) -> &str {
2433 self.as_str()
2434 }
2435 }
2436 })
2437 }
2438
2439 fn variant_field_name(
2442 &self,
2443 properties: &BTreeMap<String, crate::analysis::PropertyInfo>,
2444 ) -> String {
2445 let taken = |candidate: &str| {
2446 properties
2447 .keys()
2448 .any(|name| self.to_rust_field_name(name) == candidate)
2449 };
2450 if !taken("variant") {
2451 return "variant".to_string();
2452 }
2453 let mut suffix = 2;
2454 loop {
2455 let candidate = format!("variant{suffix}");
2456 if !taken(&candidate) {
2457 return candidate;
2458 }
2459 suffix += 1;
2460 }
2461 }
2462
2463 fn generate_struct(
2464 &self,
2465 schema: &crate::analysis::AnalyzedSchema,
2466 object: ObjectShape<'_>,
2467 analysis: &crate::analysis::SchemaAnalysis,
2468 type_context: &TypeGenerationContext<'_>,
2469 ) -> Result<TokenStream> {
2470 let ObjectShape {
2471 properties,
2472 required,
2473 additional_properties,
2474 variant,
2475 } = object;
2476 let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2477 let emitted_properties = self.emitted_object_properties(
2478 &schema.name,
2479 properties,
2480 required,
2481 additional_properties,
2482 analysis,
2483 );
2484 let variant_field_ident =
2485 variant.map(|_| format_ident!("{}", self.variant_field_name(properties)));
2486
2487 let mut fields: Vec<TokenStream> = emitted_properties
2488 .iter()
2489 .map(|emitted| {
2490 let field_name = emitted.wire_name;
2491 let property = emitted.property;
2492 let field_ident = &emitted.ident;
2493 let field_type = &emitted.field_type;
2494 let serde_attrs = if variant.is_none() {
2495 self.generate_serde_field_attrs(
2496 &schema.name,
2497 field_name,
2498 field_ident,
2499 property,
2500 emitted.is_required,
2501 analysis,
2502 )
2503 } else {
2504 TokenStream::new()
2505 };
2506 let specta_attrs = self.generate_specta_field_attrs(field_name);
2507
2508 let doc_comment = if let Some(desc) = &property.description {
2509 let sanitized_desc = self.sanitize_doc_comment(desc);
2510 quote! { #[doc = #sanitized_desc] }
2511 } else {
2512 TokenStream::new()
2513 };
2514 let constraint_doc = self.generate_constraint_doc(&property.constraints);
2515
2516 quote! {
2517 #doc_comment
2518 #constraint_doc
2519 #serde_attrs
2520 #specta_attrs
2521 pub #field_ident: #field_type,
2522 }
2523 })
2524 .collect();
2525
2526 match additional_properties {
2532 crate::analysis::ObjectAdditionalProperties::Denied
2533 | crate::analysis::ObjectAdditionalProperties::Closed => {}
2534 crate::analysis::ObjectAdditionalProperties::Untyped => {
2535 let serde_flatten = if variant.is_none() {
2536 quote! { #[serde(flatten)] }
2537 } else {
2538 TokenStream::new()
2539 };
2540 fields.push(quote! {
2541 #serde_flatten
2543 pub additional_properties:
2544 std::collections::BTreeMap<String, serde_json::Value>,
2545 });
2546 }
2547 crate::analysis::ObjectAdditionalProperties::Typed { value_type } => {
2548 let value_tokens = self.generate_array_item_type(value_type, analysis);
2549 let serde_flatten = if variant.is_none() {
2550 quote! { #[serde(flatten)] }
2551 } else {
2552 TokenStream::new()
2553 };
2554 fields.push(quote! {
2555 #serde_flatten
2558 pub additional_properties:
2559 std::collections::BTreeMap<String, #value_tokens>,
2560 });
2561 }
2562 }
2563
2564 if let (Some(variant), Some(variant_field)) = (variant, variant_field_ident.as_ref()) {
2569 let variant_type = format_ident!("{}", self.to_rust_type_name(&variant.target));
2570 fields.push(quote! {
2571 pub #variant_field: #variant_type,
2573 });
2574 }
2575
2576 let doc_comment = if let Some(desc) = &schema.description {
2577 quote! { #[doc = #desc] }
2578 } else {
2579 TokenStream::new()
2580 };
2581
2582 let can_derive_default = variant.is_none() && required.is_empty();
2590
2591 let derives = match (
2595 self.config.enable_specta,
2596 can_derive_default,
2597 variant.is_some(),
2598 ) {
2599 (true, _, true) => quote! {
2600 #[derive(Debug, Clone)]
2601 #[cfg_attr(feature = "specta", derive(specta::Type))]
2602 },
2603 (false, _, true) => quote! {
2604 #[derive(Debug, Clone)]
2605 },
2606 (true, true, false) => quote! {
2607 #[derive(Debug, Clone, Deserialize, Serialize, Default)]
2608 #[cfg_attr(feature = "specta", derive(specta::Type))]
2609 },
2610 (true, false, false) => quote! {
2611 #[derive(Debug, Clone, Deserialize, Serialize)]
2612 #[cfg_attr(feature = "specta", derive(specta::Type))]
2613 },
2614 (false, true, false) => quote! {
2615 #[derive(Debug, Clone, Deserialize, Serialize, Default)]
2616 },
2617 (false, false, false) => quote! {
2618 #[derive(Debug, Clone, Deserialize, Serialize)]
2619 },
2620 };
2621
2622 let denies_unknown_fields = variant.is_none()
2638 && additional_properties.denies_unknown_keys()
2639 && analysis.untagged_union_branches.contains(&schema.name);
2640 let deny_unknown_fields = if denies_unknown_fields {
2641 quote! { #[serde(deny_unknown_fields)] }
2642 } else {
2643 TokenStream::new()
2644 };
2645
2646 let shared_variant_serde = if let (Some(variant), Some(variant_field)) =
2652 (variant, variant_field_ident.as_ref())
2653 {
2654 let variant_type = format_ident!("{}", self.to_rust_type_name(&variant.target));
2655 let helper_name = format_ident!("__{}Base", self.to_rust_type_name(&schema.name));
2656 let variant_properties = self.union_declared_properties(
2657 &variant.target,
2658 analysis,
2659 &mut std::collections::HashSet::new(),
2660 );
2661 let variant_projection_removals = emitted_properties
2662 .iter()
2663 .filter(|emitted| !variant_properties.contains(emitted.wire_name))
2664 .map(|emitted| {
2665 let wire_name = emitted.wire_name;
2666 quote! { object.remove(#wire_name); }
2667 })
2668 .collect::<Vec<_>>();
2669 let mut helper_fields: Vec<TokenStream> = emitted_properties
2670 .iter()
2671 .map(|emitted| {
2672 let field_name = emitted.wire_name;
2673 let property = emitted.property;
2674 let field_ident = &emitted.ident;
2675 let field_type = &emitted.field_type;
2676 let serde_attrs = self.generate_serde_field_attrs(
2677 &schema.name,
2678 field_name,
2679 field_ident,
2680 property,
2681 emitted.is_required,
2682 analysis,
2683 );
2684 quote! {
2685 #serde_attrs
2686 #field_ident: #field_type,
2687 }
2688 })
2689 .collect();
2690 let mut base_initializers: Vec<TokenStream> = emitted_properties
2691 .iter()
2692 .map(|emitted| {
2693 let field_ident = &emitted.ident;
2694 quote! { #field_ident: self.#field_ident.clone(), }
2695 })
2696 .collect();
2697 let mut result_fields: Vec<TokenStream> = emitted_properties
2698 .iter()
2699 .map(|emitted| {
2700 let field_ident = &emitted.ident;
2701 quote! { #field_ident: base.#field_ident, }
2702 })
2703 .collect();
2704
2705 match additional_properties {
2706 crate::analysis::ObjectAdditionalProperties::Denied
2707 | crate::analysis::ObjectAdditionalProperties::Closed => {}
2708 crate::analysis::ObjectAdditionalProperties::Untyped => {
2709 helper_fields.push(quote! {
2710 #[serde(flatten)]
2711 additional_properties:
2712 std::collections::BTreeMap<String, serde_json::Value>,
2713 });
2714 base_initializers.push(quote! {
2715 additional_properties: self.additional_properties.clone(),
2716 });
2717 result_fields.push(quote! {
2718 additional_properties: base.additional_properties,
2719 });
2720 }
2721 crate::analysis::ObjectAdditionalProperties::Typed { value_type } => {
2722 let value_tokens = self.generate_array_item_type(value_type, analysis);
2723 helper_fields.push(quote! {
2724 #[serde(flatten)]
2725 additional_properties:
2726 std::collections::BTreeMap<String, #value_tokens>,
2727 });
2728 base_initializers.push(quote! {
2729 additional_properties: self.additional_properties.clone(),
2730 });
2731 result_fields.push(quote! {
2732 additional_properties: base.additional_properties,
2733 });
2734 }
2735 }
2736
2737 quote! {
2738 #[derive(Deserialize, Serialize)]
2739 struct #helper_name {
2740 #(#helper_fields)*
2741 }
2742
2743 impl serde::Serialize for #struct_name {
2744 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2745 where
2746 S: serde::Serializer,
2747 {
2748 let base = #helper_name {
2749 #(#base_initializers)*
2750 };
2751 let mut value = serde_json::to_value(base)
2752 .map_err(serde::ser::Error::custom)?;
2753 let variant = serde_json::to_value(&self.#variant_field)
2754 .map_err(serde::ser::Error::custom)?;
2755 let object = value.as_object_mut().ok_or_else(|| {
2756 serde::ser::Error::custom("shared union base did not serialize as an object")
2757 })?;
2758 let variant_object = variant.as_object().ok_or_else(|| {
2759 serde::ser::Error::custom("shared union variant did not serialize as an object")
2760 })?;
2761 for (key, variant_value) in variant_object {
2762 if let Some(base_value) = object.get(key)
2763 && base_value != variant_value
2764 {
2765 return Err(serde::ser::Error::custom(format!(
2766 "shared union field `{key}` serialized conflicting values",
2767 )));
2768 }
2769 object.insert(key.clone(), variant_value.clone());
2770 }
2771 serde::Serialize::serialize(&value, serializer)
2772 }
2773 }
2774
2775 impl<'de> serde::Deserialize<'de> for #struct_name {
2776 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2777 where
2778 D: serde::Deserializer<'de>,
2779 {
2780 let value = serde_json::Value::deserialize(deserializer)?;
2781 let base = serde_json::from_value::<#helper_name>(value.clone())
2782 .map_err(serde::de::Error::custom)?;
2783 let variant = match serde_json::from_value::<#variant_type>(value.clone()) {
2784 Ok(variant) => variant,
2785 Err(complete_error) => {
2786 let mut variant_input = value;
2787 if let Some(object) = variant_input.as_object_mut() {
2788 #(#variant_projection_removals)*
2789 }
2790 serde_json::from_value::<#variant_type>(variant_input).map_err(
2791 |projected_error| serde::de::Error::custom(format!(
2792 "complete shared-union input failed: {complete_error}; projected input failed: {projected_error}",
2793 )),
2794 )?
2795 }
2796 };
2797 Ok(Self {
2798 #(#result_fields)*
2799 #variant_field: variant,
2800 })
2801 }
2802 }
2803 }
2804 } else {
2805 TokenStream::new()
2806 };
2807
2808 let builder = if type_context.index.request_body_roots.contains(&schema.name)
2809 && variant.is_none()
2810 && emitted_properties
2811 .iter()
2812 .any(|property| property.is_required)
2813 && (emitted_properties
2814 .iter()
2815 .any(|property| !property.is_required)
2816 || additional_properties.is_open())
2817 {
2818 self.generate_request_model_builder(
2819 schema,
2820 &emitted_properties,
2821 additional_properties,
2822 analysis,
2823 type_context.index,
2824 )
2825 } else {
2826 TokenStream::new()
2827 };
2828
2829 Ok(quote! {
2830 #doc_comment
2831 #derives
2832 #deny_unknown_fields
2833 pub struct #struct_name {
2834 #(#fields)*
2835 }
2836
2837 #shared_variant_serde
2838 #builder
2839 })
2840 }
2841
2842 pub(crate) fn emitted_object_properties<'a>(
2847 &self,
2848 schema_name: &str,
2849 properties: &'a BTreeMap<String, crate::analysis::PropertyInfo>,
2850 required: &std::collections::HashSet<String>,
2851 additional_properties: &crate::analysis::ObjectAdditionalProperties,
2852 analysis: &crate::analysis::SchemaAnalysis,
2853 ) -> Vec<EmittedObjectProperty<'a>> {
2854 let mut sorted_properties: Vec<_> = properties.iter().collect();
2855 sorted_properties.sort_by_key(|(name, _)| name.as_str());
2856
2857 let mut used_field_idents = std::collections::HashSet::new();
2858 if additional_properties.is_open() {
2859 used_field_idents.insert("additional_properties".to_string());
2860 }
2861
2862 let mut emitted = Vec::new();
2863 for (field_name, property) in sorted_properties {
2864 let raw = self.to_rust_field_name(field_name);
2865 let mut chosen = raw.clone();
2866 let mut suffix = 2;
2867 while !used_field_idents.insert(chosen.clone()) {
2868 chosen = format!("{raw}_{suffix}");
2869 suffix += 1;
2870 }
2871 let is_required = required.contains(field_name);
2872 emitted.push(EmittedObjectProperty {
2873 wire_name: field_name,
2874 property,
2875 ident: Self::to_field_ident(&chosen),
2876 is_required,
2877 field_type: self.generate_field_type(
2878 schema_name,
2879 field_name,
2880 property,
2881 is_required,
2882 analysis,
2883 ),
2884 });
2885 }
2886 emitted
2887 }
2888
2889 fn type_generation_index(
2890 &self,
2891 analysis: &crate::analysis::SchemaAnalysis,
2892 ) -> TypeGenerationIndex {
2893 let reserved_type_names = analysis
2894 .schemas
2895 .keys()
2896 .map(|name| self.to_rust_type_name(name))
2897 .collect();
2898 let mut request_body_roots = std::collections::HashSet::new();
2899 for operation in analysis.operations.values() {
2900 let Some(mut current) = operation
2901 .request_body
2902 .as_ref()
2903 .and_then(crate::analysis::RequestBodyContent::schema_name)
2904 else {
2905 continue;
2906 };
2907 while request_body_roots.insert(current.to_string()) {
2908 let Some(crate::analysis::AnalyzedSchema {
2909 schema_type: crate::analysis::SchemaType::Reference { target },
2910 ..
2911 }) = analysis.schemas.get(current)
2912 else {
2913 break;
2914 };
2915 current = target;
2916 }
2917 }
2918 TypeGenerationIndex {
2919 request_body_roots,
2920 reserved_type_names,
2921 }
2922 }
2923
2924 fn generate_request_model_builder(
2925 &self,
2926 schema: &crate::analysis::AnalyzedSchema,
2927 properties: &[EmittedObjectProperty<'_>],
2928 additional_properties: &crate::analysis::ObjectAdditionalProperties,
2929 analysis: &crate::analysis::SchemaAnalysis,
2930 type_index: &TypeGenerationIndex,
2931 ) -> TokenStream {
2932 let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2933 let builder_base = format!("{}Builder", struct_name);
2934 let mut builder_name = builder_base.clone();
2935 let mut suffix = 2;
2936 while type_index.reserved_type_names.contains(&builder_name) {
2937 builder_name = format!("{builder_base}{suffix}");
2938 suffix += 1;
2939 }
2940 let builder_name = format_ident!("{builder_name}");
2941
2942 let required_parameters: Vec<TokenStream> = properties
2943 .iter()
2944 .filter(|property| property.is_required)
2945 .map(|property| {
2946 let ident = &property.ident;
2947 let field_type = &property.field_type;
2948 quote! { #ident: #field_type }
2949 })
2950 .collect();
2951 let required_idents: Vec<&syn::Ident> = properties
2952 .iter()
2953 .filter(|property| property.is_required)
2954 .map(|property| &property.ident)
2955 .collect();
2956 let optional_initializers: Vec<TokenStream> = properties
2957 .iter()
2958 .filter(|property| !property.is_required)
2959 .map(|property| {
2960 let ident = &property.ident;
2961 quote! { #ident: None }
2962 })
2963 .collect();
2964
2965 let additional_initializer = match additional_properties {
2966 crate::analysis::ObjectAdditionalProperties::Denied
2967 | crate::analysis::ObjectAdditionalProperties::Closed => TokenStream::new(),
2968 crate::analysis::ObjectAdditionalProperties::Untyped
2969 | crate::analysis::ObjectAdditionalProperties::Typed { .. } => quote! {
2970 additional_properties: ::std::collections::BTreeMap::new(),
2971 },
2972 };
2973
2974 let mut used_builder_methods =
2975 std::collections::HashSet::from(["new".to_string(), "build".to_string()]);
2976 if additional_properties.is_open() {
2977 used_builder_methods.insert("additional_properties".to_string());
2978 }
2979 let optional_setters: Vec<TokenStream> = properties
2980 .iter()
2981 .filter(|property| !property.is_required)
2982 .map(|property| {
2983 let field_ident = &property.ident;
2984 let field_type = self.generate_property_base_type(
2985 &schema.name,
2986 property.wire_name,
2987 property.property,
2988 analysis,
2989 );
2990 let field_name = field_ident.to_string();
2994 let plain_field_name = field_name.strip_prefix("r#").unwrap_or(&field_name);
2995 let mut setter_name = if matches!(plain_field_name, "new" | "build") {
2996 format!("with_{plain_field_name}")
2997 } else {
2998 field_name.clone()
2999 };
3000 let setter_base = setter_name.clone();
3001 let mut suffix = 2;
3002 while !used_builder_methods.insert(setter_name.clone()) {
3003 setter_name = format!("{setter_base}_{suffix}");
3004 suffix += 1;
3005 }
3006 let setter_ident = Self::to_field_ident(&setter_name);
3007 let wire_name = property.wire_name;
3008 if self.property_is_tri_state(
3009 &schema.name,
3010 property.wire_name,
3011 property.property,
3012 property.is_required,
3013 ) {
3014 let mut null_name = format!("{plain_field_name}_null");
3015 let null_base = null_name.clone();
3016 let mut suffix = 2;
3017 while !used_builder_methods.insert(null_name.clone()) {
3018 null_name = format!("{null_base}_{suffix}");
3019 suffix += 1;
3020 }
3021 let null_ident = Self::to_field_ident(&null_name);
3022
3023 let mut absent_name = format!("{plain_field_name}_absent");
3024 let absent_base = absent_name.clone();
3025 let mut suffix = 2;
3026 while !used_builder_methods.insert(absent_name.clone()) {
3027 absent_name = format!("{absent_base}_{suffix}");
3028 suffix += 1;
3029 }
3030 let absent_ident = Self::to_field_ident(&absent_name);
3031
3032 quote! {
3033 #[doc = concat!("Set the optional nullable `", #wire_name, "` request field to a value.")]
3034 #[must_use]
3035 pub fn #setter_ident(mut self, #field_ident: #field_type) -> Self {
3036 self.value.#field_ident = Some(Some(#field_ident));
3037 self
3038 }
3039
3040 #[doc = concat!("Set the optional nullable `", #wire_name, "` request field to JSON null.")]
3041 #[must_use]
3042 pub fn #null_ident(mut self) -> Self {
3043 self.value.#field_ident = Some(None);
3044 self
3045 }
3046
3047 #[doc = concat!("Omit the optional nullable `", #wire_name, "` request field.")]
3048 #[must_use]
3049 pub fn #absent_ident(mut self) -> Self {
3050 self.value.#field_ident = None;
3051 self
3052 }
3053 }
3054 } else {
3055 quote! {
3056 #[doc = concat!("Set the optional `", #wire_name, "` request field.")]
3057 #[must_use]
3058 pub fn #setter_ident(mut self, #field_ident: #field_type) -> Self {
3059 self.value.#field_ident = Some(#field_ident);
3060 self
3061 }
3062 }
3063 }
3064 })
3065 .collect();
3066
3067 let additional_setter = match additional_properties {
3068 crate::analysis::ObjectAdditionalProperties::Denied
3069 | crate::analysis::ObjectAdditionalProperties::Closed => TokenStream::new(),
3070 crate::analysis::ObjectAdditionalProperties::Untyped => quote! {
3071 #[must_use]
3073 pub fn additional_properties(
3074 mut self,
3075 additional_properties: ::std::collections::BTreeMap<
3076 String,
3077 serde_json::Value,
3078 >,
3079 ) -> Self {
3080 self.value.additional_properties = additional_properties;
3081 self
3082 }
3083 },
3084 crate::analysis::ObjectAdditionalProperties::Typed { value_type } => {
3085 let value_type = self.generate_array_item_type(value_type, analysis);
3086 quote! {
3087 #[must_use]
3089 pub fn additional_properties(
3090 mut self,
3091 additional_properties: ::std::collections::BTreeMap<
3092 String,
3093 #value_type,
3094 >,
3095 ) -> Self {
3096 self.value.additional_properties = additional_properties;
3097 self
3098 }
3099 }
3100 }
3101 };
3102
3103 quote! {
3104 impl #struct_name {
3105 pub fn new(#(#required_parameters),*) -> Self {
3107 Self {
3108 #(#required_idents,)*
3109 #(#optional_initializers,)*
3110 #additional_initializer
3111 }
3112 }
3113
3114 pub fn builder(#(#required_parameters),*) -> #builder_name {
3116 #builder_name::new(#(#required_idents),*)
3117 }
3118 }
3119
3120 #[derive(Debug, Clone)]
3122 #[must_use]
3123 pub struct #builder_name {
3124 value: #struct_name,
3125 }
3126
3127 impl #builder_name {
3128 pub fn new(#(#required_parameters),*) -> Self {
3130 Self {
3131 value: #struct_name::new(#(#required_idents),*),
3132 }
3133 }
3134
3135 #(#optional_setters)*
3136 #additional_setter
3137
3138 pub fn build(self) -> #struct_name {
3140 self.value
3141 }
3142 }
3143 }
3144 }
3145
3146 fn generate_discriminated_enum(
3147 &self,
3148 schema: &crate::analysis::AnalyzedSchema,
3149 discriminator_field: &str,
3150 variants: &[crate::analysis::UnionVariant],
3151 exclusive: bool,
3152 analysis: &crate::analysis::SchemaAnalysis,
3153 ) -> Result<TokenStream> {
3154 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
3155
3156 let has_nested_discriminated_union = variants.iter().any(|variant| {
3158 if let Some(variant_schema) = analysis.schemas.get(&variant.type_name) {
3159 matches!(
3160 variant_schema.schema_type,
3161 crate::analysis::SchemaType::DiscriminatedUnion { .. }
3162 )
3163 } else {
3164 false
3165 }
3166 });
3167
3168 if has_nested_discriminated_union {
3170 let schema_refs: Vec<crate::analysis::SchemaRef> = variants
3172 .iter()
3173 .map(|v| crate::analysis::SchemaRef {
3174 target: v.type_name.clone(),
3175 nullable: false,
3176 })
3177 .collect();
3178 return self.generate_union_enum(schema, &schema_refs, exclusive, analysis);
3179 }
3180
3181 let enclosing = self.to_rust_type_name(&schema.name);
3182 let variant_shapes: Vec<_> = variants
3183 .iter()
3184 .map(|variant| {
3185 let variant_name = format_ident!("{}", variant.rust_name);
3186 let variant_type = format_ident!("{}", self.to_rust_type_name(&variant.type_name));
3187 let payload = if self.to_rust_type_name(&variant.type_name) == enclosing
3191 || analysis
3192 .dependencies
3193 .recursive_schemas
3194 .contains(&variant.type_name)
3195 {
3196 quote! { Box<#variant_type> }
3197 } else {
3198 quote! { #variant_type }
3199 };
3200 (variant, variant_name, payload)
3201 })
3202 .collect();
3203 let enum_variants = variant_shapes.iter().map(|(_, variant_name, payload)| {
3204 quote! { #variant_name(#payload), }
3205 });
3206 let serialize_arms = variant_shapes.iter().map(|(variant, variant_name, _)| {
3207 let canonical_value = &variant.discriminator_value;
3208 let variant_values = &variant.discriminator_values;
3209 let serialize_discriminator = if variant.discriminator_field_declared {
3210 quote! {
3211 match object.get(#discriminator_field) {
3212 Some(serde_json::Value::String(tag))
3213 if matches!(tag.as_str(), #(#variant_values)|*) => {}
3214 Some(serde_json::Value::String(tag)) => {
3215 return Err(serde::ser::Error::custom(format!(
3216 "discriminator `{}` value `{tag}` is not valid for variant `{}`",
3217 #discriminator_field,
3218 stringify!(#variant_name),
3219 )));
3220 }
3221 Some(_) => {
3222 return Err(serde::ser::Error::custom(concat!(
3223 "discriminator `",
3224 #discriminator_field,
3225 "` did not serialize as a string",
3226 )));
3227 }
3228 None => {
3229 object.insert(
3230 #discriminator_field.to_string(),
3231 serde_json::Value::String(#canonical_value.to_string()),
3232 );
3233 }
3234 }
3235 }
3236 } else {
3237 TokenStream::new()
3238 };
3239 quote! {
3240 Self::#variant_name(payload) => {
3241 let mut value = serde_json::to_value(payload)
3242 .map_err(serde::ser::Error::custom)?;
3243 let object = value.as_object_mut().ok_or_else(|| {
3244 serde::ser::Error::custom(concat!(
3245 "discriminated union variant `",
3246 stringify!(#variant_name),
3247 "` did not serialize as an object",
3248 ))
3249 })?;
3250 #serialize_discriminator
3251 value.serialize(serializer)
3252 }
3253 }
3254 });
3255 let mut deserialize_arms = Vec::new();
3256 for (primary_index, (variant, variant_name, payload)) in variant_shapes.iter().enumerate() {
3257 for tag in &variant.preferred_discriminator_values {
3258 let fallback_attempts = variant_shapes.iter().enumerate().filter_map(
3259 |(fallback_index, (_, fallback_name, fallback_payload))| {
3260 if fallback_index == primary_index {
3261 return None;
3262 }
3263 if exclusive {
3264 Some(quote! {
3265 if let Ok(payload) =
3266 serde_json::from_value::<#fallback_payload>(value.clone())
3267 {
3268 if let Some((_, first_name)) = &structural_match {
3269 return Err(serde::de::Error::custom(format!(
3270 "discriminator `{}` value `{}` did not fit its mapped branch and structurally matched both `{}` and `{}`",
3271 #discriminator_field,
3272 #tag,
3273 first_name,
3274 stringify!(#fallback_name),
3275 )));
3276 }
3277 structural_match = Some((
3278 Self::#fallback_name(payload),
3279 stringify!(#fallback_name),
3280 ));
3281 }
3282 })
3283 } else {
3284 Some(quote! {
3285 if let Ok(payload) =
3286 serde_json::from_value::<#fallback_payload>(value.clone())
3287 {
3288 return Ok(Self::#fallback_name(payload));
3289 }
3290 })
3291 }
3292 },
3293 );
3294 let structural_fallback = if exclusive {
3295 quote! {
3296 let mut structural_match: Option<(Self, &'static str)> = None;
3297 #(#fallback_attempts)*
3298 match structural_match {
3299 Some((payload, _)) => Ok(payload),
3300 None => Err(serde::de::Error::custom(primary_error)),
3301 }
3302 }
3303 } else {
3304 quote! {
3305 #(#fallback_attempts)*
3306 Err(serde::de::Error::custom(primary_error))
3307 }
3308 };
3309 deserialize_arms.push(quote! {
3310 #tag => {
3311 let primary_error = match serde_json::from_value::<#payload>(value.clone()) {
3312 Ok(payload) => return Ok(Self::#variant_name(payload)),
3313 Err(error) => error,
3314 };
3315 #structural_fallback
3316 }
3317 });
3318 }
3319 }
3320 let missing_discriminator_attempts = variant_shapes.iter().filter_map(
3321 |(variant, variant_name, payload)| {
3322 if variant.discriminator_field_required {
3323 return None;
3324 }
3325 if exclusive {
3326 Some(quote! {
3327 if let Ok(payload) = serde_json::from_value::<#payload>(value.clone()) {
3328 if let Some((_, first_name)) = &structural_match {
3329 return Err(serde::de::Error::custom(format!(
3330 "missing discriminator `{}` structurally matched both `{}` and `{}`",
3331 #discriminator_field,
3332 first_name,
3333 stringify!(#variant_name),
3334 )));
3335 }
3336 structural_match = Some((
3337 Self::#variant_name(payload),
3338 stringify!(#variant_name),
3339 ));
3340 }
3341 })
3342 } else {
3343 Some(quote! {
3344 if let Ok(payload) = serde_json::from_value::<#payload>(value.clone()) {
3345 return Ok(Self::#variant_name(payload));
3346 }
3347 })
3348 }
3349 },
3350 );
3351 let has_missing_discriminator_candidates = variants
3352 .iter()
3353 .any(|variant| !variant.discriminator_field_required);
3354 let missing_discriminator_fallback = if has_missing_discriminator_candidates && exclusive {
3355 quote! {
3356 let mut structural_match: Option<(Self, &'static str)> = None;
3357 #(#missing_discriminator_attempts)*
3358 structural_match
3359 .map(|(payload, _)| payload)
3360 .ok_or_else(|| serde::de::Error::custom(concat!(
3361 "missing string discriminator `",
3362 #discriminator_field,
3363 "` and no tagless branch matched",
3364 )))
3365 }
3366 } else if has_missing_discriminator_candidates {
3367 quote! {
3368 #(#missing_discriminator_attempts)*
3369 Err(serde::de::Error::custom(concat!(
3370 "missing string discriminator `",
3371 #discriminator_field,
3372 "` and no tagless branch matched",
3373 )))
3374 }
3375 } else {
3376 quote! {
3377 Err(serde::de::Error::custom(concat!(
3378 "missing string discriminator `",
3379 #discriminator_field,
3380 "`",
3381 )))
3382 }
3383 };
3384
3385 let doc_comment = if let Some(desc) = &schema.description {
3386 quote! { #[doc = #desc] }
3387 } else {
3388 TokenStream::new()
3389 };
3390
3391 let derives = if self.config.enable_specta {
3398 quote! {
3399 #[derive(Debug, Clone)]
3400 #[cfg_attr(feature = "specta", derive(specta::Type))]
3401 }
3402 } else {
3403 quote! {
3404 #[derive(Debug, Clone)]
3405 }
3406 };
3407
3408 Ok(quote! {
3409 #doc_comment
3410 #derives
3411 pub enum #enum_name {
3412 #(#enum_variants)*
3413 }
3414
3415 impl serde::Serialize for #enum_name {
3416 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
3417 where
3418 S: serde::Serializer,
3419 {
3420 match self {
3421 #(#serialize_arms)*
3422 }
3423 }
3424 }
3425
3426 impl<'de> serde::Deserialize<'de> for #enum_name {
3427 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3428 where
3429 D: serde::Deserializer<'de>,
3430 {
3431 let value = serde_json::Value::deserialize(deserializer)?;
3432 let discriminator = match value.get(#discriminator_field) {
3433 Some(serde_json::Value::String(discriminator)) => {
3434 Some(discriminator.as_str())
3435 }
3436 Some(_) => {
3437 return Err(serde::de::Error::custom(concat!(
3438 "non-string discriminator `",
3439 #discriminator_field,
3440 "`",
3441 )));
3442 }
3443 None => None,
3444 };
3445 match discriminator {
3446 Some(discriminator) => match discriminator {
3447 #(#deserialize_arms)*
3448 other => Err(serde::de::Error::custom(format!(
3449 "unknown discriminator value `{other}` for `{}`",
3450 #discriminator_field,
3451 ))),
3452 },
3453 None => { #missing_discriminator_fallback }
3454 }
3455 }
3456 }
3457 })
3458 }
3459
3460 fn should_use_untagged_discriminated_union(
3462 &self,
3463 schema: &crate::analysis::AnalyzedSchema,
3464 analysis: &crate::analysis::SchemaAnalysis,
3465 ) -> bool {
3466 for other_schema in analysis.schemas.values() {
3471 if let crate::analysis::SchemaType::DiscriminatedUnion { variants, .. } =
3472 &other_schema.schema_type
3473 {
3474 for variant in variants {
3475 if variant.type_name == schema.name {
3476 if let crate::analysis::SchemaType::DiscriminatedUnion {
3481 discriminator_field: current_discriminator,
3482 variants: current_variants,
3483 ..
3484 } = &schema.schema_type
3485 {
3486 for current_variant in current_variants {
3488 if let Some(variant_schema) =
3489 analysis.schemas.get(¤t_variant.type_name)
3490 {
3491 if let crate::analysis::SchemaType::Object {
3492 properties, ..
3493 } = &variant_schema.schema_type
3494 {
3495 if properties.contains_key(current_discriminator) {
3496 return false;
3499 }
3500 }
3501 }
3502 }
3503 }
3504
3505 return true;
3507 }
3508 }
3509 }
3510 }
3511 false
3512 }
3513
3514 fn generate_union_enum(
3515 &self,
3516 schema: &crate::analysis::AnalyzedSchema,
3517 variants: &[crate::analysis::SchemaRef],
3518 exclusive: bool,
3519 analysis: &crate::analysis::SchemaAnalysis,
3520 ) -> Result<TokenStream> {
3521 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
3522
3523 let mut used_variant_names = std::collections::HashSet::new();
3525 let enum_variants = variants
3526 .iter()
3527 .enumerate()
3528 .map(|(i, variant)| {
3529 let base_variant_name = self.type_name_to_variant_name(&variant.target);
3531 let variant_name = self.ensure_unique_variant_name_generator(
3532 base_variant_name,
3533 &mut used_variant_names,
3534 i,
3535 );
3536 let variant_name_ident = format_ident!("{}", variant_name);
3537
3538 let variant_type_tokens = if matches!(
3540 variant.target.as_str(),
3541 "bool"
3542 | "i8"
3543 | "i16"
3544 | "i32"
3545 | "i64"
3546 | "i128"
3547 | "u8"
3548 | "u16"
3549 | "u32"
3550 | "u64"
3551 | "u128"
3552 | "f32"
3553 | "f64"
3554 | "String"
3555 ) {
3556 let type_ident = format_ident!("{}", variant.target);
3557 quote! { #type_ident }
3558 } else if variant.target == "serde_json::Value" {
3559 quote! { serde_json::Value }
3562 } else if variant.target.starts_with("Vec<") && variant.target.ends_with(">") {
3563 let inner = &variant.target[4..variant.target.len() - 1];
3565
3566 if inner.starts_with("Vec<") && inner.ends_with(">") {
3568 let inner_inner = &inner[4..inner.len() - 1];
3569 if inner_inner == "serde_json::Value" {
3570 quote! { Vec<Vec<serde_json::Value>> }
3571 } else {
3572 let inner_inner_type = if matches!(
3573 inner_inner,
3574 "bool"
3575 | "i8"
3576 | "i16"
3577 | "i32"
3578 | "i64"
3579 | "i128"
3580 | "u8"
3581 | "u16"
3582 | "u32"
3583 | "u64"
3584 | "u128"
3585 | "f32"
3586 | "f64"
3587 | "String"
3588 ) {
3589 format_ident!("{}", inner_inner)
3590 } else {
3591 format_ident!("{}", self.to_rust_type_name(inner_inner))
3592 };
3593 quote! { Vec<Vec<#inner_inner_type>> }
3594 }
3595 } else if inner == "serde_json::Value" {
3596 quote! { Vec<serde_json::Value> }
3597 } else {
3598 let inner_type = if matches!(
3599 inner,
3600 "bool"
3601 | "i8"
3602 | "i16"
3603 | "i32"
3604 | "i64"
3605 | "i128"
3606 | "u8"
3607 | "u16"
3608 | "u32"
3609 | "u64"
3610 | "u128"
3611 | "f32"
3612 | "f64"
3613 | "String"
3614 ) {
3615 format_ident!("{}", inner)
3616 } else {
3617 format_ident!("{}", self.to_rust_type_name(inner))
3618 };
3619 quote! { Vec<#inner_type> }
3620 }
3621 } else if variant.target.contains("::") || variant.target.contains('<') {
3622 parse_rust_type(&variant.target).unwrap_or_else(|_| {
3627 let fallback = format_ident!("{}", self.to_rust_type_name(&variant.target));
3628 quote! { #fallback }
3629 })
3630 } else {
3631 let type_ident = format_ident!("{}", self.to_rust_type_name(&variant.target));
3632 quote! { #type_ident }
3633 };
3634
3635 let target_rust_name = self.to_rust_type_name(&variant.target);
3639 let enclosing_name = self.to_rust_type_name(&schema.name);
3640 let is_self_ref = target_rust_name == enclosing_name;
3641 let is_recursive_target = analysis
3645 .dependencies
3646 .recursive_schemas
3647 .contains(&variant.target);
3648 let variant_type_tokens = if is_self_ref || is_recursive_target {
3649 quote! { Box<#variant_type_tokens> }
3650 } else {
3651 variant_type_tokens
3652 };
3653 let variant_type_tokens = if variant.nullable {
3654 quote! { Option<#variant_type_tokens> }
3655 } else {
3656 variant_type_tokens
3657 };
3658
3659 (variant_name_ident, variant_type_tokens)
3660 })
3661 .collect::<Vec<_>>();
3662 let variant_declarations = enum_variants
3663 .iter()
3664 .map(|(variant_name, variant_type)| quote! { #variant_name(#variant_type), })
3665 .collect::<Vec<_>>();
3666
3667 let doc_comment = if let Some(desc) = &schema.description {
3668 quote! { #[doc = #desc] }
3669 } else {
3670 TokenStream::new()
3671 };
3672
3673 let object_only = variants.iter().all(|variant| {
3674 self.union_target_serializes_as_object(
3675 &variant.target,
3676 analysis,
3677 &mut std::collections::HashSet::new(),
3678 )
3679 });
3680
3681 if exclusive || object_only {
3682 let derives = if self.config.enable_specta {
3683 quote! {
3684 #[derive(Debug, Clone)]
3685 #[cfg_attr(feature = "specta", derive(specta::Type))]
3686 }
3687 } else {
3688 quote! { #[derive(Debug, Clone)] }
3689 };
3690 let serialize_arms = enum_variants
3691 .iter()
3692 .map(|(variant_name, _)| {
3693 quote! {
3694 Self::#variant_name(value) => serde::Serialize::serialize(value, serializer),
3695 }
3696 })
3697 .collect::<Vec<_>>();
3698 let deserialize_attempts = enum_variants
3699 .iter()
3700 .zip(variants)
3701 .map(|((variant_name, variant_type), variant)| {
3702 if exclusive {
3703 let constraints = self.union_branch_literal_constraints(
3704 &variant.target,
3705 analysis,
3706 &mut std::collections::HashSet::new(),
3707 );
3708 let constraint_checks =
3709 constraints.iter().map(|(field, (required, allowed))| {
3710 let allowed = allowed
3711 .iter()
3712 .map(serde_json::Value::to_string)
3713 .collect::<Vec<_>>();
3714 if allowed.is_empty() && *required {
3715 quote! { false }
3716 } else if allowed.is_empty() {
3717 quote! { object.get(#field).is_none() }
3718 } else if *required {
3719 quote! {
3720 object.get(#field).is_some_and(|value| {
3721 value.is_null()
3722 || matches!(value.to_string().as_str(), #(#allowed)|*)
3723 })
3724 }
3725 } else {
3726 quote! {
3727 object.get(#field).is_none_or(|value| {
3728 value.is_null()
3729 || matches!(value.to_string().as_str(), #(#allowed)|*)
3730 })
3731 }
3732 }
3733 });
3734 let constraints_match = if constraints.is_empty() {
3735 quote! { true }
3736 } else {
3737 quote! {
3738 input.as_object().is_some_and(|object| {
3739 true #(&& #constraint_checks)*
3740 })
3741 }
3742 };
3743 quote! {
3744 if #constraints_match {
3745 if let Ok(candidate) =
3746 serde_json::from_value::<#variant_type>(input.clone())
3747 {
3748 match serde_json::to_value(&candidate) {
3749 Ok(encoded) if encoded == input => {
3750 if matched.is_some() {
3751 return Err(serde::de::Error::custom(concat!(
3752 "ambiguous oneOf value for ",
3753 stringify!(#enum_name),
3754 ": more than one branch preserved the complete input",
3755 )));
3756 }
3757 matched = Some(Self::#variant_name(candidate));
3758 }
3759 Ok(encoded)
3760 if preserves_complete_json_input(
3761 &encoded, &input, true, false,
3762 ) =>
3763 {
3764 equivalent_matches += 1;
3765 equivalent.get_or_insert(Self::#variant_name(candidate));
3766 }
3767 _ => {}
3768 }
3769 }
3770 }
3771 }
3772 } else {
3773 quote! {
3774 if let Ok(candidate) =
3775 serde_json::from_value::<#variant_type>(input.clone())
3776 {
3777 match serde_json::to_value(&candidate) {
3778 Ok(encoded)
3779 if preserves_complete_json_input(
3780 &encoded, &input, false, true,
3781 ) =>
3782 {
3783 return Ok(Self::#variant_name(candidate));
3784 }
3785 Ok(encoded)
3786 if equivalent.is_none()
3787 && preserves_complete_json_input(
3788 &encoded, &input, true, false,
3789 ) =>
3790 {
3791 equivalent = Some(Self::#variant_name(candidate));
3792 }
3793 _ => {}
3794 }
3795 }
3796 }
3797 }
3798 })
3799 .collect::<Vec<_>>();
3800 let no_match = if exclusive {
3805 quote! {
3806 if let Some(matched) = matched {
3807 return Ok(matched);
3808 }
3809 if equivalent_matches > 1 {
3810 return Err(serde::de::Error::custom(concat!(
3811 "ambiguous oneOf value for ",
3812 stringify!(#enum_name),
3813 ": more than one branch preserved an equivalent input",
3814 )));
3815 }
3816 equivalent.ok_or_else(|| serde::de::Error::custom(concat!(
3817 "no oneOf branch for ",
3818 stringify!(#enum_name),
3819 " preserved the complete input",
3820 )))
3821 }
3822 } else {
3823 quote! {
3824 equivalent.ok_or_else(|| serde::de::Error::custom(concat!(
3825 "no anyOf branch for ",
3826 stringify!(#enum_name),
3827 " preserved the complete input",
3828 )))
3829 }
3830 };
3831 let matched_declaration = if exclusive {
3832 quote! {
3833 let mut matched = None;
3834 let mut equivalent = None;
3835 let mut equivalent_matches = 0usize;
3836 }
3837 } else {
3838 quote! { let mut equivalent = None; }
3839 };
3840 let preservation_helper = {
3841 quote! {
3842 fn exact_json_integer(number: &serde_json::Number) -> Option<i128> {
3843 number
3844 .as_i64()
3845 .map(i128::from)
3846 .or_else(|| number.as_u64().map(i128::from))
3847 }
3848
3849 fn json_numbers_have_same_value(
3850 encoded: &serde_json::Number,
3851 input: &serde_json::Number,
3852 ) -> bool {
3853 match (exact_json_integer(encoded), exact_json_integer(input)) {
3854 (Some(encoded), Some(input)) => encoded == input,
3855 (Some(encoded), None) => input.as_f64().is_some_and(|input| {
3856 input.is_finite()
3857 && input.fract() == 0.0
3858 && input as i128 == encoded
3859 }),
3860 (None, Some(input)) => encoded.as_f64().is_some_and(|encoded| {
3861 encoded.is_finite()
3862 && encoded.fract() == 0.0
3863 && encoded as i128 == input
3864 }),
3865 (None, None) => encoded.as_f64() == input.as_f64(),
3866 }
3867 }
3868
3869 fn preserves_complete_json_input(
3873 encoded: &serde_json::Value,
3874 input: &serde_json::Value,
3875 nulls_may_be_absent: bool,
3876 encoded_keys_may_be_extra: bool,
3877 ) -> bool {
3878 match (encoded, input) {
3879 (
3880 serde_json::Value::Object(encoded),
3881 serde_json::Value::Object(input),
3882 ) => {
3883 (encoded_keys_may_be_extra
3884 || encoded.keys().all(|key| input.contains_key(key)))
3885 && input.iter().all(|(key, value)| match encoded.get(key) {
3886 Some(encoded_value) => preserves_complete_json_input(
3887 encoded_value,
3888 value,
3889 nulls_may_be_absent,
3890 encoded_keys_may_be_extra,
3891 ),
3892 None => nulls_may_be_absent && value.is_null(),
3893 })
3894 }
3895 (
3896 serde_json::Value::Array(encoded),
3897 serde_json::Value::Array(input),
3898 ) => {
3899 encoded.len() == input.len()
3900 && encoded.iter().zip(input).all(|(encoded, input)| {
3901 preserves_complete_json_input(
3902 encoded,
3903 input,
3904 nulls_may_be_absent,
3905 encoded_keys_may_be_extra,
3906 )
3907 })
3908 }
3909 (
3910 serde_json::Value::Number(encoded),
3911 serde_json::Value::Number(input),
3912 ) => json_numbers_have_same_value(encoded, input),
3913 _ => encoded == input,
3914 }
3915 }
3916 }
3917 };
3918
3919 return Ok(quote! {
3920 #doc_comment
3921 #derives
3922 pub enum #enum_name {
3923 #(#variant_declarations)*
3924 }
3925
3926 impl Serialize for #enum_name {
3927 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
3928 where
3929 S: serde::Serializer,
3930 {
3931 match self {
3932 #(#serialize_arms)*
3933 }
3934 }
3935 }
3936
3937 impl<'de> Deserialize<'de> for #enum_name {
3938 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3939 where
3940 D: serde::Deserializer<'de>,
3941 {
3942 #preservation_helper
3943 let input = <serde_json::Value as Deserialize>::deserialize(deserializer)?;
3944 #matched_declaration
3945 #(#deserialize_attempts)*
3946 #no_match
3947 }
3948 }
3949 });
3950 }
3951
3952 let derives = if self.config.enable_specta {
3955 quote! {
3956 #[derive(Debug, Clone, Deserialize, Serialize)]
3957 #[cfg_attr(feature = "specta", derive(specta::Type))]
3958 #[serde(untagged)]
3959 }
3960 } else {
3961 quote! {
3962 #[derive(Debug, Clone, Deserialize, Serialize)]
3963 #[serde(untagged)]
3964 }
3965 };
3966
3967 Ok(quote! {
3968 #doc_comment
3969 #derives
3970 pub enum #enum_name {
3971 #(#variant_declarations)*
3972 }
3973 })
3974 }
3975
3976 fn union_branch_literal_constraints(
3977 &self,
3978 target: &str,
3979 analysis: &crate::analysis::SchemaAnalysis,
3980 visited: &mut std::collections::HashSet<String>,
3981 ) -> BTreeMap<String, (bool, Vec<serde_json::Value>)> {
3982 if !visited.insert(target.to_string()) {
3983 return BTreeMap::new();
3984 }
3985 let constraints = analysis
3986 .schemas
3987 .get(target)
3988 .map(|schema| {
3989 self.schema_literal_property_constraints(&schema.original, analysis, visited)
3990 })
3991 .unwrap_or_default();
3992 visited.remove(target);
3993 constraints
3994 }
3995
3996 fn schema_literal_property_constraints(
3997 &self,
3998 schema: &serde_json::Value,
3999 analysis: &crate::analysis::SchemaAnalysis,
4000 visited: &mut std::collections::HashSet<String>,
4001 ) -> BTreeMap<String, (bool, Vec<serde_json::Value>)> {
4002 let Some(object) = schema.as_object() else {
4003 return BTreeMap::new();
4004 };
4005 if let Some(reference) = object.get("$ref").and_then(serde_json::Value::as_str)
4006 && let Some(target) = reference.rsplit('/').next()
4007 {
4008 return self.union_branch_literal_constraints(target, analysis, visited);
4009 }
4010
4011 let required = object
4012 .get("required")
4013 .and_then(serde_json::Value::as_array)
4014 .into_iter()
4015 .flatten()
4016 .filter_map(serde_json::Value::as_str)
4017 .collect::<std::collections::HashSet<_>>();
4018 let mut constraints = BTreeMap::new();
4019 if let Some(properties) = object
4020 .get("properties")
4021 .and_then(serde_json::Value::as_object)
4022 {
4023 for (field, property) in properties {
4024 if let Some(values) = self.schema_literal_domain(
4025 property,
4026 analysis,
4027 &mut std::collections::HashSet::new(),
4028 ) && !values.is_empty()
4029 {
4030 constraints.insert(field.clone(), (required.contains(field.as_str()), values));
4031 }
4032 }
4033 }
4034 if let Some(branches) = object.get("allOf").and_then(serde_json::Value::as_array) {
4035 for branch in branches {
4036 for (field, (branch_required, values)) in
4037 self.schema_literal_property_constraints(branch, analysis, visited)
4038 {
4039 constraints
4040 .entry(field)
4041 .and_modify(|(is_required, existing)| {
4042 *is_required |= branch_required;
4043 existing.retain(|value| values.contains(value));
4044 })
4045 .or_insert((branch_required, values));
4046 }
4047 }
4048 }
4049 constraints
4050 }
4051
4052 fn schema_literal_domain(
4053 &self,
4054 schema: &serde_json::Value,
4055 analysis: &crate::analysis::SchemaAnalysis,
4056 visited: &mut std::collections::HashSet<String>,
4057 ) -> Option<Vec<serde_json::Value>> {
4058 let object = schema.as_object()?;
4059 if let Some(reference) = object.get("$ref").and_then(serde_json::Value::as_str)
4060 && let Some(target) = reference.rsplit('/').next()
4061 {
4062 if !visited.insert(target.to_string()) {
4063 return None;
4064 }
4065 let result = analysis
4066 .schemas
4067 .get(target)
4068 .and_then(|schema| self.schema_literal_domain(&schema.original, analysis, visited));
4069 visited.remove(target);
4070 return result;
4071 }
4072 let own = object
4073 .get("const")
4074 .map(|value| vec![value.clone()])
4075 .or_else(|| {
4076 object
4077 .get("enum")
4078 .and_then(serde_json::Value::as_array)
4079 .cloned()
4080 });
4081 let composed = object
4082 .get("allOf")
4083 .and_then(serde_json::Value::as_array)
4084 .and_then(|branches| {
4085 branches.iter().fold(None, |domain, branch| {
4086 let branch_domain = self.schema_literal_domain(branch, analysis, visited);
4087 match (domain, branch_domain) {
4088 (None, other) | (other, None) => other,
4089 (Some(mut left), Some(right)) => {
4090 left.retain(|value| right.contains(value));
4091 Some(left)
4092 }
4093 }
4094 })
4095 });
4096 match (own, composed) {
4097 (None, other) | (other, None) => other,
4098 (Some(mut left), Some(right)) => {
4099 left.retain(|value| right.contains(value));
4100 Some(left)
4101 }
4102 }
4103 }
4104
4105 fn union_target_serializes_as_object(
4106 &self,
4107 target: &str,
4108 analysis: &crate::analysis::SchemaAnalysis,
4109 visited: &mut std::collections::HashSet<String>,
4110 ) -> bool {
4111 if !visited.insert(target.to_string()) {
4112 return false;
4113 }
4114 let result = analysis
4115 .schemas
4116 .get(target)
4117 .is_some_and(|schema| match &schema.schema_type {
4118 crate::analysis::SchemaType::Object { .. }
4119 | crate::analysis::SchemaType::Composition { .. }
4120 | crate::analysis::SchemaType::DiscriminatedUnion { .. } => true,
4121 crate::analysis::SchemaType::Reference { target } => {
4122 self.union_target_serializes_as_object(target, analysis, visited)
4123 }
4124 crate::analysis::SchemaType::Union { variants, .. } => {
4125 variants.iter().all(|variant| {
4126 self.union_target_serializes_as_object(&variant.target, analysis, visited)
4127 })
4128 }
4129 _ => false,
4130 });
4131 visited.remove(target);
4132 result
4133 }
4134
4135 fn target_aliases_back_to(
4140 &self,
4141 target: &str,
4142 enclosing_rust_name: &str,
4143 analysis: &crate::analysis::SchemaAnalysis,
4144 ) -> bool {
4145 let mut current = target.to_string();
4146 let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
4147 for _ in 0..16 {
4148 if !visited.insert(current.clone()) {
4149 return true;
4150 }
4151 let Some(schema) = analysis.schemas.get(¤t) else {
4152 return false;
4153 };
4154 if let crate::analysis::SchemaType::Reference { target: next } = &schema.schema_type {
4155 if self.to_rust_type_name(next) == enclosing_rust_name {
4156 return true;
4157 }
4158 current = next.clone();
4159 continue;
4160 }
4161 return false;
4162 }
4163 false
4164 }
4165
4166 fn generate_field_type(
4167 &self,
4168 schema_name: &str,
4169 field_name: &str,
4170 prop: &crate::analysis::PropertyInfo,
4171 is_required: bool,
4172 analysis: &crate::analysis::SchemaAnalysis,
4173 ) -> TokenStream {
4174 let base_type = self.generate_property_base_type(schema_name, field_name, prop, analysis);
4175
4176 if !is_required && self.property_is_nullable(schema_name, field_name, prop) {
4177 quote! { Option<Option<#base_type>> }
4178 } else if self.property_is_option_wrapped(
4179 schema_name,
4180 field_name,
4181 prop,
4182 is_required,
4183 analysis,
4184 ) {
4185 quote! { Option<#base_type> }
4186 } else {
4187 base_type
4188 }
4189 }
4190
4191 pub(crate) fn property_is_nullable(
4192 &self,
4193 schema_name: &str,
4194 field_name: &str,
4195 prop: &crate::analysis::PropertyInfo,
4196 ) -> bool {
4197 let override_key = format!("{schema_name}.{field_name}");
4198 prop.nullable
4199 || self
4200 .config
4201 .nullable_field_overrides
4202 .get(&override_key)
4203 .copied()
4204 .unwrap_or(false)
4205 }
4206
4207 pub(crate) fn property_is_tri_state(
4208 &self,
4209 schema_name: &str,
4210 field_name: &str,
4211 prop: &crate::analysis::PropertyInfo,
4212 is_required: bool,
4213 ) -> bool {
4214 !is_required && self.property_is_nullable(schema_name, field_name, prop)
4215 }
4216
4217 fn property_is_option_wrapped(
4218 &self,
4219 schema_name: &str,
4220 field_name: &str,
4221 prop: &crate::analysis::PropertyInfo,
4222 is_required: bool,
4223 analysis: &crate::analysis::SchemaAnalysis,
4224 ) -> bool {
4225 !is_required
4226 || self.property_is_nullable(schema_name, field_name, prop)
4227 || (prop.default.is_some() && self.type_lacks_default(&prop.schema_type, analysis))
4228 }
4229
4230 pub(crate) fn generate_property_base_type(
4231 &self,
4232 schema_name: &str,
4233 _field_name: &str,
4234 prop: &crate::analysis::PropertyInfo,
4235 analysis: &crate::analysis::SchemaAnalysis,
4236 ) -> TokenStream {
4237 use crate::analysis::SchemaType;
4238
4239 match &prop.schema_type {
4240 SchemaType::Primitive { rust_type, .. } => {
4241 parse_rust_type(rust_type).unwrap_or_else(|_| {
4244 eprintln!(
4249 "⚠️ TypeMapper produced un-parseable type `{rust_type}`; \
4250 falling back to String"
4251 );
4252 quote! { String }
4253 })
4254 }
4255 SchemaType::Reference { target } => {
4256 let target_rust_name = self.to_rust_type_name(target);
4257 let target_type = format_ident!("{}", target_rust_name);
4258 let enclosing_rust_name = self.to_rust_type_name(schema_name);
4271 let is_self_via_rust_name = target_rust_name == enclosing_rust_name;
4272 let is_alias_chain_self =
4273 self.target_aliases_back_to(target, &enclosing_rust_name, analysis);
4274 if analysis.dependencies.recursive_schemas.contains(target)
4275 || is_self_via_rust_name
4276 || is_alias_chain_self
4277 {
4278 quote! { Box<#target_type> }
4279 } else {
4280 quote! { #target_type }
4281 }
4282 }
4283 SchemaType::Array { item_type } => {
4284 let inner_type = self.generate_array_item_type(item_type, analysis);
4285 quote! { Vec<#inner_type> }
4286 }
4287 SchemaType::Nullable { inner_type } => {
4288 let inner_type = self.generate_array_item_type(inner_type, analysis);
4289 quote! { Option<#inner_type> }
4290 }
4291 SchemaType::Tuple { element_types } => {
4292 self.generate_tuple_type(element_types, analysis)
4293 }
4294 SchemaType::Untyped { shape, .. } => untyped_tokens(*shape),
4295 _ => {
4296 quote! { serde_json::Value }
4298 }
4299 }
4300 }
4301
4302 fn generate_tuple_type(
4306 &self,
4307 element_types: &[crate::analysis::SchemaType],
4308 analysis: &crate::analysis::SchemaAnalysis,
4309 ) -> TokenStream {
4310 let elements = element_types
4311 .iter()
4312 .map(|element_type| self.generate_array_item_type(element_type, analysis))
4313 .collect::<Vec<_>>();
4314 if let [only] = elements.as_slice() {
4318 return quote! { (#only,) };
4319 }
4320 quote! { (#(#elements),*) }
4321 }
4322
4323 fn generate_serde_field_attrs(
4324 &self,
4325 schema_name: &str,
4326 field_name: &str,
4327 field_ident: &syn::Ident,
4328 prop: &crate::analysis::PropertyInfo,
4329 is_required: bool,
4330 analysis: &crate::analysis::SchemaAnalysis,
4331 ) -> TokenStream {
4332 let mut attrs = Vec::new();
4333
4334 let rust_field_name = field_ident.to_string();
4337 let comparison_name = rust_field_name
4338 .strip_prefix("r#")
4339 .unwrap_or(&rust_field_name);
4340 if comparison_name != field_name {
4341 attrs.push(quote! { rename = #field_name });
4342 }
4343
4344 let is_tri_state = self.property_is_tri_state(schema_name, field_name, prop, is_required);
4345
4346 if !is_required {
4351 attrs.push(quote! { skip_serializing_if = "Option::is_none" });
4352 }
4353
4354 if prop.default.is_some()
4358 && (is_required && !prop.nullable)
4359 && !self.type_lacks_default(&prop.schema_type, analysis)
4360 {
4361 attrs.push(quote! { default });
4362 }
4363
4364 if let Some(codec) = self.schema_type_serde_codec(&prop.schema_type, analysis) {
4372 let is_option_wrapped = self.property_is_option_wrapped(
4373 schema_name,
4374 field_name,
4375 prop,
4376 is_required,
4377 analysis,
4378 );
4379 let codec_path = if is_tri_state {
4380 Self::double_option_codec_path(&codec)
4381 } else if is_option_wrapped {
4382 format!("{codec}::option")
4383 } else {
4384 codec
4385 };
4386 attrs.push(quote! { with = #codec_path });
4387 if is_option_wrapped {
4392 attrs.push(quote! { default });
4393 }
4394 } else if is_tri_state {
4395 attrs.push(quote! { default });
4396 attrs.push(quote! { deserialize_with = "tri_state_serde::deserialize" });
4397 }
4398
4399 if attrs.is_empty() {
4400 TokenStream::new()
4401 } else {
4402 quote! { #[serde(#(#attrs),*)] }
4403 }
4404 }
4405
4406 fn double_option_codec_path(codec: &str) -> String {
4407 match codec {
4408 "time::serde::rfc3339" => "time_rfc3339_double_option".to_string(),
4409 "time_date_format" => "time_date_double_option".to_string(),
4410 "time_time_format" => "time_time_double_option".to_string(),
4411 _ => format!("{codec}::double_option"),
4412 }
4413 }
4414
4415 fn schema_type_serde_codec(
4420 &self,
4421 schema_type: &crate::analysis::SchemaType,
4422 analysis: &crate::analysis::SchemaAnalysis,
4423 ) -> Option<String> {
4424 let mut current = schema_type;
4425 let mut visited = std::collections::HashSet::new();
4426 loop {
4427 match current {
4428 crate::analysis::SchemaType::Primitive {
4429 serde_with: Some(codec),
4430 ..
4431 } => return Some(codec.clone()),
4432 crate::analysis::SchemaType::Reference { target }
4433 if visited.insert(target.clone()) =>
4434 {
4435 current = &analysis.schemas.get(target)?.schema_type;
4436 }
4437 crate::analysis::SchemaType::Nullable { inner_type } => {
4438 current = inner_type;
4439 }
4440 _ => return None,
4441 }
4442 }
4443 }
4444
4445 fn type_lacks_default(
4449 &self,
4450 schema_type: &crate::analysis::SchemaType,
4451 analysis: &crate::analysis::SchemaAnalysis,
4452 ) -> bool {
4453 use crate::analysis::SchemaType;
4454 match schema_type {
4455 SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => true,
4456 SchemaType::Primitive { rust_type, .. } => matches!(
4460 rust_type.as_str(),
4461 "chrono::DateTime<chrono::Utc>"
4462 | "chrono::NaiveDate"
4463 | "chrono::NaiveTime"
4464 | "chrono::Duration"
4465 | "url::Url"
4466 | "time::OffsetDateTime"
4467 | "time::Date"
4468 | "time::Time"
4469 | "iso8601::Duration"
4470 | "email_address::EmailAddress"
4471 ),
4472 SchemaType::Reference { target } => {
4473 if let Some(schema) = analysis.schemas.get(target) {
4474 self.type_lacks_default(&schema.schema_type, analysis)
4475 } else {
4476 false
4477 }
4478 }
4479 _ => false,
4480 }
4481 }
4482
4483 fn generate_specta_field_attrs(&self, field_name: &str) -> TokenStream {
4484 if !self.config.enable_specta {
4485 return TokenStream::new();
4486 }
4487
4488 let camel_case_name = self.to_camel_case(field_name);
4490
4491 if camel_case_name != field_name {
4493 quote! { #[cfg_attr(feature = "specta", specta(rename = #camel_case_name))] }
4494 } else {
4495 TokenStream::new()
4496 }
4497 }
4498
4499 pub(crate) fn to_rust_enum_variant(&self, s: &str) -> String {
4500 let neg_prefix =
4504 if s.starts_with('-') && s.chars().skip(1).all(|c| c.is_ascii_digit() || c == '.') {
4505 "Neg"
4506 } else {
4507 ""
4508 };
4509
4510 let mut result = String::new();
4512 let mut next_upper = true;
4513 let mut prev_was_upper = false;
4514
4515 for (i, c) in s.chars().enumerate() {
4516 match c {
4517 'a'..='z' => {
4518 if next_upper {
4519 result.push(c.to_ascii_uppercase());
4520 next_upper = false;
4521 } else {
4522 result.push(c);
4523 }
4524 prev_was_upper = false;
4525 }
4526 'A'..='Z' => {
4527 if next_upper || (!prev_was_upper && i > 0) {
4528 result.push(c);
4530 next_upper = false;
4531 } else {
4532 result.push(c.to_ascii_lowercase());
4534 }
4535 prev_was_upper = true;
4536 }
4537 '0'..='9' => {
4538 result.push(c);
4539 next_upper = false;
4540 prev_was_upper = false;
4541 }
4542 '.' | '-' | '_' | ' ' | '@' | '#' | '$' | '/' | '\\' => {
4543 next_upper = true;
4545 prev_was_upper = false;
4546 }
4547 _ => {
4548 next_upper = true;
4550 prev_was_upper = false;
4551 }
4552 }
4553 }
4554
4555 if result.is_empty() {
4557 result = "Value".to_string();
4558 }
4559
4560 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
4562 result = format!("Variant{neg_prefix}{result}");
4563 } else if !neg_prefix.is_empty() {
4564 result = format!("{neg_prefix}{result}");
4567 }
4568
4569 match result.as_str() {
4571 "Null" => "NullValue".to_string(),
4572 "True" => "TrueValue".to_string(),
4573 "False" => "FalseValue".to_string(),
4574 "Type" => "Type_".to_string(),
4575 "Match" => "Match_".to_string(),
4576 "Fn" => "Fn_".to_string(),
4577 "Impl" => "Impl_".to_string(),
4578 "Trait" => "Trait_".to_string(),
4579 "Struct" => "Struct_".to_string(),
4580 "Enum" => "Enum_".to_string(),
4581 "Mod" => "Mod_".to_string(),
4582 "Use" => "Use_".to_string(),
4583 "Pub" => "Pub_".to_string(),
4584 "Const" => "Const_".to_string(),
4585 "Static" => "Static_".to_string(),
4586 "Let" => "Let_".to_string(),
4587 "Mut" => "Mut_".to_string(),
4588 "Ref" => "Ref_".to_string(),
4589 "Move" => "Move_".to_string(),
4590 "Return" => "Return_".to_string(),
4591 "If" => "If_".to_string(),
4592 "Else" => "Else_".to_string(),
4593 "While" => "While_".to_string(),
4594 "For" => "For_".to_string(),
4595 "Loop" => "Loop_".to_string(),
4596 "Break" => "Break_".to_string(),
4597 "Continue" => "Continue_".to_string(),
4598 "Self" => "Self_".to_string(),
4599 "Super" => "Super_".to_string(),
4600 "Crate" => "Crate_".to_string(),
4601 "Async" => "Async_".to_string(),
4602 "Await" => "Await_".to_string(),
4603 _ => result,
4604 }
4605 }
4606
4607 #[allow(dead_code)]
4608 fn to_rust_identifier(&self, s: &str) -> String {
4609 let mut result = s
4611 .chars()
4612 .map(|c| match c {
4613 'a'..='z' | 'A'..='Z' | '0'..='9' => c,
4614 '.' | '-' | '_' | ' ' | '@' | '#' | '$' | '/' | '\\' => '_',
4615 _ => '_',
4616 })
4617 .collect::<String>();
4618
4619 result = result.trim_matches('_').to_string();
4621
4622 if result.is_empty() {
4624 result = "value".to_string();
4625 }
4626
4627 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
4629 result = format!("variant_{result}");
4630 }
4631
4632 match result.as_str() {
4634 "null" => "null_value".to_string(),
4635 "true" => "true_value".to_string(),
4636 "false" => "false_value".to_string(),
4637 "type" => "type_".to_string(),
4638 "match" => "match_".to_string(),
4639 "fn" => "fn_".to_string(),
4640 "impl" => "impl_".to_string(),
4641 "trait" => "trait_".to_string(),
4642 "struct" => "struct_".to_string(),
4643 "enum" => "enum_".to_string(),
4644 "mod" => "mod_".to_string(),
4645 "use" => "use_".to_string(),
4646 "pub" => "pub_".to_string(),
4647 "const" => "const_".to_string(),
4648 "static" => "static_".to_string(),
4649 "let" => "let_".to_string(),
4650 "mut" => "mut_".to_string(),
4651 "ref" => "ref_".to_string(),
4652 "move" => "move_".to_string(),
4653 "return" => "return_".to_string(),
4654 "if" => "if_".to_string(),
4655 "else" => "else_".to_string(),
4656 "while" => "while_".to_string(),
4657 "for" => "for_".to_string(),
4658 "loop" => "loop_".to_string(),
4659 "break" => "break_".to_string(),
4660 "continue" => "continue_".to_string(),
4661 "self" => "self_".to_string(),
4662 "super" => "super_".to_string(),
4663 "crate" => "crate_".to_string(),
4664 "async" => "async_".to_string(),
4665 "await" => "await_".to_string(),
4666 "override" => "override_".to_string(),
4668 "box" => "box_".to_string(),
4669 "dyn" => "dyn_".to_string(),
4670 "where" => "where_".to_string(),
4671 "in" => "in_".to_string(),
4672 "abstract" => "abstract_".to_string(),
4674 "become" => "become_".to_string(),
4675 "do" => "do_".to_string(),
4676 "final" => "final_".to_string(),
4677 "macro" => "macro_".to_string(),
4678 "priv" => "priv_".to_string(),
4679 "try" => "try_".to_string(),
4680 "typeof" => "typeof_".to_string(),
4681 "unsized" => "unsized_".to_string(),
4682 "virtual" => "virtual_".to_string(),
4683 "yield" => "yield_".to_string(),
4684 _ => result,
4685 }
4686 }
4687
4688 fn generate_constraint_doc(
4696 &self,
4697 constraints: &crate::analysis::PropertyConstraints,
4698 ) -> TokenStream {
4699 use crate::type_mapping::ConstraintMode;
4700
4701 if constraints.is_empty() {
4702 return TokenStream::new();
4703 }
4704 match self.config.types.constraint_mode() {
4705 ConstraintMode::Off => TokenStream::new(),
4706 ConstraintMode::Doc => {
4707 let formatted = format_constraints_doc(constraints);
4708 quote! { #[doc = #formatted] }
4709 }
4710 }
4711 }
4712
4713 fn sanitize_doc_comment(&self, desc: &str) -> String {
4714 let mut result = desc.to_string();
4716
4717 if result.contains('\n')
4725 && (result.contains('{')
4726 || result.contains("```")
4727 || result.contains("Human:")
4728 || result.contains("Assistant:")
4729 || result
4730 .lines()
4731 .any(|line| line.trim().starts_with('"') && line.trim().ends_with('"')))
4732 {
4733 if result.contains("```") {
4735 result = result.replace("```", "```ignore");
4736 } else {
4737 if result.lines().any(|line| {
4739 let trimmed = line.trim();
4740 trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() > 2
4741 }) {
4742 result = format!("```ignore\n{result}\n```");
4743 }
4744 }
4745 }
4746
4747 result
4748 }
4749
4750 pub(crate) fn to_rust_type_name(&self, s: &str) -> String {
4751 rust_type_name(s)
4752 }
4753
4754 pub(crate) fn to_rust_field_name(&self, s: &str) -> String {
4755 let leading_marker = match s.chars().next() {
4759 Some('-') if s.len() > 1 => "neg_",
4760 Some('+') if s.len() > 1 => "pos_",
4761 _ => "",
4762 };
4763
4764 let mut result = String::new();
4766 let mut prev_was_upper = false;
4767 let mut prev_was_underscore = false;
4768
4769 for (i, c) in s.chars().enumerate() {
4770 match c {
4771 'A'..='Z' => {
4772 if i > 0 && !prev_was_upper && !prev_was_underscore {
4774 result.push('_');
4775 }
4776 result.push(c.to_ascii_lowercase());
4777 prev_was_upper = true;
4778 prev_was_underscore = false;
4779 }
4780 'a'..='z' | '0'..='9' => {
4781 result.push(c);
4782 prev_was_upper = false;
4783 prev_was_underscore = false;
4784 }
4785 '-' | '.' | '_' | '@' | '#' | '$' | ' ' => {
4786 if !prev_was_underscore && !result.is_empty() {
4787 result.push('_');
4788 prev_was_underscore = true;
4789 }
4790 prev_was_upper = false;
4791 }
4792 _ => {
4793 if !prev_was_underscore && !result.is_empty() {
4795 result.push('_');
4796 }
4797 prev_was_upper = false;
4798 prev_was_underscore = true;
4799 }
4800 }
4801 }
4802
4803 let mut result = result.trim_matches('_').to_string();
4805 if result.is_empty() {
4806 return "field".to_string();
4807 }
4808
4809 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
4811 result = format!("field_{leading_marker}{result}");
4812 } else if !leading_marker.is_empty() {
4813 result = format!("{leading_marker}{result}");
4814 }
4815
4816 if matches!(result.as_str(), "self" | "super" | "crate" | "Self") {
4819 return format!("{result}_field");
4820 }
4821 if matches!(result.as_str(), "true" | "false") {
4825 return format!("{result}_field");
4826 }
4827 if Self::is_rust_keyword(&result) {
4829 format!("r#{result}")
4830 } else {
4831 result
4832 }
4833 }
4834
4835 pub fn is_rust_keyword(s: &str) -> bool {
4837 matches!(
4838 s,
4839 "type"
4840 | "match"
4841 | "fn"
4842 | "struct"
4843 | "enum"
4844 | "impl"
4845 | "trait"
4846 | "mod"
4847 | "use"
4848 | "pub"
4849 | "const"
4850 | "static"
4851 | "let"
4852 | "mut"
4853 | "ref"
4854 | "move"
4855 | "return"
4856 | "if"
4857 | "else"
4858 | "while"
4859 | "for"
4860 | "loop"
4861 | "break"
4862 | "continue"
4863 | "self"
4864 | "super"
4865 | "crate"
4866 | "async"
4867 | "await"
4868 | "override"
4869 | "box"
4870 | "dyn"
4871 | "where"
4872 | "in"
4873 | "abstract"
4874 | "become"
4875 | "do"
4876 | "final"
4877 | "macro"
4878 | "priv"
4879 | "try"
4880 | "typeof"
4881 | "unsized"
4882 | "virtual"
4883 | "yield"
4884 | "gen"
4886 )
4887 }
4888
4889 pub fn to_field_ident(name: &str) -> proc_macro2::Ident {
4891 if let Some(raw) = name.strip_prefix("r#") {
4892 proc_macro2::Ident::new_raw(raw, proc_macro2::Span::call_site())
4893 } else {
4894 proc_macro2::Ident::new(name, proc_macro2::Span::call_site())
4895 }
4896 }
4897
4898 fn to_camel_case(&self, s: &str) -> String {
4899 let mut result = String::new();
4901 let mut capitalize_next = false;
4902
4903 for (i, c) in s.chars().enumerate() {
4904 match c {
4905 '_' | '-' | '.' | ' ' => {
4906 capitalize_next = true;
4908 }
4909 'A'..='Z' => {
4910 if i == 0 {
4911 result.push(c.to_ascii_lowercase());
4913 } else if capitalize_next {
4914 result.push(c);
4915 capitalize_next = false;
4916 } else {
4917 result.push(c.to_ascii_lowercase());
4918 }
4919 }
4920 'a'..='z' | '0'..='9' => {
4921 if capitalize_next {
4922 result.push(c.to_ascii_uppercase());
4923 capitalize_next = false;
4924 } else {
4925 result.push(c);
4926 }
4927 }
4928 _ => {
4929 capitalize_next = true;
4931 }
4932 }
4933 }
4934
4935 if result.is_empty() {
4936 return "field".to_string();
4937 }
4938
4939 result
4940 }
4941
4942 fn generate_composition_struct(
4943 &self,
4944 schema: &crate::analysis::AnalyzedSchema,
4945 schemas: &[crate::analysis::SchemaRef],
4946 ) -> Result<TokenStream> {
4947 let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
4948
4949 let fields = schemas.iter().enumerate().map(|(i, schema_ref)| {
4955 let field_name = format_ident!("part_{}", i);
4956 let field_type = format_ident!("{}", self.to_rust_type_name(&schema_ref.target));
4957
4958 quote! {
4959 #[serde(flatten)]
4960 pub #field_name: #field_type,
4961 }
4962 });
4963
4964 let doc_comment = if let Some(desc) = &schema.description {
4965 quote! { #[doc = #desc] }
4966 } else {
4967 TokenStream::new()
4968 };
4969
4970 let derives = if self.config.enable_specta {
4972 quote! {
4973 #[derive(Debug, Clone, Deserialize, Serialize)]
4974 #[cfg_attr(feature = "specta", derive(specta::Type))]
4975 }
4976 } else {
4977 quote! {
4978 #[derive(Debug, Clone, Deserialize, Serialize)]
4979 }
4980 };
4981
4982 Ok(quote! {
4983 #doc_comment
4984 #derives
4985 pub struct #struct_name {
4986 #(#fields)*
4987 }
4988 })
4989 }
4990
4991 fn union_declared_properties(
4992 &self,
4993 target: &str,
4994 analysis: &crate::analysis::SchemaAnalysis,
4995 visited: &mut std::collections::HashSet<String>,
4996 ) -> std::collections::HashSet<String> {
4997 if !visited.insert(target.to_string()) {
4998 return std::collections::HashSet::new();
4999 }
5000 let mut properties = std::collections::HashSet::new();
5001 if let Some(schema) = analysis.schemas.get(target) {
5002 match &schema.schema_type {
5003 crate::analysis::SchemaType::Object {
5004 properties: own,
5005 variant,
5006 ..
5007 } => {
5008 properties.extend(own.keys().cloned());
5009 if let Some(variant) = variant {
5010 properties.extend(self.union_declared_properties(
5011 &variant.target,
5012 analysis,
5013 visited,
5014 ));
5015 }
5016 }
5017 crate::analysis::SchemaType::DiscriminatedUnion { variants, .. } => {
5018 for variant in variants {
5019 properties.extend(self.union_declared_properties(
5020 &variant.type_name,
5021 analysis,
5022 visited,
5023 ));
5024 }
5025 }
5026 crate::analysis::SchemaType::Union { variants, .. }
5027 | crate::analysis::SchemaType::Composition { schemas: variants } => {
5028 for variant in variants {
5029 properties.extend(self.union_declared_properties(
5030 &variant.target,
5031 analysis,
5032 visited,
5033 ));
5034 }
5035 }
5036 crate::analysis::SchemaType::Reference { target } => {
5037 properties.extend(self.union_declared_properties(target, analysis, visited));
5038 }
5039 _ => {}
5040 }
5041 }
5042 visited.remove(target);
5043 properties
5044 }
5045
5046 #[allow(dead_code)]
5047 fn find_missing_types(&self, analysis: &SchemaAnalysis) -> std::collections::HashSet<String> {
5048 let mut missing = std::collections::HashSet::new();
5049 let defined_types: std::collections::HashSet<String> =
5050 analysis.schemas.keys().cloned().collect();
5051
5052 for schema in analysis.schemas.values() {
5054 match &schema.schema_type {
5055 crate::analysis::SchemaType::Union { variants, .. } => {
5056 for variant in variants {
5057 if !defined_types.contains(&variant.target) {
5058 missing.insert(variant.target.clone());
5059 }
5060 }
5061 }
5062 crate::analysis::SchemaType::DiscriminatedUnion { variants, .. } => {
5063 for variant in variants {
5064 if !defined_types.contains(&variant.type_name) {
5065 missing.insert(variant.type_name.clone());
5066 }
5067 }
5068 }
5069 crate::analysis::SchemaType::Object { properties, .. } => {
5070 let mut sorted_props: Vec<_> = properties.iter().collect();
5072 sorted_props.sort_by_key(|(name, _)| name.as_str());
5073 for (_, prop) in sorted_props {
5074 if let crate::analysis::SchemaType::Reference { target } = &prop.schema_type
5075 {
5076 if !defined_types.contains(target) {
5077 missing.insert(target.clone());
5078 }
5079 }
5080 }
5081 }
5082 crate::analysis::SchemaType::Reference { target }
5083 if !defined_types.contains(target) =>
5084 {
5085 missing.insert(target.clone());
5086 }
5087 _ => {}
5088 }
5089 }
5090
5091 missing
5092 }
5093
5094 #[allow(clippy::only_used_in_recursion)]
5095 fn generate_array_item_type(
5096 &self,
5097 item_type: &crate::analysis::SchemaType,
5098 analysis: &crate::analysis::SchemaAnalysis,
5099 ) -> TokenStream {
5100 use crate::analysis::SchemaType;
5101
5102 match item_type {
5103 SchemaType::Primitive { rust_type, .. } => {
5104 if let Ok(parsed) = syn::parse_str::<syn::Type>(rust_type) {
5109 quote! { #parsed }
5110 } else if rust_type.contains("::") {
5111 let parts: Vec<_> = rust_type
5112 .split("::")
5113 .map(|p| format_ident!("{}", p))
5114 .collect();
5115 quote! { #(#parts)::* }
5116 } else {
5117 let type_ident = format_ident!("{}", rust_type);
5118 quote! { #type_ident }
5119 }
5120 }
5121 SchemaType::Reference { target } => {
5122 let target_type = format_ident!("{}", self.to_rust_type_name(target));
5123 if analysis.dependencies.recursive_schemas.contains(target) {
5125 quote! { Box<#target_type> }
5126 } else {
5127 quote! { #target_type }
5128 }
5129 }
5130 SchemaType::Array { item_type } => {
5131 let inner_type = self.generate_array_item_type(item_type, analysis);
5133 quote! { Vec<#inner_type> }
5134 }
5135 SchemaType::Nullable { inner_type } => {
5136 let inner_type = self.generate_array_item_type(inner_type, analysis);
5137 quote! { Option<#inner_type> }
5138 }
5139 SchemaType::Tuple { element_types } => {
5140 self.generate_tuple_type(element_types, analysis)
5141 }
5142 SchemaType::Untyped { shape, .. } => untyped_tokens(*shape),
5143 _ => {
5144 quote! { serde_json::Value }
5146 }
5147 }
5148 }
5149
5150 fn type_name_to_variant_name(&self, type_name: &str) -> String {
5152 match type_name {
5154 "bool" => return "Boolean".to_string(),
5155 "i8" | "i16" | "i32" | "i64" | "i128" => return "Integer".to_string(),
5156 "u8" | "u16" | "u32" | "u64" | "u128" => return "UnsignedInteger".to_string(),
5157 "f32" | "f64" => return "Number".to_string(),
5158 "String" => return "String".to_string(),
5159 "serde_json::Value" => return "Value".to_string(),
5160 "bytes::Bytes" => return "Binary".to_string(),
5164 "chrono::DateTime<chrono::Utc>" => return "DateTime".to_string(),
5165 "chrono::NaiveDate" => return "Date".to_string(),
5166 "chrono::NaiveTime" => return "Time".to_string(),
5167 "uuid::Uuid" => return "Uuid".to_string(),
5168 "url::Url" => return "Url".to_string(),
5169 "std::net::Ipv4Addr" => return "Ipv4".to_string(),
5170 "std::net::Ipv6Addr" => return "Ipv6".to_string(),
5171 _ => {}
5172 }
5173
5174 if type_name.starts_with("Vec<") && type_name.ends_with(">") {
5176 let inner = &type_name[4..type_name.len() - 1];
5177 if inner.starts_with("Vec<") && inner.ends_with(">") {
5179 let inner_inner = &inner[4..inner.len() - 1];
5180 return format!("{}ArrayArray", self.type_name_to_variant_name(inner_inner));
5181 }
5182 return format!("{}Array", self.type_name_to_variant_name(inner));
5183 }
5184
5185 let clean_name = type_name
5191 .trim_end_matches("Type")
5192 .trim_end_matches("Schema")
5193 .trim_end_matches("Item");
5194
5195 self.to_rust_type_name(clean_name)
5197 }
5198
5199 fn ensure_unique_variant_name_generator(
5201 &self,
5202 base_name: String,
5203 used_names: &mut std::collections::HashSet<String>,
5204 fallback_index: usize,
5205 ) -> String {
5206 if used_names.insert(base_name.clone()) {
5207 return base_name;
5208 }
5209
5210 for i in 2..100 {
5212 let numbered_name = format!("{base_name}{i}");
5213 if used_names.insert(numbered_name.clone()) {
5214 return numbered_name;
5215 }
5216 }
5217
5218 let fallback = format!("Variant{fallback_index}");
5220 used_names.insert(fallback.clone());
5221 fallback
5222 }
5223
5224 fn find_request_type_for_operation(
5226 &self,
5227 operation_id: &str,
5228 analysis: &SchemaAnalysis,
5229 ) -> Option<String> {
5230 analysis.operations.get(operation_id).and_then(|op| {
5232 op.request_body
5233 .as_ref()
5234 .and_then(|rb| rb.schema_name().map(|s| s.to_string()))
5235 })
5236 }
5237
5238 fn resolve_streaming_event_type(
5240 &self,
5241 endpoint: &crate::streaming::StreamingEndpoint,
5242 analysis: &SchemaAnalysis,
5243 ) -> Result<String> {
5244 match &endpoint.event_flow {
5245 crate::streaming::EventFlow::Simple => {
5246 if analysis.schemas.contains_key(&endpoint.event_union_type) {
5249 Ok(endpoint.event_union_type.to_string())
5250 } else {
5251 Err(crate::error::GeneratorError::ValidationError(format!(
5252 "Streaming response type '{}' not found in schema for simple streaming endpoint '{}'",
5253 endpoint.event_union_type, endpoint.operation_id
5254 )))
5255 }
5256 }
5257 crate::streaming::EventFlow::StartDeltaStop { .. } => {
5258 if analysis.schemas.contains_key(&endpoint.event_union_type) {
5261 Ok(endpoint.event_union_type.to_string())
5262 } else {
5263 Err(crate::error::GeneratorError::ValidationError(format!(
5264 "Event union type '{}' not found in schema for complex streaming endpoint '{}'",
5265 endpoint.event_union_type, endpoint.operation_id
5266 )))
5267 }
5268 }
5269 }
5270 }
5271
5272 fn generate_streaming_error_types(&self) -> Result<TokenStream> {
5274 Ok(quote! {
5275 #[derive(Debug, thiserror::Error)]
5277 pub enum StreamingError {
5278 #[error("Connection error: {0}")]
5279 Connection(String),
5280 #[error("HTTP error: {status}")]
5281 Http { status: u16 },
5282 #[error("SSE parsing error: {0}")]
5283 Parsing(String),
5284 #[error("Authentication error: {0}")]
5285 Authentication(String),
5286 #[error("Rate limit error: {0}")]
5287 RateLimit(String),
5288 #[error("API error: {0}")]
5289 Api(String),
5290 #[error("Timeout error: {0}")]
5291 Timeout(String),
5292 #[error("Response body exceeded configured limit of {limit} bytes")]
5293 ResponseTooLarge { limit: usize },
5294 #[error("JSON serialization/deserialization error: {0}")]
5295 Json(#[from] serde_json::Error),
5296 #[error("Request error: {0}")]
5297 Request(reqwest::Error),
5298 }
5299
5300 impl From<reqwest::header::InvalidHeaderValue> for StreamingError {
5301 fn from(err: reqwest::header::InvalidHeaderValue) -> Self {
5302 StreamingError::Api(format!("Invalid header value: {}", err))
5303 }
5304 }
5305
5306 impl From<reqwest::Error> for StreamingError {
5307 fn from(err: reqwest::Error) -> Self {
5308 if err.is_timeout() {
5309 StreamingError::Timeout(err.to_string())
5310 } else if err.is_status() {
5311 if let Some(status) = err.status() {
5312 StreamingError::Http { status: status.as_u16() }
5313 } else {
5314 StreamingError::Connection(err.to_string())
5315 }
5316 } else {
5317 StreamingError::Request(err)
5318 }
5319 }
5320 }
5321 })
5322 }
5323
5324 fn generate_endpoint_trait(
5326 &self,
5327 endpoint: &crate::streaming::StreamingEndpoint,
5328 analysis: &SchemaAnalysis,
5329 ) -> Result<TokenStream> {
5330 use crate::streaming::HttpMethod;
5331
5332 let trait_name = format_ident!(
5333 "{}StreamingClient",
5334 self.to_rust_type_name(&endpoint.operation_id)
5335 );
5336 let method_name =
5337 format_ident!("stream_{}", self.to_rust_field_name(&endpoint.operation_id));
5338 let event_type =
5339 format_ident!("{}", self.resolve_streaming_event_type(endpoint, analysis)?);
5340
5341 let method_signature = match endpoint.http_method {
5343 HttpMethod::Get => {
5344 let mut param_defs = Vec::new();
5346 for qp in &endpoint.query_parameters {
5347 let param_name = format_ident!("{}", self.to_rust_field_name(&qp.name));
5348 if qp.required {
5349 param_defs.push(quote! { #param_name: &str });
5350 } else {
5351 param_defs.push(quote! { #param_name: Option<&str> });
5352 }
5353 }
5354 quote! {
5355 async fn #method_name(
5356 &self,
5357 #(#param_defs),*
5358 ) -> Result<BoxSseStream<Result<#event_type, Self::Error>>, Self::Error>;
5359 }
5360 }
5361 HttpMethod::Post => {
5362 let request_type = self
5364 .find_request_type_for_operation(&endpoint.operation_id, analysis)
5365 .unwrap_or_else(|| "serde_json::Value".to_string());
5366 let request_type_ident = if request_type.contains("::") {
5367 let parts: Vec<&str> = request_type.split("::").collect();
5368 let path_parts: Vec<_> = parts.iter().map(|p| format_ident!("{}", p)).collect();
5369 quote! { #(#path_parts)::* }
5370 } else {
5371 let ident = format_ident!("{}", request_type);
5372 quote! { #ident }
5373 };
5374 quote! {
5375 async fn #method_name(
5376 &self,
5377 request: #request_type_ident,
5378 ) -> Result<BoxSseStream<Result<#event_type, Self::Error>>, Self::Error>;
5379 }
5380 }
5381 };
5382
5383 Ok(quote! {
5384 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
5386 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
5387 pub trait #trait_name {
5388 type Error: std::error::Error + Send + Sync + 'static;
5389
5390 #method_signature
5392 }
5393 })
5394 }
5395
5396 fn generate_streaming_client_impl(
5398 &self,
5399 streaming_config: &crate::streaming::StreamingConfig,
5400 analysis: &SchemaAnalysis,
5401 ) -> Result<TokenStream> {
5402 let client_name = format_ident!(
5403 "{}Client",
5404 self.to_rust_type_name(&streaming_config.client_module_name)
5405 );
5406
5407 let mut struct_fields = vec![
5410 quote! { base_url: String },
5411 quote! { api_key: Option<String> },
5412 quote! { sse_client: SseClient },
5413 quote! { custom_headers: std::collections::BTreeMap<String, String> },
5414 ];
5415
5416 let has_optional_headers = !streaming_config
5417 .endpoints
5418 .iter()
5419 .all(|e| e.optional_headers.is_empty());
5420
5421 if has_optional_headers {
5422 struct_fields
5423 .push(quote! { optional_headers: std::collections::BTreeMap<String, String> });
5424 }
5425
5426 let default_base_url = if let Some(ref streaming_config) = self.config.streaming_config {
5429 streaming_config
5430 .endpoints
5431 .first()
5432 .and_then(|e| e.base_url.as_deref())
5433 .unwrap_or("https://api.example.com")
5434 } else {
5435 "https://api.example.com"
5436 };
5437 let max_response_body_bytes = self
5438 .config()
5439 .http_client_config
5440 .as_ref()
5441 .and_then(|http| http.max_response_body_bytes)
5442 .unwrap_or(8 * 1024 * 1024);
5443 let sse_client_initializer = if let Some(reconnect) = &streaming_config.reconnection_config
5444 {
5445 let max_retries = reconnect.max_retries;
5446 let initial_delay_ms = reconnect.initial_delay_ms;
5447 let max_delay_ms = reconnect.max_delay_ms;
5448 let backoff_multiplier = reconnect.backoff_multiplier;
5449 quote! {
5450 SseClient::new()
5451 .with_max_error_body_bytes(#max_response_body_bytes)
5452 .with_reconnect_options(SseReconnectOptions {
5453 max_retries: #max_retries,
5454 initial_retry_delay: std::time::Duration::from_millis(#initial_delay_ms),
5455 max_retry_delay: std::time::Duration::from_millis(#max_delay_ms),
5456 backoff_multiplier: #backoff_multiplier,
5457 })
5458 }
5459 } else {
5460 quote! {
5461 SseClient::new()
5462 .with_max_error_body_bytes(#max_response_body_bytes)
5463 }
5464 };
5465
5466 let constructor_fields = if has_optional_headers {
5468 quote! {
5469 base_url: #default_base_url.to_string(),
5470 api_key: None,
5471 sse_client: #sse_client_initializer,
5472 custom_headers: std::collections::BTreeMap::new(),
5473 optional_headers: std::collections::BTreeMap::new(),
5474 }
5475 } else {
5476 quote! {
5477 base_url: #default_base_url.to_string(),
5478 api_key: None,
5479 sse_client: #sse_client_initializer,
5480 custom_headers: std::collections::BTreeMap::new(),
5481 }
5482 };
5483
5484 let optional_headers_method = if has_optional_headers {
5486 quote! {
5487 pub fn set_optional_headers(&mut self, headers: std::collections::BTreeMap<String, String>) {
5489 self.optional_headers = headers;
5490 }
5491 }
5492 } else {
5493 TokenStream::new()
5494 };
5495
5496 let constructor = quote! {
5497 impl #client_name {
5498 pub fn new() -> Self {
5500 Self {
5501 #constructor_fields
5502 }
5503 }
5504
5505 pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
5507 self.base_url = base_url.into();
5508 self
5509 }
5510
5511 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
5513 self.api_key = Some(api_key.into());
5514 self
5515 }
5516
5517 pub fn with_max_response_body_bytes(mut self, limit: usize) -> Self {
5519 self.sse_client = self.sse_client.with_max_error_body_bytes(limit);
5520 self
5521 }
5522
5523 pub fn with_header(
5525 mut self,
5526 name: impl Into<String>,
5527 value: impl Into<String>,
5528 ) -> Self {
5529 self.custom_headers.insert(name.into(), value.into());
5530 self
5531 }
5532
5533 pub fn with_http_client(mut self, client: reqwest::Client) -> Self {
5535 self.sse_client = self.sse_client.with_http_client(client);
5536 self
5537 }
5538
5539 #optional_headers_method
5540 }
5541 };
5542
5543 let mut trait_impls = Vec::new();
5545 for endpoint in &streaming_config.endpoints {
5546 let trait_impl = self.generate_endpoint_trait_impl(endpoint, &client_name, analysis)?;
5547 trait_impls.push(trait_impl);
5548 }
5549
5550 let default_impl = quote! {
5552 impl Default for #client_name {
5553 fn default() -> Self {
5554 Self::new()
5555 }
5556 }
5557 };
5558
5559 Ok(quote! {
5560 #[derive(Debug, Clone)]
5562 pub struct #client_name {
5563 #(#struct_fields,)*
5564 }
5565
5566 #constructor
5567
5568 #default_impl
5569
5570 #(#trait_impls)*
5571 })
5572 }
5573
5574 fn generate_endpoint_trait_impl(
5576 &self,
5577 endpoint: &crate::streaming::StreamingEndpoint,
5578 client_name: &proc_macro2::Ident,
5579 analysis: &SchemaAnalysis,
5580 ) -> Result<TokenStream> {
5581 use crate::streaming::HttpMethod;
5582
5583 let trait_name = format_ident!(
5584 "{}StreamingClient",
5585 self.to_rust_type_name(&endpoint.operation_id)
5586 );
5587 let method_name =
5588 format_ident!("stream_{}", self.to_rust_field_name(&endpoint.operation_id));
5589 let event_type =
5590 format_ident!("{}", self.resolve_streaming_event_type(endpoint, analysis)?);
5591
5592 let mut header_setup = Vec::new();
5594 for (name, value) in &endpoint.required_headers {
5595 header_setup.push(quote! {
5596 headers.insert(#name, HeaderValue::from_static(#value));
5597 });
5598 }
5599
5600 if let Some(auth_header) = &endpoint.auth_header {
5603 match auth_header {
5604 crate::streaming::AuthHeader::Bearer(header_name) => {
5605 header_setup.push(quote! {
5606 if let Some(ref api_key) = self.api_key {
5607 headers.insert(#header_name, HeaderValue::from_str(&format!("Bearer {}", api_key))?);
5608 }
5609 });
5610 }
5611 crate::streaming::AuthHeader::ApiKey(header_name) => {
5612 header_setup.push(quote! {
5613 if let Some(ref api_key) = self.api_key {
5614 headers.insert(#header_name, HeaderValue::from_str(api_key)?);
5615 }
5616 });
5617 }
5618 }
5619 } else {
5620 header_setup.push(quote! {
5622 if let Some(ref api_key) = self.api_key {
5623 headers.insert("Authorization", HeaderValue::from_str(&format!("Bearer {}", api_key))?);
5624 }
5625 });
5626 }
5627
5628 header_setup.push(quote! {
5630 for (name, value) in &self.custom_headers {
5631 if let (Ok(header_name), Ok(header_value)) = (reqwest::header::HeaderName::from_bytes(name.as_bytes()), HeaderValue::from_str(value)) {
5632 headers.insert(header_name, header_value);
5633 }
5634 }
5635 });
5636
5637 if !endpoint.optional_headers.is_empty() {
5639 header_setup.push(quote! {
5640 for (key, value) in &self.optional_headers {
5641 if let (Ok(header_name), Ok(header_value)) = (reqwest::header::HeaderName::from_bytes(key.as_bytes()), HeaderValue::from_str(value)) {
5642 headers.insert(header_name, header_value);
5643 }
5644 }
5645 });
5646 }
5647
5648 match endpoint.http_method {
5650 HttpMethod::Get => self.generate_get_streaming_impl(
5651 endpoint,
5652 client_name,
5653 &trait_name,
5654 &method_name,
5655 &event_type,
5656 &header_setup,
5657 ),
5658 HttpMethod::Post => self.generate_post_streaming_impl(
5659 endpoint,
5660 client_name,
5661 &trait_name,
5662 &method_name,
5663 &event_type,
5664 &header_setup,
5665 analysis,
5666 ),
5667 }
5668 }
5669
5670 fn generate_get_streaming_impl(
5672 &self,
5673 endpoint: &crate::streaming::StreamingEndpoint,
5674 client_name: &proc_macro2::Ident,
5675 trait_name: &proc_macro2::Ident,
5676 method_name: &proc_macro2::Ident,
5677 event_type: &proc_macro2::Ident,
5678 header_setup: &[TokenStream],
5679 ) -> Result<TokenStream> {
5680 let path = &endpoint.path;
5681
5682 let mut param_defs = Vec::new();
5684 let mut query_params = Vec::new();
5685
5686 for qp in &endpoint.query_parameters {
5687 let param_name = format_ident!("{}", self.to_rust_field_name(&qp.name));
5688 let param_name_str = &qp.name;
5689
5690 if qp.required {
5691 param_defs.push(quote! { #param_name: &str });
5692 query_params.push(quote! {
5693 url.query_pairs_mut().append_pair(#param_name_str, #param_name);
5694 });
5695 } else {
5696 param_defs.push(quote! { #param_name: Option<&str> });
5697 query_params.push(quote! {
5698 if let Some(v) = #param_name {
5699 url.query_pairs_mut().append_pair(#param_name_str, v);
5700 }
5701 });
5702 }
5703 }
5704
5705 let url_construction = quote! {
5707 let base_url = url::Url::parse(&self.base_url)
5708 .map_err(|e| StreamingError::Connection(format!("Invalid base URL: {}", e)))?;
5709 let path_to_join = #path.trim_start_matches('/');
5710 let mut url = base_url.join(path_to_join)
5711 .map_err(|e| StreamingError::Connection(format!("URL join error: {}", e)))?;
5712 #(#query_params)*
5713 };
5714
5715 let instrument_skip = quote! { #[instrument(skip(self), name = "streaming_get_request")] };
5716
5717 Ok(quote! {
5718 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
5719 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
5720 impl #trait_name for #client_name {
5721 type Error = StreamingError;
5722
5723 #instrument_skip
5724 async fn #method_name(
5725 &self,
5726 #(#param_defs),*
5727 ) -> Result<BoxSseStream<Result<#event_type, Self::Error>>, Self::Error> {
5728 debug!("Starting streaming GET request");
5729
5730 let mut headers = HeaderMap::new();
5731 #(#header_setup)*
5732
5733 #url_construction
5734 let url_str = url.to_string();
5735 debug!("Making streaming GET request to: {}", url_str);
5736
5737 let request_builder = self.sse_client
5738 .get(url_str)
5739 .headers(headers);
5740
5741 debug!("Creating SSE stream from request");
5742 let stream = self
5743 .sse_client
5744 .stream::<#event_type>(request_builder)
5745 .await?;
5746 info!("SSE stream created successfully");
5747 Ok(stream)
5748 }
5749 }
5750 })
5751 }
5752
5753 #[allow(clippy::too_many_arguments)]
5755 fn generate_post_streaming_impl(
5756 &self,
5757 endpoint: &crate::streaming::StreamingEndpoint,
5758 client_name: &proc_macro2::Ident,
5759 trait_name: &proc_macro2::Ident,
5760 method_name: &proc_macro2::Ident,
5761 event_type: &proc_macro2::Ident,
5762 header_setup: &[TokenStream],
5763 analysis: &SchemaAnalysis,
5764 ) -> Result<TokenStream> {
5765 let path = &endpoint.path;
5766
5767 let request_type = self
5769 .find_request_type_for_operation(&endpoint.operation_id, analysis)
5770 .unwrap_or_else(|| "serde_json::Value".to_string());
5771 let request_type_ident = if request_type.contains("::") {
5772 let parts: Vec<&str> = request_type.split("::").collect();
5773 let path_parts: Vec<_> = parts.iter().map(|p| format_ident!("{}", p)).collect();
5774 quote! { #(#path_parts)::* }
5775 } else {
5776 let ident = format_ident!("{}", request_type);
5777 quote! { #ident }
5778 };
5779
5780 let url_construction = quote! {
5782 let base_url = url::Url::parse(&self.base_url)
5783 .map_err(|e| StreamingError::Connection(format!("Invalid base URL: {}", e)))?;
5784 let path_to_join = #path.trim_start_matches('/');
5785 let url = base_url.join(path_to_join)
5786 .map_err(|e| StreamingError::Connection(format!("URL join error: {}", e)))?
5787 .to_string();
5788 };
5789
5790 let stream_param = &endpoint.stream_parameter;
5792 let stream_setup = if stream_param.is_empty() {
5793 quote! {
5794 let streaming_request = request;
5795 }
5796 } else {
5797 quote! {
5798 let mut streaming_request = request;
5800 if let Ok(mut request_value) = serde_json::to_value(&streaming_request) {
5801 if let Some(obj) = request_value.as_object_mut() {
5802 obj.insert(#stream_param.to_string(), serde_json::Value::Bool(true));
5803 }
5804 streaming_request = serde_json::from_value(request_value)?;
5805 }
5806 }
5807 };
5808
5809 Ok(quote! {
5810 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
5811 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
5812 impl #trait_name for #client_name {
5813 type Error = StreamingError;
5814
5815 #[instrument(skip(self, request), name = "streaming_post_request")]
5816 async fn #method_name(
5817 &self,
5818 request: #request_type_ident,
5819 ) -> Result<BoxSseStream<Result<#event_type, Self::Error>>, Self::Error> {
5820 debug!("Starting streaming POST request");
5821
5822 #stream_setup
5823
5824 let mut headers = HeaderMap::new();
5825 #(#header_setup)*
5826
5827 #url_construction
5828 debug!("Making streaming POST request to: {}", url);
5829
5830 let request_builder = self.sse_client
5831 .post(&url)
5832 .headers(headers)
5833 .json(&streaming_request);
5834
5835 debug!("Creating SSE stream from request");
5836 let stream = self
5837 .sse_client
5838 .stream::<#event_type>(request_builder)
5839 .await?;
5840 info!("SSE stream created successfully");
5841 Ok(stream)
5842 }
5843 }
5844 })
5845 }
5846
5847 fn generate_sse_runtime(&self) -> Result<String> {
5849 self.generate_sse_runtime_with_bindings(None)
5850 }
5851
5852 fn generate_sse_runtime_with_bindings(
5853 &self,
5854 bindings: Option<&mut crate::bindings::BindingsMetadata>,
5855 ) -> Result<String> {
5856 let provenance_attribute = self.provenance_attribute();
5857 let error_types = self.generate_streaming_error_types()?;
5858 let parser = self.generate_sse_parser_utilities()?;
5859 let tokens = quote! {
5860 #provenance_attribute
5864 #![allow(clippy::format_in_format_args)]
5865
5866 use futures_util::{Stream, StreamExt};
5867 use std::pin::Pin;
5868 use std::time::Duration;
5869 use tracing::debug;
5870
5871 #[cfg(not(target_arch = "wasm32"))]
5880 pub type BoxSseStream<T> = Pin<Box<dyn Stream<Item = T> + Send>>;
5881 #[cfg(target_arch = "wasm32")]
5882 pub type BoxSseStream<T> = Pin<Box<dyn Stream<Item = T>>>;
5883
5884 #error_types
5885
5886 #[derive(Debug, Clone)]
5888 pub struct SseClient {
5889 http_client: reqwest::Client,
5890 max_error_body_bytes: usize,
5891 reconnect_options: Option<SseReconnectOptions>,
5892 }
5893
5894 impl SseClient {
5895 pub fn new() -> Self {
5896 Self {
5897 http_client: reqwest::Client::new(),
5898 max_error_body_bytes: DEFAULT_MAX_SSE_ERROR_BODY_BYTES,
5899 reconnect_options: None,
5900 }
5901 }
5902
5903 pub fn with_http_client(mut self, client: reqwest::Client) -> Self {
5904 self.http_client = client;
5905 self
5906 }
5907
5908 pub fn with_max_error_body_bytes(mut self, limit: usize) -> Self {
5909 self.max_error_body_bytes = limit;
5910 self
5911 }
5912
5913 pub fn with_reconnect_options(mut self, options: SseReconnectOptions) -> Self {
5915 self.reconnect_options = Some(options);
5916 self
5917 }
5918
5919 pub fn get(&self, url: impl reqwest::IntoUrl) -> reqwest::RequestBuilder {
5920 self.http_client.get(url)
5921 }
5922
5923 pub fn post(&self, url: impl reqwest::IntoUrl) -> reqwest::RequestBuilder {
5924 self.http_client.post(url)
5925 }
5926
5927 pub async fn stream<T>(
5928 &self,
5929 request_builder: reqwest::RequestBuilder,
5930 ) -> Result<BoxSseStream<Result<T, StreamingError>>, StreamingError>
5931 where
5932 T: serde::de::DeserializeOwned + Send + 'static,
5933 {
5934 if let Some(options) = self.reconnect_options.clone() {
5935 parse_sse_json_reconnecting_with_limit(
5936 request_builder,
5937 self.max_error_body_bytes,
5938 options,
5939 ).await
5940 } else {
5941 parse_sse_json_stream_with_limit(
5942 request_builder,
5943 self.max_error_body_bytes,
5944 ).await
5945 }
5946 }
5947
5948 pub async fn stream_raw(
5950 &self,
5951 request_builder: reqwest::RequestBuilder,
5952 ) -> Result<BoxSseStream<Result<SseEvent<String>, StreamingError>>, StreamingError> {
5953 parse_sse_raw_stream_with_limit(request_builder, self.max_error_body_bytes).await
5954 }
5955
5956 pub async fn stream_json<T>(
5958 &self,
5959 request_builder: reqwest::RequestBuilder,
5960 ) -> Result<BoxSseStream<Result<SseEvent<T>, StreamingError>>, StreamingError>
5961 where
5962 T: serde::de::DeserializeOwned + Send + 'static,
5963 {
5964 parse_sse_json_events_with_limit(request_builder, self.max_error_body_bytes).await
5965 }
5966
5967 pub async fn stream_raw_reconnecting(
5969 &self,
5970 request_builder: reqwest::RequestBuilder,
5971 ) -> Result<BoxSseStream<Result<SseEvent<String>, StreamingError>>, StreamingError> {
5972 parse_sse_raw_reconnecting_with_limit(
5973 request_builder,
5974 self.max_error_body_bytes,
5975 self.reconnect_options.clone().unwrap_or_default(),
5976 ).await
5977 }
5978
5979 pub async fn stream_json_reconnecting<T>(
5981 &self,
5982 request_builder: reqwest::RequestBuilder,
5983 ) -> Result<BoxSseStream<Result<SseEvent<T>, StreamingError>>, StreamingError>
5984 where
5985 T: serde::de::DeserializeOwned + Send + 'static,
5986 {
5987 parse_sse_json_reconnecting_events_with_limit(
5988 request_builder,
5989 self.max_error_body_bytes,
5990 self.reconnect_options.clone().unwrap_or_default(),
5991 ).await
5992 }
5993 }
5994
5995 impl Default for SseClient {
5996 fn default() -> Self {
5997 Self::new()
5998 }
5999 }
6000
6001 #parser
6002 };
6003 let syntax_tree = syn::parse2::<syn::File>(tokens).map_err(|error| {
6004 GeneratorError::CodeGenError(format!("Failed to parse generated sse.rs: {error}"))
6005 })?;
6006 if let Some(bindings) = bindings {
6007 bindings.collect("sse", &syntax_tree);
6008 }
6009 Ok(prettyplease::unparse(&syntax_tree))
6010 }
6011
6012 fn generate_sse_parser_utilities(&self) -> Result<TokenStream> {
6014 Ok(quote! {
6015 pub const DEFAULT_MAX_SSE_ERROR_BODY_BYTES: usize = 8 * 1024 * 1024;
6017
6018 async fn __read_bounded_streaming_error_body(
6019 response: reqwest::Response,
6020 limit: usize,
6021 ) -> Result<Vec<u8>, StreamingError> {
6022 let mut body = Vec::new();
6023 let mut chunks = response.bytes_stream();
6026 while let Some(chunk) = chunks.next().await {
6027 let chunk = chunk?;
6028 let next_len = body.len().checked_add(chunk.len());
6029 if next_len.is_none_or(|next_len| next_len > limit) {
6030 return Err(StreamingError::ResponseTooLarge { limit });
6031 }
6032 body.extend_from_slice(&chunk);
6033 }
6034 Ok(body)
6035 }
6036
6037 #[derive(Debug, Clone, PartialEq, Eq)]
6039 pub struct SseEvent<T> {
6040 pub event: String,
6042 pub data: T,
6044 pub id: Option<String>,
6046 pub retry: Option<Duration>,
6048 }
6049
6050 #[derive(Debug, Clone)]
6052 pub struct SseReconnectOptions {
6053 pub max_retries: u32,
6055 pub initial_retry_delay: Duration,
6057 pub max_retry_delay: Duration,
6059 pub backoff_multiplier: f64,
6061 }
6062
6063 impl Default for SseReconnectOptions {
6064 fn default() -> Self {
6065 Self {
6066 max_retries: 3,
6067 initial_retry_delay: Duration::from_secs(3),
6068 max_retry_delay: Duration::from_secs(30),
6069 backoff_multiplier: 2.0,
6070 }
6071 }
6072 }
6073
6074 impl SseReconnectOptions {
6075 fn delay(&self, attempt: u32, server_retry: Option<Duration>) -> Duration {
6076 if let Some(delay) = server_retry {
6077 return delay.min(self.max_retry_delay);
6078 }
6079 let multiplier = self.backoff_multiplier.max(1.0);
6080 let millis = self.initial_retry_delay.as_millis() as f64
6081 * multiplier.powi(attempt.min(63) as i32);
6082 Duration::from_millis(
6083 millis.min(self.max_retry_delay.as_millis() as f64) as u64,
6084 )
6085 }
6086 }
6087
6088 #[derive(Default)]
6089 struct __SseDecoder {
6090 line: Vec<u8>,
6091 event: String,
6092 data: Vec<String>,
6093 last_event_id: Option<String>,
6094 retry_delay: Option<Duration>,
6095 event_retry: Option<Duration>,
6096 saw_carriage_return: bool,
6097 }
6098
6099 impl __SseDecoder {
6100 fn feed(
6101 &mut self,
6102 chunk: &[u8],
6103 ) -> Vec<Result<SseEvent<String>, StreamingError>> {
6104 let mut messages = Vec::new();
6105 for &byte in chunk {
6106 if self.saw_carriage_return {
6107 self.saw_carriage_return = false;
6108 if byte == b'\n' {
6109 continue;
6110 }
6111 }
6112
6113 match byte {
6114 b'\n' => self.finish_line(&mut messages),
6115 b'\r' => {
6116 self.finish_line(&mut messages);
6117 self.saw_carriage_return = true;
6118 }
6119 _ => self.line.push(byte),
6120 }
6121 }
6122 messages
6123 }
6124
6125 fn finish(&mut self) -> Vec<Result<SseEvent<String>, StreamingError>> {
6126 let mut messages = Vec::new();
6127 if !self.line.is_empty() {
6128 self.finish_line(&mut messages);
6129 }
6130 self.dispatch(&mut messages);
6131 messages
6132 }
6133
6134 fn finish_line(
6135 &mut self,
6136 messages: &mut Vec<Result<SseEvent<String>, StreamingError>>,
6137 ) {
6138 let line = std::mem::take(&mut self.line);
6139 let line = match String::from_utf8(line) {
6140 Ok(line) => line,
6141 Err(error) => {
6142 messages.push(Err(StreamingError::Parsing(format!(
6143 "SSE line is not valid UTF-8: {}",
6144 error
6145 ))));
6146 return;
6147 }
6148 };
6149
6150 if line.is_empty() {
6151 self.dispatch(messages);
6152 return;
6153 }
6154 if line.starts_with(':') {
6155 return;
6156 }
6157
6158 let (field, value) = line
6159 .split_once(':')
6160 .map_or((line.as_str(), ""), |(field, value)| {
6161 (field, value.strip_prefix(' ').unwrap_or(value))
6162 });
6163 match field {
6164 "event" => self.event = value.to_string(),
6165 "data" => self.data.push(value.to_string()),
6166 "id" if !value.contains('\0') => {
6167 self.last_event_id = (!value.is_empty()).then(|| value.to_string());
6168 }
6169 "retry" if value.bytes().all(|byte| byte.is_ascii_digit()) => {
6170 if let Ok(milliseconds) = value.parse::<u64>() {
6171 let delay = Duration::from_millis(milliseconds);
6172 self.retry_delay = Some(delay);
6173 self.event_retry = Some(delay);
6174 }
6175 }
6176 _ => {}
6177 }
6178 }
6179
6180 fn dispatch(
6181 &mut self,
6182 messages: &mut Vec<Result<SseEvent<String>, StreamingError>>,
6183 ) {
6184 if self.data.is_empty() {
6185 self.event.clear();
6186 self.event_retry = None;
6187 return;
6188 }
6189 messages.push(Ok(SseEvent {
6190 event: if self.event.is_empty() {
6191 "message".to_string()
6192 } else {
6193 std::mem::take(&mut self.event)
6194 },
6195 data: std::mem::take(&mut self.data).join("\n"),
6196 id: self.last_event_id.clone(),
6197 retry: self.event_retry.take(),
6198 }));
6199 self.event.clear();
6200 }
6201
6202 fn reset_for_reconnect(&mut self) {
6203 self.line.clear();
6204 self.event.clear();
6205 self.data.clear();
6206 self.event_retry = None;
6207 self.saw_carriage_return = false;
6208 }
6209 }
6210
6211 fn __deserialize_sse_event<T>(
6212 event: SseEvent<String>,
6213 ) -> Option<Result<SseEvent<T>, StreamingError>>
6214 where
6215 T: serde::de::DeserializeOwned,
6216 {
6217 if event.data.trim() == "[DONE]" {
6218 return None;
6219 }
6220 if event.event == "ping" {
6221 debug!("Received SSE ping event, skipping");
6222 return None;
6223 }
6224 if event.data.trim().is_empty() {
6225 debug!("Empty SSE data, skipping");
6226 return None;
6227 }
6228
6229 let json_value = match serde_json::from_str::<serde_json::Value>(&event.data) {
6230 Ok(value) => value,
6231 Err(error) => {
6232 return Some(Err(StreamingError::Parsing(format!(
6233 "SSE event is not valid JSON: {} ({})",
6234 event.data, error
6235 ))));
6236 }
6237 };
6238 let is_ping = json_value
6239 .get("event")
6240 .or_else(|| json_value.get("type"))
6241 .and_then(serde_json::Value::as_str)
6242 .is_some_and(|event| event == "ping");
6243 if is_ping {
6244 debug!("Received ping event in JSON data, skipping");
6245 return None;
6246 }
6247
6248 Some(
6249 serde_json::from_value::<T>(json_value)
6250 .map(|data| SseEvent {
6251 event: event.event.clone(),
6252 data,
6253 id: event.id.clone(),
6254 retry: event.retry,
6255 })
6256 .map_err(|error| StreamingError::Parsing(format!(
6257 "Failed to parse SSE event: {} (raw: {}, event: {})",
6258 error, event.data, event.event
6259 ))),
6260 )
6261 }
6262
6263 pub async fn parse_sse_stream<T>(
6265 request_builder: reqwest::RequestBuilder
6266 ) -> Result<BoxSseStream<Result<T, StreamingError>>, StreamingError>
6267 where
6268 T: serde::de::DeserializeOwned + Send + 'static,
6269 {
6270 parse_sse_json_stream_with_limit(
6271 request_builder,
6272 DEFAULT_MAX_SSE_ERROR_BODY_BYTES,
6273 ).await
6274 }
6275
6276 struct __SseOpenError {
6277 error: StreamingError,
6278 retryable: bool,
6279 }
6280
6281 async fn __open_sse_response(
6282 request_builder: reqwest::RequestBuilder,
6283 max_response_body_bytes: usize,
6284 ) -> Result<reqwest::Response, __SseOpenError> {
6285 let response = request_builder.send().await.map_err(|error| __SseOpenError {
6286 error: error.into(),
6287 retryable: true,
6288 })?;
6289 if !response.status().is_success() {
6290 let status = response.status();
6291 let retryable = status.as_u16() == 429 || status.is_server_error();
6292 let error = match __read_bounded_streaming_error_body(
6293 response,
6294 max_response_body_bytes,
6295 ).await {
6296 Ok(body) => StreamingError::Connection(format!(
6297 "HTTP {} error: {}",
6298 status.as_u16(),
6299 String::from_utf8_lossy(&body)
6300 )),
6301 Err(error) => error,
6302 };
6303 return Err(__SseOpenError { error, retryable });
6304 }
6305
6306 let content_type = response
6307 .headers()
6308 .get(reqwest::header::CONTENT_TYPE)
6309 .and_then(|value| value.to_str().ok())
6310 .unwrap_or_default();
6311 if !content_type
6312 .split(';')
6313 .next()
6314 .is_some_and(|value| value.trim().eq_ignore_ascii_case("text/event-stream"))
6315 {
6316 let error = StreamingError::Parsing(format!(
6317 "Expected text/event-stream response, received {}",
6318 if content_type.is_empty() { "no Content-Type" } else { content_type }
6319 ));
6320 return Err(__SseOpenError { error, retryable: false });
6321 }
6322
6323 debug!("SSE connection opened");
6324 Ok(response)
6325 }
6326
6327 pub fn parse_sse_response(
6330 response: reqwest::Response,
6331 ) -> BoxSseStream<Result<SseEvent<String>, StreamingError>> {
6332 __raw_response_stream(response)
6333 }
6334
6335 fn __raw_response_stream(
6336 response: reqwest::Response,
6337 ) -> BoxSseStream<Result<SseEvent<String>, StreamingError>> {
6338 let stream = futures_util::stream::unfold(
6339 (
6340 response.bytes_stream(),
6341 __SseDecoder::default(),
6342 std::collections::VecDeque::<Result<SseEvent<String>, StreamingError>>::new(),
6343 false,
6344 ),
6345 |(mut chunks, mut decoder, mut pending, mut done)| async move {
6346 loop {
6347 if let Some(item) = pending.pop_front() {
6348 return Some((item, (chunks, decoder, pending, done)));
6349 }
6350 if done {
6351 debug!("SSE stream completed normally");
6352 return None;
6353 }
6354
6355 match chunks.next().await {
6356 Some(Ok(chunk)) => {
6357 for event in decoder.feed(&chunk) {
6358 let is_done = event
6359 .as_ref()
6360 .is_ok_and(|event| event.data.trim() == "[DONE]");
6361 pending.push_back(event);
6362 if is_done {
6363 done = true;
6364 break;
6365 }
6366 }
6367 }
6368 Some(Err(error)) => {
6369 done = true;
6370 pending.push_back(Err(error.into()));
6371 }
6372 None => {
6373 done = true;
6374 for event in decoder.finish() {
6375 pending.push_back(event);
6376 }
6377 }
6378 }
6379 }
6380 }
6381 );
6382
6383 Box::pin(stream)
6384 }
6385
6386 async fn parse_sse_raw_stream_with_limit(
6387 request_builder: reqwest::RequestBuilder,
6388 max_response_body_bytes: usize,
6389 ) -> Result<BoxSseStream<Result<SseEvent<String>, StreamingError>>, StreamingError> {
6390 Ok(match __open_sse_response(request_builder, max_response_body_bytes).await {
6391 Ok(response) => __raw_response_stream(response),
6392 Err(error) => Box::pin(futures_util::stream::once(async move { Err(error.error) })),
6393 })
6394 }
6395
6396 fn __json_event_stream<T>(
6397 raw: BoxSseStream<Result<SseEvent<String>, StreamingError>>,
6398 ) -> BoxSseStream<Result<SseEvent<T>, StreamingError>>
6399 where
6400 T: serde::de::DeserializeOwned + Send + 'static,
6401 {
6402 Box::pin(raw.filter_map(|event| async move {
6403 match event {
6404 Ok(event) => __deserialize_sse_event(event),
6405 Err(error) => Some(Err(error)),
6406 }
6407 }))
6408 }
6409
6410 async fn parse_sse_json_events_with_limit<T>(
6411 request_builder: reqwest::RequestBuilder,
6412 max_response_body_bytes: usize,
6413 ) -> Result<BoxSseStream<Result<SseEvent<T>, StreamingError>>, StreamingError>
6414 where
6415 T: serde::de::DeserializeOwned + Send + 'static,
6416 {
6417 Ok(__json_event_stream(
6418 parse_sse_raw_stream_with_limit(request_builder, max_response_body_bytes).await?,
6419 ))
6420 }
6421
6422 async fn parse_sse_json_stream_with_limit<T>(
6423 request_builder: reqwest::RequestBuilder,
6424 max_response_body_bytes: usize,
6425 ) -> Result<BoxSseStream<Result<T, StreamingError>>, StreamingError>
6426 where
6427 T: serde::de::DeserializeOwned + Send + 'static,
6428 {
6429 let events = parse_sse_json_events_with_limit(request_builder, max_response_body_bytes).await?;
6430 Ok(Box::pin(events.map(|event| event.map(|event| event.data))))
6431 }
6432
6433 struct __ReconnectState {
6434 request: reqwest::RequestBuilder,
6435 body: Option<BoxSseStream<Result<bytes::Bytes, reqwest::Error>>>,
6436 decoder: __SseDecoder,
6437 pending: std::collections::VecDeque<Result<SseEvent<String>, StreamingError>>,
6438 options: SseReconnectOptions,
6439 max_response_body_bytes: usize,
6440 attempts: u32,
6441 wait_before_open: bool,
6442 done: bool,
6443 }
6444
6445 async fn parse_sse_raw_reconnecting_with_limit(
6446 request_builder: reqwest::RequestBuilder,
6447 max_response_body_bytes: usize,
6448 options: SseReconnectOptions,
6449 ) -> Result<BoxSseStream<Result<SseEvent<String>, StreamingError>>, StreamingError> {
6450 if request_builder.try_clone().is_none() {
6451 return Err(StreamingError::Connection(
6452 "SSE reconnection requires a cloneable request body".to_string(),
6453 ));
6454 }
6455
6456 let stream = futures_util::stream::unfold(
6457 __ReconnectState {
6458 request: request_builder,
6459 body: None,
6460 decoder: __SseDecoder::default(),
6461 pending: std::collections::VecDeque::new(),
6462 options,
6463 max_response_body_bytes,
6464 attempts: 0,
6465 wait_before_open: false,
6466 done: false,
6467 },
6468 |mut state| async move {
6469 loop {
6470 if let Some(item) = state.pending.pop_front() {
6471 return Some((item, state));
6472 }
6473 if state.done {
6474 return None;
6475 }
6476
6477 if state.body.is_none() {
6478 if state.wait_before_open {
6479 let delay = state.options.delay(
6480 state.attempts.saturating_sub(1),
6481 state.decoder.retry_delay,
6482 );
6483 debug!(?delay, attempt = state.attempts, "Reconnecting SSE stream");
6484 futures_timer::Delay::new(delay).await;
6485 state.wait_before_open = false;
6486 }
6487
6488 let mut request = state.request.try_clone().expect("request clone checked");
6489 if let Some(last_event_id) = state.decoder.last_event_id.as_deref() {
6490 request = request.header("Last-Event-ID", last_event_id);
6491 }
6492 match __open_sse_response(request, state.max_response_body_bytes).await {
6493 Ok(response) => state.body = Some(Box::pin(response.bytes_stream())),
6494 Err(error) if error.retryable && state.attempts < state.options.max_retries => {
6495 state.attempts += 1;
6496 state.wait_before_open = true;
6497 continue;
6498 }
6499 Err(error) => {
6500 state.done = true;
6501 state.pending.push_back(Err(error.error));
6502 continue;
6503 }
6504 }
6505 }
6506
6507 let next = state.body.as_mut().expect("response opened").next().await;
6508 match next {
6509 Some(Ok(chunk)) => {
6510 let events = state.decoder.feed(&chunk);
6511 if !events.is_empty() {
6512 state.attempts = 0;
6513 }
6514 for event in events {
6515 let is_done = event
6516 .as_ref()
6517 .is_ok_and(|event| event.data.trim() == "[DONE]");
6518 state.pending.push_back(event);
6519 if is_done {
6520 state.done = true;
6521 state.body = None;
6522 break;
6523 }
6524 }
6525 }
6526 None => {
6527 let events = state.decoder.finish();
6528 if !events.is_empty() {
6529 state.attempts = 0;
6530 }
6531 for event in events {
6532 let is_done = event
6533 .as_ref()
6534 .is_ok_and(|event| event.data.trim() == "[DONE]");
6535 state.pending.push_back(event);
6536 if is_done {
6537 state.done = true;
6538 break;
6539 }
6540 }
6541 state.body = None;
6542 state.decoder.reset_for_reconnect();
6543 if !state.done {
6544 if state.attempts < state.options.max_retries {
6545 state.attempts += 1;
6546 state.wait_before_open = true;
6547 } else {
6548 state.done = true;
6549 }
6550 }
6551 }
6552 Some(Err(error)) => {
6553 state.body = None;
6554 state.decoder.reset_for_reconnect();
6555 if state.attempts < state.options.max_retries {
6556 state.attempts += 1;
6557 state.wait_before_open = true;
6558 } else {
6559 state.done = true;
6560 state.pending.push_back(Err(error.into()));
6561 }
6562 }
6563 }
6564 }
6565 },
6566 );
6567 Ok(Box::pin(stream))
6568 }
6569
6570 async fn parse_sse_json_reconnecting_events_with_limit<T>(
6571 request_builder: reqwest::RequestBuilder,
6572 max_response_body_bytes: usize,
6573 options: SseReconnectOptions,
6574 ) -> Result<BoxSseStream<Result<SseEvent<T>, StreamingError>>, StreamingError>
6575 where
6576 T: serde::de::DeserializeOwned + Send + 'static,
6577 {
6578 Ok(__json_event_stream(
6579 parse_sse_raw_reconnecting_with_limit(
6580 request_builder,
6581 max_response_body_bytes,
6582 options,
6583 ).await?,
6584 ))
6585 }
6586
6587 async fn parse_sse_json_reconnecting_with_limit<T>(
6588 request_builder: reqwest::RequestBuilder,
6589 max_response_body_bytes: usize,
6590 options: SseReconnectOptions,
6591 ) -> Result<BoxSseStream<Result<T, StreamingError>>, StreamingError>
6592 where
6593 T: serde::de::DeserializeOwned + Send + 'static,
6594 {
6595 let events = parse_sse_json_reconnecting_events_with_limit(
6596 request_builder,
6597 max_response_body_bytes,
6598 options,
6599 ).await?;
6600 Ok(Box::pin(events.map(|event| event.map(|event| event.data))))
6601 }
6602 })
6603 }
6604
6605 fn generate_reconnection_utilities(
6607 &self,
6608 reconnect_config: &crate::streaming::ReconnectionConfig,
6609 ) -> Result<TokenStream> {
6610 let max_retries = reconnect_config.max_retries;
6611 let initial_delay = reconnect_config.initial_delay_ms;
6612 let max_delay = reconnect_config.max_delay_ms;
6613 let backoff_multiplier = reconnect_config.backoff_multiplier;
6614
6615 Ok(quote! {
6616 #[derive(Debug, Clone)]
6618 pub struct ReconnectionManager {
6619 max_retries: u32,
6620 initial_delay_ms: u64,
6621 max_delay_ms: u64,
6622 backoff_multiplier: f64,
6623 current_attempt: u32,
6624 }
6625
6626 impl ReconnectionManager {
6627 pub fn new() -> Self {
6629 Self {
6630 max_retries: #max_retries,
6631 initial_delay_ms: #initial_delay,
6632 max_delay_ms: #max_delay,
6633 backoff_multiplier: #backoff_multiplier,
6634 current_attempt: 0,
6635 }
6636 }
6637
6638 pub fn should_retry(&self) -> bool {
6640 self.current_attempt < self.max_retries
6641 }
6642
6643 pub fn next_retry_delay(&mut self) -> Duration {
6645 if !self.should_retry() {
6646 return Duration::from_secs(0);
6647 }
6648
6649 let delay_ms = (self.initial_delay_ms as f64
6650 * self.backoff_multiplier.powi(self.current_attempt as i32)) as u64;
6651 let delay_ms = delay_ms.min(self.max_delay_ms);
6652
6653 self.current_attempt += 1;
6654 Duration::from_millis(delay_ms)
6655 }
6656
6657 pub fn reset(&mut self) {
6659 self.current_attempt = 0;
6660 }
6661
6662 pub fn current_attempt(&self) -> u32 {
6664 self.current_attempt
6665 }
6666 }
6667
6668 impl Default for ReconnectionManager {
6669 fn default() -> Self {
6670 Self::new()
6671 }
6672 }
6673 })
6674 }
6675}