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