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