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