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.
980            #[derive(Debug, Clone)]
981            pub struct ApiError<E> {
982                pub status: u16,
983                pub headers: reqwest::header::HeaderMap,
984                pub body: String,
985                pub typed: Option<E>,
986                pub parse_error: Option<String>,
987            }
988
989            impl<E> ApiError<E> {
990                pub fn is_client_error(&self) -> bool {
991                    (400..500).contains(&self.status)
992                }
993
994                pub fn is_server_error(&self) -> bool {
995                    (500..600).contains(&self.status)
996                }
997
998                /// Retry guidance for the response. Mirrors the previous
999                /// HttpError logic for backwards-compatible retry middleware.
1000                pub fn is_retryable(&self) -> bool {
1001                    matches!(self.status, 429 | 500 | 502 | 503 | 504)
1002                }
1003            }
1004
1005            impl<E: std::fmt::Debug> std::fmt::Display for ApiError<E> {
1006                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1007                    write!(f, "API error {}: {}", self.status, self.body)
1008                }
1009            }
1010
1011            impl<E: std::fmt::Debug> std::error::Error for ApiError<E> {}
1012
1013            /// Result error type returned by every generated operation method.
1014            ///
1015            /// `Transport` covers failures where we never got an inspectable
1016            /// response (network, timeout, middleware, request-side
1017            /// serialization). `Api` covers any case where the server *did*
1018            /// respond — the envelope always carries status + headers + raw
1019            /// body even when the typed deserialize fails.
1020            #[derive(Debug, Error)]
1021            pub enum ApiOpError<E: std::fmt::Debug> {
1022                #[error(transparent)]
1023                Transport(#[from] HttpError),
1024
1025                #[error(transparent)]
1026                Api(ApiError<E>),
1027            }
1028
1029            impl<E: std::fmt::Debug> ApiOpError<E> {
1030                /// Returns the API envelope when this is an `Api` variant.
1031                pub fn api(&self) -> Option<&ApiError<E>> {
1032                    match self {
1033                        Self::Api(e) => Some(e),
1034                        Self::Transport(_) => None,
1035                    }
1036                }
1037
1038                /// True when the underlying error came from the server (i.e.
1039                /// any `Api` variant) rather than the transport layer.
1040                pub fn is_api_error(&self) -> bool {
1041                    matches!(self, Self::Api(_))
1042                }
1043            }
1044
1045            // Direct From impls so `?` works without going through HttpError
1046            // first. Rust's `?` only chains a single `From` conversion.
1047            impl<E: std::fmt::Debug> From<reqwest::Error> for ApiOpError<E> {
1048                fn from(e: reqwest::Error) -> Self {
1049                    Self::Transport(HttpError::Network(e))
1050                }
1051            }
1052
1053            impl<E: std::fmt::Debug> From<reqwest_middleware::Error> for ApiOpError<E> {
1054                fn from(e: reqwest_middleware::Error) -> Self {
1055                    Self::Transport(HttpError::Middleware(e))
1056                }
1057            }
1058
1059            /// Result alias for transport-only error paths (e.g. helpers that
1060            /// don't have a per-operation error type). Generated operation
1061            /// methods use [`ApiOpError`] directly.
1062            pub type HttpResult<T> = Result<T, HttpError>;
1063        }
1064    }
1065
1066    /// Generate mod.rs file that exports all modules
1067    fn generate_mod_file(&self, files: &[GeneratedFile]) -> Result<String> {
1068        let mut module_names = std::collections::BTreeSet::new();
1069
1070        for file in files {
1071            let module_name = if file.path.components().count() > 1 {
1072                file.path.iter().next().and_then(|part| part.to_str())
1073            } else {
1074                file.path.file_stem().and_then(|stem| stem.to_str())
1075            };
1076            if let Some(module_name) = module_name.filter(|name| *name != "mod") {
1077                module_names.insert(module_name.to_string());
1078            }
1079        }
1080        let module_declarations = module_names
1081            .iter()
1082            .map(|name| format!("pub mod {name};"))
1083            .collect::<Vec<_>>();
1084        let pub_uses = module_names
1085            .iter()
1086            .map(|name| format!("pub use {name}::*;"))
1087            .collect::<Vec<_>>();
1088
1089        // `module_name` is a configurable *label* — it does NOT pick
1090        // the on-disk directory (that's `output_dir`) and it does NOT
1091        // determine the Rust module path the user mounts this tree
1092        // at. Surfacing it in the header doc comment is the most
1093        // honest place: a hint to the user about what name was
1094        // configured and how to mount it.
1095        let mount_hint = format!(
1096            "//! Configured `module_name` = `{name}`. Mount this tree under your\n\
1097             //! preferred path, e.g. `pub mod {name};` in your crate root.\n",
1098            name = self.config.module_name,
1099        );
1100        let source_hint = self
1101            .source_provenance
1102            .as_ref()
1103            .map(|source| {
1104                format!(
1105                    "//! Generated by openapi-to-rust v{}. Source OpenAPI document: {source}\n",
1106                    env!("CARGO_PKG_VERSION")
1107                )
1108            })
1109            .unwrap_or_default();
1110
1111        let content = format!(
1112            r#"//! Generated API modules
1113//!
1114//! This module exports all generated API types and clients.
1115//! Do not edit manually - regenerate using the appropriate script.
1116//!
1117{source_hint}
1118{mount_hint}
1119#![allow(unused_imports)]
1120
1121{decls}
1122
1123{uses}
1124"#,
1125            mount_hint = mount_hint,
1126            source_hint = source_hint,
1127            decls = module_declarations.join("\n"),
1128            uses = pub_uses.join("\n"),
1129        );
1130
1131        Ok(content)
1132    }
1133
1134    /// Helper method to write all generated files to disk
1135    pub fn output_artifacts(
1136        &self,
1137        result: &GenerationResult,
1138    ) -> std::collections::BTreeMap<PathBuf, String> {
1139        let mut artifacts = std::collections::BTreeMap::new();
1140        for file in &result.files {
1141            artifacts.insert(file.path.clone(), file.content.clone());
1142        }
1143        artifacts.insert(
1144            result.mod_file.path.clone(),
1145            result.mod_file.content.clone(),
1146        );
1147        if let Some(mut fragment) =
1148            crate::type_mapping::render_required_deps_toml(&result.required_deps)
1149        {
1150            if let Some(source) = &self.source_provenance {
1151                let header = format!(
1152                    "# Generated by openapi-to-rust v{}. Source OpenAPI document: {source}",
1153                    env!("CARGO_PKG_VERSION")
1154                );
1155                fragment = fragment.replacen("# Generated by openapi-to-rust.", &header, 1);
1156            }
1157            artifacts.insert(PathBuf::from("REQUIRED_DEPS.toml"), fragment);
1158        }
1159        artifacts
1160    }
1161
1162    /// Write a generation result using the same rendered artifact set exposed
1163    /// to dry-run and check-mode callers.
1164    pub fn write_files(&self, result: &GenerationResult) -> Result<()> {
1165        use std::fs;
1166
1167        // Create output directory if it doesn't exist
1168        fs::create_dir_all(&self.config.output_dir)?;
1169
1170        let artifacts = self.output_artifacts(result);
1171        for (relative, content) in &artifacts {
1172            let file_path = self.config.output_dir.join(relative);
1173            if let Some(parent) = file_path.parent() {
1174                fs::create_dir_all(parent)?;
1175            }
1176            fs::write(&file_path, content)?;
1177        }
1178
1179        let deps_path = self.config.output_dir.join("REQUIRED_DEPS.toml");
1180        if !artifacts.contains_key(std::path::Path::new("REQUIRED_DEPS.toml")) && deps_path.exists()
1181        {
1182            fs::remove_file(&deps_path)?;
1183        }
1184
1185        Ok(())
1186    }
1187
1188    fn generate_type_definition(
1189        &self,
1190        schema: &crate::analysis::AnalyzedSchema,
1191        analysis: &crate::analysis::SchemaAnalysis,
1192        type_context: &TypeGenerationContext<'_>,
1193    ) -> Result<TokenStream> {
1194        use crate::analysis::SchemaType;
1195
1196        match &schema.schema_type {
1197            SchemaType::Primitive { rust_type, .. } => {
1198                // Generate type alias for primitives that are referenced by other schemas
1199                self.generate_type_alias(schema, rust_type)
1200            }
1201            SchemaType::StringEnum { values } => {
1202                let ext = analysis.enum_extensions.get(&schema.name);
1203                // [extensible_enums] override: opt a closed string-enum into an
1204                // extensible enum when the spec is known to lag the API (e.g.
1205                // Cloudflare R2 returning "WNAM" against a lowercase-only enum).
1206                // Accept either the raw spec name (e.g. "r2_bucket_location")
1207                // or the rendered Rust type name (e.g. "R2BucketLocation") so
1208                // users can write whichever they see in the generated code.
1209                let rust_name = self.to_rust_type_name(&schema.name);
1210                let force_extensible = self
1211                    .config
1212                    .extensible_enum_overrides
1213                    .get(&schema.name)
1214                    .or_else(|| self.config.extensible_enum_overrides.get(&rust_name))
1215                    .copied()
1216                    .unwrap_or(false);
1217                if force_extensible {
1218                    self.generate_extensible_enum(schema, values, ext)
1219                } else {
1220                    self.generate_string_enum(schema, values, ext)
1221                }
1222            }
1223            SchemaType::ExtensibleEnum { known_values } => {
1224                let ext = analysis.enum_extensions.get(&schema.name);
1225                self.generate_extensible_enum(schema, known_values, ext)
1226            }
1227            SchemaType::Object {
1228                properties,
1229                required,
1230                additional_properties,
1231            } => self.generate_struct(
1232                schema,
1233                properties,
1234                required,
1235                additional_properties,
1236                analysis,
1237                type_context,
1238            ),
1239            SchemaType::DiscriminatedUnion {
1240                discriminator_field,
1241                variants,
1242            } => {
1243                // Check if this discriminated union should be untagged due to being nested
1244                if self.should_use_untagged_discriminated_union(schema, analysis) {
1245                    // Convert variants to SchemaRef format for union enum generation
1246                    let schema_refs: Vec<crate::analysis::SchemaRef> = variants
1247                        .iter()
1248                        .map(|v| crate::analysis::SchemaRef {
1249                            target: v.type_name.clone(),
1250                            nullable: false,
1251                        })
1252                        .collect();
1253                    self.generate_union_enum(schema, &schema_refs, analysis)
1254                } else {
1255                    self.generate_discriminated_enum(
1256                        schema,
1257                        discriminator_field,
1258                        variants,
1259                        analysis,
1260                    )
1261                }
1262            }
1263            SchemaType::Union { variants } => self.generate_union_enum(schema, variants, analysis),
1264            SchemaType::Reference { target } => {
1265                // For references, check if we need to generate a type alias
1266                // This handles cases like nullable patterns
1267                if schema.name != *target {
1268                    // Generate a type alias
1269                    let alias_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1270                    let target_type = format_ident!("{}", self.to_rust_type_name(target));
1271
1272                    let doc_comment = if let Some(desc) = &schema.description {
1273                        quote! { #[doc = #desc] }
1274                    } else {
1275                        TokenStream::new()
1276                    };
1277
1278                    Ok(quote! {
1279                        #doc_comment
1280                        pub type #alias_name = #target_type;
1281                    })
1282                } else {
1283                    // Same name as target, no need for alias
1284                    Ok(TokenStream::new())
1285                }
1286            }
1287            SchemaType::Array { item_type } => {
1288                // Generate type alias for named array schemas.
1289                //
1290                // Special case: if the array item is a struct whose discriminator
1291                // field was stripped (because it's used in a tagged enum), the bare
1292                // struct won't serialize the discriminator in standalone contexts.
1293                // Generate a single-variant tagged wrapper enum so the discriminator
1294                // field is re-added by serde's tag attribute.
1295                let array_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1296
1297                // Check if the item type is a Reference to a discriminator-stripped struct
1298                if let SchemaType::Reference { target } = item_type.as_ref() {
1299                    if let Some(info) = type_context.discriminated_variants.get(target) {
1300                        if !info.is_parent_untagged {
1301                            // Generate a wrapper enum that re-adds the discriminator tag
1302                            let wrapper_name =
1303                                format_ident!("{}Item", self.to_rust_type_name(&schema.name));
1304                            let variant_type = format_ident!("{}", self.to_rust_type_name(target));
1305                            let disc_field = &info.discriminator_field;
1306                            let disc_value = &info.discriminator_value;
1307
1308                            let doc_comment = if let Some(desc) = &schema.description {
1309                                quote! { #[doc = #desc] }
1310                            } else {
1311                                TokenStream::new()
1312                            };
1313
1314                            return Ok(quote! {
1315                                /// Wrapper enum that re-adds the discriminator tag
1316                                /// for array contexts where the inner struct had its
1317                                /// discriminator field stripped for tagged enum use.
1318                                #[derive(Debug, Clone, Deserialize, Serialize)]
1319                                #[serde(tag = #disc_field)]
1320                                pub enum #wrapper_name {
1321                                    #[serde(rename = #disc_value)]
1322                                    #variant_type(#variant_type),
1323                                }
1324                                #doc_comment
1325                                pub type #array_name = Vec<#wrapper_name>;
1326                            });
1327                        }
1328                    }
1329                }
1330
1331                let inner_type = self.generate_array_item_type(item_type, analysis);
1332
1333                let doc_comment = if let Some(desc) = &schema.description {
1334                    quote! { #[doc = #desc] }
1335                } else {
1336                    TokenStream::new()
1337                };
1338
1339                Ok(quote! {
1340                    #doc_comment
1341                    pub type #array_name = Vec<#inner_type>;
1342                })
1343            }
1344            SchemaType::Composition { schemas } => {
1345                self.generate_composition_struct(schema, schemas)
1346            }
1347        }
1348    }
1349
1350    fn generate_type_alias(
1351        &self,
1352        schema: &crate::analysis::AnalyzedSchema,
1353        rust_type: &str,
1354    ) -> Result<TokenStream> {
1355        let type_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1356        // syn parses any valid Rust type expression including
1357        // generics (`chrono::DateTime<chrono::Utc>`, `Vec<u8>`).
1358        // The pre-Q2 ad-hoc `::`-splitter choked on `<`.
1359        let base_type = parse_rust_type(rust_type)?;
1360
1361        let doc_comment = if let Some(desc) = &schema.description {
1362            let sanitized_desc = self.sanitize_doc_comment(desc);
1363            quote! { #[doc = #sanitized_desc] }
1364        } else {
1365            TokenStream::new()
1366        };
1367
1368        Ok(quote! {
1369            #doc_comment
1370            pub type #type_name = #base_type;
1371        })
1372    }
1373
1374    fn generate_extensible_enum(
1375        &self,
1376        schema: &crate::analysis::AnalyzedSchema,
1377        known_values: &[String],
1378        ext: Option<&crate::analysis::EnumExtensions>,
1379    ) -> Result<TokenStream> {
1380        let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1381
1382        let doc_comment = if let Some(desc) = &schema.description {
1383            quote! { #[doc = #desc] }
1384        } else {
1385            TokenStream::new()
1386        };
1387
1388        // Q2.6: pre-resolve variant idents from x-enum-varnames when
1389        // available + length-matched + toggle on. Same fallback rule
1390        // as generate_string_enum.
1391        let varnames_override: Option<&Vec<String>> = ext
1392            .filter(|_| self.config.types.x_enum_varnames_enabled())
1393            .map(|e| &e.varnames)
1394            .filter(|v| !v.is_empty() && v.len() == known_values.len());
1395        let descriptions_override: Option<&Vec<String>> = ext
1396            .filter(|_| self.config.types.x_enum_descriptions_enabled())
1397            .map(|e| &e.descriptions)
1398            .filter(|v| !v.is_empty() && v.len() == known_values.len());
1399
1400        let variant_ident_for = |index: usize, value: &str| -> proc_macro2::Ident {
1401            let name = match varnames_override {
1402                Some(v) => v[index].clone(),
1403                None => self.to_rust_enum_variant(value),
1404            };
1405            format_ident!("{}", name)
1406        };
1407
1408        // For extensible enums, we need a different approach:
1409        // 1. Create a regular enum with known variants + Custom
1410        // 2. Implement custom serialization/deserialization
1411
1412        let known_variants = known_values.iter().enumerate().map(|(i, value)| {
1413            let variant_ident = variant_ident_for(i, value);
1414            let doc = descriptions_override
1415                .map(|d| {
1416                    let s = self.sanitize_doc_comment(&d[i]);
1417                    quote! { #[doc = #s] }
1418                })
1419                .unwrap_or_default();
1420            quote! {
1421                #doc
1422                #variant_ident,
1423            }
1424        });
1425
1426        let match_arms_de = known_values.iter().enumerate().map(|(i, value)| {
1427            let variant_ident = variant_ident_for(i, value);
1428            quote! {
1429                #value => Ok(#enum_name::#variant_ident),
1430            }
1431        });
1432
1433        let match_arms_ser = known_values.iter().enumerate().map(|(i, value)| {
1434            let variant_ident = variant_ident_for(i, value);
1435            quote! {
1436                #enum_name::#variant_ident => #value,
1437            }
1438        });
1439
1440        let derives = if self.config.enable_specta {
1441            quote! {
1442                #[derive(Debug, Clone, PartialEq, Eq)]
1443                #[cfg_attr(feature = "specta", derive(specta::Type))]
1444            }
1445        } else {
1446            quote! {
1447                #[derive(Debug, Clone, PartialEq, Eq)]
1448            }
1449        };
1450
1451        Ok(quote! {
1452            #doc_comment
1453            #derives
1454            pub enum #enum_name {
1455                #(#known_variants)*
1456                /// Custom or unknown model identifier
1457                Custom(String),
1458            }
1459
1460            impl<'de> serde::Deserialize<'de> for #enum_name {
1461                fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1462                where
1463                    D: serde::Deserializer<'de>,
1464                {
1465                    let value = String::deserialize(deserializer)?;
1466                    match value.as_str() {
1467                        #(#match_arms_de)*
1468                        _ => Ok(#enum_name::Custom(value)),
1469                    }
1470                }
1471            }
1472
1473            impl serde::Serialize for #enum_name {
1474                fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1475                where
1476                    S: serde::Serializer,
1477                {
1478                    let value = match self {
1479                        #(#match_arms_ser)*
1480                        #enum_name::Custom(s) => s.as_str(),
1481                    };
1482                    serializer.serialize_str(value)
1483                }
1484            }
1485        })
1486    }
1487
1488    fn generate_string_enum(
1489        &self,
1490        schema: &crate::analysis::AnalyzedSchema,
1491        values: &[String],
1492        ext: Option<&crate::analysis::EnumExtensions>,
1493    ) -> Result<TokenStream> {
1494        let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1495
1496        // Determine which variant should be the default. The spec's `default`
1497        // may not exactly match any enum value (telnyx has
1498        // `default: "en"` on a language enum that lists `en-US`, `en-AU`,
1499        // … — no exact match). When that happens, drop the `Default` derive
1500        // entirely instead of emitting it on an enum where no variant has
1501        // `#[default]` (E0665).
1502        let default_value = schema
1503            .default
1504            .as_ref()
1505            .and_then(|v| v.as_str())
1506            .map(|s| s.to_string());
1507        let has_default_match = match &default_value {
1508            Some(d) => values.iter().any(|v| v == d),
1509            None => !values.is_empty(),
1510        };
1511
1512        // Q2.6: x-enum-varnames overrides the default heuristic when
1513        // present, length-matched, and the toggle is on. Falls back
1514        // to the to_rust_enum_variant heuristic otherwise.
1515        let varnames_override: Option<&Vec<String>> = ext
1516            .filter(|_| self.config.types.x_enum_varnames_enabled())
1517            .map(|e| &e.varnames)
1518            .filter(|v| !v.is_empty() && v.len() == values.len());
1519        let descriptions_override: Option<&Vec<String>> = ext
1520            .filter(|_| self.config.types.x_enum_descriptions_enabled())
1521            .map(|e| &e.descriptions)
1522            .filter(|v| !v.is_empty() && v.len() == values.len());
1523
1524        // Variant-name uniqueness: enum values that PascalCase to the same
1525        // identifier (e.g. `ASC`/`asc` both → `Asc`) collide and produce
1526        // E0428 + non-exhaustive matches downstream. Dedupe by suffixing
1527        // `_2`, `_3`, … on collisions while preserving the first occurrence's
1528        // name, and keeping each variant's `#[serde(rename)]` pointed at the
1529        // original wire string.
1530        let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
1531        let variant_pairs: Vec<(syn::Ident, &String, bool, Option<String>)> = values
1532            .iter()
1533            .enumerate()
1534            .map(|(i, value)| {
1535                let base = match varnames_override {
1536                    Some(v) => v[i].clone(),
1537                    None => self.to_rust_enum_variant(value),
1538                };
1539                let mut variant_name = base.clone();
1540                let mut suffix = 2;
1541                while !used.insert(variant_name.clone()) {
1542                    variant_name = format!("{base}_{suffix}");
1543                    suffix += 1;
1544                }
1545                let variant_ident = format_ident!("{}", variant_name);
1546                let is_default = if let Some(ref default) = default_value {
1547                    value == default
1548                } else {
1549                    i == 0
1550                };
1551                let description = descriptions_override.map(|d| d[i].clone());
1552                (variant_ident, value, is_default, description)
1553            })
1554            .collect();
1555
1556        let variants =
1557            variant_pairs
1558                .iter()
1559                .map(|(variant_ident, value, is_default, description)| {
1560                    let doc = description
1561                        .as_ref()
1562                        .map(|d| {
1563                            let s = self.sanitize_doc_comment(d);
1564                            quote! { #[doc = #s] }
1565                        })
1566                        .unwrap_or_default();
1567                    if *is_default {
1568                        quote! {
1569                            #doc
1570                            #[default]
1571                            #[serde(rename = #value)]
1572                            #variant_ident,
1573                        }
1574                    } else {
1575                        quote! {
1576                            #doc
1577                            #[serde(rename = #value)]
1578                            #variant_ident,
1579                        }
1580                    }
1581                });
1582
1583        // T13/T10: emit `as_str` and `Display` so the enum can be embedded in
1584        // query strings, headers, and path segments without requiring callers
1585        // to reach for `serde_json` round-trips.
1586        let as_str_arms = variant_pairs.iter().map(|(variant_ident, value, _, _)| {
1587            quote! { Self::#variant_ident => #value, }
1588        });
1589
1590        let doc_comment = if let Some(desc) = &schema.description {
1591            quote! { #[doc = #desc] }
1592        } else {
1593            TokenStream::new()
1594        };
1595
1596        // Generate derives with optional Specta support. Drop `Default` if
1597        // no variant ends up tagged `#[default]` (would trigger E0665).
1598        let derives = match (self.config.enable_specta, has_default_match) {
1599            (true, true) => quote! {
1600                #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
1601                #[cfg_attr(feature = "specta", derive(specta::Type))]
1602            },
1603            (true, false) => quote! {
1604                #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1605                #[cfg_attr(feature = "specta", derive(specta::Type))]
1606            },
1607            (false, true) => quote! {
1608                #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
1609            },
1610            (false, false) => quote! {
1611                #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1612            },
1613        };
1614
1615        Ok(quote! {
1616            #doc_comment
1617            #derives
1618            pub enum #enum_name {
1619                #(#variants)*
1620            }
1621
1622            impl #enum_name {
1623                pub fn as_str(&self) -> &'static str {
1624                    match self {
1625                        #(#as_str_arms)*
1626                    }
1627                }
1628            }
1629
1630            impl ::std::fmt::Display for #enum_name {
1631                fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
1632                    f.write_str(self.as_str())
1633                }
1634            }
1635
1636            impl AsRef<str> for #enum_name {
1637                fn as_ref(&self) -> &str {
1638                    self.as_str()
1639                }
1640            }
1641        })
1642    }
1643
1644    fn generate_struct(
1645        &self,
1646        schema: &crate::analysis::AnalyzedSchema,
1647        properties: &BTreeMap<String, crate::analysis::PropertyInfo>,
1648        required: &std::collections::HashSet<String>,
1649        additional_properties: &crate::analysis::ObjectAdditionalProperties,
1650        analysis: &crate::analysis::SchemaAnalysis,
1651        type_context: &TypeGenerationContext<'_>,
1652    ) -> Result<TokenStream> {
1653        let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1654        let emitted_properties = self.emitted_object_properties(
1655            &schema.name,
1656            properties,
1657            required,
1658            additional_properties,
1659            analysis,
1660            type_context.discriminated_variants.get(&schema.name),
1661        );
1662
1663        let mut fields: Vec<TokenStream> = emitted_properties
1664            .iter()
1665            .map(|emitted| {
1666                let field_name = emitted.wire_name;
1667                let property = emitted.property;
1668                let field_ident = &emitted.ident;
1669                let field_type = &emitted.field_type;
1670                let serde_attrs = self.generate_serde_field_attrs(
1671                    &schema.name,
1672                    field_name,
1673                    field_ident,
1674                    property,
1675                    emitted.is_required,
1676                    analysis,
1677                );
1678                let specta_attrs = self.generate_specta_field_attrs(field_name);
1679
1680                let doc_comment = if let Some(desc) = &property.description {
1681                    let sanitized_desc = self.sanitize_doc_comment(desc);
1682                    quote! { #[doc = #sanitized_desc] }
1683                } else {
1684                    TokenStream::new()
1685                };
1686                let constraint_doc = self.generate_constraint_doc(&property.constraints);
1687
1688                quote! {
1689                    #doc_comment
1690                    #constraint_doc
1691                    #serde_attrs
1692                    #specta_attrs
1693                    pub #field_ident: #field_type,
1694                }
1695            })
1696            .collect();
1697
1698        // Q2.3: emit the catch-all additional-properties field with
1699        // the right value type. `Untyped` keeps pre-Q2.3 behavior
1700        // (BTreeMap<String, serde_json::Value>); `Typed { value_type }`
1701        // surfaces the actual schema-declared type, e.g.
1702        // BTreeMap<String, MyValue>. `Forbidden` emits no field.
1703        match additional_properties {
1704            crate::analysis::ObjectAdditionalProperties::Forbidden => {}
1705            crate::analysis::ObjectAdditionalProperties::Untyped => {
1706                fields.push(quote! {
1707                    /// Additional properties not explicitly defined in the schema
1708                    #[serde(flatten)]
1709                    pub additional_properties:
1710                        std::collections::BTreeMap<String, serde_json::Value>,
1711                });
1712            }
1713            crate::analysis::ObjectAdditionalProperties::Typed { value_type } => {
1714                let value_tokens = self.generate_array_item_type(value_type, analysis);
1715                fields.push(quote! {
1716                    /// Additional properties matching the spec's
1717                    /// `additionalProperties` value schema.
1718                    #[serde(flatten)]
1719                    pub additional_properties:
1720                        std::collections::BTreeMap<String, #value_tokens>,
1721                });
1722            }
1723        }
1724
1725        let doc_comment = if let Some(desc) = &schema.description {
1726            quote! { #[doc = #desc] }
1727        } else {
1728            TokenStream::new()
1729        };
1730
1731        // Default is safe only when no emitted wire property is required.
1732        // Optional fields are represented as Option<T>, and the generated
1733        // additional-properties map (when present) is empty by default. We do
1734        // not invent values for required data, even when the Rust type itself
1735        // happens to implement Default.
1736        let can_derive_default = emitted_properties
1737            .iter()
1738            .all(|property| !property.is_required);
1739
1740        // Generate derives with optional Specta support
1741        // Note: We use snake_case everywhere (matching the OpenAPI spec) for consistency
1742        // between Rust, JSON API, and TypeScript
1743        let derives = match (self.config.enable_specta, can_derive_default) {
1744            (true, true) => quote! {
1745                #[derive(Debug, Clone, Deserialize, Serialize, Default)]
1746                #[cfg_attr(feature = "specta", derive(specta::Type))]
1747            },
1748            (true, false) => quote! {
1749                #[derive(Debug, Clone, Deserialize, Serialize)]
1750                #[cfg_attr(feature = "specta", derive(specta::Type))]
1751            },
1752            (false, true) => quote! {
1753                #[derive(Debug, Clone, Deserialize, Serialize, Default)]
1754            },
1755            (false, false) => quote! {
1756                #[derive(Debug, Clone, Deserialize, Serialize)]
1757            },
1758        };
1759
1760        let builder = if type_context.index.request_body_roots.contains(&schema.name)
1761            && emitted_properties
1762                .iter()
1763                .any(|property| property.is_required)
1764            && (emitted_properties
1765                .iter()
1766                .any(|property| !property.is_required)
1767                || !matches!(
1768                    additional_properties,
1769                    crate::analysis::ObjectAdditionalProperties::Forbidden
1770                )) {
1771            self.generate_request_model_builder(
1772                schema,
1773                &emitted_properties,
1774                additional_properties,
1775                analysis,
1776                type_context.index,
1777            )
1778        } else {
1779            TokenStream::new()
1780        };
1781
1782        Ok(quote! {
1783            #doc_comment
1784            #derives
1785            pub struct #struct_name {
1786                #(#fields)*
1787            }
1788
1789            #builder
1790        })
1791    }
1792
1793    /// Project an object schema into the exact public fields emitted in
1794    /// `types.rs`. Request-model and operation builders share this metadata so
1795    /// identifier disambiguation, discriminator filtering, and Option wrapping
1796    /// cannot drift.
1797    pub(crate) fn emitted_object_properties<'a>(
1798        &self,
1799        schema_name: &str,
1800        properties: &'a BTreeMap<String, crate::analysis::PropertyInfo>,
1801        required: &std::collections::HashSet<String>,
1802        additional_properties: &crate::analysis::ObjectAdditionalProperties,
1803        analysis: &crate::analysis::SchemaAnalysis,
1804        discriminator_info: Option<&DiscriminatedVariantInfo>,
1805    ) -> Vec<EmittedObjectProperty<'a>> {
1806        let mut sorted_properties: Vec<_> = properties.iter().collect();
1807        sorted_properties.sort_by_key(|(name, _)| name.as_str());
1808
1809        let mut used_field_idents = std::collections::HashSet::new();
1810        if !matches!(
1811            additional_properties,
1812            crate::analysis::ObjectAdditionalProperties::Forbidden
1813        ) {
1814            used_field_idents.insert("additional_properties".to_string());
1815        }
1816
1817        let mut emitted = Vec::new();
1818        for (field_name, property) in sorted_properties {
1819            if discriminator_info.is_some_and(|info| {
1820                !info.is_parent_untagged && field_name.as_str() == info.discriminator_field.as_str()
1821            }) {
1822                continue;
1823            }
1824
1825            let raw = self.to_rust_field_name(field_name);
1826            let mut chosen = raw.clone();
1827            let mut suffix = 2;
1828            while !used_field_idents.insert(chosen.clone()) {
1829                chosen = format!("{raw}_{suffix}");
1830                suffix += 1;
1831            }
1832            let is_required = required.contains(field_name);
1833            emitted.push(EmittedObjectProperty {
1834                wire_name: field_name,
1835                property,
1836                ident: Self::to_field_ident(&chosen),
1837                is_required,
1838                field_type: self.generate_field_type(
1839                    schema_name,
1840                    field_name,
1841                    property,
1842                    is_required,
1843                    analysis,
1844                ),
1845            });
1846        }
1847        emitted
1848    }
1849
1850    fn type_generation_index(
1851        &self,
1852        analysis: &crate::analysis::SchemaAnalysis,
1853    ) -> TypeGenerationIndex {
1854        let reserved_type_names = analysis
1855            .schemas
1856            .keys()
1857            .map(|name| self.to_rust_type_name(name))
1858            .collect();
1859        let mut request_body_roots = std::collections::HashSet::new();
1860        for operation in analysis.operations.values() {
1861            let Some(mut current) = operation
1862                .request_body
1863                .as_ref()
1864                .and_then(crate::analysis::RequestBodyContent::schema_name)
1865            else {
1866                continue;
1867            };
1868            while request_body_roots.insert(current.to_string()) {
1869                let Some(crate::analysis::AnalyzedSchema {
1870                    schema_type: crate::analysis::SchemaType::Reference { target },
1871                    ..
1872                }) = analysis.schemas.get(current)
1873                else {
1874                    break;
1875                };
1876                current = target;
1877            }
1878        }
1879        TypeGenerationIndex {
1880            request_body_roots,
1881            reserved_type_names,
1882        }
1883    }
1884
1885    fn generate_request_model_builder(
1886        &self,
1887        schema: &crate::analysis::AnalyzedSchema,
1888        properties: &[EmittedObjectProperty<'_>],
1889        additional_properties: &crate::analysis::ObjectAdditionalProperties,
1890        analysis: &crate::analysis::SchemaAnalysis,
1891        type_index: &TypeGenerationIndex,
1892    ) -> TokenStream {
1893        let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1894        let builder_base = format!("{}Builder", struct_name);
1895        let mut builder_name = builder_base.clone();
1896        let mut suffix = 2;
1897        while type_index.reserved_type_names.contains(&builder_name) {
1898            builder_name = format!("{builder_base}{suffix}");
1899            suffix += 1;
1900        }
1901        let builder_name = format_ident!("{builder_name}");
1902
1903        let required_parameters: Vec<TokenStream> = properties
1904            .iter()
1905            .filter(|property| property.is_required)
1906            .map(|property| {
1907                let ident = &property.ident;
1908                let field_type = &property.field_type;
1909                quote! { #ident: #field_type }
1910            })
1911            .collect();
1912        let required_idents: Vec<&syn::Ident> = properties
1913            .iter()
1914            .filter(|property| property.is_required)
1915            .map(|property| &property.ident)
1916            .collect();
1917        let optional_initializers: Vec<TokenStream> = properties
1918            .iter()
1919            .filter(|property| !property.is_required)
1920            .map(|property| {
1921                let ident = &property.ident;
1922                quote! { #ident: None }
1923            })
1924            .collect();
1925
1926        let additional_initializer = match additional_properties {
1927            crate::analysis::ObjectAdditionalProperties::Forbidden => TokenStream::new(),
1928            crate::analysis::ObjectAdditionalProperties::Untyped
1929            | crate::analysis::ObjectAdditionalProperties::Typed { .. } => quote! {
1930                additional_properties: ::std::collections::BTreeMap::new(),
1931            },
1932        };
1933
1934        let mut used_builder_methods =
1935            std::collections::HashSet::from(["new".to_string(), "build".to_string()]);
1936        if !matches!(
1937            additional_properties,
1938            crate::analysis::ObjectAdditionalProperties::Forbidden
1939        ) {
1940            used_builder_methods.insert("additional_properties".to_string());
1941        }
1942        let optional_setters: Vec<TokenStream> = properties
1943            .iter()
1944            .filter(|property| !property.is_required)
1945            .map(|property| {
1946                let field_ident = &property.ident;
1947                let field_type = self.generate_property_base_type(
1948                    &schema.name,
1949                    property.wire_name,
1950                    property.property,
1951                    analysis,
1952                );
1953                // Allocate every setter in the builder's method namespace.
1954                // `new` and `build` keep their documented `with_` escape;
1955                // further collisions receive deterministic numeric suffixes.
1956                let field_name = field_ident.to_string();
1957                let plain_field_name = field_name.strip_prefix("r#").unwrap_or(&field_name);
1958                let mut setter_name = if matches!(plain_field_name, "new" | "build") {
1959                    format!("with_{plain_field_name}")
1960                } else {
1961                    field_name.clone()
1962                };
1963                let setter_base = setter_name.clone();
1964                let mut suffix = 2;
1965                while !used_builder_methods.insert(setter_name.clone()) {
1966                    setter_name = format!("{setter_base}_{suffix}");
1967                    suffix += 1;
1968                }
1969                let setter_ident = Self::to_field_ident(&setter_name);
1970                let wire_name = property.wire_name;
1971                quote! {
1972                    #[doc = concat!("Set the optional `", #wire_name, "` request field.")]
1973                    #[must_use]
1974                    pub fn #setter_ident(mut self, #field_ident: #field_type) -> Self {
1975                        self.value.#field_ident = Some(#field_ident);
1976                        self
1977                    }
1978                }
1979            })
1980            .collect();
1981
1982        let additional_setter = match additional_properties {
1983            crate::analysis::ObjectAdditionalProperties::Forbidden => TokenStream::new(),
1984            crate::analysis::ObjectAdditionalProperties::Untyped => quote! {
1985                /// Replace the request's additional properties.
1986                #[must_use]
1987                pub fn additional_properties(
1988                    mut self,
1989                    additional_properties: ::std::collections::BTreeMap<
1990                        String,
1991                        serde_json::Value,
1992                    >,
1993                ) -> Self {
1994                    self.value.additional_properties = additional_properties;
1995                    self
1996                }
1997            },
1998            crate::analysis::ObjectAdditionalProperties::Typed { value_type } => {
1999                let value_type = self.generate_array_item_type(value_type, analysis);
2000                quote! {
2001                    /// Replace the request's additional properties.
2002                    #[must_use]
2003                    pub fn additional_properties(
2004                        mut self,
2005                        additional_properties: ::std::collections::BTreeMap<
2006                            String,
2007                            #value_type,
2008                        >,
2009                    ) -> Self {
2010                        self.value.additional_properties = additional_properties;
2011                        self
2012                    }
2013                }
2014            }
2015        };
2016
2017        quote! {
2018            impl #struct_name {
2019                /// Construct this request with every required wire field.
2020                pub fn new(#(#required_parameters),*) -> Self {
2021                    Self {
2022                        #(#required_idents,)*
2023                        #(#optional_initializers,)*
2024                        #additional_initializer
2025                    }
2026                }
2027
2028                /// Start a dependency-free builder with every required wire field.
2029                pub fn builder(#(#required_parameters),*) -> #builder_name {
2030                    #builder_name::new(#(#required_idents),*)
2031                }
2032            }
2033
2034            /// Dependency-free builder for [`#struct_name`].
2035            #[derive(Debug, Clone)]
2036            #[must_use]
2037            pub struct #builder_name {
2038                value: #struct_name,
2039            }
2040
2041            impl #builder_name {
2042                /// Start a builder with every required wire field.
2043                pub fn new(#(#required_parameters),*) -> Self {
2044                    Self {
2045                        value: #struct_name::new(#(#required_idents),*),
2046                    }
2047                }
2048
2049                #(#optional_setters)*
2050                #additional_setter
2051
2052                /// Finish building the request model.
2053                pub fn build(self) -> #struct_name {
2054                    self.value
2055                }
2056            }
2057        }
2058    }
2059
2060    fn generate_discriminated_enum(
2061        &self,
2062        schema: &crate::analysis::AnalyzedSchema,
2063        discriminator_field: &str,
2064        variants: &[crate::analysis::UnionVariant],
2065        analysis: &crate::analysis::SchemaAnalysis,
2066    ) -> Result<TokenStream> {
2067        let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2068
2069        // Check if any variant references another discriminated union
2070        let has_nested_discriminated_union = variants.iter().any(|variant| {
2071            if let Some(variant_schema) = analysis.schemas.get(&variant.type_name) {
2072                matches!(
2073                    variant_schema.schema_type,
2074                    crate::analysis::SchemaType::DiscriminatedUnion { .. }
2075                )
2076            } else {
2077                false
2078            }
2079        });
2080
2081        // If we have a nested discriminated union, make this enum untagged
2082        if has_nested_discriminated_union {
2083            // Generate as untagged union
2084            let schema_refs: Vec<crate::analysis::SchemaRef> = variants
2085                .iter()
2086                .map(|v| crate::analysis::SchemaRef {
2087                    target: v.type_name.clone(),
2088                    nullable: false,
2089                })
2090                .collect();
2091            return self.generate_union_enum(schema, &schema_refs, analysis);
2092        }
2093
2094        let enclosing = self.to_rust_type_name(&schema.name);
2095        let enum_variants = variants.iter().map(|variant| {
2096            let variant_name = format_ident!("{}", variant.rust_name);
2097            let variant_value = &variant.discriminator_value;
2098
2099            let variant_type = format_ident!("{}", self.to_rust_type_name(&variant.type_name));
2100            // Box variant payloads that point at the enclosing enum or any
2101            // schema in the analysis's recursive set, otherwise the enum has
2102            // infinite size (E0072).
2103            let payload = if self.to_rust_type_name(&variant.type_name) == enclosing
2104                || analysis
2105                    .dependencies
2106                    .recursive_schemas
2107                    .contains(&variant.type_name)
2108            {
2109                quote! { Box<#variant_type> }
2110            } else {
2111                quote! { #variant_type }
2112            };
2113            quote! {
2114                #[serde(rename = #variant_value)]
2115                #variant_name(#payload),
2116            }
2117        });
2118
2119        let doc_comment = if let Some(desc) = &schema.description {
2120            quote! { #[doc = #desc] }
2121        } else {
2122            TokenStream::new()
2123        };
2124
2125        // Generate derives with optional Specta support
2126        let derives = if self.config.enable_specta {
2127            quote! {
2128                #[derive(Debug, Clone, Deserialize, Serialize)]
2129                #[cfg_attr(feature = "specta", derive(specta::Type))]
2130                #[serde(tag = #discriminator_field)]
2131            }
2132        } else {
2133            quote! {
2134                #[derive(Debug, Clone, Deserialize, Serialize)]
2135                #[serde(tag = #discriminator_field)]
2136            }
2137        };
2138
2139        Ok(quote! {
2140            #doc_comment
2141            #derives
2142            pub enum #enum_name {
2143                #(#enum_variants)*
2144            }
2145        })
2146    }
2147
2148    /// Check if a discriminated union should be generated as untagged due to being nested
2149    fn should_use_untagged_discriminated_union(
2150        &self,
2151        schema: &crate::analysis::AnalyzedSchema,
2152        analysis: &crate::analysis::SchemaAnalysis,
2153    ) -> bool {
2154        // Only make discriminated unions untagged if they are nested AND their variants
2155        // don't need the discriminator field for API compatibility
2156
2157        // Check if this schema is used as a variant in another discriminated union
2158        for other_schema in analysis.schemas.values() {
2159            if let crate::analysis::SchemaType::DiscriminatedUnion {
2160                variants,
2161                discriminator_field: _,
2162            } = &other_schema.schema_type
2163            {
2164                for variant in variants {
2165                    if variant.type_name == schema.name {
2166                        // This discriminated union is nested inside another discriminated union
2167
2168                        // Check if the current schema's variants have the discriminator field in their properties
2169                        // If they do, we need to keep this union tagged to preserve the discriminator
2170                        if let crate::analysis::SchemaType::DiscriminatedUnion {
2171                            discriminator_field: current_discriminator,
2172                            variants: current_variants,
2173                            ..
2174                        } = &schema.schema_type
2175                        {
2176                            // Check if any variant schemas have the discriminator field as a property
2177                            for current_variant in current_variants {
2178                                if let Some(variant_schema) =
2179                                    analysis.schemas.get(&current_variant.type_name)
2180                                {
2181                                    if let crate::analysis::SchemaType::Object {
2182                                        properties, ..
2183                                    } = &variant_schema.schema_type
2184                                    {
2185                                        if properties.contains_key(current_discriminator) {
2186                                            // This variant has the discriminator field as a property,
2187                                            // so we need to keep the union tagged to preserve it
2188                                            return false;
2189                                        }
2190                                    }
2191                                }
2192                            }
2193                        }
2194
2195                        // No variants have the discriminator as a property, safe to make untagged
2196                        return true;
2197                    }
2198                }
2199            }
2200        }
2201        false
2202    }
2203
2204    fn generate_union_enum(
2205        &self,
2206        schema: &crate::analysis::AnalyzedSchema,
2207        variants: &[crate::analysis::SchemaRef],
2208        analysis: &crate::analysis::SchemaAnalysis,
2209    ) -> Result<TokenStream> {
2210        let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2211
2212        // Generate meaningful variant names based on type names
2213        let mut used_variant_names = std::collections::HashSet::new();
2214        let enum_variants = variants.iter().enumerate().map(|(i, variant)| {
2215            // Generate a meaningful variant name from the type name
2216            let base_variant_name = self.type_name_to_variant_name(&variant.target);
2217            let variant_name = self.ensure_unique_variant_name_generator(
2218                base_variant_name,
2219                &mut used_variant_names,
2220                i,
2221            );
2222            let variant_name_ident = format_ident!("{}", variant_name);
2223
2224            // For primitive types and Vec types, use them directly without conversion
2225            let variant_type_tokens = if matches!(
2226                variant.target.as_str(),
2227                "bool"
2228                    | "i8"
2229                    | "i16"
2230                    | "i32"
2231                    | "i64"
2232                    | "i128"
2233                    | "u8"
2234                    | "u16"
2235                    | "u32"
2236                    | "u64"
2237                    | "u128"
2238                    | "f32"
2239                    | "f64"
2240                    | "String"
2241            ) {
2242                let type_ident = format_ident!("{}", variant.target);
2243                quote! { #type_ident }
2244            } else if variant.target == "serde_json::Value" {
2245                // The target is a fully-qualified path; emit it as a path so
2246                // it doesn't get mangled into a phantom `SerdeJsonValue` ident.
2247                quote! { serde_json::Value }
2248            } else if variant.target.starts_with("Vec<") && variant.target.ends_with(">") {
2249                // Handle Vec types by parsing the inner type
2250                let inner = &variant.target[4..variant.target.len() - 1];
2251
2252                // Handle nested Vec types (e.g., Vec<Vec<i64>>)
2253                if inner.starts_with("Vec<") && inner.ends_with(">") {
2254                    let inner_inner = &inner[4..inner.len() - 1];
2255                    if inner_inner == "serde_json::Value" {
2256                        quote! { Vec<Vec<serde_json::Value>> }
2257                    } else {
2258                        let inner_inner_type = if matches!(
2259                            inner_inner,
2260                            "bool"
2261                                | "i8"
2262                                | "i16"
2263                                | "i32"
2264                                | "i64"
2265                                | "i128"
2266                                | "u8"
2267                                | "u16"
2268                                | "u32"
2269                                | "u64"
2270                                | "u128"
2271                                | "f32"
2272                                | "f64"
2273                                | "String"
2274                        ) {
2275                            format_ident!("{}", inner_inner)
2276                        } else {
2277                            format_ident!("{}", self.to_rust_type_name(inner_inner))
2278                        };
2279                        quote! { Vec<Vec<#inner_inner_type>> }
2280                    }
2281                } else if inner == "serde_json::Value" {
2282                    quote! { Vec<serde_json::Value> }
2283                } else {
2284                    let inner_type = if matches!(
2285                        inner,
2286                        "bool"
2287                            | "i8"
2288                            | "i16"
2289                            | "i32"
2290                            | "i64"
2291                            | "i128"
2292                            | "u8"
2293                            | "u16"
2294                            | "u32"
2295                            | "u64"
2296                            | "u128"
2297                            | "f32"
2298                            | "f64"
2299                            | "String"
2300                    ) {
2301                        format_ident!("{}", inner)
2302                    } else {
2303                        format_ident!("{}", self.to_rust_type_name(inner))
2304                    };
2305                    quote! { Vec<#inner_type> }
2306                }
2307            } else if variant.target.contains("::") || variant.target.contains('<') {
2308                // Qualified Rust path or generic (chrono::DateTime<chrono::Utc>,
2309                // bytes::Bytes, std::net::Ipv4Addr) emitted by TypeMapper. Pass
2310                // it straight to syn — the to_rust_type_name PascalCase
2311                // pipeline below would mangle it into a non-existent ident.
2312                parse_rust_type(&variant.target).unwrap_or_else(|_| {
2313                    let fallback = format_ident!("{}", self.to_rust_type_name(&variant.target));
2314                    quote! { #fallback }
2315                })
2316            } else {
2317                let type_ident = format_ident!("{}", self.to_rust_type_name(&variant.target));
2318                quote! { #type_ident }
2319            };
2320
2321            // Self-referential variant (variant payload type == enclosing
2322            // enum) yields an infinite-size enum (E0072). Wrap in `Box<T>` to
2323            // break the cycle. Observed in microsoft-graph.yaml.
2324            let target_rust_name = self.to_rust_type_name(&variant.target);
2325            let enclosing_name = self.to_rust_type_name(&schema.name);
2326            let is_self_ref = target_rust_name == enclosing_name;
2327            // Indirect cycles (stripe BankAccount → BankAccountCustomer →
2328            // Customer → BankAccountCustomer): variants pointing into the
2329            // analysis's recursive_schemas set must also be heap-allocated.
2330            let is_recursive_target = analysis
2331                .dependencies
2332                .recursive_schemas
2333                .contains(&variant.target);
2334            let variant_type_tokens = if is_self_ref || is_recursive_target {
2335                quote! { Box<#variant_type_tokens> }
2336            } else {
2337                variant_type_tokens
2338            };
2339
2340            quote! {
2341                #variant_name_ident(#variant_type_tokens),
2342            }
2343        });
2344
2345        let doc_comment = if let Some(desc) = &schema.description {
2346            quote! { #[doc = #desc] }
2347        } else {
2348            TokenStream::new()
2349        };
2350
2351        // Generate derives with optional Specta support
2352        let derives = if self.config.enable_specta {
2353            quote! {
2354                #[derive(Debug, Clone, Deserialize, Serialize)]
2355                #[cfg_attr(feature = "specta", derive(specta::Type))]
2356                #[serde(untagged)]
2357            }
2358        } else {
2359            quote! {
2360                #[derive(Debug, Clone, Deserialize, Serialize)]
2361                #[serde(untagged)]
2362            }
2363        };
2364
2365        Ok(quote! {
2366            #doc_comment
2367            #derives
2368            pub enum #enum_name {
2369                #(#enum_variants)*
2370            }
2371        })
2372    }
2373
2374    /// Walk a chain of type-alias `Reference`s starting from `target` and
2375    /// return true if the chain reaches the schema named by
2376    /// `enclosing_rust_name` (Rust name). Bounded depth to prevent infinite
2377    /// loops on truly cyclic aliases.
2378    fn target_aliases_back_to(
2379        &self,
2380        target: &str,
2381        enclosing_rust_name: &str,
2382        analysis: &crate::analysis::SchemaAnalysis,
2383    ) -> bool {
2384        let mut current = target.to_string();
2385        let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
2386        for _ in 0..16 {
2387            if !visited.insert(current.clone()) {
2388                return true;
2389            }
2390            let Some(schema) = analysis.schemas.get(&current) else {
2391                return false;
2392            };
2393            if let crate::analysis::SchemaType::Reference { target: next } = &schema.schema_type {
2394                if self.to_rust_type_name(next) == enclosing_rust_name {
2395                    return true;
2396                }
2397                current = next.clone();
2398                continue;
2399            }
2400            return false;
2401        }
2402        false
2403    }
2404
2405    fn generate_field_type(
2406        &self,
2407        schema_name: &str,
2408        field_name: &str,
2409        prop: &crate::analysis::PropertyInfo,
2410        is_required: bool,
2411        analysis: &crate::analysis::SchemaAnalysis,
2412    ) -> TokenStream {
2413        let base_type = self.generate_property_base_type(schema_name, field_name, prop, analysis);
2414
2415        if self.property_is_option_wrapped(schema_name, field_name, prop, is_required, analysis) {
2416            quote! { Option<#base_type> }
2417        } else {
2418            base_type
2419        }
2420    }
2421
2422    fn property_is_option_wrapped(
2423        &self,
2424        schema_name: &str,
2425        field_name: &str,
2426        prop: &crate::analysis::PropertyInfo,
2427        is_required: bool,
2428        analysis: &crate::analysis::SchemaAnalysis,
2429    ) -> bool {
2430        let override_key = format!("{schema_name}.{field_name}");
2431        let is_nullable_override = self
2432            .config
2433            .nullable_field_overrides
2434            .get(&override_key)
2435            .copied()
2436            .unwrap_or(false);
2437
2438        !is_required
2439            || prop.nullable
2440            || is_nullable_override
2441            || (prop.default.is_some() && self.type_lacks_default(&prop.schema_type, analysis))
2442    }
2443
2444    pub(crate) fn generate_property_base_type(
2445        &self,
2446        schema_name: &str,
2447        _field_name: &str,
2448        prop: &crate::analysis::PropertyInfo,
2449        analysis: &crate::analysis::SchemaAnalysis,
2450    ) -> TokenStream {
2451        use crate::analysis::SchemaType;
2452
2453        match &prop.schema_type {
2454            SchemaType::Primitive { rust_type, .. } => {
2455                // syn handles generics + complex paths
2456                // (chrono::DateTime<chrono::Utc>, Vec<u8>, …).
2457                parse_rust_type(rust_type).unwrap_or_else(|_| {
2458                    // Pathological mapper output: fall back to bare
2459                    // String so the generated file at least
2460                    // compiles. Emit a stderr warning so the
2461                    // operator can investigate.
2462                    eprintln!(
2463                        "⚠️  TypeMapper produced un-parseable type `{rust_type}`; \
2464                         falling back to String"
2465                    );
2466                    quote! { String }
2467                })
2468            }
2469            SchemaType::Reference { target } => {
2470                let target_rust_name = self.to_rust_type_name(target);
2471                let target_type = format_ident!("{}", target_rust_name);
2472                // Wrap recursive references in Box<T> for heap allocation.
2473                // Three ways to detect the cycle:
2474                // 1. Target is in the analysis-level recursive set (catches
2475                //    direct + indirect cycles via the dependency graph).
2476                // 2. Target's Rust name equals the enclosing struct's Rust
2477                //    name (catches cloudflare-style cases where two distinct
2478                //    spec schemas PascalCase to the same ident).
2479                // 3. Target is a type alias whose resolution chain reaches
2480                //    the enclosing schema (catches cal-com's
2481                //    `ReassignBookingOutput20240813Data = Reassign...`
2482                //    pattern: the synthesized inline name aliases back to
2483                //    its parent).
2484                let enclosing_rust_name = self.to_rust_type_name(schema_name);
2485                let is_self_via_rust_name = target_rust_name == enclosing_rust_name;
2486                let is_alias_chain_self =
2487                    self.target_aliases_back_to(target, &enclosing_rust_name, analysis);
2488                if analysis.dependencies.recursive_schemas.contains(target)
2489                    || is_self_via_rust_name
2490                    || is_alias_chain_self
2491                {
2492                    quote! { Box<#target_type> }
2493                } else {
2494                    quote! { #target_type }
2495                }
2496            }
2497            SchemaType::Array { item_type } => {
2498                let inner_type = self.generate_array_item_type(item_type, analysis);
2499                quote! { Vec<#inner_type> }
2500            }
2501            _ => {
2502                // Fallback for complex types
2503                quote! { serde_json::Value }
2504            }
2505        }
2506    }
2507
2508    fn generate_serde_field_attrs(
2509        &self,
2510        schema_name: &str,
2511        field_name: &str,
2512        field_ident: &syn::Ident,
2513        prop: &crate::analysis::PropertyInfo,
2514        is_required: bool,
2515        analysis: &crate::analysis::SchemaAnalysis,
2516    ) -> TokenStream {
2517        let mut attrs = Vec::new();
2518
2519        // Generate rename attribute if field name differs from Rust identifier
2520        // Strip r# prefix for comparison since serde handles raw idents transparently
2521        let rust_field_name = field_ident.to_string();
2522        let comparison_name = rust_field_name
2523            .strip_prefix("r#")
2524            .unwrap_or(&rust_field_name);
2525        if comparison_name != field_name {
2526            attrs.push(quote! { rename = #field_name });
2527        }
2528
2529        // Add skip_serializing_if for optional fields to avoid sending null values
2530        if !is_required || prop.nullable {
2531            attrs.push(quote! { skip_serializing_if = "Option::is_none" });
2532        }
2533
2534        // Only add default attribute for required fields that have default values.
2535        // Skip #[serde(default)] for types that don't implement Default (discriminated
2536        // unions, union enums) — those fields should be Option<T> instead.
2537        if prop.default.is_some()
2538            && (is_required && !prop.nullable)
2539            && !self.type_lacks_default(&prop.schema_type, analysis)
2540        {
2541            attrs.push(quote! { default });
2542        }
2543
2544        // Codec hint from TypeMapper (Q2): `format: byte` →
2545        // `with = "base64_serde"`, etc. Fields whose mapped type
2546        // carries no codec (e.g. chrono::DateTime<Utc> uses its
2547        // built-in serde) skip this attribute. Option fields need
2548        // the `::option` submodule of the codec — serde dispatches
2549        // on field type, and the base codec works on Vec<u8> /
2550        // chrono::Duration / etc., not their Option wrappers.
2551        if let crate::analysis::SchemaType::Primitive {
2552            serde_with: Some(codec),
2553            ..
2554        } = &prop.schema_type
2555        {
2556            let is_option_wrapped = self.property_is_option_wrapped(
2557                schema_name,
2558                field_name,
2559                prop,
2560                is_required,
2561                analysis,
2562            );
2563            let codec_path = if is_option_wrapped {
2564                format!("{codec}::option")
2565            } else {
2566                codec.clone()
2567            };
2568            attrs.push(quote! { with = #codec_path });
2569            // A `with` codec disables serde's implicit
2570            // missing-field → None handling for Option fields
2571            // (serde-rs/serde#2878); without `default` a request
2572            // that simply omits the field fails to deserialize.
2573            if is_option_wrapped {
2574                attrs.push(quote! { default });
2575            }
2576        }
2577
2578        if attrs.is_empty() {
2579            TokenStream::new()
2580        } else {
2581            quote! { #[serde(#(#attrs),*)] }
2582        }
2583    }
2584
2585    /// Check if a schema type resolves to a type that doesn't implement `Default`.
2586    /// Discriminated unions and union enums don't derive Default, so fields with
2587    /// these types can't use `#[serde(default)]`.
2588    fn type_lacks_default(
2589        &self,
2590        schema_type: &crate::analysis::SchemaType,
2591        analysis: &crate::analysis::SchemaAnalysis,
2592    ) -> bool {
2593        use crate::analysis::SchemaType;
2594        match schema_type {
2595            SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => true,
2596            // Q2 typed scalars: chrono / url have no Default impl.
2597            // uuid::Uuid, bytes::Bytes, std::net::Ip*Addr all derive
2598            // Default, so they're safe to leave under #[serde(default)].
2599            SchemaType::Primitive { rust_type, .. } => matches!(
2600                rust_type.as_str(),
2601                "chrono::DateTime<chrono::Utc>"
2602                    | "chrono::NaiveDate"
2603                    | "chrono::NaiveTime"
2604                    | "chrono::Duration"
2605                    | "url::Url"
2606                    | "time::OffsetDateTime"
2607                    | "time::Date"
2608                    | "time::Time"
2609                    | "iso8601::Duration"
2610                    | "email_address::EmailAddress"
2611            ),
2612            SchemaType::Reference { target } => {
2613                if let Some(schema) = analysis.schemas.get(target) {
2614                    self.type_lacks_default(&schema.schema_type, analysis)
2615                } else {
2616                    false
2617                }
2618            }
2619            _ => false,
2620        }
2621    }
2622
2623    fn generate_specta_field_attrs(&self, field_name: &str) -> TokenStream {
2624        if !self.config.enable_specta {
2625            return TokenStream::new();
2626        }
2627
2628        // Convert field name to camelCase for TypeScript
2629        let camel_case_name = self.to_camel_case(field_name);
2630
2631        // Only add specta rename if it differs from the original field name
2632        if camel_case_name != field_name {
2633            quote! { #[cfg_attr(feature = "specta", specta(rename = #camel_case_name))] }
2634        } else {
2635            TokenStream::new()
2636        }
2637    }
2638
2639    pub(crate) fn to_rust_enum_variant(&self, s: &str) -> String {
2640        // Preserve sign for numeric values so e.g. `-1` and `1` produce
2641        // distinct variants (`VariantNeg1` vs `Variant1`). Without this,
2642        // strict-namespace enums in github.json collide on `1`/`-1`.
2643        let neg_prefix =
2644            if s.starts_with('-') && s.chars().skip(1).all(|c| c.is_ascii_digit() || c == '.') {
2645                "Neg"
2646            } else {
2647                ""
2648            };
2649
2650        // Convert string to valid Rust enum variant (PascalCase)
2651        let mut result = String::new();
2652        let mut next_upper = true;
2653        let mut prev_was_upper = false;
2654
2655        for (i, c) in s.chars().enumerate() {
2656            match c {
2657                'a'..='z' => {
2658                    if next_upper {
2659                        result.push(c.to_ascii_uppercase());
2660                        next_upper = false;
2661                    } else {
2662                        result.push(c);
2663                    }
2664                    prev_was_upper = false;
2665                }
2666                'A'..='Z' => {
2667                    if next_upper || (!prev_was_upper && i > 0) {
2668                        // Start of word or transition from lowercase
2669                        result.push(c);
2670                        next_upper = false;
2671                    } else {
2672                        // Continue uppercase sequence, convert to lowercase
2673                        result.push(c.to_ascii_lowercase());
2674                    }
2675                    prev_was_upper = true;
2676                }
2677                '0'..='9' => {
2678                    result.push(c);
2679                    next_upper = false;
2680                    prev_was_upper = false;
2681                }
2682                '.' | '-' | '_' | ' ' | '@' | '#' | '$' | '/' | '\\' => {
2683                    // Word boundaries - next char should be uppercase
2684                    next_upper = true;
2685                    prev_was_upper = false;
2686                }
2687                _ => {
2688                    // Other special characters - treat as word boundary
2689                    next_upper = true;
2690                    prev_was_upper = false;
2691                }
2692            }
2693        }
2694
2695        // Handle empty result
2696        if result.is_empty() {
2697            result = "Value".to_string();
2698        }
2699
2700        // Ensure variant starts with a letter (not a number)
2701        if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2702            result = format!("Variant{neg_prefix}{result}");
2703        } else if !neg_prefix.is_empty() {
2704            // String happened to start with `-<digits>` but produced a
2705            // non-empty alphabetic prefix. Tag the negative anyway.
2706            result = format!("{neg_prefix}{result}");
2707        }
2708
2709        // Handle special cases for enum variants
2710        match result.as_str() {
2711            "Null" => "NullValue".to_string(),
2712            "True" => "TrueValue".to_string(),
2713            "False" => "FalseValue".to_string(),
2714            "Type" => "Type_".to_string(),
2715            "Match" => "Match_".to_string(),
2716            "Fn" => "Fn_".to_string(),
2717            "Impl" => "Impl_".to_string(),
2718            "Trait" => "Trait_".to_string(),
2719            "Struct" => "Struct_".to_string(),
2720            "Enum" => "Enum_".to_string(),
2721            "Mod" => "Mod_".to_string(),
2722            "Use" => "Use_".to_string(),
2723            "Pub" => "Pub_".to_string(),
2724            "Const" => "Const_".to_string(),
2725            "Static" => "Static_".to_string(),
2726            "Let" => "Let_".to_string(),
2727            "Mut" => "Mut_".to_string(),
2728            "Ref" => "Ref_".to_string(),
2729            "Move" => "Move_".to_string(),
2730            "Return" => "Return_".to_string(),
2731            "If" => "If_".to_string(),
2732            "Else" => "Else_".to_string(),
2733            "While" => "While_".to_string(),
2734            "For" => "For_".to_string(),
2735            "Loop" => "Loop_".to_string(),
2736            "Break" => "Break_".to_string(),
2737            "Continue" => "Continue_".to_string(),
2738            "Self" => "Self_".to_string(),
2739            "Super" => "Super_".to_string(),
2740            "Crate" => "Crate_".to_string(),
2741            "Async" => "Async_".to_string(),
2742            "Await" => "Await_".to_string(),
2743            _ => result,
2744        }
2745    }
2746
2747    #[allow(dead_code)]
2748    fn to_rust_identifier(&self, s: &str) -> String {
2749        // Convert string to valid Rust identifier
2750        let mut result = s
2751            .chars()
2752            .map(|c| match c {
2753                'a'..='z' | 'A'..='Z' | '0'..='9' => c,
2754                '.' | '-' | '_' | ' ' | '@' | '#' | '$' | '/' | '\\' => '_',
2755                _ => '_',
2756            })
2757            .collect::<String>();
2758
2759        // Remove leading/trailing underscores
2760        result = result.trim_matches('_').to_string();
2761
2762        // Handle empty result
2763        if result.is_empty() {
2764            result = "value".to_string();
2765        }
2766
2767        // Ensure identifier starts with a letter (not a number)
2768        if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2769            result = format!("variant_{result}");
2770        }
2771
2772        // Handle special cases for enum values
2773        match result.as_str() {
2774            "null" => "null_value".to_string(),
2775            "true" => "true_value".to_string(),
2776            "false" => "false_value".to_string(),
2777            "type" => "type_".to_string(),
2778            "match" => "match_".to_string(),
2779            "fn" => "fn_".to_string(),
2780            "impl" => "impl_".to_string(),
2781            "trait" => "trait_".to_string(),
2782            "struct" => "struct_".to_string(),
2783            "enum" => "enum_".to_string(),
2784            "mod" => "mod_".to_string(),
2785            "use" => "use_".to_string(),
2786            "pub" => "pub_".to_string(),
2787            "const" => "const_".to_string(),
2788            "static" => "static_".to_string(),
2789            "let" => "let_".to_string(),
2790            "mut" => "mut_".to_string(),
2791            "ref" => "ref_".to_string(),
2792            "move" => "move_".to_string(),
2793            "return" => "return_".to_string(),
2794            "if" => "if_".to_string(),
2795            "else" => "else_".to_string(),
2796            "while" => "while_".to_string(),
2797            "for" => "for_".to_string(),
2798            "loop" => "loop_".to_string(),
2799            "break" => "break_".to_string(),
2800            "continue" => "continue_".to_string(),
2801            "self" => "self_".to_string(),
2802            "super" => "super_".to_string(),
2803            "crate" => "crate_".to_string(),
2804            "async" => "async_".to_string(),
2805            "await" => "await_".to_string(),
2806            // Reserved keywords for edition 2018+
2807            "override" => "override_".to_string(),
2808            "box" => "box_".to_string(),
2809            "dyn" => "dyn_".to_string(),
2810            "where" => "where_".to_string(),
2811            "in" => "in_".to_string(),
2812            // Reserved for future use
2813            "abstract" => "abstract_".to_string(),
2814            "become" => "become_".to_string(),
2815            "do" => "do_".to_string(),
2816            "final" => "final_".to_string(),
2817            "macro" => "macro_".to_string(),
2818            "priv" => "priv_".to_string(),
2819            "try" => "try_".to_string(),
2820            "typeof" => "typeof_".to_string(),
2821            "unsized" => "unsized_".to_string(),
2822            "virtual" => "virtual_".to_string(),
2823            "yield" => "yield_".to_string(),
2824            _ => result,
2825        }
2826    }
2827
2828    /// Q2.4: render a `/// Constraint: …` doc comment for a field
2829    /// when its OpenAPI schema declares any constraint annotations.
2830    /// No-op when constraints are empty or `mode = "off"`.
2831    ///
2832    /// **Doc-comment only** — by deliberate design we never emit
2833    /// `#[validate(...)]` attributes. Constraints belong to the wire
2834    /// contract; the server is the source of truth.
2835    fn generate_constraint_doc(
2836        &self,
2837        constraints: &crate::analysis::PropertyConstraints,
2838    ) -> TokenStream {
2839        use crate::type_mapping::ConstraintMode;
2840
2841        if constraints.is_empty() {
2842            return TokenStream::new();
2843        }
2844        match self.config.types.constraint_mode() {
2845            ConstraintMode::Off => TokenStream::new(),
2846            ConstraintMode::Doc => {
2847                let formatted = format_constraints_doc(constraints);
2848                quote! { #[doc = #formatted] }
2849            }
2850        }
2851    }
2852
2853    fn sanitize_doc_comment(&self, desc: &str) -> String {
2854        // Sanitize description to prevent doctest failures
2855        let mut result = desc.to_string();
2856
2857        // Look for potential code examples that might be interpreted as doctests
2858        // Common patterns that cause issues:
2859        // - Lines that look like standalone expressions
2860        // - JSON-like content
2861        // - Template strings with {}
2862
2863        // If the description contains what looks like code, wrap it in a text block
2864        if result.contains('\n')
2865            && (result.contains('{')
2866                || result.contains("```")
2867                || result.contains("Human:")
2868                || result.contains("Assistant:")
2869                || result
2870                    .lines()
2871                    .any(|line| line.trim().starts_with('"') && line.trim().ends_with('"')))
2872        {
2873            // If it already has code blocks, add ignore annotation
2874            if result.contains("```") {
2875                result = result.replace("```", "```ignore");
2876            } else {
2877                // Wrap the entire description in an ignored code block if it looks like code
2878                if result.lines().any(|line| {
2879                    let trimmed = line.trim();
2880                    trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() > 2
2881                }) {
2882                    result = format!("```ignore\n{result}\n```");
2883                }
2884            }
2885        }
2886
2887        result
2888    }
2889
2890    pub(crate) fn to_rust_type_name(&self, s: &str) -> String {
2891        // Convert string to valid Rust type name (PascalCase)
2892        let mut result = String::new();
2893        let mut next_upper = true;
2894        let mut prev_was_lower = false;
2895
2896        for c in s.chars() {
2897            match c {
2898                'a'..='z' => {
2899                    if next_upper {
2900                        result.push(c.to_ascii_uppercase());
2901                        next_upper = false;
2902                    } else {
2903                        result.push(c);
2904                    }
2905                    prev_was_lower = true;
2906                }
2907                'A'..='Z' => {
2908                    result.push(c);
2909                    next_upper = false;
2910                    prev_was_lower = false;
2911                }
2912                '0'..='9' => {
2913                    // If previous was lowercase letter and this is start of a number sequence,
2914                    // make it uppercase to improve readability (e.g., Tool20241022 instead of Tool20241022)
2915                    if prev_was_lower && !result.chars().last().unwrap_or(' ').is_ascii_digit() {
2916                        // This is fine as-is, the number follows naturally
2917                    }
2918                    result.push(c);
2919                    next_upper = false;
2920                    prev_was_lower = false;
2921                }
2922                '_' | '-' | '.' | ' ' => {
2923                    // Skip underscore/separator and make next char uppercase
2924                    next_upper = true;
2925                    prev_was_lower = false;
2926                }
2927                _ => {
2928                    // Other special characters - treat as word boundary
2929                    next_upper = true;
2930                    prev_was_lower = false;
2931                }
2932            }
2933        }
2934
2935        // Handle empty result
2936        if result.is_empty() {
2937            result = "Type".to_string();
2938        }
2939
2940        // Ensure type name starts with a letter (not a number)
2941        if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2942            result = format!("Type{result}");
2943        }
2944
2945        // Avoid masking ubiquitous std types and traits. cloudflare has a
2946        // schema literally named `Result`, gcore has `Default`; emitting
2947        // `pub enum Result { ... }` shadows std::result::Result and breaks
2948        // every method's `-> Result<T, ApiOpError<...>>`. Same for impls
2949        // like `impl Default for HttpClient { ... }` when `Default` resolves
2950        // to the local type alias.
2951        if matches!(
2952            result.as_str(),
2953            "Result"
2954                | "Option"
2955                | "Box"
2956                | "Vec"
2957                | "String"
2958                | "Some"
2959                | "None"
2960                | "Ok"
2961                | "Err"
2962                | "Default"
2963                | "Clone"
2964                | "Debug"
2965                | "Send"
2966                | "Sync"
2967                | "Sized"
2968                | "Iterator"
2969                | "From"
2970                | "Into"
2971                | "TryFrom"
2972                | "TryInto"
2973                | "AsRef"
2974                | "AsMut"
2975        ) {
2976            result.push_str("Type");
2977        }
2978
2979        result
2980    }
2981
2982    fn to_rust_field_name(&self, s: &str) -> String {
2983        // Track sign / leading-non-alpha so e.g. `+1` and `-1` produce
2984        // distinct field names instead of both collapsing to `field_1`
2985        // (observed in github.json's reactions schemas).
2986        let leading_marker = match s.chars().next() {
2987            Some('-') if s.len() > 1 => "neg_",
2988            Some('+') if s.len() > 1 => "pos_",
2989            _ => "",
2990        };
2991
2992        // Convert field name to snake_case properly
2993        let mut result = String::new();
2994        let mut prev_was_upper = false;
2995        let mut prev_was_underscore = false;
2996
2997        for (i, c) in s.chars().enumerate() {
2998            match c {
2999                'A'..='Z' => {
3000                    // Add underscore before uppercase if previous was lowercase
3001                    if i > 0 && !prev_was_upper && !prev_was_underscore {
3002                        result.push('_');
3003                    }
3004                    result.push(c.to_ascii_lowercase());
3005                    prev_was_upper = true;
3006                    prev_was_underscore = false;
3007                }
3008                'a'..='z' | '0'..='9' => {
3009                    result.push(c);
3010                    prev_was_upper = false;
3011                    prev_was_underscore = false;
3012                }
3013                '-' | '.' | '_' | '@' | '#' | '$' | ' ' => {
3014                    if !prev_was_underscore && !result.is_empty() {
3015                        result.push('_');
3016                        prev_was_underscore = true;
3017                    }
3018                    prev_was_upper = false;
3019                }
3020                _ => {
3021                    // For other special characters, convert to underscore
3022                    if !prev_was_underscore && !result.is_empty() {
3023                        result.push('_');
3024                    }
3025                    prev_was_upper = false;
3026                    prev_was_underscore = true;
3027                }
3028            }
3029        }
3030
3031        // Clean up result
3032        let mut result = result.trim_matches('_').to_string();
3033        if result.is_empty() {
3034            return "field".to_string();
3035        }
3036
3037        // Ensure field name starts with a letter or underscore (not a number)
3038        if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
3039            result = format!("field_{leading_marker}{result}");
3040        } else if !leading_marker.is_empty() {
3041            result = format!("{leading_marker}{result}");
3042        }
3043
3044        // `self`, `super`, `crate`, `Self` are NOT permitted as raw identifiers
3045        // (they trigger an `r#self cannot be a raw identifier` panic in
3046        // proc_macro2). Suffix them instead.
3047        if matches!(result.as_str(), "self" | "super" | "crate" | "Self") {
3048            return format!("{result}_field");
3049        }
3050        // Handle reserved keywords using raw identifiers (r#keyword)
3051        if Self::is_rust_keyword(&result) {
3052            format!("r#{result}")
3053        } else {
3054            result
3055        }
3056    }
3057
3058    /// Check if a string is a Rust keyword that needs raw identifier treatment
3059    pub fn is_rust_keyword(s: &str) -> bool {
3060        matches!(
3061            s,
3062            "type"
3063                | "match"
3064                | "fn"
3065                | "struct"
3066                | "enum"
3067                | "impl"
3068                | "trait"
3069                | "mod"
3070                | "use"
3071                | "pub"
3072                | "const"
3073                | "static"
3074                | "let"
3075                | "mut"
3076                | "ref"
3077                | "move"
3078                | "return"
3079                | "if"
3080                | "else"
3081                | "while"
3082                | "for"
3083                | "loop"
3084                | "break"
3085                | "continue"
3086                | "self"
3087                | "super"
3088                | "crate"
3089                | "async"
3090                | "await"
3091                | "override"
3092                | "box"
3093                | "dyn"
3094                | "where"
3095                | "in"
3096                | "abstract"
3097                | "become"
3098                | "do"
3099                | "final"
3100                | "macro"
3101                | "priv"
3102                | "try"
3103                | "typeof"
3104                | "unsized"
3105                | "virtual"
3106                | "yield"
3107                // Rust 2024 edition reservations.
3108                | "gen"
3109        )
3110    }
3111
3112    /// Create a proc_macro2::Ident from a field name, handling r# raw identifiers
3113    pub fn to_field_ident(name: &str) -> proc_macro2::Ident {
3114        if let Some(raw) = name.strip_prefix("r#") {
3115            proc_macro2::Ident::new_raw(raw, proc_macro2::Span::call_site())
3116        } else {
3117            proc_macro2::Ident::new(name, proc_macro2::Span::call_site())
3118        }
3119    }
3120
3121    fn to_camel_case(&self, s: &str) -> String {
3122        // Convert snake_case or other formats to camelCase
3123        let mut result = String::new();
3124        let mut capitalize_next = false;
3125
3126        for (i, c) in s.chars().enumerate() {
3127            match c {
3128                '_' | '-' | '.' | ' ' => {
3129                    // Word boundary - capitalize next letter
3130                    capitalize_next = true;
3131                }
3132                'A'..='Z' => {
3133                    if i == 0 {
3134                        // First character should be lowercase in camelCase
3135                        result.push(c.to_ascii_lowercase());
3136                    } else if capitalize_next {
3137                        result.push(c);
3138                        capitalize_next = false;
3139                    } else {
3140                        result.push(c.to_ascii_lowercase());
3141                    }
3142                }
3143                'a'..='z' | '0'..='9' => {
3144                    if capitalize_next {
3145                        result.push(c.to_ascii_uppercase());
3146                        capitalize_next = false;
3147                    } else {
3148                        result.push(c);
3149                    }
3150                }
3151                _ => {
3152                    // Other characters - treat as word boundary
3153                    capitalize_next = true;
3154                }
3155            }
3156        }
3157
3158        if result.is_empty() {
3159            return "field".to_string();
3160        }
3161
3162        result
3163    }
3164
3165    fn generate_composition_struct(
3166        &self,
3167        schema: &crate::analysis::AnalyzedSchema,
3168        schemas: &[crate::analysis::SchemaRef],
3169    ) -> Result<TokenStream> {
3170        let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
3171
3172        // For composition, we can either:
3173        // 1. Flatten all referenced schemas into one struct (if they're all objects)
3174        // 2. Use serde(flatten) to compose them at runtime
3175        // For now, let's use approach 2 with serde(flatten)
3176
3177        let fields = schemas.iter().enumerate().map(|(i, schema_ref)| {
3178            let field_name = format_ident!("part_{}", i);
3179            let field_type = format_ident!("{}", self.to_rust_type_name(&schema_ref.target));
3180
3181            quote! {
3182                #[serde(flatten)]
3183                pub #field_name: #field_type,
3184            }
3185        });
3186
3187        let doc_comment = if let Some(desc) = &schema.description {
3188            quote! { #[doc = #desc] }
3189        } else {
3190            TokenStream::new()
3191        };
3192
3193        // Generate derives with optional Specta support
3194        let derives = if self.config.enable_specta {
3195            quote! {
3196                #[derive(Debug, Clone, Deserialize, Serialize)]
3197                #[cfg_attr(feature = "specta", derive(specta::Type))]
3198            }
3199        } else {
3200            quote! {
3201                #[derive(Debug, Clone, Deserialize, Serialize)]
3202            }
3203        };
3204
3205        Ok(quote! {
3206            #doc_comment
3207            #derives
3208            pub struct #struct_name {
3209                #(#fields)*
3210            }
3211        })
3212    }
3213
3214    #[allow(dead_code)]
3215    fn find_missing_types(&self, analysis: &SchemaAnalysis) -> std::collections::HashSet<String> {
3216        let mut missing = std::collections::HashSet::new();
3217        let defined_types: std::collections::HashSet<String> =
3218            analysis.schemas.keys().cloned().collect();
3219
3220        // Check all references in union variants
3221        for schema in analysis.schemas.values() {
3222            match &schema.schema_type {
3223                crate::analysis::SchemaType::Union { variants } => {
3224                    for variant in variants {
3225                        if !defined_types.contains(&variant.target) {
3226                            missing.insert(variant.target.clone());
3227                        }
3228                    }
3229                }
3230                crate::analysis::SchemaType::DiscriminatedUnion { variants, .. } => {
3231                    for variant in variants {
3232                        if !defined_types.contains(&variant.type_name) {
3233                            missing.insert(variant.type_name.clone());
3234                        }
3235                    }
3236                }
3237                crate::analysis::SchemaType::Object { properties, .. } => {
3238                    // Sort properties for deterministic iteration
3239                    let mut sorted_props: Vec<_> = properties.iter().collect();
3240                    sorted_props.sort_by_key(|(name, _)| name.as_str());
3241                    for (_, prop) in sorted_props {
3242                        if let crate::analysis::SchemaType::Reference { target } = &prop.schema_type
3243                        {
3244                            if !defined_types.contains(target) {
3245                                missing.insert(target.clone());
3246                            }
3247                        }
3248                    }
3249                }
3250                crate::analysis::SchemaType::Reference { target }
3251                    if !defined_types.contains(target) =>
3252                {
3253                    missing.insert(target.clone());
3254                }
3255                _ => {}
3256            }
3257        }
3258
3259        missing
3260    }
3261
3262    #[allow(clippy::only_used_in_recursion)]
3263    fn generate_array_item_type(
3264        &self,
3265        item_type: &crate::analysis::SchemaType,
3266        analysis: &crate::analysis::SchemaAnalysis,
3267    ) -> TokenStream {
3268        use crate::analysis::SchemaType;
3269
3270        match item_type {
3271            SchemaType::Primitive { rust_type, .. } => {
3272                // The string here may be anything from `i64` / `String` to
3273                // `serde_json::Value` to `Vec<serde_json::Value>` to
3274                // `BTreeMap<String, T>`. Parse it as a syn::Type so we get
3275                // the right tokens regardless of generics.
3276                if let Ok(parsed) = syn::parse_str::<syn::Type>(rust_type) {
3277                    quote! { #parsed }
3278                } else if rust_type.contains("::") {
3279                    let parts: Vec<_> = rust_type
3280                        .split("::")
3281                        .map(|p| format_ident!("{}", p))
3282                        .collect();
3283                    quote! { #(#parts)::* }
3284                } else {
3285                    let type_ident = format_ident!("{}", rust_type);
3286                    quote! { #type_ident }
3287                }
3288            }
3289            SchemaType::Reference { target } => {
3290                let target_type = format_ident!("{}", self.to_rust_type_name(target));
3291                // Wrap recursive references in Box<T> for heap allocation in arrays
3292                if analysis.dependencies.recursive_schemas.contains(target) {
3293                    quote! { Box<#target_type> }
3294                } else {
3295                    quote! { #target_type }
3296                }
3297            }
3298            SchemaType::Array { item_type } => {
3299                // Nested arrays
3300                let inner_type = self.generate_array_item_type(item_type, analysis);
3301                quote! { Vec<#inner_type> }
3302            }
3303            _ => {
3304                // Fallback for complex types
3305                quote! { serde_json::Value }
3306            }
3307        }
3308    }
3309
3310    /// Convert a type name to a variant name (e.g., OutputMessage -> OutputMessage, FileSearchToolCall -> FileSearchToolCall)
3311    fn type_name_to_variant_name(&self, type_name: &str) -> String {
3312        // Handle primitive types specially
3313        match type_name {
3314            "bool" => return "Boolean".to_string(),
3315            "i8" | "i16" | "i32" | "i64" | "i128" => return "Integer".to_string(),
3316            "u8" | "u16" | "u32" | "u64" | "u128" => return "UnsignedInteger".to_string(),
3317            "f32" | "f64" => return "Number".to_string(),
3318            "String" => return "String".to_string(),
3319            "serde_json::Value" => return "Value".to_string(),
3320            // Q2 typed-scalar paths. Without these the fallback PascalCase
3321            // pass over `bytes::Bytes` produces `BytesBytes(BytesBytes)`,
3322            // which then can't compile because no `BytesBytes` type exists.
3323            "bytes::Bytes" => return "Binary".to_string(),
3324            "chrono::DateTime<chrono::Utc>" => return "DateTime".to_string(),
3325            "chrono::NaiveDate" => return "Date".to_string(),
3326            "chrono::NaiveTime" => return "Time".to_string(),
3327            "uuid::Uuid" => return "Uuid".to_string(),
3328            "url::Url" => return "Url".to_string(),
3329            "std::net::Ipv4Addr" => return "Ipv4".to_string(),
3330            "std::net::Ipv6Addr" => return "Ipv6".to_string(),
3331            _ => {}
3332        }
3333
3334        // Handle Vec types
3335        if type_name.starts_with("Vec<") && type_name.ends_with(">") {
3336            let inner = &type_name[4..type_name.len() - 1];
3337            // Handle nested Vec types specially
3338            if inner.starts_with("Vec<") && inner.ends_with(">") {
3339                let inner_inner = &inner[4..inner.len() - 1];
3340                return format!("{}ArrayArray", self.type_name_to_variant_name(inner_inner));
3341            }
3342            return format!("{}Array", self.type_name_to_variant_name(inner));
3343        }
3344
3345        // For untagged unions, we want to use the type name itself as the variant name
3346        // since it's already meaningful. This gives us OutputMessage instead of Variant0,
3347        // FileSearchToolCall instead of Variant1, etc.
3348
3349        // Remove common suffixes that might make variant names redundant
3350        let clean_name = type_name
3351            .trim_end_matches("Type")
3352            .trim_end_matches("Schema")
3353            .trim_end_matches("Item");
3354
3355        // Always convert to proper PascalCase to ensure no underscores in enum variants
3356        self.to_rust_type_name(clean_name)
3357    }
3358
3359    /// Ensure unique variant name for generator (similar to analyzer but for generator context)
3360    fn ensure_unique_variant_name_generator(
3361        &self,
3362        base_name: String,
3363        used_names: &mut std::collections::HashSet<String>,
3364        fallback_index: usize,
3365    ) -> String {
3366        if used_names.insert(base_name.clone()) {
3367            return base_name;
3368        }
3369
3370        // Try with numbers
3371        for i in 2..100 {
3372            let numbered_name = format!("{base_name}{i}");
3373            if used_names.insert(numbered_name.clone()) {
3374                return numbered_name;
3375            }
3376        }
3377
3378        // Fallback to Variant{index} if all else fails
3379        let fallback = format!("Variant{fallback_index}");
3380        used_names.insert(fallback.clone());
3381        fallback
3382    }
3383
3384    /// Find the request type for a given operation ID using the analyzed operation info
3385    fn find_request_type_for_operation(
3386        &self,
3387        operation_id: &str,
3388        analysis: &SchemaAnalysis,
3389    ) -> Option<String> {
3390        // Use the operation analysis to get the actual request body schema
3391        analysis.operations.get(operation_id).and_then(|op| {
3392            op.request_body
3393                .as_ref()
3394                .and_then(|rb| rb.schema_name().map(|s| s.to_string()))
3395        })
3396    }
3397
3398    /// Resolve the correct streaming event type based on EventFlow pattern
3399    fn resolve_streaming_event_type(
3400        &self,
3401        endpoint: &crate::streaming::StreamingEndpoint,
3402        analysis: &SchemaAnalysis,
3403    ) -> Result<String> {
3404        match &endpoint.event_flow {
3405            crate::streaming::EventFlow::Simple => {
3406                // For simple streaming, use the response type directly
3407                // Validate that the specified type exists in the schema
3408                if analysis.schemas.contains_key(&endpoint.event_union_type) {
3409                    Ok(endpoint.event_union_type.to_string())
3410                } else {
3411                    Err(crate::error::GeneratorError::ValidationError(format!(
3412                        "Streaming response type '{}' not found in schema for simple streaming endpoint '{}'",
3413                        endpoint.event_union_type, endpoint.operation_id
3414                    )))
3415                }
3416            }
3417            crate::streaming::EventFlow::StartDeltaStop { .. } => {
3418                // For complex event-based streaming, ensure we have a proper union type
3419                // For now, use the specified event_union_type but add validation
3420                if analysis.schemas.contains_key(&endpoint.event_union_type) {
3421                    Ok(endpoint.event_union_type.to_string())
3422                } else {
3423                    Err(crate::error::GeneratorError::ValidationError(format!(
3424                        "Event union type '{}' not found in schema for complex streaming endpoint '{}'",
3425                        endpoint.event_union_type, endpoint.operation_id
3426                    )))
3427                }
3428            }
3429        }
3430    }
3431
3432    /// Generate streaming error types
3433    fn generate_streaming_error_types(&self) -> Result<TokenStream> {
3434        Ok(quote! {
3435            /// Error type for streaming operations
3436            #[derive(Debug, thiserror::Error)]
3437            pub enum StreamingError {
3438                #[error("Connection error: {0}")]
3439                Connection(String),
3440                #[error("HTTP error: {status}")]
3441                Http { status: u16 },
3442                #[error("SSE parsing error: {0}")]
3443                Parsing(String),
3444                #[error("Authentication error: {0}")]
3445                Authentication(String),
3446                #[error("Rate limit error: {0}")]
3447                RateLimit(String),
3448                #[error("API error: {0}")]
3449                Api(String),
3450                #[error("Timeout error: {0}")]
3451                Timeout(String),
3452                #[error("JSON serialization/deserialization error: {0}")]
3453                Json(#[from] serde_json::Error),
3454                #[error("Request error: {0}")]
3455                Request(reqwest::Error),
3456            }
3457
3458            impl From<reqwest::header::InvalidHeaderValue> for StreamingError {
3459                fn from(err: reqwest::header::InvalidHeaderValue) -> Self {
3460                    StreamingError::Api(format!("Invalid header value: {}", err))
3461                }
3462            }
3463
3464            impl From<reqwest::Error> for StreamingError {
3465                fn from(err: reqwest::Error) -> Self {
3466                    if err.is_timeout() {
3467                        StreamingError::Timeout(err.to_string())
3468                    } else if err.is_status() {
3469                        if let Some(status) = err.status() {
3470                            StreamingError::Http { status: status.as_u16() }
3471                        } else {
3472                            StreamingError::Connection(err.to_string())
3473                        }
3474                    } else {
3475                        StreamingError::Request(err)
3476                    }
3477                }
3478            }
3479        })
3480    }
3481
3482    /// Generate trait for a streaming endpoint
3483    fn generate_endpoint_trait(
3484        &self,
3485        endpoint: &crate::streaming::StreamingEndpoint,
3486        analysis: &SchemaAnalysis,
3487    ) -> Result<TokenStream> {
3488        use crate::streaming::HttpMethod;
3489
3490        let trait_name = format_ident!(
3491            "{}StreamingClient",
3492            self.to_rust_type_name(&endpoint.operation_id)
3493        );
3494        let method_name =
3495            format_ident!("stream_{}", self.to_rust_field_name(&endpoint.operation_id));
3496        let event_type =
3497            format_ident!("{}", self.resolve_streaming_event_type(endpoint, analysis)?);
3498
3499        // Generate method signature based on HTTP method
3500        let method_signature = match endpoint.http_method {
3501            HttpMethod::Get => {
3502                // Generate parameters from query_parameters
3503                let mut param_defs = Vec::new();
3504                for qp in &endpoint.query_parameters {
3505                    let param_name = format_ident!("{}", self.to_rust_field_name(&qp.name));
3506                    if qp.required {
3507                        param_defs.push(quote! { #param_name: &str });
3508                    } else {
3509                        param_defs.push(quote! { #param_name: Option<&str> });
3510                    }
3511                }
3512                quote! {
3513                    async fn #method_name(
3514                        &self,
3515                        #(#param_defs),*
3516                    ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error>;
3517                }
3518            }
3519            HttpMethod::Post => {
3520                // Find the request type for this operation
3521                let request_type = self
3522                    .find_request_type_for_operation(&endpoint.operation_id, analysis)
3523                    .unwrap_or_else(|| "serde_json::Value".to_string());
3524                let request_type_ident = if request_type.contains("::") {
3525                    let parts: Vec<&str> = request_type.split("::").collect();
3526                    let path_parts: Vec<_> = parts.iter().map(|p| format_ident!("{}", p)).collect();
3527                    quote! { #(#path_parts)::* }
3528                } else {
3529                    let ident = format_ident!("{}", request_type);
3530                    quote! { #ident }
3531                };
3532                quote! {
3533                    async fn #method_name(
3534                        &self,
3535                        request: #request_type_ident,
3536                    ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error>;
3537                }
3538            }
3539        };
3540
3541        Ok(quote! {
3542            /// Streaming client trait for this endpoint
3543            #[async_trait]
3544            pub trait #trait_name {
3545                type Error: std::error::Error + Send + Sync + 'static;
3546
3547                /// Stream events from the API
3548                #method_signature
3549            }
3550        })
3551    }
3552
3553    /// Generate streaming client implementation
3554    fn generate_streaming_client_impl(
3555        &self,
3556        streaming_config: &crate::streaming::StreamingConfig,
3557        analysis: &SchemaAnalysis,
3558    ) -> Result<TokenStream> {
3559        let client_name = format_ident!(
3560            "{}Client",
3561            self.to_rust_type_name(&streaming_config.client_module_name)
3562        );
3563
3564        // Generate struct fields
3565        // Always include custom_headers for flexibility (like HttpClient does)
3566        let mut struct_fields = vec![
3567            quote! { base_url: String },
3568            quote! { api_key: Option<String> },
3569            quote! { http_client: reqwest::Client },
3570            quote! { custom_headers: std::collections::BTreeMap<String, String> },
3571        ];
3572
3573        let has_optional_headers = !streaming_config
3574            .endpoints
3575            .iter()
3576            .all(|e| e.optional_headers.is_empty());
3577
3578        if has_optional_headers {
3579            struct_fields
3580                .push(quote! { optional_headers: std::collections::BTreeMap<String, String> });
3581        }
3582
3583        // Generate constructor
3584        // Use configured base URL as default, or fallback to generic example
3585        let default_base_url = if let Some(ref streaming_config) = self.config.streaming_config {
3586            streaming_config
3587                .endpoints
3588                .first()
3589                .and_then(|e| e.base_url.as_deref())
3590                .unwrap_or("https://api.example.com")
3591        } else {
3592            "https://api.example.com"
3593        };
3594
3595        // Build constructor fields based on what the struct has
3596        let constructor_fields = if has_optional_headers {
3597            quote! {
3598                base_url: #default_base_url.to_string(),
3599                api_key: None,
3600                http_client: reqwest::Client::new(),
3601                custom_headers: std::collections::BTreeMap::new(),
3602                optional_headers: std::collections::BTreeMap::new(),
3603            }
3604        } else {
3605            quote! {
3606                base_url: #default_base_url.to_string(),
3607                api_key: None,
3608                http_client: reqwest::Client::new(),
3609                custom_headers: std::collections::BTreeMap::new(),
3610            }
3611        };
3612
3613        // Optional headers method only if the struct has the field
3614        let optional_headers_method = if has_optional_headers {
3615            quote! {
3616                /// Set optional headers for all requests
3617                pub fn set_optional_headers(&mut self, headers: std::collections::BTreeMap<String, String>) {
3618                    self.optional_headers = headers;
3619                }
3620            }
3621        } else {
3622            TokenStream::new()
3623        };
3624
3625        let constructor = quote! {
3626            impl #client_name {
3627                /// Create a new streaming client
3628                pub fn new() -> Self {
3629                    Self {
3630                        #constructor_fields
3631                    }
3632                }
3633
3634                /// Set the base URL for API requests
3635                pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
3636                    self.base_url = base_url.into();
3637                    self
3638                }
3639
3640                /// Set the API key for authentication
3641                pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
3642                    self.api_key = Some(api_key.into());
3643                    self
3644                }
3645
3646                /// Add a custom header to all requests
3647                pub fn with_header(
3648                    mut self,
3649                    name: impl Into<String>,
3650                    value: impl Into<String>,
3651                ) -> Self {
3652                    self.custom_headers.insert(name.into(), value.into());
3653                    self
3654                }
3655
3656                /// Set the HTTP client
3657                pub fn with_http_client(mut self, client: reqwest::Client) -> Self {
3658                    self.http_client = client;
3659                    self
3660                }
3661
3662                #optional_headers_method
3663            }
3664        };
3665
3666        // Generate trait implementations for each endpoint
3667        let mut trait_impls = Vec::new();
3668        for endpoint in &streaming_config.endpoints {
3669            let trait_impl = self.generate_endpoint_trait_impl(endpoint, &client_name, analysis)?;
3670            trait_impls.push(trait_impl);
3671        }
3672
3673        // Add Default implementation
3674        let default_impl = quote! {
3675            impl Default for #client_name {
3676                fn default() -> Self {
3677                    Self::new()
3678                }
3679            }
3680        };
3681
3682        Ok(quote! {
3683            /// Streaming client implementation
3684            #[derive(Debug, Clone)]
3685            pub struct #client_name {
3686                #(#struct_fields,)*
3687            }
3688
3689            #constructor
3690
3691            #default_impl
3692
3693            #(#trait_impls)*
3694        })
3695    }
3696
3697    /// Generate trait implementation for a specific endpoint
3698    fn generate_endpoint_trait_impl(
3699        &self,
3700        endpoint: &crate::streaming::StreamingEndpoint,
3701        client_name: &proc_macro2::Ident,
3702        analysis: &SchemaAnalysis,
3703    ) -> Result<TokenStream> {
3704        use crate::streaming::HttpMethod;
3705
3706        let trait_name = format_ident!(
3707            "{}StreamingClient",
3708            self.to_rust_type_name(&endpoint.operation_id)
3709        );
3710        let method_name =
3711            format_ident!("stream_{}", self.to_rust_field_name(&endpoint.operation_id));
3712        let event_type =
3713            format_ident!("{}", self.resolve_streaming_event_type(endpoint, analysis)?);
3714
3715        // Generate required headers
3716        let mut header_setup = Vec::new();
3717        for (name, value) in &endpoint.required_headers {
3718            header_setup.push(quote! {
3719                headers.insert(#name, HeaderValue::from_static(#value));
3720            });
3721        }
3722
3723        // Add authentication header
3724        // If auth_header is configured, use that; otherwise default to Bearer auth on Authorization header
3725        if let Some(auth_header) = &endpoint.auth_header {
3726            match auth_header {
3727                crate::streaming::AuthHeader::Bearer(header_name) => {
3728                    header_setup.push(quote! {
3729                        if let Some(ref api_key) = self.api_key {
3730                            headers.insert(#header_name, HeaderValue::from_str(&format!("Bearer {}", api_key))?);
3731                        }
3732                    });
3733                }
3734                crate::streaming::AuthHeader::ApiKey(header_name) => {
3735                    header_setup.push(quote! {
3736                        if let Some(ref api_key) = self.api_key {
3737                            headers.insert(#header_name, HeaderValue::from_str(api_key)?);
3738                        }
3739                    });
3740                }
3741            }
3742        } else {
3743            // Default: use api_key as Bearer token on Authorization header
3744            header_setup.push(quote! {
3745                if let Some(ref api_key) = self.api_key {
3746                    headers.insert("Authorization", HeaderValue::from_str(&format!("Bearer {}", api_key))?);
3747                }
3748            });
3749        }
3750
3751        // Always add custom_headers (like HttpClient does)
3752        header_setup.push(quote! {
3753            for (name, value) in &self.custom_headers {
3754                if let (Ok(header_name), Ok(header_value)) = (reqwest::header::HeaderName::from_bytes(name.as_bytes()), HeaderValue::from_str(value)) {
3755                    headers.insert(header_name, header_value);
3756                }
3757            }
3758        });
3759
3760        // Add optional headers (for endpoint-specific optional headers)
3761        if !endpoint.optional_headers.is_empty() {
3762            header_setup.push(quote! {
3763                for (key, value) in &self.optional_headers {
3764                    if let (Ok(header_name), Ok(header_value)) = (reqwest::header::HeaderName::from_bytes(key.as_bytes()), HeaderValue::from_str(value)) {
3765                        headers.insert(header_name, header_value);
3766                    }
3767                }
3768            });
3769        }
3770
3771        // Generate different code for GET vs POST
3772        match endpoint.http_method {
3773            HttpMethod::Get => self.generate_get_streaming_impl(
3774                endpoint,
3775                client_name,
3776                &trait_name,
3777                &method_name,
3778                &event_type,
3779                &header_setup,
3780            ),
3781            HttpMethod::Post => self.generate_post_streaming_impl(
3782                endpoint,
3783                client_name,
3784                &trait_name,
3785                &method_name,
3786                &event_type,
3787                &header_setup,
3788                analysis,
3789            ),
3790        }
3791    }
3792
3793    /// Generate streaming implementation for GET endpoints
3794    fn generate_get_streaming_impl(
3795        &self,
3796        endpoint: &crate::streaming::StreamingEndpoint,
3797        client_name: &proc_macro2::Ident,
3798        trait_name: &proc_macro2::Ident,
3799        method_name: &proc_macro2::Ident,
3800        event_type: &proc_macro2::Ident,
3801        header_setup: &[TokenStream],
3802    ) -> Result<TokenStream> {
3803        let path = &endpoint.path;
3804
3805        // Generate method parameters from query_parameters
3806        let mut param_defs = Vec::new();
3807        let mut query_params = Vec::new();
3808
3809        for qp in &endpoint.query_parameters {
3810            let param_name = format_ident!("{}", self.to_rust_field_name(&qp.name));
3811            let param_name_str = &qp.name;
3812
3813            if qp.required {
3814                param_defs.push(quote! { #param_name: &str });
3815                query_params.push(quote! {
3816                    url.query_pairs_mut().append_pair(#param_name_str, #param_name);
3817                });
3818            } else {
3819                param_defs.push(quote! { #param_name: Option<&str> });
3820                query_params.push(quote! {
3821                    if let Some(v) = #param_name {
3822                        url.query_pairs_mut().append_pair(#param_name_str, v);
3823                    }
3824                });
3825            }
3826        }
3827
3828        // Generate URL construction for GET
3829        let url_construction = quote! {
3830            let base_url = url::Url::parse(&self.base_url)
3831                .map_err(|e| StreamingError::Connection(format!("Invalid base URL: {}", e)))?;
3832            let path_to_join = #path.trim_start_matches('/');
3833            let mut url = base_url.join(path_to_join)
3834                .map_err(|e| StreamingError::Connection(format!("URL join error: {}", e)))?;
3835            #(#query_params)*
3836        };
3837
3838        let instrument_skip = quote! { #[instrument(skip(self), name = "streaming_get_request")] };
3839
3840        Ok(quote! {
3841            #[async_trait]
3842            impl #trait_name for #client_name {
3843                type Error = StreamingError;
3844
3845                #instrument_skip
3846                async fn #method_name(
3847                    &self,
3848                    #(#param_defs),*
3849                ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error> {
3850                    debug!("Starting streaming GET request");
3851
3852                    let mut headers = HeaderMap::new();
3853                    #(#header_setup)*
3854
3855                    #url_construction
3856                    let url_str = url.to_string();
3857                    debug!("Making streaming GET request to: {}", url_str);
3858
3859                    let request_builder = self.http_client
3860                        .get(url_str)
3861                        .headers(headers);
3862
3863                    debug!("Creating SSE stream from request");
3864                    let stream = parse_sse_stream::<#event_type>(request_builder).await?;
3865                    info!("SSE stream created successfully");
3866                    Ok(Box::pin(stream))
3867                }
3868            }
3869        })
3870    }
3871
3872    /// Generate streaming implementation for POST endpoints
3873    #[allow(clippy::too_many_arguments)]
3874    fn generate_post_streaming_impl(
3875        &self,
3876        endpoint: &crate::streaming::StreamingEndpoint,
3877        client_name: &proc_macro2::Ident,
3878        trait_name: &proc_macro2::Ident,
3879        method_name: &proc_macro2::Ident,
3880        event_type: &proc_macro2::Ident,
3881        header_setup: &[TokenStream],
3882        analysis: &SchemaAnalysis,
3883    ) -> Result<TokenStream> {
3884        let path = &endpoint.path;
3885
3886        // Find the request type for this operation
3887        let request_type = self
3888            .find_request_type_for_operation(&endpoint.operation_id, analysis)
3889            .unwrap_or_else(|| "serde_json::Value".to_string());
3890        let request_type_ident = if request_type.contains("::") {
3891            let parts: Vec<&str> = request_type.split("::").collect();
3892            let path_parts: Vec<_> = parts.iter().map(|p| format_ident!("{}", p)).collect();
3893            quote! { #(#path_parts)::* }
3894        } else {
3895            let ident = format_ident!("{}", request_type);
3896            quote! { #ident }
3897        };
3898
3899        // Generate URL construction for POST
3900        let url_construction = quote! {
3901            let base_url = url::Url::parse(&self.base_url)
3902                .map_err(|e| StreamingError::Connection(format!("Invalid base URL: {}", e)))?;
3903            let path_to_join = #path.trim_start_matches('/');
3904            let url = base_url.join(path_to_join)
3905                .map_err(|e| StreamingError::Connection(format!("URL join error: {}", e)))?
3906                .to_string();
3907        };
3908
3909        // Generate stream parameter setup (only for POST with stream_parameter)
3910        let stream_param = &endpoint.stream_parameter;
3911        let stream_setup = if stream_param.is_empty() {
3912            quote! {
3913                let streaming_request = request;
3914            }
3915        } else {
3916            quote! {
3917                // Ensure streaming is enabled
3918                let mut streaming_request = request;
3919                if let Ok(mut request_value) = serde_json::to_value(&streaming_request) {
3920                    if let Some(obj) = request_value.as_object_mut() {
3921                        obj.insert(#stream_param.to_string(), serde_json::Value::Bool(true));
3922                    }
3923                    streaming_request = serde_json::from_value(request_value)?;
3924                }
3925            }
3926        };
3927
3928        Ok(quote! {
3929            #[async_trait]
3930            impl #trait_name for #client_name {
3931                type Error = StreamingError;
3932
3933                #[instrument(skip(self, request), name = "streaming_post_request")]
3934                async fn #method_name(
3935                    &self,
3936                    request: #request_type_ident,
3937                ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error> {
3938                    debug!("Starting streaming POST request");
3939
3940                    #stream_setup
3941
3942                    let mut headers = HeaderMap::new();
3943                    #(#header_setup)*
3944
3945                    #url_construction
3946                    debug!("Making streaming POST request to: {}", url);
3947
3948                    let request_builder = self.http_client
3949                        .post(&url)
3950                        .headers(headers)
3951                        .json(&streaming_request);
3952
3953                    debug!("Creating SSE stream from request");
3954                    let stream = parse_sse_stream::<#event_type>(request_builder).await?;
3955                    info!("SSE stream created successfully");
3956                    Ok(Box::pin(stream))
3957                }
3958            }
3959        })
3960    }
3961
3962    /// Generate SSE parsing utilities using reqwest-eventsource
3963    fn generate_sse_parser_utilities(
3964        &self,
3965        _streaming_config: &crate::streaming::StreamingConfig,
3966    ) -> Result<TokenStream> {
3967        Ok(quote! {
3968            /// Parse SSE stream from HTTP request using reqwest-eventsource
3969            pub async fn parse_sse_stream<T>(
3970                request_builder: reqwest::RequestBuilder
3971            ) -> Result<impl Stream<Item = Result<T, StreamingError>>, StreamingError>
3972            where
3973                T: serde::de::DeserializeOwned + Send + 'static,
3974            {
3975                let mut event_source = reqwest_eventsource::EventSource::new(request_builder).map_err(|e| {
3976                    StreamingError::Connection(format!("Failed to create event source: {}", e))
3977                })?;
3978
3979                let stream = event_source.filter_map(|event_result| async move {
3980                    match event_result {
3981                        Ok(reqwest_eventsource::Event::Open) => {
3982                            debug!("SSE connection opened");
3983                            None
3984                        }
3985                        Ok(reqwest_eventsource::Event::Message(message)) => {
3986                            // Check if this is a ping event by SSE event type
3987                            if message.event == "ping" {
3988                                debug!("Received SSE ping event, skipping");
3989                                return None;
3990                            }
3991
3992                            // Special handling for empty data
3993                            if message.data.trim().is_empty() {
3994                                debug!("Empty SSE data, skipping");
3995                                return None;
3996                            }
3997
3998                            // Check if this is a ping event in the JSON data
3999                            if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&message.data) {
4000                                if let Some(event_type) = json_value.get("event").and_then(|v| v.as_str()) {
4001                                    if event_type == "ping" {
4002                                        debug!("Received ping event in JSON data, skipping");
4003                                        return None;
4004                                    }
4005                                }
4006
4007                                // Try to parse the full event normally
4008                                match serde_json::from_value::<T>(json_value) {
4009                                    Ok(parsed_event) => {
4010                                        Some(Ok(parsed_event))
4011                                    }
4012                                    Err(e) => {
4013                                        if message.data.contains("ping") || message.event.contains("ping") {
4014                                            debug!("Ignoring ping-related event: {}", message.data);
4015                                            None
4016                                        } else {
4017                                            Some(Err(StreamingError::Parsing(
4018                                                format!("Failed to parse SSE event: {} (raw: {})", e, message.data)
4019                                            )))
4020                                        }
4021                                    }
4022                                }
4023                            } else {
4024                                // Not valid JSON at all
4025                                Some(Err(StreamingError::Parsing(
4026                                    format!("SSE event is not valid JSON: {}", message.data)
4027                                )))
4028                            }
4029                        }
4030                        Err(e) => {
4031                            // Check if this is a normal stream end vs actual error
4032                            match e {
4033                                reqwest_eventsource::Error::StreamEnded => {
4034                                    debug!("SSE stream completed normally");
4035                                    None // Normal stream end, not an error
4036                                }
4037                                reqwest_eventsource::Error::InvalidStatusCode(status, response) => {
4038                                    // We have access to the response body for error details
4039                                    let status_code = status.as_u16();
4040
4041                                    // Read the response body to get error details
4042                                    let error_body = match response.text().await {
4043                                        Ok(body) => body,
4044                                        Err(_) => "Failed to read error response body".to_string()
4045                                    };
4046
4047                                    error!("SSE connection error - HTTP {}: {}", status_code, error_body);
4048
4049                                    let detailed_error = format!(
4050                                        "HTTP {} error: {}",
4051                                        status_code,
4052                                        error_body
4053                                    );
4054
4055                                    Some(Err(StreamingError::Connection(detailed_error)))
4056                                }
4057                                _ => {
4058                                    let error_str = e.to_string();
4059                                    if error_str.contains("stream closed") {
4060                                        debug!("SSE stream closed");
4061                                        None
4062                                    } else {
4063                                        error!("SSE connection error: {}", e);
4064                                        Some(Err(StreamingError::Connection(error_str)))
4065                                    }
4066                                }
4067                            }
4068                        }
4069                    }
4070                });
4071
4072                Ok(stream)
4073            }
4074        })
4075    }
4076
4077    /// Generate reconnection utilities
4078    fn generate_reconnection_utilities(
4079        &self,
4080        reconnect_config: &crate::streaming::ReconnectionConfig,
4081    ) -> Result<TokenStream> {
4082        let max_retries = reconnect_config.max_retries;
4083        let initial_delay = reconnect_config.initial_delay_ms;
4084        let max_delay = reconnect_config.max_delay_ms;
4085        let backoff_multiplier = reconnect_config.backoff_multiplier;
4086
4087        Ok(quote! {
4088            /// Reconnection configuration and utilities
4089            #[derive(Debug, Clone)]
4090            pub struct ReconnectionManager {
4091                max_retries: u32,
4092                initial_delay_ms: u64,
4093                max_delay_ms: u64,
4094                backoff_multiplier: f64,
4095                current_attempt: u32,
4096            }
4097
4098            impl ReconnectionManager {
4099                /// Create a new reconnection manager
4100                pub fn new() -> Self {
4101                    Self {
4102                        max_retries: #max_retries,
4103                        initial_delay_ms: #initial_delay,
4104                        max_delay_ms: #max_delay,
4105                        backoff_multiplier: #backoff_multiplier,
4106                        current_attempt: 0,
4107                    }
4108                }
4109
4110                /// Check if we should retry the connection
4111                pub fn should_retry(&self) -> bool {
4112                    self.current_attempt < self.max_retries
4113                }
4114
4115                /// Get the delay for the next retry attempt
4116                pub fn next_retry_delay(&mut self) -> Duration {
4117                    if !self.should_retry() {
4118                        return Duration::from_secs(0);
4119                    }
4120
4121                    let delay_ms = (self.initial_delay_ms as f64
4122                        * self.backoff_multiplier.powi(self.current_attempt as i32)) as u64;
4123                    let delay_ms = delay_ms.min(self.max_delay_ms);
4124
4125                    self.current_attempt += 1;
4126                    Duration::from_millis(delay_ms)
4127                }
4128
4129                /// Reset the retry counter after a successful connection
4130                pub fn reset(&mut self) {
4131                    self.current_attempt = 0;
4132                }
4133
4134                /// Get the current attempt number
4135                pub fn current_attempt(&self) -> u32 {
4136                    self.current_attempt
4137                }
4138            }
4139
4140            impl Default for ReconnectionManager {
4141                fn default() -> Self {
4142                    Self::new()
4143                }
4144            }
4145        })
4146    }
4147}