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