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 pub schemas: BTreeMap<String, AnalyzedSchema>,
11 pub dependencies: DependencyGraph,
13 pub patterns: DetectedPatterns,
15 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 Primitive { rust_type: String },
34 Object {
36 properties: BTreeMap<String, PropertyInfo>,
37 required: HashSet<String>,
38 additional_properties: bool,
39 },
40 DiscriminatedUnion {
42 discriminator_field: String,
43 variants: Vec<UnionVariant>,
44 },
45 Union { variants: Vec<SchemaRef> },
47 Array { item_type: Box<SchemaType> },
49 StringEnum { values: Vec<String> },
51 ExtensibleEnum { known_values: Vec<String> },
53 Composition { schemas: Vec<SchemaRef> },
55 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 pub recursive_schemas: HashSet<String>,
87}
88
89#[derive(Debug, Clone)]
90pub struct DetectedPatterns {
91 pub tagged_enum_schemas: HashSet<String>,
93 pub untagged_enum_schemas: HashSet<String>,
95 pub type_mappings: BTreeMap<String, BTreeMap<String, String>>,
97}
98
99#[derive(Debug, Clone, serde::Serialize)]
101pub struct OperationInfo {
102 pub operation_id: String,
104 pub method: String,
106 pub path: String,
108 pub request_body: Option<RequestBodyContent>,
110 pub response_schemas: BTreeMap<String, String>,
112 pub parameters: Vec<ParameterInfo>,
114 pub supports_streaming: bool,
116 pub stream_parameter: Option<String>,
118}
119
120#[derive(Debug, Clone, serde::Serialize)]
122#[serde(tag = "kind")]
123pub enum RequestBodyContent {
124 Json { schema_name: String },
125 FormUrlEncoded { schema_name: String },
126 Multipart,
127 OctetStream,
128 TextPlain,
129}
130
131impl RequestBodyContent {
132 pub fn schema_name(&self) -> Option<&str> {
134 match self {
135 Self::Json { schema_name } | Self::FormUrlEncoded { schema_name } => Some(schema_name),
136 _ => None,
137 }
138 }
139}
140
141#[derive(Debug, Clone, serde::Serialize)]
143pub struct ParameterInfo {
144 pub name: String,
146 pub location: String,
148 pub required: bool,
150 pub schema_ref: Option<String>,
152 pub rust_type: String,
154}
155
156impl Default for DependencyGraph {
157 fn default() -> Self {
158 Self::new()
159 }
160}
161
162impl DependencyGraph {
163 pub fn new() -> Self {
164 Self {
165 edges: BTreeMap::new(),
166 recursive_schemas: HashSet::new(),
167 }
168 }
169
170 pub fn add_dependency(&mut self, from: String, to: String) {
171 self.edges.entry(from).or_default().insert(to);
172 }
173
174 pub fn topological_sort(&mut self) -> Result<Vec<String>> {
176 self.detect_recursive_schemas();
178
179 let mut temp_edges = self.edges.clone();
181 for (schema, deps) in &mut temp_edges {
182 deps.remove(schema); }
184
185 let mut visited = HashSet::new();
186 let mut temp_visited = HashSet::new();
187 let mut result = Vec::new();
188
189 let mut all_nodes: Vec<_> = temp_edges.keys().collect();
191 all_nodes.sort();
192 for node in all_nodes {
193 if !visited.contains(node) {
194 self.visit_node_recursive(
195 node,
196 &temp_edges,
197 &mut visited,
198 &mut temp_visited,
199 &mut result,
200 )?;
201 }
202 }
203
204 result.reverse();
205 Ok(result)
206 }
207
208 fn detect_recursive_schemas(&mut self) {
209 for (schema, deps) in &self.edges {
210 if deps.contains(schema) {
211 self.recursive_schemas.insert(schema.clone());
213 } else {
214 if self.has_cycle_from(schema, schema, &mut HashSet::new()) {
216 self.recursive_schemas.insert(schema.clone());
217 }
218 }
219 }
220
221 for (schema, deps) in &self.edges {
223 for dep in deps {
224 if let Some(dep_deps) = self.edges.get(dep) {
225 if dep_deps.contains(schema) {
226 self.recursive_schemas.insert(schema.clone());
228 self.recursive_schemas.insert(dep.clone());
229 }
230 }
231 }
232 }
233 }
234
235 fn has_cycle_from(&self, start: &str, current: &str, visited: &mut HashSet<String>) -> bool {
236 if visited.contains(current) {
237 return false; }
239
240 visited.insert(current.to_string());
241
242 if let Some(deps) = self.edges.get(current) {
243 for dep in deps {
244 if dep == start {
245 return true; }
247 if self.has_cycle_from(start, dep, visited) {
248 return true;
249 }
250 }
251 }
252
253 false
254 }
255
256 #[allow(clippy::only_used_in_recursion)]
257 fn visit_node_recursive(
258 &self,
259 node: &str,
260 temp_edges: &BTreeMap<String, HashSet<String>>,
261 visited: &mut HashSet<String>,
262 temp_visited: &mut HashSet<String>,
263 result: &mut Vec<String>,
264 ) -> Result<()> {
265 if temp_visited.contains(node) {
266 return Ok(());
268 }
269
270 if visited.contains(node) {
271 return Ok(());
272 }
273
274 temp_visited.insert(node.to_string());
275
276 if let Some(dependencies) = temp_edges.get(node) {
277 let mut sorted_deps: Vec<_> = dependencies.iter().collect();
279 sorted_deps.sort();
280 for dep in sorted_deps {
281 self.visit_node_recursive(dep, temp_edges, visited, temp_visited, result)?;
282 }
283 }
284
285 temp_visited.remove(node);
286 visited.insert(node.to_string());
287 result.push(node.to_string());
288
289 Ok(())
290 }
291}
292
293pub fn merge_schema_extensions(
296 main_spec: Value,
297 extension_paths: &[impl AsRef<Path>],
298) -> Result<Value> {
299 let mut result = main_spec;
300
301 for path in extension_paths {
302 let extension = load_extension_file(path.as_ref())?;
303 result = merge_json_objects_with_replacements(result, extension)?;
304 }
305
306 Ok(result)
307}
308
309fn load_extension_file(path: &Path) -> Result<Value> {
311 let content = std::fs::read_to_string(path).map_err(|e| GeneratorError::FileError {
312 message: format!("Failed to read file {}: {}", path.display(), e),
313 })?;
314
315 serde_json::from_str(&content).map_err(GeneratorError::ParseError)
316}
317
318fn merge_json_objects_with_replacements(main: Value, extension: Value) -> Result<Value> {
320 let replacements = extract_replacement_rules(&extension);
322
323 Ok(merge_json_objects_with_rules(
325 main,
326 extension,
327 &replacements,
328 ))
329}
330
331fn extract_replacement_rules(
333 extension: &Value,
334) -> std::collections::HashMap<String, (String, String)> {
335 let mut rules = std::collections::HashMap::new();
336
337 if let Some(x_replacements) = extension.get("x-replacements") {
338 if let Some(x_replacements_obj) = x_replacements.as_object() {
339 for (schema_name, replacement_rule) in x_replacements_obj {
340 if let Some(rule_obj) = replacement_rule.as_object() {
341 if let (Some(replace), Some(with)) = (
342 rule_obj.get("replace").and_then(|v| v.as_str()),
343 rule_obj.get("with").and_then(|v| v.as_str()),
344 ) {
345 rules.insert(schema_name.clone(), (replace.to_string(), with.to_string()));
346 }
348 }
349 }
350 }
351 }
352
353 rules
354}
355
356fn should_replace_variant(
358 schema_name: &str,
359 extension_refs: &[String],
360 replacements: &std::collections::HashMap<String, (String, String)>,
361) -> bool {
362 for (replace_schema, with_schema) in replacements.values() {
364 if schema_name == replace_schema {
365 let replacement_exists = extension_refs.iter().any(|ext_ref| {
367 let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
368 ext_schema_name == with_schema
369 });
370
371 if replacement_exists {
372 return true;
373 }
374 }
375 }
376
377 extension_refs.iter().any(|ext_ref| {
379 let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
380 schema_name == ext_schema_name
381 })
382}
383
384fn merge_json_objects_with_rules(
389 main: Value,
390 extension: Value,
391 replacements: &std::collections::HashMap<String, (String, String)>,
392) -> Value {
393 match (main, extension) {
394 (Value::Object(mut main_obj), Value::Object(ext_obj)) => {
396 if let (Some(main_variants), Some(ext_variants)) = (
398 extract_schema_variants(&Value::Object(main_obj.clone())),
399 extract_schema_variants(&Value::Object(ext_obj.clone())),
400 ) {
401 println!(
402 "🔍 Merging union schemas: {} main variants, {} extension variants",
403 main_variants.len(),
404 ext_variants.len()
405 );
406 let mut merged_variants = Vec::new();
409 let extension_refs: Vec<String> = ext_variants
410 .iter()
411 .filter_map(|v| v.get("$ref").and_then(|r| r.as_str()))
412 .map(|s| s.to_string())
413 .collect();
414
415 for main_variant in main_variants {
417 if let Some(main_ref) = main_variant.get("$ref").and_then(|r| r.as_str()) {
418 let schema_name = main_ref.split('/').next_back().unwrap_or("");
420 let should_replace =
421 should_replace_variant(schema_name, &extension_refs, replacements);
422
423 if should_replace {
424 println!("🔄 REPLACING {} (explicit rule)", schema_name);
425 }
426
427 if !should_replace {
428 merged_variants.push(main_variant);
429 }
430 } else {
431 merged_variants.push(main_variant);
433 }
434 }
435
436 for ext_variant in ext_variants {
438 merged_variants.push(ext_variant);
439 }
440
441 main_obj.remove("oneOf");
443 main_obj.remove("anyOf");
444 main_obj.insert("oneOf".to_string(), Value::Array(merged_variants));
445
446 for (key, ext_value) in ext_obj {
448 if key != "oneOf" && key != "anyOf" {
449 match main_obj.get(&key) {
450 Some(main_value) => {
451 let merged_value = merge_json_objects_with_rules(
452 main_value.clone(),
453 ext_value,
454 replacements,
455 );
456 main_obj.insert(key, merged_value);
457 }
458 None => {
459 main_obj.insert(key, ext_value);
460 }
461 }
462 }
463 }
464
465 return Value::Object(main_obj);
466 }
467
468 for (key, ext_value) in ext_obj {
470 match main_obj.get(&key) {
471 Some(main_value) => {
472 let merged_value = merge_json_objects_with_rules(
474 main_value.clone(),
475 ext_value,
476 replacements,
477 );
478 main_obj.insert(key, merged_value);
479 }
480 None => {
481 main_obj.insert(key, ext_value);
483 }
484 }
485 }
486 Value::Object(main_obj)
487 }
488
489 (Value::Array(mut main_arr), Value::Array(ext_arr)) => {
491 main_arr.extend(ext_arr);
492 Value::Array(main_arr)
493 }
494
495 (_, extension) => extension,
497 }
498}
499
500fn extract_schema_variants(obj: &Value) -> Option<Vec<Value>> {
502 if let Value::Object(map) = obj {
503 if let Some(Value::Array(variants)) = map.get("oneOf") {
504 return Some(variants.clone());
505 }
506 if let Some(Value::Array(variants)) = map.get("anyOf") {
507 return Some(variants.clone());
508 }
509 }
510 None
511}
512
513pub struct SchemaAnalyzer {
514 schemas: BTreeMap<String, Schema>,
515 resolved_cache: BTreeMap<String, AnalyzedSchema>,
516 openapi_spec: Value,
517 current_schema_name: Option<String>,
518}
519
520impl SchemaAnalyzer {
521 pub fn new(openapi_spec: Value) -> Result<Self> {
522 let spec: OpenApiSpec =
523 serde_json::from_value(openapi_spec.clone()).map_err(GeneratorError::ParseError)?;
524 let schemas = Self::extract_schemas(&spec)?;
525
526 Ok(Self {
527 schemas,
528 resolved_cache: BTreeMap::new(),
529 openapi_spec,
530 current_schema_name: None,
531 })
532 }
533
534 pub fn new_with_extensions(
536 openapi_spec: Value,
537 extension_paths: &[std::path::PathBuf],
538 ) -> Result<Self> {
539 let merged_spec = merge_schema_extensions(openapi_spec, extension_paths)?;
540 Self::new(merged_spec)
541 }
542
543 fn generate_context_aware_name(
546 &self,
547 base_context: &str,
548 type_hint: &str,
549 index: usize,
550 schema: Option<&Schema>,
551 ) -> String {
552 if let Some(schema) = schema {
554 if type_hint == "Array"
556 && matches!(schema.schema_type(), Some(OpenApiSchemaType::Array))
557 {
558 if let Some(items_schema) = &schema.details().items {
559 if let Some(item_type) = items_schema.schema_type() {
561 match item_type {
562 OpenApiSchemaType::Object => {
563 return format!("{base_context}ItemArray");
564 }
565 OpenApiSchemaType::String => {
566 return format!("{base_context}StringArray");
567 }
568 _ => {}
569 }
570 }
571 }
572 }
573 }
574
575 match type_hint {
577 "Array" => {
578 format!("{base_context}Array")
580 }
581 "Variant" | "InlineVariant" => {
582 if index == 0 {
584 format!("{base_context}{type_hint}")
585 } else {
586 format!("{}{}{}", base_context, type_hint, index + 1)
587 }
588 }
589 _ => {
590 format!("{base_context}{type_hint}{index}")
592 }
593 }
594 }
595
596 fn to_pascal_case(&self, s: &str) -> String {
598 s.split(['_', '-'])
599 .filter(|part| !part.is_empty())
600 .map(|part| {
601 let mut chars = part.chars();
602 match chars.next() {
603 None => String::new(),
604 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
605 }
606 })
607 .collect()
608 }
609
610 fn extract_schemas(spec: &OpenApiSpec) -> Result<BTreeMap<String, Schema>> {
611 let schemas = spec
612 .components
613 .as_ref()
614 .and_then(|c| c.schemas.as_ref())
615 .ok_or_else(|| {
616 GeneratorError::InvalidSchema("No schemas found in OpenAPI spec".to_string())
617 })?;
618
619 Ok(schemas
621 .iter()
622 .map(|(k, v)| (k.clone(), v.clone()))
623 .collect())
624 }
625
626 pub fn analyze(&mut self) -> Result<SchemaAnalysis> {
627 let mut analysis = SchemaAnalysis {
628 schemas: BTreeMap::new(),
629 dependencies: DependencyGraph::new(),
630 patterns: DetectedPatterns {
631 tagged_enum_schemas: HashSet::new(),
632 untagged_enum_schemas: HashSet::new(),
633 type_mappings: BTreeMap::new(),
634 },
635 operations: BTreeMap::new(),
636 };
637
638 self.detect_patterns(&mut analysis.patterns)?;
640
641 let schema_names: Vec<String> = self.schemas.keys().cloned().collect();
643 for schema_name in schema_names {
644 let analyzed = self.analyze_schema(&schema_name)?;
645
646 for dep in &analyzed.dependencies {
648 analysis
649 .dependencies
650 .add_dependency(schema_name.clone(), dep.clone());
651 }
652
653 analysis.schemas.insert(schema_name, analyzed);
654 }
655
656 for (inline_name, inline_schema) in &self.resolved_cache {
659 if !analysis.schemas.contains_key(inline_name) {
660 analysis
662 .schemas
663 .insert(inline_name.clone(), inline_schema.clone());
664
665 for dep in &inline_schema.dependencies {
667 analysis
668 .dependencies
669 .add_dependency(inline_name.clone(), dep.clone());
670 }
671
672 let mut schemas_to_update = Vec::new();
677 for (schema_name, schema) in &analysis.schemas {
678 if schema_name == inline_name {
680 continue;
681 }
682
683 if schema.dependencies.contains(inline_name) {
684 schemas_to_update.push(schema_name.clone());
686 }
687 }
688
689 for schema_name in schemas_to_update {
691 analysis
692 .dependencies
693 .add_dependency(schema_name, inline_name.clone());
694 }
695 }
696 }
697
698 self.analyze_operations(&mut analysis)?;
700
701 for (inline_name, inline_schema) in &self.resolved_cache {
704 if !analysis.schemas.contains_key(inline_name) {
705 analysis
706 .schemas
707 .insert(inline_name.clone(), inline_schema.clone());
708
709 for dep in &inline_schema.dependencies {
711 analysis
712 .dependencies
713 .add_dependency(inline_name.clone(), dep.clone());
714 }
715 }
716 }
717
718 Ok(analysis)
719 }
720
721 fn detect_patterns(&self, patterns: &mut DetectedPatterns) -> Result<()> {
722 for (schema_name, schema) in &self.schemas {
723 if self.is_discriminated_union(schema) {
725 patterns.tagged_enum_schemas.insert(schema_name.clone());
726
727 if let Some(mappings) = self.extract_type_mappings(schema)? {
729 patterns.type_mappings.insert(schema_name.clone(), mappings);
730 }
731 }
732 else if self.is_simple_union(schema) {
734 patterns.untagged_enum_schemas.insert(schema_name.clone());
735 }
736 }
737
738 Ok(())
739 }
740
741 fn is_discriminated_union(&self, schema: &Schema) -> bool {
742 if schema.is_discriminated_union() {
744 return true;
745 }
746
747 if let Some(variants) = schema.union_variants() {
749 return variants.len() > 2 && self.detect_discriminator_field(variants).is_some();
750 }
751
752 false
753 }
754
755 fn all_variants_have_const_field(&self, variants: &[Schema], field_name: &str) -> bool {
756 variants.iter().all(|variant| {
757 if let Some(ref_str) = variant.reference() {
758 if let Some(schema_name) = self.extract_schema_name(ref_str) {
760 if let Some(schema) = self.schemas.get(schema_name) {
761 return self.has_const_discriminator_field(schema, field_name);
762 }
763 }
764 } else {
765 return self.has_const_discriminator_field(variant, field_name);
767 }
768 false
769 })
770 }
771
772 fn detect_discriminator_field(&self, variants: &[Schema]) -> Option<String> {
776 if variants.is_empty() {
777 return None;
778 }
779
780 let first_variant = &variants[0];
782 let first_schema = if let Some(ref_str) = first_variant.reference() {
783 let schema_name = self.extract_schema_name(ref_str)?;
784 self.schemas.get(schema_name)?
785 } else {
786 first_variant
787 };
788
789 let properties = first_schema.details().properties.as_ref()?;
790 let mut candidates: Vec<String> = Vec::new();
791
792 for (field_name, field_schema) in properties {
793 let details = field_schema.details();
794 let is_const = details.const_value.is_some()
795 || details.enum_values.as_ref().is_some_and(|v| v.len() == 1)
796 || details.extra.contains_key("const");
797 if is_const {
798 candidates.push(field_name.clone());
799 }
800 }
801
802 if candidates.is_empty() {
803 return None;
804 }
805
806 candidates.sort_by(|a, b| {
808 if a == "type" {
809 std::cmp::Ordering::Less
810 } else if b == "type" {
811 std::cmp::Ordering::Greater
812 } else {
813 a.cmp(b)
814 }
815 });
816
817 for candidate in &candidates {
819 if self.all_variants_have_const_field(variants, candidate) {
820 return Some(candidate.clone());
821 }
822 }
823
824 None
825 }
826
827 fn has_const_discriminator_field(&self, schema: &Schema, field_name: &str) -> bool {
828 if let Some(properties) = &schema.details().properties {
829 if let Some(field) = properties.get(field_name) {
830 if field.details().const_value.is_some() {
832 return true;
833 }
834 if let Some(enum_vals) = &field.details().enum_values {
836 return enum_vals.len() == 1;
837 }
838 return field.details().extra.contains_key("const");
840 }
841 }
842 false
843 }
844
845 fn is_simple_union(&self, schema: &Schema) -> bool {
846 if let Some(variants) = schema.union_variants() {
847 if variants.len() > 1 && !schema.is_nullable_pattern() {
849 let has_refs = variants.iter().any(|v| v.is_reference());
850 return has_refs;
851 }
852 }
853 false
854 }
855
856 fn extract_type_mappings(&self, schema: &Schema) -> Result<Option<BTreeMap<String, String>>> {
857 let variants = schema.union_variants().ok_or_else(|| {
858 GeneratorError::InvalidSchema("No variants found for discriminated union".to_string())
859 })?;
860
861 let discriminator_field = if let Some(discriminator) = schema.discriminator() {
863 discriminator.property_name.clone()
864 } else if let Some(detected) = self.detect_discriminator_field(variants) {
865 detected
866 } else {
867 "type".to_string() };
869
870 let mut mappings = BTreeMap::new();
871
872 for variant in variants {
873 if let Some(ref_str) = variant.reference() {
874 if let Some(type_name) = self.extract_schema_name(ref_str) {
875 if let Some(variant_schema) = self.schemas.get(type_name) {
876 if let Some(discriminator_value) = self
877 .extract_discriminator_value_for_field(
878 variant_schema,
879 &discriminator_field,
880 )
881 {
882 mappings.insert(type_name.to_string(), discriminator_value);
883 }
884 }
885 }
886 }
887 }
888
889 if mappings.is_empty() {
890 Ok(None)
891 } else {
892 Ok(Some(mappings))
893 }
894 }
895
896 #[allow(dead_code)]
897 fn extract_discriminator_value(&self, schema: &Schema) -> Option<String> {
898 self.extract_discriminator_value_for_field(schema, "type")
899 }
900
901 fn extract_discriminator_value_for_field(
902 &self,
903 schema: &Schema,
904 field_name: &str,
905 ) -> Option<String> {
906 if let Some(properties) = &schema.details().properties {
907 if let Some(type_field) = properties.get(field_name) {
908 if let Some(const_value) = &type_field.details().const_value {
910 if let Some(value) = const_value.as_str() {
911 return Some(value.to_string());
912 }
913 }
914 if let Some(enum_values) = &type_field.details().enum_values {
916 if enum_values.len() == 1 {
917 return enum_values[0].as_str().map(|s| s.to_string());
918 }
919 }
920 if let Some(const_value) = type_field.details().extra.get("const") {
922 return const_value.as_str().map(|s| s.to_string());
923 }
924 if let Some(stainless_const) = type_field.details().extra.get("x-stainless-const") {
926 if stainless_const.as_bool() == Some(true) {
927 if let Some(default_value) = &type_field.details().default {
928 if let Some(value) = default_value.as_str() {
929 return Some(value.to_string());
930 }
931 }
932 }
933 }
934 }
935 }
936 None
937 }
938
939 fn get_any_reference<'a>(&self, schema: &'a Schema) -> Option<&'a str> {
940 schema.reference().or_else(|| schema.recursive_reference())
941 }
942
943 fn extract_schema_name<'a>(&self, ref_str: &'a str) -> Option<&'a str> {
944 if ref_str == "#" {
945 None } else {
947 ref_str.split('/').next_back()
948 }
949 }
950
951 fn analyze_schema(&mut self, schema_name: &str) -> Result<AnalyzedSchema> {
952 if let Some(cached) = self.resolved_cache.get(schema_name) {
954 return Ok(cached.clone());
955 }
956
957 self.current_schema_name = Some(schema_name.to_string());
959
960 let schema = self
961 .schemas
962 .get(schema_name)
963 .ok_or_else(|| GeneratorError::UnresolvedReference(schema_name.to_string()))?
964 .clone();
965
966 self.resolved_cache.insert(
968 schema_name.to_string(),
969 AnalyzedSchema {
970 name: schema_name.to_string(),
971 original: serde_json::to_value(&schema).unwrap_or(Value::Null),
972 schema_type: SchemaType::Reference {
973 target: "placeholder".to_string(),
974 },
975 dependencies: HashSet::new(),
976 nullable: false,
977 description: None,
978 default: None,
979 },
980 );
981
982 let analyzed = self.analyze_schema_value(&schema, schema_name)?;
983
984 self.resolved_cache
986 .insert(schema_name.to_string(), analyzed.clone());
987
988 Ok(analyzed)
989 }
990
991 fn analyze_schema_value(
992 &mut self,
993 schema: &Schema,
994 schema_name: &str,
995 ) -> Result<AnalyzedSchema> {
996 let details = schema.details();
997 let description = details.description.clone();
998 let nullable = details.is_nullable();
999 let mut dependencies = HashSet::new();
1000
1001 let schema_type = match schema {
1002 Schema::Reference { reference, .. } => {
1003 let target = self
1004 .extract_schema_name(reference)
1005 .ok_or_else(|| GeneratorError::UnresolvedReference(reference.to_string()))?
1006 .to_string();
1007 dependencies.insert(target.clone());
1008 SchemaType::Reference { target }
1009 }
1010 Schema::RecursiveRef { recursive_ref, .. } => {
1011 if recursive_ref == "#" {
1013 dependencies.insert(schema_name.to_string());
1015 SchemaType::Reference {
1016 target: schema_name.to_string(),
1017 }
1018 } else {
1019 let target = self
1021 .extract_schema_name(recursive_ref)
1022 .unwrap_or(schema_name)
1023 .to_string();
1024 dependencies.insert(target.clone());
1025 SchemaType::Reference { target }
1026 }
1027 }
1028 Schema::Typed { schema_type, .. } => {
1029 match schema_type {
1030 OpenApiSchemaType::String => {
1031 if let Some(values) = details.string_enum_values() {
1032 SchemaType::StringEnum { values }
1033 } else {
1034 SchemaType::Primitive {
1035 rust_type: "String".to_string(),
1036 }
1037 }
1038 }
1039 OpenApiSchemaType::Integer => {
1040 let rust_type =
1041 self.get_number_rust_type(OpenApiSchemaType::Integer, details);
1042 SchemaType::Primitive { rust_type }
1043 }
1044 OpenApiSchemaType::Number => {
1045 let rust_type =
1046 self.get_number_rust_type(OpenApiSchemaType::Number, details);
1047 SchemaType::Primitive { rust_type }
1048 }
1049 OpenApiSchemaType::Boolean => SchemaType::Primitive {
1050 rust_type: "bool".to_string(),
1051 },
1052 OpenApiSchemaType::Array => {
1053 self.analyze_array_schema(schema, schema_name, &mut dependencies)?
1055 }
1056 OpenApiSchemaType::Object => {
1057 if self.should_use_dynamic_json(schema) {
1059 SchemaType::Primitive {
1060 rust_type: "serde_json::Value".to_string(),
1061 }
1062 } else {
1063 self.analyze_object_schema(schema, &mut dependencies)?
1065 }
1066 }
1067 _ => SchemaType::Primitive {
1068 rust_type: "serde_json::Value".to_string(),
1069 },
1070 }
1071 }
1072 Schema::AnyOf {
1073 any_of,
1074 discriminator,
1075 ..
1076 } => {
1077 self.analyze_anyof_union(
1079 any_of,
1080 discriminator.as_ref(),
1081 &mut dependencies,
1082 schema_name,
1083 )?
1084 }
1085 Schema::OneOf {
1086 one_of,
1087 discriminator,
1088 ..
1089 } => {
1090 self.analyze_oneof_union(one_of, discriminator.as_ref(), None, &mut dependencies)?
1092 }
1093 Schema::AllOf { all_of, .. } => {
1094 self.analyze_allof_composition(all_of, &mut dependencies)?
1096 }
1097 Schema::Untyped { .. } => {
1098 if let Some(inferred) = schema.inferred_type() {
1100 match inferred {
1101 OpenApiSchemaType::Object => {
1102 if self.should_use_dynamic_json(schema) {
1103 SchemaType::Primitive {
1104 rust_type: "serde_json::Value".to_string(),
1105 }
1106 } else {
1107 self.analyze_object_schema(schema, &mut dependencies)?
1108 }
1109 }
1110 OpenApiSchemaType::String if details.is_string_enum() => {
1111 SchemaType::StringEnum {
1112 values: details.string_enum_values().unwrap_or_default(),
1113 }
1114 }
1115 _ => SchemaType::Primitive {
1116 rust_type: "serde_json::Value".to_string(),
1117 },
1118 }
1119 } else {
1120 SchemaType::Primitive {
1121 rust_type: "serde_json::Value".to_string(),
1122 }
1123 }
1124 }
1125 };
1126
1127 Ok(AnalyzedSchema {
1128 name: schema_name.to_string(),
1129 original: serde_json::to_value(schema).unwrap_or(Value::Null), schema_type,
1131 dependencies,
1132 nullable,
1133 description,
1134 default: details.default.clone(),
1135 })
1136 }
1137
1138 fn analyze_object_schema(
1139 &mut self,
1140 schema: &Schema,
1141 dependencies: &mut HashSet<String>,
1142 ) -> Result<SchemaType> {
1143 let details = schema.details();
1144 let properties = &details.properties;
1145 let required = details
1146 .required
1147 .as_ref()
1148 .map(|req| req.iter().cloned().collect::<HashSet<String>>())
1149 .unwrap_or_default();
1150
1151 let mut property_info = BTreeMap::new();
1152
1153 if let Some(props) = properties {
1154 for (prop_name, prop_schema) in props {
1155 let prop_type = if let Schema::AnyOf { any_of, .. } = prop_schema {
1157 if self.should_use_dynamic_json(prop_schema) {
1159 SchemaType::Primitive {
1161 rust_type: "serde_json::Value".to_string(),
1162 }
1163 } else {
1164 let context_name = self
1167 .current_schema_name
1168 .clone()
1169 .unwrap_or_else(|| "Unknown".to_string());
1170
1171 let prop_pascal = self.to_pascal_case(prop_name);
1173 let union_type_name = format!("{context_name}{prop_pascal}");
1174
1175 let union_schema_type = self.analyze_anyof_union(
1177 any_of,
1178 prop_schema.discriminator(),
1179 dependencies,
1180 &union_type_name,
1181 )?;
1182
1183 self.resolved_cache.insert(
1185 union_type_name.clone(),
1186 AnalyzedSchema {
1187 name: union_type_name.clone(),
1188 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
1189 schema_type: union_schema_type,
1190 dependencies: HashSet::new(),
1191 nullable: false,
1192 description: prop_schema.details().description.clone(),
1193 default: None,
1194 },
1195 );
1196
1197 dependencies.insert(union_type_name.clone());
1199 SchemaType::Reference {
1200 target: union_type_name,
1201 }
1202 }
1203 } else if let Schema::OneOf {
1204 one_of,
1205 discriminator,
1206 ..
1207 } = prop_schema
1208 {
1209 let context_name = self
1212 .current_schema_name
1213 .clone()
1214 .unwrap_or_else(|| "Unknown".to_string());
1215 let prop_pascal = self.to_pascal_case(prop_name);
1216 let union_type_name = format!("{context_name}{prop_pascal}");
1217
1218 let union_schema_type = self.analyze_oneof_union(
1220 one_of,
1221 discriminator.as_ref(),
1222 Some(&union_type_name),
1223 dependencies,
1224 )?;
1225
1226 self.resolved_cache.insert(
1228 union_type_name.clone(),
1229 AnalyzedSchema {
1230 name: union_type_name.clone(),
1231 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
1232 schema_type: union_schema_type,
1233 dependencies: HashSet::new(),
1234 nullable: false,
1235 description: prop_schema.details().description.clone(),
1236 default: None,
1237 },
1238 );
1239
1240 dependencies.insert(union_type_name.clone());
1242 SchemaType::Reference {
1243 target: union_type_name,
1244 }
1245 } else {
1246 self.analyze_property_schema_with_context(
1248 prop_schema,
1249 Some(prop_name),
1250 dependencies,
1251 )?
1252 };
1253
1254 let prop_details = prop_schema.details();
1255 let prop_nullable = prop_details.is_nullable() || prop_schema.is_nullable_pattern();
1257 let prop_description = prop_details.description.clone();
1258 let prop_default = prop_details.default.clone();
1259
1260 property_info.insert(
1261 prop_name.clone(),
1262 PropertyInfo {
1263 schema_type: prop_type,
1264 nullable: prop_nullable,
1265 description: prop_description,
1266 default: prop_default,
1267 serde_attrs: Vec::new(),
1268 },
1269 );
1270 }
1271 }
1272
1273 let additional_properties = match &details.additional_properties {
1275 Some(crate::openapi::AdditionalProperties::Boolean(true)) => true,
1276 Some(crate::openapi::AdditionalProperties::Boolean(false)) => false,
1277 Some(crate::openapi::AdditionalProperties::Schema(_)) => {
1278 true
1281 }
1282 None => false, };
1284
1285 Ok(SchemaType::Object {
1286 properties: property_info,
1287 required,
1288 additional_properties,
1289 })
1290 }
1291
1292 fn analyze_property_schema_with_context(
1293 &mut self,
1294 schema: &Schema,
1295 property_name: Option<&str>,
1296 dependencies: &mut HashSet<String>,
1297 ) -> Result<SchemaType> {
1298 if let Some(ref_str) = self.get_any_reference(schema) {
1299 let target = if ref_str == "#" {
1300 self.find_recursive_anchor_schema()
1302 .unwrap_or_else(|| "UnknownRecursive".to_string())
1303 } else {
1304 self.extract_schema_name(ref_str)
1305 .ok_or_else(|| GeneratorError::UnresolvedReference(ref_str.to_string()))?
1306 .to_string()
1307 };
1308 dependencies.insert(target.clone());
1309 return Ok(SchemaType::Reference { target });
1310 }
1311
1312 if let Some(schema_type) = schema.schema_type() {
1313 match schema_type {
1314 OpenApiSchemaType::String => {
1315 if let Some(enum_values) = schema.details().string_enum_values() {
1317 let context_name = self
1320 .current_schema_name
1321 .clone()
1322 .unwrap_or_else(|| "Unknown".to_string());
1323
1324 let enum_type_name = if let Some(prop_name) = property_name {
1326 let prop_pascal = self.to_pascal_case(prop_name);
1328 format!("{context_name}{prop_pascal}")
1329 } else {
1330 let suffix = if !enum_values.is_empty() {
1333 let first_value = self.to_pascal_case(&enum_values[0]);
1334 format!("{first_value}Enum")
1335 } else {
1336 "StringEnum".to_string()
1337 };
1338 format!("{context_name}{suffix}")
1339 };
1340
1341 let should_create_new = !self
1344 .resolved_cache
1345 .get(&enum_type_name)
1346 .map(|existing| {
1347 if let SchemaType::StringEnum {
1348 values: existing_values,
1349 } = &existing.schema_type
1350 {
1351 existing_values == &enum_values
1352 } else {
1353 false
1354 }
1355 })
1356 .unwrap_or(false);
1357
1358 if should_create_new {
1359 self.resolved_cache.insert(
1361 enum_type_name.clone(),
1362 AnalyzedSchema {
1363 name: enum_type_name.clone(),
1364 original: serde_json::to_value(schema).unwrap_or(Value::Null),
1365 schema_type: SchemaType::StringEnum {
1366 values: enum_values,
1367 },
1368 dependencies: HashSet::new(),
1369 nullable: false,
1370 description: schema.details().description.clone(),
1371 default: schema.details().default.clone(),
1372 },
1373 );
1374 }
1375
1376 dependencies.insert(enum_type_name.clone());
1378 return Ok(SchemaType::Reference {
1379 target: enum_type_name,
1380 });
1381 } else {
1382 return Ok(SchemaType::Primitive {
1383 rust_type: "String".to_string(),
1384 });
1385 }
1386 }
1387 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
1388 let details = schema.details();
1389 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
1390 return Ok(SchemaType::Primitive { rust_type });
1391 }
1392 OpenApiSchemaType::Boolean => {
1393 return Ok(SchemaType::Primitive {
1394 rust_type: "bool".to_string(),
1395 });
1396 }
1397 OpenApiSchemaType::Array => {
1398 let context_name = if let Some(prop_name) = property_name {
1400 let prop_pascal = self.to_pascal_case(prop_name);
1402 format!(
1403 "{}{}",
1404 self.current_schema_name.as_deref().unwrap_or("Unknown"),
1405 prop_pascal
1406 )
1407 } else {
1408 "ArrayItem".to_string()
1410 };
1411 return self.analyze_array_schema(schema, &context_name, dependencies);
1412 }
1413 OpenApiSchemaType::Object => {
1414 if self.should_use_dynamic_json(schema) {
1416 return Ok(SchemaType::Primitive {
1417 rust_type: "serde_json::Value".to_string(),
1418 });
1419 }
1420 let object_type_name = if let Some(prop_name) = property_name {
1422 let prop_pascal = self.to_pascal_case(prop_name);
1424 format!(
1425 "{}{}",
1426 self.current_schema_name.as_deref().unwrap_or("Unknown"),
1427 prop_pascal
1428 )
1429 } else {
1430 format!(
1432 "{}Object",
1433 self.current_schema_name.as_deref().unwrap_or("Unknown")
1434 )
1435 };
1436
1437 let object_type = self.analyze_object_schema(schema, dependencies)?;
1439
1440 let inline_schema = AnalyzedSchema {
1442 name: object_type_name.clone(),
1443 original: serde_json::to_value(schema).unwrap_or(Value::Null),
1444 schema_type: object_type,
1445 dependencies: dependencies.clone(),
1446 nullable: false,
1447 description: schema.details().description.clone(),
1448 default: None,
1449 };
1450
1451 self.resolved_cache
1453 .insert(object_type_name.clone(), inline_schema);
1454 dependencies.insert(object_type_name.clone());
1455
1456 return Ok(SchemaType::Reference {
1458 target: object_type_name,
1459 });
1460 }
1461 _ => {
1462 return Ok(SchemaType::Primitive {
1463 rust_type: "serde_json::Value".to_string(),
1464 });
1465 }
1466 }
1467 }
1468
1469 if schema.is_nullable_pattern() {
1471 if let Some(non_null) = schema.non_null_variant() {
1472 return self.analyze_property_schema_with_context(
1473 non_null,
1474 property_name,
1475 dependencies,
1476 );
1477 }
1478 }
1479
1480 if self.should_use_dynamic_json(schema) {
1482 return Ok(SchemaType::Primitive {
1483 rust_type: "serde_json::Value".to_string(),
1484 });
1485 }
1486
1487 if let Schema::AllOf { all_of, .. } = schema {
1489 return self.analyze_allof_composition(all_of, dependencies);
1490 }
1491
1492 if let Some(variants) = schema.union_variants() {
1494 match variants.len().cmp(&1) {
1495 std::cmp::Ordering::Equal => {
1496 return self.analyze_property_schema_with_context(
1498 &variants[0],
1499 property_name,
1500 dependencies,
1501 );
1502 }
1503 std::cmp::Ordering::Greater => {
1504 let union_name = if let Some(prop_name) = property_name {
1507 let prop_pascal = self.to_pascal_case(prop_name);
1509 format!(
1510 "{}{}",
1511 self.current_schema_name.as_deref().unwrap_or(""),
1512 prop_pascal
1513 )
1514 } else {
1515 "UnionType".to_string()
1516 };
1517
1518 if let Schema::OneOf {
1520 one_of,
1521 discriminator,
1522 ..
1523 } = schema
1524 {
1525 let oneof_result = self.analyze_oneof_union(
1527 one_of,
1528 discriminator.as_ref(),
1529 Some(&union_name),
1530 dependencies,
1531 )?;
1532
1533 if let SchemaType::Union {
1535 variants: _union_variants,
1536 } = &oneof_result
1537 {
1538 self.resolved_cache.insert(
1540 union_name.clone(),
1541 AnalyzedSchema {
1542 name: union_name.clone(),
1543 original: serde_json::to_value(schema).unwrap_or(Value::Null),
1544 schema_type: oneof_result.clone(),
1545 dependencies: dependencies.clone(),
1546 nullable: false,
1547 description: schema.details().description.clone(),
1548 default: None,
1549 },
1550 );
1551
1552 dependencies.insert(union_name.clone());
1554 return Ok(SchemaType::Reference { target: union_name });
1555 }
1556
1557 return Ok(oneof_result);
1558 } else if let Schema::AnyOf {
1559 any_of,
1560 discriminator,
1561 ..
1562 } = schema
1563 {
1564 let union_analysis = self.analyze_anyof_union(
1566 any_of,
1567 discriminator.as_ref(),
1568 dependencies,
1569 &union_name,
1570 )?;
1571 return Ok(union_analysis);
1572 } else {
1573 let mut union_variants = Vec::new();
1576 for variant in variants {
1577 if let Some(ref_str) = variant.reference() {
1578 if let Some(target) = self.extract_schema_name(ref_str) {
1579 dependencies.insert(target.to_string());
1580 union_variants.push(SchemaRef {
1581 target: target.to_string(),
1582 nullable: false,
1583 });
1584 }
1585 }
1586 }
1587 return Ok(SchemaType::Union {
1588 variants: union_variants,
1589 });
1590 }
1591 }
1592 std::cmp::Ordering::Less => {}
1593 }
1594 }
1595
1596 if let Some(inferred_type) = schema.inferred_type() {
1598 match inferred_type {
1599 OpenApiSchemaType::Object => {
1600 if self.should_use_dynamic_json(schema) {
1602 return Ok(SchemaType::Primitive {
1603 rust_type: "serde_json::Value".to_string(),
1604 });
1605 }
1606 return self.analyze_object_schema(schema, dependencies);
1607 }
1608 OpenApiSchemaType::Array => {
1609 let context_name = if let Some(prop_name) = property_name {
1610 let prop_pascal = self.to_pascal_case(prop_name);
1612 format!(
1613 "{}{}",
1614 self.current_schema_name.as_deref().unwrap_or("Unknown"),
1615 prop_pascal
1616 )
1617 } else {
1618 "ArrayItem".to_string()
1620 };
1621 return self.analyze_array_schema(schema, &context_name, dependencies);
1622 }
1623 OpenApiSchemaType::String => {
1624 if let Some(enum_values) = schema.details().string_enum_values() {
1625 return Ok(SchemaType::StringEnum {
1626 values: enum_values,
1627 });
1628 } else {
1629 return Ok(SchemaType::Primitive {
1630 rust_type: "String".to_string(),
1631 });
1632 }
1633 }
1634 _ => {
1635 let rust_type = self.openapi_type_to_rust_type(inferred_type, schema.details());
1637 return Ok(SchemaType::Primitive { rust_type });
1638 }
1639 }
1640 }
1641
1642 Ok(SchemaType::Primitive {
1643 rust_type: "serde_json::Value".to_string(),
1644 })
1645 }
1646
1647 fn analyze_allof_composition(
1648 &mut self,
1649 all_of_schemas: &[Schema],
1650 dependencies: &mut HashSet<String>,
1651 ) -> Result<SchemaType> {
1652 if all_of_schemas.len() == 1 {
1655 if let Schema::Reference { reference, .. } = &all_of_schemas[0] {
1656 if let Some(target) = self.extract_schema_name(reference) {
1657 dependencies.insert(target.to_string());
1658 return Ok(SchemaType::Reference {
1659 target: target.to_string(),
1660 });
1661 }
1662 }
1663 }
1664
1665 let mut merged_properties = BTreeMap::new();
1667 let mut merged_required = HashSet::new();
1668 let mut descriptions = Vec::new();
1669
1670 let current_context = self.current_schema_name.clone();
1672
1673 for schema in all_of_schemas {
1674 match schema {
1675 Schema::Reference { reference, .. } => {
1676 if let Some(target) = self.extract_schema_name(reference) {
1678 dependencies.insert(target.to_string());
1679
1680 let analyzed_ref = self.analyze_schema(target)?;
1682
1683 match &analyzed_ref.schema_type {
1685 SchemaType::Object {
1686 properties,
1687 required,
1688 ..
1689 } => {
1690 for (prop_name, prop_info) in properties {
1692 merged_properties.insert(prop_name.clone(), prop_info.clone());
1693 }
1694 for req in required {
1696 merged_required.insert(req.clone());
1697 }
1698 }
1699 _ => {
1700 if let Some(ref_schema) = self.schemas.get(target).cloned() {
1702 self.merge_schema_into_properties(
1703 &ref_schema,
1704 &mut merged_properties,
1705 &mut merged_required,
1706 dependencies,
1707 )?;
1708 }
1709 }
1710 }
1711 }
1712 }
1713 Schema::Typed {
1714 schema_type: OpenApiSchemaType::Object,
1715 ..
1716 }
1717 | Schema::Untyped { .. } => {
1718 let saved_context = self.current_schema_name.clone();
1720 self.current_schema_name = current_context.clone();
1721
1722 self.merge_schema_into_properties(
1724 schema,
1725 &mut merged_properties,
1726 &mut merged_required,
1727 dependencies,
1728 )?;
1729
1730 self.current_schema_name = saved_context;
1732 }
1733 _ => {
1734 self.merge_schema_into_properties(
1737 schema,
1738 &mut merged_properties,
1739 &mut merged_required,
1740 dependencies,
1741 )?;
1742 }
1743 }
1744
1745 if let Some(desc) = &schema.details().description {
1747 descriptions.push(desc.clone());
1748 }
1749 }
1750
1751 if !merged_properties.is_empty() {
1753 Ok(SchemaType::Object {
1754 properties: merged_properties,
1755 required: merged_required,
1756 additional_properties: false,
1757 })
1758 } else {
1759 Ok(SchemaType::Composition {
1761 schemas: all_of_schemas
1762 .iter()
1763 .filter_map(|s| {
1764 if let Some(ref_str) = s.reference() {
1765 if let Some(target) = self.extract_schema_name(ref_str) {
1766 dependencies.insert(target.to_string());
1767 Some(SchemaRef {
1768 target: target.to_string(),
1769 nullable: false,
1770 })
1771 } else {
1772 None
1773 }
1774 } else {
1775 None
1776 }
1777 })
1778 .collect(),
1779 })
1780 }
1781 }
1782
1783 fn merge_schema_into_properties(
1784 &mut self,
1785 schema: &Schema,
1786 merged_properties: &mut BTreeMap<String, PropertyInfo>,
1787 merged_required: &mut HashSet<String>,
1788 dependencies: &mut HashSet<String>,
1789 ) -> Result<()> {
1790 let details = schema.details();
1791
1792 if let Some(properties) = &details.properties {
1794 for (prop_name, prop_schema) in properties {
1795 let prop_type = self.analyze_property_schema_with_context(
1796 prop_schema,
1797 Some(prop_name),
1798 dependencies,
1799 )?;
1800 let prop_details = prop_schema.details();
1801
1802 merged_properties.insert(
1803 prop_name.clone(),
1804 PropertyInfo {
1805 schema_type: prop_type,
1806 nullable: prop_details.is_nullable(),
1807 description: prop_details.description.clone(),
1808 default: prop_details.default.clone(),
1809 serde_attrs: Vec::new(),
1810 },
1811 );
1812 }
1813 }
1814
1815 if let Some(required) = &details.required {
1817 for field in required {
1818 merged_required.insert(field.clone());
1819 }
1820 }
1821
1822 Ok(())
1823 }
1824
1825 fn analyze_oneof_union(
1826 &mut self,
1827 one_of_schemas: &[Schema],
1828 discriminator: Option<&crate::openapi::Discriminator>,
1829 parent_name: Option<&str>,
1830 dependencies: &mut HashSet<String>,
1831 ) -> Result<SchemaType> {
1832 if discriminator.is_none() {
1834 return self.analyze_untagged_oneof_union(one_of_schemas, parent_name, dependencies);
1836 }
1837
1838 let discriminator_field = discriminator
1840 .ok_or_else(|| {
1841 GeneratorError::InvalidDiscriminator(
1842 "expected discriminator after guard check".to_string(),
1843 )
1844 })?
1845 .property_name
1846 .clone();
1847
1848 let mut variants = Vec::new();
1849 let mut used_variant_names = std::collections::HashSet::new();
1850
1851 for variant_schema in one_of_schemas {
1852 let ref_info = if let Some(ref_str) = variant_schema.reference() {
1854 Some((ref_str, false))
1855 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
1856 Some((recursive_ref, true))
1857 } else if let Schema::AllOf { all_of, .. } = variant_schema {
1858 if all_of.len() == 1 {
1860 if let Some(ref_str) = all_of[0].reference() {
1861 Some((ref_str, false))
1862 } else {
1863 all_of[0]
1864 .recursive_reference()
1865 .map(|recursive_ref| (recursive_ref, true))
1866 }
1867 } else {
1868 None
1869 }
1870 } else {
1871 None
1872 };
1873
1874 if let Some((ref_str, is_recursive)) = ref_info {
1875 let schema_name = if is_recursive && ref_str == "#" {
1876 self.find_recursive_anchor_schema()
1878 .or_else(|| self.current_schema_name.clone())
1879 .unwrap_or_else(|| "CompoundFilter".to_string())
1880 } else {
1881 self.extract_schema_name(ref_str)
1882 .map(|s| s.to_string())
1883 .unwrap_or_else(|| "UnknownRef".to_string())
1884 };
1885
1886 if !schema_name.is_empty() {
1887 dependencies.insert(schema_name.clone());
1888
1889 let discriminator_value = if let Some(disc) = discriminator {
1894 if let Some(mappings) = &disc.mapping {
1895 mappings
1898 .iter()
1899 .find(|(_, target_ref)| {
1900 target_ref.as_str() == ref_str
1902 || self
1903 .extract_schema_name(target_ref)
1904 .map(|s| s.to_string())
1905 == Some(schema_name.clone())
1906 })
1907 .map(|(key, _)| key.clone())
1908 .unwrap_or_else(|| {
1909 self.fallback_discriminator_value_for_field(
1910 &schema_name,
1911 &discriminator_field,
1912 )
1913 })
1914 } else {
1915 self.fallback_discriminator_value_for_field(
1916 &schema_name,
1917 &discriminator_field,
1918 )
1919 }
1920 } else {
1921 self.fallback_discriminator_value_for_field(
1922 &schema_name,
1923 &discriminator_field,
1924 )
1925 };
1926
1927 let base_name = self.to_rust_variant_name(&schema_name);
1929 let rust_name =
1930 self.ensure_unique_variant_name(base_name, &mut used_variant_names);
1931
1932 let final_discriminator_value = discriminator_value;
1934
1935 variants.push(UnionVariant {
1936 rust_name,
1937 type_name: schema_name,
1938 discriminator_value: final_discriminator_value,
1939 schema_ref: ref_str.to_string(),
1940 });
1941 }
1942 } else {
1943 let variant_index = variants.len();
1945 let inline_type_name =
1946 self.generate_inline_type_name(variant_schema, variant_index);
1947
1948 let discriminator_value = if let Some(disc) = discriminator {
1950 if let Some(mappings) = &disc.mapping {
1951 mappings
1953 .iter()
1954 .find(|(_, target_ref)| {
1955 target_ref.contains(&format!("variant_{variant_index}"))
1956 })
1957 .map(|(key, _)| key.clone())
1958 .unwrap_or_else(|| {
1959 self.extract_inline_discriminator_value(
1960 variant_schema,
1961 &discriminator_field,
1962 variant_index,
1963 )
1964 })
1965 } else {
1966 self.extract_inline_discriminator_value(
1967 variant_schema,
1968 &discriminator_field,
1969 variant_index,
1970 )
1971 }
1972 } else {
1973 self.extract_inline_discriminator_value(
1974 variant_schema,
1975 &discriminator_field,
1976 variant_index,
1977 )
1978 };
1979
1980 let base_name = if discriminator_value.starts_with("variant_") {
1982 format!("Variant{variant_index}")
1983 } else {
1984 let clean_name = self.discriminator_to_variant_name(&discriminator_value);
1986 self.to_rust_variant_name(&clean_name)
1987 };
1988 let rust_name = self.ensure_unique_variant_name(base_name, &mut used_variant_names);
1989
1990 let final_discriminator_value = discriminator_value;
1992
1993 variants.push(UnionVariant {
1994 rust_name,
1995 type_name: inline_type_name.clone(),
1996 discriminator_value: final_discriminator_value,
1997 schema_ref: format!("inline_{variant_index}"),
1998 });
1999
2000 self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
2002 }
2003 }
2004
2005 if variants.is_empty() {
2006 let mut union_variants = Vec::new();
2009
2010 for (variant_index, variant_schema) in one_of_schemas.iter().enumerate() {
2011 if let Some(ref_str) = variant_schema.reference() {
2013 if let Some(schema_name) = self.extract_schema_name(ref_str) {
2014 dependencies.insert(schema_name.to_string());
2015 union_variants.push(SchemaRef {
2016 target: schema_name.to_string(),
2017 nullable: false,
2018 });
2019 }
2020 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2021 let schema_name = if recursive_ref == "#" {
2022 self.find_recursive_anchor_schema()
2024 .or_else(|| self.current_schema_name.clone())
2025 .unwrap_or_else(|| "CompoundFilter".to_string())
2026 } else {
2027 self.extract_schema_name(recursive_ref)
2028 .map(|s| s.to_string())
2029 .unwrap_or_else(|| "RecursiveType".to_string())
2030 };
2031 dependencies.insert(schema_name.clone());
2032 union_variants.push(SchemaRef {
2033 target: schema_name,
2034 nullable: false,
2035 });
2036 } else {
2037 let context = parent_name.unwrap_or("Union");
2039 let inline_name = self.generate_context_aware_name(
2040 context,
2041 "InlineVariant",
2042 variant_index,
2043 Some(variant_schema),
2044 );
2045 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
2046 let variant_type = analyzed.schema_type;
2047
2048 for dep in &analyzed.dependencies {
2050 dependencies.insert(dep.clone());
2051 }
2052
2053 match &variant_type {
2054 SchemaType::Primitive { rust_type } => {
2056 union_variants.push(SchemaRef {
2057 target: rust_type.clone(),
2058 nullable: false,
2059 });
2060 }
2061 SchemaType::Array { item_type } => {
2063 match item_type.as_ref() {
2064 SchemaType::Primitive { rust_type } => {
2065 let type_name = format!("Vec<{rust_type}>");
2066 union_variants.push(SchemaRef {
2067 target: type_name,
2068 nullable: false,
2069 });
2070 }
2071 SchemaType::Reference { target } => {
2072 let type_name = format!("Vec<{target}>");
2073 union_variants.push(SchemaRef {
2074 target: type_name,
2075 nullable: false,
2076 });
2077 }
2078 _ => {
2079 let context = parent_name.unwrap_or("Inline");
2081 let inline_type_name = self.generate_context_aware_name(
2082 context,
2083 "Variant",
2084 variant_index,
2085 None,
2086 );
2087 self.add_inline_schema(
2088 &inline_type_name,
2089 variant_schema,
2090 dependencies,
2091 )?;
2092 union_variants.push(SchemaRef {
2093 target: inline_type_name,
2094 nullable: false,
2095 });
2096 }
2097 }
2098 }
2099 SchemaType::Reference { target } => {
2101 union_variants.push(SchemaRef {
2102 target: target.clone(),
2103 nullable: false,
2104 });
2105 }
2106 _ => {
2108 let inline_type_name = format!(
2109 "{}Variant{}",
2110 parent_name.unwrap_or("Inline"),
2111 variant_index + 1
2112 );
2113 self.add_inline_schema(
2114 &inline_type_name,
2115 variant_schema,
2116 dependencies,
2117 )?;
2118 union_variants.push(SchemaRef {
2119 target: inline_type_name,
2120 nullable: false,
2121 });
2122 }
2123 }
2124 }
2125 }
2126
2127 if !union_variants.is_empty() {
2128 return Ok(SchemaType::Union {
2129 variants: union_variants,
2130 });
2131 }
2132
2133 return Ok(SchemaType::Primitive {
2135 rust_type: "serde_json::Value".to_string(),
2136 });
2137 }
2138
2139 Ok(SchemaType::DiscriminatedUnion {
2140 discriminator_field,
2141 variants,
2142 })
2143 }
2144
2145 fn analyze_untagged_oneof_union(
2146 &mut self,
2147 one_of_schemas: &[Schema],
2148 parent_name: Option<&str>,
2149 dependencies: &mut HashSet<String>,
2150 ) -> Result<SchemaType> {
2151 let mut union_variants = Vec::new();
2152
2153 for (variant_index, variant_schema) in one_of_schemas.iter().enumerate() {
2154 if let Some(ref_str) = variant_schema.reference() {
2156 if let Some(schema_name) = self.extract_schema_name(ref_str) {
2157 dependencies.insert(schema_name.to_string());
2158 union_variants.push(SchemaRef {
2159 target: schema_name.to_string(),
2160 nullable: false,
2161 });
2162 }
2163 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2164 let schema_name = if recursive_ref == "#" {
2165 self.find_recursive_anchor_schema()
2167 .or_else(|| self.current_schema_name.clone())
2168 .unwrap_or_else(|| "CompoundFilter".to_string())
2169 } else {
2170 self.extract_schema_name(recursive_ref)
2171 .map(|s| s.to_string())
2172 .unwrap_or_else(|| "RecursiveType".to_string())
2173 };
2174 dependencies.insert(schema_name.clone());
2175 union_variants.push(SchemaRef {
2176 target: schema_name,
2177 nullable: false,
2178 });
2179 } else {
2180 let context = parent_name.unwrap_or("Union");
2182 let inline_name = self.generate_context_aware_name(
2183 context,
2184 "InlineVariant",
2185 variant_index,
2186 Some(variant_schema),
2187 );
2188 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
2189 let variant_type = analyzed.schema_type;
2190
2191 for dep in &analyzed.dependencies {
2193 dependencies.insert(dep.clone());
2194 }
2195
2196 match &variant_type {
2197 SchemaType::Primitive { rust_type } => {
2199 union_variants.push(SchemaRef {
2200 target: rust_type.clone(),
2201 nullable: false,
2202 });
2203 }
2204 SchemaType::Array { item_type } => {
2206 match item_type.as_ref() {
2207 SchemaType::Primitive { rust_type } => {
2208 let type_name = format!("Vec<{rust_type}>");
2209 union_variants.push(SchemaRef {
2210 target: type_name,
2211 nullable: false,
2212 });
2213 }
2214 SchemaType::Reference { target } => {
2215 let type_name = format!("Vec<{target}>");
2216 union_variants.push(SchemaRef {
2217 target: type_name,
2218 nullable: false,
2219 });
2220 }
2221 SchemaType::Array {
2223 item_type: inner_item_type,
2224 } => {
2225 match inner_item_type.as_ref() {
2226 SchemaType::Primitive { rust_type } => {
2227 let type_name = format!("Vec<Vec<{rust_type}>>");
2228 union_variants.push(SchemaRef {
2229 target: type_name,
2230 nullable: false,
2231 });
2232 }
2233 SchemaType::Reference { target } => {
2234 let type_name = format!("Vec<Vec<{target}>>");
2235 union_variants.push(SchemaRef {
2236 target: type_name,
2237 nullable: false,
2238 });
2239 }
2240 _ => {
2241 let context = parent_name.unwrap_or("Inline");
2243 let inline_type_name = self.generate_context_aware_name(
2244 context,
2245 "Variant",
2246 variant_index,
2247 None,
2248 );
2249 self.add_inline_schema(
2250 &inline_type_name,
2251 variant_schema,
2252 dependencies,
2253 )?;
2254 union_variants.push(SchemaRef {
2255 target: inline_type_name,
2256 nullable: false,
2257 });
2258 }
2259 }
2260 }
2261 _ => {
2262 let context = parent_name.unwrap_or("Inline");
2264 let inline_type_name = self.generate_context_aware_name(
2265 context,
2266 "Variant",
2267 variant_index,
2268 None,
2269 );
2270 self.add_inline_schema(
2271 &inline_type_name,
2272 variant_schema,
2273 dependencies,
2274 )?;
2275 union_variants.push(SchemaRef {
2276 target: inline_type_name,
2277 nullable: false,
2278 });
2279 }
2280 }
2281 }
2282 SchemaType::Reference { target } => {
2284 union_variants.push(SchemaRef {
2285 target: target.clone(),
2286 nullable: false,
2287 });
2288 }
2289 _ => {
2291 let context = parent_name.unwrap_or("Inline");
2292 let inline_type_name = self.generate_context_aware_name(
2293 context,
2294 "Variant",
2295 variant_index,
2296 None,
2297 );
2298 self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
2299 union_variants.push(SchemaRef {
2300 target: inline_type_name,
2301 nullable: false,
2302 });
2303 }
2304 }
2305 }
2306 }
2307
2308 if !union_variants.is_empty() {
2309 return Ok(SchemaType::Union {
2310 variants: union_variants,
2311 });
2312 }
2313
2314 Ok(SchemaType::Primitive {
2316 rust_type: "serde_json::Value".to_string(),
2317 })
2318 }
2319
2320 fn add_inline_schema(
2321 &mut self,
2322 type_name: &str,
2323 schema: &Schema,
2324 dependencies: &mut HashSet<String>,
2325 ) -> Result<()> {
2326 if let Some(schema_type) = schema.schema_type() {
2328 match schema_type {
2329 OpenApiSchemaType::String
2330 | OpenApiSchemaType::Integer
2331 | OpenApiSchemaType::Number
2332 | OpenApiSchemaType::Boolean => {
2333 let rust_type =
2334 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
2335
2336 self.resolved_cache.insert(
2338 type_name.to_string(),
2339 AnalyzedSchema {
2340 name: type_name.to_string(),
2341 original: serde_json::to_value(schema).unwrap_or(Value::Null),
2342 schema_type: SchemaType::Primitive { rust_type },
2343 dependencies: HashSet::new(),
2344 nullable: false,
2345 description: schema.details().description.clone(),
2346 default: None,
2347 },
2348 );
2349 return Ok(());
2350 }
2351 _ => {}
2352 }
2353 }
2354
2355 let previous_schema_name = self.current_schema_name.take();
2359 self.current_schema_name = Some(type_name.to_string());
2360 let analyzed = self.analyze_schema_value(schema, type_name)?;
2361 self.current_schema_name = previous_schema_name;
2362
2363 self.resolved_cache.insert(type_name.to_string(), analyzed);
2365
2366 if let Some(cached) = self.resolved_cache.get(type_name) {
2368 for dep in &cached.dependencies {
2369 dependencies.insert(dep.clone());
2370 }
2371 }
2372
2373 Ok(())
2374 }
2375
2376 fn extract_inline_discriminator_value(
2377 &self,
2378 schema: &Schema,
2379 discriminator_field: &str,
2380 variant_index: usize,
2381 ) -> String {
2382 if let Some(properties) = &schema.details().properties {
2384 if let Some(discriminator_prop) = properties.get(discriminator_field) {
2385 if let Some(enum_values) = &discriminator_prop.details().enum_values {
2387 if enum_values.len() == 1 {
2388 if let Some(value) = enum_values[0].as_str() {
2389 return value.to_string();
2390 }
2391 }
2392 }
2393 if let Some(const_value) = discriminator_prop.details().extra.get("const") {
2395 if let Some(value) = const_value.as_str() {
2396 return value.to_string();
2397 }
2398 }
2399 if let Some(const_value) = &discriminator_prop.details().const_value {
2401 if let Some(value) = const_value.as_str() {
2402 return value.to_string();
2403 }
2404 }
2405 }
2406 }
2407
2408 if let Some(inferred_name) = self.infer_variant_name_from_structure(schema, variant_index) {
2410 return inferred_name;
2411 }
2412
2413 format!("variant_{variant_index}")
2415 }
2416
2417 fn infer_variant_name_from_structure(
2418 &self,
2419 schema: &Schema,
2420 _variant_index: usize,
2421 ) -> Option<String> {
2422 let details = schema.details();
2423
2424 if let Some(properties) = &details.properties {
2426 if properties.contains_key("text") && properties.len() <= 3 {
2428 return Some("text".to_string());
2429 }
2430 if properties.contains_key("image") || properties.contains_key("source") {
2431 return Some("image".to_string());
2432 }
2433 if properties.contains_key("document") {
2434 return Some("document".to_string());
2435 }
2436 if properties.contains_key("tool_use_id") || properties.contains_key("tool_result") {
2437 return Some("tool_result".to_string());
2438 }
2439 if properties.contains_key("content") && properties.contains_key("is_error") {
2440 return Some("tool_result".to_string());
2441 }
2442 if properties.contains_key("partial_json") {
2443 return Some("partial_json".to_string());
2444 }
2445
2446 let property_names: Vec<&String> = properties.keys().collect();
2448
2449 for prop_name in &property_names {
2451 if prop_name.contains("result") {
2452 return Some("result".to_string());
2453 }
2454 if prop_name.contains("error") {
2455 return Some("error".to_string());
2456 }
2457 if prop_name.contains("content") && property_names.len() <= 2 {
2458 return Some("content".to_string());
2459 }
2460 }
2461
2462 let significant_props = property_names
2464 .iter()
2465 .filter(|&name| !["type", "id", "cache_control"].contains(&name.as_str()))
2466 .collect::<Vec<_>>();
2467
2468 if significant_props.len() == 1 {
2469 return Some((*significant_props[0]).clone());
2470 }
2471 }
2472
2473 if let Some(description) = &details.description {
2475 let desc_lower = description.to_lowercase();
2476 if desc_lower.contains("text") && desc_lower.len() < 100 {
2477 return Some("text".to_string());
2478 }
2479 if desc_lower.contains("image") {
2480 return Some("image".to_string());
2481 }
2482 if desc_lower.contains("document") {
2483 return Some("document".to_string());
2484 }
2485 if desc_lower.contains("tool") && desc_lower.contains("result") {
2486 return Some("tool_result".to_string());
2487 }
2488 }
2489
2490 None
2491 }
2492
2493 fn discriminator_to_variant_name(&self, discriminator: &str) -> String {
2494 if discriminator.is_empty() {
2496 return "Variant".to_string();
2497 }
2498
2499 let mut result = String::new();
2500 let mut next_upper = true;
2501
2502 for c in discriminator.chars() {
2503 match c {
2504 'a'..='z' => {
2505 if next_upper {
2506 result.push(c.to_ascii_uppercase());
2507 next_upper = false;
2508 } else {
2509 result.push(c);
2510 }
2511 }
2512 'A'..='Z' => {
2513 result.push(c);
2514 next_upper = false;
2515 }
2516 '0'..='9' => {
2517 result.push(c);
2518 next_upper = false;
2519 }
2520 '_' | '-' | '.' | ' ' | '/' | '\\' => {
2521 next_upper = true;
2523 }
2524 _ => {
2525 next_upper = true;
2527 }
2528 }
2529 }
2530
2531 if result.is_empty() || result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2533 result = format!("Variant{result}");
2534 }
2535
2536 result
2537 }
2538
2539 fn ensure_unique_variant_name(
2540 &self,
2541 base_name: String,
2542 used_names: &mut std::collections::HashSet<String>,
2543 ) -> String {
2544 let mut candidate = base_name.clone();
2545 let mut counter = 1;
2546
2547 while used_names.contains(&candidate) {
2548 counter += 1;
2549 candidate = format!("{base_name}{counter}");
2550 }
2551
2552 used_names.insert(candidate.clone());
2553 candidate
2554 }
2555
2556 fn generate_inline_type_name(&self, schema: &Schema, variant_index: usize) -> String {
2557 if let Some(meaningful_name) = self.infer_type_name_from_structure(schema) {
2559 return meaningful_name;
2560 }
2561
2562 let context = self.current_schema_name.as_deref().unwrap_or("Inline");
2564 self.generate_context_aware_name(context, "Variant", variant_index, Some(schema))
2565 }
2566
2567 fn infer_type_name_from_structure(&self, schema: &Schema) -> Option<String> {
2568 let details = schema.details();
2569
2570 if let Some(description) = &details.description {
2572 if let Some(name_from_desc) = self.extract_type_name_from_description(description) {
2573 return Some(name_from_desc);
2574 }
2575 }
2576
2577 if let Some(properties) = &details.properties {
2579 if let Some(name_from_props) = self.extract_type_name_from_properties(properties) {
2580 return Some(format!("{name_from_props}Block"));
2581 }
2582 }
2583
2584 None
2585 }
2586
2587 fn extract_type_name_from_description(&self, description: &str) -> Option<String> {
2588 if description.len() > 100 || description.contains('\n') {
2590 return None;
2591 }
2592
2593 let words: Vec<&str> = description
2595 .split_whitespace()
2596 .take(2) .filter(|word| {
2598 let w = word.to_lowercase();
2599 word.len() > 2
2600 && ![
2601 "the", "and", "for", "with", "that", "this", "are", "can", "will", "was",
2602 ]
2603 .contains(&w.as_str())
2604 })
2605 .collect();
2606
2607 if words.is_empty() {
2608 return None;
2609 }
2610
2611 let combined = words.join("_");
2613 let pascal_name = self.discriminator_to_variant_name(&combined);
2614
2615 if !pascal_name.ends_with("Content")
2617 && !pascal_name.ends_with("Block")
2618 && !pascal_name.ends_with("Type")
2619 {
2620 Some(format!("{pascal_name}Content"))
2621 } else {
2622 Some(pascal_name)
2623 }
2624 }
2625
2626 fn extract_type_name_from_properties(
2627 &self,
2628 properties: &std::collections::BTreeMap<String, crate::openapi::Schema>,
2629 ) -> Option<String> {
2630 let significant_props: Vec<&String> = properties
2632 .keys()
2633 .filter(|name| !["type", "id", "cache_control"].contains(&name.as_str()))
2634 .collect();
2635
2636 if significant_props.is_empty() {
2637 return None;
2638 }
2639
2640 if significant_props.len() == 1 {
2642 let prop_name = significant_props[0];
2643 return Some(self.discriminator_to_variant_name(prop_name));
2644 }
2645
2646 let mut sorted_props = significant_props.clone();
2649 sorted_props.sort();
2650 if let Some(first_prop) = sorted_props.first() {
2651 return Some(self.discriminator_to_variant_name(first_prop));
2652 }
2653
2654 None
2655 }
2656
2657 fn openapi_type_to_rust_type(
2658 &self,
2659 openapi_type: OpenApiSchemaType,
2660 details: &crate::openapi::SchemaDetails,
2661 ) -> String {
2662 match openapi_type {
2663 OpenApiSchemaType::String => "String".to_string(),
2664 OpenApiSchemaType::Integer => self.get_number_rust_type(openapi_type, details),
2665 OpenApiSchemaType::Number => self.get_number_rust_type(openapi_type, details),
2666 OpenApiSchemaType::Boolean => "bool".to_string(),
2667 OpenApiSchemaType::Array => "Vec<serde_json::Value>".to_string(), OpenApiSchemaType::Object => "serde_json::Value".to_string(), OpenApiSchemaType::Null => "()".to_string(), }
2671 }
2672
2673 #[allow(dead_code)]
2674 fn fallback_discriminator_value(&self, schema_name: &str) -> String {
2675 self.fallback_discriminator_value_for_field(schema_name, "type")
2676 }
2677
2678 fn fallback_discriminator_value_for_field(
2679 &self,
2680 schema_name: &str,
2681 field_name: &str,
2682 ) -> String {
2683 if let Some(ref_schema) = self.schemas.get(schema_name) {
2685 if let Some(extracted) =
2686 self.extract_discriminator_value_for_field(ref_schema, field_name)
2687 {
2688 return extracted;
2689 }
2690 }
2691
2692 self.generate_discriminator_value_from_name(schema_name)
2694 }
2695
2696 fn generate_discriminator_value_from_name(&self, schema_name: &str) -> String {
2697 let mut result = String::new();
2699 let mut chars = schema_name.chars().peekable();
2700 let mut first = true;
2701
2702 while let Some(c) = chars.next() {
2703 if c.is_uppercase()
2704 && !first
2705 && chars
2706 .peek()
2707 .map(|&next| next.is_lowercase())
2708 .unwrap_or(false)
2709 {
2710 result.push('.');
2711 }
2712 result.push(c.to_ascii_lowercase());
2713 first = false;
2714 }
2715
2716 if result.ends_with("event") {
2718 result = result[..result.len() - 5].to_string();
2719 }
2720
2721 if schema_name.starts_with("Response") && !result.starts_with("response.") {
2723 result = format!("response.{}", result.trim_start_matches("response"));
2724 }
2725
2726 result
2727 }
2728
2729 fn to_rust_variant_name(&self, schema_name: &str) -> String {
2730 let mut name = schema_name;
2732
2733 if name.starts_with("Response") && name.len() > 8 {
2735 name = &name[8..]; }
2737
2738 if name.ends_with("Event") && name.len() > 5 {
2740 name = &name[..name.len() - 5]; }
2742
2743 name = name.trim_matches('_');
2745
2746 if name.is_empty() {
2748 schema_name.to_string()
2749 } else {
2750 self.discriminator_to_variant_name(name)
2752 }
2753 }
2754
2755 fn analyze_array_schema(
2756 &mut self,
2757 schema: &Schema,
2758 parent_schema_name: &str,
2759 dependencies: &mut HashSet<String>,
2760 ) -> Result<SchemaType> {
2761 let details = schema.details();
2762
2763 if let Some(items_schema) = &details.items {
2765 let item_type = match items_schema.as_ref() {
2767 Schema::Reference { reference, .. } => {
2768 let target = self
2770 .extract_schema_name(reference)
2771 .ok_or_else(|| GeneratorError::UnresolvedReference(reference.to_string()))?
2772 .to_string();
2773 dependencies.insert(target.clone());
2774 SchemaType::Reference { target }
2775 }
2776 Schema::RecursiveRef { recursive_ref, .. } => {
2777 if recursive_ref == "#" {
2779 let target = self
2781 .find_recursive_anchor_schema()
2782 .unwrap_or_else(|| parent_schema_name.to_string());
2783 dependencies.insert(target.clone());
2784 SchemaType::Reference { target }
2785 } else {
2786 let target = self
2787 .extract_schema_name(recursive_ref)
2788 .unwrap_or("RecursiveType")
2789 .to_string();
2790 dependencies.insert(target.clone());
2791 SchemaType::Reference { target }
2792 }
2793 }
2794 Schema::Typed { schema_type, .. } => {
2795 match schema_type {
2797 OpenApiSchemaType::String => SchemaType::Primitive {
2798 rust_type: "String".to_string(),
2799 },
2800 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
2801 let details = items_schema.details();
2802 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
2803 SchemaType::Primitive { rust_type }
2804 }
2805 OpenApiSchemaType::Boolean => SchemaType::Primitive {
2806 rust_type: "bool".to_string(),
2807 },
2808 OpenApiSchemaType::Object => {
2809 let object_type_name = format!("{parent_schema_name}Item");
2811
2812 let object_type =
2814 self.analyze_object_schema(items_schema, dependencies)?;
2815
2816 let inline_schema = AnalyzedSchema {
2818 name: object_type_name.clone(),
2819 original: serde_json::to_value(items_schema).unwrap_or(Value::Null),
2820 schema_type: object_type,
2821 dependencies: dependencies.clone(),
2822 nullable: false,
2823 description: items_schema.details().description.clone(),
2824 default: None,
2825 };
2826
2827 self.resolved_cache
2829 .insert(object_type_name.clone(), inline_schema);
2830 dependencies.insert(object_type_name.clone());
2831
2832 SchemaType::Reference {
2834 target: object_type_name,
2835 }
2836 }
2837 OpenApiSchemaType::Array => {
2838 self.analyze_array_schema(
2840 items_schema,
2841 parent_schema_name,
2842 dependencies,
2843 )?
2844 }
2845 _ => SchemaType::Primitive {
2846 rust_type: "serde_json::Value".to_string(),
2847 },
2848 }
2849 }
2850 Schema::OneOf { .. } | Schema::AnyOf { .. } => {
2851 let analyzed = self.analyze_schema_value(items_schema, "ArrayItem")?;
2853
2854 match &analyzed.schema_type {
2856 SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => {
2857 let union_name = format!("{parent_schema_name}ItemUnion");
2860
2861 let mut union_schema = analyzed;
2863 union_schema.name = union_name.clone();
2864
2865 self.resolved_cache.insert(union_name.clone(), union_schema);
2867
2868 dependencies.insert(union_name.clone());
2870
2871 SchemaType::Reference { target: union_name }
2873 }
2874 _ => analyzed.schema_type,
2875 }
2876 }
2877 Schema::Untyped { .. } => {
2878 if let Some(inferred) = items_schema.inferred_type() {
2880 match inferred {
2881 OpenApiSchemaType::Object => {
2882 let object_type_name = format!("{parent_schema_name}Item");
2884
2885 let object_type =
2887 self.analyze_object_schema(items_schema, dependencies)?;
2888
2889 let inline_schema = AnalyzedSchema {
2891 name: object_type_name.clone(),
2892 original: serde_json::to_value(items_schema)
2893 .unwrap_or(Value::Null),
2894 schema_type: object_type,
2895 dependencies: dependencies.clone(),
2896 nullable: false,
2897 description: items_schema.details().description.clone(),
2898 default: None,
2899 };
2900
2901 self.resolved_cache
2903 .insert(object_type_name.clone(), inline_schema);
2904 dependencies.insert(object_type_name.clone());
2905
2906 SchemaType::Reference {
2908 target: object_type_name,
2909 }
2910 }
2911 OpenApiSchemaType::String => SchemaType::Primitive {
2912 rust_type: "String".to_string(),
2913 },
2914 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
2915 let details = items_schema.details();
2916 let rust_type = self.get_number_rust_type(inferred, details);
2917 SchemaType::Primitive { rust_type }
2918 }
2919 OpenApiSchemaType::Boolean => SchemaType::Primitive {
2920 rust_type: "bool".to_string(),
2921 },
2922 _ => SchemaType::Primitive {
2923 rust_type: "serde_json::Value".to_string(),
2924 },
2925 }
2926 } else {
2927 SchemaType::Primitive {
2928 rust_type: "serde_json::Value".to_string(),
2929 }
2930 }
2931 }
2932 _ => SchemaType::Primitive {
2933 rust_type: "serde_json::Value".to_string(),
2934 },
2935 };
2936
2937 Ok(SchemaType::Array {
2938 item_type: Box::new(item_type),
2939 })
2940 } else {
2941 Ok(SchemaType::Primitive {
2943 rust_type: "Vec<serde_json::Value>".to_string(),
2944 })
2945 }
2946 }
2947
2948 fn get_number_rust_type(
2949 &self,
2950 schema_type: OpenApiSchemaType,
2951 details: &crate::openapi::SchemaDetails,
2952 ) -> String {
2953 match schema_type {
2954 OpenApiSchemaType::Integer => {
2955 match details.format.as_deref() {
2957 Some("int32") => "i32".to_string(),
2958 Some("int64") => "i64".to_string(),
2959 _ => "i64".to_string(), }
2961 }
2962 OpenApiSchemaType::Number => {
2963 match details.format.as_deref() {
2965 Some("float") => "f32".to_string(),
2966 Some("double") => "f64".to_string(),
2967 _ => "f64".to_string(), }
2969 }
2970 _ => "serde_json::Value".to_string(), }
2972 }
2973
2974 fn analyze_anyof_union(
2975 &mut self,
2976 any_of_schemas: &[Schema],
2977 discriminator: Option<&Discriminator>,
2978 dependencies: &mut HashSet<String>,
2979 context_name: &str,
2980 ) -> Result<SchemaType> {
2981 if any_of_schemas.len() == 2 {
2985 let null_count = any_of_schemas
2986 .iter()
2987 .filter(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
2988 .count();
2989 if null_count == 1 {
2990 for schema in any_of_schemas {
2992 if !matches!(schema.schema_type(), Some(OpenApiSchemaType::Null)) {
2993 return self
2996 .analyze_schema_value(schema, context_name)
2997 .map(|a| a.schema_type);
2998 }
2999 }
3000 }
3001 }
3002
3003 let has_refs = any_of_schemas.iter().any(|s| s.is_reference());
3005 let has_objects = any_of_schemas.iter().any(|s| {
3006 matches!(s.schema_type(), Some(OpenApiSchemaType::Object))
3007 || s.inferred_type() == Some(OpenApiSchemaType::Object)
3008 });
3009 let has_arrays = any_of_schemas
3010 .iter()
3011 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Array)));
3012
3013 let all_string_like = any_of_schemas.iter().all(|s| {
3016 matches!(s.schema_type(), Some(OpenApiSchemaType::String))
3017 || s.details().const_value.is_some()
3018 });
3019
3020 if (has_refs || has_objects || has_arrays || any_of_schemas.len() > 1) && !all_string_like {
3021 if let Some(disc) = discriminator {
3023 return self.analyze_oneof_union(any_of_schemas, Some(disc), None, dependencies);
3025 }
3026
3027 if let Some(disc_field) = self.detect_discriminator_field(any_of_schemas) {
3029 return self.analyze_oneof_union(
3030 any_of_schemas,
3031 Some(&Discriminator {
3032 property_name: disc_field,
3033 mapping: None,
3034 extra: BTreeMap::new(),
3035 }),
3036 None,
3037 dependencies,
3038 );
3039 }
3040
3041 let mut variants = Vec::new();
3043
3044 for schema in any_of_schemas {
3045 if let Some(ref_str) = schema.reference() {
3046 if let Some(target) = self.extract_schema_name(ref_str) {
3047 dependencies.insert(target.to_string());
3048 variants.push(SchemaRef {
3049 target: target.to_string(),
3050 nullable: false,
3051 });
3052 }
3053 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object))
3054 || schema.inferred_type() == Some(OpenApiSchemaType::Object)
3055 {
3056 let inline_index = variants.len();
3058 let inline_type_name = self.generate_inline_type_name(schema, inline_index);
3059
3060 self.add_inline_schema(&inline_type_name, schema, dependencies)?;
3062
3063 variants.push(SchemaRef {
3064 target: inline_type_name,
3065 nullable: false,
3066 });
3067 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Array)) {
3068 let array_type =
3070 self.analyze_array_schema(schema, context_name, dependencies)?;
3071
3072 let array_type_name = if let Some(items_schema) = &schema.details().items {
3074 if let Some(ref_str) = items_schema.reference() {
3075 if let Some(item_type_name) = self.extract_schema_name(ref_str) {
3076 dependencies.insert(item_type_name.to_string());
3077 format!("{item_type_name}Array")
3078 } else {
3079 self.generate_context_aware_name(
3080 context_name,
3081 "Array",
3082 variants.len(),
3083 Some(schema),
3084 )
3085 }
3086 } else {
3087 self.generate_context_aware_name(
3088 context_name,
3089 "Array",
3090 variants.len(),
3091 Some(schema),
3092 )
3093 }
3094 } else {
3095 self.generate_context_aware_name(
3096 context_name,
3097 "Array",
3098 variants.len(),
3099 Some(schema),
3100 )
3101 };
3102
3103 self.resolved_cache.insert(
3105 array_type_name.clone(),
3106 AnalyzedSchema {
3107 name: array_type_name.clone(),
3108 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3109 schema_type: array_type,
3110 dependencies: HashSet::new(),
3111 nullable: false,
3112 description: Some("Array variant in union".to_string()),
3113 default: None,
3114 },
3115 );
3116
3117 dependencies.insert(array_type_name.clone());
3119
3120 variants.push(SchemaRef {
3121 target: array_type_name,
3122 nullable: false,
3123 });
3124 } else if let Some(schema_type) = schema.schema_type() {
3125 let inline_index = variants.len();
3127
3128 let inline_type_name = match schema_type {
3130 OpenApiSchemaType::String => {
3131 if inline_index == 0 {
3134 format!("{context_name}String")
3135 } else {
3136 format!("{context_name}StringVariant{inline_index}")
3137 }
3138 }
3139 OpenApiSchemaType::Number => {
3140 if inline_index == 0 {
3141 format!("{context_name}Number")
3142 } else {
3143 format!("{context_name}NumberVariant{inline_index}")
3144 }
3145 }
3146 OpenApiSchemaType::Integer => {
3147 if inline_index == 0 {
3148 format!("{context_name}Integer")
3149 } else {
3150 format!("{context_name}IntegerVariant{inline_index}")
3151 }
3152 }
3153 OpenApiSchemaType::Boolean => {
3154 if inline_index == 0 {
3155 format!("{context_name}Boolean")
3156 } else {
3157 format!("{context_name}BooleanVariant{inline_index}")
3158 }
3159 }
3160 _ => format!("{context_name}Variant{inline_index}"),
3161 };
3162
3163 let rust_type =
3164 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
3165
3166 self.resolved_cache.insert(
3168 inline_type_name.clone(),
3169 AnalyzedSchema {
3170 name: inline_type_name.clone(),
3171 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3172 schema_type: SchemaType::Primitive { rust_type },
3173 dependencies: HashSet::new(),
3174 nullable: false,
3175 description: schema.details().description.clone(),
3176 default: None,
3177 },
3178 );
3179
3180 dependencies.insert(inline_type_name.clone());
3182
3183 variants.push(SchemaRef {
3184 target: inline_type_name,
3185 nullable: false,
3186 });
3187 }
3188 }
3189
3190 if !variants.is_empty() {
3191 return Ok(SchemaType::Union { variants });
3192 }
3193 }
3194
3195 let all_strings = any_of_schemas.iter().all(|schema| {
3197 matches!(schema.schema_type(), Some(OpenApiSchemaType::String))
3198 || schema.details().const_value.is_some()
3199 });
3200
3201 if all_strings {
3202 let mut enum_values = Vec::new();
3204 let mut has_open_string = false;
3205
3206 for schema in any_of_schemas {
3207 if let Some(const_val) = &schema.details().const_value {
3208 if let Some(const_str) = const_val.as_str() {
3209 enum_values.push(const_str.to_string());
3210 }
3211 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::String)) {
3212 has_open_string = true;
3213 }
3214 }
3215
3216 if !enum_values.is_empty() {
3217 if has_open_string {
3218 return Ok(SchemaType::ExtensibleEnum {
3221 known_values: enum_values,
3222 });
3223 } else {
3224 return Ok(SchemaType::StringEnum {
3226 values: enum_values,
3227 });
3228 }
3229 }
3230 }
3231
3232 Ok(SchemaType::Primitive {
3234 rust_type: "serde_json::Value".to_string(),
3235 })
3236 }
3237
3238 fn find_recursive_anchor_schema(&self) -> Option<String> {
3240 for (schema_name, schema) in &self.schemas {
3242 let details = schema.details();
3243 if details.recursive_anchor == Some(true) {
3244 return Some(schema_name.clone());
3245 }
3246 }
3247
3248 None
3252 }
3253
3254 fn should_use_dynamic_json(&self, schema: &Schema) -> bool {
3257 if let Schema::AnyOf { any_of, .. } = schema {
3259 if any_of.len() == 2 {
3260 let has_null = any_of
3261 .iter()
3262 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)));
3263 let has_empty_object = any_of.iter().any(|s| self.is_dynamic_object_pattern(s));
3264
3265 if has_null && has_empty_object {
3266 return true;
3267 }
3268 }
3269 }
3270
3271 self.is_dynamic_object_pattern(schema)
3273 }
3274
3275 fn is_dynamic_object_pattern(&self, schema: &Schema) -> bool {
3277 let is_object = match schema.schema_type() {
3279 Some(OpenApiSchemaType::Object) => true,
3280 None => schema.inferred_type() == Some(OpenApiSchemaType::Object),
3281 _ => false,
3282 };
3283
3284 if !is_object {
3285 return false;
3286 }
3287
3288 let details = schema.details();
3289
3290 if self.has_explicit_additional_properties(schema) {
3293 return false;
3294 }
3295
3296 let no_properties = details
3298 .properties
3299 .as_ref()
3300 .map(|props| props.is_empty())
3301 .unwrap_or(true);
3302
3303 if no_properties {
3304 let has_structural_constraints =
3306 details.required.as_ref()
3308 .map(|req| req.iter().any(|r| r != "type"))
3309 .unwrap_or(false)
3310 || details.extra.contains_key("patternProperties")
3312 || details.extra.contains_key("propertyNames")
3314 || details.extra.contains_key("minProperties")
3316 || details.extra.contains_key("maxProperties")
3317 || details.extra.contains_key("dependencies")
3319 || details.extra.contains_key("if")
3321 || details.extra.contains_key("then")
3322 || details.extra.contains_key("else");
3323
3324 return !has_structural_constraints;
3325 }
3326
3327 false
3328 }
3329
3330 fn has_explicit_additional_properties(&self, schema: &Schema) -> bool {
3332 let details = schema.details();
3333
3334 matches!(
3336 &details.additional_properties,
3337 Some(crate::openapi::AdditionalProperties::Boolean(true))
3338 | Some(crate::openapi::AdditionalProperties::Schema(_))
3339 )
3340 }
3341
3342 fn analyze_operations(&mut self, analysis: &mut SchemaAnalysis) -> Result<()> {
3344 let spec: crate::openapi::OpenApiSpec = serde_json::from_value(self.openapi_spec.clone())
3345 .map_err(GeneratorError::ParseError)?;
3346
3347 if let Some(paths) = &spec.paths {
3348 for (path, path_item) in paths {
3349 for (method, operation) in path_item.operations() {
3350 let operation_id = operation
3352 .operation_id
3353 .clone()
3354 .unwrap_or_else(|| Self::generate_operation_id(method, path));
3355
3356 let op_info = self.analyze_single_operation(
3357 &operation_id,
3358 method,
3359 path,
3360 operation,
3361 analysis,
3362 )?;
3363 analysis.operations.insert(operation_id, op_info);
3364 }
3365 }
3366 }
3367 Ok(())
3368 }
3369
3370 fn generate_operation_id(method: &str, path: &str) -> String {
3373 let mut operation_id = method.to_lowercase();
3375
3376 let path_parts: Vec<&str> = path.trim_start_matches('/').split('/').collect();
3378
3379 for part in path_parts {
3380 if part.is_empty() {
3381 continue;
3382 }
3383
3384 let cleaned_part = if part.starts_with('{') && part.ends_with('}') {
3386 &part[1..part.len() - 1]
3387 } else {
3388 part
3389 };
3390
3391 let pascal_case_part = cleaned_part
3393 .split(&['-', '_'][..])
3394 .map(|s| {
3395 let mut chars = s.chars();
3396 match chars.next() {
3397 None => String::new(),
3398 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
3399 }
3400 })
3401 .collect::<String>();
3402
3403 operation_id.push_str(&pascal_case_part);
3404 }
3405
3406 operation_id
3407 }
3408
3409 fn analyze_single_operation(
3411 &mut self,
3412 operation_id: &str,
3413 method: &str,
3414 path: &str,
3415 operation: &crate::openapi::Operation,
3416 _analysis: &mut SchemaAnalysis,
3417 ) -> Result<OperationInfo> {
3418 let mut op_info = OperationInfo {
3419 operation_id: operation_id.to_string(),
3420 method: method.to_uppercase(),
3421 path: path.to_string(),
3422 request_body: None,
3423 response_schemas: BTreeMap::new(),
3424 parameters: Vec::new(),
3425 supports_streaming: false, stream_parameter: None, };
3428
3429 if let Some(request_body) = &operation.request_body
3431 && let Some((content_type, maybe_schema)) = request_body.best_content()
3432 {
3433 op_info.request_body = match content_type {
3434 "application/json" => maybe_schema
3435 .map(|s| {
3436 self.resolve_or_inline_schema(s, operation_id, "Request")
3437 .map(|name| RequestBodyContent::Json { schema_name: name })
3438 })
3439 .transpose()?,
3440 "application/x-www-form-urlencoded" => maybe_schema
3441 .map(|s| {
3442 self.resolve_or_inline_schema(s, operation_id, "Request")
3443 .map(|name| RequestBodyContent::FormUrlEncoded { schema_name: name })
3444 })
3445 .transpose()?,
3446 "multipart/form-data" => Some(RequestBodyContent::Multipart),
3447 "application/octet-stream" => Some(RequestBodyContent::OctetStream),
3448 "text/plain" => Some(RequestBodyContent::TextPlain),
3449 _ => None,
3450 };
3451 }
3452
3453 if let Some(responses) = &operation.responses {
3455 for (status_code, response) in responses {
3456 if let Some(schema) = response.json_schema() {
3457 if let Some(schema_ref) = schema.reference() {
3458 if let Some(schema_name) = self.extract_schema_name(schema_ref) {
3460 op_info
3461 .response_schemas
3462 .insert(status_code.clone(), schema_name.to_string());
3463 }
3464 } else {
3465 let synthetic_name =
3467 self.generate_inline_response_type_name(operation_id, status_code);
3468
3469 let mut deps = HashSet::new();
3471 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
3472
3473 op_info
3474 .response_schemas
3475 .insert(status_code.clone(), synthetic_name);
3476 }
3477 }
3478 }
3479 }
3480
3481 if let Some(parameters) = &operation.parameters {
3483 for param in parameters {
3484 if let Some(param_info) = self.analyze_parameter(param)? {
3485 op_info.parameters.push(param_info);
3486 }
3487 }
3488 }
3489
3490 Ok(op_info)
3491 }
3492
3493 fn generate_inline_response_type_name(&self, operation_id: &str, _status_code: &str) -> String {
3495 use heck::ToPascalCase;
3496 let base_name = operation_id.replace('.', "_").to_pascal_case();
3500 format!("{}Response", base_name)
3501 }
3502
3503 fn generate_inline_request_type_name(&self, operation_id: &str) -> String {
3505 use heck::ToPascalCase;
3506 let base_name = operation_id.replace('.', "_").to_pascal_case();
3510 format!("{}Request", base_name)
3511 }
3512
3513 fn resolve_or_inline_schema(
3516 &mut self,
3517 schema: &crate::openapi::Schema,
3518 operation_id: &str,
3519 suffix: &str,
3520 ) -> Result<String> {
3521 if let Some(schema_ref) = schema.reference()
3522 && let Some(schema_name) = self.extract_schema_name(schema_ref)
3523 {
3524 return Ok(schema_name.to_string());
3525 }
3526 let synthetic_name = if suffix == "Request" {
3528 self.generate_inline_request_type_name(operation_id)
3529 } else {
3530 self.generate_inline_response_type_name(operation_id, "")
3531 };
3532 let mut deps = HashSet::new();
3533 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
3534 Ok(synthetic_name)
3535 }
3536
3537 fn analyze_parameter(
3539 &self,
3540 param: &crate::openapi::Parameter,
3541 ) -> Result<Option<ParameterInfo>> {
3542 let name = param.name.as_deref().unwrap_or("");
3543 let location = param.location.as_deref().unwrap_or("");
3544 let required = param.required.unwrap_or(false);
3545
3546 let mut rust_type = "String".to_string();
3547 let mut schema_ref = None;
3548
3549 if let Some(schema) = ¶m.schema {
3550 if let Some(ref_str) = schema.reference() {
3551 schema_ref = self.extract_schema_name(ref_str).map(|s| s.to_string());
3552 } else if let Some(schema_type) = schema.schema_type() {
3553 rust_type = match schema_type {
3554 crate::openapi::SchemaType::Boolean => "bool",
3555 crate::openapi::SchemaType::Integer => "i64",
3556 crate::openapi::SchemaType::Number => "f64",
3557 crate::openapi::SchemaType::String => "String",
3558 _ => "String",
3559 }
3560 .to_string();
3561 }
3562 }
3563
3564 Ok(Some(ParameterInfo {
3565 name: name.to_string(),
3566 location: location.to_string(),
3567 required,
3568 schema_ref,
3569 rust_type,
3570 }))
3571 }
3572}