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