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