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