Skip to main content

openapi_to_rust/
generator.rs

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