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