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::{SseClient, SseReconnectOptions};
1118 pub use super::sse::StreamingError;
1119 });
1120 } else {
1121 client_code.extend(quote! {
1122 use super::sse::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::Forbidden => {}
2300 crate::analysis::ObjectAdditionalProperties::Untyped => {
2301 let serde_flatten = if variant.is_none() {
2302 quote! { #[serde(flatten)] }
2303 } else {
2304 TokenStream::new()
2305 };
2306 fields.push(quote! {
2307 #serde_flatten
2309 pub additional_properties:
2310 std::collections::BTreeMap<String, serde_json::Value>,
2311 });
2312 }
2313 crate::analysis::ObjectAdditionalProperties::Typed { value_type } => {
2314 let value_tokens = self.generate_array_item_type(value_type, analysis);
2315 let serde_flatten = if variant.is_none() {
2316 quote! { #[serde(flatten)] }
2317 } else {
2318 TokenStream::new()
2319 };
2320 fields.push(quote! {
2321 #serde_flatten
2324 pub additional_properties:
2325 std::collections::BTreeMap<String, #value_tokens>,
2326 });
2327 }
2328 }
2329
2330 if let (Some(variant), Some(variant_field)) = (variant, variant_field_ident.as_ref()) {
2335 let variant_type = format_ident!("{}", self.to_rust_type_name(&variant.target));
2336 fields.push(quote! {
2337 pub #variant_field: #variant_type,
2339 });
2340 }
2341
2342 let doc_comment = if let Some(desc) = &schema.description {
2343 quote! { #[doc = #desc] }
2344 } else {
2345 TokenStream::new()
2346 };
2347
2348 let can_derive_default = variant.is_none() && required.is_empty();
2356
2357 let derives = match (
2361 self.config.enable_specta,
2362 can_derive_default,
2363 variant.is_some(),
2364 ) {
2365 (true, _, true) => quote! {
2366 #[derive(Debug, Clone)]
2367 #[cfg_attr(feature = "specta", derive(specta::Type))]
2368 },
2369 (false, _, true) => quote! {
2370 #[derive(Debug, Clone)]
2371 },
2372 (true, true, false) => quote! {
2373 #[derive(Debug, Clone, Deserialize, Serialize, Default)]
2374 #[cfg_attr(feature = "specta", derive(specta::Type))]
2375 },
2376 (true, false, false) => quote! {
2377 #[derive(Debug, Clone, Deserialize, Serialize)]
2378 #[cfg_attr(feature = "specta", derive(specta::Type))]
2379 },
2380 (false, true, false) => quote! {
2381 #[derive(Debug, Clone, Deserialize, Serialize, Default)]
2382 },
2383 (false, false, false) => quote! {
2384 #[derive(Debug, Clone, Deserialize, Serialize)]
2385 },
2386 };
2387
2388 let shared_variant_serde = if let (Some(variant), Some(variant_field)) =
2394 (variant, variant_field_ident.as_ref())
2395 {
2396 let variant_type = format_ident!("{}", self.to_rust_type_name(&variant.target));
2397 let helper_name = format_ident!("__{}Base", self.to_rust_type_name(&schema.name));
2398 let variant_properties = self.union_declared_properties(
2399 &variant.target,
2400 analysis,
2401 &mut std::collections::HashSet::new(),
2402 );
2403 let variant_projection_removals = emitted_properties
2404 .iter()
2405 .filter(|emitted| !variant_properties.contains(emitted.wire_name))
2406 .map(|emitted| {
2407 let wire_name = emitted.wire_name;
2408 quote! { object.remove(#wire_name); }
2409 })
2410 .collect::<Vec<_>>();
2411 let mut helper_fields: Vec<TokenStream> = emitted_properties
2412 .iter()
2413 .map(|emitted| {
2414 let field_name = emitted.wire_name;
2415 let property = emitted.property;
2416 let field_ident = &emitted.ident;
2417 let field_type = &emitted.field_type;
2418 let serde_attrs = self.generate_serde_field_attrs(
2419 &schema.name,
2420 field_name,
2421 field_ident,
2422 property,
2423 emitted.is_required,
2424 analysis,
2425 );
2426 quote! {
2427 #serde_attrs
2428 #field_ident: #field_type,
2429 }
2430 })
2431 .collect();
2432 let mut base_initializers: Vec<TokenStream> = emitted_properties
2433 .iter()
2434 .map(|emitted| {
2435 let field_ident = &emitted.ident;
2436 quote! { #field_ident: self.#field_ident.clone(), }
2437 })
2438 .collect();
2439 let mut result_fields: Vec<TokenStream> = emitted_properties
2440 .iter()
2441 .map(|emitted| {
2442 let field_ident = &emitted.ident;
2443 quote! { #field_ident: base.#field_ident, }
2444 })
2445 .collect();
2446
2447 match additional_properties {
2448 crate::analysis::ObjectAdditionalProperties::Forbidden => {}
2449 crate::analysis::ObjectAdditionalProperties::Untyped => {
2450 helper_fields.push(quote! {
2451 #[serde(flatten)]
2452 additional_properties:
2453 std::collections::BTreeMap<String, serde_json::Value>,
2454 });
2455 base_initializers.push(quote! {
2456 additional_properties: self.additional_properties.clone(),
2457 });
2458 result_fields.push(quote! {
2459 additional_properties: base.additional_properties,
2460 });
2461 }
2462 crate::analysis::ObjectAdditionalProperties::Typed { value_type } => {
2463 let value_tokens = self.generate_array_item_type(value_type, analysis);
2464 helper_fields.push(quote! {
2465 #[serde(flatten)]
2466 additional_properties:
2467 std::collections::BTreeMap<String, #value_tokens>,
2468 });
2469 base_initializers.push(quote! {
2470 additional_properties: self.additional_properties.clone(),
2471 });
2472 result_fields.push(quote! {
2473 additional_properties: base.additional_properties,
2474 });
2475 }
2476 }
2477
2478 quote! {
2479 #[derive(Deserialize, Serialize)]
2480 struct #helper_name {
2481 #(#helper_fields)*
2482 }
2483
2484 impl serde::Serialize for #struct_name {
2485 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2486 where
2487 S: serde::Serializer,
2488 {
2489 let base = #helper_name {
2490 #(#base_initializers)*
2491 };
2492 let mut value = serde_json::to_value(base)
2493 .map_err(serde::ser::Error::custom)?;
2494 let variant = serde_json::to_value(&self.#variant_field)
2495 .map_err(serde::ser::Error::custom)?;
2496 let object = value.as_object_mut().ok_or_else(|| {
2497 serde::ser::Error::custom("shared union base did not serialize as an object")
2498 })?;
2499 let variant_object = variant.as_object().ok_or_else(|| {
2500 serde::ser::Error::custom("shared union variant did not serialize as an object")
2501 })?;
2502 for (key, variant_value) in variant_object {
2503 if let Some(base_value) = object.get(key)
2504 && base_value != variant_value
2505 {
2506 return Err(serde::ser::Error::custom(format!(
2507 "shared union field `{key}` serialized conflicting values",
2508 )));
2509 }
2510 object.insert(key.clone(), variant_value.clone());
2511 }
2512 serde::Serialize::serialize(&value, serializer)
2513 }
2514 }
2515
2516 impl<'de> serde::Deserialize<'de> for #struct_name {
2517 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2518 where
2519 D: serde::Deserializer<'de>,
2520 {
2521 let value = serde_json::Value::deserialize(deserializer)?;
2522 let base = serde_json::from_value::<#helper_name>(value.clone())
2523 .map_err(serde::de::Error::custom)?;
2524 let variant = match serde_json::from_value::<#variant_type>(value.clone()) {
2525 Ok(variant) => variant,
2526 Err(complete_error) => {
2527 let mut variant_input = value;
2528 if let Some(object) = variant_input.as_object_mut() {
2529 #(#variant_projection_removals)*
2530 }
2531 serde_json::from_value::<#variant_type>(variant_input).map_err(
2532 |projected_error| serde::de::Error::custom(format!(
2533 "complete shared-union input failed: {complete_error}; projected input failed: {projected_error}",
2534 )),
2535 )?
2536 }
2537 };
2538 Ok(Self {
2539 #(#result_fields)*
2540 #variant_field: variant,
2541 })
2542 }
2543 }
2544 }
2545 } else {
2546 TokenStream::new()
2547 };
2548
2549 let builder = if type_context.index.request_body_roots.contains(&schema.name)
2550 && variant.is_none()
2551 && emitted_properties
2552 .iter()
2553 .any(|property| property.is_required)
2554 && (emitted_properties
2555 .iter()
2556 .any(|property| !property.is_required)
2557 || !matches!(
2558 additional_properties,
2559 crate::analysis::ObjectAdditionalProperties::Forbidden
2560 )) {
2561 self.generate_request_model_builder(
2562 schema,
2563 &emitted_properties,
2564 additional_properties,
2565 analysis,
2566 type_context.index,
2567 )
2568 } else {
2569 TokenStream::new()
2570 };
2571
2572 Ok(quote! {
2573 #doc_comment
2574 #derives
2575 pub struct #struct_name {
2576 #(#fields)*
2577 }
2578
2579 #shared_variant_serde
2580 #builder
2581 })
2582 }
2583
2584 pub(crate) fn emitted_object_properties<'a>(
2589 &self,
2590 schema_name: &str,
2591 properties: &'a BTreeMap<String, crate::analysis::PropertyInfo>,
2592 required: &std::collections::HashSet<String>,
2593 additional_properties: &crate::analysis::ObjectAdditionalProperties,
2594 analysis: &crate::analysis::SchemaAnalysis,
2595 ) -> Vec<EmittedObjectProperty<'a>> {
2596 let mut sorted_properties: Vec<_> = properties.iter().collect();
2597 sorted_properties.sort_by_key(|(name, _)| name.as_str());
2598
2599 let mut used_field_idents = std::collections::HashSet::new();
2600 if !matches!(
2601 additional_properties,
2602 crate::analysis::ObjectAdditionalProperties::Forbidden
2603 ) {
2604 used_field_idents.insert("additional_properties".to_string());
2605 }
2606
2607 let mut emitted = Vec::new();
2608 for (field_name, property) in sorted_properties {
2609 let raw = self.to_rust_field_name(field_name);
2610 let mut chosen = raw.clone();
2611 let mut suffix = 2;
2612 while !used_field_idents.insert(chosen.clone()) {
2613 chosen = format!("{raw}_{suffix}");
2614 suffix += 1;
2615 }
2616 let is_required = required.contains(field_name);
2617 emitted.push(EmittedObjectProperty {
2618 wire_name: field_name,
2619 property,
2620 ident: Self::to_field_ident(&chosen),
2621 is_required,
2622 field_type: self.generate_field_type(
2623 schema_name,
2624 field_name,
2625 property,
2626 is_required,
2627 analysis,
2628 ),
2629 });
2630 }
2631 emitted
2632 }
2633
2634 fn type_generation_index(
2635 &self,
2636 analysis: &crate::analysis::SchemaAnalysis,
2637 ) -> TypeGenerationIndex {
2638 let reserved_type_names = analysis
2639 .schemas
2640 .keys()
2641 .map(|name| self.to_rust_type_name(name))
2642 .collect();
2643 let mut request_body_roots = std::collections::HashSet::new();
2644 for operation in analysis.operations.values() {
2645 let Some(mut current) = operation
2646 .request_body
2647 .as_ref()
2648 .and_then(crate::analysis::RequestBodyContent::schema_name)
2649 else {
2650 continue;
2651 };
2652 while request_body_roots.insert(current.to_string()) {
2653 let Some(crate::analysis::AnalyzedSchema {
2654 schema_type: crate::analysis::SchemaType::Reference { target },
2655 ..
2656 }) = analysis.schemas.get(current)
2657 else {
2658 break;
2659 };
2660 current = target;
2661 }
2662 }
2663 TypeGenerationIndex {
2664 request_body_roots,
2665 reserved_type_names,
2666 }
2667 }
2668
2669 fn generate_request_model_builder(
2670 &self,
2671 schema: &crate::analysis::AnalyzedSchema,
2672 properties: &[EmittedObjectProperty<'_>],
2673 additional_properties: &crate::analysis::ObjectAdditionalProperties,
2674 analysis: &crate::analysis::SchemaAnalysis,
2675 type_index: &TypeGenerationIndex,
2676 ) -> TokenStream {
2677 let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2678 let builder_base = format!("{}Builder", struct_name);
2679 let mut builder_name = builder_base.clone();
2680 let mut suffix = 2;
2681 while type_index.reserved_type_names.contains(&builder_name) {
2682 builder_name = format!("{builder_base}{suffix}");
2683 suffix += 1;
2684 }
2685 let builder_name = format_ident!("{builder_name}");
2686
2687 let required_parameters: Vec<TokenStream> = properties
2688 .iter()
2689 .filter(|property| property.is_required)
2690 .map(|property| {
2691 let ident = &property.ident;
2692 let field_type = &property.field_type;
2693 quote! { #ident: #field_type }
2694 })
2695 .collect();
2696 let required_idents: Vec<&syn::Ident> = properties
2697 .iter()
2698 .filter(|property| property.is_required)
2699 .map(|property| &property.ident)
2700 .collect();
2701 let optional_initializers: Vec<TokenStream> = properties
2702 .iter()
2703 .filter(|property| !property.is_required)
2704 .map(|property| {
2705 let ident = &property.ident;
2706 quote! { #ident: None }
2707 })
2708 .collect();
2709
2710 let additional_initializer = match additional_properties {
2711 crate::analysis::ObjectAdditionalProperties::Forbidden => TokenStream::new(),
2712 crate::analysis::ObjectAdditionalProperties::Untyped
2713 | crate::analysis::ObjectAdditionalProperties::Typed { .. } => quote! {
2714 additional_properties: ::std::collections::BTreeMap::new(),
2715 },
2716 };
2717
2718 let mut used_builder_methods =
2719 std::collections::HashSet::from(["new".to_string(), "build".to_string()]);
2720 if !matches!(
2721 additional_properties,
2722 crate::analysis::ObjectAdditionalProperties::Forbidden
2723 ) {
2724 used_builder_methods.insert("additional_properties".to_string());
2725 }
2726 let optional_setters: Vec<TokenStream> = properties
2727 .iter()
2728 .filter(|property| !property.is_required)
2729 .map(|property| {
2730 let field_ident = &property.ident;
2731 let field_type = self.generate_property_base_type(
2732 &schema.name,
2733 property.wire_name,
2734 property.property,
2735 analysis,
2736 );
2737 let field_name = field_ident.to_string();
2741 let plain_field_name = field_name.strip_prefix("r#").unwrap_or(&field_name);
2742 let mut setter_name = if matches!(plain_field_name, "new" | "build") {
2743 format!("with_{plain_field_name}")
2744 } else {
2745 field_name.clone()
2746 };
2747 let setter_base = setter_name.clone();
2748 let mut suffix = 2;
2749 while !used_builder_methods.insert(setter_name.clone()) {
2750 setter_name = format!("{setter_base}_{suffix}");
2751 suffix += 1;
2752 }
2753 let setter_ident = Self::to_field_ident(&setter_name);
2754 let wire_name = property.wire_name;
2755 if self.property_is_tri_state(
2756 &schema.name,
2757 property.wire_name,
2758 property.property,
2759 property.is_required,
2760 ) {
2761 let mut null_name = format!("{plain_field_name}_null");
2762 let null_base = null_name.clone();
2763 let mut suffix = 2;
2764 while !used_builder_methods.insert(null_name.clone()) {
2765 null_name = format!("{null_base}_{suffix}");
2766 suffix += 1;
2767 }
2768 let null_ident = Self::to_field_ident(&null_name);
2769
2770 let mut absent_name = format!("{plain_field_name}_absent");
2771 let absent_base = absent_name.clone();
2772 let mut suffix = 2;
2773 while !used_builder_methods.insert(absent_name.clone()) {
2774 absent_name = format!("{absent_base}_{suffix}");
2775 suffix += 1;
2776 }
2777 let absent_ident = Self::to_field_ident(&absent_name);
2778
2779 quote! {
2780 #[doc = concat!("Set the optional nullable `", #wire_name, "` request field to a value.")]
2781 #[must_use]
2782 pub fn #setter_ident(mut self, #field_ident: #field_type) -> Self {
2783 self.value.#field_ident = Some(Some(#field_ident));
2784 self
2785 }
2786
2787 #[doc = concat!("Set the optional nullable `", #wire_name, "` request field to JSON null.")]
2788 #[must_use]
2789 pub fn #null_ident(mut self) -> Self {
2790 self.value.#field_ident = Some(None);
2791 self
2792 }
2793
2794 #[doc = concat!("Omit the optional nullable `", #wire_name, "` request field.")]
2795 #[must_use]
2796 pub fn #absent_ident(mut self) -> Self {
2797 self.value.#field_ident = None;
2798 self
2799 }
2800 }
2801 } else {
2802 quote! {
2803 #[doc = concat!("Set the optional `", #wire_name, "` request field.")]
2804 #[must_use]
2805 pub fn #setter_ident(mut self, #field_ident: #field_type) -> Self {
2806 self.value.#field_ident = Some(#field_ident);
2807 self
2808 }
2809 }
2810 }
2811 })
2812 .collect();
2813
2814 let additional_setter = match additional_properties {
2815 crate::analysis::ObjectAdditionalProperties::Forbidden => TokenStream::new(),
2816 crate::analysis::ObjectAdditionalProperties::Untyped => quote! {
2817 #[must_use]
2819 pub fn additional_properties(
2820 mut self,
2821 additional_properties: ::std::collections::BTreeMap<
2822 String,
2823 serde_json::Value,
2824 >,
2825 ) -> Self {
2826 self.value.additional_properties = additional_properties;
2827 self
2828 }
2829 },
2830 crate::analysis::ObjectAdditionalProperties::Typed { value_type } => {
2831 let value_type = self.generate_array_item_type(value_type, analysis);
2832 quote! {
2833 #[must_use]
2835 pub fn additional_properties(
2836 mut self,
2837 additional_properties: ::std::collections::BTreeMap<
2838 String,
2839 #value_type,
2840 >,
2841 ) -> Self {
2842 self.value.additional_properties = additional_properties;
2843 self
2844 }
2845 }
2846 }
2847 };
2848
2849 quote! {
2850 impl #struct_name {
2851 pub fn new(#(#required_parameters),*) -> Self {
2853 Self {
2854 #(#required_idents,)*
2855 #(#optional_initializers,)*
2856 #additional_initializer
2857 }
2858 }
2859
2860 pub fn builder(#(#required_parameters),*) -> #builder_name {
2862 #builder_name::new(#(#required_idents),*)
2863 }
2864 }
2865
2866 #[derive(Debug, Clone)]
2868 #[must_use]
2869 pub struct #builder_name {
2870 value: #struct_name,
2871 }
2872
2873 impl #builder_name {
2874 pub fn new(#(#required_parameters),*) -> Self {
2876 Self {
2877 value: #struct_name::new(#(#required_idents),*),
2878 }
2879 }
2880
2881 #(#optional_setters)*
2882 #additional_setter
2883
2884 pub fn build(self) -> #struct_name {
2886 self.value
2887 }
2888 }
2889 }
2890 }
2891
2892 fn generate_discriminated_enum(
2893 &self,
2894 schema: &crate::analysis::AnalyzedSchema,
2895 discriminator_field: &str,
2896 variants: &[crate::analysis::UnionVariant],
2897 exclusive: bool,
2898 analysis: &crate::analysis::SchemaAnalysis,
2899 ) -> Result<TokenStream> {
2900 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2901
2902 let has_nested_discriminated_union = variants.iter().any(|variant| {
2904 if let Some(variant_schema) = analysis.schemas.get(&variant.type_name) {
2905 matches!(
2906 variant_schema.schema_type,
2907 crate::analysis::SchemaType::DiscriminatedUnion { .. }
2908 )
2909 } else {
2910 false
2911 }
2912 });
2913
2914 if has_nested_discriminated_union {
2916 let schema_refs: Vec<crate::analysis::SchemaRef> = variants
2918 .iter()
2919 .map(|v| crate::analysis::SchemaRef {
2920 target: v.type_name.clone(),
2921 nullable: false,
2922 })
2923 .collect();
2924 return self.generate_union_enum(schema, &schema_refs, exclusive, analysis);
2925 }
2926
2927 let enclosing = self.to_rust_type_name(&schema.name);
2928 let variant_shapes: Vec<_> = variants
2929 .iter()
2930 .map(|variant| {
2931 let variant_name = format_ident!("{}", variant.rust_name);
2932 let variant_type = format_ident!("{}", self.to_rust_type_name(&variant.type_name));
2933 let payload = if self.to_rust_type_name(&variant.type_name) == enclosing
2937 || analysis
2938 .dependencies
2939 .recursive_schemas
2940 .contains(&variant.type_name)
2941 {
2942 quote! { Box<#variant_type> }
2943 } else {
2944 quote! { #variant_type }
2945 };
2946 (variant, variant_name, payload)
2947 })
2948 .collect();
2949 let enum_variants = variant_shapes.iter().map(|(_, variant_name, payload)| {
2950 quote! { #variant_name(#payload), }
2951 });
2952 let serialize_arms = variant_shapes.iter().map(|(variant, variant_name, _)| {
2953 let canonical_value = &variant.discriminator_value;
2954 let variant_values = &variant.discriminator_values;
2955 let serialize_discriminator = if variant.discriminator_field_declared {
2956 quote! {
2957 match object.get(#discriminator_field) {
2958 Some(serde_json::Value::String(tag))
2959 if matches!(tag.as_str(), #(#variant_values)|*) => {}
2960 Some(serde_json::Value::String(tag)) => {
2961 return Err(serde::ser::Error::custom(format!(
2962 "discriminator `{}` value `{tag}` is not valid for variant `{}`",
2963 #discriminator_field,
2964 stringify!(#variant_name),
2965 )));
2966 }
2967 Some(_) => {
2968 return Err(serde::ser::Error::custom(concat!(
2969 "discriminator `",
2970 #discriminator_field,
2971 "` did not serialize as a string",
2972 )));
2973 }
2974 None => {
2975 object.insert(
2976 #discriminator_field.to_string(),
2977 serde_json::Value::String(#canonical_value.to_string()),
2978 );
2979 }
2980 }
2981 }
2982 } else {
2983 TokenStream::new()
2984 };
2985 quote! {
2986 Self::#variant_name(payload) => {
2987 let mut value = serde_json::to_value(payload)
2988 .map_err(serde::ser::Error::custom)?;
2989 let object = value.as_object_mut().ok_or_else(|| {
2990 serde::ser::Error::custom(concat!(
2991 "discriminated union variant `",
2992 stringify!(#variant_name),
2993 "` did not serialize as an object",
2994 ))
2995 })?;
2996 #serialize_discriminator
2997 value.serialize(serializer)
2998 }
2999 }
3000 });
3001 let mut deserialize_arms = Vec::new();
3002 for (primary_index, (variant, variant_name, payload)) in variant_shapes.iter().enumerate() {
3003 for tag in &variant.preferred_discriminator_values {
3004 let fallback_attempts = variant_shapes.iter().enumerate().filter_map(
3005 |(fallback_index, (_, fallback_name, fallback_payload))| {
3006 if fallback_index == primary_index {
3007 return None;
3008 }
3009 if exclusive {
3010 Some(quote! {
3011 if let Ok(payload) =
3012 serde_json::from_value::<#fallback_payload>(value.clone())
3013 {
3014 if let Some((_, first_name)) = &structural_match {
3015 return Err(serde::de::Error::custom(format!(
3016 "discriminator `{}` value `{}` did not fit its mapped branch and structurally matched both `{}` and `{}`",
3017 #discriminator_field,
3018 #tag,
3019 first_name,
3020 stringify!(#fallback_name),
3021 )));
3022 }
3023 structural_match = Some((
3024 Self::#fallback_name(payload),
3025 stringify!(#fallback_name),
3026 ));
3027 }
3028 })
3029 } else {
3030 Some(quote! {
3031 if let Ok(payload) =
3032 serde_json::from_value::<#fallback_payload>(value.clone())
3033 {
3034 return Ok(Self::#fallback_name(payload));
3035 }
3036 })
3037 }
3038 },
3039 );
3040 let structural_fallback = if exclusive {
3041 quote! {
3042 let mut structural_match: Option<(Self, &'static str)> = None;
3043 #(#fallback_attempts)*
3044 match structural_match {
3045 Some((payload, _)) => Ok(payload),
3046 None => Err(serde::de::Error::custom(primary_error)),
3047 }
3048 }
3049 } else {
3050 quote! {
3051 #(#fallback_attempts)*
3052 Err(serde::de::Error::custom(primary_error))
3053 }
3054 };
3055 deserialize_arms.push(quote! {
3056 #tag => {
3057 let primary_error = match serde_json::from_value::<#payload>(value.clone()) {
3058 Ok(payload) => return Ok(Self::#variant_name(payload)),
3059 Err(error) => error,
3060 };
3061 #structural_fallback
3062 }
3063 });
3064 }
3065 }
3066 let missing_discriminator_attempts = variant_shapes.iter().filter_map(
3067 |(variant, variant_name, payload)| {
3068 if variant.discriminator_field_required {
3069 return None;
3070 }
3071 if exclusive {
3072 Some(quote! {
3073 if let Ok(payload) = serde_json::from_value::<#payload>(value.clone()) {
3074 if let Some((_, first_name)) = &structural_match {
3075 return Err(serde::de::Error::custom(format!(
3076 "missing discriminator `{}` structurally matched both `{}` and `{}`",
3077 #discriminator_field,
3078 first_name,
3079 stringify!(#variant_name),
3080 )));
3081 }
3082 structural_match = Some((
3083 Self::#variant_name(payload),
3084 stringify!(#variant_name),
3085 ));
3086 }
3087 })
3088 } else {
3089 Some(quote! {
3090 if let Ok(payload) = serde_json::from_value::<#payload>(value.clone()) {
3091 return Ok(Self::#variant_name(payload));
3092 }
3093 })
3094 }
3095 },
3096 );
3097 let has_missing_discriminator_candidates = variants
3098 .iter()
3099 .any(|variant| !variant.discriminator_field_required);
3100 let missing_discriminator_fallback = if has_missing_discriminator_candidates && exclusive {
3101 quote! {
3102 let mut structural_match: Option<(Self, &'static str)> = None;
3103 #(#missing_discriminator_attempts)*
3104 structural_match
3105 .map(|(payload, _)| payload)
3106 .ok_or_else(|| serde::de::Error::custom(concat!(
3107 "missing string discriminator `",
3108 #discriminator_field,
3109 "` and no tagless branch matched",
3110 )))
3111 }
3112 } else if has_missing_discriminator_candidates {
3113 quote! {
3114 #(#missing_discriminator_attempts)*
3115 Err(serde::de::Error::custom(concat!(
3116 "missing string discriminator `",
3117 #discriminator_field,
3118 "` and no tagless branch matched",
3119 )))
3120 }
3121 } else {
3122 quote! {
3123 Err(serde::de::Error::custom(concat!(
3124 "missing string discriminator `",
3125 #discriminator_field,
3126 "`",
3127 )))
3128 }
3129 };
3130
3131 let doc_comment = if let Some(desc) = &schema.description {
3132 quote! { #[doc = #desc] }
3133 } else {
3134 TokenStream::new()
3135 };
3136
3137 let derives = if self.config.enable_specta {
3144 quote! {
3145 #[derive(Debug, Clone)]
3146 #[cfg_attr(feature = "specta", derive(specta::Type))]
3147 }
3148 } else {
3149 quote! {
3150 #[derive(Debug, Clone)]
3151 }
3152 };
3153
3154 Ok(quote! {
3155 #doc_comment
3156 #derives
3157 pub enum #enum_name {
3158 #(#enum_variants)*
3159 }
3160
3161 impl serde::Serialize for #enum_name {
3162 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
3163 where
3164 S: serde::Serializer,
3165 {
3166 match self {
3167 #(#serialize_arms)*
3168 }
3169 }
3170 }
3171
3172 impl<'de> serde::Deserialize<'de> for #enum_name {
3173 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3174 where
3175 D: serde::Deserializer<'de>,
3176 {
3177 let value = serde_json::Value::deserialize(deserializer)?;
3178 let discriminator = match value.get(#discriminator_field) {
3179 Some(serde_json::Value::String(discriminator)) => {
3180 Some(discriminator.as_str())
3181 }
3182 Some(_) => {
3183 return Err(serde::de::Error::custom(concat!(
3184 "non-string discriminator `",
3185 #discriminator_field,
3186 "`",
3187 )));
3188 }
3189 None => None,
3190 };
3191 match discriminator {
3192 Some(discriminator) => match discriminator {
3193 #(#deserialize_arms)*
3194 other => Err(serde::de::Error::custom(format!(
3195 "unknown discriminator value `{other}` for `{}`",
3196 #discriminator_field,
3197 ))),
3198 },
3199 None => { #missing_discriminator_fallback }
3200 }
3201 }
3202 }
3203 })
3204 }
3205
3206 fn should_use_untagged_discriminated_union(
3208 &self,
3209 schema: &crate::analysis::AnalyzedSchema,
3210 analysis: &crate::analysis::SchemaAnalysis,
3211 ) -> bool {
3212 for other_schema in analysis.schemas.values() {
3217 if let crate::analysis::SchemaType::DiscriminatedUnion { variants, .. } =
3218 &other_schema.schema_type
3219 {
3220 for variant in variants {
3221 if variant.type_name == schema.name {
3222 if let crate::analysis::SchemaType::DiscriminatedUnion {
3227 discriminator_field: current_discriminator,
3228 variants: current_variants,
3229 ..
3230 } = &schema.schema_type
3231 {
3232 for current_variant in current_variants {
3234 if let Some(variant_schema) =
3235 analysis.schemas.get(¤t_variant.type_name)
3236 {
3237 if let crate::analysis::SchemaType::Object {
3238 properties, ..
3239 } = &variant_schema.schema_type
3240 {
3241 if properties.contains_key(current_discriminator) {
3242 return false;
3245 }
3246 }
3247 }
3248 }
3249 }
3250
3251 return true;
3253 }
3254 }
3255 }
3256 }
3257 false
3258 }
3259
3260 fn generate_union_enum(
3261 &self,
3262 schema: &crate::analysis::AnalyzedSchema,
3263 variants: &[crate::analysis::SchemaRef],
3264 exclusive: bool,
3265 analysis: &crate::analysis::SchemaAnalysis,
3266 ) -> Result<TokenStream> {
3267 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
3268
3269 let mut used_variant_names = std::collections::HashSet::new();
3271 let enum_variants = variants
3272 .iter()
3273 .enumerate()
3274 .map(|(i, variant)| {
3275 let base_variant_name = self.type_name_to_variant_name(&variant.target);
3277 let variant_name = self.ensure_unique_variant_name_generator(
3278 base_variant_name,
3279 &mut used_variant_names,
3280 i,
3281 );
3282 let variant_name_ident = format_ident!("{}", variant_name);
3283
3284 let variant_type_tokens = if matches!(
3286 variant.target.as_str(),
3287 "bool"
3288 | "i8"
3289 | "i16"
3290 | "i32"
3291 | "i64"
3292 | "i128"
3293 | "u8"
3294 | "u16"
3295 | "u32"
3296 | "u64"
3297 | "u128"
3298 | "f32"
3299 | "f64"
3300 | "String"
3301 ) {
3302 let type_ident = format_ident!("{}", variant.target);
3303 quote! { #type_ident }
3304 } else if variant.target == "serde_json::Value" {
3305 quote! { serde_json::Value }
3308 } else if variant.target.starts_with("Vec<") && variant.target.ends_with(">") {
3309 let inner = &variant.target[4..variant.target.len() - 1];
3311
3312 if inner.starts_with("Vec<") && inner.ends_with(">") {
3314 let inner_inner = &inner[4..inner.len() - 1];
3315 if inner_inner == "serde_json::Value" {
3316 quote! { Vec<Vec<serde_json::Value>> }
3317 } else {
3318 let inner_inner_type = if matches!(
3319 inner_inner,
3320 "bool"
3321 | "i8"
3322 | "i16"
3323 | "i32"
3324 | "i64"
3325 | "i128"
3326 | "u8"
3327 | "u16"
3328 | "u32"
3329 | "u64"
3330 | "u128"
3331 | "f32"
3332 | "f64"
3333 | "String"
3334 ) {
3335 format_ident!("{}", inner_inner)
3336 } else {
3337 format_ident!("{}", self.to_rust_type_name(inner_inner))
3338 };
3339 quote! { Vec<Vec<#inner_inner_type>> }
3340 }
3341 } else if inner == "serde_json::Value" {
3342 quote! { Vec<serde_json::Value> }
3343 } else {
3344 let inner_type = if matches!(
3345 inner,
3346 "bool"
3347 | "i8"
3348 | "i16"
3349 | "i32"
3350 | "i64"
3351 | "i128"
3352 | "u8"
3353 | "u16"
3354 | "u32"
3355 | "u64"
3356 | "u128"
3357 | "f32"
3358 | "f64"
3359 | "String"
3360 ) {
3361 format_ident!("{}", inner)
3362 } else {
3363 format_ident!("{}", self.to_rust_type_name(inner))
3364 };
3365 quote! { Vec<#inner_type> }
3366 }
3367 } else if variant.target.contains("::") || variant.target.contains('<') {
3368 parse_rust_type(&variant.target).unwrap_or_else(|_| {
3373 let fallback = format_ident!("{}", self.to_rust_type_name(&variant.target));
3374 quote! { #fallback }
3375 })
3376 } else {
3377 let type_ident = format_ident!("{}", self.to_rust_type_name(&variant.target));
3378 quote! { #type_ident }
3379 };
3380
3381 let target_rust_name = self.to_rust_type_name(&variant.target);
3385 let enclosing_name = self.to_rust_type_name(&schema.name);
3386 let is_self_ref = target_rust_name == enclosing_name;
3387 let is_recursive_target = analysis
3391 .dependencies
3392 .recursive_schemas
3393 .contains(&variant.target);
3394 let variant_type_tokens = if is_self_ref || is_recursive_target {
3395 quote! { Box<#variant_type_tokens> }
3396 } else {
3397 variant_type_tokens
3398 };
3399 let variant_type_tokens = if variant.nullable {
3400 quote! { Option<#variant_type_tokens> }
3401 } else {
3402 variant_type_tokens
3403 };
3404
3405 (variant_name_ident, variant_type_tokens)
3406 })
3407 .collect::<Vec<_>>();
3408 let variant_declarations = enum_variants
3409 .iter()
3410 .map(|(variant_name, variant_type)| quote! { #variant_name(#variant_type), })
3411 .collect::<Vec<_>>();
3412
3413 let doc_comment = if let Some(desc) = &schema.description {
3414 quote! { #[doc = #desc] }
3415 } else {
3416 TokenStream::new()
3417 };
3418
3419 let object_only = variants.iter().all(|variant| {
3420 self.union_target_serializes_as_object(
3421 &variant.target,
3422 analysis,
3423 &mut std::collections::HashSet::new(),
3424 )
3425 });
3426
3427 if exclusive || object_only {
3428 let derives = if self.config.enable_specta {
3429 quote! {
3430 #[derive(Debug, Clone)]
3431 #[cfg_attr(feature = "specta", derive(specta::Type))]
3432 }
3433 } else {
3434 quote! { #[derive(Debug, Clone)] }
3435 };
3436 let serialize_arms = enum_variants
3437 .iter()
3438 .map(|(variant_name, _)| {
3439 quote! {
3440 Self::#variant_name(value) => serde::Serialize::serialize(value, serializer),
3441 }
3442 })
3443 .collect::<Vec<_>>();
3444 let deserialize_attempts = enum_variants
3445 .iter()
3446 .zip(variants)
3447 .map(|((variant_name, variant_type), variant)| {
3448 if exclusive {
3449 let constraints = self.union_branch_literal_constraints(
3450 &variant.target,
3451 analysis,
3452 &mut std::collections::HashSet::new(),
3453 );
3454 let constraint_checks =
3455 constraints.iter().map(|(field, (required, allowed))| {
3456 let allowed = allowed
3457 .iter()
3458 .map(serde_json::Value::to_string)
3459 .collect::<Vec<_>>();
3460 if allowed.is_empty() && *required {
3461 quote! { false }
3462 } else if allowed.is_empty() {
3463 quote! { object.get(#field).is_none() }
3464 } else if *required {
3465 quote! {
3466 object.get(#field).is_some_and(|value| {
3467 value.is_null()
3468 || matches!(value.to_string().as_str(), #(#allowed)|*)
3469 })
3470 }
3471 } else {
3472 quote! {
3473 object.get(#field).is_none_or(|value| {
3474 value.is_null()
3475 || matches!(value.to_string().as_str(), #(#allowed)|*)
3476 })
3477 }
3478 }
3479 });
3480 let constraints_match = if constraints.is_empty() {
3481 quote! { true }
3482 } else {
3483 quote! {
3484 input.as_object().is_some_and(|object| {
3485 true #(&& #constraint_checks)*
3486 })
3487 }
3488 };
3489 quote! {
3490 if #constraints_match {
3491 if let Ok(candidate) =
3492 serde_json::from_value::<#variant_type>(input.clone())
3493 {
3494 let preserves_complete_input = serde_json::to_value(&candidate)
3495 .map(|encoded| encoded == input)
3496 .unwrap_or(false);
3497 if preserves_complete_input {
3498 if matched.is_some() {
3499 return Err(serde::de::Error::custom(concat!(
3500 "ambiguous oneOf value for ",
3501 stringify!(#enum_name),
3502 ": more than one branch preserved the complete input",
3503 )));
3504 }
3505 matched = Some(Self::#variant_name(candidate));
3506 }
3507 }
3508 }
3509 }
3510 } else {
3511 quote! {
3512 if let Ok(candidate) =
3513 serde_json::from_value::<#variant_type>(input.clone())
3514 {
3515 let preserves_complete_input = serde_json::to_value(&candidate)
3516 .map(|encoded| {
3517 preserves_complete_json_input(&encoded, &input)
3518 })
3519 .unwrap_or(false);
3520 if preserves_complete_input {
3521 return Ok(Self::#variant_name(candidate));
3522 }
3523 }
3524 }
3525 }
3526 })
3527 .collect::<Vec<_>>();
3528 let no_match = if exclusive {
3529 quote! {
3530 matched.ok_or_else(|| serde::de::Error::custom(concat!(
3531 "no oneOf branch for ",
3532 stringify!(#enum_name),
3533 " preserved the complete input",
3534 )))
3535 }
3536 } else {
3537 quote! {
3538 Err(serde::de::Error::custom(concat!(
3539 "no anyOf branch for ",
3540 stringify!(#enum_name),
3541 " preserved the complete input",
3542 )))
3543 }
3544 };
3545 let matched_declaration = exclusive.then(|| quote! { let mut matched = None; });
3546 let preservation_helper = (!exclusive).then(|| {
3547 quote! {
3548 fn exact_json_integer(number: &serde_json::Number) -> Option<i128> {
3549 number
3550 .as_i64()
3551 .map(i128::from)
3552 .or_else(|| number.as_u64().map(i128::from))
3553 }
3554
3555 fn json_numbers_have_same_value(
3556 encoded: &serde_json::Number,
3557 input: &serde_json::Number,
3558 ) -> bool {
3559 match (exact_json_integer(encoded), exact_json_integer(input)) {
3560 (Some(encoded), Some(input)) => encoded == input,
3561 (Some(encoded), None) => input.as_f64().is_some_and(|input| {
3562 input.is_finite()
3563 && input.fract() == 0.0
3564 && input as i128 == encoded
3565 }),
3566 (None, Some(input)) => encoded.as_f64().is_some_and(|encoded| {
3567 encoded.is_finite()
3568 && encoded.fract() == 0.0
3569 && encoded as i128 == input
3570 }),
3571 (None, None) => encoded.as_f64() == input.as_f64(),
3572 }
3573 }
3574
3575 fn preserves_complete_json_input(
3576 encoded: &serde_json::Value,
3577 input: &serde_json::Value,
3578 ) -> bool {
3579 match (encoded, input) {
3580 (
3581 serde_json::Value::Object(encoded),
3582 serde_json::Value::Object(input),
3583 ) => input.iter().all(|(key, value)| {
3584 encoded.get(key).is_some_and(|encoded_value| {
3585 preserves_complete_json_input(encoded_value, value)
3586 })
3587 }),
3588 (
3589 serde_json::Value::Array(encoded),
3590 serde_json::Value::Array(input),
3591 ) => {
3592 encoded.len() == input.len()
3593 && encoded.iter().zip(input).all(|(encoded, input)| {
3594 preserves_complete_json_input(encoded, input)
3595 })
3596 }
3597 (
3598 serde_json::Value::Number(encoded),
3599 serde_json::Value::Number(input),
3600 ) => json_numbers_have_same_value(encoded, input),
3601 _ => encoded == input,
3602 }
3603 }
3604 }
3605 });
3606
3607 return Ok(quote! {
3608 #doc_comment
3609 #derives
3610 pub enum #enum_name {
3611 #(#variant_declarations)*
3612 }
3613
3614 impl Serialize for #enum_name {
3615 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
3616 where
3617 S: serde::Serializer,
3618 {
3619 match self {
3620 #(#serialize_arms)*
3621 }
3622 }
3623 }
3624
3625 impl<'de> Deserialize<'de> for #enum_name {
3626 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3627 where
3628 D: serde::Deserializer<'de>,
3629 {
3630 #preservation_helper
3631 let input = <serde_json::Value as Deserialize>::deserialize(deserializer)?;
3632 #matched_declaration
3633 #(#deserialize_attempts)*
3634 #no_match
3635 }
3636 }
3637 });
3638 }
3639
3640 let derives = if self.config.enable_specta {
3643 quote! {
3644 #[derive(Debug, Clone, Deserialize, Serialize)]
3645 #[cfg_attr(feature = "specta", derive(specta::Type))]
3646 #[serde(untagged)]
3647 }
3648 } else {
3649 quote! {
3650 #[derive(Debug, Clone, Deserialize, Serialize)]
3651 #[serde(untagged)]
3652 }
3653 };
3654
3655 Ok(quote! {
3656 #doc_comment
3657 #derives
3658 pub enum #enum_name {
3659 #(#variant_declarations)*
3660 }
3661 })
3662 }
3663
3664 fn union_branch_literal_constraints(
3665 &self,
3666 target: &str,
3667 analysis: &crate::analysis::SchemaAnalysis,
3668 visited: &mut std::collections::HashSet<String>,
3669 ) -> BTreeMap<String, (bool, Vec<serde_json::Value>)> {
3670 if !visited.insert(target.to_string()) {
3671 return BTreeMap::new();
3672 }
3673 let constraints = analysis
3674 .schemas
3675 .get(target)
3676 .map(|schema| {
3677 self.schema_literal_property_constraints(&schema.original, analysis, visited)
3678 })
3679 .unwrap_or_default();
3680 visited.remove(target);
3681 constraints
3682 }
3683
3684 fn schema_literal_property_constraints(
3685 &self,
3686 schema: &serde_json::Value,
3687 analysis: &crate::analysis::SchemaAnalysis,
3688 visited: &mut std::collections::HashSet<String>,
3689 ) -> BTreeMap<String, (bool, Vec<serde_json::Value>)> {
3690 let Some(object) = schema.as_object() else {
3691 return BTreeMap::new();
3692 };
3693 if let Some(reference) = object.get("$ref").and_then(serde_json::Value::as_str)
3694 && let Some(target) = reference.rsplit('/').next()
3695 {
3696 return self.union_branch_literal_constraints(target, analysis, visited);
3697 }
3698
3699 let required = object
3700 .get("required")
3701 .and_then(serde_json::Value::as_array)
3702 .into_iter()
3703 .flatten()
3704 .filter_map(serde_json::Value::as_str)
3705 .collect::<std::collections::HashSet<_>>();
3706 let mut constraints = BTreeMap::new();
3707 if let Some(properties) = object
3708 .get("properties")
3709 .and_then(serde_json::Value::as_object)
3710 {
3711 for (field, property) in properties {
3712 if let Some(values) = self.schema_literal_domain(
3713 property,
3714 analysis,
3715 &mut std::collections::HashSet::new(),
3716 ) && !values.is_empty()
3717 {
3718 constraints.insert(field.clone(), (required.contains(field.as_str()), values));
3719 }
3720 }
3721 }
3722 if let Some(branches) = object.get("allOf").and_then(serde_json::Value::as_array) {
3723 for branch in branches {
3724 for (field, (branch_required, values)) in
3725 self.schema_literal_property_constraints(branch, analysis, visited)
3726 {
3727 constraints
3728 .entry(field)
3729 .and_modify(|(is_required, existing)| {
3730 *is_required |= branch_required;
3731 existing.retain(|value| values.contains(value));
3732 })
3733 .or_insert((branch_required, values));
3734 }
3735 }
3736 }
3737 constraints
3738 }
3739
3740 fn schema_literal_domain(
3741 &self,
3742 schema: &serde_json::Value,
3743 analysis: &crate::analysis::SchemaAnalysis,
3744 visited: &mut std::collections::HashSet<String>,
3745 ) -> Option<Vec<serde_json::Value>> {
3746 let object = schema.as_object()?;
3747 if let Some(reference) = object.get("$ref").and_then(serde_json::Value::as_str)
3748 && let Some(target) = reference.rsplit('/').next()
3749 {
3750 if !visited.insert(target.to_string()) {
3751 return None;
3752 }
3753 let result = analysis
3754 .schemas
3755 .get(target)
3756 .and_then(|schema| self.schema_literal_domain(&schema.original, analysis, visited));
3757 visited.remove(target);
3758 return result;
3759 }
3760 let own = object
3761 .get("const")
3762 .map(|value| vec![value.clone()])
3763 .or_else(|| {
3764 object
3765 .get("enum")
3766 .and_then(serde_json::Value::as_array)
3767 .cloned()
3768 });
3769 let composed = object
3770 .get("allOf")
3771 .and_then(serde_json::Value::as_array)
3772 .and_then(|branches| {
3773 branches.iter().fold(None, |domain, branch| {
3774 let branch_domain = self.schema_literal_domain(branch, analysis, visited);
3775 match (domain, branch_domain) {
3776 (None, other) | (other, None) => other,
3777 (Some(mut left), Some(right)) => {
3778 left.retain(|value| right.contains(value));
3779 Some(left)
3780 }
3781 }
3782 })
3783 });
3784 match (own, composed) {
3785 (None, other) | (other, None) => other,
3786 (Some(mut left), Some(right)) => {
3787 left.retain(|value| right.contains(value));
3788 Some(left)
3789 }
3790 }
3791 }
3792
3793 fn union_target_serializes_as_object(
3794 &self,
3795 target: &str,
3796 analysis: &crate::analysis::SchemaAnalysis,
3797 visited: &mut std::collections::HashSet<String>,
3798 ) -> bool {
3799 if !visited.insert(target.to_string()) {
3800 return false;
3801 }
3802 let result = analysis
3803 .schemas
3804 .get(target)
3805 .is_some_and(|schema| match &schema.schema_type {
3806 crate::analysis::SchemaType::Object { .. }
3807 | crate::analysis::SchemaType::Composition { .. }
3808 | crate::analysis::SchemaType::DiscriminatedUnion { .. } => true,
3809 crate::analysis::SchemaType::Reference { target } => {
3810 self.union_target_serializes_as_object(target, analysis, visited)
3811 }
3812 crate::analysis::SchemaType::Union { variants, .. } => {
3813 variants.iter().all(|variant| {
3814 self.union_target_serializes_as_object(&variant.target, analysis, visited)
3815 })
3816 }
3817 _ => false,
3818 });
3819 visited.remove(target);
3820 result
3821 }
3822
3823 fn target_aliases_back_to(
3828 &self,
3829 target: &str,
3830 enclosing_rust_name: &str,
3831 analysis: &crate::analysis::SchemaAnalysis,
3832 ) -> bool {
3833 let mut current = target.to_string();
3834 let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
3835 for _ in 0..16 {
3836 if !visited.insert(current.clone()) {
3837 return true;
3838 }
3839 let Some(schema) = analysis.schemas.get(¤t) else {
3840 return false;
3841 };
3842 if let crate::analysis::SchemaType::Reference { target: next } = &schema.schema_type {
3843 if self.to_rust_type_name(next) == enclosing_rust_name {
3844 return true;
3845 }
3846 current = next.clone();
3847 continue;
3848 }
3849 return false;
3850 }
3851 false
3852 }
3853
3854 fn generate_field_type(
3855 &self,
3856 schema_name: &str,
3857 field_name: &str,
3858 prop: &crate::analysis::PropertyInfo,
3859 is_required: bool,
3860 analysis: &crate::analysis::SchemaAnalysis,
3861 ) -> TokenStream {
3862 let base_type = self.generate_property_base_type(schema_name, field_name, prop, analysis);
3863
3864 if !is_required && self.property_is_nullable(schema_name, field_name, prop) {
3865 quote! { Option<Option<#base_type>> }
3866 } else if self.property_is_option_wrapped(
3867 schema_name,
3868 field_name,
3869 prop,
3870 is_required,
3871 analysis,
3872 ) {
3873 quote! { Option<#base_type> }
3874 } else {
3875 base_type
3876 }
3877 }
3878
3879 pub(crate) fn property_is_nullable(
3880 &self,
3881 schema_name: &str,
3882 field_name: &str,
3883 prop: &crate::analysis::PropertyInfo,
3884 ) -> bool {
3885 let override_key = format!("{schema_name}.{field_name}");
3886 prop.nullable
3887 || self
3888 .config
3889 .nullable_field_overrides
3890 .get(&override_key)
3891 .copied()
3892 .unwrap_or(false)
3893 }
3894
3895 pub(crate) fn property_is_tri_state(
3896 &self,
3897 schema_name: &str,
3898 field_name: &str,
3899 prop: &crate::analysis::PropertyInfo,
3900 is_required: bool,
3901 ) -> bool {
3902 !is_required && self.property_is_nullable(schema_name, field_name, prop)
3903 }
3904
3905 fn property_is_option_wrapped(
3906 &self,
3907 schema_name: &str,
3908 field_name: &str,
3909 prop: &crate::analysis::PropertyInfo,
3910 is_required: bool,
3911 analysis: &crate::analysis::SchemaAnalysis,
3912 ) -> bool {
3913 !is_required
3914 || self.property_is_nullable(schema_name, field_name, prop)
3915 || (prop.default.is_some() && self.type_lacks_default(&prop.schema_type, analysis))
3916 }
3917
3918 pub(crate) fn generate_property_base_type(
3919 &self,
3920 schema_name: &str,
3921 _field_name: &str,
3922 prop: &crate::analysis::PropertyInfo,
3923 analysis: &crate::analysis::SchemaAnalysis,
3924 ) -> TokenStream {
3925 use crate::analysis::SchemaType;
3926
3927 match &prop.schema_type {
3928 SchemaType::Primitive { rust_type, .. } => {
3929 parse_rust_type(rust_type).unwrap_or_else(|_| {
3932 eprintln!(
3937 "⚠️ TypeMapper produced un-parseable type `{rust_type}`; \
3938 falling back to String"
3939 );
3940 quote! { String }
3941 })
3942 }
3943 SchemaType::Reference { target } => {
3944 let target_rust_name = self.to_rust_type_name(target);
3945 let target_type = format_ident!("{}", target_rust_name);
3946 let enclosing_rust_name = self.to_rust_type_name(schema_name);
3959 let is_self_via_rust_name = target_rust_name == enclosing_rust_name;
3960 let is_alias_chain_self =
3961 self.target_aliases_back_to(target, &enclosing_rust_name, analysis);
3962 if analysis.dependencies.recursive_schemas.contains(target)
3963 || is_self_via_rust_name
3964 || is_alias_chain_self
3965 {
3966 quote! { Box<#target_type> }
3967 } else {
3968 quote! { #target_type }
3969 }
3970 }
3971 SchemaType::Array { item_type } => {
3972 let inner_type = self.generate_array_item_type(item_type, analysis);
3973 quote! { Vec<#inner_type> }
3974 }
3975 SchemaType::Nullable { inner_type } => {
3976 let inner_type = self.generate_array_item_type(inner_type, analysis);
3977 quote! { Option<#inner_type> }
3978 }
3979 SchemaType::Tuple { element_types } => {
3980 self.generate_tuple_type(element_types, analysis)
3981 }
3982 SchemaType::Untyped { shape, .. } => untyped_tokens(*shape),
3983 _ => {
3984 quote! { serde_json::Value }
3986 }
3987 }
3988 }
3989
3990 fn generate_tuple_type(
3994 &self,
3995 element_types: &[crate::analysis::SchemaType],
3996 analysis: &crate::analysis::SchemaAnalysis,
3997 ) -> TokenStream {
3998 let elements = element_types
3999 .iter()
4000 .map(|element_type| self.generate_array_item_type(element_type, analysis))
4001 .collect::<Vec<_>>();
4002 if let [only] = elements.as_slice() {
4006 return quote! { (#only,) };
4007 }
4008 quote! { (#(#elements),*) }
4009 }
4010
4011 fn generate_serde_field_attrs(
4012 &self,
4013 schema_name: &str,
4014 field_name: &str,
4015 field_ident: &syn::Ident,
4016 prop: &crate::analysis::PropertyInfo,
4017 is_required: bool,
4018 analysis: &crate::analysis::SchemaAnalysis,
4019 ) -> TokenStream {
4020 let mut attrs = Vec::new();
4021
4022 let rust_field_name = field_ident.to_string();
4025 let comparison_name = rust_field_name
4026 .strip_prefix("r#")
4027 .unwrap_or(&rust_field_name);
4028 if comparison_name != field_name {
4029 attrs.push(quote! { rename = #field_name });
4030 }
4031
4032 let is_tri_state = self.property_is_tri_state(schema_name, field_name, prop, is_required);
4033
4034 if !is_required {
4039 attrs.push(quote! { skip_serializing_if = "Option::is_none" });
4040 }
4041
4042 if prop.default.is_some()
4046 && (is_required && !prop.nullable)
4047 && !self.type_lacks_default(&prop.schema_type, analysis)
4048 {
4049 attrs.push(quote! { default });
4050 }
4051
4052 if let Some(codec) = self.schema_type_serde_codec(&prop.schema_type, analysis) {
4060 let is_option_wrapped = self.property_is_option_wrapped(
4061 schema_name,
4062 field_name,
4063 prop,
4064 is_required,
4065 analysis,
4066 );
4067 let codec_path = if is_tri_state {
4068 Self::double_option_codec_path(&codec)
4069 } else if is_option_wrapped {
4070 format!("{codec}::option")
4071 } else {
4072 codec
4073 };
4074 attrs.push(quote! { with = #codec_path });
4075 if is_option_wrapped {
4080 attrs.push(quote! { default });
4081 }
4082 } else if is_tri_state {
4083 attrs.push(quote! { default });
4084 attrs.push(quote! { deserialize_with = "tri_state_serde::deserialize" });
4085 }
4086
4087 if attrs.is_empty() {
4088 TokenStream::new()
4089 } else {
4090 quote! { #[serde(#(#attrs),*)] }
4091 }
4092 }
4093
4094 fn double_option_codec_path(codec: &str) -> String {
4095 match codec {
4096 "time::serde::rfc3339" => "time_rfc3339_double_option".to_string(),
4097 "time_date_format" => "time_date_double_option".to_string(),
4098 "time_time_format" => "time_time_double_option".to_string(),
4099 _ => format!("{codec}::double_option"),
4100 }
4101 }
4102
4103 fn schema_type_serde_codec(
4108 &self,
4109 schema_type: &crate::analysis::SchemaType,
4110 analysis: &crate::analysis::SchemaAnalysis,
4111 ) -> Option<String> {
4112 let mut current = schema_type;
4113 let mut visited = std::collections::HashSet::new();
4114 loop {
4115 match current {
4116 crate::analysis::SchemaType::Primitive {
4117 serde_with: Some(codec),
4118 ..
4119 } => return Some(codec.clone()),
4120 crate::analysis::SchemaType::Reference { target }
4121 if visited.insert(target.clone()) =>
4122 {
4123 current = &analysis.schemas.get(target)?.schema_type;
4124 }
4125 crate::analysis::SchemaType::Nullable { inner_type } => {
4126 current = inner_type;
4127 }
4128 _ => return None,
4129 }
4130 }
4131 }
4132
4133 fn type_lacks_default(
4137 &self,
4138 schema_type: &crate::analysis::SchemaType,
4139 analysis: &crate::analysis::SchemaAnalysis,
4140 ) -> bool {
4141 use crate::analysis::SchemaType;
4142 match schema_type {
4143 SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => true,
4144 SchemaType::Primitive { rust_type, .. } => matches!(
4148 rust_type.as_str(),
4149 "chrono::DateTime<chrono::Utc>"
4150 | "chrono::NaiveDate"
4151 | "chrono::NaiveTime"
4152 | "chrono::Duration"
4153 | "url::Url"
4154 | "time::OffsetDateTime"
4155 | "time::Date"
4156 | "time::Time"
4157 | "iso8601::Duration"
4158 | "email_address::EmailAddress"
4159 ),
4160 SchemaType::Reference { target } => {
4161 if let Some(schema) = analysis.schemas.get(target) {
4162 self.type_lacks_default(&schema.schema_type, analysis)
4163 } else {
4164 false
4165 }
4166 }
4167 _ => false,
4168 }
4169 }
4170
4171 fn generate_specta_field_attrs(&self, field_name: &str) -> TokenStream {
4172 if !self.config.enable_specta {
4173 return TokenStream::new();
4174 }
4175
4176 let camel_case_name = self.to_camel_case(field_name);
4178
4179 if camel_case_name != field_name {
4181 quote! { #[cfg_attr(feature = "specta", specta(rename = #camel_case_name))] }
4182 } else {
4183 TokenStream::new()
4184 }
4185 }
4186
4187 pub(crate) fn to_rust_enum_variant(&self, s: &str) -> String {
4188 let neg_prefix =
4192 if s.starts_with('-') && s.chars().skip(1).all(|c| c.is_ascii_digit() || c == '.') {
4193 "Neg"
4194 } else {
4195 ""
4196 };
4197
4198 let mut result = String::new();
4200 let mut next_upper = true;
4201 let mut prev_was_upper = false;
4202
4203 for (i, c) in s.chars().enumerate() {
4204 match c {
4205 'a'..='z' => {
4206 if next_upper {
4207 result.push(c.to_ascii_uppercase());
4208 next_upper = false;
4209 } else {
4210 result.push(c);
4211 }
4212 prev_was_upper = false;
4213 }
4214 'A'..='Z' => {
4215 if next_upper || (!prev_was_upper && i > 0) {
4216 result.push(c);
4218 next_upper = false;
4219 } else {
4220 result.push(c.to_ascii_lowercase());
4222 }
4223 prev_was_upper = true;
4224 }
4225 '0'..='9' => {
4226 result.push(c);
4227 next_upper = false;
4228 prev_was_upper = false;
4229 }
4230 '.' | '-' | '_' | ' ' | '@' | '#' | '$' | '/' | '\\' => {
4231 next_upper = true;
4233 prev_was_upper = false;
4234 }
4235 _ => {
4236 next_upper = true;
4238 prev_was_upper = false;
4239 }
4240 }
4241 }
4242
4243 if result.is_empty() {
4245 result = "Value".to_string();
4246 }
4247
4248 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
4250 result = format!("Variant{neg_prefix}{result}");
4251 } else if !neg_prefix.is_empty() {
4252 result = format!("{neg_prefix}{result}");
4255 }
4256
4257 match result.as_str() {
4259 "Null" => "NullValue".to_string(),
4260 "True" => "TrueValue".to_string(),
4261 "False" => "FalseValue".to_string(),
4262 "Type" => "Type_".to_string(),
4263 "Match" => "Match_".to_string(),
4264 "Fn" => "Fn_".to_string(),
4265 "Impl" => "Impl_".to_string(),
4266 "Trait" => "Trait_".to_string(),
4267 "Struct" => "Struct_".to_string(),
4268 "Enum" => "Enum_".to_string(),
4269 "Mod" => "Mod_".to_string(),
4270 "Use" => "Use_".to_string(),
4271 "Pub" => "Pub_".to_string(),
4272 "Const" => "Const_".to_string(),
4273 "Static" => "Static_".to_string(),
4274 "Let" => "Let_".to_string(),
4275 "Mut" => "Mut_".to_string(),
4276 "Ref" => "Ref_".to_string(),
4277 "Move" => "Move_".to_string(),
4278 "Return" => "Return_".to_string(),
4279 "If" => "If_".to_string(),
4280 "Else" => "Else_".to_string(),
4281 "While" => "While_".to_string(),
4282 "For" => "For_".to_string(),
4283 "Loop" => "Loop_".to_string(),
4284 "Break" => "Break_".to_string(),
4285 "Continue" => "Continue_".to_string(),
4286 "Self" => "Self_".to_string(),
4287 "Super" => "Super_".to_string(),
4288 "Crate" => "Crate_".to_string(),
4289 "Async" => "Async_".to_string(),
4290 "Await" => "Await_".to_string(),
4291 _ => result,
4292 }
4293 }
4294
4295 #[allow(dead_code)]
4296 fn to_rust_identifier(&self, s: &str) -> String {
4297 let mut result = s
4299 .chars()
4300 .map(|c| match c {
4301 'a'..='z' | 'A'..='Z' | '0'..='9' => c,
4302 '.' | '-' | '_' | ' ' | '@' | '#' | '$' | '/' | '\\' => '_',
4303 _ => '_',
4304 })
4305 .collect::<String>();
4306
4307 result = result.trim_matches('_').to_string();
4309
4310 if result.is_empty() {
4312 result = "value".to_string();
4313 }
4314
4315 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
4317 result = format!("variant_{result}");
4318 }
4319
4320 match result.as_str() {
4322 "null" => "null_value".to_string(),
4323 "true" => "true_value".to_string(),
4324 "false" => "false_value".to_string(),
4325 "type" => "type_".to_string(),
4326 "match" => "match_".to_string(),
4327 "fn" => "fn_".to_string(),
4328 "impl" => "impl_".to_string(),
4329 "trait" => "trait_".to_string(),
4330 "struct" => "struct_".to_string(),
4331 "enum" => "enum_".to_string(),
4332 "mod" => "mod_".to_string(),
4333 "use" => "use_".to_string(),
4334 "pub" => "pub_".to_string(),
4335 "const" => "const_".to_string(),
4336 "static" => "static_".to_string(),
4337 "let" => "let_".to_string(),
4338 "mut" => "mut_".to_string(),
4339 "ref" => "ref_".to_string(),
4340 "move" => "move_".to_string(),
4341 "return" => "return_".to_string(),
4342 "if" => "if_".to_string(),
4343 "else" => "else_".to_string(),
4344 "while" => "while_".to_string(),
4345 "for" => "for_".to_string(),
4346 "loop" => "loop_".to_string(),
4347 "break" => "break_".to_string(),
4348 "continue" => "continue_".to_string(),
4349 "self" => "self_".to_string(),
4350 "super" => "super_".to_string(),
4351 "crate" => "crate_".to_string(),
4352 "async" => "async_".to_string(),
4353 "await" => "await_".to_string(),
4354 "override" => "override_".to_string(),
4356 "box" => "box_".to_string(),
4357 "dyn" => "dyn_".to_string(),
4358 "where" => "where_".to_string(),
4359 "in" => "in_".to_string(),
4360 "abstract" => "abstract_".to_string(),
4362 "become" => "become_".to_string(),
4363 "do" => "do_".to_string(),
4364 "final" => "final_".to_string(),
4365 "macro" => "macro_".to_string(),
4366 "priv" => "priv_".to_string(),
4367 "try" => "try_".to_string(),
4368 "typeof" => "typeof_".to_string(),
4369 "unsized" => "unsized_".to_string(),
4370 "virtual" => "virtual_".to_string(),
4371 "yield" => "yield_".to_string(),
4372 _ => result,
4373 }
4374 }
4375
4376 fn generate_constraint_doc(
4384 &self,
4385 constraints: &crate::analysis::PropertyConstraints,
4386 ) -> TokenStream {
4387 use crate::type_mapping::ConstraintMode;
4388
4389 if constraints.is_empty() {
4390 return TokenStream::new();
4391 }
4392 match self.config.types.constraint_mode() {
4393 ConstraintMode::Off => TokenStream::new(),
4394 ConstraintMode::Doc => {
4395 let formatted = format_constraints_doc(constraints);
4396 quote! { #[doc = #formatted] }
4397 }
4398 }
4399 }
4400
4401 fn sanitize_doc_comment(&self, desc: &str) -> String {
4402 let mut result = desc.to_string();
4404
4405 if result.contains('\n')
4413 && (result.contains('{')
4414 || result.contains("```")
4415 || result.contains("Human:")
4416 || result.contains("Assistant:")
4417 || result
4418 .lines()
4419 .any(|line| line.trim().starts_with('"') && line.trim().ends_with('"')))
4420 {
4421 if result.contains("```") {
4423 result = result.replace("```", "```ignore");
4424 } else {
4425 if result.lines().any(|line| {
4427 let trimmed = line.trim();
4428 trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() > 2
4429 }) {
4430 result = format!("```ignore\n{result}\n```");
4431 }
4432 }
4433 }
4434
4435 result
4436 }
4437
4438 pub(crate) fn to_rust_type_name(&self, s: &str) -> String {
4439 rust_type_name(s)
4440 }
4441
4442 pub(crate) fn to_rust_field_name(&self, s: &str) -> String {
4443 let leading_marker = match s.chars().next() {
4447 Some('-') if s.len() > 1 => "neg_",
4448 Some('+') if s.len() > 1 => "pos_",
4449 _ => "",
4450 };
4451
4452 let mut result = String::new();
4454 let mut prev_was_upper = false;
4455 let mut prev_was_underscore = false;
4456
4457 for (i, c) in s.chars().enumerate() {
4458 match c {
4459 'A'..='Z' => {
4460 if i > 0 && !prev_was_upper && !prev_was_underscore {
4462 result.push('_');
4463 }
4464 result.push(c.to_ascii_lowercase());
4465 prev_was_upper = true;
4466 prev_was_underscore = false;
4467 }
4468 'a'..='z' | '0'..='9' => {
4469 result.push(c);
4470 prev_was_upper = false;
4471 prev_was_underscore = false;
4472 }
4473 '-' | '.' | '_' | '@' | '#' | '$' | ' ' => {
4474 if !prev_was_underscore && !result.is_empty() {
4475 result.push('_');
4476 prev_was_underscore = true;
4477 }
4478 prev_was_upper = false;
4479 }
4480 _ => {
4481 if !prev_was_underscore && !result.is_empty() {
4483 result.push('_');
4484 }
4485 prev_was_upper = false;
4486 prev_was_underscore = true;
4487 }
4488 }
4489 }
4490
4491 let mut result = result.trim_matches('_').to_string();
4493 if result.is_empty() {
4494 return "field".to_string();
4495 }
4496
4497 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
4499 result = format!("field_{leading_marker}{result}");
4500 } else if !leading_marker.is_empty() {
4501 result = format!("{leading_marker}{result}");
4502 }
4503
4504 if matches!(result.as_str(), "self" | "super" | "crate" | "Self") {
4507 return format!("{result}_field");
4508 }
4509 if matches!(result.as_str(), "true" | "false") {
4513 return format!("{result}_field");
4514 }
4515 if Self::is_rust_keyword(&result) {
4517 format!("r#{result}")
4518 } else {
4519 result
4520 }
4521 }
4522
4523 pub fn is_rust_keyword(s: &str) -> bool {
4525 matches!(
4526 s,
4527 "type"
4528 | "match"
4529 | "fn"
4530 | "struct"
4531 | "enum"
4532 | "impl"
4533 | "trait"
4534 | "mod"
4535 | "use"
4536 | "pub"
4537 | "const"
4538 | "static"
4539 | "let"
4540 | "mut"
4541 | "ref"
4542 | "move"
4543 | "return"
4544 | "if"
4545 | "else"
4546 | "while"
4547 | "for"
4548 | "loop"
4549 | "break"
4550 | "continue"
4551 | "self"
4552 | "super"
4553 | "crate"
4554 | "async"
4555 | "await"
4556 | "override"
4557 | "box"
4558 | "dyn"
4559 | "where"
4560 | "in"
4561 | "abstract"
4562 | "become"
4563 | "do"
4564 | "final"
4565 | "macro"
4566 | "priv"
4567 | "try"
4568 | "typeof"
4569 | "unsized"
4570 | "virtual"
4571 | "yield"
4572 | "gen"
4574 )
4575 }
4576
4577 pub fn to_field_ident(name: &str) -> proc_macro2::Ident {
4579 if let Some(raw) = name.strip_prefix("r#") {
4580 proc_macro2::Ident::new_raw(raw, proc_macro2::Span::call_site())
4581 } else {
4582 proc_macro2::Ident::new(name, proc_macro2::Span::call_site())
4583 }
4584 }
4585
4586 fn to_camel_case(&self, s: &str) -> String {
4587 let mut result = String::new();
4589 let mut capitalize_next = false;
4590
4591 for (i, c) in s.chars().enumerate() {
4592 match c {
4593 '_' | '-' | '.' | ' ' => {
4594 capitalize_next = true;
4596 }
4597 'A'..='Z' => {
4598 if i == 0 {
4599 result.push(c.to_ascii_lowercase());
4601 } else if capitalize_next {
4602 result.push(c);
4603 capitalize_next = false;
4604 } else {
4605 result.push(c.to_ascii_lowercase());
4606 }
4607 }
4608 'a'..='z' | '0'..='9' => {
4609 if capitalize_next {
4610 result.push(c.to_ascii_uppercase());
4611 capitalize_next = false;
4612 } else {
4613 result.push(c);
4614 }
4615 }
4616 _ => {
4617 capitalize_next = true;
4619 }
4620 }
4621 }
4622
4623 if result.is_empty() {
4624 return "field".to_string();
4625 }
4626
4627 result
4628 }
4629
4630 fn generate_composition_struct(
4631 &self,
4632 schema: &crate::analysis::AnalyzedSchema,
4633 schemas: &[crate::analysis::SchemaRef],
4634 ) -> Result<TokenStream> {
4635 let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
4636
4637 let fields = schemas.iter().enumerate().map(|(i, schema_ref)| {
4643 let field_name = format_ident!("part_{}", i);
4644 let field_type = format_ident!("{}", self.to_rust_type_name(&schema_ref.target));
4645
4646 quote! {
4647 #[serde(flatten)]
4648 pub #field_name: #field_type,
4649 }
4650 });
4651
4652 let doc_comment = if let Some(desc) = &schema.description {
4653 quote! { #[doc = #desc] }
4654 } else {
4655 TokenStream::new()
4656 };
4657
4658 let derives = if self.config.enable_specta {
4660 quote! {
4661 #[derive(Debug, Clone, Deserialize, Serialize)]
4662 #[cfg_attr(feature = "specta", derive(specta::Type))]
4663 }
4664 } else {
4665 quote! {
4666 #[derive(Debug, Clone, Deserialize, Serialize)]
4667 }
4668 };
4669
4670 Ok(quote! {
4671 #doc_comment
4672 #derives
4673 pub struct #struct_name {
4674 #(#fields)*
4675 }
4676 })
4677 }
4678
4679 fn union_declared_properties(
4680 &self,
4681 target: &str,
4682 analysis: &crate::analysis::SchemaAnalysis,
4683 visited: &mut std::collections::HashSet<String>,
4684 ) -> std::collections::HashSet<String> {
4685 if !visited.insert(target.to_string()) {
4686 return std::collections::HashSet::new();
4687 }
4688 let mut properties = std::collections::HashSet::new();
4689 if let Some(schema) = analysis.schemas.get(target) {
4690 match &schema.schema_type {
4691 crate::analysis::SchemaType::Object {
4692 properties: own,
4693 variant,
4694 ..
4695 } => {
4696 properties.extend(own.keys().cloned());
4697 if let Some(variant) = variant {
4698 properties.extend(self.union_declared_properties(
4699 &variant.target,
4700 analysis,
4701 visited,
4702 ));
4703 }
4704 }
4705 crate::analysis::SchemaType::DiscriminatedUnion { variants, .. } => {
4706 for variant in variants {
4707 properties.extend(self.union_declared_properties(
4708 &variant.type_name,
4709 analysis,
4710 visited,
4711 ));
4712 }
4713 }
4714 crate::analysis::SchemaType::Union { variants, .. }
4715 | crate::analysis::SchemaType::Composition { schemas: variants } => {
4716 for variant in variants {
4717 properties.extend(self.union_declared_properties(
4718 &variant.target,
4719 analysis,
4720 visited,
4721 ));
4722 }
4723 }
4724 crate::analysis::SchemaType::Reference { target } => {
4725 properties.extend(self.union_declared_properties(target, analysis, visited));
4726 }
4727 _ => {}
4728 }
4729 }
4730 visited.remove(target);
4731 properties
4732 }
4733
4734 #[allow(dead_code)]
4735 fn find_missing_types(&self, analysis: &SchemaAnalysis) -> std::collections::HashSet<String> {
4736 let mut missing = std::collections::HashSet::new();
4737 let defined_types: std::collections::HashSet<String> =
4738 analysis.schemas.keys().cloned().collect();
4739
4740 for schema in analysis.schemas.values() {
4742 match &schema.schema_type {
4743 crate::analysis::SchemaType::Union { variants, .. } => {
4744 for variant in variants {
4745 if !defined_types.contains(&variant.target) {
4746 missing.insert(variant.target.clone());
4747 }
4748 }
4749 }
4750 crate::analysis::SchemaType::DiscriminatedUnion { variants, .. } => {
4751 for variant in variants {
4752 if !defined_types.contains(&variant.type_name) {
4753 missing.insert(variant.type_name.clone());
4754 }
4755 }
4756 }
4757 crate::analysis::SchemaType::Object { properties, .. } => {
4758 let mut sorted_props: Vec<_> = properties.iter().collect();
4760 sorted_props.sort_by_key(|(name, _)| name.as_str());
4761 for (_, prop) in sorted_props {
4762 if let crate::analysis::SchemaType::Reference { target } = &prop.schema_type
4763 {
4764 if !defined_types.contains(target) {
4765 missing.insert(target.clone());
4766 }
4767 }
4768 }
4769 }
4770 crate::analysis::SchemaType::Reference { target }
4771 if !defined_types.contains(target) =>
4772 {
4773 missing.insert(target.clone());
4774 }
4775 _ => {}
4776 }
4777 }
4778
4779 missing
4780 }
4781
4782 #[allow(clippy::only_used_in_recursion)]
4783 fn generate_array_item_type(
4784 &self,
4785 item_type: &crate::analysis::SchemaType,
4786 analysis: &crate::analysis::SchemaAnalysis,
4787 ) -> TokenStream {
4788 use crate::analysis::SchemaType;
4789
4790 match item_type {
4791 SchemaType::Primitive { rust_type, .. } => {
4792 if let Ok(parsed) = syn::parse_str::<syn::Type>(rust_type) {
4797 quote! { #parsed }
4798 } else if rust_type.contains("::") {
4799 let parts: Vec<_> = rust_type
4800 .split("::")
4801 .map(|p| format_ident!("{}", p))
4802 .collect();
4803 quote! { #(#parts)::* }
4804 } else {
4805 let type_ident = format_ident!("{}", rust_type);
4806 quote! { #type_ident }
4807 }
4808 }
4809 SchemaType::Reference { target } => {
4810 let target_type = format_ident!("{}", self.to_rust_type_name(target));
4811 if analysis.dependencies.recursive_schemas.contains(target) {
4813 quote! { Box<#target_type> }
4814 } else {
4815 quote! { #target_type }
4816 }
4817 }
4818 SchemaType::Array { item_type } => {
4819 let inner_type = self.generate_array_item_type(item_type, analysis);
4821 quote! { Vec<#inner_type> }
4822 }
4823 SchemaType::Nullable { inner_type } => {
4824 let inner_type = self.generate_array_item_type(inner_type, analysis);
4825 quote! { Option<#inner_type> }
4826 }
4827 SchemaType::Tuple { element_types } => {
4828 self.generate_tuple_type(element_types, analysis)
4829 }
4830 SchemaType::Untyped { shape, .. } => untyped_tokens(*shape),
4831 _ => {
4832 quote! { serde_json::Value }
4834 }
4835 }
4836 }
4837
4838 fn type_name_to_variant_name(&self, type_name: &str) -> String {
4840 match type_name {
4842 "bool" => return "Boolean".to_string(),
4843 "i8" | "i16" | "i32" | "i64" | "i128" => return "Integer".to_string(),
4844 "u8" | "u16" | "u32" | "u64" | "u128" => return "UnsignedInteger".to_string(),
4845 "f32" | "f64" => return "Number".to_string(),
4846 "String" => return "String".to_string(),
4847 "serde_json::Value" => return "Value".to_string(),
4848 "bytes::Bytes" => return "Binary".to_string(),
4852 "chrono::DateTime<chrono::Utc>" => return "DateTime".to_string(),
4853 "chrono::NaiveDate" => return "Date".to_string(),
4854 "chrono::NaiveTime" => return "Time".to_string(),
4855 "uuid::Uuid" => return "Uuid".to_string(),
4856 "url::Url" => return "Url".to_string(),
4857 "std::net::Ipv4Addr" => return "Ipv4".to_string(),
4858 "std::net::Ipv6Addr" => return "Ipv6".to_string(),
4859 _ => {}
4860 }
4861
4862 if type_name.starts_with("Vec<") && type_name.ends_with(">") {
4864 let inner = &type_name[4..type_name.len() - 1];
4865 if inner.starts_with("Vec<") && inner.ends_with(">") {
4867 let inner_inner = &inner[4..inner.len() - 1];
4868 return format!("{}ArrayArray", self.type_name_to_variant_name(inner_inner));
4869 }
4870 return format!("{}Array", self.type_name_to_variant_name(inner));
4871 }
4872
4873 let clean_name = type_name
4879 .trim_end_matches("Type")
4880 .trim_end_matches("Schema")
4881 .trim_end_matches("Item");
4882
4883 self.to_rust_type_name(clean_name)
4885 }
4886
4887 fn ensure_unique_variant_name_generator(
4889 &self,
4890 base_name: String,
4891 used_names: &mut std::collections::HashSet<String>,
4892 fallback_index: usize,
4893 ) -> String {
4894 if used_names.insert(base_name.clone()) {
4895 return base_name;
4896 }
4897
4898 for i in 2..100 {
4900 let numbered_name = format!("{base_name}{i}");
4901 if used_names.insert(numbered_name.clone()) {
4902 return numbered_name;
4903 }
4904 }
4905
4906 let fallback = format!("Variant{fallback_index}");
4908 used_names.insert(fallback.clone());
4909 fallback
4910 }
4911
4912 fn find_request_type_for_operation(
4914 &self,
4915 operation_id: &str,
4916 analysis: &SchemaAnalysis,
4917 ) -> Option<String> {
4918 analysis.operations.get(operation_id).and_then(|op| {
4920 op.request_body
4921 .as_ref()
4922 .and_then(|rb| rb.schema_name().map(|s| s.to_string()))
4923 })
4924 }
4925
4926 fn resolve_streaming_event_type(
4928 &self,
4929 endpoint: &crate::streaming::StreamingEndpoint,
4930 analysis: &SchemaAnalysis,
4931 ) -> Result<String> {
4932 match &endpoint.event_flow {
4933 crate::streaming::EventFlow::Simple => {
4934 if analysis.schemas.contains_key(&endpoint.event_union_type) {
4937 Ok(endpoint.event_union_type.to_string())
4938 } else {
4939 Err(crate::error::GeneratorError::ValidationError(format!(
4940 "Streaming response type '{}' not found in schema for simple streaming endpoint '{}'",
4941 endpoint.event_union_type, endpoint.operation_id
4942 )))
4943 }
4944 }
4945 crate::streaming::EventFlow::StartDeltaStop { .. } => {
4946 if analysis.schemas.contains_key(&endpoint.event_union_type) {
4949 Ok(endpoint.event_union_type.to_string())
4950 } else {
4951 Err(crate::error::GeneratorError::ValidationError(format!(
4952 "Event union type '{}' not found in schema for complex streaming endpoint '{}'",
4953 endpoint.event_union_type, endpoint.operation_id
4954 )))
4955 }
4956 }
4957 }
4958 }
4959
4960 fn generate_streaming_error_types(&self) -> Result<TokenStream> {
4962 Ok(quote! {
4963 #[derive(Debug, thiserror::Error)]
4965 pub enum StreamingError {
4966 #[error("Connection error: {0}")]
4967 Connection(String),
4968 #[error("HTTP error: {status}")]
4969 Http { status: u16 },
4970 #[error("SSE parsing error: {0}")]
4971 Parsing(String),
4972 #[error("Authentication error: {0}")]
4973 Authentication(String),
4974 #[error("Rate limit error: {0}")]
4975 RateLimit(String),
4976 #[error("API error: {0}")]
4977 Api(String),
4978 #[error("Timeout error: {0}")]
4979 Timeout(String),
4980 #[error("Response body exceeded configured limit of {limit} bytes")]
4981 ResponseTooLarge { limit: usize },
4982 #[error("JSON serialization/deserialization error: {0}")]
4983 Json(#[from] serde_json::Error),
4984 #[error("Request error: {0}")]
4985 Request(reqwest::Error),
4986 }
4987
4988 impl From<reqwest::header::InvalidHeaderValue> for StreamingError {
4989 fn from(err: reqwest::header::InvalidHeaderValue) -> Self {
4990 StreamingError::Api(format!("Invalid header value: {}", err))
4991 }
4992 }
4993
4994 impl From<reqwest::Error> for StreamingError {
4995 fn from(err: reqwest::Error) -> Self {
4996 if err.is_timeout() {
4997 StreamingError::Timeout(err.to_string())
4998 } else if err.is_status() {
4999 if let Some(status) = err.status() {
5000 StreamingError::Http { status: status.as_u16() }
5001 } else {
5002 StreamingError::Connection(err.to_string())
5003 }
5004 } else {
5005 StreamingError::Request(err)
5006 }
5007 }
5008 }
5009 })
5010 }
5011
5012 fn generate_endpoint_trait(
5014 &self,
5015 endpoint: &crate::streaming::StreamingEndpoint,
5016 analysis: &SchemaAnalysis,
5017 ) -> Result<TokenStream> {
5018 use crate::streaming::HttpMethod;
5019
5020 let trait_name = format_ident!(
5021 "{}StreamingClient",
5022 self.to_rust_type_name(&endpoint.operation_id)
5023 );
5024 let method_name =
5025 format_ident!("stream_{}", self.to_rust_field_name(&endpoint.operation_id));
5026 let event_type =
5027 format_ident!("{}", self.resolve_streaming_event_type(endpoint, analysis)?);
5028
5029 let method_signature = match endpoint.http_method {
5031 HttpMethod::Get => {
5032 let mut param_defs = Vec::new();
5034 for qp in &endpoint.query_parameters {
5035 let param_name = format_ident!("{}", self.to_rust_field_name(&qp.name));
5036 if qp.required {
5037 param_defs.push(quote! { #param_name: &str });
5038 } else {
5039 param_defs.push(quote! { #param_name: Option<&str> });
5040 }
5041 }
5042 quote! {
5043 async fn #method_name(
5044 &self,
5045 #(#param_defs),*
5046 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error>;
5047 }
5048 }
5049 HttpMethod::Post => {
5050 let request_type = self
5052 .find_request_type_for_operation(&endpoint.operation_id, analysis)
5053 .unwrap_or_else(|| "serde_json::Value".to_string());
5054 let request_type_ident = if request_type.contains("::") {
5055 let parts: Vec<&str> = request_type.split("::").collect();
5056 let path_parts: Vec<_> = parts.iter().map(|p| format_ident!("{}", p)).collect();
5057 quote! { #(#path_parts)::* }
5058 } else {
5059 let ident = format_ident!("{}", request_type);
5060 quote! { #ident }
5061 };
5062 quote! {
5063 async fn #method_name(
5064 &self,
5065 request: #request_type_ident,
5066 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error>;
5067 }
5068 }
5069 };
5070
5071 Ok(quote! {
5072 #[async_trait]
5074 pub trait #trait_name {
5075 type Error: std::error::Error + Send + Sync + 'static;
5076
5077 #method_signature
5079 }
5080 })
5081 }
5082
5083 fn generate_streaming_client_impl(
5085 &self,
5086 streaming_config: &crate::streaming::StreamingConfig,
5087 analysis: &SchemaAnalysis,
5088 ) -> Result<TokenStream> {
5089 let client_name = format_ident!(
5090 "{}Client",
5091 self.to_rust_type_name(&streaming_config.client_module_name)
5092 );
5093
5094 let mut struct_fields = vec![
5097 quote! { base_url: String },
5098 quote! { api_key: Option<String> },
5099 quote! { sse_client: SseClient },
5100 quote! { custom_headers: std::collections::BTreeMap<String, String> },
5101 ];
5102
5103 let has_optional_headers = !streaming_config
5104 .endpoints
5105 .iter()
5106 .all(|e| e.optional_headers.is_empty());
5107
5108 if has_optional_headers {
5109 struct_fields
5110 .push(quote! { optional_headers: std::collections::BTreeMap<String, String> });
5111 }
5112
5113 let default_base_url = if let Some(ref streaming_config) = self.config.streaming_config {
5116 streaming_config
5117 .endpoints
5118 .first()
5119 .and_then(|e| e.base_url.as_deref())
5120 .unwrap_or("https://api.example.com")
5121 } else {
5122 "https://api.example.com"
5123 };
5124 let max_response_body_bytes = self
5125 .config()
5126 .http_client_config
5127 .as_ref()
5128 .and_then(|http| http.max_response_body_bytes)
5129 .unwrap_or(8 * 1024 * 1024);
5130 let sse_client_initializer = if let Some(reconnect) = &streaming_config.reconnection_config
5131 {
5132 let max_retries = reconnect.max_retries;
5133 let initial_delay_ms = reconnect.initial_delay_ms;
5134 let max_delay_ms = reconnect.max_delay_ms;
5135 let backoff_multiplier = reconnect.backoff_multiplier;
5136 quote! {
5137 SseClient::new()
5138 .with_max_error_body_bytes(#max_response_body_bytes)
5139 .with_reconnect_options(SseReconnectOptions {
5140 max_retries: #max_retries,
5141 initial_retry_delay: std::time::Duration::from_millis(#initial_delay_ms),
5142 max_retry_delay: std::time::Duration::from_millis(#max_delay_ms),
5143 backoff_multiplier: #backoff_multiplier,
5144 })
5145 }
5146 } else {
5147 quote! {
5148 SseClient::new()
5149 .with_max_error_body_bytes(#max_response_body_bytes)
5150 }
5151 };
5152
5153 let constructor_fields = if has_optional_headers {
5155 quote! {
5156 base_url: #default_base_url.to_string(),
5157 api_key: None,
5158 sse_client: #sse_client_initializer,
5159 custom_headers: std::collections::BTreeMap::new(),
5160 optional_headers: std::collections::BTreeMap::new(),
5161 }
5162 } else {
5163 quote! {
5164 base_url: #default_base_url.to_string(),
5165 api_key: None,
5166 sse_client: #sse_client_initializer,
5167 custom_headers: std::collections::BTreeMap::new(),
5168 }
5169 };
5170
5171 let optional_headers_method = if has_optional_headers {
5173 quote! {
5174 pub fn set_optional_headers(&mut self, headers: std::collections::BTreeMap<String, String>) {
5176 self.optional_headers = headers;
5177 }
5178 }
5179 } else {
5180 TokenStream::new()
5181 };
5182
5183 let constructor = quote! {
5184 impl #client_name {
5185 pub fn new() -> Self {
5187 Self {
5188 #constructor_fields
5189 }
5190 }
5191
5192 pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
5194 self.base_url = base_url.into();
5195 self
5196 }
5197
5198 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
5200 self.api_key = Some(api_key.into());
5201 self
5202 }
5203
5204 pub fn with_max_response_body_bytes(mut self, limit: usize) -> Self {
5206 self.sse_client = self.sse_client.with_max_error_body_bytes(limit);
5207 self
5208 }
5209
5210 pub fn with_header(
5212 mut self,
5213 name: impl Into<String>,
5214 value: impl Into<String>,
5215 ) -> Self {
5216 self.custom_headers.insert(name.into(), value.into());
5217 self
5218 }
5219
5220 pub fn with_http_client(mut self, client: reqwest::Client) -> Self {
5222 self.sse_client = self.sse_client.with_http_client(client);
5223 self
5224 }
5225
5226 #optional_headers_method
5227 }
5228 };
5229
5230 let mut trait_impls = Vec::new();
5232 for endpoint in &streaming_config.endpoints {
5233 let trait_impl = self.generate_endpoint_trait_impl(endpoint, &client_name, analysis)?;
5234 trait_impls.push(trait_impl);
5235 }
5236
5237 let default_impl = quote! {
5239 impl Default for #client_name {
5240 fn default() -> Self {
5241 Self::new()
5242 }
5243 }
5244 };
5245
5246 Ok(quote! {
5247 #[derive(Debug, Clone)]
5249 pub struct #client_name {
5250 #(#struct_fields,)*
5251 }
5252
5253 #constructor
5254
5255 #default_impl
5256
5257 #(#trait_impls)*
5258 })
5259 }
5260
5261 fn generate_endpoint_trait_impl(
5263 &self,
5264 endpoint: &crate::streaming::StreamingEndpoint,
5265 client_name: &proc_macro2::Ident,
5266 analysis: &SchemaAnalysis,
5267 ) -> Result<TokenStream> {
5268 use crate::streaming::HttpMethod;
5269
5270 let trait_name = format_ident!(
5271 "{}StreamingClient",
5272 self.to_rust_type_name(&endpoint.operation_id)
5273 );
5274 let method_name =
5275 format_ident!("stream_{}", self.to_rust_field_name(&endpoint.operation_id));
5276 let event_type =
5277 format_ident!("{}", self.resolve_streaming_event_type(endpoint, analysis)?);
5278
5279 let mut header_setup = Vec::new();
5281 for (name, value) in &endpoint.required_headers {
5282 header_setup.push(quote! {
5283 headers.insert(#name, HeaderValue::from_static(#value));
5284 });
5285 }
5286
5287 if let Some(auth_header) = &endpoint.auth_header {
5290 match auth_header {
5291 crate::streaming::AuthHeader::Bearer(header_name) => {
5292 header_setup.push(quote! {
5293 if let Some(ref api_key) = self.api_key {
5294 headers.insert(#header_name, HeaderValue::from_str(&format!("Bearer {}", api_key))?);
5295 }
5296 });
5297 }
5298 crate::streaming::AuthHeader::ApiKey(header_name) => {
5299 header_setup.push(quote! {
5300 if let Some(ref api_key) = self.api_key {
5301 headers.insert(#header_name, HeaderValue::from_str(api_key)?);
5302 }
5303 });
5304 }
5305 }
5306 } else {
5307 header_setup.push(quote! {
5309 if let Some(ref api_key) = self.api_key {
5310 headers.insert("Authorization", HeaderValue::from_str(&format!("Bearer {}", api_key))?);
5311 }
5312 });
5313 }
5314
5315 header_setup.push(quote! {
5317 for (name, value) in &self.custom_headers {
5318 if let (Ok(header_name), Ok(header_value)) = (reqwest::header::HeaderName::from_bytes(name.as_bytes()), HeaderValue::from_str(value)) {
5319 headers.insert(header_name, header_value);
5320 }
5321 }
5322 });
5323
5324 if !endpoint.optional_headers.is_empty() {
5326 header_setup.push(quote! {
5327 for (key, value) in &self.optional_headers {
5328 if let (Ok(header_name), Ok(header_value)) = (reqwest::header::HeaderName::from_bytes(key.as_bytes()), HeaderValue::from_str(value)) {
5329 headers.insert(header_name, header_value);
5330 }
5331 }
5332 });
5333 }
5334
5335 match endpoint.http_method {
5337 HttpMethod::Get => self.generate_get_streaming_impl(
5338 endpoint,
5339 client_name,
5340 &trait_name,
5341 &method_name,
5342 &event_type,
5343 &header_setup,
5344 ),
5345 HttpMethod::Post => self.generate_post_streaming_impl(
5346 endpoint,
5347 client_name,
5348 &trait_name,
5349 &method_name,
5350 &event_type,
5351 &header_setup,
5352 analysis,
5353 ),
5354 }
5355 }
5356
5357 fn generate_get_streaming_impl(
5359 &self,
5360 endpoint: &crate::streaming::StreamingEndpoint,
5361 client_name: &proc_macro2::Ident,
5362 trait_name: &proc_macro2::Ident,
5363 method_name: &proc_macro2::Ident,
5364 event_type: &proc_macro2::Ident,
5365 header_setup: &[TokenStream],
5366 ) -> Result<TokenStream> {
5367 let path = &endpoint.path;
5368
5369 let mut param_defs = Vec::new();
5371 let mut query_params = Vec::new();
5372
5373 for qp in &endpoint.query_parameters {
5374 let param_name = format_ident!("{}", self.to_rust_field_name(&qp.name));
5375 let param_name_str = &qp.name;
5376
5377 if qp.required {
5378 param_defs.push(quote! { #param_name: &str });
5379 query_params.push(quote! {
5380 url.query_pairs_mut().append_pair(#param_name_str, #param_name);
5381 });
5382 } else {
5383 param_defs.push(quote! { #param_name: Option<&str> });
5384 query_params.push(quote! {
5385 if let Some(v) = #param_name {
5386 url.query_pairs_mut().append_pair(#param_name_str, v);
5387 }
5388 });
5389 }
5390 }
5391
5392 let url_construction = quote! {
5394 let base_url = url::Url::parse(&self.base_url)
5395 .map_err(|e| StreamingError::Connection(format!("Invalid base URL: {}", e)))?;
5396 let path_to_join = #path.trim_start_matches('/');
5397 let mut url = base_url.join(path_to_join)
5398 .map_err(|e| StreamingError::Connection(format!("URL join error: {}", e)))?;
5399 #(#query_params)*
5400 };
5401
5402 let instrument_skip = quote! { #[instrument(skip(self), name = "streaming_get_request")] };
5403
5404 Ok(quote! {
5405 #[async_trait]
5406 impl #trait_name for #client_name {
5407 type Error = StreamingError;
5408
5409 #instrument_skip
5410 async fn #method_name(
5411 &self,
5412 #(#param_defs),*
5413 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error> {
5414 debug!("Starting streaming GET request");
5415
5416 let mut headers = HeaderMap::new();
5417 #(#header_setup)*
5418
5419 #url_construction
5420 let url_str = url.to_string();
5421 debug!("Making streaming GET request to: {}", url_str);
5422
5423 let request_builder = self.sse_client
5424 .get(url_str)
5425 .headers(headers);
5426
5427 debug!("Creating SSE stream from request");
5428 let stream = self
5429 .sse_client
5430 .stream::<#event_type>(request_builder)
5431 .await?;
5432 info!("SSE stream created successfully");
5433 Ok(stream)
5434 }
5435 }
5436 })
5437 }
5438
5439 #[allow(clippy::too_many_arguments)]
5441 fn generate_post_streaming_impl(
5442 &self,
5443 endpoint: &crate::streaming::StreamingEndpoint,
5444 client_name: &proc_macro2::Ident,
5445 trait_name: &proc_macro2::Ident,
5446 method_name: &proc_macro2::Ident,
5447 event_type: &proc_macro2::Ident,
5448 header_setup: &[TokenStream],
5449 analysis: &SchemaAnalysis,
5450 ) -> Result<TokenStream> {
5451 let path = &endpoint.path;
5452
5453 let request_type = self
5455 .find_request_type_for_operation(&endpoint.operation_id, analysis)
5456 .unwrap_or_else(|| "serde_json::Value".to_string());
5457 let request_type_ident = if request_type.contains("::") {
5458 let parts: Vec<&str> = request_type.split("::").collect();
5459 let path_parts: Vec<_> = parts.iter().map(|p| format_ident!("{}", p)).collect();
5460 quote! { #(#path_parts)::* }
5461 } else {
5462 let ident = format_ident!("{}", request_type);
5463 quote! { #ident }
5464 };
5465
5466 let url_construction = quote! {
5468 let base_url = url::Url::parse(&self.base_url)
5469 .map_err(|e| StreamingError::Connection(format!("Invalid base URL: {}", e)))?;
5470 let path_to_join = #path.trim_start_matches('/');
5471 let url = base_url.join(path_to_join)
5472 .map_err(|e| StreamingError::Connection(format!("URL join error: {}", e)))?
5473 .to_string();
5474 };
5475
5476 let stream_param = &endpoint.stream_parameter;
5478 let stream_setup = if stream_param.is_empty() {
5479 quote! {
5480 let streaming_request = request;
5481 }
5482 } else {
5483 quote! {
5484 let mut streaming_request = request;
5486 if let Ok(mut request_value) = serde_json::to_value(&streaming_request) {
5487 if let Some(obj) = request_value.as_object_mut() {
5488 obj.insert(#stream_param.to_string(), serde_json::Value::Bool(true));
5489 }
5490 streaming_request = serde_json::from_value(request_value)?;
5491 }
5492 }
5493 };
5494
5495 Ok(quote! {
5496 #[async_trait]
5497 impl #trait_name for #client_name {
5498 type Error = StreamingError;
5499
5500 #[instrument(skip(self, request), name = "streaming_post_request")]
5501 async fn #method_name(
5502 &self,
5503 request: #request_type_ident,
5504 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error> {
5505 debug!("Starting streaming POST request");
5506
5507 #stream_setup
5508
5509 let mut headers = HeaderMap::new();
5510 #(#header_setup)*
5511
5512 #url_construction
5513 debug!("Making streaming POST request to: {}", url);
5514
5515 let request_builder = self.sse_client
5516 .post(&url)
5517 .headers(headers)
5518 .json(&streaming_request);
5519
5520 debug!("Creating SSE stream from request");
5521 let stream = self
5522 .sse_client
5523 .stream::<#event_type>(request_builder)
5524 .await?;
5525 info!("SSE stream created successfully");
5526 Ok(stream)
5527 }
5528 }
5529 })
5530 }
5531
5532 fn generate_sse_runtime(&self) -> Result<String> {
5534 let provenance_attribute = self.provenance_attribute();
5535 let error_types = self.generate_streaming_error_types()?;
5536 let parser = self.generate_sse_parser_utilities()?;
5537 let tokens = quote! {
5538 #provenance_attribute
5542 #![allow(clippy::format_in_format_args)]
5543
5544 use futures_util::{Stream, StreamExt};
5545 use std::pin::Pin;
5546 use std::time::Duration;
5547 use tracing::debug;
5548
5549 #error_types
5550
5551 #[derive(Debug, Clone)]
5553 pub struct SseClient {
5554 http_client: reqwest::Client,
5555 max_error_body_bytes: usize,
5556 reconnect_options: Option<SseReconnectOptions>,
5557 }
5558
5559 impl SseClient {
5560 pub fn new() -> Self {
5561 Self {
5562 http_client: reqwest::Client::new(),
5563 max_error_body_bytes: DEFAULT_MAX_SSE_ERROR_BODY_BYTES,
5564 reconnect_options: None,
5565 }
5566 }
5567
5568 pub fn with_http_client(mut self, client: reqwest::Client) -> Self {
5569 self.http_client = client;
5570 self
5571 }
5572
5573 pub fn with_max_error_body_bytes(mut self, limit: usize) -> Self {
5574 self.max_error_body_bytes = limit;
5575 self
5576 }
5577
5578 pub fn with_reconnect_options(mut self, options: SseReconnectOptions) -> Self {
5580 self.reconnect_options = Some(options);
5581 self
5582 }
5583
5584 pub fn get(&self, url: impl reqwest::IntoUrl) -> reqwest::RequestBuilder {
5585 self.http_client.get(url)
5586 }
5587
5588 pub fn post(&self, url: impl reqwest::IntoUrl) -> reqwest::RequestBuilder {
5589 self.http_client.post(url)
5590 }
5591
5592 pub async fn stream<T>(
5593 &self,
5594 request_builder: reqwest::RequestBuilder,
5595 ) -> Result<Pin<Box<dyn Stream<Item = Result<T, StreamingError>> + Send>>, StreamingError>
5596 where
5597 T: serde::de::DeserializeOwned + Send + 'static,
5598 {
5599 if let Some(options) = self.reconnect_options.clone() {
5600 parse_sse_json_reconnecting_with_limit(
5601 request_builder,
5602 self.max_error_body_bytes,
5603 options,
5604 ).await
5605 } else {
5606 parse_sse_json_stream_with_limit(
5607 request_builder,
5608 self.max_error_body_bytes,
5609 ).await
5610 }
5611 }
5612
5613 pub async fn stream_raw(
5615 &self,
5616 request_builder: reqwest::RequestBuilder,
5617 ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<String>, StreamingError>> + Send>>, StreamingError> {
5618 parse_sse_raw_stream_with_limit(request_builder, self.max_error_body_bytes).await
5619 }
5620
5621 pub async fn stream_json<T>(
5623 &self,
5624 request_builder: reqwest::RequestBuilder,
5625 ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<T>, StreamingError>> + Send>>, StreamingError>
5626 where
5627 T: serde::de::DeserializeOwned + Send + 'static,
5628 {
5629 parse_sse_json_events_with_limit(request_builder, self.max_error_body_bytes).await
5630 }
5631
5632 pub async fn stream_raw_reconnecting(
5634 &self,
5635 request_builder: reqwest::RequestBuilder,
5636 ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<String>, StreamingError>> + Send>>, StreamingError> {
5637 parse_sse_raw_reconnecting_with_limit(
5638 request_builder,
5639 self.max_error_body_bytes,
5640 self.reconnect_options.clone().unwrap_or_default(),
5641 ).await
5642 }
5643
5644 pub async fn stream_json_reconnecting<T>(
5646 &self,
5647 request_builder: reqwest::RequestBuilder,
5648 ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<T>, StreamingError>> + Send>>, StreamingError>
5649 where
5650 T: serde::de::DeserializeOwned + Send + 'static,
5651 {
5652 parse_sse_json_reconnecting_events_with_limit(
5653 request_builder,
5654 self.max_error_body_bytes,
5655 self.reconnect_options.clone().unwrap_or_default(),
5656 ).await
5657 }
5658 }
5659
5660 impl Default for SseClient {
5661 fn default() -> Self {
5662 Self::new()
5663 }
5664 }
5665
5666 #parser
5667 };
5668 let syntax_tree = syn::parse2::<syn::File>(tokens).map_err(|error| {
5669 GeneratorError::CodeGenError(format!("Failed to parse generated sse.rs: {error}"))
5670 })?;
5671 Ok(prettyplease::unparse(&syntax_tree))
5672 }
5673
5674 fn generate_sse_parser_utilities(&self) -> Result<TokenStream> {
5676 Ok(quote! {
5677 pub const DEFAULT_MAX_SSE_ERROR_BODY_BYTES: usize = 8 * 1024 * 1024;
5679
5680 async fn __read_bounded_streaming_error_body(
5681 mut response: reqwest::Response,
5682 limit: usize,
5683 ) -> Result<Vec<u8>, StreamingError> {
5684 let mut body = Vec::new();
5685 while let Some(chunk) = response.chunk().await? {
5686 let next_len = body.len().checked_add(chunk.len());
5687 if next_len.is_none_or(|next_len| next_len > limit) {
5688 return Err(StreamingError::ResponseTooLarge { limit });
5689 }
5690 body.extend_from_slice(&chunk);
5691 }
5692 Ok(body)
5693 }
5694
5695 #[derive(Debug, Clone, PartialEq, Eq)]
5697 pub struct SseEvent<T> {
5698 pub event: String,
5700 pub data: T,
5702 pub id: Option<String>,
5704 pub retry: Option<Duration>,
5706 }
5707
5708 #[derive(Debug, Clone)]
5710 pub struct SseReconnectOptions {
5711 pub max_retries: u32,
5713 pub initial_retry_delay: Duration,
5715 pub max_retry_delay: Duration,
5717 pub backoff_multiplier: f64,
5719 }
5720
5721 impl Default for SseReconnectOptions {
5722 fn default() -> Self {
5723 Self {
5724 max_retries: 3,
5725 initial_retry_delay: Duration::from_secs(3),
5726 max_retry_delay: Duration::from_secs(30),
5727 backoff_multiplier: 2.0,
5728 }
5729 }
5730 }
5731
5732 impl SseReconnectOptions {
5733 fn delay(&self, attempt: u32, server_retry: Option<Duration>) -> Duration {
5734 if let Some(delay) = server_retry {
5735 return delay.min(self.max_retry_delay);
5736 }
5737 let multiplier = self.backoff_multiplier.max(1.0);
5738 let millis = self.initial_retry_delay.as_millis() as f64
5739 * multiplier.powi(attempt.min(63) as i32);
5740 Duration::from_millis(
5741 millis.min(self.max_retry_delay.as_millis() as f64) as u64,
5742 )
5743 }
5744 }
5745
5746 #[derive(Default)]
5747 struct __SseDecoder {
5748 line: Vec<u8>,
5749 event: String,
5750 data: Vec<String>,
5751 last_event_id: Option<String>,
5752 retry_delay: Option<Duration>,
5753 event_retry: Option<Duration>,
5754 saw_carriage_return: bool,
5755 }
5756
5757 impl __SseDecoder {
5758 fn feed(
5759 &mut self,
5760 chunk: &[u8],
5761 ) -> Vec<Result<SseEvent<String>, StreamingError>> {
5762 let mut messages = Vec::new();
5763 for &byte in chunk {
5764 if self.saw_carriage_return {
5765 self.saw_carriage_return = false;
5766 if byte == b'\n' {
5767 continue;
5768 }
5769 }
5770
5771 match byte {
5772 b'\n' => self.finish_line(&mut messages),
5773 b'\r' => {
5774 self.finish_line(&mut messages);
5775 self.saw_carriage_return = true;
5776 }
5777 _ => self.line.push(byte),
5778 }
5779 }
5780 messages
5781 }
5782
5783 fn finish(&mut self) -> Vec<Result<SseEvent<String>, StreamingError>> {
5784 let mut messages = Vec::new();
5785 if !self.line.is_empty() {
5786 self.finish_line(&mut messages);
5787 }
5788 self.dispatch(&mut messages);
5789 messages
5790 }
5791
5792 fn finish_line(
5793 &mut self,
5794 messages: &mut Vec<Result<SseEvent<String>, StreamingError>>,
5795 ) {
5796 let line = std::mem::take(&mut self.line);
5797 let line = match String::from_utf8(line) {
5798 Ok(line) => line,
5799 Err(error) => {
5800 messages.push(Err(StreamingError::Parsing(format!(
5801 "SSE line is not valid UTF-8: {}",
5802 error
5803 ))));
5804 return;
5805 }
5806 };
5807
5808 if line.is_empty() {
5809 self.dispatch(messages);
5810 return;
5811 }
5812 if line.starts_with(':') {
5813 return;
5814 }
5815
5816 let (field, value) = line
5817 .split_once(':')
5818 .map_or((line.as_str(), ""), |(field, value)| {
5819 (field, value.strip_prefix(' ').unwrap_or(value))
5820 });
5821 match field {
5822 "event" => self.event = value.to_string(),
5823 "data" => self.data.push(value.to_string()),
5824 "id" if !value.contains('\0') => {
5825 self.last_event_id = (!value.is_empty()).then(|| value.to_string());
5826 }
5827 "retry" if value.bytes().all(|byte| byte.is_ascii_digit()) => {
5828 if let Ok(milliseconds) = value.parse::<u64>() {
5829 let delay = Duration::from_millis(milliseconds);
5830 self.retry_delay = Some(delay);
5831 self.event_retry = Some(delay);
5832 }
5833 }
5834 _ => {}
5835 }
5836 }
5837
5838 fn dispatch(
5839 &mut self,
5840 messages: &mut Vec<Result<SseEvent<String>, StreamingError>>,
5841 ) {
5842 if self.data.is_empty() {
5843 self.event.clear();
5844 self.event_retry = None;
5845 return;
5846 }
5847 messages.push(Ok(SseEvent {
5848 event: if self.event.is_empty() {
5849 "message".to_string()
5850 } else {
5851 std::mem::take(&mut self.event)
5852 },
5853 data: std::mem::take(&mut self.data).join("\n"),
5854 id: self.last_event_id.clone(),
5855 retry: self.event_retry.take(),
5856 }));
5857 self.event.clear();
5858 }
5859
5860 fn reset_for_reconnect(&mut self) {
5861 self.line.clear();
5862 self.event.clear();
5863 self.data.clear();
5864 self.event_retry = None;
5865 self.saw_carriage_return = false;
5866 }
5867 }
5868
5869 fn __deserialize_sse_event<T>(
5870 event: SseEvent<String>,
5871 ) -> Option<Result<SseEvent<T>, StreamingError>>
5872 where
5873 T: serde::de::DeserializeOwned,
5874 {
5875 if event.data.trim() == "[DONE]" {
5876 return None;
5877 }
5878 if event.event == "ping" {
5879 debug!("Received SSE ping event, skipping");
5880 return None;
5881 }
5882 if event.data.trim().is_empty() {
5883 debug!("Empty SSE data, skipping");
5884 return None;
5885 }
5886
5887 let json_value = match serde_json::from_str::<serde_json::Value>(&event.data) {
5888 Ok(value) => value,
5889 Err(error) => {
5890 return Some(Err(StreamingError::Parsing(format!(
5891 "SSE event is not valid JSON: {} ({})",
5892 event.data, error
5893 ))));
5894 }
5895 };
5896 let is_ping = json_value
5897 .get("event")
5898 .or_else(|| json_value.get("type"))
5899 .and_then(serde_json::Value::as_str)
5900 .is_some_and(|event| event == "ping");
5901 if is_ping {
5902 debug!("Received ping event in JSON data, skipping");
5903 return None;
5904 }
5905
5906 Some(
5907 serde_json::from_value::<T>(json_value)
5908 .map(|data| SseEvent {
5909 event: event.event.clone(),
5910 data,
5911 id: event.id.clone(),
5912 retry: event.retry,
5913 })
5914 .map_err(|error| StreamingError::Parsing(format!(
5915 "Failed to parse SSE event: {} (raw: {}, event: {})",
5916 error, event.data, event.event
5917 ))),
5918 )
5919 }
5920
5921 pub async fn parse_sse_stream<T>(
5923 request_builder: reqwest::RequestBuilder
5924 ) -> Result<Pin<Box<dyn Stream<Item = Result<T, StreamingError>> + Send>>, StreamingError>
5925 where
5926 T: serde::de::DeserializeOwned + Send + 'static,
5927 {
5928 parse_sse_json_stream_with_limit(
5929 request_builder,
5930 DEFAULT_MAX_SSE_ERROR_BODY_BYTES,
5931 ).await
5932 }
5933
5934 struct __SseOpenError {
5935 error: StreamingError,
5936 retryable: bool,
5937 }
5938
5939 async fn __open_sse_response(
5940 request_builder: reqwest::RequestBuilder,
5941 max_response_body_bytes: usize,
5942 ) -> Result<reqwest::Response, __SseOpenError> {
5943 let response = request_builder.send().await.map_err(|error| __SseOpenError {
5944 error: error.into(),
5945 retryable: true,
5946 })?;
5947 if !response.status().is_success() {
5948 let status = response.status();
5949 let retryable = status.as_u16() == 429 || status.is_server_error();
5950 let error = match __read_bounded_streaming_error_body(
5951 response,
5952 max_response_body_bytes,
5953 ).await {
5954 Ok(body) => StreamingError::Connection(format!(
5955 "HTTP {} error: {}",
5956 status.as_u16(),
5957 String::from_utf8_lossy(&body)
5958 )),
5959 Err(error) => error,
5960 };
5961 return Err(__SseOpenError { error, retryable });
5962 }
5963
5964 let content_type = response
5965 .headers()
5966 .get(reqwest::header::CONTENT_TYPE)
5967 .and_then(|value| value.to_str().ok())
5968 .unwrap_or_default();
5969 if !content_type
5970 .split(';')
5971 .next()
5972 .is_some_and(|value| value.trim().eq_ignore_ascii_case("text/event-stream"))
5973 {
5974 let error = StreamingError::Parsing(format!(
5975 "Expected text/event-stream response, received {}",
5976 if content_type.is_empty() { "no Content-Type" } else { content_type }
5977 ));
5978 return Err(__SseOpenError { error, retryable: false });
5979 }
5980
5981 debug!("SSE connection opened");
5982 Ok(response)
5983 }
5984
5985 fn __raw_response_stream(
5986 response: reqwest::Response,
5987 ) -> Pin<Box<dyn Stream<Item = Result<SseEvent<String>, StreamingError>> + Send>> {
5988 let stream = futures_util::stream::unfold(
5989 (
5990 response,
5991 __SseDecoder::default(),
5992 std::collections::VecDeque::<Result<SseEvent<String>, StreamingError>>::new(),
5993 false,
5994 ),
5995 |(mut response, mut decoder, mut pending, mut done)| async move {
5996 loop {
5997 if let Some(item) = pending.pop_front() {
5998 return Some((item, (response, decoder, pending, done)));
5999 }
6000 if done {
6001 debug!("SSE stream completed normally");
6002 return None;
6003 }
6004
6005 match response.chunk().await {
6006 Ok(Some(chunk)) => {
6007 for event in decoder.feed(&chunk) {
6008 let is_done = event
6009 .as_ref()
6010 .is_ok_and(|event| event.data.trim() == "[DONE]");
6011 pending.push_back(event);
6012 if is_done {
6013 done = true;
6014 break;
6015 }
6016 }
6017 }
6018 Err(error) => {
6019 done = true;
6020 pending.push_back(Err(error.into()));
6021 }
6022 Ok(None) => {
6023 done = true;
6024 for event in decoder.finish() {
6025 pending.push_back(event);
6026 }
6027 }
6028 }
6029 }
6030 }
6031 );
6032
6033 Box::pin(stream)
6034 }
6035
6036 async fn parse_sse_raw_stream_with_limit(
6037 request_builder: reqwest::RequestBuilder,
6038 max_response_body_bytes: usize,
6039 ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<String>, StreamingError>> + Send>>, StreamingError> {
6040 Ok(match __open_sse_response(request_builder, max_response_body_bytes).await {
6041 Ok(response) => __raw_response_stream(response),
6042 Err(error) => Box::pin(futures_util::stream::once(async move { Err(error.error) })),
6043 })
6044 }
6045
6046 fn __json_event_stream<T>(
6047 raw: Pin<Box<dyn Stream<Item = Result<SseEvent<String>, StreamingError>> + Send>>,
6048 ) -> Pin<Box<dyn Stream<Item = Result<SseEvent<T>, StreamingError>> + Send>>
6049 where
6050 T: serde::de::DeserializeOwned + Send + 'static,
6051 {
6052 Box::pin(raw.filter_map(|event| async move {
6053 match event {
6054 Ok(event) => __deserialize_sse_event(event),
6055 Err(error) => Some(Err(error)),
6056 }
6057 }))
6058 }
6059
6060 async fn parse_sse_json_events_with_limit<T>(
6061 request_builder: reqwest::RequestBuilder,
6062 max_response_body_bytes: usize,
6063 ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<T>, StreamingError>> + Send>>, StreamingError>
6064 where
6065 T: serde::de::DeserializeOwned + Send + 'static,
6066 {
6067 Ok(__json_event_stream(
6068 parse_sse_raw_stream_with_limit(request_builder, max_response_body_bytes).await?,
6069 ))
6070 }
6071
6072 async fn parse_sse_json_stream_with_limit<T>(
6073 request_builder: reqwest::RequestBuilder,
6074 max_response_body_bytes: usize,
6075 ) -> Result<Pin<Box<dyn Stream<Item = Result<T, StreamingError>> + Send>>, StreamingError>
6076 where
6077 T: serde::de::DeserializeOwned + Send + 'static,
6078 {
6079 let events = parse_sse_json_events_with_limit(request_builder, max_response_body_bytes).await?;
6080 Ok(Box::pin(events.map(|event| event.map(|event| event.data))))
6081 }
6082
6083 struct __ReconnectState {
6084 request: reqwest::RequestBuilder,
6085 response: Option<reqwest::Response>,
6086 decoder: __SseDecoder,
6087 pending: std::collections::VecDeque<Result<SseEvent<String>, StreamingError>>,
6088 options: SseReconnectOptions,
6089 max_response_body_bytes: usize,
6090 attempts: u32,
6091 wait_before_open: bool,
6092 done: bool,
6093 }
6094
6095 async fn parse_sse_raw_reconnecting_with_limit(
6096 request_builder: reqwest::RequestBuilder,
6097 max_response_body_bytes: usize,
6098 options: SseReconnectOptions,
6099 ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<String>, StreamingError>> + Send>>, StreamingError> {
6100 if request_builder.try_clone().is_none() {
6101 return Err(StreamingError::Connection(
6102 "SSE reconnection requires a cloneable request body".to_string(),
6103 ));
6104 }
6105
6106 let stream = futures_util::stream::unfold(
6107 __ReconnectState {
6108 request: request_builder,
6109 response: None,
6110 decoder: __SseDecoder::default(),
6111 pending: std::collections::VecDeque::new(),
6112 options,
6113 max_response_body_bytes,
6114 attempts: 0,
6115 wait_before_open: false,
6116 done: false,
6117 },
6118 |mut state| async move {
6119 loop {
6120 if let Some(item) = state.pending.pop_front() {
6121 return Some((item, state));
6122 }
6123 if state.done {
6124 return None;
6125 }
6126
6127 if state.response.is_none() {
6128 if state.wait_before_open {
6129 let delay = state.options.delay(
6130 state.attempts.saturating_sub(1),
6131 state.decoder.retry_delay,
6132 );
6133 debug!(?delay, attempt = state.attempts, "Reconnecting SSE stream");
6134 futures_timer::Delay::new(delay).await;
6135 state.wait_before_open = false;
6136 }
6137
6138 let mut request = state.request.try_clone().expect("request clone checked");
6139 if let Some(last_event_id) = state.decoder.last_event_id.as_deref() {
6140 request = request.header("Last-Event-ID", last_event_id);
6141 }
6142 match __open_sse_response(request, state.max_response_body_bytes).await {
6143 Ok(response) => state.response = Some(response),
6144 Err(error) if error.retryable && state.attempts < state.options.max_retries => {
6145 state.attempts += 1;
6146 state.wait_before_open = true;
6147 continue;
6148 }
6149 Err(error) => {
6150 state.done = true;
6151 state.pending.push_back(Err(error.error));
6152 continue;
6153 }
6154 }
6155 }
6156
6157 let next = state.response.as_mut().expect("response opened").chunk().await;
6158 match next {
6159 Ok(Some(chunk)) => {
6160 let events = state.decoder.feed(&chunk);
6161 if !events.is_empty() {
6162 state.attempts = 0;
6163 }
6164 for event in events {
6165 let is_done = event
6166 .as_ref()
6167 .is_ok_and(|event| event.data.trim() == "[DONE]");
6168 state.pending.push_back(event);
6169 if is_done {
6170 state.done = true;
6171 state.response = None;
6172 break;
6173 }
6174 }
6175 }
6176 Ok(None) => {
6177 let events = state.decoder.finish();
6178 if !events.is_empty() {
6179 state.attempts = 0;
6180 }
6181 for event in events {
6182 let is_done = event
6183 .as_ref()
6184 .is_ok_and(|event| event.data.trim() == "[DONE]");
6185 state.pending.push_back(event);
6186 if is_done {
6187 state.done = true;
6188 break;
6189 }
6190 }
6191 state.response = None;
6192 state.decoder.reset_for_reconnect();
6193 if !state.done {
6194 if state.attempts < state.options.max_retries {
6195 state.attempts += 1;
6196 state.wait_before_open = true;
6197 } else {
6198 state.done = true;
6199 }
6200 }
6201 }
6202 Err(error) => {
6203 state.response = None;
6204 state.decoder.reset_for_reconnect();
6205 if state.attempts < state.options.max_retries {
6206 state.attempts += 1;
6207 state.wait_before_open = true;
6208 } else {
6209 state.done = true;
6210 state.pending.push_back(Err(error.into()));
6211 }
6212 }
6213 }
6214 }
6215 },
6216 );
6217 Ok(Box::pin(stream))
6218 }
6219
6220 async fn parse_sse_json_reconnecting_events_with_limit<T>(
6221 request_builder: reqwest::RequestBuilder,
6222 max_response_body_bytes: usize,
6223 options: SseReconnectOptions,
6224 ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<T>, StreamingError>> + Send>>, StreamingError>
6225 where
6226 T: serde::de::DeserializeOwned + Send + 'static,
6227 {
6228 Ok(__json_event_stream(
6229 parse_sse_raw_reconnecting_with_limit(
6230 request_builder,
6231 max_response_body_bytes,
6232 options,
6233 ).await?,
6234 ))
6235 }
6236
6237 async fn parse_sse_json_reconnecting_with_limit<T>(
6238 request_builder: reqwest::RequestBuilder,
6239 max_response_body_bytes: usize,
6240 options: SseReconnectOptions,
6241 ) -> Result<Pin<Box<dyn Stream<Item = Result<T, StreamingError>> + Send>>, StreamingError>
6242 where
6243 T: serde::de::DeserializeOwned + Send + 'static,
6244 {
6245 let events = parse_sse_json_reconnecting_events_with_limit(
6246 request_builder,
6247 max_response_body_bytes,
6248 options,
6249 ).await?;
6250 Ok(Box::pin(events.map(|event| event.map(|event| event.data))))
6251 }
6252 })
6253 }
6254
6255 fn generate_reconnection_utilities(
6257 &self,
6258 reconnect_config: &crate::streaming::ReconnectionConfig,
6259 ) -> Result<TokenStream> {
6260 let max_retries = reconnect_config.max_retries;
6261 let initial_delay = reconnect_config.initial_delay_ms;
6262 let max_delay = reconnect_config.max_delay_ms;
6263 let backoff_multiplier = reconnect_config.backoff_multiplier;
6264
6265 Ok(quote! {
6266 #[derive(Debug, Clone)]
6268 pub struct ReconnectionManager {
6269 max_retries: u32,
6270 initial_delay_ms: u64,
6271 max_delay_ms: u64,
6272 backoff_multiplier: f64,
6273 current_attempt: u32,
6274 }
6275
6276 impl ReconnectionManager {
6277 pub fn new() -> Self {
6279 Self {
6280 max_retries: #max_retries,
6281 initial_delay_ms: #initial_delay,
6282 max_delay_ms: #max_delay,
6283 backoff_multiplier: #backoff_multiplier,
6284 current_attempt: 0,
6285 }
6286 }
6287
6288 pub fn should_retry(&self) -> bool {
6290 self.current_attempt < self.max_retries
6291 }
6292
6293 pub fn next_retry_delay(&mut self) -> Duration {
6295 if !self.should_retry() {
6296 return Duration::from_secs(0);
6297 }
6298
6299 let delay_ms = (self.initial_delay_ms as f64
6300 * self.backoff_multiplier.powi(self.current_attempt as i32)) as u64;
6301 let delay_ms = delay_ms.min(self.max_delay_ms);
6302
6303 self.current_attempt += 1;
6304 Duration::from_millis(delay_ms)
6305 }
6306
6307 pub fn reset(&mut self) {
6309 self.current_attempt = 0;
6310 }
6311
6312 pub fn current_attempt(&self) -> u32 {
6314 self.current_attempt
6315 }
6316 }
6317
6318 impl Default for ReconnectionManager {
6319 fn default() -> Self {
6320 Self::new()
6321 }
6322 }
6323 })
6324 }
6325}