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