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(one_of, discriminator.as_ref(), None, &mut dependencies)?
1131            }
1132            Schema::AllOf { all_of, .. } => {
1133                // Handle allOf composition (schema inheritance)
1134                self.analyze_allof_composition(all_of, &mut dependencies)?
1135            }
1136            Schema::Untyped { .. } => {
1137                // Try to infer type from structure
1138                if let Some(inferred) = schema.inferred_type() {
1139                    match inferred {
1140                        OpenApiSchemaType::Object => {
1141                            if self.should_use_dynamic_json(schema) {
1142                                SchemaType::Primitive {
1143                                    rust_type: "serde_json::Value".to_string(),
1144                                }
1145                            } else {
1146                                self.analyze_object_schema(schema, &mut dependencies)?
1147                            }
1148                        }
1149                        OpenApiSchemaType::String if details.is_string_enum() => {
1150                            SchemaType::StringEnum {
1151                                values: details.string_enum_values().unwrap_or_default(),
1152                            }
1153                        }
1154                        _ => SchemaType::Primitive {
1155                            rust_type: "serde_json::Value".to_string(),
1156                        },
1157                    }
1158                } else {
1159                    SchemaType::Primitive {
1160                        rust_type: "serde_json::Value".to_string(),
1161                    }
1162                }
1163            }
1164        };
1165
1166        Ok(AnalyzedSchema {
1167            name: schema_name.to_string(),
1168            original: serde_json::to_value(schema).unwrap_or(Value::Null), // Convert back to Value for now
1169            schema_type,
1170            dependencies,
1171            nullable,
1172            description,
1173            default: details.default.clone(),
1174        })
1175    }
1176
1177    fn analyze_object_schema(
1178        &mut self,
1179        schema: &Schema,
1180        dependencies: &mut HashSet<String>,
1181    ) -> Result<SchemaType> {
1182        let details = schema.details();
1183        let properties = &details.properties;
1184        let required = details
1185            .required
1186            .as_ref()
1187            .map(|req| req.iter().cloned().collect::<HashSet<String>>())
1188            .unwrap_or_default();
1189
1190        let mut property_info = BTreeMap::new();
1191
1192        if let Some(props) = properties {
1193            for (prop_name, prop_schema) in props {
1194                // Check if this property is a union that needs a named type
1195                let prop_type = if let Schema::AnyOf { any_of, .. } = prop_schema {
1196                    // First check if this should be a dynamic JSON pattern
1197                    if self.should_use_dynamic_json(prop_schema) {
1198                        // This is a dynamic JSON pattern, use serde_json::Value directly
1199                        SchemaType::Primitive {
1200                            rust_type: "serde_json::Value".to_string(),
1201                        }
1202                    } else {
1203                        // This is an anyOf union in a property - create a named union type
1204                        // Use the current schema name as context to make the union name unique
1205                        let context_name = self
1206                            .current_schema_name
1207                            .clone()
1208                            .unwrap_or_else(|| "Unknown".to_string());
1209
1210                        // Generate a name based on both the schema and property name
1211                        let prop_pascal = self.to_pascal_case(prop_name);
1212                        let union_type_name = format!("{context_name}{prop_pascal}");
1213
1214                        // Analyze the union
1215                        let union_schema_type = self.analyze_anyof_union(
1216                            any_of,
1217                            prop_schema.discriminator(),
1218                            dependencies,
1219                            &union_type_name,
1220                        )?;
1221
1222                        // Store the union as a named schema
1223                        self.resolved_cache.insert(
1224                            union_type_name.clone(),
1225                            AnalyzedSchema {
1226                                name: union_type_name.clone(),
1227                                original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
1228                                schema_type: union_schema_type,
1229                                dependencies: HashSet::new(),
1230                                nullable: false,
1231                                description: prop_schema.details().description.clone(),
1232                                default: None,
1233                            },
1234                        );
1235
1236                        // Return a reference to the named union type
1237                        dependencies.insert(union_type_name.clone());
1238                        SchemaType::Reference {
1239                            target: union_type_name,
1240                        }
1241                    }
1242                } else if let Schema::OneOf {
1243                    one_of,
1244                    discriminator,
1245                    ..
1246                } = prop_schema
1247                {
1248                    // Handle oneOf discriminated unions in properties
1249                    // Generate a name based on the property name
1250                    let context_name = self
1251                        .current_schema_name
1252                        .clone()
1253                        .unwrap_or_else(|| "Unknown".to_string());
1254                    let prop_pascal = self.to_pascal_case(prop_name);
1255                    let union_type_name = format!("{context_name}{prop_pascal}");
1256
1257                    // Analyze the discriminated union
1258                    let union_schema_type = self.analyze_oneof_union(
1259                        one_of,
1260                        discriminator.as_ref(),
1261                        Some(&union_type_name),
1262                        dependencies,
1263                    )?;
1264
1265                    // Store the union as a named schema
1266                    self.resolved_cache.insert(
1267                        union_type_name.clone(),
1268                        AnalyzedSchema {
1269                            name: union_type_name.clone(),
1270                            original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
1271                            schema_type: union_schema_type,
1272                            dependencies: HashSet::new(),
1273                            nullable: false,
1274                            description: prop_schema.details().description.clone(),
1275                            default: None,
1276                        },
1277                    );
1278
1279                    // Return a reference to the named union type
1280                    dependencies.insert(union_type_name.clone());
1281                    SchemaType::Reference {
1282                        target: union_type_name,
1283                    }
1284                } else {
1285                    // Regular property schema analysis - pass property name for context
1286                    self.analyze_property_schema_with_context(
1287                        prop_schema,
1288                        Some(prop_name),
1289                        dependencies,
1290                    )?
1291                };
1292
1293                let prop_details = prop_schema.details();
1294                // Check for both explicit nullable and anyOf nullable patterns
1295                let prop_nullable = prop_details.is_nullable() || prop_schema.is_nullable_pattern();
1296                let prop_description = prop_details.description.clone();
1297                let prop_default = prop_details.default.clone();
1298
1299                property_info.insert(
1300                    prop_name.clone(),
1301                    PropertyInfo {
1302                        schema_type: prop_type,
1303                        nullable: prop_nullable,
1304                        description: prop_description,
1305                        default: prop_default,
1306                        serde_attrs: Vec::new(),
1307                    },
1308                );
1309            }
1310        }
1311
1312        // Check additionalProperties setting
1313        let additional_properties = match &details.additional_properties {
1314            Some(crate::openapi::AdditionalProperties::Boolean(true)) => true,
1315            Some(crate::openapi::AdditionalProperties::Boolean(false)) => false,
1316            Some(crate::openapi::AdditionalProperties::Schema(_)) => {
1317                // For now, treat schema-based additionalProperties as true
1318                // TODO: Could analyze the schema to determine the value type
1319                true
1320            }
1321            None => false, // Default is false if not specified
1322        };
1323
1324        Ok(SchemaType::Object {
1325            properties: property_info,
1326            required,
1327            additional_properties,
1328        })
1329    }
1330
1331    fn analyze_property_schema_with_context(
1332        &mut self,
1333        schema: &Schema,
1334        property_name: Option<&str>,
1335        dependencies: &mut HashSet<String>,
1336    ) -> Result<SchemaType> {
1337        if let Some(ref_str) = self.get_any_reference(schema) {
1338            let target = if ref_str == "#" {
1339                // $recursiveRef: "#" - need to find the schema with $recursiveAnchor: true
1340                self.find_recursive_anchor_schema()
1341                    .unwrap_or_else(|| "UnknownRecursive".to_string())
1342            } else {
1343                self.extract_schema_name(ref_str)
1344                    .ok_or_else(|| GeneratorError::UnresolvedReference(ref_str.to_string()))?
1345                    .to_string()
1346            };
1347            dependencies.insert(target.clone());
1348            return Ok(SchemaType::Reference { target });
1349        }
1350
1351        if let Some(schema_type) = schema.schema_type() {
1352            match schema_type {
1353                OpenApiSchemaType::String => {
1354                    // Check if this string type has enum values
1355                    if let Some(enum_values) = schema.details().string_enum_values() {
1356                        // This is an inline enum in a property - create a named enum type
1357                        // Use the current schema name as context to make the enum name unique
1358                        let context_name = self
1359                            .current_schema_name
1360                            .clone()
1361                            .unwrap_or_else(|| "Unknown".to_string());
1362
1363                        // Generate a candidate name based on both the schema and property context.
1364                        let primary_name = if let Some(prop_name) = property_name {
1365                            // We have property name context - use it for a unique name
1366                            let prop_pascal = self.to_pascal_case(prop_name);
1367                            format!("{context_name}{prop_pascal}")
1368                        } else {
1369                            // No property name context - generate a unique name using enum values
1370                            // Use the first enum value to help make the name unique
1371                            let suffix = if !enum_values.is_empty() {
1372                                let first_value = self.to_pascal_case(&enum_values[0]);
1373                                format!("{first_value}Enum")
1374                            } else {
1375                                "StringEnum".to_string()
1376                            };
1377                            format!("{context_name}{suffix}")
1378                        };
1379
1380                        // Resolve a name that either matches an existing same-valued
1381                        // enum (dedup) or doesn't collide with a different one.
1382                        //
1383                        // Two distinct inline enums can land on the same primary
1384                        // candidate when a parent schema has a property like
1385                        // `type` that recurs at multiple nesting levels — e.g.
1386                        // Latitude.sh's `plan_data.type = ["plans"]` (the
1387                        // JSON-API resource type) and
1388                        // `plan_data.attributes.specs.drives[].type =
1389                        // ["SSD","HDD","NVME"]` both want to become
1390                        // `PlanDataType`. We must NOT silently overwrite the
1391                        // first registration: that breaks deserialization
1392                        // because both fields end up referencing whichever
1393                        // enum was processed last.
1394                        //
1395                        // Disambiguation strategy: append the PascalCase first
1396                        // enum value (`PlanDataTypeNVME` vs `PlanDataTypePlans`)
1397                        // and, if that's also claimed with different values,
1398                        // fall back to a numeric `_2`, `_3`, … suffix.
1399                        fn matches_values(existing: &AnalyzedSchema, values: &[String]) -> bool {
1400                            matches!(
1401                                &existing.schema_type,
1402                                SchemaType::StringEnum { values: existing_values }
1403                                    if existing_values == values
1404                            )
1405                        }
1406
1407                        let mut enum_type_name = primary_name.clone();
1408                        let mut should_insert = match self.resolved_cache.get(&enum_type_name) {
1409                            None => true,
1410                            Some(existing) if matches_values(existing, &enum_values) => false,
1411                            Some(_) => {
1412                                // Collision with different values — try a
1413                                // value-suffixed name first.
1414                                let suffix = enum_values
1415                                    .first()
1416                                    .map(|v| self.to_pascal_case(v))
1417                                    .unwrap_or_else(|| "Variant".to_string());
1418                                let candidate = format!("{primary_name}{suffix}");
1419
1420                                let resolved = match self.resolved_cache.get(&candidate) {
1421                                    None => Some((candidate.clone(), true)),
1422                                    Some(existing) if matches_values(existing, &enum_values) => {
1423                                        Some((candidate.clone(), false))
1424                                    }
1425                                    Some(_) => {
1426                                        // Walk a numeric suffix until we find
1427                                        // a slot that's free or matches.
1428                                        let mut found = None;
1429                                        for n in 2..1000 {
1430                                            let numbered = format!("{candidate}_{n}");
1431                                            match self.resolved_cache.get(&numbered) {
1432                                                None => {
1433                                                    found = Some((numbered, true));
1434                                                    break;
1435                                                }
1436                                                Some(existing)
1437                                                    if matches_values(existing, &enum_values) =>
1438                                                {
1439                                                    found = Some((numbered, false));
1440                                                    break;
1441                                                }
1442                                                Some(_) => continue,
1443                                            }
1444                                        }
1445                                        found
1446                                    }
1447                                };
1448
1449                                let (resolved_name, insert) = resolved.unwrap_or((candidate, true));
1450                                enum_type_name = resolved_name;
1451                                insert
1452                            }
1453                        };
1454
1455                        // Store the enum as a named schema if this is the
1456                        // first time we've seen this exact (name, values) pair.
1457                        if should_insert {
1458                            self.resolved_cache.insert(
1459                                enum_type_name.clone(),
1460                                AnalyzedSchema {
1461                                    name: enum_type_name.clone(),
1462                                    original: serde_json::to_value(schema).unwrap_or(Value::Null),
1463                                    schema_type: SchemaType::StringEnum {
1464                                        values: enum_values,
1465                                    },
1466                                    dependencies: HashSet::new(),
1467                                    nullable: false,
1468                                    description: schema.details().description.clone(),
1469                                    default: schema.details().default.clone(),
1470                                },
1471                            );
1472                            // Silence unused-write warnings when the value
1473                            // is not consulted again on this path.
1474                            let _ = &mut should_insert;
1475                        }
1476
1477                        // Return a reference to the named enum type
1478                        dependencies.insert(enum_type_name.clone());
1479                        return Ok(SchemaType::Reference {
1480                            target: enum_type_name,
1481                        });
1482                    } else {
1483                        return Ok(SchemaType::Primitive {
1484                            rust_type: "String".to_string(),
1485                        });
1486                    }
1487                }
1488                OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
1489                    let details = schema.details();
1490                    let rust_type = self.get_number_rust_type(schema_type.clone(), details);
1491                    return Ok(SchemaType::Primitive { rust_type });
1492                }
1493                OpenApiSchemaType::Boolean => {
1494                    return Ok(SchemaType::Primitive {
1495                        rust_type: "bool".to_string(),
1496                    });
1497                }
1498                OpenApiSchemaType::Array => {
1499                    // Analyze array property with context
1500                    let context_name = if let Some(prop_name) = property_name {
1501                        // Use property name for context
1502                        let prop_pascal = self.to_pascal_case(prop_name);
1503                        format!(
1504                            "{}{}",
1505                            self.current_schema_name.as_deref().unwrap_or("Unknown"),
1506                            prop_pascal
1507                        )
1508                    } else {
1509                        // Fallback to generic name
1510                        "ArrayItem".to_string()
1511                    };
1512                    return self.analyze_array_schema(schema, &context_name, dependencies);
1513                }
1514                OpenApiSchemaType::Object => {
1515                    // Check if this is a dynamic JSON object
1516                    if self.should_use_dynamic_json(schema) {
1517                        return Ok(SchemaType::Primitive {
1518                            rust_type: "serde_json::Value".to_string(),
1519                        });
1520                    }
1521                    // Inline object in property - create a named schema for it
1522                    let object_type_name = if let Some(prop_name) = property_name {
1523                        // Use property name for context
1524                        let prop_pascal = self.to_pascal_case(prop_name);
1525                        format!(
1526                            "{}{}",
1527                            self.current_schema_name.as_deref().unwrap_or("Unknown"),
1528                            prop_pascal
1529                        )
1530                    } else {
1531                        // Fallback to generic name
1532                        format!(
1533                            "{}Object",
1534                            self.current_schema_name.as_deref().unwrap_or("Unknown")
1535                        )
1536                    };
1537
1538                    // Analyze the object schema
1539                    let object_type = self.analyze_object_schema(schema, dependencies)?;
1540
1541                    // Create an analyzed schema for the inline object
1542                    let inline_schema = AnalyzedSchema {
1543                        name: object_type_name.clone(),
1544                        original: serde_json::to_value(schema).unwrap_or(Value::Null),
1545                        schema_type: object_type,
1546                        dependencies: dependencies.clone(),
1547                        nullable: false,
1548                        description: schema.details().description.clone(),
1549                        default: None,
1550                    };
1551
1552                    // Add the inline object as a named schema
1553                    self.resolved_cache
1554                        .insert(object_type_name.clone(), inline_schema);
1555                    dependencies.insert(object_type_name.clone());
1556
1557                    // Return a reference to the named schema
1558                    return Ok(SchemaType::Reference {
1559                        target: object_type_name,
1560                    });
1561                }
1562                _ => {
1563                    return Ok(SchemaType::Primitive {
1564                        rust_type: "serde_json::Value".to_string(),
1565                    });
1566                }
1567            }
1568        }
1569
1570        // Handle nullable patterns
1571        if schema.is_nullable_pattern() {
1572            if let Some(non_null) = schema.non_null_variant() {
1573                return self.analyze_property_schema_with_context(
1574                    non_null,
1575                    property_name,
1576                    dependencies,
1577                );
1578            }
1579        }
1580
1581        // Check if this should be dynamic JSON before further analysis
1582        if self.should_use_dynamic_json(schema) {
1583            return Ok(SchemaType::Primitive {
1584                rust_type: "serde_json::Value".to_string(),
1585            });
1586        }
1587
1588        // Handle allOf composition patterns
1589        if let Schema::AllOf { all_of, .. } = schema {
1590            return self.analyze_allof_composition(all_of, dependencies);
1591        }
1592
1593        // Handle union patterns (anyOf/oneOf) that weren't caught earlier
1594        if let Some(variants) = schema.union_variants() {
1595            match variants.len().cmp(&1) {
1596                std::cmp::Ordering::Equal => {
1597                    // Single variant - analyze it directly
1598                    return self.analyze_property_schema_with_context(
1599                        &variants[0],
1600                        property_name,
1601                        dependencies,
1602                    );
1603                }
1604                std::cmp::Ordering::Greater => {
1605                    // Multiple variants - try to analyze as a union
1606                    // Generate a context-aware name for the union type
1607                    let union_name = if let Some(prop_name) = property_name {
1608                        // We have property context - create a proper union name
1609                        let prop_pascal = self.to_pascal_case(prop_name);
1610                        format!(
1611                            "{}{}",
1612                            self.current_schema_name.as_deref().unwrap_or(""),
1613                            prop_pascal
1614                        )
1615                    } else {
1616                        "UnionType".to_string()
1617                    };
1618
1619                    // Check if this is a oneOf or anyOf
1620                    if let Schema::OneOf {
1621                        one_of,
1622                        discriminator,
1623                        ..
1624                    } = schema
1625                    {
1626                        // This is a oneOf - analyze it properly with potential discriminator
1627                        let oneof_result = self.analyze_oneof_union(
1628                            one_of,
1629                            discriminator.as_ref(),
1630                            Some(&union_name),
1631                            dependencies,
1632                        )?;
1633
1634                        // If we got a union type (not discriminated), we need to store it as a named type
1635                        if let SchemaType::Union {
1636                            variants: _union_variants,
1637                        } = &oneof_result
1638                        {
1639                            // Store the union as a named type in resolved_cache
1640                            self.resolved_cache.insert(
1641                                union_name.clone(),
1642                                AnalyzedSchema {
1643                                    name: union_name.clone(),
1644                                    original: serde_json::to_value(schema).unwrap_or(Value::Null),
1645                                    schema_type: oneof_result.clone(),
1646                                    dependencies: dependencies.clone(),
1647                                    nullable: false,
1648                                    description: schema.details().description.clone(),
1649                                    default: None,
1650                                },
1651                            );
1652
1653                            // Return a reference to the named union type
1654                            dependencies.insert(union_name.clone());
1655                            return Ok(SchemaType::Reference { target: union_name });
1656                        }
1657
1658                        return Ok(oneof_result);
1659                    } else if let Schema::AnyOf {
1660                        any_of,
1661                        discriminator,
1662                        ..
1663                    } = schema
1664                    {
1665                        // This is anyOf - use existing logic with discriminator support
1666                        let union_analysis = self.analyze_anyof_union(
1667                            any_of,
1668                            discriminator.as_ref(),
1669                            dependencies,
1670                            &union_name,
1671                        )?;
1672                        return Ok(union_analysis);
1673                    } else {
1674                        // This shouldn't happen, but handle gracefully
1675                        // Create a simple union from variants
1676                        let mut union_variants = Vec::new();
1677                        for variant in variants {
1678                            if let Some(ref_str) = variant.reference() {
1679                                if let Some(target) = self.extract_schema_name(ref_str) {
1680                                    dependencies.insert(target.to_string());
1681                                    union_variants.push(SchemaRef {
1682                                        target: target.to_string(),
1683                                        nullable: false,
1684                                    });
1685                                }
1686                            }
1687                        }
1688                        return Ok(SchemaType::Union {
1689                            variants: union_variants,
1690                        });
1691                    }
1692                }
1693                std::cmp::Ordering::Less => {}
1694            }
1695        }
1696
1697        // Handle untyped schemas by trying to infer from structure
1698        if let Some(inferred_type) = schema.inferred_type() {
1699            match inferred_type {
1700                OpenApiSchemaType::Object => {
1701                    // Double-check for dynamic JSON pattern even for inferred objects
1702                    if self.should_use_dynamic_json(schema) {
1703                        return Ok(SchemaType::Primitive {
1704                            rust_type: "serde_json::Value".to_string(),
1705                        });
1706                    }
1707                    return self.analyze_object_schema(schema, dependencies);
1708                }
1709                OpenApiSchemaType::Array => {
1710                    let context_name = if let Some(prop_name) = property_name {
1711                        // Use property name for context
1712                        let prop_pascal = self.to_pascal_case(prop_name);
1713                        format!(
1714                            "{}{}",
1715                            self.current_schema_name.as_deref().unwrap_or("Unknown"),
1716                            prop_pascal
1717                        )
1718                    } else {
1719                        // Fallback to generic name
1720                        "ArrayItem".to_string()
1721                    };
1722                    return self.analyze_array_schema(schema, &context_name, dependencies);
1723                }
1724                OpenApiSchemaType::String => {
1725                    if let Some(enum_values) = schema.details().string_enum_values() {
1726                        return Ok(SchemaType::StringEnum {
1727                            values: enum_values,
1728                        });
1729                    } else {
1730                        return Ok(SchemaType::Primitive {
1731                            rust_type: "String".to_string(),
1732                        });
1733                    }
1734                }
1735                _ => {
1736                    // Handle other inferred types
1737                    let rust_type = self.openapi_type_to_rust_type(inferred_type, schema.details());
1738                    return Ok(SchemaType::Primitive { rust_type });
1739                }
1740            }
1741        }
1742
1743        Ok(SchemaType::Primitive {
1744            rust_type: "serde_json::Value".to_string(),
1745        })
1746    }
1747
1748    fn analyze_allof_composition(
1749        &mut self,
1750        all_of_schemas: &[Schema],
1751        dependencies: &mut HashSet<String>,
1752    ) -> Result<SchemaType> {
1753        // Special case: if allOf contains only a single reference, treat it as a direct type alias
1754        // This handles patterns like: "allOf": [{"$ref": "#/components/schemas/Usage"}]
1755        if all_of_schemas.len() == 1 {
1756            if let Schema::Reference { reference, .. } = &all_of_schemas[0] {
1757                if let Some(target) = self.extract_schema_name(reference) {
1758                    dependencies.insert(target.to_string());
1759                    return Ok(SchemaType::Reference {
1760                        target: target.to_string(),
1761                    });
1762                }
1763            }
1764        }
1765
1766        // AllOf represents schema composition - merge all schemas into one
1767        let mut merged_properties = BTreeMap::new();
1768        let mut merged_required = HashSet::new();
1769        let mut descriptions = Vec::new();
1770
1771        // Save the current schema context to restore it when analyzing properties
1772        let current_context = self.current_schema_name.clone();
1773
1774        for schema in all_of_schemas {
1775            match schema {
1776                Schema::Reference { reference, .. } => {
1777                    // Add dependency on referenced schema
1778                    if let Some(target) = self.extract_schema_name(reference) {
1779                        dependencies.insert(target.to_string());
1780
1781                        // First ensure the referenced schema is analyzed
1782                        let analyzed_ref = self.analyze_schema(target)?;
1783
1784                        // Now merge the analyzed schema's properties
1785                        match &analyzed_ref.schema_type {
1786                            SchemaType::Object {
1787                                properties,
1788                                required,
1789                                ..
1790                            } => {
1791                                // Merge properties from the analyzed schema
1792                                for (prop_name, prop_info) in properties {
1793                                    merged_properties.insert(prop_name.clone(), prop_info.clone());
1794                                }
1795                                // Merge required fields
1796                                for req in required {
1797                                    merged_required.insert(req.clone());
1798                                }
1799                            }
1800                            _ => {
1801                                // If the referenced schema is not an object, fall back to raw merge
1802                                if let Some(ref_schema) = self.schemas.get(target).cloned() {
1803                                    self.merge_schema_into_properties(
1804                                        &ref_schema,
1805                                        &mut merged_properties,
1806                                        &mut merged_required,
1807                                        dependencies,
1808                                    )?;
1809                                }
1810                            }
1811                        }
1812                    }
1813                }
1814                Schema::Typed {
1815                    schema_type: OpenApiSchemaType::Object,
1816                    ..
1817                }
1818                | Schema::Untyped { .. } => {
1819                    // Restore the original context when analyzing inline properties
1820                    let saved_context = self.current_schema_name.clone();
1821                    self.current_schema_name = current_context.clone();
1822
1823                    // Merge object properties directly
1824                    self.merge_schema_into_properties(
1825                        schema,
1826                        &mut merged_properties,
1827                        &mut merged_required,
1828                        dependencies,
1829                    )?;
1830
1831                    // Restore the previous context
1832                    self.current_schema_name = saved_context;
1833                }
1834                _ => {
1835                    // For non-object typed schemas in allOf, try to merge them as well
1836                    // This handles cases like allOf with enum or string constraints
1837                    self.merge_schema_into_properties(
1838                        schema,
1839                        &mut merged_properties,
1840                        &mut merged_required,
1841                        dependencies,
1842                    )?;
1843                }
1844            }
1845
1846            // Collect descriptions
1847            if let Some(desc) = &schema.details().description {
1848                descriptions.push(desc.clone());
1849            }
1850        }
1851
1852        // If we successfully merged properties, return an object
1853        if !merged_properties.is_empty() {
1854            Ok(SchemaType::Object {
1855                properties: merged_properties,
1856                required: merged_required,
1857                additional_properties: false,
1858            })
1859        } else {
1860            // Fall back to composition if we couldn't merge
1861            Ok(SchemaType::Composition {
1862                schemas: all_of_schemas
1863                    .iter()
1864                    .filter_map(|s| {
1865                        if let Some(ref_str) = s.reference() {
1866                            if let Some(target) = self.extract_schema_name(ref_str) {
1867                                dependencies.insert(target.to_string());
1868                                Some(SchemaRef {
1869                                    target: target.to_string(),
1870                                    nullable: false,
1871                                })
1872                            } else {
1873                                None
1874                            }
1875                        } else {
1876                            None
1877                        }
1878                    })
1879                    .collect(),
1880            })
1881        }
1882    }
1883
1884    fn merge_schema_into_properties(
1885        &mut self,
1886        schema: &Schema,
1887        merged_properties: &mut BTreeMap<String, PropertyInfo>,
1888        merged_required: &mut HashSet<String>,
1889        dependencies: &mut HashSet<String>,
1890    ) -> Result<()> {
1891        let details = schema.details();
1892
1893        // Merge properties
1894        if let Some(properties) = &details.properties {
1895            for (prop_name, prop_schema) in properties {
1896                let prop_type = self.analyze_property_schema_with_context(
1897                    prop_schema,
1898                    Some(prop_name),
1899                    dependencies,
1900                )?;
1901                let prop_details = prop_schema.details();
1902
1903                merged_properties.insert(
1904                    prop_name.clone(),
1905                    PropertyInfo {
1906                        schema_type: prop_type,
1907                        nullable: prop_details.is_nullable(),
1908                        description: prop_details.description.clone(),
1909                        default: prop_details.default.clone(),
1910                        serde_attrs: Vec::new(),
1911                    },
1912                );
1913            }
1914        }
1915
1916        // Merge required fields
1917        if let Some(required) = &details.required {
1918            for field in required {
1919                merged_required.insert(field.clone());
1920            }
1921        }
1922
1923        Ok(())
1924    }
1925
1926    fn analyze_oneof_union(
1927        &mut self,
1928        one_of_schemas: &[Schema],
1929        discriminator: Option<&crate::openapi::Discriminator>,
1930        parent_name: Option<&str>,
1931        dependencies: &mut HashSet<String>,
1932    ) -> Result<SchemaType> {
1933        // If there's no discriminator, we should create an untagged union
1934        if discriminator.is_none() {
1935            // Handle untagged unions (oneOf without discriminator)
1936            return self.analyze_untagged_oneof_union(one_of_schemas, parent_name, dependencies);
1937        }
1938
1939        // This is a discriminated union
1940        let discriminator_field = discriminator
1941            .ok_or_else(|| {
1942                GeneratorError::InvalidDiscriminator(
1943                    "expected discriminator after guard check".to_string(),
1944                )
1945            })?
1946            .property_name
1947            .clone();
1948
1949        let mut variants = Vec::new();
1950        let mut used_variant_names = std::collections::HashSet::new();
1951
1952        for variant_schema in one_of_schemas {
1953            // Check if this is a direct reference, recursive reference, or an allOf wrapper with a reference
1954            let ref_info = if let Some(ref_str) = variant_schema.reference() {
1955                Some((ref_str, false))
1956            } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
1957                Some((recursive_ref, true))
1958            } else if let Schema::AllOf { all_of, .. } = variant_schema {
1959                // Check if this is an allOf with a single reference
1960                if all_of.len() == 1 {
1961                    if let Some(ref_str) = all_of[0].reference() {
1962                        Some((ref_str, false))
1963                    } else {
1964                        all_of[0]
1965                            .recursive_reference()
1966                            .map(|recursive_ref| (recursive_ref, true))
1967                    }
1968                } else {
1969                    None
1970                }
1971            } else {
1972                None
1973            };
1974
1975            if let Some((ref_str, is_recursive)) = ref_info {
1976                let schema_name = if is_recursive && ref_str == "#" {
1977                    // Handle recursive reference to the schema with recursiveAnchor
1978                    self.find_recursive_anchor_schema()
1979                        .or_else(|| self.current_schema_name.clone())
1980                        .unwrap_or_else(|| "CompoundFilter".to_string())
1981                } else {
1982                    self.extract_schema_name(ref_str)
1983                        .map(|s| s.to_string())
1984                        .unwrap_or_else(|| "UnknownRef".to_string())
1985                };
1986
1987                if !schema_name.is_empty() {
1988                    dependencies.insert(schema_name.clone());
1989
1990                    // Determine discriminator value with priority order:
1991                    // 1. Explicit mapping in discriminator
1992                    // 2. Extract from referenced schema
1993                    // 3. Generate from schema name
1994                    let discriminator_value = if let Some(disc) = discriminator {
1995                        if let Some(mappings) = &disc.mapping {
1996                            // Find the mapping key that points to this schema reference
1997                            // Mapping format is: "discriminator_value" -> "#/components/schemas/SchemaName"
1998                            mappings
1999                                .iter()
2000                                .find(|(_, target_ref)| {
2001                                    // Check if this mapping target matches our reference
2002                                    target_ref.as_str() == ref_str
2003                                        || self
2004                                            .extract_schema_name(target_ref)
2005                                            .map(|s| s.to_string())
2006                                            == Some(schema_name.clone())
2007                                })
2008                                .map(|(key, _)| key.clone())
2009                                .unwrap_or_else(|| {
2010                                    self.fallback_discriminator_value_for_field(
2011                                        &schema_name,
2012                                        &discriminator_field,
2013                                    )
2014                                })
2015                        } else {
2016                            self.fallback_discriminator_value_for_field(
2017                                &schema_name,
2018                                &discriminator_field,
2019                            )
2020                        }
2021                    } else {
2022                        self.fallback_discriminator_value_for_field(
2023                            &schema_name,
2024                            &discriminator_field,
2025                        )
2026                    };
2027
2028                    // Generate Rust-friendly variant name and ensure uniqueness
2029                    let base_name = self.to_rust_variant_name(&schema_name);
2030                    let rust_name =
2031                        self.ensure_unique_variant_name(base_name, &mut used_variant_names);
2032
2033                    // Use the discriminator value as-is from the schema
2034                    let final_discriminator_value = discriminator_value;
2035
2036                    variants.push(UnionVariant {
2037                        rust_name,
2038                        type_name: schema_name,
2039                        discriminator_value: final_discriminator_value,
2040                        schema_ref: ref_str.to_string(),
2041                    });
2042                }
2043            } else {
2044                // Handle inline schemas in oneOf
2045                let variant_index = variants.len();
2046                let inline_type_name =
2047                    self.generate_inline_type_name(variant_schema, variant_index);
2048
2049                // Try to extract discriminator value from inline schema
2050                let discriminator_value = if let Some(disc) = discriminator {
2051                    if let Some(mappings) = &disc.mapping {
2052                        // Look for mapping that points to this inline variant by index
2053                        mappings
2054                            .iter()
2055                            .find(|(_, target_ref)| {
2056                                target_ref.contains(&format!("variant_{variant_index}"))
2057                            })
2058                            .map(|(key, _)| key.clone())
2059                            .unwrap_or_else(|| {
2060                                self.extract_inline_discriminator_value(
2061                                    variant_schema,
2062                                    &discriminator_field,
2063                                    variant_index,
2064                                )
2065                            })
2066                    } else {
2067                        self.extract_inline_discriminator_value(
2068                            variant_schema,
2069                            &discriminator_field,
2070                            variant_index,
2071                        )
2072                    }
2073                } else {
2074                    self.extract_inline_discriminator_value(
2075                        variant_schema,
2076                        &discriminator_field,
2077                        variant_index,
2078                    )
2079                };
2080
2081                // Generate Rust-friendly variant name based on discriminator or fallback to generic
2082                let base_name = if discriminator_value.starts_with("variant_") {
2083                    format!("Variant{variant_index}")
2084                } else {
2085                    // Convert discriminator value to a meaningful Rust variant name
2086                    let clean_name = self.discriminator_to_variant_name(&discriminator_value);
2087                    self.to_rust_variant_name(&clean_name)
2088                };
2089                let rust_name = self.ensure_unique_variant_name(base_name, &mut used_variant_names);
2090
2091                // Use the discriminator value as-is from the schema
2092                let final_discriminator_value = discriminator_value;
2093
2094                variants.push(UnionVariant {
2095                    rust_name,
2096                    type_name: inline_type_name.clone(),
2097                    discriminator_value: final_discriminator_value,
2098                    schema_ref: format!("inline_{variant_index}"),
2099                });
2100
2101                // Store inline schema for later analysis and generation
2102                self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
2103            }
2104        }
2105
2106        if variants.is_empty() {
2107            // If we couldn't create a discriminated union, fall back to an untagged union
2108            // This handles cases where oneOf contains references or inline schemas without proper discriminators
2109            let mut union_variants = Vec::new();
2110
2111            for (variant_index, variant_schema) in one_of_schemas.iter().enumerate() {
2112                // First check if it's a reference or recursive reference
2113                if let Some(ref_str) = variant_schema.reference() {
2114                    if let Some(schema_name) = self.extract_schema_name(ref_str) {
2115                        dependencies.insert(schema_name.to_string());
2116                        union_variants.push(SchemaRef {
2117                            target: schema_name.to_string(),
2118                            nullable: false,
2119                        });
2120                    }
2121                } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2122                    let schema_name = if recursive_ref == "#" {
2123                        // Handle recursive reference to the schema with recursiveAnchor
2124                        self.find_recursive_anchor_schema()
2125                            .or_else(|| self.current_schema_name.clone())
2126                            .unwrap_or_else(|| "CompoundFilter".to_string())
2127                    } else {
2128                        self.extract_schema_name(recursive_ref)
2129                            .map(|s| s.to_string())
2130                            .unwrap_or_else(|| "RecursiveType".to_string())
2131                    };
2132                    dependencies.insert(schema_name.clone());
2133                    union_variants.push(SchemaRef {
2134                        target: schema_name,
2135                        nullable: false,
2136                    });
2137                } else {
2138                    // Handle inline schemas by creating type aliases or using primitive types directly
2139                    let context = parent_name.unwrap_or("Union");
2140                    let inline_name = self.generate_context_aware_name(
2141                        context,
2142                        "InlineVariant",
2143                        variant_index,
2144                        Some(variant_schema),
2145                    );
2146                    let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
2147                    let variant_type = analyzed.schema_type;
2148
2149                    // Add dependencies from the analyzed schema
2150                    for dep in &analyzed.dependencies {
2151                        dependencies.insert(dep.clone());
2152                    }
2153
2154                    match &variant_type {
2155                        // For primitive types, we can use them directly in the union
2156                        SchemaType::Primitive { rust_type } => {
2157                            union_variants.push(SchemaRef {
2158                                target: rust_type.clone(),
2159                                nullable: false,
2160                            });
2161                        }
2162                        // For arrays, check if we can determine the item type
2163                        SchemaType::Array { item_type } => {
2164                            match item_type.as_ref() {
2165                                SchemaType::Primitive { rust_type } => {
2166                                    let type_name = format!("Vec<{rust_type}>");
2167                                    union_variants.push(SchemaRef {
2168                                        target: type_name,
2169                                        nullable: false,
2170                                    });
2171                                }
2172                                SchemaType::Reference { target } => {
2173                                    let type_name = format!("Vec<{target}>");
2174                                    union_variants.push(SchemaRef {
2175                                        target: type_name,
2176                                        nullable: false,
2177                                    });
2178                                }
2179                                _ => {
2180                                    // For other array types, create an inline type
2181                                    let context = parent_name.unwrap_or("Inline");
2182                                    let inline_type_name = self.generate_context_aware_name(
2183                                        context,
2184                                        "Variant",
2185                                        variant_index,
2186                                        None,
2187                                    );
2188                                    self.add_inline_schema(
2189                                        &inline_type_name,
2190                                        variant_schema,
2191                                        dependencies,
2192                                    )?;
2193                                    union_variants.push(SchemaRef {
2194                                        target: inline_type_name,
2195                                        nullable: false,
2196                                    });
2197                                }
2198                            }
2199                        }
2200                        // For reference types, use the reference target directly
2201                        SchemaType::Reference { target } => {
2202                            union_variants.push(SchemaRef {
2203                                target: target.clone(),
2204                                nullable: false,
2205                            });
2206                        }
2207                        // For other complex types, create an inline type
2208                        _ => {
2209                            let inline_type_name = format!(
2210                                "{}Variant{}",
2211                                parent_name.unwrap_or("Inline"),
2212                                variant_index + 1
2213                            );
2214                            self.add_inline_schema(
2215                                &inline_type_name,
2216                                variant_schema,
2217                                dependencies,
2218                            )?;
2219                            union_variants.push(SchemaRef {
2220                                target: inline_type_name,
2221                                nullable: false,
2222                            });
2223                        }
2224                    }
2225                }
2226            }
2227
2228            if !union_variants.is_empty() {
2229                return Ok(SchemaType::Union {
2230                    variants: union_variants,
2231                });
2232            }
2233
2234            // Only fall back to serde_json::Value if we truly can't analyze the union
2235            return Ok(SchemaType::Primitive {
2236                rust_type: "serde_json::Value".to_string(),
2237            });
2238        }
2239
2240        Ok(SchemaType::DiscriminatedUnion {
2241            discriminator_field,
2242            variants,
2243        })
2244    }
2245
2246    fn analyze_untagged_oneof_union(
2247        &mut self,
2248        one_of_schemas: &[Schema],
2249        parent_name: Option<&str>,
2250        dependencies: &mut HashSet<String>,
2251    ) -> Result<SchemaType> {
2252        let mut union_variants = Vec::new();
2253
2254        for (variant_index, variant_schema) in one_of_schemas.iter().enumerate() {
2255            // First check if it's a reference or recursive reference
2256            if let Some(ref_str) = variant_schema.reference() {
2257                if let Some(schema_name) = self.extract_schema_name(ref_str) {
2258                    dependencies.insert(schema_name.to_string());
2259                    union_variants.push(SchemaRef {
2260                        target: schema_name.to_string(),
2261                        nullable: false,
2262                    });
2263                }
2264            } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2265                let schema_name = if recursive_ref == "#" {
2266                    // Handle recursive reference to the schema with recursiveAnchor
2267                    self.find_recursive_anchor_schema()
2268                        .or_else(|| self.current_schema_name.clone())
2269                        .unwrap_or_else(|| "CompoundFilter".to_string())
2270                } else {
2271                    self.extract_schema_name(recursive_ref)
2272                        .map(|s| s.to_string())
2273                        .unwrap_or_else(|| "RecursiveType".to_string())
2274                };
2275                dependencies.insert(schema_name.clone());
2276                union_variants.push(SchemaRef {
2277                    target: schema_name,
2278                    nullable: false,
2279                });
2280            } else {
2281                // Handle inline schemas by creating type aliases or using primitive types directly
2282                let context = parent_name.unwrap_or("Union");
2283                let inline_name = self.generate_context_aware_name(
2284                    context,
2285                    "InlineVariant",
2286                    variant_index,
2287                    Some(variant_schema),
2288                );
2289                let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
2290                let variant_type = analyzed.schema_type;
2291
2292                // Add dependencies from the analyzed schema
2293                for dep in &analyzed.dependencies {
2294                    dependencies.insert(dep.clone());
2295                }
2296
2297                match &variant_type {
2298                    // For primitive types, we can use them directly in the union
2299                    SchemaType::Primitive { rust_type } => {
2300                        union_variants.push(SchemaRef {
2301                            target: rust_type.clone(),
2302                            nullable: false,
2303                        });
2304                    }
2305                    // For arrays, check if we can determine the item type
2306                    SchemaType::Array { item_type } => {
2307                        match item_type.as_ref() {
2308                            SchemaType::Primitive { rust_type } => {
2309                                let type_name = format!("Vec<{rust_type}>");
2310                                union_variants.push(SchemaRef {
2311                                    target: type_name,
2312                                    nullable: false,
2313                                });
2314                            }
2315                            SchemaType::Reference { target } => {
2316                                let type_name = format!("Vec<{target}>");
2317                                union_variants.push(SchemaRef {
2318                                    target: type_name,
2319                                    nullable: false,
2320                                });
2321                            }
2322                            // Handle arrays of arrays (e.g., Vec<Vec<i64>>)
2323                            SchemaType::Array {
2324                                item_type: inner_item_type,
2325                            } => {
2326                                match inner_item_type.as_ref() {
2327                                    SchemaType::Primitive { rust_type } => {
2328                                        let type_name = format!("Vec<Vec<{rust_type}>>");
2329                                        union_variants.push(SchemaRef {
2330                                            target: type_name,
2331                                            nullable: false,
2332                                        });
2333                                    }
2334                                    SchemaType::Reference { target } => {
2335                                        let type_name = format!("Vec<Vec<{target}>>");
2336                                        union_variants.push(SchemaRef {
2337                                            target: type_name,
2338                                            nullable: false,
2339                                        });
2340                                    }
2341                                    _ => {
2342                                        // For deeper nesting, create an inline type
2343                                        let context = parent_name.unwrap_or("Inline");
2344                                        let inline_type_name = self.generate_context_aware_name(
2345                                            context,
2346                                            "Variant",
2347                                            variant_index,
2348                                            None,
2349                                        );
2350                                        self.add_inline_schema(
2351                                            &inline_type_name,
2352                                            variant_schema,
2353                                            dependencies,
2354                                        )?;
2355                                        union_variants.push(SchemaRef {
2356                                            target: inline_type_name,
2357                                            nullable: false,
2358                                        });
2359                                    }
2360                                }
2361                            }
2362                            _ => {
2363                                // For other array types, create an inline type
2364                                let context = parent_name.unwrap_or("Inline");
2365                                let inline_type_name = self.generate_context_aware_name(
2366                                    context,
2367                                    "Variant",
2368                                    variant_index,
2369                                    None,
2370                                );
2371                                self.add_inline_schema(
2372                                    &inline_type_name,
2373                                    variant_schema,
2374                                    dependencies,
2375                                )?;
2376                                union_variants.push(SchemaRef {
2377                                    target: inline_type_name,
2378                                    nullable: false,
2379                                });
2380                            }
2381                        }
2382                    }
2383                    // For reference types, use the reference target directly
2384                    SchemaType::Reference { target } => {
2385                        union_variants.push(SchemaRef {
2386                            target: target.clone(),
2387                            nullable: false,
2388                        });
2389                    }
2390                    // For other complex types, create an inline type
2391                    _ => {
2392                        let context = parent_name.unwrap_or("Inline");
2393                        let inline_type_name = self.generate_context_aware_name(
2394                            context,
2395                            "Variant",
2396                            variant_index,
2397                            None,
2398                        );
2399                        self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
2400                        union_variants.push(SchemaRef {
2401                            target: inline_type_name,
2402                            nullable: false,
2403                        });
2404                    }
2405                }
2406            }
2407        }
2408
2409        if !union_variants.is_empty() {
2410            return Ok(SchemaType::Union {
2411                variants: union_variants,
2412            });
2413        }
2414
2415        // Only fall back to serde_json::Value if we truly can't analyze the union
2416        Ok(SchemaType::Primitive {
2417            rust_type: "serde_json::Value".to_string(),
2418        })
2419    }
2420
2421    fn add_inline_schema(
2422        &mut self,
2423        type_name: &str,
2424        schema: &Schema,
2425        dependencies: &mut HashSet<String>,
2426    ) -> Result<()> {
2427        // For primitive types, we need to ensure they are stored as type aliases
2428        if let Some(schema_type) = schema.schema_type() {
2429            match schema_type {
2430                OpenApiSchemaType::String
2431                | OpenApiSchemaType::Integer
2432                | OpenApiSchemaType::Number
2433                | OpenApiSchemaType::Boolean => {
2434                    let rust_type =
2435                        self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
2436
2437                    // Store as a type alias
2438                    self.resolved_cache.insert(
2439                        type_name.to_string(),
2440                        AnalyzedSchema {
2441                            name: type_name.to_string(),
2442                            original: serde_json::to_value(schema).unwrap_or(Value::Null),
2443                            schema_type: SchemaType::Primitive { rust_type },
2444                            dependencies: HashSet::new(),
2445                            nullable: false,
2446                            description: schema.details().description.clone(),
2447                            default: None,
2448                        },
2449                    );
2450                    return Ok(());
2451                }
2452                _ => {}
2453            }
2454        }
2455
2456        // For non-primitive types, analyze the inline schema and add it to our collection
2457        // Set current_schema_name so nested inline properties (enums, unions, objects)
2458        // get named with the correct parent context instead of inheriting a stale name
2459        let previous_schema_name = self.current_schema_name.take();
2460        self.current_schema_name = Some(type_name.to_string());
2461        let analyzed = self.analyze_schema_value(schema, type_name)?;
2462        self.current_schema_name = previous_schema_name;
2463
2464        // Add to resolved cache so it can be generated
2465        self.resolved_cache.insert(type_name.to_string(), analyzed);
2466
2467        // Add dependencies
2468        if let Some(cached) = self.resolved_cache.get(type_name) {
2469            for dep in &cached.dependencies {
2470                dependencies.insert(dep.clone());
2471            }
2472        }
2473
2474        Ok(())
2475    }
2476
2477    fn extract_inline_discriminator_value(
2478        &self,
2479        schema: &Schema,
2480        discriminator_field: &str,
2481        variant_index: usize,
2482    ) -> String {
2483        // Try to extract discriminator value from inline schema properties
2484        if let Some(properties) = &schema.details().properties {
2485            if let Some(discriminator_prop) = properties.get(discriminator_field) {
2486                // Check for enum with single value
2487                if let Some(enum_values) = &discriminator_prop.details().enum_values {
2488                    if enum_values.len() == 1 {
2489                        if let Some(value) = enum_values[0].as_str() {
2490                            return value.to_string();
2491                        }
2492                    }
2493                }
2494                // Check for const value in extra fields
2495                if let Some(const_value) = discriminator_prop.details().extra.get("const") {
2496                    if let Some(value) = const_value.as_str() {
2497                        return value.to_string();
2498                    }
2499                }
2500                // Check for const value in the discriminator_prop.details().const_value
2501                if let Some(const_value) = &discriminator_prop.details().const_value {
2502                    if let Some(value) = const_value.as_str() {
2503                        return value.to_string();
2504                    }
2505                }
2506            }
2507        }
2508
2509        // Try to infer from schema structure and properties
2510        if let Some(inferred_name) = self.infer_variant_name_from_structure(schema, variant_index) {
2511            return inferred_name;
2512        }
2513
2514        // Fall back to generic variant name
2515        format!("variant_{variant_index}")
2516    }
2517
2518    fn infer_variant_name_from_structure(
2519        &self,
2520        schema: &Schema,
2521        _variant_index: usize,
2522    ) -> Option<String> {
2523        let details = schema.details();
2524
2525        // Strategy 1: Look for unique property combinations that suggest the variant type
2526        if let Some(properties) = &details.properties {
2527            // Common patterns for content blocks
2528            if properties.contains_key("text") && properties.len() <= 3 {
2529                return Some("text".to_string());
2530            }
2531            if properties.contains_key("image") || properties.contains_key("source") {
2532                return Some("image".to_string());
2533            }
2534            if properties.contains_key("document") {
2535                return Some("document".to_string());
2536            }
2537            if properties.contains_key("tool_use_id") || properties.contains_key("tool_result") {
2538                return Some("tool_result".to_string());
2539            }
2540            if properties.contains_key("content") && properties.contains_key("is_error") {
2541                return Some("tool_result".to_string());
2542            }
2543            if properties.contains_key("partial_json") {
2544                return Some("partial_json".to_string());
2545            }
2546
2547            // Strategy 2: Look for properties that hint at the variant purpose
2548            let property_names: Vec<&String> = properties.keys().collect();
2549
2550            // Try to find the most descriptive property name
2551            for prop_name in &property_names {
2552                if prop_name.contains("result") {
2553                    return Some("result".to_string());
2554                }
2555                if prop_name.contains("error") {
2556                    return Some("error".to_string());
2557                }
2558                if prop_name.contains("content") && property_names.len() <= 2 {
2559                    return Some("content".to_string());
2560                }
2561            }
2562
2563            // Strategy 3: Use the most significant unique property
2564            let significant_props = property_names
2565                .iter()
2566                .filter(|&name| !["type", "id", "cache_control"].contains(&name.as_str()))
2567                .collect::<Vec<_>>();
2568
2569            if significant_props.len() == 1 {
2570                return Some((*significant_props[0]).clone());
2571            }
2572        }
2573
2574        // Strategy 4: Look at description for hints
2575        if let Some(description) = &details.description {
2576            let desc_lower = description.to_lowercase();
2577            if desc_lower.contains("text") && desc_lower.len() < 100 {
2578                return Some("text".to_string());
2579            }
2580            if desc_lower.contains("image") {
2581                return Some("image".to_string());
2582            }
2583            if desc_lower.contains("document") {
2584                return Some("document".to_string());
2585            }
2586            if desc_lower.contains("tool") && desc_lower.contains("result") {
2587                return Some("tool_result".to_string());
2588            }
2589        }
2590
2591        None
2592    }
2593
2594    fn discriminator_to_variant_name(&self, discriminator: &str) -> String {
2595        // Convert discriminator values to PascalCase variant names using general rules
2596        if discriminator.is_empty() {
2597            return "Variant".to_string();
2598        }
2599
2600        let mut result = String::new();
2601        let mut next_upper = true;
2602
2603        for c in discriminator.chars() {
2604            match c {
2605                'a'..='z' => {
2606                    if next_upper {
2607                        result.push(c.to_ascii_uppercase());
2608                        next_upper = false;
2609                    } else {
2610                        result.push(c);
2611                    }
2612                }
2613                'A'..='Z' => {
2614                    result.push(c);
2615                    next_upper = false;
2616                }
2617                '0'..='9' => {
2618                    result.push(c);
2619                    next_upper = false;
2620                }
2621                '_' | '-' | '.' | ' ' | '/' | '\\' => {
2622                    // Word separators - next char should be uppercase
2623                    next_upper = true;
2624                }
2625                _ => {
2626                    // Other special characters - treat as word boundary
2627                    next_upper = true;
2628                }
2629            }
2630        }
2631
2632        // Ensure it starts with a letter
2633        if result.is_empty() || result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2634            result = format!("Variant{result}");
2635        }
2636
2637        result
2638    }
2639
2640    fn ensure_unique_variant_name(
2641        &self,
2642        base_name: String,
2643        used_names: &mut std::collections::HashSet<String>,
2644    ) -> String {
2645        let mut candidate = base_name.clone();
2646        let mut counter = 1;
2647
2648        while used_names.contains(&candidate) {
2649            counter += 1;
2650            candidate = format!("{base_name}{counter}");
2651        }
2652
2653        used_names.insert(candidate.clone());
2654        candidate
2655    }
2656
2657    fn generate_inline_type_name(&self, schema: &Schema, variant_index: usize) -> String {
2658        // Try to generate a meaningful name for inline schemas
2659        if let Some(meaningful_name) = self.infer_type_name_from_structure(schema) {
2660            return meaningful_name;
2661        }
2662
2663        // Fallback to context-aware name
2664        let context = self.current_schema_name.as_deref().unwrap_or("Inline");
2665        self.generate_context_aware_name(context, "Variant", variant_index, Some(schema))
2666    }
2667
2668    fn infer_type_name_from_structure(&self, schema: &Schema) -> Option<String> {
2669        let details = schema.details();
2670
2671        // Strategy 1: Use description if it's short and descriptive
2672        if let Some(description) = &details.description {
2673            if let Some(name_from_desc) = self.extract_type_name_from_description(description) {
2674                return Some(name_from_desc);
2675            }
2676        }
2677
2678        // Strategy 2: Use the most significant property name as the type identifier
2679        if let Some(properties) = &details.properties {
2680            if let Some(name_from_props) = self.extract_type_name_from_properties(properties) {
2681                return Some(format!("{name_from_props}Block"));
2682            }
2683        }
2684
2685        None
2686    }
2687
2688    fn extract_type_name_from_description(&self, description: &str) -> Option<String> {
2689        // Only use descriptions that are short and likely to be type identifiers
2690        if description.len() > 100 || description.contains('\n') {
2691            return None;
2692        }
2693
2694        // Extract the first meaningful word(s) from the description
2695        let words: Vec<&str> = description
2696            .split_whitespace()
2697            .take(2) // Only take first 2 words to avoid long names
2698            .filter(|word| {
2699                let w = word.to_lowercase();
2700                word.len() > 2
2701                    && ![
2702                        "the", "and", "for", "with", "that", "this", "are", "can", "will", "was",
2703                    ]
2704                    .contains(&w.as_str())
2705            })
2706            .collect();
2707
2708        if words.is_empty() {
2709            return None;
2710        }
2711
2712        // Convert to PascalCase using our existing logic
2713        let combined = words.join("_");
2714        let pascal_name = self.discriminator_to_variant_name(&combined);
2715
2716        // Add suffix if it doesn't already have one
2717        if !pascal_name.ends_with("Content")
2718            && !pascal_name.ends_with("Block")
2719            && !pascal_name.ends_with("Type")
2720        {
2721            Some(format!("{pascal_name}Content"))
2722        } else {
2723            Some(pascal_name)
2724        }
2725    }
2726
2727    fn extract_type_name_from_properties(
2728        &self,
2729        properties: &std::collections::BTreeMap<String, crate::openapi::Schema>,
2730    ) -> Option<String> {
2731        // Get property names, excluding common structural properties
2732        let significant_props: Vec<&String> = properties
2733            .keys()
2734            .filter(|name| !["type", "id", "cache_control"].contains(&name.as_str()))
2735            .collect();
2736
2737        if significant_props.is_empty() {
2738            return None;
2739        }
2740
2741        // Strategy 1: If there's only one significant property, use it
2742        if significant_props.len() == 1 {
2743            let prop_name = significant_props[0];
2744            return Some(self.discriminator_to_variant_name(prop_name));
2745        }
2746
2747        // Strategy 2: Use the first property alphabetically for consistency
2748        // This provides deterministic naming without hardcoded preferences
2749        let mut sorted_props = significant_props.clone();
2750        sorted_props.sort();
2751        if let Some(first_prop) = sorted_props.first() {
2752            return Some(self.discriminator_to_variant_name(first_prop));
2753        }
2754
2755        None
2756    }
2757
2758    fn openapi_type_to_rust_type(
2759        &self,
2760        openapi_type: OpenApiSchemaType,
2761        details: &crate::openapi::SchemaDetails,
2762    ) -> String {
2763        match openapi_type {
2764            OpenApiSchemaType::String => "String".to_string(),
2765            OpenApiSchemaType::Integer => self.get_number_rust_type(openapi_type, details),
2766            OpenApiSchemaType::Number => self.get_number_rust_type(openapi_type, details),
2767            OpenApiSchemaType::Boolean => "bool".to_string(),
2768            OpenApiSchemaType::Array => "Vec<serde_json::Value>".to_string(), // Fallback for arrays without items
2769            OpenApiSchemaType::Object => "serde_json::Value".to_string(), // Fallback for untyped objects
2770            OpenApiSchemaType::Null => "()".to_string(),                  // Null type
2771        }
2772    }
2773
2774    #[allow(dead_code)]
2775    fn fallback_discriminator_value(&self, schema_name: &str) -> String {
2776        self.fallback_discriminator_value_for_field(schema_name, "type")
2777    }
2778
2779    fn fallback_discriminator_value_for_field(
2780        &self,
2781        schema_name: &str,
2782        field_name: &str,
2783    ) -> String {
2784        // Try to extract from referenced schema first
2785        if let Some(ref_schema) = self.schemas.get(schema_name) {
2786            if let Some(extracted) =
2787                self.extract_discriminator_value_for_field(ref_schema, field_name)
2788            {
2789                return extracted;
2790            }
2791        }
2792
2793        // Fall back to generating from name
2794        self.generate_discriminator_value_from_name(schema_name)
2795    }
2796
2797    fn generate_discriminator_value_from_name(&self, schema_name: &str) -> String {
2798        // Convert schema names like "ResponseCreatedEvent" to "response.created"
2799        let mut result = String::new();
2800        let mut chars = schema_name.chars().peekable();
2801        let mut first = true;
2802
2803        while let Some(c) = chars.next() {
2804            if c.is_uppercase()
2805                && !first
2806                && chars
2807                    .peek()
2808                    .map(|&next| next.is_lowercase())
2809                    .unwrap_or(false)
2810            {
2811                result.push('.');
2812            }
2813            result.push(c.to_ascii_lowercase());
2814            first = false;
2815        }
2816
2817        // Remove common suffixes
2818        if result.ends_with("event") {
2819            result = result[..result.len() - 5].to_string();
2820        }
2821
2822        // Add "response." prefix if it looks like a response event
2823        if schema_name.starts_with("Response") && !result.starts_with("response.") {
2824            result = format!("response.{}", result.trim_start_matches("response"));
2825        }
2826
2827        result
2828    }
2829
2830    fn to_rust_variant_name(&self, schema_name: &str) -> String {
2831        // Convert "ResponseCreatedEvent" to "Created", "UserStatus" to "UserStatus", etc.
2832        let mut name = schema_name;
2833
2834        // Remove common prefixes for cleaner variant names
2835        if name.starts_with("Response") && name.len() > 8 {
2836            name = &name[8..]; // Remove "Response"
2837        }
2838
2839        // Remove common suffixes
2840        if name.ends_with("Event") && name.len() > 5 {
2841            name = &name[..name.len() - 5]; // Remove "Event"
2842        }
2843
2844        // Trim leading and trailing underscores
2845        name = name.trim_matches('_');
2846
2847        // Convert underscores to camel case using our existing function
2848        if name.is_empty() {
2849            schema_name.to_string()
2850        } else {
2851            // Use discriminator_to_variant_name to properly handle underscores
2852            self.discriminator_to_variant_name(name)
2853        }
2854    }
2855
2856    fn analyze_array_schema(
2857        &mut self,
2858        schema: &Schema,
2859        parent_schema_name: &str,
2860        dependencies: &mut HashSet<String>,
2861    ) -> Result<SchemaType> {
2862        let details = schema.details();
2863
2864        // Check if items field is present
2865        if let Some(items_schema) = &details.items {
2866            // Analyze the item type
2867            let item_type = match items_schema.as_ref() {
2868                Schema::Reference { reference, .. } => {
2869                    // Array of referenced types
2870                    let target = self
2871                        .extract_schema_name(reference)
2872                        .ok_or_else(|| GeneratorError::UnresolvedReference(reference.to_string()))?
2873                        .to_string();
2874                    dependencies.insert(target.clone());
2875                    SchemaType::Reference { target }
2876                }
2877                Schema::RecursiveRef { recursive_ref, .. } => {
2878                    // Array of recursive references
2879                    if recursive_ref == "#" {
2880                        // Self-reference to the current schema
2881                        let target = self
2882                            .find_recursive_anchor_schema()
2883                            .unwrap_or_else(|| parent_schema_name.to_string());
2884                        dependencies.insert(target.clone());
2885                        SchemaType::Reference { target }
2886                    } else {
2887                        let target = self
2888                            .extract_schema_name(recursive_ref)
2889                            .unwrap_or("RecursiveType")
2890                            .to_string();
2891                        dependencies.insert(target.clone());
2892                        SchemaType::Reference { target }
2893                    }
2894                }
2895                Schema::Typed { schema_type, .. } => {
2896                    // Array of primitive types
2897                    match schema_type {
2898                        OpenApiSchemaType::String => SchemaType::Primitive {
2899                            rust_type: "String".to_string(),
2900                        },
2901                        OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
2902                            let details = items_schema.details();
2903                            let rust_type = self.get_number_rust_type(schema_type.clone(), details);
2904                            SchemaType::Primitive { rust_type }
2905                        }
2906                        OpenApiSchemaType::Boolean => SchemaType::Primitive {
2907                            rust_type: "bool".to_string(),
2908                        },
2909                        OpenApiSchemaType::Object => {
2910                            // Inline object in array - create a named schema for it
2911                            let object_type_name = format!("{parent_schema_name}Item");
2912
2913                            // Analyze the object schema
2914                            let object_type =
2915                                self.analyze_object_schema(items_schema, dependencies)?;
2916
2917                            // Create an analyzed schema for the inline object
2918                            let inline_schema = AnalyzedSchema {
2919                                name: object_type_name.clone(),
2920                                original: serde_json::to_value(items_schema).unwrap_or(Value::Null),
2921                                schema_type: object_type,
2922                                dependencies: dependencies.clone(),
2923                                nullable: false,
2924                                description: items_schema.details().description.clone(),
2925                                default: None,
2926                            };
2927
2928                            // Add the inline object as a named schema
2929                            self.resolved_cache
2930                                .insert(object_type_name.clone(), inline_schema);
2931                            dependencies.insert(object_type_name.clone());
2932
2933                            // Return a reference to the named schema
2934                            SchemaType::Reference {
2935                                target: object_type_name,
2936                            }
2937                        }
2938                        OpenApiSchemaType::Array => {
2939                            // Array of arrays - recursively analyze
2940                            self.analyze_array_schema(
2941                                items_schema,
2942                                parent_schema_name,
2943                                dependencies,
2944                            )?
2945                        }
2946                        _ => SchemaType::Primitive {
2947                            rust_type: "serde_json::Value".to_string(),
2948                        },
2949                    }
2950                }
2951                Schema::OneOf { .. } | Schema::AnyOf { .. } => {
2952                    // Union types in arrays - analyze recursively
2953                    let analyzed = self.analyze_schema_value(items_schema, "ArrayItem")?;
2954
2955                    // If we got a discriminated union or union, we need to create a separate schema for it
2956                    match &analyzed.schema_type {
2957                        SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => {
2958                            // Generate a unique name for the union schema based on the parent context
2959                            // Use the parent context directly to maintain consistent naming
2960                            let union_name = format!("{parent_schema_name}ItemUnion");
2961
2962                            // Create a new analyzed schema with the correct name
2963                            let mut union_schema = analyzed;
2964                            union_schema.name = union_name.clone();
2965
2966                            // Add the union as a separate schema
2967                            self.resolved_cache.insert(union_name.clone(), union_schema);
2968
2969                            // Add dependency
2970                            dependencies.insert(union_name.clone());
2971
2972                            // Return a reference to the union schema
2973                            SchemaType::Reference { target: union_name }
2974                        }
2975                        _ => analyzed.schema_type,
2976                    }
2977                }
2978                Schema::Untyped { .. } => {
2979                    // Try to infer the type
2980                    if let Some(inferred) = items_schema.inferred_type() {
2981                        match inferred {
2982                            OpenApiSchemaType::Object => {
2983                                // Inline object in array - create a named schema for it
2984                                let object_type_name = format!("{parent_schema_name}Item");
2985
2986                                // Analyze the object schema
2987                                let object_type =
2988                                    self.analyze_object_schema(items_schema, dependencies)?;
2989
2990                                // Create an analyzed schema for the inline object
2991                                let inline_schema = AnalyzedSchema {
2992                                    name: object_type_name.clone(),
2993                                    original: serde_json::to_value(items_schema)
2994                                        .unwrap_or(Value::Null),
2995                                    schema_type: object_type,
2996                                    dependencies: dependencies.clone(),
2997                                    nullable: false,
2998                                    description: items_schema.details().description.clone(),
2999                                    default: None,
3000                                };
3001
3002                                // Add the inline object as a named schema
3003                                self.resolved_cache
3004                                    .insert(object_type_name.clone(), inline_schema);
3005                                dependencies.insert(object_type_name.clone());
3006
3007                                // Return a reference to the named schema
3008                                SchemaType::Reference {
3009                                    target: object_type_name,
3010                                }
3011                            }
3012                            OpenApiSchemaType::String => SchemaType::Primitive {
3013                                rust_type: "String".to_string(),
3014                            },
3015                            OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
3016                                let details = items_schema.details();
3017                                let rust_type = self.get_number_rust_type(inferred, details);
3018                                SchemaType::Primitive { rust_type }
3019                            }
3020                            OpenApiSchemaType::Boolean => SchemaType::Primitive {
3021                                rust_type: "bool".to_string(),
3022                            },
3023                            _ => SchemaType::Primitive {
3024                                rust_type: "serde_json::Value".to_string(),
3025                            },
3026                        }
3027                    } else {
3028                        SchemaType::Primitive {
3029                            rust_type: "serde_json::Value".to_string(),
3030                        }
3031                    }
3032                }
3033                _ => SchemaType::Primitive {
3034                    rust_type: "serde_json::Value".to_string(),
3035                },
3036            };
3037
3038            Ok(SchemaType::Array {
3039                item_type: Box::new(item_type),
3040            })
3041        } else {
3042            // No items specified, fall back to generic array
3043            Ok(SchemaType::Primitive {
3044                rust_type: "Vec<serde_json::Value>".to_string(),
3045            })
3046        }
3047    }
3048
3049    fn get_number_rust_type(
3050        &self,
3051        schema_type: OpenApiSchemaType,
3052        details: &crate::openapi::SchemaDetails,
3053    ) -> String {
3054        match schema_type {
3055            OpenApiSchemaType::Integer => {
3056                // Check format field for integer types
3057                match details.format.as_deref() {
3058                    Some("int32") => "i32".to_string(),
3059                    Some("int64") => "i64".to_string(),
3060                    _ => "i64".to_string(), // Default for integer
3061                }
3062            }
3063            OpenApiSchemaType::Number => {
3064                // Check format field for number types
3065                match details.format.as_deref() {
3066                    Some("float") => "f32".to_string(),
3067                    Some("double") => "f64".to_string(),
3068                    _ => "f64".to_string(), // Default for number
3069                }
3070            }
3071            _ => "serde_json::Value".to_string(), // Fallback
3072        }
3073    }
3074
3075    fn analyze_anyof_union(
3076        &mut self,
3077        any_of_schemas: &[Schema],
3078        discriminator: Option<&Discriminator>,
3079        dependencies: &mut HashSet<String>,
3080        context_name: &str,
3081    ) -> Result<SchemaType> {
3082        // Analyze the semantics of this anyOf
3083
3084        // Pattern 1: Nullable type [Type, null]
3085        if any_of_schemas.len() == 2 {
3086            let null_count = any_of_schemas
3087                .iter()
3088                .filter(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
3089                .count();
3090            if null_count == 1 {
3091                // This is a nullable pattern - find the non-null type
3092                for schema in any_of_schemas {
3093                    if !matches!(schema.schema_type(), Some(OpenApiSchemaType::Null)) {
3094                        // For nullable pattern, return the non-null type directly
3095                        // The nullable information is handled at the property level
3096                        return self
3097                            .analyze_schema_value(schema, context_name)
3098                            .map(|a| a.schema_type);
3099                    }
3100                }
3101            }
3102        }
3103
3104        // Pattern 2: Multiple complex types or mixed primitive/complex = flexible union
3105        let has_refs = any_of_schemas.iter().any(|s| s.is_reference());
3106        let has_objects = any_of_schemas.iter().any(|s| {
3107            matches!(s.schema_type(), Some(OpenApiSchemaType::Object))
3108                || s.inferred_type() == Some(OpenApiSchemaType::Object)
3109        });
3110        let has_arrays = any_of_schemas
3111            .iter()
3112            .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Array)));
3113
3114        // Handle mixed primitive and complex types (like string + array of objects)
3115        // Skip this pattern if all schemas are strings or const values (handle in pattern 3)
3116        let all_string_like = any_of_schemas.iter().all(|s| {
3117            matches!(s.schema_type(), Some(OpenApiSchemaType::String))
3118                || s.details().const_value.is_some()
3119        });
3120
3121        if (has_refs || has_objects || has_arrays || any_of_schemas.len() > 1) && !all_string_like {
3122            // Check if this is a discriminated union
3123            if let Some(disc) = discriminator {
3124                // This is a discriminated anyOf union, analyze it the same way as oneOf
3125                return self.analyze_oneof_union(any_of_schemas, Some(disc), None, dependencies);
3126            }
3127
3128            // Auto-detect implicit discriminator from const fields across all variants
3129            if let Some(disc_field) = self.detect_discriminator_field(any_of_schemas) {
3130                return self.analyze_oneof_union(
3131                    any_of_schemas,
3132                    Some(&Discriminator {
3133                        property_name: disc_field,
3134                        mapping: None,
3135                        extra: BTreeMap::new(),
3136                    }),
3137                    None,
3138                    dependencies,
3139                );
3140            }
3141
3142            // Create an untagged union for flexible matching
3143            let mut variants = Vec::new();
3144
3145            for schema in any_of_schemas {
3146                if let Some(ref_str) = schema.reference() {
3147                    if let Some(target) = self.extract_schema_name(ref_str) {
3148                        dependencies.insert(target.to_string());
3149                        variants.push(SchemaRef {
3150                            target: target.to_string(),
3151                            nullable: false,
3152                        });
3153                    }
3154                } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object))
3155                    || schema.inferred_type() == Some(OpenApiSchemaType::Object)
3156                {
3157                    // Generate inline object type for anyOf union
3158                    let inline_index = variants.len();
3159                    let inline_type_name = self.generate_inline_type_name(schema, inline_index);
3160
3161                    // Store inline schema for later analysis and generation
3162                    self.add_inline_schema(&inline_type_name, schema, dependencies)?;
3163
3164                    variants.push(SchemaRef {
3165                        target: inline_type_name,
3166                        nullable: false,
3167                    });
3168                } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Array)) {
3169                    // Handle array types in unions by creating a type alias
3170                    let array_type =
3171                        self.analyze_array_schema(schema, context_name, dependencies)?;
3172
3173                    // Create a unique name for this array type in the union
3174                    let array_type_name = if let Some(items_schema) = &schema.details().items {
3175                        if let Some(ref_str) = items_schema.reference() {
3176                            if let Some(item_type_name) = self.extract_schema_name(ref_str) {
3177                                dependencies.insert(item_type_name.to_string());
3178                                format!("{item_type_name}Array")
3179                            } else {
3180                                self.generate_context_aware_name(
3181                                    context_name,
3182                                    "Array",
3183                                    variants.len(),
3184                                    Some(schema),
3185                                )
3186                            }
3187                        } else {
3188                            self.generate_context_aware_name(
3189                                context_name,
3190                                "Array",
3191                                variants.len(),
3192                                Some(schema),
3193                            )
3194                        }
3195                    } else {
3196                        self.generate_context_aware_name(
3197                            context_name,
3198                            "Array",
3199                            variants.len(),
3200                            Some(schema),
3201                        )
3202                    };
3203
3204                    // Store the array as a type alias
3205                    self.resolved_cache.insert(
3206                        array_type_name.clone(),
3207                        AnalyzedSchema {
3208                            name: array_type_name.clone(),
3209                            original: serde_json::to_value(schema).unwrap_or(Value::Null),
3210                            schema_type: array_type,
3211                            dependencies: HashSet::new(),
3212                            nullable: false,
3213                            description: Some("Array variant in union".to_string()),
3214                            default: None,
3215                        },
3216                    );
3217
3218                    // Add array type as a dependency
3219                    dependencies.insert(array_type_name.clone());
3220
3221                    variants.push(SchemaRef {
3222                        target: array_type_name,
3223                        nullable: false,
3224                    });
3225                } else if let Some(schema_type) = schema.schema_type() {
3226                    // Handle primitive types by creating type aliases for consistency
3227                    let inline_index = variants.len();
3228
3229                    // Generate a better name for primitive types
3230                    let inline_type_name = match schema_type {
3231                        OpenApiSchemaType::String => {
3232                            // For string types, check if we can infer a better name from context
3233                            // If this is the first variant and it's a string, use a simple name
3234                            if inline_index == 0 {
3235                                format!("{context_name}String")
3236                            } else {
3237                                format!("{context_name}StringVariant{inline_index}")
3238                            }
3239                        }
3240                        OpenApiSchemaType::Number => {
3241                            if inline_index == 0 {
3242                                format!("{context_name}Number")
3243                            } else {
3244                                format!("{context_name}NumberVariant{inline_index}")
3245                            }
3246                        }
3247                        OpenApiSchemaType::Integer => {
3248                            if inline_index == 0 {
3249                                format!("{context_name}Integer")
3250                            } else {
3251                                format!("{context_name}IntegerVariant{inline_index}")
3252                            }
3253                        }
3254                        OpenApiSchemaType::Boolean => {
3255                            if inline_index == 0 {
3256                                format!("{context_name}Boolean")
3257                            } else {
3258                                format!("{context_name}BooleanVariant{inline_index}")
3259                            }
3260                        }
3261                        _ => format!("{context_name}Variant{inline_index}"),
3262                    };
3263
3264                    let rust_type =
3265                        self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
3266
3267                    // Store as a type alias
3268                    self.resolved_cache.insert(
3269                        inline_type_name.clone(),
3270                        AnalyzedSchema {
3271                            name: inline_type_name.clone(),
3272                            original: serde_json::to_value(schema).unwrap_or(Value::Null),
3273                            schema_type: SchemaType::Primitive { rust_type },
3274                            dependencies: HashSet::new(),
3275                            nullable: false,
3276                            description: schema.details().description.clone(),
3277                            default: None,
3278                        },
3279                    );
3280
3281                    // Add inline type as a dependency
3282                    dependencies.insert(inline_type_name.clone());
3283
3284                    variants.push(SchemaRef {
3285                        target: inline_type_name,
3286                        nullable: false,
3287                    });
3288                }
3289            }
3290
3291            if !variants.is_empty() {
3292                return Ok(SchemaType::Union { variants });
3293            }
3294        }
3295
3296        // Pattern 3: String enum pattern (mix of "type": "string" and const values)
3297        let all_strings = any_of_schemas.iter().all(|schema| {
3298            matches!(schema.schema_type(), Some(OpenApiSchemaType::String))
3299                || schema.details().const_value.is_some()
3300        });
3301
3302        if all_strings {
3303            // Collect all constant values as enum variants
3304            let mut enum_values = Vec::new();
3305            let mut has_open_string = false;
3306
3307            for schema in any_of_schemas {
3308                if let Some(const_val) = &schema.details().const_value {
3309                    if let Some(const_str) = const_val.as_str() {
3310                        enum_values.push(const_str.to_string());
3311                    }
3312                } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::String)) {
3313                    has_open_string = true;
3314                }
3315            }
3316
3317            if !enum_values.is_empty() {
3318                if has_open_string {
3319                    // Has both constants and open string - create an extensible enum
3320                    // This generates an enum with known variants plus a Custom(String) variant
3321                    return Ok(SchemaType::ExtensibleEnum {
3322                        known_values: enum_values,
3323                    });
3324                } else {
3325                    // All constants - create string enum
3326                    return Ok(SchemaType::StringEnum {
3327                        values: enum_values,
3328                    });
3329                }
3330            }
3331        }
3332
3333        // Pattern 4: Mixed primitives = fall back to serde_json::Value
3334        Ok(SchemaType::Primitive {
3335            rust_type: "serde_json::Value".to_string(),
3336        })
3337    }
3338
3339    /// Find the schema with $recursiveAnchor: true for resolving $recursiveRef: "#"
3340    fn find_recursive_anchor_schema(&self) -> Option<String> {
3341        // Search through all schemas to find one with $recursiveAnchor: true
3342        for (schema_name, schema) in &self.schemas {
3343            let details = schema.details();
3344            if details.recursive_anchor == Some(true) {
3345                return Some(schema_name.clone());
3346            }
3347        }
3348
3349        // If no schema has $recursiveAnchor: true, this might be an older spec
3350        // In that case, $recursiveRef: "#" typically refers to the root schema
3351        // For now, return None to indicate we couldn't resolve it
3352        None
3353    }
3354
3355    /// Detect if a schema should use serde_json::Value for dynamic JSON
3356    /// Based on structural patterns identified in real-world APIs
3357    fn should_use_dynamic_json(&self, schema: &Schema) -> bool {
3358        // Pattern 1: anyOf with [object, null] where object has no properties
3359        if let Schema::AnyOf { any_of, .. } = schema {
3360            if any_of.len() == 2 {
3361                let has_null = any_of
3362                    .iter()
3363                    .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)));
3364                let has_empty_object = any_of.iter().any(|s| self.is_dynamic_object_pattern(s));
3365
3366                if has_null && has_empty_object {
3367                    return true;
3368                }
3369            }
3370        }
3371
3372        // Pattern 2: Direct empty object pattern
3373        self.is_dynamic_object_pattern(schema)
3374    }
3375
3376    /// Check if a schema represents a dynamic object pattern
3377    fn is_dynamic_object_pattern(&self, schema: &Schema) -> bool {
3378        // Must be object type or untyped with object inference
3379        let is_object = match schema.schema_type() {
3380            Some(OpenApiSchemaType::Object) => true,
3381            None => schema.inferred_type() == Some(OpenApiSchemaType::Object),
3382            _ => false,
3383        };
3384
3385        if !is_object {
3386            return false;
3387        }
3388
3389        let details = schema.details();
3390
3391        // If it has explicit additionalProperties, it should remain as a typed object
3392        // that will be generated as BTreeMap<String, serde_json::Value> or similar
3393        if self.has_explicit_additional_properties(schema) {
3394            return false;
3395        }
3396
3397        // Pattern 1: Object with no properties at all (and no additionalProperties)
3398        let no_properties = details
3399            .properties
3400            .as_ref()
3401            .map(|props| props.is_empty())
3402            .unwrap_or(true);
3403
3404        if no_properties {
3405            // Check for constraints that would make this a structured type
3406            let has_structural_constraints =
3407                // Has required fields (other than just 'type')
3408                details.required.as_ref()
3409                    .map(|req| req.iter().any(|r| r != "type"))
3410                    .unwrap_or(false)
3411                // Has pattern-based property definitions    
3412                || details.extra.contains_key("patternProperties")
3413                // Has property name schema
3414                || details.extra.contains_key("propertyNames")
3415                // Has min/max property constraints
3416                || details.extra.contains_key("minProperties")
3417                || details.extra.contains_key("maxProperties")
3418                // Has specific property dependencies
3419                || details.extra.contains_key("dependencies")
3420                // Has conditional schemas
3421                || details.extra.contains_key("if")
3422                || details.extra.contains_key("then")
3423                || details.extra.contains_key("else");
3424
3425            return !has_structural_constraints;
3426        }
3427
3428        false
3429    }
3430
3431    /// Check if this is an object that explicitly allows arbitrary additional properties
3432    fn has_explicit_additional_properties(&self, schema: &Schema) -> bool {
3433        let details = schema.details();
3434
3435        // Check if additionalProperties is explicitly set to true or a schema
3436        matches!(
3437            &details.additional_properties,
3438            Some(crate::openapi::AdditionalProperties::Boolean(true))
3439                | Some(crate::openapi::AdditionalProperties::Schema(_))
3440        )
3441    }
3442
3443    /// Analyze OpenAPI operations to extract request/response schemas
3444    fn analyze_operations(&mut self, analysis: &mut SchemaAnalysis) -> Result<()> {
3445        let spec: crate::openapi::OpenApiSpec = serde_json::from_value(self.openapi_spec.clone())
3446            .map_err(GeneratorError::ParseError)?;
3447
3448        if let Some(paths) = &spec.paths {
3449            for (path, path_item) in paths {
3450                for (method, operation) in path_item.operations() {
3451                    // Generate operation ID if missing
3452                    let operation_id = operation
3453                        .operation_id
3454                        .clone()
3455                        .unwrap_or_else(|| Self::generate_operation_id(method, path));
3456
3457                    let op_info = self.analyze_single_operation(
3458                        &operation_id,
3459                        method,
3460                        path,
3461                        operation,
3462                        path_item.parameters.as_ref(),
3463                        analysis,
3464                    )?;
3465                    analysis.operations.insert(operation_id, op_info);
3466                }
3467            }
3468        }
3469        Ok(())
3470    }
3471
3472    /// Generate an operation ID from method and path when not provided
3473    /// Converts paths like "/v0/servers/{serverId}" + "get" to "getV0ServersServerId"
3474    fn generate_operation_id(method: &str, path: &str) -> String {
3475        // Start with the HTTP method in lowercase
3476        let mut operation_id = method.to_lowercase();
3477
3478        // Process the path: remove leading slash, split by /, convert to camelCase
3479        let path_parts: Vec<&str> = path.trim_start_matches('/').split('/').collect();
3480
3481        for part in path_parts {
3482            if part.is_empty() {
3483                continue;
3484            }
3485
3486            // Handle path parameters: {serverId} -> ServerId
3487            let cleaned_part = if part.starts_with('{') && part.ends_with('}') {
3488                &part[1..part.len() - 1]
3489            } else {
3490                part
3491            };
3492
3493            // Convert to PascalCase and append
3494            let pascal_case_part = cleaned_part
3495                .split(&['-', '_'][..])
3496                .map(|s| {
3497                    let mut chars = s.chars();
3498                    match chars.next() {
3499                        None => String::new(),
3500                        Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
3501                    }
3502                })
3503                .collect::<String>();
3504
3505            operation_id.push_str(&pascal_case_part);
3506        }
3507
3508        operation_id
3509    }
3510
3511    /// Analyze a single OpenAPI operation
3512    fn analyze_single_operation(
3513        &mut self,
3514        operation_id: &str,
3515        method: &str,
3516        path: &str,
3517        operation: &crate::openapi::Operation,
3518        path_item_parameters: Option<&Vec<crate::openapi::Parameter>>,
3519        _analysis: &mut SchemaAnalysis,
3520    ) -> Result<OperationInfo> {
3521        let mut op_info = OperationInfo {
3522            operation_id: operation_id.to_string(),
3523            method: method.to_uppercase(),
3524            path: path.to_string(),
3525            summary: operation.summary.clone(),
3526            description: operation.description.clone(),
3527            request_body: None,
3528            response_schemas: BTreeMap::new(),
3529            parameters: Vec::new(),
3530            supports_streaming: false, // Will be determined by StreamingConfig, not spec
3531            stream_parameter: None,    // Will be determined by StreamingConfig, not spec
3532        };
3533
3534        // Extract request body schema with content-type awareness
3535        if let Some(request_body) = &operation.request_body
3536            && let Some((content_type, maybe_schema)) = request_body.best_content()
3537        {
3538            use crate::openapi::{is_form_urlencoded_media_type, is_json_media_type};
3539            op_info.request_body = if is_json_media_type(content_type) {
3540                maybe_schema
3541                    .map(|s| {
3542                        self.resolve_or_inline_schema(s, operation_id, "Request")
3543                            .map(|name| RequestBodyContent::Json { schema_name: name })
3544                    })
3545                    .transpose()?
3546            } else if is_form_urlencoded_media_type(content_type) {
3547                maybe_schema
3548                    .map(|s| {
3549                        self.resolve_or_inline_schema(s, operation_id, "Request")
3550                            .map(|name| RequestBodyContent::FormUrlEncoded { schema_name: name })
3551                    })
3552                    .transpose()?
3553            } else {
3554                match content_type {
3555                    "multipart/form-data" => Some(RequestBodyContent::Multipart),
3556                    "application/octet-stream" => Some(RequestBodyContent::OctetStream),
3557                    "text/plain" => Some(RequestBodyContent::TextPlain),
3558                    _ => None,
3559                }
3560            };
3561        }
3562
3563        // Extract response schemas
3564        if let Some(responses) = &operation.responses {
3565            for (status_code, response) in responses {
3566                if let Some(schema) = response.json_schema() {
3567                    if let Some(schema_ref) = schema.reference() {
3568                        // Named schema reference
3569                        if let Some(schema_name) = self.extract_schema_name(schema_ref) {
3570                            op_info
3571                                .response_schemas
3572                                .insert(status_code.clone(), schema_name.to_string());
3573                        }
3574                    } else {
3575                        // Inline schema - generate a synthetic type name and analyze it
3576                        let synthetic_name =
3577                            self.generate_inline_response_type_name(operation_id, status_code);
3578
3579                        // Use the existing inline schema infrastructure
3580                        let mut deps = HashSet::new();
3581                        self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
3582
3583                        op_info
3584                            .response_schemas
3585                            .insert(status_code.clone(), synthetic_name);
3586                    }
3587                }
3588            }
3589        }
3590
3591        // Extract parameters (operation-level first, then merge path-item-level)
3592        if let Some(parameters) = &operation.parameters {
3593            for param in parameters {
3594                let resolved = self.resolve_parameter(param);
3595                if let Some(param_info) = self.analyze_parameter(&resolved)? {
3596                    op_info.parameters.push(param_info);
3597                }
3598            }
3599        }
3600
3601        // Merge path-item-level parameters (operation params take precedence per OpenAPI spec)
3602        if let Some(path_params) = path_item_parameters {
3603            let existing_keys: std::collections::HashSet<(String, String)> = op_info
3604                .parameters
3605                .iter()
3606                .map(|p| (p.name.clone(), p.location.clone()))
3607                .collect();
3608            for param in path_params {
3609                let resolved = self.resolve_parameter(param);
3610                if let Some(param_info) = self.analyze_parameter(&resolved)? {
3611                    if !existing_keys
3612                        .contains(&(param_info.name.clone(), param_info.location.clone()))
3613                    {
3614                        op_info.parameters.push(param_info);
3615                    }
3616                }
3617            }
3618        }
3619
3620        Ok(op_info)
3621    }
3622
3623    /// Generate a type name for an inline response schema
3624    fn generate_inline_response_type_name(&self, operation_id: &str, _status_code: &str) -> String {
3625        use heck::ToPascalCase;
3626        // Convert operation_id to PascalCase and append Response
3627        // e.g., "app.skills" -> "AppSkillsResponse"
3628        // e.g., "getUser" + "200" -> "GetUserResponse"
3629        let base_name = operation_id.replace('.', "_").to_pascal_case();
3630        format!("{}Response", base_name)
3631    }
3632
3633    /// Generate a type name for an inline request body schema
3634    fn generate_inline_request_type_name(&self, operation_id: &str) -> String {
3635        use heck::ToPascalCase;
3636        // Convert operation_id to PascalCase and append Request
3637        // e.g., "session.prompt" -> "SessionPromptRequest"
3638        // e.g., "pty.create" -> "PtyCreateRequest"
3639        let base_name = operation_id.replace('.', "_").to_pascal_case();
3640        format!("{}Request", base_name)
3641    }
3642
3643    /// Resolve a schema reference to a name, or inline it with a synthetic name.
3644    /// `suffix` controls the generated name (e.g. "Request" or "Response").
3645    fn resolve_or_inline_schema(
3646        &mut self,
3647        schema: &crate::openapi::Schema,
3648        operation_id: &str,
3649        suffix: &str,
3650    ) -> Result<String> {
3651        if let Some(schema_ref) = schema.reference()
3652            && let Some(schema_name) = self.extract_schema_name(schema_ref)
3653        {
3654            return Ok(schema_name.to_string());
3655        }
3656        // Inline schema - generate a synthetic type name and analyze it
3657        let synthetic_name = if suffix == "Request" {
3658            self.generate_inline_request_type_name(operation_id)
3659        } else {
3660            self.generate_inline_response_type_name(operation_id, "")
3661        };
3662        let mut deps = HashSet::new();
3663        self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
3664        Ok(synthetic_name)
3665    }
3666
3667    /// Resolve a parameter reference ($ref) to the actual parameter definition.
3668    /// Returns the resolved parameter, or the original if it's not a reference.
3669    fn resolve_parameter<'a>(
3670        &'a self,
3671        param: &'a crate::openapi::Parameter,
3672    ) -> std::borrow::Cow<'a, crate::openapi::Parameter> {
3673        if let Some(ref_str) = param.extra.get("$ref").and_then(|v| v.as_str()) {
3674            if let Some(param_name) = ref_str.strip_prefix("#/components/parameters/") {
3675                if let Some(resolved) = self.component_parameters.get(param_name) {
3676                    return std::borrow::Cow::Borrowed(resolved);
3677                }
3678            }
3679        }
3680        std::borrow::Cow::Borrowed(param)
3681    }
3682
3683    /// Analyze a parameter
3684    fn analyze_parameter(
3685        &self,
3686        param: &crate::openapi::Parameter,
3687    ) -> Result<Option<ParameterInfo>> {
3688        let name = param.name.as_deref().unwrap_or("");
3689        let location = param.location.as_deref().unwrap_or("");
3690        let required = param.required.unwrap_or(false);
3691
3692        let mut rust_type = "String".to_string();
3693        let mut schema_ref = None;
3694
3695        if let Some(schema) = &param.schema {
3696            if let Some(ref_str) = schema.reference() {
3697                schema_ref = self.extract_schema_name(ref_str).map(|s| s.to_string());
3698            } else if let Some(schema_type) = schema.schema_type() {
3699                rust_type = match schema_type {
3700                    crate::openapi::SchemaType::Boolean => "bool",
3701                    crate::openapi::SchemaType::Integer => "i64",
3702                    crate::openapi::SchemaType::Number => "f64",
3703                    crate::openapi::SchemaType::String => "String",
3704                    _ => "String",
3705                }
3706                .to_string();
3707            }
3708        }
3709
3710        Ok(Some(ParameterInfo {
3711            name: name.to_string(),
3712            location: location.to_string(),
3713            required,
3714            schema_ref,
3715            rust_type,
3716            description: param.description.clone(),
3717        }))
3718    }
3719}