Skip to main content

openapi_to_rust/
generator.rs

1use crate::{GeneratorError, Result, analysis::SchemaAnalysis, streaming::StreamingConfig};
2use proc_macro2::TokenStream;
3use quote::{format_ident, quote};
4use std::collections::BTreeMap;
5use std::path::PathBuf;
6
7/// Parse a Rust type string (possibly with generics, e.g.
8/// `chrono::DateTime<chrono::Utc>`) into a `TokenStream`. The pre-Q2
9/// ad-hoc `::`-splitter choked on `<` and `>`; `syn::parse_str` handles
10/// every valid type expression. Errors here mean the [`TypeMapper`]
11/// produced a string that doesn't parse as a Rust type — a generator
12/// bug, surfaced as a `GeneratorError::CodeGenError`.
13///
14/// [`TypeMapper`]: crate::type_mapping::TypeMapper
15fn parse_rust_type(rust_type: &str) -> Result<TokenStream> {
16    let parsed: syn::Type = syn::parse_str(rust_type).map_err(|e| {
17        GeneratorError::CodeGenError(format!(
18            "TypeMapper produced un-parseable type `{rust_type}`: {e}"
19        ))
20    })?;
21    Ok(quote! { #parsed })
22}
23
24/// Q2.4 — render OpenAPI constraint annotations as a single-line
25/// human-readable doc comment, e.g.
26///   "Constraint: minimum=0, maximum=100, pattern=`^foo$`"
27///
28/// The pattern is wrapped in backticks so backticks/braces inside
29/// it don't trip prettyplease/rustdoc parsing. Triple-slash and
30/// `*/` sequences are escaped so embedded patterns can't terminate
31/// the surrounding doc comment / block comment.
32fn format_constraints_doc(c: &crate::analysis::PropertyConstraints) -> String {
33    let mut parts: Vec<String> = Vec::new();
34
35    if let Some(v) = c.minimum {
36        parts.push(format!("minimum={}", strip_trailing_zero(v)));
37    }
38    if let Some(v) = c.maximum {
39        parts.push(format!("maximum={}", strip_trailing_zero(v)));
40    }
41    if let Some(v) = c.exclusive_minimum {
42        parts.push(format!("exclusiveMinimum={}", strip_trailing_zero(v)));
43    }
44    if let Some(v) = c.exclusive_maximum {
45        parts.push(format!("exclusiveMaximum={}", strip_trailing_zero(v)));
46    }
47    if let Some(v) = c.multiple_of {
48        parts.push(format!("multipleOf={}", strip_trailing_zero(v)));
49    }
50    if let Some(v) = c.min_length {
51        parts.push(format!("minLength={v}"));
52    }
53    if let Some(v) = c.max_length {
54        parts.push(format!("maxLength={v}"));
55    }
56    if let Some(v) = c.min_items {
57        parts.push(format!("minItems={v}"));
58    }
59    if let Some(v) = c.max_items {
60        parts.push(format!("maxItems={v}"));
61    }
62    if c.unique_items == Some(true) {
63        parts.push("uniqueItems=true".to_string());
64    }
65    if let Some(p) = &c.pattern {
66        // Insert a zero-width-space inside `///` and `*/` so they
67        // can't terminate the surrounding doc/block comment. Using
68        // the `\u{200B}` escape (vs. a literal U+200B) keeps clippy's
69        // `invisible_characters` lint happy.
70        let safe = p.replace("///", "/\u{200B}//").replace("*/", "*\u{200B}/");
71        parts.push(format!("pattern=`{safe}`"));
72    }
73
74    format!("Constraint: {}", parts.join(", "))
75}
76
77/// `1.0` and `1` should both render as `1` in doc comments.
78/// `1.5` stays `1.5`.
79fn strip_trailing_zero(v: f64) -> String {
80    if v.fract() == 0.0 && v.is_finite() {
81        format!("{}", v as i64)
82    } else {
83        format!("{v}")
84    }
85}
86
87/// Info about schemas that are variants in discriminated unions
88#[derive(Clone)]
89struct DiscriminatedVariantInfo {
90    /// The discriminator field name (e.g., "type")
91    discriminator_field: String,
92    /// The const value of the discriminator (e.g., "text")
93    discriminator_value: String,
94    /// Whether the parent union is untagged
95    is_parent_untagged: bool,
96}
97
98#[derive(Debug, Clone)]
99pub struct GeneratorConfig {
100    /// Path to OpenAPI specification file
101    pub spec_path: PathBuf,
102    /// Output directory for generated code (e.g., "src/gen")
103    pub output_dir: PathBuf,
104    /// Informational label for the generated module. Does NOT pick
105    /// the on-disk directory (that's `output_dir`) or the Rust module
106    /// path the user mounts the tree at — both of those are the
107    /// user's choice. The label is surfaced in the generated mod.rs
108    /// header as a hint and is otherwise used only by the streaming
109    /// codegen for naming the SSE client module.
110    pub module_name: String,
111    /// Enable SSE streaming client generation
112    pub enable_sse_client: bool,
113    /// Enable async HTTP client generation
114    pub enable_async_client: bool,
115    /// Enable Specta type derives for frontend integration
116    pub enable_specta: bool,
117    /// Custom type mappings
118    pub type_mappings: BTreeMap<String, String>,
119    /// Optional streaming configuration for SSE client generation
120    pub streaming_config: Option<StreamingConfig>,
121    /// Fields that should be treated as nullable even if not marked in the spec
122    /// Format: "SchemaName.fieldName" -> true
123    pub nullable_field_overrides: BTreeMap<String, bool>,
124    /// String-enum schemas that should be rendered as extensible (with a
125    /// `Custom(String)` fallback variant) instead of closed enums. Useful when
126    /// the spec declares a fixed set of values but the API actually returns
127    /// values outside that set (real-world drift: Cloudflare's r2_bucket_location
128    /// declares lowercase but returns uppercase).
129    /// Format: "SchemaName" -> true
130    pub extensible_enum_overrides: BTreeMap<String, bool>,
131    /// Additional schema extension files to merge into the main spec
132    /// These files will be merged additively using simple JSON object merging
133    pub schema_extensions: Vec<PathBuf>,
134    /// HTTP client configuration
135    pub http_client_config: Option<crate::http_config::HttpClientConfig>,
136    /// Retry configuration for HTTP requests
137    pub retry_config: Option<crate::http_config::RetryConfig>,
138    /// Enable request/response tracing
139    pub tracing_enabled: bool,
140    /// Authentication configuration
141    pub auth_config: Option<crate::http_config::AuthConfig>,
142    /// Enable operation registry generation (static metadata for CLI/proxy routing)
143    pub enable_registry: bool,
144    /// Generate only the operation registry (skip types, client, streaming)
145    pub registry_only: bool,
146    /// Per-format type-mapping strategies driven by the `[generator.types]`
147    /// TOML section. Q2.0 introduces this field; with the default value
148    /// every mapping preserves pre-refactor behavior.
149    pub types: crate::type_mapping::TypeMappingConfig,
150    /// Opt-in server codegen scope. `None` ⇒ emit no server code.
151    /// Set by the `[server]` section in the TOML config.
152    pub server: Option<crate::config::ServerSection>,
153}
154
155impl Default for GeneratorConfig {
156    fn default() -> Self {
157        Self {
158            spec_path: "openapi.json".into(),
159            output_dir: "src/gen".into(),
160            module_name: "api_types".to_string(),
161            enable_sse_client: true,
162            enable_async_client: true,
163            enable_specta: false,
164            type_mappings: default_type_mappings(),
165            streaming_config: None,
166            nullable_field_overrides: BTreeMap::new(),
167            extensible_enum_overrides: BTreeMap::new(),
168            schema_extensions: Vec::new(),
169            http_client_config: None,
170            retry_config: None,
171            tracing_enabled: true,
172            auth_config: None,
173            enable_registry: false,
174            registry_only: false,
175            types: crate::type_mapping::TypeMappingConfig::default(),
176            server: None,
177        }
178    }
179}
180
181pub fn default_type_mappings() -> BTreeMap<String, String> {
182    let mut mappings = BTreeMap::new();
183    mappings.insert("integer".to_string(), "i64".to_string());
184    mappings.insert("number".to_string(), "f64".to_string());
185    mappings.insert("string".to_string(), "String".to_string());
186    mappings.insert("boolean".to_string(), "bool".to_string());
187    mappings
188}
189
190/// Represents a generated file
191#[derive(Debug, Clone)]
192pub struct GeneratedFile {
193    /// Relative path from output directory (e.g., "types.rs", "streaming.rs")
194    pub path: PathBuf,
195    /// Generated Rust code content
196    pub content: String,
197}
198
199/// Result of code generation containing multiple files
200#[derive(Debug, Clone)]
201pub struct GenerationResult {
202    /// All generated files
203    pub files: Vec<GeneratedFile>,
204    /// Generated mod.rs content that exports all modules
205    pub mod_file: GeneratedFile,
206    /// Optional crates the generated code references (chrono, uuid,
207    /// url, …) — populated from the analyzer's TypeMapper
208    /// used-features set. The CLI uses this to write
209    /// `REQUIRED_DEPS.toml` next to the generated module and to
210    /// print a stderr summary so users know exactly what to add to
211    /// their `Cargo.toml`. Empty when no typed-scalar crates were
212    /// referenced.
213    pub required_deps: Vec<crate::type_mapping::DepRequirement>,
214}
215
216pub struct CodeGenerator {
217    config: GeneratorConfig,
218}
219
220impl CodeGenerator {
221    pub fn new(config: GeneratorConfig) -> Self {
222        Self { config }
223    }
224
225    /// Get reference to the generator configuration
226    pub fn config(&self) -> &GeneratorConfig {
227        &self.config
228    }
229
230    /// Generate all files for the API
231    pub fn generate_all(&self, analysis: &mut SchemaAnalysis) -> Result<GenerationResult> {
232        let mut files = Vec::new();
233
234        if !self.config.registry_only {
235            // Generate types file
236            let types_content = self.generate_types(analysis)?;
237            files.push(GeneratedFile {
238                path: "types.rs".into(),
239                content: types_content,
240            });
241
242            // Generate streaming client if configured
243            if let Some(ref streaming_config) = self.config.streaming_config {
244                let streaming_content =
245                    self.generate_streaming_client(streaming_config, analysis)?;
246                files.push(GeneratedFile {
247                    path: "streaming.rs".into(),
248                    content: streaming_content,
249                });
250            }
251
252            // Generate HTTP client if enabled
253            if self.config.enable_async_client {
254                let http_content = self.generate_http_client(analysis)?;
255                files.push(GeneratedFile {
256                    path: "client.rs".into(),
257                    content: http_content,
258                });
259            }
260        }
261
262        // Generate operation registry if enabled
263        if self.config.enable_registry || self.config.registry_only {
264            let registry_content = self.generate_registry(analysis)?;
265            files.push(GeneratedFile {
266                path: "registry.rs".into(),
267                content: registry_content,
268            });
269        }
270
271        // Generate mod.rs file
272        let mod_content = self.generate_mod_file(&files)?;
273        let mod_file = GeneratedFile {
274            path: "mod.rs".into(),
275            content: mod_content,
276        };
277
278        // Snapshot the optional crates the analyzer's TypeMapper
279        // touched. Q2.8 surfaces these via REQUIRED_DEPS.toml
280        // (written by `write_files`) and a CLI stderr summary.
281        let required_deps =
282            crate::type_mapping::collect_dep_requirements(&analysis.used_type_features);
283
284        Ok(GenerationResult {
285            files,
286            mod_file,
287            required_deps,
288        })
289    }
290
291    /// Generate just the types (legacy single-file interface)
292    pub fn generate(&self, analysis: &mut SchemaAnalysis) -> Result<String> {
293        self.generate_types(analysis)
294    }
295
296    /// Generate the types.rs file content
297    fn generate_types(&self, analysis: &mut SchemaAnalysis) -> Result<String> {
298        let mut type_definitions = TokenStream::new();
299
300        // Collect all schemas that are used as variants in discriminated unions
301        // Only include direct references, not schemas wrapped in allOf
302        let mut discriminated_variant_info: BTreeMap<String, DiscriminatedVariantInfo> =
303            BTreeMap::new();
304
305        // Sort schemas for deterministic processing
306        let mut sorted_schemas: Vec<_> = analysis.schemas.iter().collect();
307        sorted_schemas.sort_by_key(|(name, _)| name.as_str());
308
309        for (_parent_name, schema) in sorted_schemas {
310            if let crate::analysis::SchemaType::DiscriminatedUnion {
311                variants,
312                discriminator_field,
313            } = &schema.schema_type
314            {
315                // Check if this discriminated union will be generated as untagged
316                let is_parent_untagged =
317                    self.should_use_untagged_discriminated_union(schema, analysis);
318
319                for variant in variants {
320                    // Only add if it's a direct reference to a schema that will have the discriminator field
321                    // Check if the schema exists and has the discriminator field as a property
322                    if let Some(variant_schema) = analysis.schemas.get(&variant.type_name) {
323                        if let crate::analysis::SchemaType::Object { properties, .. } =
324                            &variant_schema.schema_type
325                        {
326                            if properties.contains_key(discriminator_field) {
327                                discriminated_variant_info.insert(
328                                    variant.type_name.clone(),
329                                    DiscriminatedVariantInfo {
330                                        discriminator_field: discriminator_field.clone(),
331                                        discriminator_value: variant.discriminator_value.clone(),
332                                        is_parent_untagged,
333                                    },
334                                );
335                            }
336                        }
337                    }
338                }
339            }
340        }
341
342        // Generate types based on dependency order
343        let generation_order = analysis.dependencies.topological_sort()?;
344
345        // Defensive layer: track emitted Rust type names so that two
346        // analyzed schemas which sanitize to the same Rust ident don't
347        // produce two definitions (E0119 conflicting impls / E0428 name
348        // defined multiple times). The first occurrence wins; later
349        // occurrences are silently dropped. Schema-name uniqueness at the
350        // analysis layer is a follow-up; this stops the generated file from
351        // failing to compile.
352        let mut emitted_rust_names: std::collections::HashSet<String> =
353            std::collections::HashSet::new();
354        let mut processed = std::collections::HashSet::new();
355
356        // First, generate schemas in dependency order
357        for schema_name in generation_order {
358            if let Some(schema) = analysis.schemas.get(&schema_name) {
359                let rust_name = self.to_rust_type_name(&schema.name);
360                if !emitted_rust_names.insert(rust_name) {
361                    processed.insert(schema_name);
362                    continue;
363                }
364                let type_def =
365                    self.generate_type_definition(schema, analysis, &discriminated_variant_info)?;
366                if !type_def.is_empty() {
367                    type_definitions.extend(type_def);
368                }
369                processed.insert(schema_name);
370            }
371        }
372
373        // Then generate any remaining schemas not in dependency graph
374        let mut remaining_schemas: Vec<_> = analysis
375            .schemas
376            .iter()
377            .filter(|(name, _)| !processed.contains(*name))
378            .collect();
379        remaining_schemas.sort_by_key(|(name, _)| name.as_str());
380
381        for (_schema_name, schema) in remaining_schemas {
382            let rust_name = self.to_rust_type_name(&schema.name);
383            if !emitted_rust_names.insert(rust_name) {
384                continue;
385            }
386            let type_def =
387                self.generate_type_definition(schema, analysis, &discriminated_variant_info)?;
388            if !type_def.is_empty() {
389                type_definitions.extend(type_def);
390            }
391        }
392
393        // Helper modules emitted only when the analyzer actually
394        // referenced their codecs. Avoids polluting every generated
395        // file (and every snapshot) with dead code for specs that
396        // don't use `format: byte`.
397        let base64_helper = if analysis
398            .used_type_features
399            .contains(crate::type_mapping::TypeFeature::Base64)
400        {
401            quote! {
402                /// base64 codec for `Vec<u8>` fields produced from
403                /// `format: byte`. Used via `#[serde(with = "base64_serde")]`
404                /// for required/non-null fields; `with = "base64_serde::option"`
405                /// for the Option<Vec<u8>> case.
406                mod base64_serde {
407                    use base64::{Engine as _, engine::general_purpose::STANDARD};
408                    use serde::{Deserialize, Deserializer, Serializer};
409
410                    pub fn serialize<S: Serializer>(
411                        bytes: &Vec<u8>,
412                        ser: S,
413                    ) -> Result<S::Ok, S::Error> {
414                        ser.serialize_str(&STANDARD.encode(bytes))
415                    }
416
417                    pub fn deserialize<'de, D: Deserializer<'de>>(
418                        de: D,
419                    ) -> Result<Vec<u8>, D::Error> {
420                        let s = String::deserialize(de)?;
421                        STANDARD
422                            .decode(s.as_bytes())
423                            .map_err(serde::de::Error::custom)
424                    }
425
426                    /// Codec for Option<Vec<u8>> fields (optional /
427                    /// nullable `format: byte`). serde dispatches on
428                    /// the field type; without this submodule the
429                    /// `?` operator in the generated code would fail
430                    /// to convert Vec<u8> to Option<Vec<u8>>.
431                    pub mod option {
432                        use super::*;
433                        use serde::{Deserialize, Deserializer, Serializer};
434
435                        pub fn serialize<S: Serializer>(
436                            opt: &Option<Vec<u8>>,
437                            ser: S,
438                        ) -> Result<S::Ok, S::Error> {
439                            match opt {
440                                Some(bytes) => super::serialize(bytes, ser),
441                                None => ser.serialize_none(),
442                            }
443                        }
444
445                        pub fn deserialize<'de, D: Deserializer<'de>>(
446                            de: D,
447                        ) -> Result<Option<Vec<u8>>, D::Error> {
448                            let opt = Option::<String>::deserialize(de)?;
449                            opt.map(|s| {
450                                STANDARD
451                                    .decode(s.as_bytes())
452                                    .map_err(serde::de::Error::custom)
453                            })
454                            .transpose()
455                        }
456                    }
457                }
458            }
459        } else {
460            TokenStream::new()
461        };
462
463        // Generate file with imports and types (no module wrapper).
464        let generated = quote! {
465            //! Generated types from OpenAPI specification
466            //!
467            //! This file contains all the generated types for the API.
468            //! Do not edit manually - regenerate using the appropriate script.
469
470            #![allow(clippy::large_enum_variant)]
471            #![allow(clippy::format_in_format_args)]
472            #![allow(clippy::let_unit_value)]
473            #![allow(unreachable_patterns)]
474
475            use serde::{Deserialize, Serialize};
476
477            #base64_helper
478
479            #type_definitions
480        };
481
482        // Format the generated code
483        let syntax_tree = syn::parse2::<syn::File>(generated).map_err(|e| {
484            GeneratorError::CodeGenError(format!("Failed to parse generated code: {e}"))
485        })?;
486
487        let formatted = prettyplease::unparse(&syntax_tree);
488
489        Ok(formatted)
490    }
491
492    /// Generate streaming client code
493    fn generate_streaming_client(
494        &self,
495        streaming_config: &StreamingConfig,
496        analysis: &SchemaAnalysis,
497    ) -> Result<String> {
498        let mut client_code = TokenStream::new();
499
500        // Generate imports
501        let imports = quote! {
502            //! Generated streaming client for SSE (Server-Sent Events)
503            //!
504            //! This file contains the streaming client implementation.
505            //! Do not edit manually - regenerate using the appropriate script.
506            #![allow(clippy::format_in_format_args)]
507            #![allow(clippy::let_unit_value)]
508            #![allow(unused_mut)]
509
510            use super::types::*;
511            use async_trait::async_trait;
512            use futures_util::{Stream, StreamExt};
513            use std::pin::Pin;
514            use std::time::Duration;
515            use reqwest::header::{HeaderMap, HeaderValue};
516            use tracing::{debug, error, info, warn, instrument};
517        };
518        client_code.extend(imports);
519
520        // Generate error types
521        if streaming_config.generate_client {
522            let error_types = self.generate_streaming_error_types()?;
523            client_code.extend(error_types);
524        }
525
526        // Generate client trait for each endpoint
527        for endpoint in &streaming_config.endpoints {
528            let trait_code = self.generate_endpoint_trait(endpoint, analysis)?;
529            client_code.extend(trait_code);
530        }
531
532        // Generate client implementation
533        if streaming_config.generate_client {
534            let client_impl = self.generate_streaming_client_impl(streaming_config, analysis)?;
535            client_code.extend(client_impl);
536        }
537
538        // Generate SSE parsing utilities
539        if streaming_config.event_parser_helpers {
540            let parser_code = self.generate_sse_parser_utilities(streaming_config)?;
541            client_code.extend(parser_code);
542        }
543
544        // Generate reconnection utilities if configured
545        if let Some(reconnect_config) = &streaming_config.reconnection_config {
546            let reconnect_code = self.generate_reconnection_utilities(reconnect_config)?;
547            client_code.extend(reconnect_code);
548        }
549
550        let syntax_tree = syn::parse2::<syn::File>(client_code).map_err(|e| {
551            GeneratorError::CodeGenError(format!("Failed to parse streaming client code: {e}"))
552        })?;
553
554        Ok(prettyplease::unparse(&syntax_tree))
555    }
556
557    /// Generate HTTP client code for regular (non-streaming) requests
558    pub fn generate_http_client(&self, analysis: &SchemaAnalysis) -> Result<String> {
559        let error_types = self.generate_http_error_types();
560        let client_struct = self.generate_http_client_struct();
561        let operation_methods = self.generate_operation_methods(analysis);
562
563        let generated = quote! {
564            //! Generated HTTP client for regular API requests
565            //!
566            //! This file contains the HTTP client implementation for GET, POST, etc.
567            //! Do not edit manually - regenerate using the appropriate script.
568            #![allow(clippy::format_in_format_args)]
569            #![allow(clippy::let_unit_value)]
570
571            use super::types::*;
572
573            #error_types
574
575            #client_struct
576
577            #operation_methods
578        };
579
580        let syntax_tree = syn::parse2::<syn::File>(generated).map_err(|e| {
581            GeneratorError::CodeGenError(format!("Failed to parse HTTP client code: {e}"))
582        })?;
583
584        Ok(prettyplease::unparse(&syntax_tree))
585    }
586
587    /// Generate HTTP error type and result alias
588    fn generate_http_error_types(&self) -> TokenStream {
589        quote! {
590            use thiserror::Error;
591
592            /// Transport-level errors: failures where we never received an
593            /// inspectable HTTP response from the server.
594            ///
595            /// HTTP responses with non-2xx status codes are surfaced as
596            /// [`ApiError`] inside [`ApiOpError::Api`], not here, so callers can
597            /// always inspect status, headers, and the raw body when the server
598            /// actually responded.
599            #[derive(Error, Debug)]
600            pub enum HttpError {
601                /// Network or connection error (from reqwest)
602                #[error("Network error: {0}")]
603                Network(#[from] reqwest::Error),
604
605                /// Middleware error (from reqwest-middleware)
606                #[error("Middleware error: {0}")]
607                Middleware(#[from] reqwest_middleware::Error),
608
609                /// Request serialization error
610                #[error("Failed to serialize request: {0}")]
611                Serialization(String),
612
613                /// Authentication error
614                #[error("Authentication error: {0}")]
615                Auth(String),
616
617                /// Request timeout
618                #[error("Request timeout")]
619                Timeout,
620
621                /// Invalid configuration
622                #[error("Configuration error: {0}")]
623                Config(String),
624
625                /// Generic error
626                #[error("{0}")]
627                Other(String),
628            }
629
630            impl HttpError {
631                /// Create a serialization error
632                pub fn serialization_error(error: impl std::fmt::Display) -> Self {
633                    Self::Serialization(error.to_string())
634                }
635
636                /// Check if this transport error is retryable
637                pub fn is_retryable(&self) -> bool {
638                    matches!(self, Self::Network(_) | Self::Middleware(_) | Self::Timeout)
639                }
640            }
641
642            /// Envelope returned for any HTTP response that we received but
643            /// couldn't (or didn't) treat as a successful typed result.
644            ///
645            /// Includes both non-2xx responses and 2xx responses whose body
646            /// failed to deserialize into the expected success type. `status`,
647            /// `headers`, and `body` are always populated so callers can
648            /// inspect what the server sent without modifying the generated
649            /// code. `typed` carries the parsed per-operation error variant
650            /// when the body matched a declared schema.
651            #[derive(Debug, Clone)]
652            pub struct ApiError<E> {
653                pub status: u16,
654                pub headers: reqwest::header::HeaderMap,
655                pub body: String,
656                pub typed: Option<E>,
657                pub parse_error: Option<String>,
658            }
659
660            impl<E> ApiError<E> {
661                pub fn is_client_error(&self) -> bool {
662                    (400..500).contains(&self.status)
663                }
664
665                pub fn is_server_error(&self) -> bool {
666                    (500..600).contains(&self.status)
667                }
668
669                /// Retry guidance for the response. Mirrors the previous
670                /// HttpError logic for backwards-compatible retry middleware.
671                pub fn is_retryable(&self) -> bool {
672                    matches!(self.status, 429 | 500 | 502 | 503 | 504)
673                }
674            }
675
676            impl<E: std::fmt::Debug> std::fmt::Display for ApiError<E> {
677                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
678                    write!(f, "API error {}: {}", self.status, self.body)
679                }
680            }
681
682            impl<E: std::fmt::Debug> std::error::Error for ApiError<E> {}
683
684            /// Result error type returned by every generated operation method.
685            ///
686            /// `Transport` covers failures where we never got an inspectable
687            /// response (network, timeout, middleware, request-side
688            /// serialization). `Api` covers any case where the server *did*
689            /// respond — the envelope always carries status + headers + raw
690            /// body even when the typed deserialize fails.
691            #[derive(Debug, Error)]
692            pub enum ApiOpError<E: std::fmt::Debug> {
693                #[error(transparent)]
694                Transport(#[from] HttpError),
695
696                #[error(transparent)]
697                Api(ApiError<E>),
698            }
699
700            impl<E: std::fmt::Debug> ApiOpError<E> {
701                /// Returns the API envelope when this is an `Api` variant.
702                pub fn api(&self) -> Option<&ApiError<E>> {
703                    match self {
704                        Self::Api(e) => Some(e),
705                        Self::Transport(_) => None,
706                    }
707                }
708
709                /// True when the underlying error came from the server (i.e.
710                /// any `Api` variant) rather than the transport layer.
711                pub fn is_api_error(&self) -> bool {
712                    matches!(self, Self::Api(_))
713                }
714            }
715
716            // Direct From impls so `?` works without going through HttpError
717            // first. Rust's `?` only chains a single `From` conversion.
718            impl<E: std::fmt::Debug> From<reqwest::Error> for ApiOpError<E> {
719                fn from(e: reqwest::Error) -> Self {
720                    Self::Transport(HttpError::Network(e))
721                }
722            }
723
724            impl<E: std::fmt::Debug> From<reqwest_middleware::Error> for ApiOpError<E> {
725                fn from(e: reqwest_middleware::Error) -> Self {
726                    Self::Transport(HttpError::Middleware(e))
727                }
728            }
729
730            /// Result alias for transport-only error paths (e.g. helpers that
731            /// don't have a per-operation error type). Generated operation
732            /// methods use [`ApiOpError`] directly.
733            pub type HttpResult<T> = Result<T, HttpError>;
734        }
735    }
736
737    /// Generate mod.rs file that exports all modules
738    fn generate_mod_file(&self, files: &[GeneratedFile]) -> Result<String> {
739        let mut module_declarations = Vec::new();
740        let mut pub_uses = Vec::new();
741
742        for file in files {
743            if let Some(module_name) = file.path.file_stem().and_then(|s| s.to_str()) {
744                if module_name != "mod" {
745                    module_declarations.push(format!("pub mod {module_name};"));
746                    pub_uses.push(format!("pub use {module_name}::*;"));
747                }
748            }
749        }
750
751        // `module_name` is a configurable *label* — it does NOT pick
752        // the on-disk directory (that's `output_dir`) and it does NOT
753        // determine the Rust module path the user mounts this tree
754        // at. Surfacing it in the header doc comment is the most
755        // honest place: a hint to the user about what name was
756        // configured and how to mount it.
757        let mount_hint = format!(
758            "//! Configured `module_name` = `{name}`. Mount this tree under your\n\
759             //! preferred path, e.g. `pub mod {name};` in your crate root.\n",
760            name = self.config.module_name,
761        );
762
763        let content = format!(
764            r#"//! Generated API modules
765//!
766//! This module exports all generated API types and clients.
767//! Do not edit manually - regenerate using the appropriate script.
768//!
769{mount_hint}
770#![allow(unused_imports)]
771
772{decls}
773
774{uses}
775"#,
776            mount_hint = mount_hint,
777            decls = module_declarations.join("\n"),
778            uses = pub_uses.join("\n"),
779        );
780
781        Ok(content)
782    }
783
784    /// Helper method to write all generated files to disk
785    pub fn write_files(&self, result: &GenerationResult) -> Result<()> {
786        use std::fs;
787
788        // Create output directory if it doesn't exist
789        fs::create_dir_all(&self.config.output_dir)?;
790
791        // Write all files
792        for file in &result.files {
793            let file_path = self.config.output_dir.join(&file.path);
794            fs::write(&file_path, &file.content)?;
795        }
796
797        // Write mod.rs
798        let mod_path = self.config.output_dir.join(&result.mod_file.path);
799        fs::write(&mod_path, &result.mod_file.content)?;
800
801        // Q2.8: write REQUIRED_DEPS.toml when the generated code
802        // references any optional crates (chrono, uuid, url, …).
803        // Skipped silently when the set is empty so we don't litter
804        // the output dir for specs whose generated types only use
805        // std/serde/serde_json.
806        if let Some(toml) = crate::type_mapping::render_required_deps_toml(&result.required_deps) {
807            let deps_path = self.config.output_dir.join("REQUIRED_DEPS.toml");
808            fs::write(&deps_path, toml)?;
809        }
810
811        Ok(())
812    }
813
814    fn generate_type_definition(
815        &self,
816        schema: &crate::analysis::AnalyzedSchema,
817        analysis: &crate::analysis::SchemaAnalysis,
818        discriminated_variant_info: &BTreeMap<String, DiscriminatedVariantInfo>,
819    ) -> Result<TokenStream> {
820        use crate::analysis::SchemaType;
821
822        match &schema.schema_type {
823            SchemaType::Primitive { rust_type, .. } => {
824                // Generate type alias for primitives that are referenced by other schemas
825                self.generate_type_alias(schema, rust_type)
826            }
827            SchemaType::StringEnum { values } => {
828                let ext = analysis.enum_extensions.get(&schema.name);
829                // [extensible_enums] override: opt a closed string-enum into an
830                // extensible enum when the spec is known to lag the API (e.g.
831                // Cloudflare R2 returning "WNAM" against a lowercase-only enum).
832                // Accept either the raw spec name (e.g. "r2_bucket_location")
833                // or the rendered Rust type name (e.g. "R2BucketLocation") so
834                // users can write whichever they see in the generated code.
835                let rust_name = self.to_rust_type_name(&schema.name);
836                let force_extensible = self
837                    .config
838                    .extensible_enum_overrides
839                    .get(&schema.name)
840                    .or_else(|| self.config.extensible_enum_overrides.get(&rust_name))
841                    .copied()
842                    .unwrap_or(false);
843                if force_extensible {
844                    self.generate_extensible_enum(schema, values, ext)
845                } else {
846                    self.generate_string_enum(schema, values, ext)
847                }
848            }
849            SchemaType::ExtensibleEnum { known_values } => {
850                let ext = analysis.enum_extensions.get(&schema.name);
851                self.generate_extensible_enum(schema, known_values, ext)
852            }
853            SchemaType::Object {
854                properties,
855                required,
856                additional_properties,
857            } => self.generate_struct(
858                schema,
859                properties,
860                required,
861                additional_properties,
862                analysis,
863                discriminated_variant_info.get(&schema.name),
864            ),
865            SchemaType::DiscriminatedUnion {
866                discriminator_field,
867                variants,
868            } => {
869                // Check if this discriminated union should be untagged due to being nested
870                if self.should_use_untagged_discriminated_union(schema, analysis) {
871                    // Convert variants to SchemaRef format for union enum generation
872                    let schema_refs: Vec<crate::analysis::SchemaRef> = variants
873                        .iter()
874                        .map(|v| crate::analysis::SchemaRef {
875                            target: v.type_name.clone(),
876                            nullable: false,
877                        })
878                        .collect();
879                    self.generate_union_enum(schema, &schema_refs, analysis)
880                } else {
881                    self.generate_discriminated_enum(
882                        schema,
883                        discriminator_field,
884                        variants,
885                        analysis,
886                    )
887                }
888            }
889            SchemaType::Union { variants } => self.generate_union_enum(schema, variants, analysis),
890            SchemaType::Reference { target } => {
891                // For references, check if we need to generate a type alias
892                // This handles cases like nullable patterns
893                if schema.name != *target {
894                    // Generate a type alias
895                    let alias_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
896                    let target_type = format_ident!("{}", self.to_rust_type_name(target));
897
898                    let doc_comment = if let Some(desc) = &schema.description {
899                        quote! { #[doc = #desc] }
900                    } else {
901                        TokenStream::new()
902                    };
903
904                    Ok(quote! {
905                        #doc_comment
906                        pub type #alias_name = #target_type;
907                    })
908                } else {
909                    // Same name as target, no need for alias
910                    Ok(TokenStream::new())
911                }
912            }
913            SchemaType::Array { item_type } => {
914                // Generate type alias for named array schemas.
915                //
916                // Special case: if the array item is a struct whose discriminator
917                // field was stripped (because it's used in a tagged enum), the bare
918                // struct won't serialize the discriminator in standalone contexts.
919                // Generate a single-variant tagged wrapper enum so the discriminator
920                // field is re-added by serde's tag attribute.
921                let array_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
922
923                // Check if the item type is a Reference to a discriminator-stripped struct
924                if let SchemaType::Reference { target } = item_type.as_ref() {
925                    if let Some(info) = discriminated_variant_info.get(target) {
926                        if !info.is_parent_untagged {
927                            // Generate a wrapper enum that re-adds the discriminator tag
928                            let wrapper_name =
929                                format_ident!("{}Item", self.to_rust_type_name(&schema.name));
930                            let variant_type = format_ident!("{}", self.to_rust_type_name(target));
931                            let disc_field = &info.discriminator_field;
932                            let disc_value = &info.discriminator_value;
933
934                            let doc_comment = if let Some(desc) = &schema.description {
935                                quote! { #[doc = #desc] }
936                            } else {
937                                TokenStream::new()
938                            };
939
940                            return Ok(quote! {
941                                /// Wrapper enum that re-adds the discriminator tag
942                                /// for array contexts where the inner struct had its
943                                /// discriminator field stripped for tagged enum use.
944                                #[derive(Debug, Clone, Deserialize, Serialize)]
945                                #[serde(tag = #disc_field)]
946                                pub enum #wrapper_name {
947                                    #[serde(rename = #disc_value)]
948                                    #variant_type(#variant_type),
949                                }
950                                #doc_comment
951                                pub type #array_name = Vec<#wrapper_name>;
952                            });
953                        }
954                    }
955                }
956
957                let inner_type = self.generate_array_item_type(item_type, analysis);
958
959                let doc_comment = if let Some(desc) = &schema.description {
960                    quote! { #[doc = #desc] }
961                } else {
962                    TokenStream::new()
963                };
964
965                Ok(quote! {
966                    #doc_comment
967                    pub type #array_name = Vec<#inner_type>;
968                })
969            }
970            SchemaType::Composition { schemas } => {
971                self.generate_composition_struct(schema, schemas)
972            }
973        }
974    }
975
976    fn generate_type_alias(
977        &self,
978        schema: &crate::analysis::AnalyzedSchema,
979        rust_type: &str,
980    ) -> Result<TokenStream> {
981        let type_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
982        // syn parses any valid Rust type expression including
983        // generics (`chrono::DateTime<chrono::Utc>`, `Vec<u8>`).
984        // The pre-Q2 ad-hoc `::`-splitter choked on `<`.
985        let base_type = parse_rust_type(rust_type)?;
986
987        let doc_comment = if let Some(desc) = &schema.description {
988            let sanitized_desc = self.sanitize_doc_comment(desc);
989            quote! { #[doc = #sanitized_desc] }
990        } else {
991            TokenStream::new()
992        };
993
994        Ok(quote! {
995            #doc_comment
996            pub type #type_name = #base_type;
997        })
998    }
999
1000    fn generate_extensible_enum(
1001        &self,
1002        schema: &crate::analysis::AnalyzedSchema,
1003        known_values: &[String],
1004        ext: Option<&crate::analysis::EnumExtensions>,
1005    ) -> Result<TokenStream> {
1006        let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1007
1008        let doc_comment = if let Some(desc) = &schema.description {
1009            quote! { #[doc = #desc] }
1010        } else {
1011            TokenStream::new()
1012        };
1013
1014        // Q2.6: pre-resolve variant idents from x-enum-varnames when
1015        // available + length-matched + toggle on. Same fallback rule
1016        // as generate_string_enum.
1017        let varnames_override: Option<&Vec<String>> = ext
1018            .filter(|_| self.config.types.x_enum_varnames_enabled())
1019            .map(|e| &e.varnames)
1020            .filter(|v| !v.is_empty() && v.len() == known_values.len());
1021        let descriptions_override: Option<&Vec<String>> = ext
1022            .filter(|_| self.config.types.x_enum_descriptions_enabled())
1023            .map(|e| &e.descriptions)
1024            .filter(|v| !v.is_empty() && v.len() == known_values.len());
1025
1026        let variant_ident_for = |index: usize, value: &str| -> proc_macro2::Ident {
1027            let name = match varnames_override {
1028                Some(v) => v[index].clone(),
1029                None => self.to_rust_enum_variant(value),
1030            };
1031            format_ident!("{}", name)
1032        };
1033
1034        // For extensible enums, we need a different approach:
1035        // 1. Create a regular enum with known variants + Custom
1036        // 2. Implement custom serialization/deserialization
1037
1038        let known_variants = known_values.iter().enumerate().map(|(i, value)| {
1039            let variant_ident = variant_ident_for(i, value);
1040            let doc = descriptions_override
1041                .map(|d| {
1042                    let s = self.sanitize_doc_comment(&d[i]);
1043                    quote! { #[doc = #s] }
1044                })
1045                .unwrap_or_default();
1046            quote! {
1047                #doc
1048                #variant_ident,
1049            }
1050        });
1051
1052        let match_arms_de = known_values.iter().enumerate().map(|(i, value)| {
1053            let variant_ident = variant_ident_for(i, value);
1054            quote! {
1055                #value => Ok(#enum_name::#variant_ident),
1056            }
1057        });
1058
1059        let match_arms_ser = known_values.iter().enumerate().map(|(i, value)| {
1060            let variant_ident = variant_ident_for(i, value);
1061            quote! {
1062                #enum_name::#variant_ident => #value,
1063            }
1064        });
1065
1066        let derives = if self.config.enable_specta {
1067            quote! {
1068                #[derive(Debug, Clone, PartialEq, Eq)]
1069                #[cfg_attr(feature = "specta", derive(specta::Type))]
1070            }
1071        } else {
1072            quote! {
1073                #[derive(Debug, Clone, PartialEq, Eq)]
1074            }
1075        };
1076
1077        Ok(quote! {
1078            #doc_comment
1079            #derives
1080            pub enum #enum_name {
1081                #(#known_variants)*
1082                /// Custom or unknown model identifier
1083                Custom(String),
1084            }
1085
1086            impl<'de> serde::Deserialize<'de> for #enum_name {
1087                fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1088                where
1089                    D: serde::Deserializer<'de>,
1090                {
1091                    let value = String::deserialize(deserializer)?;
1092                    match value.as_str() {
1093                        #(#match_arms_de)*
1094                        _ => Ok(#enum_name::Custom(value)),
1095                    }
1096                }
1097            }
1098
1099            impl serde::Serialize for #enum_name {
1100                fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1101                where
1102                    S: serde::Serializer,
1103                {
1104                    let value = match self {
1105                        #(#match_arms_ser)*
1106                        #enum_name::Custom(s) => s.as_str(),
1107                    };
1108                    serializer.serialize_str(value)
1109                }
1110            }
1111        })
1112    }
1113
1114    fn generate_string_enum(
1115        &self,
1116        schema: &crate::analysis::AnalyzedSchema,
1117        values: &[String],
1118        ext: Option<&crate::analysis::EnumExtensions>,
1119    ) -> Result<TokenStream> {
1120        let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1121
1122        // Determine which variant should be the default. The spec's `default`
1123        // may not exactly match any enum value (telnyx has
1124        // `default: "en"` on a language enum that lists `en-US`, `en-AU`,
1125        // … — no exact match). When that happens, drop the `Default` derive
1126        // entirely instead of emitting it on an enum where no variant has
1127        // `#[default]` (E0665).
1128        let default_value = schema
1129            .default
1130            .as_ref()
1131            .and_then(|v| v.as_str())
1132            .map(|s| s.to_string());
1133        let has_default_match = match &default_value {
1134            Some(d) => values.iter().any(|v| v == d),
1135            None => !values.is_empty(),
1136        };
1137
1138        // Q2.6: x-enum-varnames overrides the default heuristic when
1139        // present, length-matched, and the toggle is on. Falls back
1140        // to the to_rust_enum_variant heuristic otherwise.
1141        let varnames_override: Option<&Vec<String>> = ext
1142            .filter(|_| self.config.types.x_enum_varnames_enabled())
1143            .map(|e| &e.varnames)
1144            .filter(|v| !v.is_empty() && v.len() == values.len());
1145        let descriptions_override: Option<&Vec<String>> = ext
1146            .filter(|_| self.config.types.x_enum_descriptions_enabled())
1147            .map(|e| &e.descriptions)
1148            .filter(|v| !v.is_empty() && v.len() == values.len());
1149
1150        // Variant-name uniqueness: enum values that PascalCase to the same
1151        // identifier (e.g. `ASC`/`asc` both → `Asc`) collide and produce
1152        // E0428 + non-exhaustive matches downstream. Dedupe by suffixing
1153        // `_2`, `_3`, … on collisions while preserving the first occurrence's
1154        // name, and keeping each variant's `#[serde(rename)]` pointed at the
1155        // original wire string.
1156        let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
1157        let variant_pairs: Vec<(syn::Ident, &String, bool, Option<String>)> = values
1158            .iter()
1159            .enumerate()
1160            .map(|(i, value)| {
1161                let base = match varnames_override {
1162                    Some(v) => v[i].clone(),
1163                    None => self.to_rust_enum_variant(value),
1164                };
1165                let mut variant_name = base.clone();
1166                let mut suffix = 2;
1167                while !used.insert(variant_name.clone()) {
1168                    variant_name = format!("{base}_{suffix}");
1169                    suffix += 1;
1170                }
1171                let variant_ident = format_ident!("{}", variant_name);
1172                let is_default = if let Some(ref default) = default_value {
1173                    value == default
1174                } else {
1175                    i == 0
1176                };
1177                let description = descriptions_override.map(|d| d[i].clone());
1178                (variant_ident, value, is_default, description)
1179            })
1180            .collect();
1181
1182        let variants =
1183            variant_pairs
1184                .iter()
1185                .map(|(variant_ident, value, is_default, description)| {
1186                    let doc = description
1187                        .as_ref()
1188                        .map(|d| {
1189                            let s = self.sanitize_doc_comment(d);
1190                            quote! { #[doc = #s] }
1191                        })
1192                        .unwrap_or_default();
1193                    if *is_default {
1194                        quote! {
1195                            #doc
1196                            #[default]
1197                            #[serde(rename = #value)]
1198                            #variant_ident,
1199                        }
1200                    } else {
1201                        quote! {
1202                            #doc
1203                            #[serde(rename = #value)]
1204                            #variant_ident,
1205                        }
1206                    }
1207                });
1208
1209        // T13/T10: emit `as_str` and `Display` so the enum can be embedded in
1210        // query strings, headers, and path segments without requiring callers
1211        // to reach for `serde_json` round-trips.
1212        let as_str_arms = variant_pairs.iter().map(|(variant_ident, value, _, _)| {
1213            quote! { Self::#variant_ident => #value, }
1214        });
1215
1216        let doc_comment = if let Some(desc) = &schema.description {
1217            quote! { #[doc = #desc] }
1218        } else {
1219            TokenStream::new()
1220        };
1221
1222        // Generate derives with optional Specta support. Drop `Default` if
1223        // no variant ends up tagged `#[default]` (would trigger E0665).
1224        let derives = match (self.config.enable_specta, has_default_match) {
1225            (true, true) => quote! {
1226                #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
1227                #[cfg_attr(feature = "specta", derive(specta::Type))]
1228            },
1229            (true, false) => quote! {
1230                #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1231                #[cfg_attr(feature = "specta", derive(specta::Type))]
1232            },
1233            (false, true) => quote! {
1234                #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
1235            },
1236            (false, false) => quote! {
1237                #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1238            },
1239        };
1240
1241        Ok(quote! {
1242            #doc_comment
1243            #derives
1244            pub enum #enum_name {
1245                #(#variants)*
1246            }
1247
1248            impl #enum_name {
1249                pub fn as_str(&self) -> &'static str {
1250                    match self {
1251                        #(#as_str_arms)*
1252                    }
1253                }
1254            }
1255
1256            impl ::std::fmt::Display for #enum_name {
1257                fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
1258                    f.write_str(self.as_str())
1259                }
1260            }
1261
1262            impl AsRef<str> for #enum_name {
1263                fn as_ref(&self) -> &str {
1264                    self.as_str()
1265                }
1266            }
1267        })
1268    }
1269
1270    fn generate_struct(
1271        &self,
1272        schema: &crate::analysis::AnalyzedSchema,
1273        properties: &BTreeMap<String, crate::analysis::PropertyInfo>,
1274        required: &std::collections::HashSet<String>,
1275        additional_properties: &crate::analysis::ObjectAdditionalProperties,
1276        analysis: &crate::analysis::SchemaAnalysis,
1277        discriminator_info: Option<&DiscriminatedVariantInfo>,
1278    ) -> Result<TokenStream> {
1279        let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1280
1281        // Sort properties by name for deterministic output
1282        let mut sorted_properties: Vec<_> = properties.iter().collect();
1283        sorted_properties.sort_by_key(|(name, _)| name.as_str());
1284
1285        // Track Rust field-name uniqueness inside the struct. Two spec
1286        // properties whose names sanitize to the same Rust identifier
1287        // (e.g. `connectionString` and `connection_string` both → `connection_string`)
1288        // would otherwise emit duplicate fields and trigger E0124 / E0062.
1289        // We disambiguate by suffixing `_2`, `_3`, … on collisions, and we
1290        // skip the duplicate entirely when the spec literally repeats the
1291        // same key (impossible in JSON but tolerated in YAML merging).
1292        let mut used_field_idents: std::collections::HashSet<String> =
1293            std::collections::HashSet::new();
1294
1295        let mut fields: Vec<TokenStream> = sorted_properties
1296            .into_iter()
1297            .filter(|(field_name, _)| {
1298                // Skip the discriminator field ONLY if:
1299                // 1. This struct is a variant in a discriminated union, AND
1300                // 2. The parent union is tagged (not untagged)
1301                if let Some(info) = discriminator_info {
1302                    if !info.is_parent_untagged
1303                        && field_name.as_str() == info.discriminator_field.as_str()
1304                    {
1305                        false // Skip the field
1306                    } else {
1307                        true // Keep the field
1308                    }
1309                } else {
1310                    true // No discriminator info, keep all fields
1311                }
1312            })
1313            .map(|(field_name, prop)| {
1314                let raw = self.to_rust_field_name(field_name);
1315                let mut chosen = raw.clone();
1316                let mut suffix = 2;
1317                while !used_field_idents.insert(chosen.clone()) {
1318                    chosen = format!("{raw}_{suffix}");
1319                    suffix += 1;
1320                }
1321                let field_ident = Self::to_field_ident(&chosen);
1322                let is_required = required.contains(field_name);
1323                let field_type =
1324                    self.generate_field_type(&schema.name, field_name, prop, is_required, analysis);
1325
1326                let serde_attrs =
1327                    self.generate_serde_field_attrs(field_name, prop, is_required, analysis);
1328                let specta_attrs = self.generate_specta_field_attrs(field_name);
1329
1330                let doc_comment = if let Some(desc) = &prop.description {
1331                    let sanitized_desc = self.sanitize_doc_comment(desc);
1332                    quote! { #[doc = #sanitized_desc] }
1333                } else {
1334                    TokenStream::new()
1335                };
1336                let constraint_doc = self.generate_constraint_doc(&prop.constraints);
1337
1338                quote! {
1339                    #doc_comment
1340                    #constraint_doc
1341                    #serde_attrs
1342                    #specta_attrs
1343                    pub #field_ident: #field_type,
1344                }
1345            })
1346            .collect();
1347
1348        // Q2.3: emit the catch-all additional-properties field with
1349        // the right value type. `Untyped` keeps pre-Q2.3 behavior
1350        // (BTreeMap<String, serde_json::Value>); `Typed { value_type }`
1351        // surfaces the actual schema-declared type, e.g.
1352        // BTreeMap<String, MyValue>. `Forbidden` emits no field.
1353        match additional_properties {
1354            crate::analysis::ObjectAdditionalProperties::Forbidden => {}
1355            crate::analysis::ObjectAdditionalProperties::Untyped => {
1356                fields.push(quote! {
1357                    /// Additional properties not explicitly defined in the schema
1358                    #[serde(flatten)]
1359                    pub additional_properties:
1360                        std::collections::BTreeMap<String, serde_json::Value>,
1361                });
1362            }
1363            crate::analysis::ObjectAdditionalProperties::Typed { value_type } => {
1364                let value_tokens = self.generate_array_item_type(value_type, analysis);
1365                fields.push(quote! {
1366                    /// Additional properties matching the spec's
1367                    /// `additionalProperties` value schema.
1368                    #[serde(flatten)]
1369                    pub additional_properties:
1370                        std::collections::BTreeMap<String, #value_tokens>,
1371                });
1372            }
1373        }
1374
1375        let doc_comment = if let Some(desc) = &schema.description {
1376            quote! { #[doc = #desc] }
1377        } else {
1378            TokenStream::new()
1379        };
1380
1381        // Generate derives with optional Specta support
1382        // Note: We use snake_case everywhere (matching the OpenAPI spec) for consistency
1383        // between Rust, JSON API, and TypeScript
1384        let derives = if self.config.enable_specta {
1385            quote! {
1386                #[derive(Debug, Clone, Deserialize, Serialize)]
1387                #[cfg_attr(feature = "specta", derive(specta::Type))]
1388            }
1389        } else {
1390            quote! {
1391                #[derive(Debug, Clone, Deserialize, Serialize)]
1392            }
1393        };
1394
1395        Ok(quote! {
1396            #doc_comment
1397            #derives
1398            pub struct #struct_name {
1399                #(#fields)*
1400            }
1401        })
1402    }
1403
1404    fn generate_discriminated_enum(
1405        &self,
1406        schema: &crate::analysis::AnalyzedSchema,
1407        discriminator_field: &str,
1408        variants: &[crate::analysis::UnionVariant],
1409        analysis: &crate::analysis::SchemaAnalysis,
1410    ) -> Result<TokenStream> {
1411        let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1412
1413        // Check if any variant references another discriminated union
1414        let has_nested_discriminated_union = variants.iter().any(|variant| {
1415            if let Some(variant_schema) = analysis.schemas.get(&variant.type_name) {
1416                matches!(
1417                    variant_schema.schema_type,
1418                    crate::analysis::SchemaType::DiscriminatedUnion { .. }
1419                )
1420            } else {
1421                false
1422            }
1423        });
1424
1425        // If we have a nested discriminated union, make this enum untagged
1426        if has_nested_discriminated_union {
1427            // Generate as untagged union
1428            let schema_refs: Vec<crate::analysis::SchemaRef> = variants
1429                .iter()
1430                .map(|v| crate::analysis::SchemaRef {
1431                    target: v.type_name.clone(),
1432                    nullable: false,
1433                })
1434                .collect();
1435            return self.generate_union_enum(schema, &schema_refs, analysis);
1436        }
1437
1438        let enclosing = self.to_rust_type_name(&schema.name);
1439        let enum_variants = variants.iter().map(|variant| {
1440            let variant_name = format_ident!("{}", variant.rust_name);
1441            let variant_value = &variant.discriminator_value;
1442
1443            let variant_type = format_ident!("{}", self.to_rust_type_name(&variant.type_name));
1444            // Box variant payloads that point at the enclosing enum or any
1445            // schema in the analysis's recursive set, otherwise the enum has
1446            // infinite size (E0072).
1447            let payload = if self.to_rust_type_name(&variant.type_name) == enclosing
1448                || analysis
1449                    .dependencies
1450                    .recursive_schemas
1451                    .contains(&variant.type_name)
1452            {
1453                quote! { Box<#variant_type> }
1454            } else {
1455                quote! { #variant_type }
1456            };
1457            quote! {
1458                #[serde(rename = #variant_value)]
1459                #variant_name(#payload),
1460            }
1461        });
1462
1463        let doc_comment = if let Some(desc) = &schema.description {
1464            quote! { #[doc = #desc] }
1465        } else {
1466            TokenStream::new()
1467        };
1468
1469        // Generate derives with optional Specta support
1470        let derives = if self.config.enable_specta {
1471            quote! {
1472                #[derive(Debug, Clone, Deserialize, Serialize)]
1473                #[cfg_attr(feature = "specta", derive(specta::Type))]
1474                #[serde(tag = #discriminator_field)]
1475            }
1476        } else {
1477            quote! {
1478                #[derive(Debug, Clone, Deserialize, Serialize)]
1479                #[serde(tag = #discriminator_field)]
1480            }
1481        };
1482
1483        Ok(quote! {
1484            #doc_comment
1485            #derives
1486            pub enum #enum_name {
1487                #(#enum_variants)*
1488            }
1489        })
1490    }
1491
1492    /// Check if a discriminated union should be generated as untagged due to being nested
1493    fn should_use_untagged_discriminated_union(
1494        &self,
1495        schema: &crate::analysis::AnalyzedSchema,
1496        analysis: &crate::analysis::SchemaAnalysis,
1497    ) -> bool {
1498        // Only make discriminated unions untagged if they are nested AND their variants
1499        // don't need the discriminator field for API compatibility
1500
1501        // Check if this schema is used as a variant in another discriminated union
1502        for other_schema in analysis.schemas.values() {
1503            if let crate::analysis::SchemaType::DiscriminatedUnion {
1504                variants,
1505                discriminator_field: _,
1506            } = &other_schema.schema_type
1507            {
1508                for variant in variants {
1509                    if variant.type_name == schema.name {
1510                        // This discriminated union is nested inside another discriminated union
1511
1512                        // Check if the current schema's variants have the discriminator field in their properties
1513                        // If they do, we need to keep this union tagged to preserve the discriminator
1514                        if let crate::analysis::SchemaType::DiscriminatedUnion {
1515                            discriminator_field: current_discriminator,
1516                            variants: current_variants,
1517                            ..
1518                        } = &schema.schema_type
1519                        {
1520                            // Check if any variant schemas have the discriminator field as a property
1521                            for current_variant in current_variants {
1522                                if let Some(variant_schema) =
1523                                    analysis.schemas.get(&current_variant.type_name)
1524                                {
1525                                    if let crate::analysis::SchemaType::Object {
1526                                        properties, ..
1527                                    } = &variant_schema.schema_type
1528                                    {
1529                                        if properties.contains_key(current_discriminator) {
1530                                            // This variant has the discriminator field as a property,
1531                                            // so we need to keep the union tagged to preserve it
1532                                            return false;
1533                                        }
1534                                    }
1535                                }
1536                            }
1537                        }
1538
1539                        // No variants have the discriminator as a property, safe to make untagged
1540                        return true;
1541                    }
1542                }
1543            }
1544        }
1545        false
1546    }
1547
1548    fn generate_union_enum(
1549        &self,
1550        schema: &crate::analysis::AnalyzedSchema,
1551        variants: &[crate::analysis::SchemaRef],
1552        analysis: &crate::analysis::SchemaAnalysis,
1553    ) -> Result<TokenStream> {
1554        let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1555
1556        // Generate meaningful variant names based on type names
1557        let mut used_variant_names = std::collections::HashSet::new();
1558        let enum_variants = variants.iter().enumerate().map(|(i, variant)| {
1559            // Generate a meaningful variant name from the type name
1560            let base_variant_name = self.type_name_to_variant_name(&variant.target);
1561            let variant_name = self.ensure_unique_variant_name_generator(
1562                base_variant_name,
1563                &mut used_variant_names,
1564                i,
1565            );
1566            let variant_name_ident = format_ident!("{}", variant_name);
1567
1568            // For primitive types and Vec types, use them directly without conversion
1569            let variant_type_tokens = if matches!(
1570                variant.target.as_str(),
1571                "bool"
1572                    | "i8"
1573                    | "i16"
1574                    | "i32"
1575                    | "i64"
1576                    | "i128"
1577                    | "u8"
1578                    | "u16"
1579                    | "u32"
1580                    | "u64"
1581                    | "u128"
1582                    | "f32"
1583                    | "f64"
1584                    | "String"
1585            ) {
1586                let type_ident = format_ident!("{}", variant.target);
1587                quote! { #type_ident }
1588            } else if variant.target == "serde_json::Value" {
1589                // The target is a fully-qualified path; emit it as a path so
1590                // it doesn't get mangled into a phantom `SerdeJsonValue` ident.
1591                quote! { serde_json::Value }
1592            } else if variant.target.starts_with("Vec<") && variant.target.ends_with(">") {
1593                // Handle Vec types by parsing the inner type
1594                let inner = &variant.target[4..variant.target.len() - 1];
1595
1596                // Handle nested Vec types (e.g., Vec<Vec<i64>>)
1597                if inner.starts_with("Vec<") && inner.ends_with(">") {
1598                    let inner_inner = &inner[4..inner.len() - 1];
1599                    if inner_inner == "serde_json::Value" {
1600                        quote! { Vec<Vec<serde_json::Value>> }
1601                    } else {
1602                        let inner_inner_type = if matches!(
1603                            inner_inner,
1604                            "bool"
1605                                | "i8"
1606                                | "i16"
1607                                | "i32"
1608                                | "i64"
1609                                | "i128"
1610                                | "u8"
1611                                | "u16"
1612                                | "u32"
1613                                | "u64"
1614                                | "u128"
1615                                | "f32"
1616                                | "f64"
1617                                | "String"
1618                        ) {
1619                            format_ident!("{}", inner_inner)
1620                        } else {
1621                            format_ident!("{}", self.to_rust_type_name(inner_inner))
1622                        };
1623                        quote! { Vec<Vec<#inner_inner_type>> }
1624                    }
1625                } else if inner == "serde_json::Value" {
1626                    quote! { Vec<serde_json::Value> }
1627                } else {
1628                    let inner_type = if matches!(
1629                        inner,
1630                        "bool"
1631                            | "i8"
1632                            | "i16"
1633                            | "i32"
1634                            | "i64"
1635                            | "i128"
1636                            | "u8"
1637                            | "u16"
1638                            | "u32"
1639                            | "u64"
1640                            | "u128"
1641                            | "f32"
1642                            | "f64"
1643                            | "String"
1644                    ) {
1645                        format_ident!("{}", inner)
1646                    } else {
1647                        format_ident!("{}", self.to_rust_type_name(inner))
1648                    };
1649                    quote! { Vec<#inner_type> }
1650                }
1651            } else if variant.target.contains("::") || variant.target.contains('<') {
1652                // Qualified Rust path or generic (chrono::DateTime<chrono::Utc>,
1653                // bytes::Bytes, std::net::Ipv4Addr) emitted by TypeMapper. Pass
1654                // it straight to syn — the to_rust_type_name PascalCase
1655                // pipeline below would mangle it into a non-existent ident.
1656                parse_rust_type(&variant.target).unwrap_or_else(|_| {
1657                    let fallback = format_ident!("{}", self.to_rust_type_name(&variant.target));
1658                    quote! { #fallback }
1659                })
1660            } else {
1661                let type_ident = format_ident!("{}", self.to_rust_type_name(&variant.target));
1662                quote! { #type_ident }
1663            };
1664
1665            // Self-referential variant (variant payload type == enclosing
1666            // enum) yields an infinite-size enum (E0072). Wrap in `Box<T>` to
1667            // break the cycle. Observed in microsoft-graph.yaml.
1668            let target_rust_name = self.to_rust_type_name(&variant.target);
1669            let enclosing_name = self.to_rust_type_name(&schema.name);
1670            let is_self_ref = target_rust_name == enclosing_name;
1671            // Indirect cycles (stripe BankAccount → BankAccountCustomer →
1672            // Customer → BankAccountCustomer): variants pointing into the
1673            // analysis's recursive_schemas set must also be heap-allocated.
1674            let is_recursive_target = analysis
1675                .dependencies
1676                .recursive_schemas
1677                .contains(&variant.target);
1678            let variant_type_tokens = if is_self_ref || is_recursive_target {
1679                quote! { Box<#variant_type_tokens> }
1680            } else {
1681                variant_type_tokens
1682            };
1683
1684            quote! {
1685                #variant_name_ident(#variant_type_tokens),
1686            }
1687        });
1688
1689        let doc_comment = if let Some(desc) = &schema.description {
1690            quote! { #[doc = #desc] }
1691        } else {
1692            TokenStream::new()
1693        };
1694
1695        // Generate derives with optional Specta support
1696        let derives = if self.config.enable_specta {
1697            quote! {
1698                #[derive(Debug, Clone, Deserialize, Serialize)]
1699                #[cfg_attr(feature = "specta", derive(specta::Type))]
1700                #[serde(untagged)]
1701            }
1702        } else {
1703            quote! {
1704                #[derive(Debug, Clone, Deserialize, Serialize)]
1705                #[serde(untagged)]
1706            }
1707        };
1708
1709        Ok(quote! {
1710            #doc_comment
1711            #derives
1712            pub enum #enum_name {
1713                #(#enum_variants)*
1714            }
1715        })
1716    }
1717
1718    /// Walk a chain of type-alias `Reference`s starting from `target` and
1719    /// return true if the chain reaches the schema named by
1720    /// `enclosing_rust_name` (Rust name). Bounded depth to prevent infinite
1721    /// loops on truly cyclic aliases.
1722    fn target_aliases_back_to(
1723        &self,
1724        target: &str,
1725        enclosing_rust_name: &str,
1726        analysis: &crate::analysis::SchemaAnalysis,
1727    ) -> bool {
1728        let mut current = target.to_string();
1729        let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
1730        for _ in 0..16 {
1731            if !visited.insert(current.clone()) {
1732                return true;
1733            }
1734            let Some(schema) = analysis.schemas.get(&current) else {
1735                return false;
1736            };
1737            if let crate::analysis::SchemaType::Reference { target: next } = &schema.schema_type {
1738                if self.to_rust_type_name(next) == enclosing_rust_name {
1739                    return true;
1740                }
1741                current = next.clone();
1742                continue;
1743            }
1744            return false;
1745        }
1746        false
1747    }
1748
1749    fn generate_field_type(
1750        &self,
1751        schema_name: &str,
1752        field_name: &str,
1753        prop: &crate::analysis::PropertyInfo,
1754        is_required: bool,
1755        analysis: &crate::analysis::SchemaAnalysis,
1756    ) -> TokenStream {
1757        use crate::analysis::SchemaType;
1758
1759        let base_type = match &prop.schema_type {
1760            SchemaType::Primitive { rust_type, .. } => {
1761                // syn handles generics + complex paths
1762                // (chrono::DateTime<chrono::Utc>, Vec<u8>, …).
1763                parse_rust_type(rust_type).unwrap_or_else(|_| {
1764                    // Pathological mapper output: fall back to bare
1765                    // String so the generated file at least
1766                    // compiles. Emit a stderr warning so the
1767                    // operator can investigate.
1768                    eprintln!(
1769                        "⚠️  TypeMapper produced un-parseable type `{rust_type}`; \
1770                         falling back to String"
1771                    );
1772                    quote! { String }
1773                })
1774            }
1775            SchemaType::Reference { target } => {
1776                let target_rust_name = self.to_rust_type_name(target);
1777                let target_type = format_ident!("{}", target_rust_name);
1778                // Wrap recursive references in Box<T> for heap allocation.
1779                // Three ways to detect the cycle:
1780                // 1. Target is in the analysis-level recursive set (catches
1781                //    direct + indirect cycles via the dependency graph).
1782                // 2. Target's Rust name equals the enclosing struct's Rust
1783                //    name (catches cloudflare-style cases where two distinct
1784                //    spec schemas PascalCase to the same ident).
1785                // 3. Target is a type alias whose resolution chain reaches
1786                //    the enclosing schema (catches cal-com's
1787                //    `ReassignBookingOutput20240813Data = Reassign...`
1788                //    pattern: the synthesized inline name aliases back to
1789                //    its parent).
1790                let enclosing_rust_name = self.to_rust_type_name(schema_name);
1791                let is_self_via_rust_name = target_rust_name == enclosing_rust_name;
1792                let is_alias_chain_self =
1793                    self.target_aliases_back_to(target, &enclosing_rust_name, analysis);
1794                if analysis.dependencies.recursive_schemas.contains(target)
1795                    || is_self_via_rust_name
1796                    || is_alias_chain_self
1797                {
1798                    quote! { Box<#target_type> }
1799                } else {
1800                    quote! { #target_type }
1801                }
1802            }
1803            SchemaType::Array { item_type } => {
1804                let inner_type = self.generate_array_item_type(item_type, analysis);
1805                quote! { Vec<#inner_type> }
1806            }
1807            _ => {
1808                // Fallback for complex types
1809                quote! { serde_json::Value }
1810            }
1811        };
1812
1813        // Check if this field has a nullable override
1814        let override_key = format!("{schema_name}.{field_name}");
1815        let is_nullable_override = self
1816            .config
1817            .nullable_field_overrides
1818            .get(&override_key)
1819            .copied()
1820            .unwrap_or(false);
1821
1822        if is_required && !prop.nullable && !is_nullable_override {
1823            // If the field has a default value but its type doesn't implement Default,
1824            // wrap in Option<T> so serde can default to None instead of requiring Default.
1825            if prop.default.is_some() && self.type_lacks_default(&prop.schema_type, analysis) {
1826                quote! { Option<#base_type> }
1827            } else {
1828                base_type
1829            }
1830        } else {
1831            quote! { Option<#base_type> }
1832        }
1833    }
1834
1835    fn generate_serde_field_attrs(
1836        &self,
1837        field_name: &str,
1838        prop: &crate::analysis::PropertyInfo,
1839        is_required: bool,
1840        analysis: &crate::analysis::SchemaAnalysis,
1841    ) -> TokenStream {
1842        let mut attrs = Vec::new();
1843
1844        // Generate rename attribute if field name differs from Rust identifier
1845        // Strip r# prefix for comparison since serde handles raw idents transparently
1846        let rust_field_name = self.to_rust_field_name(field_name);
1847        let comparison_name = rust_field_name
1848            .strip_prefix("r#")
1849            .unwrap_or(&rust_field_name);
1850        if comparison_name != field_name {
1851            attrs.push(quote! { rename = #field_name });
1852        }
1853
1854        // Add skip_serializing_if for optional fields to avoid sending null values
1855        if !is_required || prop.nullable {
1856            attrs.push(quote! { skip_serializing_if = "Option::is_none" });
1857        }
1858
1859        // Only add default attribute for required fields that have default values.
1860        // Skip #[serde(default)] for types that don't implement Default (discriminated
1861        // unions, union enums) — those fields should be Option<T> instead.
1862        if prop.default.is_some()
1863            && (is_required && !prop.nullable)
1864            && !self.type_lacks_default(&prop.schema_type, analysis)
1865        {
1866            attrs.push(quote! { default });
1867        }
1868
1869        // Codec hint from TypeMapper (Q2): `format: byte` →
1870        // `with = "base64_serde"`, etc. Fields whose mapped type
1871        // carries no codec (e.g. chrono::DateTime<Utc> uses its
1872        // built-in serde) skip this attribute. Option fields need
1873        // the `::option` submodule of the codec — serde dispatches
1874        // on field type, and the base codec works on Vec<u8> /
1875        // chrono::Duration / etc., not their Option wrappers.
1876        if let crate::analysis::SchemaType::Primitive {
1877            serde_with: Some(codec),
1878            ..
1879        } = &prop.schema_type
1880        {
1881            // Mirrors the wrapping logic in generate_field_type:
1882            // the field is Option<T> when the schema marks it
1883            // optional or nullable.
1884            let is_option_wrapped = !is_required || prop.nullable;
1885            let codec_path = if is_option_wrapped {
1886                format!("{codec}::option")
1887            } else {
1888                codec.clone()
1889            };
1890            attrs.push(quote! { with = #codec_path });
1891        }
1892
1893        if attrs.is_empty() {
1894            TokenStream::new()
1895        } else {
1896            quote! { #[serde(#(#attrs),*)] }
1897        }
1898    }
1899
1900    /// Check if a schema type resolves to a type that doesn't implement `Default`.
1901    /// Discriminated unions and union enums don't derive Default, so fields with
1902    /// these types can't use `#[serde(default)]`.
1903    fn type_lacks_default(
1904        &self,
1905        schema_type: &crate::analysis::SchemaType,
1906        analysis: &crate::analysis::SchemaAnalysis,
1907    ) -> bool {
1908        use crate::analysis::SchemaType;
1909        match schema_type {
1910            SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => true,
1911            // Q2 typed scalars: chrono / url have no Default impl.
1912            // uuid::Uuid, bytes::Bytes, std::net::Ip*Addr all derive
1913            // Default, so they're safe to leave under #[serde(default)].
1914            SchemaType::Primitive { rust_type, .. } => matches!(
1915                rust_type.as_str(),
1916                "chrono::DateTime<chrono::Utc>"
1917                    | "chrono::NaiveDate"
1918                    | "chrono::NaiveTime"
1919                    | "chrono::Duration"
1920                    | "url::Url"
1921                    | "time::OffsetDateTime"
1922                    | "time::Date"
1923                    | "time::Time"
1924                    | "iso8601::Duration"
1925                    | "email_address::EmailAddress"
1926            ),
1927            SchemaType::Reference { target } => {
1928                if let Some(schema) = analysis.schemas.get(target) {
1929                    self.type_lacks_default(&schema.schema_type, analysis)
1930                } else {
1931                    false
1932                }
1933            }
1934            _ => false,
1935        }
1936    }
1937
1938    fn generate_specta_field_attrs(&self, field_name: &str) -> TokenStream {
1939        if !self.config.enable_specta {
1940            return TokenStream::new();
1941        }
1942
1943        // Convert field name to camelCase for TypeScript
1944        let camel_case_name = self.to_camel_case(field_name);
1945
1946        // Only add specta rename if it differs from the original field name
1947        if camel_case_name != field_name {
1948            quote! { #[cfg_attr(feature = "specta", specta(rename = #camel_case_name))] }
1949        } else {
1950            TokenStream::new()
1951        }
1952    }
1953
1954    pub(crate) fn to_rust_enum_variant(&self, s: &str) -> String {
1955        // Preserve sign for numeric values so e.g. `-1` and `1` produce
1956        // distinct variants (`VariantNeg1` vs `Variant1`). Without this,
1957        // strict-namespace enums in github.json collide on `1`/`-1`.
1958        let neg_prefix =
1959            if s.starts_with('-') && s.chars().skip(1).all(|c| c.is_ascii_digit() || c == '.') {
1960                "Neg"
1961            } else {
1962                ""
1963            };
1964
1965        // Convert string to valid Rust enum variant (PascalCase)
1966        let mut result = String::new();
1967        let mut next_upper = true;
1968        let mut prev_was_upper = false;
1969
1970        for (i, c) in s.chars().enumerate() {
1971            match c {
1972                'a'..='z' => {
1973                    if next_upper {
1974                        result.push(c.to_ascii_uppercase());
1975                        next_upper = false;
1976                    } else {
1977                        result.push(c);
1978                    }
1979                    prev_was_upper = false;
1980                }
1981                'A'..='Z' => {
1982                    if next_upper || (!prev_was_upper && i > 0) {
1983                        // Start of word or transition from lowercase
1984                        result.push(c);
1985                        next_upper = false;
1986                    } else {
1987                        // Continue uppercase sequence, convert to lowercase
1988                        result.push(c.to_ascii_lowercase());
1989                    }
1990                    prev_was_upper = true;
1991                }
1992                '0'..='9' => {
1993                    result.push(c);
1994                    next_upper = false;
1995                    prev_was_upper = false;
1996                }
1997                '.' | '-' | '_' | ' ' | '@' | '#' | '$' | '/' | '\\' => {
1998                    // Word boundaries - next char should be uppercase
1999                    next_upper = true;
2000                    prev_was_upper = false;
2001                }
2002                _ => {
2003                    // Other special characters - treat as word boundary
2004                    next_upper = true;
2005                    prev_was_upper = false;
2006                }
2007            }
2008        }
2009
2010        // Handle empty result
2011        if result.is_empty() {
2012            result = "Value".to_string();
2013        }
2014
2015        // Ensure variant starts with a letter (not a number)
2016        if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2017            result = format!("Variant{neg_prefix}{result}");
2018        } else if !neg_prefix.is_empty() {
2019            // String happened to start with `-<digits>` but produced a
2020            // non-empty alphabetic prefix. Tag the negative anyway.
2021            result = format!("{neg_prefix}{result}");
2022        }
2023
2024        // Handle special cases for enum variants
2025        match result.as_str() {
2026            "Null" => "NullValue".to_string(),
2027            "True" => "TrueValue".to_string(),
2028            "False" => "FalseValue".to_string(),
2029            "Type" => "Type_".to_string(),
2030            "Match" => "Match_".to_string(),
2031            "Fn" => "Fn_".to_string(),
2032            "Impl" => "Impl_".to_string(),
2033            "Trait" => "Trait_".to_string(),
2034            "Struct" => "Struct_".to_string(),
2035            "Enum" => "Enum_".to_string(),
2036            "Mod" => "Mod_".to_string(),
2037            "Use" => "Use_".to_string(),
2038            "Pub" => "Pub_".to_string(),
2039            "Const" => "Const_".to_string(),
2040            "Static" => "Static_".to_string(),
2041            "Let" => "Let_".to_string(),
2042            "Mut" => "Mut_".to_string(),
2043            "Ref" => "Ref_".to_string(),
2044            "Move" => "Move_".to_string(),
2045            "Return" => "Return_".to_string(),
2046            "If" => "If_".to_string(),
2047            "Else" => "Else_".to_string(),
2048            "While" => "While_".to_string(),
2049            "For" => "For_".to_string(),
2050            "Loop" => "Loop_".to_string(),
2051            "Break" => "Break_".to_string(),
2052            "Continue" => "Continue_".to_string(),
2053            "Self" => "Self_".to_string(),
2054            "Super" => "Super_".to_string(),
2055            "Crate" => "Crate_".to_string(),
2056            "Async" => "Async_".to_string(),
2057            "Await" => "Await_".to_string(),
2058            _ => result,
2059        }
2060    }
2061
2062    #[allow(dead_code)]
2063    fn to_rust_identifier(&self, s: &str) -> String {
2064        // Convert string to valid Rust identifier
2065        let mut result = s
2066            .chars()
2067            .map(|c| match c {
2068                'a'..='z' | 'A'..='Z' | '0'..='9' => c,
2069                '.' | '-' | '_' | ' ' | '@' | '#' | '$' | '/' | '\\' => '_',
2070                _ => '_',
2071            })
2072            .collect::<String>();
2073
2074        // Remove leading/trailing underscores
2075        result = result.trim_matches('_').to_string();
2076
2077        // Handle empty result
2078        if result.is_empty() {
2079            result = "value".to_string();
2080        }
2081
2082        // Ensure identifier starts with a letter (not a number)
2083        if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2084            result = format!("variant_{result}");
2085        }
2086
2087        // Handle special cases for enum values
2088        match result.as_str() {
2089            "null" => "null_value".to_string(),
2090            "true" => "true_value".to_string(),
2091            "false" => "false_value".to_string(),
2092            "type" => "type_".to_string(),
2093            "match" => "match_".to_string(),
2094            "fn" => "fn_".to_string(),
2095            "impl" => "impl_".to_string(),
2096            "trait" => "trait_".to_string(),
2097            "struct" => "struct_".to_string(),
2098            "enum" => "enum_".to_string(),
2099            "mod" => "mod_".to_string(),
2100            "use" => "use_".to_string(),
2101            "pub" => "pub_".to_string(),
2102            "const" => "const_".to_string(),
2103            "static" => "static_".to_string(),
2104            "let" => "let_".to_string(),
2105            "mut" => "mut_".to_string(),
2106            "ref" => "ref_".to_string(),
2107            "move" => "move_".to_string(),
2108            "return" => "return_".to_string(),
2109            "if" => "if_".to_string(),
2110            "else" => "else_".to_string(),
2111            "while" => "while_".to_string(),
2112            "for" => "for_".to_string(),
2113            "loop" => "loop_".to_string(),
2114            "break" => "break_".to_string(),
2115            "continue" => "continue_".to_string(),
2116            "self" => "self_".to_string(),
2117            "super" => "super_".to_string(),
2118            "crate" => "crate_".to_string(),
2119            "async" => "async_".to_string(),
2120            "await" => "await_".to_string(),
2121            // Reserved keywords for edition 2018+
2122            "override" => "override_".to_string(),
2123            "box" => "box_".to_string(),
2124            "dyn" => "dyn_".to_string(),
2125            "where" => "where_".to_string(),
2126            "in" => "in_".to_string(),
2127            // Reserved for future use
2128            "abstract" => "abstract_".to_string(),
2129            "become" => "become_".to_string(),
2130            "do" => "do_".to_string(),
2131            "final" => "final_".to_string(),
2132            "macro" => "macro_".to_string(),
2133            "priv" => "priv_".to_string(),
2134            "try" => "try_".to_string(),
2135            "typeof" => "typeof_".to_string(),
2136            "unsized" => "unsized_".to_string(),
2137            "virtual" => "virtual_".to_string(),
2138            "yield" => "yield_".to_string(),
2139            _ => result,
2140        }
2141    }
2142
2143    /// Q2.4: render a `/// Constraint: …` doc comment for a field
2144    /// when its OpenAPI schema declares any constraint annotations.
2145    /// No-op when constraints are empty or `mode = "off"`.
2146    ///
2147    /// **Doc-comment only** — by deliberate design we never emit
2148    /// `#[validate(...)]` attributes. Constraints belong to the wire
2149    /// contract; the server is the source of truth.
2150    fn generate_constraint_doc(
2151        &self,
2152        constraints: &crate::analysis::PropertyConstraints,
2153    ) -> TokenStream {
2154        use crate::type_mapping::ConstraintMode;
2155
2156        if constraints.is_empty() {
2157            return TokenStream::new();
2158        }
2159        match self.config.types.constraint_mode() {
2160            ConstraintMode::Off => TokenStream::new(),
2161            ConstraintMode::Doc => {
2162                let formatted = format_constraints_doc(constraints);
2163                quote! { #[doc = #formatted] }
2164            }
2165        }
2166    }
2167
2168    fn sanitize_doc_comment(&self, desc: &str) -> String {
2169        // Sanitize description to prevent doctest failures
2170        let mut result = desc.to_string();
2171
2172        // Look for potential code examples that might be interpreted as doctests
2173        // Common patterns that cause issues:
2174        // - Lines that look like standalone expressions
2175        // - JSON-like content
2176        // - Template strings with {}
2177
2178        // If the description contains what looks like code, wrap it in a text block
2179        if result.contains('\n')
2180            && (result.contains('{')
2181                || result.contains("```")
2182                || result.contains("Human:")
2183                || result.contains("Assistant:")
2184                || result
2185                    .lines()
2186                    .any(|line| line.trim().starts_with('"') && line.trim().ends_with('"')))
2187        {
2188            // If it already has code blocks, add ignore annotation
2189            if result.contains("```") {
2190                result = result.replace("```", "```ignore");
2191            } else {
2192                // Wrap the entire description in an ignored code block if it looks like code
2193                if result.lines().any(|line| {
2194                    let trimmed = line.trim();
2195                    trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() > 2
2196                }) {
2197                    result = format!("```ignore\n{result}\n```");
2198                }
2199            }
2200        }
2201
2202        result
2203    }
2204
2205    pub(crate) fn to_rust_type_name(&self, s: &str) -> String {
2206        // Convert string to valid Rust type name (PascalCase)
2207        let mut result = String::new();
2208        let mut next_upper = true;
2209        let mut prev_was_lower = false;
2210
2211        for c in s.chars() {
2212            match c {
2213                'a'..='z' => {
2214                    if next_upper {
2215                        result.push(c.to_ascii_uppercase());
2216                        next_upper = false;
2217                    } else {
2218                        result.push(c);
2219                    }
2220                    prev_was_lower = true;
2221                }
2222                'A'..='Z' => {
2223                    result.push(c);
2224                    next_upper = false;
2225                    prev_was_lower = false;
2226                }
2227                '0'..='9' => {
2228                    // If previous was lowercase letter and this is start of a number sequence,
2229                    // make it uppercase to improve readability (e.g., Tool20241022 instead of Tool20241022)
2230                    if prev_was_lower && !result.chars().last().unwrap_or(' ').is_ascii_digit() {
2231                        // This is fine as-is, the number follows naturally
2232                    }
2233                    result.push(c);
2234                    next_upper = false;
2235                    prev_was_lower = false;
2236                }
2237                '_' | '-' | '.' | ' ' => {
2238                    // Skip underscore/separator and make next char uppercase
2239                    next_upper = true;
2240                    prev_was_lower = false;
2241                }
2242                _ => {
2243                    // Other special characters - treat as word boundary
2244                    next_upper = true;
2245                    prev_was_lower = false;
2246                }
2247            }
2248        }
2249
2250        // Handle empty result
2251        if result.is_empty() {
2252            result = "Type".to_string();
2253        }
2254
2255        // Ensure type name starts with a letter (not a number)
2256        if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2257            result = format!("Type{result}");
2258        }
2259
2260        // Avoid masking ubiquitous std types and traits. cloudflare has a
2261        // schema literally named `Result`, gcore has `Default`; emitting
2262        // `pub enum Result { ... }` shadows std::result::Result and breaks
2263        // every method's `-> Result<T, ApiOpError<...>>`. Same for impls
2264        // like `impl Default for HttpClient { ... }` when `Default` resolves
2265        // to the local type alias.
2266        if matches!(
2267            result.as_str(),
2268            "Result"
2269                | "Option"
2270                | "Box"
2271                | "Vec"
2272                | "String"
2273                | "Some"
2274                | "None"
2275                | "Ok"
2276                | "Err"
2277                | "Default"
2278                | "Clone"
2279                | "Debug"
2280                | "Send"
2281                | "Sync"
2282                | "Sized"
2283                | "Iterator"
2284                | "From"
2285                | "Into"
2286                | "TryFrom"
2287                | "TryInto"
2288                | "AsRef"
2289                | "AsMut"
2290        ) {
2291            result.push_str("Type");
2292        }
2293
2294        result
2295    }
2296
2297    fn to_rust_field_name(&self, s: &str) -> String {
2298        // Track sign / leading-non-alpha so e.g. `+1` and `-1` produce
2299        // distinct field names instead of both collapsing to `field_1`
2300        // (observed in github.json's reactions schemas).
2301        let leading_marker = match s.chars().next() {
2302            Some('-') if s.len() > 1 => "neg_",
2303            Some('+') if s.len() > 1 => "pos_",
2304            _ => "",
2305        };
2306
2307        // Convert field name to snake_case properly
2308        let mut result = String::new();
2309        let mut prev_was_upper = false;
2310        let mut prev_was_underscore = false;
2311
2312        for (i, c) in s.chars().enumerate() {
2313            match c {
2314                'A'..='Z' => {
2315                    // Add underscore before uppercase if previous was lowercase
2316                    if i > 0 && !prev_was_upper && !prev_was_underscore {
2317                        result.push('_');
2318                    }
2319                    result.push(c.to_ascii_lowercase());
2320                    prev_was_upper = true;
2321                    prev_was_underscore = false;
2322                }
2323                'a'..='z' | '0'..='9' => {
2324                    result.push(c);
2325                    prev_was_upper = false;
2326                    prev_was_underscore = false;
2327                }
2328                '-' | '.' | '_' | '@' | '#' | '$' | ' ' => {
2329                    if !prev_was_underscore && !result.is_empty() {
2330                        result.push('_');
2331                        prev_was_underscore = true;
2332                    }
2333                    prev_was_upper = false;
2334                }
2335                _ => {
2336                    // For other special characters, convert to underscore
2337                    if !prev_was_underscore && !result.is_empty() {
2338                        result.push('_');
2339                    }
2340                    prev_was_upper = false;
2341                    prev_was_underscore = true;
2342                }
2343            }
2344        }
2345
2346        // Clean up result
2347        let mut result = result.trim_matches('_').to_string();
2348        if result.is_empty() {
2349            return "field".to_string();
2350        }
2351
2352        // Ensure field name starts with a letter or underscore (not a number)
2353        if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2354            result = format!("field_{leading_marker}{result}");
2355        } else if !leading_marker.is_empty() {
2356            result = format!("{leading_marker}{result}");
2357        }
2358
2359        // `self`, `super`, `crate`, `Self` are NOT permitted as raw identifiers
2360        // (they trigger an `r#self cannot be a raw identifier` panic in
2361        // proc_macro2). Suffix them instead.
2362        if matches!(result.as_str(), "self" | "super" | "crate" | "Self") {
2363            return format!("{result}_field");
2364        }
2365        // Handle reserved keywords using raw identifiers (r#keyword)
2366        if Self::is_rust_keyword(&result) {
2367            format!("r#{result}")
2368        } else {
2369            result
2370        }
2371    }
2372
2373    /// Check if a string is a Rust keyword that needs raw identifier treatment
2374    pub fn is_rust_keyword(s: &str) -> bool {
2375        matches!(
2376            s,
2377            "type"
2378                | "match"
2379                | "fn"
2380                | "struct"
2381                | "enum"
2382                | "impl"
2383                | "trait"
2384                | "mod"
2385                | "use"
2386                | "pub"
2387                | "const"
2388                | "static"
2389                | "let"
2390                | "mut"
2391                | "ref"
2392                | "move"
2393                | "return"
2394                | "if"
2395                | "else"
2396                | "while"
2397                | "for"
2398                | "loop"
2399                | "break"
2400                | "continue"
2401                | "self"
2402                | "super"
2403                | "crate"
2404                | "async"
2405                | "await"
2406                | "override"
2407                | "box"
2408                | "dyn"
2409                | "where"
2410                | "in"
2411                | "abstract"
2412                | "become"
2413                | "do"
2414                | "final"
2415                | "macro"
2416                | "priv"
2417                | "try"
2418                | "typeof"
2419                | "unsized"
2420                | "virtual"
2421                | "yield"
2422                // Rust 2024 edition reservations.
2423                | "gen"
2424        )
2425    }
2426
2427    /// Create a proc_macro2::Ident from a field name, handling r# raw identifiers
2428    pub fn to_field_ident(name: &str) -> proc_macro2::Ident {
2429        if let Some(raw) = name.strip_prefix("r#") {
2430            proc_macro2::Ident::new_raw(raw, proc_macro2::Span::call_site())
2431        } else {
2432            proc_macro2::Ident::new(name, proc_macro2::Span::call_site())
2433        }
2434    }
2435
2436    fn to_camel_case(&self, s: &str) -> String {
2437        // Convert snake_case or other formats to camelCase
2438        let mut result = String::new();
2439        let mut capitalize_next = false;
2440
2441        for (i, c) in s.chars().enumerate() {
2442            match c {
2443                '_' | '-' | '.' | ' ' => {
2444                    // Word boundary - capitalize next letter
2445                    capitalize_next = true;
2446                }
2447                'A'..='Z' => {
2448                    if i == 0 {
2449                        // First character should be lowercase in camelCase
2450                        result.push(c.to_ascii_lowercase());
2451                    } else if capitalize_next {
2452                        result.push(c);
2453                        capitalize_next = false;
2454                    } else {
2455                        result.push(c.to_ascii_lowercase());
2456                    }
2457                }
2458                'a'..='z' | '0'..='9' => {
2459                    if capitalize_next {
2460                        result.push(c.to_ascii_uppercase());
2461                        capitalize_next = false;
2462                    } else {
2463                        result.push(c);
2464                    }
2465                }
2466                _ => {
2467                    // Other characters - treat as word boundary
2468                    capitalize_next = true;
2469                }
2470            }
2471        }
2472
2473        if result.is_empty() {
2474            return "field".to_string();
2475        }
2476
2477        result
2478    }
2479
2480    fn generate_composition_struct(
2481        &self,
2482        schema: &crate::analysis::AnalyzedSchema,
2483        schemas: &[crate::analysis::SchemaRef],
2484    ) -> Result<TokenStream> {
2485        let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2486
2487        // For composition, we can either:
2488        // 1. Flatten all referenced schemas into one struct (if they're all objects)
2489        // 2. Use serde(flatten) to compose them at runtime
2490        // For now, let's use approach 2 with serde(flatten)
2491
2492        let fields = schemas.iter().enumerate().map(|(i, schema_ref)| {
2493            let field_name = format_ident!("part_{}", i);
2494            let field_type = format_ident!("{}", self.to_rust_type_name(&schema_ref.target));
2495
2496            quote! {
2497                #[serde(flatten)]
2498                pub #field_name: #field_type,
2499            }
2500        });
2501
2502        let doc_comment = if let Some(desc) = &schema.description {
2503            quote! { #[doc = #desc] }
2504        } else {
2505            TokenStream::new()
2506        };
2507
2508        // Generate derives with optional Specta support
2509        let derives = if self.config.enable_specta {
2510            quote! {
2511                #[derive(Debug, Clone, Deserialize, Serialize)]
2512                #[cfg_attr(feature = "specta", derive(specta::Type))]
2513            }
2514        } else {
2515            quote! {
2516                #[derive(Debug, Clone, Deserialize, Serialize)]
2517            }
2518        };
2519
2520        Ok(quote! {
2521            #doc_comment
2522            #derives
2523            pub struct #struct_name {
2524                #(#fields)*
2525            }
2526        })
2527    }
2528
2529    #[allow(dead_code)]
2530    fn find_missing_types(&self, analysis: &SchemaAnalysis) -> std::collections::HashSet<String> {
2531        let mut missing = std::collections::HashSet::new();
2532        let defined_types: std::collections::HashSet<String> =
2533            analysis.schemas.keys().cloned().collect();
2534
2535        // Check all references in union variants
2536        for schema in analysis.schemas.values() {
2537            match &schema.schema_type {
2538                crate::analysis::SchemaType::Union { variants } => {
2539                    for variant in variants {
2540                        if !defined_types.contains(&variant.target) {
2541                            missing.insert(variant.target.clone());
2542                        }
2543                    }
2544                }
2545                crate::analysis::SchemaType::DiscriminatedUnion { variants, .. } => {
2546                    for variant in variants {
2547                        if !defined_types.contains(&variant.type_name) {
2548                            missing.insert(variant.type_name.clone());
2549                        }
2550                    }
2551                }
2552                crate::analysis::SchemaType::Object { properties, .. } => {
2553                    // Sort properties for deterministic iteration
2554                    let mut sorted_props: Vec<_> = properties.iter().collect();
2555                    sorted_props.sort_by_key(|(name, _)| name.as_str());
2556                    for (_, prop) in sorted_props {
2557                        if let crate::analysis::SchemaType::Reference { target } = &prop.schema_type
2558                        {
2559                            if !defined_types.contains(target) {
2560                                missing.insert(target.clone());
2561                            }
2562                        }
2563                    }
2564                }
2565                crate::analysis::SchemaType::Reference { target }
2566                    if !defined_types.contains(target) =>
2567                {
2568                    missing.insert(target.clone());
2569                }
2570                _ => {}
2571            }
2572        }
2573
2574        missing
2575    }
2576
2577    #[allow(clippy::only_used_in_recursion)]
2578    fn generate_array_item_type(
2579        &self,
2580        item_type: &crate::analysis::SchemaType,
2581        analysis: &crate::analysis::SchemaAnalysis,
2582    ) -> TokenStream {
2583        use crate::analysis::SchemaType;
2584
2585        match item_type {
2586            SchemaType::Primitive { rust_type, .. } => {
2587                // The string here may be anything from `i64` / `String` to
2588                // `serde_json::Value` to `Vec<serde_json::Value>` to
2589                // `BTreeMap<String, T>`. Parse it as a syn::Type so we get
2590                // the right tokens regardless of generics.
2591                if let Ok(parsed) = syn::parse_str::<syn::Type>(rust_type) {
2592                    quote! { #parsed }
2593                } else if rust_type.contains("::") {
2594                    let parts: Vec<_> = rust_type
2595                        .split("::")
2596                        .map(|p| format_ident!("{}", p))
2597                        .collect();
2598                    quote! { #(#parts)::* }
2599                } else {
2600                    let type_ident = format_ident!("{}", rust_type);
2601                    quote! { #type_ident }
2602                }
2603            }
2604            SchemaType::Reference { target } => {
2605                let target_type = format_ident!("{}", self.to_rust_type_name(target));
2606                // Wrap recursive references in Box<T> for heap allocation in arrays
2607                if analysis.dependencies.recursive_schemas.contains(target) {
2608                    quote! { Box<#target_type> }
2609                } else {
2610                    quote! { #target_type }
2611                }
2612            }
2613            SchemaType::Array { item_type } => {
2614                // Nested arrays
2615                let inner_type = self.generate_array_item_type(item_type, analysis);
2616                quote! { Vec<#inner_type> }
2617            }
2618            _ => {
2619                // Fallback for complex types
2620                quote! { serde_json::Value }
2621            }
2622        }
2623    }
2624
2625    /// Convert a type name to a variant name (e.g., OutputMessage -> OutputMessage, FileSearchToolCall -> FileSearchToolCall)
2626    fn type_name_to_variant_name(&self, type_name: &str) -> String {
2627        // Handle primitive types specially
2628        match type_name {
2629            "bool" => return "Boolean".to_string(),
2630            "i8" | "i16" | "i32" | "i64" | "i128" => return "Integer".to_string(),
2631            "u8" | "u16" | "u32" | "u64" | "u128" => return "UnsignedInteger".to_string(),
2632            "f32" | "f64" => return "Number".to_string(),
2633            "String" => return "String".to_string(),
2634            "serde_json::Value" => return "Value".to_string(),
2635            // Q2 typed-scalar paths. Without these the fallback PascalCase
2636            // pass over `bytes::Bytes` produces `BytesBytes(BytesBytes)`,
2637            // which then can't compile because no `BytesBytes` type exists.
2638            "bytes::Bytes" => return "Binary".to_string(),
2639            "chrono::DateTime<chrono::Utc>" => return "DateTime".to_string(),
2640            "chrono::NaiveDate" => return "Date".to_string(),
2641            "chrono::NaiveTime" => return "Time".to_string(),
2642            "uuid::Uuid" => return "Uuid".to_string(),
2643            "url::Url" => return "Url".to_string(),
2644            "std::net::Ipv4Addr" => return "Ipv4".to_string(),
2645            "std::net::Ipv6Addr" => return "Ipv6".to_string(),
2646            _ => {}
2647        }
2648
2649        // Handle Vec types
2650        if type_name.starts_with("Vec<") && type_name.ends_with(">") {
2651            let inner = &type_name[4..type_name.len() - 1];
2652            // Handle nested Vec types specially
2653            if inner.starts_with("Vec<") && inner.ends_with(">") {
2654                let inner_inner = &inner[4..inner.len() - 1];
2655                return format!("{}ArrayArray", self.type_name_to_variant_name(inner_inner));
2656            }
2657            return format!("{}Array", self.type_name_to_variant_name(inner));
2658        }
2659
2660        // For untagged unions, we want to use the type name itself as the variant name
2661        // since it's already meaningful. This gives us OutputMessage instead of Variant0,
2662        // FileSearchToolCall instead of Variant1, etc.
2663
2664        // Remove common suffixes that might make variant names redundant
2665        let clean_name = type_name
2666            .trim_end_matches("Type")
2667            .trim_end_matches("Schema")
2668            .trim_end_matches("Item");
2669
2670        // Always convert to proper PascalCase to ensure no underscores in enum variants
2671        self.to_rust_type_name(clean_name)
2672    }
2673
2674    /// Ensure unique variant name for generator (similar to analyzer but for generator context)
2675    fn ensure_unique_variant_name_generator(
2676        &self,
2677        base_name: String,
2678        used_names: &mut std::collections::HashSet<String>,
2679        fallback_index: usize,
2680    ) -> String {
2681        if used_names.insert(base_name.clone()) {
2682            return base_name;
2683        }
2684
2685        // Try with numbers
2686        for i in 2..100 {
2687            let numbered_name = format!("{base_name}{i}");
2688            if used_names.insert(numbered_name.clone()) {
2689                return numbered_name;
2690            }
2691        }
2692
2693        // Fallback to Variant{index} if all else fails
2694        let fallback = format!("Variant{fallback_index}");
2695        used_names.insert(fallback.clone());
2696        fallback
2697    }
2698
2699    /// Find the request type for a given operation ID using the analyzed operation info
2700    fn find_request_type_for_operation(
2701        &self,
2702        operation_id: &str,
2703        analysis: &SchemaAnalysis,
2704    ) -> Option<String> {
2705        // Use the operation analysis to get the actual request body schema
2706        analysis.operations.get(operation_id).and_then(|op| {
2707            op.request_body
2708                .as_ref()
2709                .and_then(|rb| rb.schema_name().map(|s| s.to_string()))
2710        })
2711    }
2712
2713    /// Resolve the correct streaming event type based on EventFlow pattern
2714    fn resolve_streaming_event_type(
2715        &self,
2716        endpoint: &crate::streaming::StreamingEndpoint,
2717        analysis: &SchemaAnalysis,
2718    ) -> Result<String> {
2719        match &endpoint.event_flow {
2720            crate::streaming::EventFlow::Simple => {
2721                // For simple streaming, use the response type directly
2722                // Validate that the specified type exists in the schema
2723                if analysis.schemas.contains_key(&endpoint.event_union_type) {
2724                    Ok(endpoint.event_union_type.to_string())
2725                } else {
2726                    Err(crate::error::GeneratorError::ValidationError(format!(
2727                        "Streaming response type '{}' not found in schema for simple streaming endpoint '{}'",
2728                        endpoint.event_union_type, endpoint.operation_id
2729                    )))
2730                }
2731            }
2732            crate::streaming::EventFlow::StartDeltaStop { .. } => {
2733                // For complex event-based streaming, ensure we have a proper union type
2734                // For now, use the specified event_union_type but add validation
2735                if analysis.schemas.contains_key(&endpoint.event_union_type) {
2736                    Ok(endpoint.event_union_type.to_string())
2737                } else {
2738                    Err(crate::error::GeneratorError::ValidationError(format!(
2739                        "Event union type '{}' not found in schema for complex streaming endpoint '{}'",
2740                        endpoint.event_union_type, endpoint.operation_id
2741                    )))
2742                }
2743            }
2744        }
2745    }
2746
2747    /// Generate streaming error types
2748    fn generate_streaming_error_types(&self) -> Result<TokenStream> {
2749        Ok(quote! {
2750            /// Error type for streaming operations
2751            #[derive(Debug, thiserror::Error)]
2752            pub enum StreamingError {
2753                #[error("Connection error: {0}")]
2754                Connection(String),
2755                #[error("HTTP error: {status}")]
2756                Http { status: u16 },
2757                #[error("SSE parsing error: {0}")]
2758                Parsing(String),
2759                #[error("Authentication error: {0}")]
2760                Authentication(String),
2761                #[error("Rate limit error: {0}")]
2762                RateLimit(String),
2763                #[error("API error: {0}")]
2764                Api(String),
2765                #[error("Timeout error: {0}")]
2766                Timeout(String),
2767                #[error("JSON serialization/deserialization error: {0}")]
2768                Json(#[from] serde_json::Error),
2769                #[error("Request error: {0}")]
2770                Request(reqwest::Error),
2771            }
2772
2773            impl From<reqwest::header::InvalidHeaderValue> for StreamingError {
2774                fn from(err: reqwest::header::InvalidHeaderValue) -> Self {
2775                    StreamingError::Api(format!("Invalid header value: {}", err))
2776                }
2777            }
2778
2779            impl From<reqwest::Error> for StreamingError {
2780                fn from(err: reqwest::Error) -> Self {
2781                    if err.is_timeout() {
2782                        StreamingError::Timeout(err.to_string())
2783                    } else if err.is_status() {
2784                        if let Some(status) = err.status() {
2785                            StreamingError::Http { status: status.as_u16() }
2786                        } else {
2787                            StreamingError::Connection(err.to_string())
2788                        }
2789                    } else {
2790                        StreamingError::Request(err)
2791                    }
2792                }
2793            }
2794        })
2795    }
2796
2797    /// Generate trait for a streaming endpoint
2798    fn generate_endpoint_trait(
2799        &self,
2800        endpoint: &crate::streaming::StreamingEndpoint,
2801        analysis: &SchemaAnalysis,
2802    ) -> Result<TokenStream> {
2803        use crate::streaming::HttpMethod;
2804
2805        let trait_name = format_ident!(
2806            "{}StreamingClient",
2807            self.to_rust_type_name(&endpoint.operation_id)
2808        );
2809        let method_name =
2810            format_ident!("stream_{}", self.to_rust_field_name(&endpoint.operation_id));
2811        let event_type =
2812            format_ident!("{}", self.resolve_streaming_event_type(endpoint, analysis)?);
2813
2814        // Generate method signature based on HTTP method
2815        let method_signature = match endpoint.http_method {
2816            HttpMethod::Get => {
2817                // Generate parameters from query_parameters
2818                let mut param_defs = Vec::new();
2819                for qp in &endpoint.query_parameters {
2820                    let param_name = format_ident!("{}", self.to_rust_field_name(&qp.name));
2821                    if qp.required {
2822                        param_defs.push(quote! { #param_name: &str });
2823                    } else {
2824                        param_defs.push(quote! { #param_name: Option<&str> });
2825                    }
2826                }
2827                quote! {
2828                    async fn #method_name(
2829                        &self,
2830                        #(#param_defs),*
2831                    ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error>;
2832                }
2833            }
2834            HttpMethod::Post => {
2835                // Find the request type for this operation
2836                let request_type = self
2837                    .find_request_type_for_operation(&endpoint.operation_id, analysis)
2838                    .unwrap_or_else(|| "serde_json::Value".to_string());
2839                let request_type_ident = if request_type.contains("::") {
2840                    let parts: Vec<&str> = request_type.split("::").collect();
2841                    let path_parts: Vec<_> = parts.iter().map(|p| format_ident!("{}", p)).collect();
2842                    quote! { #(#path_parts)::* }
2843                } else {
2844                    let ident = format_ident!("{}", request_type);
2845                    quote! { #ident }
2846                };
2847                quote! {
2848                    async fn #method_name(
2849                        &self,
2850                        request: #request_type_ident,
2851                    ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error>;
2852                }
2853            }
2854        };
2855
2856        Ok(quote! {
2857            /// Streaming client trait for this endpoint
2858            #[async_trait]
2859            pub trait #trait_name {
2860                type Error: std::error::Error + Send + Sync + 'static;
2861
2862                /// Stream events from the API
2863                #method_signature
2864            }
2865        })
2866    }
2867
2868    /// Generate streaming client implementation
2869    fn generate_streaming_client_impl(
2870        &self,
2871        streaming_config: &crate::streaming::StreamingConfig,
2872        analysis: &SchemaAnalysis,
2873    ) -> Result<TokenStream> {
2874        let client_name = format_ident!(
2875            "{}Client",
2876            self.to_rust_type_name(&streaming_config.client_module_name)
2877        );
2878
2879        // Generate struct fields
2880        // Always include custom_headers for flexibility (like HttpClient does)
2881        let mut struct_fields = vec![
2882            quote! { base_url: String },
2883            quote! { api_key: Option<String> },
2884            quote! { http_client: reqwest::Client },
2885            quote! { custom_headers: std::collections::BTreeMap<String, String> },
2886        ];
2887
2888        let has_optional_headers = !streaming_config
2889            .endpoints
2890            .iter()
2891            .all(|e| e.optional_headers.is_empty());
2892
2893        if has_optional_headers {
2894            struct_fields
2895                .push(quote! { optional_headers: std::collections::BTreeMap<String, String> });
2896        }
2897
2898        // Generate constructor
2899        // Use configured base URL as default, or fallback to generic example
2900        let default_base_url = if let Some(ref streaming_config) = self.config.streaming_config {
2901            streaming_config
2902                .endpoints
2903                .first()
2904                .and_then(|e| e.base_url.as_deref())
2905                .unwrap_or("https://api.example.com")
2906        } else {
2907            "https://api.example.com"
2908        };
2909
2910        // Build constructor fields based on what the struct has
2911        let constructor_fields = if has_optional_headers {
2912            quote! {
2913                base_url: #default_base_url.to_string(),
2914                api_key: None,
2915                http_client: reqwest::Client::new(),
2916                custom_headers: std::collections::BTreeMap::new(),
2917                optional_headers: std::collections::BTreeMap::new(),
2918            }
2919        } else {
2920            quote! {
2921                base_url: #default_base_url.to_string(),
2922                api_key: None,
2923                http_client: reqwest::Client::new(),
2924                custom_headers: std::collections::BTreeMap::new(),
2925            }
2926        };
2927
2928        // Optional headers method only if the struct has the field
2929        let optional_headers_method = if has_optional_headers {
2930            quote! {
2931                /// Set optional headers for all requests
2932                pub fn set_optional_headers(&mut self, headers: std::collections::BTreeMap<String, String>) {
2933                    self.optional_headers = headers;
2934                }
2935            }
2936        } else {
2937            TokenStream::new()
2938        };
2939
2940        let constructor = quote! {
2941            impl #client_name {
2942                /// Create a new streaming client
2943                pub fn new() -> Self {
2944                    Self {
2945                        #constructor_fields
2946                    }
2947                }
2948
2949                /// Set the base URL for API requests
2950                pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
2951                    self.base_url = base_url.into();
2952                    self
2953                }
2954
2955                /// Set the API key for authentication
2956                pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
2957                    self.api_key = Some(api_key.into());
2958                    self
2959                }
2960
2961                /// Add a custom header to all requests
2962                pub fn with_header(
2963                    mut self,
2964                    name: impl Into<String>,
2965                    value: impl Into<String>,
2966                ) -> Self {
2967                    self.custom_headers.insert(name.into(), value.into());
2968                    self
2969                }
2970
2971                /// Set the HTTP client
2972                pub fn with_http_client(mut self, client: reqwest::Client) -> Self {
2973                    self.http_client = client;
2974                    self
2975                }
2976
2977                #optional_headers_method
2978            }
2979        };
2980
2981        // Generate trait implementations for each endpoint
2982        let mut trait_impls = Vec::new();
2983        for endpoint in &streaming_config.endpoints {
2984            let trait_impl = self.generate_endpoint_trait_impl(endpoint, &client_name, analysis)?;
2985            trait_impls.push(trait_impl);
2986        }
2987
2988        // Add Default implementation
2989        let default_impl = quote! {
2990            impl Default for #client_name {
2991                fn default() -> Self {
2992                    Self::new()
2993                }
2994            }
2995        };
2996
2997        Ok(quote! {
2998            /// Streaming client implementation
2999            #[derive(Debug, Clone)]
3000            pub struct #client_name {
3001                #(#struct_fields,)*
3002            }
3003
3004            #constructor
3005
3006            #default_impl
3007
3008            #(#trait_impls)*
3009        })
3010    }
3011
3012    /// Generate trait implementation for a specific endpoint
3013    fn generate_endpoint_trait_impl(
3014        &self,
3015        endpoint: &crate::streaming::StreamingEndpoint,
3016        client_name: &proc_macro2::Ident,
3017        analysis: &SchemaAnalysis,
3018    ) -> Result<TokenStream> {
3019        use crate::streaming::HttpMethod;
3020
3021        let trait_name = format_ident!(
3022            "{}StreamingClient",
3023            self.to_rust_type_name(&endpoint.operation_id)
3024        );
3025        let method_name =
3026            format_ident!("stream_{}", self.to_rust_field_name(&endpoint.operation_id));
3027        let event_type =
3028            format_ident!("{}", self.resolve_streaming_event_type(endpoint, analysis)?);
3029
3030        // Generate required headers
3031        let mut header_setup = Vec::new();
3032        for (name, value) in &endpoint.required_headers {
3033            header_setup.push(quote! {
3034                headers.insert(#name, HeaderValue::from_static(#value));
3035            });
3036        }
3037
3038        // Add authentication header
3039        // If auth_header is configured, use that; otherwise default to Bearer auth on Authorization header
3040        if let Some(auth_header) = &endpoint.auth_header {
3041            match auth_header {
3042                crate::streaming::AuthHeader::Bearer(header_name) => {
3043                    header_setup.push(quote! {
3044                        if let Some(ref api_key) = self.api_key {
3045                            headers.insert(#header_name, HeaderValue::from_str(&format!("Bearer {}", api_key))?);
3046                        }
3047                    });
3048                }
3049                crate::streaming::AuthHeader::ApiKey(header_name) => {
3050                    header_setup.push(quote! {
3051                        if let Some(ref api_key) = self.api_key {
3052                            headers.insert(#header_name, HeaderValue::from_str(api_key)?);
3053                        }
3054                    });
3055                }
3056            }
3057        } else {
3058            // Default: use api_key as Bearer token on Authorization header
3059            header_setup.push(quote! {
3060                if let Some(ref api_key) = self.api_key {
3061                    headers.insert("Authorization", HeaderValue::from_str(&format!("Bearer {}", api_key))?);
3062                }
3063            });
3064        }
3065
3066        // Always add custom_headers (like HttpClient does)
3067        header_setup.push(quote! {
3068            for (name, value) in &self.custom_headers {
3069                if let (Ok(header_name), Ok(header_value)) = (reqwest::header::HeaderName::from_bytes(name.as_bytes()), HeaderValue::from_str(value)) {
3070                    headers.insert(header_name, header_value);
3071                }
3072            }
3073        });
3074
3075        // Add optional headers (for endpoint-specific optional headers)
3076        if !endpoint.optional_headers.is_empty() {
3077            header_setup.push(quote! {
3078                for (key, value) in &self.optional_headers {
3079                    if let (Ok(header_name), Ok(header_value)) = (reqwest::header::HeaderName::from_bytes(key.as_bytes()), HeaderValue::from_str(value)) {
3080                        headers.insert(header_name, header_value);
3081                    }
3082                }
3083            });
3084        }
3085
3086        // Generate different code for GET vs POST
3087        match endpoint.http_method {
3088            HttpMethod::Get => self.generate_get_streaming_impl(
3089                endpoint,
3090                client_name,
3091                &trait_name,
3092                &method_name,
3093                &event_type,
3094                &header_setup,
3095            ),
3096            HttpMethod::Post => self.generate_post_streaming_impl(
3097                endpoint,
3098                client_name,
3099                &trait_name,
3100                &method_name,
3101                &event_type,
3102                &header_setup,
3103                analysis,
3104            ),
3105        }
3106    }
3107
3108    /// Generate streaming implementation for GET endpoints
3109    fn generate_get_streaming_impl(
3110        &self,
3111        endpoint: &crate::streaming::StreamingEndpoint,
3112        client_name: &proc_macro2::Ident,
3113        trait_name: &proc_macro2::Ident,
3114        method_name: &proc_macro2::Ident,
3115        event_type: &proc_macro2::Ident,
3116        header_setup: &[TokenStream],
3117    ) -> Result<TokenStream> {
3118        let path = &endpoint.path;
3119
3120        // Generate method parameters from query_parameters
3121        let mut param_defs = Vec::new();
3122        let mut query_params = Vec::new();
3123
3124        for qp in &endpoint.query_parameters {
3125            let param_name = format_ident!("{}", self.to_rust_field_name(&qp.name));
3126            let param_name_str = &qp.name;
3127
3128            if qp.required {
3129                param_defs.push(quote! { #param_name: &str });
3130                query_params.push(quote! {
3131                    url.query_pairs_mut().append_pair(#param_name_str, #param_name);
3132                });
3133            } else {
3134                param_defs.push(quote! { #param_name: Option<&str> });
3135                query_params.push(quote! {
3136                    if let Some(v) = #param_name {
3137                        url.query_pairs_mut().append_pair(#param_name_str, v);
3138                    }
3139                });
3140            }
3141        }
3142
3143        // Generate URL construction for GET
3144        let url_construction = quote! {
3145            let base_url = url::Url::parse(&self.base_url)
3146                .map_err(|e| StreamingError::Connection(format!("Invalid base URL: {}", e)))?;
3147            let path_to_join = #path.trim_start_matches('/');
3148            let mut url = base_url.join(path_to_join)
3149                .map_err(|e| StreamingError::Connection(format!("URL join error: {}", e)))?;
3150            #(#query_params)*
3151        };
3152
3153        let instrument_skip = quote! { #[instrument(skip(self), name = "streaming_get_request")] };
3154
3155        Ok(quote! {
3156            #[async_trait]
3157            impl #trait_name for #client_name {
3158                type Error = StreamingError;
3159
3160                #instrument_skip
3161                async fn #method_name(
3162                    &self,
3163                    #(#param_defs),*
3164                ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error> {
3165                    debug!("Starting streaming GET request");
3166
3167                    let mut headers = HeaderMap::new();
3168                    #(#header_setup)*
3169
3170                    #url_construction
3171                    let url_str = url.to_string();
3172                    debug!("Making streaming GET request to: {}", url_str);
3173
3174                    let request_builder = self.http_client
3175                        .get(url_str)
3176                        .headers(headers);
3177
3178                    debug!("Creating SSE stream from request");
3179                    let stream = parse_sse_stream::<#event_type>(request_builder).await?;
3180                    info!("SSE stream created successfully");
3181                    Ok(Box::pin(stream))
3182                }
3183            }
3184        })
3185    }
3186
3187    /// Generate streaming implementation for POST endpoints
3188    #[allow(clippy::too_many_arguments)]
3189    fn generate_post_streaming_impl(
3190        &self,
3191        endpoint: &crate::streaming::StreamingEndpoint,
3192        client_name: &proc_macro2::Ident,
3193        trait_name: &proc_macro2::Ident,
3194        method_name: &proc_macro2::Ident,
3195        event_type: &proc_macro2::Ident,
3196        header_setup: &[TokenStream],
3197        analysis: &SchemaAnalysis,
3198    ) -> Result<TokenStream> {
3199        let path = &endpoint.path;
3200
3201        // Find the request type for this operation
3202        let request_type = self
3203            .find_request_type_for_operation(&endpoint.operation_id, analysis)
3204            .unwrap_or_else(|| "serde_json::Value".to_string());
3205        let request_type_ident = if request_type.contains("::") {
3206            let parts: Vec<&str> = request_type.split("::").collect();
3207            let path_parts: Vec<_> = parts.iter().map(|p| format_ident!("{}", p)).collect();
3208            quote! { #(#path_parts)::* }
3209        } else {
3210            let ident = format_ident!("{}", request_type);
3211            quote! { #ident }
3212        };
3213
3214        // Generate URL construction for POST
3215        let url_construction = quote! {
3216            let base_url = url::Url::parse(&self.base_url)
3217                .map_err(|e| StreamingError::Connection(format!("Invalid base URL: {}", e)))?;
3218            let path_to_join = #path.trim_start_matches('/');
3219            let url = base_url.join(path_to_join)
3220                .map_err(|e| StreamingError::Connection(format!("URL join error: {}", e)))?
3221                .to_string();
3222        };
3223
3224        // Generate stream parameter setup (only for POST with stream_parameter)
3225        let stream_param = &endpoint.stream_parameter;
3226        let stream_setup = if stream_param.is_empty() {
3227            quote! {
3228                let streaming_request = request;
3229            }
3230        } else {
3231            quote! {
3232                // Ensure streaming is enabled
3233                let mut streaming_request = request;
3234                if let Ok(mut request_value) = serde_json::to_value(&streaming_request) {
3235                    if let Some(obj) = request_value.as_object_mut() {
3236                        obj.insert(#stream_param.to_string(), serde_json::Value::Bool(true));
3237                    }
3238                    streaming_request = serde_json::from_value(request_value)?;
3239                }
3240            }
3241        };
3242
3243        Ok(quote! {
3244            #[async_trait]
3245            impl #trait_name for #client_name {
3246                type Error = StreamingError;
3247
3248                #[instrument(skip(self, request), name = "streaming_post_request")]
3249                async fn #method_name(
3250                    &self,
3251                    request: #request_type_ident,
3252                ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error> {
3253                    debug!("Starting streaming POST request");
3254
3255                    #stream_setup
3256
3257                    let mut headers = HeaderMap::new();
3258                    #(#header_setup)*
3259
3260                    #url_construction
3261                    debug!("Making streaming POST request to: {}", url);
3262
3263                    let request_builder = self.http_client
3264                        .post(&url)
3265                        .headers(headers)
3266                        .json(&streaming_request);
3267
3268                    debug!("Creating SSE stream from request");
3269                    let stream = parse_sse_stream::<#event_type>(request_builder).await?;
3270                    info!("SSE stream created successfully");
3271                    Ok(Box::pin(stream))
3272                }
3273            }
3274        })
3275    }
3276
3277    /// Generate SSE parsing utilities using reqwest-eventsource
3278    fn generate_sse_parser_utilities(
3279        &self,
3280        _streaming_config: &crate::streaming::StreamingConfig,
3281    ) -> Result<TokenStream> {
3282        Ok(quote! {
3283            /// Parse SSE stream from HTTP request using reqwest-eventsource
3284            pub async fn parse_sse_stream<T>(
3285                request_builder: reqwest::RequestBuilder
3286            ) -> Result<impl Stream<Item = Result<T, StreamingError>>, StreamingError>
3287            where
3288                T: serde::de::DeserializeOwned + Send + 'static,
3289            {
3290                let mut event_source = reqwest_eventsource::EventSource::new(request_builder).map_err(|e| {
3291                    StreamingError::Connection(format!("Failed to create event source: {}", e))
3292                })?;
3293
3294                let stream = event_source.filter_map(|event_result| async move {
3295                    match event_result {
3296                        Ok(reqwest_eventsource::Event::Open) => {
3297                            debug!("SSE connection opened");
3298                            None
3299                        }
3300                        Ok(reqwest_eventsource::Event::Message(message)) => {
3301                            // Check if this is a ping event by SSE event type
3302                            if message.event == "ping" {
3303                                debug!("Received SSE ping event, skipping");
3304                                return None;
3305                            }
3306
3307                            // Special handling for empty data
3308                            if message.data.trim().is_empty() {
3309                                debug!("Empty SSE data, skipping");
3310                                return None;
3311                            }
3312
3313                            // Check if this is a ping event in the JSON data
3314                            if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&message.data) {
3315                                if let Some(event_type) = json_value.get("event").and_then(|v| v.as_str()) {
3316                                    if event_type == "ping" {
3317                                        debug!("Received ping event in JSON data, skipping");
3318                                        return None;
3319                                    }
3320                                }
3321
3322                                // Try to parse the full event normally
3323                                match serde_json::from_value::<T>(json_value) {
3324                                    Ok(parsed_event) => {
3325                                        Some(Ok(parsed_event))
3326                                    }
3327                                    Err(e) => {
3328                                        if message.data.contains("ping") || message.event.contains("ping") {
3329                                            debug!("Ignoring ping-related event: {}", message.data);
3330                                            None
3331                                        } else {
3332                                            Some(Err(StreamingError::Parsing(
3333                                                format!("Failed to parse SSE event: {} (raw: {})", e, message.data)
3334                                            )))
3335                                        }
3336                                    }
3337                                }
3338                            } else {
3339                                // Not valid JSON at all
3340                                Some(Err(StreamingError::Parsing(
3341                                    format!("SSE event is not valid JSON: {}", message.data)
3342                                )))
3343                            }
3344                        }
3345                        Err(e) => {
3346                            // Check if this is a normal stream end vs actual error
3347                            match e {
3348                                reqwest_eventsource::Error::StreamEnded => {
3349                                    debug!("SSE stream completed normally");
3350                                    None // Normal stream end, not an error
3351                                }
3352                                reqwest_eventsource::Error::InvalidStatusCode(status, response) => {
3353                                    // We have access to the response body for error details
3354                                    let status_code = status.as_u16();
3355
3356                                    // Read the response body to get error details
3357                                    let error_body = match response.text().await {
3358                                        Ok(body) => body,
3359                                        Err(_) => "Failed to read error response body".to_string()
3360                                    };
3361
3362                                    error!("SSE connection error - HTTP {}: {}", status_code, error_body);
3363
3364                                    let detailed_error = format!(
3365                                        "HTTP {} error: {}",
3366                                        status_code,
3367                                        error_body
3368                                    );
3369
3370                                    Some(Err(StreamingError::Connection(detailed_error)))
3371                                }
3372                                _ => {
3373                                    let error_str = e.to_string();
3374                                    if error_str.contains("stream closed") {
3375                                        debug!("SSE stream closed");
3376                                        None
3377                                    } else {
3378                                        error!("SSE connection error: {}", e);
3379                                        Some(Err(StreamingError::Connection(error_str)))
3380                                    }
3381                                }
3382                            }
3383                        }
3384                    }
3385                });
3386
3387                Ok(stream)
3388            }
3389        })
3390    }
3391
3392    /// Generate reconnection utilities
3393    fn generate_reconnection_utilities(
3394        &self,
3395        reconnect_config: &crate::streaming::ReconnectionConfig,
3396    ) -> Result<TokenStream> {
3397        let max_retries = reconnect_config.max_retries;
3398        let initial_delay = reconnect_config.initial_delay_ms;
3399        let max_delay = reconnect_config.max_delay_ms;
3400        let backoff_multiplier = reconnect_config.backoff_multiplier;
3401
3402        Ok(quote! {
3403            /// Reconnection configuration and utilities
3404            #[derive(Debug, Clone)]
3405            pub struct ReconnectionManager {
3406                max_retries: u32,
3407                initial_delay_ms: u64,
3408                max_delay_ms: u64,
3409                backoff_multiplier: f64,
3410                current_attempt: u32,
3411            }
3412
3413            impl ReconnectionManager {
3414                /// Create a new reconnection manager
3415                pub fn new() -> Self {
3416                    Self {
3417                        max_retries: #max_retries,
3418                        initial_delay_ms: #initial_delay,
3419                        max_delay_ms: #max_delay,
3420                        backoff_multiplier: #backoff_multiplier,
3421                        current_attempt: 0,
3422                    }
3423                }
3424
3425                /// Check if we should retry the connection
3426                pub fn should_retry(&self) -> bool {
3427                    self.current_attempt < self.max_retries
3428                }
3429
3430                /// Get the delay for the next retry attempt
3431                pub fn next_retry_delay(&mut self) -> Duration {
3432                    if !self.should_retry() {
3433                        return Duration::from_secs(0);
3434                    }
3435
3436                    let delay_ms = (self.initial_delay_ms as f64
3437                        * self.backoff_multiplier.powi(self.current_attempt as i32)) as u64;
3438                    let delay_ms = delay_ms.min(self.max_delay_ms);
3439
3440                    self.current_attempt += 1;
3441                    Duration::from_millis(delay_ms)
3442                }
3443
3444                /// Reset the retry counter after a successful connection
3445                pub fn reset(&mut self) {
3446                    self.current_attempt = 0;
3447                }
3448
3449                /// Get the current attempt number
3450                pub fn current_attempt(&self) -> u32 {
3451                    self.current_attempt
3452                }
3453            }
3454
3455            impl Default for ReconnectionManager {
3456                fn default() -> Self {
3457                    Self::new()
3458                }
3459            }
3460        })
3461    }
3462}