1use crate::{GeneratorError, Result, analysis::SchemaAnalysis, streaming::StreamingConfig};
2use proc_macro2::TokenStream;
3use quote::{format_ident, quote};
4use std::collections::BTreeMap;
5use std::path::PathBuf;
6
7fn parse_rust_type(rust_type: &str) -> Result<TokenStream> {
16 let parsed: syn::Type = syn::parse_str(rust_type).map_err(|e| {
17 GeneratorError::CodeGenError(format!(
18 "TypeMapper produced un-parseable type `{rust_type}`: {e}"
19 ))
20 })?;
21 Ok(quote! { #parsed })
22}
23
24fn format_constraints_doc(c: &crate::analysis::PropertyConstraints) -> String {
33 let mut parts: Vec<String> = Vec::new();
34
35 if let Some(v) = c.minimum {
36 parts.push(format!("minimum={}", strip_trailing_zero(v)));
37 }
38 if let Some(v) = c.maximum {
39 parts.push(format!("maximum={}", strip_trailing_zero(v)));
40 }
41 if let Some(v) = c.exclusive_minimum {
42 parts.push(format!("exclusiveMinimum={}", strip_trailing_zero(v)));
43 }
44 if let Some(v) = c.exclusive_maximum {
45 parts.push(format!("exclusiveMaximum={}", strip_trailing_zero(v)));
46 }
47 if let Some(v) = c.multiple_of {
48 parts.push(format!("multipleOf={}", strip_trailing_zero(v)));
49 }
50 if let Some(v) = c.min_length {
51 parts.push(format!("minLength={v}"));
52 }
53 if let Some(v) = c.max_length {
54 parts.push(format!("maxLength={v}"));
55 }
56 if let Some(v) = c.min_items {
57 parts.push(format!("minItems={v}"));
58 }
59 if let Some(v) = c.max_items {
60 parts.push(format!("maxItems={v}"));
61 }
62 if c.unique_items == Some(true) {
63 parts.push("uniqueItems=true".to_string());
64 }
65 if let Some(p) = &c.pattern {
66 let safe = p.replace("///", "/\u{200B}//").replace("*/", "*\u{200B}/");
71 parts.push(format!("pattern=`{safe}`"));
72 }
73
74 format!("Constraint: {}", parts.join(", "))
75}
76
77fn strip_trailing_zero(v: f64) -> String {
80 if v.fract() == 0.0 && v.is_finite() {
81 format!("{}", v as i64)
82 } else {
83 format!("{v}")
84 }
85}
86
87#[derive(Clone)]
89pub(crate) struct DiscriminatedVariantInfo {
90 pub(crate) discriminator_field: String,
92 pub(crate) discriminator_value: String,
94 pub(crate) is_parent_untagged: bool,
96}
97
98pub(crate) struct EmittedObjectProperty<'a> {
102 pub(crate) wire_name: &'a str,
103 pub(crate) property: &'a crate::analysis::PropertyInfo,
104 pub(crate) ident: syn::Ident,
105 pub(crate) is_required: bool,
106 pub(crate) field_type: TokenStream,
107}
108
109struct TypeGenerationIndex {
113 request_body_roots: std::collections::HashSet<String>,
114 reserved_type_names: std::collections::HashSet<String>,
115}
116
117struct TypeGenerationContext<'a> {
118 discriminated_variants: &'a BTreeMap<String, DiscriminatedVariantInfo>,
119 index: &'a TypeGenerationIndex,
120}
121
122#[derive(Debug, Clone)]
123pub struct GeneratorConfig {
124 pub spec_path: PathBuf,
126 pub output_dir: PathBuf,
128 pub module_name: String,
135 pub enable_sse_client: bool,
137 pub enable_async_client: bool,
139 pub enable_specta: bool,
141 pub type_mappings: BTreeMap<String, String>,
143 pub streaming_config: Option<StreamingConfig>,
145 pub nullable_field_overrides: BTreeMap<String, bool>,
148 pub extensible_enum_overrides: BTreeMap<String, bool>,
155 pub schema_extensions: Vec<PathBuf>,
158 pub http_client_config: Option<crate::http_config::HttpClientConfig>,
160 pub retry_config: Option<crate::http_config::RetryConfig>,
162 pub tracing_enabled: bool,
164 pub auth_config: Option<crate::http_config::AuthConfig>,
166 pub enable_registry: bool,
168 pub registry_only: bool,
170 pub types: crate::type_mapping::TypeMappingConfig,
174 pub builders: crate::config::BuildersSection,
176 pub server: Option<crate::config::ServerSection>,
179 pub client: Option<crate::config::ClientSection>,
182}
183
184impl Default for GeneratorConfig {
185 fn default() -> Self {
186 Self {
187 spec_path: "openapi.json".into(),
188 output_dir: "src/gen".into(),
189 module_name: "api_types".to_string(),
190 enable_sse_client: true,
191 enable_async_client: true,
192 enable_specta: false,
193 type_mappings: default_type_mappings(),
194 streaming_config: None,
195 nullable_field_overrides: BTreeMap::new(),
196 extensible_enum_overrides: BTreeMap::new(),
197 schema_extensions: Vec::new(),
198 http_client_config: None,
199 retry_config: None,
200 tracing_enabled: true,
201 auth_config: None,
202 enable_registry: false,
203 registry_only: false,
204 types: crate::type_mapping::TypeMappingConfig::default(),
205 builders: crate::config::BuildersSection::default(),
206 server: None,
207 client: None,
208 }
209 }
210}
211
212impl GeneratorConfig {
213 pub fn apply_spec_server_default(&mut self, spec: &serde_json::Value) {
224 let already_configured = self
225 .http_client_config
226 .as_ref()
227 .and_then(|http| http.base_url.as_deref())
228 .is_some_and(|url| !url.is_empty());
229 if already_configured {
230 return;
231 }
232
233 let Some(url) = spec
234 .pointer("/servers/0/url")
235 .and_then(serde_json::Value::as_str)
236 .map(str::trim)
237 .filter(|url| !url.is_empty())
238 .filter(|url| !url.starts_with('/'))
239 .filter(|url| !url.contains('{'))
240 else {
241 return;
242 };
243
244 match self.http_client_config.as_mut() {
245 Some(http) => http.base_url = Some(url.to_string()),
246 None => {
247 self.http_client_config = Some(crate::http_config::HttpClientConfig {
248 base_url: Some(url.to_string()),
249 timeout_seconds: None,
250 max_response_body_bytes: None,
251 default_headers: Default::default(),
252 })
253 }
254 }
255 }
256}
257
258pub fn default_type_mappings() -> BTreeMap<String, String> {
259 let mut mappings = BTreeMap::new();
260 mappings.insert("integer".to_string(), "i64".to_string());
261 mappings.insert("number".to_string(), "f64".to_string());
262 mappings.insert("string".to_string(), "String".to_string());
263 mappings.insert("boolean".to_string(), "bool".to_string());
264 mappings
265}
266
267pub(crate) fn rust_type_name(s: &str) -> String {
273 let mut result = String::new();
274 let mut next_upper = true;
275
276 for c in s.chars() {
277 match c {
278 'a'..='z' => {
279 result.push(if next_upper {
280 c.to_ascii_uppercase()
281 } else {
282 c
283 });
284 next_upper = false;
285 }
286 'A'..='Z' | '0'..='9' => {
287 result.push(c);
288 next_upper = false;
289 }
290 _ => next_upper = true,
291 }
292 }
293
294 if result.is_empty() {
295 result = "Type".to_string();
296 }
297 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
298 result = format!("Type{result}");
299 }
300 if matches!(
301 result.as_str(),
302 "Result"
303 | "Option"
304 | "Box"
305 | "Vec"
306 | "String"
307 | "Some"
308 | "None"
309 | "Ok"
310 | "Err"
311 | "Default"
312 | "Clone"
313 | "Debug"
314 | "Send"
315 | "Sync"
316 | "Sized"
317 | "Iterator"
318 | "From"
319 | "Into"
320 | "TryFrom"
321 | "TryInto"
322 | "AsRef"
323 | "AsMut"
324 ) {
325 result.push_str("Type");
326 }
327 result
328}
329
330#[derive(Debug, Clone)]
332pub struct GeneratedFile {
333 pub path: PathBuf,
335 pub content: String,
337}
338
339#[derive(Debug, Clone)]
341pub struct GenerationResult {
342 pub files: Vec<GeneratedFile>,
344 pub mod_file: GeneratedFile,
346 pub required_deps: Vec<crate::type_mapping::DepRequirement>,
350 pub pruned_schemas: usize,
352}
353
354#[derive(Debug)]
355struct OperationScopes {
356 client_ids: Option<std::collections::BTreeSet<String>>,
358 server_ids: std::collections::BTreeSet<String>,
359 streaming_ids: std::collections::BTreeSet<String>,
360 prune_models: bool,
361 extra_schema_roots: Vec<String>,
362}
363
364pub struct CodeGenerator {
365 config: GeneratorConfig,
366 source_provenance: Option<String>,
367}
368
369impl CodeGenerator {
370 pub fn new(config: GeneratorConfig) -> Self {
371 Self {
372 config,
373 source_provenance: None,
374 }
375 }
376
377 pub fn with_source_provenance(mut self, source: impl Into<String>) -> Self {
379 self.source_provenance = Some(source.into());
380 self
381 }
382
383 pub fn config(&self) -> &GeneratorConfig {
385 &self.config
386 }
387
388 pub(crate) fn provenance_attribute(&self) -> TokenStream {
389 self.source_provenance
390 .as_ref()
391 .map(|source| {
392 let provenance = format!(
393 " Generated by openapi-to-rust v{}. Source OpenAPI document: {source}",
394 env!("CARGO_PKG_VERSION")
395 );
396 quote! { #![doc = #provenance] }
397 })
398 .unwrap_or_default()
399 }
400
401 pub fn generate_all(&self, analysis: &mut SchemaAnalysis) -> Result<GenerationResult> {
403 let scopes = self.resolve_operation_scopes(analysis)?;
406 let pruned_schemas = self.prune_models_to_scopes(analysis, &scopes);
407 let mut files = Vec::new();
408
409 if !self.config.registry_only {
410 let types_content = self.generate_types(analysis)?;
412 files.push(GeneratedFile {
413 path: "types.rs".into(),
414 content: types_content,
415 });
416
417 if self.config.enable_sse_client
419 && let Some(ref streaming_config) = self.config.streaming_config
420 {
421 if streaming_config.generate_client && !streaming_config.event_parser_helpers {
422 return Err(GeneratorError::ValidationError(
423 "streaming generate_client=true requires event_parser_helpers=true"
424 .to_string(),
425 ));
426 }
427 if streaming_config.event_parser_helpers {
428 files.push(GeneratedFile {
429 path: "sse.rs".into(),
430 content: self.generate_sse_runtime()?,
431 });
432 }
433 let streaming_content =
434 self.generate_streaming_client(streaming_config, analysis)?;
435 files.push(GeneratedFile {
436 path: "streaming.rs".into(),
437 content: streaming_content,
438 });
439 }
440
441 if self.config.enable_async_client {
443 let operations = self.client_operations(analysis, scopes.client_ids.as_ref());
444 let http_content =
445 self.generate_http_client_for_operations(analysis, &operations)?;
446 files.push(GeneratedFile {
447 path: "client.rs".into(),
448 content: http_content,
449 });
450 }
451 }
452
453 if self.config.enable_registry || self.config.registry_only {
455 let registry_content = self.generate_registry(analysis)?;
456 files.push(GeneratedFile {
457 path: "registry.rs".into(),
458 content: registry_content,
459 });
460 }
461
462 if !self.config.registry_only
466 && let Some(server) = self
467 .config
468 .server
469 .as_ref()
470 .filter(|server| !server.operations.is_empty())
471 {
472 let server_files =
473 crate::server::codegen::ServerCodegen::new(&self.config, analysis, server)
474 .with_source_provenance(self.source_provenance.as_deref())
475 .generate()
476 .map_err(|error| {
477 GeneratorError::CodeGenError(format!(
478 "server code generation failed: {error}"
479 ))
480 })?;
481 files.extend(server_files);
482 }
483
484 let mod_content = self.generate_mod_file(&files)?;
486 let mod_file = GeneratedFile {
487 path: "mod.rs".into(),
488 content: mod_content,
489 };
490
491 let required_deps = crate::type_mapping::collect_generated_dep_requirements(
492 files.iter().map(|file| file.content.as_str()),
493 self.config.enable_specta,
494 );
495
496 Ok(GenerationResult {
497 files,
498 mod_file,
499 required_deps,
500 pruned_schemas,
501 })
502 }
503
504 pub fn generate(&self, analysis: &mut SchemaAnalysis) -> Result<String> {
506 self.generate_types(analysis)
507 }
508
509 fn generate_types(&self, analysis: &mut SchemaAnalysis) -> Result<String> {
511 self.validate_schema_type_names(analysis)?;
512
513 let provenance_attribute = self.provenance_attribute();
514 let mut type_definitions = TokenStream::new();
515
516 let mut discriminated_variant_info: BTreeMap<String, DiscriminatedVariantInfo> =
519 BTreeMap::new();
520
521 let mut sorted_schemas: Vec<_> = analysis.schemas.iter().collect();
523 sorted_schemas.sort_by_key(|(name, _)| name.as_str());
524
525 for (_parent_name, schema) in sorted_schemas {
526 if let crate::analysis::SchemaType::DiscriminatedUnion {
527 variants,
528 discriminator_field,
529 } = &schema.schema_type
530 {
531 let is_parent_untagged =
533 self.should_use_untagged_discriminated_union(schema, analysis);
534
535 for variant in variants {
536 if let Some(variant_schema) = analysis.schemas.get(&variant.type_name) {
539 if let crate::analysis::SchemaType::Object { properties, .. } =
540 &variant_schema.schema_type
541 {
542 if properties.contains_key(discriminator_field) {
543 discriminated_variant_info.insert(
544 variant.type_name.clone(),
545 DiscriminatedVariantInfo {
546 discriminator_field: discriminator_field.clone(),
547 discriminator_value: variant.discriminator_value.clone(),
548 is_parent_untagged,
549 },
550 );
551 }
552 }
553 }
554 }
555 }
556 }
557
558 let type_index = self.type_generation_index(analysis);
559 let type_context = TypeGenerationContext {
560 discriminated_variants: &discriminated_variant_info,
561 index: &type_index,
562 };
563
564 let generation_order = analysis.dependencies.topological_sort()?;
566
567 let mut processed = std::collections::HashSet::new();
568
569 for schema_name in generation_order {
571 if let Some(schema) = analysis.schemas.get(&schema_name) {
572 let type_def = self.generate_type_definition(schema, analysis, &type_context)?;
573 if !type_def.is_empty() {
574 type_definitions.extend(type_def);
575 }
576 processed.insert(schema_name);
577 }
578 }
579
580 let mut remaining_schemas: Vec<_> = analysis
582 .schemas
583 .iter()
584 .filter(|(name, _)| !processed.contains(*name))
585 .collect();
586 remaining_schemas.sort_by_key(|(name, _)| name.as_str());
587
588 for (_schema_name, schema) in remaining_schemas {
589 let type_def = self.generate_type_definition(schema, analysis, &type_context)?;
590 if !type_def.is_empty() {
591 type_definitions.extend(type_def);
592 }
593 }
594
595 let base64_helper = if analysis
600 .used_type_features
601 .contains(crate::type_mapping::TypeFeature::Base64)
602 {
603 let engine = match self.config.types.byte {
604 crate::type_mapping::ByteStrategy::Base64UrlUnpadded => {
605 quote::format_ident!("URL_SAFE_NO_PAD")
606 }
607 _ => quote::format_ident!("STANDARD"),
608 };
609 quote! {
610 mod base64_serde {
615 use base64::{Engine as _, engine::general_purpose::#engine as ENGINE};
616 use serde::{Deserialize, Deserializer, Serializer};
617
618 pub fn serialize<S: Serializer>(
619 bytes: &Vec<u8>,
620 ser: S,
621 ) -> Result<S::Ok, S::Error> {
622 ser.serialize_str(&ENGINE.encode(bytes))
623 }
624
625 pub fn deserialize<'de, D: Deserializer<'de>>(
626 de: D,
627 ) -> Result<Vec<u8>, D::Error> {
628 let s = String::deserialize(de)?;
629 ENGINE
630 .decode(s.as_bytes())
631 .map_err(serde::de::Error::custom)
632 }
633
634 pub mod option {
640 use super::*;
641 use serde::{Deserialize, Deserializer, Serializer};
642
643 pub fn serialize<S: Serializer>(
644 opt: &Option<Vec<u8>>,
645 ser: S,
646 ) -> Result<S::Ok, S::Error> {
647 match opt {
648 Some(bytes) => super::serialize(bytes, ser),
649 None => ser.serialize_none(),
650 }
651 }
652
653 pub fn deserialize<'de, D: Deserializer<'de>>(
654 de: D,
655 ) -> Result<Option<Vec<u8>>, D::Error> {
656 let opt = Option::<String>::deserialize(de)?;
657 opt.map(|s| {
658 ENGINE
659 .decode(s.as_bytes())
660 .map_err(serde::de::Error::custom)
661 })
662 .transpose()
663 }
664 }
665 }
666 }
667 } else {
668 TokenStream::new()
669 };
670
671 let time_date_helper = if analysis
678 .used_type_features
679 .contains(crate::type_mapping::TypeFeature::TimeDate)
680 {
681 quote! {
682 time::serde::format_description!(
683 time_date_format,
684 Date,
685 "[year]-[month]-[day]"
686 );
687 }
688 } else {
689 TokenStream::new()
690 };
691
692 let time_time_helper = if analysis
697 .used_type_features
698 .contains(crate::type_mapping::TypeFeature::TimeTime)
699 {
700 quote! {
701 time::serde::format_description!(
702 version = 2,
703 time_time_format,
704 Time,
705 "[hour]:[minute]:[second][optional [.[subsecond]]]"
706 );
707 }
708 } else {
709 TokenStream::new()
710 };
711
712 let generated = quote! {
714 #provenance_attribute
720
721 #![allow(clippy::large_enum_variant)]
722 #![allow(clippy::format_in_format_args)]
723 #![allow(clippy::let_unit_value)]
724 #![allow(unreachable_patterns)]
725
726 use serde::{Deserialize, Serialize};
727
728 #base64_helper
729
730 #time_date_helper
731
732 #time_time_helper
733
734 #type_definitions
735 };
736
737 let syntax_tree = syn::parse2::<syn::File>(generated).map_err(|e| {
739 GeneratorError::CodeGenError(format!("Failed to parse generated code: {e}"))
740 })?;
741
742 let formatted = prettyplease::unparse(&syntax_tree);
743
744 Ok(formatted)
745 }
746
747 fn generate_streaming_client(
749 &self,
750 streaming_config: &StreamingConfig,
751 analysis: &SchemaAnalysis,
752 ) -> Result<String> {
753 let mut client_code = TokenStream::new();
754 let provenance_attribute = self.provenance_attribute();
755 let duration_import = streaming_config
756 .reconnection_config
757 .as_ref()
758 .map(|_| quote! { use std::time::Duration; });
759
760 let imports = quote! {
762 #provenance_attribute
767 #![allow(clippy::format_in_format_args)]
768 #![allow(clippy::let_unit_value)]
769 #![allow(unused_mut)]
770
771 use super::types::*;
772 use async_trait::async_trait;
773 use futures_util::Stream;
774 use std::pin::Pin;
775 use reqwest::header::{HeaderMap, HeaderValue};
776 use tracing::{debug, info, instrument};
777 #duration_import
778 };
779 client_code.extend(imports);
780
781 if streaming_config.generate_client {
782 if streaming_config.reconnection_config.is_some() {
783 client_code.extend(quote! {
784 use super::sse::{SseClient, SseReconnectOptions};
785 pub use super::sse::StreamingError;
786 });
787 } else {
788 client_code.extend(quote! {
789 use super::sse::SseClient;
790 pub use super::sse::StreamingError;
791 });
792 }
793 }
794
795 for endpoint in &streaming_config.endpoints {
797 let trait_code = self.generate_endpoint_trait(endpoint, analysis)?;
798 client_code.extend(trait_code);
799 }
800
801 if streaming_config.generate_client {
803 let client_impl = self.generate_streaming_client_impl(streaming_config, analysis)?;
804 client_code.extend(client_impl);
805 }
806
807 if let Some(reconnect_config) = &streaming_config.reconnection_config {
809 let reconnect_code = self.generate_reconnection_utilities(reconnect_config)?;
810 client_code.extend(reconnect_code);
811 }
812
813 let syntax_tree = syn::parse2::<syn::File>(client_code).map_err(|e| {
814 GeneratorError::CodeGenError(format!("Failed to parse streaming client code: {e}"))
815 })?;
816
817 Ok(prettyplease::unparse(&syntax_tree))
818 }
819
820 fn validate_schema_type_names(&self, analysis: &SchemaAnalysis) -> Result<()> {
821 let mut source_by_rust_name = BTreeMap::<String, String>::new();
822
823 for schema in analysis.schemas.values() {
824 let rust_name = self.to_rust_type_name(&schema.name);
825 if let Some(first) = source_by_rust_name.insert(rust_name.clone(), schema.name.clone())
826 {
827 return Err(GeneratorError::InvalidSchema(format!(
828 "schema names `{first}` and `{}` both map to Rust type `{rust_name}`",
829 schema.name
830 )));
831 }
832 }
833
834 Ok(())
835 }
836
837 pub fn generate_http_client(&self, analysis: &SchemaAnalysis) -> Result<String> {
843 let client_ids = self.resolve_client_operation_ids(analysis)?;
844 let operations = self.client_operations(analysis, client_ids.as_ref());
845 self.generate_http_client_for_operations(analysis, &operations)
846 }
847
848 fn generate_http_client_for_operations(
849 &self,
850 analysis: &SchemaAnalysis,
851 operations: &[&crate::analysis::OperationInfo],
852 ) -> Result<String> {
853 let provenance_attribute = self.provenance_attribute();
854 let error_types = self.generate_http_error_types();
855 let client_struct = self.generate_http_client_struct();
856 let operation_methods = self.generate_operation_methods_for(analysis, operations);
857
858 let generated = quote! {
859 #provenance_attribute
864 #![allow(clippy::format_in_format_args)]
865 #![allow(clippy::let_unit_value)]
866
867 use super::types::*;
868
869 #error_types
870
871 #client_struct
872
873 #operation_methods
874 };
875
876 let syntax_tree = syn::parse2::<syn::File>(generated.clone()).map_err(|e| {
877 if let Ok(dump) = std::env::var("OATR_DUMP_TOKENS_ON_PARSE_ERROR") {
878 let _ = std::fs::write(&dump, generated.to_string());
879 }
880 GeneratorError::CodeGenError(format!("Failed to parse HTTP client code: {e}"))
881 })?;
882
883 Ok(prettyplease::unparse(&syntax_tree))
884 }
885
886 fn resolve_operation_scopes(&self, analysis: &SchemaAnalysis) -> Result<OperationScopes> {
887 let client_ids = if self.config.enable_async_client && !self.config.registry_only {
888 self.resolve_client_operation_ids(analysis)?
889 } else {
890 None
891 };
892
893 let server_ids = match &self.config.server {
894 Some(server) if !server.operations.is_empty() => {
895 crate::server::resolve_operation_selectors(&server.operations, analysis)
896 .map_err(|error| {
897 GeneratorError::ValidationError(format!(
898 "Invalid [server].operations: {error}"
899 ))
900 })?
901 .operations
902 .into_iter()
903 .map(|operation| operation.operation_id)
904 .collect()
905 }
906 _ => Default::default(),
907 };
908
909 let streaming_ids = if self.config.registry_only || !self.config.enable_sse_client {
910 Default::default()
911 } else if let Some(streaming) = &self.config.streaming_config {
912 let mut ids = std::collections::BTreeSet::new();
913 for (index, endpoint) in streaming.endpoints.iter().enumerate() {
914 let resolution =
915 crate::server::resolve_operation_id(&endpoint.operation_id, analysis).map_err(
916 |error| {
917 GeneratorError::ValidationError(format!(
918 "Invalid [streaming].endpoints[{index}].operation_id: {error}"
919 ))
920 },
921 )?;
922 ids.extend(
923 resolution
924 .operations
925 .into_iter()
926 .map(|operation| operation.operation_id),
927 );
928 }
929 ids
930 } else {
931 Default::default()
932 };
933
934 let client_prunes = self.config.enable_async_client
935 && !self.config.registry_only
936 && self
937 .config
938 .client
939 .as_ref()
940 .is_some_and(|client| client.prune_models);
941 let server_prunes = self
942 .config
943 .server
944 .as_ref()
945 .is_some_and(|server| server.prune_models && !server.operations.is_empty());
946 let extra_schema_roots = if self.config.registry_only || !self.config.enable_sse_client {
947 Vec::new()
948 } else {
949 self.config
950 .streaming_config
951 .as_ref()
952 .map(|streaming| {
953 streaming
954 .endpoints
955 .iter()
956 .map(|endpoint| endpoint.event_union_type.clone())
957 .collect()
958 })
959 .unwrap_or_default()
960 };
961
962 Ok(OperationScopes {
963 client_ids,
964 server_ids,
965 streaming_ids,
966 prune_models: client_prunes || server_prunes,
967 extra_schema_roots,
968 })
969 }
970
971 fn resolve_client_operation_ids(
972 &self,
973 analysis: &SchemaAnalysis,
974 ) -> Result<Option<std::collections::BTreeSet<String>>> {
975 match &self.config.client {
976 Some(client) if !client.operations.is_empty() => {
977 let resolution =
978 crate::server::resolve_operation_selectors(&client.operations, analysis)
979 .map_err(|error| {
980 GeneratorError::ValidationError(format!(
981 "Invalid [client].operations: {error}"
982 ))
983 })?;
984 Ok(Some(
985 resolution
986 .operations
987 .into_iter()
988 .map(|operation| operation.operation_id)
989 .collect(),
990 ))
991 }
992 _ => Ok(None),
993 }
994 }
995
996 fn client_operations<'a>(
997 &self,
998 analysis: &'a SchemaAnalysis,
999 selected: Option<&std::collections::BTreeSet<String>>,
1000 ) -> Vec<&'a crate::analysis::OperationInfo> {
1001 analysis
1002 .operations
1003 .iter()
1004 .filter(|(operation_id, _)| selected.is_none_or(|ids| ids.contains(*operation_id)))
1005 .map(|(_, operation)| operation)
1006 .collect()
1007 }
1008
1009 fn prune_models_to_scopes(
1010 &self,
1011 analysis: &mut SchemaAnalysis,
1012 scopes: &OperationScopes,
1013 ) -> usize {
1014 if !scopes.prune_models {
1015 return 0;
1016 }
1017
1018 let mut consumer_ids = scopes.server_ids.clone();
1019 if self.config.enable_async_client && !self.config.registry_only {
1020 match &scopes.client_ids {
1021 Some(ids) => consumer_ids.extend(ids.iter().cloned()),
1022 None => consumer_ids.extend(analysis.operations.keys().cloned()),
1023 }
1024 }
1025 consumer_ids.extend(scopes.streaming_ids.iter().cloned());
1026
1027 let operations: Vec<&crate::analysis::OperationInfo> = consumer_ids
1028 .iter()
1029 .filter_map(|operation_id| analysis.operations.get(operation_id))
1030 .collect();
1031 let keep = crate::server::codegen::reachable_schemas_with_roots(
1032 analysis,
1033 &operations,
1034 &scopes.extra_schema_roots,
1035 );
1036 let before = analysis.schemas.len();
1037 analysis.schemas.retain(|name, _| keep.contains(name));
1038 before - analysis.schemas.len()
1039 }
1040
1041 fn generate_http_error_types(&self) -> TokenStream {
1043 quote! {
1044 use thiserror::Error;
1045
1046 pub mod openapi_to_rust_problem {
1049 #[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
1050 pub struct ProblemDetails {
1051 #[serde(rename = "type")]
1052 pub type_uri: String,
1053 pub title: String,
1054 pub status: u16,
1055 pub code: String,
1056 #[serde(default)]
1057 pub errors: Vec<InvalidParameter>,
1058 #[serde(default, skip_serializing_if = "Option::is_none")]
1059 pub detail: Option<String>,
1060 #[serde(default, skip_serializing_if = "Option::is_none")]
1061 pub instance: Option<String>,
1062 }
1063
1064 #[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
1065 pub struct InvalidParameter {
1066 pub code: String,
1067 pub location: String,
1068 pub message: String,
1069 }
1070 }
1071
1072 #[derive(Error, Debug)]
1080 pub enum HttpError {
1081 #[error("Network error: {0}")]
1083 Network(#[from] reqwest::Error),
1084
1085 #[error("Middleware error: {0}")]
1087 Middleware(#[from] reqwest_middleware::Error),
1088
1089 #[error("Failed to serialize request: {0}")]
1091 Serialization(String),
1092
1093 #[error("Authentication error: {0}")]
1095 Auth(String),
1096
1097 #[error("Request timeout")]
1099 Timeout,
1100
1101 #[error("Response body exceeded configured limit of {limit} bytes")]
1103 ResponseTooLarge { limit: usize },
1104
1105 #[error("Configuration error: {0}")]
1107 Config(String),
1108
1109 #[error("{0}")]
1111 Other(String),
1112 }
1113
1114 impl HttpError {
1115 pub fn serialization_error(error: impl std::fmt::Display) -> Self {
1117 Self::Serialization(error.to_string())
1118 }
1119
1120 pub fn is_retryable(&self) -> bool {
1122 matches!(self, Self::Network(_) | Self::Middleware(_) | Self::Timeout)
1123 }
1124 }
1125
1126 #[derive(Debug, Clone)]
1138 pub struct ApiError<E> {
1139 pub status: u16,
1140 pub headers: reqwest::header::HeaderMap,
1141 pub body: String,
1142 pub raw_body: Vec<u8>,
1144 pub typed: Option<E>,
1145 pub parse_error: Option<String>,
1146 }
1147
1148 const API_ERROR_BODY_DISPLAY_LIMIT: usize = 500;
1149 const API_ERROR_BODY_TRUNCATION_MARKER: &str = "... [truncated]";
1150
1151 fn display_api_error_body(body: &str) -> std::borrow::Cow<'_, str> {
1152 let Some((end, _)) = body.char_indices().nth(API_ERROR_BODY_DISPLAY_LIMIT) else {
1153 return std::borrow::Cow::Borrowed(body);
1154 };
1155
1156 let mut displayed =
1157 String::with_capacity(end + API_ERROR_BODY_TRUNCATION_MARKER.len());
1158 displayed.push_str(&body[..end]);
1159 displayed.push_str(API_ERROR_BODY_TRUNCATION_MARKER);
1160 std::borrow::Cow::Owned(displayed)
1161 }
1162
1163 impl<E> ApiError<E> {
1164 pub fn is_client_error(&self) -> bool {
1165 (400..500).contains(&self.status)
1166 }
1167
1168 pub fn is_server_error(&self) -> bool {
1169 (500..600).contains(&self.status)
1170 }
1171
1172 pub fn is_retryable(&self) -> bool {
1175 matches!(self.status, 429 | 500 | 502 | 503 | 504)
1176 }
1177
1178 pub fn problem_details(
1190 &self,
1191 ) -> Option<openapi_to_rust_problem::ProblemDetails> {
1192 let content_type = self
1193 .headers
1194 .get(reqwest::header::CONTENT_TYPE)?
1195 .to_str()
1196 .ok()?;
1197 let media_type = content_type.split(';').next()?.trim();
1198 if !media_type.eq_ignore_ascii_case("application/problem+json") {
1199 return None;
1200 }
1201 serde_json::from_str(&self.body).ok()
1202 }
1203 }
1204
1205 impl<E: std::fmt::Debug> std::fmt::Display for ApiError<E> {
1206 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1207 write!(
1208 f,
1209 "API error {}: {}",
1210 self.status,
1211 display_api_error_body(&self.body)
1212 )?;
1213
1214 if let Some(typed) = &self.typed {
1215 write!(f, "; typed: {typed:?}")?;
1216 }
1217
1218 if let Some(parse_error) = &self.parse_error {
1219 write!(f, "; parse error: {parse_error}")?;
1220 }
1221
1222 Ok(())
1223 }
1224 }
1225
1226 impl<E: std::fmt::Debug> std::error::Error for ApiError<E> {}
1227
1228 #[derive(Debug, Error)]
1236 pub enum ApiOpError<E: std::fmt::Debug> {
1237 #[error(transparent)]
1238 Transport(#[from] HttpError),
1239
1240 #[error(transparent)]
1241 Api(ApiError<E>),
1242 }
1243
1244 impl<E: std::fmt::Debug> ApiOpError<E> {
1245 pub fn api(&self) -> Option<&ApiError<E>> {
1247 match self {
1248 Self::Api(e) => Some(e),
1249 Self::Transport(_) => None,
1250 }
1251 }
1252
1253 pub fn is_api_error(&self) -> bool {
1256 matches!(self, Self::Api(_))
1257 }
1258 }
1259
1260 impl<E: std::fmt::Debug> From<reqwest::Error> for ApiOpError<E> {
1263 fn from(e: reqwest::Error) -> Self {
1264 Self::Transport(HttpError::Network(e))
1265 }
1266 }
1267
1268 impl<E: std::fmt::Debug> From<reqwest_middleware::Error> for ApiOpError<E> {
1269 fn from(e: reqwest_middleware::Error) -> Self {
1270 Self::Transport(HttpError::Middleware(e))
1271 }
1272 }
1273
1274 pub type HttpResult<T> = Result<T, HttpError>;
1278 }
1279 }
1280
1281 fn generate_mod_file(&self, files: &[GeneratedFile]) -> Result<String> {
1283 let mut module_names = std::collections::BTreeSet::new();
1284
1285 for file in files {
1286 let module_name = if file.path.components().count() > 1 {
1287 file.path.iter().next().and_then(|part| part.to_str())
1288 } else {
1289 file.path.file_stem().and_then(|stem| stem.to_str())
1290 };
1291 if let Some(module_name) = module_name.filter(|name| *name != "mod") {
1292 module_names.insert(module_name.to_string());
1293 }
1294 }
1295 let module_declarations = module_names
1296 .iter()
1297 .map(|name| format!("pub mod {name};"))
1298 .collect::<Vec<_>>();
1299 let pub_uses = module_names
1300 .iter()
1301 .filter(|name| name.as_str() != "sse")
1304 .map(|name| format!("pub use {name}::*;"))
1305 .collect::<Vec<_>>();
1306
1307 let mount_hint = format!(
1314 "//! Configured `module_name` = `{name}`. Mount this tree under your\n\
1315 //! preferred path, e.g. `pub mod {name};` in your crate root.\n",
1316 name = self.config.module_name,
1317 );
1318 let source_hint = self
1319 .source_provenance
1320 .as_ref()
1321 .map(|source| {
1322 format!(
1323 "//! Generated by openapi-to-rust v{}. Source OpenAPI document: {source}\n",
1324 env!("CARGO_PKG_VERSION")
1325 )
1326 })
1327 .unwrap_or_default();
1328
1329 let content = format!(
1330 r#"//! Generated API modules
1331//!
1332//! This module exports all generated API types and clients.
1333//! Do not edit manually - regenerate using the appropriate script.
1334//!
1335{source_hint}
1336{mount_hint}
1337#![allow(unused_imports)]
1338
1339{decls}
1340
1341{uses}
1342"#,
1343 mount_hint = mount_hint,
1344 source_hint = source_hint,
1345 decls = module_declarations.join("\n"),
1346 uses = pub_uses.join("\n"),
1347 );
1348
1349 Ok(content)
1350 }
1351
1352 pub fn output_artifacts(
1354 &self,
1355 result: &GenerationResult,
1356 ) -> std::collections::BTreeMap<PathBuf, String> {
1357 let mut artifacts = std::collections::BTreeMap::new();
1358 for file in &result.files {
1359 artifacts.insert(file.path.clone(), file.content.clone());
1360 }
1361 artifacts.insert(
1362 result.mod_file.path.clone(),
1363 result.mod_file.content.clone(),
1364 );
1365 if let Some(mut fragment) =
1366 crate::type_mapping::render_required_deps_toml(&result.required_deps)
1367 {
1368 if let Some(source) = &self.source_provenance {
1369 let header = format!(
1370 "# Generated by openapi-to-rust v{}. Source OpenAPI document: {source}",
1371 env!("CARGO_PKG_VERSION")
1372 );
1373 fragment = fragment.replacen("# Generated by openapi-to-rust.", &header, 1);
1374 }
1375 artifacts.insert(PathBuf::from("REQUIRED_DEPS.toml"), fragment);
1376 }
1377 artifacts
1378 }
1379
1380 pub fn write_files(&self, result: &GenerationResult) -> Result<()> {
1383 use std::fs;
1384
1385 fs::create_dir_all(&self.config.output_dir)?;
1387
1388 let artifacts = self.output_artifacts(result);
1389 for (relative, content) in &artifacts {
1390 let file_path = self.config.output_dir.join(relative);
1391 if let Some(parent) = file_path.parent() {
1392 fs::create_dir_all(parent)?;
1393 }
1394 fs::write(&file_path, content)?;
1395 }
1396
1397 let deps_path = self.config.output_dir.join("REQUIRED_DEPS.toml");
1398 if !artifacts.contains_key(std::path::Path::new("REQUIRED_DEPS.toml")) && deps_path.exists()
1399 {
1400 fs::remove_file(&deps_path)?;
1401 }
1402
1403 Ok(())
1404 }
1405
1406 fn generate_type_definition(
1407 &self,
1408 schema: &crate::analysis::AnalyzedSchema,
1409 analysis: &crate::analysis::SchemaAnalysis,
1410 type_context: &TypeGenerationContext<'_>,
1411 ) -> Result<TokenStream> {
1412 use crate::analysis::SchemaType;
1413
1414 match &schema.schema_type {
1415 SchemaType::Primitive { rust_type, .. } => {
1416 self.generate_type_alias(schema, rust_type)
1418 }
1419 SchemaType::StringEnum { values } => {
1420 let ext = analysis.enum_extensions.get(&schema.name);
1421 let rust_name = self.to_rust_type_name(&schema.name);
1428 let force_extensible = self
1429 .config
1430 .extensible_enum_overrides
1431 .get(&schema.name)
1432 .or_else(|| self.config.extensible_enum_overrides.get(&rust_name))
1433 .copied()
1434 .unwrap_or(false);
1435 if force_extensible {
1436 self.generate_extensible_enum(schema, values, ext)
1437 } else {
1438 self.generate_string_enum(schema, values, ext)
1439 }
1440 }
1441 SchemaType::ExtensibleEnum { known_values } => {
1442 let ext = analysis.enum_extensions.get(&schema.name);
1443 self.generate_extensible_enum(schema, known_values, ext)
1444 }
1445 SchemaType::Object {
1446 properties,
1447 required,
1448 additional_properties,
1449 } => self.generate_struct(
1450 schema,
1451 properties,
1452 required,
1453 additional_properties,
1454 analysis,
1455 type_context,
1456 ),
1457 SchemaType::DiscriminatedUnion {
1458 discriminator_field,
1459 variants,
1460 } => {
1461 if self.should_use_untagged_discriminated_union(schema, analysis) {
1463 let schema_refs: Vec<crate::analysis::SchemaRef> = variants
1465 .iter()
1466 .map(|v| crate::analysis::SchemaRef {
1467 target: v.type_name.clone(),
1468 nullable: false,
1469 })
1470 .collect();
1471 self.generate_union_enum(schema, &schema_refs, analysis)
1472 } else {
1473 self.generate_discriminated_enum(
1474 schema,
1475 discriminator_field,
1476 variants,
1477 analysis,
1478 )
1479 }
1480 }
1481 SchemaType::Union { variants } => self.generate_union_enum(schema, variants, analysis),
1482 SchemaType::Reference { target } => {
1483 if schema.name != *target {
1486 let alias_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1488 let target_type = format_ident!("{}", self.to_rust_type_name(target));
1489
1490 let doc_comment = if let Some(desc) = &schema.description {
1491 quote! { #[doc = #desc] }
1492 } else {
1493 TokenStream::new()
1494 };
1495
1496 Ok(quote! {
1497 #doc_comment
1498 pub type #alias_name = #target_type;
1499 })
1500 } else {
1501 Ok(TokenStream::new())
1503 }
1504 }
1505 SchemaType::Array { item_type } => {
1506 let array_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1514
1515 if let SchemaType::Reference { target } = item_type.as_ref() {
1517 if let Some(info) = type_context.discriminated_variants.get(target) {
1518 if !info.is_parent_untagged {
1519 let wrapper_name =
1521 format_ident!("{}Item", self.to_rust_type_name(&schema.name));
1522 let variant_type = format_ident!("{}", self.to_rust_type_name(target));
1523 let disc_field = &info.discriminator_field;
1524 let disc_value = &info.discriminator_value;
1525
1526 let doc_comment = if let Some(desc) = &schema.description {
1527 quote! { #[doc = #desc] }
1528 } else {
1529 TokenStream::new()
1530 };
1531
1532 return Ok(quote! {
1533 #[derive(Debug, Clone, Deserialize, Serialize)]
1537 #[serde(tag = #disc_field)]
1538 pub enum #wrapper_name {
1539 #[serde(rename = #disc_value)]
1540 #variant_type(#variant_type),
1541 }
1542 #doc_comment
1543 pub type #array_name = Vec<#wrapper_name>;
1544 });
1545 }
1546 }
1547 }
1548
1549 let inner_type = self.generate_array_item_type(item_type, analysis);
1550
1551 let doc_comment = if let Some(desc) = &schema.description {
1552 quote! { #[doc = #desc] }
1553 } else {
1554 TokenStream::new()
1555 };
1556
1557 Ok(quote! {
1558 #doc_comment
1559 pub type #array_name = Vec<#inner_type>;
1560 })
1561 }
1562 SchemaType::Composition { schemas } => {
1563 self.generate_composition_struct(schema, schemas)
1564 }
1565 }
1566 }
1567
1568 fn generate_type_alias(
1569 &self,
1570 schema: &crate::analysis::AnalyzedSchema,
1571 rust_type: &str,
1572 ) -> Result<TokenStream> {
1573 let type_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1574 let base_type = parse_rust_type(rust_type)?;
1578
1579 let doc_comment = if let Some(desc) = &schema.description {
1580 let sanitized_desc = self.sanitize_doc_comment(desc);
1581 quote! { #[doc = #sanitized_desc] }
1582 } else {
1583 TokenStream::new()
1584 };
1585
1586 Ok(quote! {
1587 #doc_comment
1588 pub type #type_name = #base_type;
1589 })
1590 }
1591
1592 fn generate_extensible_enum(
1593 &self,
1594 schema: &crate::analysis::AnalyzedSchema,
1595 known_values: &[String],
1596 ext: Option<&crate::analysis::EnumExtensions>,
1597 ) -> Result<TokenStream> {
1598 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1599
1600 let doc_comment = if let Some(desc) = &schema.description {
1601 quote! { #[doc = #desc] }
1602 } else {
1603 TokenStream::new()
1604 };
1605
1606 let varnames_override: Option<&Vec<String>> = ext
1610 .filter(|_| self.config.types.x_enum_varnames_enabled())
1611 .map(|e| &e.varnames)
1612 .filter(|v| !v.is_empty() && v.len() == known_values.len());
1613 let descriptions_override: Option<&Vec<String>> = ext
1614 .filter(|_| self.config.types.x_enum_descriptions_enabled())
1615 .map(|e| &e.descriptions)
1616 .filter(|v| !v.is_empty() && v.len() == known_values.len());
1617
1618 let variant_ident_for = |index: usize, value: &str| -> proc_macro2::Ident {
1619 let name = match varnames_override {
1620 Some(v) => v[index].clone(),
1621 None => self.to_rust_enum_variant(value),
1622 };
1623 format_ident!("{}", name)
1624 };
1625
1626 let known_variants = known_values.iter().enumerate().map(|(i, value)| {
1631 let variant_ident = variant_ident_for(i, value);
1632 let doc = descriptions_override
1633 .map(|d| {
1634 let s = self.sanitize_doc_comment(&d[i]);
1635 quote! { #[doc = #s] }
1636 })
1637 .unwrap_or_default();
1638 quote! {
1639 #doc
1640 #variant_ident,
1641 }
1642 });
1643
1644 let match_arms_de = known_values.iter().enumerate().map(|(i, value)| {
1645 let variant_ident = variant_ident_for(i, value);
1646 quote! {
1647 #value => Ok(#enum_name::#variant_ident),
1648 }
1649 });
1650
1651 let match_arms_ser = known_values.iter().enumerate().map(|(i, value)| {
1652 let variant_ident = variant_ident_for(i, value);
1653 quote! {
1654 #enum_name::#variant_ident => #value,
1655 }
1656 });
1657
1658 let derives = if self.config.enable_specta {
1659 quote! {
1660 #[derive(Debug, Clone, PartialEq, Eq)]
1661 #[cfg_attr(feature = "specta", derive(specta::Type))]
1662 }
1663 } else {
1664 quote! {
1665 #[derive(Debug, Clone, PartialEq, Eq)]
1666 }
1667 };
1668
1669 Ok(quote! {
1670 #doc_comment
1671 #derives
1672 pub enum #enum_name {
1673 #(#known_variants)*
1674 Custom(String),
1676 }
1677
1678 impl<'de> serde::Deserialize<'de> for #enum_name {
1679 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1680 where
1681 D: serde::Deserializer<'de>,
1682 {
1683 let value = String::deserialize(deserializer)?;
1684 match value.as_str() {
1685 #(#match_arms_de)*
1686 _ => Ok(#enum_name::Custom(value)),
1687 }
1688 }
1689 }
1690
1691 impl serde::Serialize for #enum_name {
1692 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1693 where
1694 S: serde::Serializer,
1695 {
1696 let value = match self {
1697 #(#match_arms_ser)*
1698 #enum_name::Custom(s) => s.as_str(),
1699 };
1700 serializer.serialize_str(value)
1701 }
1702 }
1703 })
1704 }
1705
1706 fn generate_string_enum(
1707 &self,
1708 schema: &crate::analysis::AnalyzedSchema,
1709 values: &[String],
1710 ext: Option<&crate::analysis::EnumExtensions>,
1711 ) -> Result<TokenStream> {
1712 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1713
1714 let default_value = schema
1721 .default
1722 .as_ref()
1723 .and_then(|v| v.as_str())
1724 .map(|s| s.to_string());
1725 let has_default_match = match &default_value {
1726 Some(d) => values.iter().any(|v| v == d),
1727 None => !values.is_empty(),
1728 };
1729
1730 let varnames_override: Option<&Vec<String>> = ext
1734 .filter(|_| self.config.types.x_enum_varnames_enabled())
1735 .map(|e| &e.varnames)
1736 .filter(|v| !v.is_empty() && v.len() == values.len());
1737 let descriptions_override: Option<&Vec<String>> = ext
1738 .filter(|_| self.config.types.x_enum_descriptions_enabled())
1739 .map(|e| &e.descriptions)
1740 .filter(|v| !v.is_empty() && v.len() == values.len());
1741
1742 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
1749 let variant_pairs: Vec<(syn::Ident, &String, bool, Option<String>)> = values
1750 .iter()
1751 .enumerate()
1752 .map(|(i, value)| {
1753 let base = match varnames_override {
1754 Some(v) => v[i].clone(),
1755 None => self.to_rust_enum_variant(value),
1756 };
1757 let mut variant_name = base.clone();
1758 let mut suffix = 2;
1759 while !used.insert(variant_name.clone()) {
1760 variant_name = format!("{base}_{suffix}");
1761 suffix += 1;
1762 }
1763 let variant_ident = format_ident!("{}", variant_name);
1764 let is_default = if let Some(ref default) = default_value {
1765 value == default
1766 } else {
1767 i == 0
1768 };
1769 let description = descriptions_override.map(|d| d[i].clone());
1770 (variant_ident, value, is_default, description)
1771 })
1772 .collect();
1773
1774 let variants =
1775 variant_pairs
1776 .iter()
1777 .map(|(variant_ident, value, is_default, description)| {
1778 let doc = description
1779 .as_ref()
1780 .map(|d| {
1781 let s = self.sanitize_doc_comment(d);
1782 quote! { #[doc = #s] }
1783 })
1784 .unwrap_or_default();
1785 if *is_default {
1786 quote! {
1787 #doc
1788 #[default]
1789 #[serde(rename = #value)]
1790 #variant_ident,
1791 }
1792 } else {
1793 quote! {
1794 #doc
1795 #[serde(rename = #value)]
1796 #variant_ident,
1797 }
1798 }
1799 });
1800
1801 let as_str_arms = variant_pairs.iter().map(|(variant_ident, value, _, _)| {
1805 quote! { Self::#variant_ident => #value, }
1806 });
1807
1808 let doc_comment = if let Some(desc) = &schema.description {
1809 quote! { #[doc = #desc] }
1810 } else {
1811 TokenStream::new()
1812 };
1813
1814 let derives = match (self.config.enable_specta, has_default_match) {
1817 (true, true) => quote! {
1818 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
1819 #[cfg_attr(feature = "specta", derive(specta::Type))]
1820 },
1821 (true, false) => quote! {
1822 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1823 #[cfg_attr(feature = "specta", derive(specta::Type))]
1824 },
1825 (false, true) => quote! {
1826 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
1827 },
1828 (false, false) => quote! {
1829 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1830 },
1831 };
1832
1833 Ok(quote! {
1834 #doc_comment
1835 #derives
1836 pub enum #enum_name {
1837 #(#variants)*
1838 }
1839
1840 impl #enum_name {
1841 pub fn as_str(&self) -> &'static str {
1842 match self {
1843 #(#as_str_arms)*
1844 }
1845 }
1846 }
1847
1848 impl ::std::fmt::Display for #enum_name {
1849 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
1850 f.write_str(self.as_str())
1851 }
1852 }
1853
1854 impl AsRef<str> for #enum_name {
1855 fn as_ref(&self) -> &str {
1856 self.as_str()
1857 }
1858 }
1859 })
1860 }
1861
1862 fn generate_struct(
1863 &self,
1864 schema: &crate::analysis::AnalyzedSchema,
1865 properties: &BTreeMap<String, crate::analysis::PropertyInfo>,
1866 required: &std::collections::HashSet<String>,
1867 additional_properties: &crate::analysis::ObjectAdditionalProperties,
1868 analysis: &crate::analysis::SchemaAnalysis,
1869 type_context: &TypeGenerationContext<'_>,
1870 ) -> Result<TokenStream> {
1871 let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1872 let emitted_properties = self.emitted_object_properties(
1873 &schema.name,
1874 properties,
1875 required,
1876 additional_properties,
1877 analysis,
1878 type_context.discriminated_variants.get(&schema.name),
1879 );
1880
1881 let mut fields: Vec<TokenStream> = emitted_properties
1882 .iter()
1883 .map(|emitted| {
1884 let field_name = emitted.wire_name;
1885 let property = emitted.property;
1886 let field_ident = &emitted.ident;
1887 let field_type = &emitted.field_type;
1888 let serde_attrs = self.generate_serde_field_attrs(
1889 &schema.name,
1890 field_name,
1891 field_ident,
1892 property,
1893 emitted.is_required,
1894 analysis,
1895 );
1896 let specta_attrs = self.generate_specta_field_attrs(field_name);
1897
1898 let doc_comment = if let Some(desc) = &property.description {
1899 let sanitized_desc = self.sanitize_doc_comment(desc);
1900 quote! { #[doc = #sanitized_desc] }
1901 } else {
1902 TokenStream::new()
1903 };
1904 let constraint_doc = self.generate_constraint_doc(&property.constraints);
1905
1906 quote! {
1907 #doc_comment
1908 #constraint_doc
1909 #serde_attrs
1910 #specta_attrs
1911 pub #field_ident: #field_type,
1912 }
1913 })
1914 .collect();
1915
1916 match additional_properties {
1922 crate::analysis::ObjectAdditionalProperties::Forbidden => {}
1923 crate::analysis::ObjectAdditionalProperties::Untyped => {
1924 fields.push(quote! {
1925 #[serde(flatten)]
1927 pub additional_properties:
1928 std::collections::BTreeMap<String, serde_json::Value>,
1929 });
1930 }
1931 crate::analysis::ObjectAdditionalProperties::Typed { value_type } => {
1932 let value_tokens = self.generate_array_item_type(value_type, analysis);
1933 fields.push(quote! {
1934 #[serde(flatten)]
1937 pub additional_properties:
1938 std::collections::BTreeMap<String, #value_tokens>,
1939 });
1940 }
1941 }
1942
1943 let doc_comment = if let Some(desc) = &schema.description {
1944 quote! { #[doc = #desc] }
1945 } else {
1946 TokenStream::new()
1947 };
1948
1949 let can_derive_default = emitted_properties
1955 .iter()
1956 .all(|property| !property.is_required);
1957
1958 let derives = match (self.config.enable_specta, can_derive_default) {
1962 (true, true) => quote! {
1963 #[derive(Debug, Clone, Deserialize, Serialize, Default)]
1964 #[cfg_attr(feature = "specta", derive(specta::Type))]
1965 },
1966 (true, false) => quote! {
1967 #[derive(Debug, Clone, Deserialize, Serialize)]
1968 #[cfg_attr(feature = "specta", derive(specta::Type))]
1969 },
1970 (false, true) => quote! {
1971 #[derive(Debug, Clone, Deserialize, Serialize, Default)]
1972 },
1973 (false, false) => quote! {
1974 #[derive(Debug, Clone, Deserialize, Serialize)]
1975 },
1976 };
1977
1978 let builder = if type_context.index.request_body_roots.contains(&schema.name)
1979 && emitted_properties
1980 .iter()
1981 .any(|property| property.is_required)
1982 && (emitted_properties
1983 .iter()
1984 .any(|property| !property.is_required)
1985 || !matches!(
1986 additional_properties,
1987 crate::analysis::ObjectAdditionalProperties::Forbidden
1988 )) {
1989 self.generate_request_model_builder(
1990 schema,
1991 &emitted_properties,
1992 additional_properties,
1993 analysis,
1994 type_context.index,
1995 )
1996 } else {
1997 TokenStream::new()
1998 };
1999
2000 Ok(quote! {
2001 #doc_comment
2002 #derives
2003 pub struct #struct_name {
2004 #(#fields)*
2005 }
2006
2007 #builder
2008 })
2009 }
2010
2011 pub(crate) fn emitted_object_properties<'a>(
2016 &self,
2017 schema_name: &str,
2018 properties: &'a BTreeMap<String, crate::analysis::PropertyInfo>,
2019 required: &std::collections::HashSet<String>,
2020 additional_properties: &crate::analysis::ObjectAdditionalProperties,
2021 analysis: &crate::analysis::SchemaAnalysis,
2022 discriminator_info: Option<&DiscriminatedVariantInfo>,
2023 ) -> Vec<EmittedObjectProperty<'a>> {
2024 let mut sorted_properties: Vec<_> = properties.iter().collect();
2025 sorted_properties.sort_by_key(|(name, _)| name.as_str());
2026
2027 let mut used_field_idents = std::collections::HashSet::new();
2028 if !matches!(
2029 additional_properties,
2030 crate::analysis::ObjectAdditionalProperties::Forbidden
2031 ) {
2032 used_field_idents.insert("additional_properties".to_string());
2033 }
2034
2035 let mut emitted = Vec::new();
2036 for (field_name, property) in sorted_properties {
2037 if discriminator_info.is_some_and(|info| {
2038 !info.is_parent_untagged && field_name.as_str() == info.discriminator_field.as_str()
2039 }) {
2040 continue;
2041 }
2042
2043 let raw = self.to_rust_field_name(field_name);
2044 let mut chosen = raw.clone();
2045 let mut suffix = 2;
2046 while !used_field_idents.insert(chosen.clone()) {
2047 chosen = format!("{raw}_{suffix}");
2048 suffix += 1;
2049 }
2050 let is_required = required.contains(field_name);
2051 emitted.push(EmittedObjectProperty {
2052 wire_name: field_name,
2053 property,
2054 ident: Self::to_field_ident(&chosen),
2055 is_required,
2056 field_type: self.generate_field_type(
2057 schema_name,
2058 field_name,
2059 property,
2060 is_required,
2061 analysis,
2062 ),
2063 });
2064 }
2065 emitted
2066 }
2067
2068 fn type_generation_index(
2069 &self,
2070 analysis: &crate::analysis::SchemaAnalysis,
2071 ) -> TypeGenerationIndex {
2072 let reserved_type_names = analysis
2073 .schemas
2074 .keys()
2075 .map(|name| self.to_rust_type_name(name))
2076 .collect();
2077 let mut request_body_roots = std::collections::HashSet::new();
2078 for operation in analysis.operations.values() {
2079 let Some(mut current) = operation
2080 .request_body
2081 .as_ref()
2082 .and_then(crate::analysis::RequestBodyContent::schema_name)
2083 else {
2084 continue;
2085 };
2086 while request_body_roots.insert(current.to_string()) {
2087 let Some(crate::analysis::AnalyzedSchema {
2088 schema_type: crate::analysis::SchemaType::Reference { target },
2089 ..
2090 }) = analysis.schemas.get(current)
2091 else {
2092 break;
2093 };
2094 current = target;
2095 }
2096 }
2097 TypeGenerationIndex {
2098 request_body_roots,
2099 reserved_type_names,
2100 }
2101 }
2102
2103 fn generate_request_model_builder(
2104 &self,
2105 schema: &crate::analysis::AnalyzedSchema,
2106 properties: &[EmittedObjectProperty<'_>],
2107 additional_properties: &crate::analysis::ObjectAdditionalProperties,
2108 analysis: &crate::analysis::SchemaAnalysis,
2109 type_index: &TypeGenerationIndex,
2110 ) -> TokenStream {
2111 let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2112 let builder_base = format!("{}Builder", struct_name);
2113 let mut builder_name = builder_base.clone();
2114 let mut suffix = 2;
2115 while type_index.reserved_type_names.contains(&builder_name) {
2116 builder_name = format!("{builder_base}{suffix}");
2117 suffix += 1;
2118 }
2119 let builder_name = format_ident!("{builder_name}");
2120
2121 let required_parameters: Vec<TokenStream> = properties
2122 .iter()
2123 .filter(|property| property.is_required)
2124 .map(|property| {
2125 let ident = &property.ident;
2126 let field_type = &property.field_type;
2127 quote! { #ident: #field_type }
2128 })
2129 .collect();
2130 let required_idents: Vec<&syn::Ident> = properties
2131 .iter()
2132 .filter(|property| property.is_required)
2133 .map(|property| &property.ident)
2134 .collect();
2135 let optional_initializers: Vec<TokenStream> = properties
2136 .iter()
2137 .filter(|property| !property.is_required)
2138 .map(|property| {
2139 let ident = &property.ident;
2140 quote! { #ident: None }
2141 })
2142 .collect();
2143
2144 let additional_initializer = match additional_properties {
2145 crate::analysis::ObjectAdditionalProperties::Forbidden => TokenStream::new(),
2146 crate::analysis::ObjectAdditionalProperties::Untyped
2147 | crate::analysis::ObjectAdditionalProperties::Typed { .. } => quote! {
2148 additional_properties: ::std::collections::BTreeMap::new(),
2149 },
2150 };
2151
2152 let mut used_builder_methods =
2153 std::collections::HashSet::from(["new".to_string(), "build".to_string()]);
2154 if !matches!(
2155 additional_properties,
2156 crate::analysis::ObjectAdditionalProperties::Forbidden
2157 ) {
2158 used_builder_methods.insert("additional_properties".to_string());
2159 }
2160 let optional_setters: Vec<TokenStream> = properties
2161 .iter()
2162 .filter(|property| !property.is_required)
2163 .map(|property| {
2164 let field_ident = &property.ident;
2165 let field_type = self.generate_property_base_type(
2166 &schema.name,
2167 property.wire_name,
2168 property.property,
2169 analysis,
2170 );
2171 let field_name = field_ident.to_string();
2175 let plain_field_name = field_name.strip_prefix("r#").unwrap_or(&field_name);
2176 let mut setter_name = if matches!(plain_field_name, "new" | "build") {
2177 format!("with_{plain_field_name}")
2178 } else {
2179 field_name.clone()
2180 };
2181 let setter_base = setter_name.clone();
2182 let mut suffix = 2;
2183 while !used_builder_methods.insert(setter_name.clone()) {
2184 setter_name = format!("{setter_base}_{suffix}");
2185 suffix += 1;
2186 }
2187 let setter_ident = Self::to_field_ident(&setter_name);
2188 let wire_name = property.wire_name;
2189 quote! {
2190 #[doc = concat!("Set the optional `", #wire_name, "` request field.")]
2191 #[must_use]
2192 pub fn #setter_ident(mut self, #field_ident: #field_type) -> Self {
2193 self.value.#field_ident = Some(#field_ident);
2194 self
2195 }
2196 }
2197 })
2198 .collect();
2199
2200 let additional_setter = match additional_properties {
2201 crate::analysis::ObjectAdditionalProperties::Forbidden => TokenStream::new(),
2202 crate::analysis::ObjectAdditionalProperties::Untyped => quote! {
2203 #[must_use]
2205 pub fn additional_properties(
2206 mut self,
2207 additional_properties: ::std::collections::BTreeMap<
2208 String,
2209 serde_json::Value,
2210 >,
2211 ) -> Self {
2212 self.value.additional_properties = additional_properties;
2213 self
2214 }
2215 },
2216 crate::analysis::ObjectAdditionalProperties::Typed { value_type } => {
2217 let value_type = self.generate_array_item_type(value_type, analysis);
2218 quote! {
2219 #[must_use]
2221 pub fn additional_properties(
2222 mut self,
2223 additional_properties: ::std::collections::BTreeMap<
2224 String,
2225 #value_type,
2226 >,
2227 ) -> Self {
2228 self.value.additional_properties = additional_properties;
2229 self
2230 }
2231 }
2232 }
2233 };
2234
2235 quote! {
2236 impl #struct_name {
2237 pub fn new(#(#required_parameters),*) -> Self {
2239 Self {
2240 #(#required_idents,)*
2241 #(#optional_initializers,)*
2242 #additional_initializer
2243 }
2244 }
2245
2246 pub fn builder(#(#required_parameters),*) -> #builder_name {
2248 #builder_name::new(#(#required_idents),*)
2249 }
2250 }
2251
2252 #[derive(Debug, Clone)]
2254 #[must_use]
2255 pub struct #builder_name {
2256 value: #struct_name,
2257 }
2258
2259 impl #builder_name {
2260 pub fn new(#(#required_parameters),*) -> Self {
2262 Self {
2263 value: #struct_name::new(#(#required_idents),*),
2264 }
2265 }
2266
2267 #(#optional_setters)*
2268 #additional_setter
2269
2270 pub fn build(self) -> #struct_name {
2272 self.value
2273 }
2274 }
2275 }
2276 }
2277
2278 fn generate_discriminated_enum(
2279 &self,
2280 schema: &crate::analysis::AnalyzedSchema,
2281 discriminator_field: &str,
2282 variants: &[crate::analysis::UnionVariant],
2283 analysis: &crate::analysis::SchemaAnalysis,
2284 ) -> Result<TokenStream> {
2285 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2286
2287 let has_nested_discriminated_union = variants.iter().any(|variant| {
2289 if let Some(variant_schema) = analysis.schemas.get(&variant.type_name) {
2290 matches!(
2291 variant_schema.schema_type,
2292 crate::analysis::SchemaType::DiscriminatedUnion { .. }
2293 )
2294 } else {
2295 false
2296 }
2297 });
2298
2299 if has_nested_discriminated_union {
2301 let schema_refs: Vec<crate::analysis::SchemaRef> = variants
2303 .iter()
2304 .map(|v| crate::analysis::SchemaRef {
2305 target: v.type_name.clone(),
2306 nullable: false,
2307 })
2308 .collect();
2309 return self.generate_union_enum(schema, &schema_refs, analysis);
2310 }
2311
2312 let enclosing = self.to_rust_type_name(&schema.name);
2313 let enum_variants = variants.iter().map(|variant| {
2314 let variant_name = format_ident!("{}", variant.rust_name);
2315 let variant_value = &variant.discriminator_value;
2316
2317 let variant_type = format_ident!("{}", self.to_rust_type_name(&variant.type_name));
2318 let payload = if self.to_rust_type_name(&variant.type_name) == enclosing
2322 || analysis
2323 .dependencies
2324 .recursive_schemas
2325 .contains(&variant.type_name)
2326 {
2327 quote! { Box<#variant_type> }
2328 } else {
2329 quote! { #variant_type }
2330 };
2331 quote! {
2332 #[serde(rename = #variant_value)]
2333 #variant_name(#payload),
2334 }
2335 });
2336
2337 let doc_comment = if let Some(desc) = &schema.description {
2338 quote! { #[doc = #desc] }
2339 } else {
2340 TokenStream::new()
2341 };
2342
2343 let derives = if self.config.enable_specta {
2345 quote! {
2346 #[derive(Debug, Clone, Deserialize, Serialize)]
2347 #[cfg_attr(feature = "specta", derive(specta::Type))]
2348 #[serde(tag = #discriminator_field)]
2349 }
2350 } else {
2351 quote! {
2352 #[derive(Debug, Clone, Deserialize, Serialize)]
2353 #[serde(tag = #discriminator_field)]
2354 }
2355 };
2356
2357 Ok(quote! {
2358 #doc_comment
2359 #derives
2360 pub enum #enum_name {
2361 #(#enum_variants)*
2362 }
2363 })
2364 }
2365
2366 fn should_use_untagged_discriminated_union(
2368 &self,
2369 schema: &crate::analysis::AnalyzedSchema,
2370 analysis: &crate::analysis::SchemaAnalysis,
2371 ) -> bool {
2372 for other_schema in analysis.schemas.values() {
2377 if let crate::analysis::SchemaType::DiscriminatedUnion {
2378 variants,
2379 discriminator_field: _,
2380 } = &other_schema.schema_type
2381 {
2382 for variant in variants {
2383 if variant.type_name == schema.name {
2384 if let crate::analysis::SchemaType::DiscriminatedUnion {
2389 discriminator_field: current_discriminator,
2390 variants: current_variants,
2391 ..
2392 } = &schema.schema_type
2393 {
2394 for current_variant in current_variants {
2396 if let Some(variant_schema) =
2397 analysis.schemas.get(¤t_variant.type_name)
2398 {
2399 if let crate::analysis::SchemaType::Object {
2400 properties, ..
2401 } = &variant_schema.schema_type
2402 {
2403 if properties.contains_key(current_discriminator) {
2404 return false;
2407 }
2408 }
2409 }
2410 }
2411 }
2412
2413 return true;
2415 }
2416 }
2417 }
2418 }
2419 false
2420 }
2421
2422 fn generate_union_enum(
2423 &self,
2424 schema: &crate::analysis::AnalyzedSchema,
2425 variants: &[crate::analysis::SchemaRef],
2426 analysis: &crate::analysis::SchemaAnalysis,
2427 ) -> Result<TokenStream> {
2428 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2429
2430 let mut used_variant_names = std::collections::HashSet::new();
2432 let enum_variants = variants.iter().enumerate().map(|(i, variant)| {
2433 let base_variant_name = self.type_name_to_variant_name(&variant.target);
2435 let variant_name = self.ensure_unique_variant_name_generator(
2436 base_variant_name,
2437 &mut used_variant_names,
2438 i,
2439 );
2440 let variant_name_ident = format_ident!("{}", variant_name);
2441
2442 let variant_type_tokens = if matches!(
2444 variant.target.as_str(),
2445 "bool"
2446 | "i8"
2447 | "i16"
2448 | "i32"
2449 | "i64"
2450 | "i128"
2451 | "u8"
2452 | "u16"
2453 | "u32"
2454 | "u64"
2455 | "u128"
2456 | "f32"
2457 | "f64"
2458 | "String"
2459 ) {
2460 let type_ident = format_ident!("{}", variant.target);
2461 quote! { #type_ident }
2462 } else if variant.target == "serde_json::Value" {
2463 quote! { serde_json::Value }
2466 } else if variant.target.starts_with("Vec<") && variant.target.ends_with(">") {
2467 let inner = &variant.target[4..variant.target.len() - 1];
2469
2470 if inner.starts_with("Vec<") && inner.ends_with(">") {
2472 let inner_inner = &inner[4..inner.len() - 1];
2473 if inner_inner == "serde_json::Value" {
2474 quote! { Vec<Vec<serde_json::Value>> }
2475 } else {
2476 let inner_inner_type = if matches!(
2477 inner_inner,
2478 "bool"
2479 | "i8"
2480 | "i16"
2481 | "i32"
2482 | "i64"
2483 | "i128"
2484 | "u8"
2485 | "u16"
2486 | "u32"
2487 | "u64"
2488 | "u128"
2489 | "f32"
2490 | "f64"
2491 | "String"
2492 ) {
2493 format_ident!("{}", inner_inner)
2494 } else {
2495 format_ident!("{}", self.to_rust_type_name(inner_inner))
2496 };
2497 quote! { Vec<Vec<#inner_inner_type>> }
2498 }
2499 } else if inner == "serde_json::Value" {
2500 quote! { Vec<serde_json::Value> }
2501 } else {
2502 let inner_type = if matches!(
2503 inner,
2504 "bool"
2505 | "i8"
2506 | "i16"
2507 | "i32"
2508 | "i64"
2509 | "i128"
2510 | "u8"
2511 | "u16"
2512 | "u32"
2513 | "u64"
2514 | "u128"
2515 | "f32"
2516 | "f64"
2517 | "String"
2518 ) {
2519 format_ident!("{}", inner)
2520 } else {
2521 format_ident!("{}", self.to_rust_type_name(inner))
2522 };
2523 quote! { Vec<#inner_type> }
2524 }
2525 } else if variant.target.contains("::") || variant.target.contains('<') {
2526 parse_rust_type(&variant.target).unwrap_or_else(|_| {
2531 let fallback = format_ident!("{}", self.to_rust_type_name(&variant.target));
2532 quote! { #fallback }
2533 })
2534 } else {
2535 let type_ident = format_ident!("{}", self.to_rust_type_name(&variant.target));
2536 quote! { #type_ident }
2537 };
2538
2539 let target_rust_name = self.to_rust_type_name(&variant.target);
2543 let enclosing_name = self.to_rust_type_name(&schema.name);
2544 let is_self_ref = target_rust_name == enclosing_name;
2545 let is_recursive_target = analysis
2549 .dependencies
2550 .recursive_schemas
2551 .contains(&variant.target);
2552 let variant_type_tokens = if is_self_ref || is_recursive_target {
2553 quote! { Box<#variant_type_tokens> }
2554 } else {
2555 variant_type_tokens
2556 };
2557
2558 quote! {
2559 #variant_name_ident(#variant_type_tokens),
2560 }
2561 });
2562
2563 let doc_comment = if let Some(desc) = &schema.description {
2564 quote! { #[doc = #desc] }
2565 } else {
2566 TokenStream::new()
2567 };
2568
2569 let derives = if self.config.enable_specta {
2571 quote! {
2572 #[derive(Debug, Clone, Deserialize, Serialize)]
2573 #[cfg_attr(feature = "specta", derive(specta::Type))]
2574 #[serde(untagged)]
2575 }
2576 } else {
2577 quote! {
2578 #[derive(Debug, Clone, Deserialize, Serialize)]
2579 #[serde(untagged)]
2580 }
2581 };
2582
2583 Ok(quote! {
2584 #doc_comment
2585 #derives
2586 pub enum #enum_name {
2587 #(#enum_variants)*
2588 }
2589 })
2590 }
2591
2592 fn target_aliases_back_to(
2597 &self,
2598 target: &str,
2599 enclosing_rust_name: &str,
2600 analysis: &crate::analysis::SchemaAnalysis,
2601 ) -> bool {
2602 let mut current = target.to_string();
2603 let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
2604 for _ in 0..16 {
2605 if !visited.insert(current.clone()) {
2606 return true;
2607 }
2608 let Some(schema) = analysis.schemas.get(¤t) else {
2609 return false;
2610 };
2611 if let crate::analysis::SchemaType::Reference { target: next } = &schema.schema_type {
2612 if self.to_rust_type_name(next) == enclosing_rust_name {
2613 return true;
2614 }
2615 current = next.clone();
2616 continue;
2617 }
2618 return false;
2619 }
2620 false
2621 }
2622
2623 fn generate_field_type(
2624 &self,
2625 schema_name: &str,
2626 field_name: &str,
2627 prop: &crate::analysis::PropertyInfo,
2628 is_required: bool,
2629 analysis: &crate::analysis::SchemaAnalysis,
2630 ) -> TokenStream {
2631 let base_type = self.generate_property_base_type(schema_name, field_name, prop, analysis);
2632
2633 if self.property_is_option_wrapped(schema_name, field_name, prop, is_required, analysis) {
2634 quote! { Option<#base_type> }
2635 } else {
2636 base_type
2637 }
2638 }
2639
2640 fn property_is_option_wrapped(
2641 &self,
2642 schema_name: &str,
2643 field_name: &str,
2644 prop: &crate::analysis::PropertyInfo,
2645 is_required: bool,
2646 analysis: &crate::analysis::SchemaAnalysis,
2647 ) -> bool {
2648 let override_key = format!("{schema_name}.{field_name}");
2649 let is_nullable_override = self
2650 .config
2651 .nullable_field_overrides
2652 .get(&override_key)
2653 .copied()
2654 .unwrap_or(false);
2655
2656 !is_required
2657 || prop.nullable
2658 || is_nullable_override
2659 || (prop.default.is_some() && self.type_lacks_default(&prop.schema_type, analysis))
2660 }
2661
2662 pub(crate) fn generate_property_base_type(
2663 &self,
2664 schema_name: &str,
2665 _field_name: &str,
2666 prop: &crate::analysis::PropertyInfo,
2667 analysis: &crate::analysis::SchemaAnalysis,
2668 ) -> TokenStream {
2669 use crate::analysis::SchemaType;
2670
2671 match &prop.schema_type {
2672 SchemaType::Primitive { rust_type, .. } => {
2673 parse_rust_type(rust_type).unwrap_or_else(|_| {
2676 eprintln!(
2681 "⚠️ TypeMapper produced un-parseable type `{rust_type}`; \
2682 falling back to String"
2683 );
2684 quote! { String }
2685 })
2686 }
2687 SchemaType::Reference { target } => {
2688 let target_rust_name = self.to_rust_type_name(target);
2689 let target_type = format_ident!("{}", target_rust_name);
2690 let enclosing_rust_name = self.to_rust_type_name(schema_name);
2703 let is_self_via_rust_name = target_rust_name == enclosing_rust_name;
2704 let is_alias_chain_self =
2705 self.target_aliases_back_to(target, &enclosing_rust_name, analysis);
2706 if analysis.dependencies.recursive_schemas.contains(target)
2707 || is_self_via_rust_name
2708 || is_alias_chain_self
2709 {
2710 quote! { Box<#target_type> }
2711 } else {
2712 quote! { #target_type }
2713 }
2714 }
2715 SchemaType::Array { item_type } => {
2716 let inner_type = self.generate_array_item_type(item_type, analysis);
2717 quote! { Vec<#inner_type> }
2718 }
2719 _ => {
2720 quote! { serde_json::Value }
2722 }
2723 }
2724 }
2725
2726 fn generate_serde_field_attrs(
2727 &self,
2728 schema_name: &str,
2729 field_name: &str,
2730 field_ident: &syn::Ident,
2731 prop: &crate::analysis::PropertyInfo,
2732 is_required: bool,
2733 analysis: &crate::analysis::SchemaAnalysis,
2734 ) -> TokenStream {
2735 let mut attrs = Vec::new();
2736
2737 let rust_field_name = field_ident.to_string();
2740 let comparison_name = rust_field_name
2741 .strip_prefix("r#")
2742 .unwrap_or(&rust_field_name);
2743 if comparison_name != field_name {
2744 attrs.push(quote! { rename = #field_name });
2745 }
2746
2747 if !is_required || prop.nullable {
2749 attrs.push(quote! { skip_serializing_if = "Option::is_none" });
2750 }
2751
2752 if prop.default.is_some()
2756 && (is_required && !prop.nullable)
2757 && !self.type_lacks_default(&prop.schema_type, analysis)
2758 {
2759 attrs.push(quote! { default });
2760 }
2761
2762 if let crate::analysis::SchemaType::Primitive {
2770 serde_with: Some(codec),
2771 ..
2772 } = &prop.schema_type
2773 {
2774 let is_option_wrapped = self.property_is_option_wrapped(
2775 schema_name,
2776 field_name,
2777 prop,
2778 is_required,
2779 analysis,
2780 );
2781 let codec_path = if is_option_wrapped {
2782 format!("{codec}::option")
2783 } else {
2784 codec.clone()
2785 };
2786 attrs.push(quote! { with = #codec_path });
2787 if is_option_wrapped {
2792 attrs.push(quote! { default });
2793 }
2794 }
2795
2796 if attrs.is_empty() {
2797 TokenStream::new()
2798 } else {
2799 quote! { #[serde(#(#attrs),*)] }
2800 }
2801 }
2802
2803 fn type_lacks_default(
2807 &self,
2808 schema_type: &crate::analysis::SchemaType,
2809 analysis: &crate::analysis::SchemaAnalysis,
2810 ) -> bool {
2811 use crate::analysis::SchemaType;
2812 match schema_type {
2813 SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => true,
2814 SchemaType::Primitive { rust_type, .. } => matches!(
2818 rust_type.as_str(),
2819 "chrono::DateTime<chrono::Utc>"
2820 | "chrono::NaiveDate"
2821 | "chrono::NaiveTime"
2822 | "chrono::Duration"
2823 | "url::Url"
2824 | "time::OffsetDateTime"
2825 | "time::Date"
2826 | "time::Time"
2827 | "iso8601::Duration"
2828 | "email_address::EmailAddress"
2829 ),
2830 SchemaType::Reference { target } => {
2831 if let Some(schema) = analysis.schemas.get(target) {
2832 self.type_lacks_default(&schema.schema_type, analysis)
2833 } else {
2834 false
2835 }
2836 }
2837 _ => false,
2838 }
2839 }
2840
2841 fn generate_specta_field_attrs(&self, field_name: &str) -> TokenStream {
2842 if !self.config.enable_specta {
2843 return TokenStream::new();
2844 }
2845
2846 let camel_case_name = self.to_camel_case(field_name);
2848
2849 if camel_case_name != field_name {
2851 quote! { #[cfg_attr(feature = "specta", specta(rename = #camel_case_name))] }
2852 } else {
2853 TokenStream::new()
2854 }
2855 }
2856
2857 pub(crate) fn to_rust_enum_variant(&self, s: &str) -> String {
2858 let neg_prefix =
2862 if s.starts_with('-') && s.chars().skip(1).all(|c| c.is_ascii_digit() || c == '.') {
2863 "Neg"
2864 } else {
2865 ""
2866 };
2867
2868 let mut result = String::new();
2870 let mut next_upper = true;
2871 let mut prev_was_upper = false;
2872
2873 for (i, c) in s.chars().enumerate() {
2874 match c {
2875 'a'..='z' => {
2876 if next_upper {
2877 result.push(c.to_ascii_uppercase());
2878 next_upper = false;
2879 } else {
2880 result.push(c);
2881 }
2882 prev_was_upper = false;
2883 }
2884 'A'..='Z' => {
2885 if next_upper || (!prev_was_upper && i > 0) {
2886 result.push(c);
2888 next_upper = false;
2889 } else {
2890 result.push(c.to_ascii_lowercase());
2892 }
2893 prev_was_upper = true;
2894 }
2895 '0'..='9' => {
2896 result.push(c);
2897 next_upper = false;
2898 prev_was_upper = false;
2899 }
2900 '.' | '-' | '_' | ' ' | '@' | '#' | '$' | '/' | '\\' => {
2901 next_upper = true;
2903 prev_was_upper = false;
2904 }
2905 _ => {
2906 next_upper = true;
2908 prev_was_upper = false;
2909 }
2910 }
2911 }
2912
2913 if result.is_empty() {
2915 result = "Value".to_string();
2916 }
2917
2918 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2920 result = format!("Variant{neg_prefix}{result}");
2921 } else if !neg_prefix.is_empty() {
2922 result = format!("{neg_prefix}{result}");
2925 }
2926
2927 match result.as_str() {
2929 "Null" => "NullValue".to_string(),
2930 "True" => "TrueValue".to_string(),
2931 "False" => "FalseValue".to_string(),
2932 "Type" => "Type_".to_string(),
2933 "Match" => "Match_".to_string(),
2934 "Fn" => "Fn_".to_string(),
2935 "Impl" => "Impl_".to_string(),
2936 "Trait" => "Trait_".to_string(),
2937 "Struct" => "Struct_".to_string(),
2938 "Enum" => "Enum_".to_string(),
2939 "Mod" => "Mod_".to_string(),
2940 "Use" => "Use_".to_string(),
2941 "Pub" => "Pub_".to_string(),
2942 "Const" => "Const_".to_string(),
2943 "Static" => "Static_".to_string(),
2944 "Let" => "Let_".to_string(),
2945 "Mut" => "Mut_".to_string(),
2946 "Ref" => "Ref_".to_string(),
2947 "Move" => "Move_".to_string(),
2948 "Return" => "Return_".to_string(),
2949 "If" => "If_".to_string(),
2950 "Else" => "Else_".to_string(),
2951 "While" => "While_".to_string(),
2952 "For" => "For_".to_string(),
2953 "Loop" => "Loop_".to_string(),
2954 "Break" => "Break_".to_string(),
2955 "Continue" => "Continue_".to_string(),
2956 "Self" => "Self_".to_string(),
2957 "Super" => "Super_".to_string(),
2958 "Crate" => "Crate_".to_string(),
2959 "Async" => "Async_".to_string(),
2960 "Await" => "Await_".to_string(),
2961 _ => result,
2962 }
2963 }
2964
2965 #[allow(dead_code)]
2966 fn to_rust_identifier(&self, s: &str) -> String {
2967 let mut result = s
2969 .chars()
2970 .map(|c| match c {
2971 'a'..='z' | 'A'..='Z' | '0'..='9' => c,
2972 '.' | '-' | '_' | ' ' | '@' | '#' | '$' | '/' | '\\' => '_',
2973 _ => '_',
2974 })
2975 .collect::<String>();
2976
2977 result = result.trim_matches('_').to_string();
2979
2980 if result.is_empty() {
2982 result = "value".to_string();
2983 }
2984
2985 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2987 result = format!("variant_{result}");
2988 }
2989
2990 match result.as_str() {
2992 "null" => "null_value".to_string(),
2993 "true" => "true_value".to_string(),
2994 "false" => "false_value".to_string(),
2995 "type" => "type_".to_string(),
2996 "match" => "match_".to_string(),
2997 "fn" => "fn_".to_string(),
2998 "impl" => "impl_".to_string(),
2999 "trait" => "trait_".to_string(),
3000 "struct" => "struct_".to_string(),
3001 "enum" => "enum_".to_string(),
3002 "mod" => "mod_".to_string(),
3003 "use" => "use_".to_string(),
3004 "pub" => "pub_".to_string(),
3005 "const" => "const_".to_string(),
3006 "static" => "static_".to_string(),
3007 "let" => "let_".to_string(),
3008 "mut" => "mut_".to_string(),
3009 "ref" => "ref_".to_string(),
3010 "move" => "move_".to_string(),
3011 "return" => "return_".to_string(),
3012 "if" => "if_".to_string(),
3013 "else" => "else_".to_string(),
3014 "while" => "while_".to_string(),
3015 "for" => "for_".to_string(),
3016 "loop" => "loop_".to_string(),
3017 "break" => "break_".to_string(),
3018 "continue" => "continue_".to_string(),
3019 "self" => "self_".to_string(),
3020 "super" => "super_".to_string(),
3021 "crate" => "crate_".to_string(),
3022 "async" => "async_".to_string(),
3023 "await" => "await_".to_string(),
3024 "override" => "override_".to_string(),
3026 "box" => "box_".to_string(),
3027 "dyn" => "dyn_".to_string(),
3028 "where" => "where_".to_string(),
3029 "in" => "in_".to_string(),
3030 "abstract" => "abstract_".to_string(),
3032 "become" => "become_".to_string(),
3033 "do" => "do_".to_string(),
3034 "final" => "final_".to_string(),
3035 "macro" => "macro_".to_string(),
3036 "priv" => "priv_".to_string(),
3037 "try" => "try_".to_string(),
3038 "typeof" => "typeof_".to_string(),
3039 "unsized" => "unsized_".to_string(),
3040 "virtual" => "virtual_".to_string(),
3041 "yield" => "yield_".to_string(),
3042 _ => result,
3043 }
3044 }
3045
3046 fn generate_constraint_doc(
3054 &self,
3055 constraints: &crate::analysis::PropertyConstraints,
3056 ) -> TokenStream {
3057 use crate::type_mapping::ConstraintMode;
3058
3059 if constraints.is_empty() {
3060 return TokenStream::new();
3061 }
3062 match self.config.types.constraint_mode() {
3063 ConstraintMode::Off => TokenStream::new(),
3064 ConstraintMode::Doc => {
3065 let formatted = format_constraints_doc(constraints);
3066 quote! { #[doc = #formatted] }
3067 }
3068 }
3069 }
3070
3071 fn sanitize_doc_comment(&self, desc: &str) -> String {
3072 let mut result = desc.to_string();
3074
3075 if result.contains('\n')
3083 && (result.contains('{')
3084 || result.contains("```")
3085 || result.contains("Human:")
3086 || result.contains("Assistant:")
3087 || result
3088 .lines()
3089 .any(|line| line.trim().starts_with('"') && line.trim().ends_with('"')))
3090 {
3091 if result.contains("```") {
3093 result = result.replace("```", "```ignore");
3094 } else {
3095 if result.lines().any(|line| {
3097 let trimmed = line.trim();
3098 trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() > 2
3099 }) {
3100 result = format!("```ignore\n{result}\n```");
3101 }
3102 }
3103 }
3104
3105 result
3106 }
3107
3108 pub(crate) fn to_rust_type_name(&self, s: &str) -> String {
3109 rust_type_name(s)
3110 }
3111
3112 pub(crate) fn to_rust_field_name(&self, s: &str) -> String {
3113 let leading_marker = match s.chars().next() {
3117 Some('-') if s.len() > 1 => "neg_",
3118 Some('+') if s.len() > 1 => "pos_",
3119 _ => "",
3120 };
3121
3122 let mut result = String::new();
3124 let mut prev_was_upper = false;
3125 let mut prev_was_underscore = false;
3126
3127 for (i, c) in s.chars().enumerate() {
3128 match c {
3129 'A'..='Z' => {
3130 if i > 0 && !prev_was_upper && !prev_was_underscore {
3132 result.push('_');
3133 }
3134 result.push(c.to_ascii_lowercase());
3135 prev_was_upper = true;
3136 prev_was_underscore = false;
3137 }
3138 'a'..='z' | '0'..='9' => {
3139 result.push(c);
3140 prev_was_upper = false;
3141 prev_was_underscore = false;
3142 }
3143 '-' | '.' | '_' | '@' | '#' | '$' | ' ' => {
3144 if !prev_was_underscore && !result.is_empty() {
3145 result.push('_');
3146 prev_was_underscore = true;
3147 }
3148 prev_was_upper = false;
3149 }
3150 _ => {
3151 if !prev_was_underscore && !result.is_empty() {
3153 result.push('_');
3154 }
3155 prev_was_upper = false;
3156 prev_was_underscore = true;
3157 }
3158 }
3159 }
3160
3161 let mut result = result.trim_matches('_').to_string();
3163 if result.is_empty() {
3164 return "field".to_string();
3165 }
3166
3167 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
3169 result = format!("field_{leading_marker}{result}");
3170 } else if !leading_marker.is_empty() {
3171 result = format!("{leading_marker}{result}");
3172 }
3173
3174 if matches!(result.as_str(), "self" | "super" | "crate" | "Self") {
3178 return format!("{result}_field");
3179 }
3180 if Self::is_rust_keyword(&result) {
3182 format!("r#{result}")
3183 } else {
3184 result
3185 }
3186 }
3187
3188 pub fn is_rust_keyword(s: &str) -> bool {
3190 matches!(
3191 s,
3192 "type"
3193 | "match"
3194 | "fn"
3195 | "struct"
3196 | "enum"
3197 | "impl"
3198 | "trait"
3199 | "mod"
3200 | "use"
3201 | "pub"
3202 | "const"
3203 | "static"
3204 | "let"
3205 | "mut"
3206 | "ref"
3207 | "move"
3208 | "return"
3209 | "if"
3210 | "else"
3211 | "while"
3212 | "for"
3213 | "loop"
3214 | "break"
3215 | "continue"
3216 | "self"
3217 | "super"
3218 | "crate"
3219 | "async"
3220 | "await"
3221 | "override"
3222 | "box"
3223 | "dyn"
3224 | "where"
3225 | "in"
3226 | "abstract"
3227 | "become"
3228 | "do"
3229 | "final"
3230 | "macro"
3231 | "priv"
3232 | "try"
3233 | "typeof"
3234 | "unsized"
3235 | "virtual"
3236 | "yield"
3237 | "gen"
3239 )
3240 }
3241
3242 pub fn to_field_ident(name: &str) -> proc_macro2::Ident {
3244 if let Some(raw) = name.strip_prefix("r#") {
3245 proc_macro2::Ident::new_raw(raw, proc_macro2::Span::call_site())
3246 } else {
3247 proc_macro2::Ident::new(name, proc_macro2::Span::call_site())
3248 }
3249 }
3250
3251 fn to_camel_case(&self, s: &str) -> String {
3252 let mut result = String::new();
3254 let mut capitalize_next = false;
3255
3256 for (i, c) in s.chars().enumerate() {
3257 match c {
3258 '_' | '-' | '.' | ' ' => {
3259 capitalize_next = true;
3261 }
3262 'A'..='Z' => {
3263 if i == 0 {
3264 result.push(c.to_ascii_lowercase());
3266 } else if capitalize_next {
3267 result.push(c);
3268 capitalize_next = false;
3269 } else {
3270 result.push(c.to_ascii_lowercase());
3271 }
3272 }
3273 'a'..='z' | '0'..='9' => {
3274 if capitalize_next {
3275 result.push(c.to_ascii_uppercase());
3276 capitalize_next = false;
3277 } else {
3278 result.push(c);
3279 }
3280 }
3281 _ => {
3282 capitalize_next = true;
3284 }
3285 }
3286 }
3287
3288 if result.is_empty() {
3289 return "field".to_string();
3290 }
3291
3292 result
3293 }
3294
3295 fn generate_composition_struct(
3296 &self,
3297 schema: &crate::analysis::AnalyzedSchema,
3298 schemas: &[crate::analysis::SchemaRef],
3299 ) -> Result<TokenStream> {
3300 let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
3301
3302 let fields = schemas.iter().enumerate().map(|(i, schema_ref)| {
3308 let field_name = format_ident!("part_{}", i);
3309 let field_type = format_ident!("{}", self.to_rust_type_name(&schema_ref.target));
3310
3311 quote! {
3312 #[serde(flatten)]
3313 pub #field_name: #field_type,
3314 }
3315 });
3316
3317 let doc_comment = if let Some(desc) = &schema.description {
3318 quote! { #[doc = #desc] }
3319 } else {
3320 TokenStream::new()
3321 };
3322
3323 let derives = if self.config.enable_specta {
3325 quote! {
3326 #[derive(Debug, Clone, Deserialize, Serialize)]
3327 #[cfg_attr(feature = "specta", derive(specta::Type))]
3328 }
3329 } else {
3330 quote! {
3331 #[derive(Debug, Clone, Deserialize, Serialize)]
3332 }
3333 };
3334
3335 Ok(quote! {
3336 #doc_comment
3337 #derives
3338 pub struct #struct_name {
3339 #(#fields)*
3340 }
3341 })
3342 }
3343
3344 #[allow(dead_code)]
3345 fn find_missing_types(&self, analysis: &SchemaAnalysis) -> std::collections::HashSet<String> {
3346 let mut missing = std::collections::HashSet::new();
3347 let defined_types: std::collections::HashSet<String> =
3348 analysis.schemas.keys().cloned().collect();
3349
3350 for schema in analysis.schemas.values() {
3352 match &schema.schema_type {
3353 crate::analysis::SchemaType::Union { variants } => {
3354 for variant in variants {
3355 if !defined_types.contains(&variant.target) {
3356 missing.insert(variant.target.clone());
3357 }
3358 }
3359 }
3360 crate::analysis::SchemaType::DiscriminatedUnion { variants, .. } => {
3361 for variant in variants {
3362 if !defined_types.contains(&variant.type_name) {
3363 missing.insert(variant.type_name.clone());
3364 }
3365 }
3366 }
3367 crate::analysis::SchemaType::Object { properties, .. } => {
3368 let mut sorted_props: Vec<_> = properties.iter().collect();
3370 sorted_props.sort_by_key(|(name, _)| name.as_str());
3371 for (_, prop) in sorted_props {
3372 if let crate::analysis::SchemaType::Reference { target } = &prop.schema_type
3373 {
3374 if !defined_types.contains(target) {
3375 missing.insert(target.clone());
3376 }
3377 }
3378 }
3379 }
3380 crate::analysis::SchemaType::Reference { target }
3381 if !defined_types.contains(target) =>
3382 {
3383 missing.insert(target.clone());
3384 }
3385 _ => {}
3386 }
3387 }
3388
3389 missing
3390 }
3391
3392 #[allow(clippy::only_used_in_recursion)]
3393 fn generate_array_item_type(
3394 &self,
3395 item_type: &crate::analysis::SchemaType,
3396 analysis: &crate::analysis::SchemaAnalysis,
3397 ) -> TokenStream {
3398 use crate::analysis::SchemaType;
3399
3400 match item_type {
3401 SchemaType::Primitive { rust_type, .. } => {
3402 if let Ok(parsed) = syn::parse_str::<syn::Type>(rust_type) {
3407 quote! { #parsed }
3408 } else if rust_type.contains("::") {
3409 let parts: Vec<_> = rust_type
3410 .split("::")
3411 .map(|p| format_ident!("{}", p))
3412 .collect();
3413 quote! { #(#parts)::* }
3414 } else {
3415 let type_ident = format_ident!("{}", rust_type);
3416 quote! { #type_ident }
3417 }
3418 }
3419 SchemaType::Reference { target } => {
3420 let target_type = format_ident!("{}", self.to_rust_type_name(target));
3421 if analysis.dependencies.recursive_schemas.contains(target) {
3423 quote! { Box<#target_type> }
3424 } else {
3425 quote! { #target_type }
3426 }
3427 }
3428 SchemaType::Array { item_type } => {
3429 let inner_type = self.generate_array_item_type(item_type, analysis);
3431 quote! { Vec<#inner_type> }
3432 }
3433 _ => {
3434 quote! { serde_json::Value }
3436 }
3437 }
3438 }
3439
3440 fn type_name_to_variant_name(&self, type_name: &str) -> String {
3442 match type_name {
3444 "bool" => return "Boolean".to_string(),
3445 "i8" | "i16" | "i32" | "i64" | "i128" => return "Integer".to_string(),
3446 "u8" | "u16" | "u32" | "u64" | "u128" => return "UnsignedInteger".to_string(),
3447 "f32" | "f64" => return "Number".to_string(),
3448 "String" => return "String".to_string(),
3449 "serde_json::Value" => return "Value".to_string(),
3450 "bytes::Bytes" => return "Binary".to_string(),
3454 "chrono::DateTime<chrono::Utc>" => return "DateTime".to_string(),
3455 "chrono::NaiveDate" => return "Date".to_string(),
3456 "chrono::NaiveTime" => return "Time".to_string(),
3457 "uuid::Uuid" => return "Uuid".to_string(),
3458 "url::Url" => return "Url".to_string(),
3459 "std::net::Ipv4Addr" => return "Ipv4".to_string(),
3460 "std::net::Ipv6Addr" => return "Ipv6".to_string(),
3461 _ => {}
3462 }
3463
3464 if type_name.starts_with("Vec<") && type_name.ends_with(">") {
3466 let inner = &type_name[4..type_name.len() - 1];
3467 if inner.starts_with("Vec<") && inner.ends_with(">") {
3469 let inner_inner = &inner[4..inner.len() - 1];
3470 return format!("{}ArrayArray", self.type_name_to_variant_name(inner_inner));
3471 }
3472 return format!("{}Array", self.type_name_to_variant_name(inner));
3473 }
3474
3475 let clean_name = type_name
3481 .trim_end_matches("Type")
3482 .trim_end_matches("Schema")
3483 .trim_end_matches("Item");
3484
3485 self.to_rust_type_name(clean_name)
3487 }
3488
3489 fn ensure_unique_variant_name_generator(
3491 &self,
3492 base_name: String,
3493 used_names: &mut std::collections::HashSet<String>,
3494 fallback_index: usize,
3495 ) -> String {
3496 if used_names.insert(base_name.clone()) {
3497 return base_name;
3498 }
3499
3500 for i in 2..100 {
3502 let numbered_name = format!("{base_name}{i}");
3503 if used_names.insert(numbered_name.clone()) {
3504 return numbered_name;
3505 }
3506 }
3507
3508 let fallback = format!("Variant{fallback_index}");
3510 used_names.insert(fallback.clone());
3511 fallback
3512 }
3513
3514 fn find_request_type_for_operation(
3516 &self,
3517 operation_id: &str,
3518 analysis: &SchemaAnalysis,
3519 ) -> Option<String> {
3520 analysis.operations.get(operation_id).and_then(|op| {
3522 op.request_body
3523 .as_ref()
3524 .and_then(|rb| rb.schema_name().map(|s| s.to_string()))
3525 })
3526 }
3527
3528 fn resolve_streaming_event_type(
3530 &self,
3531 endpoint: &crate::streaming::StreamingEndpoint,
3532 analysis: &SchemaAnalysis,
3533 ) -> Result<String> {
3534 match &endpoint.event_flow {
3535 crate::streaming::EventFlow::Simple => {
3536 if analysis.schemas.contains_key(&endpoint.event_union_type) {
3539 Ok(endpoint.event_union_type.to_string())
3540 } else {
3541 Err(crate::error::GeneratorError::ValidationError(format!(
3542 "Streaming response type '{}' not found in schema for simple streaming endpoint '{}'",
3543 endpoint.event_union_type, endpoint.operation_id
3544 )))
3545 }
3546 }
3547 crate::streaming::EventFlow::StartDeltaStop { .. } => {
3548 if analysis.schemas.contains_key(&endpoint.event_union_type) {
3551 Ok(endpoint.event_union_type.to_string())
3552 } else {
3553 Err(crate::error::GeneratorError::ValidationError(format!(
3554 "Event union type '{}' not found in schema for complex streaming endpoint '{}'",
3555 endpoint.event_union_type, endpoint.operation_id
3556 )))
3557 }
3558 }
3559 }
3560 }
3561
3562 fn generate_streaming_error_types(&self) -> Result<TokenStream> {
3564 Ok(quote! {
3565 #[derive(Debug, thiserror::Error)]
3567 pub enum StreamingError {
3568 #[error("Connection error: {0}")]
3569 Connection(String),
3570 #[error("HTTP error: {status}")]
3571 Http { status: u16 },
3572 #[error("SSE parsing error: {0}")]
3573 Parsing(String),
3574 #[error("Authentication error: {0}")]
3575 Authentication(String),
3576 #[error("Rate limit error: {0}")]
3577 RateLimit(String),
3578 #[error("API error: {0}")]
3579 Api(String),
3580 #[error("Timeout error: {0}")]
3581 Timeout(String),
3582 #[error("Response body exceeded configured limit of {limit} bytes")]
3583 ResponseTooLarge { limit: usize },
3584 #[error("JSON serialization/deserialization error: {0}")]
3585 Json(#[from] serde_json::Error),
3586 #[error("Request error: {0}")]
3587 Request(reqwest::Error),
3588 }
3589
3590 impl From<reqwest::header::InvalidHeaderValue> for StreamingError {
3591 fn from(err: reqwest::header::InvalidHeaderValue) -> Self {
3592 StreamingError::Api(format!("Invalid header value: {}", err))
3593 }
3594 }
3595
3596 impl From<reqwest::Error> for StreamingError {
3597 fn from(err: reqwest::Error) -> Self {
3598 if err.is_timeout() {
3599 StreamingError::Timeout(err.to_string())
3600 } else if err.is_status() {
3601 if let Some(status) = err.status() {
3602 StreamingError::Http { status: status.as_u16() }
3603 } else {
3604 StreamingError::Connection(err.to_string())
3605 }
3606 } else {
3607 StreamingError::Request(err)
3608 }
3609 }
3610 }
3611 })
3612 }
3613
3614 fn generate_endpoint_trait(
3616 &self,
3617 endpoint: &crate::streaming::StreamingEndpoint,
3618 analysis: &SchemaAnalysis,
3619 ) -> Result<TokenStream> {
3620 use crate::streaming::HttpMethod;
3621
3622 let trait_name = format_ident!(
3623 "{}StreamingClient",
3624 self.to_rust_type_name(&endpoint.operation_id)
3625 );
3626 let method_name =
3627 format_ident!("stream_{}", self.to_rust_field_name(&endpoint.operation_id));
3628 let event_type =
3629 format_ident!("{}", self.resolve_streaming_event_type(endpoint, analysis)?);
3630
3631 let method_signature = match endpoint.http_method {
3633 HttpMethod::Get => {
3634 let mut param_defs = Vec::new();
3636 for qp in &endpoint.query_parameters {
3637 let param_name = format_ident!("{}", self.to_rust_field_name(&qp.name));
3638 if qp.required {
3639 param_defs.push(quote! { #param_name: &str });
3640 } else {
3641 param_defs.push(quote! { #param_name: Option<&str> });
3642 }
3643 }
3644 quote! {
3645 async fn #method_name(
3646 &self,
3647 #(#param_defs),*
3648 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error>;
3649 }
3650 }
3651 HttpMethod::Post => {
3652 let request_type = self
3654 .find_request_type_for_operation(&endpoint.operation_id, analysis)
3655 .unwrap_or_else(|| "serde_json::Value".to_string());
3656 let request_type_ident = if request_type.contains("::") {
3657 let parts: Vec<&str> = request_type.split("::").collect();
3658 let path_parts: Vec<_> = parts.iter().map(|p| format_ident!("{}", p)).collect();
3659 quote! { #(#path_parts)::* }
3660 } else {
3661 let ident = format_ident!("{}", request_type);
3662 quote! { #ident }
3663 };
3664 quote! {
3665 async fn #method_name(
3666 &self,
3667 request: #request_type_ident,
3668 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error>;
3669 }
3670 }
3671 };
3672
3673 Ok(quote! {
3674 #[async_trait]
3676 pub trait #trait_name {
3677 type Error: std::error::Error + Send + Sync + 'static;
3678
3679 #method_signature
3681 }
3682 })
3683 }
3684
3685 fn generate_streaming_client_impl(
3687 &self,
3688 streaming_config: &crate::streaming::StreamingConfig,
3689 analysis: &SchemaAnalysis,
3690 ) -> Result<TokenStream> {
3691 let client_name = format_ident!(
3692 "{}Client",
3693 self.to_rust_type_name(&streaming_config.client_module_name)
3694 );
3695
3696 let mut struct_fields = vec![
3699 quote! { base_url: String },
3700 quote! { api_key: Option<String> },
3701 quote! { sse_client: SseClient },
3702 quote! { custom_headers: std::collections::BTreeMap<String, String> },
3703 ];
3704
3705 let has_optional_headers = !streaming_config
3706 .endpoints
3707 .iter()
3708 .all(|e| e.optional_headers.is_empty());
3709
3710 if has_optional_headers {
3711 struct_fields
3712 .push(quote! { optional_headers: std::collections::BTreeMap<String, String> });
3713 }
3714
3715 let default_base_url = if let Some(ref streaming_config) = self.config.streaming_config {
3718 streaming_config
3719 .endpoints
3720 .first()
3721 .and_then(|e| e.base_url.as_deref())
3722 .unwrap_or("https://api.example.com")
3723 } else {
3724 "https://api.example.com"
3725 };
3726 let max_response_body_bytes = self
3727 .config()
3728 .http_client_config
3729 .as_ref()
3730 .and_then(|http| http.max_response_body_bytes)
3731 .unwrap_or(8 * 1024 * 1024);
3732 let sse_client_initializer = if let Some(reconnect) = &streaming_config.reconnection_config
3733 {
3734 let max_retries = reconnect.max_retries;
3735 let initial_delay_ms = reconnect.initial_delay_ms;
3736 let max_delay_ms = reconnect.max_delay_ms;
3737 let backoff_multiplier = reconnect.backoff_multiplier;
3738 quote! {
3739 SseClient::new()
3740 .with_max_error_body_bytes(#max_response_body_bytes)
3741 .with_reconnect_options(SseReconnectOptions {
3742 max_retries: #max_retries,
3743 initial_retry_delay: std::time::Duration::from_millis(#initial_delay_ms),
3744 max_retry_delay: std::time::Duration::from_millis(#max_delay_ms),
3745 backoff_multiplier: #backoff_multiplier,
3746 })
3747 }
3748 } else {
3749 quote! {
3750 SseClient::new()
3751 .with_max_error_body_bytes(#max_response_body_bytes)
3752 }
3753 };
3754
3755 let constructor_fields = if has_optional_headers {
3757 quote! {
3758 base_url: #default_base_url.to_string(),
3759 api_key: None,
3760 sse_client: #sse_client_initializer,
3761 custom_headers: std::collections::BTreeMap::new(),
3762 optional_headers: std::collections::BTreeMap::new(),
3763 }
3764 } else {
3765 quote! {
3766 base_url: #default_base_url.to_string(),
3767 api_key: None,
3768 sse_client: #sse_client_initializer,
3769 custom_headers: std::collections::BTreeMap::new(),
3770 }
3771 };
3772
3773 let optional_headers_method = if has_optional_headers {
3775 quote! {
3776 pub fn set_optional_headers(&mut self, headers: std::collections::BTreeMap<String, String>) {
3778 self.optional_headers = headers;
3779 }
3780 }
3781 } else {
3782 TokenStream::new()
3783 };
3784
3785 let constructor = quote! {
3786 impl #client_name {
3787 pub fn new() -> Self {
3789 Self {
3790 #constructor_fields
3791 }
3792 }
3793
3794 pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
3796 self.base_url = base_url.into();
3797 self
3798 }
3799
3800 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
3802 self.api_key = Some(api_key.into());
3803 self
3804 }
3805
3806 pub fn with_max_response_body_bytes(mut self, limit: usize) -> Self {
3808 self.sse_client = self.sse_client.with_max_error_body_bytes(limit);
3809 self
3810 }
3811
3812 pub fn with_header(
3814 mut self,
3815 name: impl Into<String>,
3816 value: impl Into<String>,
3817 ) -> Self {
3818 self.custom_headers.insert(name.into(), value.into());
3819 self
3820 }
3821
3822 pub fn with_http_client(mut self, client: reqwest::Client) -> Self {
3824 self.sse_client = self.sse_client.with_http_client(client);
3825 self
3826 }
3827
3828 #optional_headers_method
3829 }
3830 };
3831
3832 let mut trait_impls = Vec::new();
3834 for endpoint in &streaming_config.endpoints {
3835 let trait_impl = self.generate_endpoint_trait_impl(endpoint, &client_name, analysis)?;
3836 trait_impls.push(trait_impl);
3837 }
3838
3839 let default_impl = quote! {
3841 impl Default for #client_name {
3842 fn default() -> Self {
3843 Self::new()
3844 }
3845 }
3846 };
3847
3848 Ok(quote! {
3849 #[derive(Debug, Clone)]
3851 pub struct #client_name {
3852 #(#struct_fields,)*
3853 }
3854
3855 #constructor
3856
3857 #default_impl
3858
3859 #(#trait_impls)*
3860 })
3861 }
3862
3863 fn generate_endpoint_trait_impl(
3865 &self,
3866 endpoint: &crate::streaming::StreamingEndpoint,
3867 client_name: &proc_macro2::Ident,
3868 analysis: &SchemaAnalysis,
3869 ) -> Result<TokenStream> {
3870 use crate::streaming::HttpMethod;
3871
3872 let trait_name = format_ident!(
3873 "{}StreamingClient",
3874 self.to_rust_type_name(&endpoint.operation_id)
3875 );
3876 let method_name =
3877 format_ident!("stream_{}", self.to_rust_field_name(&endpoint.operation_id));
3878 let event_type =
3879 format_ident!("{}", self.resolve_streaming_event_type(endpoint, analysis)?);
3880
3881 let mut header_setup = Vec::new();
3883 for (name, value) in &endpoint.required_headers {
3884 header_setup.push(quote! {
3885 headers.insert(#name, HeaderValue::from_static(#value));
3886 });
3887 }
3888
3889 if let Some(auth_header) = &endpoint.auth_header {
3892 match auth_header {
3893 crate::streaming::AuthHeader::Bearer(header_name) => {
3894 header_setup.push(quote! {
3895 if let Some(ref api_key) = self.api_key {
3896 headers.insert(#header_name, HeaderValue::from_str(&format!("Bearer {}", api_key))?);
3897 }
3898 });
3899 }
3900 crate::streaming::AuthHeader::ApiKey(header_name) => {
3901 header_setup.push(quote! {
3902 if let Some(ref api_key) = self.api_key {
3903 headers.insert(#header_name, HeaderValue::from_str(api_key)?);
3904 }
3905 });
3906 }
3907 }
3908 } else {
3909 header_setup.push(quote! {
3911 if let Some(ref api_key) = self.api_key {
3912 headers.insert("Authorization", HeaderValue::from_str(&format!("Bearer {}", api_key))?);
3913 }
3914 });
3915 }
3916
3917 header_setup.push(quote! {
3919 for (name, value) in &self.custom_headers {
3920 if let (Ok(header_name), Ok(header_value)) = (reqwest::header::HeaderName::from_bytes(name.as_bytes()), HeaderValue::from_str(value)) {
3921 headers.insert(header_name, header_value);
3922 }
3923 }
3924 });
3925
3926 if !endpoint.optional_headers.is_empty() {
3928 header_setup.push(quote! {
3929 for (key, value) in &self.optional_headers {
3930 if let (Ok(header_name), Ok(header_value)) = (reqwest::header::HeaderName::from_bytes(key.as_bytes()), HeaderValue::from_str(value)) {
3931 headers.insert(header_name, header_value);
3932 }
3933 }
3934 });
3935 }
3936
3937 match endpoint.http_method {
3939 HttpMethod::Get => self.generate_get_streaming_impl(
3940 endpoint,
3941 client_name,
3942 &trait_name,
3943 &method_name,
3944 &event_type,
3945 &header_setup,
3946 ),
3947 HttpMethod::Post => self.generate_post_streaming_impl(
3948 endpoint,
3949 client_name,
3950 &trait_name,
3951 &method_name,
3952 &event_type,
3953 &header_setup,
3954 analysis,
3955 ),
3956 }
3957 }
3958
3959 fn generate_get_streaming_impl(
3961 &self,
3962 endpoint: &crate::streaming::StreamingEndpoint,
3963 client_name: &proc_macro2::Ident,
3964 trait_name: &proc_macro2::Ident,
3965 method_name: &proc_macro2::Ident,
3966 event_type: &proc_macro2::Ident,
3967 header_setup: &[TokenStream],
3968 ) -> Result<TokenStream> {
3969 let path = &endpoint.path;
3970
3971 let mut param_defs = Vec::new();
3973 let mut query_params = Vec::new();
3974
3975 for qp in &endpoint.query_parameters {
3976 let param_name = format_ident!("{}", self.to_rust_field_name(&qp.name));
3977 let param_name_str = &qp.name;
3978
3979 if qp.required {
3980 param_defs.push(quote! { #param_name: &str });
3981 query_params.push(quote! {
3982 url.query_pairs_mut().append_pair(#param_name_str, #param_name);
3983 });
3984 } else {
3985 param_defs.push(quote! { #param_name: Option<&str> });
3986 query_params.push(quote! {
3987 if let Some(v) = #param_name {
3988 url.query_pairs_mut().append_pair(#param_name_str, v);
3989 }
3990 });
3991 }
3992 }
3993
3994 let url_construction = quote! {
3996 let base_url = url::Url::parse(&self.base_url)
3997 .map_err(|e| StreamingError::Connection(format!("Invalid base URL: {}", e)))?;
3998 let path_to_join = #path.trim_start_matches('/');
3999 let mut url = base_url.join(path_to_join)
4000 .map_err(|e| StreamingError::Connection(format!("URL join error: {}", e)))?;
4001 #(#query_params)*
4002 };
4003
4004 let instrument_skip = quote! { #[instrument(skip(self), name = "streaming_get_request")] };
4005
4006 Ok(quote! {
4007 #[async_trait]
4008 impl #trait_name for #client_name {
4009 type Error = StreamingError;
4010
4011 #instrument_skip
4012 async fn #method_name(
4013 &self,
4014 #(#param_defs),*
4015 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error> {
4016 debug!("Starting streaming GET request");
4017
4018 let mut headers = HeaderMap::new();
4019 #(#header_setup)*
4020
4021 #url_construction
4022 let url_str = url.to_string();
4023 debug!("Making streaming GET request to: {}", url_str);
4024
4025 let request_builder = self.sse_client
4026 .get(url_str)
4027 .headers(headers);
4028
4029 debug!("Creating SSE stream from request");
4030 let stream = self
4031 .sse_client
4032 .stream::<#event_type>(request_builder)
4033 .await?;
4034 info!("SSE stream created successfully");
4035 Ok(stream)
4036 }
4037 }
4038 })
4039 }
4040
4041 #[allow(clippy::too_many_arguments)]
4043 fn generate_post_streaming_impl(
4044 &self,
4045 endpoint: &crate::streaming::StreamingEndpoint,
4046 client_name: &proc_macro2::Ident,
4047 trait_name: &proc_macro2::Ident,
4048 method_name: &proc_macro2::Ident,
4049 event_type: &proc_macro2::Ident,
4050 header_setup: &[TokenStream],
4051 analysis: &SchemaAnalysis,
4052 ) -> Result<TokenStream> {
4053 let path = &endpoint.path;
4054
4055 let request_type = self
4057 .find_request_type_for_operation(&endpoint.operation_id, analysis)
4058 .unwrap_or_else(|| "serde_json::Value".to_string());
4059 let request_type_ident = if request_type.contains("::") {
4060 let parts: Vec<&str> = request_type.split("::").collect();
4061 let path_parts: Vec<_> = parts.iter().map(|p| format_ident!("{}", p)).collect();
4062 quote! { #(#path_parts)::* }
4063 } else {
4064 let ident = format_ident!("{}", request_type);
4065 quote! { #ident }
4066 };
4067
4068 let url_construction = quote! {
4070 let base_url = url::Url::parse(&self.base_url)
4071 .map_err(|e| StreamingError::Connection(format!("Invalid base URL: {}", e)))?;
4072 let path_to_join = #path.trim_start_matches('/');
4073 let url = base_url.join(path_to_join)
4074 .map_err(|e| StreamingError::Connection(format!("URL join error: {}", e)))?
4075 .to_string();
4076 };
4077
4078 let stream_param = &endpoint.stream_parameter;
4080 let stream_setup = if stream_param.is_empty() {
4081 quote! {
4082 let streaming_request = request;
4083 }
4084 } else {
4085 quote! {
4086 let mut streaming_request = request;
4088 if let Ok(mut request_value) = serde_json::to_value(&streaming_request) {
4089 if let Some(obj) = request_value.as_object_mut() {
4090 obj.insert(#stream_param.to_string(), serde_json::Value::Bool(true));
4091 }
4092 streaming_request = serde_json::from_value(request_value)?;
4093 }
4094 }
4095 };
4096
4097 Ok(quote! {
4098 #[async_trait]
4099 impl #trait_name for #client_name {
4100 type Error = StreamingError;
4101
4102 #[instrument(skip(self, request), name = "streaming_post_request")]
4103 async fn #method_name(
4104 &self,
4105 request: #request_type_ident,
4106 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error> {
4107 debug!("Starting streaming POST request");
4108
4109 #stream_setup
4110
4111 let mut headers = HeaderMap::new();
4112 #(#header_setup)*
4113
4114 #url_construction
4115 debug!("Making streaming POST request to: {}", url);
4116
4117 let request_builder = self.sse_client
4118 .post(&url)
4119 .headers(headers)
4120 .json(&streaming_request);
4121
4122 debug!("Creating SSE stream from request");
4123 let stream = self
4124 .sse_client
4125 .stream::<#event_type>(request_builder)
4126 .await?;
4127 info!("SSE stream created successfully");
4128 Ok(stream)
4129 }
4130 }
4131 })
4132 }
4133
4134 fn generate_sse_runtime(&self) -> Result<String> {
4136 let provenance_attribute = self.provenance_attribute();
4137 let error_types = self.generate_streaming_error_types()?;
4138 let parser = self.generate_sse_parser_utilities()?;
4139 let tokens = quote! {
4140 #provenance_attribute
4144 #![allow(clippy::format_in_format_args)]
4145
4146 use futures_util::{Stream, StreamExt};
4147 use std::pin::Pin;
4148 use std::time::Duration;
4149 use tracing::debug;
4150
4151 #error_types
4152
4153 #[derive(Debug, Clone)]
4155 pub struct SseClient {
4156 http_client: reqwest::Client,
4157 max_error_body_bytes: usize,
4158 reconnect_options: Option<SseReconnectOptions>,
4159 }
4160
4161 impl SseClient {
4162 pub fn new() -> Self {
4163 Self {
4164 http_client: reqwest::Client::new(),
4165 max_error_body_bytes: DEFAULT_MAX_SSE_ERROR_BODY_BYTES,
4166 reconnect_options: None,
4167 }
4168 }
4169
4170 pub fn with_http_client(mut self, client: reqwest::Client) -> Self {
4171 self.http_client = client;
4172 self
4173 }
4174
4175 pub fn with_max_error_body_bytes(mut self, limit: usize) -> Self {
4176 self.max_error_body_bytes = limit;
4177 self
4178 }
4179
4180 pub fn with_reconnect_options(mut self, options: SseReconnectOptions) -> Self {
4182 self.reconnect_options = Some(options);
4183 self
4184 }
4185
4186 pub fn get(&self, url: impl reqwest::IntoUrl) -> reqwest::RequestBuilder {
4187 self.http_client.get(url)
4188 }
4189
4190 pub fn post(&self, url: impl reqwest::IntoUrl) -> reqwest::RequestBuilder {
4191 self.http_client.post(url)
4192 }
4193
4194 pub async fn stream<T>(
4195 &self,
4196 request_builder: reqwest::RequestBuilder,
4197 ) -> Result<Pin<Box<dyn Stream<Item = Result<T, StreamingError>> + Send>>, StreamingError>
4198 where
4199 T: serde::de::DeserializeOwned + Send + 'static,
4200 {
4201 if let Some(options) = self.reconnect_options.clone() {
4202 parse_sse_json_reconnecting_with_limit(
4203 request_builder,
4204 self.max_error_body_bytes,
4205 options,
4206 ).await
4207 } else {
4208 parse_sse_json_stream_with_limit(
4209 request_builder,
4210 self.max_error_body_bytes,
4211 ).await
4212 }
4213 }
4214
4215 pub async fn stream_raw(
4217 &self,
4218 request_builder: reqwest::RequestBuilder,
4219 ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<String>, StreamingError>> + Send>>, StreamingError> {
4220 parse_sse_raw_stream_with_limit(request_builder, self.max_error_body_bytes).await
4221 }
4222
4223 pub async fn stream_json<T>(
4225 &self,
4226 request_builder: reqwest::RequestBuilder,
4227 ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<T>, StreamingError>> + Send>>, StreamingError>
4228 where
4229 T: serde::de::DeserializeOwned + Send + 'static,
4230 {
4231 parse_sse_json_events_with_limit(request_builder, self.max_error_body_bytes).await
4232 }
4233
4234 pub async fn stream_raw_reconnecting(
4236 &self,
4237 request_builder: reqwest::RequestBuilder,
4238 ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<String>, StreamingError>> + Send>>, StreamingError> {
4239 parse_sse_raw_reconnecting_with_limit(
4240 request_builder,
4241 self.max_error_body_bytes,
4242 self.reconnect_options.clone().unwrap_or_default(),
4243 ).await
4244 }
4245
4246 pub async fn stream_json_reconnecting<T>(
4248 &self,
4249 request_builder: reqwest::RequestBuilder,
4250 ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<T>, StreamingError>> + Send>>, StreamingError>
4251 where
4252 T: serde::de::DeserializeOwned + Send + 'static,
4253 {
4254 parse_sse_json_reconnecting_events_with_limit(
4255 request_builder,
4256 self.max_error_body_bytes,
4257 self.reconnect_options.clone().unwrap_or_default(),
4258 ).await
4259 }
4260 }
4261
4262 impl Default for SseClient {
4263 fn default() -> Self {
4264 Self::new()
4265 }
4266 }
4267
4268 #parser
4269 };
4270 let syntax_tree = syn::parse2::<syn::File>(tokens).map_err(|error| {
4271 GeneratorError::CodeGenError(format!("Failed to parse generated sse.rs: {error}"))
4272 })?;
4273 Ok(prettyplease::unparse(&syntax_tree))
4274 }
4275
4276 fn generate_sse_parser_utilities(&self) -> Result<TokenStream> {
4278 Ok(quote! {
4279 pub const DEFAULT_MAX_SSE_ERROR_BODY_BYTES: usize = 8 * 1024 * 1024;
4281
4282 async fn __read_bounded_streaming_error_body(
4283 mut response: reqwest::Response,
4284 limit: usize,
4285 ) -> Result<Vec<u8>, StreamingError> {
4286 let mut body = Vec::new();
4287 while let Some(chunk) = response.chunk().await? {
4288 let next_len = body.len().checked_add(chunk.len());
4289 if next_len.is_none_or(|next_len| next_len > limit) {
4290 return Err(StreamingError::ResponseTooLarge { limit });
4291 }
4292 body.extend_from_slice(&chunk);
4293 }
4294 Ok(body)
4295 }
4296
4297 #[derive(Debug, Clone, PartialEq, Eq)]
4299 pub struct SseEvent<T> {
4300 pub event: String,
4302 pub data: T,
4304 pub id: Option<String>,
4306 pub retry: Option<Duration>,
4308 }
4309
4310 #[derive(Debug, Clone)]
4312 pub struct SseReconnectOptions {
4313 pub max_retries: u32,
4315 pub initial_retry_delay: Duration,
4317 pub max_retry_delay: Duration,
4319 pub backoff_multiplier: f64,
4321 }
4322
4323 impl Default for SseReconnectOptions {
4324 fn default() -> Self {
4325 Self {
4326 max_retries: 3,
4327 initial_retry_delay: Duration::from_secs(3),
4328 max_retry_delay: Duration::from_secs(30),
4329 backoff_multiplier: 2.0,
4330 }
4331 }
4332 }
4333
4334 impl SseReconnectOptions {
4335 fn delay(&self, attempt: u32, server_retry: Option<Duration>) -> Duration {
4336 if let Some(delay) = server_retry {
4337 return delay.min(self.max_retry_delay);
4338 }
4339 let multiplier = self.backoff_multiplier.max(1.0);
4340 let millis = self.initial_retry_delay.as_millis() as f64
4341 * multiplier.powi(attempt.min(63) as i32);
4342 Duration::from_millis(
4343 millis.min(self.max_retry_delay.as_millis() as f64) as u64,
4344 )
4345 }
4346 }
4347
4348 #[derive(Default)]
4349 struct __SseDecoder {
4350 line: Vec<u8>,
4351 event: String,
4352 data: Vec<String>,
4353 last_event_id: Option<String>,
4354 retry_delay: Option<Duration>,
4355 event_retry: Option<Duration>,
4356 saw_carriage_return: bool,
4357 }
4358
4359 impl __SseDecoder {
4360 fn feed(
4361 &mut self,
4362 chunk: &[u8],
4363 ) -> Vec<Result<SseEvent<String>, StreamingError>> {
4364 let mut messages = Vec::new();
4365 for &byte in chunk {
4366 if self.saw_carriage_return {
4367 self.saw_carriage_return = false;
4368 if byte == b'\n' {
4369 continue;
4370 }
4371 }
4372
4373 match byte {
4374 b'\n' => self.finish_line(&mut messages),
4375 b'\r' => {
4376 self.finish_line(&mut messages);
4377 self.saw_carriage_return = true;
4378 }
4379 _ => self.line.push(byte),
4380 }
4381 }
4382 messages
4383 }
4384
4385 fn finish(&mut self) -> Vec<Result<SseEvent<String>, StreamingError>> {
4386 let mut messages = Vec::new();
4387 if !self.line.is_empty() {
4388 self.finish_line(&mut messages);
4389 }
4390 self.dispatch(&mut messages);
4391 messages
4392 }
4393
4394 fn finish_line(
4395 &mut self,
4396 messages: &mut Vec<Result<SseEvent<String>, StreamingError>>,
4397 ) {
4398 let line = std::mem::take(&mut self.line);
4399 let line = match String::from_utf8(line) {
4400 Ok(line) => line,
4401 Err(error) => {
4402 messages.push(Err(StreamingError::Parsing(format!(
4403 "SSE line is not valid UTF-8: {}",
4404 error
4405 ))));
4406 return;
4407 }
4408 };
4409
4410 if line.is_empty() {
4411 self.dispatch(messages);
4412 return;
4413 }
4414 if line.starts_with(':') {
4415 return;
4416 }
4417
4418 let (field, value) = line
4419 .split_once(':')
4420 .map_or((line.as_str(), ""), |(field, value)| {
4421 (field, value.strip_prefix(' ').unwrap_or(value))
4422 });
4423 match field {
4424 "event" => self.event = value.to_string(),
4425 "data" => self.data.push(value.to_string()),
4426 "id" if !value.contains('\0') => {
4427 self.last_event_id = (!value.is_empty()).then(|| value.to_string());
4428 }
4429 "retry" if value.bytes().all(|byte| byte.is_ascii_digit()) => {
4430 if let Ok(milliseconds) = value.parse::<u64>() {
4431 let delay = Duration::from_millis(milliseconds);
4432 self.retry_delay = Some(delay);
4433 self.event_retry = Some(delay);
4434 }
4435 }
4436 _ => {}
4437 }
4438 }
4439
4440 fn dispatch(
4441 &mut self,
4442 messages: &mut Vec<Result<SseEvent<String>, StreamingError>>,
4443 ) {
4444 if self.data.is_empty() {
4445 self.event.clear();
4446 self.event_retry = None;
4447 return;
4448 }
4449 messages.push(Ok(SseEvent {
4450 event: if self.event.is_empty() {
4451 "message".to_string()
4452 } else {
4453 std::mem::take(&mut self.event)
4454 },
4455 data: std::mem::take(&mut self.data).join("\n"),
4456 id: self.last_event_id.clone(),
4457 retry: self.event_retry.take(),
4458 }));
4459 self.event.clear();
4460 }
4461
4462 fn reset_for_reconnect(&mut self) {
4463 self.line.clear();
4464 self.event.clear();
4465 self.data.clear();
4466 self.event_retry = None;
4467 self.saw_carriage_return = false;
4468 }
4469 }
4470
4471 fn __deserialize_sse_event<T>(
4472 event: SseEvent<String>,
4473 ) -> Option<Result<SseEvent<T>, StreamingError>>
4474 where
4475 T: serde::de::DeserializeOwned,
4476 {
4477 if event.data.trim() == "[DONE]" {
4478 return None;
4479 }
4480 if event.event == "ping" {
4481 debug!("Received SSE ping event, skipping");
4482 return None;
4483 }
4484 if event.data.trim().is_empty() {
4485 debug!("Empty SSE data, skipping");
4486 return None;
4487 }
4488
4489 let json_value = match serde_json::from_str::<serde_json::Value>(&event.data) {
4490 Ok(value) => value,
4491 Err(error) => {
4492 return Some(Err(StreamingError::Parsing(format!(
4493 "SSE event is not valid JSON: {} ({})",
4494 event.data, error
4495 ))));
4496 }
4497 };
4498 let is_ping = json_value
4499 .get("event")
4500 .or_else(|| json_value.get("type"))
4501 .and_then(serde_json::Value::as_str)
4502 .is_some_and(|event| event == "ping");
4503 if is_ping {
4504 debug!("Received ping event in JSON data, skipping");
4505 return None;
4506 }
4507
4508 Some(
4509 serde_json::from_value::<T>(json_value)
4510 .map(|data| SseEvent {
4511 event: event.event.clone(),
4512 data,
4513 id: event.id.clone(),
4514 retry: event.retry,
4515 })
4516 .map_err(|error| StreamingError::Parsing(format!(
4517 "Failed to parse SSE event: {} (raw: {}, event: {})",
4518 error, event.data, event.event
4519 ))),
4520 )
4521 }
4522
4523 pub async fn parse_sse_stream<T>(
4525 request_builder: reqwest::RequestBuilder
4526 ) -> Result<Pin<Box<dyn Stream<Item = Result<T, StreamingError>> + Send>>, StreamingError>
4527 where
4528 T: serde::de::DeserializeOwned + Send + 'static,
4529 {
4530 parse_sse_json_stream_with_limit(
4531 request_builder,
4532 DEFAULT_MAX_SSE_ERROR_BODY_BYTES,
4533 ).await
4534 }
4535
4536 struct __SseOpenError {
4537 error: StreamingError,
4538 retryable: bool,
4539 }
4540
4541 async fn __open_sse_response(
4542 request_builder: reqwest::RequestBuilder,
4543 max_response_body_bytes: usize,
4544 ) -> Result<reqwest::Response, __SseOpenError> {
4545 let response = request_builder.send().await.map_err(|error| __SseOpenError {
4546 error: error.into(),
4547 retryable: true,
4548 })?;
4549 if !response.status().is_success() {
4550 let status = response.status();
4551 let retryable = status.as_u16() == 429 || status.is_server_error();
4552 let error = match __read_bounded_streaming_error_body(
4553 response,
4554 max_response_body_bytes,
4555 ).await {
4556 Ok(body) => StreamingError::Connection(format!(
4557 "HTTP {} error: {}",
4558 status.as_u16(),
4559 String::from_utf8_lossy(&body)
4560 )),
4561 Err(error) => error,
4562 };
4563 return Err(__SseOpenError { error, retryable });
4564 }
4565
4566 let content_type = response
4567 .headers()
4568 .get(reqwest::header::CONTENT_TYPE)
4569 .and_then(|value| value.to_str().ok())
4570 .unwrap_or_default();
4571 if !content_type
4572 .split(';')
4573 .next()
4574 .is_some_and(|value| value.trim().eq_ignore_ascii_case("text/event-stream"))
4575 {
4576 let error = StreamingError::Parsing(format!(
4577 "Expected text/event-stream response, received {}",
4578 if content_type.is_empty() { "no Content-Type" } else { content_type }
4579 ));
4580 return Err(__SseOpenError { error, retryable: false });
4581 }
4582
4583 debug!("SSE connection opened");
4584 Ok(response)
4585 }
4586
4587 fn __raw_response_stream(
4588 response: reqwest::Response,
4589 ) -> Pin<Box<dyn Stream<Item = Result<SseEvent<String>, StreamingError>> + Send>> {
4590 let stream = futures_util::stream::unfold(
4591 (
4592 response,
4593 __SseDecoder::default(),
4594 std::collections::VecDeque::<Result<SseEvent<String>, StreamingError>>::new(),
4595 false,
4596 ),
4597 |(mut response, mut decoder, mut pending, mut done)| async move {
4598 loop {
4599 if let Some(item) = pending.pop_front() {
4600 return Some((item, (response, decoder, pending, done)));
4601 }
4602 if done {
4603 debug!("SSE stream completed normally");
4604 return None;
4605 }
4606
4607 match response.chunk().await {
4608 Ok(Some(chunk)) => {
4609 for event in decoder.feed(&chunk) {
4610 let is_done = event
4611 .as_ref()
4612 .is_ok_and(|event| event.data.trim() == "[DONE]");
4613 pending.push_back(event);
4614 if is_done {
4615 done = true;
4616 break;
4617 }
4618 }
4619 }
4620 Err(error) => {
4621 done = true;
4622 pending.push_back(Err(error.into()));
4623 }
4624 Ok(None) => {
4625 done = true;
4626 for event in decoder.finish() {
4627 pending.push_back(event);
4628 }
4629 }
4630 }
4631 }
4632 }
4633 );
4634
4635 Box::pin(stream)
4636 }
4637
4638 async fn parse_sse_raw_stream_with_limit(
4639 request_builder: reqwest::RequestBuilder,
4640 max_response_body_bytes: usize,
4641 ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<String>, StreamingError>> + Send>>, StreamingError> {
4642 Ok(match __open_sse_response(request_builder, max_response_body_bytes).await {
4643 Ok(response) => __raw_response_stream(response),
4644 Err(error) => Box::pin(futures_util::stream::once(async move { Err(error.error) })),
4645 })
4646 }
4647
4648 fn __json_event_stream<T>(
4649 raw: Pin<Box<dyn Stream<Item = Result<SseEvent<String>, StreamingError>> + Send>>,
4650 ) -> Pin<Box<dyn Stream<Item = Result<SseEvent<T>, StreamingError>> + Send>>
4651 where
4652 T: serde::de::DeserializeOwned + Send + 'static,
4653 {
4654 Box::pin(raw.filter_map(|event| async move {
4655 match event {
4656 Ok(event) => __deserialize_sse_event(event),
4657 Err(error) => Some(Err(error)),
4658 }
4659 }))
4660 }
4661
4662 async fn parse_sse_json_events_with_limit<T>(
4663 request_builder: reqwest::RequestBuilder,
4664 max_response_body_bytes: usize,
4665 ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<T>, StreamingError>> + Send>>, StreamingError>
4666 where
4667 T: serde::de::DeserializeOwned + Send + 'static,
4668 {
4669 Ok(__json_event_stream(
4670 parse_sse_raw_stream_with_limit(request_builder, max_response_body_bytes).await?,
4671 ))
4672 }
4673
4674 async fn parse_sse_json_stream_with_limit<T>(
4675 request_builder: reqwest::RequestBuilder,
4676 max_response_body_bytes: usize,
4677 ) -> Result<Pin<Box<dyn Stream<Item = Result<T, StreamingError>> + Send>>, StreamingError>
4678 where
4679 T: serde::de::DeserializeOwned + Send + 'static,
4680 {
4681 let events = parse_sse_json_events_with_limit(request_builder, max_response_body_bytes).await?;
4682 Ok(Box::pin(events.map(|event| event.map(|event| event.data))))
4683 }
4684
4685 struct __ReconnectState {
4686 request: reqwest::RequestBuilder,
4687 response: Option<reqwest::Response>,
4688 decoder: __SseDecoder,
4689 pending: std::collections::VecDeque<Result<SseEvent<String>, StreamingError>>,
4690 options: SseReconnectOptions,
4691 max_response_body_bytes: usize,
4692 attempts: u32,
4693 wait_before_open: bool,
4694 done: bool,
4695 }
4696
4697 async fn parse_sse_raw_reconnecting_with_limit(
4698 request_builder: reqwest::RequestBuilder,
4699 max_response_body_bytes: usize,
4700 options: SseReconnectOptions,
4701 ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<String>, StreamingError>> + Send>>, StreamingError> {
4702 if request_builder.try_clone().is_none() {
4703 return Err(StreamingError::Connection(
4704 "SSE reconnection requires a cloneable request body".to_string(),
4705 ));
4706 }
4707
4708 let stream = futures_util::stream::unfold(
4709 __ReconnectState {
4710 request: request_builder,
4711 response: None,
4712 decoder: __SseDecoder::default(),
4713 pending: std::collections::VecDeque::new(),
4714 options,
4715 max_response_body_bytes,
4716 attempts: 0,
4717 wait_before_open: false,
4718 done: false,
4719 },
4720 |mut state| async move {
4721 loop {
4722 if let Some(item) = state.pending.pop_front() {
4723 return Some((item, state));
4724 }
4725 if state.done {
4726 return None;
4727 }
4728
4729 if state.response.is_none() {
4730 if state.wait_before_open {
4731 let delay = state.options.delay(
4732 state.attempts.saturating_sub(1),
4733 state.decoder.retry_delay,
4734 );
4735 debug!(?delay, attempt = state.attempts, "Reconnecting SSE stream");
4736 futures_timer::Delay::new(delay).await;
4737 state.wait_before_open = false;
4738 }
4739
4740 let mut request = state.request.try_clone().expect("request clone checked");
4741 if let Some(last_event_id) = state.decoder.last_event_id.as_deref() {
4742 request = request.header("Last-Event-ID", last_event_id);
4743 }
4744 match __open_sse_response(request, state.max_response_body_bytes).await {
4745 Ok(response) => state.response = Some(response),
4746 Err(error) if error.retryable && state.attempts < state.options.max_retries => {
4747 state.attempts += 1;
4748 state.wait_before_open = true;
4749 continue;
4750 }
4751 Err(error) => {
4752 state.done = true;
4753 state.pending.push_back(Err(error.error));
4754 continue;
4755 }
4756 }
4757 }
4758
4759 let next = state.response.as_mut().expect("response opened").chunk().await;
4760 match next {
4761 Ok(Some(chunk)) => {
4762 let events = state.decoder.feed(&chunk);
4763 if !events.is_empty() {
4764 state.attempts = 0;
4765 }
4766 for event in events {
4767 let is_done = event
4768 .as_ref()
4769 .is_ok_and(|event| event.data.trim() == "[DONE]");
4770 state.pending.push_back(event);
4771 if is_done {
4772 state.done = true;
4773 state.response = None;
4774 break;
4775 }
4776 }
4777 }
4778 Ok(None) => {
4779 let events = state.decoder.finish();
4780 if !events.is_empty() {
4781 state.attempts = 0;
4782 }
4783 for event in events {
4784 let is_done = event
4785 .as_ref()
4786 .is_ok_and(|event| event.data.trim() == "[DONE]");
4787 state.pending.push_back(event);
4788 if is_done {
4789 state.done = true;
4790 break;
4791 }
4792 }
4793 state.response = None;
4794 state.decoder.reset_for_reconnect();
4795 if !state.done {
4796 if state.attempts < state.options.max_retries {
4797 state.attempts += 1;
4798 state.wait_before_open = true;
4799 } else {
4800 state.done = true;
4801 }
4802 }
4803 }
4804 Err(error) => {
4805 state.response = None;
4806 state.decoder.reset_for_reconnect();
4807 if state.attempts < state.options.max_retries {
4808 state.attempts += 1;
4809 state.wait_before_open = true;
4810 } else {
4811 state.done = true;
4812 state.pending.push_back(Err(error.into()));
4813 }
4814 }
4815 }
4816 }
4817 },
4818 );
4819 Ok(Box::pin(stream))
4820 }
4821
4822 async fn parse_sse_json_reconnecting_events_with_limit<T>(
4823 request_builder: reqwest::RequestBuilder,
4824 max_response_body_bytes: usize,
4825 options: SseReconnectOptions,
4826 ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<T>, StreamingError>> + Send>>, StreamingError>
4827 where
4828 T: serde::de::DeserializeOwned + Send + 'static,
4829 {
4830 Ok(__json_event_stream(
4831 parse_sse_raw_reconnecting_with_limit(
4832 request_builder,
4833 max_response_body_bytes,
4834 options,
4835 ).await?,
4836 ))
4837 }
4838
4839 async fn parse_sse_json_reconnecting_with_limit<T>(
4840 request_builder: reqwest::RequestBuilder,
4841 max_response_body_bytes: usize,
4842 options: SseReconnectOptions,
4843 ) -> Result<Pin<Box<dyn Stream<Item = Result<T, StreamingError>> + Send>>, StreamingError>
4844 where
4845 T: serde::de::DeserializeOwned + Send + 'static,
4846 {
4847 let events = parse_sse_json_reconnecting_events_with_limit(
4848 request_builder,
4849 max_response_body_bytes,
4850 options,
4851 ).await?;
4852 Ok(Box::pin(events.map(|event| event.map(|event| event.data))))
4853 }
4854 })
4855 }
4856
4857 fn generate_reconnection_utilities(
4859 &self,
4860 reconnect_config: &crate::streaming::ReconnectionConfig,
4861 ) -> Result<TokenStream> {
4862 let max_retries = reconnect_config.max_retries;
4863 let initial_delay = reconnect_config.initial_delay_ms;
4864 let max_delay = reconnect_config.max_delay_ms;
4865 let backoff_multiplier = reconnect_config.backoff_multiplier;
4866
4867 Ok(quote! {
4868 #[derive(Debug, Clone)]
4870 pub struct ReconnectionManager {
4871 max_retries: u32,
4872 initial_delay_ms: u64,
4873 max_delay_ms: u64,
4874 backoff_multiplier: f64,
4875 current_attempt: u32,
4876 }
4877
4878 impl ReconnectionManager {
4879 pub fn new() -> Self {
4881 Self {
4882 max_retries: #max_retries,
4883 initial_delay_ms: #initial_delay,
4884 max_delay_ms: #max_delay,
4885 backoff_multiplier: #backoff_multiplier,
4886 current_attempt: 0,
4887 }
4888 }
4889
4890 pub fn should_retry(&self) -> bool {
4892 self.current_attempt < self.max_retries
4893 }
4894
4895 pub fn next_retry_delay(&mut self) -> Duration {
4897 if !self.should_retry() {
4898 return Duration::from_secs(0);
4899 }
4900
4901 let delay_ms = (self.initial_delay_ms as f64
4902 * self.backoff_multiplier.powi(self.current_attempt as i32)) as u64;
4903 let delay_ms = delay_ms.min(self.max_delay_ms);
4904
4905 self.current_attempt += 1;
4906 Duration::from_millis(delay_ms)
4907 }
4908
4909 pub fn reset(&mut self) {
4911 self.current_attempt = 0;
4912 }
4913
4914 pub fn current_attempt(&self) -> u32 {
4916 self.current_attempt
4917 }
4918 }
4919
4920 impl Default for ReconnectionManager {
4921 fn default() -> Self {
4922 Self::new()
4923 }
4924 }
4925 })
4926 }
4927}