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