Skip to main content

openapi_to_rust/
generator.rs

1use crate::{
2    GeneratorError, Result,
3    analysis::{SchemaAnalysis, SchemaType},
4    streaming::StreamingConfig,
5};
6use proc_macro2::TokenStream;
7use quote::{format_ident, quote};
8use std::collections::BTreeMap;
9use std::path::PathBuf;
10
11/// Parse a Rust type string (possibly with generics, e.g.
12/// `chrono::DateTime<chrono::Utc>`) into a `TokenStream`. The pre-Q2
13/// ad-hoc `::`-splitter choked on `<` and `>`; `syn::parse_str` handles
14/// every valid type expression. Errors here mean the [`TypeMapper`]
15/// produced a string that doesn't parse as a Rust type — a generator
16/// bug, surfaced as a `GeneratorError::CodeGenError`.
17///
18/// [`TypeMapper`]: crate::type_mapping::TypeMapper
19fn parse_rust_type(rust_type: &str) -> Result<TokenStream> {
20    let parsed: syn::Type = syn::parse_str(rust_type).map_err(|e| {
21        GeneratorError::CodeGenError(format!(
22            "TypeMapper produced un-parseable type `{rust_type}`: {e}"
23        ))
24    })?;
25    Ok(quote! { #parsed })
26}
27
28/// Q2.4 — render OpenAPI constraint annotations as a single-line
29/// human-readable doc comment, e.g.
30///   "Constraint: minimum=0, maximum=100, pattern=`^foo$`"
31///
32/// The pattern is wrapped in backticks so backticks/braces inside
33/// it don't trip prettyplease/rustdoc parsing. Triple-slash and
34/// `*/` sequences are escaped so embedded patterns can't terminate
35/// the surrounding doc comment / block comment.
36fn format_constraints_doc(c: &crate::analysis::PropertyConstraints) -> String {
37    let mut parts: Vec<String> = Vec::new();
38
39    if let Some(v) = c.minimum {
40        parts.push(format!("minimum={}", strip_trailing_zero(v)));
41    }
42    if let Some(v) = c.maximum {
43        parts.push(format!("maximum={}", strip_trailing_zero(v)));
44    }
45    if let Some(v) = c.exclusive_minimum {
46        parts.push(format!("exclusiveMinimum={}", strip_trailing_zero(v)));
47    }
48    if let Some(v) = c.exclusive_maximum {
49        parts.push(format!("exclusiveMaximum={}", strip_trailing_zero(v)));
50    }
51    if let Some(v) = c.multiple_of {
52        parts.push(format!("multipleOf={}", strip_trailing_zero(v)));
53    }
54    if let Some(v) = c.min_length {
55        parts.push(format!("minLength={v}"));
56    }
57    if let Some(v) = c.max_length {
58        parts.push(format!("maxLength={v}"));
59    }
60    if let Some(v) = c.min_items {
61        parts.push(format!("minItems={v}"));
62    }
63    if let Some(v) = c.max_items {
64        parts.push(format!("maxItems={v}"));
65    }
66    if c.unique_items == Some(true) {
67        parts.push("uniqueItems=true".to_string());
68    }
69    if let Some(p) = &c.pattern {
70        // Insert a zero-width-space inside `///` and `*/` so they
71        // can't terminate the surrounding doc/block comment. Using
72        // the `\u{200B}` escape (vs. a literal U+200B) keeps clippy's
73        // `invisible_characters` lint happy.
74        let safe = p.replace("///", "/\u{200B}//").replace("*/", "*\u{200B}/");
75        parts.push(format!("pattern=`{safe}`"));
76    }
77
78    format!("Constraint: {}", parts.join(", "))
79}
80
81/// `1.0` and `1` should both render as `1` in doc comments.
82/// `1.5` stays `1.5`.
83fn strip_trailing_zero(v: f64) -> String {
84    if v.fract() == 0.0 && v.is_finite() {
85        format!("{}", v as i64)
86    } else {
87        format!("{v}")
88    }
89}
90
91/// One object property after Rust-identifier
92/// disambiguation. Struct fields, request-model constructors, and builders
93/// all consume this shared projection so their names and types cannot drift.
94pub(crate) struct EmittedObjectProperty<'a> {
95    pub(crate) wire_name: &'a str,
96    pub(crate) property: &'a crate::analysis::PropertyInfo,
97    pub(crate) ident: syn::Ident,
98    pub(crate) is_required: bool,
99    pub(crate) field_type: TokenStream,
100}
101
102/// Shared lookups for one types.rs generation pass. Large schemas can contain
103/// thousands of operations and types, so request-root and type-name queries
104/// must not rescan the full analysis for each emitted object.
105struct TypeGenerationIndex {
106    request_body_roots: std::collections::HashSet<String>,
107    reserved_type_names: std::collections::HashSet<String>,
108}
109
110struct TypeGenerationContext<'a> {
111    index: &'a TypeGenerationIndex,
112    enum_wire_names: Option<&'a std::cell::RefCell<EnumWireNames>>,
113}
114
115type EnumWireNames = BTreeMap<String, BTreeMap<String, Option<String>>>;
116
117#[derive(Debug, Clone)]
118pub struct GeneratorConfig {
119    /// Path to OpenAPI specification file
120    pub spec_path: PathBuf,
121    /// Output directory for generated code (e.g., "src/gen")
122    pub output_dir: PathBuf,
123    /// Informational label for the generated module. Does NOT pick
124    /// the on-disk directory (that's `output_dir`) or the Rust module
125    /// path the user mounts the tree at — both of those are the
126    /// user's choice. The label is surfaced in the generated mod.rs
127    /// header as a hint and is otherwise used only by the streaming
128    /// codegen for naming the SSE client module.
129    pub module_name: String,
130    /// Enable SSE streaming client generation
131    pub enable_sse_client: bool,
132    /// Enable async HTTP client generation
133    pub enable_async_client: bool,
134    /// Enable Specta type derives for frontend integration
135    pub enable_specta: bool,
136    /// Custom type mappings
137    pub type_mappings: BTreeMap<String, String>,
138    /// Optional streaming configuration for SSE client generation
139    pub streaming_config: Option<StreamingConfig>,
140    /// Fields that should be treated as nullable even if not marked in the spec
141    /// Format: "SchemaName.fieldName" -> true
142    pub nullable_field_overrides: BTreeMap<String, bool>,
143    /// String-enum schemas that should be rendered as extensible (with a
144    /// `Custom(String)` fallback variant) instead of closed enums. Useful when
145    /// the spec declares a fixed set of values but the API actually returns
146    /// values outside that set (real-world drift: Cloudflare's r2_bucket_location
147    /// declares lowercase but returns uppercase).
148    /// Format: "SchemaName" -> true
149    pub extensible_enum_overrides: BTreeMap<String, bool>,
150    /// Additional schema extension files to merge into the main spec
151    /// These files will be merged additively using simple JSON object merging
152    pub schema_extensions: Vec<PathBuf>,
153    /// Ordered OpenAPI Overlay documents, applied after schema extensions.
154    pub overlays: Vec<PathBuf>,
155    /// Optional output-relative effective OpenAPI JSON artifact.
156    pub effective_spec: Option<PathBuf>,
157    /// Optional output-relative versioned Rust bindings metadata artifact.
158    pub bindings_metadata: Option<String>,
159    /// HTTP client configuration
160    pub http_client_config: Option<crate::http_config::HttpClientConfig>,
161    /// Retry configuration for HTTP requests
162    pub retry_config: Option<crate::http_config::RetryConfig>,
163    /// Enable request/response tracing
164    pub tracing_enabled: bool,
165    /// Authentication configuration
166    pub auth_config: Option<crate::http_config::AuthConfig>,
167    /// Enable operation registry generation (static metadata for CLI/proxy routing)
168    pub enable_registry: bool,
169    /// Generate only the operation registry (skip types, client, streaming)
170    pub registry_only: bool,
171    /// Per-format type-mapping strategies driven by the `[generator.types]`
172    /// TOML section. Q2.0 introduces this field; with the default value
173    /// every mapping preserves pre-refactor behavior.
174    pub types: crate::type_mapping::TypeMappingConfig,
175    /// Additive operation-builder generation policy.
176    pub builders: crate::config::BuildersSection,
177    /// Additional derives for models and HTTP parameter enums.
178    pub derives: crate::derives::DerivesConfig,
179    /// Opt-in server codegen scope. `None` ⇒ emit no server code.
180    /// Set by the `[server]` section in the TOML config.
181    pub server: Option<crate::config::ServerSection>,
182    /// Optional HTTP-client operation scope. `None` or an empty selector list
183    /// preserves generation of every operation.
184    pub client: Option<crate::config::ClientSection>,
185}
186
187impl Default for GeneratorConfig {
188    fn default() -> Self {
189        Self {
190            spec_path: "openapi.json".into(),
191            output_dir: "src/gen".into(),
192            module_name: "api_types".to_string(),
193            enable_sse_client: true,
194            enable_async_client: true,
195            enable_specta: false,
196            type_mappings: default_type_mappings(),
197            streaming_config: None,
198            nullable_field_overrides: BTreeMap::new(),
199            extensible_enum_overrides: BTreeMap::new(),
200            schema_extensions: Vec::new(),
201            overlays: Vec::new(),
202            effective_spec: None,
203            bindings_metadata: None,
204            http_client_config: None,
205            retry_config: None,
206            tracing_enabled: true,
207            auth_config: None,
208            enable_registry: false,
209            registry_only: false,
210            types: crate::type_mapping::TypeMappingConfig::default(),
211            builders: crate::config::BuildersSection::default(),
212            derives: crate::derives::DerivesConfig::default(),
213            server: None,
214            client: None,
215        }
216    }
217}
218
219impl GeneratorConfig {
220    /// Adopt the document's `servers[0].url` as the client's default base URL
221    /// when configuration didn't supply one.
222    ///
223    /// The spec already states where the API lives; making every user restate
224    /// it in TOML (or discover at runtime that requests go nowhere) is friction
225    /// with no upside. Explicit configuration always wins.
226    ///
227    /// Two server URLs are deliberately ignored: relative ones (`/v1`), which
228    /// are meaningless without an origin, and templated ones containing `{}`
229    /// server variables, which are not usable until substituted.
230    pub fn apply_spec_server_default(&mut self, spec: &serde_json::Value) {
231        let already_configured = self
232            .http_client_config
233            .as_ref()
234            .and_then(|http| http.base_url.as_deref())
235            .is_some_and(|url| !url.is_empty());
236        if already_configured {
237            return;
238        }
239
240        let Some(url) = spec
241            .pointer("/servers/0/url")
242            .and_then(serde_json::Value::as_str)
243            .map(str::trim)
244            .filter(|url| !url.is_empty())
245            .filter(|url| !url.starts_with('/'))
246            .filter(|url| !url.contains('{'))
247        else {
248            return;
249        };
250
251        match self.http_client_config.as_mut() {
252            Some(http) => http.base_url = Some(url.to_string()),
253            None => {
254                self.http_client_config = Some(crate::http_config::HttpClientConfig {
255                    base_url: Some(url.to_string()),
256                    timeout_seconds: None,
257                    max_response_body_bytes: None,
258                    default_headers: Default::default(),
259                })
260            }
261        }
262    }
263}
264
265pub fn default_type_mappings() -> BTreeMap<String, String> {
266    let mut mappings = BTreeMap::new();
267    mappings.insert("integer".to_string(), "i64".to_string());
268    mappings.insert("number".to_string(), "f64".to_string());
269    mappings.insert("string".to_string(), "String".to_string());
270    mappings.insert("boolean".to_string(), "bool".to_string());
271    mappings
272}
273
274/// The `n`th variant named `base`, for enum values whose names collide.
275///
276/// `rustc`'s `non_camel_case_types` lint accepts an underscore only between
277/// two characters without case, so `AmberStrict_2` warns while `AmberStrict2`
278/// and `V1_2` don't. The underscore is kept after a digit, where dropping it
279/// would run two numbers together (`V12`).
280pub(crate) fn numbered_variant_name(base: &str, n: usize) -> String {
281    let base = base.trim_end_matches('_');
282    if base.ends_with(|c: char| c.is_ascii_digit()) {
283        format!("{base}_{n}")
284    } else {
285        format!("{base}{n}")
286    }
287}
288
289/// Convert an OpenAPI schema name to the canonical Rust model identifier.
290///
291/// Every code-generation surface must use this helper rather than parsing raw
292/// component keys as Rust types; otherwise names such as `not-found` panic and
293/// names such as `inline_response_200` refer to models that were never emitted.
294pub(crate) fn rust_type_name(s: &str) -> String {
295    let mut result = String::new();
296    let mut next_upper = true;
297
298    for c in s.chars() {
299        match c {
300            'a'..='z' => {
301                result.push(if next_upper {
302                    c.to_ascii_uppercase()
303                } else {
304                    c
305                });
306                next_upper = false;
307            }
308            'A'..='Z' | '0'..='9' => {
309                result.push(c);
310                next_upper = false;
311            }
312            _ => next_upper = true,
313        }
314    }
315
316    if result.is_empty() {
317        result = "Type".to_string();
318    }
319    if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
320        result = format!("Type{result}");
321    }
322    if matches!(
323        result.as_str(),
324        "Result"
325            | "Option"
326            | "Box"
327            | "Vec"
328            | "String"
329            | "Some"
330            | "None"
331            | "Ok"
332            | "Err"
333            | "Default"
334            | "Clone"
335            | "Debug"
336            | "Send"
337            | "Sync"
338            | "Sized"
339            | "Iterator"
340            | "From"
341            | "Into"
342            | "TryFrom"
343            | "TryInto"
344            | "AsRef"
345            | "AsMut"
346    ) {
347        result.push_str("Type");
348    }
349    result
350}
351
352/// Represents a generated file
353#[derive(Debug, Clone)]
354pub struct GeneratedFile {
355    /// Relative path from output directory (e.g., "types.rs", "streaming.rs")
356    pub path: PathBuf,
357    /// Generated Rust code content
358    pub content: String,
359}
360
361/// Result of code generation containing multiple files
362#[derive(Debug, Clone)]
363pub struct GenerationResult {
364    /// All generated files
365    pub files: Vec<GeneratedFile>,
366    /// Generated mod.rs content that exports all modules
367    pub mod_file: GeneratedFile,
368    /// Complete direct dependencies for the exact files in this result,
369    /// including required crate features and compatible versions. The CLI
370    /// writes these as `REQUIRED_DEPS.toml` next to the generated module.
371    pub required_deps: Vec<crate::type_mapping::DepRequirement>,
372    /// Number of schemas removed by opt-in client/server model pruning.
373    pub pruned_schemas: usize,
374}
375
376#[derive(Debug)]
377struct OperationScopes {
378    /// `None` means the enabled HTTP client keeps every operation.
379    client_ids: Option<std::collections::BTreeSet<String>>,
380    server_ids: std::collections::BTreeSet<String>,
381    streaming_ids: std::collections::BTreeSet<String>,
382    prune_models: bool,
383    extra_schema_roots: Vec<String>,
384}
385
386pub struct CodeGenerator {
387    config: GeneratorConfig,
388    source_provenance: Option<String>,
389}
390
391/// The parts of an analyzed object a struct is generated from, bundled so the
392/// generator's entry point keeps a readable signature.
393struct ObjectShape<'a> {
394    properties: &'a BTreeMap<String, crate::analysis::PropertyInfo>,
395    required: &'a std::collections::HashSet<String>,
396    additional_properties: &'a crate::analysis::ObjectAdditionalProperties,
397    /// A union declared alongside the properties, flattened into the struct.
398    variant: Option<&'a crate::analysis::SchemaRef>,
399}
400
401/// The Rust type an untyped schema renders to.
402fn untyped_rust_type(shape: crate::analysis::UntypedShape) -> &'static str {
403    use crate::analysis::UntypedShape;
404    match shape {
405        UntypedShape::Value => "serde_json::Value",
406        UntypedShape::ValueArray => "Vec<serde_json::Value>",
407        UntypedShape::ValueMap => "std::collections::BTreeMap<String, serde_json::Value>",
408    }
409}
410
411fn untyped_tokens(shape: crate::analysis::UntypedShape) -> TokenStream {
412    use crate::analysis::UntypedShape;
413    match shape {
414        UntypedShape::Value => quote! { serde_json::Value },
415        UntypedShape::ValueArray => quote! { Vec<serde_json::Value> },
416        UntypedShape::ValueMap => quote! { std::collections::BTreeMap<String, serde_json::Value> },
417    }
418}
419
420/// Whether `schema_type` uses the Rust type `rust_type` anywhere a model
421/// field can hold it.
422fn schema_type_uses_rust_type(schema_type: &SchemaType, rust_type: &str) -> bool {
423    match schema_type {
424        SchemaType::Primitive {
425            rust_type: actual, ..
426        } => actual == rust_type,
427        SchemaType::Object { properties, .. } => properties
428            .values()
429            .any(|property| schema_type_uses_rust_type(&property.schema_type, rust_type)),
430        SchemaType::Array { item_type }
431        | SchemaType::Nullable {
432            inner_type: item_type,
433        } => schema_type_uses_rust_type(item_type, rust_type),
434        _ => false,
435    }
436}
437
438fn schema_type_uses_serde_codec(schema_type: &SchemaType, codec: &str) -> bool {
439    match schema_type {
440        SchemaType::Primitive {
441            serde_with: Some(actual),
442            ..
443        } => actual == codec,
444        SchemaType::Object {
445            properties,
446            additional_properties,
447            ..
448        } => {
449            properties
450                .values()
451                .any(|property| schema_type_uses_serde_codec(&property.schema_type, codec))
452                || matches!(
453                    additional_properties,
454                    crate::analysis::ObjectAdditionalProperties::Typed { value_type }
455                        if schema_type_uses_serde_codec(value_type, codec)
456                )
457        }
458        SchemaType::Array { item_type }
459        | SchemaType::Nullable {
460            inner_type: item_type,
461        } => schema_type_uses_serde_codec(item_type, codec),
462        SchemaType::Tuple { element_types } => element_types
463            .iter()
464            .any(|element| schema_type_uses_serde_codec(element, codec)),
465        _ => false,
466    }
467}
468
469impl CodeGenerator {
470    /// `MultipartFile`, or the free name the analysis gave it in its place,
471    /// the type of a multipart file field: the part's content,
472    /// in the configured `types.binary` carrier, with its own filename and
473    /// content type. It converts from the content, so a plain value still
474    /// works. In a JSON document it's the content alone, as a UTF-8 string,
475    /// like any other `format: binary` field.
476    fn generate_multipart_file_type(&self, type_name: &str) -> TokenStream {
477        use crate::type_mapping::BinaryStrategy;
478        let ident = format_ident!("{}", type_name);
479        let from_doc = format!(" It converts from its content, so `{type_name}::from(content)`");
480        let (content, conversions, serialize) = match self.config.types.binary {
481            BinaryStrategy::Bytes => (
482                quote! { bytes::Bytes },
483                quote! {
484                    impl From<bytes::Bytes> for #ident {
485                        fn from(content: bytes::Bytes) -> Self { Self::new(content) }
486                    }
487                    impl From<Vec<u8>> for #ident {
488                        fn from(content: Vec<u8>) -> Self { Self::new(content) }
489                    }
490                    impl From<&'static [u8]> for #ident {
491                        fn from(content: &'static [u8]) -> Self { Self::new(content) }
492                    }
493                    impl From<String> for #ident {
494                        fn from(content: String) -> Self { Self::new(content) }
495                    }
496                    impl From<&'static str> for #ident {
497                        fn from(content: &'static str) -> Self { Self::new(content) }
498                    }
499                },
500                quote! {
501                    let content = std::str::from_utf8(self.content.as_ref())
502                        .map_err(serde::ser::Error::custom)?;
503                    ser.serialize_str(content)
504                },
505            ),
506            BinaryStrategy::VecU8 => (
507                quote! { Vec<u8> },
508                quote! {
509                    impl From<Vec<u8>> for #ident {
510                        fn from(content: Vec<u8>) -> Self { Self::new(content) }
511                    }
512                    impl From<&[u8]> for #ident {
513                        fn from(content: &[u8]) -> Self { Self::new(content) }
514                    }
515                    impl From<String> for #ident {
516                        fn from(content: String) -> Self { Self::new(content) }
517                    }
518                    impl From<&str> for #ident {
519                        fn from(content: &str) -> Self { Self::new(content.as_bytes()) }
520                    }
521                },
522                quote! {
523                    let content = std::str::from_utf8(&self.content)
524                        .map_err(serde::ser::Error::custom)?;
525                    ser.serialize_str(content)
526                },
527            ),
528            BinaryStrategy::String => (
529                quote! { String },
530                quote! {
531                    impl From<String> for #ident {
532                        fn from(content: String) -> Self { Self::new(content) }
533                    }
534                    impl From<&str> for #ident {
535                        fn from(content: &str) -> Self { Self::new(content) }
536                    }
537                },
538                quote! { ser.serialize_str(&self.content) },
539            ),
540        };
541        quote! {
542            /// A multipart file part: its content, with the filename and
543            /// content type to send it under. Each defaults to none, and a
544            /// content type declared by the document's `encoding` applies when
545            /// this one is unset.
546            ///
547            #[doc = #from_doc]
548            /// or `content.into()` sends a part without either. In a JSON
549            /// document it's the content alone.
550            #[derive(Debug, Clone, Default, PartialEq, Eq)]
551            pub struct #ident {
552                pub content: #content,
553                pub file_name: Option<String>,
554                pub content_type: Option<String>,
555            }
556
557            impl #ident {
558                /// A part with this content, and no filename or content type.
559                pub fn new(content: impl Into<#content>) -> Self {
560                    Self { content: content.into(), file_name: None, content_type: None }
561                }
562
563                /// Send the part under this filename.
564                #[must_use]
565                pub fn with_file_name(mut self, file_name: impl Into<String>) -> Self {
566                    self.file_name = Some(file_name.into());
567                    self
568                }
569
570                /// Send the part with this `Content-Type`.
571                #[must_use]
572                pub fn with_content_type(mut self, content_type: impl Into<String>) -> Self {
573                    self.content_type = Some(content_type.into());
574                    self
575                }
576            }
577
578            #conversions
579
580            impl Serialize for #ident {
581                fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
582                    #serialize
583                }
584            }
585
586            impl<'de> Deserialize<'de> for #ident {
587                fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
588                    String::deserialize(de).map(Self::new)
589                }
590            }
591        }
592    }
593
594    pub fn new(config: GeneratorConfig) -> Self {
595        Self {
596            config,
597            source_provenance: None,
598        }
599    }
600
601    /// Attach a sanitized source label to generated module headers.
602    pub fn with_source_provenance(mut self, source: impl Into<String>) -> Self {
603        self.source_provenance = Some(source.into());
604        self
605    }
606
607    /// Get reference to the generator configuration
608    pub fn config(&self) -> &GeneratorConfig {
609        &self.config
610    }
611
612    pub(crate) fn provenance_attribute(&self) -> TokenStream {
613        self.source_provenance
614            .as_ref()
615            .map(|source| {
616                let provenance = format!(
617                    " Generated by openapi-to-rust v{}. Source OpenAPI document: {source}",
618                    env!("CARGO_PKG_VERSION")
619                );
620                quote! { #![doc = #provenance] }
621            })
622            .unwrap_or_default()
623    }
624
625    /// Generate all files for the API
626    pub fn generate_all(&self, analysis: &mut SchemaAnalysis) -> Result<GenerationResult> {
627        Ok(self
628            .generate_all_internal(analysis, self.config.bindings_metadata.is_some())?
629            .generation)
630    }
631
632    /// Generate Rust output and versioned metadata from the same emission ASTs.
633    /// Metadata paths are relative to the module chosen by the consumer.
634    pub fn generate_all_with_bindings(
635        &self,
636        analysis: &mut SchemaAnalysis,
637    ) -> Result<crate::bindings::GenerationWithBindings> {
638        self.generate_all_internal(analysis, true)
639    }
640
641    fn generate_all_internal(
642        &self,
643        analysis: &mut SchemaAnalysis,
644        collect_bindings: bool,
645    ) -> Result<crate::bindings::GenerationWithBindings> {
646        self.validate_derives(analysis)?;
647        if collect_bindings
648            && (self.config.registry_only
649                || self.config.enable_registry
650                || self
651                    .config
652                    .server
653                    .as_ref()
654                    .is_some_and(|server| !server.operations.is_empty())
655                || (self.config.enable_sse_client && self.config.streaming_config.is_some()))
656        {
657            return Err(GeneratorError::ValidationError(
658                "bindings metadata v1 supports models and HTTP client output; registry, server, and configured streaming output are unsupported".to_string(),
659            ));
660        }
661        let mut bindings = crate::bindings::BindingsMetadata::new(
662            &self.config.module_name,
663            self.config.enable_async_client,
664        );
665        // Resolve client/server selectors exactly once for this generation.
666        // The same scopes drive client artifacts and the union model closure.
667        let scopes = self.resolve_operation_scopes(analysis)?;
668        let pruned_schemas = self.prune_models_to_scopes(analysis, &scopes);
669        let mut files = Vec::new();
670
671        if !self.config.registry_only {
672            // Generate types file
673            let types_content = self.generate_types_with_bindings(
674                analysis,
675                collect_bindings.then_some(&mut bindings),
676            )?;
677            files.push(GeneratedFile {
678                path: "types.rs".into(),
679                content: types_content,
680            });
681
682            // Generate streaming client if configured
683            if self.config.enable_sse_client
684                && let Some(ref streaming_config) = self.config.streaming_config
685            {
686                if streaming_config.generate_client && !streaming_config.event_parser_helpers {
687                    return Err(GeneratorError::ValidationError(
688                        "streaming generate_client=true requires event_parser_helpers=true"
689                            .to_string(),
690                    ));
691                }
692                if streaming_config.event_parser_helpers {
693                    files.push(GeneratedFile {
694                        path: "sse.rs".into(),
695                        content: self.generate_sse_runtime()?,
696                    });
697                }
698                let streaming_content =
699                    self.generate_streaming_client(streaming_config, analysis)?;
700                files.push(GeneratedFile {
701                    path: "streaming.rs".into(),
702                    content: streaming_content,
703                });
704            }
705
706            // Generate HTTP client if enabled
707            if self.config.enable_async_client {
708                let operations = self.client_operations(analysis, scopes.client_ids.as_ref());
709                let needs_sse_runtime = self
710                    .client_method_plans(analysis, &operations)
711                    .iter()
712                    .any(|plan| plan.response.consumption == "parsed_sse");
713                if needs_sse_runtime
714                    && !files
715                        .iter()
716                        .any(|file| file.path == std::path::Path::new("sse.rs"))
717                {
718                    files.push(GeneratedFile {
719                        path: "sse.rs".into(),
720                        content: self.generate_sse_runtime_with_bindings(
721                            collect_bindings.then_some(&mut bindings),
722                        )?,
723                    });
724                }
725                let http_content = self.generate_http_client_for_operations_with_bindings(
726                    analysis,
727                    &operations,
728                    collect_bindings.then_some(&mut bindings),
729                )?;
730                files.push(GeneratedFile {
731                    path: "client.rs".into(),
732                    content: http_content,
733                });
734            }
735        }
736
737        // Generate operation registry if enabled
738        if self.config.enable_registry || self.config.registry_only {
739            let registry_content = self.generate_registry(analysis)?;
740            files.push(GeneratedFile {
741                path: "registry.rs".into(),
742                content: registry_content,
743            });
744        }
745
746        // Server files are part of the same generation result so module wiring,
747        // dependency collection, and disk writes cannot drift from the CLI's
748        // post-processing path.
749        if !self.config.registry_only
750            && let Some(server) = self
751                .config
752                .server
753                .as_ref()
754                .filter(|server| !server.operations.is_empty())
755        {
756            let server_files =
757                crate::server::codegen::ServerCodegen::new(&self.config, analysis, server)
758                    .with_source_provenance(self.source_provenance.as_deref())
759                    .generate()
760                    .map_err(|error| {
761                        GeneratorError::CodeGenError(format!(
762                            "server code generation failed: {error}"
763                        ))
764                    })?;
765            files.extend(server_files);
766        }
767
768        // Generate mod.rs file
769        let mod_content = self.generate_mod_file(&files)?;
770        let mod_file = GeneratedFile {
771            path: "mod.rs".into(),
772            content: mod_content,
773        };
774
775        let required_deps = crate::type_mapping::collect_generated_dep_requirements(
776            files.iter().map(|file| file.content.as_str()),
777            self.config.enable_specta,
778        );
779
780        if collect_bindings {
781            let top_level_modules: Vec<_> = files
782                .iter()
783                .filter_map(|file| {
784                    file.path
785                        .file_stem()
786                        .and_then(|stem| stem.to_str())
787                        .map(str::to_string)
788                })
789                .collect();
790            bindings.modules.extend(top_level_modules.iter().cloned());
791            bindings.reexports.extend(
792                top_level_modules
793                    .iter()
794                    .filter(|module| module.as_str() != "sse")
795                    .map(|module| format!("{module}::*")),
796            );
797            bindings.finish();
798        }
799        if let Some(path) = &self.config.bindings_metadata {
800            let artifact_path = std::path::Path::new(path);
801            crate::overlay::validate_artifact_path(artifact_path)?;
802            let overlaps = |other: &std::path::Path| {
803                artifact_path.starts_with(other) || other.starts_with(artifact_path)
804            };
805            if files.iter().any(|file| overlaps(&file.path))
806                || overlaps(std::path::Path::new("mod.rs"))
807                || overlaps(std::path::Path::new("REQUIRED_DEPS.toml"))
808                || self.config.effective_spec.as_deref().is_some_and(overlaps)
809            {
810                return Err(GeneratorError::ValidationError(format!(
811                    "bindings metadata path conflicts with generated artifact: {path}"
812                )));
813            }
814            files.push(GeneratedFile {
815                path: path.into(),
816                content: bindings.to_json()?,
817            });
818        }
819        Ok(crate::bindings::GenerationWithBindings {
820            generation: GenerationResult {
821                files,
822                mod_file,
823                required_deps,
824                pruned_schemas,
825            },
826            bindings,
827        })
828    }
829
830    /// Generate just the types (legacy single-file interface)
831    pub fn generate(&self, analysis: &mut SchemaAnalysis) -> Result<String> {
832        self.validate_derives(analysis)?;
833        self.generate_types(analysis)
834    }
835
836    /// Generate the types.rs file content
837    fn generate_types(&self, analysis: &mut SchemaAnalysis) -> Result<String> {
838        self.generate_types_with_bindings(analysis, None)
839    }
840
841    fn generate_types_with_bindings(
842        &self,
843        analysis: &mut SchemaAnalysis,
844        bindings: Option<&mut crate::bindings::BindingsMetadata>,
845    ) -> Result<String> {
846        self.validate_schema_type_names(analysis)?;
847
848        let provenance_attribute = self.provenance_attribute();
849        let mut type_definitions = TokenStream::new();
850
851        let type_index = self.type_generation_index(analysis);
852        let enum_wire_names = std::cell::RefCell::new(BTreeMap::new());
853        let type_context = TypeGenerationContext {
854            index: &type_index,
855            enum_wire_names: bindings.as_ref().map(|_| &enum_wire_names),
856        };
857
858        // Generate types based on dependency order
859        let generation_order = analysis.dependencies.topological_sort()?;
860
861        let mut processed = std::collections::HashSet::new();
862
863        // First, generate schemas in dependency order
864        for schema_name in generation_order {
865            if let Some(schema) = analysis.schemas.get(&schema_name) {
866                let type_def = self.generate_type_definition(schema, analysis, &type_context)?;
867                if !type_def.is_empty() {
868                    type_definitions.extend(type_def);
869                }
870                processed.insert(schema_name);
871            }
872        }
873
874        // Then generate any remaining schemas not in dependency graph
875        let mut remaining_schemas: Vec<_> = analysis
876            .schemas
877            .iter()
878            .filter(|(name, _)| !processed.contains(*name))
879            .collect();
880        remaining_schemas.sort_by_key(|(name, _)| name.as_str());
881
882        for (_schema_name, schema) in remaining_schemas {
883            let type_def = self.generate_type_definition(schema, analysis, &type_context)?;
884            if !type_def.is_empty() {
885                type_definitions.extend(type_def);
886            }
887        }
888
889        let mut uses_plain_tri_state = false;
890        let mut tri_state_codecs = std::collections::HashSet::new();
891        for (schema_name, schema) in &analysis.schemas {
892            let crate::analysis::SchemaType::Object {
893                properties,
894                required,
895                ..
896            } = &schema.schema_type
897            else {
898                continue;
899            };
900            for (field_name, property) in properties {
901                if !self.property_is_tri_state(
902                    schema_name,
903                    field_name,
904                    property,
905                    required.contains(field_name),
906                ) {
907                    continue;
908                }
909                if let Some(codec) = self.schema_type_serde_codec(&property.schema_type, analysis) {
910                    tri_state_codecs.insert(codec);
911                } else {
912                    uses_plain_tri_state = true;
913                }
914            }
915        }
916
917        // Helper modules emitted only when the analyzer actually
918        // referenced their codecs. Avoids polluting every generated
919        // file (and every snapshot) with dead code for specs that
920        // don't use `format: byte`.
921        let base64_double_option = if tri_state_codecs.contains("base64_serde") {
922            quote! {
923                pub mod double_option {
924                    use serde::{Deserializer, Serializer};
925
926                    pub fn serialize<S: Serializer>(
927                        value: &Option<Option<Vec<u8>>>,
928                        ser: S,
929                    ) -> Result<S::Ok, S::Error> {
930                        match value {
931                            Some(value) => super::option::serialize(value, ser),
932                            None => ser.serialize_none(),
933                        }
934                    }
935
936                    pub fn deserialize<'de, D: Deserializer<'de>>(
937                        de: D,
938                    ) -> Result<Option<Option<Vec<u8>>>, D::Error> {
939                        super::option::deserialize(de).map(Some)
940                    }
941                }
942            }
943        } else {
944            TokenStream::new()
945        };
946        let base64_helper = if analysis
947            .used_type_features
948            .contains(crate::type_mapping::TypeFeature::Base64)
949        {
950            let engine = match self.config.types.byte {
951                crate::type_mapping::ByteStrategy::Base64UrlUnpadded => {
952                    quote::format_ident!("URL_SAFE_NO_PAD")
953                }
954                _ => quote::format_ident!("STANDARD"),
955            };
956            quote! {
957                /// base64 codec for `Vec<u8>` fields produced from
958                /// `format: byte`. Used via `#[serde(with = "base64_serde")]`
959                /// for required/non-null fields; `with = "base64_serde::option"`
960                /// for the Option<Vec<u8>> case.
961                mod base64_serde {
962                    use base64::{Engine as _, engine::general_purpose::#engine as ENGINE};
963                    use serde::{Deserialize, Deserializer, Serializer};
964
965                    pub fn serialize<S: Serializer>(
966                        bytes: &Vec<u8>,
967                        ser: S,
968                    ) -> Result<S::Ok, S::Error> {
969                        ser.serialize_str(&ENGINE.encode(bytes))
970                    }
971
972                    pub fn deserialize<'de, D: Deserializer<'de>>(
973                        de: D,
974                    ) -> Result<Vec<u8>, D::Error> {
975                        let s = String::deserialize(de)?;
976                        ENGINE
977                            .decode(s.as_bytes())
978                            .map_err(serde::de::Error::custom)
979                    }
980
981                    /// Codec for Option<Vec<u8>> fields (optional /
982                    /// nullable `format: byte`). serde dispatches on
983                    /// the field type; without this submodule the
984                    /// `?` operator in the generated code would fail
985                    /// to convert Vec<u8> to Option<Vec<u8>>.
986                    pub mod option {
987                        use super::*;
988                        use serde::{Deserialize, Deserializer, Serializer};
989
990                        pub fn serialize<S: Serializer>(
991                            opt: &Option<Vec<u8>>,
992                            ser: S,
993                        ) -> Result<S::Ok, S::Error> {
994                            match opt {
995                                Some(bytes) => super::serialize(bytes, ser),
996                                None => ser.serialize_none(),
997                            }
998                        }
999
1000                        pub fn deserialize<'de, D: Deserializer<'de>>(
1001                            de: D,
1002                        ) -> Result<Option<Vec<u8>>, D::Error> {
1003                            let opt = Option::<String>::deserialize(de)?;
1004                            opt.map(|s| {
1005                                ENGINE
1006                                    .decode(s.as_bytes())
1007                                    .map_err(serde::de::Error::custom)
1008                            })
1009                            .transpose()
1010                        }
1011                    }
1012
1013                    #base64_double_option
1014                }
1015            }
1016        } else {
1017            TokenStream::new()
1018        };
1019
1020        let uses_binary_bytes_codec = analysis
1021            .schemas
1022            .values()
1023            .any(|schema| schema_type_uses_serde_codec(&schema.schema_type, "binary_bytes_serde"));
1024        let binary_bytes_double_option = if tri_state_codecs.contains("binary_bytes_serde") {
1025            quote! {
1026                pub mod double_option {
1027                    use serde::{Deserializer, Serializer};
1028
1029                    pub fn serialize<S: Serializer>(
1030                        value: &Option<Option<bytes::Bytes>>,
1031                        ser: S,
1032                    ) -> Result<S::Ok, S::Error> {
1033                        match value {
1034                            Some(value) => super::option::serialize(value, ser),
1035                            None => ser.serialize_none(),
1036                        }
1037                    }
1038
1039                    pub fn deserialize<'de, D: Deserializer<'de>>(
1040                        de: D,
1041                    ) -> Result<Option<Option<bytes::Bytes>>, D::Error> {
1042                        super::option::deserialize(de).map(Some)
1043                    }
1044                }
1045            }
1046        } else {
1047            TokenStream::new()
1048        };
1049        let binary_bytes_helper = if uses_binary_bytes_codec {
1050            quote! {
1051                /// UTF-8 JSON string codec for `bytes::Bytes` model fields
1052                /// produced from `format: binary`. Raw HTTP body and multipart
1053                /// paths use their byte carriers directly and do not invoke it.
1054                mod binary_bytes_serde {
1055                    use serde::{Deserialize, Deserializer, Serializer};
1056
1057                    pub fn serialize<S: Serializer>(
1058                        bytes: &bytes::Bytes,
1059                        ser: S,
1060                    ) -> Result<S::Ok, S::Error> {
1061                        let value = std::str::from_utf8(bytes.as_ref())
1062                            .map_err(serde::ser::Error::custom)?;
1063                        ser.serialize_str(value)
1064                    }
1065
1066                    pub fn deserialize<'de, D: Deserializer<'de>>(
1067                        de: D,
1068                    ) -> Result<bytes::Bytes, D::Error> {
1069                        String::deserialize(de).map(bytes::Bytes::from)
1070                    }
1071
1072                    pub mod option {
1073                        use serde::{Deserialize, Deserializer, Serializer};
1074
1075                        pub fn serialize<S: Serializer>(
1076                            value: &Option<bytes::Bytes>,
1077                            ser: S,
1078                        ) -> Result<S::Ok, S::Error> {
1079                            match value {
1080                                Some(bytes) => super::serialize(bytes, ser),
1081                                None => ser.serialize_none(),
1082                            }
1083                        }
1084
1085                        pub fn deserialize<'de, D: Deserializer<'de>>(
1086                            de: D,
1087                        ) -> Result<Option<bytes::Bytes>, D::Error> {
1088                            Option::<String>::deserialize(de)
1089                                .map(|value| value.map(bytes::Bytes::from))
1090                        }
1091                    }
1092
1093                    #binary_bytes_double_option
1094                }
1095            }
1096        } else {
1097            TokenStream::new()
1098        };
1099
1100        let uses_binary_vec_codec = analysis
1101            .schemas
1102            .values()
1103            .any(|schema| schema_type_uses_serde_codec(&schema.schema_type, "binary_vec_serde"));
1104        let binary_vec_double_option = if tri_state_codecs.contains("binary_vec_serde") {
1105            quote! {
1106                /// Preserve the distinction between an omitted field and an
1107                /// explicit JSON null while retaining the binary codec.
1108                pub mod double_option {
1109                    use serde::{Deserializer, Serializer};
1110
1111                    pub fn serialize<S: Serializer>(
1112                        value: &Option<Option<Vec<u8>>>,
1113                        ser: S,
1114                    ) -> Result<S::Ok, S::Error> {
1115                        match value {
1116                            Some(value) => super::option::serialize(value, ser),
1117                            None => ser.serialize_none(),
1118                        }
1119                    }
1120
1121                    pub fn deserialize<'de, D: Deserializer<'de>>(
1122                        de: D,
1123                    ) -> Result<Option<Option<Vec<u8>>>, D::Error> {
1124                        super::option::deserialize(de).map(Some)
1125                    }
1126                }
1127            }
1128        } else {
1129            TokenStream::new()
1130        };
1131        let binary_vec_helper = if uses_binary_vec_codec {
1132            quote! {
1133                /// UTF-8 JSON string codec for `Vec<u8>` model fields produced
1134                /// from `format: binary` under the vec_u8 strategy.
1135                mod binary_vec_serde {
1136                    use serde::{Deserialize, Deserializer, Serializer};
1137
1138                    pub fn serialize<S: Serializer>(
1139                        bytes: &Vec<u8>,
1140                        ser: S,
1141                    ) -> Result<S::Ok, S::Error> {
1142                        let value = std::str::from_utf8(bytes)
1143                            .map_err(serde::ser::Error::custom)?;
1144                        ser.serialize_str(value)
1145                    }
1146
1147                    pub fn deserialize<'de, D: Deserializer<'de>>(
1148                        de: D,
1149                    ) -> Result<Vec<u8>, D::Error> {
1150                        String::deserialize(de).map(String::into_bytes)
1151                    }
1152
1153                    pub mod option {
1154                        use serde::{Deserialize, Deserializer, Serializer};
1155
1156                        pub fn serialize<S: Serializer>(
1157                            value: &Option<Vec<u8>>,
1158                            ser: S,
1159                        ) -> Result<S::Ok, S::Error> {
1160                            match value {
1161                                Some(bytes) => super::serialize(bytes, ser),
1162                                None => ser.serialize_none(),
1163                            }
1164                        }
1165
1166                        pub fn deserialize<'de, D: Deserializer<'de>>(
1167                            de: D,
1168                        ) -> Result<Option<Vec<u8>>, D::Error> {
1169                            Option::<String>::deserialize(de)
1170                                .map(|value| value.map(String::into_bytes))
1171                        }
1172                    }
1173
1174                    #binary_vec_double_option
1175                }
1176            }
1177        } else {
1178            TokenStream::new()
1179        };
1180
1181        let multipart_file_helper = match &analysis.multipart_file_type {
1182            Some(type_name)
1183                if analysis
1184                    .schemas
1185                    .values()
1186                    .any(|schema| schema_type_uses_rust_type(&schema.schema_type, type_name)) =>
1187            {
1188                self.generate_multipart_file_type(type_name)
1189            }
1190            _ => TokenStream::new(),
1191        };
1192
1193        let tri_state_helper = if uses_plain_tri_state {
1194            quote! {
1195                /// Serde normally maps both a missing `Option<T>` field and an
1196                /// explicit JSON null to `None`. Wrapping the decoded value in
1197                /// `Some` retains the field-presence bit for `Option<Option<T>>`.
1198                mod tri_state_serde {
1199                    use serde::{Deserialize, Deserializer};
1200
1201                    pub fn deserialize<'de, D, T>(de: D) -> Result<Option<T>, D::Error>
1202                    where
1203                        D: Deserializer<'de>,
1204                        T: Deserialize<'de>,
1205                    {
1206                        T::deserialize(de).map(Some)
1207                    }
1208                }
1209            }
1210        } else {
1211            TokenStream::new()
1212        };
1213
1214        // `time::Date` / `time::Time` have no built-in serde codec
1215        // in the `time` crate (`time::serde::iso8601` is
1216        // OffsetDateTime-only — GH #25), so declare one per type via
1217        // the `format_description!` macro. It expands to a module
1218        // (with an `::option` submodule) referenced from fields as
1219        // `#[serde(with = "time_date_format")]` etc.
1220        let time_date_double_option = if tri_state_codecs.contains("time_date_format") {
1221            quote! {
1222                mod time_date_double_option {
1223                    use serde::{Deserializer, Serializer};
1224
1225                    pub fn serialize<S: Serializer>(
1226                        value: &Option<Option<time::Date>>,
1227                        ser: S,
1228                    ) -> Result<S::Ok, S::Error> {
1229                        match value {
1230                            Some(value) => time_date_format::option::serialize(value, ser),
1231                            None => ser.serialize_none(),
1232                        }
1233                    }
1234
1235                    pub fn deserialize<'de, D: Deserializer<'de>>(
1236                        de: D,
1237                    ) -> Result<Option<Option<time::Date>>, D::Error> {
1238                        time_date_format::option::deserialize(de).map(Some)
1239                    }
1240                }
1241            }
1242        } else {
1243            TokenStream::new()
1244        };
1245        let time_date_helper = if analysis
1246            .used_type_features
1247            .contains(crate::type_mapping::TypeFeature::TimeDate)
1248        {
1249            quote! {
1250                time::serde::format_description!(
1251                    time_date_format,
1252                    Date,
1253                    "[year]-[month]-[day]"
1254                );
1255
1256                #time_date_double_option
1257            }
1258        } else {
1259            TokenStream::new()
1260        };
1261
1262        // RFC 3339 partial-time. `[optional [...]]` groups always
1263        // format their contents, so whole seconds serialize with a
1264        // trailing ".0" — in exchange, parsing accepts inputs both
1265        // with and without fractional seconds.
1266        let time_time_double_option = if tri_state_codecs.contains("time_time_format") {
1267            quote! {
1268                mod time_time_double_option {
1269                    use serde::{Deserializer, Serializer};
1270
1271                    pub fn serialize<S: Serializer>(
1272                        value: &Option<Option<time::Time>>,
1273                        ser: S,
1274                    ) -> Result<S::Ok, S::Error> {
1275                        match value {
1276                            Some(value) => time_time_format::option::serialize(value, ser),
1277                            None => ser.serialize_none(),
1278                        }
1279                    }
1280
1281                    pub fn deserialize<'de, D: Deserializer<'de>>(
1282                        de: D,
1283                    ) -> Result<Option<Option<time::Time>>, D::Error> {
1284                        time_time_format::option::deserialize(de).map(Some)
1285                    }
1286                }
1287            }
1288        } else {
1289            TokenStream::new()
1290        };
1291        let time_time_helper = if analysis
1292            .used_type_features
1293            .contains(crate::type_mapping::TypeFeature::TimeTime)
1294        {
1295            quote! {
1296                time::serde::format_description!(
1297                    version = 2,
1298                    time_time_format,
1299                    Time,
1300                    "[hour]:[minute]:[second][optional [.[subsecond]]]"
1301                );
1302
1303                #time_time_double_option
1304            }
1305        } else {
1306            TokenStream::new()
1307        };
1308
1309        let time_rfc3339_double_option_helper = if tri_state_codecs.contains("time::serde::rfc3339")
1310        {
1311            quote! {
1312                mod time_rfc3339_double_option {
1313                    use serde::{Deserializer, Serializer};
1314
1315                    pub fn serialize<S: Serializer>(
1316                        value: &Option<Option<time::OffsetDateTime>>,
1317                        ser: S,
1318                    ) -> Result<S::Ok, S::Error> {
1319                        match value {
1320                            Some(value) => time::serde::rfc3339::option::serialize(value, ser),
1321                            None => ser.serialize_none(),
1322                        }
1323                    }
1324
1325                    pub fn deserialize<'de, D: Deserializer<'de>>(
1326                        de: D,
1327                    ) -> Result<Option<Option<time::OffsetDateTime>>, D::Error> {
1328                        time::serde::rfc3339::option::deserialize(de).map(Some)
1329                    }
1330                }
1331            }
1332        } else {
1333            TokenStream::new()
1334        };
1335
1336        // Generate file with imports and types (no module wrapper).
1337        let generated = quote! {
1338            //! Generated types from OpenAPI specification
1339            //!
1340            //! This file contains all the generated types for the API.
1341            //! Do not edit manually - regenerate using the appropriate script.
1342
1343            #provenance_attribute
1344
1345            #![allow(clippy::large_enum_variant)]
1346            #![allow(clippy::format_in_format_args)]
1347            #![allow(clippy::let_unit_value)]
1348            #![allow(unreachable_patterns)]
1349
1350            use serde::{Deserialize, Serialize};
1351
1352            #base64_helper
1353
1354            #binary_bytes_helper
1355
1356            #binary_vec_helper
1357
1358            #multipart_file_helper
1359
1360            #tri_state_helper
1361
1362            #time_date_helper
1363
1364            #time_time_helper
1365
1366            #time_rfc3339_double_option_helper
1367
1368            #type_definitions
1369        };
1370
1371        // Format the generated code
1372        let mut syntax_tree = syn::parse2::<syn::File>(generated).map_err(|e| {
1373            GeneratorError::CodeGenError(format!("Failed to parse generated code: {e}"))
1374        })?;
1375
1376        if !self.config.derives.is_empty() {
1377            self.config
1378                .derives
1379                .apply(&mut syntax_tree, &self.model_derive_targets(analysis))?;
1380        }
1381
1382        if let Some(bindings) = bindings {
1383            bindings.collect("types", &syntax_tree);
1384            for (name, variants) in enum_wire_names.into_inner() {
1385                if let Some(symbol) = bindings
1386                    .symbols
1387                    .iter_mut()
1388                    .find(|symbol| symbol.path == format!("types::{name}"))
1389                {
1390                    for variant in &mut symbol.variants {
1391                        variant.wire_name = variants.get(&variant.name).cloned().flatten();
1392                    }
1393                }
1394            }
1395        }
1396
1397        let formatted = prettyplease::unparse(&syntax_tree);
1398
1399        Ok(formatted)
1400    }
1401
1402    /// Generate streaming client code
1403    fn generate_streaming_client(
1404        &self,
1405        streaming_config: &StreamingConfig,
1406        analysis: &SchemaAnalysis,
1407    ) -> Result<String> {
1408        let mut client_code = TokenStream::new();
1409        let provenance_attribute = self.provenance_attribute();
1410        let duration_import = streaming_config
1411            .reconnection_config
1412            .as_ref()
1413            .map(|_| quote! { use std::time::Duration; });
1414
1415        // Generate imports
1416        let imports = quote! {
1417            //! Generated streaming client for SSE (Server-Sent Events)
1418            //!
1419            //! This file contains the streaming client implementation.
1420            //! Do not edit manually - regenerate using the appropriate script.
1421            #provenance_attribute
1422            #![allow(clippy::format_in_format_args)]
1423            #![allow(clippy::let_unit_value)]
1424            #![allow(unused_mut)]
1425
1426            use super::types::*;
1427            use async_trait::async_trait;
1428            use futures_util::Stream;
1429            use std::pin::Pin;
1430            use reqwest::header::{HeaderMap, HeaderValue};
1431            use tracing::{debug, info, instrument};
1432            #duration_import
1433        };
1434        client_code.extend(imports);
1435
1436        if streaming_config.generate_client {
1437            if streaming_config.reconnection_config.is_some() {
1438                client_code.extend(quote! {
1439                    use super::sse::{BoxSseStream, SseClient, SseReconnectOptions};
1440                    pub use super::sse::StreamingError;
1441                });
1442            } else {
1443                client_code.extend(quote! {
1444                    use super::sse::{BoxSseStream, SseClient};
1445                    pub use super::sse::StreamingError;
1446                });
1447            }
1448        }
1449
1450        // Generate client trait for each endpoint
1451        for endpoint in &streaming_config.endpoints {
1452            let trait_code = self.generate_endpoint_trait(endpoint, analysis)?;
1453            client_code.extend(trait_code);
1454        }
1455
1456        // Generate client implementation
1457        if streaming_config.generate_client {
1458            let client_impl = self.generate_streaming_client_impl(streaming_config, analysis)?;
1459            client_code.extend(client_impl);
1460        }
1461
1462        // Generate reconnection utilities if configured
1463        if let Some(reconnect_config) = &streaming_config.reconnection_config {
1464            let reconnect_code = self.generate_reconnection_utilities(reconnect_config)?;
1465            client_code.extend(reconnect_code);
1466        }
1467
1468        let syntax_tree = syn::parse2::<syn::File>(client_code).map_err(|e| {
1469            GeneratorError::CodeGenError(format!("Failed to parse streaming client code: {e}"))
1470        })?;
1471
1472        Ok(prettyplease::unparse(&syntax_tree))
1473    }
1474
1475    fn validate_schema_type_names(&self, analysis: &SchemaAnalysis) -> Result<()> {
1476        let mut source_by_rust_name = BTreeMap::<String, String>::new();
1477
1478        for schema in analysis.schemas.values() {
1479            let rust_name = self.to_rust_type_name(&schema.name);
1480            if let Some(first) = source_by_rust_name.insert(rust_name.clone(), schema.name.clone())
1481            {
1482                return Err(GeneratorError::InvalidSchema(format!(
1483                    "schema names `{first}` and `{}` both map to Rust type `{rust_name}`",
1484                    schema.name
1485                )));
1486            }
1487        }
1488
1489        Ok(())
1490    }
1491
1492    /// Generate HTTP client code for regular (non-streaming) requests.
1493    ///
1494    /// This standalone entry point honors `[client].operations` but does not
1495    /// validate unrelated server or streaming scopes. Use [`Self::generate_all`]
1496    /// when generating the complete configured output set.
1497    /// SSE variants use raw byte streams in this standalone file. The bundled
1498    /// entry point emits parsed SSE variants together with their `sse.rs` runtime.
1499    pub fn generate_http_client(&self, analysis: &SchemaAnalysis) -> Result<String> {
1500        self.validate_derives(analysis)?;
1501        let client_ids = self.resolve_client_operation_ids(analysis)?;
1502        let operations = self.client_operations(analysis, client_ids.as_ref());
1503        let mut config = self.config.clone();
1504        config.enable_sse_client = false;
1505        let standalone = Self {
1506            config,
1507            source_provenance: self.source_provenance.clone(),
1508        };
1509        standalone.generate_http_client_for_operations(analysis, &operations)
1510    }
1511
1512    fn generate_http_client_for_operations(
1513        &self,
1514        analysis: &SchemaAnalysis,
1515        operations: &[&crate::analysis::OperationInfo],
1516    ) -> Result<String> {
1517        self.generate_http_client_for_operations_with_bindings(analysis, operations, None)
1518    }
1519
1520    fn generate_http_client_for_operations_with_bindings(
1521        &self,
1522        analysis: &SchemaAnalysis,
1523        operations: &[&crate::analysis::OperationInfo],
1524        bindings: Option<&mut crate::bindings::BindingsMetadata>,
1525    ) -> Result<String> {
1526        let provenance_attribute = self.provenance_attribute();
1527        let error_types = self.generate_http_error_types();
1528        let client_struct = self.generate_http_client_struct();
1529        let operation_methods = self.generate_operation_methods_for(analysis, operations);
1530
1531        let generated = quote! {
1532            //! Generated HTTP client for regular API requests
1533            //!
1534            //! This file contains the HTTP client implementation for GET, POST, etc.
1535            //! Do not edit manually - regenerate using the appropriate script.
1536            #provenance_attribute
1537            #![allow(clippy::format_in_format_args)]
1538            #![allow(clippy::let_unit_value)]
1539
1540            use super::types::*;
1541
1542            #error_types
1543
1544            #client_struct
1545
1546            #operation_methods
1547        };
1548
1549        let mut syntax_tree = syn::parse2::<syn::File>(generated.clone()).map_err(|e| {
1550            if let Ok(dump) = std::env::var("OATR_DUMP_TOKENS_ON_PARSE_ERROR") {
1551                let _ = std::fs::write(&dump, generated.to_string());
1552            }
1553            GeneratorError::CodeGenError(format!("Failed to parse HTTP client code: {e}"))
1554        })?;
1555
1556        if !self.config.derives.is_empty() {
1557            self.config
1558                .derives
1559                .apply(&mut syntax_tree, &self.parameter_derive_targets(operations))?;
1560        }
1561
1562        if let Some(bindings) = bindings {
1563            bindings.collect("client", &syntax_tree);
1564            self.attach_operation_bindings(analysis, operations, bindings)?;
1565        }
1566
1567        Ok(prettyplease::unparse(&syntax_tree))
1568    }
1569
1570    fn attach_operation_bindings(
1571        &self,
1572        analysis: &SchemaAnalysis,
1573        operations: &[&crate::analysis::OperationInfo],
1574        bindings: &mut crate::bindings::BindingsMetadata,
1575    ) -> Result<()> {
1576        for plan in self.client_method_plans(analysis, operations) {
1577            let source = analysis
1578                .operation_sources
1579                .get(&plan.operation.operation_id)
1580                .ok_or_else(|| {
1581                    GeneratorError::ValidationError(format!(
1582                        "bindings metadata requires effective-source identity for operation '{}'",
1583                        plan.operation.operation_id
1584                    ))
1585                })?;
1586            let path = format!("client::HttpClient::{}", plan.method_ident);
1587            let content_type_path = plan
1588                .content_type_method_ident
1589                .as_ref()
1590                .map(|ident| format!("client::HttpClient::{ident}"));
1591            let builder_entry = plan
1592                .builder_method_ident
1593                .as_ref()
1594                .map(|ident| format!("client::HttpClient::{ident}"));
1595            let builder_type = plan
1596                .builder_type_ident
1597                .as_ref()
1598                .map(|ident| format!("client::{ident}"));
1599            let operation_binding = crate::bindings::BindingOperation {
1600                operation_id: plan.operation.operation_id.clone(),
1601                source_json_pointer: source.json_pointer.clone(),
1602                source_method: source.method.clone(),
1603                source_path: source.path.clone(),
1604                source_operation_id: source.operation_id.clone(),
1605                webhook: source.webhook,
1606                response_statuses: plan.response.statuses,
1607                response_excluded_statuses: plan.response.excluded_statuses,
1608                response_media_type: plan.response.media_type,
1609                response_kind: plan.response.kind,
1610                consumption: plan.response.consumption,
1611            };
1612            for symbol in &mut bindings.symbols {
1613                if symbol.path == path
1614                    || content_type_path.as_ref() == Some(&symbol.path)
1615                    || builder_entry.as_ref() == Some(&symbol.path)
1616                    || builder_type.as_ref().is_some_and(|owner| {
1617                        symbol.path == *owner || symbol.path.starts_with(&format!("{owner}::"))
1618                    })
1619                {
1620                    symbol.operation = Some(operation_binding.clone());
1621                }
1622            }
1623        }
1624        Ok(())
1625    }
1626
1627    fn resolve_operation_scopes(&self, analysis: &SchemaAnalysis) -> Result<OperationScopes> {
1628        let client_ids = if self.config.enable_async_client && !self.config.registry_only {
1629            self.resolve_client_operation_ids(analysis)?
1630        } else {
1631            None
1632        };
1633
1634        let server_ids = match &self.config.server {
1635            Some(server) if !server.operations.is_empty() => {
1636                crate::server::resolve_operation_selectors(&server.operations, analysis)
1637                    .map_err(|error| {
1638                        GeneratorError::ValidationError(format!(
1639                            "Invalid [server].operations: {error}"
1640                        ))
1641                    })?
1642                    .operations
1643                    .into_iter()
1644                    .map(|operation| operation.operation_id)
1645                    .collect()
1646            }
1647            _ => Default::default(),
1648        };
1649
1650        let streaming_ids = if self.config.registry_only || !self.config.enable_sse_client {
1651            Default::default()
1652        } else if let Some(streaming) = &self.config.streaming_config {
1653            let mut ids = std::collections::BTreeSet::new();
1654            for (index, endpoint) in streaming.endpoints.iter().enumerate() {
1655                let resolution =
1656                    crate::server::resolve_operation_id(&endpoint.operation_id, analysis).map_err(
1657                        |error| {
1658                            GeneratorError::ValidationError(format!(
1659                                "Invalid [streaming].endpoints[{index}].operation_id: {error}"
1660                            ))
1661                        },
1662                    )?;
1663                ids.extend(
1664                    resolution
1665                        .operations
1666                        .into_iter()
1667                        .map(|operation| operation.operation_id),
1668                );
1669            }
1670            ids
1671        } else {
1672            Default::default()
1673        };
1674
1675        let client_prunes = self.config.enable_async_client
1676            && !self.config.registry_only
1677            && self
1678                .config
1679                .client
1680                .as_ref()
1681                .is_some_and(|client| client.prune_models);
1682        let server_prunes = self
1683            .config
1684            .server
1685            .as_ref()
1686            .is_some_and(|server| server.prune_models && !server.operations.is_empty());
1687        let extra_schema_roots = if self.config.registry_only || !self.config.enable_sse_client {
1688            Vec::new()
1689        } else {
1690            self.config
1691                .streaming_config
1692                .as_ref()
1693                .map(|streaming| {
1694                    streaming
1695                        .endpoints
1696                        .iter()
1697                        .map(|endpoint| endpoint.event_union_type.clone())
1698                        .collect()
1699                })
1700                .unwrap_or_default()
1701        };
1702
1703        Ok(OperationScopes {
1704            client_ids,
1705            server_ids,
1706            streaming_ids,
1707            prune_models: client_prunes || server_prunes,
1708            extra_schema_roots,
1709        })
1710    }
1711
1712    fn resolve_client_operation_ids(
1713        &self,
1714        analysis: &SchemaAnalysis,
1715    ) -> Result<Option<std::collections::BTreeSet<String>>> {
1716        match &self.config.client {
1717            Some(client) if !client.operations.is_empty() => {
1718                let resolution =
1719                    crate::server::resolve_operation_selectors(&client.operations, analysis)
1720                        .map_err(|error| {
1721                            GeneratorError::ValidationError(format!(
1722                                "Invalid [client].operations: {error}"
1723                            ))
1724                        })?;
1725                Ok(Some(
1726                    resolution
1727                        .operations
1728                        .into_iter()
1729                        .map(|operation| operation.operation_id)
1730                        .collect(),
1731                ))
1732            }
1733            _ => Ok(None),
1734        }
1735    }
1736
1737    fn client_operations<'a>(
1738        &self,
1739        analysis: &'a SchemaAnalysis,
1740        selected: Option<&std::collections::BTreeSet<String>>,
1741    ) -> Vec<&'a crate::analysis::OperationInfo> {
1742        analysis
1743            .operations
1744            .iter()
1745            .filter(|(operation_id, _)| selected.is_none_or(|ids| ids.contains(*operation_id)))
1746            .map(|(_, operation)| operation)
1747            .collect()
1748    }
1749
1750    fn prune_models_to_scopes(
1751        &self,
1752        analysis: &mut SchemaAnalysis,
1753        scopes: &OperationScopes,
1754    ) -> usize {
1755        if !scopes.prune_models {
1756            return 0;
1757        }
1758
1759        let mut consumer_ids = scopes.server_ids.clone();
1760        if self.config.enable_async_client && !self.config.registry_only {
1761            match &scopes.client_ids {
1762                Some(ids) => consumer_ids.extend(ids.iter().cloned()),
1763                None => consumer_ids.extend(analysis.operations.keys().cloned()),
1764            }
1765        }
1766        consumer_ids.extend(scopes.streaming_ids.iter().cloned());
1767
1768        let operations: Vec<&crate::analysis::OperationInfo> = consumer_ids
1769            .iter()
1770            .filter_map(|operation_id| analysis.operations.get(operation_id))
1771            .collect();
1772        let keep = crate::server::codegen::reachable_schemas_with_roots(
1773            analysis,
1774            &operations,
1775            &scopes.extra_schema_roots,
1776        );
1777        let before = analysis.schemas.len();
1778        analysis.schemas.retain(|name, _| keep.contains(name));
1779        before - analysis.schemas.len()
1780    }
1781
1782    /// Generate HTTP error type and result alias
1783    fn generate_http_error_types(&self) -> TokenStream {
1784        quote! {
1785            use thiserror::Error;
1786
1787            /// The generated validation-problem profile based on RFC 9457.
1788            /// The distinctive namespace avoids collisions with user schemas.
1789            pub mod openapi_to_rust_problem {
1790                #[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
1791                pub struct ProblemDetails {
1792                    #[serde(rename = "type")]
1793                    pub type_uri: String,
1794                    pub title: String,
1795                    pub status: u16,
1796                    pub code: String,
1797                    #[serde(default)]
1798                    pub errors: Vec<InvalidParameter>,
1799                    #[serde(default, skip_serializing_if = "Option::is_none")]
1800                    pub detail: Option<String>,
1801                    #[serde(default, skip_serializing_if = "Option::is_none")]
1802                    pub instance: Option<String>,
1803                }
1804
1805                #[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
1806                pub struct InvalidParameter {
1807                    pub code: String,
1808                    pub location: String,
1809                    pub message: String,
1810                }
1811            }
1812
1813            /// Transport-level errors: failures where no safely inspectable
1814            /// HTTP response is available to the caller.
1815            ///
1816            /// HTTP responses with non-2xx status codes are surfaced as
1817            /// [`ApiError`] inside [`ApiOpError::Api`], not here, so callers can
1818            /// always inspect status, headers, and the raw body when the server
1819            /// actually responded.
1820            #[derive(Error, Debug)]
1821            pub enum HttpError {
1822                /// Network or connection error (from reqwest)
1823                #[error("Network error: {0}")]
1824                Network(#[from] reqwest::Error),
1825
1826                /// Middleware error (from reqwest-middleware)
1827                #[error("Middleware error: {0}")]
1828                Middleware(#[from] reqwest_middleware::Error),
1829
1830                /// Request serialization error
1831                #[error("Failed to serialize request: {0}")]
1832                Serialization(String),
1833
1834                /// Authentication error
1835                #[error("Authentication error: {0}")]
1836                Auth(String),
1837
1838                /// Request timeout
1839                #[error("Request timeout")]
1840                Timeout,
1841
1842                /// A response body exceeded the configured in-memory limit
1843                #[error("Response body exceeded configured limit of {limit} bytes")]
1844                ResponseTooLarge { limit: usize },
1845
1846                /// Invalid configuration
1847                #[error("Configuration error: {0}")]
1848                Config(String),
1849
1850                /// Generic error
1851                #[error("{0}")]
1852                Other(String),
1853            }
1854
1855            impl HttpError {
1856                /// Create a serialization error
1857                pub fn serialization_error(error: impl std::fmt::Display) -> Self {
1858                    Self::Serialization(error.to_string())
1859                }
1860
1861                /// Check if this transport error is retryable
1862                pub fn is_retryable(&self) -> bool {
1863                    matches!(self, Self::Network(_) | Self::Middleware(_) | Self::Timeout)
1864                }
1865            }
1866
1867            /// Envelope returned for any HTTP response that we received but
1868            /// couldn't (or didn't) treat as a successful typed result.
1869            ///
1870            /// Includes both non-2xx responses and 2xx responses whose body
1871            /// failed to deserialize into the expected success type. `status`,
1872            /// `headers`, and `raw_body` preserve what the server actually sent,
1873            /// while `body` is a convenient lossy UTF-8 rendering. `typed`
1874            /// carries the parsed per-operation error variant
1875            /// when the body matched a declared schema. Formatting the error
1876            /// limits only the displayed body preview; the public fields
1877            /// retain the complete response and parsing details.
1878            #[derive(Debug, Clone)]
1879            pub struct ApiError<E> {
1880                pub status: u16,
1881                pub headers: reqwest::header::HeaderMap,
1882                pub body: String,
1883                /// Exact response bytes before lossy UTF-8 conversion.
1884                pub raw_body: Vec<u8>,
1885                pub typed: Option<E>,
1886                pub parse_error: Option<String>,
1887            }
1888
1889            const API_ERROR_BODY_DISPLAY_LIMIT: usize = 500;
1890            const API_ERROR_BODY_TRUNCATION_MARKER: &str = "... [truncated]";
1891
1892            fn display_api_error_body(body: &str) -> std::borrow::Cow<'_, str> {
1893                let Some((end, _)) = body.char_indices().nth(API_ERROR_BODY_DISPLAY_LIMIT) else {
1894                    return std::borrow::Cow::Borrowed(body);
1895                };
1896
1897                let mut displayed =
1898                    String::with_capacity(end + API_ERROR_BODY_TRUNCATION_MARKER.len());
1899                displayed.push_str(&body[..end]);
1900                displayed.push_str(API_ERROR_BODY_TRUNCATION_MARKER);
1901                std::borrow::Cow::Owned(displayed)
1902            }
1903
1904            impl<E> ApiError<E> {
1905                pub fn is_client_error(&self) -> bool {
1906                    (400..500).contains(&self.status)
1907                }
1908
1909                pub fn is_server_error(&self) -> bool {
1910                    (500..600).contains(&self.status)
1911                }
1912
1913                /// Retry guidance for the response. Mirrors the previous
1914                /// HttpError logic for backwards-compatible retry middleware.
1915                pub fn is_retryable(&self) -> bool {
1916                    matches!(self.status, 429 | 500 | 502 | 503 | 504)
1917                }
1918
1919                /// Decode the generated RFC 9457 validation-problem profile
1920                /// without replacing a documented per-operation error in `typed`.
1921                ///
1922                /// Returns `None` unless the response's `Content-Type` is
1923                /// `application/problem+json`, which is how RFC 9457 identifies
1924                /// a problem document. Most third-party APIs return their
1925                /// errors as plain `application/json`, so this yields `None`
1926                /// against them by design — use `typed` for a documented
1927                /// per-operation error body, or `body` for the raw payload.
1928                /// Servers generated by this tool always emit the problem
1929                /// media type, so this succeeds against them.
1930                pub fn problem_details(
1931                    &self,
1932                ) -> Option<openapi_to_rust_problem::ProblemDetails> {
1933                    let content_type = self
1934                        .headers
1935                        .get(reqwest::header::CONTENT_TYPE)?
1936                        .to_str()
1937                        .ok()?;
1938                    let media_type = content_type.split(';').next()?.trim();
1939                    if !media_type.eq_ignore_ascii_case("application/problem+json") {
1940                        return None;
1941                    }
1942                    serde_json::from_str(&self.body).ok()
1943                }
1944            }
1945
1946            impl<E: std::fmt::Debug> std::fmt::Display for ApiError<E> {
1947                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1948                    write!(
1949                        f,
1950                        "API error {}: {}",
1951                        self.status,
1952                        display_api_error_body(&self.body)
1953                    )?;
1954
1955                    if let Some(typed) = &self.typed {
1956                        write!(f, "; typed: {typed:?}")?;
1957                    }
1958
1959                    if let Some(parse_error) = &self.parse_error {
1960                        write!(f, "; parse error: {parse_error}")?;
1961                    }
1962
1963                    Ok(())
1964                }
1965            }
1966
1967            impl<E: std::fmt::Debug> std::error::Error for ApiError<E> {}
1968
1969            /// Result error type returned by every generated operation method.
1970            ///
1971            /// `Transport` covers failures where we never got an inspectable
1972            /// response (network, timeout, middleware, request-side
1973            /// serialization). `Api` covers any case where the server *did*
1974            /// respond — the envelope always carries status + headers + raw
1975            /// body even when the typed deserialize fails.
1976            #[derive(Debug, Error)]
1977            pub enum ApiOpError<E: std::fmt::Debug> {
1978                #[error(transparent)]
1979                Transport(#[from] HttpError),
1980
1981                #[error(transparent)]
1982                Api(ApiError<E>),
1983            }
1984
1985            impl<E: std::fmt::Debug> ApiOpError<E> {
1986                /// Returns the API envelope when this is an `Api` variant.
1987                pub fn api(&self) -> Option<&ApiError<E>> {
1988                    match self {
1989                        Self::Api(e) => Some(e),
1990                        Self::Transport(_) => None,
1991                    }
1992                }
1993
1994                /// True when the underlying error came from the server (i.e.
1995                /// any `Api` variant) rather than the transport layer.
1996                pub fn is_api_error(&self) -> bool {
1997                    matches!(self, Self::Api(_))
1998                }
1999            }
2000
2001            // Direct From impls so `?` works without going through HttpError
2002            // first. Rust's `?` only chains a single `From` conversion.
2003            impl<E: std::fmt::Debug> From<reqwest::Error> for ApiOpError<E> {
2004                fn from(e: reqwest::Error) -> Self {
2005                    Self::Transport(HttpError::Network(e))
2006                }
2007            }
2008
2009            impl<E: std::fmt::Debug> From<reqwest_middleware::Error> for ApiOpError<E> {
2010                fn from(e: reqwest_middleware::Error) -> Self {
2011                    Self::Transport(HttpError::Middleware(e))
2012                }
2013            }
2014
2015            /// Result alias for transport-only error paths (e.g. helpers that
2016            /// don't have a per-operation error type). Generated operation
2017            /// methods use [`ApiOpError`] directly.
2018            pub type HttpResult<T> = Result<T, HttpError>;
2019        }
2020    }
2021
2022    /// Generate mod.rs file that exports all modules
2023    fn generate_mod_file(&self, files: &[GeneratedFile]) -> Result<String> {
2024        let mut module_names = std::collections::BTreeSet::new();
2025
2026        for file in files {
2027            let module_name = if file.path.components().count() > 1 {
2028                file.path.iter().next().and_then(|part| part.to_str())
2029            } else {
2030                file.path.file_stem().and_then(|stem| stem.to_str())
2031            };
2032            if let Some(module_name) = module_name.filter(|name| *name != "mod") {
2033                module_names.insert(module_name.to_string());
2034            }
2035        }
2036        let module_declarations = module_names
2037            .iter()
2038            .map(|name| format!("pub mod {name};"))
2039            .collect::<Vec<_>>();
2040        let pub_uses = module_names
2041            .iter()
2042            // The SSE runtime intentionally keeps transport-level names under
2043            // `sse::` so they cannot collide with API-specific streaming types.
2044            .filter(|name| name.as_str() != "sse")
2045            .map(|name| format!("pub use {name}::*;"))
2046            .collect::<Vec<_>>();
2047
2048        // `module_name` is a configurable *label* — it does NOT pick
2049        // the on-disk directory (that's `output_dir`) and it does NOT
2050        // determine the Rust module path the user mounts this tree
2051        // at. Surfacing it in the header doc comment is the most
2052        // honest place: a hint to the user about what name was
2053        // configured and how to mount it.
2054        let mount_hint = format!(
2055            "//! Configured `module_name` = `{name}`. Mount this tree under your\n\
2056             //! preferred path, e.g. `pub mod {name};` in your crate root.\n",
2057            name = self.config.module_name,
2058        );
2059        let source_hint = self
2060            .source_provenance
2061            .as_ref()
2062            .map(|source| {
2063                format!(
2064                    "//! Generated by openapi-to-rust v{}. Source OpenAPI document: {source}\n",
2065                    env!("CARGO_PKG_VERSION")
2066                )
2067            })
2068            .unwrap_or_default();
2069
2070        let content = format!(
2071            r#"//! Generated API modules
2072//!
2073//! This module exports all generated API types and clients.
2074//! Do not edit manually - regenerate using the appropriate script.
2075//!
2076{source_hint}
2077{mount_hint}
2078#![allow(unused_imports)]
2079
2080{decls}
2081
2082{uses}
2083"#,
2084            mount_hint = mount_hint,
2085            source_hint = source_hint,
2086            decls = module_declarations.join("\n"),
2087            uses = pub_uses.join("\n"),
2088        );
2089
2090        Ok(content)
2091    }
2092
2093    /// Helper method to write all generated files to disk
2094    pub fn output_artifacts(
2095        &self,
2096        result: &GenerationResult,
2097    ) -> std::collections::BTreeMap<PathBuf, String> {
2098        let mut artifacts = std::collections::BTreeMap::new();
2099        for file in &result.files {
2100            artifacts.insert(file.path.clone(), file.content.clone());
2101        }
2102        artifacts.insert(
2103            result.mod_file.path.clone(),
2104            result.mod_file.content.clone(),
2105        );
2106        if let Some(mut fragment) =
2107            crate::type_mapping::render_required_deps_toml(&result.required_deps)
2108        {
2109            if let Some(source) = &self.source_provenance {
2110                let header = format!(
2111                    "# Generated by openapi-to-rust v{}. Source OpenAPI document: {source}",
2112                    env!("CARGO_PKG_VERSION")
2113                );
2114                fragment = fragment.replacen("# Generated by openapi-to-rust.", &header, 1);
2115            }
2116            artifacts.insert(PathBuf::from("REQUIRED_DEPS.toml"), fragment);
2117        }
2118        artifacts
2119    }
2120
2121    /// Write a generation result using the same rendered artifact set exposed
2122    /// to dry-run and check-mode callers.
2123    pub fn write_files(&self, result: &GenerationResult) -> Result<()> {
2124        use std::fs;
2125
2126        // Create output directory if it doesn't exist
2127        fs::create_dir_all(&self.config.output_dir)?;
2128
2129        let artifacts = self.output_artifacts(result);
2130        for (relative, content) in &artifacts {
2131            let file_path = self.config.output_dir.join(relative);
2132            if let Some(parent) = file_path.parent() {
2133                fs::create_dir_all(parent)?;
2134            }
2135            fs::write(&file_path, content)?;
2136        }
2137
2138        let deps_path = self.config.output_dir.join("REQUIRED_DEPS.toml");
2139        if !artifacts.contains_key(std::path::Path::new("REQUIRED_DEPS.toml")) && deps_path.exists()
2140        {
2141            fs::remove_file(&deps_path)?;
2142        }
2143
2144        Ok(())
2145    }
2146
2147    fn generate_type_definition(
2148        &self,
2149        schema: &crate::analysis::AnalyzedSchema,
2150        analysis: &crate::analysis::SchemaAnalysis,
2151        type_context: &TypeGenerationContext<'_>,
2152    ) -> Result<TokenStream> {
2153        use crate::analysis::SchemaType;
2154
2155        match &schema.schema_type {
2156            SchemaType::Primitive { rust_type, .. } => {
2157                // Generate type alias for primitives that are referenced by other schemas
2158                self.generate_type_alias(schema, rust_type)
2159            }
2160            SchemaType::StringEnum { values } => {
2161                let ext = analysis.enum_extensions.get(&schema.name);
2162                // [extensible_enums] override: opt a closed string-enum into an
2163                // extensible enum when the spec is known to lag the API (e.g.
2164                // Cloudflare R2 returning "WNAM" against a lowercase-only enum).
2165                // Accept either the raw spec name (e.g. "r2_bucket_location")
2166                // or the rendered Rust type name (e.g. "R2BucketLocation") so
2167                // users can write whichever they see in the generated code.
2168                let rust_name = self.to_rust_type_name(&schema.name);
2169                let force_extensible = self
2170                    .config
2171                    .extensible_enum_overrides
2172                    .get(&schema.name)
2173                    .or_else(|| self.config.extensible_enum_overrides.get(&rust_name))
2174                    .copied()
2175                    .unwrap_or(false);
2176                if force_extensible {
2177                    self.generate_extensible_enum(schema, values, ext, type_context)
2178                } else {
2179                    self.generate_string_enum(schema, values, ext)
2180                }
2181            }
2182            SchemaType::ExtensibleEnum { known_values } => {
2183                let ext = analysis.enum_extensions.get(&schema.name);
2184                self.generate_extensible_enum(schema, known_values, ext, type_context)
2185            }
2186            SchemaType::Object {
2187                properties,
2188                required,
2189                additional_properties,
2190                variant,
2191            } => self.generate_struct(
2192                schema,
2193                ObjectShape {
2194                    properties,
2195                    required,
2196                    additional_properties,
2197                    variant: variant.as_ref(),
2198                },
2199                analysis,
2200                type_context,
2201            ),
2202            SchemaType::DiscriminatedUnion {
2203                discriminator_field,
2204                variants,
2205                exclusive,
2206            } => {
2207                // Check if this discriminated union should be untagged due to being nested
2208                if self.should_use_untagged_discriminated_union(schema, analysis) {
2209                    // Convert variants to SchemaRef format for union enum generation
2210                    let schema_refs: Vec<crate::analysis::SchemaRef> = variants
2211                        .iter()
2212                        .map(|v| crate::analysis::SchemaRef {
2213                            target: v.type_name.clone(),
2214                            nullable: false,
2215                        })
2216                        .collect();
2217                    self.generate_union_enum(schema, &schema_refs, *exclusive, analysis)
2218                } else {
2219                    self.generate_discriminated_enum(
2220                        schema,
2221                        discriminator_field,
2222                        variants,
2223                        *exclusive,
2224                        analysis,
2225                    )
2226                }
2227            }
2228            SchemaType::Union {
2229                variants,
2230                exclusive,
2231            } => self.generate_union_enum(schema, variants, *exclusive, analysis),
2232            SchemaType::Reference { target } => {
2233                // For references, check if we need to generate a type alias
2234                // This handles cases like nullable patterns
2235                if schema.name != *target {
2236                    // Generate a type alias
2237                    let alias_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2238                    let target_type = format_ident!("{}", self.to_rust_type_name(target));
2239
2240                    let doc_comment = if let Some(desc) = &schema.description {
2241                        quote! { #[doc = #desc] }
2242                    } else {
2243                        TokenStream::new()
2244                    };
2245
2246                    Ok(quote! {
2247                        #doc_comment
2248                        pub type #alias_name = #target_type;
2249                    })
2250                } else {
2251                    // Same name as target, no need for alias
2252                    Ok(TokenStream::new())
2253                }
2254            }
2255            SchemaType::Untyped { shape, .. } => {
2256                self.generate_type_alias(schema, untyped_rust_type(*shape))
2257            }
2258            SchemaType::Tuple { element_types } => {
2259                let tuple_type = self.generate_tuple_type(element_types, analysis);
2260                let type_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2261                let doc_comment = if let Some(description) = &schema.description {
2262                    let sanitized = self.sanitize_doc_comment(description);
2263                    quote! { #[doc = #sanitized] }
2264                } else {
2265                    TokenStream::new()
2266                };
2267                Ok(quote! {
2268                    #doc_comment
2269                    pub type #type_name = #tuple_type;
2270                })
2271            }
2272            SchemaType::Array { item_type } => {
2273                // Generate type alias for named array schemas.
2274                //
2275                let array_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2276
2277                let inner_type = self.generate_array_item_type(item_type, analysis);
2278
2279                let doc_comment = if let Some(desc) = &schema.description {
2280                    quote! { #[doc = #desc] }
2281                } else {
2282                    TokenStream::new()
2283                };
2284
2285                Ok(quote! {
2286                    #doc_comment
2287                    pub type #array_name = Vec<#inner_type>;
2288                })
2289            }
2290            SchemaType::Nullable { inner_type } => {
2291                let nullable_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2292                let inner_type = self.generate_array_item_type(inner_type, analysis);
2293                Ok(quote! {
2294                    pub type #nullable_name = Option<#inner_type>;
2295                })
2296            }
2297            SchemaType::Composition { schemas } => {
2298                self.generate_composition_struct(schema, schemas)
2299            }
2300        }
2301    }
2302
2303    fn generate_type_alias(
2304        &self,
2305        schema: &crate::analysis::AnalyzedSchema,
2306        rust_type: &str,
2307    ) -> Result<TokenStream> {
2308        let type_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2309        // syn parses any valid Rust type expression including
2310        // generics (`chrono::DateTime<chrono::Utc>`, `Vec<u8>`).
2311        // The pre-Q2 ad-hoc `::`-splitter choked on `<`.
2312        let base_type = parse_rust_type(rust_type)?;
2313
2314        let doc_comment = if let Some(desc) = &schema.description {
2315            let sanitized_desc = self.sanitize_doc_comment(desc);
2316            quote! { #[doc = #sanitized_desc] }
2317        } else {
2318            TokenStream::new()
2319        };
2320
2321        Ok(quote! {
2322            #doc_comment
2323            pub type #type_name = #base_type;
2324        })
2325    }
2326
2327    fn generate_extensible_enum(
2328        &self,
2329        schema: &crate::analysis::AnalyzedSchema,
2330        known_values: &[String],
2331        ext: Option<&crate::analysis::EnumExtensions>,
2332        type_context: &TypeGenerationContext<'_>,
2333    ) -> Result<TokenStream> {
2334        let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2335
2336        let doc_comment = if let Some(desc) = &schema.description {
2337            quote! { #[doc = #desc] }
2338        } else {
2339            TokenStream::new()
2340        };
2341
2342        // Q2.6: pre-resolve variant idents from x-enum-varnames when
2343        // available + length-matched + toggle on. Same fallback rule
2344        // as generate_string_enum.
2345        let varnames_override: Option<&Vec<String>> = ext
2346            .filter(|_| self.config.types.x_enum_varnames_enabled())
2347            .map(|e| &e.varnames)
2348            .filter(|v| !v.is_empty() && v.len() == known_values.len());
2349        let descriptions_override: Option<&Vec<String>> = ext
2350            .filter(|_| self.config.types.x_enum_descriptions_enabled())
2351            .map(|e| &e.descriptions)
2352            .filter(|v| !v.is_empty() && v.len() == known_values.len());
2353
2354        let variant_ident_for = |index: usize, value: &str| -> proc_macro2::Ident {
2355            let name = match varnames_override {
2356                Some(v) => v[index].clone(),
2357                None => self.to_rust_enum_variant(value),
2358            };
2359            format_ident!("{}", name)
2360        };
2361
2362        if let Some(metadata) = type_context.enum_wire_names {
2363            let mut variants: BTreeMap<_, _> = known_values
2364                .iter()
2365                .enumerate()
2366                .map(|(index, value)| {
2367                    (
2368                        variant_ident_for(index, value).to_string(),
2369                        Some(value.clone()),
2370                    )
2371                })
2372                .collect();
2373            variants.insert("Custom".to_string(), None);
2374            metadata
2375                .borrow_mut()
2376                .insert(enum_name.to_string(), variants);
2377        }
2378
2379        // For extensible enums, we need a different approach:
2380        // 1. Create a regular enum with known variants + Custom
2381        // 2. Implement custom serialization/deserialization
2382
2383        let known_variants = known_values.iter().enumerate().map(|(i, value)| {
2384            let variant_ident = variant_ident_for(i, value);
2385            let doc = descriptions_override
2386                .map(|d| {
2387                    let s = self.sanitize_doc_comment(&d[i]);
2388                    quote! { #[doc = #s] }
2389                })
2390                .unwrap_or_default();
2391            quote! {
2392                #doc
2393                #variant_ident,
2394            }
2395        });
2396
2397        let match_arms_de = known_values.iter().enumerate().map(|(i, value)| {
2398            let variant_ident = variant_ident_for(i, value);
2399            quote! {
2400                #value => Ok(#enum_name::#variant_ident),
2401            }
2402        });
2403
2404        let match_arms_ser = known_values.iter().enumerate().map(|(i, value)| {
2405            let variant_ident = variant_ident_for(i, value);
2406            quote! {
2407                #enum_name::#variant_ident => #value,
2408            }
2409        });
2410
2411        let derives = if self.config.enable_specta {
2412            quote! {
2413                #[derive(Debug, Clone, PartialEq, Eq)]
2414                #[cfg_attr(feature = "specta", derive(specta::Type))]
2415            }
2416        } else {
2417            quote! {
2418                #[derive(Debug, Clone, PartialEq, Eq)]
2419            }
2420        };
2421
2422        Ok(quote! {
2423            #doc_comment
2424            #derives
2425            pub enum #enum_name {
2426                #(#known_variants)*
2427                /// Custom or unknown model identifier
2428                Custom(String),
2429            }
2430
2431            impl<'de> serde::Deserialize<'de> for #enum_name {
2432                fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2433                where
2434                    D: serde::Deserializer<'de>,
2435                {
2436                    let value = String::deserialize(deserializer)?;
2437                    match value.as_str() {
2438                        #(#match_arms_de)*
2439                        _ => Ok(#enum_name::Custom(value)),
2440                    }
2441                }
2442            }
2443
2444            impl serde::Serialize for #enum_name {
2445                fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2446                where
2447                    S: serde::Serializer,
2448                {
2449                    serializer.serialize_str(self.as_str())
2450                }
2451            }
2452
2453            impl #enum_name {
2454                pub fn as_str(&self) -> &str {
2455                    match self {
2456                        #(#match_arms_ser)*
2457                        #enum_name::Custom(s) => s.as_str(),
2458                    }
2459                }
2460            }
2461
2462            impl ::std::fmt::Display for #enum_name {
2463                fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
2464                    f.write_str(self.as_str())
2465                }
2466            }
2467
2468            impl AsRef<str> for #enum_name {
2469                fn as_ref(&self) -> &str {
2470                    self.as_str()
2471                }
2472            }
2473        })
2474    }
2475
2476    fn generate_string_enum(
2477        &self,
2478        schema: &crate::analysis::AnalyzedSchema,
2479        values: &[String],
2480        ext: Option<&crate::analysis::EnumExtensions>,
2481    ) -> Result<TokenStream> {
2482        let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2483
2484        // Determine which variant should be the default. The spec's `default`
2485        // may not exactly match any enum value (telnyx has
2486        // `default: "en"` on a language enum that lists `en-US`, `en-AU`,
2487        // … — no exact match). When that happens, drop the `Default` derive
2488        // entirely instead of emitting it on an enum where no variant has
2489        // `#[default]` (E0665).
2490        let default_value = schema
2491            .default
2492            .as_ref()
2493            .and_then(|v| v.as_str())
2494            .map(|s| s.to_string());
2495        let has_default_match = match &default_value {
2496            Some(d) => values.iter().any(|v| v == d),
2497            None => !values.is_empty(),
2498        };
2499
2500        // Q2.6: x-enum-varnames overrides the default heuristic when
2501        // present, length-matched, and the toggle is on. Falls back
2502        // to the to_rust_enum_variant heuristic otherwise.
2503        let varnames_override: Option<&Vec<String>> = ext
2504            .filter(|_| self.config.types.x_enum_varnames_enabled())
2505            .map(|e| &e.varnames)
2506            .filter(|v| !v.is_empty() && v.len() == values.len());
2507        let descriptions_override: Option<&Vec<String>> = ext
2508            .filter(|_| self.config.types.x_enum_descriptions_enabled())
2509            .map(|e| &e.descriptions)
2510            .filter(|v| !v.is_empty() && v.len() == values.len());
2511
2512        // Variant-name uniqueness: enum values that PascalCase to the same
2513        // identifier (e.g. `ASC`/`asc` both → `Asc`) collide and produce
2514        // E0428 + non-exhaustive matches downstream. Dedupe by numbering
2515        // collisions `2`, `3`, … while preserving the first occurrence's
2516        // name, and keeping each variant's `#[serde(rename)]` pointed at the
2517        // original wire string.
2518        let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
2519        let variant_pairs: Vec<(syn::Ident, &String, bool, Option<String>)> = values
2520            .iter()
2521            .enumerate()
2522            .map(|(i, value)| {
2523                let base = match varnames_override {
2524                    Some(v) => v[i].clone(),
2525                    None => self.to_rust_enum_variant(value),
2526                };
2527                let mut variant_name = base.clone();
2528                let mut suffix = 2;
2529                while !used.insert(variant_name.clone()) {
2530                    variant_name = numbered_variant_name(&base, suffix);
2531                    suffix += 1;
2532                }
2533                let variant_ident = format_ident!("{}", variant_name);
2534                let is_default = if let Some(ref default) = default_value {
2535                    value == default
2536                } else {
2537                    i == 0
2538                };
2539                let description = descriptions_override.map(|d| d[i].clone());
2540                (variant_ident, value, is_default, description)
2541            })
2542            .collect();
2543
2544        let variants =
2545            variant_pairs
2546                .iter()
2547                .map(|(variant_ident, value, is_default, description)| {
2548                    let doc = description
2549                        .as_ref()
2550                        .map(|d| {
2551                            let s = self.sanitize_doc_comment(d);
2552                            quote! { #[doc = #s] }
2553                        })
2554                        .unwrap_or_default();
2555                    if *is_default {
2556                        quote! {
2557                            #doc
2558                            #[default]
2559                            #[serde(rename = #value)]
2560                            #variant_ident,
2561                        }
2562                    } else {
2563                        quote! {
2564                            #doc
2565                            #[serde(rename = #value)]
2566                            #variant_ident,
2567                        }
2568                    }
2569                });
2570
2571        // T13/T10: emit `as_str` and `Display` so the enum can be embedded in
2572        // query strings, headers, and path segments without requiring callers
2573        // to reach for `serde_json` round-trips.
2574        let as_str_arms = variant_pairs.iter().map(|(variant_ident, value, _, _)| {
2575            quote! { Self::#variant_ident => #value, }
2576        });
2577
2578        let doc_comment = if let Some(desc) = &schema.description {
2579            quote! { #[doc = #desc] }
2580        } else {
2581            TokenStream::new()
2582        };
2583
2584        // Generate derives with optional Specta support. Drop `Default` if
2585        // no variant ends up tagged `#[default]` (would trigger E0665).
2586        let derives = match (self.config.enable_specta, has_default_match) {
2587            (true, true) => quote! {
2588                #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
2589                #[cfg_attr(feature = "specta", derive(specta::Type))]
2590            },
2591            (true, false) => quote! {
2592                #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
2593                #[cfg_attr(feature = "specta", derive(specta::Type))]
2594            },
2595            (false, true) => quote! {
2596                #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
2597            },
2598            (false, false) => quote! {
2599                #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
2600            },
2601        };
2602
2603        Ok(quote! {
2604            #doc_comment
2605            #derives
2606            pub enum #enum_name {
2607                #(#variants)*
2608            }
2609
2610            impl #enum_name {
2611                pub fn as_str(&self) -> &'static str {
2612                    match self {
2613                        #(#as_str_arms)*
2614                    }
2615                }
2616            }
2617
2618            impl ::std::fmt::Display for #enum_name {
2619                fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
2620                    f.write_str(self.as_str())
2621                }
2622            }
2623
2624            impl AsRef<str> for #enum_name {
2625                fn as_ref(&self) -> &str {
2626                    self.as_str()
2627                }
2628            }
2629        })
2630    }
2631
2632    /// Field name for a flattened variant union, avoiding any property the
2633    /// schema already declares.
2634    fn variant_field_name(
2635        &self,
2636        properties: &BTreeMap<String, crate::analysis::PropertyInfo>,
2637    ) -> String {
2638        let taken = |candidate: &str| {
2639            properties
2640                .keys()
2641                .any(|name| self.to_rust_field_name(name) == candidate)
2642        };
2643        if !taken("variant") {
2644            return "variant".to_string();
2645        }
2646        let mut suffix = 2;
2647        loop {
2648            let candidate = format!("variant{suffix}");
2649            if !taken(&candidate) {
2650                return candidate;
2651            }
2652            suffix += 1;
2653        }
2654    }
2655
2656    fn generate_struct(
2657        &self,
2658        schema: &crate::analysis::AnalyzedSchema,
2659        object: ObjectShape<'_>,
2660        analysis: &crate::analysis::SchemaAnalysis,
2661        type_context: &TypeGenerationContext<'_>,
2662    ) -> Result<TokenStream> {
2663        let ObjectShape {
2664            properties,
2665            required,
2666            additional_properties,
2667            variant,
2668        } = object;
2669        let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2670        let emitted_properties = self.emitted_object_properties(
2671            &schema.name,
2672            properties,
2673            required,
2674            additional_properties,
2675            analysis,
2676        );
2677        let variant_field_ident =
2678            variant.map(|_| format_ident!("{}", self.variant_field_name(properties)));
2679
2680        let mut fields: Vec<TokenStream> = emitted_properties
2681            .iter()
2682            .map(|emitted| {
2683                let field_name = emitted.wire_name;
2684                let property = emitted.property;
2685                let field_ident = &emitted.ident;
2686                let field_type = &emitted.field_type;
2687                let serde_attrs = if variant.is_none() {
2688                    self.generate_serde_field_attrs(
2689                        &schema.name,
2690                        field_name,
2691                        field_ident,
2692                        property,
2693                        emitted.is_required,
2694                        analysis,
2695                    )
2696                } else {
2697                    TokenStream::new()
2698                };
2699                let specta_attrs = self.generate_specta_field_attrs(field_name);
2700
2701                let doc_comment = if let Some(desc) = &property.description {
2702                    let sanitized_desc = self.sanitize_doc_comment(desc);
2703                    quote! { #[doc = #sanitized_desc] }
2704                } else {
2705                    TokenStream::new()
2706                };
2707                let constraint_doc = self.generate_constraint_doc(&property.constraints);
2708
2709                quote! {
2710                    #doc_comment
2711                    #constraint_doc
2712                    #serde_attrs
2713                    #specta_attrs
2714                    pub #field_ident: #field_type,
2715                }
2716            })
2717            .collect();
2718
2719        // Q2.3: emit the catch-all additional-properties field with
2720        // the right value type. `Untyped` keeps pre-Q2.3 behavior
2721        // (BTreeMap<String, serde_json::Value>); `Typed { value_type }`
2722        // surfaces the actual schema-declared type, e.g.
2723        // BTreeMap<String, MyValue>. `Forbidden` emits no field.
2724        match additional_properties {
2725            crate::analysis::ObjectAdditionalProperties::Denied
2726            | crate::analysis::ObjectAdditionalProperties::Closed => {}
2727            crate::analysis::ObjectAdditionalProperties::Untyped => {
2728                let serde_flatten = if variant.is_none() {
2729                    quote! { #[serde(flatten)] }
2730                } else {
2731                    TokenStream::new()
2732                };
2733                fields.push(quote! {
2734                    /// Additional properties not explicitly defined in the schema
2735                    #serde_flatten
2736                    pub additional_properties:
2737                        std::collections::BTreeMap<String, serde_json::Value>,
2738                });
2739            }
2740            crate::analysis::ObjectAdditionalProperties::Typed { value_type } => {
2741                let value_tokens = self.generate_array_item_type(value_type, analysis);
2742                let serde_flatten = if variant.is_none() {
2743                    quote! { #[serde(flatten)] }
2744                } else {
2745                    TokenStream::new()
2746                };
2747                fields.push(quote! {
2748                    /// Additional properties matching the spec's
2749                    /// `additionalProperties` value schema.
2750                    #serde_flatten
2751                    pub additional_properties:
2752                        std::collections::BTreeMap<String, #value_tokens>,
2753                });
2754            }
2755        }
2756
2757        // A schema that declares `properties` *and* a union means "these
2758        // fields, and one of these shapes". The union rides in a flattened
2759        // field so both halves round-trip: serde reads the declared properties
2760        // and hands the remaining keys to the variant enum.
2761        if let (Some(variant), Some(variant_field)) = (variant, variant_field_ident.as_ref()) {
2762            let variant_type = format_ident!("{}", self.to_rust_type_name(&variant.target));
2763            fields.push(quote! {
2764                /// The variant this value takes, alongside the fields above.
2765                pub #variant_field: #variant_type,
2766            });
2767        }
2768
2769        let doc_comment = if let Some(desc) = &schema.description {
2770            quote! { #[doc = #desc] }
2771        } else {
2772            TokenStream::new()
2773        };
2774
2775        // Default is safe only when the schema requires no wire property.
2776        // Optional fields are represented as Option<T>, and the generated
2777        // additional-properties map (when present) is empty by default. We do
2778        // not invent values for required data, even when the Rust type itself
2779        // happens to implement Default.
2780        // A flattened variant is one of several shapes, and picking one would
2781        // invent data the same way a required field would.
2782        let can_derive_default = variant.is_none() && required.is_empty();
2783
2784        // Generate derives with optional Specta support
2785        // Note: We use snake_case everywhere (matching the OpenAPI spec) for consistency
2786        // between Rust, JSON API, and TypeScript
2787        let derives = match (
2788            self.config.enable_specta,
2789            can_derive_default,
2790            variant.is_some(),
2791        ) {
2792            (true, _, true) => quote! {
2793                #[derive(Debug, Clone)]
2794                #[cfg_attr(feature = "specta", derive(specta::Type))]
2795            },
2796            (false, _, true) => quote! {
2797                #[derive(Debug, Clone)]
2798            },
2799            (true, true, false) => quote! {
2800                #[derive(Debug, Clone, Deserialize, Serialize, Default)]
2801                #[cfg_attr(feature = "specta", derive(specta::Type))]
2802            },
2803            (true, false, false) => quote! {
2804                #[derive(Debug, Clone, Deserialize, Serialize)]
2805                #[cfg_attr(feature = "specta", derive(specta::Type))]
2806            },
2807            (false, true, false) => quote! {
2808                #[derive(Debug, Clone, Deserialize, Serialize, Default)]
2809            },
2810            (false, false, false) => quote! {
2811                #[derive(Debug, Clone, Deserialize, Serialize)]
2812            },
2813        };
2814
2815        // `#[serde(untagged)]` takes the first branch that deserializes, so a
2816        // branch accepting more than its schema allows claims values that
2817        // belong to a later one: with `anyOf: [Closed, {type: object}]` and a
2818        // `Closed` whose fields are all optional, `{"other": 1}` matches
2819        // `Closed` — which the document forbids — and the key is dropped
2820        // instead of reaching the open branch.
2821        //
2822        // Only a *stated* `additionalProperties: false` earns this. An omitted
2823        // keyword leaves the object open in JSON Schema (the generator merely
2824        // projects it closed), and tightening those would make every generated
2825        // client fail the first time a server adds a field.
2826        //
2827        // Serde rejects `deny_unknown_fields` alongside `flatten` at compile
2828        // time; `Denied` emits no catch-all field, and a flattened variant is
2829        // excluded here as well.
2830        let denies_unknown_fields = variant.is_none()
2831            && additional_properties.denies_unknown_keys()
2832            && analysis.untagged_union_branches.contains(&schema.name);
2833        let deny_unknown_fields = if denies_unknown_fields {
2834            quote! { #[serde(deny_unknown_fields)] }
2835        } else {
2836            TokenStream::new()
2837        };
2838
2839        // `#[serde(flatten)]` removes keys already consumed by sibling fields
2840        // before it invokes the flattened value's deserializer. That is wrong
2841        // for a sibling-property + oneOf schema when both halves intentionally
2842        // share the discriminator. Decode both halves from the complete JSON
2843        // object, and merge their serialized maps with duplicate-value checks.
2844        let shared_variant_serde = if let (Some(variant), Some(variant_field)) =
2845            (variant, variant_field_ident.as_ref())
2846        {
2847            let variant_type = format_ident!("{}", self.to_rust_type_name(&variant.target));
2848            let helper_name = format_ident!("__{}Base", self.to_rust_type_name(&schema.name));
2849            let variant_properties = self.union_declared_properties(
2850                &variant.target,
2851                analysis,
2852                &mut std::collections::HashSet::new(),
2853            );
2854            let variant_projection_removals = emitted_properties
2855                .iter()
2856                .filter(|emitted| !variant_properties.contains(emitted.wire_name))
2857                .map(|emitted| {
2858                    let wire_name = emitted.wire_name;
2859                    quote! { object.remove(#wire_name); }
2860                })
2861                .collect::<Vec<_>>();
2862            let mut helper_fields: Vec<TokenStream> = emitted_properties
2863                .iter()
2864                .map(|emitted| {
2865                    let field_name = emitted.wire_name;
2866                    let property = emitted.property;
2867                    let field_ident = &emitted.ident;
2868                    let field_type = &emitted.field_type;
2869                    let serde_attrs = self.generate_serde_field_attrs(
2870                        &schema.name,
2871                        field_name,
2872                        field_ident,
2873                        property,
2874                        emitted.is_required,
2875                        analysis,
2876                    );
2877                    quote! {
2878                        #serde_attrs
2879                        #field_ident: #field_type,
2880                    }
2881                })
2882                .collect();
2883            let mut base_initializers: Vec<TokenStream> = emitted_properties
2884                .iter()
2885                .map(|emitted| {
2886                    let field_ident = &emitted.ident;
2887                    quote! { #field_ident: self.#field_ident.clone(), }
2888                })
2889                .collect();
2890            let mut result_fields: Vec<TokenStream> = emitted_properties
2891                .iter()
2892                .map(|emitted| {
2893                    let field_ident = &emitted.ident;
2894                    quote! { #field_ident: base.#field_ident, }
2895                })
2896                .collect();
2897
2898            match additional_properties {
2899                crate::analysis::ObjectAdditionalProperties::Denied
2900                | crate::analysis::ObjectAdditionalProperties::Closed => {}
2901                crate::analysis::ObjectAdditionalProperties::Untyped => {
2902                    helper_fields.push(quote! {
2903                        #[serde(flatten)]
2904                        additional_properties:
2905                            std::collections::BTreeMap<String, serde_json::Value>,
2906                    });
2907                    base_initializers.push(quote! {
2908                        additional_properties: self.additional_properties.clone(),
2909                    });
2910                    result_fields.push(quote! {
2911                        additional_properties: base.additional_properties,
2912                    });
2913                }
2914                crate::analysis::ObjectAdditionalProperties::Typed { value_type } => {
2915                    let value_tokens = self.generate_array_item_type(value_type, analysis);
2916                    helper_fields.push(quote! {
2917                        #[serde(flatten)]
2918                        additional_properties:
2919                            std::collections::BTreeMap<String, #value_tokens>,
2920                    });
2921                    base_initializers.push(quote! {
2922                        additional_properties: self.additional_properties.clone(),
2923                    });
2924                    result_fields.push(quote! {
2925                        additional_properties: base.additional_properties,
2926                    });
2927                }
2928            }
2929
2930            quote! {
2931                #[derive(Deserialize, Serialize)]
2932                struct #helper_name {
2933                    #(#helper_fields)*
2934                }
2935
2936                impl serde::Serialize for #struct_name {
2937                    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2938                    where
2939                        S: serde::Serializer,
2940                    {
2941                        let base = #helper_name {
2942                            #(#base_initializers)*
2943                        };
2944                        let mut value = serde_json::to_value(base)
2945                            .map_err(serde::ser::Error::custom)?;
2946                        let variant = serde_json::to_value(&self.#variant_field)
2947                            .map_err(serde::ser::Error::custom)?;
2948                        let object = value.as_object_mut().ok_or_else(|| {
2949                            serde::ser::Error::custom("shared union base did not serialize as an object")
2950                        })?;
2951                        let variant_object = variant.as_object().ok_or_else(|| {
2952                            serde::ser::Error::custom("shared union variant did not serialize as an object")
2953                        })?;
2954                        for (key, variant_value) in variant_object {
2955                            if let Some(base_value) = object.get(key)
2956                                && base_value != variant_value
2957                            {
2958                                return Err(serde::ser::Error::custom(format!(
2959                                    "shared union field `{key}` serialized conflicting values",
2960                                )));
2961                            }
2962                            object.insert(key.clone(), variant_value.clone());
2963                        }
2964                        serde::Serialize::serialize(&value, serializer)
2965                    }
2966                }
2967
2968                impl<'de> serde::Deserialize<'de> for #struct_name {
2969                    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2970                    where
2971                        D: serde::Deserializer<'de>,
2972                    {
2973                        let value = serde_json::Value::deserialize(deserializer)?;
2974                        let base = serde_json::from_value::<#helper_name>(value.clone())
2975                            .map_err(serde::de::Error::custom)?;
2976                        let variant = match serde_json::from_value::<#variant_type>(value.clone()) {
2977                            Ok(variant) => variant,
2978                            Err(complete_error) => {
2979                                let mut variant_input = value;
2980                                if let Some(object) = variant_input.as_object_mut() {
2981                                    #(#variant_projection_removals)*
2982                                }
2983                                serde_json::from_value::<#variant_type>(variant_input).map_err(
2984                                    |projected_error| serde::de::Error::custom(format!(
2985                                        "complete shared-union input failed: {complete_error}; projected input failed: {projected_error}",
2986                                    )),
2987                                )?
2988                            }
2989                        };
2990                        Ok(Self {
2991                            #(#result_fields)*
2992                            #variant_field: variant,
2993                        })
2994                    }
2995                }
2996            }
2997        } else {
2998            TokenStream::new()
2999        };
3000
3001        let builder = if type_context.index.request_body_roots.contains(&schema.name)
3002            && variant.is_none()
3003            && emitted_properties
3004                .iter()
3005                .any(|property| property.is_required)
3006            && (emitted_properties
3007                .iter()
3008                .any(|property| !property.is_required)
3009                || additional_properties.is_open())
3010        {
3011            self.generate_request_model_builder(
3012                schema,
3013                &emitted_properties,
3014                additional_properties,
3015                analysis,
3016                type_context.index,
3017            )
3018        } else {
3019            TokenStream::new()
3020        };
3021
3022        Ok(quote! {
3023            #doc_comment
3024            #derives
3025            #deny_unknown_fields
3026            pub struct #struct_name {
3027                #(#fields)*
3028            }
3029
3030            #shared_variant_serde
3031            #builder
3032        })
3033    }
3034
3035    /// Project an object schema into the exact public fields emitted in
3036    /// `types.rs`. Request-model and operation builders share this metadata so
3037    /// identifier disambiguation, discriminator filtering, and Option wrapping
3038    /// cannot drift.
3039    pub(crate) fn emitted_object_properties<'a>(
3040        &self,
3041        schema_name: &str,
3042        properties: &'a BTreeMap<String, crate::analysis::PropertyInfo>,
3043        required: &std::collections::HashSet<String>,
3044        additional_properties: &crate::analysis::ObjectAdditionalProperties,
3045        analysis: &crate::analysis::SchemaAnalysis,
3046    ) -> Vec<EmittedObjectProperty<'a>> {
3047        let mut sorted_properties: Vec<_> = properties.iter().collect();
3048        sorted_properties.sort_by_key(|(name, _)| name.as_str());
3049
3050        let mut used_field_idents = std::collections::HashSet::new();
3051        if additional_properties.is_open() {
3052            used_field_idents.insert("additional_properties".to_string());
3053        }
3054
3055        let mut emitted = Vec::new();
3056        for (field_name, property) in sorted_properties {
3057            let raw = self.to_rust_field_name(field_name);
3058            let mut chosen = raw.clone();
3059            let mut suffix = 2;
3060            while !used_field_idents.insert(chosen.clone()) {
3061                chosen = format!("{raw}_{suffix}");
3062                suffix += 1;
3063            }
3064            let is_required = required.contains(field_name);
3065            emitted.push(EmittedObjectProperty {
3066                wire_name: field_name,
3067                property,
3068                ident: Self::to_field_ident(&chosen),
3069                is_required,
3070                field_type: self.generate_field_type(
3071                    schema_name,
3072                    field_name,
3073                    property,
3074                    is_required,
3075                    analysis,
3076                ),
3077            });
3078        }
3079        emitted
3080    }
3081
3082    fn type_generation_index(
3083        &self,
3084        analysis: &crate::analysis::SchemaAnalysis,
3085    ) -> TypeGenerationIndex {
3086        let reserved_type_names = analysis
3087            .schemas
3088            .keys()
3089            .map(|name| self.to_rust_type_name(name))
3090            .collect();
3091        let mut request_body_roots = std::collections::HashSet::new();
3092        for operation in analysis.operations.values() {
3093            let Some(mut current) = operation
3094                .request_body
3095                .as_ref()
3096                .and_then(crate::analysis::RequestBodyContent::schema_name)
3097            else {
3098                continue;
3099            };
3100            while request_body_roots.insert(current.to_string()) {
3101                let Some(crate::analysis::AnalyzedSchema {
3102                    schema_type: crate::analysis::SchemaType::Reference { target },
3103                    ..
3104                }) = analysis.schemas.get(current)
3105                else {
3106                    break;
3107                };
3108                current = target;
3109            }
3110        }
3111        TypeGenerationIndex {
3112            request_body_roots,
3113            reserved_type_names,
3114        }
3115    }
3116
3117    fn generate_request_model_builder(
3118        &self,
3119        schema: &crate::analysis::AnalyzedSchema,
3120        properties: &[EmittedObjectProperty<'_>],
3121        additional_properties: &crate::analysis::ObjectAdditionalProperties,
3122        analysis: &crate::analysis::SchemaAnalysis,
3123        type_index: &TypeGenerationIndex,
3124    ) -> TokenStream {
3125        let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
3126        let builder_base = format!("{}Builder", struct_name);
3127        let mut builder_name = builder_base.clone();
3128        let mut suffix = 2;
3129        while type_index.reserved_type_names.contains(&builder_name) {
3130            builder_name = format!("{builder_base}{suffix}");
3131            suffix += 1;
3132        }
3133        let builder_name = format_ident!("{builder_name}");
3134
3135        let required_parameters: Vec<TokenStream> = properties
3136            .iter()
3137            .filter(|property| property.is_required)
3138            .map(|property| {
3139                let ident = &property.ident;
3140                let field_type = &property.field_type;
3141                quote! { #ident: #field_type }
3142            })
3143            .collect();
3144        let required_idents: Vec<&syn::Ident> = properties
3145            .iter()
3146            .filter(|property| property.is_required)
3147            .map(|property| &property.ident)
3148            .collect();
3149        let optional_initializers: Vec<TokenStream> = properties
3150            .iter()
3151            .filter(|property| !property.is_required)
3152            .map(|property| {
3153                let ident = &property.ident;
3154                quote! { #ident: None }
3155            })
3156            .collect();
3157
3158        let additional_initializer = match additional_properties {
3159            crate::analysis::ObjectAdditionalProperties::Denied
3160            | crate::analysis::ObjectAdditionalProperties::Closed => TokenStream::new(),
3161            crate::analysis::ObjectAdditionalProperties::Untyped
3162            | crate::analysis::ObjectAdditionalProperties::Typed { .. } => quote! {
3163                additional_properties: ::std::collections::BTreeMap::new(),
3164            },
3165        };
3166
3167        let mut used_builder_methods =
3168            std::collections::HashSet::from(["new".to_string(), "build".to_string()]);
3169        if additional_properties.is_open() {
3170            used_builder_methods.insert("additional_properties".to_string());
3171        }
3172        let optional_setters: Vec<TokenStream> = properties
3173            .iter()
3174            .filter(|property| !property.is_required)
3175            .map(|property| {
3176                let field_ident = &property.ident;
3177                let field_type = self.generate_property_base_type(
3178                    &schema.name,
3179                    property.wire_name,
3180                    property.property,
3181                    analysis,
3182                );
3183                // Allocate every setter in the builder's method namespace.
3184                // `new` and `build` keep their documented `with_` escape;
3185                // further collisions receive deterministic numeric suffixes.
3186                let field_name = field_ident.to_string();
3187                let plain_field_name = field_name.strip_prefix("r#").unwrap_or(&field_name);
3188                let mut setter_name = if matches!(plain_field_name, "new" | "build") {
3189                    format!("with_{plain_field_name}")
3190                } else {
3191                    field_name.clone()
3192                };
3193                let setter_base = setter_name.clone();
3194                let mut suffix = 2;
3195                while !used_builder_methods.insert(setter_name.clone()) {
3196                    setter_name = format!("{setter_base}_{suffix}");
3197                    suffix += 1;
3198                }
3199                let setter_ident = Self::to_field_ident(&setter_name);
3200                let wire_name = property.wire_name;
3201                if self.property_is_tri_state(
3202                    &schema.name,
3203                    property.wire_name,
3204                    property.property,
3205                    property.is_required,
3206                ) {
3207                    let mut null_name = format!("{plain_field_name}_null");
3208                    let null_base = null_name.clone();
3209                    let mut suffix = 2;
3210                    while !used_builder_methods.insert(null_name.clone()) {
3211                        null_name = format!("{null_base}_{suffix}");
3212                        suffix += 1;
3213                    }
3214                    let null_ident = Self::to_field_ident(&null_name);
3215
3216                    let mut absent_name = format!("{plain_field_name}_absent");
3217                    let absent_base = absent_name.clone();
3218                    let mut suffix = 2;
3219                    while !used_builder_methods.insert(absent_name.clone()) {
3220                        absent_name = format!("{absent_base}_{suffix}");
3221                        suffix += 1;
3222                    }
3223                    let absent_ident = Self::to_field_ident(&absent_name);
3224
3225                    quote! {
3226                        #[doc = concat!("Set the optional nullable `", #wire_name, "` request field to a value.")]
3227                        #[must_use]
3228                        pub fn #setter_ident(mut self, #field_ident: #field_type) -> Self {
3229                            self.value.#field_ident = Some(Some(#field_ident));
3230                            self
3231                        }
3232
3233                        #[doc = concat!("Set the optional nullable `", #wire_name, "` request field to JSON null.")]
3234                        #[must_use]
3235                        pub fn #null_ident(mut self) -> Self {
3236                            self.value.#field_ident = Some(None);
3237                            self
3238                        }
3239
3240                        #[doc = concat!("Omit the optional nullable `", #wire_name, "` request field.")]
3241                        #[must_use]
3242                        pub fn #absent_ident(mut self) -> Self {
3243                            self.value.#field_ident = None;
3244                            self
3245                        }
3246                    }
3247                } else {
3248                    quote! {
3249                        #[doc = concat!("Set the optional `", #wire_name, "` request field.")]
3250                        #[must_use]
3251                        pub fn #setter_ident(mut self, #field_ident: #field_type) -> Self {
3252                            self.value.#field_ident = Some(#field_ident);
3253                            self
3254                        }
3255                    }
3256                }
3257            })
3258            .collect();
3259
3260        let additional_setter = match additional_properties {
3261            crate::analysis::ObjectAdditionalProperties::Denied
3262            | crate::analysis::ObjectAdditionalProperties::Closed => TokenStream::new(),
3263            crate::analysis::ObjectAdditionalProperties::Untyped => quote! {
3264                /// Replace the request's additional properties.
3265                #[must_use]
3266                pub fn additional_properties(
3267                    mut self,
3268                    additional_properties: ::std::collections::BTreeMap<
3269                        String,
3270                        serde_json::Value,
3271                    >,
3272                ) -> Self {
3273                    self.value.additional_properties = additional_properties;
3274                    self
3275                }
3276            },
3277            crate::analysis::ObjectAdditionalProperties::Typed { value_type } => {
3278                let value_type = self.generate_array_item_type(value_type, analysis);
3279                quote! {
3280                    /// Replace the request's additional properties.
3281                    #[must_use]
3282                    pub fn additional_properties(
3283                        mut self,
3284                        additional_properties: ::std::collections::BTreeMap<
3285                            String,
3286                            #value_type,
3287                        >,
3288                    ) -> Self {
3289                        self.value.additional_properties = additional_properties;
3290                        self
3291                    }
3292                }
3293            }
3294        };
3295
3296        quote! {
3297            impl #struct_name {
3298                /// Construct this request with every required wire field.
3299                pub fn new(#(#required_parameters),*) -> Self {
3300                    Self {
3301                        #(#required_idents,)*
3302                        #(#optional_initializers,)*
3303                        #additional_initializer
3304                    }
3305                }
3306
3307                /// Start a dependency-free builder with every required wire field.
3308                pub fn builder(#(#required_parameters),*) -> #builder_name {
3309                    #builder_name::new(#(#required_idents),*)
3310                }
3311            }
3312
3313            /// Dependency-free builder for [`#struct_name`].
3314            #[derive(Debug, Clone)]
3315            #[must_use]
3316            pub struct #builder_name {
3317                value: #struct_name,
3318            }
3319
3320            impl #builder_name {
3321                /// Start a builder with every required wire field.
3322                pub fn new(#(#required_parameters),*) -> Self {
3323                    Self {
3324                        value: #struct_name::new(#(#required_idents),*),
3325                    }
3326                }
3327
3328                #(#optional_setters)*
3329                #additional_setter
3330
3331                /// Finish building the request model.
3332                pub fn build(self) -> #struct_name {
3333                    self.value
3334                }
3335            }
3336        }
3337    }
3338
3339    fn generate_discriminated_enum(
3340        &self,
3341        schema: &crate::analysis::AnalyzedSchema,
3342        discriminator_field: &str,
3343        variants: &[crate::analysis::UnionVariant],
3344        exclusive: bool,
3345        analysis: &crate::analysis::SchemaAnalysis,
3346    ) -> Result<TokenStream> {
3347        let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
3348
3349        // Check if any variant references another discriminated union
3350        let has_nested_discriminated_union = variants.iter().any(|variant| {
3351            if let Some(variant_schema) = analysis.schemas.get(&variant.type_name) {
3352                matches!(
3353                    variant_schema.schema_type,
3354                    crate::analysis::SchemaType::DiscriminatedUnion { .. }
3355                )
3356            } else {
3357                false
3358            }
3359        });
3360
3361        // If we have a nested discriminated union, make this enum untagged
3362        if has_nested_discriminated_union {
3363            // Generate as untagged union
3364            let schema_refs: Vec<crate::analysis::SchemaRef> = variants
3365                .iter()
3366                .map(|v| crate::analysis::SchemaRef {
3367                    target: v.type_name.clone(),
3368                    nullable: false,
3369                })
3370                .collect();
3371            return self.generate_union_enum(schema, &schema_refs, exclusive, analysis);
3372        }
3373
3374        let enclosing = self.to_rust_type_name(&schema.name);
3375        let variant_shapes: Vec<_> = variants
3376            .iter()
3377            .map(|variant| {
3378                let variant_name = format_ident!("{}", variant.rust_name);
3379                let variant_type = format_ident!("{}", self.to_rust_type_name(&variant.type_name));
3380                // Box variant payloads that point at the enclosing enum or any
3381                // schema in the analysis's recursive set, otherwise the enum has
3382                // infinite size (E0072).
3383                let payload = if self.to_rust_type_name(&variant.type_name) == enclosing
3384                    || analysis
3385                        .dependencies
3386                        .recursive_schemas
3387                        .contains(&variant.type_name)
3388                {
3389                    quote! { Box<#variant_type> }
3390                } else {
3391                    quote! { #variant_type }
3392                };
3393                (variant, variant_name, payload)
3394            })
3395            .collect();
3396        let enum_variants = variant_shapes.iter().map(|(_, variant_name, payload)| {
3397            quote! { #variant_name(#payload), }
3398        });
3399        let serialize_arms = variant_shapes.iter().map(|(variant, variant_name, _)| {
3400            let canonical_value = &variant.discriminator_value;
3401            let variant_values = &variant.discriminator_values;
3402            let serialize_discriminator = if variant.discriminator_field_declared {
3403                quote! {
3404                    match object.get(#discriminator_field) {
3405                        Some(serde_json::Value::String(tag))
3406                            if matches!(tag.as_str(), #(#variant_values)|*) => {}
3407                        Some(serde_json::Value::String(tag)) => {
3408                            return Err(serde::ser::Error::custom(format!(
3409                                "discriminator `{}` value `{tag}` is not valid for variant `{}`",
3410                                #discriminator_field,
3411                                stringify!(#variant_name),
3412                            )));
3413                        }
3414                        Some(_) => {
3415                            return Err(serde::ser::Error::custom(concat!(
3416                                "discriminator `",
3417                                #discriminator_field,
3418                                "` did not serialize as a string",
3419                            )));
3420                        }
3421                        None => {
3422                            object.insert(
3423                                #discriminator_field.to_string(),
3424                                serde_json::Value::String(#canonical_value.to_string()),
3425                            );
3426                        }
3427                    }
3428                }
3429            } else {
3430                TokenStream::new()
3431            };
3432            quote! {
3433                Self::#variant_name(payload) => {
3434                    let mut value = serde_json::to_value(payload)
3435                        .map_err(serde::ser::Error::custom)?;
3436                    let object = value.as_object_mut().ok_or_else(|| {
3437                        serde::ser::Error::custom(concat!(
3438                            "discriminated union variant `",
3439                            stringify!(#variant_name),
3440                            "` did not serialize as an object",
3441                        ))
3442                    })?;
3443                    #serialize_discriminator
3444                    value.serialize(serializer)
3445                }
3446            }
3447        });
3448        let mut deserialize_arms = Vec::new();
3449        for (primary_index, (variant, variant_name, payload)) in variant_shapes.iter().enumerate() {
3450            for tag in &variant.preferred_discriminator_values {
3451                let fallback_attempts = variant_shapes.iter().enumerate().filter_map(
3452                    |(fallback_index, (_, fallback_name, fallback_payload))| {
3453                        if fallback_index == primary_index {
3454                            return None;
3455                        }
3456                        if exclusive {
3457                            Some(quote! {
3458                                if let Ok(payload) =
3459                                    serde_json::from_value::<#fallback_payload>(value.clone())
3460                                {
3461                                    if let Some((_, first_name)) = &structural_match {
3462                                        return Err(serde::de::Error::custom(format!(
3463                                            "discriminator `{}` value `{}` did not fit its mapped branch and structurally matched both `{}` and `{}`",
3464                                            #discriminator_field,
3465                                            #tag,
3466                                            first_name,
3467                                            stringify!(#fallback_name),
3468                                        )));
3469                                    }
3470                                    structural_match = Some((
3471                                        Self::#fallback_name(payload),
3472                                        stringify!(#fallback_name),
3473                                    ));
3474                                }
3475                            })
3476                        } else {
3477                            Some(quote! {
3478                                if let Ok(payload) =
3479                                    serde_json::from_value::<#fallback_payload>(value.clone())
3480                                {
3481                                    return Ok(Self::#fallback_name(payload));
3482                                }
3483                            })
3484                        }
3485                    },
3486                );
3487                let structural_fallback = if exclusive {
3488                    quote! {
3489                        let mut structural_match: Option<(Self, &'static str)> = None;
3490                        #(#fallback_attempts)*
3491                        match structural_match {
3492                            Some((payload, _)) => Ok(payload),
3493                            None => Err(serde::de::Error::custom(primary_error)),
3494                        }
3495                    }
3496                } else {
3497                    quote! {
3498                        #(#fallback_attempts)*
3499                        Err(serde::de::Error::custom(primary_error))
3500                    }
3501                };
3502                deserialize_arms.push(quote! {
3503                    #tag => {
3504                        let primary_error = match serde_json::from_value::<#payload>(value.clone()) {
3505                            Ok(payload) => return Ok(Self::#variant_name(payload)),
3506                            Err(error) => error,
3507                        };
3508                        #structural_fallback
3509                    }
3510                });
3511            }
3512        }
3513        let missing_discriminator_attempts = variant_shapes.iter().filter_map(
3514            |(variant, variant_name, payload)| {
3515                if variant.discriminator_field_required {
3516                    return None;
3517                }
3518                if exclusive {
3519                    Some(quote! {
3520                        if let Ok(payload) = serde_json::from_value::<#payload>(value.clone()) {
3521                            if let Some((_, first_name)) = &structural_match {
3522                                return Err(serde::de::Error::custom(format!(
3523                                    "missing discriminator `{}` structurally matched both `{}` and `{}`",
3524                                    #discriminator_field,
3525                                    first_name,
3526                                    stringify!(#variant_name),
3527                                )));
3528                            }
3529                            structural_match = Some((
3530                                Self::#variant_name(payload),
3531                                stringify!(#variant_name),
3532                            ));
3533                        }
3534                    })
3535                } else {
3536                    Some(quote! {
3537                        if let Ok(payload) = serde_json::from_value::<#payload>(value.clone()) {
3538                            return Ok(Self::#variant_name(payload));
3539                        }
3540                    })
3541                }
3542            },
3543        );
3544        let has_missing_discriminator_candidates = variants
3545            .iter()
3546            .any(|variant| !variant.discriminator_field_required);
3547        let missing_discriminator_fallback = if has_missing_discriminator_candidates && exclusive {
3548            quote! {
3549                let mut structural_match: Option<(Self, &'static str)> = None;
3550                #(#missing_discriminator_attempts)*
3551                structural_match
3552                    .map(|(payload, _)| payload)
3553                    .ok_or_else(|| serde::de::Error::custom(concat!(
3554                        "missing string discriminator `",
3555                        #discriminator_field,
3556                        "` and no tagless branch matched",
3557                    )))
3558            }
3559        } else if has_missing_discriminator_candidates {
3560            quote! {
3561                #(#missing_discriminator_attempts)*
3562                Err(serde::de::Error::custom(concat!(
3563                    "missing string discriminator `",
3564                    #discriminator_field,
3565                    "` and no tagless branch matched",
3566                )))
3567            }
3568        } else {
3569            quote! {
3570                Err(serde::de::Error::custom(concat!(
3571                    "missing string discriminator `",
3572                    #discriminator_field,
3573                    "`",
3574                )))
3575            }
3576        };
3577
3578        let doc_comment = if let Some(desc) = &schema.description {
3579            quote! { #[doc = #desc] }
3580        } else {
3581            TokenStream::new()
3582        };
3583
3584        // Keep the discriminator on each standalone component model, then do
3585        // explicit discriminator-directed dispatch here. A derive-based
3586        // internally tagged enum requires stripping the tag from its payload;
3587        // that made the same component serialize invalid JSON when used
3588        // directly or in an array. Explicit dispatch retains O(1)-by-tag
3589        // behavior without giving the payload two incompatible wire shapes.
3590        let derives = if self.config.enable_specta {
3591            quote! {
3592                #[derive(Debug, Clone)]
3593                #[cfg_attr(feature = "specta", derive(specta::Type))]
3594            }
3595        } else {
3596            quote! {
3597                #[derive(Debug, Clone)]
3598            }
3599        };
3600
3601        Ok(quote! {
3602            #doc_comment
3603            #derives
3604            pub enum #enum_name {
3605                #(#enum_variants)*
3606            }
3607
3608            impl serde::Serialize for #enum_name {
3609                fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
3610                where
3611                    S: serde::Serializer,
3612                {
3613                    match self {
3614                        #(#serialize_arms)*
3615                    }
3616                }
3617            }
3618
3619            impl<'de> serde::Deserialize<'de> for #enum_name {
3620                fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3621                where
3622                    D: serde::Deserializer<'de>,
3623                {
3624                    let value = serde_json::Value::deserialize(deserializer)?;
3625                    let discriminator = match value.get(#discriminator_field) {
3626                        Some(serde_json::Value::String(discriminator)) => {
3627                            Some(discriminator.as_str())
3628                        }
3629                        Some(_) => {
3630                            return Err(serde::de::Error::custom(concat!(
3631                                "non-string discriminator `",
3632                                #discriminator_field,
3633                                "`",
3634                            )));
3635                        }
3636                        None => None,
3637                    };
3638                    match discriminator {
3639                        Some(discriminator) => match discriminator {
3640                            #(#deserialize_arms)*
3641                            other => Err(serde::de::Error::custom(format!(
3642                                "unknown discriminator value `{other}` for `{}`",
3643                                #discriminator_field,
3644                            ))),
3645                        },
3646                        None => { #missing_discriminator_fallback }
3647                    }
3648                }
3649            }
3650        })
3651    }
3652
3653    /// Check if a discriminated union should be generated as untagged due to being nested
3654    fn should_use_untagged_discriminated_union(
3655        &self,
3656        schema: &crate::analysis::AnalyzedSchema,
3657        analysis: &crate::analysis::SchemaAnalysis,
3658    ) -> bool {
3659        // Only make discriminated unions untagged if they are nested AND their variants
3660        // don't need the discriminator field for API compatibility
3661
3662        // Check if this schema is used as a variant in another discriminated union
3663        for other_schema in analysis.schemas.values() {
3664            if let crate::analysis::SchemaType::DiscriminatedUnion { variants, .. } =
3665                &other_schema.schema_type
3666            {
3667                for variant in variants {
3668                    if variant.type_name == schema.name {
3669                        // This discriminated union is nested inside another discriminated union
3670
3671                        // Check if the current schema's variants have the discriminator field in their properties
3672                        // If they do, we need to keep this union tagged to preserve the discriminator
3673                        if let crate::analysis::SchemaType::DiscriminatedUnion {
3674                            discriminator_field: current_discriminator,
3675                            variants: current_variants,
3676                            ..
3677                        } = &schema.schema_type
3678                        {
3679                            // Check if any variant schemas have the discriminator field as a property
3680                            for current_variant in current_variants {
3681                                if let Some(variant_schema) =
3682                                    analysis.schemas.get(&current_variant.type_name)
3683                                {
3684                                    if let crate::analysis::SchemaType::Object {
3685                                        properties, ..
3686                                    } = &variant_schema.schema_type
3687                                    {
3688                                        if properties.contains_key(current_discriminator) {
3689                                            // This variant has the discriminator field as a property,
3690                                            // so we need to keep the union tagged to preserve it
3691                                            return false;
3692                                        }
3693                                    }
3694                                }
3695                            }
3696                        }
3697
3698                        // No variants have the discriminator as a property, safe to make untagged
3699                        return true;
3700                    }
3701                }
3702            }
3703        }
3704        false
3705    }
3706
3707    fn generate_union_enum(
3708        &self,
3709        schema: &crate::analysis::AnalyzedSchema,
3710        variants: &[crate::analysis::SchemaRef],
3711        exclusive: bool,
3712        analysis: &crate::analysis::SchemaAnalysis,
3713    ) -> Result<TokenStream> {
3714        let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
3715
3716        // Generate meaningful variant names based on type names
3717        let mut used_variant_names = std::collections::HashSet::new();
3718        let enum_variants = variants
3719            .iter()
3720            .enumerate()
3721            .map(|(i, variant)| {
3722                // Generate a meaningful variant name from the type name
3723                let base_variant_name = self.type_name_to_variant_name(&variant.target);
3724                let variant_name = self.ensure_unique_variant_name_generator(
3725                    base_variant_name,
3726                    &mut used_variant_names,
3727                    i,
3728                );
3729                let variant_name_ident = format_ident!("{}", variant_name);
3730
3731                // For primitive types and Vec types, use them directly without conversion
3732                let variant_type_tokens = if matches!(
3733                    variant.target.as_str(),
3734                    "bool"
3735                        | "i8"
3736                        | "i16"
3737                        | "i32"
3738                        | "i64"
3739                        | "i128"
3740                        | "u8"
3741                        | "u16"
3742                        | "u32"
3743                        | "u64"
3744                        | "u128"
3745                        | "f32"
3746                        | "f64"
3747                        | "String"
3748                ) {
3749                    let type_ident = format_ident!("{}", variant.target);
3750                    quote! { #type_ident }
3751                } else if variant.target == "serde_json::Value" {
3752                    // The target is a fully-qualified path; emit it as a path so
3753                    // it doesn't get mangled into a phantom `SerdeJsonValue` ident.
3754                    quote! { serde_json::Value }
3755                } else if variant.target.starts_with("Vec<") && variant.target.ends_with(">") {
3756                    // Handle Vec types by parsing the inner type
3757                    let inner = &variant.target[4..variant.target.len() - 1];
3758
3759                    // Handle nested Vec types (e.g., Vec<Vec<i64>>)
3760                    if inner.starts_with("Vec<") && inner.ends_with(">") {
3761                        let inner_inner = &inner[4..inner.len() - 1];
3762                        if inner_inner == "serde_json::Value" {
3763                            quote! { Vec<Vec<serde_json::Value>> }
3764                        } else {
3765                            let inner_inner_type = if matches!(
3766                                inner_inner,
3767                                "bool"
3768                                    | "i8"
3769                                    | "i16"
3770                                    | "i32"
3771                                    | "i64"
3772                                    | "i128"
3773                                    | "u8"
3774                                    | "u16"
3775                                    | "u32"
3776                                    | "u64"
3777                                    | "u128"
3778                                    | "f32"
3779                                    | "f64"
3780                                    | "String"
3781                            ) {
3782                                format_ident!("{}", inner_inner)
3783                            } else {
3784                                format_ident!("{}", self.to_rust_type_name(inner_inner))
3785                            };
3786                            quote! { Vec<Vec<#inner_inner_type>> }
3787                        }
3788                    } else if inner == "serde_json::Value" {
3789                        quote! { Vec<serde_json::Value> }
3790                    } else {
3791                        let inner_type = if matches!(
3792                            inner,
3793                            "bool"
3794                                | "i8"
3795                                | "i16"
3796                                | "i32"
3797                                | "i64"
3798                                | "i128"
3799                                | "u8"
3800                                | "u16"
3801                                | "u32"
3802                                | "u64"
3803                                | "u128"
3804                                | "f32"
3805                                | "f64"
3806                                | "String"
3807                        ) {
3808                            format_ident!("{}", inner)
3809                        } else {
3810                            format_ident!("{}", self.to_rust_type_name(inner))
3811                        };
3812                        quote! { Vec<#inner_type> }
3813                    }
3814                } else if variant.target.contains("::") || variant.target.contains('<') {
3815                    // Qualified Rust path or generic (chrono::DateTime<chrono::Utc>,
3816                    // bytes::Bytes, std::net::Ipv4Addr) emitted by TypeMapper. Pass
3817                    // it straight to syn — the to_rust_type_name PascalCase
3818                    // pipeline below would mangle it into a non-existent ident.
3819                    parse_rust_type(&variant.target).unwrap_or_else(|_| {
3820                        let fallback = format_ident!("{}", self.to_rust_type_name(&variant.target));
3821                        quote! { #fallback }
3822                    })
3823                } else {
3824                    let type_ident = format_ident!("{}", self.to_rust_type_name(&variant.target));
3825                    quote! { #type_ident }
3826                };
3827
3828                // Self-referential variant (variant payload type == enclosing
3829                // enum) yields an infinite-size enum (E0072). Wrap in `Box<T>` to
3830                // break the cycle. Observed in microsoft-graph.yaml.
3831                let target_rust_name = self.to_rust_type_name(&variant.target);
3832                let enclosing_name = self.to_rust_type_name(&schema.name);
3833                let is_self_ref = target_rust_name == enclosing_name;
3834                // Indirect cycles (stripe BankAccount → BankAccountCustomer →
3835                // Customer → BankAccountCustomer): variants pointing into the
3836                // analysis's recursive_schemas set must also be heap-allocated.
3837                let is_recursive_target = analysis
3838                    .dependencies
3839                    .recursive_schemas
3840                    .contains(&variant.target);
3841                let variant_type_tokens = if is_self_ref || is_recursive_target {
3842                    quote! { Box<#variant_type_tokens> }
3843                } else {
3844                    variant_type_tokens
3845                };
3846                let variant_type_tokens = if variant.nullable {
3847                    quote! { Option<#variant_type_tokens> }
3848                } else {
3849                    variant_type_tokens
3850                };
3851
3852                (variant_name_ident, variant_type_tokens)
3853            })
3854            .collect::<Vec<_>>();
3855        let variant_declarations = enum_variants
3856            .iter()
3857            .map(|(variant_name, variant_type)| quote! { #variant_name(#variant_type), })
3858            .collect::<Vec<_>>();
3859
3860        let doc_comment = if let Some(desc) = &schema.description {
3861            quote! { #[doc = #desc] }
3862        } else {
3863            TokenStream::new()
3864        };
3865
3866        let object_only = variants.iter().all(|variant| {
3867            self.union_target_serializes_as_object(
3868                &variant.target,
3869                analysis,
3870                &mut std::collections::HashSet::new(),
3871            )
3872        });
3873
3874        if exclusive || object_only {
3875            let derives = if self.config.enable_specta {
3876                quote! {
3877                    #[derive(Debug, Clone)]
3878                    #[cfg_attr(feature = "specta", derive(specta::Type))]
3879                }
3880            } else {
3881                quote! { #[derive(Debug, Clone)] }
3882            };
3883            let serialize_arms = enum_variants
3884                .iter()
3885                .map(|(variant_name, _)| {
3886                    quote! {
3887                        Self::#variant_name(value) => serde::Serialize::serialize(value, serializer),
3888                    }
3889                })
3890                .collect::<Vec<_>>();
3891            let deserialize_attempts = enum_variants
3892                .iter()
3893                .zip(variants)
3894                .map(|((variant_name, variant_type), variant)| {
3895                    if exclusive {
3896                        let constraints = self.union_branch_literal_constraints(
3897                            &variant.target,
3898                            analysis,
3899                            &mut std::collections::HashSet::new(),
3900                        );
3901                        let constraint_checks =
3902                            constraints.iter().map(|(field, (required, allowed))| {
3903                                let allowed = allowed
3904                                    .iter()
3905                                    .map(serde_json::Value::to_string)
3906                                    .collect::<Vec<_>>();
3907                                if allowed.is_empty() && *required {
3908                                    quote! { false }
3909                                } else if allowed.is_empty() {
3910                                    quote! { object.get(#field).is_none() }
3911                                } else if *required {
3912                                    quote! {
3913                                        object.get(#field).is_some_and(|value| {
3914                                            value.is_null()
3915                                                || matches!(value.to_string().as_str(), #(#allowed)|*)
3916                                        })
3917                                    }
3918                                } else {
3919                                    quote! {
3920                                        object.get(#field).is_none_or(|value| {
3921                                            value.is_null()
3922                                                || matches!(value.to_string().as_str(), #(#allowed)|*)
3923                                        })
3924                                    }
3925                                }
3926                            });
3927                        let constraints_match = if constraints.is_empty() {
3928                            quote! { true }
3929                        } else {
3930                            quote! {
3931                                input.as_object().is_some_and(|object| {
3932                                    true #(&& #constraint_checks)*
3933                                })
3934                            }
3935                        };
3936                        quote! {
3937                            if #constraints_match {
3938                                if let Ok(candidate) =
3939                                    serde_json::from_value::<#variant_type>(input.clone())
3940                                {
3941                                    match serde_json::to_value(&candidate) {
3942                                        Ok(encoded) if encoded == input => {
3943                                            if matched.is_some() {
3944                                                return Err(serde::de::Error::custom(concat!(
3945                                                    "ambiguous oneOf value for ",
3946                                                    stringify!(#enum_name),
3947                                                    ": more than one branch preserved the complete input",
3948                                                )));
3949                                            }
3950                                            matched = Some(Self::#variant_name(candidate));
3951                                        }
3952                                        Ok(encoded)
3953                                            if preserves_complete_json_input(
3954                                                &encoded, &input, true, false,
3955                                            ) =>
3956                                        {
3957                                            equivalent_matches += 1;
3958                                            equivalent.get_or_insert(Self::#variant_name(candidate));
3959                                        }
3960                                        _ => {}
3961                                    }
3962                                }
3963                            }
3964                        }
3965                    } else {
3966                        quote! {
3967                            if let Ok(candidate) =
3968                                serde_json::from_value::<#variant_type>(input.clone())
3969                            {
3970                                match serde_json::to_value(&candidate) {
3971                                    Ok(encoded)
3972                                        if preserves_complete_json_input(
3973                                            &encoded, &input, false, true,
3974                                        ) =>
3975                                    {
3976                                        return Ok(Self::#variant_name(candidate));
3977                                    }
3978                                    Ok(encoded)
3979                                        if equivalent.is_none()
3980                                            && preserves_complete_json_input(
3981                                                &encoded, &input, true, false,
3982                                            ) =>
3983                                    {
3984                                        equivalent = Some(Self::#variant_name(candidate));
3985                                    }
3986                                    _ => {}
3987                                }
3988                            }
3989                        }
3990                    }
3991                })
3992                .collect::<Vec<_>>();
3993            // A branch that reproduces the input exactly always wins. Only when
3994            // none does is a branch accepted whose encoding differs solely by
3995            // wire-equivalent forms: `1.0` for `1`, or an omitted key for an
3996            // input `null`. Inputs that decoded before keep their branch.
3997            let no_match = if exclusive {
3998                quote! {
3999                    if let Some(matched) = matched {
4000                        return Ok(matched);
4001                    }
4002                    if equivalent_matches > 1 {
4003                        return Err(serde::de::Error::custom(concat!(
4004                            "ambiguous oneOf value for ",
4005                            stringify!(#enum_name),
4006                            ": more than one branch preserved an equivalent input",
4007                        )));
4008                    }
4009                    equivalent.ok_or_else(|| serde::de::Error::custom(concat!(
4010                        "no oneOf branch for ",
4011                        stringify!(#enum_name),
4012                        " preserved the complete input",
4013                    )))
4014                }
4015            } else {
4016                quote! {
4017                    equivalent.ok_or_else(|| serde::de::Error::custom(concat!(
4018                        "no anyOf branch for ",
4019                        stringify!(#enum_name),
4020                        " preserved the complete input",
4021                    )))
4022                }
4023            };
4024            let matched_declaration = if exclusive {
4025                quote! {
4026                    let mut matched = None;
4027                    let mut equivalent = None;
4028                    let mut equivalent_matches = 0usize;
4029                }
4030            } else {
4031                quote! { let mut equivalent = None; }
4032            };
4033            let preservation_helper = {
4034                quote! {
4035                    fn exact_json_integer(number: &serde_json::Number) -> Option<i128> {
4036                        number
4037                            .as_i64()
4038                            .map(i128::from)
4039                            .or_else(|| number.as_u64().map(i128::from))
4040                    }
4041
4042                    fn json_numbers_have_same_value(
4043                        encoded: &serde_json::Number,
4044                        input: &serde_json::Number,
4045                    ) -> bool {
4046                        match (exact_json_integer(encoded), exact_json_integer(input)) {
4047                            (Some(encoded), Some(input)) => encoded == input,
4048                            (Some(encoded), None) => input.as_f64().is_some_and(|input| {
4049                                input.is_finite()
4050                                    && input.fract() == 0.0
4051                                    && input as i128 == encoded
4052                            }),
4053                            (None, Some(input)) => encoded.as_f64().is_some_and(|encoded| {
4054                                encoded.is_finite()
4055                                    && encoded.fract() == 0.0
4056                                    && encoded as i128 == input
4057                            }),
4058                            (None, None) => encoded.as_f64() == input.as_f64(),
4059                        }
4060                    }
4061
4062                    /// `nulls_may_be_absent` also accepts an input `null` that the
4063                    /// branch omits, as a skipped `None` does. Extra encoded
4064                    /// keys are allowed only by the pre-existing anyOf match.
4065                    fn preserves_complete_json_input(
4066                        encoded: &serde_json::Value,
4067                        input: &serde_json::Value,
4068                        nulls_may_be_absent: bool,
4069                        encoded_keys_may_be_extra: bool,
4070                    ) -> bool {
4071                        match (encoded, input) {
4072                            (
4073                                serde_json::Value::Object(encoded),
4074                                serde_json::Value::Object(input),
4075                            ) => {
4076                                (encoded_keys_may_be_extra
4077                                    || encoded.keys().all(|key| input.contains_key(key)))
4078                                    && input.iter().all(|(key, value)| match encoded.get(key) {
4079                                        Some(encoded_value) => preserves_complete_json_input(
4080                                            encoded_value,
4081                                            value,
4082                                            nulls_may_be_absent,
4083                                            encoded_keys_may_be_extra,
4084                                        ),
4085                                        None => nulls_may_be_absent && value.is_null(),
4086                                    })
4087                            }
4088                            (
4089                                serde_json::Value::Array(encoded),
4090                                serde_json::Value::Array(input),
4091                            ) => {
4092                                encoded.len() == input.len()
4093                                    && encoded.iter().zip(input).all(|(encoded, input)| {
4094                                        preserves_complete_json_input(
4095                                            encoded,
4096                                            input,
4097                                            nulls_may_be_absent,
4098                                            encoded_keys_may_be_extra,
4099                                        )
4100                                    })
4101                            }
4102                            (
4103                                serde_json::Value::Number(encoded),
4104                                serde_json::Value::Number(input),
4105                            ) => json_numbers_have_same_value(encoded, input),
4106                            _ => encoded == input,
4107                        }
4108                    }
4109                }
4110            };
4111
4112            return Ok(quote! {
4113                #doc_comment
4114                #derives
4115                pub enum #enum_name {
4116                    #(#variant_declarations)*
4117                }
4118
4119                impl Serialize for #enum_name {
4120                    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
4121                    where
4122                        S: serde::Serializer,
4123                    {
4124                        match self {
4125                            #(#serialize_arms)*
4126                        }
4127                    }
4128                }
4129
4130                impl<'de> Deserialize<'de> for #enum_name {
4131                    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
4132                    where
4133                        D: serde::Deserializer<'de>,
4134                    {
4135                        #preservation_helper
4136                        let input = <serde_json::Value as Deserialize>::deserialize(deserializer)?;
4137                        #matched_declaration
4138                        #(#deserialize_attempts)*
4139                        #no_match
4140                    }
4141                }
4142            });
4143        }
4144
4145        // Generate derives with optional Specta support for non-exclusive
4146        // unions, where multiple anyOf branches may legitimately match.
4147        let derives = if self.config.enable_specta {
4148            quote! {
4149                #[derive(Debug, Clone, Deserialize, Serialize)]
4150                #[cfg_attr(feature = "specta", derive(specta::Type))]
4151                #[serde(untagged)]
4152            }
4153        } else {
4154            quote! {
4155                #[derive(Debug, Clone, Deserialize, Serialize)]
4156                #[serde(untagged)]
4157            }
4158        };
4159
4160        Ok(quote! {
4161            #doc_comment
4162            #derives
4163            pub enum #enum_name {
4164                #(#variant_declarations)*
4165            }
4166        })
4167    }
4168
4169    fn union_branch_literal_constraints(
4170        &self,
4171        target: &str,
4172        analysis: &crate::analysis::SchemaAnalysis,
4173        visited: &mut std::collections::HashSet<String>,
4174    ) -> BTreeMap<String, (bool, Vec<serde_json::Value>)> {
4175        if !visited.insert(target.to_string()) {
4176            return BTreeMap::new();
4177        }
4178        let constraints = analysis
4179            .schemas
4180            .get(target)
4181            .map(|schema| {
4182                self.schema_literal_property_constraints(&schema.original, analysis, visited)
4183            })
4184            .unwrap_or_default();
4185        visited.remove(target);
4186        constraints
4187    }
4188
4189    fn schema_literal_property_constraints(
4190        &self,
4191        schema: &serde_json::Value,
4192        analysis: &crate::analysis::SchemaAnalysis,
4193        visited: &mut std::collections::HashSet<String>,
4194    ) -> BTreeMap<String, (bool, Vec<serde_json::Value>)> {
4195        let Some(object) = schema.as_object() else {
4196            return BTreeMap::new();
4197        };
4198        if let Some(reference) = object.get("$ref").and_then(serde_json::Value::as_str)
4199            && let Some(target) = reference.rsplit('/').next()
4200        {
4201            return self.union_branch_literal_constraints(target, analysis, visited);
4202        }
4203
4204        let required = object
4205            .get("required")
4206            .and_then(serde_json::Value::as_array)
4207            .into_iter()
4208            .flatten()
4209            .filter_map(serde_json::Value::as_str)
4210            .collect::<std::collections::HashSet<_>>();
4211        let mut constraints = BTreeMap::new();
4212        if let Some(properties) = object
4213            .get("properties")
4214            .and_then(serde_json::Value::as_object)
4215        {
4216            for (field, property) in properties {
4217                if let Some(values) = self.schema_literal_domain(
4218                    property,
4219                    analysis,
4220                    &mut std::collections::HashSet::new(),
4221                ) && !values.is_empty()
4222                {
4223                    constraints.insert(field.clone(), (required.contains(field.as_str()), values));
4224                }
4225            }
4226        }
4227        if let Some(branches) = object.get("allOf").and_then(serde_json::Value::as_array) {
4228            for branch in branches {
4229                for (field, (branch_required, values)) in
4230                    self.schema_literal_property_constraints(branch, analysis, visited)
4231                {
4232                    constraints
4233                        .entry(field)
4234                        .and_modify(|(is_required, existing)| {
4235                            *is_required |= branch_required;
4236                            existing.retain(|value| values.contains(value));
4237                        })
4238                        .or_insert((branch_required, values));
4239                }
4240            }
4241        }
4242        constraints
4243    }
4244
4245    fn schema_literal_domain(
4246        &self,
4247        schema: &serde_json::Value,
4248        analysis: &crate::analysis::SchemaAnalysis,
4249        visited: &mut std::collections::HashSet<String>,
4250    ) -> Option<Vec<serde_json::Value>> {
4251        let object = schema.as_object()?;
4252        if let Some(reference) = object.get("$ref").and_then(serde_json::Value::as_str)
4253            && let Some(target) = reference.rsplit('/').next()
4254        {
4255            if !visited.insert(target.to_string()) {
4256                return None;
4257            }
4258            let result = analysis
4259                .schemas
4260                .get(target)
4261                .and_then(|schema| self.schema_literal_domain(&schema.original, analysis, visited));
4262            visited.remove(target);
4263            return result;
4264        }
4265        let own = object
4266            .get("const")
4267            .map(|value| vec![value.clone()])
4268            .or_else(|| {
4269                object
4270                    .get("enum")
4271                    .and_then(serde_json::Value::as_array)
4272                    .cloned()
4273            });
4274        let composed = object
4275            .get("allOf")
4276            .and_then(serde_json::Value::as_array)
4277            .and_then(|branches| {
4278                branches.iter().fold(None, |domain, branch| {
4279                    let branch_domain = self.schema_literal_domain(branch, analysis, visited);
4280                    match (domain, branch_domain) {
4281                        (None, other) | (other, None) => other,
4282                        (Some(mut left), Some(right)) => {
4283                            left.retain(|value| right.contains(value));
4284                            Some(left)
4285                        }
4286                    }
4287                })
4288            });
4289        match (own, composed) {
4290            (None, other) | (other, None) => other,
4291            (Some(mut left), Some(right)) => {
4292                left.retain(|value| right.contains(value));
4293                Some(left)
4294            }
4295        }
4296    }
4297
4298    fn union_target_serializes_as_object(
4299        &self,
4300        target: &str,
4301        analysis: &crate::analysis::SchemaAnalysis,
4302        visited: &mut std::collections::HashSet<String>,
4303    ) -> bool {
4304        if !visited.insert(target.to_string()) {
4305            return false;
4306        }
4307        let result = analysis
4308            .schemas
4309            .get(target)
4310            .is_some_and(|schema| match &schema.schema_type {
4311                crate::analysis::SchemaType::Object { .. }
4312                | crate::analysis::SchemaType::Composition { .. }
4313                | crate::analysis::SchemaType::DiscriminatedUnion { .. } => true,
4314                crate::analysis::SchemaType::Reference { target } => {
4315                    self.union_target_serializes_as_object(target, analysis, visited)
4316                }
4317                crate::analysis::SchemaType::Union { variants, .. } => {
4318                    variants.iter().all(|variant| {
4319                        self.union_target_serializes_as_object(&variant.target, analysis, visited)
4320                    })
4321                }
4322                _ => false,
4323            });
4324        visited.remove(target);
4325        result
4326    }
4327
4328    /// Walk a chain of type-alias `Reference`s starting from `target` and
4329    /// return true if the chain reaches the schema named by
4330    /// `enclosing_rust_name` (Rust name). Bounded depth to prevent infinite
4331    /// loops on truly cyclic aliases.
4332    fn target_aliases_back_to(
4333        &self,
4334        target: &str,
4335        enclosing_rust_name: &str,
4336        analysis: &crate::analysis::SchemaAnalysis,
4337    ) -> bool {
4338        let mut current = target.to_string();
4339        let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
4340        for _ in 0..16 {
4341            if !visited.insert(current.clone()) {
4342                return true;
4343            }
4344            let Some(schema) = analysis.schemas.get(&current) else {
4345                return false;
4346            };
4347            if let crate::analysis::SchemaType::Reference { target: next } = &schema.schema_type {
4348                if self.to_rust_type_name(next) == enclosing_rust_name {
4349                    return true;
4350                }
4351                current = next.clone();
4352                continue;
4353            }
4354            return false;
4355        }
4356        false
4357    }
4358
4359    fn generate_field_type(
4360        &self,
4361        schema_name: &str,
4362        field_name: &str,
4363        prop: &crate::analysis::PropertyInfo,
4364        is_required: bool,
4365        analysis: &crate::analysis::SchemaAnalysis,
4366    ) -> TokenStream {
4367        let base_type = self.generate_property_base_type(schema_name, field_name, prop, analysis);
4368
4369        if !is_required && self.property_is_nullable(schema_name, field_name, prop) {
4370            quote! { Option<Option<#base_type>> }
4371        } else if self.property_is_option_wrapped(
4372            schema_name,
4373            field_name,
4374            prop,
4375            is_required,
4376            analysis,
4377        ) {
4378            quote! { Option<#base_type> }
4379        } else {
4380            base_type
4381        }
4382    }
4383
4384    pub(crate) fn property_is_nullable(
4385        &self,
4386        schema_name: &str,
4387        field_name: &str,
4388        prop: &crate::analysis::PropertyInfo,
4389    ) -> bool {
4390        let override_key = format!("{schema_name}.{field_name}");
4391        prop.nullable
4392            || self
4393                .config
4394                .nullable_field_overrides
4395                .get(&override_key)
4396                .copied()
4397                .unwrap_or(false)
4398    }
4399
4400    pub(crate) fn property_is_tri_state(
4401        &self,
4402        schema_name: &str,
4403        field_name: &str,
4404        prop: &crate::analysis::PropertyInfo,
4405        is_required: bool,
4406    ) -> bool {
4407        !is_required && self.property_is_nullable(schema_name, field_name, prop)
4408    }
4409
4410    fn property_is_option_wrapped(
4411        &self,
4412        schema_name: &str,
4413        field_name: &str,
4414        prop: &crate::analysis::PropertyInfo,
4415        is_required: bool,
4416        analysis: &crate::analysis::SchemaAnalysis,
4417    ) -> bool {
4418        !is_required
4419            || self.property_is_nullable(schema_name, field_name, prop)
4420            || (prop.default.is_some() && self.type_lacks_default(&prop.schema_type, analysis))
4421    }
4422
4423    pub(crate) fn generate_property_base_type(
4424        &self,
4425        schema_name: &str,
4426        _field_name: &str,
4427        prop: &crate::analysis::PropertyInfo,
4428        analysis: &crate::analysis::SchemaAnalysis,
4429    ) -> TokenStream {
4430        use crate::analysis::SchemaType;
4431
4432        match &prop.schema_type {
4433            SchemaType::Primitive { rust_type, .. } => {
4434                // syn handles generics + complex paths
4435                // (chrono::DateTime<chrono::Utc>, Vec<u8>, …).
4436                parse_rust_type(rust_type).unwrap_or_else(|_| {
4437                    // Pathological mapper output: fall back to bare
4438                    // String so the generated file at least
4439                    // compiles. Emit a stderr warning so the
4440                    // operator can investigate.
4441                    eprintln!(
4442                        "⚠️  TypeMapper produced un-parseable type `{rust_type}`; \
4443                         falling back to String"
4444                    );
4445                    quote! { String }
4446                })
4447            }
4448            SchemaType::Reference { target } => {
4449                let target_rust_name = self.to_rust_type_name(target);
4450                let target_type = format_ident!("{}", target_rust_name);
4451                // Wrap recursive references in Box<T> for heap allocation.
4452                // Three ways to detect the cycle:
4453                // 1. Target is in the analysis-level recursive set (catches
4454                //    direct + indirect cycles via the dependency graph).
4455                // 2. Target's Rust name equals the enclosing struct's Rust
4456                //    name (catches cloudflare-style cases where two distinct
4457                //    spec schemas PascalCase to the same ident).
4458                // 3. Target is a type alias whose resolution chain reaches
4459                //    the enclosing schema (catches cal-com's
4460                //    `ReassignBookingOutput20240813Data = Reassign...`
4461                //    pattern: the synthesized inline name aliases back to
4462                //    its parent).
4463                let enclosing_rust_name = self.to_rust_type_name(schema_name);
4464                let is_self_via_rust_name = target_rust_name == enclosing_rust_name;
4465                let is_alias_chain_self =
4466                    self.target_aliases_back_to(target, &enclosing_rust_name, analysis);
4467                if analysis.dependencies.recursive_schemas.contains(target)
4468                    || is_self_via_rust_name
4469                    || is_alias_chain_self
4470                {
4471                    quote! { Box<#target_type> }
4472                } else {
4473                    quote! { #target_type }
4474                }
4475            }
4476            SchemaType::Array { item_type } => {
4477                let inner_type = self.generate_array_item_type(item_type, analysis);
4478                quote! { Vec<#inner_type> }
4479            }
4480            SchemaType::Nullable { inner_type } => {
4481                let inner_type = self.generate_array_item_type(inner_type, analysis);
4482                quote! { Option<#inner_type> }
4483            }
4484            SchemaType::Tuple { element_types } => {
4485                self.generate_tuple_type(element_types, analysis)
4486            }
4487            SchemaType::Untyped { shape, .. } => untyped_tokens(*shape),
4488            _ => {
4489                // Fallback for complex types
4490                quote! { serde_json::Value }
4491            }
4492        }
4493    }
4494
4495    /// Render positional element types as a Rust tuple. serde reads and writes
4496    /// these as JSON arrays of exactly this length, which is what makes the
4497    /// analyzer's exact-length rule load-bearing.
4498    fn generate_tuple_type(
4499        &self,
4500        element_types: &[crate::analysis::SchemaType],
4501        analysis: &crate::analysis::SchemaAnalysis,
4502    ) -> TokenStream {
4503        let elements = element_types
4504            .iter()
4505            .map(|element_type| self.generate_array_item_type(element_type, analysis))
4506            .collect::<Vec<_>>();
4507        // A one-element Rust tuple needs the trailing comma; `(T)` is just `T`,
4508        // which serde would read as a bare value instead of a single-element
4509        // array.
4510        if let [only] = elements.as_slice() {
4511            return quote! { (#only,) };
4512        }
4513        quote! { (#(#elements),*) }
4514    }
4515
4516    fn generate_serde_field_attrs(
4517        &self,
4518        schema_name: &str,
4519        field_name: &str,
4520        field_ident: &syn::Ident,
4521        prop: &crate::analysis::PropertyInfo,
4522        is_required: bool,
4523        analysis: &crate::analysis::SchemaAnalysis,
4524    ) -> TokenStream {
4525        let mut attrs = Vec::new();
4526
4527        // Generate rename attribute if field name differs from Rust identifier
4528        // Strip r# prefix for comparison since serde handles raw idents transparently
4529        let rust_field_name = field_ident.to_string();
4530        let comparison_name = rust_field_name
4531            .strip_prefix("r#")
4532            .unwrap_or(&rust_field_name);
4533        if comparison_name != field_name {
4534            attrs.push(quote! { rename = #field_name });
4535        }
4536
4537        let is_tri_state = self.property_is_tri_state(schema_name, field_name, prop, is_required);
4538
4539        // Optional fields may be omitted when their outer Option is None.
4540        // Optional nullable fields use Option<Option<T>> so Some(None) remains
4541        // an explicit JSON null. Required nullable fields stay Option<T> and
4542        // must serialize None rather than skipping the required wire name.
4543        if !is_required {
4544            attrs.push(quote! { skip_serializing_if = "Option::is_none" });
4545        }
4546
4547        // Only add default attribute for required fields that have default values.
4548        // Skip #[serde(default)] for types that don't implement Default (discriminated
4549        // unions, union enums) — those fields should be Option<T> instead.
4550        if prop.default.is_some()
4551            && (is_required && !prop.nullable)
4552            && !self.type_lacks_default(&prop.schema_type, analysis)
4553        {
4554            attrs.push(quote! { default });
4555        }
4556
4557        // Codec hint from TypeMapper (Q2): `format: byte` →
4558        // `with = "base64_serde"`, etc. Fields whose mapped type
4559        // carries no codec (e.g. chrono::DateTime<Utc> uses its
4560        // built-in serde) skip this attribute. Option fields need
4561        // the `::option` submodule of the codec — serde dispatches
4562        // on field type, and the base codec works on Vec<u8> /
4563        // chrono::Duration / etc., not their Option wrappers.
4564        if let Some(codec) = self.schema_type_serde_codec(&prop.schema_type, analysis) {
4565            let is_option_wrapped = self.property_is_option_wrapped(
4566                schema_name,
4567                field_name,
4568                prop,
4569                is_required,
4570                analysis,
4571            );
4572            let codec_path = if is_tri_state {
4573                Self::double_option_codec_path(&codec)
4574            } else if is_option_wrapped {
4575                format!("{codec}::option")
4576            } else {
4577                codec
4578            };
4579            attrs.push(quote! { with = #codec_path });
4580            // A `with` codec disables serde's implicit
4581            // missing-field → None handling for Option fields
4582            // (serde-rs/serde#2878); without `default` a request
4583            // that simply omits the field fails to deserialize.
4584            if is_option_wrapped {
4585                attrs.push(quote! { default });
4586            }
4587        } else if is_tri_state {
4588            attrs.push(quote! { default });
4589            attrs.push(quote! { deserialize_with = "tri_state_serde::deserialize" });
4590        }
4591
4592        if attrs.is_empty() {
4593            TokenStream::new()
4594        } else {
4595            quote! { #[serde(#(#attrs),*)] }
4596        }
4597    }
4598
4599    fn double_option_codec_path(codec: &str) -> String {
4600        match codec {
4601            "time::serde::rfc3339" => "time_rfc3339_double_option".to_string(),
4602            "time_date_format" => "time_date_double_option".to_string(),
4603            "time_time_format" => "time_time_double_option".to_string(),
4604            _ => format!("{codec}::double_option"),
4605        }
4606    }
4607
4608    /// Resolve a field codec through named scalar aliases. A property may
4609    /// reference a component whose generated Rust type is `bytes::Bytes`; the
4610    /// codec belongs on the property field, because a Rust type alias cannot
4611    /// carry serde attributes of its own.
4612    fn schema_type_serde_codec(
4613        &self,
4614        schema_type: &crate::analysis::SchemaType,
4615        analysis: &crate::analysis::SchemaAnalysis,
4616    ) -> Option<String> {
4617        let mut current = schema_type;
4618        let mut visited = std::collections::HashSet::new();
4619        loop {
4620            match current {
4621                crate::analysis::SchemaType::Primitive {
4622                    serde_with: Some(codec),
4623                    ..
4624                } => return Some(codec.clone()),
4625                crate::analysis::SchemaType::Reference { target }
4626                    if visited.insert(target.clone()) =>
4627                {
4628                    current = &analysis.schemas.get(target)?.schema_type;
4629                }
4630                crate::analysis::SchemaType::Nullable { inner_type } => {
4631                    current = inner_type;
4632                }
4633                _ => return None,
4634            }
4635        }
4636    }
4637
4638    /// Check if a schema type resolves to a type that doesn't implement `Default`.
4639    /// Discriminated unions and union enums don't derive Default, so fields with
4640    /// these types can't use `#[serde(default)]`.
4641    fn type_lacks_default(
4642        &self,
4643        schema_type: &crate::analysis::SchemaType,
4644        analysis: &crate::analysis::SchemaAnalysis,
4645    ) -> bool {
4646        use crate::analysis::SchemaType;
4647        match schema_type {
4648            SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => true,
4649            // Q2 typed scalars: chrono / url have no Default impl.
4650            // uuid::Uuid, bytes::Bytes, std::net::Ip*Addr all derive
4651            // Default, so they're safe to leave under #[serde(default)].
4652            SchemaType::Primitive { rust_type, .. } => matches!(
4653                rust_type.as_str(),
4654                "chrono::DateTime<chrono::Utc>"
4655                    | "chrono::NaiveDate"
4656                    | "chrono::NaiveTime"
4657                    | "chrono::Duration"
4658                    | "url::Url"
4659                    | "time::OffsetDateTime"
4660                    | "time::Date"
4661                    | "time::Time"
4662                    | "iso8601::Duration"
4663                    | "email_address::EmailAddress"
4664            ),
4665            SchemaType::Reference { target } => {
4666                if let Some(schema) = analysis.schemas.get(target) {
4667                    self.type_lacks_default(&schema.schema_type, analysis)
4668                } else {
4669                    false
4670                }
4671            }
4672            _ => false,
4673        }
4674    }
4675
4676    fn generate_specta_field_attrs(&self, field_name: &str) -> TokenStream {
4677        if !self.config.enable_specta {
4678            return TokenStream::new();
4679        }
4680
4681        // Convert field name to camelCase for TypeScript
4682        let camel_case_name = self.to_camel_case(field_name);
4683
4684        // Only add specta rename if it differs from the original field name
4685        if camel_case_name != field_name {
4686            quote! { #[cfg_attr(feature = "specta", specta(rename = #camel_case_name))] }
4687        } else {
4688            TokenStream::new()
4689        }
4690    }
4691
4692    pub(crate) fn to_rust_enum_variant(&self, s: &str) -> String {
4693        // Preserve sign for numeric values so e.g. `-1` and `1` produce
4694        // distinct variants (`VariantNeg1` vs `Variant1`). Without this,
4695        // strict-namespace enums in github.json collide on `1`/`-1`.
4696        let neg_prefix =
4697            if s.starts_with('-') && s.chars().skip(1).all(|c| c.is_ascii_digit() || c == '.') {
4698                "Neg"
4699            } else {
4700                ""
4701            };
4702
4703        // Convert string to valid Rust enum variant (PascalCase)
4704        let mut result = String::new();
4705        let mut next_upper = true;
4706        let mut prev_was_upper = false;
4707
4708        for (i, c) in s.chars().enumerate() {
4709            match c {
4710                'a'..='z' => {
4711                    if next_upper {
4712                        result.push(c.to_ascii_uppercase());
4713                        next_upper = false;
4714                    } else {
4715                        result.push(c);
4716                    }
4717                    prev_was_upper = false;
4718                }
4719                'A'..='Z' => {
4720                    if next_upper || (!prev_was_upper && i > 0) {
4721                        // Start of word or transition from lowercase
4722                        result.push(c);
4723                        next_upper = false;
4724                    } else {
4725                        // Continue uppercase sequence, convert to lowercase
4726                        result.push(c.to_ascii_lowercase());
4727                    }
4728                    prev_was_upper = true;
4729                }
4730                '0'..='9' => {
4731                    result.push(c);
4732                    next_upper = false;
4733                    prev_was_upper = false;
4734                }
4735                '.' | '-' | '_' | ' ' | '@' | '#' | '$' | '/' | '\\' => {
4736                    // Word boundaries - next char should be uppercase
4737                    next_upper = true;
4738                    prev_was_upper = false;
4739                }
4740                _ => {
4741                    // Other special characters - treat as word boundary
4742                    next_upper = true;
4743                    prev_was_upper = false;
4744                }
4745            }
4746        }
4747
4748        // Handle empty result
4749        if result.is_empty() {
4750            result = "Value".to_string();
4751        }
4752
4753        // Ensure variant starts with a letter (not a number)
4754        if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
4755            result = format!("Variant{neg_prefix}{result}");
4756        } else if !neg_prefix.is_empty() {
4757            // String happened to start with `-<digits>` but produced a
4758            // non-empty alphabetic prefix. Tag the negative anyway.
4759            result = format!("{neg_prefix}{result}");
4760        }
4761
4762        // Handle special cases for enum variants
4763        match result.as_str() {
4764            "Null" => "NullValue".to_string(),
4765            "True" => "TrueValue".to_string(),
4766            "False" => "FalseValue".to_string(),
4767            "Type" => "Type_".to_string(),
4768            "Match" => "Match_".to_string(),
4769            "Fn" => "Fn_".to_string(),
4770            "Impl" => "Impl_".to_string(),
4771            "Trait" => "Trait_".to_string(),
4772            "Struct" => "Struct_".to_string(),
4773            "Enum" => "Enum_".to_string(),
4774            "Mod" => "Mod_".to_string(),
4775            "Use" => "Use_".to_string(),
4776            "Pub" => "Pub_".to_string(),
4777            "Const" => "Const_".to_string(),
4778            "Static" => "Static_".to_string(),
4779            "Let" => "Let_".to_string(),
4780            "Mut" => "Mut_".to_string(),
4781            "Ref" => "Ref_".to_string(),
4782            "Move" => "Move_".to_string(),
4783            "Return" => "Return_".to_string(),
4784            "If" => "If_".to_string(),
4785            "Else" => "Else_".to_string(),
4786            "While" => "While_".to_string(),
4787            "For" => "For_".to_string(),
4788            "Loop" => "Loop_".to_string(),
4789            "Break" => "Break_".to_string(),
4790            "Continue" => "Continue_".to_string(),
4791            "Self" => "Self_".to_string(),
4792            "Super" => "Super_".to_string(),
4793            "Crate" => "Crate_".to_string(),
4794            "Async" => "Async_".to_string(),
4795            "Await" => "Await_".to_string(),
4796            _ => result,
4797        }
4798    }
4799
4800    #[allow(dead_code)]
4801    fn to_rust_identifier(&self, s: &str) -> String {
4802        // Convert string to valid Rust identifier
4803        let mut result = s
4804            .chars()
4805            .map(|c| match c {
4806                'a'..='z' | 'A'..='Z' | '0'..='9' => c,
4807                '.' | '-' | '_' | ' ' | '@' | '#' | '$' | '/' | '\\' => '_',
4808                _ => '_',
4809            })
4810            .collect::<String>();
4811
4812        // Remove leading/trailing underscores
4813        result = result.trim_matches('_').to_string();
4814
4815        // Handle empty result
4816        if result.is_empty() {
4817            result = "value".to_string();
4818        }
4819
4820        // Ensure identifier starts with a letter (not a number)
4821        if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
4822            result = format!("variant_{result}");
4823        }
4824
4825        // Handle special cases for enum values
4826        match result.as_str() {
4827            "null" => "null_value".to_string(),
4828            "true" => "true_value".to_string(),
4829            "false" => "false_value".to_string(),
4830            "type" => "type_".to_string(),
4831            "match" => "match_".to_string(),
4832            "fn" => "fn_".to_string(),
4833            "impl" => "impl_".to_string(),
4834            "trait" => "trait_".to_string(),
4835            "struct" => "struct_".to_string(),
4836            "enum" => "enum_".to_string(),
4837            "mod" => "mod_".to_string(),
4838            "use" => "use_".to_string(),
4839            "pub" => "pub_".to_string(),
4840            "const" => "const_".to_string(),
4841            "static" => "static_".to_string(),
4842            "let" => "let_".to_string(),
4843            "mut" => "mut_".to_string(),
4844            "ref" => "ref_".to_string(),
4845            "move" => "move_".to_string(),
4846            "return" => "return_".to_string(),
4847            "if" => "if_".to_string(),
4848            "else" => "else_".to_string(),
4849            "while" => "while_".to_string(),
4850            "for" => "for_".to_string(),
4851            "loop" => "loop_".to_string(),
4852            "break" => "break_".to_string(),
4853            "continue" => "continue_".to_string(),
4854            "self" => "self_".to_string(),
4855            "super" => "super_".to_string(),
4856            "crate" => "crate_".to_string(),
4857            "async" => "async_".to_string(),
4858            "await" => "await_".to_string(),
4859            // Reserved keywords for edition 2018+
4860            "override" => "override_".to_string(),
4861            "box" => "box_".to_string(),
4862            "dyn" => "dyn_".to_string(),
4863            "where" => "where_".to_string(),
4864            "in" => "in_".to_string(),
4865            // Reserved for future use
4866            "abstract" => "abstract_".to_string(),
4867            "become" => "become_".to_string(),
4868            "do" => "do_".to_string(),
4869            "final" => "final_".to_string(),
4870            "macro" => "macro_".to_string(),
4871            "priv" => "priv_".to_string(),
4872            "try" => "try_".to_string(),
4873            "typeof" => "typeof_".to_string(),
4874            "unsized" => "unsized_".to_string(),
4875            "virtual" => "virtual_".to_string(),
4876            "yield" => "yield_".to_string(),
4877            _ => result,
4878        }
4879    }
4880
4881    /// Q2.4: render a `/// Constraint: …` doc comment for a field
4882    /// when its OpenAPI schema declares any constraint annotations.
4883    /// No-op when constraints are empty or `mode = "off"`.
4884    ///
4885    /// **Doc-comment only** — by deliberate design we never emit
4886    /// `#[validate(...)]` attributes. Constraints belong to the wire
4887    /// contract; the server is the source of truth.
4888    fn generate_constraint_doc(
4889        &self,
4890        constraints: &crate::analysis::PropertyConstraints,
4891    ) -> TokenStream {
4892        use crate::type_mapping::ConstraintMode;
4893
4894        if constraints.is_empty() {
4895            return TokenStream::new();
4896        }
4897        match self.config.types.constraint_mode() {
4898            ConstraintMode::Off => TokenStream::new(),
4899            ConstraintMode::Doc => {
4900                let formatted = format_constraints_doc(constraints);
4901                quote! { #[doc = #formatted] }
4902            }
4903        }
4904    }
4905
4906    fn sanitize_doc_comment(&self, desc: &str) -> String {
4907        // Sanitize description to prevent doctest failures
4908        let mut result = desc.to_string();
4909
4910        // Look for potential code examples that might be interpreted as doctests
4911        // Common patterns that cause issues:
4912        // - Lines that look like standalone expressions
4913        // - JSON-like content
4914        // - Template strings with {}
4915
4916        // If the description contains what looks like code, wrap it in a text block
4917        if result.contains('\n')
4918            && (result.contains('{')
4919                || result.contains("```")
4920                || result.contains("Human:")
4921                || result.contains("Assistant:")
4922                || result
4923                    .lines()
4924                    .any(|line| line.trim().starts_with('"') && line.trim().ends_with('"')))
4925        {
4926            // If it already has code blocks, add ignore annotation
4927            if result.contains("```") {
4928                result = result.replace("```", "```ignore");
4929            } else {
4930                // Wrap the entire description in an ignored code block if it looks like code
4931                if result.lines().any(|line| {
4932                    let trimmed = line.trim();
4933                    trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() > 2
4934                }) {
4935                    result = format!("```ignore\n{result}\n```");
4936                }
4937            }
4938        }
4939
4940        result
4941    }
4942
4943    pub(crate) fn to_rust_type_name(&self, s: &str) -> String {
4944        rust_type_name(s)
4945    }
4946
4947    pub(crate) fn to_rust_field_name(&self, s: &str) -> String {
4948        // Track sign / leading-non-alpha so e.g. `+1` and `-1` produce
4949        // distinct field names instead of both collapsing to `field_1`
4950        // (observed in github.json's reactions schemas).
4951        let leading_marker = match s.chars().next() {
4952            Some('-') if s.len() > 1 => "neg_",
4953            Some('+') if s.len() > 1 => "pos_",
4954            _ => "",
4955        };
4956
4957        // Convert field name to snake_case properly
4958        let mut result = String::new();
4959        let mut prev_was_upper = false;
4960        let mut prev_was_underscore = false;
4961
4962        for (i, c) in s.chars().enumerate() {
4963            match c {
4964                'A'..='Z' => {
4965                    // Add underscore before uppercase if previous was lowercase
4966                    if i > 0 && !prev_was_upper && !prev_was_underscore {
4967                        result.push('_');
4968                    }
4969                    result.push(c.to_ascii_lowercase());
4970                    prev_was_upper = true;
4971                    prev_was_underscore = false;
4972                }
4973                'a'..='z' | '0'..='9' => {
4974                    result.push(c);
4975                    prev_was_upper = false;
4976                    prev_was_underscore = false;
4977                }
4978                '-' | '.' | '_' | '@' | '#' | '$' | ' ' => {
4979                    if !prev_was_underscore && !result.is_empty() {
4980                        result.push('_');
4981                        prev_was_underscore = true;
4982                    }
4983                    prev_was_upper = false;
4984                }
4985                _ => {
4986                    // For other special characters, convert to underscore
4987                    if !prev_was_underscore && !result.is_empty() {
4988                        result.push('_');
4989                    }
4990                    prev_was_upper = false;
4991                    prev_was_underscore = true;
4992                }
4993            }
4994        }
4995
4996        // Clean up result
4997        let mut result = result.trim_matches('_').to_string();
4998        if result.is_empty() {
4999            return "field".to_string();
5000        }
5001
5002        // Ensure field name starts with a letter or underscore (not a number)
5003        if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
5004            result = format!("field_{leading_marker}{result}");
5005        } else if !leading_marker.is_empty() {
5006            result = format!("{leading_marker}{result}");
5007        }
5008
5009        // `self`, `super`, `crate`, and `Self` are not permitted as raw
5010        // identifiers. Suffix them before constructing a proc-macro ident.
5011        if matches!(result.as_str(), "self" | "super" | "crate" | "Self") {
5012            return format!("{result}_field");
5013        }
5014        // Keep boolean literal property names as stable ordinary identifiers
5015        // and let the field allocator disambiguate an actual `true_field` or
5016        // `false_field` property. Serde preserves the original wire name.
5017        if matches!(result.as_str(), "true" | "false") {
5018            return format!("{result}_field");
5019        }
5020        // Handle reserved keywords using raw identifiers (r#keyword)
5021        if Self::is_rust_keyword(&result) {
5022            format!("r#{result}")
5023        } else {
5024            result
5025        }
5026    }
5027
5028    /// Check if a string is a Rust keyword that needs raw identifier treatment
5029    pub fn is_rust_keyword(s: &str) -> bool {
5030        matches!(
5031            s,
5032            "type"
5033                | "match"
5034                | "fn"
5035                | "struct"
5036                | "enum"
5037                | "impl"
5038                | "trait"
5039                | "mod"
5040                | "use"
5041                | "pub"
5042                | "const"
5043                | "static"
5044                | "let"
5045                | "mut"
5046                | "ref"
5047                | "move"
5048                | "return"
5049                | "if"
5050                | "else"
5051                | "while"
5052                | "for"
5053                | "loop"
5054                | "break"
5055                | "continue"
5056                | "self"
5057                | "super"
5058                | "crate"
5059                | "async"
5060                | "await"
5061                | "override"
5062                | "box"
5063                | "dyn"
5064                | "where"
5065                | "in"
5066                | "abstract"
5067                | "become"
5068                | "do"
5069                | "final"
5070                | "macro"
5071                | "priv"
5072                | "try"
5073                | "typeof"
5074                | "unsized"
5075                | "virtual"
5076                | "yield"
5077                // Rust 2024 edition reservations.
5078                | "gen"
5079        )
5080    }
5081
5082    /// Create a proc_macro2::Ident from a field name, handling r# raw identifiers
5083    pub fn to_field_ident(name: &str) -> proc_macro2::Ident {
5084        if let Some(raw) = name.strip_prefix("r#") {
5085            proc_macro2::Ident::new_raw(raw, proc_macro2::Span::call_site())
5086        } else {
5087            proc_macro2::Ident::new(name, proc_macro2::Span::call_site())
5088        }
5089    }
5090
5091    fn to_camel_case(&self, s: &str) -> String {
5092        // Convert snake_case or other formats to camelCase
5093        let mut result = String::new();
5094        let mut capitalize_next = false;
5095
5096        for (i, c) in s.chars().enumerate() {
5097            match c {
5098                '_' | '-' | '.' | ' ' => {
5099                    // Word boundary - capitalize next letter
5100                    capitalize_next = true;
5101                }
5102                'A'..='Z' => {
5103                    if i == 0 {
5104                        // First character should be lowercase in camelCase
5105                        result.push(c.to_ascii_lowercase());
5106                    } else if capitalize_next {
5107                        result.push(c);
5108                        capitalize_next = false;
5109                    } else {
5110                        result.push(c.to_ascii_lowercase());
5111                    }
5112                }
5113                'a'..='z' | '0'..='9' => {
5114                    if capitalize_next {
5115                        result.push(c.to_ascii_uppercase());
5116                        capitalize_next = false;
5117                    } else {
5118                        result.push(c);
5119                    }
5120                }
5121                _ => {
5122                    // Other characters - treat as word boundary
5123                    capitalize_next = true;
5124                }
5125            }
5126        }
5127
5128        if result.is_empty() {
5129            return "field".to_string();
5130        }
5131
5132        result
5133    }
5134
5135    fn generate_composition_struct(
5136        &self,
5137        schema: &crate::analysis::AnalyzedSchema,
5138        schemas: &[crate::analysis::SchemaRef],
5139    ) -> Result<TokenStream> {
5140        let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
5141
5142        // For composition, we can either:
5143        // 1. Flatten all referenced schemas into one struct (if they're all objects)
5144        // 2. Use serde(flatten) to compose them at runtime
5145        // For now, let's use approach 2 with serde(flatten)
5146
5147        let fields = schemas.iter().enumerate().map(|(i, schema_ref)| {
5148            let field_name = format_ident!("part_{}", i);
5149            let field_type = format_ident!("{}", self.to_rust_type_name(&schema_ref.target));
5150
5151            quote! {
5152                #[serde(flatten)]
5153                pub #field_name: #field_type,
5154            }
5155        });
5156
5157        let doc_comment = if let Some(desc) = &schema.description {
5158            quote! { #[doc = #desc] }
5159        } else {
5160            TokenStream::new()
5161        };
5162
5163        // Generate derives with optional Specta support
5164        let derives = if self.config.enable_specta {
5165            quote! {
5166                #[derive(Debug, Clone, Deserialize, Serialize)]
5167                #[cfg_attr(feature = "specta", derive(specta::Type))]
5168            }
5169        } else {
5170            quote! {
5171                #[derive(Debug, Clone, Deserialize, Serialize)]
5172            }
5173        };
5174
5175        Ok(quote! {
5176            #doc_comment
5177            #derives
5178            pub struct #struct_name {
5179                #(#fields)*
5180            }
5181        })
5182    }
5183
5184    fn union_declared_properties(
5185        &self,
5186        target: &str,
5187        analysis: &crate::analysis::SchemaAnalysis,
5188        visited: &mut std::collections::HashSet<String>,
5189    ) -> std::collections::HashSet<String> {
5190        if !visited.insert(target.to_string()) {
5191            return std::collections::HashSet::new();
5192        }
5193        let mut properties = std::collections::HashSet::new();
5194        if let Some(schema) = analysis.schemas.get(target) {
5195            match &schema.schema_type {
5196                crate::analysis::SchemaType::Object {
5197                    properties: own,
5198                    variant,
5199                    ..
5200                } => {
5201                    properties.extend(own.keys().cloned());
5202                    if let Some(variant) = variant {
5203                        properties.extend(self.union_declared_properties(
5204                            &variant.target,
5205                            analysis,
5206                            visited,
5207                        ));
5208                    }
5209                }
5210                crate::analysis::SchemaType::DiscriminatedUnion { variants, .. } => {
5211                    for variant in variants {
5212                        properties.extend(self.union_declared_properties(
5213                            &variant.type_name,
5214                            analysis,
5215                            visited,
5216                        ));
5217                    }
5218                }
5219                crate::analysis::SchemaType::Union { variants, .. }
5220                | crate::analysis::SchemaType::Composition { schemas: variants } => {
5221                    for variant in variants {
5222                        properties.extend(self.union_declared_properties(
5223                            &variant.target,
5224                            analysis,
5225                            visited,
5226                        ));
5227                    }
5228                }
5229                crate::analysis::SchemaType::Reference { target } => {
5230                    properties.extend(self.union_declared_properties(target, analysis, visited));
5231                }
5232                _ => {}
5233            }
5234        }
5235        visited.remove(target);
5236        properties
5237    }
5238
5239    #[allow(dead_code)]
5240    fn find_missing_types(&self, analysis: &SchemaAnalysis) -> std::collections::HashSet<String> {
5241        let mut missing = std::collections::HashSet::new();
5242        let defined_types: std::collections::HashSet<String> =
5243            analysis.schemas.keys().cloned().collect();
5244
5245        // Check all references in union variants
5246        for schema in analysis.schemas.values() {
5247            match &schema.schema_type {
5248                crate::analysis::SchemaType::Union { variants, .. } => {
5249                    for variant in variants {
5250                        if !defined_types.contains(&variant.target) {
5251                            missing.insert(variant.target.clone());
5252                        }
5253                    }
5254                }
5255                crate::analysis::SchemaType::DiscriminatedUnion { variants, .. } => {
5256                    for variant in variants {
5257                        if !defined_types.contains(&variant.type_name) {
5258                            missing.insert(variant.type_name.clone());
5259                        }
5260                    }
5261                }
5262                crate::analysis::SchemaType::Object { properties, .. } => {
5263                    // Sort properties for deterministic iteration
5264                    let mut sorted_props: Vec<_> = properties.iter().collect();
5265                    sorted_props.sort_by_key(|(name, _)| name.as_str());
5266                    for (_, prop) in sorted_props {
5267                        if let crate::analysis::SchemaType::Reference { target } = &prop.schema_type
5268                        {
5269                            if !defined_types.contains(target) {
5270                                missing.insert(target.clone());
5271                            }
5272                        }
5273                    }
5274                }
5275                crate::analysis::SchemaType::Reference { target }
5276                    if !defined_types.contains(target) =>
5277                {
5278                    missing.insert(target.clone());
5279                }
5280                _ => {}
5281            }
5282        }
5283
5284        missing
5285    }
5286
5287    #[allow(clippy::only_used_in_recursion)]
5288    fn generate_array_item_type(
5289        &self,
5290        item_type: &crate::analysis::SchemaType,
5291        analysis: &crate::analysis::SchemaAnalysis,
5292    ) -> TokenStream {
5293        use crate::analysis::SchemaType;
5294
5295        match item_type {
5296            SchemaType::Primitive { rust_type, .. } => {
5297                // The string here may be anything from `i64` / `String` to
5298                // `serde_json::Value` to `Vec<serde_json::Value>` to
5299                // `BTreeMap<String, T>`. Parse it as a syn::Type so we get
5300                // the right tokens regardless of generics.
5301                if let Ok(parsed) = syn::parse_str::<syn::Type>(rust_type) {
5302                    quote! { #parsed }
5303                } else if rust_type.contains("::") {
5304                    let parts: Vec<_> = rust_type
5305                        .split("::")
5306                        .map(|p| format_ident!("{}", p))
5307                        .collect();
5308                    quote! { #(#parts)::* }
5309                } else {
5310                    let type_ident = format_ident!("{}", rust_type);
5311                    quote! { #type_ident }
5312                }
5313            }
5314            SchemaType::Reference { target } => {
5315                let target_type = format_ident!("{}", self.to_rust_type_name(target));
5316                // Wrap recursive references in Box<T> for heap allocation in arrays
5317                if analysis.dependencies.recursive_schemas.contains(target) {
5318                    quote! { Box<#target_type> }
5319                } else {
5320                    quote! { #target_type }
5321                }
5322            }
5323            SchemaType::Array { item_type } => {
5324                // Nested arrays
5325                let inner_type = self.generate_array_item_type(item_type, analysis);
5326                quote! { Vec<#inner_type> }
5327            }
5328            SchemaType::Nullable { inner_type } => {
5329                let inner_type = self.generate_array_item_type(inner_type, analysis);
5330                quote! { Option<#inner_type> }
5331            }
5332            SchemaType::Tuple { element_types } => {
5333                self.generate_tuple_type(element_types, analysis)
5334            }
5335            SchemaType::Untyped { shape, .. } => untyped_tokens(*shape),
5336            _ => {
5337                // Fallback for complex types
5338                quote! { serde_json::Value }
5339            }
5340        }
5341    }
5342
5343    /// Convert a type name to a variant name (e.g., OutputMessage -> OutputMessage, FileSearchToolCall -> FileSearchToolCall)
5344    fn type_name_to_variant_name(&self, type_name: &str) -> String {
5345        // Handle primitive types specially
5346        match type_name {
5347            "bool" => return "Boolean".to_string(),
5348            "i8" | "i16" | "i32" | "i64" | "i128" => return "Integer".to_string(),
5349            "u8" | "u16" | "u32" | "u64" | "u128" => return "UnsignedInteger".to_string(),
5350            "f32" | "f64" => return "Number".to_string(),
5351            "String" => return "String".to_string(),
5352            "serde_json::Value" => return "Value".to_string(),
5353            // Q2 typed-scalar paths. Without these the fallback PascalCase
5354            // pass over `bytes::Bytes` produces `BytesBytes(BytesBytes)`,
5355            // which then can't compile because no `BytesBytes` type exists.
5356            "bytes::Bytes" => return "Binary".to_string(),
5357            "chrono::DateTime<chrono::Utc>" => return "DateTime".to_string(),
5358            "chrono::NaiveDate" => return "Date".to_string(),
5359            "chrono::NaiveTime" => return "Time".to_string(),
5360            "uuid::Uuid" => return "Uuid".to_string(),
5361            "url::Url" => return "Url".to_string(),
5362            "std::net::Ipv4Addr" => return "Ipv4".to_string(),
5363            "std::net::Ipv6Addr" => return "Ipv6".to_string(),
5364            _ => {}
5365        }
5366
5367        // Handle Vec types
5368        if type_name.starts_with("Vec<") && type_name.ends_with(">") {
5369            let inner = &type_name[4..type_name.len() - 1];
5370            // Handle nested Vec types specially
5371            if inner.starts_with("Vec<") && inner.ends_with(">") {
5372                let inner_inner = &inner[4..inner.len() - 1];
5373                return format!("{}ArrayArray", self.type_name_to_variant_name(inner_inner));
5374            }
5375            return format!("{}Array", self.type_name_to_variant_name(inner));
5376        }
5377
5378        // For untagged unions, we want to use the type name itself as the variant name
5379        // since it's already meaningful. This gives us OutputMessage instead of Variant0,
5380        // FileSearchToolCall instead of Variant1, etc.
5381
5382        // Remove common suffixes that might make variant names redundant
5383        let clean_name = type_name
5384            .trim_end_matches("Type")
5385            .trim_end_matches("Schema")
5386            .trim_end_matches("Item");
5387
5388        // Always convert to proper PascalCase to ensure no underscores in enum variants
5389        self.to_rust_type_name(clean_name)
5390    }
5391
5392    /// Ensure unique variant name for generator (similar to analyzer but for generator context)
5393    fn ensure_unique_variant_name_generator(
5394        &self,
5395        base_name: String,
5396        used_names: &mut std::collections::HashSet<String>,
5397        fallback_index: usize,
5398    ) -> String {
5399        if used_names.insert(base_name.clone()) {
5400            return base_name;
5401        }
5402
5403        // Try with numbers
5404        for i in 2..100 {
5405            let numbered_name = format!("{base_name}{i}");
5406            if used_names.insert(numbered_name.clone()) {
5407                return numbered_name;
5408            }
5409        }
5410
5411        // Fallback to Variant{index} if all else fails
5412        let fallback = format!("Variant{fallback_index}");
5413        used_names.insert(fallback.clone());
5414        fallback
5415    }
5416
5417    /// Find the request type for a given operation ID using the analyzed operation info
5418    fn find_request_type_for_operation(
5419        &self,
5420        operation_id: &str,
5421        analysis: &SchemaAnalysis,
5422    ) -> Option<String> {
5423        // Use the operation analysis to get the actual request body schema
5424        analysis.operations.get(operation_id).and_then(|op| {
5425            op.request_body
5426                .as_ref()
5427                .and_then(|rb| rb.schema_name().map(|s| s.to_string()))
5428        })
5429    }
5430
5431    /// Resolve the correct streaming event type based on EventFlow pattern
5432    fn resolve_streaming_event_type(
5433        &self,
5434        endpoint: &crate::streaming::StreamingEndpoint,
5435        analysis: &SchemaAnalysis,
5436    ) -> Result<String> {
5437        match &endpoint.event_flow {
5438            crate::streaming::EventFlow::Simple => {
5439                // For simple streaming, use the response type directly
5440                // Validate that the specified type exists in the schema
5441                if analysis.schemas.contains_key(&endpoint.event_union_type) {
5442                    Ok(endpoint.event_union_type.to_string())
5443                } else {
5444                    Err(crate::error::GeneratorError::ValidationError(format!(
5445                        "Streaming response type '{}' not found in schema for simple streaming endpoint '{}'",
5446                        endpoint.event_union_type, endpoint.operation_id
5447                    )))
5448                }
5449            }
5450            crate::streaming::EventFlow::StartDeltaStop { .. } => {
5451                // For complex event-based streaming, ensure we have a proper union type
5452                // For now, use the specified event_union_type but add validation
5453                if analysis.schemas.contains_key(&endpoint.event_union_type) {
5454                    Ok(endpoint.event_union_type.to_string())
5455                } else {
5456                    Err(crate::error::GeneratorError::ValidationError(format!(
5457                        "Event union type '{}' not found in schema for complex streaming endpoint '{}'",
5458                        endpoint.event_union_type, endpoint.operation_id
5459                    )))
5460                }
5461            }
5462        }
5463    }
5464
5465    /// Generate streaming error types
5466    fn generate_streaming_error_types(&self) -> Result<TokenStream> {
5467        Ok(quote! {
5468            /// Error type for streaming operations
5469            #[derive(Debug, thiserror::Error)]
5470            pub enum StreamingError {
5471                #[error("Connection error: {0}")]
5472                Connection(String),
5473                #[error("HTTP error: {status}")]
5474                Http { status: u16 },
5475                #[error("SSE parsing error: {0}")]
5476                Parsing(String),
5477                #[error("Authentication error: {0}")]
5478                Authentication(String),
5479                #[error("Rate limit error: {0}")]
5480                RateLimit(String),
5481                #[error("API error: {0}")]
5482                Api(String),
5483                #[error("Timeout error: {0}")]
5484                Timeout(String),
5485                #[error("Response body exceeded configured limit of {limit} bytes")]
5486                ResponseTooLarge { limit: usize },
5487                #[error("JSON serialization/deserialization error: {0}")]
5488                Json(#[from] serde_json::Error),
5489                #[error("Request error: {0}")]
5490                Request(reqwest::Error),
5491            }
5492
5493            impl From<reqwest::header::InvalidHeaderValue> for StreamingError {
5494                fn from(err: reqwest::header::InvalidHeaderValue) -> Self {
5495                    StreamingError::Api(format!("Invalid header value: {}", err))
5496                }
5497            }
5498
5499            impl From<reqwest::Error> for StreamingError {
5500                fn from(err: reqwest::Error) -> Self {
5501                    if err.is_timeout() {
5502                        StreamingError::Timeout(err.to_string())
5503                    } else if err.is_status() {
5504                        if let Some(status) = err.status() {
5505                            StreamingError::Http { status: status.as_u16() }
5506                        } else {
5507                            StreamingError::Connection(err.to_string())
5508                        }
5509                    } else {
5510                        StreamingError::Request(err)
5511                    }
5512                }
5513            }
5514        })
5515    }
5516
5517    /// Generate trait for a streaming endpoint
5518    fn generate_endpoint_trait(
5519        &self,
5520        endpoint: &crate::streaming::StreamingEndpoint,
5521        analysis: &SchemaAnalysis,
5522    ) -> Result<TokenStream> {
5523        use crate::streaming::HttpMethod;
5524
5525        let trait_name = format_ident!(
5526            "{}StreamingClient",
5527            self.to_rust_type_name(&endpoint.operation_id)
5528        );
5529        let method_name =
5530            format_ident!("stream_{}", self.to_rust_field_name(&endpoint.operation_id));
5531        let event_type =
5532            format_ident!("{}", self.resolve_streaming_event_type(endpoint, analysis)?);
5533
5534        // Generate method signature based on HTTP method
5535        let method_signature = match endpoint.http_method {
5536            HttpMethod::Get => {
5537                // Generate parameters from query_parameters
5538                let mut param_defs = Vec::new();
5539                for qp in &endpoint.query_parameters {
5540                    let param_name = format_ident!("{}", self.to_rust_field_name(&qp.name));
5541                    if qp.required {
5542                        param_defs.push(quote! { #param_name: &str });
5543                    } else {
5544                        param_defs.push(quote! { #param_name: Option<&str> });
5545                    }
5546                }
5547                quote! {
5548                    async fn #method_name(
5549                        &self,
5550                        #(#param_defs),*
5551                    ) -> Result<BoxSseStream<Result<#event_type, Self::Error>>, Self::Error>;
5552                }
5553            }
5554            HttpMethod::Post => {
5555                // Find the request type for this operation
5556                let request_type = self
5557                    .find_request_type_for_operation(&endpoint.operation_id, analysis)
5558                    .unwrap_or_else(|| "serde_json::Value".to_string());
5559                let request_type_ident = if request_type.contains("::") {
5560                    let parts: Vec<&str> = request_type.split("::").collect();
5561                    let path_parts: Vec<_> = parts.iter().map(|p| format_ident!("{}", p)).collect();
5562                    quote! { #(#path_parts)::* }
5563                } else {
5564                    let ident = format_ident!("{}", request_type);
5565                    quote! { #ident }
5566                };
5567                quote! {
5568                    async fn #method_name(
5569                        &self,
5570                        request: #request_type_ident,
5571                    ) -> Result<BoxSseStream<Result<#event_type, Self::Error>>, Self::Error>;
5572                }
5573            }
5574        };
5575
5576        Ok(quote! {
5577            /// Streaming client trait for this endpoint
5578            #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
5579            #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
5580            pub trait #trait_name {
5581                type Error: std::error::Error + Send + Sync + 'static;
5582
5583                /// Stream events from the API
5584                #method_signature
5585            }
5586        })
5587    }
5588
5589    /// Generate streaming client implementation
5590    fn generate_streaming_client_impl(
5591        &self,
5592        streaming_config: &crate::streaming::StreamingConfig,
5593        analysis: &SchemaAnalysis,
5594    ) -> Result<TokenStream> {
5595        let client_name = format_ident!(
5596            "{}Client",
5597            self.to_rust_type_name(&streaming_config.client_module_name)
5598        );
5599
5600        // Generate struct fields
5601        // Always include custom_headers for flexibility (like HttpClient does)
5602        let mut struct_fields = vec![
5603            quote! { base_url: String },
5604            quote! { api_key: Option<String> },
5605            quote! { sse_client: SseClient },
5606            quote! { custom_headers: std::collections::BTreeMap<String, String> },
5607        ];
5608
5609        let has_optional_headers = !streaming_config
5610            .endpoints
5611            .iter()
5612            .all(|e| e.optional_headers.is_empty());
5613
5614        if has_optional_headers {
5615            struct_fields
5616                .push(quote! { optional_headers: std::collections::BTreeMap<String, String> });
5617        }
5618
5619        // Generate constructor
5620        // Use configured base URL as default, or fallback to generic example
5621        let default_base_url = if let Some(ref streaming_config) = self.config.streaming_config {
5622            streaming_config
5623                .endpoints
5624                .first()
5625                .and_then(|e| e.base_url.as_deref())
5626                .unwrap_or("https://api.example.com")
5627        } else {
5628            "https://api.example.com"
5629        };
5630        let max_response_body_bytes = self
5631            .config()
5632            .http_client_config
5633            .as_ref()
5634            .and_then(|http| http.max_response_body_bytes)
5635            .unwrap_or(8 * 1024 * 1024);
5636        let sse_client_initializer = if let Some(reconnect) = &streaming_config.reconnection_config
5637        {
5638            let max_retries = reconnect.max_retries;
5639            let initial_delay_ms = reconnect.initial_delay_ms;
5640            let max_delay_ms = reconnect.max_delay_ms;
5641            let backoff_multiplier = reconnect.backoff_multiplier;
5642            quote! {
5643                SseClient::new()
5644                    .with_max_error_body_bytes(#max_response_body_bytes)
5645                    .with_reconnect_options(SseReconnectOptions {
5646                        max_retries: #max_retries,
5647                        initial_retry_delay: std::time::Duration::from_millis(#initial_delay_ms),
5648                        max_retry_delay: std::time::Duration::from_millis(#max_delay_ms),
5649                        backoff_multiplier: #backoff_multiplier,
5650                    })
5651            }
5652        } else {
5653            quote! {
5654                SseClient::new()
5655                    .with_max_error_body_bytes(#max_response_body_bytes)
5656            }
5657        };
5658
5659        // Build constructor fields based on what the struct has
5660        let constructor_fields = if has_optional_headers {
5661            quote! {
5662                base_url: #default_base_url.to_string(),
5663                api_key: None,
5664                sse_client: #sse_client_initializer,
5665                custom_headers: std::collections::BTreeMap::new(),
5666                optional_headers: std::collections::BTreeMap::new(),
5667            }
5668        } else {
5669            quote! {
5670                base_url: #default_base_url.to_string(),
5671                api_key: None,
5672                sse_client: #sse_client_initializer,
5673                custom_headers: std::collections::BTreeMap::new(),
5674            }
5675        };
5676
5677        // Optional headers method only if the struct has the field
5678        let optional_headers_method = if has_optional_headers {
5679            quote! {
5680                /// Set optional headers for all requests
5681                pub fn set_optional_headers(&mut self, headers: std::collections::BTreeMap<String, String>) {
5682                    self.optional_headers = headers;
5683                }
5684            }
5685        } else {
5686            TokenStream::new()
5687        };
5688
5689        let constructor = quote! {
5690            impl #client_name {
5691                /// Create a new streaming client
5692                pub fn new() -> Self {
5693                    Self {
5694                        #constructor_fields
5695                    }
5696                }
5697
5698                /// Set the base URL for API requests
5699                pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
5700                    self.base_url = base_url.into();
5701                    self
5702                }
5703
5704                /// Set the API key for authentication
5705                pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
5706                    self.api_key = Some(api_key.into());
5707                    self
5708                }
5709
5710                /// Set the maximum number of error-response bytes buffered in memory.
5711                pub fn with_max_response_body_bytes(mut self, limit: usize) -> Self {
5712                    self.sse_client = self.sse_client.with_max_error_body_bytes(limit);
5713                    self
5714                }
5715
5716                /// Add a custom header to all requests
5717                pub fn with_header(
5718                    mut self,
5719                    name: impl Into<String>,
5720                    value: impl Into<String>,
5721                ) -> Self {
5722                    self.custom_headers.insert(name.into(), value.into());
5723                    self
5724                }
5725
5726                /// Set the HTTP client
5727                pub fn with_http_client(mut self, client: reqwest::Client) -> Self {
5728                    self.sse_client = self.sse_client.with_http_client(client);
5729                    self
5730                }
5731
5732                #optional_headers_method
5733            }
5734        };
5735
5736        // Generate trait implementations for each endpoint
5737        let mut trait_impls = Vec::new();
5738        for endpoint in &streaming_config.endpoints {
5739            let trait_impl = self.generate_endpoint_trait_impl(endpoint, &client_name, analysis)?;
5740            trait_impls.push(trait_impl);
5741        }
5742
5743        // Add Default implementation
5744        let default_impl = quote! {
5745            impl Default for #client_name {
5746                fn default() -> Self {
5747                    Self::new()
5748                }
5749            }
5750        };
5751
5752        Ok(quote! {
5753            /// Streaming client implementation
5754            #[derive(Debug, Clone)]
5755            pub struct #client_name {
5756                #(#struct_fields,)*
5757            }
5758
5759            #constructor
5760
5761            #default_impl
5762
5763            #(#trait_impls)*
5764        })
5765    }
5766
5767    /// Generate trait implementation for a specific endpoint
5768    fn generate_endpoint_trait_impl(
5769        &self,
5770        endpoint: &crate::streaming::StreamingEndpoint,
5771        client_name: &proc_macro2::Ident,
5772        analysis: &SchemaAnalysis,
5773    ) -> Result<TokenStream> {
5774        use crate::streaming::HttpMethod;
5775
5776        let trait_name = format_ident!(
5777            "{}StreamingClient",
5778            self.to_rust_type_name(&endpoint.operation_id)
5779        );
5780        let method_name =
5781            format_ident!("stream_{}", self.to_rust_field_name(&endpoint.operation_id));
5782        let event_type =
5783            format_ident!("{}", self.resolve_streaming_event_type(endpoint, analysis)?);
5784
5785        // Generate required headers
5786        let mut header_setup = Vec::new();
5787        for (name, value) in &endpoint.required_headers {
5788            header_setup.push(quote! {
5789                headers.insert(#name, HeaderValue::from_static(#value));
5790            });
5791        }
5792
5793        // Add authentication header
5794        // If auth_header is configured, use that; otherwise default to Bearer auth on Authorization header
5795        if let Some(auth_header) = &endpoint.auth_header {
5796            match auth_header {
5797                crate::streaming::AuthHeader::Bearer(header_name) => {
5798                    header_setup.push(quote! {
5799                        if let Some(ref api_key) = self.api_key {
5800                            headers.insert(#header_name, HeaderValue::from_str(&format!("Bearer {}", api_key))?);
5801                        }
5802                    });
5803                }
5804                crate::streaming::AuthHeader::ApiKey(header_name) => {
5805                    header_setup.push(quote! {
5806                        if let Some(ref api_key) = self.api_key {
5807                            headers.insert(#header_name, HeaderValue::from_str(api_key)?);
5808                        }
5809                    });
5810                }
5811            }
5812        } else {
5813            // Default: use api_key as Bearer token on Authorization header
5814            header_setup.push(quote! {
5815                if let Some(ref api_key) = self.api_key {
5816                    headers.insert("Authorization", HeaderValue::from_str(&format!("Bearer {}", api_key))?);
5817                }
5818            });
5819        }
5820
5821        // Always add custom_headers (like HttpClient does)
5822        header_setup.push(quote! {
5823            for (name, value) in &self.custom_headers {
5824                if let (Ok(header_name), Ok(header_value)) = (reqwest::header::HeaderName::from_bytes(name.as_bytes()), HeaderValue::from_str(value)) {
5825                    headers.insert(header_name, header_value);
5826                }
5827            }
5828        });
5829
5830        // Add optional headers (for endpoint-specific optional headers)
5831        if !endpoint.optional_headers.is_empty() {
5832            header_setup.push(quote! {
5833                for (key, value) in &self.optional_headers {
5834                    if let (Ok(header_name), Ok(header_value)) = (reqwest::header::HeaderName::from_bytes(key.as_bytes()), HeaderValue::from_str(value)) {
5835                        headers.insert(header_name, header_value);
5836                    }
5837                }
5838            });
5839        }
5840
5841        // Generate different code for GET vs POST
5842        match endpoint.http_method {
5843            HttpMethod::Get => self.generate_get_streaming_impl(
5844                endpoint,
5845                client_name,
5846                &trait_name,
5847                &method_name,
5848                &event_type,
5849                &header_setup,
5850            ),
5851            HttpMethod::Post => self.generate_post_streaming_impl(
5852                endpoint,
5853                client_name,
5854                &trait_name,
5855                &method_name,
5856                &event_type,
5857                &header_setup,
5858                analysis,
5859            ),
5860        }
5861    }
5862
5863    /// Generate streaming implementation for GET endpoints
5864    fn generate_get_streaming_impl(
5865        &self,
5866        endpoint: &crate::streaming::StreamingEndpoint,
5867        client_name: &proc_macro2::Ident,
5868        trait_name: &proc_macro2::Ident,
5869        method_name: &proc_macro2::Ident,
5870        event_type: &proc_macro2::Ident,
5871        header_setup: &[TokenStream],
5872    ) -> Result<TokenStream> {
5873        let path = &endpoint.path;
5874
5875        // Generate method parameters from query_parameters
5876        let mut param_defs = Vec::new();
5877        let mut query_params = Vec::new();
5878
5879        for qp in &endpoint.query_parameters {
5880            let param_name = format_ident!("{}", self.to_rust_field_name(&qp.name));
5881            let param_name_str = &qp.name;
5882
5883            if qp.required {
5884                param_defs.push(quote! { #param_name: &str });
5885                query_params.push(quote! {
5886                    url.query_pairs_mut().append_pair(#param_name_str, #param_name);
5887                });
5888            } else {
5889                param_defs.push(quote! { #param_name: Option<&str> });
5890                query_params.push(quote! {
5891                    if let Some(v) = #param_name {
5892                        url.query_pairs_mut().append_pair(#param_name_str, v);
5893                    }
5894                });
5895            }
5896        }
5897
5898        // Generate URL construction for GET
5899        let url_construction = quote! {
5900            let base_url = url::Url::parse(&self.base_url)
5901                .map_err(|e| StreamingError::Connection(format!("Invalid base URL: {}", e)))?;
5902            let path_to_join = #path.trim_start_matches('/');
5903            let mut url = base_url.join(path_to_join)
5904                .map_err(|e| StreamingError::Connection(format!("URL join error: {}", e)))?;
5905            #(#query_params)*
5906        };
5907
5908        let instrument_skip = quote! { #[instrument(skip(self), name = "streaming_get_request")] };
5909
5910        Ok(quote! {
5911            #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
5912            #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
5913            impl #trait_name for #client_name {
5914                type Error = StreamingError;
5915
5916                #instrument_skip
5917                async fn #method_name(
5918                    &self,
5919                    #(#param_defs),*
5920                ) -> Result<BoxSseStream<Result<#event_type, Self::Error>>, Self::Error> {
5921                    debug!("Starting streaming GET request");
5922
5923                    let mut headers = HeaderMap::new();
5924                    #(#header_setup)*
5925
5926                    #url_construction
5927                    let url_str = url.to_string();
5928                    debug!("Making streaming GET request to: {}", url_str);
5929
5930                    let request_builder = self.sse_client
5931                        .get(url_str)
5932                        .headers(headers);
5933
5934                    debug!("Creating SSE stream from request");
5935                    let stream = self
5936                        .sse_client
5937                        .stream::<#event_type>(request_builder)
5938                        .await?;
5939                    info!("SSE stream created successfully");
5940                    Ok(stream)
5941                }
5942            }
5943        })
5944    }
5945
5946    /// Generate streaming implementation for POST endpoints
5947    #[allow(clippy::too_many_arguments)]
5948    fn generate_post_streaming_impl(
5949        &self,
5950        endpoint: &crate::streaming::StreamingEndpoint,
5951        client_name: &proc_macro2::Ident,
5952        trait_name: &proc_macro2::Ident,
5953        method_name: &proc_macro2::Ident,
5954        event_type: &proc_macro2::Ident,
5955        header_setup: &[TokenStream],
5956        analysis: &SchemaAnalysis,
5957    ) -> Result<TokenStream> {
5958        let path = &endpoint.path;
5959
5960        // Find the request type for this operation
5961        let request_type = self
5962            .find_request_type_for_operation(&endpoint.operation_id, analysis)
5963            .unwrap_or_else(|| "serde_json::Value".to_string());
5964        let request_type_ident = if request_type.contains("::") {
5965            let parts: Vec<&str> = request_type.split("::").collect();
5966            let path_parts: Vec<_> = parts.iter().map(|p| format_ident!("{}", p)).collect();
5967            quote! { #(#path_parts)::* }
5968        } else {
5969            let ident = format_ident!("{}", request_type);
5970            quote! { #ident }
5971        };
5972
5973        // Generate URL construction for POST
5974        let url_construction = quote! {
5975            let base_url = url::Url::parse(&self.base_url)
5976                .map_err(|e| StreamingError::Connection(format!("Invalid base URL: {}", e)))?;
5977            let path_to_join = #path.trim_start_matches('/');
5978            let url = base_url.join(path_to_join)
5979                .map_err(|e| StreamingError::Connection(format!("URL join error: {}", e)))?
5980                .to_string();
5981        };
5982
5983        // Generate stream parameter setup (only for POST with stream_parameter)
5984        let stream_param = &endpoint.stream_parameter;
5985        let stream_setup = if stream_param.is_empty() {
5986            quote! {
5987                let streaming_request = request;
5988            }
5989        } else {
5990            quote! {
5991                // Ensure streaming is enabled
5992                let mut streaming_request = request;
5993                if let Ok(mut request_value) = serde_json::to_value(&streaming_request) {
5994                    if let Some(obj) = request_value.as_object_mut() {
5995                        obj.insert(#stream_param.to_string(), serde_json::Value::Bool(true));
5996                    }
5997                    streaming_request = serde_json::from_value(request_value)?;
5998                }
5999            }
6000        };
6001
6002        Ok(quote! {
6003            #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
6004            #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
6005            impl #trait_name for #client_name {
6006                type Error = StreamingError;
6007
6008                #[instrument(skip(self, request), name = "streaming_post_request")]
6009                async fn #method_name(
6010                    &self,
6011                    request: #request_type_ident,
6012                ) -> Result<BoxSseStream<Result<#event_type, Self::Error>>, Self::Error> {
6013                    debug!("Starting streaming POST request");
6014
6015                    #stream_setup
6016
6017                    let mut headers = HeaderMap::new();
6018                    #(#header_setup)*
6019
6020                    #url_construction
6021                    debug!("Making streaming POST request to: {}", url);
6022
6023                    let request_builder = self.sse_client
6024                        .post(&url)
6025                        .headers(headers)
6026                        .json(&streaming_request);
6027
6028                    debug!("Creating SSE stream from request");
6029                    let stream = self
6030                        .sse_client
6031                        .stream::<#event_type>(request_builder)
6032                        .await?;
6033                    info!("SSE stream created successfully");
6034                    Ok(stream)
6035                }
6036            }
6037        })
6038    }
6039
6040    /// Generate the reusable SSE transport module emitted as `sse.rs`.
6041    fn generate_sse_runtime(&self) -> Result<String> {
6042        self.generate_sse_runtime_with_bindings(None)
6043    }
6044
6045    fn generate_sse_runtime_with_bindings(
6046        &self,
6047        bindings: Option<&mut crate::bindings::BindingsMetadata>,
6048    ) -> Result<String> {
6049        let provenance_attribute = self.provenance_attribute();
6050        let error_types = self.generate_streaming_error_types()?;
6051        let parser = self.generate_sse_parser_utilities()?;
6052        let tokens = quote! {
6053            //! Generated SSE transport, framing, and JSON decoding support.
6054            //!
6055            //! This module is emitted only when SSE generation is enabled.
6056            #provenance_attribute
6057            #![allow(clippy::format_in_format_args)]
6058
6059            use futures_util::{Stream, StreamExt};
6060            use std::pin::Pin;
6061            use std::time::Duration;
6062            use tracing::debug;
6063
6064            /// Boxed stream of SSE events.
6065            ///
6066            /// `Send` on native targets so callers can move the stream between
6067            /// tasks. On `wasm32` the reqwest response wraps a JS `fetch` body
6068            /// (holding `Rc<RefCell<..>>` and a wasm-bindgen closure) and cannot
6069            /// be `Send`, so the bound is omitted and the stream stays on the
6070            /// single wasm thread. The public signatures do not change on
6071            /// native targets.
6072            #[cfg(not(target_arch = "wasm32"))]
6073            pub type BoxSseStream<T> = Pin<Box<dyn Stream<Item = T> + Send>>;
6074            #[cfg(target_arch = "wasm32")]
6075            pub type BoxSseStream<T> = Pin<Box<dyn Stream<Item = T>>>;
6076
6077            #error_types
6078
6079            /// Reusable transport client for generated SSE operations.
6080            #[derive(Debug, Clone)]
6081            pub struct SseClient {
6082                http_client: reqwest::Client,
6083                max_error_body_bytes: usize,
6084                reconnect_options: Option<SseReconnectOptions>,
6085            }
6086
6087            impl SseClient {
6088                pub fn new() -> Self {
6089                    Self {
6090                        http_client: reqwest::Client::new(),
6091                        max_error_body_bytes: DEFAULT_MAX_SSE_ERROR_BODY_BYTES,
6092                        reconnect_options: None,
6093                    }
6094                }
6095
6096                pub fn with_http_client(mut self, client: reqwest::Client) -> Self {
6097                    self.http_client = client;
6098                    self
6099                }
6100
6101                pub fn with_max_error_body_bytes(mut self, limit: usize) -> Self {
6102                    self.max_error_body_bytes = limit;
6103                    self
6104                }
6105
6106                /// Enable automatic reconnection for [`Self::stream`].
6107                pub fn with_reconnect_options(mut self, options: SseReconnectOptions) -> Self {
6108                    self.reconnect_options = Some(options);
6109                    self
6110                }
6111
6112                pub fn get(&self, url: impl reqwest::IntoUrl) -> reqwest::RequestBuilder {
6113                    self.http_client.get(url)
6114                }
6115
6116                pub fn post(&self, url: impl reqwest::IntoUrl) -> reqwest::RequestBuilder {
6117                    self.http_client.post(url)
6118                }
6119
6120                pub async fn stream<T>(
6121                    &self,
6122                    request_builder: reqwest::RequestBuilder,
6123                ) -> Result<BoxSseStream<Result<T, StreamingError>>, StreamingError>
6124                where
6125                    T: serde::de::DeserializeOwned + Send + 'static,
6126                {
6127                    if let Some(options) = self.reconnect_options.clone() {
6128                        parse_sse_json_reconnecting_with_limit(
6129                            request_builder,
6130                            self.max_error_body_bytes,
6131                            options,
6132                        ).await
6133                    } else {
6134                        parse_sse_json_stream_with_limit(
6135                            request_builder,
6136                            self.max_error_body_bytes,
6137                        ).await
6138                    }
6139                }
6140
6141                /// Stream raw SSE events from one HTTP connection.
6142                pub async fn stream_raw(
6143                    &self,
6144                    request_builder: reqwest::RequestBuilder,
6145                ) -> Result<BoxSseStream<Result<SseEvent<String>, StreamingError>>, StreamingError> {
6146                    parse_sse_raw_stream_with_limit(request_builder, self.max_error_body_bytes).await
6147                }
6148
6149                /// Stream typed JSON SSE events from one HTTP connection.
6150                pub async fn stream_json<T>(
6151                    &self,
6152                    request_builder: reqwest::RequestBuilder,
6153                ) -> Result<BoxSseStream<Result<SseEvent<T>, StreamingError>>, StreamingError>
6154                where
6155                    T: serde::de::DeserializeOwned + Send + 'static,
6156                {
6157                    parse_sse_json_events_with_limit(request_builder, self.max_error_body_bytes).await
6158                }
6159
6160                /// Stream raw SSE events and reconnect retryable connections.
6161                pub async fn stream_raw_reconnecting(
6162                    &self,
6163                    request_builder: reqwest::RequestBuilder,
6164                ) -> Result<BoxSseStream<Result<SseEvent<String>, StreamingError>>, StreamingError> {
6165                    parse_sse_raw_reconnecting_with_limit(
6166                        request_builder,
6167                        self.max_error_body_bytes,
6168                        self.reconnect_options.clone().unwrap_or_default(),
6169                    ).await
6170                }
6171
6172                /// Stream typed JSON SSE events and reconnect retryable connections.
6173                pub async fn stream_json_reconnecting<T>(
6174                    &self,
6175                    request_builder: reqwest::RequestBuilder,
6176                ) -> Result<BoxSseStream<Result<SseEvent<T>, StreamingError>>, StreamingError>
6177                where
6178                    T: serde::de::DeserializeOwned + Send + 'static,
6179                {
6180                    parse_sse_json_reconnecting_events_with_limit(
6181                        request_builder,
6182                        self.max_error_body_bytes,
6183                        self.reconnect_options.clone().unwrap_or_default(),
6184                    ).await
6185                }
6186            }
6187
6188            impl Default for SseClient {
6189                fn default() -> Self {
6190                    Self::new()
6191                }
6192            }
6193
6194            #parser
6195        };
6196        let syntax_tree = syn::parse2::<syn::File>(tokens).map_err(|error| {
6197            GeneratorError::CodeGenError(format!("Failed to parse generated sse.rs: {error}"))
6198        })?;
6199        if let Some(bindings) = bindings {
6200            bindings.collect("sse", &syntax_tree);
6201        }
6202        Ok(prettyplease::unparse(&syntax_tree))
6203    }
6204
6205    /// Generate the standalone SSE framing and JSON parsing utilities.
6206    fn generate_sse_parser_utilities(&self) -> Result<TokenStream> {
6207        Ok(quote! {
6208            /// Default upper bound for an SSE error response buffered in memory.
6209            pub const DEFAULT_MAX_SSE_ERROR_BODY_BYTES: usize = 8 * 1024 * 1024;
6210
6211            async fn __read_bounded_streaming_error_body(
6212                response: reqwest::Response,
6213                limit: usize,
6214            ) -> Result<Vec<u8>, StreamingError> {
6215                let mut body = Vec::new();
6216                // `bytes_stream()` rather than `Response::chunk()`: `chunk()` is
6217                // native-only in reqwest and does not exist on wasm32.
6218                let mut chunks = response.bytes_stream();
6219                while let Some(chunk) = chunks.next().await {
6220                    let chunk = chunk?;
6221                    let next_len = body.len().checked_add(chunk.len());
6222                    if next_len.is_none_or(|next_len| next_len > limit) {
6223                        return Err(StreamingError::ResponseTooLarge { limit });
6224                    }
6225                    body.extend_from_slice(&chunk);
6226                }
6227                Ok(body)
6228            }
6229
6230            /// A decoded SSE event with its transport metadata preserved.
6231            #[derive(Debug, Clone, PartialEq, Eq)]
6232            pub struct SseEvent<T> {
6233                /// Event name, or `message` when the server omitted `event:`.
6234                pub event: String,
6235                /// Raw or deserialized event payload.
6236                pub data: T,
6237                /// Most recent event ID, used to resume a reconnected stream.
6238                pub id: Option<String>,
6239                /// Server-supplied reconnection delay on this event, if present.
6240                pub retry: Option<Duration>,
6241            }
6242
6243            /// Controls automatic SSE reconnection behavior.
6244            #[derive(Debug, Clone)]
6245            pub struct SseReconnectOptions {
6246                /// Maximum consecutive reconnection attempts.
6247                pub max_retries: u32,
6248                /// Delay before the first reconnection when the server did not send `retry:`.
6249                pub initial_retry_delay: Duration,
6250                /// Upper bound for client-computed and server-supplied delays.
6251                pub max_retry_delay: Duration,
6252                /// Exponential backoff multiplier for consecutive failures.
6253                pub backoff_multiplier: f64,
6254            }
6255
6256            impl Default for SseReconnectOptions {
6257                fn default() -> Self {
6258                    Self {
6259                        max_retries: 3,
6260                        initial_retry_delay: Duration::from_secs(3),
6261                        max_retry_delay: Duration::from_secs(30),
6262                        backoff_multiplier: 2.0,
6263                    }
6264                }
6265            }
6266
6267            impl SseReconnectOptions {
6268                fn delay(&self, attempt: u32, server_retry: Option<Duration>) -> Duration {
6269                    if let Some(delay) = server_retry {
6270                        return delay.min(self.max_retry_delay);
6271                    }
6272                    let multiplier = self.backoff_multiplier.max(1.0);
6273                    let millis = self.initial_retry_delay.as_millis() as f64
6274                        * multiplier.powi(attempt.min(63) as i32);
6275                    Duration::from_millis(
6276                        millis.min(self.max_retry_delay.as_millis() as f64) as u64,
6277                    )
6278                }
6279            }
6280
6281            #[derive(Default)]
6282            struct __SseDecoder {
6283                line: Vec<u8>,
6284                event: String,
6285                data: Vec<String>,
6286                last_event_id: Option<String>,
6287                retry_delay: Option<Duration>,
6288                event_retry: Option<Duration>,
6289                saw_carriage_return: bool,
6290            }
6291
6292            impl __SseDecoder {
6293                fn feed(
6294                    &mut self,
6295                    chunk: &[u8],
6296                ) -> Vec<Result<SseEvent<String>, StreamingError>> {
6297                    let mut messages = Vec::new();
6298                    for &byte in chunk {
6299                        if self.saw_carriage_return {
6300                            self.saw_carriage_return = false;
6301                            if byte == b'\n' {
6302                                continue;
6303                            }
6304                        }
6305
6306                        match byte {
6307                            b'\n' => self.finish_line(&mut messages),
6308                            b'\r' => {
6309                                self.finish_line(&mut messages);
6310                                self.saw_carriage_return = true;
6311                            }
6312                            _ => self.line.push(byte),
6313                        }
6314                    }
6315                    messages
6316                }
6317
6318                fn finish(&mut self) -> Vec<Result<SseEvent<String>, StreamingError>> {
6319                    let mut messages = Vec::new();
6320                    if !self.line.is_empty() {
6321                        self.finish_line(&mut messages);
6322                    }
6323                    self.dispatch(&mut messages);
6324                    messages
6325                }
6326
6327                fn finish_line(
6328                    &mut self,
6329                    messages: &mut Vec<Result<SseEvent<String>, StreamingError>>,
6330                ) {
6331                    let line = std::mem::take(&mut self.line);
6332                    let line = match String::from_utf8(line) {
6333                        Ok(line) => line,
6334                        Err(error) => {
6335                            messages.push(Err(StreamingError::Parsing(format!(
6336                                "SSE line is not valid UTF-8: {}",
6337                                error
6338                            ))));
6339                            return;
6340                        }
6341                    };
6342
6343                    if line.is_empty() {
6344                        self.dispatch(messages);
6345                        return;
6346                    }
6347                    if line.starts_with(':') {
6348                        return;
6349                    }
6350
6351                    let (field, value) = line
6352                        .split_once(':')
6353                        .map_or((line.as_str(), ""), |(field, value)| {
6354                            (field, value.strip_prefix(' ').unwrap_or(value))
6355                        });
6356                    match field {
6357                        "event" => self.event = value.to_string(),
6358                        "data" => self.data.push(value.to_string()),
6359                        "id" if !value.contains('\0') => {
6360                            self.last_event_id = (!value.is_empty()).then(|| value.to_string());
6361                        }
6362                        "retry" if value.bytes().all(|byte| byte.is_ascii_digit()) => {
6363                            if let Ok(milliseconds) = value.parse::<u64>() {
6364                                let delay = Duration::from_millis(milliseconds);
6365                                self.retry_delay = Some(delay);
6366                                self.event_retry = Some(delay);
6367                            }
6368                        }
6369                        _ => {}
6370                    }
6371                }
6372
6373                fn dispatch(
6374                    &mut self,
6375                    messages: &mut Vec<Result<SseEvent<String>, StreamingError>>,
6376                ) {
6377                    if self.data.is_empty() {
6378                        self.event.clear();
6379                        self.event_retry = None;
6380                        return;
6381                    }
6382                    messages.push(Ok(SseEvent {
6383                        event: if self.event.is_empty() {
6384                            "message".to_string()
6385                        } else {
6386                            std::mem::take(&mut self.event)
6387                        },
6388                        data: std::mem::take(&mut self.data).join("\n"),
6389                        id: self.last_event_id.clone(),
6390                        retry: self.event_retry.take(),
6391                    }));
6392                    self.event.clear();
6393                }
6394
6395                fn reset_for_reconnect(&mut self) {
6396                    self.line.clear();
6397                    self.event.clear();
6398                    self.data.clear();
6399                    self.event_retry = None;
6400                    self.saw_carriage_return = false;
6401                }
6402            }
6403
6404            fn __deserialize_sse_event<T>(
6405                event: SseEvent<String>,
6406            ) -> Option<Result<SseEvent<T>, StreamingError>>
6407            where
6408                T: serde::de::DeserializeOwned,
6409            {
6410                if event.data.trim() == "[DONE]" {
6411                    return None;
6412                }
6413                if event.event == "ping" {
6414                    debug!("Received SSE ping event, skipping");
6415                    return None;
6416                }
6417                if event.data.trim().is_empty() {
6418                    debug!("Empty SSE data, skipping");
6419                    return None;
6420                }
6421
6422                let json_value = match serde_json::from_str::<serde_json::Value>(&event.data) {
6423                    Ok(value) => value,
6424                    Err(error) => {
6425                        return Some(Err(StreamingError::Parsing(format!(
6426                            "SSE event is not valid JSON: {} ({})",
6427                            event.data, error
6428                        ))));
6429                    }
6430                };
6431                let is_ping = json_value
6432                    .get("event")
6433                    .or_else(|| json_value.get("type"))
6434                    .and_then(serde_json::Value::as_str)
6435                    .is_some_and(|event| event == "ping");
6436                if is_ping {
6437                    debug!("Received ping event in JSON data, skipping");
6438                    return None;
6439                }
6440
6441                Some(
6442                    serde_json::from_value::<T>(json_value)
6443                        .map(|data| SseEvent {
6444                            event: event.event.clone(),
6445                            data,
6446                            id: event.id.clone(),
6447                            retry: event.retry,
6448                        })
6449                        .map_err(|error| StreamingError::Parsing(format!(
6450                            "Failed to parse SSE event: {} (raw: {}, event: {})",
6451                            error, event.data, event.event
6452                        ))),
6453                )
6454            }
6455
6456            /// Parse an SSE response without an external EventSource wrapper.
6457            pub async fn parse_sse_stream<T>(
6458                request_builder: reqwest::RequestBuilder
6459            ) -> Result<BoxSseStream<Result<T, StreamingError>>, StreamingError>
6460            where
6461                T: serde::de::DeserializeOwned + Send + 'static,
6462            {
6463                parse_sse_json_stream_with_limit(
6464                    request_builder,
6465                    DEFAULT_MAX_SSE_ERROR_BODY_BYTES,
6466                ).await
6467            }
6468
6469            struct __SseOpenError {
6470                error: StreamingError,
6471                retryable: bool,
6472            }
6473
6474            async fn __open_sse_response(
6475                request_builder: reqwest::RequestBuilder,
6476                max_response_body_bytes: usize,
6477            ) -> Result<reqwest::Response, __SseOpenError> {
6478                let response = request_builder.send().await.map_err(|error| __SseOpenError {
6479                    error: error.into(),
6480                    retryable: true,
6481                })?;
6482                if !response.status().is_success() {
6483                    let status = response.status();
6484                    let retryable = status.as_u16() == 429 || status.is_server_error();
6485                    let error = match __read_bounded_streaming_error_body(
6486                        response,
6487                        max_response_body_bytes,
6488                    ).await {
6489                        Ok(body) => StreamingError::Connection(format!(
6490                            "HTTP {} error: {}",
6491                            status.as_u16(),
6492                            String::from_utf8_lossy(&body)
6493                        )),
6494                        Err(error) => error,
6495                    };
6496                    return Err(__SseOpenError { error, retryable });
6497                }
6498
6499                let content_type = response
6500                    .headers()
6501                    .get(reqwest::header::CONTENT_TYPE)
6502                    .and_then(|value| value.to_str().ok())
6503                    .unwrap_or_default();
6504                if !content_type
6505                    .split(';')
6506                    .next()
6507                    .is_some_and(|value| value.trim().eq_ignore_ascii_case("text/event-stream"))
6508                {
6509                    let error = StreamingError::Parsing(format!(
6510                        "Expected text/event-stream response, received {}",
6511                        if content_type.is_empty() { "no Content-Type" } else { content_type }
6512                    ));
6513                    return Err(__SseOpenError { error, retryable: false });
6514                }
6515
6516                debug!("SSE connection opened");
6517                Ok(response)
6518            }
6519
6520            /// Parse an already validated HTTP response as SSE events.
6521            /// This preserves the request middleware of the calling HTTP client.
6522            pub fn parse_sse_response(
6523                response: reqwest::Response,
6524            ) -> BoxSseStream<Result<SseEvent<String>, StreamingError>> {
6525                __raw_response_stream(response)
6526            }
6527
6528            fn __raw_response_stream(
6529                response: reqwest::Response,
6530            ) -> BoxSseStream<Result<SseEvent<String>, StreamingError>> {
6531                let stream = futures_util::stream::unfold(
6532                    (
6533                        response.bytes_stream(),
6534                        __SseDecoder::default(),
6535                        std::collections::VecDeque::<Result<SseEvent<String>, StreamingError>>::new(),
6536                        false,
6537                    ),
6538                    |(mut chunks, mut decoder, mut pending, mut done)| async move {
6539                        loop {
6540                            if let Some(item) = pending.pop_front() {
6541                                return Some((item, (chunks, decoder, pending, done)));
6542                            }
6543                            if done {
6544                                debug!("SSE stream completed normally");
6545                                return None;
6546                            }
6547
6548                            match chunks.next().await {
6549                                Some(Ok(chunk)) => {
6550                                    for event in decoder.feed(&chunk) {
6551                                        let is_done = event
6552                                            .as_ref()
6553                                            .is_ok_and(|event| event.data.trim() == "[DONE]");
6554                                        pending.push_back(event);
6555                                        if is_done {
6556                                            done = true;
6557                                            break;
6558                                        }
6559                                    }
6560                                }
6561                                Some(Err(error)) => {
6562                                    done = true;
6563                                    pending.push_back(Err(error.into()));
6564                                }
6565                                None => {
6566                                    done = true;
6567                                    for event in decoder.finish() {
6568                                        pending.push_back(event);
6569                                    }
6570                                }
6571                            }
6572                        }
6573                    }
6574                );
6575
6576                Box::pin(stream)
6577            }
6578
6579            async fn parse_sse_raw_stream_with_limit(
6580                request_builder: reqwest::RequestBuilder,
6581                max_response_body_bytes: usize,
6582            ) -> Result<BoxSseStream<Result<SseEvent<String>, StreamingError>>, StreamingError> {
6583                Ok(match __open_sse_response(request_builder, max_response_body_bytes).await {
6584                    Ok(response) => __raw_response_stream(response),
6585                    Err(error) => Box::pin(futures_util::stream::once(async move { Err(error.error) })),
6586                })
6587            }
6588
6589            fn __json_event_stream<T>(
6590                raw: BoxSseStream<Result<SseEvent<String>, StreamingError>>,
6591            ) -> BoxSseStream<Result<SseEvent<T>, StreamingError>>
6592            where
6593                T: serde::de::DeserializeOwned + Send + 'static,
6594            {
6595                Box::pin(raw.filter_map(|event| async move {
6596                    match event {
6597                        Ok(event) => __deserialize_sse_event(event),
6598                        Err(error) => Some(Err(error)),
6599                    }
6600                }))
6601            }
6602
6603            async fn parse_sse_json_events_with_limit<T>(
6604                request_builder: reqwest::RequestBuilder,
6605                max_response_body_bytes: usize,
6606            ) -> Result<BoxSseStream<Result<SseEvent<T>, StreamingError>>, StreamingError>
6607            where
6608                T: serde::de::DeserializeOwned + Send + 'static,
6609            {
6610                Ok(__json_event_stream(
6611                    parse_sse_raw_stream_with_limit(request_builder, max_response_body_bytes).await?,
6612                ))
6613            }
6614
6615            async fn parse_sse_json_stream_with_limit<T>(
6616                request_builder: reqwest::RequestBuilder,
6617                max_response_body_bytes: usize,
6618            ) -> Result<BoxSseStream<Result<T, StreamingError>>, StreamingError>
6619            where
6620                T: serde::de::DeserializeOwned + Send + 'static,
6621            {
6622                let events = parse_sse_json_events_with_limit(request_builder, max_response_body_bytes).await?;
6623                Ok(Box::pin(events.map(|event| event.map(|event| event.data))))
6624            }
6625
6626            struct __ReconnectState {
6627                request: reqwest::RequestBuilder,
6628                body: Option<BoxSseStream<Result<bytes::Bytes, reqwest::Error>>>,
6629                decoder: __SseDecoder,
6630                pending: std::collections::VecDeque<Result<SseEvent<String>, StreamingError>>,
6631                options: SseReconnectOptions,
6632                max_response_body_bytes: usize,
6633                attempts: u32,
6634                wait_before_open: bool,
6635                done: bool,
6636            }
6637
6638            async fn parse_sse_raw_reconnecting_with_limit(
6639                request_builder: reqwest::RequestBuilder,
6640                max_response_body_bytes: usize,
6641                options: SseReconnectOptions,
6642            ) -> Result<BoxSseStream<Result<SseEvent<String>, StreamingError>>, StreamingError> {
6643                if request_builder.try_clone().is_none() {
6644                    return Err(StreamingError::Connection(
6645                        "SSE reconnection requires a cloneable request body".to_string(),
6646                    ));
6647                }
6648
6649                let stream = futures_util::stream::unfold(
6650                    __ReconnectState {
6651                        request: request_builder,
6652                        body: None,
6653                        decoder: __SseDecoder::default(),
6654                        pending: std::collections::VecDeque::new(),
6655                        options,
6656                        max_response_body_bytes,
6657                        attempts: 0,
6658                        wait_before_open: false,
6659                        done: false,
6660                    },
6661                    |mut state| async move {
6662                        loop {
6663                            if let Some(item) = state.pending.pop_front() {
6664                                return Some((item, state));
6665                            }
6666                            if state.done {
6667                                return None;
6668                            }
6669
6670                            if state.body.is_none() {
6671                                if state.wait_before_open {
6672                                    let delay = state.options.delay(
6673                                        state.attempts.saturating_sub(1),
6674                                        state.decoder.retry_delay,
6675                                    );
6676                                    debug!(?delay, attempt = state.attempts, "Reconnecting SSE stream");
6677                                    futures_timer::Delay::new(delay).await;
6678                                    state.wait_before_open = false;
6679                                }
6680
6681                                let mut request = state.request.try_clone().expect("request clone checked");
6682                                if let Some(last_event_id) = state.decoder.last_event_id.as_deref() {
6683                                    request = request.header("Last-Event-ID", last_event_id);
6684                                }
6685                                match __open_sse_response(request, state.max_response_body_bytes).await {
6686                                    Ok(response) => state.body = Some(Box::pin(response.bytes_stream())),
6687                                    Err(error) if error.retryable && state.attempts < state.options.max_retries => {
6688                                        state.attempts += 1;
6689                                        state.wait_before_open = true;
6690                                        continue;
6691                                    }
6692                                    Err(error) => {
6693                                        state.done = true;
6694                                        state.pending.push_back(Err(error.error));
6695                                        continue;
6696                                    }
6697                                }
6698                            }
6699
6700                            let next = state.body.as_mut().expect("response opened").next().await;
6701                            match next {
6702                                Some(Ok(chunk)) => {
6703                                    let events = state.decoder.feed(&chunk);
6704                                    if !events.is_empty() {
6705                                        state.attempts = 0;
6706                                    }
6707                                    for event in events {
6708                                        let is_done = event
6709                                            .as_ref()
6710                                            .is_ok_and(|event| event.data.trim() == "[DONE]");
6711                                        state.pending.push_back(event);
6712                                        if is_done {
6713                                            state.done = true;
6714                                            state.body = None;
6715                                            break;
6716                                        }
6717                                    }
6718                                }
6719                                None => {
6720                                    let events = state.decoder.finish();
6721                                    if !events.is_empty() {
6722                                        state.attempts = 0;
6723                                    }
6724                                    for event in events {
6725                                        let is_done = event
6726                                            .as_ref()
6727                                            .is_ok_and(|event| event.data.trim() == "[DONE]");
6728                                        state.pending.push_back(event);
6729                                        if is_done {
6730                                            state.done = true;
6731                                            break;
6732                                        }
6733                                    }
6734                                    state.body = None;
6735                                    state.decoder.reset_for_reconnect();
6736                                    if !state.done {
6737                                        if state.attempts < state.options.max_retries {
6738                                            state.attempts += 1;
6739                                            state.wait_before_open = true;
6740                                        } else {
6741                                            state.done = true;
6742                                        }
6743                                    }
6744                                }
6745                                Some(Err(error)) => {
6746                                    state.body = None;
6747                                    state.decoder.reset_for_reconnect();
6748                                    if state.attempts < state.options.max_retries {
6749                                        state.attempts += 1;
6750                                        state.wait_before_open = true;
6751                                    } else {
6752                                        state.done = true;
6753                                        state.pending.push_back(Err(error.into()));
6754                                    }
6755                                }
6756                            }
6757                        }
6758                    },
6759                );
6760                Ok(Box::pin(stream))
6761            }
6762
6763            async fn parse_sse_json_reconnecting_events_with_limit<T>(
6764                request_builder: reqwest::RequestBuilder,
6765                max_response_body_bytes: usize,
6766                options: SseReconnectOptions,
6767            ) -> Result<BoxSseStream<Result<SseEvent<T>, StreamingError>>, StreamingError>
6768            where
6769                T: serde::de::DeserializeOwned + Send + 'static,
6770            {
6771                Ok(__json_event_stream(
6772                    parse_sse_raw_reconnecting_with_limit(
6773                        request_builder,
6774                        max_response_body_bytes,
6775                        options,
6776                    ).await?,
6777                ))
6778            }
6779
6780            async fn parse_sse_json_reconnecting_with_limit<T>(
6781                request_builder: reqwest::RequestBuilder,
6782                max_response_body_bytes: usize,
6783                options: SseReconnectOptions,
6784            ) -> Result<BoxSseStream<Result<T, StreamingError>>, StreamingError>
6785            where
6786                T: serde::de::DeserializeOwned + Send + 'static,
6787            {
6788                let events = parse_sse_json_reconnecting_events_with_limit(
6789                    request_builder,
6790                    max_response_body_bytes,
6791                    options,
6792                ).await?;
6793                Ok(Box::pin(events.map(|event| event.map(|event| event.data))))
6794            }
6795        })
6796    }
6797
6798    /// Generate reconnection utilities
6799    fn generate_reconnection_utilities(
6800        &self,
6801        reconnect_config: &crate::streaming::ReconnectionConfig,
6802    ) -> Result<TokenStream> {
6803        let max_retries = reconnect_config.max_retries;
6804        let initial_delay = reconnect_config.initial_delay_ms;
6805        let max_delay = reconnect_config.max_delay_ms;
6806        let backoff_multiplier = reconnect_config.backoff_multiplier;
6807
6808        Ok(quote! {
6809            /// Reconnection configuration and utilities
6810            #[derive(Debug, Clone)]
6811            pub struct ReconnectionManager {
6812                max_retries: u32,
6813                initial_delay_ms: u64,
6814                max_delay_ms: u64,
6815                backoff_multiplier: f64,
6816                current_attempt: u32,
6817            }
6818
6819            impl ReconnectionManager {
6820                /// Create a new reconnection manager
6821                pub fn new() -> Self {
6822                    Self {
6823                        max_retries: #max_retries,
6824                        initial_delay_ms: #initial_delay,
6825                        max_delay_ms: #max_delay,
6826                        backoff_multiplier: #backoff_multiplier,
6827                        current_attempt: 0,
6828                    }
6829                }
6830
6831                /// Check if we should retry the connection
6832                pub fn should_retry(&self) -> bool {
6833                    self.current_attempt < self.max_retries
6834                }
6835
6836                /// Get the delay for the next retry attempt
6837                pub fn next_retry_delay(&mut self) -> Duration {
6838                    if !self.should_retry() {
6839                        return Duration::from_secs(0);
6840                    }
6841
6842                    let delay_ms = (self.initial_delay_ms as f64
6843                        * self.backoff_multiplier.powi(self.current_attempt as i32)) as u64;
6844                    let delay_ms = delay_ms.min(self.max_delay_ms);
6845
6846                    self.current_attempt += 1;
6847                    Duration::from_millis(delay_ms)
6848                }
6849
6850                /// Reset the retry counter after a successful connection
6851                pub fn reset(&mut self) {
6852                    self.current_attempt = 0;
6853                }
6854
6855                /// Get the current attempt number
6856                pub fn current_attempt(&self) -> u32 {
6857                    self.current_attempt
6858                }
6859            }
6860
6861            impl Default for ReconnectionManager {
6862                fn default() -> Self {
6863                    Self::new()
6864                }
6865            }
6866        })
6867    }
6868}