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