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