Skip to main content

openapi_to_rust/
generator.rs

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