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 analyzed = self.analyze_schema_value(schema, type_name)?;
2357
2358 self.resolved_cache.insert(type_name.to_string(), analyzed);
2360
2361 if let Some(cached) = self.resolved_cache.get(type_name) {
2363 for dep in &cached.dependencies {
2364 dependencies.insert(dep.clone());
2365 }
2366 }
2367
2368 Ok(())
2369 }
2370
2371 fn extract_inline_discriminator_value(
2372 &self,
2373 schema: &Schema,
2374 discriminator_field: &str,
2375 variant_index: usize,
2376 ) -> String {
2377 if let Some(properties) = &schema.details().properties {
2379 if let Some(discriminator_prop) = properties.get(discriminator_field) {
2380 if let Some(enum_values) = &discriminator_prop.details().enum_values {
2382 if enum_values.len() == 1 {
2383 if let Some(value) = enum_values[0].as_str() {
2384 return value.to_string();
2385 }
2386 }
2387 }
2388 if let Some(const_value) = discriminator_prop.details().extra.get("const") {
2390 if let Some(value) = const_value.as_str() {
2391 return value.to_string();
2392 }
2393 }
2394 if let Some(const_value) = &discriminator_prop.details().const_value {
2396 if let Some(value) = const_value.as_str() {
2397 return value.to_string();
2398 }
2399 }
2400 }
2401 }
2402
2403 if let Some(inferred_name) = self.infer_variant_name_from_structure(schema, variant_index) {
2405 return inferred_name;
2406 }
2407
2408 format!("variant_{variant_index}")
2410 }
2411
2412 fn infer_variant_name_from_structure(
2413 &self,
2414 schema: &Schema,
2415 _variant_index: usize,
2416 ) -> Option<String> {
2417 let details = schema.details();
2418
2419 if let Some(properties) = &details.properties {
2421 if properties.contains_key("text") && properties.len() <= 3 {
2423 return Some("text".to_string());
2424 }
2425 if properties.contains_key("image") || properties.contains_key("source") {
2426 return Some("image".to_string());
2427 }
2428 if properties.contains_key("document") {
2429 return Some("document".to_string());
2430 }
2431 if properties.contains_key("tool_use_id") || properties.contains_key("tool_result") {
2432 return Some("tool_result".to_string());
2433 }
2434 if properties.contains_key("content") && properties.contains_key("is_error") {
2435 return Some("tool_result".to_string());
2436 }
2437 if properties.contains_key("partial_json") {
2438 return Some("partial_json".to_string());
2439 }
2440
2441 let property_names: Vec<&String> = properties.keys().collect();
2443
2444 for prop_name in &property_names {
2446 if prop_name.contains("result") {
2447 return Some("result".to_string());
2448 }
2449 if prop_name.contains("error") {
2450 return Some("error".to_string());
2451 }
2452 if prop_name.contains("content") && property_names.len() <= 2 {
2453 return Some("content".to_string());
2454 }
2455 }
2456
2457 let significant_props = property_names
2459 .iter()
2460 .filter(|&name| !["type", "id", "cache_control"].contains(&name.as_str()))
2461 .collect::<Vec<_>>();
2462
2463 if significant_props.len() == 1 {
2464 return Some((*significant_props[0]).clone());
2465 }
2466 }
2467
2468 if let Some(description) = &details.description {
2470 let desc_lower = description.to_lowercase();
2471 if desc_lower.contains("text") && desc_lower.len() < 100 {
2472 return Some("text".to_string());
2473 }
2474 if desc_lower.contains("image") {
2475 return Some("image".to_string());
2476 }
2477 if desc_lower.contains("document") {
2478 return Some("document".to_string());
2479 }
2480 if desc_lower.contains("tool") && desc_lower.contains("result") {
2481 return Some("tool_result".to_string());
2482 }
2483 }
2484
2485 None
2486 }
2487
2488 fn discriminator_to_variant_name(&self, discriminator: &str) -> String {
2489 if discriminator.is_empty() {
2491 return "Variant".to_string();
2492 }
2493
2494 let mut result = String::new();
2495 let mut next_upper = true;
2496
2497 for c in discriminator.chars() {
2498 match c {
2499 'a'..='z' => {
2500 if next_upper {
2501 result.push(c.to_ascii_uppercase());
2502 next_upper = false;
2503 } else {
2504 result.push(c);
2505 }
2506 }
2507 'A'..='Z' => {
2508 result.push(c);
2509 next_upper = false;
2510 }
2511 '0'..='9' => {
2512 result.push(c);
2513 next_upper = false;
2514 }
2515 '_' | '-' | '.' | ' ' | '/' | '\\' => {
2516 next_upper = true;
2518 }
2519 _ => {
2520 next_upper = true;
2522 }
2523 }
2524 }
2525
2526 if result.is_empty() || result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2528 result = format!("Variant{result}");
2529 }
2530
2531 result
2532 }
2533
2534 fn ensure_unique_variant_name(
2535 &self,
2536 base_name: String,
2537 used_names: &mut std::collections::HashSet<String>,
2538 ) -> String {
2539 let mut candidate = base_name.clone();
2540 let mut counter = 1;
2541
2542 while used_names.contains(&candidate) {
2543 counter += 1;
2544 candidate = format!("{base_name}{counter}");
2545 }
2546
2547 used_names.insert(candidate.clone());
2548 candidate
2549 }
2550
2551 fn generate_inline_type_name(&self, schema: &Schema, variant_index: usize) -> String {
2552 if let Some(meaningful_name) = self.infer_type_name_from_structure(schema) {
2554 return meaningful_name;
2555 }
2556
2557 let context = self.current_schema_name.as_deref().unwrap_or("Inline");
2559 self.generate_context_aware_name(context, "Variant", variant_index, Some(schema))
2560 }
2561
2562 fn infer_type_name_from_structure(&self, schema: &Schema) -> Option<String> {
2563 let details = schema.details();
2564
2565 if let Some(description) = &details.description {
2567 if let Some(name_from_desc) = self.extract_type_name_from_description(description) {
2568 return Some(name_from_desc);
2569 }
2570 }
2571
2572 if let Some(properties) = &details.properties {
2574 if let Some(name_from_props) = self.extract_type_name_from_properties(properties) {
2575 return Some(format!("{name_from_props}Block"));
2576 }
2577 }
2578
2579 None
2580 }
2581
2582 fn extract_type_name_from_description(&self, description: &str) -> Option<String> {
2583 if description.len() > 100 || description.contains('\n') {
2585 return None;
2586 }
2587
2588 let words: Vec<&str> = description
2590 .split_whitespace()
2591 .take(2) .filter(|word| {
2593 let w = word.to_lowercase();
2594 word.len() > 2
2595 && ![
2596 "the", "and", "for", "with", "that", "this", "are", "can", "will", "was",
2597 ]
2598 .contains(&w.as_str())
2599 })
2600 .collect();
2601
2602 if words.is_empty() {
2603 return None;
2604 }
2605
2606 let combined = words.join("_");
2608 let pascal_name = self.discriminator_to_variant_name(&combined);
2609
2610 if !pascal_name.ends_with("Content")
2612 && !pascal_name.ends_with("Block")
2613 && !pascal_name.ends_with("Type")
2614 {
2615 Some(format!("{pascal_name}Content"))
2616 } else {
2617 Some(pascal_name)
2618 }
2619 }
2620
2621 fn extract_type_name_from_properties(
2622 &self,
2623 properties: &std::collections::BTreeMap<String, crate::openapi::Schema>,
2624 ) -> Option<String> {
2625 let significant_props: Vec<&String> = properties
2627 .keys()
2628 .filter(|name| !["type", "id", "cache_control"].contains(&name.as_str()))
2629 .collect();
2630
2631 if significant_props.is_empty() {
2632 return None;
2633 }
2634
2635 if significant_props.len() == 1 {
2637 let prop_name = significant_props[0];
2638 return Some(self.discriminator_to_variant_name(prop_name));
2639 }
2640
2641 let mut sorted_props = significant_props.clone();
2644 sorted_props.sort();
2645 if let Some(first_prop) = sorted_props.first() {
2646 return Some(self.discriminator_to_variant_name(first_prop));
2647 }
2648
2649 None
2650 }
2651
2652 fn openapi_type_to_rust_type(
2653 &self,
2654 openapi_type: OpenApiSchemaType,
2655 details: &crate::openapi::SchemaDetails,
2656 ) -> String {
2657 match openapi_type {
2658 OpenApiSchemaType::String => "String".to_string(),
2659 OpenApiSchemaType::Integer => self.get_number_rust_type(openapi_type, details),
2660 OpenApiSchemaType::Number => self.get_number_rust_type(openapi_type, details),
2661 OpenApiSchemaType::Boolean => "bool".to_string(),
2662 OpenApiSchemaType::Array => "Vec<serde_json::Value>".to_string(), OpenApiSchemaType::Object => "serde_json::Value".to_string(), OpenApiSchemaType::Null => "()".to_string(), }
2666 }
2667
2668 #[allow(dead_code)]
2669 fn fallback_discriminator_value(&self, schema_name: &str) -> String {
2670 self.fallback_discriminator_value_for_field(schema_name, "type")
2671 }
2672
2673 fn fallback_discriminator_value_for_field(
2674 &self,
2675 schema_name: &str,
2676 field_name: &str,
2677 ) -> String {
2678 if let Some(ref_schema) = self.schemas.get(schema_name) {
2680 if let Some(extracted) =
2681 self.extract_discriminator_value_for_field(ref_schema, field_name)
2682 {
2683 return extracted;
2684 }
2685 }
2686
2687 self.generate_discriminator_value_from_name(schema_name)
2689 }
2690
2691 fn generate_discriminator_value_from_name(&self, schema_name: &str) -> String {
2692 let mut result = String::new();
2694 let mut chars = schema_name.chars().peekable();
2695 let mut first = true;
2696
2697 while let Some(c) = chars.next() {
2698 if c.is_uppercase()
2699 && !first
2700 && chars
2701 .peek()
2702 .map(|&next| next.is_lowercase())
2703 .unwrap_or(false)
2704 {
2705 result.push('.');
2706 }
2707 result.push(c.to_ascii_lowercase());
2708 first = false;
2709 }
2710
2711 if result.ends_with("event") {
2713 result = result[..result.len() - 5].to_string();
2714 }
2715
2716 if schema_name.starts_with("Response") && !result.starts_with("response.") {
2718 result = format!("response.{}", result.trim_start_matches("response"));
2719 }
2720
2721 result
2722 }
2723
2724 fn to_rust_variant_name(&self, schema_name: &str) -> String {
2725 let mut name = schema_name;
2727
2728 if name.starts_with("Response") && name.len() > 8 {
2730 name = &name[8..]; }
2732
2733 if name.ends_with("Event") && name.len() > 5 {
2735 name = &name[..name.len() - 5]; }
2737
2738 name = name.trim_matches('_');
2740
2741 if name.is_empty() {
2743 schema_name.to_string()
2744 } else {
2745 self.discriminator_to_variant_name(name)
2747 }
2748 }
2749
2750 fn analyze_array_schema(
2751 &mut self,
2752 schema: &Schema,
2753 parent_schema_name: &str,
2754 dependencies: &mut HashSet<String>,
2755 ) -> Result<SchemaType> {
2756 let details = schema.details();
2757
2758 if let Some(items_schema) = &details.items {
2760 let item_type = match items_schema.as_ref() {
2762 Schema::Reference { reference, .. } => {
2763 let target = self
2765 .extract_schema_name(reference)
2766 .ok_or_else(|| GeneratorError::UnresolvedReference(reference.to_string()))?
2767 .to_string();
2768 dependencies.insert(target.clone());
2769 SchemaType::Reference { target }
2770 }
2771 Schema::RecursiveRef { recursive_ref, .. } => {
2772 if recursive_ref == "#" {
2774 let target = self
2776 .find_recursive_anchor_schema()
2777 .unwrap_or_else(|| parent_schema_name.to_string());
2778 dependencies.insert(target.clone());
2779 SchemaType::Reference { target }
2780 } else {
2781 let target = self
2782 .extract_schema_name(recursive_ref)
2783 .unwrap_or("RecursiveType")
2784 .to_string();
2785 dependencies.insert(target.clone());
2786 SchemaType::Reference { target }
2787 }
2788 }
2789 Schema::Typed { schema_type, .. } => {
2790 match schema_type {
2792 OpenApiSchemaType::String => SchemaType::Primitive {
2793 rust_type: "String".to_string(),
2794 },
2795 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
2796 let details = items_schema.details();
2797 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
2798 SchemaType::Primitive { rust_type }
2799 }
2800 OpenApiSchemaType::Boolean => SchemaType::Primitive {
2801 rust_type: "bool".to_string(),
2802 },
2803 OpenApiSchemaType::Object => {
2804 let object_type_name = format!("{parent_schema_name}Item");
2806
2807 let object_type =
2809 self.analyze_object_schema(items_schema, dependencies)?;
2810
2811 let inline_schema = AnalyzedSchema {
2813 name: object_type_name.clone(),
2814 original: serde_json::to_value(items_schema).unwrap_or(Value::Null),
2815 schema_type: object_type,
2816 dependencies: dependencies.clone(),
2817 nullable: false,
2818 description: items_schema.details().description.clone(),
2819 default: None,
2820 };
2821
2822 self.resolved_cache
2824 .insert(object_type_name.clone(), inline_schema);
2825 dependencies.insert(object_type_name.clone());
2826
2827 SchemaType::Reference {
2829 target: object_type_name,
2830 }
2831 }
2832 OpenApiSchemaType::Array => {
2833 self.analyze_array_schema(
2835 items_schema,
2836 parent_schema_name,
2837 dependencies,
2838 )?
2839 }
2840 _ => SchemaType::Primitive {
2841 rust_type: "serde_json::Value".to_string(),
2842 },
2843 }
2844 }
2845 Schema::OneOf { .. } | Schema::AnyOf { .. } => {
2846 let analyzed = self.analyze_schema_value(items_schema, "ArrayItem")?;
2848
2849 match &analyzed.schema_type {
2851 SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => {
2852 let union_name = format!("{parent_schema_name}ItemUnion");
2855
2856 let mut union_schema = analyzed;
2858 union_schema.name = union_name.clone();
2859
2860 self.resolved_cache.insert(union_name.clone(), union_schema);
2862
2863 dependencies.insert(union_name.clone());
2865
2866 SchemaType::Reference { target: union_name }
2868 }
2869 _ => analyzed.schema_type,
2870 }
2871 }
2872 Schema::Untyped { .. } => {
2873 if let Some(inferred) = items_schema.inferred_type() {
2875 match inferred {
2876 OpenApiSchemaType::Object => {
2877 let object_type_name = format!("{parent_schema_name}Item");
2879
2880 let object_type =
2882 self.analyze_object_schema(items_schema, dependencies)?;
2883
2884 let inline_schema = AnalyzedSchema {
2886 name: object_type_name.clone(),
2887 original: serde_json::to_value(items_schema)
2888 .unwrap_or(Value::Null),
2889 schema_type: object_type,
2890 dependencies: dependencies.clone(),
2891 nullable: false,
2892 description: items_schema.details().description.clone(),
2893 default: None,
2894 };
2895
2896 self.resolved_cache
2898 .insert(object_type_name.clone(), inline_schema);
2899 dependencies.insert(object_type_name.clone());
2900
2901 SchemaType::Reference {
2903 target: object_type_name,
2904 }
2905 }
2906 OpenApiSchemaType::String => SchemaType::Primitive {
2907 rust_type: "String".to_string(),
2908 },
2909 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
2910 let details = items_schema.details();
2911 let rust_type = self.get_number_rust_type(inferred, details);
2912 SchemaType::Primitive { rust_type }
2913 }
2914 OpenApiSchemaType::Boolean => SchemaType::Primitive {
2915 rust_type: "bool".to_string(),
2916 },
2917 _ => SchemaType::Primitive {
2918 rust_type: "serde_json::Value".to_string(),
2919 },
2920 }
2921 } else {
2922 SchemaType::Primitive {
2923 rust_type: "serde_json::Value".to_string(),
2924 }
2925 }
2926 }
2927 _ => SchemaType::Primitive {
2928 rust_type: "serde_json::Value".to_string(),
2929 },
2930 };
2931
2932 Ok(SchemaType::Array {
2933 item_type: Box::new(item_type),
2934 })
2935 } else {
2936 Ok(SchemaType::Primitive {
2938 rust_type: "Vec<serde_json::Value>".to_string(),
2939 })
2940 }
2941 }
2942
2943 fn get_number_rust_type(
2944 &self,
2945 schema_type: OpenApiSchemaType,
2946 details: &crate::openapi::SchemaDetails,
2947 ) -> String {
2948 match schema_type {
2949 OpenApiSchemaType::Integer => {
2950 match details.format.as_deref() {
2952 Some("int32") => "i32".to_string(),
2953 Some("int64") => "i64".to_string(),
2954 _ => "i64".to_string(), }
2956 }
2957 OpenApiSchemaType::Number => {
2958 match details.format.as_deref() {
2960 Some("float") => "f32".to_string(),
2961 Some("double") => "f64".to_string(),
2962 _ => "f64".to_string(), }
2964 }
2965 _ => "serde_json::Value".to_string(), }
2967 }
2968
2969 fn analyze_anyof_union(
2970 &mut self,
2971 any_of_schemas: &[Schema],
2972 discriminator: Option<&Discriminator>,
2973 dependencies: &mut HashSet<String>,
2974 context_name: &str,
2975 ) -> Result<SchemaType> {
2976 if any_of_schemas.len() == 2 {
2980 let null_count = any_of_schemas
2981 .iter()
2982 .filter(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
2983 .count();
2984 if null_count == 1 {
2985 for schema in any_of_schemas {
2987 if !matches!(schema.schema_type(), Some(OpenApiSchemaType::Null)) {
2988 return self
2991 .analyze_schema_value(schema, context_name)
2992 .map(|a| a.schema_type);
2993 }
2994 }
2995 }
2996 }
2997
2998 let has_refs = any_of_schemas.iter().any(|s| s.is_reference());
3000 let has_objects = any_of_schemas.iter().any(|s| {
3001 matches!(s.schema_type(), Some(OpenApiSchemaType::Object))
3002 || s.inferred_type() == Some(OpenApiSchemaType::Object)
3003 });
3004 let has_arrays = any_of_schemas
3005 .iter()
3006 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Array)));
3007
3008 let all_string_like = any_of_schemas.iter().all(|s| {
3011 matches!(s.schema_type(), Some(OpenApiSchemaType::String))
3012 || s.details().const_value.is_some()
3013 });
3014
3015 if (has_refs || has_objects || has_arrays || any_of_schemas.len() > 1) && !all_string_like {
3016 if let Some(disc) = discriminator {
3018 return self.analyze_oneof_union(any_of_schemas, Some(disc), None, dependencies);
3020 }
3021
3022 if let Some(disc_field) = self.detect_discriminator_field(any_of_schemas) {
3024 return self.analyze_oneof_union(
3025 any_of_schemas,
3026 Some(&Discriminator {
3027 property_name: disc_field,
3028 mapping: None,
3029 extra: BTreeMap::new(),
3030 }),
3031 None,
3032 dependencies,
3033 );
3034 }
3035
3036 let mut variants = Vec::new();
3038
3039 for schema in any_of_schemas {
3040 if let Some(ref_str) = schema.reference() {
3041 if let Some(target) = self.extract_schema_name(ref_str) {
3042 dependencies.insert(target.to_string());
3043 variants.push(SchemaRef {
3044 target: target.to_string(),
3045 nullable: false,
3046 });
3047 }
3048 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object))
3049 || schema.inferred_type() == Some(OpenApiSchemaType::Object)
3050 {
3051 let inline_index = variants.len();
3053 let inline_type_name = self.generate_inline_type_name(schema, inline_index);
3054
3055 self.add_inline_schema(&inline_type_name, schema, dependencies)?;
3057
3058 variants.push(SchemaRef {
3059 target: inline_type_name,
3060 nullable: false,
3061 });
3062 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Array)) {
3063 let array_type =
3065 self.analyze_array_schema(schema, context_name, dependencies)?;
3066
3067 let array_type_name = if let Some(items_schema) = &schema.details().items {
3069 if let Some(ref_str) = items_schema.reference() {
3070 if let Some(item_type_name) = self.extract_schema_name(ref_str) {
3071 dependencies.insert(item_type_name.to_string());
3072 format!("{item_type_name}Array")
3073 } else {
3074 self.generate_context_aware_name(
3075 context_name,
3076 "Array",
3077 variants.len(),
3078 Some(schema),
3079 )
3080 }
3081 } else {
3082 self.generate_context_aware_name(
3083 context_name,
3084 "Array",
3085 variants.len(),
3086 Some(schema),
3087 )
3088 }
3089 } else {
3090 self.generate_context_aware_name(
3091 context_name,
3092 "Array",
3093 variants.len(),
3094 Some(schema),
3095 )
3096 };
3097
3098 self.resolved_cache.insert(
3100 array_type_name.clone(),
3101 AnalyzedSchema {
3102 name: array_type_name.clone(),
3103 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3104 schema_type: array_type,
3105 dependencies: HashSet::new(),
3106 nullable: false,
3107 description: Some("Array variant in union".to_string()),
3108 default: None,
3109 },
3110 );
3111
3112 dependencies.insert(array_type_name.clone());
3114
3115 variants.push(SchemaRef {
3116 target: array_type_name,
3117 nullable: false,
3118 });
3119 } else if let Some(schema_type) = schema.schema_type() {
3120 let inline_index = variants.len();
3122
3123 let inline_type_name = match schema_type {
3125 OpenApiSchemaType::String => {
3126 if inline_index == 0 {
3129 format!("{context_name}String")
3130 } else {
3131 format!("{context_name}StringVariant{inline_index}")
3132 }
3133 }
3134 OpenApiSchemaType::Number => {
3135 if inline_index == 0 {
3136 format!("{context_name}Number")
3137 } else {
3138 format!("{context_name}NumberVariant{inline_index}")
3139 }
3140 }
3141 OpenApiSchemaType::Integer => {
3142 if inline_index == 0 {
3143 format!("{context_name}Integer")
3144 } else {
3145 format!("{context_name}IntegerVariant{inline_index}")
3146 }
3147 }
3148 OpenApiSchemaType::Boolean => {
3149 if inline_index == 0 {
3150 format!("{context_name}Boolean")
3151 } else {
3152 format!("{context_name}BooleanVariant{inline_index}")
3153 }
3154 }
3155 _ => format!("{context_name}Variant{inline_index}"),
3156 };
3157
3158 let rust_type =
3159 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
3160
3161 self.resolved_cache.insert(
3163 inline_type_name.clone(),
3164 AnalyzedSchema {
3165 name: inline_type_name.clone(),
3166 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3167 schema_type: SchemaType::Primitive { rust_type },
3168 dependencies: HashSet::new(),
3169 nullable: false,
3170 description: schema.details().description.clone(),
3171 default: None,
3172 },
3173 );
3174
3175 dependencies.insert(inline_type_name.clone());
3177
3178 variants.push(SchemaRef {
3179 target: inline_type_name,
3180 nullable: false,
3181 });
3182 }
3183 }
3184
3185 if !variants.is_empty() {
3186 return Ok(SchemaType::Union { variants });
3187 }
3188 }
3189
3190 let all_strings = any_of_schemas.iter().all(|schema| {
3192 matches!(schema.schema_type(), Some(OpenApiSchemaType::String))
3193 || schema.details().const_value.is_some()
3194 });
3195
3196 if all_strings {
3197 let mut enum_values = Vec::new();
3199 let mut has_open_string = false;
3200
3201 for schema in any_of_schemas {
3202 if let Some(const_val) = &schema.details().const_value {
3203 if let Some(const_str) = const_val.as_str() {
3204 enum_values.push(const_str.to_string());
3205 }
3206 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::String)) {
3207 has_open_string = true;
3208 }
3209 }
3210
3211 if !enum_values.is_empty() {
3212 if has_open_string {
3213 return Ok(SchemaType::ExtensibleEnum {
3216 known_values: enum_values,
3217 });
3218 } else {
3219 return Ok(SchemaType::StringEnum {
3221 values: enum_values,
3222 });
3223 }
3224 }
3225 }
3226
3227 Ok(SchemaType::Primitive {
3229 rust_type: "serde_json::Value".to_string(),
3230 })
3231 }
3232
3233 fn find_recursive_anchor_schema(&self) -> Option<String> {
3235 for (schema_name, schema) in &self.schemas {
3237 let details = schema.details();
3238 if details.recursive_anchor == Some(true) {
3239 return Some(schema_name.clone());
3240 }
3241 }
3242
3243 None
3247 }
3248
3249 fn should_use_dynamic_json(&self, schema: &Schema) -> bool {
3252 if let Schema::AnyOf { any_of, .. } = schema {
3254 if any_of.len() == 2 {
3255 let has_null = any_of
3256 .iter()
3257 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)));
3258 let has_empty_object = any_of.iter().any(|s| self.is_dynamic_object_pattern(s));
3259
3260 if has_null && has_empty_object {
3261 return true;
3262 }
3263 }
3264 }
3265
3266 self.is_dynamic_object_pattern(schema)
3268 }
3269
3270 fn is_dynamic_object_pattern(&self, schema: &Schema) -> bool {
3272 let is_object = match schema.schema_type() {
3274 Some(OpenApiSchemaType::Object) => true,
3275 None => schema.inferred_type() == Some(OpenApiSchemaType::Object),
3276 _ => false,
3277 };
3278
3279 if !is_object {
3280 return false;
3281 }
3282
3283 let details = schema.details();
3284
3285 if self.has_explicit_additional_properties(schema) {
3288 return false;
3289 }
3290
3291 let no_properties = details
3293 .properties
3294 .as_ref()
3295 .map(|props| props.is_empty())
3296 .unwrap_or(true);
3297
3298 if no_properties {
3299 let has_structural_constraints =
3301 details.required.as_ref()
3303 .map(|req| req.iter().any(|r| r != "type"))
3304 .unwrap_or(false)
3305 || details.extra.contains_key("patternProperties")
3307 || details.extra.contains_key("propertyNames")
3309 || details.extra.contains_key("minProperties")
3311 || details.extra.contains_key("maxProperties")
3312 || details.extra.contains_key("dependencies")
3314 || details.extra.contains_key("if")
3316 || details.extra.contains_key("then")
3317 || details.extra.contains_key("else");
3318
3319 return !has_structural_constraints;
3320 }
3321
3322 false
3323 }
3324
3325 fn has_explicit_additional_properties(&self, schema: &Schema) -> bool {
3327 let details = schema.details();
3328
3329 matches!(
3331 &details.additional_properties,
3332 Some(crate::openapi::AdditionalProperties::Boolean(true))
3333 | Some(crate::openapi::AdditionalProperties::Schema(_))
3334 )
3335 }
3336
3337 fn analyze_operations(&mut self, analysis: &mut SchemaAnalysis) -> Result<()> {
3339 let spec: crate::openapi::OpenApiSpec = serde_json::from_value(self.openapi_spec.clone())
3340 .map_err(GeneratorError::ParseError)?;
3341
3342 if let Some(paths) = &spec.paths {
3343 for (path, path_item) in paths {
3344 for (method, operation) in path_item.operations() {
3345 let operation_id = operation
3347 .operation_id
3348 .clone()
3349 .unwrap_or_else(|| Self::generate_operation_id(method, path));
3350
3351 let op_info = self.analyze_single_operation(
3352 &operation_id,
3353 method,
3354 path,
3355 operation,
3356 analysis,
3357 )?;
3358 analysis.operations.insert(operation_id, op_info);
3359 }
3360 }
3361 }
3362 Ok(())
3363 }
3364
3365 fn generate_operation_id(method: &str, path: &str) -> String {
3368 let mut operation_id = method.to_lowercase();
3370
3371 let path_parts: Vec<&str> = path.trim_start_matches('/').split('/').collect();
3373
3374 for part in path_parts {
3375 if part.is_empty() {
3376 continue;
3377 }
3378
3379 let cleaned_part = if part.starts_with('{') && part.ends_with('}') {
3381 &part[1..part.len() - 1]
3382 } else {
3383 part
3384 };
3385
3386 let pascal_case_part = cleaned_part
3388 .split(&['-', '_'][..])
3389 .map(|s| {
3390 let mut chars = s.chars();
3391 match chars.next() {
3392 None => String::new(),
3393 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
3394 }
3395 })
3396 .collect::<String>();
3397
3398 operation_id.push_str(&pascal_case_part);
3399 }
3400
3401 operation_id
3402 }
3403
3404 fn analyze_single_operation(
3406 &mut self,
3407 operation_id: &str,
3408 method: &str,
3409 path: &str,
3410 operation: &crate::openapi::Operation,
3411 _analysis: &mut SchemaAnalysis,
3412 ) -> Result<OperationInfo> {
3413 let mut op_info = OperationInfo {
3414 operation_id: operation_id.to_string(),
3415 method: method.to_uppercase(),
3416 path: path.to_string(),
3417 request_body: None,
3418 response_schemas: BTreeMap::new(),
3419 parameters: Vec::new(),
3420 supports_streaming: false, stream_parameter: None, };
3423
3424 if let Some(request_body) = &operation.request_body
3426 && let Some((content_type, maybe_schema)) = request_body.best_content()
3427 {
3428 op_info.request_body = match content_type {
3429 "application/json" => maybe_schema
3430 .map(|s| {
3431 self.resolve_or_inline_schema(s, operation_id, "Request")
3432 .map(|name| RequestBodyContent::Json { schema_name: name })
3433 })
3434 .transpose()?,
3435 "application/x-www-form-urlencoded" => maybe_schema
3436 .map(|s| {
3437 self.resolve_or_inline_schema(s, operation_id, "Request")
3438 .map(|name| RequestBodyContent::FormUrlEncoded { schema_name: name })
3439 })
3440 .transpose()?,
3441 "multipart/form-data" => Some(RequestBodyContent::Multipart),
3442 "application/octet-stream" => Some(RequestBodyContent::OctetStream),
3443 "text/plain" => Some(RequestBodyContent::TextPlain),
3444 _ => None,
3445 };
3446 }
3447
3448 if let Some(responses) = &operation.responses {
3450 for (status_code, response) in responses {
3451 if let Some(schema) = response.json_schema() {
3452 if let Some(schema_ref) = schema.reference() {
3453 if let Some(schema_name) = self.extract_schema_name(schema_ref) {
3455 op_info
3456 .response_schemas
3457 .insert(status_code.clone(), schema_name.to_string());
3458 }
3459 } else {
3460 let synthetic_name =
3462 self.generate_inline_response_type_name(operation_id, status_code);
3463
3464 let mut deps = HashSet::new();
3466 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
3467
3468 op_info
3469 .response_schemas
3470 .insert(status_code.clone(), synthetic_name);
3471 }
3472 }
3473 }
3474 }
3475
3476 if let Some(parameters) = &operation.parameters {
3478 for param in parameters {
3479 if let Some(param_info) = self.analyze_parameter(param)? {
3480 op_info.parameters.push(param_info);
3481 }
3482 }
3483 }
3484
3485 Ok(op_info)
3486 }
3487
3488 fn generate_inline_response_type_name(&self, operation_id: &str, _status_code: &str) -> String {
3490 use heck::ToPascalCase;
3491 let base_name = operation_id.replace('.', "_").to_pascal_case();
3495 format!("{}Response", base_name)
3496 }
3497
3498 fn generate_inline_request_type_name(&self, operation_id: &str) -> String {
3500 use heck::ToPascalCase;
3501 let base_name = operation_id.replace('.', "_").to_pascal_case();
3505 format!("{}Request", base_name)
3506 }
3507
3508 fn resolve_or_inline_schema(
3511 &mut self,
3512 schema: &crate::openapi::Schema,
3513 operation_id: &str,
3514 suffix: &str,
3515 ) -> Result<String> {
3516 if let Some(schema_ref) = schema.reference()
3517 && let Some(schema_name) = self.extract_schema_name(schema_ref)
3518 {
3519 return Ok(schema_name.to_string());
3520 }
3521 let synthetic_name = if suffix == "Request" {
3523 self.generate_inline_request_type_name(operation_id)
3524 } else {
3525 self.generate_inline_response_type_name(operation_id, "")
3526 };
3527 let mut deps = HashSet::new();
3528 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
3529 Ok(synthetic_name)
3530 }
3531
3532 fn analyze_parameter(
3534 &self,
3535 param: &crate::openapi::Parameter,
3536 ) -> Result<Option<ParameterInfo>> {
3537 let name = param.name.as_deref().unwrap_or("");
3538 let location = param.location.as_deref().unwrap_or("");
3539 let required = param.required.unwrap_or(false);
3540
3541 let mut rust_type = "String".to_string();
3542 let mut schema_ref = None;
3543
3544 if let Some(schema) = ¶m.schema {
3545 if let Some(ref_str) = schema.reference() {
3546 schema_ref = self.extract_schema_name(ref_str).map(|s| s.to_string());
3547 } else if let Some(schema_type) = schema.schema_type() {
3548 rust_type = match schema_type {
3549 crate::openapi::SchemaType::Boolean => "bool",
3550 crate::openapi::SchemaType::Integer => "i64",
3551 crate::openapi::SchemaType::Number => "f64",
3552 crate::openapi::SchemaType::String => "String",
3553 _ => "String",
3554 }
3555 .to_string();
3556 }
3557 }
3558
3559 Ok(Some(ParameterInfo {
3560 name: name.to_string(),
3561 location: location.to_string(),
3562 required,
3563 schema_ref,
3564 rust_type,
3565 }))
3566 }
3567}