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