Skip to main content

openapi_to_rust/
analysis.rs

1use crate::openapi::{Discriminator, OpenApiSpec, Schema, SchemaType as OpenApiSchemaType};
2use crate::{GeneratorError, Result};
3use serde_json::Value;
4use std::collections::{BTreeMap, HashSet};
5use std::path::Path;
6
7#[derive(Debug, Clone)]
8pub struct SchemaAnalysis {
9    /// All schemas indexed by name
10    pub schemas: BTreeMap<String, AnalyzedSchema>,
11    /// Dependency graph for generation ordering
12    pub dependencies: DependencyGraph,
13    /// Detected patterns and transformations
14    pub patterns: DetectedPatterns,
15    /// OpenAPI operations and their request/response schemas
16    pub operations: BTreeMap<String, OperationInfo>,
17}
18
19#[derive(Debug, Clone)]
20pub struct AnalyzedSchema {
21    pub name: String,
22    pub original: Value,
23    pub schema_type: SchemaType,
24    pub dependencies: HashSet<String>,
25    pub nullable: bool,
26    pub description: Option<String>,
27    pub default: Option<serde_json::Value>,
28}
29
30#[derive(Debug, Clone)]
31pub enum SchemaType {
32    /// Simple primitive type
33    Primitive { rust_type: String },
34    /// Object with properties
35    Object {
36        properties: BTreeMap<String, PropertyInfo>,
37        required: HashSet<String>,
38        additional_properties: bool,
39    },
40    /// Discriminated union (oneOf + discriminator)
41    DiscriminatedUnion {
42        discriminator_field: String,
43        variants: Vec<UnionVariant>,
44    },
45    /// Simple union (anyOf without discriminator)
46    Union { variants: Vec<SchemaRef> },
47    /// Array type
48    Array { item_type: Box<SchemaType> },
49    /// String enum
50    StringEnum { values: Vec<String> },
51    /// Extensible enum with known values and custom variant
52    ExtensibleEnum { known_values: Vec<String> },
53    /// Schema composition (allOf)
54    Composition { schemas: Vec<SchemaRef> },
55    /// Reference to another schema
56    Reference { target: String },
57}
58
59#[derive(Debug, Clone)]
60pub struct PropertyInfo {
61    pub schema_type: SchemaType,
62    pub nullable: bool,
63    pub description: Option<String>,
64    pub default: Option<serde_json::Value>,
65    pub serde_attrs: Vec<String>,
66}
67
68#[derive(Debug, Clone)]
69pub struct UnionVariant {
70    pub rust_name: String,
71    pub type_name: String,
72    pub discriminator_value: String,
73    pub schema_ref: String,
74}
75
76#[derive(Debug, Clone)]
77pub struct SchemaRef {
78    pub target: String,
79    pub nullable: bool,
80}
81
82#[derive(Debug, Clone)]
83pub struct DependencyGraph {
84    pub edges: BTreeMap<String, HashSet<String>>,
85    /// Set of schemas that have recursive dependencies
86    pub recursive_schemas: HashSet<String>,
87}
88
89#[derive(Debug, Clone)]
90pub struct DetectedPatterns {
91    /// Schemas that should use tagged enums (discriminated unions)
92    pub tagged_enum_schemas: HashSet<String>,
93    /// Schemas that should use untagged enums (simple unions)  
94    pub untagged_enum_schemas: HashSet<String>,
95    /// Auto-detected type mappings for discriminated unions
96    pub type_mappings: BTreeMap<String, BTreeMap<String, String>>,
97}
98
99/// Information about an OpenAPI operation
100#[derive(Debug, Clone, serde::Serialize)]
101pub struct OperationInfo {
102    /// Operation ID
103    pub operation_id: String,
104    /// HTTP method (GET, POST, etc.)
105    pub method: String,
106    /// Path template
107    pub path: String,
108    /// Short summary from OpenAPI spec
109    pub summary: Option<String>,
110    /// Longer description from OpenAPI spec
111    pub description: Option<String>,
112    /// Request body content type and schema (if any)
113    pub request_body: Option<RequestBodyContent>,
114    /// Response schemas by status code
115    pub response_schemas: BTreeMap<String, String>,
116    /// Parameters (path, query, header)
117    pub parameters: Vec<ParameterInfo>,
118    /// Whether this operation supports streaming
119    pub supports_streaming: bool,
120    /// Stream parameter name if applicable
121    pub stream_parameter: Option<String>,
122}
123
124/// Content type and schema for a request body
125#[derive(Debug, Clone, serde::Serialize)]
126#[serde(tag = "kind")]
127pub enum RequestBodyContent {
128    Json { schema_name: String },
129    FormUrlEncoded { schema_name: String },
130    Multipart,
131    OctetStream,
132    TextPlain,
133}
134
135impl RequestBodyContent {
136    /// Get the schema name if this content type has one
137    pub fn schema_name(&self) -> Option<&str> {
138        match self {
139            Self::Json { schema_name } | Self::FormUrlEncoded { schema_name } => Some(schema_name),
140            _ => None,
141        }
142    }
143}
144
145/// Information about an operation parameter
146#[derive(Debug, Clone, serde::Serialize)]
147pub struct ParameterInfo {
148    /// Parameter name
149    pub name: String,
150    /// Parameter location (path, query, header, cookie)
151    pub location: String,
152    /// Whether the parameter is required
153    pub required: bool,
154    /// Schema reference for the parameter type
155    pub schema_ref: Option<String>,
156    /// Rust type for this parameter
157    pub rust_type: String,
158    /// Description from OpenAPI spec
159    pub description: Option<String>,
160    /// String enum values when the parameter's inline schema is a string with
161    /// `enum` or `const`. When set, `rust_type` is the synthetic enum type
162    /// name (e.g. `GetItemTheConstant`) and the client generator emits an
163    /// inline enum so the parameter is constrained to the declared values.
164    /// See issue #10 follow-up.
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub enum_values: Option<Vec<String>>,
167}
168
169impl Default for DependencyGraph {
170    fn default() -> Self {
171        Self::new()
172    }
173}
174
175impl DependencyGraph {
176    pub fn new() -> Self {
177        Self {
178            edges: BTreeMap::new(),
179            recursive_schemas: HashSet::new(),
180        }
181    }
182
183    pub fn add_dependency(&mut self, from: String, to: String) {
184        self.edges.entry(from).or_default().insert(to);
185    }
186
187    /// Get topological sort order for generation
188    pub fn topological_sort(&mut self) -> Result<Vec<String>> {
189        // First, detect and handle recursive dependencies
190        self.detect_recursive_schemas();
191
192        // Create a temporary graph without self-referencing edges for sorting
193        let mut temp_edges = self.edges.clone();
194        for (schema, deps) in &mut temp_edges {
195            deps.remove(schema); // Remove self-references
196        }
197
198        let mut visited = HashSet::new();
199        let mut temp_visited = HashSet::new();
200        let mut result = Vec::new();
201
202        // Visit all nodes using the temporary graph in sorted order for deterministic output
203        let mut all_nodes: Vec<_> = temp_edges.keys().collect();
204        all_nodes.sort();
205        for node in all_nodes {
206            if !visited.contains(node) {
207                self.visit_node_recursive(
208                    node,
209                    &temp_edges,
210                    &mut visited,
211                    &mut temp_visited,
212                    &mut result,
213                )?;
214            }
215        }
216
217        result.reverse();
218        Ok(result)
219    }
220
221    fn detect_recursive_schemas(&mut self) {
222        for (schema, deps) in &self.edges {
223            if deps.contains(schema) {
224                // Direct self-reference
225                self.recursive_schemas.insert(schema.clone());
226            } else {
227                // Check for indirect cycles
228                if self.has_cycle_from(schema, schema, &mut HashSet::new()) {
229                    self.recursive_schemas.insert(schema.clone());
230                }
231            }
232        }
233
234        // Also detect mutual recursion (like GraphNode <-> GraphEdge)
235        for (schema, deps) in &self.edges {
236            for dep in deps {
237                if let Some(dep_deps) = self.edges.get(dep) {
238                    if dep_deps.contains(schema) {
239                        // Mutual recursion detected
240                        self.recursive_schemas.insert(schema.clone());
241                        self.recursive_schemas.insert(dep.clone());
242                    }
243                }
244            }
245        }
246    }
247
248    fn has_cycle_from(&self, start: &str, current: &str, visited: &mut HashSet<String>) -> bool {
249        if visited.contains(current) {
250            return false; // Already checked this path
251        }
252
253        visited.insert(current.to_string());
254
255        if let Some(deps) = self.edges.get(current) {
256            for dep in deps {
257                if dep == start {
258                    return true; // Found cycle back to start
259                }
260                if self.has_cycle_from(start, dep, visited) {
261                    return true;
262                }
263            }
264        }
265
266        false
267    }
268
269    #[allow(clippy::only_used_in_recursion)]
270    fn visit_node_recursive(
271        &self,
272        node: &str,
273        temp_edges: &BTreeMap<String, HashSet<String>>,
274        visited: &mut HashSet<String>,
275        temp_visited: &mut HashSet<String>,
276        result: &mut Vec<String>,
277    ) -> Result<()> {
278        if temp_visited.contains(node) {
279            // This should not happen with cycle-free temp graph, but just in case
280            return Ok(());
281        }
282
283        if visited.contains(node) {
284            return Ok(());
285        }
286
287        temp_visited.insert(node.to_string());
288
289        if let Some(dependencies) = temp_edges.get(node) {
290            // Sort dependencies for deterministic topological order
291            let mut sorted_deps: Vec<_> = dependencies.iter().collect();
292            sorted_deps.sort();
293            for dep in sorted_deps {
294                self.visit_node_recursive(dep, temp_edges, visited, temp_visited, result)?;
295            }
296        }
297
298        temp_visited.remove(node);
299        visited.insert(node.to_string());
300        result.push(node.to_string());
301
302        Ok(())
303    }
304}
305
306/// Merge schema extension files into the main OpenAPI specification
307/// Uses simple recursive JSON object merging
308pub fn merge_schema_extensions(
309    main_spec: Value,
310    extension_paths: &[impl AsRef<Path>],
311) -> Result<Value> {
312    let mut result = main_spec;
313
314    for path in extension_paths {
315        let extension = load_extension_file(path.as_ref())?;
316        result = merge_json_objects_with_replacements(result, extension)?;
317    }
318
319    Ok(result)
320}
321
322/// Load an extension file and parse as JSON
323fn load_extension_file(path: &Path) -> Result<Value> {
324    let content = std::fs::read_to_string(path).map_err(|e| GeneratorError::FileError {
325        message: format!("Failed to read file {}: {}", path.display(), e),
326    })?;
327
328    serde_json::from_str(&content).map_err(GeneratorError::ParseError)
329}
330
331/// Merge JSON objects with explicit replacement support
332fn merge_json_objects_with_replacements(main: Value, extension: Value) -> Result<Value> {
333    // Extract replacement rules from the extension
334    let replacements = extract_replacement_rules(&extension);
335
336    // Perform the merge with replacement awareness
337    Ok(merge_json_objects_with_rules(
338        main,
339        extension,
340        &replacements,
341    ))
342}
343
344/// Extract x-replacements rules from extension
345fn extract_replacement_rules(
346    extension: &Value,
347) -> std::collections::HashMap<String, (String, String)> {
348    let mut rules = std::collections::HashMap::new();
349
350    if let Some(x_replacements) = extension.get("x-replacements") {
351        if let Some(x_replacements_obj) = x_replacements.as_object() {
352            for (schema_name, replacement_rule) in x_replacements_obj {
353                if let Some(rule_obj) = replacement_rule.as_object() {
354                    if let (Some(replace), Some(with)) = (
355                        rule_obj.get("replace").and_then(|v| v.as_str()),
356                        rule_obj.get("with").and_then(|v| v.as_str()),
357                    ) {
358                        rules.insert(schema_name.clone(), (replace.to_string(), with.to_string()));
359                        // println!("📋 Replacement rule: In {}, replace {} with {}", schema_name, replace, with);
360                    }
361                }
362            }
363        }
364    }
365
366    rules
367}
368
369/// Check if a variant should be replaced based on explicit replacement rules
370fn should_replace_variant(
371    schema_name: &str,
372    extension_refs: &[String],
373    replacements: &std::collections::HashMap<String, (String, String)>,
374) -> bool {
375    // Check all replacement rules
376    for (replace_schema, with_schema) in replacements.values() {
377        if schema_name == replace_schema {
378            // This schema should be replaced - check if the replacement schema is in extensions
379            let replacement_exists = extension_refs.iter().any(|ext_ref| {
380                let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
381                ext_schema_name == with_schema
382            });
383
384            if replacement_exists {
385                return true;
386            }
387        }
388    }
389
390    // Fallback to exact name match for complete replacement
391    extension_refs.iter().any(|ext_ref| {
392        let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
393        schema_name == ext_schema_name
394    })
395}
396
397/// Recursively merge two JSON values with replacement rules
398/// Objects are merged by combining properties
399/// Arrays are merged by concatenating
400/// Primitives in the extension override the main value
401fn merge_json_objects_with_rules(
402    main: Value,
403    extension: Value,
404    replacements: &std::collections::HashMap<String, (String, String)>,
405) -> Value {
406    match (main, extension) {
407        // Both objects - merge properties
408        (Value::Object(mut main_obj), Value::Object(ext_obj)) => {
409            // Special handling for schema objects with oneOf/anyOf variants.
410            // Detect which keyword the MAIN spec uses so we preserve it after merging.
411            let main_union_keyword = if main_obj.contains_key("oneOf") {
412                Some("oneOf")
413            } else if main_obj.contains_key("anyOf") {
414                Some("anyOf")
415            } else {
416                None
417            };
418            if let (Some(main_variants), Some(ext_variants)) = (
419                extract_schema_variants(&Value::Object(main_obj.clone())),
420                extract_schema_variants(&Value::Object(ext_obj.clone())),
421            ) {
422                let union_key = main_union_keyword.unwrap_or("oneOf");
423                println!(
424                    "🔍 Merging union schemas ({union_key}): {} main variants, {} extension variants",
425                    main_variants.len(),
426                    ext_variants.len()
427                );
428                // Merge the variant arrays, preserving the original union keyword
429                // First, collect main variants, but filter out any that will be replaced by extension
430                let mut merged_variants = Vec::new();
431                let extension_refs: Vec<String> = ext_variants
432                    .iter()
433                    .filter_map(|v| v.get("$ref").and_then(|r| r.as_str()))
434                    .map(|s| s.to_string())
435                    .collect();
436
437                // Add main variants that aren't being replaced
438                for main_variant in main_variants {
439                    if let Some(main_ref) = main_variant.get("$ref").and_then(|r| r.as_str()) {
440                        // Check if this main variant should be replaced by an extension variant
441                        let schema_name = main_ref.split('/').next_back().unwrap_or("");
442                        let should_replace =
443                            should_replace_variant(schema_name, &extension_refs, replacements);
444
445                        if should_replace {
446                            println!("🔄 REPLACING {} (explicit rule)", schema_name);
447                        }
448
449                        if !should_replace {
450                            merged_variants.push(main_variant);
451                        }
452                    } else {
453                        // Keep non-ref variants
454                        merged_variants.push(main_variant);
455                    }
456                }
457
458                // Add all extension variants
459                for ext_variant in ext_variants {
460                    merged_variants.push(ext_variant);
461                }
462
463                // Remove old oneOf/anyOf keys and add merged variants under the original keyword
464                main_obj.remove("oneOf");
465                main_obj.remove("anyOf");
466                main_obj.insert(union_key.to_string(), Value::Array(merged_variants));
467
468                // Merge other properties normally
469                for (key, ext_value) in ext_obj {
470                    if key != "oneOf" && key != "anyOf" {
471                        match main_obj.get(&key) {
472                            Some(main_value) => {
473                                let merged_value = merge_json_objects_with_rules(
474                                    main_value.clone(),
475                                    ext_value,
476                                    replacements,
477                                );
478                                main_obj.insert(key, merged_value);
479                            }
480                            None => {
481                                main_obj.insert(key, ext_value);
482                            }
483                        }
484                    }
485                }
486
487                return Value::Object(main_obj);
488            }
489
490            // Normal object merging
491            for (key, ext_value) in ext_obj {
492                match main_obj.get(&key) {
493                    Some(main_value) => {
494                        // Key exists in both - recursively merge
495                        let merged_value = merge_json_objects_with_rules(
496                            main_value.clone(),
497                            ext_value,
498                            replacements,
499                        );
500                        main_obj.insert(key, merged_value);
501                    }
502                    None => {
503                        // Key only in extension - add it
504                        main_obj.insert(key, ext_value);
505                    }
506                }
507            }
508            Value::Object(main_obj)
509        }
510
511        // Both arrays - concatenate
512        (Value::Array(mut main_arr), Value::Array(ext_arr)) => {
513            main_arr.extend(ext_arr);
514            Value::Array(main_arr)
515        }
516
517        // Extension overrides main for all other cases
518        (_, extension) => extension,
519    }
520}
521
522/// Extract schema variants from oneOf or anyOf properties
523fn extract_schema_variants(obj: &Value) -> Option<Vec<Value>> {
524    if let Value::Object(map) = obj {
525        if let Some(Value::Array(variants)) = map.get("oneOf") {
526            return Some(variants.clone());
527        }
528        if let Some(Value::Array(variants)) = map.get("anyOf") {
529            return Some(variants.clone());
530        }
531    }
532    None
533}
534
535pub struct SchemaAnalyzer {
536    schemas: BTreeMap<String, Schema>,
537    resolved_cache: BTreeMap<String, AnalyzedSchema>,
538    openapi_spec: Value,
539    current_schema_name: Option<String>,
540    component_parameters: BTreeMap<String, crate::openapi::Parameter>,
541}
542
543impl SchemaAnalyzer {
544    pub fn new(openapi_spec: Value) -> Result<Self> {
545        let spec: OpenApiSpec =
546            serde_json::from_value(openapi_spec.clone()).map_err(GeneratorError::ParseError)?;
547        let schemas = Self::extract_schemas(&spec)?;
548
549        let component_parameters = spec
550            .components
551            .as_ref()
552            .and_then(|c| c.parameters.as_ref())
553            .cloned()
554            .unwrap_or_default();
555
556        Ok(Self {
557            schemas,
558            resolved_cache: BTreeMap::new(),
559            openapi_spec,
560            current_schema_name: None,
561            component_parameters,
562        })
563    }
564
565    /// Create a new analyzer with schema extensions merged in
566    pub fn new_with_extensions(
567        openapi_spec: Value,
568        extension_paths: &[std::path::PathBuf],
569    ) -> Result<Self> {
570        let merged_spec = merge_schema_extensions(openapi_spec, extension_paths)?;
571        Self::new(merged_spec)
572    }
573
574    /// Generate a context-aware name for inline types, arrays, and variants
575    /// This provides better naming than generic names like UnionArray1, InlineVariant2, etc.
576    fn generate_context_aware_name(
577        &self,
578        base_context: &str,
579        type_hint: &str,
580        index: usize,
581        schema: Option<&Schema>,
582    ) -> String {
583        // First, try to infer a better name from the schema structure
584        if let Some(schema) = schema {
585            // For arrays, check if we can derive name from items
586            if type_hint == "Array"
587                && matches!(schema.schema_type(), Some(OpenApiSchemaType::Array))
588            {
589                if let Some(items_schema) = &schema.details().items {
590                    // Check for specific item types
591                    if let Some(item_type) = items_schema.schema_type() {
592                        match item_type {
593                            OpenApiSchemaType::Object => {
594                                return format!("{base_context}ItemArray");
595                            }
596                            OpenApiSchemaType::String => {
597                                return format!("{base_context}StringArray");
598                            }
599                            _ => {}
600                        }
601                    }
602                }
603            }
604        }
605
606        // Generate context-aware name based on type hint
607        match type_hint {
608            "Array" => {
609                // For arrays, always use context name instead of generic numbering
610                format!("{base_context}Array")
611            }
612            "Variant" | "InlineVariant" => {
613                // For variants, include index only if > 0 to keep first variant clean
614                if index == 0 {
615                    format!("{base_context}{type_hint}")
616                } else {
617                    format!("{}{}{}", base_context, type_hint, index + 1)
618                }
619            }
620            _ => {
621                // Default case
622                format!("{base_context}{type_hint}{index}")
623            }
624        }
625    }
626
627    /// Convert a string to PascalCase, handling underscores and hyphens
628    fn to_pascal_case(&self, s: &str) -> String {
629        s.split(['_', '-'])
630            .filter(|part| !part.is_empty())
631            .map(|part| {
632                let mut chars = part.chars();
633                match chars.next() {
634                    None => String::new(),
635                    Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
636                }
637            })
638            .collect()
639    }
640
641    fn extract_schemas(spec: &OpenApiSpec) -> Result<BTreeMap<String, Schema>> {
642        let schemas = spec
643            .components
644            .as_ref()
645            .and_then(|c| c.schemas.as_ref())
646            .ok_or_else(|| {
647                GeneratorError::InvalidSchema("No schemas found in OpenAPI spec".to_string())
648            })?;
649
650        // Convert BTreeMap to BTreeMap for deterministic iteration order
651        Ok(schemas
652            .iter()
653            .map(|(k, v)| (k.clone(), v.clone()))
654            .collect())
655    }
656
657    pub fn analyze(&mut self) -> Result<SchemaAnalysis> {
658        let mut analysis = SchemaAnalysis {
659            schemas: BTreeMap::new(),
660            dependencies: DependencyGraph::new(),
661            patterns: DetectedPatterns {
662                tagged_enum_schemas: HashSet::new(),
663                untagged_enum_schemas: HashSet::new(),
664                type_mappings: BTreeMap::new(),
665            },
666            operations: BTreeMap::new(),
667        };
668
669        // First pass: detect patterns
670        self.detect_patterns(&mut analysis.patterns)?;
671
672        // Second pass: analyze each schema
673        let schema_names: Vec<String> = self.schemas.keys().cloned().collect();
674        for schema_name in schema_names {
675            let analyzed = self.analyze_schema(&schema_name)?;
676
677            // Build dependency graph
678            for dep in &analyzed.dependencies {
679                analysis
680                    .dependencies
681                    .add_dependency(schema_name.clone(), dep.clone());
682            }
683
684            analysis.schemas.insert(schema_name, analyzed);
685        }
686
687        // Third pass: include any inline schemas that were generated during analysis
688        // BTreeMap maintains sorted order, so iteration is deterministic
689        for (inline_name, inline_schema) in &self.resolved_cache {
690            if !analysis.schemas.contains_key(inline_name) {
691                // Add the inline schema first
692                analysis
693                    .schemas
694                    .insert(inline_name.clone(), inline_schema.clone());
695
696                // Build dependency graph for inline schema's own dependencies
697                for dep in &inline_schema.dependencies {
698                    analysis
699                        .dependencies
700                        .add_dependency(inline_name.clone(), dep.clone());
701                }
702
703                // Check if any existing schemas depend on this inline schema
704                // We need to check ALL schemas, not just the ones already in analysis.schemas,
705                // because parent schemas might have been analyzed but their dependencies
706                // on inline schemas might not have been added to the dependency graph yet
707                let mut schemas_to_update = Vec::new();
708                for (schema_name, schema) in &analysis.schemas {
709                    // Skip self-reference
710                    if schema_name == inline_name {
711                        continue;
712                    }
713
714                    if schema.dependencies.contains(inline_name) {
715                        // The parent schema depends on this inline schema
716                        schemas_to_update.push(schema_name.clone());
717                    }
718                }
719
720                // Add the dependencies to the graph
721                for schema_name in schemas_to_update {
722                    analysis
723                        .dependencies
724                        .add_dependency(schema_name, inline_name.clone());
725                }
726            }
727        }
728
729        // Fourth pass: analyze OpenAPI operations
730        self.analyze_operations(&mut analysis)?;
731
732        // Fifth pass: include any inline schemas generated during operation analysis
733        // (e.g., inline response types)
734        for (inline_name, inline_schema) in &self.resolved_cache {
735            if !analysis.schemas.contains_key(inline_name) {
736                analysis
737                    .schemas
738                    .insert(inline_name.clone(), inline_schema.clone());
739
740                // Build dependency graph for inline schema's dependencies
741                for dep in &inline_schema.dependencies {
742                    analysis
743                        .dependencies
744                        .add_dependency(inline_name.clone(), dep.clone());
745                }
746            }
747        }
748
749        Ok(analysis)
750    }
751
752    fn detect_patterns(&self, patterns: &mut DetectedPatterns) -> Result<()> {
753        for (schema_name, schema) in &self.schemas {
754            // Detect discriminated unions
755            if self.is_discriminated_union(schema) {
756                patterns.tagged_enum_schemas.insert(schema_name.clone());
757
758                // Extract type mappings for this union
759                if let Some(mappings) = self.extract_type_mappings(schema)? {
760                    patterns.type_mappings.insert(schema_name.clone(), mappings);
761                }
762            }
763            // Detect simple unions
764            else if self.is_simple_union(schema) {
765                patterns.untagged_enum_schemas.insert(schema_name.clone());
766            }
767        }
768
769        Ok(())
770    }
771
772    fn is_discriminated_union(&self, schema: &Schema) -> bool {
773        // Check for explicit discriminator
774        if schema.is_discriminated_union() {
775            return true;
776        }
777
778        // Auto-detect from union patterns with any common const field
779        if let Some(variants) = schema.union_variants() {
780            return variants.len() > 2 && self.detect_discriminator_field(variants).is_some();
781        }
782
783        false
784    }
785
786    fn all_variants_have_const_field(&self, variants: &[Schema], field_name: &str) -> bool {
787        variants.iter().all(|variant| {
788            if let Some(ref_str) = variant.reference() {
789                // $ref variant: resolve and check the referenced schema
790                if let Some(schema_name) = self.extract_schema_name(ref_str) {
791                    if let Some(schema) = self.schemas.get(schema_name) {
792                        return self.has_const_discriminator_field(schema, field_name);
793                    }
794                }
795            } else {
796                // Inline variant: check properties directly
797                return self.has_const_discriminator_field(variant, field_name);
798            }
799            false
800        })
801    }
802
803    /// Scan all variants to find any common property that has a const/single-enum value
804    /// across all variants. Returns the field name if found.
805    /// Prioritizes "type" if it matches (most common convention).
806    fn detect_discriminator_field(&self, variants: &[Schema]) -> Option<String> {
807        if variants.is_empty() {
808            return None;
809        }
810
811        // Collect candidate field names from the first variant
812        let first_variant = &variants[0];
813        let first_schema = if let Some(ref_str) = first_variant.reference() {
814            let schema_name = self.extract_schema_name(ref_str)?;
815            self.schemas.get(schema_name)?
816        } else {
817            first_variant
818        };
819
820        let properties = first_schema.details().properties.as_ref()?;
821        let mut candidates: Vec<String> = Vec::new();
822
823        for (field_name, field_schema) in properties {
824            let details = field_schema.details();
825            let is_const = details.const_value.is_some()
826                || details.enum_values.as_ref().is_some_and(|v| v.len() == 1)
827                || details.extra.contains_key("const");
828            if is_const {
829                candidates.push(field_name.clone());
830            }
831        }
832
833        if candidates.is_empty() {
834            return None;
835        }
836
837        // Prioritize "type" if it's among candidates
838        candidates.sort_by(|a, b| {
839            if a == "type" {
840                std::cmp::Ordering::Less
841            } else if b == "type" {
842                std::cmp::Ordering::Greater
843            } else {
844                a.cmp(b)
845            }
846        });
847
848        // Check each candidate against all variants
849        for candidate in &candidates {
850            if self.all_variants_have_const_field(variants, candidate) {
851                return Some(candidate.clone());
852            }
853        }
854
855        None
856    }
857
858    fn has_const_discriminator_field(&self, schema: &Schema, field_name: &str) -> bool {
859        if let Some(properties) = &schema.details().properties {
860            if let Some(field) = properties.get(field_name) {
861                // Check for const value (OpenAPI 3.1 style)
862                if field.details().const_value.is_some() {
863                    return true;
864                }
865                // Check if it's an enum field with a single value
866                if let Some(enum_vals) = &field.details().enum_values {
867                    return enum_vals.len() == 1;
868                }
869                // Fallback: check extra fields for const
870                return field.details().extra.contains_key("const");
871            }
872        }
873        false
874    }
875
876    fn is_simple_union(&self, schema: &Schema) -> bool {
877        if let Some(variants) = schema.union_variants() {
878            // Simple union: multiple types but not nullable pattern
879            if variants.len() > 1 && !schema.is_nullable_pattern() {
880                let has_refs = variants.iter().any(|v| v.is_reference());
881                return has_refs;
882            }
883        }
884        false
885    }
886
887    fn extract_type_mappings(&self, schema: &Schema) -> Result<Option<BTreeMap<String, String>>> {
888        let variants = schema.union_variants().ok_or_else(|| {
889            GeneratorError::InvalidSchema("No variants found for discriminated union".to_string())
890        })?;
891
892        // Get the discriminator field name from the schema
893        let discriminator_field = if let Some(discriminator) = schema.discriminator() {
894            discriminator.property_name.clone()
895        } else if let Some(detected) = self.detect_discriminator_field(variants) {
896            detected
897        } else {
898            "type".to_string() // fallback to "type" for auto-detected discriminated unions
899        };
900
901        let mut mappings = BTreeMap::new();
902
903        for variant in variants {
904            if let Some(ref_str) = variant.reference() {
905                if let Some(type_name) = self.extract_schema_name(ref_str) {
906                    if let Some(variant_schema) = self.schemas.get(type_name) {
907                        if let Some(discriminator_value) = self
908                            .extract_discriminator_value_for_field(
909                                variant_schema,
910                                &discriminator_field,
911                            )
912                        {
913                            mappings.insert(type_name.to_string(), discriminator_value);
914                        }
915                    }
916                }
917            }
918        }
919
920        if mappings.is_empty() {
921            Ok(None)
922        } else {
923            Ok(Some(mappings))
924        }
925    }
926
927    #[allow(dead_code)]
928    fn extract_discriminator_value(&self, schema: &Schema) -> Option<String> {
929        self.extract_discriminator_value_for_field(schema, "type")
930    }
931
932    fn extract_discriminator_value_for_field(
933        &self,
934        schema: &Schema,
935        field_name: &str,
936    ) -> Option<String> {
937        if let Some(properties) = &schema.details().properties {
938            if let Some(type_field) = properties.get(field_name) {
939                // Check for const value first (highest priority)
940                if let Some(const_value) = &type_field.details().const_value {
941                    if let Some(value) = const_value.as_str() {
942                        return Some(value.to_string());
943                    }
944                }
945                // Check for enum with single value
946                if let Some(enum_values) = &type_field.details().enum_values {
947                    if enum_values.len() == 1 {
948                        return enum_values[0].as_str().map(|s| s.to_string());
949                    }
950                }
951                // Check for const value in extra fields
952                if let Some(const_value) = type_field.details().extra.get("const") {
953                    return const_value.as_str().map(|s| s.to_string());
954                }
955                // Check for x-stainless-const with default value
956                if let Some(stainless_const) = type_field.details().extra.get("x-stainless-const") {
957                    if stainless_const.as_bool() == Some(true) {
958                        if let Some(default_value) = &type_field.details().default {
959                            if let Some(value) = default_value.as_str() {
960                                return Some(value.to_string());
961                            }
962                        }
963                    }
964                }
965            }
966        }
967        None
968    }
969
970    fn get_any_reference<'a>(&self, schema: &'a Schema) -> Option<&'a str> {
971        schema.reference().or_else(|| schema.recursive_reference())
972    }
973
974    fn extract_schema_name<'a>(&self, ref_str: &'a str) -> Option<&'a str> {
975        if ref_str == "#" {
976            return None; // Special case for self-reference
977        }
978
979        let parts: Vec<&str> = ref_str.split('/').collect();
980
981        // Standard pattern: #/components/schemas/{SchemaName}[/deeper/path]
982        // parts[0]="#", parts[1]="components", parts[2]="schemas", parts[3]="SchemaName"
983        if parts.len() >= 4 && parts[0] == "#" && parts[2] == "schemas" {
984            return Some(parts[3]);
985        }
986
987        // Fallback for other ref patterns: use last segment,
988        // but only if it looks like a schema name (not a bare number)
989        let last = parts.last()?;
990        if last.is_empty() || last.chars().all(|c| c.is_ascii_digit()) {
991            None
992        } else {
993            Some(last)
994        }
995    }
996
997    fn analyze_schema(&mut self, schema_name: &str) -> Result<AnalyzedSchema> {
998        // Check cache first
999        if let Some(cached) = self.resolved_cache.get(schema_name) {
1000            return Ok(cached.clone());
1001        }
1002
1003        // Set current schema name for context
1004        self.current_schema_name = Some(schema_name.to_string());
1005
1006        let schema = self
1007            .schemas
1008            .get(schema_name)
1009            .ok_or_else(|| GeneratorError::UnresolvedReference(schema_name.to_string()))?
1010            .clone();
1011
1012        // Prevent infinite recursion with placeholder
1013        self.resolved_cache.insert(
1014            schema_name.to_string(),
1015            AnalyzedSchema {
1016                name: schema_name.to_string(),
1017                original: serde_json::to_value(&schema).unwrap_or(Value::Null),
1018                schema_type: SchemaType::Reference {
1019                    target: "placeholder".to_string(),
1020                },
1021                dependencies: HashSet::new(),
1022                nullable: false,
1023                description: None,
1024                default: None,
1025            },
1026        );
1027
1028        let analyzed = self.analyze_schema_value(&schema, schema_name)?;
1029
1030        // Update cache with real result
1031        self.resolved_cache
1032            .insert(schema_name.to_string(), analyzed.clone());
1033
1034        Ok(analyzed)
1035    }
1036
1037    fn analyze_schema_value(
1038        &mut self,
1039        schema: &Schema,
1040        schema_name: &str,
1041    ) -> Result<AnalyzedSchema> {
1042        let details = schema.details();
1043        let description = details.description.clone();
1044        let nullable = details.is_nullable();
1045        let mut dependencies = HashSet::new();
1046
1047        let schema_type = match schema {
1048            Schema::Reference { reference, .. } => {
1049                let target = self
1050                    .extract_schema_name(reference)
1051                    .ok_or_else(|| GeneratorError::UnresolvedReference(reference.to_string()))?
1052                    .to_string();
1053                dependencies.insert(target.clone());
1054                SchemaType::Reference { target }
1055            }
1056            Schema::RecursiveRef { recursive_ref, .. } => {
1057                // Handle recursive references
1058                if recursive_ref == "#" {
1059                    // Self-reference to the current schema
1060                    dependencies.insert(schema_name.to_string());
1061                    SchemaType::Reference {
1062                        target: schema_name.to_string(),
1063                    }
1064                } else {
1065                    // Handle other recursive reference patterns
1066                    let target = self
1067                        .extract_schema_name(recursive_ref)
1068                        .unwrap_or(schema_name)
1069                        .to_string();
1070                    dependencies.insert(target.clone());
1071                    SchemaType::Reference { target }
1072                }
1073            }
1074            Schema::Typed { schema_type, .. } => {
1075                match schema_type {
1076                    OpenApiSchemaType::String => {
1077                        if let Some(values) = details.string_enum_values() {
1078                            SchemaType::StringEnum { values }
1079                        } else {
1080                            SchemaType::Primitive {
1081                                rust_type: "String".to_string(),
1082                            }
1083                        }
1084                    }
1085                    OpenApiSchemaType::Integer => {
1086                        let rust_type =
1087                            self.get_number_rust_type(OpenApiSchemaType::Integer, details);
1088                        SchemaType::Primitive { rust_type }
1089                    }
1090                    OpenApiSchemaType::Number => {
1091                        let rust_type =
1092                            self.get_number_rust_type(OpenApiSchemaType::Number, details);
1093                        SchemaType::Primitive { rust_type }
1094                    }
1095                    OpenApiSchemaType::Boolean => SchemaType::Primitive {
1096                        rust_type: "bool".to_string(),
1097                    },
1098                    OpenApiSchemaType::Array => {
1099                        // Analyze array item type
1100                        self.analyze_array_schema(schema, schema_name, &mut dependencies)?
1101                    }
1102                    OpenApiSchemaType::Object => {
1103                        // Check if this is a dynamic JSON object
1104                        if self.should_use_dynamic_json(schema) {
1105                            SchemaType::Primitive {
1106                                rust_type: "serde_json::Value".to_string(),
1107                            }
1108                        } else {
1109                            // Analyze object properties
1110                            self.analyze_object_schema(schema, &mut dependencies)?
1111                        }
1112                    }
1113                    _ => SchemaType::Primitive {
1114                        rust_type: "serde_json::Value".to_string(),
1115                    },
1116                }
1117            }
1118            Schema::AnyOf {
1119                any_of,
1120                discriminator,
1121                ..
1122            } => {
1123                // Handle anyOf patterns (nullable vs flexible union vs discriminated)
1124                self.analyze_anyof_union(
1125                    any_of,
1126                    discriminator.as_ref(),
1127                    &mut dependencies,
1128                    schema_name,
1129                )?
1130            }
1131            Schema::OneOf {
1132                one_of,
1133                discriminator,
1134                ..
1135            } => {
1136                // Handle oneOf discriminated unions
1137                self.analyze_oneof_union(
1138                    one_of,
1139                    discriminator.as_ref(),
1140                    schema_name,
1141                    &mut dependencies,
1142                )?
1143            }
1144            Schema::AllOf { all_of, .. } => {
1145                // Handle allOf composition (schema inheritance)
1146                self.analyze_allof_composition(all_of, &mut dependencies)?
1147            }
1148            Schema::Untyped { .. } => {
1149                // Try to infer type from structure
1150                if let Some(inferred) = schema.inferred_type() {
1151                    match inferred {
1152                        OpenApiSchemaType::Object => {
1153                            if self.should_use_dynamic_json(schema) {
1154                                SchemaType::Primitive {
1155                                    rust_type: "serde_json::Value".to_string(),
1156                                }
1157                            } else {
1158                                self.analyze_object_schema(schema, &mut dependencies)?
1159                            }
1160                        }
1161                        OpenApiSchemaType::String if details.is_string_enum() => {
1162                            SchemaType::StringEnum {
1163                                values: details.string_enum_values().unwrap_or_default(),
1164                            }
1165                        }
1166                        _ => SchemaType::Primitive {
1167                            rust_type: "serde_json::Value".to_string(),
1168                        },
1169                    }
1170                } else {
1171                    SchemaType::Primitive {
1172                        rust_type: "serde_json::Value".to_string(),
1173                    }
1174                }
1175            }
1176        };
1177
1178        Ok(AnalyzedSchema {
1179            name: schema_name.to_string(),
1180            original: serde_json::to_value(schema).unwrap_or(Value::Null), // Convert back to Value for now
1181            schema_type,
1182            dependencies,
1183            nullable,
1184            description,
1185            default: details.default.clone(),
1186        })
1187    }
1188
1189    fn analyze_object_schema(
1190        &mut self,
1191        schema: &Schema,
1192        dependencies: &mut HashSet<String>,
1193    ) -> Result<SchemaType> {
1194        let details = schema.details();
1195        let properties = &details.properties;
1196        let required = details
1197            .required
1198            .as_ref()
1199            .map(|req| req.iter().cloned().collect::<HashSet<String>>())
1200            .unwrap_or_default();
1201
1202        let mut property_info = BTreeMap::new();
1203
1204        if let Some(props) = properties {
1205            for (prop_name, prop_schema) in props {
1206                // Check if this property is a union that needs a named type
1207                let prop_type = if let Schema::AnyOf { any_of, .. } = prop_schema {
1208                    // First check if this should be a dynamic JSON pattern
1209                    if self.should_use_dynamic_json(prop_schema) {
1210                        // This is a dynamic JSON pattern, use serde_json::Value directly
1211                        SchemaType::Primitive {
1212                            rust_type: "serde_json::Value".to_string(),
1213                        }
1214                    } else {
1215                        // This is an anyOf union in a property - create a named union type
1216                        // Use the current schema name as context to make the union name unique
1217                        let context_name = self
1218                            .current_schema_name
1219                            .clone()
1220                            .unwrap_or_else(|| "Unknown".to_string());
1221
1222                        // Generate a name based on both the schema and property name
1223                        let prop_pascal = self.to_pascal_case(prop_name);
1224                        let union_type_name = format!("{context_name}{prop_pascal}");
1225
1226                        // Analyze the union
1227                        let union_schema_type = self.analyze_anyof_union(
1228                            any_of,
1229                            prop_schema.discriminator(),
1230                            dependencies,
1231                            &union_type_name,
1232                        )?;
1233
1234                        // Store the union as a named schema
1235                        self.resolved_cache.insert(
1236                            union_type_name.clone(),
1237                            AnalyzedSchema {
1238                                name: union_type_name.clone(),
1239                                original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
1240                                schema_type: union_schema_type,
1241                                dependencies: HashSet::new(),
1242                                nullable: false,
1243                                description: prop_schema.details().description.clone(),
1244                                default: None,
1245                            },
1246                        );
1247
1248                        // Return a reference to the named union type
1249                        dependencies.insert(union_type_name.clone());
1250                        SchemaType::Reference {
1251                            target: union_type_name,
1252                        }
1253                    }
1254                } else if let Schema::OneOf {
1255                    one_of,
1256                    discriminator,
1257                    ..
1258                } = prop_schema
1259                {
1260                    // Handle oneOf discriminated unions in properties
1261                    // Generate a name based on the property name
1262                    let context_name = self
1263                        .current_schema_name
1264                        .clone()
1265                        .unwrap_or_else(|| "Unknown".to_string());
1266                    let prop_pascal = self.to_pascal_case(prop_name);
1267                    let union_type_name = format!("{context_name}{prop_pascal}");
1268
1269                    // Analyze the discriminated union
1270                    let union_schema_type = self.analyze_oneof_union(
1271                        one_of,
1272                        discriminator.as_ref(),
1273                        &union_type_name,
1274                        dependencies,
1275                    )?;
1276
1277                    // Store the union as a named schema
1278                    self.resolved_cache.insert(
1279                        union_type_name.clone(),
1280                        AnalyzedSchema {
1281                            name: union_type_name.clone(),
1282                            original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
1283                            schema_type: union_schema_type,
1284                            dependencies: HashSet::new(),
1285                            nullable: false,
1286                            description: prop_schema.details().description.clone(),
1287                            default: None,
1288                        },
1289                    );
1290
1291                    // Return a reference to the named union type
1292                    dependencies.insert(union_type_name.clone());
1293                    SchemaType::Reference {
1294                        target: union_type_name,
1295                    }
1296                } else {
1297                    // Regular property schema analysis - pass property name for context
1298                    self.analyze_property_schema_with_context(
1299                        prop_schema,
1300                        Some(prop_name),
1301                        dependencies,
1302                    )?
1303                };
1304
1305                let prop_details = prop_schema.details();
1306                // Check for both explicit nullable and anyOf nullable patterns
1307                let prop_nullable = prop_details.is_nullable() || prop_schema.is_nullable_pattern();
1308                let prop_description = prop_details.description.clone();
1309                let prop_default = prop_details.default.clone();
1310
1311                property_info.insert(
1312                    prop_name.clone(),
1313                    PropertyInfo {
1314                        schema_type: prop_type,
1315                        nullable: prop_nullable,
1316                        description: prop_description,
1317                        default: prop_default,
1318                        serde_attrs: Vec::new(),
1319                    },
1320                );
1321            }
1322        }
1323
1324        // Check additionalProperties setting
1325        let additional_properties = match &details.additional_properties {
1326            Some(crate::openapi::AdditionalProperties::Boolean(true)) => true,
1327            Some(crate::openapi::AdditionalProperties::Boolean(false)) => false,
1328            Some(crate::openapi::AdditionalProperties::Schema(_)) => {
1329                // For now, treat schema-based additionalProperties as true
1330                // TODO: Could analyze the schema to determine the value type
1331                true
1332            }
1333            None => false, // Default is false if not specified
1334        };
1335
1336        Ok(SchemaType::Object {
1337            properties: property_info,
1338            required,
1339            additional_properties,
1340        })
1341    }
1342
1343    fn analyze_property_schema_with_context(
1344        &mut self,
1345        schema: &Schema,
1346        property_name: Option<&str>,
1347        dependencies: &mut HashSet<String>,
1348    ) -> Result<SchemaType> {
1349        if let Some(ref_str) = self.get_any_reference(schema) {
1350            let target = if ref_str == "#" {
1351                // $recursiveRef: "#" - need to find the schema with $recursiveAnchor: true
1352                self.find_recursive_anchor_schema()
1353                    .unwrap_or_else(|| "UnknownRecursive".to_string())
1354            } else {
1355                self.extract_schema_name(ref_str)
1356                    .ok_or_else(|| GeneratorError::UnresolvedReference(ref_str.to_string()))?
1357                    .to_string()
1358            };
1359            dependencies.insert(target.clone());
1360            return Ok(SchemaType::Reference { target });
1361        }
1362
1363        if let Some(schema_type) = schema.schema_type() {
1364            match schema_type {
1365                OpenApiSchemaType::String => {
1366                    // Check if this string type has enum values
1367                    if let Some(enum_values) = schema.details().string_enum_values() {
1368                        // This is an inline enum in a property - create a named enum type
1369                        // Use the current schema name as context to make the enum name unique
1370                        let context_name = self
1371                            .current_schema_name
1372                            .clone()
1373                            .unwrap_or_else(|| "Unknown".to_string());
1374
1375                        // Generate a candidate name based on both the schema and property context.
1376                        let primary_name = if let Some(prop_name) = property_name {
1377                            // We have property name context - use it for a unique name
1378                            let prop_pascal = self.to_pascal_case(prop_name);
1379                            format!("{context_name}{prop_pascal}")
1380                        } else {
1381                            // No property name context - generate a unique name using enum values
1382                            // Use the first enum value to help make the name unique
1383                            let suffix = if !enum_values.is_empty() {
1384                                let first_value = self.to_pascal_case(&enum_values[0]);
1385                                format!("{first_value}Enum")
1386                            } else {
1387                                "StringEnum".to_string()
1388                            };
1389                            format!("{context_name}{suffix}")
1390                        };
1391
1392                        // Resolve a name that either matches an existing same-valued
1393                        // enum (dedup) or doesn't collide with a different one.
1394                        //
1395                        // Two distinct inline enums can land on the same primary
1396                        // candidate when a parent schema has a property like
1397                        // `type` that recurs at multiple nesting levels — e.g.
1398                        // Latitude.sh's `plan_data.type = ["plans"]` (the
1399                        // JSON-API resource type) and
1400                        // `plan_data.attributes.specs.drives[].type =
1401                        // ["SSD","HDD","NVME"]` both want to become
1402                        // `PlanDataType`. We must NOT silently overwrite the
1403                        // first registration: that breaks deserialization
1404                        // because both fields end up referencing whichever
1405                        // enum was processed last.
1406                        //
1407                        // Disambiguation strategy: append the PascalCase first
1408                        // enum value (`PlanDataTypeNVME` vs `PlanDataTypePlans`)
1409                        // and, if that's also claimed with different values,
1410                        // fall back to a numeric `_2`, `_3`, … suffix.
1411                        fn matches_values(existing: &AnalyzedSchema, values: &[String]) -> bool {
1412                            matches!(
1413                                &existing.schema_type,
1414                                SchemaType::StringEnum { values: existing_values }
1415                                    if existing_values == values
1416                            )
1417                        }
1418
1419                        let mut enum_type_name = primary_name.clone();
1420                        let mut should_insert = match self.resolved_cache.get(&enum_type_name) {
1421                            None => true,
1422                            Some(existing) if matches_values(existing, &enum_values) => false,
1423                            Some(_) => {
1424                                // Collision with different values — try a
1425                                // value-suffixed name first.
1426                                let suffix = enum_values
1427                                    .first()
1428                                    .map(|v| self.to_pascal_case(v))
1429                                    .unwrap_or_else(|| "Variant".to_string());
1430                                let candidate = format!("{primary_name}{suffix}");
1431
1432                                let resolved = match self.resolved_cache.get(&candidate) {
1433                                    None => Some((candidate.clone(), true)),
1434                                    Some(existing) if matches_values(existing, &enum_values) => {
1435                                        Some((candidate.clone(), false))
1436                                    }
1437                                    Some(_) => {
1438                                        // Walk a numeric suffix until we find
1439                                        // a slot that's free or matches.
1440                                        let mut found = None;
1441                                        for n in 2..1000 {
1442                                            let numbered = format!("{candidate}_{n}");
1443                                            match self.resolved_cache.get(&numbered) {
1444                                                None => {
1445                                                    found = Some((numbered, true));
1446                                                    break;
1447                                                }
1448                                                Some(existing)
1449                                                    if matches_values(existing, &enum_values) =>
1450                                                {
1451                                                    found = Some((numbered, false));
1452                                                    break;
1453                                                }
1454                                                Some(_) => continue,
1455                                            }
1456                                        }
1457                                        found
1458                                    }
1459                                };
1460
1461                                let (resolved_name, insert) = resolved.unwrap_or((candidate, true));
1462                                enum_type_name = resolved_name;
1463                                insert
1464                            }
1465                        };
1466
1467                        // Store the enum as a named schema if this is the
1468                        // first time we've seen this exact (name, values) pair.
1469                        if should_insert {
1470                            self.resolved_cache.insert(
1471                                enum_type_name.clone(),
1472                                AnalyzedSchema {
1473                                    name: enum_type_name.clone(),
1474                                    original: serde_json::to_value(schema).unwrap_or(Value::Null),
1475                                    schema_type: SchemaType::StringEnum {
1476                                        values: enum_values,
1477                                    },
1478                                    dependencies: HashSet::new(),
1479                                    nullable: false,
1480                                    description: schema.details().description.clone(),
1481                                    default: schema.details().default.clone(),
1482                                },
1483                            );
1484                            // Silence unused-write warnings when the value
1485                            // is not consulted again on this path.
1486                            let _ = &mut should_insert;
1487                        }
1488
1489                        // Return a reference to the named enum type
1490                        dependencies.insert(enum_type_name.clone());
1491                        return Ok(SchemaType::Reference {
1492                            target: enum_type_name,
1493                        });
1494                    } else {
1495                        return Ok(SchemaType::Primitive {
1496                            rust_type: "String".to_string(),
1497                        });
1498                    }
1499                }
1500                OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
1501                    let details = schema.details();
1502                    let rust_type = self.get_number_rust_type(schema_type.clone(), details);
1503                    return Ok(SchemaType::Primitive { rust_type });
1504                }
1505                OpenApiSchemaType::Boolean => {
1506                    return Ok(SchemaType::Primitive {
1507                        rust_type: "bool".to_string(),
1508                    });
1509                }
1510                OpenApiSchemaType::Array => {
1511                    // Analyze array property with context
1512                    let context_name = if let Some(prop_name) = property_name {
1513                        // Use property name for context
1514                        let prop_pascal = self.to_pascal_case(prop_name);
1515                        format!(
1516                            "{}{}",
1517                            self.current_schema_name.as_deref().unwrap_or("Unknown"),
1518                            prop_pascal
1519                        )
1520                    } else {
1521                        // Fallback to generic name
1522                        "ArrayItem".to_string()
1523                    };
1524                    return self.analyze_array_schema(schema, &context_name, dependencies);
1525                }
1526                OpenApiSchemaType::Object => {
1527                    // Check if this is a dynamic JSON object
1528                    if self.should_use_dynamic_json(schema) {
1529                        return Ok(SchemaType::Primitive {
1530                            rust_type: "serde_json::Value".to_string(),
1531                        });
1532                    }
1533                    // Inline object in property - create a named schema for it
1534                    let object_type_name = if let Some(prop_name) = property_name {
1535                        // Use property name for context
1536                        let prop_pascal = self.to_pascal_case(prop_name);
1537                        format!(
1538                            "{}{}",
1539                            self.current_schema_name.as_deref().unwrap_or("Unknown"),
1540                            prop_pascal
1541                        )
1542                    } else {
1543                        // Fallback to generic name
1544                        format!(
1545                            "{}Object",
1546                            self.current_schema_name.as_deref().unwrap_or("Unknown")
1547                        )
1548                    };
1549
1550                    // Analyze the object schema
1551                    let object_type = self.analyze_object_schema(schema, dependencies)?;
1552
1553                    // Create an analyzed schema for the inline object
1554                    let inline_schema = AnalyzedSchema {
1555                        name: object_type_name.clone(),
1556                        original: serde_json::to_value(schema).unwrap_or(Value::Null),
1557                        schema_type: object_type,
1558                        dependencies: dependencies.clone(),
1559                        nullable: false,
1560                        description: schema.details().description.clone(),
1561                        default: None,
1562                    };
1563
1564                    // Add the inline object as a named schema
1565                    self.resolved_cache
1566                        .insert(object_type_name.clone(), inline_schema);
1567                    dependencies.insert(object_type_name.clone());
1568
1569                    // Return a reference to the named schema
1570                    return Ok(SchemaType::Reference {
1571                        target: object_type_name,
1572                    });
1573                }
1574                _ => {
1575                    return Ok(SchemaType::Primitive {
1576                        rust_type: "serde_json::Value".to_string(),
1577                    });
1578                }
1579            }
1580        }
1581
1582        // Handle nullable patterns
1583        if schema.is_nullable_pattern() {
1584            if let Some(non_null) = schema.non_null_variant() {
1585                return self.analyze_property_schema_with_context(
1586                    non_null,
1587                    property_name,
1588                    dependencies,
1589                );
1590            }
1591        }
1592
1593        // Check if this should be dynamic JSON before further analysis
1594        if self.should_use_dynamic_json(schema) {
1595            return Ok(SchemaType::Primitive {
1596                rust_type: "serde_json::Value".to_string(),
1597            });
1598        }
1599
1600        // Handle allOf composition patterns
1601        if let Schema::AllOf { all_of, .. } = schema {
1602            return self.analyze_allof_composition(all_of, dependencies);
1603        }
1604
1605        // Handle union patterns (anyOf/oneOf) that weren't caught earlier
1606        if let Some(variants) = schema.union_variants() {
1607            match variants.len().cmp(&1) {
1608                std::cmp::Ordering::Equal => {
1609                    // Single variant - analyze it directly
1610                    return self.analyze_property_schema_with_context(
1611                        &variants[0],
1612                        property_name,
1613                        dependencies,
1614                    );
1615                }
1616                std::cmp::Ordering::Greater => {
1617                    // Multiple variants - try to analyze as a union
1618                    // Generate a context-aware name for the union type
1619                    let union_name = if let Some(prop_name) = property_name {
1620                        // We have property context - create a proper union name
1621                        let prop_pascal = self.to_pascal_case(prop_name);
1622                        format!(
1623                            "{}{}",
1624                            self.current_schema_name.as_deref().unwrap_or(""),
1625                            prop_pascal
1626                        )
1627                    } else {
1628                        "UnionType".to_string()
1629                    };
1630
1631                    // Check if this is a oneOf or anyOf
1632                    if let Schema::OneOf {
1633                        one_of,
1634                        discriminator,
1635                        ..
1636                    } = schema
1637                    {
1638                        // This is a oneOf - analyze it properly with potential discriminator
1639                        let oneof_result = self.analyze_oneof_union(
1640                            one_of,
1641                            discriminator.as_ref(),
1642                            &union_name,
1643                            dependencies,
1644                        )?;
1645
1646                        // If we got a union type (not discriminated), we need to store it as a named type
1647                        if let SchemaType::Union {
1648                            variants: _union_variants,
1649                        } = &oneof_result
1650                        {
1651                            // Store the union as a named type in resolved_cache
1652                            self.resolved_cache.insert(
1653                                union_name.clone(),
1654                                AnalyzedSchema {
1655                                    name: union_name.clone(),
1656                                    original: serde_json::to_value(schema).unwrap_or(Value::Null),
1657                                    schema_type: oneof_result.clone(),
1658                                    dependencies: dependencies.clone(),
1659                                    nullable: false,
1660                                    description: schema.details().description.clone(),
1661                                    default: None,
1662                                },
1663                            );
1664
1665                            // Return a reference to the named union type
1666                            dependencies.insert(union_name.clone());
1667                            return Ok(SchemaType::Reference { target: union_name });
1668                        }
1669
1670                        return Ok(oneof_result);
1671                    } else if let Schema::AnyOf {
1672                        any_of,
1673                        discriminator,
1674                        ..
1675                    } = schema
1676                    {
1677                        // This is anyOf - use existing logic with discriminator support
1678                        let union_analysis = self.analyze_anyof_union(
1679                            any_of,
1680                            discriminator.as_ref(),
1681                            dependencies,
1682                            &union_name,
1683                        )?;
1684                        return Ok(union_analysis);
1685                    } else {
1686                        // This shouldn't happen, but handle gracefully
1687                        // Create a simple union from variants
1688                        let mut union_variants = Vec::new();
1689                        for variant in variants {
1690                            if let Some(ref_str) = variant.reference() {
1691                                if let Some(target) = self.extract_schema_name(ref_str) {
1692                                    dependencies.insert(target.to_string());
1693                                    union_variants.push(SchemaRef {
1694                                        target: target.to_string(),
1695                                        nullable: false,
1696                                    });
1697                                }
1698                            }
1699                        }
1700                        return Ok(SchemaType::Union {
1701                            variants: union_variants,
1702                        });
1703                    }
1704                }
1705                std::cmp::Ordering::Less => {}
1706            }
1707        }
1708
1709        // Handle untyped schemas by trying to infer from structure
1710        if let Some(inferred_type) = schema.inferred_type() {
1711            match inferred_type {
1712                OpenApiSchemaType::Object => {
1713                    // Double-check for dynamic JSON pattern even for inferred objects
1714                    if self.should_use_dynamic_json(schema) {
1715                        return Ok(SchemaType::Primitive {
1716                            rust_type: "serde_json::Value".to_string(),
1717                        });
1718                    }
1719                    return self.analyze_object_schema(schema, dependencies);
1720                }
1721                OpenApiSchemaType::Array => {
1722                    let context_name = if let Some(prop_name) = property_name {
1723                        // Use property name for context
1724                        let prop_pascal = self.to_pascal_case(prop_name);
1725                        format!(
1726                            "{}{}",
1727                            self.current_schema_name.as_deref().unwrap_or("Unknown"),
1728                            prop_pascal
1729                        )
1730                    } else {
1731                        // Fallback to generic name
1732                        "ArrayItem".to_string()
1733                    };
1734                    return self.analyze_array_schema(schema, &context_name, dependencies);
1735                }
1736                OpenApiSchemaType::String => {
1737                    if let Some(enum_values) = schema.details().string_enum_values() {
1738                        return Ok(SchemaType::StringEnum {
1739                            values: enum_values,
1740                        });
1741                    } else {
1742                        return Ok(SchemaType::Primitive {
1743                            rust_type: "String".to_string(),
1744                        });
1745                    }
1746                }
1747                _ => {
1748                    // Handle other inferred types
1749                    let rust_type = self.openapi_type_to_rust_type(inferred_type, schema.details());
1750                    return Ok(SchemaType::Primitive { rust_type });
1751                }
1752            }
1753        }
1754
1755        Ok(SchemaType::Primitive {
1756            rust_type: "serde_json::Value".to_string(),
1757        })
1758    }
1759
1760    fn analyze_allof_composition(
1761        &mut self,
1762        all_of_schemas: &[Schema],
1763        dependencies: &mut HashSet<String>,
1764    ) -> Result<SchemaType> {
1765        // Special case: if allOf contains only a single reference, treat it as a direct type alias
1766        // This handles patterns like: "allOf": [{"$ref": "#/components/schemas/Usage"}]
1767        if all_of_schemas.len() == 1 {
1768            if let Schema::Reference { reference, .. } = &all_of_schemas[0] {
1769                if let Some(target) = self.extract_schema_name(reference) {
1770                    dependencies.insert(target.to_string());
1771                    return Ok(SchemaType::Reference {
1772                        target: target.to_string(),
1773                    });
1774                }
1775            }
1776        }
1777
1778        // AllOf represents schema composition - merge all schemas into one
1779        let mut merged_properties = BTreeMap::new();
1780        let mut merged_required = HashSet::new();
1781        let mut descriptions = Vec::new();
1782
1783        // Save the current schema context to restore it when analyzing properties
1784        let current_context = self.current_schema_name.clone();
1785
1786        for schema in all_of_schemas {
1787            match schema {
1788                Schema::Reference { reference, .. } => {
1789                    // Add dependency on referenced schema
1790                    if let Some(target) = self.extract_schema_name(reference) {
1791                        dependencies.insert(target.to_string());
1792
1793                        // First ensure the referenced schema is analyzed
1794                        let analyzed_ref = self.analyze_schema(target)?;
1795
1796                        // Now merge the analyzed schema's properties
1797                        match &analyzed_ref.schema_type {
1798                            SchemaType::Object {
1799                                properties,
1800                                required,
1801                                ..
1802                            } => {
1803                                // Merge properties from the analyzed schema
1804                                for (prop_name, prop_info) in properties {
1805                                    merged_properties.insert(prop_name.clone(), prop_info.clone());
1806                                }
1807                                // Merge required fields
1808                                for req in required {
1809                                    merged_required.insert(req.clone());
1810                                }
1811                            }
1812                            _ => {
1813                                // If the referenced schema is not an object, fall back to raw merge
1814                                if let Some(ref_schema) = self.schemas.get(target).cloned() {
1815                                    self.merge_schema_into_properties(
1816                                        &ref_schema,
1817                                        &mut merged_properties,
1818                                        &mut merged_required,
1819                                        dependencies,
1820                                    )?;
1821                                }
1822                            }
1823                        }
1824                    }
1825                }
1826                Schema::Typed {
1827                    schema_type: OpenApiSchemaType::Object,
1828                    ..
1829                }
1830                | Schema::Untyped { .. } => {
1831                    // Restore the original context when analyzing inline properties
1832                    let saved_context = self.current_schema_name.clone();
1833                    self.current_schema_name = current_context.clone();
1834
1835                    // Merge object properties directly
1836                    self.merge_schema_into_properties(
1837                        schema,
1838                        &mut merged_properties,
1839                        &mut merged_required,
1840                        dependencies,
1841                    )?;
1842
1843                    // Restore the previous context
1844                    self.current_schema_name = saved_context;
1845                }
1846                _ => {
1847                    // For non-object typed schemas in allOf, try to merge them as well
1848                    // This handles cases like allOf with enum or string constraints
1849                    self.merge_schema_into_properties(
1850                        schema,
1851                        &mut merged_properties,
1852                        &mut merged_required,
1853                        dependencies,
1854                    )?;
1855                }
1856            }
1857
1858            // Collect descriptions
1859            if let Some(desc) = &schema.details().description {
1860                descriptions.push(desc.clone());
1861            }
1862        }
1863
1864        // If we successfully merged properties, return an object
1865        if !merged_properties.is_empty() {
1866            Ok(SchemaType::Object {
1867                properties: merged_properties,
1868                required: merged_required,
1869                additional_properties: false,
1870            })
1871        } else {
1872            // Fall back to composition if we couldn't merge
1873            Ok(SchemaType::Composition {
1874                schemas: all_of_schemas
1875                    .iter()
1876                    .filter_map(|s| {
1877                        if let Some(ref_str) = s.reference() {
1878                            if let Some(target) = self.extract_schema_name(ref_str) {
1879                                dependencies.insert(target.to_string());
1880                                Some(SchemaRef {
1881                                    target: target.to_string(),
1882                                    nullable: false,
1883                                })
1884                            } else {
1885                                None
1886                            }
1887                        } else {
1888                            None
1889                        }
1890                    })
1891                    .collect(),
1892            })
1893        }
1894    }
1895
1896    fn merge_schema_into_properties(
1897        &mut self,
1898        schema: &Schema,
1899        merged_properties: &mut BTreeMap<String, PropertyInfo>,
1900        merged_required: &mut HashSet<String>,
1901        dependencies: &mut HashSet<String>,
1902    ) -> Result<()> {
1903        let details = schema.details();
1904
1905        // Merge properties
1906        if let Some(properties) = &details.properties {
1907            for (prop_name, prop_schema) in properties {
1908                let prop_type = self.analyze_property_schema_with_context(
1909                    prop_schema,
1910                    Some(prop_name),
1911                    dependencies,
1912                )?;
1913                let prop_details = prop_schema.details();
1914
1915                merged_properties.insert(
1916                    prop_name.clone(),
1917                    PropertyInfo {
1918                        schema_type: prop_type,
1919                        nullable: prop_details.is_nullable(),
1920                        description: prop_details.description.clone(),
1921                        default: prop_details.default.clone(),
1922                        serde_attrs: Vec::new(),
1923                    },
1924                );
1925            }
1926        }
1927
1928        // Merge required fields
1929        if let Some(required) = &details.required {
1930            for field in required {
1931                merged_required.insert(field.clone());
1932            }
1933        }
1934
1935        Ok(())
1936    }
1937
1938    fn analyze_oneof_union(
1939        &mut self,
1940        one_of_schemas: &[Schema],
1941        discriminator: Option<&crate::openapi::Discriminator>,
1942        parent_name: &str,
1943        dependencies: &mut HashSet<String>,
1944    ) -> Result<SchemaType> {
1945        // Pattern: nullable [Type, null] — return the non-null type directly.
1946        // The nullable bit is recorded at the property level via is_nullable_pattern().
1947        if one_of_schemas.len() == 2 {
1948            let null_count = one_of_schemas
1949                .iter()
1950                .filter(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
1951                .count();
1952            if null_count == 1 {
1953                if let Some(non_null) = one_of_schemas
1954                    .iter()
1955                    .find(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
1956                {
1957                    return self
1958                        .analyze_schema_value(non_null, parent_name)
1959                        .map(|a| a.schema_type);
1960                }
1961            }
1962        }
1963
1964        // If there's no discriminator, we should create an untagged union
1965        if discriminator.is_none() {
1966            // Handle untagged unions (oneOf without discriminator)
1967            return self.analyze_untagged_oneof_union(one_of_schemas, parent_name, dependencies);
1968        }
1969
1970        // This is a discriminated union
1971        let discriminator_field = discriminator
1972            .ok_or_else(|| {
1973                GeneratorError::InvalidDiscriminator(
1974                    "expected discriminator after guard check".to_string(),
1975                )
1976            })?
1977            .property_name
1978            .clone();
1979
1980        let mut variants = Vec::new();
1981        let mut used_variant_names = std::collections::HashSet::new();
1982
1983        for variant_schema in one_of_schemas {
1984            // Check if this is a direct reference, recursive reference, or an allOf wrapper with a reference
1985            let ref_info = if let Some(ref_str) = variant_schema.reference() {
1986                Some((ref_str, false))
1987            } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
1988                Some((recursive_ref, true))
1989            } else if let Schema::AllOf { all_of, .. } = variant_schema {
1990                // Check if this is an allOf with a single reference
1991                if all_of.len() == 1 {
1992                    if let Some(ref_str) = all_of[0].reference() {
1993                        Some((ref_str, false))
1994                    } else {
1995                        all_of[0]
1996                            .recursive_reference()
1997                            .map(|recursive_ref| (recursive_ref, true))
1998                    }
1999                } else {
2000                    None
2001                }
2002            } else {
2003                None
2004            };
2005
2006            if let Some((ref_str, is_recursive)) = ref_info {
2007                let schema_name = if is_recursive && ref_str == "#" {
2008                    // Handle recursive reference to the schema with recursiveAnchor
2009                    self.find_recursive_anchor_schema()
2010                        .or_else(|| self.current_schema_name.clone())
2011                        .unwrap_or_else(|| "CompoundFilter".to_string())
2012                } else {
2013                    self.extract_schema_name(ref_str)
2014                        .map(|s| s.to_string())
2015                        .unwrap_or_else(|| "UnknownRef".to_string())
2016                };
2017
2018                if !schema_name.is_empty() {
2019                    dependencies.insert(schema_name.clone());
2020
2021                    // Determine discriminator value with priority order:
2022                    // 1. Explicit mapping in discriminator
2023                    // 2. Extract from referenced schema
2024                    // 3. Generate from schema name
2025                    let discriminator_value = if let Some(disc) = discriminator {
2026                        if let Some(mappings) = &disc.mapping {
2027                            // Find the mapping key that points to this schema reference
2028                            // Mapping format is: "discriminator_value" -> "#/components/schemas/SchemaName"
2029                            mappings
2030                                .iter()
2031                                .find(|(_, target_ref)| {
2032                                    // Check if this mapping target matches our reference
2033                                    target_ref.as_str() == ref_str
2034                                        || self
2035                                            .extract_schema_name(target_ref)
2036                                            .map(|s| s.to_string())
2037                                            == Some(schema_name.clone())
2038                                })
2039                                .map(|(key, _)| key.clone())
2040                                .unwrap_or_else(|| {
2041                                    self.fallback_discriminator_value_for_field(
2042                                        &schema_name,
2043                                        &discriminator_field,
2044                                    )
2045                                })
2046                        } else {
2047                            self.fallback_discriminator_value_for_field(
2048                                &schema_name,
2049                                &discriminator_field,
2050                            )
2051                        }
2052                    } else {
2053                        self.fallback_discriminator_value_for_field(
2054                            &schema_name,
2055                            &discriminator_field,
2056                        )
2057                    };
2058
2059                    // Generate Rust-friendly variant name and ensure uniqueness
2060                    let base_name = self.to_rust_variant_name(&schema_name);
2061                    let rust_name =
2062                        self.ensure_unique_variant_name(base_name, &mut used_variant_names);
2063
2064                    // Use the discriminator value as-is from the schema
2065                    let final_discriminator_value = discriminator_value;
2066
2067                    variants.push(UnionVariant {
2068                        rust_name,
2069                        type_name: schema_name,
2070                        discriminator_value: final_discriminator_value,
2071                        schema_ref: ref_str.to_string(),
2072                    });
2073                }
2074            } else {
2075                // Handle inline schemas in oneOf
2076                let variant_index = variants.len();
2077                let inline_type_name =
2078                    self.generate_inline_type_name(variant_schema, variant_index);
2079
2080                // Try to extract discriminator value from inline schema
2081                let discriminator_value = if let Some(disc) = discriminator {
2082                    if let Some(mappings) = &disc.mapping {
2083                        // Look for mapping that points to this inline variant by index
2084                        mappings
2085                            .iter()
2086                            .find(|(_, target_ref)| {
2087                                target_ref.contains(&format!("variant_{variant_index}"))
2088                            })
2089                            .map(|(key, _)| key.clone())
2090                            .unwrap_or_else(|| {
2091                                self.extract_inline_discriminator_value(
2092                                    variant_schema,
2093                                    &discriminator_field,
2094                                    variant_index,
2095                                )
2096                            })
2097                    } else {
2098                        self.extract_inline_discriminator_value(
2099                            variant_schema,
2100                            &discriminator_field,
2101                            variant_index,
2102                        )
2103                    }
2104                } else {
2105                    self.extract_inline_discriminator_value(
2106                        variant_schema,
2107                        &discriminator_field,
2108                        variant_index,
2109                    )
2110                };
2111
2112                // Generate Rust-friendly variant name based on discriminator or fallback to generic
2113                let base_name = if discriminator_value.starts_with("variant_") {
2114                    format!("Variant{variant_index}")
2115                } else {
2116                    // Convert discriminator value to a meaningful Rust variant name
2117                    let clean_name = self.discriminator_to_variant_name(&discriminator_value);
2118                    self.to_rust_variant_name(&clean_name)
2119                };
2120                let rust_name = self.ensure_unique_variant_name(base_name, &mut used_variant_names);
2121
2122                // Use the discriminator value as-is from the schema
2123                let final_discriminator_value = discriminator_value;
2124
2125                variants.push(UnionVariant {
2126                    rust_name,
2127                    type_name: inline_type_name.clone(),
2128                    discriminator_value: final_discriminator_value,
2129                    schema_ref: format!("inline_{variant_index}"),
2130                });
2131
2132                // Store inline schema for later analysis and generation
2133                self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
2134            }
2135        }
2136
2137        if variants.is_empty() {
2138            // If we couldn't create a discriminated union, fall back to an untagged union
2139            // This handles cases where oneOf contains references or inline schemas without proper discriminators
2140            let mut union_variants = Vec::new();
2141
2142            for (variant_index, variant_schema) in one_of_schemas.iter().enumerate() {
2143                // First check if it's a reference or recursive reference
2144                if let Some(ref_str) = variant_schema.reference() {
2145                    if let Some(schema_name) = self.extract_schema_name(ref_str) {
2146                        dependencies.insert(schema_name.to_string());
2147                        union_variants.push(SchemaRef {
2148                            target: schema_name.to_string(),
2149                            nullable: false,
2150                        });
2151                    }
2152                } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2153                    let schema_name = if recursive_ref == "#" {
2154                        // Handle recursive reference to the schema with recursiveAnchor
2155                        self.find_recursive_anchor_schema()
2156                            .or_else(|| self.current_schema_name.clone())
2157                            .unwrap_or_else(|| "CompoundFilter".to_string())
2158                    } else {
2159                        self.extract_schema_name(recursive_ref)
2160                            .map(|s| s.to_string())
2161                            .unwrap_or_else(|| "RecursiveType".to_string())
2162                    };
2163                    dependencies.insert(schema_name.clone());
2164                    union_variants.push(SchemaRef {
2165                        target: schema_name,
2166                        nullable: false,
2167                    });
2168                } else {
2169                    // Handle inline schemas by creating type aliases or using primitive types directly
2170                    let inline_name = self.generate_context_aware_name(
2171                        parent_name,
2172                        "InlineVariant",
2173                        variant_index,
2174                        Some(variant_schema),
2175                    );
2176                    let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
2177                    let variant_type = analyzed.schema_type;
2178
2179                    // Add dependencies from the analyzed schema
2180                    for dep in &analyzed.dependencies {
2181                        dependencies.insert(dep.clone());
2182                    }
2183
2184                    match &variant_type {
2185                        // For primitive types, we can use them directly in the union
2186                        SchemaType::Primitive { rust_type } => {
2187                            union_variants.push(SchemaRef {
2188                                target: rust_type.clone(),
2189                                nullable: false,
2190                            });
2191                        }
2192                        // For arrays, check if we can determine the item type
2193                        SchemaType::Array { item_type } => {
2194                            match item_type.as_ref() {
2195                                SchemaType::Primitive { rust_type } => {
2196                                    let type_name = format!("Vec<{rust_type}>");
2197                                    union_variants.push(SchemaRef {
2198                                        target: type_name,
2199                                        nullable: false,
2200                                    });
2201                                }
2202                                SchemaType::Reference { target } => {
2203                                    let type_name = format!("Vec<{target}>");
2204                                    union_variants.push(SchemaRef {
2205                                        target: type_name,
2206                                        nullable: false,
2207                                    });
2208                                }
2209                                _ => {
2210                                    // For other array types, create an inline type
2211                                    let inline_type_name = self.generate_context_aware_name(
2212                                        parent_name,
2213                                        "Variant",
2214                                        variant_index,
2215                                        None,
2216                                    );
2217                                    self.add_inline_schema(
2218                                        &inline_type_name,
2219                                        variant_schema,
2220                                        dependencies,
2221                                    )?;
2222                                    union_variants.push(SchemaRef {
2223                                        target: inline_type_name,
2224                                        nullable: false,
2225                                    });
2226                                }
2227                            }
2228                        }
2229                        // For reference types, use the reference target directly
2230                        SchemaType::Reference { target } => {
2231                            union_variants.push(SchemaRef {
2232                                target: target.clone(),
2233                                nullable: false,
2234                            });
2235                        }
2236                        // For other complex types, create an inline type
2237                        _ => {
2238                            let inline_type_name =
2239                                format!("{}Variant{}", parent_name, variant_index + 1);
2240                            self.add_inline_schema(
2241                                &inline_type_name,
2242                                variant_schema,
2243                                dependencies,
2244                            )?;
2245                            union_variants.push(SchemaRef {
2246                                target: inline_type_name,
2247                                nullable: false,
2248                            });
2249                        }
2250                    }
2251                }
2252            }
2253
2254            if !union_variants.is_empty() {
2255                return Ok(SchemaType::Union {
2256                    variants: union_variants,
2257                });
2258            }
2259
2260            // Only fall back to serde_json::Value if we truly can't analyze the union
2261            return Ok(SchemaType::Primitive {
2262                rust_type: "serde_json::Value".to_string(),
2263            });
2264        }
2265
2266        Ok(SchemaType::DiscriminatedUnion {
2267            discriminator_field,
2268            variants,
2269        })
2270    }
2271
2272    fn analyze_untagged_oneof_union(
2273        &mut self,
2274        one_of_schemas: &[Schema],
2275        parent_name: &str,
2276        dependencies: &mut HashSet<String>,
2277    ) -> Result<SchemaType> {
2278        // Drop {"type": "null"} variants. They mean "may be null" and are surfaced
2279        // as Option<T> at the property level — including them here produces a junk
2280        // `SerdeJsonValue(serde_json::Value)` variant.
2281        let filtered: Vec<&Schema> = one_of_schemas
2282            .iter()
2283            .filter(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
2284            .collect();
2285
2286        // If filtering leaves a single variant, return its analyzed type directly.
2287        if filtered.len() == 1 {
2288            return self
2289                .analyze_schema_value(filtered[0], parent_name)
2290                .map(|a| a.schema_type);
2291        }
2292
2293        let mut union_variants = Vec::new();
2294
2295        for (variant_index, variant_schema) in filtered.iter().copied().enumerate() {
2296            // First check if it's a reference or recursive reference
2297            if let Some(ref_str) = variant_schema.reference() {
2298                if let Some(schema_name) = self.extract_schema_name(ref_str) {
2299                    dependencies.insert(schema_name.to_string());
2300                    union_variants.push(SchemaRef {
2301                        target: schema_name.to_string(),
2302                        nullable: false,
2303                    });
2304                }
2305            } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2306                let schema_name = if recursive_ref == "#" {
2307                    // Handle recursive reference to the schema with recursiveAnchor
2308                    self.find_recursive_anchor_schema()
2309                        .or_else(|| self.current_schema_name.clone())
2310                        .unwrap_or_else(|| "CompoundFilter".to_string())
2311                } else {
2312                    self.extract_schema_name(recursive_ref)
2313                        .map(|s| s.to_string())
2314                        .unwrap_or_else(|| "RecursiveType".to_string())
2315                };
2316                dependencies.insert(schema_name.clone());
2317                union_variants.push(SchemaRef {
2318                    target: schema_name,
2319                    nullable: false,
2320                });
2321            } else {
2322                // Handle inline schemas by creating type aliases or using primitive types directly
2323                let inline_name = self.generate_context_aware_name(
2324                    parent_name,
2325                    "InlineVariant",
2326                    variant_index,
2327                    Some(variant_schema),
2328                );
2329                let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
2330                let variant_type = analyzed.schema_type;
2331
2332                // Add dependencies from the analyzed schema
2333                for dep in &analyzed.dependencies {
2334                    dependencies.insert(dep.clone());
2335                }
2336
2337                match &variant_type {
2338                    // For primitive types, we can use them directly in the union
2339                    SchemaType::Primitive { rust_type } => {
2340                        union_variants.push(SchemaRef {
2341                            target: rust_type.clone(),
2342                            nullable: false,
2343                        });
2344                    }
2345                    // For arrays, check if we can determine the item type
2346                    SchemaType::Array { item_type } => {
2347                        match item_type.as_ref() {
2348                            SchemaType::Primitive { rust_type } => {
2349                                let type_name = format!("Vec<{rust_type}>");
2350                                union_variants.push(SchemaRef {
2351                                    target: type_name,
2352                                    nullable: false,
2353                                });
2354                            }
2355                            SchemaType::Reference { target } => {
2356                                let type_name = format!("Vec<{target}>");
2357                                union_variants.push(SchemaRef {
2358                                    target: type_name,
2359                                    nullable: false,
2360                                });
2361                            }
2362                            // Handle arrays of arrays (e.g., Vec<Vec<i64>>)
2363                            SchemaType::Array {
2364                                item_type: inner_item_type,
2365                            } => {
2366                                match inner_item_type.as_ref() {
2367                                    SchemaType::Primitive { rust_type } => {
2368                                        let type_name = format!("Vec<Vec<{rust_type}>>");
2369                                        union_variants.push(SchemaRef {
2370                                            target: type_name,
2371                                            nullable: false,
2372                                        });
2373                                    }
2374                                    SchemaType::Reference { target } => {
2375                                        let type_name = format!("Vec<Vec<{target}>>");
2376                                        union_variants.push(SchemaRef {
2377                                            target: type_name,
2378                                            nullable: false,
2379                                        });
2380                                    }
2381                                    _ => {
2382                                        // For deeper nesting, create an inline type
2383                                        let inline_type_name = self.generate_context_aware_name(
2384                                            parent_name,
2385                                            "Variant",
2386                                            variant_index,
2387                                            None,
2388                                        );
2389                                        self.add_inline_schema(
2390                                            &inline_type_name,
2391                                            variant_schema,
2392                                            dependencies,
2393                                        )?;
2394                                        union_variants.push(SchemaRef {
2395                                            target: inline_type_name,
2396                                            nullable: false,
2397                                        });
2398                                    }
2399                                }
2400                            }
2401                            _ => {
2402                                // For other array types, create an inline type
2403                                let inline_type_name = self.generate_context_aware_name(
2404                                    parent_name,
2405                                    "Variant",
2406                                    variant_index,
2407                                    None,
2408                                );
2409                                self.add_inline_schema(
2410                                    &inline_type_name,
2411                                    variant_schema,
2412                                    dependencies,
2413                                )?;
2414                                union_variants.push(SchemaRef {
2415                                    target: inline_type_name,
2416                                    nullable: false,
2417                                });
2418                            }
2419                        }
2420                    }
2421                    // For reference types, use the reference target directly
2422                    SchemaType::Reference { target } => {
2423                        union_variants.push(SchemaRef {
2424                            target: target.clone(),
2425                            nullable: false,
2426                        });
2427                    }
2428                    // For other complex types, create an inline type
2429                    _ => {
2430                        let inline_type_name = self.generate_context_aware_name(
2431                            parent_name,
2432                            "Variant",
2433                            variant_index,
2434                            None,
2435                        );
2436                        self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
2437                        union_variants.push(SchemaRef {
2438                            target: inline_type_name,
2439                            nullable: false,
2440                        });
2441                    }
2442                }
2443            }
2444        }
2445
2446        if !union_variants.is_empty() {
2447            return Ok(SchemaType::Union {
2448                variants: union_variants,
2449            });
2450        }
2451
2452        // Only fall back to serde_json::Value if we truly can't analyze the union
2453        Ok(SchemaType::Primitive {
2454            rust_type: "serde_json::Value".to_string(),
2455        })
2456    }
2457
2458    fn add_inline_schema(
2459        &mut self,
2460        type_name: &str,
2461        schema: &Schema,
2462        dependencies: &mut HashSet<String>,
2463    ) -> Result<()> {
2464        // For primitive types, we need to ensure they are stored as type aliases
2465        if let Some(schema_type) = schema.schema_type() {
2466            match schema_type {
2467                OpenApiSchemaType::String
2468                | OpenApiSchemaType::Integer
2469                | OpenApiSchemaType::Number
2470                | OpenApiSchemaType::Boolean => {
2471                    let rust_type =
2472                        self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
2473
2474                    // Store as a type alias
2475                    self.resolved_cache.insert(
2476                        type_name.to_string(),
2477                        AnalyzedSchema {
2478                            name: type_name.to_string(),
2479                            original: serde_json::to_value(schema).unwrap_or(Value::Null),
2480                            schema_type: SchemaType::Primitive { rust_type },
2481                            dependencies: HashSet::new(),
2482                            nullable: false,
2483                            description: schema.details().description.clone(),
2484                            default: None,
2485                        },
2486                    );
2487                    return Ok(());
2488                }
2489                _ => {}
2490            }
2491        }
2492
2493        // For non-primitive types, analyze the inline schema and add it to our collection
2494        // Set current_schema_name so nested inline properties (enums, unions, objects)
2495        // get named with the correct parent context instead of inheriting a stale name
2496        let previous_schema_name = self.current_schema_name.take();
2497        self.current_schema_name = Some(type_name.to_string());
2498        let analyzed = self.analyze_schema_value(schema, type_name)?;
2499        self.current_schema_name = previous_schema_name;
2500
2501        // Add to resolved cache so it can be generated
2502        self.resolved_cache.insert(type_name.to_string(), analyzed);
2503
2504        // Add dependencies
2505        if let Some(cached) = self.resolved_cache.get(type_name) {
2506            for dep in &cached.dependencies {
2507                dependencies.insert(dep.clone());
2508            }
2509        }
2510
2511        Ok(())
2512    }
2513
2514    fn extract_inline_discriminator_value(
2515        &self,
2516        schema: &Schema,
2517        discriminator_field: &str,
2518        variant_index: usize,
2519    ) -> String {
2520        // Try to extract discriminator value from inline schema properties
2521        if let Some(properties) = &schema.details().properties {
2522            if let Some(discriminator_prop) = properties.get(discriminator_field) {
2523                // Check for enum with single value
2524                if let Some(enum_values) = &discriminator_prop.details().enum_values {
2525                    if enum_values.len() == 1 {
2526                        if let Some(value) = enum_values[0].as_str() {
2527                            return value.to_string();
2528                        }
2529                    }
2530                }
2531                // Check for const value in extra fields
2532                if let Some(const_value) = discriminator_prop.details().extra.get("const") {
2533                    if let Some(value) = const_value.as_str() {
2534                        return value.to_string();
2535                    }
2536                }
2537                // Check for const value in the discriminator_prop.details().const_value
2538                if let Some(const_value) = &discriminator_prop.details().const_value {
2539                    if let Some(value) = const_value.as_str() {
2540                        return value.to_string();
2541                    }
2542                }
2543            }
2544        }
2545
2546        // Try to infer from schema structure and properties
2547        if let Some(inferred_name) = self.infer_variant_name_from_structure(schema, variant_index) {
2548            return inferred_name;
2549        }
2550
2551        // Fall back to generic variant name
2552        format!("variant_{variant_index}")
2553    }
2554
2555    fn infer_variant_name_from_structure(
2556        &self,
2557        schema: &Schema,
2558        _variant_index: usize,
2559    ) -> Option<String> {
2560        let details = schema.details();
2561
2562        // Strategy 1: Look for unique property combinations that suggest the variant type
2563        if let Some(properties) = &details.properties {
2564            // Common patterns for content blocks
2565            if properties.contains_key("text") && properties.len() <= 3 {
2566                return Some("text".to_string());
2567            }
2568            if properties.contains_key("image") || properties.contains_key("source") {
2569                return Some("image".to_string());
2570            }
2571            if properties.contains_key("document") {
2572                return Some("document".to_string());
2573            }
2574            if properties.contains_key("tool_use_id") || properties.contains_key("tool_result") {
2575                return Some("tool_result".to_string());
2576            }
2577            if properties.contains_key("content") && properties.contains_key("is_error") {
2578                return Some("tool_result".to_string());
2579            }
2580            if properties.contains_key("partial_json") {
2581                return Some("partial_json".to_string());
2582            }
2583
2584            // Strategy 2: Look for properties that hint at the variant purpose
2585            let property_names: Vec<&String> = properties.keys().collect();
2586
2587            // Try to find the most descriptive property name
2588            for prop_name in &property_names {
2589                if prop_name.contains("result") {
2590                    return Some("result".to_string());
2591                }
2592                if prop_name.contains("error") {
2593                    return Some("error".to_string());
2594                }
2595                if prop_name.contains("content") && property_names.len() <= 2 {
2596                    return Some("content".to_string());
2597                }
2598            }
2599
2600            // Strategy 3: Use the most significant unique property
2601            let significant_props = property_names
2602                .iter()
2603                .filter(|&name| !["type", "id", "cache_control"].contains(&name.as_str()))
2604                .collect::<Vec<_>>();
2605
2606            if significant_props.len() == 1 {
2607                return Some((*significant_props[0]).clone());
2608            }
2609        }
2610
2611        // Strategy 4: Look at description for hints
2612        if let Some(description) = &details.description {
2613            let desc_lower = description.to_lowercase();
2614            if desc_lower.contains("text") && desc_lower.len() < 100 {
2615                return Some("text".to_string());
2616            }
2617            if desc_lower.contains("image") {
2618                return Some("image".to_string());
2619            }
2620            if desc_lower.contains("document") {
2621                return Some("document".to_string());
2622            }
2623            if desc_lower.contains("tool") && desc_lower.contains("result") {
2624                return Some("tool_result".to_string());
2625            }
2626        }
2627
2628        None
2629    }
2630
2631    fn discriminator_to_variant_name(&self, discriminator: &str) -> String {
2632        // Convert discriminator values to PascalCase variant names using general rules
2633        if discriminator.is_empty() {
2634            return "Variant".to_string();
2635        }
2636
2637        let mut result = String::new();
2638        let mut next_upper = true;
2639
2640        for c in discriminator.chars() {
2641            match c {
2642                'a'..='z' => {
2643                    if next_upper {
2644                        result.push(c.to_ascii_uppercase());
2645                        next_upper = false;
2646                    } else {
2647                        result.push(c);
2648                    }
2649                }
2650                'A'..='Z' => {
2651                    result.push(c);
2652                    next_upper = false;
2653                }
2654                '0'..='9' => {
2655                    result.push(c);
2656                    next_upper = false;
2657                }
2658                '_' | '-' | '.' | ' ' | '/' | '\\' => {
2659                    // Word separators - next char should be uppercase
2660                    next_upper = true;
2661                }
2662                _ => {
2663                    // Other special characters - treat as word boundary
2664                    next_upper = true;
2665                }
2666            }
2667        }
2668
2669        // Ensure it starts with a letter
2670        if result.is_empty() || result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2671            result = format!("Variant{result}");
2672        }
2673
2674        result
2675    }
2676
2677    fn ensure_unique_variant_name(
2678        &self,
2679        base_name: String,
2680        used_names: &mut std::collections::HashSet<String>,
2681    ) -> String {
2682        let mut candidate = base_name.clone();
2683        let mut counter = 1;
2684
2685        while used_names.contains(&candidate) {
2686            counter += 1;
2687            candidate = format!("{base_name}{counter}");
2688        }
2689
2690        used_names.insert(candidate.clone());
2691        candidate
2692    }
2693
2694    fn generate_inline_type_name(&self, schema: &Schema, variant_index: usize) -> String {
2695        // Try to generate a meaningful name for inline schemas
2696        if let Some(meaningful_name) = self.infer_type_name_from_structure(schema) {
2697            return meaningful_name;
2698        }
2699
2700        // Fallback to context-aware name
2701        let context = self.current_schema_name.as_deref().unwrap_or("Inline");
2702        self.generate_context_aware_name(context, "Variant", variant_index, Some(schema))
2703    }
2704
2705    fn infer_type_name_from_structure(&self, schema: &Schema) -> Option<String> {
2706        let details = schema.details();
2707
2708        // Strategy 1: Use description if it's short and descriptive
2709        if let Some(description) = &details.description {
2710            if let Some(name_from_desc) = self.extract_type_name_from_description(description) {
2711                return Some(name_from_desc);
2712            }
2713        }
2714
2715        // Strategy 2: Use the most significant property name as the type identifier
2716        if let Some(properties) = &details.properties {
2717            if let Some(name_from_props) = self.extract_type_name_from_properties(properties) {
2718                return Some(format!("{name_from_props}Block"));
2719            }
2720        }
2721
2722        None
2723    }
2724
2725    fn extract_type_name_from_description(&self, description: &str) -> Option<String> {
2726        // Only use descriptions that are short and likely to be type identifiers
2727        if description.len() > 100 || description.contains('\n') {
2728            return None;
2729        }
2730
2731        // Extract the first meaningful word(s) from the description
2732        let words: Vec<&str> = description
2733            .split_whitespace()
2734            .take(2) // Only take first 2 words to avoid long names
2735            .filter(|word| {
2736                let w = word.to_lowercase();
2737                word.len() > 2
2738                    && ![
2739                        "the", "and", "for", "with", "that", "this", "are", "can", "will", "was",
2740                    ]
2741                    .contains(&w.as_str())
2742            })
2743            .collect();
2744
2745        if words.is_empty() {
2746            return None;
2747        }
2748
2749        // Convert to PascalCase using our existing logic
2750        let combined = words.join("_");
2751        let pascal_name = self.discriminator_to_variant_name(&combined);
2752
2753        // Add suffix if it doesn't already have one
2754        if !pascal_name.ends_with("Content")
2755            && !pascal_name.ends_with("Block")
2756            && !pascal_name.ends_with("Type")
2757        {
2758            Some(format!("{pascal_name}Content"))
2759        } else {
2760            Some(pascal_name)
2761        }
2762    }
2763
2764    fn extract_type_name_from_properties(
2765        &self,
2766        properties: &std::collections::BTreeMap<String, crate::openapi::Schema>,
2767    ) -> Option<String> {
2768        // Get property names, excluding common structural properties
2769        let significant_props: Vec<&String> = properties
2770            .keys()
2771            .filter(|name| !["type", "id", "cache_control"].contains(&name.as_str()))
2772            .collect();
2773
2774        if significant_props.is_empty() {
2775            return None;
2776        }
2777
2778        // Strategy 1: If there's only one significant property, use it
2779        if significant_props.len() == 1 {
2780            let prop_name = significant_props[0];
2781            return Some(self.discriminator_to_variant_name(prop_name));
2782        }
2783
2784        // Strategy 2: Use the first property alphabetically for consistency
2785        // This provides deterministic naming without hardcoded preferences
2786        let mut sorted_props = significant_props.clone();
2787        sorted_props.sort();
2788        if let Some(first_prop) = sorted_props.first() {
2789            return Some(self.discriminator_to_variant_name(first_prop));
2790        }
2791
2792        None
2793    }
2794
2795    fn openapi_type_to_rust_type(
2796        &self,
2797        openapi_type: OpenApiSchemaType,
2798        details: &crate::openapi::SchemaDetails,
2799    ) -> String {
2800        match openapi_type {
2801            OpenApiSchemaType::String => "String".to_string(),
2802            OpenApiSchemaType::Integer => self.get_number_rust_type(openapi_type, details),
2803            OpenApiSchemaType::Number => self.get_number_rust_type(openapi_type, details),
2804            OpenApiSchemaType::Boolean => "bool".to_string(),
2805            OpenApiSchemaType::Array => "Vec<serde_json::Value>".to_string(), // Fallback for arrays without items
2806            OpenApiSchemaType::Object => "serde_json::Value".to_string(), // Fallback for untyped objects
2807            OpenApiSchemaType::Null => "()".to_string(),                  // Null type
2808        }
2809    }
2810
2811    #[allow(dead_code)]
2812    fn fallback_discriminator_value(&self, schema_name: &str) -> String {
2813        self.fallback_discriminator_value_for_field(schema_name, "type")
2814    }
2815
2816    fn fallback_discriminator_value_for_field(
2817        &self,
2818        schema_name: &str,
2819        field_name: &str,
2820    ) -> String {
2821        // Try to extract from referenced schema first
2822        if let Some(ref_schema) = self.schemas.get(schema_name) {
2823            if let Some(extracted) =
2824                self.extract_discriminator_value_for_field(ref_schema, field_name)
2825            {
2826                return extracted;
2827            }
2828        }
2829
2830        // Fall back to generating from name
2831        self.generate_discriminator_value_from_name(schema_name)
2832    }
2833
2834    fn generate_discriminator_value_from_name(&self, schema_name: &str) -> String {
2835        // Convert schema names like "ResponseCreatedEvent" to "response.created"
2836        let mut result = String::new();
2837        let mut chars = schema_name.chars().peekable();
2838        let mut first = true;
2839
2840        while let Some(c) = chars.next() {
2841            if c.is_uppercase()
2842                && !first
2843                && chars
2844                    .peek()
2845                    .map(|&next| next.is_lowercase())
2846                    .unwrap_or(false)
2847            {
2848                result.push('.');
2849            }
2850            result.push(c.to_ascii_lowercase());
2851            first = false;
2852        }
2853
2854        // Remove common suffixes
2855        if result.ends_with("event") {
2856            result = result[..result.len() - 5].to_string();
2857        }
2858
2859        // Add "response." prefix if it looks like a response event
2860        if schema_name.starts_with("Response") && !result.starts_with("response.") {
2861            result = format!("response.{}", result.trim_start_matches("response"));
2862        }
2863
2864        result
2865    }
2866
2867    fn to_rust_variant_name(&self, schema_name: &str) -> String {
2868        // Convert "ResponseCreatedEvent" to "Created", "UserStatus" to "UserStatus", etc.
2869        let mut name = schema_name;
2870
2871        // Remove common prefixes for cleaner variant names
2872        if name.starts_with("Response") && name.len() > 8 {
2873            name = &name[8..]; // Remove "Response"
2874        }
2875
2876        // Remove common suffixes
2877        if name.ends_with("Event") && name.len() > 5 {
2878            name = &name[..name.len() - 5]; // Remove "Event"
2879        }
2880
2881        // Trim leading and trailing underscores
2882        name = name.trim_matches('_');
2883
2884        // Convert underscores to camel case using our existing function
2885        if name.is_empty() {
2886            schema_name.to_string()
2887        } else {
2888            // Use discriminator_to_variant_name to properly handle underscores
2889            self.discriminator_to_variant_name(name)
2890        }
2891    }
2892
2893    fn analyze_array_schema(
2894        &mut self,
2895        schema: &Schema,
2896        parent_schema_name: &str,
2897        dependencies: &mut HashSet<String>,
2898    ) -> Result<SchemaType> {
2899        let details = schema.details();
2900
2901        // Check if items field is present
2902        if let Some(items_schema) = &details.items {
2903            // Analyze the item type
2904            let item_type = match items_schema.as_ref() {
2905                Schema::Reference { reference, .. } => {
2906                    // Array of referenced types
2907                    let target = self
2908                        .extract_schema_name(reference)
2909                        .ok_or_else(|| GeneratorError::UnresolvedReference(reference.to_string()))?
2910                        .to_string();
2911                    dependencies.insert(target.clone());
2912                    SchemaType::Reference { target }
2913                }
2914                Schema::RecursiveRef { recursive_ref, .. } => {
2915                    // Array of recursive references
2916                    if recursive_ref == "#" {
2917                        // Self-reference to the current schema
2918                        let target = self
2919                            .find_recursive_anchor_schema()
2920                            .unwrap_or_else(|| parent_schema_name.to_string());
2921                        dependencies.insert(target.clone());
2922                        SchemaType::Reference { target }
2923                    } else {
2924                        let target = self
2925                            .extract_schema_name(recursive_ref)
2926                            .unwrap_or("RecursiveType")
2927                            .to_string();
2928                        dependencies.insert(target.clone());
2929                        SchemaType::Reference { target }
2930                    }
2931                }
2932                Schema::Typed { schema_type, .. } => {
2933                    // Array of primitive types
2934                    match schema_type {
2935                        OpenApiSchemaType::String => SchemaType::Primitive {
2936                            rust_type: "String".to_string(),
2937                        },
2938                        OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
2939                            let details = items_schema.details();
2940                            let rust_type = self.get_number_rust_type(schema_type.clone(), details);
2941                            SchemaType::Primitive { rust_type }
2942                        }
2943                        OpenApiSchemaType::Boolean => SchemaType::Primitive {
2944                            rust_type: "bool".to_string(),
2945                        },
2946                        OpenApiSchemaType::Object => {
2947                            // Inline object in array - create a named schema for it
2948                            let object_type_name = format!("{parent_schema_name}Item");
2949
2950                            // Analyze the object schema
2951                            let object_type =
2952                                self.analyze_object_schema(items_schema, dependencies)?;
2953
2954                            // Create an analyzed schema for the inline object
2955                            let inline_schema = AnalyzedSchema {
2956                                name: object_type_name.clone(),
2957                                original: serde_json::to_value(items_schema).unwrap_or(Value::Null),
2958                                schema_type: object_type,
2959                                dependencies: dependencies.clone(),
2960                                nullable: false,
2961                                description: items_schema.details().description.clone(),
2962                                default: None,
2963                            };
2964
2965                            // Add the inline object as a named schema
2966                            self.resolved_cache
2967                                .insert(object_type_name.clone(), inline_schema);
2968                            dependencies.insert(object_type_name.clone());
2969
2970                            // Return a reference to the named schema
2971                            SchemaType::Reference {
2972                                target: object_type_name,
2973                            }
2974                        }
2975                        OpenApiSchemaType::Array => {
2976                            // Array of arrays - recursively analyze
2977                            self.analyze_array_schema(
2978                                items_schema,
2979                                parent_schema_name,
2980                                dependencies,
2981                            )?
2982                        }
2983                        _ => SchemaType::Primitive {
2984                            rust_type: "serde_json::Value".to_string(),
2985                        },
2986                    }
2987                }
2988                Schema::OneOf { .. } | Schema::AnyOf { .. } => {
2989                    // Union types in arrays - analyze recursively
2990                    let analyzed = self.analyze_schema_value(items_schema, "ArrayItem")?;
2991
2992                    // If we got a discriminated union or union, we need to create a separate schema for it
2993                    match &analyzed.schema_type {
2994                        SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => {
2995                            // Generate a unique name for the union schema based on the parent context
2996                            // Use the parent context directly to maintain consistent naming
2997                            let union_name = format!("{parent_schema_name}ItemUnion");
2998
2999                            // Create a new analyzed schema with the correct name
3000                            let mut union_schema = analyzed;
3001                            union_schema.name = union_name.clone();
3002
3003                            // Add the union as a separate schema
3004                            self.resolved_cache.insert(union_name.clone(), union_schema);
3005
3006                            // Add dependency
3007                            dependencies.insert(union_name.clone());
3008
3009                            // Return a reference to the union schema
3010                            SchemaType::Reference { target: union_name }
3011                        }
3012                        _ => analyzed.schema_type,
3013                    }
3014                }
3015                Schema::Untyped { .. } => {
3016                    // Try to infer the type
3017                    if let Some(inferred) = items_schema.inferred_type() {
3018                        match inferred {
3019                            OpenApiSchemaType::Object => {
3020                                // Inline object in array - create a named schema for it
3021                                let object_type_name = format!("{parent_schema_name}Item");
3022
3023                                // Analyze the object schema
3024                                let object_type =
3025                                    self.analyze_object_schema(items_schema, dependencies)?;
3026
3027                                // Create an analyzed schema for the inline object
3028                                let inline_schema = AnalyzedSchema {
3029                                    name: object_type_name.clone(),
3030                                    original: serde_json::to_value(items_schema)
3031                                        .unwrap_or(Value::Null),
3032                                    schema_type: object_type,
3033                                    dependencies: dependencies.clone(),
3034                                    nullable: false,
3035                                    description: items_schema.details().description.clone(),
3036                                    default: None,
3037                                };
3038
3039                                // Add the inline object as a named schema
3040                                self.resolved_cache
3041                                    .insert(object_type_name.clone(), inline_schema);
3042                                dependencies.insert(object_type_name.clone());
3043
3044                                // Return a reference to the named schema
3045                                SchemaType::Reference {
3046                                    target: object_type_name,
3047                                }
3048                            }
3049                            OpenApiSchemaType::String => SchemaType::Primitive {
3050                                rust_type: "String".to_string(),
3051                            },
3052                            OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
3053                                let details = items_schema.details();
3054                                let rust_type = self.get_number_rust_type(inferred, details);
3055                                SchemaType::Primitive { rust_type }
3056                            }
3057                            OpenApiSchemaType::Boolean => SchemaType::Primitive {
3058                                rust_type: "bool".to_string(),
3059                            },
3060                            _ => SchemaType::Primitive {
3061                                rust_type: "serde_json::Value".to_string(),
3062                            },
3063                        }
3064                    } else {
3065                        SchemaType::Primitive {
3066                            rust_type: "serde_json::Value".to_string(),
3067                        }
3068                    }
3069                }
3070                _ => SchemaType::Primitive {
3071                    rust_type: "serde_json::Value".to_string(),
3072                },
3073            };
3074
3075            Ok(SchemaType::Array {
3076                item_type: Box::new(item_type),
3077            })
3078        } else {
3079            // No items specified, fall back to generic array
3080            Ok(SchemaType::Primitive {
3081                rust_type: "Vec<serde_json::Value>".to_string(),
3082            })
3083        }
3084    }
3085
3086    fn get_number_rust_type(
3087        &self,
3088        schema_type: OpenApiSchemaType,
3089        details: &crate::openapi::SchemaDetails,
3090    ) -> String {
3091        match schema_type {
3092            OpenApiSchemaType::Integer => {
3093                // Check format field for integer types
3094                match details.format.as_deref() {
3095                    Some("int32") => "i32".to_string(),
3096                    Some("int64") => "i64".to_string(),
3097                    _ => "i64".to_string(), // Default for integer
3098                }
3099            }
3100            OpenApiSchemaType::Number => {
3101                // Check format field for number types
3102                match details.format.as_deref() {
3103                    Some("float") => "f32".to_string(),
3104                    Some("double") => "f64".to_string(),
3105                    _ => "f64".to_string(), // Default for number
3106                }
3107            }
3108            _ => "serde_json::Value".to_string(), // Fallback
3109        }
3110    }
3111
3112    fn analyze_anyof_union(
3113        &mut self,
3114        any_of_schemas: &[Schema],
3115        discriminator: Option<&Discriminator>,
3116        dependencies: &mut HashSet<String>,
3117        context_name: &str,
3118    ) -> Result<SchemaType> {
3119        // Drop {"type": "null"} variants. Nullability is surfaced as Option<T>
3120        // at the property level via is_nullable_pattern(); leaving the null
3121        // variant in here would produce a phantom `()` or `serde_json::Value`
3122        // type alias that the generator can't render.
3123        let filtered_owned: Vec<Schema>;
3124        let any_of_schemas: &[Schema] = if any_of_schemas
3125            .iter()
3126            .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
3127        {
3128            filtered_owned = any_of_schemas
3129                .iter()
3130                .filter(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
3131                .cloned()
3132                .collect();
3133            if filtered_owned.is_empty() {
3134                return Ok(SchemaType::Primitive {
3135                    rust_type: "serde_json::Value".to_string(),
3136                });
3137            }
3138            if filtered_owned.len() == 1 {
3139                return self
3140                    .analyze_schema_value(&filtered_owned[0], context_name)
3141                    .map(|a| a.schema_type);
3142            }
3143            &filtered_owned
3144        } else {
3145            any_of_schemas
3146        };
3147
3148        // Pattern 2: Multiple complex types or mixed primitive/complex = flexible union
3149        let has_refs = any_of_schemas.iter().any(|s| s.is_reference());
3150        let has_objects = any_of_schemas.iter().any(|s| {
3151            matches!(s.schema_type(), Some(OpenApiSchemaType::Object))
3152                || s.inferred_type() == Some(OpenApiSchemaType::Object)
3153        });
3154        let has_arrays = any_of_schemas
3155            .iter()
3156            .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Array)));
3157
3158        // Handle mixed primitive and complex types (like string + array of objects)
3159        // Skip this pattern if all schemas are strings or const values (handle in pattern 3)
3160        let all_string_like = any_of_schemas.iter().all(|s| {
3161            matches!(s.schema_type(), Some(OpenApiSchemaType::String))
3162                || s.details().const_value.is_some()
3163        });
3164
3165        if (has_refs || has_objects || has_arrays || any_of_schemas.len() > 1) && !all_string_like {
3166            // Check if this is a discriminated union
3167            if let Some(disc) = discriminator {
3168                // This is a discriminated anyOf union, analyze it the same way as oneOf
3169                return self.analyze_oneof_union(
3170                    any_of_schemas,
3171                    Some(disc),
3172                    context_name,
3173                    dependencies,
3174                );
3175            }
3176
3177            // Auto-detect implicit discriminator from const fields across all variants
3178            if let Some(disc_field) = self.detect_discriminator_field(any_of_schemas) {
3179                return self.analyze_oneof_union(
3180                    any_of_schemas,
3181                    Some(&Discriminator {
3182                        property_name: disc_field,
3183                        mapping: None,
3184                        extra: BTreeMap::new(),
3185                    }),
3186                    context_name,
3187                    dependencies,
3188                );
3189            }
3190
3191            // Create an untagged union for flexible matching
3192            let mut variants = Vec::new();
3193
3194            for schema in any_of_schemas {
3195                if let Some(ref_str) = schema.reference() {
3196                    if let Some(target) = self.extract_schema_name(ref_str) {
3197                        dependencies.insert(target.to_string());
3198                        variants.push(SchemaRef {
3199                            target: target.to_string(),
3200                            nullable: false,
3201                        });
3202                    }
3203                } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object))
3204                    || schema.inferred_type() == Some(OpenApiSchemaType::Object)
3205                {
3206                    // Generate inline object type for anyOf union
3207                    let inline_index = variants.len();
3208                    let inline_type_name = self.generate_inline_type_name(schema, inline_index);
3209
3210                    // Store inline schema for later analysis and generation
3211                    self.add_inline_schema(&inline_type_name, schema, dependencies)?;
3212
3213                    variants.push(SchemaRef {
3214                        target: inline_type_name,
3215                        nullable: false,
3216                    });
3217                } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Array)) {
3218                    // Handle array types in unions by creating a type alias
3219                    let array_type =
3220                        self.analyze_array_schema(schema, context_name, dependencies)?;
3221
3222                    // Create a unique name for this array type in the union
3223                    let array_type_name = if let Some(items_schema) = &schema.details().items {
3224                        if let Some(ref_str) = items_schema.reference() {
3225                            if let Some(item_type_name) = self.extract_schema_name(ref_str) {
3226                                dependencies.insert(item_type_name.to_string());
3227                                format!("{item_type_name}Array")
3228                            } else {
3229                                self.generate_context_aware_name(
3230                                    context_name,
3231                                    "Array",
3232                                    variants.len(),
3233                                    Some(schema),
3234                                )
3235                            }
3236                        } else {
3237                            self.generate_context_aware_name(
3238                                context_name,
3239                                "Array",
3240                                variants.len(),
3241                                Some(schema),
3242                            )
3243                        }
3244                    } else {
3245                        self.generate_context_aware_name(
3246                            context_name,
3247                            "Array",
3248                            variants.len(),
3249                            Some(schema),
3250                        )
3251                    };
3252
3253                    // Store the array as a type alias
3254                    self.resolved_cache.insert(
3255                        array_type_name.clone(),
3256                        AnalyzedSchema {
3257                            name: array_type_name.clone(),
3258                            original: serde_json::to_value(schema).unwrap_or(Value::Null),
3259                            schema_type: array_type,
3260                            dependencies: HashSet::new(),
3261                            nullable: false,
3262                            description: Some("Array variant in union".to_string()),
3263                            default: None,
3264                        },
3265                    );
3266
3267                    // Add array type as a dependency
3268                    dependencies.insert(array_type_name.clone());
3269
3270                    variants.push(SchemaRef {
3271                        target: array_type_name,
3272                        nullable: false,
3273                    });
3274                } else if let Some(schema_type) = schema.schema_type() {
3275                    // Handle primitive types by creating type aliases for consistency
3276                    let inline_index = variants.len();
3277
3278                    // Generate a better name for primitive types
3279                    let inline_type_name = match schema_type {
3280                        OpenApiSchemaType::String => {
3281                            // For string types, check if we can infer a better name from context
3282                            // If this is the first variant and it's a string, use a simple name
3283                            if inline_index == 0 {
3284                                format!("{context_name}String")
3285                            } else {
3286                                format!("{context_name}StringVariant{inline_index}")
3287                            }
3288                        }
3289                        OpenApiSchemaType::Number => {
3290                            if inline_index == 0 {
3291                                format!("{context_name}Number")
3292                            } else {
3293                                format!("{context_name}NumberVariant{inline_index}")
3294                            }
3295                        }
3296                        OpenApiSchemaType::Integer => {
3297                            if inline_index == 0 {
3298                                format!("{context_name}Integer")
3299                            } else {
3300                                format!("{context_name}IntegerVariant{inline_index}")
3301                            }
3302                        }
3303                        OpenApiSchemaType::Boolean => {
3304                            if inline_index == 0 {
3305                                format!("{context_name}Boolean")
3306                            } else {
3307                                format!("{context_name}BooleanVariant{inline_index}")
3308                            }
3309                        }
3310                        _ => format!("{context_name}Variant{inline_index}"),
3311                    };
3312
3313                    let rust_type =
3314                        self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
3315
3316                    // Store as a type alias
3317                    self.resolved_cache.insert(
3318                        inline_type_name.clone(),
3319                        AnalyzedSchema {
3320                            name: inline_type_name.clone(),
3321                            original: serde_json::to_value(schema).unwrap_or(Value::Null),
3322                            schema_type: SchemaType::Primitive { rust_type },
3323                            dependencies: HashSet::new(),
3324                            nullable: false,
3325                            description: schema.details().description.clone(),
3326                            default: None,
3327                        },
3328                    );
3329
3330                    // Add inline type as a dependency
3331                    dependencies.insert(inline_type_name.clone());
3332
3333                    variants.push(SchemaRef {
3334                        target: inline_type_name,
3335                        nullable: false,
3336                    });
3337                }
3338            }
3339
3340            if !variants.is_empty() {
3341                return Ok(SchemaType::Union { variants });
3342            }
3343        }
3344
3345        // Pattern 3: String enum pattern (mix of "type": "string" and const values)
3346        let all_strings = any_of_schemas.iter().all(|schema| {
3347            matches!(schema.schema_type(), Some(OpenApiSchemaType::String))
3348                || schema.details().const_value.is_some()
3349        });
3350
3351        if all_strings {
3352            // Collect all constant values as enum variants
3353            let mut enum_values = Vec::new();
3354            let mut has_open_string = false;
3355
3356            for schema in any_of_schemas {
3357                if let Some(const_val) = &schema.details().const_value {
3358                    if let Some(const_str) = const_val.as_str() {
3359                        enum_values.push(const_str.to_string());
3360                    }
3361                } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::String)) {
3362                    has_open_string = true;
3363                }
3364            }
3365
3366            if !enum_values.is_empty() {
3367                if has_open_string {
3368                    // Has both constants and open string - create an extensible enum
3369                    // This generates an enum with known variants plus a Custom(String) variant
3370                    return Ok(SchemaType::ExtensibleEnum {
3371                        known_values: enum_values,
3372                    });
3373                } else {
3374                    // All constants - create string enum
3375                    return Ok(SchemaType::StringEnum {
3376                        values: enum_values,
3377                    });
3378                }
3379            }
3380        }
3381
3382        // Pattern 4: Mixed primitives = fall back to serde_json::Value
3383        Ok(SchemaType::Primitive {
3384            rust_type: "serde_json::Value".to_string(),
3385        })
3386    }
3387
3388    /// Find the schema with $recursiveAnchor: true for resolving $recursiveRef: "#"
3389    fn find_recursive_anchor_schema(&self) -> Option<String> {
3390        // Search through all schemas to find one with $recursiveAnchor: true
3391        for (schema_name, schema) in &self.schemas {
3392            let details = schema.details();
3393            if details.recursive_anchor == Some(true) {
3394                return Some(schema_name.clone());
3395            }
3396        }
3397
3398        // If no schema has $recursiveAnchor: true, this might be an older spec
3399        // In that case, $recursiveRef: "#" typically refers to the root schema
3400        // For now, return None to indicate we couldn't resolve it
3401        None
3402    }
3403
3404    /// Detect if a schema should use serde_json::Value for dynamic JSON
3405    /// Based on structural patterns identified in real-world APIs
3406    fn should_use_dynamic_json(&self, schema: &Schema) -> bool {
3407        // Pattern 1: anyOf with [object, null] where object has no properties
3408        if let Schema::AnyOf { any_of, .. } = schema {
3409            if any_of.len() == 2 {
3410                let has_null = any_of
3411                    .iter()
3412                    .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)));
3413                let has_empty_object = any_of.iter().any(|s| self.is_dynamic_object_pattern(s));
3414
3415                if has_null && has_empty_object {
3416                    return true;
3417                }
3418            }
3419        }
3420
3421        // Pattern 2: Direct empty object pattern
3422        self.is_dynamic_object_pattern(schema)
3423    }
3424
3425    /// Check if a schema represents a dynamic object pattern
3426    fn is_dynamic_object_pattern(&self, schema: &Schema) -> bool {
3427        // Must be object type or untyped with object inference
3428        let is_object = match schema.schema_type() {
3429            Some(OpenApiSchemaType::Object) => true,
3430            None => schema.inferred_type() == Some(OpenApiSchemaType::Object),
3431            _ => false,
3432        };
3433
3434        if !is_object {
3435            return false;
3436        }
3437
3438        let details = schema.details();
3439
3440        // If it has explicit additionalProperties, it should remain as a typed object
3441        // that will be generated as BTreeMap<String, serde_json::Value> or similar
3442        if self.has_explicit_additional_properties(schema) {
3443            return false;
3444        }
3445
3446        // Pattern 1: Object with no properties at all (and no additionalProperties)
3447        let no_properties = details
3448            .properties
3449            .as_ref()
3450            .map(|props| props.is_empty())
3451            .unwrap_or(true);
3452
3453        if no_properties {
3454            // Check for constraints that would make this a structured type
3455            let has_structural_constraints =
3456                // Has required fields (other than just 'type')
3457                details.required.as_ref()
3458                    .map(|req| req.iter().any(|r| r != "type"))
3459                    .unwrap_or(false)
3460                // Has pattern-based property definitions    
3461                || details.extra.contains_key("patternProperties")
3462                // Has property name schema
3463                || details.extra.contains_key("propertyNames")
3464                // Has min/max property constraints
3465                || details.extra.contains_key("minProperties")
3466                || details.extra.contains_key("maxProperties")
3467                // Has specific property dependencies
3468                || details.extra.contains_key("dependencies")
3469                // Has conditional schemas
3470                || details.extra.contains_key("if")
3471                || details.extra.contains_key("then")
3472                || details.extra.contains_key("else");
3473
3474            return !has_structural_constraints;
3475        }
3476
3477        false
3478    }
3479
3480    /// Check if this is an object that explicitly allows arbitrary additional properties
3481    fn has_explicit_additional_properties(&self, schema: &Schema) -> bool {
3482        let details = schema.details();
3483
3484        // Check if additionalProperties is explicitly set to true or a schema
3485        matches!(
3486            &details.additional_properties,
3487            Some(crate::openapi::AdditionalProperties::Boolean(true))
3488                | Some(crate::openapi::AdditionalProperties::Schema(_))
3489        )
3490    }
3491
3492    /// Analyze OpenAPI operations to extract request/response schemas
3493    fn analyze_operations(&mut self, analysis: &mut SchemaAnalysis) -> Result<()> {
3494        let spec: crate::openapi::OpenApiSpec = serde_json::from_value(self.openapi_spec.clone())
3495            .map_err(GeneratorError::ParseError)?;
3496
3497        if let Some(paths) = &spec.paths {
3498            for (path, path_item) in paths {
3499                for (method, operation) in path_item.operations() {
3500                    // Generate operation ID if missing
3501                    let operation_id = operation
3502                        .operation_id
3503                        .clone()
3504                        .unwrap_or_else(|| Self::generate_operation_id(method, path));
3505
3506                    let op_info = self.analyze_single_operation(
3507                        &operation_id,
3508                        method,
3509                        path,
3510                        operation,
3511                        path_item.parameters.as_ref(),
3512                        analysis,
3513                    )?;
3514                    analysis.operations.insert(operation_id, op_info);
3515                }
3516            }
3517        }
3518        Ok(())
3519    }
3520
3521    /// Generate an operation ID from method and path when not provided
3522    /// Converts paths like "/v0/servers/{serverId}" + "get" to "getV0ServersServerId"
3523    fn generate_operation_id(method: &str, path: &str) -> String {
3524        // Start with the HTTP method in lowercase
3525        let mut operation_id = method.to_lowercase();
3526
3527        // Process the path: remove leading slash, split by /, convert to camelCase
3528        let path_parts: Vec<&str> = path.trim_start_matches('/').split('/').collect();
3529
3530        for part in path_parts {
3531            if part.is_empty() {
3532                continue;
3533            }
3534
3535            // Handle path parameters: {serverId} -> ServerId
3536            let cleaned_part = if part.starts_with('{') && part.ends_with('}') {
3537                &part[1..part.len() - 1]
3538            } else {
3539                part
3540            };
3541
3542            // Convert to PascalCase and append
3543            let pascal_case_part = cleaned_part
3544                .split(&['-', '_'][..])
3545                .map(|s| {
3546                    let mut chars = s.chars();
3547                    match chars.next() {
3548                        None => String::new(),
3549                        Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
3550                    }
3551                })
3552                .collect::<String>();
3553
3554            operation_id.push_str(&pascal_case_part);
3555        }
3556
3557        operation_id
3558    }
3559
3560    /// Analyze a single OpenAPI operation
3561    fn analyze_single_operation(
3562        &mut self,
3563        operation_id: &str,
3564        method: &str,
3565        path: &str,
3566        operation: &crate::openapi::Operation,
3567        path_item_parameters: Option<&Vec<crate::openapi::Parameter>>,
3568        _analysis: &mut SchemaAnalysis,
3569    ) -> Result<OperationInfo> {
3570        let mut op_info = OperationInfo {
3571            operation_id: operation_id.to_string(),
3572            method: method.to_uppercase(),
3573            path: path.to_string(),
3574            summary: operation.summary.clone(),
3575            description: operation.description.clone(),
3576            request_body: None,
3577            response_schemas: BTreeMap::new(),
3578            parameters: Vec::new(),
3579            supports_streaming: false, // Will be determined by StreamingConfig, not spec
3580            stream_parameter: None,    // Will be determined by StreamingConfig, not spec
3581        };
3582
3583        // Extract request body schema with content-type awareness
3584        if let Some(request_body) = &operation.request_body
3585            && let Some((content_type, maybe_schema)) = request_body.best_content()
3586        {
3587            use crate::openapi::{is_form_urlencoded_media_type, is_json_media_type};
3588            op_info.request_body = if is_json_media_type(content_type) {
3589                maybe_schema
3590                    .map(|s| {
3591                        self.resolve_or_inline_schema(s, operation_id, "Request")
3592                            .map(|name| RequestBodyContent::Json { schema_name: name })
3593                    })
3594                    .transpose()?
3595            } else if is_form_urlencoded_media_type(content_type) {
3596                maybe_schema
3597                    .map(|s| {
3598                        self.resolve_or_inline_schema(s, operation_id, "Request")
3599                            .map(|name| RequestBodyContent::FormUrlEncoded { schema_name: name })
3600                    })
3601                    .transpose()?
3602            } else {
3603                match content_type {
3604                    "multipart/form-data" => Some(RequestBodyContent::Multipart),
3605                    "application/octet-stream" => Some(RequestBodyContent::OctetStream),
3606                    "text/plain" => Some(RequestBodyContent::TextPlain),
3607                    _ => None,
3608                }
3609            };
3610        }
3611
3612        // Extract response schemas
3613        if let Some(responses) = &operation.responses {
3614            for (status_code, response) in responses {
3615                if let Some(schema) = response.json_schema() {
3616                    if let Some(schema_ref) = schema.reference() {
3617                        // Named schema reference
3618                        if let Some(schema_name) = self.extract_schema_name(schema_ref) {
3619                            op_info
3620                                .response_schemas
3621                                .insert(status_code.clone(), schema_name.to_string());
3622                        }
3623                    } else {
3624                        // Inline schema - generate a synthetic type name and analyze it
3625                        let synthetic_name =
3626                            self.generate_inline_response_type_name(operation_id, status_code);
3627
3628                        // Use the existing inline schema infrastructure
3629                        let mut deps = HashSet::new();
3630                        self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
3631
3632                        op_info
3633                            .response_schemas
3634                            .insert(status_code.clone(), synthetic_name);
3635                    }
3636                }
3637            }
3638        }
3639
3640        // Extract parameters (operation-level first, then merge path-item-level)
3641        if let Some(parameters) = &operation.parameters {
3642            for param in parameters {
3643                let resolved = self.resolve_parameter(param);
3644                if let Some(param_info) = self.analyze_parameter(&resolved, operation_id)? {
3645                    op_info.parameters.push(param_info);
3646                }
3647            }
3648        }
3649
3650        // Merge path-item-level parameters (operation params take precedence per OpenAPI spec)
3651        if let Some(path_params) = path_item_parameters {
3652            let existing_keys: std::collections::HashSet<(String, String)> = op_info
3653                .parameters
3654                .iter()
3655                .map(|p| (p.name.clone(), p.location.clone()))
3656                .collect();
3657            for param in path_params {
3658                let resolved = self.resolve_parameter(param);
3659                if let Some(param_info) = self.analyze_parameter(&resolved, operation_id)? {
3660                    if !existing_keys
3661                        .contains(&(param_info.name.clone(), param_info.location.clone()))
3662                    {
3663                        op_info.parameters.push(param_info);
3664                    }
3665                }
3666            }
3667        }
3668
3669        Ok(op_info)
3670    }
3671
3672    /// Generate a type name for an inline response schema.
3673    ///
3674    /// 200 (the canonical success status) keeps the unsuffixed `{Op}Response`
3675    /// name so simple specs and existing snapshots are unchanged. Every other
3676    /// status code is disambiguated by suffix so that multi-response operations
3677    /// (e.g. 200 + 400) don't collide in the schema registry — see issue #8.
3678    fn generate_inline_response_type_name(&self, operation_id: &str, status_code: &str) -> String {
3679        use heck::ToPascalCase;
3680        let base_name = operation_id.replace('.', "_").to_pascal_case();
3681        let suffix = Self::status_code_suffix(status_code);
3682        format!("{}Response{}", base_name, suffix)
3683    }
3684
3685    /// Map an OpenAPI status code key to a suffix for generated type names.
3686    ///
3687    /// "200" → "" (unchanged, the dominant case)
3688    /// "201", "400", "404" → "201", "400", "404"
3689    /// "default" → "Default"
3690    /// "4XX" / "4xx" → "4xx" (lowercased range form)
3691    fn status_code_suffix(status_code: &str) -> String {
3692        match status_code {
3693            "" | "200" => String::new(),
3694            "default" | "Default" => "Default".to_string(),
3695            other if other.chars().all(|c| c.is_ascii_digit()) => other.to_string(),
3696            other => other.to_ascii_lowercase(),
3697        }
3698    }
3699
3700    /// Generate a type name for an inline request body schema
3701    fn generate_inline_request_type_name(&self, operation_id: &str) -> String {
3702        use heck::ToPascalCase;
3703        // Convert operation_id to PascalCase and append Request
3704        // e.g., "session.prompt" -> "SessionPromptRequest"
3705        // e.g., "pty.create" -> "PtyCreateRequest"
3706        let base_name = operation_id.replace('.', "_").to_pascal_case();
3707        format!("{}Request", base_name)
3708    }
3709
3710    /// Resolve a schema reference to a name, or inline it with a synthetic name.
3711    /// `suffix` controls the generated name (e.g. "Request" or "Response").
3712    fn resolve_or_inline_schema(
3713        &mut self,
3714        schema: &crate::openapi::Schema,
3715        operation_id: &str,
3716        suffix: &str,
3717    ) -> Result<String> {
3718        if let Some(schema_ref) = schema.reference()
3719            && let Some(schema_name) = self.extract_schema_name(schema_ref)
3720        {
3721            return Ok(schema_name.to_string());
3722        }
3723        // Inline schema - generate a synthetic type name and analyze it
3724        let synthetic_name = if suffix == "Request" {
3725            self.generate_inline_request_type_name(operation_id)
3726        } else {
3727            self.generate_inline_response_type_name(operation_id, "")
3728        };
3729        let mut deps = HashSet::new();
3730        self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
3731        Ok(synthetic_name)
3732    }
3733
3734    /// Resolve a parameter reference ($ref) to the actual parameter definition.
3735    /// Returns the resolved parameter, or the original if it's not a reference.
3736    fn resolve_parameter<'a>(
3737        &'a self,
3738        param: &'a crate::openapi::Parameter,
3739    ) -> std::borrow::Cow<'a, crate::openapi::Parameter> {
3740        if let Some(ref_str) = param.extra.get("$ref").and_then(|v| v.as_str()) {
3741            if let Some(param_name) = ref_str.strip_prefix("#/components/parameters/") {
3742                if let Some(resolved) = self.component_parameters.get(param_name) {
3743                    return std::borrow::Cow::Borrowed(resolved);
3744                }
3745            }
3746        }
3747        std::borrow::Cow::Borrowed(param)
3748    }
3749
3750    /// Analyze a parameter.
3751    ///
3752    /// `operation_id` is used to generate a unique synthetic enum type name
3753    /// when the parameter's inline schema is a string with `enum` or `const`
3754    /// (e.g. `GetItemTheConstant`). The client generator emits the enum
3755    /// alongside the operation methods. See issue #10 follow-up.
3756    fn analyze_parameter(
3757        &self,
3758        param: &crate::openapi::Parameter,
3759        operation_id: &str,
3760    ) -> Result<Option<ParameterInfo>> {
3761        use heck::ToPascalCase;
3762
3763        let name = param.name.as_deref().unwrap_or("");
3764        let location = param.location.as_deref().unwrap_or("");
3765        let required = param.required.unwrap_or(false);
3766
3767        let mut rust_type = "String".to_string();
3768        let mut schema_ref = None;
3769        let mut enum_values: Option<Vec<String>> = None;
3770
3771        if let Some(schema) = &param.schema {
3772            if let Some(ref_str) = schema.reference() {
3773                schema_ref = self.extract_schema_name(ref_str).map(|s| s.to_string());
3774            } else if let Some(schema_type) = schema.schema_type() {
3775                rust_type = match schema_type {
3776                    crate::openapi::SchemaType::Boolean => "bool",
3777                    crate::openapi::SchemaType::Integer => "i64",
3778                    crate::openapi::SchemaType::Number => "f64",
3779                    crate::openapi::SchemaType::String => "String",
3780                    _ => "String",
3781                }
3782                .to_string();
3783
3784                if matches!(schema_type, crate::openapi::SchemaType::String) {
3785                    let details = schema.details();
3786                    if details.is_string_enum() {
3787                        if let Some(values) = details.string_enum_values() {
3788                            if !values.is_empty() {
3789                                let op_pascal = operation_id.replace('.', "_").to_pascal_case();
3790                                let param_pascal = name.to_pascal_case();
3791                                rust_type = format!("{op_pascal}{param_pascal}");
3792                                enum_values = Some(values);
3793                            }
3794                        }
3795                    }
3796                }
3797            }
3798        }
3799
3800        Ok(Some(ParameterInfo {
3801            name: name.to_string(),
3802            location: location.to_string(),
3803            required,
3804            schema_ref,
3805            rust_type,
3806            description: param.description.clone(),
3807            enum_values,
3808        }))
3809    }
3810}