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::{SseClient, SseReconnectOptions};
1118                    pub use super::sse::StreamingError;
1119                });
1120            } else {
1121                client_code.extend(quote! {
1122                    use super::sse::SseClient;
1123                    pub use super::sse::StreamingError;
1124                });
1125            }
1126        }
1127
1128        // 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::Forbidden => {}
2300            crate::analysis::ObjectAdditionalProperties::Untyped => {
2301                let serde_flatten = if variant.is_none() {
2302                    quote! { #[serde(flatten)] }
2303                } else {
2304                    TokenStream::new()
2305                };
2306                fields.push(quote! {
2307                    /// Additional properties not explicitly defined in the schema
2308                    #serde_flatten
2309                    pub additional_properties:
2310                        std::collections::BTreeMap<String, serde_json::Value>,
2311                });
2312            }
2313            crate::analysis::ObjectAdditionalProperties::Typed { value_type } => {
2314                let value_tokens = self.generate_array_item_type(value_type, analysis);
2315                let serde_flatten = if variant.is_none() {
2316                    quote! { #[serde(flatten)] }
2317                } else {
2318                    TokenStream::new()
2319                };
2320                fields.push(quote! {
2321                    /// Additional properties matching the spec's
2322                    /// `additionalProperties` value schema.
2323                    #serde_flatten
2324                    pub additional_properties:
2325                        std::collections::BTreeMap<String, #value_tokens>,
2326                });
2327            }
2328        }
2329
2330        // A schema that declares `properties` *and* a union means "these
2331        // fields, and one of these shapes". The union rides in a flattened
2332        // field so both halves round-trip: serde reads the declared properties
2333        // and hands the remaining keys to the variant enum.
2334        if let (Some(variant), Some(variant_field)) = (variant, variant_field_ident.as_ref()) {
2335            let variant_type = format_ident!("{}", self.to_rust_type_name(&variant.target));
2336            fields.push(quote! {
2337                /// The variant this value takes, alongside the fields above.
2338                pub #variant_field: #variant_type,
2339            });
2340        }
2341
2342        let doc_comment = if let Some(desc) = &schema.description {
2343            quote! { #[doc = #desc] }
2344        } else {
2345            TokenStream::new()
2346        };
2347
2348        // Default is safe only when the schema requires no wire property.
2349        // Optional fields are represented as Option<T>, and the generated
2350        // additional-properties map (when present) is empty by default. We do
2351        // not invent values for required data, even when the Rust type itself
2352        // happens to implement Default.
2353        // A flattened variant is one of several shapes, and picking one would
2354        // invent data the same way a required field would.
2355        let can_derive_default = variant.is_none() && required.is_empty();
2356
2357        // Generate derives with optional Specta support
2358        // Note: We use snake_case everywhere (matching the OpenAPI spec) for consistency
2359        // between Rust, JSON API, and TypeScript
2360        let derives = match (
2361            self.config.enable_specta,
2362            can_derive_default,
2363            variant.is_some(),
2364        ) {
2365            (true, _, true) => quote! {
2366                #[derive(Debug, Clone)]
2367                #[cfg_attr(feature = "specta", derive(specta::Type))]
2368            },
2369            (false, _, true) => quote! {
2370                #[derive(Debug, Clone)]
2371            },
2372            (true, true, false) => quote! {
2373                #[derive(Debug, Clone, Deserialize, Serialize, Default)]
2374                #[cfg_attr(feature = "specta", derive(specta::Type))]
2375            },
2376            (true, false, false) => quote! {
2377                #[derive(Debug, Clone, Deserialize, Serialize)]
2378                #[cfg_attr(feature = "specta", derive(specta::Type))]
2379            },
2380            (false, true, false) => quote! {
2381                #[derive(Debug, Clone, Deserialize, Serialize, Default)]
2382            },
2383            (false, false, false) => quote! {
2384                #[derive(Debug, Clone, Deserialize, Serialize)]
2385            },
2386        };
2387
2388        // `#[serde(flatten)]` removes keys already consumed by sibling fields
2389        // before it invokes the flattened value's deserializer. That is wrong
2390        // for a sibling-property + oneOf schema when both halves intentionally
2391        // share the discriminator. Decode both halves from the complete JSON
2392        // object, and merge their serialized maps with duplicate-value checks.
2393        let shared_variant_serde = if let (Some(variant), Some(variant_field)) =
2394            (variant, variant_field_ident.as_ref())
2395        {
2396            let variant_type = format_ident!("{}", self.to_rust_type_name(&variant.target));
2397            let helper_name = format_ident!("__{}Base", self.to_rust_type_name(&schema.name));
2398            let variant_properties = self.union_declared_properties(
2399                &variant.target,
2400                analysis,
2401                &mut std::collections::HashSet::new(),
2402            );
2403            let variant_projection_removals = emitted_properties
2404                .iter()
2405                .filter(|emitted| !variant_properties.contains(emitted.wire_name))
2406                .map(|emitted| {
2407                    let wire_name = emitted.wire_name;
2408                    quote! { object.remove(#wire_name); }
2409                })
2410                .collect::<Vec<_>>();
2411            let mut helper_fields: Vec<TokenStream> = emitted_properties
2412                .iter()
2413                .map(|emitted| {
2414                    let field_name = emitted.wire_name;
2415                    let property = emitted.property;
2416                    let field_ident = &emitted.ident;
2417                    let field_type = &emitted.field_type;
2418                    let serde_attrs = self.generate_serde_field_attrs(
2419                        &schema.name,
2420                        field_name,
2421                        field_ident,
2422                        property,
2423                        emitted.is_required,
2424                        analysis,
2425                    );
2426                    quote! {
2427                        #serde_attrs
2428                        #field_ident: #field_type,
2429                    }
2430                })
2431                .collect();
2432            let mut base_initializers: Vec<TokenStream> = emitted_properties
2433                .iter()
2434                .map(|emitted| {
2435                    let field_ident = &emitted.ident;
2436                    quote! { #field_ident: self.#field_ident.clone(), }
2437                })
2438                .collect();
2439            let mut result_fields: Vec<TokenStream> = emitted_properties
2440                .iter()
2441                .map(|emitted| {
2442                    let field_ident = &emitted.ident;
2443                    quote! { #field_ident: base.#field_ident, }
2444                })
2445                .collect();
2446
2447            match additional_properties {
2448                crate::analysis::ObjectAdditionalProperties::Forbidden => {}
2449                crate::analysis::ObjectAdditionalProperties::Untyped => {
2450                    helper_fields.push(quote! {
2451                        #[serde(flatten)]
2452                        additional_properties:
2453                            std::collections::BTreeMap<String, serde_json::Value>,
2454                    });
2455                    base_initializers.push(quote! {
2456                        additional_properties: self.additional_properties.clone(),
2457                    });
2458                    result_fields.push(quote! {
2459                        additional_properties: base.additional_properties,
2460                    });
2461                }
2462                crate::analysis::ObjectAdditionalProperties::Typed { value_type } => {
2463                    let value_tokens = self.generate_array_item_type(value_type, analysis);
2464                    helper_fields.push(quote! {
2465                        #[serde(flatten)]
2466                        additional_properties:
2467                            std::collections::BTreeMap<String, #value_tokens>,
2468                    });
2469                    base_initializers.push(quote! {
2470                        additional_properties: self.additional_properties.clone(),
2471                    });
2472                    result_fields.push(quote! {
2473                        additional_properties: base.additional_properties,
2474                    });
2475                }
2476            }
2477
2478            quote! {
2479                #[derive(Deserialize, Serialize)]
2480                struct #helper_name {
2481                    #(#helper_fields)*
2482                }
2483
2484                impl serde::Serialize for #struct_name {
2485                    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2486                    where
2487                        S: serde::Serializer,
2488                    {
2489                        let base = #helper_name {
2490                            #(#base_initializers)*
2491                        };
2492                        let mut value = serde_json::to_value(base)
2493                            .map_err(serde::ser::Error::custom)?;
2494                        let variant = serde_json::to_value(&self.#variant_field)
2495                            .map_err(serde::ser::Error::custom)?;
2496                        let object = value.as_object_mut().ok_or_else(|| {
2497                            serde::ser::Error::custom("shared union base did not serialize as an object")
2498                        })?;
2499                        let variant_object = variant.as_object().ok_or_else(|| {
2500                            serde::ser::Error::custom("shared union variant did not serialize as an object")
2501                        })?;
2502                        for (key, variant_value) in variant_object {
2503                            if let Some(base_value) = object.get(key)
2504                                && base_value != variant_value
2505                            {
2506                                return Err(serde::ser::Error::custom(format!(
2507                                    "shared union field `{key}` serialized conflicting values",
2508                                )));
2509                            }
2510                            object.insert(key.clone(), variant_value.clone());
2511                        }
2512                        serde::Serialize::serialize(&value, serializer)
2513                    }
2514                }
2515
2516                impl<'de> serde::Deserialize<'de> for #struct_name {
2517                    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2518                    where
2519                        D: serde::Deserializer<'de>,
2520                    {
2521                        let value = serde_json::Value::deserialize(deserializer)?;
2522                        let base = serde_json::from_value::<#helper_name>(value.clone())
2523                            .map_err(serde::de::Error::custom)?;
2524                        let variant = match serde_json::from_value::<#variant_type>(value.clone()) {
2525                            Ok(variant) => variant,
2526                            Err(complete_error) => {
2527                                let mut variant_input = value;
2528                                if let Some(object) = variant_input.as_object_mut() {
2529                                    #(#variant_projection_removals)*
2530                                }
2531                                serde_json::from_value::<#variant_type>(variant_input).map_err(
2532                                    |projected_error| serde::de::Error::custom(format!(
2533                                        "complete shared-union input failed: {complete_error}; projected input failed: {projected_error}",
2534                                    )),
2535                                )?
2536                            }
2537                        };
2538                        Ok(Self {
2539                            #(#result_fields)*
2540                            #variant_field: variant,
2541                        })
2542                    }
2543                }
2544            }
2545        } else {
2546            TokenStream::new()
2547        };
2548
2549        let builder = if type_context.index.request_body_roots.contains(&schema.name)
2550            && variant.is_none()
2551            && emitted_properties
2552                .iter()
2553                .any(|property| property.is_required)
2554            && (emitted_properties
2555                .iter()
2556                .any(|property| !property.is_required)
2557                || !matches!(
2558                    additional_properties,
2559                    crate::analysis::ObjectAdditionalProperties::Forbidden
2560                )) {
2561            self.generate_request_model_builder(
2562                schema,
2563                &emitted_properties,
2564                additional_properties,
2565                analysis,
2566                type_context.index,
2567            )
2568        } else {
2569            TokenStream::new()
2570        };
2571
2572        Ok(quote! {
2573            #doc_comment
2574            #derives
2575            pub struct #struct_name {
2576                #(#fields)*
2577            }
2578
2579            #shared_variant_serde
2580            #builder
2581        })
2582    }
2583
2584    /// Project an object schema into the exact public fields emitted in
2585    /// `types.rs`. Request-model and operation builders share this metadata so
2586    /// identifier disambiguation, discriminator filtering, and Option wrapping
2587    /// cannot drift.
2588    pub(crate) fn emitted_object_properties<'a>(
2589        &self,
2590        schema_name: &str,
2591        properties: &'a BTreeMap<String, crate::analysis::PropertyInfo>,
2592        required: &std::collections::HashSet<String>,
2593        additional_properties: &crate::analysis::ObjectAdditionalProperties,
2594        analysis: &crate::analysis::SchemaAnalysis,
2595    ) -> Vec<EmittedObjectProperty<'a>> {
2596        let mut sorted_properties: Vec<_> = properties.iter().collect();
2597        sorted_properties.sort_by_key(|(name, _)| name.as_str());
2598
2599        let mut used_field_idents = std::collections::HashSet::new();
2600        if !matches!(
2601            additional_properties,
2602            crate::analysis::ObjectAdditionalProperties::Forbidden
2603        ) {
2604            used_field_idents.insert("additional_properties".to_string());
2605        }
2606
2607        let mut emitted = Vec::new();
2608        for (field_name, property) in sorted_properties {
2609            let raw = self.to_rust_field_name(field_name);
2610            let mut chosen = raw.clone();
2611            let mut suffix = 2;
2612            while !used_field_idents.insert(chosen.clone()) {
2613                chosen = format!("{raw}_{suffix}");
2614                suffix += 1;
2615            }
2616            let is_required = required.contains(field_name);
2617            emitted.push(EmittedObjectProperty {
2618                wire_name: field_name,
2619                property,
2620                ident: Self::to_field_ident(&chosen),
2621                is_required,
2622                field_type: self.generate_field_type(
2623                    schema_name,
2624                    field_name,
2625                    property,
2626                    is_required,
2627                    analysis,
2628                ),
2629            });
2630        }
2631        emitted
2632    }
2633
2634    fn type_generation_index(
2635        &self,
2636        analysis: &crate::analysis::SchemaAnalysis,
2637    ) -> TypeGenerationIndex {
2638        let reserved_type_names = analysis
2639            .schemas
2640            .keys()
2641            .map(|name| self.to_rust_type_name(name))
2642            .collect();
2643        let mut request_body_roots = std::collections::HashSet::new();
2644        for operation in analysis.operations.values() {
2645            let Some(mut current) = operation
2646                .request_body
2647                .as_ref()
2648                .and_then(crate::analysis::RequestBodyContent::schema_name)
2649            else {
2650                continue;
2651            };
2652            while request_body_roots.insert(current.to_string()) {
2653                let Some(crate::analysis::AnalyzedSchema {
2654                    schema_type: crate::analysis::SchemaType::Reference { target },
2655                    ..
2656                }) = analysis.schemas.get(current)
2657                else {
2658                    break;
2659                };
2660                current = target;
2661            }
2662        }
2663        TypeGenerationIndex {
2664            request_body_roots,
2665            reserved_type_names,
2666        }
2667    }
2668
2669    fn generate_request_model_builder(
2670        &self,
2671        schema: &crate::analysis::AnalyzedSchema,
2672        properties: &[EmittedObjectProperty<'_>],
2673        additional_properties: &crate::analysis::ObjectAdditionalProperties,
2674        analysis: &crate::analysis::SchemaAnalysis,
2675        type_index: &TypeGenerationIndex,
2676    ) -> TokenStream {
2677        let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2678        let builder_base = format!("{}Builder", struct_name);
2679        let mut builder_name = builder_base.clone();
2680        let mut suffix = 2;
2681        while type_index.reserved_type_names.contains(&builder_name) {
2682            builder_name = format!("{builder_base}{suffix}");
2683            suffix += 1;
2684        }
2685        let builder_name = format_ident!("{builder_name}");
2686
2687        let required_parameters: Vec<TokenStream> = properties
2688            .iter()
2689            .filter(|property| property.is_required)
2690            .map(|property| {
2691                let ident = &property.ident;
2692                let field_type = &property.field_type;
2693                quote! { #ident: #field_type }
2694            })
2695            .collect();
2696        let required_idents: Vec<&syn::Ident> = properties
2697            .iter()
2698            .filter(|property| property.is_required)
2699            .map(|property| &property.ident)
2700            .collect();
2701        let optional_initializers: Vec<TokenStream> = properties
2702            .iter()
2703            .filter(|property| !property.is_required)
2704            .map(|property| {
2705                let ident = &property.ident;
2706                quote! { #ident: None }
2707            })
2708            .collect();
2709
2710        let additional_initializer = match additional_properties {
2711            crate::analysis::ObjectAdditionalProperties::Forbidden => TokenStream::new(),
2712            crate::analysis::ObjectAdditionalProperties::Untyped
2713            | crate::analysis::ObjectAdditionalProperties::Typed { .. } => quote! {
2714                additional_properties: ::std::collections::BTreeMap::new(),
2715            },
2716        };
2717
2718        let mut used_builder_methods =
2719            std::collections::HashSet::from(["new".to_string(), "build".to_string()]);
2720        if !matches!(
2721            additional_properties,
2722            crate::analysis::ObjectAdditionalProperties::Forbidden
2723        ) {
2724            used_builder_methods.insert("additional_properties".to_string());
2725        }
2726        let optional_setters: Vec<TokenStream> = properties
2727            .iter()
2728            .filter(|property| !property.is_required)
2729            .map(|property| {
2730                let field_ident = &property.ident;
2731                let field_type = self.generate_property_base_type(
2732                    &schema.name,
2733                    property.wire_name,
2734                    property.property,
2735                    analysis,
2736                );
2737                // Allocate every setter in the builder's method namespace.
2738                // `new` and `build` keep their documented `with_` escape;
2739                // further collisions receive deterministic numeric suffixes.
2740                let field_name = field_ident.to_string();
2741                let plain_field_name = field_name.strip_prefix("r#").unwrap_or(&field_name);
2742                let mut setter_name = if matches!(plain_field_name, "new" | "build") {
2743                    format!("with_{plain_field_name}")
2744                } else {
2745                    field_name.clone()
2746                };
2747                let setter_base = setter_name.clone();
2748                let mut suffix = 2;
2749                while !used_builder_methods.insert(setter_name.clone()) {
2750                    setter_name = format!("{setter_base}_{suffix}");
2751                    suffix += 1;
2752                }
2753                let setter_ident = Self::to_field_ident(&setter_name);
2754                let wire_name = property.wire_name;
2755                if self.property_is_tri_state(
2756                    &schema.name,
2757                    property.wire_name,
2758                    property.property,
2759                    property.is_required,
2760                ) {
2761                    let mut null_name = format!("{plain_field_name}_null");
2762                    let null_base = null_name.clone();
2763                    let mut suffix = 2;
2764                    while !used_builder_methods.insert(null_name.clone()) {
2765                        null_name = format!("{null_base}_{suffix}");
2766                        suffix += 1;
2767                    }
2768                    let null_ident = Self::to_field_ident(&null_name);
2769
2770                    let mut absent_name = format!("{plain_field_name}_absent");
2771                    let absent_base = absent_name.clone();
2772                    let mut suffix = 2;
2773                    while !used_builder_methods.insert(absent_name.clone()) {
2774                        absent_name = format!("{absent_base}_{suffix}");
2775                        suffix += 1;
2776                    }
2777                    let absent_ident = Self::to_field_ident(&absent_name);
2778
2779                    quote! {
2780                        #[doc = concat!("Set the optional nullable `", #wire_name, "` request field to a value.")]
2781                        #[must_use]
2782                        pub fn #setter_ident(mut self, #field_ident: #field_type) -> Self {
2783                            self.value.#field_ident = Some(Some(#field_ident));
2784                            self
2785                        }
2786
2787                        #[doc = concat!("Set the optional nullable `", #wire_name, "` request field to JSON null.")]
2788                        #[must_use]
2789                        pub fn #null_ident(mut self) -> Self {
2790                            self.value.#field_ident = Some(None);
2791                            self
2792                        }
2793
2794                        #[doc = concat!("Omit the optional nullable `", #wire_name, "` request field.")]
2795                        #[must_use]
2796                        pub fn #absent_ident(mut self) -> Self {
2797                            self.value.#field_ident = None;
2798                            self
2799                        }
2800                    }
2801                } else {
2802                    quote! {
2803                        #[doc = concat!("Set the optional `", #wire_name, "` request field.")]
2804                        #[must_use]
2805                        pub fn #setter_ident(mut self, #field_ident: #field_type) -> Self {
2806                            self.value.#field_ident = Some(#field_ident);
2807                            self
2808                        }
2809                    }
2810                }
2811            })
2812            .collect();
2813
2814        let additional_setter = match additional_properties {
2815            crate::analysis::ObjectAdditionalProperties::Forbidden => TokenStream::new(),
2816            crate::analysis::ObjectAdditionalProperties::Untyped => quote! {
2817                /// Replace the request's additional properties.
2818                #[must_use]
2819                pub fn additional_properties(
2820                    mut self,
2821                    additional_properties: ::std::collections::BTreeMap<
2822                        String,
2823                        serde_json::Value,
2824                    >,
2825                ) -> Self {
2826                    self.value.additional_properties = additional_properties;
2827                    self
2828                }
2829            },
2830            crate::analysis::ObjectAdditionalProperties::Typed { value_type } => {
2831                let value_type = self.generate_array_item_type(value_type, analysis);
2832                quote! {
2833                    /// Replace the request's additional properties.
2834                    #[must_use]
2835                    pub fn additional_properties(
2836                        mut self,
2837                        additional_properties: ::std::collections::BTreeMap<
2838                            String,
2839                            #value_type,
2840                        >,
2841                    ) -> Self {
2842                        self.value.additional_properties = additional_properties;
2843                        self
2844                    }
2845                }
2846            }
2847        };
2848
2849        quote! {
2850            impl #struct_name {
2851                /// Construct this request with every required wire field.
2852                pub fn new(#(#required_parameters),*) -> Self {
2853                    Self {
2854                        #(#required_idents,)*
2855                        #(#optional_initializers,)*
2856                        #additional_initializer
2857                    }
2858                }
2859
2860                /// Start a dependency-free builder with every required wire field.
2861                pub fn builder(#(#required_parameters),*) -> #builder_name {
2862                    #builder_name::new(#(#required_idents),*)
2863                }
2864            }
2865
2866            /// Dependency-free builder for [`#struct_name`].
2867            #[derive(Debug, Clone)]
2868            #[must_use]
2869            pub struct #builder_name {
2870                value: #struct_name,
2871            }
2872
2873            impl #builder_name {
2874                /// Start a builder with every required wire field.
2875                pub fn new(#(#required_parameters),*) -> Self {
2876                    Self {
2877                        value: #struct_name::new(#(#required_idents),*),
2878                    }
2879                }
2880
2881                #(#optional_setters)*
2882                #additional_setter
2883
2884                /// Finish building the request model.
2885                pub fn build(self) -> #struct_name {
2886                    self.value
2887                }
2888            }
2889        }
2890    }
2891
2892    fn generate_discriminated_enum(
2893        &self,
2894        schema: &crate::analysis::AnalyzedSchema,
2895        discriminator_field: &str,
2896        variants: &[crate::analysis::UnionVariant],
2897        exclusive: bool,
2898        analysis: &crate::analysis::SchemaAnalysis,
2899    ) -> Result<TokenStream> {
2900        let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2901
2902        // Check if any variant references another discriminated union
2903        let has_nested_discriminated_union = variants.iter().any(|variant| {
2904            if let Some(variant_schema) = analysis.schemas.get(&variant.type_name) {
2905                matches!(
2906                    variant_schema.schema_type,
2907                    crate::analysis::SchemaType::DiscriminatedUnion { .. }
2908                )
2909            } else {
2910                false
2911            }
2912        });
2913
2914        // If we have a nested discriminated union, make this enum untagged
2915        if has_nested_discriminated_union {
2916            // Generate as untagged union
2917            let schema_refs: Vec<crate::analysis::SchemaRef> = variants
2918                .iter()
2919                .map(|v| crate::analysis::SchemaRef {
2920                    target: v.type_name.clone(),
2921                    nullable: false,
2922                })
2923                .collect();
2924            return self.generate_union_enum(schema, &schema_refs, exclusive, analysis);
2925        }
2926
2927        let enclosing = self.to_rust_type_name(&schema.name);
2928        let variant_shapes: Vec<_> = variants
2929            .iter()
2930            .map(|variant| {
2931                let variant_name = format_ident!("{}", variant.rust_name);
2932                let variant_type = format_ident!("{}", self.to_rust_type_name(&variant.type_name));
2933                // Box variant payloads that point at the enclosing enum or any
2934                // schema in the analysis's recursive set, otherwise the enum has
2935                // infinite size (E0072).
2936                let payload = if self.to_rust_type_name(&variant.type_name) == enclosing
2937                    || analysis
2938                        .dependencies
2939                        .recursive_schemas
2940                        .contains(&variant.type_name)
2941                {
2942                    quote! { Box<#variant_type> }
2943                } else {
2944                    quote! { #variant_type }
2945                };
2946                (variant, variant_name, payload)
2947            })
2948            .collect();
2949        let enum_variants = variant_shapes.iter().map(|(_, variant_name, payload)| {
2950            quote! { #variant_name(#payload), }
2951        });
2952        let serialize_arms = variant_shapes.iter().map(|(variant, variant_name, _)| {
2953            let canonical_value = &variant.discriminator_value;
2954            let variant_values = &variant.discriminator_values;
2955            let serialize_discriminator = if variant.discriminator_field_declared {
2956                quote! {
2957                    match object.get(#discriminator_field) {
2958                        Some(serde_json::Value::String(tag))
2959                            if matches!(tag.as_str(), #(#variant_values)|*) => {}
2960                        Some(serde_json::Value::String(tag)) => {
2961                            return Err(serde::ser::Error::custom(format!(
2962                                "discriminator `{}` value `{tag}` is not valid for variant `{}`",
2963                                #discriminator_field,
2964                                stringify!(#variant_name),
2965                            )));
2966                        }
2967                        Some(_) => {
2968                            return Err(serde::ser::Error::custom(concat!(
2969                                "discriminator `",
2970                                #discriminator_field,
2971                                "` did not serialize as a string",
2972                            )));
2973                        }
2974                        None => {
2975                            object.insert(
2976                                #discriminator_field.to_string(),
2977                                serde_json::Value::String(#canonical_value.to_string()),
2978                            );
2979                        }
2980                    }
2981                }
2982            } else {
2983                TokenStream::new()
2984            };
2985            quote! {
2986                Self::#variant_name(payload) => {
2987                    let mut value = serde_json::to_value(payload)
2988                        .map_err(serde::ser::Error::custom)?;
2989                    let object = value.as_object_mut().ok_or_else(|| {
2990                        serde::ser::Error::custom(concat!(
2991                            "discriminated union variant `",
2992                            stringify!(#variant_name),
2993                            "` did not serialize as an object",
2994                        ))
2995                    })?;
2996                    #serialize_discriminator
2997                    value.serialize(serializer)
2998                }
2999            }
3000        });
3001        let mut deserialize_arms = Vec::new();
3002        for (primary_index, (variant, variant_name, payload)) in variant_shapes.iter().enumerate() {
3003            for tag in &variant.preferred_discriminator_values {
3004                let fallback_attempts = variant_shapes.iter().enumerate().filter_map(
3005                    |(fallback_index, (_, fallback_name, fallback_payload))| {
3006                        if fallback_index == primary_index {
3007                            return None;
3008                        }
3009                        if exclusive {
3010                            Some(quote! {
3011                                if let Ok(payload) =
3012                                    serde_json::from_value::<#fallback_payload>(value.clone())
3013                                {
3014                                    if let Some((_, first_name)) = &structural_match {
3015                                        return Err(serde::de::Error::custom(format!(
3016                                            "discriminator `{}` value `{}` did not fit its mapped branch and structurally matched both `{}` and `{}`",
3017                                            #discriminator_field,
3018                                            #tag,
3019                                            first_name,
3020                                            stringify!(#fallback_name),
3021                                        )));
3022                                    }
3023                                    structural_match = Some((
3024                                        Self::#fallback_name(payload),
3025                                        stringify!(#fallback_name),
3026                                    ));
3027                                }
3028                            })
3029                        } else {
3030                            Some(quote! {
3031                                if let Ok(payload) =
3032                                    serde_json::from_value::<#fallback_payload>(value.clone())
3033                                {
3034                                    return Ok(Self::#fallback_name(payload));
3035                                }
3036                            })
3037                        }
3038                    },
3039                );
3040                let structural_fallback = if exclusive {
3041                    quote! {
3042                        let mut structural_match: Option<(Self, &'static str)> = None;
3043                        #(#fallback_attempts)*
3044                        match structural_match {
3045                            Some((payload, _)) => Ok(payload),
3046                            None => Err(serde::de::Error::custom(primary_error)),
3047                        }
3048                    }
3049                } else {
3050                    quote! {
3051                        #(#fallback_attempts)*
3052                        Err(serde::de::Error::custom(primary_error))
3053                    }
3054                };
3055                deserialize_arms.push(quote! {
3056                    #tag => {
3057                        let primary_error = match serde_json::from_value::<#payload>(value.clone()) {
3058                            Ok(payload) => return Ok(Self::#variant_name(payload)),
3059                            Err(error) => error,
3060                        };
3061                        #structural_fallback
3062                    }
3063                });
3064            }
3065        }
3066        let missing_discriminator_attempts = variant_shapes.iter().filter_map(
3067            |(variant, variant_name, payload)| {
3068                if variant.discriminator_field_required {
3069                    return None;
3070                }
3071                if exclusive {
3072                    Some(quote! {
3073                        if let Ok(payload) = serde_json::from_value::<#payload>(value.clone()) {
3074                            if let Some((_, first_name)) = &structural_match {
3075                                return Err(serde::de::Error::custom(format!(
3076                                    "missing discriminator `{}` structurally matched both `{}` and `{}`",
3077                                    #discriminator_field,
3078                                    first_name,
3079                                    stringify!(#variant_name),
3080                                )));
3081                            }
3082                            structural_match = Some((
3083                                Self::#variant_name(payload),
3084                                stringify!(#variant_name),
3085                            ));
3086                        }
3087                    })
3088                } else {
3089                    Some(quote! {
3090                        if let Ok(payload) = serde_json::from_value::<#payload>(value.clone()) {
3091                            return Ok(Self::#variant_name(payload));
3092                        }
3093                    })
3094                }
3095            },
3096        );
3097        let has_missing_discriminator_candidates = variants
3098            .iter()
3099            .any(|variant| !variant.discriminator_field_required);
3100        let missing_discriminator_fallback = if has_missing_discriminator_candidates && exclusive {
3101            quote! {
3102                let mut structural_match: Option<(Self, &'static str)> = None;
3103                #(#missing_discriminator_attempts)*
3104                structural_match
3105                    .map(|(payload, _)| payload)
3106                    .ok_or_else(|| serde::de::Error::custom(concat!(
3107                        "missing string discriminator `",
3108                        #discriminator_field,
3109                        "` and no tagless branch matched",
3110                    )))
3111            }
3112        } else if has_missing_discriminator_candidates {
3113            quote! {
3114                #(#missing_discriminator_attempts)*
3115                Err(serde::de::Error::custom(concat!(
3116                    "missing string discriminator `",
3117                    #discriminator_field,
3118                    "` and no tagless branch matched",
3119                )))
3120            }
3121        } else {
3122            quote! {
3123                Err(serde::de::Error::custom(concat!(
3124                    "missing string discriminator `",
3125                    #discriminator_field,
3126                    "`",
3127                )))
3128            }
3129        };
3130
3131        let doc_comment = if let Some(desc) = &schema.description {
3132            quote! { #[doc = #desc] }
3133        } else {
3134            TokenStream::new()
3135        };
3136
3137        // Keep the discriminator on each standalone component model, then do
3138        // explicit discriminator-directed dispatch here. A derive-based
3139        // internally tagged enum requires stripping the tag from its payload;
3140        // that made the same component serialize invalid JSON when used
3141        // directly or in an array. Explicit dispatch retains O(1)-by-tag
3142        // behavior without giving the payload two incompatible wire shapes.
3143        let derives = if self.config.enable_specta {
3144            quote! {
3145                #[derive(Debug, Clone)]
3146                #[cfg_attr(feature = "specta", derive(specta::Type))]
3147            }
3148        } else {
3149            quote! {
3150                #[derive(Debug, Clone)]
3151            }
3152        };
3153
3154        Ok(quote! {
3155            #doc_comment
3156            #derives
3157            pub enum #enum_name {
3158                #(#enum_variants)*
3159            }
3160
3161            impl serde::Serialize for #enum_name {
3162                fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
3163                where
3164                    S: serde::Serializer,
3165                {
3166                    match self {
3167                        #(#serialize_arms)*
3168                    }
3169                }
3170            }
3171
3172            impl<'de> serde::Deserialize<'de> for #enum_name {
3173                fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3174                where
3175                    D: serde::Deserializer<'de>,
3176                {
3177                    let value = serde_json::Value::deserialize(deserializer)?;
3178                    let discriminator = match value.get(#discriminator_field) {
3179                        Some(serde_json::Value::String(discriminator)) => {
3180                            Some(discriminator.as_str())
3181                        }
3182                        Some(_) => {
3183                            return Err(serde::de::Error::custom(concat!(
3184                                "non-string discriminator `",
3185                                #discriminator_field,
3186                                "`",
3187                            )));
3188                        }
3189                        None => None,
3190                    };
3191                    match discriminator {
3192                        Some(discriminator) => match discriminator {
3193                            #(#deserialize_arms)*
3194                            other => Err(serde::de::Error::custom(format!(
3195                                "unknown discriminator value `{other}` for `{}`",
3196                                #discriminator_field,
3197                            ))),
3198                        },
3199                        None => { #missing_discriminator_fallback }
3200                    }
3201                }
3202            }
3203        })
3204    }
3205
3206    /// Check if a discriminated union should be generated as untagged due to being nested
3207    fn should_use_untagged_discriminated_union(
3208        &self,
3209        schema: &crate::analysis::AnalyzedSchema,
3210        analysis: &crate::analysis::SchemaAnalysis,
3211    ) -> bool {
3212        // Only make discriminated unions untagged if they are nested AND their variants
3213        // don't need the discriminator field for API compatibility
3214
3215        // Check if this schema is used as a variant in another discriminated union
3216        for other_schema in analysis.schemas.values() {
3217            if let crate::analysis::SchemaType::DiscriminatedUnion { variants, .. } =
3218                &other_schema.schema_type
3219            {
3220                for variant in variants {
3221                    if variant.type_name == schema.name {
3222                        // This discriminated union is nested inside another discriminated union
3223
3224                        // Check if the current schema's variants have the discriminator field in their properties
3225                        // If they do, we need to keep this union tagged to preserve the discriminator
3226                        if let crate::analysis::SchemaType::DiscriminatedUnion {
3227                            discriminator_field: current_discriminator,
3228                            variants: current_variants,
3229                            ..
3230                        } = &schema.schema_type
3231                        {
3232                            // Check if any variant schemas have the discriminator field as a property
3233                            for current_variant in current_variants {
3234                                if let Some(variant_schema) =
3235                                    analysis.schemas.get(&current_variant.type_name)
3236                                {
3237                                    if let crate::analysis::SchemaType::Object {
3238                                        properties, ..
3239                                    } = &variant_schema.schema_type
3240                                    {
3241                                        if properties.contains_key(current_discriminator) {
3242                                            // This variant has the discriminator field as a property,
3243                                            // so we need to keep the union tagged to preserve it
3244                                            return false;
3245                                        }
3246                                    }
3247                                }
3248                            }
3249                        }
3250
3251                        // No variants have the discriminator as a property, safe to make untagged
3252                        return true;
3253                    }
3254                }
3255            }
3256        }
3257        false
3258    }
3259
3260    fn generate_union_enum(
3261        &self,
3262        schema: &crate::analysis::AnalyzedSchema,
3263        variants: &[crate::analysis::SchemaRef],
3264        exclusive: bool,
3265        analysis: &crate::analysis::SchemaAnalysis,
3266    ) -> Result<TokenStream> {
3267        let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
3268
3269        // Generate meaningful variant names based on type names
3270        let mut used_variant_names = std::collections::HashSet::new();
3271        let enum_variants = variants
3272            .iter()
3273            .enumerate()
3274            .map(|(i, variant)| {
3275                // Generate a meaningful variant name from the type name
3276                let base_variant_name = self.type_name_to_variant_name(&variant.target);
3277                let variant_name = self.ensure_unique_variant_name_generator(
3278                    base_variant_name,
3279                    &mut used_variant_names,
3280                    i,
3281                );
3282                let variant_name_ident = format_ident!("{}", variant_name);
3283
3284                // For primitive types and Vec types, use them directly without conversion
3285                let variant_type_tokens = if matches!(
3286                    variant.target.as_str(),
3287                    "bool"
3288                        | "i8"
3289                        | "i16"
3290                        | "i32"
3291                        | "i64"
3292                        | "i128"
3293                        | "u8"
3294                        | "u16"
3295                        | "u32"
3296                        | "u64"
3297                        | "u128"
3298                        | "f32"
3299                        | "f64"
3300                        | "String"
3301                ) {
3302                    let type_ident = format_ident!("{}", variant.target);
3303                    quote! { #type_ident }
3304                } else if variant.target == "serde_json::Value" {
3305                    // The target is a fully-qualified path; emit it as a path so
3306                    // it doesn't get mangled into a phantom `SerdeJsonValue` ident.
3307                    quote! { serde_json::Value }
3308                } else if variant.target.starts_with("Vec<") && variant.target.ends_with(">") {
3309                    // Handle Vec types by parsing the inner type
3310                    let inner = &variant.target[4..variant.target.len() - 1];
3311
3312                    // Handle nested Vec types (e.g., Vec<Vec<i64>>)
3313                    if inner.starts_with("Vec<") && inner.ends_with(">") {
3314                        let inner_inner = &inner[4..inner.len() - 1];
3315                        if inner_inner == "serde_json::Value" {
3316                            quote! { Vec<Vec<serde_json::Value>> }
3317                        } else {
3318                            let inner_inner_type = if matches!(
3319                                inner_inner,
3320                                "bool"
3321                                    | "i8"
3322                                    | "i16"
3323                                    | "i32"
3324                                    | "i64"
3325                                    | "i128"
3326                                    | "u8"
3327                                    | "u16"
3328                                    | "u32"
3329                                    | "u64"
3330                                    | "u128"
3331                                    | "f32"
3332                                    | "f64"
3333                                    | "String"
3334                            ) {
3335                                format_ident!("{}", inner_inner)
3336                            } else {
3337                                format_ident!("{}", self.to_rust_type_name(inner_inner))
3338                            };
3339                            quote! { Vec<Vec<#inner_inner_type>> }
3340                        }
3341                    } else if inner == "serde_json::Value" {
3342                        quote! { Vec<serde_json::Value> }
3343                    } else {
3344                        let inner_type = if matches!(
3345                            inner,
3346                            "bool"
3347                                | "i8"
3348                                | "i16"
3349                                | "i32"
3350                                | "i64"
3351                                | "i128"
3352                                | "u8"
3353                                | "u16"
3354                                | "u32"
3355                                | "u64"
3356                                | "u128"
3357                                | "f32"
3358                                | "f64"
3359                                | "String"
3360                        ) {
3361                            format_ident!("{}", inner)
3362                        } else {
3363                            format_ident!("{}", self.to_rust_type_name(inner))
3364                        };
3365                        quote! { Vec<#inner_type> }
3366                    }
3367                } else if variant.target.contains("::") || variant.target.contains('<') {
3368                    // Qualified Rust path or generic (chrono::DateTime<chrono::Utc>,
3369                    // bytes::Bytes, std::net::Ipv4Addr) emitted by TypeMapper. Pass
3370                    // it straight to syn — the to_rust_type_name PascalCase
3371                    // pipeline below would mangle it into a non-existent ident.
3372                    parse_rust_type(&variant.target).unwrap_or_else(|_| {
3373                        let fallback = format_ident!("{}", self.to_rust_type_name(&variant.target));
3374                        quote! { #fallback }
3375                    })
3376                } else {
3377                    let type_ident = format_ident!("{}", self.to_rust_type_name(&variant.target));
3378                    quote! { #type_ident }
3379                };
3380
3381                // Self-referential variant (variant payload type == enclosing
3382                // enum) yields an infinite-size enum (E0072). Wrap in `Box<T>` to
3383                // break the cycle. Observed in microsoft-graph.yaml.
3384                let target_rust_name = self.to_rust_type_name(&variant.target);
3385                let enclosing_name = self.to_rust_type_name(&schema.name);
3386                let is_self_ref = target_rust_name == enclosing_name;
3387                // Indirect cycles (stripe BankAccount → BankAccountCustomer →
3388                // Customer → BankAccountCustomer): variants pointing into the
3389                // analysis's recursive_schemas set must also be heap-allocated.
3390                let is_recursive_target = analysis
3391                    .dependencies
3392                    .recursive_schemas
3393                    .contains(&variant.target);
3394                let variant_type_tokens = if is_self_ref || is_recursive_target {
3395                    quote! { Box<#variant_type_tokens> }
3396                } else {
3397                    variant_type_tokens
3398                };
3399                let variant_type_tokens = if variant.nullable {
3400                    quote! { Option<#variant_type_tokens> }
3401                } else {
3402                    variant_type_tokens
3403                };
3404
3405                (variant_name_ident, variant_type_tokens)
3406            })
3407            .collect::<Vec<_>>();
3408        let variant_declarations = enum_variants
3409            .iter()
3410            .map(|(variant_name, variant_type)| quote! { #variant_name(#variant_type), })
3411            .collect::<Vec<_>>();
3412
3413        let doc_comment = if let Some(desc) = &schema.description {
3414            quote! { #[doc = #desc] }
3415        } else {
3416            TokenStream::new()
3417        };
3418
3419        let object_only = variants.iter().all(|variant| {
3420            self.union_target_serializes_as_object(
3421                &variant.target,
3422                analysis,
3423                &mut std::collections::HashSet::new(),
3424            )
3425        });
3426
3427        if exclusive || object_only {
3428            let derives = if self.config.enable_specta {
3429                quote! {
3430                    #[derive(Debug, Clone)]
3431                    #[cfg_attr(feature = "specta", derive(specta::Type))]
3432                }
3433            } else {
3434                quote! { #[derive(Debug, Clone)] }
3435            };
3436            let serialize_arms = enum_variants
3437                .iter()
3438                .map(|(variant_name, _)| {
3439                    quote! {
3440                        Self::#variant_name(value) => serde::Serialize::serialize(value, serializer),
3441                    }
3442                })
3443                .collect::<Vec<_>>();
3444            let deserialize_attempts = enum_variants
3445                .iter()
3446                .zip(variants)
3447                .map(|((variant_name, variant_type), variant)| {
3448                    if exclusive {
3449                        let constraints = self.union_branch_literal_constraints(
3450                            &variant.target,
3451                            analysis,
3452                            &mut std::collections::HashSet::new(),
3453                        );
3454                        let constraint_checks =
3455                            constraints.iter().map(|(field, (required, allowed))| {
3456                                let allowed = allowed
3457                                    .iter()
3458                                    .map(serde_json::Value::to_string)
3459                                    .collect::<Vec<_>>();
3460                                if allowed.is_empty() && *required {
3461                                    quote! { false }
3462                                } else if allowed.is_empty() {
3463                                    quote! { object.get(#field).is_none() }
3464                                } else if *required {
3465                                    quote! {
3466                                        object.get(#field).is_some_and(|value| {
3467                                            value.is_null()
3468                                                || matches!(value.to_string().as_str(), #(#allowed)|*)
3469                                        })
3470                                    }
3471                                } else {
3472                                    quote! {
3473                                        object.get(#field).is_none_or(|value| {
3474                                            value.is_null()
3475                                                || matches!(value.to_string().as_str(), #(#allowed)|*)
3476                                        })
3477                                    }
3478                                }
3479                            });
3480                        let constraints_match = if constraints.is_empty() {
3481                            quote! { true }
3482                        } else {
3483                            quote! {
3484                                input.as_object().is_some_and(|object| {
3485                                    true #(&& #constraint_checks)*
3486                                })
3487                            }
3488                        };
3489                        quote! {
3490                            if #constraints_match {
3491                                if let Ok(candidate) =
3492                                    serde_json::from_value::<#variant_type>(input.clone())
3493                                {
3494                                    let preserves_complete_input = serde_json::to_value(&candidate)
3495                                        .map(|encoded| encoded == input)
3496                                        .unwrap_or(false);
3497                                    if preserves_complete_input {
3498                                        if matched.is_some() {
3499                                            return Err(serde::de::Error::custom(concat!(
3500                                                "ambiguous oneOf value for ",
3501                                                stringify!(#enum_name),
3502                                                ": more than one branch preserved the complete input",
3503                                            )));
3504                                        }
3505                                        matched = Some(Self::#variant_name(candidate));
3506                                    }
3507                                }
3508                            }
3509                        }
3510                    } else {
3511                        quote! {
3512                            if let Ok(candidate) =
3513                                serde_json::from_value::<#variant_type>(input.clone())
3514                            {
3515                                let preserves_complete_input = serde_json::to_value(&candidate)
3516                                    .map(|encoded| {
3517                                        preserves_complete_json_input(&encoded, &input)
3518                                    })
3519                                    .unwrap_or(false);
3520                                if preserves_complete_input {
3521                                    return Ok(Self::#variant_name(candidate));
3522                                }
3523                            }
3524                        }
3525                    }
3526                })
3527                .collect::<Vec<_>>();
3528            let no_match = if exclusive {
3529                quote! {
3530                    matched.ok_or_else(|| serde::de::Error::custom(concat!(
3531                        "no oneOf branch for ",
3532                        stringify!(#enum_name),
3533                        " preserved the complete input",
3534                    )))
3535                }
3536            } else {
3537                quote! {
3538                    Err(serde::de::Error::custom(concat!(
3539                        "no anyOf branch for ",
3540                        stringify!(#enum_name),
3541                        " preserved the complete input",
3542                    )))
3543                }
3544            };
3545            let matched_declaration = exclusive.then(|| quote! { let mut matched = None; });
3546            let preservation_helper = (!exclusive).then(|| {
3547                quote! {
3548                    fn exact_json_integer(number: &serde_json::Number) -> Option<i128> {
3549                        number
3550                            .as_i64()
3551                            .map(i128::from)
3552                            .or_else(|| number.as_u64().map(i128::from))
3553                    }
3554
3555                    fn json_numbers_have_same_value(
3556                        encoded: &serde_json::Number,
3557                        input: &serde_json::Number,
3558                    ) -> bool {
3559                        match (exact_json_integer(encoded), exact_json_integer(input)) {
3560                            (Some(encoded), Some(input)) => encoded == input,
3561                            (Some(encoded), None) => input.as_f64().is_some_and(|input| {
3562                                input.is_finite()
3563                                    && input.fract() == 0.0
3564                                    && input as i128 == encoded
3565                            }),
3566                            (None, Some(input)) => encoded.as_f64().is_some_and(|encoded| {
3567                                encoded.is_finite()
3568                                    && encoded.fract() == 0.0
3569                                    && encoded as i128 == input
3570                            }),
3571                            (None, None) => encoded.as_f64() == input.as_f64(),
3572                        }
3573                    }
3574
3575                    fn preserves_complete_json_input(
3576                        encoded: &serde_json::Value,
3577                        input: &serde_json::Value,
3578                    ) -> bool {
3579                        match (encoded, input) {
3580                            (
3581                                serde_json::Value::Object(encoded),
3582                                serde_json::Value::Object(input),
3583                            ) => input.iter().all(|(key, value)| {
3584                                encoded.get(key).is_some_and(|encoded_value| {
3585                                    preserves_complete_json_input(encoded_value, value)
3586                                })
3587                            }),
3588                            (
3589                                serde_json::Value::Array(encoded),
3590                                serde_json::Value::Array(input),
3591                            ) => {
3592                                encoded.len() == input.len()
3593                                    && encoded.iter().zip(input).all(|(encoded, input)| {
3594                                        preserves_complete_json_input(encoded, input)
3595                                    })
3596                            }
3597                            (
3598                                serde_json::Value::Number(encoded),
3599                                serde_json::Value::Number(input),
3600                            ) => json_numbers_have_same_value(encoded, input),
3601                            _ => encoded == input,
3602                        }
3603                    }
3604                }
3605            });
3606
3607            return Ok(quote! {
3608                #doc_comment
3609                #derives
3610                pub enum #enum_name {
3611                    #(#variant_declarations)*
3612                }
3613
3614                impl Serialize for #enum_name {
3615                    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
3616                    where
3617                        S: serde::Serializer,
3618                    {
3619                        match self {
3620                            #(#serialize_arms)*
3621                        }
3622                    }
3623                }
3624
3625                impl<'de> Deserialize<'de> for #enum_name {
3626                    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3627                    where
3628                        D: serde::Deserializer<'de>,
3629                    {
3630                        #preservation_helper
3631                        let input = <serde_json::Value as Deserialize>::deserialize(deserializer)?;
3632                        #matched_declaration
3633                        #(#deserialize_attempts)*
3634                        #no_match
3635                    }
3636                }
3637            });
3638        }
3639
3640        // Generate derives with optional Specta support for non-exclusive
3641        // unions, where multiple anyOf branches may legitimately match.
3642        let derives = if self.config.enable_specta {
3643            quote! {
3644                #[derive(Debug, Clone, Deserialize, Serialize)]
3645                #[cfg_attr(feature = "specta", derive(specta::Type))]
3646                #[serde(untagged)]
3647            }
3648        } else {
3649            quote! {
3650                #[derive(Debug, Clone, Deserialize, Serialize)]
3651                #[serde(untagged)]
3652            }
3653        };
3654
3655        Ok(quote! {
3656            #doc_comment
3657            #derives
3658            pub enum #enum_name {
3659                #(#variant_declarations)*
3660            }
3661        })
3662    }
3663
3664    fn union_branch_literal_constraints(
3665        &self,
3666        target: &str,
3667        analysis: &crate::analysis::SchemaAnalysis,
3668        visited: &mut std::collections::HashSet<String>,
3669    ) -> BTreeMap<String, (bool, Vec<serde_json::Value>)> {
3670        if !visited.insert(target.to_string()) {
3671            return BTreeMap::new();
3672        }
3673        let constraints = analysis
3674            .schemas
3675            .get(target)
3676            .map(|schema| {
3677                self.schema_literal_property_constraints(&schema.original, analysis, visited)
3678            })
3679            .unwrap_or_default();
3680        visited.remove(target);
3681        constraints
3682    }
3683
3684    fn schema_literal_property_constraints(
3685        &self,
3686        schema: &serde_json::Value,
3687        analysis: &crate::analysis::SchemaAnalysis,
3688        visited: &mut std::collections::HashSet<String>,
3689    ) -> BTreeMap<String, (bool, Vec<serde_json::Value>)> {
3690        let Some(object) = schema.as_object() else {
3691            return BTreeMap::new();
3692        };
3693        if let Some(reference) = object.get("$ref").and_then(serde_json::Value::as_str)
3694            && let Some(target) = reference.rsplit('/').next()
3695        {
3696            return self.union_branch_literal_constraints(target, analysis, visited);
3697        }
3698
3699        let required = object
3700            .get("required")
3701            .and_then(serde_json::Value::as_array)
3702            .into_iter()
3703            .flatten()
3704            .filter_map(serde_json::Value::as_str)
3705            .collect::<std::collections::HashSet<_>>();
3706        let mut constraints = BTreeMap::new();
3707        if let Some(properties) = object
3708            .get("properties")
3709            .and_then(serde_json::Value::as_object)
3710        {
3711            for (field, property) in properties {
3712                if let Some(values) = self.schema_literal_domain(
3713                    property,
3714                    analysis,
3715                    &mut std::collections::HashSet::new(),
3716                ) && !values.is_empty()
3717                {
3718                    constraints.insert(field.clone(), (required.contains(field.as_str()), values));
3719                }
3720            }
3721        }
3722        if let Some(branches) = object.get("allOf").and_then(serde_json::Value::as_array) {
3723            for branch in branches {
3724                for (field, (branch_required, values)) in
3725                    self.schema_literal_property_constraints(branch, analysis, visited)
3726                {
3727                    constraints
3728                        .entry(field)
3729                        .and_modify(|(is_required, existing)| {
3730                            *is_required |= branch_required;
3731                            existing.retain(|value| values.contains(value));
3732                        })
3733                        .or_insert((branch_required, values));
3734                }
3735            }
3736        }
3737        constraints
3738    }
3739
3740    fn schema_literal_domain(
3741        &self,
3742        schema: &serde_json::Value,
3743        analysis: &crate::analysis::SchemaAnalysis,
3744        visited: &mut std::collections::HashSet<String>,
3745    ) -> Option<Vec<serde_json::Value>> {
3746        let object = schema.as_object()?;
3747        if let Some(reference) = object.get("$ref").and_then(serde_json::Value::as_str)
3748            && let Some(target) = reference.rsplit('/').next()
3749        {
3750            if !visited.insert(target.to_string()) {
3751                return None;
3752            }
3753            let result = analysis
3754                .schemas
3755                .get(target)
3756                .and_then(|schema| self.schema_literal_domain(&schema.original, analysis, visited));
3757            visited.remove(target);
3758            return result;
3759        }
3760        let own = object
3761            .get("const")
3762            .map(|value| vec![value.clone()])
3763            .or_else(|| {
3764                object
3765                    .get("enum")
3766                    .and_then(serde_json::Value::as_array)
3767                    .cloned()
3768            });
3769        let composed = object
3770            .get("allOf")
3771            .and_then(serde_json::Value::as_array)
3772            .and_then(|branches| {
3773                branches.iter().fold(None, |domain, branch| {
3774                    let branch_domain = self.schema_literal_domain(branch, analysis, visited);
3775                    match (domain, branch_domain) {
3776                        (None, other) | (other, None) => other,
3777                        (Some(mut left), Some(right)) => {
3778                            left.retain(|value| right.contains(value));
3779                            Some(left)
3780                        }
3781                    }
3782                })
3783            });
3784        match (own, composed) {
3785            (None, other) | (other, None) => other,
3786            (Some(mut left), Some(right)) => {
3787                left.retain(|value| right.contains(value));
3788                Some(left)
3789            }
3790        }
3791    }
3792
3793    fn union_target_serializes_as_object(
3794        &self,
3795        target: &str,
3796        analysis: &crate::analysis::SchemaAnalysis,
3797        visited: &mut std::collections::HashSet<String>,
3798    ) -> bool {
3799        if !visited.insert(target.to_string()) {
3800            return false;
3801        }
3802        let result = analysis
3803            .schemas
3804            .get(target)
3805            .is_some_and(|schema| match &schema.schema_type {
3806                crate::analysis::SchemaType::Object { .. }
3807                | crate::analysis::SchemaType::Composition { .. }
3808                | crate::analysis::SchemaType::DiscriminatedUnion { .. } => true,
3809                crate::analysis::SchemaType::Reference { target } => {
3810                    self.union_target_serializes_as_object(target, analysis, visited)
3811                }
3812                crate::analysis::SchemaType::Union { variants, .. } => {
3813                    variants.iter().all(|variant| {
3814                        self.union_target_serializes_as_object(&variant.target, analysis, visited)
3815                    })
3816                }
3817                _ => false,
3818            });
3819        visited.remove(target);
3820        result
3821    }
3822
3823    /// Walk a chain of type-alias `Reference`s starting from `target` and
3824    /// return true if the chain reaches the schema named by
3825    /// `enclosing_rust_name` (Rust name). Bounded depth to prevent infinite
3826    /// loops on truly cyclic aliases.
3827    fn target_aliases_back_to(
3828        &self,
3829        target: &str,
3830        enclosing_rust_name: &str,
3831        analysis: &crate::analysis::SchemaAnalysis,
3832    ) -> bool {
3833        let mut current = target.to_string();
3834        let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
3835        for _ in 0..16 {
3836            if !visited.insert(current.clone()) {
3837                return true;
3838            }
3839            let Some(schema) = analysis.schemas.get(&current) else {
3840                return false;
3841            };
3842            if let crate::analysis::SchemaType::Reference { target: next } = &schema.schema_type {
3843                if self.to_rust_type_name(next) == enclosing_rust_name {
3844                    return true;
3845                }
3846                current = next.clone();
3847                continue;
3848            }
3849            return false;
3850        }
3851        false
3852    }
3853
3854    fn generate_field_type(
3855        &self,
3856        schema_name: &str,
3857        field_name: &str,
3858        prop: &crate::analysis::PropertyInfo,
3859        is_required: bool,
3860        analysis: &crate::analysis::SchemaAnalysis,
3861    ) -> TokenStream {
3862        let base_type = self.generate_property_base_type(schema_name, field_name, prop, analysis);
3863
3864        if !is_required && self.property_is_nullable(schema_name, field_name, prop) {
3865            quote! { Option<Option<#base_type>> }
3866        } else if self.property_is_option_wrapped(
3867            schema_name,
3868            field_name,
3869            prop,
3870            is_required,
3871            analysis,
3872        ) {
3873            quote! { Option<#base_type> }
3874        } else {
3875            base_type
3876        }
3877    }
3878
3879    pub(crate) fn property_is_nullable(
3880        &self,
3881        schema_name: &str,
3882        field_name: &str,
3883        prop: &crate::analysis::PropertyInfo,
3884    ) -> bool {
3885        let override_key = format!("{schema_name}.{field_name}");
3886        prop.nullable
3887            || self
3888                .config
3889                .nullable_field_overrides
3890                .get(&override_key)
3891                .copied()
3892                .unwrap_or(false)
3893    }
3894
3895    pub(crate) fn property_is_tri_state(
3896        &self,
3897        schema_name: &str,
3898        field_name: &str,
3899        prop: &crate::analysis::PropertyInfo,
3900        is_required: bool,
3901    ) -> bool {
3902        !is_required && self.property_is_nullable(schema_name, field_name, prop)
3903    }
3904
3905    fn property_is_option_wrapped(
3906        &self,
3907        schema_name: &str,
3908        field_name: &str,
3909        prop: &crate::analysis::PropertyInfo,
3910        is_required: bool,
3911        analysis: &crate::analysis::SchemaAnalysis,
3912    ) -> bool {
3913        !is_required
3914            || self.property_is_nullable(schema_name, field_name, prop)
3915            || (prop.default.is_some() && self.type_lacks_default(&prop.schema_type, analysis))
3916    }
3917
3918    pub(crate) fn generate_property_base_type(
3919        &self,
3920        schema_name: &str,
3921        _field_name: &str,
3922        prop: &crate::analysis::PropertyInfo,
3923        analysis: &crate::analysis::SchemaAnalysis,
3924    ) -> TokenStream {
3925        use crate::analysis::SchemaType;
3926
3927        match &prop.schema_type {
3928            SchemaType::Primitive { rust_type, .. } => {
3929                // syn handles generics + complex paths
3930                // (chrono::DateTime<chrono::Utc>, Vec<u8>, …).
3931                parse_rust_type(rust_type).unwrap_or_else(|_| {
3932                    // Pathological mapper output: fall back to bare
3933                    // String so the generated file at least
3934                    // compiles. Emit a stderr warning so the
3935                    // operator can investigate.
3936                    eprintln!(
3937                        "⚠️  TypeMapper produced un-parseable type `{rust_type}`; \
3938                         falling back to String"
3939                    );
3940                    quote! { String }
3941                })
3942            }
3943            SchemaType::Reference { target } => {
3944                let target_rust_name = self.to_rust_type_name(target);
3945                let target_type = format_ident!("{}", target_rust_name);
3946                // Wrap recursive references in Box<T> for heap allocation.
3947                // Three ways to detect the cycle:
3948                // 1. Target is in the analysis-level recursive set (catches
3949                //    direct + indirect cycles via the dependency graph).
3950                // 2. Target's Rust name equals the enclosing struct's Rust
3951                //    name (catches cloudflare-style cases where two distinct
3952                //    spec schemas PascalCase to the same ident).
3953                // 3. Target is a type alias whose resolution chain reaches
3954                //    the enclosing schema (catches cal-com's
3955                //    `ReassignBookingOutput20240813Data = Reassign...`
3956                //    pattern: the synthesized inline name aliases back to
3957                //    its parent).
3958                let enclosing_rust_name = self.to_rust_type_name(schema_name);
3959                let is_self_via_rust_name = target_rust_name == enclosing_rust_name;
3960                let is_alias_chain_self =
3961                    self.target_aliases_back_to(target, &enclosing_rust_name, analysis);
3962                if analysis.dependencies.recursive_schemas.contains(target)
3963                    || is_self_via_rust_name
3964                    || is_alias_chain_self
3965                {
3966                    quote! { Box<#target_type> }
3967                } else {
3968                    quote! { #target_type }
3969                }
3970            }
3971            SchemaType::Array { item_type } => {
3972                let inner_type = self.generate_array_item_type(item_type, analysis);
3973                quote! { Vec<#inner_type> }
3974            }
3975            SchemaType::Nullable { inner_type } => {
3976                let inner_type = self.generate_array_item_type(inner_type, analysis);
3977                quote! { Option<#inner_type> }
3978            }
3979            SchemaType::Tuple { element_types } => {
3980                self.generate_tuple_type(element_types, analysis)
3981            }
3982            SchemaType::Untyped { shape, .. } => untyped_tokens(*shape),
3983            _ => {
3984                // Fallback for complex types
3985                quote! { serde_json::Value }
3986            }
3987        }
3988    }
3989
3990    /// Render positional element types as a Rust tuple. serde reads and writes
3991    /// these as JSON arrays of exactly this length, which is what makes the
3992    /// analyzer's exact-length rule load-bearing.
3993    fn generate_tuple_type(
3994        &self,
3995        element_types: &[crate::analysis::SchemaType],
3996        analysis: &crate::analysis::SchemaAnalysis,
3997    ) -> TokenStream {
3998        let elements = element_types
3999            .iter()
4000            .map(|element_type| self.generate_array_item_type(element_type, analysis))
4001            .collect::<Vec<_>>();
4002        // A one-element Rust tuple needs the trailing comma; `(T)` is just `T`,
4003        // which serde would read as a bare value instead of a single-element
4004        // array.
4005        if let [only] = elements.as_slice() {
4006            return quote! { (#only,) };
4007        }
4008        quote! { (#(#elements),*) }
4009    }
4010
4011    fn generate_serde_field_attrs(
4012        &self,
4013        schema_name: &str,
4014        field_name: &str,
4015        field_ident: &syn::Ident,
4016        prop: &crate::analysis::PropertyInfo,
4017        is_required: bool,
4018        analysis: &crate::analysis::SchemaAnalysis,
4019    ) -> TokenStream {
4020        let mut attrs = Vec::new();
4021
4022        // Generate rename attribute if field name differs from Rust identifier
4023        // Strip r# prefix for comparison since serde handles raw idents transparently
4024        let rust_field_name = field_ident.to_string();
4025        let comparison_name = rust_field_name
4026            .strip_prefix("r#")
4027            .unwrap_or(&rust_field_name);
4028        if comparison_name != field_name {
4029            attrs.push(quote! { rename = #field_name });
4030        }
4031
4032        let is_tri_state = self.property_is_tri_state(schema_name, field_name, prop, is_required);
4033
4034        // Optional fields may be omitted when their outer Option is None.
4035        // Optional nullable fields use Option<Option<T>> so Some(None) remains
4036        // an explicit JSON null. Required nullable fields stay Option<T> and
4037        // must serialize None rather than skipping the required wire name.
4038        if !is_required {
4039            attrs.push(quote! { skip_serializing_if = "Option::is_none" });
4040        }
4041
4042        // Only add default attribute for required fields that have default values.
4043        // Skip #[serde(default)] for types that don't implement Default (discriminated
4044        // unions, union enums) — those fields should be Option<T> instead.
4045        if prop.default.is_some()
4046            && (is_required && !prop.nullable)
4047            && !self.type_lacks_default(&prop.schema_type, analysis)
4048        {
4049            attrs.push(quote! { default });
4050        }
4051
4052        // Codec hint from TypeMapper (Q2): `format: byte` →
4053        // `with = "base64_serde"`, etc. Fields whose mapped type
4054        // carries no codec (e.g. chrono::DateTime<Utc> uses its
4055        // built-in serde) skip this attribute. Option fields need
4056        // the `::option` submodule of the codec — serde dispatches
4057        // on field type, and the base codec works on Vec<u8> /
4058        // chrono::Duration / etc., not their Option wrappers.
4059        if let Some(codec) = self.schema_type_serde_codec(&prop.schema_type, analysis) {
4060            let is_option_wrapped = self.property_is_option_wrapped(
4061                schema_name,
4062                field_name,
4063                prop,
4064                is_required,
4065                analysis,
4066            );
4067            let codec_path = if is_tri_state {
4068                Self::double_option_codec_path(&codec)
4069            } else if is_option_wrapped {
4070                format!("{codec}::option")
4071            } else {
4072                codec
4073            };
4074            attrs.push(quote! { with = #codec_path });
4075            // A `with` codec disables serde's implicit
4076            // missing-field → None handling for Option fields
4077            // (serde-rs/serde#2878); without `default` a request
4078            // that simply omits the field fails to deserialize.
4079            if is_option_wrapped {
4080                attrs.push(quote! { default });
4081            }
4082        } else if is_tri_state {
4083            attrs.push(quote! { default });
4084            attrs.push(quote! { deserialize_with = "tri_state_serde::deserialize" });
4085        }
4086
4087        if attrs.is_empty() {
4088            TokenStream::new()
4089        } else {
4090            quote! { #[serde(#(#attrs),*)] }
4091        }
4092    }
4093
4094    fn double_option_codec_path(codec: &str) -> String {
4095        match codec {
4096            "time::serde::rfc3339" => "time_rfc3339_double_option".to_string(),
4097            "time_date_format" => "time_date_double_option".to_string(),
4098            "time_time_format" => "time_time_double_option".to_string(),
4099            _ => format!("{codec}::double_option"),
4100        }
4101    }
4102
4103    /// Resolve a field codec through named scalar aliases. A property may
4104    /// reference a component whose generated Rust type is `bytes::Bytes`; the
4105    /// codec belongs on the property field, because a Rust type alias cannot
4106    /// carry serde attributes of its own.
4107    fn schema_type_serde_codec(
4108        &self,
4109        schema_type: &crate::analysis::SchemaType,
4110        analysis: &crate::analysis::SchemaAnalysis,
4111    ) -> Option<String> {
4112        let mut current = schema_type;
4113        let mut visited = std::collections::HashSet::new();
4114        loop {
4115            match current {
4116                crate::analysis::SchemaType::Primitive {
4117                    serde_with: Some(codec),
4118                    ..
4119                } => return Some(codec.clone()),
4120                crate::analysis::SchemaType::Reference { target }
4121                    if visited.insert(target.clone()) =>
4122                {
4123                    current = &analysis.schemas.get(target)?.schema_type;
4124                }
4125                crate::analysis::SchemaType::Nullable { inner_type } => {
4126                    current = inner_type;
4127                }
4128                _ => return None,
4129            }
4130        }
4131    }
4132
4133    /// Check if a schema type resolves to a type that doesn't implement `Default`.
4134    /// Discriminated unions and union enums don't derive Default, so fields with
4135    /// these types can't use `#[serde(default)]`.
4136    fn type_lacks_default(
4137        &self,
4138        schema_type: &crate::analysis::SchemaType,
4139        analysis: &crate::analysis::SchemaAnalysis,
4140    ) -> bool {
4141        use crate::analysis::SchemaType;
4142        match schema_type {
4143            SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => true,
4144            // Q2 typed scalars: chrono / url have no Default impl.
4145            // uuid::Uuid, bytes::Bytes, std::net::Ip*Addr all derive
4146            // Default, so they're safe to leave under #[serde(default)].
4147            SchemaType::Primitive { rust_type, .. } => matches!(
4148                rust_type.as_str(),
4149                "chrono::DateTime<chrono::Utc>"
4150                    | "chrono::NaiveDate"
4151                    | "chrono::NaiveTime"
4152                    | "chrono::Duration"
4153                    | "url::Url"
4154                    | "time::OffsetDateTime"
4155                    | "time::Date"
4156                    | "time::Time"
4157                    | "iso8601::Duration"
4158                    | "email_address::EmailAddress"
4159            ),
4160            SchemaType::Reference { target } => {
4161                if let Some(schema) = analysis.schemas.get(target) {
4162                    self.type_lacks_default(&schema.schema_type, analysis)
4163                } else {
4164                    false
4165                }
4166            }
4167            _ => false,
4168        }
4169    }
4170
4171    fn generate_specta_field_attrs(&self, field_name: &str) -> TokenStream {
4172        if !self.config.enable_specta {
4173            return TokenStream::new();
4174        }
4175
4176        // Convert field name to camelCase for TypeScript
4177        let camel_case_name = self.to_camel_case(field_name);
4178
4179        // Only add specta rename if it differs from the original field name
4180        if camel_case_name != field_name {
4181            quote! { #[cfg_attr(feature = "specta", specta(rename = #camel_case_name))] }
4182        } else {
4183            TokenStream::new()
4184        }
4185    }
4186
4187    pub(crate) fn to_rust_enum_variant(&self, s: &str) -> String {
4188        // Preserve sign for numeric values so e.g. `-1` and `1` produce
4189        // distinct variants (`VariantNeg1` vs `Variant1`). Without this,
4190        // strict-namespace enums in github.json collide on `1`/`-1`.
4191        let neg_prefix =
4192            if s.starts_with('-') && s.chars().skip(1).all(|c| c.is_ascii_digit() || c == '.') {
4193                "Neg"
4194            } else {
4195                ""
4196            };
4197
4198        // Convert string to valid Rust enum variant (PascalCase)
4199        let mut result = String::new();
4200        let mut next_upper = true;
4201        let mut prev_was_upper = false;
4202
4203        for (i, c) in s.chars().enumerate() {
4204            match c {
4205                'a'..='z' => {
4206                    if next_upper {
4207                        result.push(c.to_ascii_uppercase());
4208                        next_upper = false;
4209                    } else {
4210                        result.push(c);
4211                    }
4212                    prev_was_upper = false;
4213                }
4214                'A'..='Z' => {
4215                    if next_upper || (!prev_was_upper && i > 0) {
4216                        // Start of word or transition from lowercase
4217                        result.push(c);
4218                        next_upper = false;
4219                    } else {
4220                        // Continue uppercase sequence, convert to lowercase
4221                        result.push(c.to_ascii_lowercase());
4222                    }
4223                    prev_was_upper = true;
4224                }
4225                '0'..='9' => {
4226                    result.push(c);
4227                    next_upper = false;
4228                    prev_was_upper = false;
4229                }
4230                '.' | '-' | '_' | ' ' | '@' | '#' | '$' | '/' | '\\' => {
4231                    // Word boundaries - next char should be uppercase
4232                    next_upper = true;
4233                    prev_was_upper = false;
4234                }
4235                _ => {
4236                    // Other special characters - treat as word boundary
4237                    next_upper = true;
4238                    prev_was_upper = false;
4239                }
4240            }
4241        }
4242
4243        // Handle empty result
4244        if result.is_empty() {
4245            result = "Value".to_string();
4246        }
4247
4248        // Ensure variant starts with a letter (not a number)
4249        if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
4250            result = format!("Variant{neg_prefix}{result}");
4251        } else if !neg_prefix.is_empty() {
4252            // String happened to start with `-<digits>` but produced a
4253            // non-empty alphabetic prefix. Tag the negative anyway.
4254            result = format!("{neg_prefix}{result}");
4255        }
4256
4257        // Handle special cases for enum variants
4258        match result.as_str() {
4259            "Null" => "NullValue".to_string(),
4260            "True" => "TrueValue".to_string(),
4261            "False" => "FalseValue".to_string(),
4262            "Type" => "Type_".to_string(),
4263            "Match" => "Match_".to_string(),
4264            "Fn" => "Fn_".to_string(),
4265            "Impl" => "Impl_".to_string(),
4266            "Trait" => "Trait_".to_string(),
4267            "Struct" => "Struct_".to_string(),
4268            "Enum" => "Enum_".to_string(),
4269            "Mod" => "Mod_".to_string(),
4270            "Use" => "Use_".to_string(),
4271            "Pub" => "Pub_".to_string(),
4272            "Const" => "Const_".to_string(),
4273            "Static" => "Static_".to_string(),
4274            "Let" => "Let_".to_string(),
4275            "Mut" => "Mut_".to_string(),
4276            "Ref" => "Ref_".to_string(),
4277            "Move" => "Move_".to_string(),
4278            "Return" => "Return_".to_string(),
4279            "If" => "If_".to_string(),
4280            "Else" => "Else_".to_string(),
4281            "While" => "While_".to_string(),
4282            "For" => "For_".to_string(),
4283            "Loop" => "Loop_".to_string(),
4284            "Break" => "Break_".to_string(),
4285            "Continue" => "Continue_".to_string(),
4286            "Self" => "Self_".to_string(),
4287            "Super" => "Super_".to_string(),
4288            "Crate" => "Crate_".to_string(),
4289            "Async" => "Async_".to_string(),
4290            "Await" => "Await_".to_string(),
4291            _ => result,
4292        }
4293    }
4294
4295    #[allow(dead_code)]
4296    fn to_rust_identifier(&self, s: &str) -> String {
4297        // Convert string to valid Rust identifier
4298        let mut result = s
4299            .chars()
4300            .map(|c| match c {
4301                'a'..='z' | 'A'..='Z' | '0'..='9' => c,
4302                '.' | '-' | '_' | ' ' | '@' | '#' | '$' | '/' | '\\' => '_',
4303                _ => '_',
4304            })
4305            .collect::<String>();
4306
4307        // Remove leading/trailing underscores
4308        result = result.trim_matches('_').to_string();
4309
4310        // Handle empty result
4311        if result.is_empty() {
4312            result = "value".to_string();
4313        }
4314
4315        // Ensure identifier starts with a letter (not a number)
4316        if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
4317            result = format!("variant_{result}");
4318        }
4319
4320        // Handle special cases for enum values
4321        match result.as_str() {
4322            "null" => "null_value".to_string(),
4323            "true" => "true_value".to_string(),
4324            "false" => "false_value".to_string(),
4325            "type" => "type_".to_string(),
4326            "match" => "match_".to_string(),
4327            "fn" => "fn_".to_string(),
4328            "impl" => "impl_".to_string(),
4329            "trait" => "trait_".to_string(),
4330            "struct" => "struct_".to_string(),
4331            "enum" => "enum_".to_string(),
4332            "mod" => "mod_".to_string(),
4333            "use" => "use_".to_string(),
4334            "pub" => "pub_".to_string(),
4335            "const" => "const_".to_string(),
4336            "static" => "static_".to_string(),
4337            "let" => "let_".to_string(),
4338            "mut" => "mut_".to_string(),
4339            "ref" => "ref_".to_string(),
4340            "move" => "move_".to_string(),
4341            "return" => "return_".to_string(),
4342            "if" => "if_".to_string(),
4343            "else" => "else_".to_string(),
4344            "while" => "while_".to_string(),
4345            "for" => "for_".to_string(),
4346            "loop" => "loop_".to_string(),
4347            "break" => "break_".to_string(),
4348            "continue" => "continue_".to_string(),
4349            "self" => "self_".to_string(),
4350            "super" => "super_".to_string(),
4351            "crate" => "crate_".to_string(),
4352            "async" => "async_".to_string(),
4353            "await" => "await_".to_string(),
4354            // Reserved keywords for edition 2018+
4355            "override" => "override_".to_string(),
4356            "box" => "box_".to_string(),
4357            "dyn" => "dyn_".to_string(),
4358            "where" => "where_".to_string(),
4359            "in" => "in_".to_string(),
4360            // Reserved for future use
4361            "abstract" => "abstract_".to_string(),
4362            "become" => "become_".to_string(),
4363            "do" => "do_".to_string(),
4364            "final" => "final_".to_string(),
4365            "macro" => "macro_".to_string(),
4366            "priv" => "priv_".to_string(),
4367            "try" => "try_".to_string(),
4368            "typeof" => "typeof_".to_string(),
4369            "unsized" => "unsized_".to_string(),
4370            "virtual" => "virtual_".to_string(),
4371            "yield" => "yield_".to_string(),
4372            _ => result,
4373        }
4374    }
4375
4376    /// Q2.4: render a `/// Constraint: …` doc comment for a field
4377    /// when its OpenAPI schema declares any constraint annotations.
4378    /// No-op when constraints are empty or `mode = "off"`.
4379    ///
4380    /// **Doc-comment only** — by deliberate design we never emit
4381    /// `#[validate(...)]` attributes. Constraints belong to the wire
4382    /// contract; the server is the source of truth.
4383    fn generate_constraint_doc(
4384        &self,
4385        constraints: &crate::analysis::PropertyConstraints,
4386    ) -> TokenStream {
4387        use crate::type_mapping::ConstraintMode;
4388
4389        if constraints.is_empty() {
4390            return TokenStream::new();
4391        }
4392        match self.config.types.constraint_mode() {
4393            ConstraintMode::Off => TokenStream::new(),
4394            ConstraintMode::Doc => {
4395                let formatted = format_constraints_doc(constraints);
4396                quote! { #[doc = #formatted] }
4397            }
4398        }
4399    }
4400
4401    fn sanitize_doc_comment(&self, desc: &str) -> String {
4402        // Sanitize description to prevent doctest failures
4403        let mut result = desc.to_string();
4404
4405        // Look for potential code examples that might be interpreted as doctests
4406        // Common patterns that cause issues:
4407        // - Lines that look like standalone expressions
4408        // - JSON-like content
4409        // - Template strings with {}
4410
4411        // If the description contains what looks like code, wrap it in a text block
4412        if result.contains('\n')
4413            && (result.contains('{')
4414                || result.contains("```")
4415                || result.contains("Human:")
4416                || result.contains("Assistant:")
4417                || result
4418                    .lines()
4419                    .any(|line| line.trim().starts_with('"') && line.trim().ends_with('"')))
4420        {
4421            // If it already has code blocks, add ignore annotation
4422            if result.contains("```") {
4423                result = result.replace("```", "```ignore");
4424            } else {
4425                // Wrap the entire description in an ignored code block if it looks like code
4426                if result.lines().any(|line| {
4427                    let trimmed = line.trim();
4428                    trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() > 2
4429                }) {
4430                    result = format!("```ignore\n{result}\n```");
4431                }
4432            }
4433        }
4434
4435        result
4436    }
4437
4438    pub(crate) fn to_rust_type_name(&self, s: &str) -> String {
4439        rust_type_name(s)
4440    }
4441
4442    pub(crate) fn to_rust_field_name(&self, s: &str) -> String {
4443        // Track sign / leading-non-alpha so e.g. `+1` and `-1` produce
4444        // distinct field names instead of both collapsing to `field_1`
4445        // (observed in github.json's reactions schemas).
4446        let leading_marker = match s.chars().next() {
4447            Some('-') if s.len() > 1 => "neg_",
4448            Some('+') if s.len() > 1 => "pos_",
4449            _ => "",
4450        };
4451
4452        // Convert field name to snake_case properly
4453        let mut result = String::new();
4454        let mut prev_was_upper = false;
4455        let mut prev_was_underscore = false;
4456
4457        for (i, c) in s.chars().enumerate() {
4458            match c {
4459                'A'..='Z' => {
4460                    // Add underscore before uppercase if previous was lowercase
4461                    if i > 0 && !prev_was_upper && !prev_was_underscore {
4462                        result.push('_');
4463                    }
4464                    result.push(c.to_ascii_lowercase());
4465                    prev_was_upper = true;
4466                    prev_was_underscore = false;
4467                }
4468                'a'..='z' | '0'..='9' => {
4469                    result.push(c);
4470                    prev_was_upper = false;
4471                    prev_was_underscore = false;
4472                }
4473                '-' | '.' | '_' | '@' | '#' | '$' | ' ' => {
4474                    if !prev_was_underscore && !result.is_empty() {
4475                        result.push('_');
4476                        prev_was_underscore = true;
4477                    }
4478                    prev_was_upper = false;
4479                }
4480                _ => {
4481                    // For other special characters, convert to underscore
4482                    if !prev_was_underscore && !result.is_empty() {
4483                        result.push('_');
4484                    }
4485                    prev_was_upper = false;
4486                    prev_was_underscore = true;
4487                }
4488            }
4489        }
4490
4491        // Clean up result
4492        let mut result = result.trim_matches('_').to_string();
4493        if result.is_empty() {
4494            return "field".to_string();
4495        }
4496
4497        // Ensure field name starts with a letter or underscore (not a number)
4498        if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
4499            result = format!("field_{leading_marker}{result}");
4500        } else if !leading_marker.is_empty() {
4501            result = format!("{leading_marker}{result}");
4502        }
4503
4504        // `self`, `super`, `crate`, and `Self` are not permitted as raw
4505        // identifiers. Suffix them before constructing a proc-macro ident.
4506        if matches!(result.as_str(), "self" | "super" | "crate" | "Self") {
4507            return format!("{result}_field");
4508        }
4509        // Keep boolean literal property names as stable ordinary identifiers
4510        // and let the field allocator disambiguate an actual `true_field` or
4511        // `false_field` property. Serde preserves the original wire name.
4512        if matches!(result.as_str(), "true" | "false") {
4513            return format!("{result}_field");
4514        }
4515        // Handle reserved keywords using raw identifiers (r#keyword)
4516        if Self::is_rust_keyword(&result) {
4517            format!("r#{result}")
4518        } else {
4519            result
4520        }
4521    }
4522
4523    /// Check if a string is a Rust keyword that needs raw identifier treatment
4524    pub fn is_rust_keyword(s: &str) -> bool {
4525        matches!(
4526            s,
4527            "type"
4528                | "match"
4529                | "fn"
4530                | "struct"
4531                | "enum"
4532                | "impl"
4533                | "trait"
4534                | "mod"
4535                | "use"
4536                | "pub"
4537                | "const"
4538                | "static"
4539                | "let"
4540                | "mut"
4541                | "ref"
4542                | "move"
4543                | "return"
4544                | "if"
4545                | "else"
4546                | "while"
4547                | "for"
4548                | "loop"
4549                | "break"
4550                | "continue"
4551                | "self"
4552                | "super"
4553                | "crate"
4554                | "async"
4555                | "await"
4556                | "override"
4557                | "box"
4558                | "dyn"
4559                | "where"
4560                | "in"
4561                | "abstract"
4562                | "become"
4563                | "do"
4564                | "final"
4565                | "macro"
4566                | "priv"
4567                | "try"
4568                | "typeof"
4569                | "unsized"
4570                | "virtual"
4571                | "yield"
4572                // Rust 2024 edition reservations.
4573                | "gen"
4574        )
4575    }
4576
4577    /// Create a proc_macro2::Ident from a field name, handling r# raw identifiers
4578    pub fn to_field_ident(name: &str) -> proc_macro2::Ident {
4579        if let Some(raw) = name.strip_prefix("r#") {
4580            proc_macro2::Ident::new_raw(raw, proc_macro2::Span::call_site())
4581        } else {
4582            proc_macro2::Ident::new(name, proc_macro2::Span::call_site())
4583        }
4584    }
4585
4586    fn to_camel_case(&self, s: &str) -> String {
4587        // Convert snake_case or other formats to camelCase
4588        let mut result = String::new();
4589        let mut capitalize_next = false;
4590
4591        for (i, c) in s.chars().enumerate() {
4592            match c {
4593                '_' | '-' | '.' | ' ' => {
4594                    // Word boundary - capitalize next letter
4595                    capitalize_next = true;
4596                }
4597                'A'..='Z' => {
4598                    if i == 0 {
4599                        // First character should be lowercase in camelCase
4600                        result.push(c.to_ascii_lowercase());
4601                    } else if capitalize_next {
4602                        result.push(c);
4603                        capitalize_next = false;
4604                    } else {
4605                        result.push(c.to_ascii_lowercase());
4606                    }
4607                }
4608                'a'..='z' | '0'..='9' => {
4609                    if capitalize_next {
4610                        result.push(c.to_ascii_uppercase());
4611                        capitalize_next = false;
4612                    } else {
4613                        result.push(c);
4614                    }
4615                }
4616                _ => {
4617                    // Other characters - treat as word boundary
4618                    capitalize_next = true;
4619                }
4620            }
4621        }
4622
4623        if result.is_empty() {
4624            return "field".to_string();
4625        }
4626
4627        result
4628    }
4629
4630    fn generate_composition_struct(
4631        &self,
4632        schema: &crate::analysis::AnalyzedSchema,
4633        schemas: &[crate::analysis::SchemaRef],
4634    ) -> Result<TokenStream> {
4635        let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
4636
4637        // For composition, we can either:
4638        // 1. Flatten all referenced schemas into one struct (if they're all objects)
4639        // 2. Use serde(flatten) to compose them at runtime
4640        // For now, let's use approach 2 with serde(flatten)
4641
4642        let fields = schemas.iter().enumerate().map(|(i, schema_ref)| {
4643            let field_name = format_ident!("part_{}", i);
4644            let field_type = format_ident!("{}", self.to_rust_type_name(&schema_ref.target));
4645
4646            quote! {
4647                #[serde(flatten)]
4648                pub #field_name: #field_type,
4649            }
4650        });
4651
4652        let doc_comment = if let Some(desc) = &schema.description {
4653            quote! { #[doc = #desc] }
4654        } else {
4655            TokenStream::new()
4656        };
4657
4658        // Generate derives with optional Specta support
4659        let derives = if self.config.enable_specta {
4660            quote! {
4661                #[derive(Debug, Clone, Deserialize, Serialize)]
4662                #[cfg_attr(feature = "specta", derive(specta::Type))]
4663            }
4664        } else {
4665            quote! {
4666                #[derive(Debug, Clone, Deserialize, Serialize)]
4667            }
4668        };
4669
4670        Ok(quote! {
4671            #doc_comment
4672            #derives
4673            pub struct #struct_name {
4674                #(#fields)*
4675            }
4676        })
4677    }
4678
4679    fn union_declared_properties(
4680        &self,
4681        target: &str,
4682        analysis: &crate::analysis::SchemaAnalysis,
4683        visited: &mut std::collections::HashSet<String>,
4684    ) -> std::collections::HashSet<String> {
4685        if !visited.insert(target.to_string()) {
4686            return std::collections::HashSet::new();
4687        }
4688        let mut properties = std::collections::HashSet::new();
4689        if let Some(schema) = analysis.schemas.get(target) {
4690            match &schema.schema_type {
4691                crate::analysis::SchemaType::Object {
4692                    properties: own,
4693                    variant,
4694                    ..
4695                } => {
4696                    properties.extend(own.keys().cloned());
4697                    if let Some(variant) = variant {
4698                        properties.extend(self.union_declared_properties(
4699                            &variant.target,
4700                            analysis,
4701                            visited,
4702                        ));
4703                    }
4704                }
4705                crate::analysis::SchemaType::DiscriminatedUnion { variants, .. } => {
4706                    for variant in variants {
4707                        properties.extend(self.union_declared_properties(
4708                            &variant.type_name,
4709                            analysis,
4710                            visited,
4711                        ));
4712                    }
4713                }
4714                crate::analysis::SchemaType::Union { variants, .. }
4715                | crate::analysis::SchemaType::Composition { schemas: variants } => {
4716                    for variant in variants {
4717                        properties.extend(self.union_declared_properties(
4718                            &variant.target,
4719                            analysis,
4720                            visited,
4721                        ));
4722                    }
4723                }
4724                crate::analysis::SchemaType::Reference { target } => {
4725                    properties.extend(self.union_declared_properties(target, analysis, visited));
4726                }
4727                _ => {}
4728            }
4729        }
4730        visited.remove(target);
4731        properties
4732    }
4733
4734    #[allow(dead_code)]
4735    fn find_missing_types(&self, analysis: &SchemaAnalysis) -> std::collections::HashSet<String> {
4736        let mut missing = std::collections::HashSet::new();
4737        let defined_types: std::collections::HashSet<String> =
4738            analysis.schemas.keys().cloned().collect();
4739
4740        // Check all references in union variants
4741        for schema in analysis.schemas.values() {
4742            match &schema.schema_type {
4743                crate::analysis::SchemaType::Union { variants, .. } => {
4744                    for variant in variants {
4745                        if !defined_types.contains(&variant.target) {
4746                            missing.insert(variant.target.clone());
4747                        }
4748                    }
4749                }
4750                crate::analysis::SchemaType::DiscriminatedUnion { variants, .. } => {
4751                    for variant in variants {
4752                        if !defined_types.contains(&variant.type_name) {
4753                            missing.insert(variant.type_name.clone());
4754                        }
4755                    }
4756                }
4757                crate::analysis::SchemaType::Object { properties, .. } => {
4758                    // Sort properties for deterministic iteration
4759                    let mut sorted_props: Vec<_> = properties.iter().collect();
4760                    sorted_props.sort_by_key(|(name, _)| name.as_str());
4761                    for (_, prop) in sorted_props {
4762                        if let crate::analysis::SchemaType::Reference { target } = &prop.schema_type
4763                        {
4764                            if !defined_types.contains(target) {
4765                                missing.insert(target.clone());
4766                            }
4767                        }
4768                    }
4769                }
4770                crate::analysis::SchemaType::Reference { target }
4771                    if !defined_types.contains(target) =>
4772                {
4773                    missing.insert(target.clone());
4774                }
4775                _ => {}
4776            }
4777        }
4778
4779        missing
4780    }
4781
4782    #[allow(clippy::only_used_in_recursion)]
4783    fn generate_array_item_type(
4784        &self,
4785        item_type: &crate::analysis::SchemaType,
4786        analysis: &crate::analysis::SchemaAnalysis,
4787    ) -> TokenStream {
4788        use crate::analysis::SchemaType;
4789
4790        match item_type {
4791            SchemaType::Primitive { rust_type, .. } => {
4792                // The string here may be anything from `i64` / `String` to
4793                // `serde_json::Value` to `Vec<serde_json::Value>` to
4794                // `BTreeMap<String, T>`. Parse it as a syn::Type so we get
4795                // the right tokens regardless of generics.
4796                if let Ok(parsed) = syn::parse_str::<syn::Type>(rust_type) {
4797                    quote! { #parsed }
4798                } else if rust_type.contains("::") {
4799                    let parts: Vec<_> = rust_type
4800                        .split("::")
4801                        .map(|p| format_ident!("{}", p))
4802                        .collect();
4803                    quote! { #(#parts)::* }
4804                } else {
4805                    let type_ident = format_ident!("{}", rust_type);
4806                    quote! { #type_ident }
4807                }
4808            }
4809            SchemaType::Reference { target } => {
4810                let target_type = format_ident!("{}", self.to_rust_type_name(target));
4811                // Wrap recursive references in Box<T> for heap allocation in arrays
4812                if analysis.dependencies.recursive_schemas.contains(target) {
4813                    quote! { Box<#target_type> }
4814                } else {
4815                    quote! { #target_type }
4816                }
4817            }
4818            SchemaType::Array { item_type } => {
4819                // Nested arrays
4820                let inner_type = self.generate_array_item_type(item_type, analysis);
4821                quote! { Vec<#inner_type> }
4822            }
4823            SchemaType::Nullable { inner_type } => {
4824                let inner_type = self.generate_array_item_type(inner_type, analysis);
4825                quote! { Option<#inner_type> }
4826            }
4827            SchemaType::Tuple { element_types } => {
4828                self.generate_tuple_type(element_types, analysis)
4829            }
4830            SchemaType::Untyped { shape, .. } => untyped_tokens(*shape),
4831            _ => {
4832                // Fallback for complex types
4833                quote! { serde_json::Value }
4834            }
4835        }
4836    }
4837
4838    /// Convert a type name to a variant name (e.g., OutputMessage -> OutputMessage, FileSearchToolCall -> FileSearchToolCall)
4839    fn type_name_to_variant_name(&self, type_name: &str) -> String {
4840        // Handle primitive types specially
4841        match type_name {
4842            "bool" => return "Boolean".to_string(),
4843            "i8" | "i16" | "i32" | "i64" | "i128" => return "Integer".to_string(),
4844            "u8" | "u16" | "u32" | "u64" | "u128" => return "UnsignedInteger".to_string(),
4845            "f32" | "f64" => return "Number".to_string(),
4846            "String" => return "String".to_string(),
4847            "serde_json::Value" => return "Value".to_string(),
4848            // Q2 typed-scalar paths. Without these the fallback PascalCase
4849            // pass over `bytes::Bytes` produces `BytesBytes(BytesBytes)`,
4850            // which then can't compile because no `BytesBytes` type exists.
4851            "bytes::Bytes" => return "Binary".to_string(),
4852            "chrono::DateTime<chrono::Utc>" => return "DateTime".to_string(),
4853            "chrono::NaiveDate" => return "Date".to_string(),
4854            "chrono::NaiveTime" => return "Time".to_string(),
4855            "uuid::Uuid" => return "Uuid".to_string(),
4856            "url::Url" => return "Url".to_string(),
4857            "std::net::Ipv4Addr" => return "Ipv4".to_string(),
4858            "std::net::Ipv6Addr" => return "Ipv6".to_string(),
4859            _ => {}
4860        }
4861
4862        // Handle Vec types
4863        if type_name.starts_with("Vec<") && type_name.ends_with(">") {
4864            let inner = &type_name[4..type_name.len() - 1];
4865            // Handle nested Vec types specially
4866            if inner.starts_with("Vec<") && inner.ends_with(">") {
4867                let inner_inner = &inner[4..inner.len() - 1];
4868                return format!("{}ArrayArray", self.type_name_to_variant_name(inner_inner));
4869            }
4870            return format!("{}Array", self.type_name_to_variant_name(inner));
4871        }
4872
4873        // For untagged unions, we want to use the type name itself as the variant name
4874        // since it's already meaningful. This gives us OutputMessage instead of Variant0,
4875        // FileSearchToolCall instead of Variant1, etc.
4876
4877        // Remove common suffixes that might make variant names redundant
4878        let clean_name = type_name
4879            .trim_end_matches("Type")
4880            .trim_end_matches("Schema")
4881            .trim_end_matches("Item");
4882
4883        // Always convert to proper PascalCase to ensure no underscores in enum variants
4884        self.to_rust_type_name(clean_name)
4885    }
4886
4887    /// Ensure unique variant name for generator (similar to analyzer but for generator context)
4888    fn ensure_unique_variant_name_generator(
4889        &self,
4890        base_name: String,
4891        used_names: &mut std::collections::HashSet<String>,
4892        fallback_index: usize,
4893    ) -> String {
4894        if used_names.insert(base_name.clone()) {
4895            return base_name;
4896        }
4897
4898        // Try with numbers
4899        for i in 2..100 {
4900            let numbered_name = format!("{base_name}{i}");
4901            if used_names.insert(numbered_name.clone()) {
4902                return numbered_name;
4903            }
4904        }
4905
4906        // Fallback to Variant{index} if all else fails
4907        let fallback = format!("Variant{fallback_index}");
4908        used_names.insert(fallback.clone());
4909        fallback
4910    }
4911
4912    /// Find the request type for a given operation ID using the analyzed operation info
4913    fn find_request_type_for_operation(
4914        &self,
4915        operation_id: &str,
4916        analysis: &SchemaAnalysis,
4917    ) -> Option<String> {
4918        // Use the operation analysis to get the actual request body schema
4919        analysis.operations.get(operation_id).and_then(|op| {
4920            op.request_body
4921                .as_ref()
4922                .and_then(|rb| rb.schema_name().map(|s| s.to_string()))
4923        })
4924    }
4925
4926    /// Resolve the correct streaming event type based on EventFlow pattern
4927    fn resolve_streaming_event_type(
4928        &self,
4929        endpoint: &crate::streaming::StreamingEndpoint,
4930        analysis: &SchemaAnalysis,
4931    ) -> Result<String> {
4932        match &endpoint.event_flow {
4933            crate::streaming::EventFlow::Simple => {
4934                // For simple streaming, use the response type directly
4935                // Validate that the specified type exists in the schema
4936                if analysis.schemas.contains_key(&endpoint.event_union_type) {
4937                    Ok(endpoint.event_union_type.to_string())
4938                } else {
4939                    Err(crate::error::GeneratorError::ValidationError(format!(
4940                        "Streaming response type '{}' not found in schema for simple streaming endpoint '{}'",
4941                        endpoint.event_union_type, endpoint.operation_id
4942                    )))
4943                }
4944            }
4945            crate::streaming::EventFlow::StartDeltaStop { .. } => {
4946                // For complex event-based streaming, ensure we have a proper union type
4947                // For now, use the specified event_union_type but add validation
4948                if analysis.schemas.contains_key(&endpoint.event_union_type) {
4949                    Ok(endpoint.event_union_type.to_string())
4950                } else {
4951                    Err(crate::error::GeneratorError::ValidationError(format!(
4952                        "Event union type '{}' not found in schema for complex streaming endpoint '{}'",
4953                        endpoint.event_union_type, endpoint.operation_id
4954                    )))
4955                }
4956            }
4957        }
4958    }
4959
4960    /// Generate streaming error types
4961    fn generate_streaming_error_types(&self) -> Result<TokenStream> {
4962        Ok(quote! {
4963            /// Error type for streaming operations
4964            #[derive(Debug, thiserror::Error)]
4965            pub enum StreamingError {
4966                #[error("Connection error: {0}")]
4967                Connection(String),
4968                #[error("HTTP error: {status}")]
4969                Http { status: u16 },
4970                #[error("SSE parsing error: {0}")]
4971                Parsing(String),
4972                #[error("Authentication error: {0}")]
4973                Authentication(String),
4974                #[error("Rate limit error: {0}")]
4975                RateLimit(String),
4976                #[error("API error: {0}")]
4977                Api(String),
4978                #[error("Timeout error: {0}")]
4979                Timeout(String),
4980                #[error("Response body exceeded configured limit of {limit} bytes")]
4981                ResponseTooLarge { limit: usize },
4982                #[error("JSON serialization/deserialization error: {0}")]
4983                Json(#[from] serde_json::Error),
4984                #[error("Request error: {0}")]
4985                Request(reqwest::Error),
4986            }
4987
4988            impl From<reqwest::header::InvalidHeaderValue> for StreamingError {
4989                fn from(err: reqwest::header::InvalidHeaderValue) -> Self {
4990                    StreamingError::Api(format!("Invalid header value: {}", err))
4991                }
4992            }
4993
4994            impl From<reqwest::Error> for StreamingError {
4995                fn from(err: reqwest::Error) -> Self {
4996                    if err.is_timeout() {
4997                        StreamingError::Timeout(err.to_string())
4998                    } else if err.is_status() {
4999                        if let Some(status) = err.status() {
5000                            StreamingError::Http { status: status.as_u16() }
5001                        } else {
5002                            StreamingError::Connection(err.to_string())
5003                        }
5004                    } else {
5005                        StreamingError::Request(err)
5006                    }
5007                }
5008            }
5009        })
5010    }
5011
5012    /// Generate trait for a streaming endpoint
5013    fn generate_endpoint_trait(
5014        &self,
5015        endpoint: &crate::streaming::StreamingEndpoint,
5016        analysis: &SchemaAnalysis,
5017    ) -> Result<TokenStream> {
5018        use crate::streaming::HttpMethod;
5019
5020        let trait_name = format_ident!(
5021            "{}StreamingClient",
5022            self.to_rust_type_name(&endpoint.operation_id)
5023        );
5024        let method_name =
5025            format_ident!("stream_{}", self.to_rust_field_name(&endpoint.operation_id));
5026        let event_type =
5027            format_ident!("{}", self.resolve_streaming_event_type(endpoint, analysis)?);
5028
5029        // Generate method signature based on HTTP method
5030        let method_signature = match endpoint.http_method {
5031            HttpMethod::Get => {
5032                // Generate parameters from query_parameters
5033                let mut param_defs = Vec::new();
5034                for qp in &endpoint.query_parameters {
5035                    let param_name = format_ident!("{}", self.to_rust_field_name(&qp.name));
5036                    if qp.required {
5037                        param_defs.push(quote! { #param_name: &str });
5038                    } else {
5039                        param_defs.push(quote! { #param_name: Option<&str> });
5040                    }
5041                }
5042                quote! {
5043                    async fn #method_name(
5044                        &self,
5045                        #(#param_defs),*
5046                    ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error>;
5047                }
5048            }
5049            HttpMethod::Post => {
5050                // Find the request type for this operation
5051                let request_type = self
5052                    .find_request_type_for_operation(&endpoint.operation_id, analysis)
5053                    .unwrap_or_else(|| "serde_json::Value".to_string());
5054                let request_type_ident = if request_type.contains("::") {
5055                    let parts: Vec<&str> = request_type.split("::").collect();
5056                    let path_parts: Vec<_> = parts.iter().map(|p| format_ident!("{}", p)).collect();
5057                    quote! { #(#path_parts)::* }
5058                } else {
5059                    let ident = format_ident!("{}", request_type);
5060                    quote! { #ident }
5061                };
5062                quote! {
5063                    async fn #method_name(
5064                        &self,
5065                        request: #request_type_ident,
5066                    ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error>;
5067                }
5068            }
5069        };
5070
5071        Ok(quote! {
5072            /// Streaming client trait for this endpoint
5073            #[async_trait]
5074            pub trait #trait_name {
5075                type Error: std::error::Error + Send + Sync + 'static;
5076
5077                /// Stream events from the API
5078                #method_signature
5079            }
5080        })
5081    }
5082
5083    /// Generate streaming client implementation
5084    fn generate_streaming_client_impl(
5085        &self,
5086        streaming_config: &crate::streaming::StreamingConfig,
5087        analysis: &SchemaAnalysis,
5088    ) -> Result<TokenStream> {
5089        let client_name = format_ident!(
5090            "{}Client",
5091            self.to_rust_type_name(&streaming_config.client_module_name)
5092        );
5093
5094        // Generate struct fields
5095        // Always include custom_headers for flexibility (like HttpClient does)
5096        let mut struct_fields = vec![
5097            quote! { base_url: String },
5098            quote! { api_key: Option<String> },
5099            quote! { sse_client: SseClient },
5100            quote! { custom_headers: std::collections::BTreeMap<String, String> },
5101        ];
5102
5103        let has_optional_headers = !streaming_config
5104            .endpoints
5105            .iter()
5106            .all(|e| e.optional_headers.is_empty());
5107
5108        if has_optional_headers {
5109            struct_fields
5110                .push(quote! { optional_headers: std::collections::BTreeMap<String, String> });
5111        }
5112
5113        // Generate constructor
5114        // Use configured base URL as default, or fallback to generic example
5115        let default_base_url = if let Some(ref streaming_config) = self.config.streaming_config {
5116            streaming_config
5117                .endpoints
5118                .first()
5119                .and_then(|e| e.base_url.as_deref())
5120                .unwrap_or("https://api.example.com")
5121        } else {
5122            "https://api.example.com"
5123        };
5124        let max_response_body_bytes = self
5125            .config()
5126            .http_client_config
5127            .as_ref()
5128            .and_then(|http| http.max_response_body_bytes)
5129            .unwrap_or(8 * 1024 * 1024);
5130        let sse_client_initializer = if let Some(reconnect) = &streaming_config.reconnection_config
5131        {
5132            let max_retries = reconnect.max_retries;
5133            let initial_delay_ms = reconnect.initial_delay_ms;
5134            let max_delay_ms = reconnect.max_delay_ms;
5135            let backoff_multiplier = reconnect.backoff_multiplier;
5136            quote! {
5137                SseClient::new()
5138                    .with_max_error_body_bytes(#max_response_body_bytes)
5139                    .with_reconnect_options(SseReconnectOptions {
5140                        max_retries: #max_retries,
5141                        initial_retry_delay: std::time::Duration::from_millis(#initial_delay_ms),
5142                        max_retry_delay: std::time::Duration::from_millis(#max_delay_ms),
5143                        backoff_multiplier: #backoff_multiplier,
5144                    })
5145            }
5146        } else {
5147            quote! {
5148                SseClient::new()
5149                    .with_max_error_body_bytes(#max_response_body_bytes)
5150            }
5151        };
5152
5153        // Build constructor fields based on what the struct has
5154        let constructor_fields = if has_optional_headers {
5155            quote! {
5156                base_url: #default_base_url.to_string(),
5157                api_key: None,
5158                sse_client: #sse_client_initializer,
5159                custom_headers: std::collections::BTreeMap::new(),
5160                optional_headers: std::collections::BTreeMap::new(),
5161            }
5162        } else {
5163            quote! {
5164                base_url: #default_base_url.to_string(),
5165                api_key: None,
5166                sse_client: #sse_client_initializer,
5167                custom_headers: std::collections::BTreeMap::new(),
5168            }
5169        };
5170
5171        // Optional headers method only if the struct has the field
5172        let optional_headers_method = if has_optional_headers {
5173            quote! {
5174                /// Set optional headers for all requests
5175                pub fn set_optional_headers(&mut self, headers: std::collections::BTreeMap<String, String>) {
5176                    self.optional_headers = headers;
5177                }
5178            }
5179        } else {
5180            TokenStream::new()
5181        };
5182
5183        let constructor = quote! {
5184            impl #client_name {
5185                /// Create a new streaming client
5186                pub fn new() -> Self {
5187                    Self {
5188                        #constructor_fields
5189                    }
5190                }
5191
5192                /// Set the base URL for API requests
5193                pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
5194                    self.base_url = base_url.into();
5195                    self
5196                }
5197
5198                /// Set the API key for authentication
5199                pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
5200                    self.api_key = Some(api_key.into());
5201                    self
5202                }
5203
5204                /// Set the maximum number of error-response bytes buffered in memory.
5205                pub fn with_max_response_body_bytes(mut self, limit: usize) -> Self {
5206                    self.sse_client = self.sse_client.with_max_error_body_bytes(limit);
5207                    self
5208                }
5209
5210                /// Add a custom header to all requests
5211                pub fn with_header(
5212                    mut self,
5213                    name: impl Into<String>,
5214                    value: impl Into<String>,
5215                ) -> Self {
5216                    self.custom_headers.insert(name.into(), value.into());
5217                    self
5218                }
5219
5220                /// Set the HTTP client
5221                pub fn with_http_client(mut self, client: reqwest::Client) -> Self {
5222                    self.sse_client = self.sse_client.with_http_client(client);
5223                    self
5224                }
5225
5226                #optional_headers_method
5227            }
5228        };
5229
5230        // Generate trait implementations for each endpoint
5231        let mut trait_impls = Vec::new();
5232        for endpoint in &streaming_config.endpoints {
5233            let trait_impl = self.generate_endpoint_trait_impl(endpoint, &client_name, analysis)?;
5234            trait_impls.push(trait_impl);
5235        }
5236
5237        // Add Default implementation
5238        let default_impl = quote! {
5239            impl Default for #client_name {
5240                fn default() -> Self {
5241                    Self::new()
5242                }
5243            }
5244        };
5245
5246        Ok(quote! {
5247            /// Streaming client implementation
5248            #[derive(Debug, Clone)]
5249            pub struct #client_name {
5250                #(#struct_fields,)*
5251            }
5252
5253            #constructor
5254
5255            #default_impl
5256
5257            #(#trait_impls)*
5258        })
5259    }
5260
5261    /// Generate trait implementation for a specific endpoint
5262    fn generate_endpoint_trait_impl(
5263        &self,
5264        endpoint: &crate::streaming::StreamingEndpoint,
5265        client_name: &proc_macro2::Ident,
5266        analysis: &SchemaAnalysis,
5267    ) -> Result<TokenStream> {
5268        use crate::streaming::HttpMethod;
5269
5270        let trait_name = format_ident!(
5271            "{}StreamingClient",
5272            self.to_rust_type_name(&endpoint.operation_id)
5273        );
5274        let method_name =
5275            format_ident!("stream_{}", self.to_rust_field_name(&endpoint.operation_id));
5276        let event_type =
5277            format_ident!("{}", self.resolve_streaming_event_type(endpoint, analysis)?);
5278
5279        // Generate required headers
5280        let mut header_setup = Vec::new();
5281        for (name, value) in &endpoint.required_headers {
5282            header_setup.push(quote! {
5283                headers.insert(#name, HeaderValue::from_static(#value));
5284            });
5285        }
5286
5287        // Add authentication header
5288        // If auth_header is configured, use that; otherwise default to Bearer auth on Authorization header
5289        if let Some(auth_header) = &endpoint.auth_header {
5290            match auth_header {
5291                crate::streaming::AuthHeader::Bearer(header_name) => {
5292                    header_setup.push(quote! {
5293                        if let Some(ref api_key) = self.api_key {
5294                            headers.insert(#header_name, HeaderValue::from_str(&format!("Bearer {}", api_key))?);
5295                        }
5296                    });
5297                }
5298                crate::streaming::AuthHeader::ApiKey(header_name) => {
5299                    header_setup.push(quote! {
5300                        if let Some(ref api_key) = self.api_key {
5301                            headers.insert(#header_name, HeaderValue::from_str(api_key)?);
5302                        }
5303                    });
5304                }
5305            }
5306        } else {
5307            // Default: use api_key as Bearer token on Authorization header
5308            header_setup.push(quote! {
5309                if let Some(ref api_key) = self.api_key {
5310                    headers.insert("Authorization", HeaderValue::from_str(&format!("Bearer {}", api_key))?);
5311                }
5312            });
5313        }
5314
5315        // Always add custom_headers (like HttpClient does)
5316        header_setup.push(quote! {
5317            for (name, value) in &self.custom_headers {
5318                if let (Ok(header_name), Ok(header_value)) = (reqwest::header::HeaderName::from_bytes(name.as_bytes()), HeaderValue::from_str(value)) {
5319                    headers.insert(header_name, header_value);
5320                }
5321            }
5322        });
5323
5324        // Add optional headers (for endpoint-specific optional headers)
5325        if !endpoint.optional_headers.is_empty() {
5326            header_setup.push(quote! {
5327                for (key, value) in &self.optional_headers {
5328                    if let (Ok(header_name), Ok(header_value)) = (reqwest::header::HeaderName::from_bytes(key.as_bytes()), HeaderValue::from_str(value)) {
5329                        headers.insert(header_name, header_value);
5330                    }
5331                }
5332            });
5333        }
5334
5335        // Generate different code for GET vs POST
5336        match endpoint.http_method {
5337            HttpMethod::Get => self.generate_get_streaming_impl(
5338                endpoint,
5339                client_name,
5340                &trait_name,
5341                &method_name,
5342                &event_type,
5343                &header_setup,
5344            ),
5345            HttpMethod::Post => self.generate_post_streaming_impl(
5346                endpoint,
5347                client_name,
5348                &trait_name,
5349                &method_name,
5350                &event_type,
5351                &header_setup,
5352                analysis,
5353            ),
5354        }
5355    }
5356
5357    /// Generate streaming implementation for GET endpoints
5358    fn generate_get_streaming_impl(
5359        &self,
5360        endpoint: &crate::streaming::StreamingEndpoint,
5361        client_name: &proc_macro2::Ident,
5362        trait_name: &proc_macro2::Ident,
5363        method_name: &proc_macro2::Ident,
5364        event_type: &proc_macro2::Ident,
5365        header_setup: &[TokenStream],
5366    ) -> Result<TokenStream> {
5367        let path = &endpoint.path;
5368
5369        // Generate method parameters from query_parameters
5370        let mut param_defs = Vec::new();
5371        let mut query_params = Vec::new();
5372
5373        for qp in &endpoint.query_parameters {
5374            let param_name = format_ident!("{}", self.to_rust_field_name(&qp.name));
5375            let param_name_str = &qp.name;
5376
5377            if qp.required {
5378                param_defs.push(quote! { #param_name: &str });
5379                query_params.push(quote! {
5380                    url.query_pairs_mut().append_pair(#param_name_str, #param_name);
5381                });
5382            } else {
5383                param_defs.push(quote! { #param_name: Option<&str> });
5384                query_params.push(quote! {
5385                    if let Some(v) = #param_name {
5386                        url.query_pairs_mut().append_pair(#param_name_str, v);
5387                    }
5388                });
5389            }
5390        }
5391
5392        // Generate URL construction for GET
5393        let url_construction = quote! {
5394            let base_url = url::Url::parse(&self.base_url)
5395                .map_err(|e| StreamingError::Connection(format!("Invalid base URL: {}", e)))?;
5396            let path_to_join = #path.trim_start_matches('/');
5397            let mut url = base_url.join(path_to_join)
5398                .map_err(|e| StreamingError::Connection(format!("URL join error: {}", e)))?;
5399            #(#query_params)*
5400        };
5401
5402        let instrument_skip = quote! { #[instrument(skip(self), name = "streaming_get_request")] };
5403
5404        Ok(quote! {
5405            #[async_trait]
5406            impl #trait_name for #client_name {
5407                type Error = StreamingError;
5408
5409                #instrument_skip
5410                async fn #method_name(
5411                    &self,
5412                    #(#param_defs),*
5413                ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error> {
5414                    debug!("Starting streaming GET request");
5415
5416                    let mut headers = HeaderMap::new();
5417                    #(#header_setup)*
5418
5419                    #url_construction
5420                    let url_str = url.to_string();
5421                    debug!("Making streaming GET request to: {}", url_str);
5422
5423                    let request_builder = self.sse_client
5424                        .get(url_str)
5425                        .headers(headers);
5426
5427                    debug!("Creating SSE stream from request");
5428                    let stream = self
5429                        .sse_client
5430                        .stream::<#event_type>(request_builder)
5431                        .await?;
5432                    info!("SSE stream created successfully");
5433                    Ok(stream)
5434                }
5435            }
5436        })
5437    }
5438
5439    /// Generate streaming implementation for POST endpoints
5440    #[allow(clippy::too_many_arguments)]
5441    fn generate_post_streaming_impl(
5442        &self,
5443        endpoint: &crate::streaming::StreamingEndpoint,
5444        client_name: &proc_macro2::Ident,
5445        trait_name: &proc_macro2::Ident,
5446        method_name: &proc_macro2::Ident,
5447        event_type: &proc_macro2::Ident,
5448        header_setup: &[TokenStream],
5449        analysis: &SchemaAnalysis,
5450    ) -> Result<TokenStream> {
5451        let path = &endpoint.path;
5452
5453        // Find the request type for this operation
5454        let request_type = self
5455            .find_request_type_for_operation(&endpoint.operation_id, analysis)
5456            .unwrap_or_else(|| "serde_json::Value".to_string());
5457        let request_type_ident = if request_type.contains("::") {
5458            let parts: Vec<&str> = request_type.split("::").collect();
5459            let path_parts: Vec<_> = parts.iter().map(|p| format_ident!("{}", p)).collect();
5460            quote! { #(#path_parts)::* }
5461        } else {
5462            let ident = format_ident!("{}", request_type);
5463            quote! { #ident }
5464        };
5465
5466        // Generate URL construction for POST
5467        let url_construction = quote! {
5468            let base_url = url::Url::parse(&self.base_url)
5469                .map_err(|e| StreamingError::Connection(format!("Invalid base URL: {}", e)))?;
5470            let path_to_join = #path.trim_start_matches('/');
5471            let url = base_url.join(path_to_join)
5472                .map_err(|e| StreamingError::Connection(format!("URL join error: {}", e)))?
5473                .to_string();
5474        };
5475
5476        // Generate stream parameter setup (only for POST with stream_parameter)
5477        let stream_param = &endpoint.stream_parameter;
5478        let stream_setup = if stream_param.is_empty() {
5479            quote! {
5480                let streaming_request = request;
5481            }
5482        } else {
5483            quote! {
5484                // Ensure streaming is enabled
5485                let mut streaming_request = request;
5486                if let Ok(mut request_value) = serde_json::to_value(&streaming_request) {
5487                    if let Some(obj) = request_value.as_object_mut() {
5488                        obj.insert(#stream_param.to_string(), serde_json::Value::Bool(true));
5489                    }
5490                    streaming_request = serde_json::from_value(request_value)?;
5491                }
5492            }
5493        };
5494
5495        Ok(quote! {
5496            #[async_trait]
5497            impl #trait_name for #client_name {
5498                type Error = StreamingError;
5499
5500                #[instrument(skip(self, request), name = "streaming_post_request")]
5501                async fn #method_name(
5502                    &self,
5503                    request: #request_type_ident,
5504                ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error> {
5505                    debug!("Starting streaming POST request");
5506
5507                    #stream_setup
5508
5509                    let mut headers = HeaderMap::new();
5510                    #(#header_setup)*
5511
5512                    #url_construction
5513                    debug!("Making streaming POST request to: {}", url);
5514
5515                    let request_builder = self.sse_client
5516                        .post(&url)
5517                        .headers(headers)
5518                        .json(&streaming_request);
5519
5520                    debug!("Creating SSE stream from request");
5521                    let stream = self
5522                        .sse_client
5523                        .stream::<#event_type>(request_builder)
5524                        .await?;
5525                    info!("SSE stream created successfully");
5526                    Ok(stream)
5527                }
5528            }
5529        })
5530    }
5531
5532    /// Generate the reusable SSE transport module emitted as `sse.rs`.
5533    fn generate_sse_runtime(&self) -> Result<String> {
5534        let provenance_attribute = self.provenance_attribute();
5535        let error_types = self.generate_streaming_error_types()?;
5536        let parser = self.generate_sse_parser_utilities()?;
5537        let tokens = quote! {
5538            //! Generated SSE transport, framing, and JSON decoding support.
5539            //!
5540            //! This module is emitted only when SSE generation is enabled.
5541            #provenance_attribute
5542            #![allow(clippy::format_in_format_args)]
5543
5544            use futures_util::{Stream, StreamExt};
5545            use std::pin::Pin;
5546            use std::time::Duration;
5547            use tracing::debug;
5548
5549            #error_types
5550
5551            /// Reusable transport client for generated SSE operations.
5552            #[derive(Debug, Clone)]
5553            pub struct SseClient {
5554                http_client: reqwest::Client,
5555                max_error_body_bytes: usize,
5556                reconnect_options: Option<SseReconnectOptions>,
5557            }
5558
5559            impl SseClient {
5560                pub fn new() -> Self {
5561                    Self {
5562                        http_client: reqwest::Client::new(),
5563                        max_error_body_bytes: DEFAULT_MAX_SSE_ERROR_BODY_BYTES,
5564                        reconnect_options: None,
5565                    }
5566                }
5567
5568                pub fn with_http_client(mut self, client: reqwest::Client) -> Self {
5569                    self.http_client = client;
5570                    self
5571                }
5572
5573                pub fn with_max_error_body_bytes(mut self, limit: usize) -> Self {
5574                    self.max_error_body_bytes = limit;
5575                    self
5576                }
5577
5578                /// Enable automatic reconnection for [`Self::stream`].
5579                pub fn with_reconnect_options(mut self, options: SseReconnectOptions) -> Self {
5580                    self.reconnect_options = Some(options);
5581                    self
5582                }
5583
5584                pub fn get(&self, url: impl reqwest::IntoUrl) -> reqwest::RequestBuilder {
5585                    self.http_client.get(url)
5586                }
5587
5588                pub fn post(&self, url: impl reqwest::IntoUrl) -> reqwest::RequestBuilder {
5589                    self.http_client.post(url)
5590                }
5591
5592                pub async fn stream<T>(
5593                    &self,
5594                    request_builder: reqwest::RequestBuilder,
5595                ) -> Result<Pin<Box<dyn Stream<Item = Result<T, StreamingError>> + Send>>, StreamingError>
5596                where
5597                    T: serde::de::DeserializeOwned + Send + 'static,
5598                {
5599                    if let Some(options) = self.reconnect_options.clone() {
5600                        parse_sse_json_reconnecting_with_limit(
5601                            request_builder,
5602                            self.max_error_body_bytes,
5603                            options,
5604                        ).await
5605                    } else {
5606                        parse_sse_json_stream_with_limit(
5607                            request_builder,
5608                            self.max_error_body_bytes,
5609                        ).await
5610                    }
5611                }
5612
5613                /// Stream raw SSE events from one HTTP connection.
5614                pub async fn stream_raw(
5615                    &self,
5616                    request_builder: reqwest::RequestBuilder,
5617                ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<String>, StreamingError>> + Send>>, StreamingError> {
5618                    parse_sse_raw_stream_with_limit(request_builder, self.max_error_body_bytes).await
5619                }
5620
5621                /// Stream typed JSON SSE events from one HTTP connection.
5622                pub async fn stream_json<T>(
5623                    &self,
5624                    request_builder: reqwest::RequestBuilder,
5625                ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<T>, StreamingError>> + Send>>, StreamingError>
5626                where
5627                    T: serde::de::DeserializeOwned + Send + 'static,
5628                {
5629                    parse_sse_json_events_with_limit(request_builder, self.max_error_body_bytes).await
5630                }
5631
5632                /// Stream raw SSE events and reconnect retryable connections.
5633                pub async fn stream_raw_reconnecting(
5634                    &self,
5635                    request_builder: reqwest::RequestBuilder,
5636                ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<String>, StreamingError>> + Send>>, StreamingError> {
5637                    parse_sse_raw_reconnecting_with_limit(
5638                        request_builder,
5639                        self.max_error_body_bytes,
5640                        self.reconnect_options.clone().unwrap_or_default(),
5641                    ).await
5642                }
5643
5644                /// Stream typed JSON SSE events and reconnect retryable connections.
5645                pub async fn stream_json_reconnecting<T>(
5646                    &self,
5647                    request_builder: reqwest::RequestBuilder,
5648                ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<T>, StreamingError>> + Send>>, StreamingError>
5649                where
5650                    T: serde::de::DeserializeOwned + Send + 'static,
5651                {
5652                    parse_sse_json_reconnecting_events_with_limit(
5653                        request_builder,
5654                        self.max_error_body_bytes,
5655                        self.reconnect_options.clone().unwrap_or_default(),
5656                    ).await
5657                }
5658            }
5659
5660            impl Default for SseClient {
5661                fn default() -> Self {
5662                    Self::new()
5663                }
5664            }
5665
5666            #parser
5667        };
5668        let syntax_tree = syn::parse2::<syn::File>(tokens).map_err(|error| {
5669            GeneratorError::CodeGenError(format!("Failed to parse generated sse.rs: {error}"))
5670        })?;
5671        Ok(prettyplease::unparse(&syntax_tree))
5672    }
5673
5674    /// Generate the standalone SSE framing and JSON parsing utilities.
5675    fn generate_sse_parser_utilities(&self) -> Result<TokenStream> {
5676        Ok(quote! {
5677            /// Default upper bound for an SSE error response buffered in memory.
5678            pub const DEFAULT_MAX_SSE_ERROR_BODY_BYTES: usize = 8 * 1024 * 1024;
5679
5680            async fn __read_bounded_streaming_error_body(
5681                mut response: reqwest::Response,
5682                limit: usize,
5683            ) -> Result<Vec<u8>, StreamingError> {
5684                let mut body = Vec::new();
5685                while let Some(chunk) = response.chunk().await? {
5686                    let next_len = body.len().checked_add(chunk.len());
5687                    if next_len.is_none_or(|next_len| next_len > limit) {
5688                        return Err(StreamingError::ResponseTooLarge { limit });
5689                    }
5690                    body.extend_from_slice(&chunk);
5691                }
5692                Ok(body)
5693            }
5694
5695            /// A decoded SSE event with its transport metadata preserved.
5696            #[derive(Debug, Clone, PartialEq, Eq)]
5697            pub struct SseEvent<T> {
5698                /// Event name, or `message` when the server omitted `event:`.
5699                pub event: String,
5700                /// Raw or deserialized event payload.
5701                pub data: T,
5702                /// Most recent event ID, used to resume a reconnected stream.
5703                pub id: Option<String>,
5704                /// Server-supplied reconnection delay on this event, if present.
5705                pub retry: Option<Duration>,
5706            }
5707
5708            /// Controls automatic SSE reconnection behavior.
5709            #[derive(Debug, Clone)]
5710            pub struct SseReconnectOptions {
5711                /// Maximum consecutive reconnection attempts.
5712                pub max_retries: u32,
5713                /// Delay before the first reconnection when the server did not send `retry:`.
5714                pub initial_retry_delay: Duration,
5715                /// Upper bound for client-computed and server-supplied delays.
5716                pub max_retry_delay: Duration,
5717                /// Exponential backoff multiplier for consecutive failures.
5718                pub backoff_multiplier: f64,
5719            }
5720
5721            impl Default for SseReconnectOptions {
5722                fn default() -> Self {
5723                    Self {
5724                        max_retries: 3,
5725                        initial_retry_delay: Duration::from_secs(3),
5726                        max_retry_delay: Duration::from_secs(30),
5727                        backoff_multiplier: 2.0,
5728                    }
5729                }
5730            }
5731
5732            impl SseReconnectOptions {
5733                fn delay(&self, attempt: u32, server_retry: Option<Duration>) -> Duration {
5734                    if let Some(delay) = server_retry {
5735                        return delay.min(self.max_retry_delay);
5736                    }
5737                    let multiplier = self.backoff_multiplier.max(1.0);
5738                    let millis = self.initial_retry_delay.as_millis() as f64
5739                        * multiplier.powi(attempt.min(63) as i32);
5740                    Duration::from_millis(
5741                        millis.min(self.max_retry_delay.as_millis() as f64) as u64,
5742                    )
5743                }
5744            }
5745
5746            #[derive(Default)]
5747            struct __SseDecoder {
5748                line: Vec<u8>,
5749                event: String,
5750                data: Vec<String>,
5751                last_event_id: Option<String>,
5752                retry_delay: Option<Duration>,
5753                event_retry: Option<Duration>,
5754                saw_carriage_return: bool,
5755            }
5756
5757            impl __SseDecoder {
5758                fn feed(
5759                    &mut self,
5760                    chunk: &[u8],
5761                ) -> Vec<Result<SseEvent<String>, StreamingError>> {
5762                    let mut messages = Vec::new();
5763                    for &byte in chunk {
5764                        if self.saw_carriage_return {
5765                            self.saw_carriage_return = false;
5766                            if byte == b'\n' {
5767                                continue;
5768                            }
5769                        }
5770
5771                        match byte {
5772                            b'\n' => self.finish_line(&mut messages),
5773                            b'\r' => {
5774                                self.finish_line(&mut messages);
5775                                self.saw_carriage_return = true;
5776                            }
5777                            _ => self.line.push(byte),
5778                        }
5779                    }
5780                    messages
5781                }
5782
5783                fn finish(&mut self) -> Vec<Result<SseEvent<String>, StreamingError>> {
5784                    let mut messages = Vec::new();
5785                    if !self.line.is_empty() {
5786                        self.finish_line(&mut messages);
5787                    }
5788                    self.dispatch(&mut messages);
5789                    messages
5790                }
5791
5792                fn finish_line(
5793                    &mut self,
5794                    messages: &mut Vec<Result<SseEvent<String>, StreamingError>>,
5795                ) {
5796                    let line = std::mem::take(&mut self.line);
5797                    let line = match String::from_utf8(line) {
5798                        Ok(line) => line,
5799                        Err(error) => {
5800                            messages.push(Err(StreamingError::Parsing(format!(
5801                                "SSE line is not valid UTF-8: {}",
5802                                error
5803                            ))));
5804                            return;
5805                        }
5806                    };
5807
5808                    if line.is_empty() {
5809                        self.dispatch(messages);
5810                        return;
5811                    }
5812                    if line.starts_with(':') {
5813                        return;
5814                    }
5815
5816                    let (field, value) = line
5817                        .split_once(':')
5818                        .map_or((line.as_str(), ""), |(field, value)| {
5819                            (field, value.strip_prefix(' ').unwrap_or(value))
5820                        });
5821                    match field {
5822                        "event" => self.event = value.to_string(),
5823                        "data" => self.data.push(value.to_string()),
5824                        "id" if !value.contains('\0') => {
5825                            self.last_event_id = (!value.is_empty()).then(|| value.to_string());
5826                        }
5827                        "retry" if value.bytes().all(|byte| byte.is_ascii_digit()) => {
5828                            if let Ok(milliseconds) = value.parse::<u64>() {
5829                                let delay = Duration::from_millis(milliseconds);
5830                                self.retry_delay = Some(delay);
5831                                self.event_retry = Some(delay);
5832                            }
5833                        }
5834                        _ => {}
5835                    }
5836                }
5837
5838                fn dispatch(
5839                    &mut self,
5840                    messages: &mut Vec<Result<SseEvent<String>, StreamingError>>,
5841                ) {
5842                    if self.data.is_empty() {
5843                        self.event.clear();
5844                        self.event_retry = None;
5845                        return;
5846                    }
5847                    messages.push(Ok(SseEvent {
5848                        event: if self.event.is_empty() {
5849                            "message".to_string()
5850                        } else {
5851                            std::mem::take(&mut self.event)
5852                        },
5853                        data: std::mem::take(&mut self.data).join("\n"),
5854                        id: self.last_event_id.clone(),
5855                        retry: self.event_retry.take(),
5856                    }));
5857                    self.event.clear();
5858                }
5859
5860                fn reset_for_reconnect(&mut self) {
5861                    self.line.clear();
5862                    self.event.clear();
5863                    self.data.clear();
5864                    self.event_retry = None;
5865                    self.saw_carriage_return = false;
5866                }
5867            }
5868
5869            fn __deserialize_sse_event<T>(
5870                event: SseEvent<String>,
5871            ) -> Option<Result<SseEvent<T>, StreamingError>>
5872            where
5873                T: serde::de::DeserializeOwned,
5874            {
5875                if event.data.trim() == "[DONE]" {
5876                    return None;
5877                }
5878                if event.event == "ping" {
5879                    debug!("Received SSE ping event, skipping");
5880                    return None;
5881                }
5882                if event.data.trim().is_empty() {
5883                    debug!("Empty SSE data, skipping");
5884                    return None;
5885                }
5886
5887                let json_value = match serde_json::from_str::<serde_json::Value>(&event.data) {
5888                    Ok(value) => value,
5889                    Err(error) => {
5890                        return Some(Err(StreamingError::Parsing(format!(
5891                            "SSE event is not valid JSON: {} ({})",
5892                            event.data, error
5893                        ))));
5894                    }
5895                };
5896                let is_ping = json_value
5897                    .get("event")
5898                    .or_else(|| json_value.get("type"))
5899                    .and_then(serde_json::Value::as_str)
5900                    .is_some_and(|event| event == "ping");
5901                if is_ping {
5902                    debug!("Received ping event in JSON data, skipping");
5903                    return None;
5904                }
5905
5906                Some(
5907                    serde_json::from_value::<T>(json_value)
5908                        .map(|data| SseEvent {
5909                            event: event.event.clone(),
5910                            data,
5911                            id: event.id.clone(),
5912                            retry: event.retry,
5913                        })
5914                        .map_err(|error| StreamingError::Parsing(format!(
5915                            "Failed to parse SSE event: {} (raw: {}, event: {})",
5916                            error, event.data, event.event
5917                        ))),
5918                )
5919            }
5920
5921            /// Parse an SSE response without an external EventSource wrapper.
5922            pub async fn parse_sse_stream<T>(
5923                request_builder: reqwest::RequestBuilder
5924            ) -> Result<Pin<Box<dyn Stream<Item = Result<T, StreamingError>> + Send>>, StreamingError>
5925            where
5926                T: serde::de::DeserializeOwned + Send + 'static,
5927            {
5928                parse_sse_json_stream_with_limit(
5929                    request_builder,
5930                    DEFAULT_MAX_SSE_ERROR_BODY_BYTES,
5931                ).await
5932            }
5933
5934            struct __SseOpenError {
5935                error: StreamingError,
5936                retryable: bool,
5937            }
5938
5939            async fn __open_sse_response(
5940                request_builder: reqwest::RequestBuilder,
5941                max_response_body_bytes: usize,
5942            ) -> Result<reqwest::Response, __SseOpenError> {
5943                let response = request_builder.send().await.map_err(|error| __SseOpenError {
5944                    error: error.into(),
5945                    retryable: true,
5946                })?;
5947                if !response.status().is_success() {
5948                    let status = response.status();
5949                    let retryable = status.as_u16() == 429 || status.is_server_error();
5950                    let error = match __read_bounded_streaming_error_body(
5951                        response,
5952                        max_response_body_bytes,
5953                    ).await {
5954                        Ok(body) => StreamingError::Connection(format!(
5955                            "HTTP {} error: {}",
5956                            status.as_u16(),
5957                            String::from_utf8_lossy(&body)
5958                        )),
5959                        Err(error) => error,
5960                    };
5961                    return Err(__SseOpenError { error, retryable });
5962                }
5963
5964                let content_type = response
5965                    .headers()
5966                    .get(reqwest::header::CONTENT_TYPE)
5967                    .and_then(|value| value.to_str().ok())
5968                    .unwrap_or_default();
5969                if !content_type
5970                    .split(';')
5971                    .next()
5972                    .is_some_and(|value| value.trim().eq_ignore_ascii_case("text/event-stream"))
5973                {
5974                    let error = StreamingError::Parsing(format!(
5975                        "Expected text/event-stream response, received {}",
5976                        if content_type.is_empty() { "no Content-Type" } else { content_type }
5977                    ));
5978                    return Err(__SseOpenError { error, retryable: false });
5979                }
5980
5981                debug!("SSE connection opened");
5982                Ok(response)
5983            }
5984
5985            fn __raw_response_stream(
5986                response: reqwest::Response,
5987            ) -> Pin<Box<dyn Stream<Item = Result<SseEvent<String>, StreamingError>> + Send>> {
5988                let stream = futures_util::stream::unfold(
5989                    (
5990                        response,
5991                        __SseDecoder::default(),
5992                        std::collections::VecDeque::<Result<SseEvent<String>, StreamingError>>::new(),
5993                        false,
5994                    ),
5995                    |(mut response, mut decoder, mut pending, mut done)| async move {
5996                        loop {
5997                            if let Some(item) = pending.pop_front() {
5998                                return Some((item, (response, decoder, pending, done)));
5999                            }
6000                            if done {
6001                                debug!("SSE stream completed normally");
6002                                return None;
6003                            }
6004
6005                            match response.chunk().await {
6006                                Ok(Some(chunk)) => {
6007                                    for event in decoder.feed(&chunk) {
6008                                        let is_done = event
6009                                            .as_ref()
6010                                            .is_ok_and(|event| event.data.trim() == "[DONE]");
6011                                        pending.push_back(event);
6012                                        if is_done {
6013                                            done = true;
6014                                            break;
6015                                        }
6016                                    }
6017                                }
6018                                Err(error) => {
6019                                    done = true;
6020                                    pending.push_back(Err(error.into()));
6021                                }
6022                                Ok(None) => {
6023                                    done = true;
6024                                    for event in decoder.finish() {
6025                                        pending.push_back(event);
6026                                    }
6027                                }
6028                            }
6029                        }
6030                    }
6031                );
6032
6033                Box::pin(stream)
6034            }
6035
6036            async fn parse_sse_raw_stream_with_limit(
6037                request_builder: reqwest::RequestBuilder,
6038                max_response_body_bytes: usize,
6039            ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<String>, StreamingError>> + Send>>, StreamingError> {
6040                Ok(match __open_sse_response(request_builder, max_response_body_bytes).await {
6041                    Ok(response) => __raw_response_stream(response),
6042                    Err(error) => Box::pin(futures_util::stream::once(async move { Err(error.error) })),
6043                })
6044            }
6045
6046            fn __json_event_stream<T>(
6047                raw: Pin<Box<dyn Stream<Item = Result<SseEvent<String>, StreamingError>> + Send>>,
6048            ) -> Pin<Box<dyn Stream<Item = Result<SseEvent<T>, StreamingError>> + Send>>
6049            where
6050                T: serde::de::DeserializeOwned + Send + 'static,
6051            {
6052                Box::pin(raw.filter_map(|event| async move {
6053                    match event {
6054                        Ok(event) => __deserialize_sse_event(event),
6055                        Err(error) => Some(Err(error)),
6056                    }
6057                }))
6058            }
6059
6060            async fn parse_sse_json_events_with_limit<T>(
6061                request_builder: reqwest::RequestBuilder,
6062                max_response_body_bytes: usize,
6063            ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<T>, StreamingError>> + Send>>, StreamingError>
6064            where
6065                T: serde::de::DeserializeOwned + Send + 'static,
6066            {
6067                Ok(__json_event_stream(
6068                    parse_sse_raw_stream_with_limit(request_builder, max_response_body_bytes).await?,
6069                ))
6070            }
6071
6072            async fn parse_sse_json_stream_with_limit<T>(
6073                request_builder: reqwest::RequestBuilder,
6074                max_response_body_bytes: usize,
6075            ) -> Result<Pin<Box<dyn Stream<Item = Result<T, StreamingError>> + Send>>, StreamingError>
6076            where
6077                T: serde::de::DeserializeOwned + Send + 'static,
6078            {
6079                let events = parse_sse_json_events_with_limit(request_builder, max_response_body_bytes).await?;
6080                Ok(Box::pin(events.map(|event| event.map(|event| event.data))))
6081            }
6082
6083            struct __ReconnectState {
6084                request: reqwest::RequestBuilder,
6085                response: Option<reqwest::Response>,
6086                decoder: __SseDecoder,
6087                pending: std::collections::VecDeque<Result<SseEvent<String>, StreamingError>>,
6088                options: SseReconnectOptions,
6089                max_response_body_bytes: usize,
6090                attempts: u32,
6091                wait_before_open: bool,
6092                done: bool,
6093            }
6094
6095            async fn parse_sse_raw_reconnecting_with_limit(
6096                request_builder: reqwest::RequestBuilder,
6097                max_response_body_bytes: usize,
6098                options: SseReconnectOptions,
6099            ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<String>, StreamingError>> + Send>>, StreamingError> {
6100                if request_builder.try_clone().is_none() {
6101                    return Err(StreamingError::Connection(
6102                        "SSE reconnection requires a cloneable request body".to_string(),
6103                    ));
6104                }
6105
6106                let stream = futures_util::stream::unfold(
6107                    __ReconnectState {
6108                        request: request_builder,
6109                        response: None,
6110                        decoder: __SseDecoder::default(),
6111                        pending: std::collections::VecDeque::new(),
6112                        options,
6113                        max_response_body_bytes,
6114                        attempts: 0,
6115                        wait_before_open: false,
6116                        done: false,
6117                    },
6118                    |mut state| async move {
6119                        loop {
6120                            if let Some(item) = state.pending.pop_front() {
6121                                return Some((item, state));
6122                            }
6123                            if state.done {
6124                                return None;
6125                            }
6126
6127                            if state.response.is_none() {
6128                                if state.wait_before_open {
6129                                    let delay = state.options.delay(
6130                                        state.attempts.saturating_sub(1),
6131                                        state.decoder.retry_delay,
6132                                    );
6133                                    debug!(?delay, attempt = state.attempts, "Reconnecting SSE stream");
6134                                    futures_timer::Delay::new(delay).await;
6135                                    state.wait_before_open = false;
6136                                }
6137
6138                                let mut request = state.request.try_clone().expect("request clone checked");
6139                                if let Some(last_event_id) = state.decoder.last_event_id.as_deref() {
6140                                    request = request.header("Last-Event-ID", last_event_id);
6141                                }
6142                                match __open_sse_response(request, state.max_response_body_bytes).await {
6143                                    Ok(response) => state.response = Some(response),
6144                                    Err(error) if error.retryable && state.attempts < state.options.max_retries => {
6145                                        state.attempts += 1;
6146                                        state.wait_before_open = true;
6147                                        continue;
6148                                    }
6149                                    Err(error) => {
6150                                        state.done = true;
6151                                        state.pending.push_back(Err(error.error));
6152                                        continue;
6153                                    }
6154                                }
6155                            }
6156
6157                            let next = state.response.as_mut().expect("response opened").chunk().await;
6158                            match next {
6159                                Ok(Some(chunk)) => {
6160                                    let events = state.decoder.feed(&chunk);
6161                                    if !events.is_empty() {
6162                                        state.attempts = 0;
6163                                    }
6164                                    for event in events {
6165                                        let is_done = event
6166                                            .as_ref()
6167                                            .is_ok_and(|event| event.data.trim() == "[DONE]");
6168                                        state.pending.push_back(event);
6169                                        if is_done {
6170                                            state.done = true;
6171                                            state.response = None;
6172                                            break;
6173                                        }
6174                                    }
6175                                }
6176                                Ok(None) => {
6177                                    let events = state.decoder.finish();
6178                                    if !events.is_empty() {
6179                                        state.attempts = 0;
6180                                    }
6181                                    for event in events {
6182                                        let is_done = event
6183                                            .as_ref()
6184                                            .is_ok_and(|event| event.data.trim() == "[DONE]");
6185                                        state.pending.push_back(event);
6186                                        if is_done {
6187                                            state.done = true;
6188                                            break;
6189                                        }
6190                                    }
6191                                    state.response = None;
6192                                    state.decoder.reset_for_reconnect();
6193                                    if !state.done {
6194                                        if state.attempts < state.options.max_retries {
6195                                            state.attempts += 1;
6196                                            state.wait_before_open = true;
6197                                        } else {
6198                                            state.done = true;
6199                                        }
6200                                    }
6201                                }
6202                                Err(error) => {
6203                                    state.response = None;
6204                                    state.decoder.reset_for_reconnect();
6205                                    if state.attempts < state.options.max_retries {
6206                                        state.attempts += 1;
6207                                        state.wait_before_open = true;
6208                                    } else {
6209                                        state.done = true;
6210                                        state.pending.push_back(Err(error.into()));
6211                                    }
6212                                }
6213                            }
6214                        }
6215                    },
6216                );
6217                Ok(Box::pin(stream))
6218            }
6219
6220            async fn parse_sse_json_reconnecting_events_with_limit<T>(
6221                request_builder: reqwest::RequestBuilder,
6222                max_response_body_bytes: usize,
6223                options: SseReconnectOptions,
6224            ) -> Result<Pin<Box<dyn Stream<Item = Result<SseEvent<T>, StreamingError>> + Send>>, StreamingError>
6225            where
6226                T: serde::de::DeserializeOwned + Send + 'static,
6227            {
6228                Ok(__json_event_stream(
6229                    parse_sse_raw_reconnecting_with_limit(
6230                        request_builder,
6231                        max_response_body_bytes,
6232                        options,
6233                    ).await?,
6234                ))
6235            }
6236
6237            async fn parse_sse_json_reconnecting_with_limit<T>(
6238                request_builder: reqwest::RequestBuilder,
6239                max_response_body_bytes: usize,
6240                options: SseReconnectOptions,
6241            ) -> Result<Pin<Box<dyn Stream<Item = Result<T, StreamingError>> + Send>>, StreamingError>
6242            where
6243                T: serde::de::DeserializeOwned + Send + 'static,
6244            {
6245                let events = parse_sse_json_reconnecting_events_with_limit(
6246                    request_builder,
6247                    max_response_body_bytes,
6248                    options,
6249                ).await?;
6250                Ok(Box::pin(events.map(|event| event.map(|event| event.data))))
6251            }
6252        })
6253    }
6254
6255    /// Generate reconnection utilities
6256    fn generate_reconnection_utilities(
6257        &self,
6258        reconnect_config: &crate::streaming::ReconnectionConfig,
6259    ) -> Result<TokenStream> {
6260        let max_retries = reconnect_config.max_retries;
6261        let initial_delay = reconnect_config.initial_delay_ms;
6262        let max_delay = reconnect_config.max_delay_ms;
6263        let backoff_multiplier = reconnect_config.backoff_multiplier;
6264
6265        Ok(quote! {
6266            /// Reconnection configuration and utilities
6267            #[derive(Debug, Clone)]
6268            pub struct ReconnectionManager {
6269                max_retries: u32,
6270                initial_delay_ms: u64,
6271                max_delay_ms: u64,
6272                backoff_multiplier: f64,
6273                current_attempt: u32,
6274            }
6275
6276            impl ReconnectionManager {
6277                /// Create a new reconnection manager
6278                pub fn new() -> Self {
6279                    Self {
6280                        max_retries: #max_retries,
6281                        initial_delay_ms: #initial_delay,
6282                        max_delay_ms: #max_delay,
6283                        backoff_multiplier: #backoff_multiplier,
6284                        current_attempt: 0,
6285                    }
6286                }
6287
6288                /// Check if we should retry the connection
6289                pub fn should_retry(&self) -> bool {
6290                    self.current_attempt < self.max_retries
6291                }
6292
6293                /// Get the delay for the next retry attempt
6294                pub fn next_retry_delay(&mut self) -> Duration {
6295                    if !self.should_retry() {
6296                        return Duration::from_secs(0);
6297                    }
6298
6299                    let delay_ms = (self.initial_delay_ms as f64
6300                        * self.backoff_multiplier.powi(self.current_attempt as i32)) as u64;
6301                    let delay_ms = delay_ms.min(self.max_delay_ms);
6302
6303                    self.current_attempt += 1;
6304                    Duration::from_millis(delay_ms)
6305                }
6306
6307                /// Reset the retry counter after a successful connection
6308                pub fn reset(&mut self) {
6309                    self.current_attempt = 0;
6310                }
6311
6312                /// Get the current attempt number
6313                pub fn current_attempt(&self) -> u32 {
6314                    self.current_attempt
6315                }
6316            }
6317
6318            impl Default for ReconnectionManager {
6319                fn default() -> Self {
6320                    Self::new()
6321                }
6322            }
6323        })
6324    }
6325}