1use crate::{
2 GeneratorError, Result,
3 analysis::{SchemaAnalysis, SchemaType},
4 streaming::StreamingConfig,
5};
6use proc_macro2::TokenStream;
7use quote::{format_ident, quote};
8use std::collections::BTreeMap;
9use std::path::PathBuf;
10
11fn parse_rust_type(rust_type: &str) -> Result<TokenStream> {
20 let parsed: syn::Type = syn::parse_str(rust_type).map_err(|e| {
21 GeneratorError::CodeGenError(format!(
22 "TypeMapper produced un-parseable type `{rust_type}`: {e}"
23 ))
24 })?;
25 Ok(quote! { #parsed })
26}
27
28fn format_constraints_doc(c: &crate::analysis::PropertyConstraints) -> String {
37 let mut parts: Vec<String> = Vec::new();
38
39 if let Some(v) = c.minimum {
40 parts.push(format!("minimum={}", strip_trailing_zero(v)));
41 }
42 if let Some(v) = c.maximum {
43 parts.push(format!("maximum={}", strip_trailing_zero(v)));
44 }
45 if let Some(v) = c.exclusive_minimum {
46 parts.push(format!("exclusiveMinimum={}", strip_trailing_zero(v)));
47 }
48 if let Some(v) = c.exclusive_maximum {
49 parts.push(format!("exclusiveMaximum={}", strip_trailing_zero(v)));
50 }
51 if let Some(v) = c.multiple_of {
52 parts.push(format!("multipleOf={}", strip_trailing_zero(v)));
53 }
54 if let Some(v) = c.min_length {
55 parts.push(format!("minLength={v}"));
56 }
57 if let Some(v) = c.max_length {
58 parts.push(format!("maxLength={v}"));
59 }
60 if let Some(v) = c.min_items {
61 parts.push(format!("minItems={v}"));
62 }
63 if let Some(v) = c.max_items {
64 parts.push(format!("maxItems={v}"));
65 }
66 if c.unique_items == Some(true) {
67 parts.push("uniqueItems=true".to_string());
68 }
69 if let Some(p) = &c.pattern {
70 let safe = p.replace("///", "/\u{200B}//").replace("*/", "*\u{200B}/");
75 parts.push(format!("pattern=`{safe}`"));
76 }
77
78 format!("Constraint: {}", parts.join(", "))
79}
80
81fn strip_trailing_zero(v: f64) -> String {
84 if v.fract() == 0.0 && v.is_finite() {
85 format!("{}", v as i64)
86 } else {
87 format!("{v}")
88 }
89}
90
91pub(crate) struct EmittedObjectProperty<'a> {
95 pub(crate) wire_name: &'a str,
96 pub(crate) property: &'a crate::analysis::PropertyInfo,
97 pub(crate) ident: syn::Ident,
98 pub(crate) is_required: bool,
99 pub(crate) field_type: TokenStream,
100}
101
102struct TypeGenerationIndex {
106 request_body_roots: std::collections::HashSet<String>,
107 reserved_type_names: std::collections::HashSet<String>,
108}
109
110struct TypeGenerationContext<'a> {
111 index: &'a TypeGenerationIndex,
112}
113
114#[derive(Debug, Clone)]
115pub struct GeneratorConfig {
116 pub spec_path: PathBuf,
118 pub output_dir: PathBuf,
120 pub module_name: String,
127 pub enable_sse_client: bool,
129 pub enable_async_client: bool,
131 pub enable_specta: bool,
133 pub type_mappings: BTreeMap<String, String>,
135 pub streaming_config: Option<StreamingConfig>,
137 pub nullable_field_overrides: BTreeMap<String, bool>,
140 pub extensible_enum_overrides: BTreeMap<String, bool>,
147 pub schema_extensions: Vec<PathBuf>,
150 pub http_client_config: Option<crate::http_config::HttpClientConfig>,
152 pub retry_config: Option<crate::http_config::RetryConfig>,
154 pub tracing_enabled: bool,
156 pub auth_config: Option<crate::http_config::AuthConfig>,
158 pub enable_registry: bool,
160 pub registry_only: bool,
162 pub types: crate::type_mapping::TypeMappingConfig,
166 pub builders: crate::config::BuildersSection,
168 pub server: Option<crate::config::ServerSection>,
171 pub client: Option<crate::config::ClientSection>,
174}
175
176impl Default for GeneratorConfig {
177 fn default() -> Self {
178 Self {
179 spec_path: "openapi.json".into(),
180 output_dir: "src/gen".into(),
181 module_name: "api_types".to_string(),
182 enable_sse_client: true,
183 enable_async_client: true,
184 enable_specta: false,
185 type_mappings: default_type_mappings(),
186 streaming_config: None,
187 nullable_field_overrides: BTreeMap::new(),
188 extensible_enum_overrides: BTreeMap::new(),
189 schema_extensions: Vec::new(),
190 http_client_config: None,
191 retry_config: None,
192 tracing_enabled: true,
193 auth_config: None,
194 enable_registry: false,
195 registry_only: false,
196 types: crate::type_mapping::TypeMappingConfig::default(),
197 builders: crate::config::BuildersSection::default(),
198 server: None,
199 client: None,
200 }
201 }
202}
203
204impl GeneratorConfig {
205 pub fn apply_spec_server_default(&mut self, spec: &serde_json::Value) {
216 let already_configured = self
217 .http_client_config
218 .as_ref()
219 .and_then(|http| http.base_url.as_deref())
220 .is_some_and(|url| !url.is_empty());
221 if already_configured {
222 return;
223 }
224
225 let Some(url) = spec
226 .pointer("/servers/0/url")
227 .and_then(serde_json::Value::as_str)
228 .map(str::trim)
229 .filter(|url| !url.is_empty())
230 .filter(|url| !url.starts_with('/'))
231 .filter(|url| !url.contains('{'))
232 else {
233 return;
234 };
235
236 match self.http_client_config.as_mut() {
237 Some(http) => http.base_url = Some(url.to_string()),
238 None => {
239 self.http_client_config = Some(crate::http_config::HttpClientConfig {
240 base_url: Some(url.to_string()),
241 timeout_seconds: None,
242 max_response_body_bytes: None,
243 default_headers: Default::default(),
244 })
245 }
246 }
247 }
248}
249
250pub fn default_type_mappings() -> BTreeMap<String, String> {
251 let mut mappings = BTreeMap::new();
252 mappings.insert("integer".to_string(), "i64".to_string());
253 mappings.insert("number".to_string(), "f64".to_string());
254 mappings.insert("string".to_string(), "String".to_string());
255 mappings.insert("boolean".to_string(), "bool".to_string());
256 mappings
257}
258
259pub(crate) fn rust_type_name(s: &str) -> String {
265 let mut result = String::new();
266 let mut next_upper = true;
267
268 for c in s.chars() {
269 match c {
270 'a'..='z' => {
271 result.push(if next_upper {
272 c.to_ascii_uppercase()
273 } else {
274 c
275 });
276 next_upper = false;
277 }
278 'A'..='Z' | '0'..='9' => {
279 result.push(c);
280 next_upper = false;
281 }
282 _ => next_upper = true,
283 }
284 }
285
286 if result.is_empty() {
287 result = "Type".to_string();
288 }
289 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
290 result = format!("Type{result}");
291 }
292 if matches!(
293 result.as_str(),
294 "Result"
295 | "Option"
296 | "Box"
297 | "Vec"
298 | "String"
299 | "Some"
300 | "None"
301 | "Ok"
302 | "Err"
303 | "Default"
304 | "Clone"
305 | "Debug"
306 | "Send"
307 | "Sync"
308 | "Sized"
309 | "Iterator"
310 | "From"
311 | "Into"
312 | "TryFrom"
313 | "TryInto"
314 | "AsRef"
315 | "AsMut"
316 ) {
317 result.push_str("Type");
318 }
319 result
320}
321
322#[derive(Debug, Clone)]
324pub struct GeneratedFile {
325 pub path: PathBuf,
327 pub content: String,
329}
330
331#[derive(Debug, Clone)]
333pub struct GenerationResult {
334 pub files: Vec<GeneratedFile>,
336 pub mod_file: GeneratedFile,
338 pub required_deps: Vec<crate::type_mapping::DepRequirement>,
342 pub pruned_schemas: usize,
344}
345
346#[derive(Debug)]
347struct OperationScopes {
348 client_ids: Option<std::collections::BTreeSet<String>>,
350 server_ids: std::collections::BTreeSet<String>,
351 streaming_ids: std::collections::BTreeSet<String>,
352 prune_models: bool,
353 extra_schema_roots: Vec<String>,
354}
355
356pub struct CodeGenerator {
357 config: GeneratorConfig,
358 source_provenance: Option<String>,
359}
360
361struct ObjectShape<'a> {
364 properties: &'a BTreeMap<String, crate::analysis::PropertyInfo>,
365 required: &'a std::collections::HashSet<String>,
366 additional_properties: &'a crate::analysis::ObjectAdditionalProperties,
367 variant: Option<&'a crate::analysis::SchemaRef>,
369}
370
371fn untyped_rust_type(shape: crate::analysis::UntypedShape) -> &'static str {
373 use crate::analysis::UntypedShape;
374 match shape {
375 UntypedShape::Value => "serde_json::Value",
376 UntypedShape::ValueArray => "Vec<serde_json::Value>",
377 UntypedShape::ValueMap => "std::collections::BTreeMap<String, serde_json::Value>",
378 }
379}
380
381fn untyped_tokens(shape: crate::analysis::UntypedShape) -> TokenStream {
382 use crate::analysis::UntypedShape;
383 match shape {
384 UntypedShape::Value => quote! { serde_json::Value },
385 UntypedShape::ValueArray => quote! { Vec<serde_json::Value> },
386 UntypedShape::ValueMap => quote! { std::collections::BTreeMap<String, serde_json::Value> },
387 }
388}
389
390fn schema_type_uses_serde_codec(schema_type: &SchemaType, codec: &str) -> bool {
391 match schema_type {
392 SchemaType::Primitive {
393 serde_with: Some(actual),
394 ..
395 } => actual == codec,
396 SchemaType::Object {
397 properties,
398 additional_properties,
399 ..
400 } => {
401 properties
402 .values()
403 .any(|property| schema_type_uses_serde_codec(&property.schema_type, codec))
404 || matches!(
405 additional_properties,
406 crate::analysis::ObjectAdditionalProperties::Typed { value_type }
407 if schema_type_uses_serde_codec(value_type, codec)
408 )
409 }
410 SchemaType::Array { item_type }
411 | SchemaType::Nullable {
412 inner_type: item_type,
413 } => schema_type_uses_serde_codec(item_type, codec),
414 SchemaType::Tuple { element_types } => element_types
415 .iter()
416 .any(|element| schema_type_uses_serde_codec(element, codec)),
417 _ => false,
418 }
419}
420
421impl CodeGenerator {
422 pub fn new(config: GeneratorConfig) -> Self {
423 Self {
424 config,
425 source_provenance: None,
426 }
427 }
428
429 pub fn with_source_provenance(mut self, source: impl Into<String>) -> Self {
431 self.source_provenance = Some(source.into());
432 self
433 }
434
435 pub fn config(&self) -> &GeneratorConfig {
437 &self.config
438 }
439
440 pub(crate) fn provenance_attribute(&self) -> TokenStream {
441 self.source_provenance
442 .as_ref()
443 .map(|source| {
444 let provenance = format!(
445 " Generated by openapi-to-rust v{}. Source OpenAPI document: {source}",
446 env!("CARGO_PKG_VERSION")
447 );
448 quote! { #![doc = #provenance] }
449 })
450 .unwrap_or_default()
451 }
452
453 pub fn generate_all(&self, analysis: &mut SchemaAnalysis) -> Result<GenerationResult> {
455 let scopes = self.resolve_operation_scopes(analysis)?;
458 let pruned_schemas = self.prune_models_to_scopes(analysis, &scopes);
459 let mut files = Vec::new();
460
461 if !self.config.registry_only {
462 let types_content = self.generate_types(analysis)?;
464 files.push(GeneratedFile {
465 path: "types.rs".into(),
466 content: types_content,
467 });
468
469 if self.config.enable_sse_client
471 && let Some(ref streaming_config) = self.config.streaming_config
472 {
473 if streaming_config.generate_client && !streaming_config.event_parser_helpers {
474 return Err(GeneratorError::ValidationError(
475 "streaming generate_client=true requires event_parser_helpers=true"
476 .to_string(),
477 ));
478 }
479 if streaming_config.event_parser_helpers {
480 files.push(GeneratedFile {
481 path: "sse.rs".into(),
482 content: self.generate_sse_runtime()?,
483 });
484 }
485 let streaming_content =
486 self.generate_streaming_client(streaming_config, analysis)?;
487 files.push(GeneratedFile {
488 path: "streaming.rs".into(),
489 content: streaming_content,
490 });
491 }
492
493 if self.config.enable_async_client {
495 let operations = self.client_operations(analysis, scopes.client_ids.as_ref());
496 let http_content =
497 self.generate_http_client_for_operations(analysis, &operations)?;
498 files.push(GeneratedFile {
499 path: "client.rs".into(),
500 content: http_content,
501 });
502 }
503 }
504
505 if self.config.enable_registry || self.config.registry_only {
507 let registry_content = self.generate_registry(analysis)?;
508 files.push(GeneratedFile {
509 path: "registry.rs".into(),
510 content: registry_content,
511 });
512 }
513
514 if !self.config.registry_only
518 && let Some(server) = self
519 .config
520 .server
521 .as_ref()
522 .filter(|server| !server.operations.is_empty())
523 {
524 let server_files =
525 crate::server::codegen::ServerCodegen::new(&self.config, analysis, server)
526 .with_source_provenance(self.source_provenance.as_deref())
527 .generate()
528 .map_err(|error| {
529 GeneratorError::CodeGenError(format!(
530 "server code generation failed: {error}"
531 ))
532 })?;
533 files.extend(server_files);
534 }
535
536 let mod_content = self.generate_mod_file(&files)?;
538 let mod_file = GeneratedFile {
539 path: "mod.rs".into(),
540 content: mod_content,
541 };
542
543 let required_deps = crate::type_mapping::collect_generated_dep_requirements(
544 files.iter().map(|file| file.content.as_str()),
545 self.config.enable_specta,
546 );
547
548 Ok(GenerationResult {
549 files,
550 mod_file,
551 required_deps,
552 pruned_schemas,
553 })
554 }
555
556 pub fn generate(&self, analysis: &mut SchemaAnalysis) -> Result<String> {
558 self.generate_types(analysis)
559 }
560
561 fn generate_types(&self, analysis: &mut SchemaAnalysis) -> Result<String> {
563 self.validate_schema_type_names(analysis)?;
564
565 let provenance_attribute = self.provenance_attribute();
566 let mut type_definitions = TokenStream::new();
567
568 let type_index = self.type_generation_index(analysis);
569 let type_context = TypeGenerationContext { index: &type_index };
570
571 let generation_order = analysis.dependencies.topological_sort()?;
573
574 let mut processed = std::collections::HashSet::new();
575
576 for schema_name in generation_order {
578 if let Some(schema) = analysis.schemas.get(&schema_name) {
579 let type_def = self.generate_type_definition(schema, analysis, &type_context)?;
580 if !type_def.is_empty() {
581 type_definitions.extend(type_def);
582 }
583 processed.insert(schema_name);
584 }
585 }
586
587 let mut remaining_schemas: Vec<_> = analysis
589 .schemas
590 .iter()
591 .filter(|(name, _)| !processed.contains(*name))
592 .collect();
593 remaining_schemas.sort_by_key(|(name, _)| name.as_str());
594
595 for (_schema_name, schema) in remaining_schemas {
596 let type_def = self.generate_type_definition(schema, analysis, &type_context)?;
597 if !type_def.is_empty() {
598 type_definitions.extend(type_def);
599 }
600 }
601
602 let mut uses_plain_tri_state = false;
603 let mut tri_state_codecs = std::collections::HashSet::new();
604 for (schema_name, schema) in &analysis.schemas {
605 let crate::analysis::SchemaType::Object {
606 properties,
607 required,
608 ..
609 } = &schema.schema_type
610 else {
611 continue;
612 };
613 for (field_name, property) in properties {
614 if !self.property_is_tri_state(
615 schema_name,
616 field_name,
617 property,
618 required.contains(field_name),
619 ) {
620 continue;
621 }
622 if let Some(codec) = self.schema_type_serde_codec(&property.schema_type, analysis) {
623 tri_state_codecs.insert(codec);
624 } else {
625 uses_plain_tri_state = true;
626 }
627 }
628 }
629
630 let base64_double_option = if tri_state_codecs.contains("base64_serde") {
635 quote! {
636 pub mod double_option {
637 use serde::{Deserializer, Serializer};
638
639 pub fn serialize<S: Serializer>(
640 value: &Option<Option<Vec<u8>>>,
641 ser: S,
642 ) -> Result<S::Ok, S::Error> {
643 match value {
644 Some(value) => super::option::serialize(value, ser),
645 None => ser.serialize_none(),
646 }
647 }
648
649 pub fn deserialize<'de, D: Deserializer<'de>>(
650 de: D,
651 ) -> Result<Option<Option<Vec<u8>>>, D::Error> {
652 super::option::deserialize(de).map(Some)
653 }
654 }
655 }
656 } else {
657 TokenStream::new()
658 };
659 let base64_helper = if analysis
660 .used_type_features
661 .contains(crate::type_mapping::TypeFeature::Base64)
662 {
663 let engine = match self.config.types.byte {
664 crate::type_mapping::ByteStrategy::Base64UrlUnpadded => {
665 quote::format_ident!("URL_SAFE_NO_PAD")
666 }
667 _ => quote::format_ident!("STANDARD"),
668 };
669 quote! {
670 mod base64_serde {
675 use base64::{Engine as _, engine::general_purpose::#engine as ENGINE};
676 use serde::{Deserialize, Deserializer, Serializer};
677
678 pub fn serialize<S: Serializer>(
679 bytes: &Vec<u8>,
680 ser: S,
681 ) -> Result<S::Ok, S::Error> {
682 ser.serialize_str(&ENGINE.encode(bytes))
683 }
684
685 pub fn deserialize<'de, D: Deserializer<'de>>(
686 de: D,
687 ) -> Result<Vec<u8>, D::Error> {
688 let s = String::deserialize(de)?;
689 ENGINE
690 .decode(s.as_bytes())
691 .map_err(serde::de::Error::custom)
692 }
693
694 pub mod option {
700 use super::*;
701 use serde::{Deserialize, Deserializer, Serializer};
702
703 pub fn serialize<S: Serializer>(
704 opt: &Option<Vec<u8>>,
705 ser: S,
706 ) -> Result<S::Ok, S::Error> {
707 match opt {
708 Some(bytes) => super::serialize(bytes, ser),
709 None => ser.serialize_none(),
710 }
711 }
712
713 pub fn deserialize<'de, D: Deserializer<'de>>(
714 de: D,
715 ) -> Result<Option<Vec<u8>>, D::Error> {
716 let opt = Option::<String>::deserialize(de)?;
717 opt.map(|s| {
718 ENGINE
719 .decode(s.as_bytes())
720 .map_err(serde::de::Error::custom)
721 })
722 .transpose()
723 }
724 }
725
726 #base64_double_option
727 }
728 }
729 } else {
730 TokenStream::new()
731 };
732
733 let uses_binary_bytes_codec = analysis
734 .schemas
735 .values()
736 .any(|schema| schema_type_uses_serde_codec(&schema.schema_type, "binary_bytes_serde"));
737 let binary_bytes_double_option = if tri_state_codecs.contains("binary_bytes_serde") {
738 quote! {
739 pub mod double_option {
740 use serde::{Deserializer, Serializer};
741
742 pub fn serialize<S: Serializer>(
743 value: &Option<Option<bytes::Bytes>>,
744 ser: S,
745 ) -> Result<S::Ok, S::Error> {
746 match value {
747 Some(value) => super::option::serialize(value, ser),
748 None => ser.serialize_none(),
749 }
750 }
751
752 pub fn deserialize<'de, D: Deserializer<'de>>(
753 de: D,
754 ) -> Result<Option<Option<bytes::Bytes>>, D::Error> {
755 super::option::deserialize(de).map(Some)
756 }
757 }
758 }
759 } else {
760 TokenStream::new()
761 };
762 let binary_bytes_helper = if uses_binary_bytes_codec {
763 quote! {
764 mod binary_bytes_serde {
768 use serde::{Deserialize, Deserializer, Serializer};
769
770 pub fn serialize<S: Serializer>(
771 bytes: &bytes::Bytes,
772 ser: S,
773 ) -> Result<S::Ok, S::Error> {
774 let value = std::str::from_utf8(bytes.as_ref())
775 .map_err(serde::ser::Error::custom)?;
776 ser.serialize_str(value)
777 }
778
779 pub fn deserialize<'de, D: Deserializer<'de>>(
780 de: D,
781 ) -> Result<bytes::Bytes, D::Error> {
782 String::deserialize(de).map(bytes::Bytes::from)
783 }
784
785 pub mod option {
786 use serde::{Deserialize, Deserializer, Serializer};
787
788 pub fn serialize<S: Serializer>(
789 value: &Option<bytes::Bytes>,
790 ser: S,
791 ) -> Result<S::Ok, S::Error> {
792 match value {
793 Some(bytes) => super::serialize(bytes, ser),
794 None => ser.serialize_none(),
795 }
796 }
797
798 pub fn deserialize<'de, D: Deserializer<'de>>(
799 de: D,
800 ) -> Result<Option<bytes::Bytes>, D::Error> {
801 Option::<String>::deserialize(de)
802 .map(|value| value.map(bytes::Bytes::from))
803 }
804 }
805
806 #binary_bytes_double_option
807 }
808 }
809 } else {
810 TokenStream::new()
811 };
812
813 let uses_binary_vec_codec = analysis
814 .schemas
815 .values()
816 .any(|schema| schema_type_uses_serde_codec(&schema.schema_type, "binary_vec_serde"));
817 let binary_vec_double_option = if tri_state_codecs.contains("binary_vec_serde") {
818 quote! {
819 pub mod double_option {
822 use serde::{Deserializer, Serializer};
823
824 pub fn serialize<S: Serializer>(
825 value: &Option<Option<Vec<u8>>>,
826 ser: S,
827 ) -> Result<S::Ok, S::Error> {
828 match value {
829 Some(value) => super::option::serialize(value, ser),
830 None => ser.serialize_none(),
831 }
832 }
833
834 pub fn deserialize<'de, D: Deserializer<'de>>(
835 de: D,
836 ) -> Result<Option<Option<Vec<u8>>>, D::Error> {
837 super::option::deserialize(de).map(Some)
838 }
839 }
840 }
841 } else {
842 TokenStream::new()
843 };
844 let binary_vec_helper = if uses_binary_vec_codec {
845 quote! {
846 mod binary_vec_serde {
849 use serde::{Deserialize, Deserializer, Serializer};
850
851 pub fn serialize<S: Serializer>(
852 bytes: &Vec<u8>,
853 ser: S,
854 ) -> Result<S::Ok, S::Error> {
855 let value = std::str::from_utf8(bytes)
856 .map_err(serde::ser::Error::custom)?;
857 ser.serialize_str(value)
858 }
859
860 pub fn deserialize<'de, D: Deserializer<'de>>(
861 de: D,
862 ) -> Result<Vec<u8>, D::Error> {
863 String::deserialize(de).map(String::into_bytes)
864 }
865
866 pub mod option {
867 use serde::{Deserialize, Deserializer, Serializer};
868
869 pub fn serialize<S: Serializer>(
870 value: &Option<Vec<u8>>,
871 ser: S,
872 ) -> Result<S::Ok, S::Error> {
873 match value {
874 Some(bytes) => super::serialize(bytes, ser),
875 None => ser.serialize_none(),
876 }
877 }
878
879 pub fn deserialize<'de, D: Deserializer<'de>>(
880 de: D,
881 ) -> Result<Option<Vec<u8>>, D::Error> {
882 Option::<String>::deserialize(de)
883 .map(|value| value.map(String::into_bytes))
884 }
885 }
886
887 #binary_vec_double_option
888 }
889 }
890 } else {
891 TokenStream::new()
892 };
893
894 let tri_state_helper = if uses_plain_tri_state {
895 quote! {
896 mod tri_state_serde {
900 use serde::{Deserialize, Deserializer};
901
902 pub fn deserialize<'de, D, T>(de: D) -> Result<Option<T>, D::Error>
903 where
904 D: Deserializer<'de>,
905 T: Deserialize<'de>,
906 {
907 T::deserialize(de).map(Some)
908 }
909 }
910 }
911 } else {
912 TokenStream::new()
913 };
914
915 let time_date_double_option = if tri_state_codecs.contains("time_date_format") {
922 quote! {
923 mod time_date_double_option {
924 use serde::{Deserializer, Serializer};
925
926 pub fn serialize<S: Serializer>(
927 value: &Option<Option<time::Date>>,
928 ser: S,
929 ) -> Result<S::Ok, S::Error> {
930 match value {
931 Some(value) => time_date_format::option::serialize(value, ser),
932 None => ser.serialize_none(),
933 }
934 }
935
936 pub fn deserialize<'de, D: Deserializer<'de>>(
937 de: D,
938 ) -> Result<Option<Option<time::Date>>, D::Error> {
939 time_date_format::option::deserialize(de).map(Some)
940 }
941 }
942 }
943 } else {
944 TokenStream::new()
945 };
946 let time_date_helper = if analysis
947 .used_type_features
948 .contains(crate::type_mapping::TypeFeature::TimeDate)
949 {
950 quote! {
951 time::serde::format_description!(
952 time_date_format,
953 Date,
954 "[year]-[month]-[day]"
955 );
956
957 #time_date_double_option
958 }
959 } else {
960 TokenStream::new()
961 };
962
963 let time_time_double_option = if tri_state_codecs.contains("time_time_format") {
968 quote! {
969 mod time_time_double_option {
970 use serde::{Deserializer, Serializer};
971
972 pub fn serialize<S: Serializer>(
973 value: &Option<Option<time::Time>>,
974 ser: S,
975 ) -> Result<S::Ok, S::Error> {
976 match value {
977 Some(value) => time_time_format::option::serialize(value, ser),
978 None => ser.serialize_none(),
979 }
980 }
981
982 pub fn deserialize<'de, D: Deserializer<'de>>(
983 de: D,
984 ) -> Result<Option<Option<time::Time>>, D::Error> {
985 time_time_format::option::deserialize(de).map(Some)
986 }
987 }
988 }
989 } else {
990 TokenStream::new()
991 };
992 let time_time_helper = if analysis
993 .used_type_features
994 .contains(crate::type_mapping::TypeFeature::TimeTime)
995 {
996 quote! {
997 time::serde::format_description!(
998 version = 2,
999 time_time_format,
1000 Time,
1001 "[hour]:[minute]:[second][optional [.[subsecond]]]"
1002 );
1003
1004 #time_time_double_option
1005 }
1006 } else {
1007 TokenStream::new()
1008 };
1009
1010 let time_rfc3339_double_option_helper = if tri_state_codecs.contains("time::serde::rfc3339")
1011 {
1012 quote! {
1013 mod time_rfc3339_double_option {
1014 use serde::{Deserializer, Serializer};
1015
1016 pub fn serialize<S: Serializer>(
1017 value: &Option<Option<time::OffsetDateTime>>,
1018 ser: S,
1019 ) -> Result<S::Ok, S::Error> {
1020 match value {
1021 Some(value) => time::serde::rfc3339::option::serialize(value, ser),
1022 None => ser.serialize_none(),
1023 }
1024 }
1025
1026 pub fn deserialize<'de, D: Deserializer<'de>>(
1027 de: D,
1028 ) -> Result<Option<Option<time::OffsetDateTime>>, D::Error> {
1029 time::serde::rfc3339::option::deserialize(de).map(Some)
1030 }
1031 }
1032 }
1033 } else {
1034 TokenStream::new()
1035 };
1036
1037 let generated = quote! {
1039 #provenance_attribute
1045
1046 #![allow(clippy::large_enum_variant)]
1047 #![allow(clippy::format_in_format_args)]
1048 #![allow(clippy::let_unit_value)]
1049 #![allow(unreachable_patterns)]
1050
1051 use serde::{Deserialize, Serialize};
1052
1053 #base64_helper
1054
1055 #binary_bytes_helper
1056
1057 #binary_vec_helper
1058
1059 #tri_state_helper
1060
1061 #time_date_helper
1062
1063 #time_time_helper
1064
1065 #time_rfc3339_double_option_helper
1066
1067 #type_definitions
1068 };
1069
1070 let syntax_tree = syn::parse2::<syn::File>(generated).map_err(|e| {
1072 GeneratorError::CodeGenError(format!("Failed to parse generated code: {e}"))
1073 })?;
1074
1075 let formatted = prettyplease::unparse(&syntax_tree);
1076
1077 Ok(formatted)
1078 }
1079
1080 fn generate_streaming_client(
1082 &self,
1083 streaming_config: &StreamingConfig,
1084 analysis: &SchemaAnalysis,
1085 ) -> Result<String> {
1086 let mut client_code = TokenStream::new();
1087 let provenance_attribute = self.provenance_attribute();
1088 let duration_import = streaming_config
1089 .reconnection_config
1090 .as_ref()
1091 .map(|_| quote! { use std::time::Duration; });
1092
1093 let imports = quote! {
1095 #provenance_attribute
1100 #![allow(clippy::format_in_format_args)]
1101 #![allow(clippy::let_unit_value)]
1102 #![allow(unused_mut)]
1103
1104 use super::types::*;
1105 use async_trait::async_trait;
1106 use futures_util::Stream;
1107 use std::pin::Pin;
1108 use reqwest::header::{HeaderMap, HeaderValue};
1109 use tracing::{debug, info, instrument};
1110 #duration_import
1111 };
1112 client_code.extend(imports);
1113
1114 if streaming_config.generate_client {
1115 if streaming_config.reconnection_config.is_some() {
1116 client_code.extend(quote! {
1117 use super::sse::{SseClient, SseReconnectOptions};
1118 pub use super::sse::StreamingError;
1119 });
1120 } else {
1121 client_code.extend(quote! {
1122 use super::sse::SseClient;
1123 pub use super::sse::StreamingError;
1124 });
1125 }
1126 }
1127
1128 for endpoint in &streaming_config.endpoints {
1130 let trait_code = self.generate_endpoint_trait(endpoint, analysis)?;
1131 client_code.extend(trait_code);
1132 }
1133
1134 if streaming_config.generate_client {
1136 let client_impl = self.generate_streaming_client_impl(streaming_config, analysis)?;
1137 client_code.extend(client_impl);
1138 }
1139
1140 if let Some(reconnect_config) = &streaming_config.reconnection_config {
1142 let reconnect_code = self.generate_reconnection_utilities(reconnect_config)?;
1143 client_code.extend(reconnect_code);
1144 }
1145
1146 let syntax_tree = syn::parse2::<syn::File>(client_code).map_err(|e| {
1147 GeneratorError::CodeGenError(format!("Failed to parse streaming client code: {e}"))
1148 })?;
1149
1150 Ok(prettyplease::unparse(&syntax_tree))
1151 }
1152
1153 fn validate_schema_type_names(&self, analysis: &SchemaAnalysis) -> Result<()> {
1154 let mut source_by_rust_name = BTreeMap::<String, String>::new();
1155
1156 for schema in analysis.schemas.values() {
1157 let rust_name = self.to_rust_type_name(&schema.name);
1158 if let Some(first) = source_by_rust_name.insert(rust_name.clone(), schema.name.clone())
1159 {
1160 return Err(GeneratorError::InvalidSchema(format!(
1161 "schema names `{first}` and `{}` both map to Rust type `{rust_name}`",
1162 schema.name
1163 )));
1164 }
1165 }
1166
1167 Ok(())
1168 }
1169
1170 pub fn generate_http_client(&self, analysis: &SchemaAnalysis) -> Result<String> {
1176 let client_ids = self.resolve_client_operation_ids(analysis)?;
1177 let operations = self.client_operations(analysis, client_ids.as_ref());
1178 self.generate_http_client_for_operations(analysis, &operations)
1179 }
1180
1181 fn generate_http_client_for_operations(
1182 &self,
1183 analysis: &SchemaAnalysis,
1184 operations: &[&crate::analysis::OperationInfo],
1185 ) -> Result<String> {
1186 let provenance_attribute = self.provenance_attribute();
1187 let error_types = self.generate_http_error_types();
1188 let client_struct = self.generate_http_client_struct();
1189 let operation_methods = self.generate_operation_methods_for(analysis, operations);
1190
1191 let generated = quote! {
1192 #provenance_attribute
1197 #![allow(clippy::format_in_format_args)]
1198 #![allow(clippy::let_unit_value)]
1199
1200 use super::types::*;
1201
1202 #error_types
1203
1204 #client_struct
1205
1206 #operation_methods
1207 };
1208
1209 let syntax_tree = syn::parse2::<syn::File>(generated.clone()).map_err(|e| {
1210 if let Ok(dump) = std::env::var("OATR_DUMP_TOKENS_ON_PARSE_ERROR") {
1211 let _ = std::fs::write(&dump, generated.to_string());
1212 }
1213 GeneratorError::CodeGenError(format!("Failed to parse HTTP client code: {e}"))
1214 })?;
1215
1216 Ok(prettyplease::unparse(&syntax_tree))
1217 }
1218
1219 fn resolve_operation_scopes(&self, analysis: &SchemaAnalysis) -> Result<OperationScopes> {
1220 let client_ids = if self.config.enable_async_client && !self.config.registry_only {
1221 self.resolve_client_operation_ids(analysis)?
1222 } else {
1223 None
1224 };
1225
1226 let server_ids = match &self.config.server {
1227 Some(server) if !server.operations.is_empty() => {
1228 crate::server::resolve_operation_selectors(&server.operations, analysis)
1229 .map_err(|error| {
1230 GeneratorError::ValidationError(format!(
1231 "Invalid [server].operations: {error}"
1232 ))
1233 })?
1234 .operations
1235 .into_iter()
1236 .map(|operation| operation.operation_id)
1237 .collect()
1238 }
1239 _ => Default::default(),
1240 };
1241
1242 let streaming_ids = if self.config.registry_only || !self.config.enable_sse_client {
1243 Default::default()
1244 } else if let Some(streaming) = &self.config.streaming_config {
1245 let mut ids = std::collections::BTreeSet::new();
1246 for (index, endpoint) in streaming.endpoints.iter().enumerate() {
1247 let resolution =
1248 crate::server::resolve_operation_id(&endpoint.operation_id, analysis).map_err(
1249 |error| {
1250 GeneratorError::ValidationError(format!(
1251 "Invalid [streaming].endpoints[{index}].operation_id: {error}"
1252 ))
1253 },
1254 )?;
1255 ids.extend(
1256 resolution
1257 .operations
1258 .into_iter()
1259 .map(|operation| operation.operation_id),
1260 );
1261 }
1262 ids
1263 } else {
1264 Default::default()
1265 };
1266
1267 let client_prunes = self.config.enable_async_client
1268 && !self.config.registry_only
1269 && self
1270 .config
1271 .client
1272 .as_ref()
1273 .is_some_and(|client| client.prune_models);
1274 let server_prunes = self
1275 .config
1276 .server
1277 .as_ref()
1278 .is_some_and(|server| server.prune_models && !server.operations.is_empty());
1279 let extra_schema_roots = if self.config.registry_only || !self.config.enable_sse_client {
1280 Vec::new()
1281 } else {
1282 self.config
1283 .streaming_config
1284 .as_ref()
1285 .map(|streaming| {
1286 streaming
1287 .endpoints
1288 .iter()
1289 .map(|endpoint| endpoint.event_union_type.clone())
1290 .collect()
1291 })
1292 .unwrap_or_default()
1293 };
1294
1295 Ok(OperationScopes {
1296 client_ids,
1297 server_ids,
1298 streaming_ids,
1299 prune_models: client_prunes || server_prunes,
1300 extra_schema_roots,
1301 })
1302 }
1303
1304 fn resolve_client_operation_ids(
1305 &self,
1306 analysis: &SchemaAnalysis,
1307 ) -> Result<Option<std::collections::BTreeSet<String>>> {
1308 match &self.config.client {
1309 Some(client) if !client.operations.is_empty() => {
1310 let resolution =
1311 crate::server::resolve_operation_selectors(&client.operations, analysis)
1312 .map_err(|error| {
1313 GeneratorError::ValidationError(format!(
1314 "Invalid [client].operations: {error}"
1315 ))
1316 })?;
1317 Ok(Some(
1318 resolution
1319 .operations
1320 .into_iter()
1321 .map(|operation| operation.operation_id)
1322 .collect(),
1323 ))
1324 }
1325 _ => Ok(None),
1326 }
1327 }
1328
1329 fn client_operations<'a>(
1330 &self,
1331 analysis: &'a SchemaAnalysis,
1332 selected: Option<&std::collections::BTreeSet<String>>,
1333 ) -> Vec<&'a crate::analysis::OperationInfo> {
1334 analysis
1335 .operations
1336 .iter()
1337 .filter(|(operation_id, _)| selected.is_none_or(|ids| ids.contains(*operation_id)))
1338 .map(|(_, operation)| operation)
1339 .collect()
1340 }
1341
1342 fn prune_models_to_scopes(
1343 &self,
1344 analysis: &mut SchemaAnalysis,
1345 scopes: &OperationScopes,
1346 ) -> usize {
1347 if !scopes.prune_models {
1348 return 0;
1349 }
1350
1351 let mut consumer_ids = scopes.server_ids.clone();
1352 if self.config.enable_async_client && !self.config.registry_only {
1353 match &scopes.client_ids {
1354 Some(ids) => consumer_ids.extend(ids.iter().cloned()),
1355 None => consumer_ids.extend(analysis.operations.keys().cloned()),
1356 }
1357 }
1358 consumer_ids.extend(scopes.streaming_ids.iter().cloned());
1359
1360 let operations: Vec<&crate::analysis::OperationInfo> = consumer_ids
1361 .iter()
1362 .filter_map(|operation_id| analysis.operations.get(operation_id))
1363 .collect();
1364 let keep = crate::server::codegen::reachable_schemas_with_roots(
1365 analysis,
1366 &operations,
1367 &scopes.extra_schema_roots,
1368 );
1369 let before = analysis.schemas.len();
1370 analysis.schemas.retain(|name, _| keep.contains(name));
1371 before - analysis.schemas.len()
1372 }
1373
1374 fn generate_http_error_types(&self) -> TokenStream {
1376 quote! {
1377 use thiserror::Error;
1378
1379 pub mod openapi_to_rust_problem {
1382 #[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
1383 pub struct ProblemDetails {
1384 #[serde(rename = "type")]
1385 pub type_uri: String,
1386 pub title: String,
1387 pub status: u16,
1388 pub code: String,
1389 #[serde(default)]
1390 pub errors: Vec<InvalidParameter>,
1391 #[serde(default, skip_serializing_if = "Option::is_none")]
1392 pub detail: Option<String>,
1393 #[serde(default, skip_serializing_if = "Option::is_none")]
1394 pub instance: Option<String>,
1395 }
1396
1397 #[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
1398 pub struct InvalidParameter {
1399 pub code: String,
1400 pub location: String,
1401 pub message: String,
1402 }
1403 }
1404
1405 #[derive(Error, Debug)]
1413 pub enum HttpError {
1414 #[error("Network error: {0}")]
1416 Network(#[from] reqwest::Error),
1417
1418 #[error("Middleware error: {0}")]
1420 Middleware(#[from] reqwest_middleware::Error),
1421
1422 #[error("Failed to serialize request: {0}")]
1424 Serialization(String),
1425
1426 #[error("Authentication error: {0}")]
1428 Auth(String),
1429
1430 #[error("Request timeout")]
1432 Timeout,
1433
1434 #[error("Response body exceeded configured limit of {limit} bytes")]
1436 ResponseTooLarge { limit: usize },
1437
1438 #[error("Configuration error: {0}")]
1440 Config(String),
1441
1442 #[error("{0}")]
1444 Other(String),
1445 }
1446
1447 impl HttpError {
1448 pub fn serialization_error(error: impl std::fmt::Display) -> Self {
1450 Self::Serialization(error.to_string())
1451 }
1452
1453 pub fn is_retryable(&self) -> bool {
1455 matches!(self, Self::Network(_) | Self::Middleware(_) | Self::Timeout)
1456 }
1457 }
1458
1459 #[derive(Debug, Clone)]
1471 pub struct ApiError<E> {
1472 pub status: u16,
1473 pub headers: reqwest::header::HeaderMap,
1474 pub body: String,
1475 pub raw_body: Vec<u8>,
1477 pub typed: Option<E>,
1478 pub parse_error: Option<String>,
1479 }
1480
1481 const API_ERROR_BODY_DISPLAY_LIMIT: usize = 500;
1482 const API_ERROR_BODY_TRUNCATION_MARKER: &str = "... [truncated]";
1483
1484 fn display_api_error_body(body: &str) -> std::borrow::Cow<'_, str> {
1485 let Some((end, _)) = body.char_indices().nth(API_ERROR_BODY_DISPLAY_LIMIT) else {
1486 return std::borrow::Cow::Borrowed(body);
1487 };
1488
1489 let mut displayed =
1490 String::with_capacity(end + API_ERROR_BODY_TRUNCATION_MARKER.len());
1491 displayed.push_str(&body[..end]);
1492 displayed.push_str(API_ERROR_BODY_TRUNCATION_MARKER);
1493 std::borrow::Cow::Owned(displayed)
1494 }
1495
1496 impl<E> ApiError<E> {
1497 pub fn is_client_error(&self) -> bool {
1498 (400..500).contains(&self.status)
1499 }
1500
1501 pub fn is_server_error(&self) -> bool {
1502 (500..600).contains(&self.status)
1503 }
1504
1505 pub fn is_retryable(&self) -> bool {
1508 matches!(self.status, 429 | 500 | 502 | 503 | 504)
1509 }
1510
1511 pub fn problem_details(
1523 &self,
1524 ) -> Option<openapi_to_rust_problem::ProblemDetails> {
1525 let content_type = self
1526 .headers
1527 .get(reqwest::header::CONTENT_TYPE)?
1528 .to_str()
1529 .ok()?;
1530 let media_type = content_type.split(';').next()?.trim();
1531 if !media_type.eq_ignore_ascii_case("application/problem+json") {
1532 return None;
1533 }
1534 serde_json::from_str(&self.body).ok()
1535 }
1536 }
1537
1538 impl<E: std::fmt::Debug> std::fmt::Display for ApiError<E> {
1539 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1540 write!(
1541 f,
1542 "API error {}: {}",
1543 self.status,
1544 display_api_error_body(&self.body)
1545 )?;
1546
1547 if let Some(typed) = &self.typed {
1548 write!(f, "; typed: {typed:?}")?;
1549 }
1550
1551 if let Some(parse_error) = &self.parse_error {
1552 write!(f, "; parse error: {parse_error}")?;
1553 }
1554
1555 Ok(())
1556 }
1557 }
1558
1559 impl<E: std::fmt::Debug> std::error::Error for ApiError<E> {}
1560
1561 #[derive(Debug, Error)]
1569 pub enum ApiOpError<E: std::fmt::Debug> {
1570 #[error(transparent)]
1571 Transport(#[from] HttpError),
1572
1573 #[error(transparent)]
1574 Api(ApiError<E>),
1575 }
1576
1577 impl<E: std::fmt::Debug> ApiOpError<E> {
1578 pub fn api(&self) -> Option<&ApiError<E>> {
1580 match self {
1581 Self::Api(e) => Some(e),
1582 Self::Transport(_) => None,
1583 }
1584 }
1585
1586 pub fn is_api_error(&self) -> bool {
1589 matches!(self, Self::Api(_))
1590 }
1591 }
1592
1593 impl<E: std::fmt::Debug> From<reqwest::Error> for ApiOpError<E> {
1596 fn from(e: reqwest::Error) -> Self {
1597 Self::Transport(HttpError::Network(e))
1598 }
1599 }
1600
1601 impl<E: std::fmt::Debug> From<reqwest_middleware::Error> for ApiOpError<E> {
1602 fn from(e: reqwest_middleware::Error) -> Self {
1603 Self::Transport(HttpError::Middleware(e))
1604 }
1605 }
1606
1607 pub type HttpResult<T> = Result<T, HttpError>;
1611 }
1612 }
1613
1614 fn generate_mod_file(&self, files: &[GeneratedFile]) -> Result<String> {
1616 let mut module_names = std::collections::BTreeSet::new();
1617
1618 for file in files {
1619 let module_name = if file.path.components().count() > 1 {
1620 file.path.iter().next().and_then(|part| part.to_str())
1621 } else {
1622 file.path.file_stem().and_then(|stem| stem.to_str())
1623 };
1624 if let Some(module_name) = module_name.filter(|name| *name != "mod") {
1625 module_names.insert(module_name.to_string());
1626 }
1627 }
1628 let module_declarations = module_names
1629 .iter()
1630 .map(|name| format!("pub mod {name};"))
1631 .collect::<Vec<_>>();
1632 let pub_uses = module_names
1633 .iter()
1634 .filter(|name| name.as_str() != "sse")
1637 .map(|name| format!("pub use {name}::*;"))
1638 .collect::<Vec<_>>();
1639
1640 let mount_hint = format!(
1647 "//! Configured `module_name` = `{name}`. Mount this tree under your\n\
1648 //! preferred path, e.g. `pub mod {name};` in your crate root.\n",
1649 name = self.config.module_name,
1650 );
1651 let source_hint = self
1652 .source_provenance
1653 .as_ref()
1654 .map(|source| {
1655 format!(
1656 "//! Generated by openapi-to-rust v{}. Source OpenAPI document: {source}\n",
1657 env!("CARGO_PKG_VERSION")
1658 )
1659 })
1660 .unwrap_or_default();
1661
1662 let content = format!(
1663 r#"//! Generated API modules
1664//!
1665//! This module exports all generated API types and clients.
1666//! Do not edit manually - regenerate using the appropriate script.
1667//!
1668{source_hint}
1669{mount_hint}
1670#![allow(unused_imports)]
1671
1672{decls}
1673
1674{uses}
1675"#,
1676 mount_hint = mount_hint,
1677 source_hint = source_hint,
1678 decls = module_declarations.join("\n"),
1679 uses = pub_uses.join("\n"),
1680 );
1681
1682 Ok(content)
1683 }
1684
1685 pub fn output_artifacts(
1687 &self,
1688 result: &GenerationResult,
1689 ) -> std::collections::BTreeMap<PathBuf, String> {
1690 let mut artifacts = std::collections::BTreeMap::new();
1691 for file in &result.files {
1692 artifacts.insert(file.path.clone(), file.content.clone());
1693 }
1694 artifacts.insert(
1695 result.mod_file.path.clone(),
1696 result.mod_file.content.clone(),
1697 );
1698 if let Some(mut fragment) =
1699 crate::type_mapping::render_required_deps_toml(&result.required_deps)
1700 {
1701 if let Some(source) = &self.source_provenance {
1702 let header = format!(
1703 "# Generated by openapi-to-rust v{}. Source OpenAPI document: {source}",
1704 env!("CARGO_PKG_VERSION")
1705 );
1706 fragment = fragment.replacen("# Generated by openapi-to-rust.", &header, 1);
1707 }
1708 artifacts.insert(PathBuf::from("REQUIRED_DEPS.toml"), fragment);
1709 }
1710 artifacts
1711 }
1712
1713 pub fn write_files(&self, result: &GenerationResult) -> Result<()> {
1716 use std::fs;
1717
1718 fs::create_dir_all(&self.config.output_dir)?;
1720
1721 let artifacts = self.output_artifacts(result);
1722 for (relative, content) in &artifacts {
1723 let file_path = self.config.output_dir.join(relative);
1724 if let Some(parent) = file_path.parent() {
1725 fs::create_dir_all(parent)?;
1726 }
1727 fs::write(&file_path, content)?;
1728 }
1729
1730 let deps_path = self.config.output_dir.join("REQUIRED_DEPS.toml");
1731 if !artifacts.contains_key(std::path::Path::new("REQUIRED_DEPS.toml")) && deps_path.exists()
1732 {
1733 fs::remove_file(&deps_path)?;
1734 }
1735
1736 Ok(())
1737 }
1738
1739 fn generate_type_definition(
1740 &self,
1741 schema: &crate::analysis::AnalyzedSchema,
1742 analysis: &crate::analysis::SchemaAnalysis,
1743 type_context: &TypeGenerationContext<'_>,
1744 ) -> Result<TokenStream> {
1745 use crate::analysis::SchemaType;
1746
1747 match &schema.schema_type {
1748 SchemaType::Primitive { rust_type, .. } => {
1749 self.generate_type_alias(schema, rust_type)
1751 }
1752 SchemaType::StringEnum { values } => {
1753 let ext = analysis.enum_extensions.get(&schema.name);
1754 let rust_name = self.to_rust_type_name(&schema.name);
1761 let force_extensible = self
1762 .config
1763 .extensible_enum_overrides
1764 .get(&schema.name)
1765 .or_else(|| self.config.extensible_enum_overrides.get(&rust_name))
1766 .copied()
1767 .unwrap_or(false);
1768 if force_extensible {
1769 self.generate_extensible_enum(schema, values, ext)
1770 } else {
1771 self.generate_string_enum(schema, values, ext)
1772 }
1773 }
1774 SchemaType::ExtensibleEnum { known_values } => {
1775 let ext = analysis.enum_extensions.get(&schema.name);
1776 self.generate_extensible_enum(schema, known_values, ext)
1777 }
1778 SchemaType::Object {
1779 properties,
1780 required,
1781 additional_properties,
1782 variant,
1783 } => self.generate_struct(
1784 schema,
1785 ObjectShape {
1786 properties,
1787 required,
1788 additional_properties,
1789 variant: variant.as_ref(),
1790 },
1791 analysis,
1792 type_context,
1793 ),
1794 SchemaType::DiscriminatedUnion {
1795 discriminator_field,
1796 variants,
1797 exclusive,
1798 } => {
1799 if self.should_use_untagged_discriminated_union(schema, analysis) {
1801 let schema_refs: Vec<crate::analysis::SchemaRef> = variants
1803 .iter()
1804 .map(|v| crate::analysis::SchemaRef {
1805 target: v.type_name.clone(),
1806 nullable: false,
1807 })
1808 .collect();
1809 self.generate_union_enum(schema, &schema_refs, *exclusive, analysis)
1810 } else {
1811 self.generate_discriminated_enum(
1812 schema,
1813 discriminator_field,
1814 variants,
1815 *exclusive,
1816 analysis,
1817 )
1818 }
1819 }
1820 SchemaType::Union {
1821 variants,
1822 exclusive,
1823 } => self.generate_union_enum(schema, variants, *exclusive, analysis),
1824 SchemaType::Reference { target } => {
1825 if schema.name != *target {
1828 let alias_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1830 let target_type = format_ident!("{}", self.to_rust_type_name(target));
1831
1832 let doc_comment = if let Some(desc) = &schema.description {
1833 quote! { #[doc = #desc] }
1834 } else {
1835 TokenStream::new()
1836 };
1837
1838 Ok(quote! {
1839 #doc_comment
1840 pub type #alias_name = #target_type;
1841 })
1842 } else {
1843 Ok(TokenStream::new())
1845 }
1846 }
1847 SchemaType::Untyped { shape, .. } => {
1848 self.generate_type_alias(schema, untyped_rust_type(*shape))
1849 }
1850 SchemaType::Tuple { element_types } => {
1851 let tuple_type = self.generate_tuple_type(element_types, analysis);
1852 let type_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1853 let doc_comment = if let Some(description) = &schema.description {
1854 let sanitized = self.sanitize_doc_comment(description);
1855 quote! { #[doc = #sanitized] }
1856 } else {
1857 TokenStream::new()
1858 };
1859 Ok(quote! {
1860 #doc_comment
1861 pub type #type_name = #tuple_type;
1862 })
1863 }
1864 SchemaType::Array { item_type } => {
1865 let array_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1868
1869 let inner_type = self.generate_array_item_type(item_type, analysis);
1870
1871 let doc_comment = if let Some(desc) = &schema.description {
1872 quote! { #[doc = #desc] }
1873 } else {
1874 TokenStream::new()
1875 };
1876
1877 Ok(quote! {
1878 #doc_comment
1879 pub type #array_name = Vec<#inner_type>;
1880 })
1881 }
1882 SchemaType::Nullable { inner_type } => {
1883 let nullable_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1884 let inner_type = self.generate_array_item_type(inner_type, analysis);
1885 Ok(quote! {
1886 pub type #nullable_name = Option<#inner_type>;
1887 })
1888 }
1889 SchemaType::Composition { schemas } => {
1890 self.generate_composition_struct(schema, schemas)
1891 }
1892 }
1893 }
1894
1895 fn generate_type_alias(
1896 &self,
1897 schema: &crate::analysis::AnalyzedSchema,
1898 rust_type: &str,
1899 ) -> Result<TokenStream> {
1900 let type_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1901 let base_type = parse_rust_type(rust_type)?;
1905
1906 let doc_comment = if let Some(desc) = &schema.description {
1907 let sanitized_desc = self.sanitize_doc_comment(desc);
1908 quote! { #[doc = #sanitized_desc] }
1909 } else {
1910 TokenStream::new()
1911 };
1912
1913 Ok(quote! {
1914 #doc_comment
1915 pub type #type_name = #base_type;
1916 })
1917 }
1918
1919 fn generate_extensible_enum(
1920 &self,
1921 schema: &crate::analysis::AnalyzedSchema,
1922 known_values: &[String],
1923 ext: Option<&crate::analysis::EnumExtensions>,
1924 ) -> Result<TokenStream> {
1925 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1926
1927 let doc_comment = if let Some(desc) = &schema.description {
1928 quote! { #[doc = #desc] }
1929 } else {
1930 TokenStream::new()
1931 };
1932
1933 let varnames_override: Option<&Vec<String>> = ext
1937 .filter(|_| self.config.types.x_enum_varnames_enabled())
1938 .map(|e| &e.varnames)
1939 .filter(|v| !v.is_empty() && v.len() == known_values.len());
1940 let descriptions_override: Option<&Vec<String>> = ext
1941 .filter(|_| self.config.types.x_enum_descriptions_enabled())
1942 .map(|e| &e.descriptions)
1943 .filter(|v| !v.is_empty() && v.len() == known_values.len());
1944
1945 let variant_ident_for = |index: usize, value: &str| -> proc_macro2::Ident {
1946 let name = match varnames_override {
1947 Some(v) => v[index].clone(),
1948 None => self.to_rust_enum_variant(value),
1949 };
1950 format_ident!("{}", name)
1951 };
1952
1953 let known_variants = known_values.iter().enumerate().map(|(i, value)| {
1958 let variant_ident = variant_ident_for(i, value);
1959 let doc = descriptions_override
1960 .map(|d| {
1961 let s = self.sanitize_doc_comment(&d[i]);
1962 quote! { #[doc = #s] }
1963 })
1964 .unwrap_or_default();
1965 quote! {
1966 #doc
1967 #variant_ident,
1968 }
1969 });
1970
1971 let match_arms_de = known_values.iter().enumerate().map(|(i, value)| {
1972 let variant_ident = variant_ident_for(i, value);
1973 quote! {
1974 #value => Ok(#enum_name::#variant_ident),
1975 }
1976 });
1977
1978 let match_arms_ser = known_values.iter().enumerate().map(|(i, value)| {
1979 let variant_ident = variant_ident_for(i, value);
1980 quote! {
1981 #enum_name::#variant_ident => #value,
1982 }
1983 });
1984
1985 let derives = if self.config.enable_specta {
1986 quote! {
1987 #[derive(Debug, Clone, PartialEq, Eq)]
1988 #[cfg_attr(feature = "specta", derive(specta::Type))]
1989 }
1990 } else {
1991 quote! {
1992 #[derive(Debug, Clone, PartialEq, Eq)]
1993 }
1994 };
1995
1996 Ok(quote! {
1997 #doc_comment
1998 #derives
1999 pub enum #enum_name {
2000 #(#known_variants)*
2001 Custom(String),
2003 }
2004
2005 impl<'de> serde::Deserialize<'de> for #enum_name {
2006 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2007 where
2008 D: serde::Deserializer<'de>,
2009 {
2010 let value = String::deserialize(deserializer)?;
2011 match value.as_str() {
2012 #(#match_arms_de)*
2013 _ => Ok(#enum_name::Custom(value)),
2014 }
2015 }
2016 }
2017
2018 impl serde::Serialize for #enum_name {
2019 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2020 where
2021 S: serde::Serializer,
2022 {
2023 serializer.serialize_str(self.as_str())
2024 }
2025 }
2026
2027 impl #enum_name {
2028 pub fn as_str(&self) -> &str {
2029 match self {
2030 #(#match_arms_ser)*
2031 #enum_name::Custom(s) => s.as_str(),
2032 }
2033 }
2034 }
2035
2036 impl ::std::fmt::Display for #enum_name {
2037 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
2038 f.write_str(self.as_str())
2039 }
2040 }
2041
2042 impl AsRef<str> for #enum_name {
2043 fn as_ref(&self) -> &str {
2044 self.as_str()
2045 }
2046 }
2047 })
2048 }
2049
2050 fn generate_string_enum(
2051 &self,
2052 schema: &crate::analysis::AnalyzedSchema,
2053 values: &[String],
2054 ext: Option<&crate::analysis::EnumExtensions>,
2055 ) -> Result<TokenStream> {
2056 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2057
2058 let default_value = schema
2065 .default
2066 .as_ref()
2067 .and_then(|v| v.as_str())
2068 .map(|s| s.to_string());
2069 let has_default_match = match &default_value {
2070 Some(d) => values.iter().any(|v| v == d),
2071 None => !values.is_empty(),
2072 };
2073
2074 let varnames_override: Option<&Vec<String>> = ext
2078 .filter(|_| self.config.types.x_enum_varnames_enabled())
2079 .map(|e| &e.varnames)
2080 .filter(|v| !v.is_empty() && v.len() == values.len());
2081 let descriptions_override: Option<&Vec<String>> = ext
2082 .filter(|_| self.config.types.x_enum_descriptions_enabled())
2083 .map(|e| &e.descriptions)
2084 .filter(|v| !v.is_empty() && v.len() == values.len());
2085
2086 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
2093 let variant_pairs: Vec<(syn::Ident, &String, bool, Option<String>)> = values
2094 .iter()
2095 .enumerate()
2096 .map(|(i, value)| {
2097 let base = match varnames_override {
2098 Some(v) => v[i].clone(),
2099 None => self.to_rust_enum_variant(value),
2100 };
2101 let mut variant_name = base.clone();
2102 let mut suffix = 2;
2103 while !used.insert(variant_name.clone()) {
2104 variant_name = format!("{base}_{suffix}");
2105 suffix += 1;
2106 }
2107 let variant_ident = format_ident!("{}", variant_name);
2108 let is_default = if let Some(ref default) = default_value {
2109 value == default
2110 } else {
2111 i == 0
2112 };
2113 let description = descriptions_override.map(|d| d[i].clone());
2114 (variant_ident, value, is_default, description)
2115 })
2116 .collect();
2117
2118 let variants =
2119 variant_pairs
2120 .iter()
2121 .map(|(variant_ident, value, is_default, description)| {
2122 let doc = description
2123 .as_ref()
2124 .map(|d| {
2125 let s = self.sanitize_doc_comment(d);
2126 quote! { #[doc = #s] }
2127 })
2128 .unwrap_or_default();
2129 if *is_default {
2130 quote! {
2131 #doc
2132 #[default]
2133 #[serde(rename = #value)]
2134 #variant_ident,
2135 }
2136 } else {
2137 quote! {
2138 #doc
2139 #[serde(rename = #value)]
2140 #variant_ident,
2141 }
2142 }
2143 });
2144
2145 let as_str_arms = variant_pairs.iter().map(|(variant_ident, value, _, _)| {
2149 quote! { Self::#variant_ident => #value, }
2150 });
2151
2152 let doc_comment = if let Some(desc) = &schema.description {
2153 quote! { #[doc = #desc] }
2154 } else {
2155 TokenStream::new()
2156 };
2157
2158 let derives = match (self.config.enable_specta, has_default_match) {
2161 (true, true) => quote! {
2162 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
2163 #[cfg_attr(feature = "specta", derive(specta::Type))]
2164 },
2165 (true, false) => quote! {
2166 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
2167 #[cfg_attr(feature = "specta", derive(specta::Type))]
2168 },
2169 (false, true) => quote! {
2170 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
2171 },
2172 (false, false) => quote! {
2173 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
2174 },
2175 };
2176
2177 Ok(quote! {
2178 #doc_comment
2179 #derives
2180 pub enum #enum_name {
2181 #(#variants)*
2182 }
2183
2184 impl #enum_name {
2185 pub fn as_str(&self) -> &'static str {
2186 match self {
2187 #(#as_str_arms)*
2188 }
2189 }
2190 }
2191
2192 impl ::std::fmt::Display for #enum_name {
2193 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
2194 f.write_str(self.as_str())
2195 }
2196 }
2197
2198 impl AsRef<str> for #enum_name {
2199 fn as_ref(&self) -> &str {
2200 self.as_str()
2201 }
2202 }
2203 })
2204 }
2205
2206 fn variant_field_name(
2209 &self,
2210 properties: &BTreeMap<String, crate::analysis::PropertyInfo>,
2211 ) -> String {
2212 let taken = |candidate: &str| {
2213 properties
2214 .keys()
2215 .any(|name| self.to_rust_field_name(name) == candidate)
2216 };
2217 if !taken("variant") {
2218 return "variant".to_string();
2219 }
2220 let mut suffix = 2;
2221 loop {
2222 let candidate = format!("variant{suffix}");
2223 if !taken(&candidate) {
2224 return candidate;
2225 }
2226 suffix += 1;
2227 }
2228 }
2229
2230 fn generate_struct(
2231 &self,
2232 schema: &crate::analysis::AnalyzedSchema,
2233 object: ObjectShape<'_>,
2234 analysis: &crate::analysis::SchemaAnalysis,
2235 type_context: &TypeGenerationContext<'_>,
2236 ) -> Result<TokenStream> {
2237 let ObjectShape {
2238 properties,
2239 required,
2240 additional_properties,
2241 variant,
2242 } = object;
2243 let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2244 let emitted_properties = self.emitted_object_properties(
2245 &schema.name,
2246 properties,
2247 required,
2248 additional_properties,
2249 analysis,
2250 );
2251 let variant_field_ident =
2252 variant.map(|_| format_ident!("{}", self.variant_field_name(properties)));
2253
2254 let mut fields: Vec<TokenStream> = emitted_properties
2255 .iter()
2256 .map(|emitted| {
2257 let field_name = emitted.wire_name;
2258 let property = emitted.property;
2259 let field_ident = &emitted.ident;
2260 let field_type = &emitted.field_type;
2261 let serde_attrs = if variant.is_none() {
2262 self.generate_serde_field_attrs(
2263 &schema.name,
2264 field_name,
2265 field_ident,
2266 property,
2267 emitted.is_required,
2268 analysis,
2269 )
2270 } else {
2271 TokenStream::new()
2272 };
2273 let specta_attrs = self.generate_specta_field_attrs(field_name);
2274
2275 let doc_comment = if let Some(desc) = &property.description {
2276 let sanitized_desc = self.sanitize_doc_comment(desc);
2277 quote! { #[doc = #sanitized_desc] }
2278 } else {
2279 TokenStream::new()
2280 };
2281 let constraint_doc = self.generate_constraint_doc(&property.constraints);
2282
2283 quote! {
2284 #doc_comment
2285 #constraint_doc
2286 #serde_attrs
2287 #specta_attrs
2288 pub #field_ident: #field_type,
2289 }
2290 })
2291 .collect();
2292
2293 match additional_properties {
2299 crate::analysis::ObjectAdditionalProperties::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<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, 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<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error>;
5088 }
5089 }
5090 };
5091
5092 Ok(quote! {
5093 #[async_trait]
5095 pub trait #trait_name {
5096 type Error: std::error::Error + Send + Sync + 'static;
5097
5098 #method_signature
5100 }
5101 })
5102 }
5103
5104 fn generate_streaming_client_impl(
5106 &self,
5107 streaming_config: &crate::streaming::StreamingConfig,
5108 analysis: &SchemaAnalysis,
5109 ) -> Result<TokenStream> {
5110 let client_name = format_ident!(
5111 "{}Client",
5112 self.to_rust_type_name(&streaming_config.client_module_name)
5113 );
5114
5115 let mut struct_fields = vec![
5118 quote! { base_url: String },
5119 quote! { api_key: Option<String> },
5120 quote! { sse_client: SseClient },
5121 quote! { custom_headers: std::collections::BTreeMap<String, String> },
5122 ];
5123
5124 let has_optional_headers = !streaming_config
5125 .endpoints
5126 .iter()
5127 .all(|e| e.optional_headers.is_empty());
5128
5129 if has_optional_headers {
5130 struct_fields
5131 .push(quote! { optional_headers: std::collections::BTreeMap<String, String> });
5132 }
5133
5134 let default_base_url = if let Some(ref streaming_config) = self.config.streaming_config {
5137 streaming_config
5138 .endpoints
5139 .first()
5140 .and_then(|e| e.base_url.as_deref())
5141 .unwrap_or("https://api.example.com")
5142 } else {
5143 "https://api.example.com"
5144 };
5145 let max_response_body_bytes = self
5146 .config()
5147 .http_client_config
5148 .as_ref()
5149 .and_then(|http| http.max_response_body_bytes)
5150 .unwrap_or(8 * 1024 * 1024);
5151 let sse_client_initializer = if let Some(reconnect) = &streaming_config.reconnection_config
5152 {
5153 let max_retries = reconnect.max_retries;
5154 let initial_delay_ms = reconnect.initial_delay_ms;
5155 let max_delay_ms = reconnect.max_delay_ms;
5156 let backoff_multiplier = reconnect.backoff_multiplier;
5157 quote! {
5158 SseClient::new()
5159 .with_max_error_body_bytes(#max_response_body_bytes)
5160 .with_reconnect_options(SseReconnectOptions {
5161 max_retries: #max_retries,
5162 initial_retry_delay: std::time::Duration::from_millis(#initial_delay_ms),
5163 max_retry_delay: std::time::Duration::from_millis(#max_delay_ms),
5164 backoff_multiplier: #backoff_multiplier,
5165 })
5166 }
5167 } else {
5168 quote! {
5169 SseClient::new()
5170 .with_max_error_body_bytes(#max_response_body_bytes)
5171 }
5172 };
5173
5174 let constructor_fields = if has_optional_headers {
5176 quote! {
5177 base_url: #default_base_url.to_string(),
5178 api_key: None,
5179 sse_client: #sse_client_initializer,
5180 custom_headers: std::collections::BTreeMap::new(),
5181 optional_headers: std::collections::BTreeMap::new(),
5182 }
5183 } else {
5184 quote! {
5185 base_url: #default_base_url.to_string(),
5186 api_key: None,
5187 sse_client: #sse_client_initializer,
5188 custom_headers: std::collections::BTreeMap::new(),
5189 }
5190 };
5191
5192 let optional_headers_method = if has_optional_headers {
5194 quote! {
5195 pub fn set_optional_headers(&mut self, headers: std::collections::BTreeMap<String, String>) {
5197 self.optional_headers = headers;
5198 }
5199 }
5200 } else {
5201 TokenStream::new()
5202 };
5203
5204 let constructor = quote! {
5205 impl #client_name {
5206 pub fn new() -> Self {
5208 Self {
5209 #constructor_fields
5210 }
5211 }
5212
5213 pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
5215 self.base_url = base_url.into();
5216 self
5217 }
5218
5219 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
5221 self.api_key = Some(api_key.into());
5222 self
5223 }
5224
5225 pub fn with_max_response_body_bytes(mut self, limit: usize) -> Self {
5227 self.sse_client = self.sse_client.with_max_error_body_bytes(limit);
5228 self
5229 }
5230
5231 pub fn with_header(
5233 mut self,
5234 name: impl Into<String>,
5235 value: impl Into<String>,
5236 ) -> Self {
5237 self.custom_headers.insert(name.into(), value.into());
5238 self
5239 }
5240
5241 pub fn with_http_client(mut self, client: reqwest::Client) -> Self {
5243 self.sse_client = self.sse_client.with_http_client(client);
5244 self
5245 }
5246
5247 #optional_headers_method
5248 }
5249 };
5250
5251 let mut trait_impls = Vec::new();
5253 for endpoint in &streaming_config.endpoints {
5254 let trait_impl = self.generate_endpoint_trait_impl(endpoint, &client_name, analysis)?;
5255 trait_impls.push(trait_impl);
5256 }
5257
5258 let default_impl = quote! {
5260 impl Default for #client_name {
5261 fn default() -> Self {
5262 Self::new()
5263 }
5264 }
5265 };
5266
5267 Ok(quote! {
5268 #[derive(Debug, Clone)]
5270 pub struct #client_name {
5271 #(#struct_fields,)*
5272 }
5273
5274 #constructor
5275
5276 #default_impl
5277
5278 #(#trait_impls)*
5279 })
5280 }
5281
5282 fn generate_endpoint_trait_impl(
5284 &self,
5285 endpoint: &crate::streaming::StreamingEndpoint,
5286 client_name: &proc_macro2::Ident,
5287 analysis: &SchemaAnalysis,
5288 ) -> Result<TokenStream> {
5289 use crate::streaming::HttpMethod;
5290
5291 let trait_name = format_ident!(
5292 "{}StreamingClient",
5293 self.to_rust_type_name(&endpoint.operation_id)
5294 );
5295 let method_name =
5296 format_ident!("stream_{}", self.to_rust_field_name(&endpoint.operation_id));
5297 let event_type =
5298 format_ident!("{}", self.resolve_streaming_event_type(endpoint, analysis)?);
5299
5300 let mut header_setup = Vec::new();
5302 for (name, value) in &endpoint.required_headers {
5303 header_setup.push(quote! {
5304 headers.insert(#name, HeaderValue::from_static(#value));
5305 });
5306 }
5307
5308 if let Some(auth_header) = &endpoint.auth_header {
5311 match auth_header {
5312 crate::streaming::AuthHeader::Bearer(header_name) => {
5313 header_setup.push(quote! {
5314 if let Some(ref api_key) = self.api_key {
5315 headers.insert(#header_name, HeaderValue::from_str(&format!("Bearer {}", api_key))?);
5316 }
5317 });
5318 }
5319 crate::streaming::AuthHeader::ApiKey(header_name) => {
5320 header_setup.push(quote! {
5321 if let Some(ref api_key) = self.api_key {
5322 headers.insert(#header_name, HeaderValue::from_str(api_key)?);
5323 }
5324 });
5325 }
5326 }
5327 } else {
5328 header_setup.push(quote! {
5330 if let Some(ref api_key) = self.api_key {
5331 headers.insert("Authorization", HeaderValue::from_str(&format!("Bearer {}", api_key))?);
5332 }
5333 });
5334 }
5335
5336 header_setup.push(quote! {
5338 for (name, value) in &self.custom_headers {
5339 if let (Ok(header_name), Ok(header_value)) = (reqwest::header::HeaderName::from_bytes(name.as_bytes()), HeaderValue::from_str(value)) {
5340 headers.insert(header_name, header_value);
5341 }
5342 }
5343 });
5344
5345 if !endpoint.optional_headers.is_empty() {
5347 header_setup.push(quote! {
5348 for (key, value) in &self.optional_headers {
5349 if let (Ok(header_name), Ok(header_value)) = (reqwest::header::HeaderName::from_bytes(key.as_bytes()), HeaderValue::from_str(value)) {
5350 headers.insert(header_name, header_value);
5351 }
5352 }
5353 });
5354 }
5355
5356 match endpoint.http_method {
5358 HttpMethod::Get => self.generate_get_streaming_impl(
5359 endpoint,
5360 client_name,
5361 &trait_name,
5362 &method_name,
5363 &event_type,
5364 &header_setup,
5365 ),
5366 HttpMethod::Post => self.generate_post_streaming_impl(
5367 endpoint,
5368 client_name,
5369 &trait_name,
5370 &method_name,
5371 &event_type,
5372 &header_setup,
5373 analysis,
5374 ),
5375 }
5376 }
5377
5378 fn generate_get_streaming_impl(
5380 &self,
5381 endpoint: &crate::streaming::StreamingEndpoint,
5382 client_name: &proc_macro2::Ident,
5383 trait_name: &proc_macro2::Ident,
5384 method_name: &proc_macro2::Ident,
5385 event_type: &proc_macro2::Ident,
5386 header_setup: &[TokenStream],
5387 ) -> Result<TokenStream> {
5388 let path = &endpoint.path;
5389
5390 let mut param_defs = Vec::new();
5392 let mut query_params = Vec::new();
5393
5394 for qp in &endpoint.query_parameters {
5395 let param_name = format_ident!("{}", self.to_rust_field_name(&qp.name));
5396 let param_name_str = &qp.name;
5397
5398 if qp.required {
5399 param_defs.push(quote! { #param_name: &str });
5400 query_params.push(quote! {
5401 url.query_pairs_mut().append_pair(#param_name_str, #param_name);
5402 });
5403 } else {
5404 param_defs.push(quote! { #param_name: Option<&str> });
5405 query_params.push(quote! {
5406 if let Some(v) = #param_name {
5407 url.query_pairs_mut().append_pair(#param_name_str, v);
5408 }
5409 });
5410 }
5411 }
5412
5413 let url_construction = quote! {
5415 let base_url = url::Url::parse(&self.base_url)
5416 .map_err(|e| StreamingError::Connection(format!("Invalid base URL: {}", e)))?;
5417 let path_to_join = #path.trim_start_matches('/');
5418 let mut url = base_url.join(path_to_join)
5419 .map_err(|e| StreamingError::Connection(format!("URL join error: {}", e)))?;
5420 #(#query_params)*
5421 };
5422
5423 let instrument_skip = quote! { #[instrument(skip(self), name = "streaming_get_request")] };
5424
5425 Ok(quote! {
5426 #[async_trait]
5427 impl #trait_name for #client_name {
5428 type Error = StreamingError;
5429
5430 #instrument_skip
5431 async fn #method_name(
5432 &self,
5433 #(#param_defs),*
5434 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error> {
5435 debug!("Starting streaming GET request");
5436
5437 let mut headers = HeaderMap::new();
5438 #(#header_setup)*
5439
5440 #url_construction
5441 let url_str = url.to_string();
5442 debug!("Making streaming GET request to: {}", url_str);
5443
5444 let request_builder = self.sse_client
5445 .get(url_str)
5446 .headers(headers);
5447
5448 debug!("Creating SSE stream from request");
5449 let stream = self
5450 .sse_client
5451 .stream::<#event_type>(request_builder)
5452 .await?;
5453 info!("SSE stream created successfully");
5454 Ok(stream)
5455 }
5456 }
5457 })
5458 }
5459
5460 #[allow(clippy::too_many_arguments)]
5462 fn generate_post_streaming_impl(
5463 &self,
5464 endpoint: &crate::streaming::StreamingEndpoint,
5465 client_name: &proc_macro2::Ident,
5466 trait_name: &proc_macro2::Ident,
5467 method_name: &proc_macro2::Ident,
5468 event_type: &proc_macro2::Ident,
5469 header_setup: &[TokenStream],
5470 analysis: &SchemaAnalysis,
5471 ) -> Result<TokenStream> {
5472 let path = &endpoint.path;
5473
5474 let request_type = self
5476 .find_request_type_for_operation(&endpoint.operation_id, analysis)
5477 .unwrap_or_else(|| "serde_json::Value".to_string());
5478 let request_type_ident = if request_type.contains("::") {
5479 let parts: Vec<&str> = request_type.split("::").collect();
5480 let path_parts: Vec<_> = parts.iter().map(|p| format_ident!("{}", p)).collect();
5481 quote! { #(#path_parts)::* }
5482 } else {
5483 let ident = format_ident!("{}", request_type);
5484 quote! { #ident }
5485 };
5486
5487 let url_construction = quote! {
5489 let base_url = url::Url::parse(&self.base_url)
5490 .map_err(|e| StreamingError::Connection(format!("Invalid base URL: {}", e)))?;
5491 let path_to_join = #path.trim_start_matches('/');
5492 let url = base_url.join(path_to_join)
5493 .map_err(|e| StreamingError::Connection(format!("URL join error: {}", e)))?
5494 .to_string();
5495 };
5496
5497 let stream_param = &endpoint.stream_parameter;
5499 let stream_setup = if stream_param.is_empty() {
5500 quote! {
5501 let streaming_request = request;
5502 }
5503 } else {
5504 quote! {
5505 let mut streaming_request = request;
5507 if let Ok(mut request_value) = serde_json::to_value(&streaming_request) {
5508 if let Some(obj) = request_value.as_object_mut() {
5509 obj.insert(#stream_param.to_string(), serde_json::Value::Bool(true));
5510 }
5511 streaming_request = serde_json::from_value(request_value)?;
5512 }
5513 }
5514 };
5515
5516 Ok(quote! {
5517 #[async_trait]
5518 impl #trait_name for #client_name {
5519 type Error = StreamingError;
5520
5521 #[instrument(skip(self, request), name = "streaming_post_request")]
5522 async fn #method_name(
5523 &self,
5524 request: #request_type_ident,
5525 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error> {
5526 debug!("Starting streaming POST request");
5527
5528 #stream_setup
5529
5530 let mut headers = HeaderMap::new();
5531 #(#header_setup)*
5532
5533 #url_construction
5534 debug!("Making streaming POST request to: {}", url);
5535
5536 let request_builder = self.sse_client
5537 .post(&url)
5538 .headers(headers)
5539 .json(&streaming_request);
5540
5541 debug!("Creating SSE stream from request");
5542 let stream = self
5543 .sse_client
5544 .stream::<#event_type>(request_builder)
5545 .await?;
5546 info!("SSE stream created successfully");
5547 Ok(stream)
5548 }
5549 }
5550 })
5551 }
5552
5553 fn generate_sse_runtime(&self) -> Result<String> {
5555 let provenance_attribute = self.provenance_attribute();
5556 let error_types = self.generate_streaming_error_types()?;
5557 let parser = self.generate_sse_parser_utilities()?;
5558 let tokens = quote! {
5559 #provenance_attribute
5563 #![allow(clippy::format_in_format_args)]
5564
5565 use futures_util::{Stream, StreamExt};
5566 use std::pin::Pin;
5567 use std::time::Duration;
5568 use tracing::debug;
5569
5570 #error_types
5571
5572 #[derive(Debug, Clone)]
5574 pub struct SseClient {
5575 http_client: reqwest::Client,
5576 max_error_body_bytes: usize,
5577 reconnect_options: Option<SseReconnectOptions>,
5578 }
5579
5580 impl SseClient {
5581 pub fn new() -> Self {
5582 Self {
5583 http_client: reqwest::Client::new(),
5584 max_error_body_bytes: DEFAULT_MAX_SSE_ERROR_BODY_BYTES,
5585 reconnect_options: None,
5586 }
5587 }
5588
5589 pub fn with_http_client(mut self, client: reqwest::Client) -> Self {
5590 self.http_client = client;
5591 self
5592 }
5593
5594 pub fn with_max_error_body_bytes(mut self, limit: usize) -> Self {
5595 self.max_error_body_bytes = limit;
5596 self
5597 }
5598
5599 pub fn with_reconnect_options(mut self, options: SseReconnectOptions) -> Self {
5601 self.reconnect_options = Some(options);
5602 self
5603 }
5604
5605 pub fn get(&self, url: impl reqwest::IntoUrl) -> reqwest::RequestBuilder {
5606 self.http_client.get(url)
5607 }
5608
5609 pub fn post(&self, url: impl reqwest::IntoUrl) -> reqwest::RequestBuilder {
5610 self.http_client.post(url)
5611 }
5612
5613 pub async fn stream<T>(
5614 &self,
5615 request_builder: reqwest::RequestBuilder,
5616 ) -> Result<Pin<Box<dyn Stream<Item = Result<T, StreamingError>> + Send>>, StreamingError>
5617 where
5618 T: serde::de::DeserializeOwned + Send + 'static,
5619 {
5620 if let Some(options) = self.reconnect_options.clone() {
5621 parse_sse_json_reconnecting_with_limit(
5622 request_builder,
5623 self.max_error_body_bytes,
5624 options,
5625 ).await
5626 } else {
5627 parse_sse_json_stream_with_limit(
5628 request_builder,
5629 self.max_error_body_bytes,
5630 ).await
5631 }
5632 }
5633
5634 pub async fn stream_raw(
5636 &self,
5637 request_builder: reqwest::RequestBuilder,
5638 ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<String>, StreamingError>> + Send>>, StreamingError> {
5639 parse_sse_raw_stream_with_limit(request_builder, self.max_error_body_bytes).await
5640 }
5641
5642 pub async fn stream_json<T>(
5644 &self,
5645 request_builder: reqwest::RequestBuilder,
5646 ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<T>, StreamingError>> + Send>>, StreamingError>
5647 where
5648 T: serde::de::DeserializeOwned + Send + 'static,
5649 {
5650 parse_sse_json_events_with_limit(request_builder, self.max_error_body_bytes).await
5651 }
5652
5653 pub async fn stream_raw_reconnecting(
5655 &self,
5656 request_builder: reqwest::RequestBuilder,
5657 ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<String>, StreamingError>> + Send>>, StreamingError> {
5658 parse_sse_raw_reconnecting_with_limit(
5659 request_builder,
5660 self.max_error_body_bytes,
5661 self.reconnect_options.clone().unwrap_or_default(),
5662 ).await
5663 }
5664
5665 pub async fn stream_json_reconnecting<T>(
5667 &self,
5668 request_builder: reqwest::RequestBuilder,
5669 ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<T>, StreamingError>> + Send>>, StreamingError>
5670 where
5671 T: serde::de::DeserializeOwned + Send + 'static,
5672 {
5673 parse_sse_json_reconnecting_events_with_limit(
5674 request_builder,
5675 self.max_error_body_bytes,
5676 self.reconnect_options.clone().unwrap_or_default(),
5677 ).await
5678 }
5679 }
5680
5681 impl Default for SseClient {
5682 fn default() -> Self {
5683 Self::new()
5684 }
5685 }
5686
5687 #parser
5688 };
5689 let syntax_tree = syn::parse2::<syn::File>(tokens).map_err(|error| {
5690 GeneratorError::CodeGenError(format!("Failed to parse generated sse.rs: {error}"))
5691 })?;
5692 Ok(prettyplease::unparse(&syntax_tree))
5693 }
5694
5695 fn generate_sse_parser_utilities(&self) -> Result<TokenStream> {
5697 Ok(quote! {
5698 pub const DEFAULT_MAX_SSE_ERROR_BODY_BYTES: usize = 8 * 1024 * 1024;
5700
5701 async fn __read_bounded_streaming_error_body(
5702 mut response: reqwest::Response,
5703 limit: usize,
5704 ) -> Result<Vec<u8>, StreamingError> {
5705 let mut body = Vec::new();
5706 while let Some(chunk) = response.chunk().await? {
5707 let next_len = body.len().checked_add(chunk.len());
5708 if next_len.is_none_or(|next_len| next_len > limit) {
5709 return Err(StreamingError::ResponseTooLarge { limit });
5710 }
5711 body.extend_from_slice(&chunk);
5712 }
5713 Ok(body)
5714 }
5715
5716 #[derive(Debug, Clone, PartialEq, Eq)]
5718 pub struct SseEvent<T> {
5719 pub event: String,
5721 pub data: T,
5723 pub id: Option<String>,
5725 pub retry: Option<Duration>,
5727 }
5728
5729 #[derive(Debug, Clone)]
5731 pub struct SseReconnectOptions {
5732 pub max_retries: u32,
5734 pub initial_retry_delay: Duration,
5736 pub max_retry_delay: Duration,
5738 pub backoff_multiplier: f64,
5740 }
5741
5742 impl Default for SseReconnectOptions {
5743 fn default() -> Self {
5744 Self {
5745 max_retries: 3,
5746 initial_retry_delay: Duration::from_secs(3),
5747 max_retry_delay: Duration::from_secs(30),
5748 backoff_multiplier: 2.0,
5749 }
5750 }
5751 }
5752
5753 impl SseReconnectOptions {
5754 fn delay(&self, attempt: u32, server_retry: Option<Duration>) -> Duration {
5755 if let Some(delay) = server_retry {
5756 return delay.min(self.max_retry_delay);
5757 }
5758 let multiplier = self.backoff_multiplier.max(1.0);
5759 let millis = self.initial_retry_delay.as_millis() as f64
5760 * multiplier.powi(attempt.min(63) as i32);
5761 Duration::from_millis(
5762 millis.min(self.max_retry_delay.as_millis() as f64) as u64,
5763 )
5764 }
5765 }
5766
5767 #[derive(Default)]
5768 struct __SseDecoder {
5769 line: Vec<u8>,
5770 event: String,
5771 data: Vec<String>,
5772 last_event_id: Option<String>,
5773 retry_delay: Option<Duration>,
5774 event_retry: Option<Duration>,
5775 saw_carriage_return: bool,
5776 }
5777
5778 impl __SseDecoder {
5779 fn feed(
5780 &mut self,
5781 chunk: &[u8],
5782 ) -> Vec<Result<SseEvent<String>, StreamingError>> {
5783 let mut messages = Vec::new();
5784 for &byte in chunk {
5785 if self.saw_carriage_return {
5786 self.saw_carriage_return = false;
5787 if byte == b'\n' {
5788 continue;
5789 }
5790 }
5791
5792 match byte {
5793 b'\n' => self.finish_line(&mut messages),
5794 b'\r' => {
5795 self.finish_line(&mut messages);
5796 self.saw_carriage_return = true;
5797 }
5798 _ => self.line.push(byte),
5799 }
5800 }
5801 messages
5802 }
5803
5804 fn finish(&mut self) -> Vec<Result<SseEvent<String>, StreamingError>> {
5805 let mut messages = Vec::new();
5806 if !self.line.is_empty() {
5807 self.finish_line(&mut messages);
5808 }
5809 self.dispatch(&mut messages);
5810 messages
5811 }
5812
5813 fn finish_line(
5814 &mut self,
5815 messages: &mut Vec<Result<SseEvent<String>, StreamingError>>,
5816 ) {
5817 let line = std::mem::take(&mut self.line);
5818 let line = match String::from_utf8(line) {
5819 Ok(line) => line,
5820 Err(error) => {
5821 messages.push(Err(StreamingError::Parsing(format!(
5822 "SSE line is not valid UTF-8: {}",
5823 error
5824 ))));
5825 return;
5826 }
5827 };
5828
5829 if line.is_empty() {
5830 self.dispatch(messages);
5831 return;
5832 }
5833 if line.starts_with(':') {
5834 return;
5835 }
5836
5837 let (field, value) = line
5838 .split_once(':')
5839 .map_or((line.as_str(), ""), |(field, value)| {
5840 (field, value.strip_prefix(' ').unwrap_or(value))
5841 });
5842 match field {
5843 "event" => self.event = value.to_string(),
5844 "data" => self.data.push(value.to_string()),
5845 "id" if !value.contains('\0') => {
5846 self.last_event_id = (!value.is_empty()).then(|| value.to_string());
5847 }
5848 "retry" if value.bytes().all(|byte| byte.is_ascii_digit()) => {
5849 if let Ok(milliseconds) = value.parse::<u64>() {
5850 let delay = Duration::from_millis(milliseconds);
5851 self.retry_delay = Some(delay);
5852 self.event_retry = Some(delay);
5853 }
5854 }
5855 _ => {}
5856 }
5857 }
5858
5859 fn dispatch(
5860 &mut self,
5861 messages: &mut Vec<Result<SseEvent<String>, StreamingError>>,
5862 ) {
5863 if self.data.is_empty() {
5864 self.event.clear();
5865 self.event_retry = None;
5866 return;
5867 }
5868 messages.push(Ok(SseEvent {
5869 event: if self.event.is_empty() {
5870 "message".to_string()
5871 } else {
5872 std::mem::take(&mut self.event)
5873 },
5874 data: std::mem::take(&mut self.data).join("\n"),
5875 id: self.last_event_id.clone(),
5876 retry: self.event_retry.take(),
5877 }));
5878 self.event.clear();
5879 }
5880
5881 fn reset_for_reconnect(&mut self) {
5882 self.line.clear();
5883 self.event.clear();
5884 self.data.clear();
5885 self.event_retry = None;
5886 self.saw_carriage_return = false;
5887 }
5888 }
5889
5890 fn __deserialize_sse_event<T>(
5891 event: SseEvent<String>,
5892 ) -> Option<Result<SseEvent<T>, StreamingError>>
5893 where
5894 T: serde::de::DeserializeOwned,
5895 {
5896 if event.data.trim() == "[DONE]" {
5897 return None;
5898 }
5899 if event.event == "ping" {
5900 debug!("Received SSE ping event, skipping");
5901 return None;
5902 }
5903 if event.data.trim().is_empty() {
5904 debug!("Empty SSE data, skipping");
5905 return None;
5906 }
5907
5908 let json_value = match serde_json::from_str::<serde_json::Value>(&event.data) {
5909 Ok(value) => value,
5910 Err(error) => {
5911 return Some(Err(StreamingError::Parsing(format!(
5912 "SSE event is not valid JSON: {} ({})",
5913 event.data, error
5914 ))));
5915 }
5916 };
5917 let is_ping = json_value
5918 .get("event")
5919 .or_else(|| json_value.get("type"))
5920 .and_then(serde_json::Value::as_str)
5921 .is_some_and(|event| event == "ping");
5922 if is_ping {
5923 debug!("Received ping event in JSON data, skipping");
5924 return None;
5925 }
5926
5927 Some(
5928 serde_json::from_value::<T>(json_value)
5929 .map(|data| SseEvent {
5930 event: event.event.clone(),
5931 data,
5932 id: event.id.clone(),
5933 retry: event.retry,
5934 })
5935 .map_err(|error| StreamingError::Parsing(format!(
5936 "Failed to parse SSE event: {} (raw: {}, event: {})",
5937 error, event.data, event.event
5938 ))),
5939 )
5940 }
5941
5942 pub async fn parse_sse_stream<T>(
5944 request_builder: reqwest::RequestBuilder
5945 ) -> Result<Pin<Box<dyn Stream<Item = Result<T, StreamingError>> + Send>>, StreamingError>
5946 where
5947 T: serde::de::DeserializeOwned + Send + 'static,
5948 {
5949 parse_sse_json_stream_with_limit(
5950 request_builder,
5951 DEFAULT_MAX_SSE_ERROR_BODY_BYTES,
5952 ).await
5953 }
5954
5955 struct __SseOpenError {
5956 error: StreamingError,
5957 retryable: bool,
5958 }
5959
5960 async fn __open_sse_response(
5961 request_builder: reqwest::RequestBuilder,
5962 max_response_body_bytes: usize,
5963 ) -> Result<reqwest::Response, __SseOpenError> {
5964 let response = request_builder.send().await.map_err(|error| __SseOpenError {
5965 error: error.into(),
5966 retryable: true,
5967 })?;
5968 if !response.status().is_success() {
5969 let status = response.status();
5970 let retryable = status.as_u16() == 429 || status.is_server_error();
5971 let error = match __read_bounded_streaming_error_body(
5972 response,
5973 max_response_body_bytes,
5974 ).await {
5975 Ok(body) => StreamingError::Connection(format!(
5976 "HTTP {} error: {}",
5977 status.as_u16(),
5978 String::from_utf8_lossy(&body)
5979 )),
5980 Err(error) => error,
5981 };
5982 return Err(__SseOpenError { error, retryable });
5983 }
5984
5985 let content_type = response
5986 .headers()
5987 .get(reqwest::header::CONTENT_TYPE)
5988 .and_then(|value| value.to_str().ok())
5989 .unwrap_or_default();
5990 if !content_type
5991 .split(';')
5992 .next()
5993 .is_some_and(|value| value.trim().eq_ignore_ascii_case("text/event-stream"))
5994 {
5995 let error = StreamingError::Parsing(format!(
5996 "Expected text/event-stream response, received {}",
5997 if content_type.is_empty() { "no Content-Type" } else { content_type }
5998 ));
5999 return Err(__SseOpenError { error, retryable: false });
6000 }
6001
6002 debug!("SSE connection opened");
6003 Ok(response)
6004 }
6005
6006 fn __raw_response_stream(
6007 response: reqwest::Response,
6008 ) -> Pin<Box<dyn Stream<Item = Result<SseEvent<String>, StreamingError>> + Send>> {
6009 let stream = futures_util::stream::unfold(
6010 (
6011 response,
6012 __SseDecoder::default(),
6013 std::collections::VecDeque::<Result<SseEvent<String>, StreamingError>>::new(),
6014 false,
6015 ),
6016 |(mut response, mut decoder, mut pending, mut done)| async move {
6017 loop {
6018 if let Some(item) = pending.pop_front() {
6019 return Some((item, (response, decoder, pending, done)));
6020 }
6021 if done {
6022 debug!("SSE stream completed normally");
6023 return None;
6024 }
6025
6026 match response.chunk().await {
6027 Ok(Some(chunk)) => {
6028 for event in decoder.feed(&chunk) {
6029 let is_done = event
6030 .as_ref()
6031 .is_ok_and(|event| event.data.trim() == "[DONE]");
6032 pending.push_back(event);
6033 if is_done {
6034 done = true;
6035 break;
6036 }
6037 }
6038 }
6039 Err(error) => {
6040 done = true;
6041 pending.push_back(Err(error.into()));
6042 }
6043 Ok(None) => {
6044 done = true;
6045 for event in decoder.finish() {
6046 pending.push_back(event);
6047 }
6048 }
6049 }
6050 }
6051 }
6052 );
6053
6054 Box::pin(stream)
6055 }
6056
6057 async fn parse_sse_raw_stream_with_limit(
6058 request_builder: reqwest::RequestBuilder,
6059 max_response_body_bytes: usize,
6060 ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<String>, StreamingError>> + Send>>, StreamingError> {
6061 Ok(match __open_sse_response(request_builder, max_response_body_bytes).await {
6062 Ok(response) => __raw_response_stream(response),
6063 Err(error) => Box::pin(futures_util::stream::once(async move { Err(error.error) })),
6064 })
6065 }
6066
6067 fn __json_event_stream<T>(
6068 raw: Pin<Box<dyn Stream<Item = Result<SseEvent<String>, StreamingError>> + Send>>,
6069 ) -> Pin<Box<dyn Stream<Item = Result<SseEvent<T>, StreamingError>> + Send>>
6070 where
6071 T: serde::de::DeserializeOwned + Send + 'static,
6072 {
6073 Box::pin(raw.filter_map(|event| async move {
6074 match event {
6075 Ok(event) => __deserialize_sse_event(event),
6076 Err(error) => Some(Err(error)),
6077 }
6078 }))
6079 }
6080
6081 async fn parse_sse_json_events_with_limit<T>(
6082 request_builder: reqwest::RequestBuilder,
6083 max_response_body_bytes: usize,
6084 ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<T>, StreamingError>> + Send>>, StreamingError>
6085 where
6086 T: serde::de::DeserializeOwned + Send + 'static,
6087 {
6088 Ok(__json_event_stream(
6089 parse_sse_raw_stream_with_limit(request_builder, max_response_body_bytes).await?,
6090 ))
6091 }
6092
6093 async fn parse_sse_json_stream_with_limit<T>(
6094 request_builder: reqwest::RequestBuilder,
6095 max_response_body_bytes: usize,
6096 ) -> Result<Pin<Box<dyn Stream<Item = Result<T, StreamingError>> + Send>>, StreamingError>
6097 where
6098 T: serde::de::DeserializeOwned + Send + 'static,
6099 {
6100 let events = parse_sse_json_events_with_limit(request_builder, max_response_body_bytes).await?;
6101 Ok(Box::pin(events.map(|event| event.map(|event| event.data))))
6102 }
6103
6104 struct __ReconnectState {
6105 request: reqwest::RequestBuilder,
6106 response: Option<reqwest::Response>,
6107 decoder: __SseDecoder,
6108 pending: std::collections::VecDeque<Result<SseEvent<String>, StreamingError>>,
6109 options: SseReconnectOptions,
6110 max_response_body_bytes: usize,
6111 attempts: u32,
6112 wait_before_open: bool,
6113 done: bool,
6114 }
6115
6116 async fn parse_sse_raw_reconnecting_with_limit(
6117 request_builder: reqwest::RequestBuilder,
6118 max_response_body_bytes: usize,
6119 options: SseReconnectOptions,
6120 ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<String>, StreamingError>> + Send>>, StreamingError> {
6121 if request_builder.try_clone().is_none() {
6122 return Err(StreamingError::Connection(
6123 "SSE reconnection requires a cloneable request body".to_string(),
6124 ));
6125 }
6126
6127 let stream = futures_util::stream::unfold(
6128 __ReconnectState {
6129 request: request_builder,
6130 response: None,
6131 decoder: __SseDecoder::default(),
6132 pending: std::collections::VecDeque::new(),
6133 options,
6134 max_response_body_bytes,
6135 attempts: 0,
6136 wait_before_open: false,
6137 done: false,
6138 },
6139 |mut state| async move {
6140 loop {
6141 if let Some(item) = state.pending.pop_front() {
6142 return Some((item, state));
6143 }
6144 if state.done {
6145 return None;
6146 }
6147
6148 if state.response.is_none() {
6149 if state.wait_before_open {
6150 let delay = state.options.delay(
6151 state.attempts.saturating_sub(1),
6152 state.decoder.retry_delay,
6153 );
6154 debug!(?delay, attempt = state.attempts, "Reconnecting SSE stream");
6155 futures_timer::Delay::new(delay).await;
6156 state.wait_before_open = false;
6157 }
6158
6159 let mut request = state.request.try_clone().expect("request clone checked");
6160 if let Some(last_event_id) = state.decoder.last_event_id.as_deref() {
6161 request = request.header("Last-Event-ID", last_event_id);
6162 }
6163 match __open_sse_response(request, state.max_response_body_bytes).await {
6164 Ok(response) => state.response = Some(response),
6165 Err(error) if error.retryable && state.attempts < state.options.max_retries => {
6166 state.attempts += 1;
6167 state.wait_before_open = true;
6168 continue;
6169 }
6170 Err(error) => {
6171 state.done = true;
6172 state.pending.push_back(Err(error.error));
6173 continue;
6174 }
6175 }
6176 }
6177
6178 let next = state.response.as_mut().expect("response opened").chunk().await;
6179 match next {
6180 Ok(Some(chunk)) => {
6181 let events = state.decoder.feed(&chunk);
6182 if !events.is_empty() {
6183 state.attempts = 0;
6184 }
6185 for event in events {
6186 let is_done = event
6187 .as_ref()
6188 .is_ok_and(|event| event.data.trim() == "[DONE]");
6189 state.pending.push_back(event);
6190 if is_done {
6191 state.done = true;
6192 state.response = None;
6193 break;
6194 }
6195 }
6196 }
6197 Ok(None) => {
6198 let events = state.decoder.finish();
6199 if !events.is_empty() {
6200 state.attempts = 0;
6201 }
6202 for event in events {
6203 let is_done = event
6204 .as_ref()
6205 .is_ok_and(|event| event.data.trim() == "[DONE]");
6206 state.pending.push_back(event);
6207 if is_done {
6208 state.done = true;
6209 break;
6210 }
6211 }
6212 state.response = None;
6213 state.decoder.reset_for_reconnect();
6214 if !state.done {
6215 if state.attempts < state.options.max_retries {
6216 state.attempts += 1;
6217 state.wait_before_open = true;
6218 } else {
6219 state.done = true;
6220 }
6221 }
6222 }
6223 Err(error) => {
6224 state.response = None;
6225 state.decoder.reset_for_reconnect();
6226 if state.attempts < state.options.max_retries {
6227 state.attempts += 1;
6228 state.wait_before_open = true;
6229 } else {
6230 state.done = true;
6231 state.pending.push_back(Err(error.into()));
6232 }
6233 }
6234 }
6235 }
6236 },
6237 );
6238 Ok(Box::pin(stream))
6239 }
6240
6241 async fn parse_sse_json_reconnecting_events_with_limit<T>(
6242 request_builder: reqwest::RequestBuilder,
6243 max_response_body_bytes: usize,
6244 options: SseReconnectOptions,
6245 ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<T>, StreamingError>> + Send>>, StreamingError>
6246 where
6247 T: serde::de::DeserializeOwned + Send + 'static,
6248 {
6249 Ok(__json_event_stream(
6250 parse_sse_raw_reconnecting_with_limit(
6251 request_builder,
6252 max_response_body_bytes,
6253 options,
6254 ).await?,
6255 ))
6256 }
6257
6258 async fn parse_sse_json_reconnecting_with_limit<T>(
6259 request_builder: reqwest::RequestBuilder,
6260 max_response_body_bytes: usize,
6261 options: SseReconnectOptions,
6262 ) -> Result<Pin<Box<dyn Stream<Item = Result<T, StreamingError>> + Send>>, StreamingError>
6263 where
6264 T: serde::de::DeserializeOwned + Send + 'static,
6265 {
6266 let events = parse_sse_json_reconnecting_events_with_limit(
6267 request_builder,
6268 max_response_body_bytes,
6269 options,
6270 ).await?;
6271 Ok(Box::pin(events.map(|event| event.map(|event| event.data))))
6272 }
6273 })
6274 }
6275
6276 fn generate_reconnection_utilities(
6278 &self,
6279 reconnect_config: &crate::streaming::ReconnectionConfig,
6280 ) -> Result<TokenStream> {
6281 let max_retries = reconnect_config.max_retries;
6282 let initial_delay = reconnect_config.initial_delay_ms;
6283 let max_delay = reconnect_config.max_delay_ms;
6284 let backoff_multiplier = reconnect_config.backoff_multiplier;
6285
6286 Ok(quote! {
6287 #[derive(Debug, Clone)]
6289 pub struct ReconnectionManager {
6290 max_retries: u32,
6291 initial_delay_ms: u64,
6292 max_delay_ms: u64,
6293 backoff_multiplier: f64,
6294 current_attempt: u32,
6295 }
6296
6297 impl ReconnectionManager {
6298 pub fn new() -> Self {
6300 Self {
6301 max_retries: #max_retries,
6302 initial_delay_ms: #initial_delay,
6303 max_delay_ms: #max_delay,
6304 backoff_multiplier: #backoff_multiplier,
6305 current_attempt: 0,
6306 }
6307 }
6308
6309 pub fn should_retry(&self) -> bool {
6311 self.current_attempt < self.max_retries
6312 }
6313
6314 pub fn next_retry_delay(&mut self) -> Duration {
6316 if !self.should_retry() {
6317 return Duration::from_secs(0);
6318 }
6319
6320 let delay_ms = (self.initial_delay_ms as f64
6321 * self.backoff_multiplier.powi(self.current_attempt as i32)) as u64;
6322 let delay_ms = delay_ms.min(self.max_delay_ms);
6323
6324 self.current_attempt += 1;
6325 Duration::from_millis(delay_ms)
6326 }
6327
6328 pub fn reset(&mut self) {
6330 self.current_attempt = 0;
6331 }
6332
6333 pub fn current_attempt(&self) -> u32 {
6335 self.current_attempt
6336 }
6337 }
6338
6339 impl Default for ReconnectionManager {
6340 fn default() -> Self {
6341 Self::new()
6342 }
6343 }
6344 })
6345 }
6346}