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