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