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