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