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 summary: Option<String>,
110 pub description: Option<String>,
112 pub request_body: Option<RequestBodyContent>,
114 pub response_schemas: BTreeMap<String, String>,
116 pub parameters: Vec<ParameterInfo>,
118 pub supports_streaming: bool,
120 pub stream_parameter: Option<String>,
122}
123
124#[derive(Debug, Clone, serde::Serialize)]
126#[serde(tag = "kind")]
127pub enum RequestBodyContent {
128 Json { schema_name: String },
129 FormUrlEncoded { schema_name: String },
130 Multipart,
131 OctetStream,
132 TextPlain,
133}
134
135impl RequestBodyContent {
136 pub fn schema_name(&self) -> Option<&str> {
138 match self {
139 Self::Json { schema_name } | Self::FormUrlEncoded { schema_name } => Some(schema_name),
140 _ => None,
141 }
142 }
143}
144
145#[derive(Debug, Clone, serde::Serialize)]
147pub struct ParameterInfo {
148 pub name: String,
150 pub location: String,
152 pub required: bool,
154 pub schema_ref: Option<String>,
156 pub rust_type: String,
158 pub description: Option<String>,
160 #[serde(skip_serializing_if = "Option::is_none")]
166 pub enum_values: Option<Vec<String>>,
167}
168
169impl Default for DependencyGraph {
170 fn default() -> Self {
171 Self::new()
172 }
173}
174
175impl DependencyGraph {
176 pub fn new() -> Self {
177 Self {
178 edges: BTreeMap::new(),
179 recursive_schemas: HashSet::new(),
180 }
181 }
182
183 pub fn add_dependency(&mut self, from: String, to: String) {
184 self.edges.entry(from).or_default().insert(to);
185 }
186
187 pub fn topological_sort(&mut self) -> Result<Vec<String>> {
189 self.detect_recursive_schemas();
191
192 let mut temp_edges = self.edges.clone();
194 for (schema, deps) in &mut temp_edges {
195 deps.remove(schema); }
197
198 let mut visited = HashSet::new();
199 let mut temp_visited = HashSet::new();
200 let mut result = Vec::new();
201
202 let mut all_nodes: Vec<_> = temp_edges.keys().collect();
204 all_nodes.sort();
205 for node in all_nodes {
206 if !visited.contains(node) {
207 self.visit_node_recursive(
208 node,
209 &temp_edges,
210 &mut visited,
211 &mut temp_visited,
212 &mut result,
213 )?;
214 }
215 }
216
217 result.reverse();
218 Ok(result)
219 }
220
221 fn detect_recursive_schemas(&mut self) {
222 for (schema, deps) in &self.edges {
223 if deps.contains(schema) {
224 self.recursive_schemas.insert(schema.clone());
226 } else {
227 if self.has_cycle_from(schema, schema, &mut HashSet::new()) {
229 self.recursive_schemas.insert(schema.clone());
230 }
231 }
232 }
233
234 for (schema, deps) in &self.edges {
236 for dep in deps {
237 if let Some(dep_deps) = self.edges.get(dep) {
238 if dep_deps.contains(schema) {
239 self.recursive_schemas.insert(schema.clone());
241 self.recursive_schemas.insert(dep.clone());
242 }
243 }
244 }
245 }
246 }
247
248 fn has_cycle_from(&self, start: &str, current: &str, visited: &mut HashSet<String>) -> bool {
249 if visited.contains(current) {
250 return false; }
252
253 visited.insert(current.to_string());
254
255 if let Some(deps) = self.edges.get(current) {
256 for dep in deps {
257 if dep == start {
258 return true; }
260 if self.has_cycle_from(start, dep, visited) {
261 return true;
262 }
263 }
264 }
265
266 false
267 }
268
269 #[allow(clippy::only_used_in_recursion)]
270 fn visit_node_recursive(
271 &self,
272 node: &str,
273 temp_edges: &BTreeMap<String, HashSet<String>>,
274 visited: &mut HashSet<String>,
275 temp_visited: &mut HashSet<String>,
276 result: &mut Vec<String>,
277 ) -> Result<()> {
278 if temp_visited.contains(node) {
279 return Ok(());
281 }
282
283 if visited.contains(node) {
284 return Ok(());
285 }
286
287 temp_visited.insert(node.to_string());
288
289 if let Some(dependencies) = temp_edges.get(node) {
290 let mut sorted_deps: Vec<_> = dependencies.iter().collect();
292 sorted_deps.sort();
293 for dep in sorted_deps {
294 self.visit_node_recursive(dep, temp_edges, visited, temp_visited, result)?;
295 }
296 }
297
298 temp_visited.remove(node);
299 visited.insert(node.to_string());
300 result.push(node.to_string());
301
302 Ok(())
303 }
304}
305
306pub fn merge_schema_extensions(
309 main_spec: Value,
310 extension_paths: &[impl AsRef<Path>],
311) -> Result<Value> {
312 let mut result = main_spec;
313
314 for path in extension_paths {
315 let extension = load_extension_file(path.as_ref())?;
316 result = merge_json_objects_with_replacements(result, extension)?;
317 }
318
319 Ok(result)
320}
321
322fn load_extension_file(path: &Path) -> Result<Value> {
324 let content = std::fs::read_to_string(path).map_err(|e| GeneratorError::FileError {
325 message: format!("Failed to read file {}: {}", path.display(), e),
326 })?;
327
328 serde_json::from_str(&content).map_err(GeneratorError::ParseError)
329}
330
331fn merge_json_objects_with_replacements(main: Value, extension: Value) -> Result<Value> {
333 let replacements = extract_replacement_rules(&extension);
335
336 Ok(merge_json_objects_with_rules(
338 main,
339 extension,
340 &replacements,
341 ))
342}
343
344fn extract_replacement_rules(
346 extension: &Value,
347) -> std::collections::HashMap<String, (String, String)> {
348 let mut rules = std::collections::HashMap::new();
349
350 if let Some(x_replacements) = extension.get("x-replacements") {
351 if let Some(x_replacements_obj) = x_replacements.as_object() {
352 for (schema_name, replacement_rule) in x_replacements_obj {
353 if let Some(rule_obj) = replacement_rule.as_object() {
354 if let (Some(replace), Some(with)) = (
355 rule_obj.get("replace").and_then(|v| v.as_str()),
356 rule_obj.get("with").and_then(|v| v.as_str()),
357 ) {
358 rules.insert(schema_name.clone(), (replace.to_string(), with.to_string()));
359 }
361 }
362 }
363 }
364 }
365
366 rules
367}
368
369fn should_replace_variant(
371 schema_name: &str,
372 extension_refs: &[String],
373 replacements: &std::collections::HashMap<String, (String, String)>,
374) -> bool {
375 for (replace_schema, with_schema) in replacements.values() {
377 if schema_name == replace_schema {
378 let replacement_exists = extension_refs.iter().any(|ext_ref| {
380 let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
381 ext_schema_name == with_schema
382 });
383
384 if replacement_exists {
385 return true;
386 }
387 }
388 }
389
390 extension_refs.iter().any(|ext_ref| {
392 let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
393 schema_name == ext_schema_name
394 })
395}
396
397fn merge_json_objects_with_rules(
402 main: Value,
403 extension: Value,
404 replacements: &std::collections::HashMap<String, (String, String)>,
405) -> Value {
406 match (main, extension) {
407 (Value::Object(mut main_obj), Value::Object(ext_obj)) => {
409 let main_union_keyword = if main_obj.contains_key("oneOf") {
412 Some("oneOf")
413 } else if main_obj.contains_key("anyOf") {
414 Some("anyOf")
415 } else {
416 None
417 };
418 if let (Some(main_variants), Some(ext_variants)) = (
419 extract_schema_variants(&Value::Object(main_obj.clone())),
420 extract_schema_variants(&Value::Object(ext_obj.clone())),
421 ) {
422 let union_key = main_union_keyword.unwrap_or("oneOf");
423 println!(
424 "🔍 Merging union schemas ({union_key}): {} main variants, {} extension variants",
425 main_variants.len(),
426 ext_variants.len()
427 );
428 let mut merged_variants = Vec::new();
431 let extension_refs: Vec<String> = ext_variants
432 .iter()
433 .filter_map(|v| v.get("$ref").and_then(|r| r.as_str()))
434 .map(|s| s.to_string())
435 .collect();
436
437 for main_variant in main_variants {
439 if let Some(main_ref) = main_variant.get("$ref").and_then(|r| r.as_str()) {
440 let schema_name = main_ref.split('/').next_back().unwrap_or("");
442 let should_replace =
443 should_replace_variant(schema_name, &extension_refs, replacements);
444
445 if should_replace {
446 println!("🔄 REPLACING {} (explicit rule)", schema_name);
447 }
448
449 if !should_replace {
450 merged_variants.push(main_variant);
451 }
452 } else {
453 merged_variants.push(main_variant);
455 }
456 }
457
458 for ext_variant in ext_variants {
460 merged_variants.push(ext_variant);
461 }
462
463 main_obj.remove("oneOf");
465 main_obj.remove("anyOf");
466 main_obj.insert(union_key.to_string(), Value::Array(merged_variants));
467
468 for (key, ext_value) in ext_obj {
470 if key != "oneOf" && key != "anyOf" {
471 match main_obj.get(&key) {
472 Some(main_value) => {
473 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);
482 }
483 }
484 }
485 }
486
487 return Value::Object(main_obj);
488 }
489
490 for (key, ext_value) in ext_obj {
492 match main_obj.get(&key) {
493 Some(main_value) => {
494 let merged_value = merge_json_objects_with_rules(
496 main_value.clone(),
497 ext_value,
498 replacements,
499 );
500 main_obj.insert(key, merged_value);
501 }
502 None => {
503 main_obj.insert(key, ext_value);
505 }
506 }
507 }
508 Value::Object(main_obj)
509 }
510
511 (Value::Array(mut main_arr), Value::Array(ext_arr)) => {
513 main_arr.extend(ext_arr);
514 Value::Array(main_arr)
515 }
516
517 (_, extension) => extension,
519 }
520}
521
522fn extract_schema_variants(obj: &Value) -> Option<Vec<Value>> {
524 if let Value::Object(map) = obj {
525 if let Some(Value::Array(variants)) = map.get("oneOf") {
526 return Some(variants.clone());
527 }
528 if let Some(Value::Array(variants)) = map.get("anyOf") {
529 return Some(variants.clone());
530 }
531 }
532 None
533}
534
535pub struct SchemaAnalyzer {
536 schemas: BTreeMap<String, Schema>,
537 resolved_cache: BTreeMap<String, AnalyzedSchema>,
538 openapi_spec: Value,
539 current_schema_name: Option<String>,
540 component_parameters: BTreeMap<String, crate::openapi::Parameter>,
541}
542
543impl SchemaAnalyzer {
544 pub fn new(openapi_spec: Value) -> Result<Self> {
545 let spec: OpenApiSpec =
546 serde_json::from_value(openapi_spec.clone()).map_err(GeneratorError::ParseError)?;
547 let schemas = Self::extract_schemas(&spec)?;
548
549 let component_parameters = spec
550 .components
551 .as_ref()
552 .and_then(|c| c.parameters.as_ref())
553 .cloned()
554 .unwrap_or_default();
555
556 Ok(Self {
557 schemas,
558 resolved_cache: BTreeMap::new(),
559 openapi_spec,
560 current_schema_name: None,
561 component_parameters,
562 })
563 }
564
565 pub fn new_with_extensions(
567 openapi_spec: Value,
568 extension_paths: &[std::path::PathBuf],
569 ) -> Result<Self> {
570 let merged_spec = merge_schema_extensions(openapi_spec, extension_paths)?;
571 Self::new(merged_spec)
572 }
573
574 fn generate_context_aware_name(
577 &self,
578 base_context: &str,
579 type_hint: &str,
580 index: usize,
581 schema: Option<&Schema>,
582 ) -> String {
583 if let Some(schema) = schema {
585 if type_hint == "Array"
587 && matches!(schema.schema_type(), Some(OpenApiSchemaType::Array))
588 {
589 if let Some(items_schema) = &schema.details().items {
590 if let Some(item_type) = items_schema.schema_type() {
592 match item_type {
593 OpenApiSchemaType::Object => {
594 return format!("{base_context}ItemArray");
595 }
596 OpenApiSchemaType::String => {
597 return format!("{base_context}StringArray");
598 }
599 _ => {}
600 }
601 }
602 }
603 }
604 }
605
606 match type_hint {
608 "Array" => {
609 format!("{base_context}Array")
611 }
612 "Variant" | "InlineVariant" => {
613 if index == 0 {
615 format!("{base_context}{type_hint}")
616 } else {
617 format!("{}{}{}", base_context, type_hint, index + 1)
618 }
619 }
620 _ => {
621 format!("{base_context}{type_hint}{index}")
623 }
624 }
625 }
626
627 fn to_pascal_case(&self, s: &str) -> String {
629 s.split(['_', '-'])
630 .filter(|part| !part.is_empty())
631 .map(|part| {
632 let mut chars = part.chars();
633 match chars.next() {
634 None => String::new(),
635 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
636 }
637 })
638 .collect()
639 }
640
641 fn extract_schemas(spec: &OpenApiSpec) -> Result<BTreeMap<String, Schema>> {
642 let schemas = spec
643 .components
644 .as_ref()
645 .and_then(|c| c.schemas.as_ref())
646 .ok_or_else(|| {
647 GeneratorError::InvalidSchema("No schemas found in OpenAPI spec".to_string())
648 })?;
649
650 Ok(schemas
652 .iter()
653 .map(|(k, v)| (k.clone(), v.clone()))
654 .collect())
655 }
656
657 pub fn analyze(&mut self) -> Result<SchemaAnalysis> {
658 let mut analysis = SchemaAnalysis {
659 schemas: BTreeMap::new(),
660 dependencies: DependencyGraph::new(),
661 patterns: DetectedPatterns {
662 tagged_enum_schemas: HashSet::new(),
663 untagged_enum_schemas: HashSet::new(),
664 type_mappings: BTreeMap::new(),
665 },
666 operations: BTreeMap::new(),
667 };
668
669 self.detect_patterns(&mut analysis.patterns)?;
671
672 let schema_names: Vec<String> = self.schemas.keys().cloned().collect();
674 for schema_name in schema_names {
675 let analyzed = self.analyze_schema(&schema_name)?;
676
677 for dep in &analyzed.dependencies {
679 analysis
680 .dependencies
681 .add_dependency(schema_name.clone(), dep.clone());
682 }
683
684 analysis.schemas.insert(schema_name, analyzed);
685 }
686
687 for (inline_name, inline_schema) in &self.resolved_cache {
690 if !analysis.schemas.contains_key(inline_name) {
691 analysis
693 .schemas
694 .insert(inline_name.clone(), inline_schema.clone());
695
696 for dep in &inline_schema.dependencies {
698 analysis
699 .dependencies
700 .add_dependency(inline_name.clone(), dep.clone());
701 }
702
703 let mut schemas_to_update = Vec::new();
708 for (schema_name, schema) in &analysis.schemas {
709 if schema_name == inline_name {
711 continue;
712 }
713
714 if schema.dependencies.contains(inline_name) {
715 schemas_to_update.push(schema_name.clone());
717 }
718 }
719
720 for schema_name in schemas_to_update {
722 analysis
723 .dependencies
724 .add_dependency(schema_name, inline_name.clone());
725 }
726 }
727 }
728
729 self.analyze_operations(&mut analysis)?;
731
732 for (inline_name, inline_schema) in &self.resolved_cache {
735 if !analysis.schemas.contains_key(inline_name) {
736 analysis
737 .schemas
738 .insert(inline_name.clone(), inline_schema.clone());
739
740 for dep in &inline_schema.dependencies {
742 analysis
743 .dependencies
744 .add_dependency(inline_name.clone(), dep.clone());
745 }
746 }
747 }
748
749 Ok(analysis)
750 }
751
752 fn detect_patterns(&self, patterns: &mut DetectedPatterns) -> Result<()> {
753 for (schema_name, schema) in &self.schemas {
754 if self.is_discriminated_union(schema) {
756 patterns.tagged_enum_schemas.insert(schema_name.clone());
757
758 if let Some(mappings) = self.extract_type_mappings(schema)? {
760 patterns.type_mappings.insert(schema_name.clone(), mappings);
761 }
762 }
763 else if self.is_simple_union(schema) {
765 patterns.untagged_enum_schemas.insert(schema_name.clone());
766 }
767 }
768
769 Ok(())
770 }
771
772 fn is_discriminated_union(&self, schema: &Schema) -> bool {
773 if schema.is_discriminated_union() {
775 return true;
776 }
777
778 if let Some(variants) = schema.union_variants() {
780 return variants.len() > 2 && self.detect_discriminator_field(variants).is_some();
781 }
782
783 false
784 }
785
786 fn all_variants_have_const_field(&self, variants: &[Schema], field_name: &str) -> bool {
787 variants.iter().all(|variant| {
788 if let Some(ref_str) = variant.reference() {
789 if let Some(schema_name) = self.extract_schema_name(ref_str) {
791 if let Some(schema) = self.schemas.get(schema_name) {
792 return self.has_const_discriminator_field(schema, field_name);
793 }
794 }
795 } else {
796 return self.has_const_discriminator_field(variant, field_name);
798 }
799 false
800 })
801 }
802
803 fn detect_discriminator_field(&self, variants: &[Schema]) -> Option<String> {
807 if variants.is_empty() {
808 return None;
809 }
810
811 let first_variant = &variants[0];
813 let first_schema = if let Some(ref_str) = first_variant.reference() {
814 let schema_name = self.extract_schema_name(ref_str)?;
815 self.schemas.get(schema_name)?
816 } else {
817 first_variant
818 };
819
820 let properties = first_schema.details().properties.as_ref()?;
821 let mut candidates: Vec<String> = Vec::new();
822
823 for (field_name, field_schema) in properties {
824 let details = field_schema.details();
825 let is_const = details.const_value.is_some()
826 || details.enum_values.as_ref().is_some_and(|v| v.len() == 1)
827 || details.extra.contains_key("const");
828 if is_const {
829 candidates.push(field_name.clone());
830 }
831 }
832
833 if candidates.is_empty() {
834 return None;
835 }
836
837 candidates.sort_by(|a, b| {
839 if a == "type" {
840 std::cmp::Ordering::Less
841 } else if b == "type" {
842 std::cmp::Ordering::Greater
843 } else {
844 a.cmp(b)
845 }
846 });
847
848 for candidate in &candidates {
850 if self.all_variants_have_const_field(variants, candidate) {
851 return Some(candidate.clone());
852 }
853 }
854
855 None
856 }
857
858 fn has_const_discriminator_field(&self, schema: &Schema, field_name: &str) -> bool {
859 if let Some(properties) = &schema.details().properties {
860 if let Some(field) = properties.get(field_name) {
861 if field.details().const_value.is_some() {
863 return true;
864 }
865 if let Some(enum_vals) = &field.details().enum_values {
867 return enum_vals.len() == 1;
868 }
869 return field.details().extra.contains_key("const");
871 }
872 }
873 false
874 }
875
876 fn is_simple_union(&self, schema: &Schema) -> bool {
877 if let Some(variants) = schema.union_variants() {
878 if variants.len() > 1 && !schema.is_nullable_pattern() {
880 let has_refs = variants.iter().any(|v| v.is_reference());
881 return has_refs;
882 }
883 }
884 false
885 }
886
887 fn extract_type_mappings(&self, schema: &Schema) -> Result<Option<BTreeMap<String, String>>> {
888 let variants = schema.union_variants().ok_or_else(|| {
889 GeneratorError::InvalidSchema("No variants found for discriminated union".to_string())
890 })?;
891
892 let discriminator_field = if let Some(discriminator) = schema.discriminator() {
894 discriminator.property_name.clone()
895 } else if let Some(detected) = self.detect_discriminator_field(variants) {
896 detected
897 } else {
898 "type".to_string() };
900
901 let mut mappings = BTreeMap::new();
902
903 for variant in variants {
904 if let Some(ref_str) = variant.reference() {
905 if let Some(type_name) = self.extract_schema_name(ref_str) {
906 if let Some(variant_schema) = self.schemas.get(type_name) {
907 if let Some(discriminator_value) = self
908 .extract_discriminator_value_for_field(
909 variant_schema,
910 &discriminator_field,
911 )
912 {
913 mappings.insert(type_name.to_string(), discriminator_value);
914 }
915 }
916 }
917 }
918 }
919
920 if mappings.is_empty() {
921 Ok(None)
922 } else {
923 Ok(Some(mappings))
924 }
925 }
926
927 #[allow(dead_code)]
928 fn extract_discriminator_value(&self, schema: &Schema) -> Option<String> {
929 self.extract_discriminator_value_for_field(schema, "type")
930 }
931
932 fn extract_discriminator_value_for_field(
933 &self,
934 schema: &Schema,
935 field_name: &str,
936 ) -> Option<String> {
937 if let Some(properties) = &schema.details().properties {
938 if let Some(type_field) = properties.get(field_name) {
939 if let Some(const_value) = &type_field.details().const_value {
941 if let Some(value) = const_value.as_str() {
942 return Some(value.to_string());
943 }
944 }
945 if let Some(enum_values) = &type_field.details().enum_values {
947 if enum_values.len() == 1 {
948 return enum_values[0].as_str().map(|s| s.to_string());
949 }
950 }
951 if let Some(const_value) = type_field.details().extra.get("const") {
953 return const_value.as_str().map(|s| s.to_string());
954 }
955 if let Some(stainless_const) = type_field.details().extra.get("x-stainless-const") {
957 if stainless_const.as_bool() == Some(true) {
958 if let Some(default_value) = &type_field.details().default {
959 if let Some(value) = default_value.as_str() {
960 return Some(value.to_string());
961 }
962 }
963 }
964 }
965 }
966 }
967 None
968 }
969
970 fn get_any_reference<'a>(&self, schema: &'a Schema) -> Option<&'a str> {
971 schema.reference().or_else(|| schema.recursive_reference())
972 }
973
974 fn extract_schema_name<'a>(&self, ref_str: &'a str) -> Option<&'a str> {
975 if ref_str == "#" {
976 return None; }
978
979 let parts: Vec<&str> = ref_str.split('/').collect();
980
981 if parts.len() >= 4 && parts[0] == "#" && parts[2] == "schemas" {
984 return Some(parts[3]);
985 }
986
987 let last = parts.last()?;
990 if last.is_empty() || last.chars().all(|c| c.is_ascii_digit()) {
991 None
992 } else {
993 Some(last)
994 }
995 }
996
997 fn analyze_schema(&mut self, schema_name: &str) -> Result<AnalyzedSchema> {
998 if let Some(cached) = self.resolved_cache.get(schema_name) {
1000 return Ok(cached.clone());
1001 }
1002
1003 self.current_schema_name = Some(schema_name.to_string());
1005
1006 let schema = self
1007 .schemas
1008 .get(schema_name)
1009 .ok_or_else(|| GeneratorError::UnresolvedReference(schema_name.to_string()))?
1010 .clone();
1011
1012 self.resolved_cache.insert(
1014 schema_name.to_string(),
1015 AnalyzedSchema {
1016 name: schema_name.to_string(),
1017 original: serde_json::to_value(&schema).unwrap_or(Value::Null),
1018 schema_type: SchemaType::Reference {
1019 target: "placeholder".to_string(),
1020 },
1021 dependencies: HashSet::new(),
1022 nullable: false,
1023 description: None,
1024 default: None,
1025 },
1026 );
1027
1028 let analyzed = self.analyze_schema_value(&schema, schema_name)?;
1029
1030 self.resolved_cache
1032 .insert(schema_name.to_string(), analyzed.clone());
1033
1034 Ok(analyzed)
1035 }
1036
1037 fn analyze_schema_value(
1038 &mut self,
1039 schema: &Schema,
1040 schema_name: &str,
1041 ) -> Result<AnalyzedSchema> {
1042 let details = schema.details();
1043 let description = details.description.clone();
1044 let nullable = details.is_nullable();
1045 let mut dependencies = HashSet::new();
1046
1047 let schema_type = match schema {
1048 Schema::Reference { reference, .. } => {
1049 let target = self
1050 .extract_schema_name(reference)
1051 .ok_or_else(|| GeneratorError::UnresolvedReference(reference.to_string()))?
1052 .to_string();
1053 dependencies.insert(target.clone());
1054 SchemaType::Reference { target }
1055 }
1056 Schema::RecursiveRef { recursive_ref, .. } => {
1057 if recursive_ref == "#" {
1059 dependencies.insert(schema_name.to_string());
1061 SchemaType::Reference {
1062 target: schema_name.to_string(),
1063 }
1064 } else {
1065 let target = self
1067 .extract_schema_name(recursive_ref)
1068 .unwrap_or(schema_name)
1069 .to_string();
1070 dependencies.insert(target.clone());
1071 SchemaType::Reference { target }
1072 }
1073 }
1074 Schema::Typed { schema_type, .. } => {
1075 match schema_type {
1076 OpenApiSchemaType::String => {
1077 if let Some(values) = details.string_enum_values() {
1078 SchemaType::StringEnum { values }
1079 } else {
1080 SchemaType::Primitive {
1081 rust_type: "String".to_string(),
1082 }
1083 }
1084 }
1085 OpenApiSchemaType::Integer => {
1086 let rust_type =
1087 self.get_number_rust_type(OpenApiSchemaType::Integer, details);
1088 SchemaType::Primitive { rust_type }
1089 }
1090 OpenApiSchemaType::Number => {
1091 let rust_type =
1092 self.get_number_rust_type(OpenApiSchemaType::Number, details);
1093 SchemaType::Primitive { rust_type }
1094 }
1095 OpenApiSchemaType::Boolean => SchemaType::Primitive {
1096 rust_type: "bool".to_string(),
1097 },
1098 OpenApiSchemaType::Array => {
1099 self.analyze_array_schema(schema, schema_name, &mut dependencies)?
1101 }
1102 OpenApiSchemaType::Object => {
1103 if self.should_use_dynamic_json(schema) {
1105 SchemaType::Primitive {
1106 rust_type: "serde_json::Value".to_string(),
1107 }
1108 } else {
1109 self.analyze_object_schema(schema, &mut dependencies)?
1111 }
1112 }
1113 _ => SchemaType::Primitive {
1114 rust_type: "serde_json::Value".to_string(),
1115 },
1116 }
1117 }
1118 Schema::AnyOf {
1119 any_of,
1120 discriminator,
1121 ..
1122 } => {
1123 self.analyze_anyof_union(
1125 any_of,
1126 discriminator.as_ref(),
1127 &mut dependencies,
1128 schema_name,
1129 )?
1130 }
1131 Schema::OneOf {
1132 one_of,
1133 discriminator,
1134 ..
1135 } => {
1136 self.analyze_oneof_union(
1138 one_of,
1139 discriminator.as_ref(),
1140 schema_name,
1141 &mut dependencies,
1142 )?
1143 }
1144 Schema::AllOf { all_of, .. } => {
1145 self.analyze_allof_composition(all_of, &mut dependencies)?
1147 }
1148 Schema::Untyped { .. } => {
1149 if let Some(inferred) = schema.inferred_type() {
1151 match inferred {
1152 OpenApiSchemaType::Object => {
1153 if self.should_use_dynamic_json(schema) {
1154 SchemaType::Primitive {
1155 rust_type: "serde_json::Value".to_string(),
1156 }
1157 } else {
1158 self.analyze_object_schema(schema, &mut dependencies)?
1159 }
1160 }
1161 OpenApiSchemaType::String if details.is_string_enum() => {
1162 SchemaType::StringEnum {
1163 values: details.string_enum_values().unwrap_or_default(),
1164 }
1165 }
1166 _ => SchemaType::Primitive {
1167 rust_type: "serde_json::Value".to_string(),
1168 },
1169 }
1170 } else {
1171 SchemaType::Primitive {
1172 rust_type: "serde_json::Value".to_string(),
1173 }
1174 }
1175 }
1176 };
1177
1178 Ok(AnalyzedSchema {
1179 name: schema_name.to_string(),
1180 original: serde_json::to_value(schema).unwrap_or(Value::Null), schema_type,
1182 dependencies,
1183 nullable,
1184 description,
1185 default: details.default.clone(),
1186 })
1187 }
1188
1189 fn analyze_object_schema(
1190 &mut self,
1191 schema: &Schema,
1192 dependencies: &mut HashSet<String>,
1193 ) -> Result<SchemaType> {
1194 let details = schema.details();
1195 let properties = &details.properties;
1196 let required = details
1197 .required
1198 .as_ref()
1199 .map(|req| req.iter().cloned().collect::<HashSet<String>>())
1200 .unwrap_or_default();
1201
1202 let mut property_info = BTreeMap::new();
1203
1204 if let Some(props) = properties {
1205 for (prop_name, prop_schema) in props {
1206 let prop_type = if let Schema::AnyOf { any_of, .. } = prop_schema {
1208 if self.should_use_dynamic_json(prop_schema) {
1210 SchemaType::Primitive {
1212 rust_type: "serde_json::Value".to_string(),
1213 }
1214 } else {
1215 let context_name = self
1218 .current_schema_name
1219 .clone()
1220 .unwrap_or_else(|| "Unknown".to_string());
1221
1222 let prop_pascal = self.to_pascal_case(prop_name);
1224 let union_type_name = format!("{context_name}{prop_pascal}");
1225
1226 let union_schema_type = self.analyze_anyof_union(
1228 any_of,
1229 prop_schema.discriminator(),
1230 dependencies,
1231 &union_type_name,
1232 )?;
1233
1234 self.resolved_cache.insert(
1236 union_type_name.clone(),
1237 AnalyzedSchema {
1238 name: union_type_name.clone(),
1239 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
1240 schema_type: union_schema_type,
1241 dependencies: HashSet::new(),
1242 nullable: false,
1243 description: prop_schema.details().description.clone(),
1244 default: None,
1245 },
1246 );
1247
1248 dependencies.insert(union_type_name.clone());
1250 SchemaType::Reference {
1251 target: union_type_name,
1252 }
1253 }
1254 } else if let Schema::OneOf {
1255 one_of,
1256 discriminator,
1257 ..
1258 } = prop_schema
1259 {
1260 let context_name = self
1263 .current_schema_name
1264 .clone()
1265 .unwrap_or_else(|| "Unknown".to_string());
1266 let prop_pascal = self.to_pascal_case(prop_name);
1267 let union_type_name = format!("{context_name}{prop_pascal}");
1268
1269 let union_schema_type = self.analyze_oneof_union(
1271 one_of,
1272 discriminator.as_ref(),
1273 &union_type_name,
1274 dependencies,
1275 )?;
1276
1277 self.resolved_cache.insert(
1279 union_type_name.clone(),
1280 AnalyzedSchema {
1281 name: union_type_name.clone(),
1282 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
1283 schema_type: union_schema_type,
1284 dependencies: HashSet::new(),
1285 nullable: false,
1286 description: prop_schema.details().description.clone(),
1287 default: None,
1288 },
1289 );
1290
1291 dependencies.insert(union_type_name.clone());
1293 SchemaType::Reference {
1294 target: union_type_name,
1295 }
1296 } else {
1297 self.analyze_property_schema_with_context(
1299 prop_schema,
1300 Some(prop_name),
1301 dependencies,
1302 )?
1303 };
1304
1305 let prop_details = prop_schema.details();
1306 let prop_nullable = prop_details.is_nullable() || prop_schema.is_nullable_pattern();
1308 let prop_description = prop_details.description.clone();
1309 let prop_default = prop_details.default.clone();
1310
1311 property_info.insert(
1312 prop_name.clone(),
1313 PropertyInfo {
1314 schema_type: prop_type,
1315 nullable: prop_nullable,
1316 description: prop_description,
1317 default: prop_default,
1318 serde_attrs: Vec::new(),
1319 },
1320 );
1321 }
1322 }
1323
1324 let additional_properties = match &details.additional_properties {
1326 Some(crate::openapi::AdditionalProperties::Boolean(true)) => true,
1327 Some(crate::openapi::AdditionalProperties::Boolean(false)) => false,
1328 Some(crate::openapi::AdditionalProperties::Schema(_)) => {
1329 true
1332 }
1333 None => false, };
1335
1336 Ok(SchemaType::Object {
1337 properties: property_info,
1338 required,
1339 additional_properties,
1340 })
1341 }
1342
1343 fn analyze_property_schema_with_context(
1344 &mut self,
1345 schema: &Schema,
1346 property_name: Option<&str>,
1347 dependencies: &mut HashSet<String>,
1348 ) -> Result<SchemaType> {
1349 if let Some(ref_str) = self.get_any_reference(schema) {
1350 let target = if ref_str == "#" {
1351 self.find_recursive_anchor_schema()
1353 .unwrap_or_else(|| "UnknownRecursive".to_string())
1354 } else {
1355 self.extract_schema_name(ref_str)
1356 .ok_or_else(|| GeneratorError::UnresolvedReference(ref_str.to_string()))?
1357 .to_string()
1358 };
1359 dependencies.insert(target.clone());
1360 return Ok(SchemaType::Reference { target });
1361 }
1362
1363 if let Some(schema_type) = schema.schema_type() {
1364 match schema_type {
1365 OpenApiSchemaType::String => {
1366 if let Some(enum_values) = schema.details().string_enum_values() {
1368 let context_name = self
1371 .current_schema_name
1372 .clone()
1373 .unwrap_or_else(|| "Unknown".to_string());
1374
1375 let primary_name = if let Some(prop_name) = property_name {
1377 let prop_pascal = self.to_pascal_case(prop_name);
1379 format!("{context_name}{prop_pascal}")
1380 } else {
1381 let suffix = if !enum_values.is_empty() {
1384 let first_value = self.to_pascal_case(&enum_values[0]);
1385 format!("{first_value}Enum")
1386 } else {
1387 "StringEnum".to_string()
1388 };
1389 format!("{context_name}{suffix}")
1390 };
1391
1392 fn matches_values(existing: &AnalyzedSchema, values: &[String]) -> bool {
1412 matches!(
1413 &existing.schema_type,
1414 SchemaType::StringEnum { values: existing_values }
1415 if existing_values == values
1416 )
1417 }
1418
1419 let mut enum_type_name = primary_name.clone();
1420 let mut should_insert = match self.resolved_cache.get(&enum_type_name) {
1421 None => true,
1422 Some(existing) if matches_values(existing, &enum_values) => false,
1423 Some(_) => {
1424 let suffix = enum_values
1427 .first()
1428 .map(|v| self.to_pascal_case(v))
1429 .unwrap_or_else(|| "Variant".to_string());
1430 let candidate = format!("{primary_name}{suffix}");
1431
1432 let resolved = match self.resolved_cache.get(&candidate) {
1433 None => Some((candidate.clone(), true)),
1434 Some(existing) if matches_values(existing, &enum_values) => {
1435 Some((candidate.clone(), false))
1436 }
1437 Some(_) => {
1438 let mut found = None;
1441 for n in 2..1000 {
1442 let numbered = format!("{candidate}_{n}");
1443 match self.resolved_cache.get(&numbered) {
1444 None => {
1445 found = Some((numbered, true));
1446 break;
1447 }
1448 Some(existing)
1449 if matches_values(existing, &enum_values) =>
1450 {
1451 found = Some((numbered, false));
1452 break;
1453 }
1454 Some(_) => continue,
1455 }
1456 }
1457 found
1458 }
1459 };
1460
1461 let (resolved_name, insert) = resolved.unwrap_or((candidate, true));
1462 enum_type_name = resolved_name;
1463 insert
1464 }
1465 };
1466
1467 if should_insert {
1470 self.resolved_cache.insert(
1471 enum_type_name.clone(),
1472 AnalyzedSchema {
1473 name: enum_type_name.clone(),
1474 original: serde_json::to_value(schema).unwrap_or(Value::Null),
1475 schema_type: SchemaType::StringEnum {
1476 values: enum_values,
1477 },
1478 dependencies: HashSet::new(),
1479 nullable: false,
1480 description: schema.details().description.clone(),
1481 default: schema.details().default.clone(),
1482 },
1483 );
1484 let _ = &mut should_insert;
1487 }
1488
1489 dependencies.insert(enum_type_name.clone());
1491 return Ok(SchemaType::Reference {
1492 target: enum_type_name,
1493 });
1494 } else {
1495 return Ok(SchemaType::Primitive {
1496 rust_type: "String".to_string(),
1497 });
1498 }
1499 }
1500 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
1501 let details = schema.details();
1502 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
1503 return Ok(SchemaType::Primitive { rust_type });
1504 }
1505 OpenApiSchemaType::Boolean => {
1506 return Ok(SchemaType::Primitive {
1507 rust_type: "bool".to_string(),
1508 });
1509 }
1510 OpenApiSchemaType::Array => {
1511 let context_name = if let Some(prop_name) = property_name {
1513 let prop_pascal = self.to_pascal_case(prop_name);
1515 format!(
1516 "{}{}",
1517 self.current_schema_name.as_deref().unwrap_or("Unknown"),
1518 prop_pascal
1519 )
1520 } else {
1521 "ArrayItem".to_string()
1523 };
1524 return self.analyze_array_schema(schema, &context_name, dependencies);
1525 }
1526 OpenApiSchemaType::Object => {
1527 if self.should_use_dynamic_json(schema) {
1529 return Ok(SchemaType::Primitive {
1530 rust_type: "serde_json::Value".to_string(),
1531 });
1532 }
1533 let object_type_name = if let Some(prop_name) = property_name {
1535 let prop_pascal = self.to_pascal_case(prop_name);
1537 format!(
1538 "{}{}",
1539 self.current_schema_name.as_deref().unwrap_or("Unknown"),
1540 prop_pascal
1541 )
1542 } else {
1543 format!(
1545 "{}Object",
1546 self.current_schema_name.as_deref().unwrap_or("Unknown")
1547 )
1548 };
1549
1550 let object_type = self.analyze_object_schema(schema, dependencies)?;
1552
1553 let inline_schema = AnalyzedSchema {
1555 name: object_type_name.clone(),
1556 original: serde_json::to_value(schema).unwrap_or(Value::Null),
1557 schema_type: object_type,
1558 dependencies: dependencies.clone(),
1559 nullable: false,
1560 description: schema.details().description.clone(),
1561 default: None,
1562 };
1563
1564 self.resolved_cache
1566 .insert(object_type_name.clone(), inline_schema);
1567 dependencies.insert(object_type_name.clone());
1568
1569 return Ok(SchemaType::Reference {
1571 target: object_type_name,
1572 });
1573 }
1574 _ => {
1575 return Ok(SchemaType::Primitive {
1576 rust_type: "serde_json::Value".to_string(),
1577 });
1578 }
1579 }
1580 }
1581
1582 if schema.is_nullable_pattern() {
1584 if let Some(non_null) = schema.non_null_variant() {
1585 return self.analyze_property_schema_with_context(
1586 non_null,
1587 property_name,
1588 dependencies,
1589 );
1590 }
1591 }
1592
1593 if self.should_use_dynamic_json(schema) {
1595 return Ok(SchemaType::Primitive {
1596 rust_type: "serde_json::Value".to_string(),
1597 });
1598 }
1599
1600 if let Schema::AllOf { all_of, .. } = schema {
1602 return self.analyze_allof_composition(all_of, dependencies);
1603 }
1604
1605 if let Some(variants) = schema.union_variants() {
1607 match variants.len().cmp(&1) {
1608 std::cmp::Ordering::Equal => {
1609 return self.analyze_property_schema_with_context(
1611 &variants[0],
1612 property_name,
1613 dependencies,
1614 );
1615 }
1616 std::cmp::Ordering::Greater => {
1617 let union_name = if let Some(prop_name) = property_name {
1620 let prop_pascal = self.to_pascal_case(prop_name);
1622 format!(
1623 "{}{}",
1624 self.current_schema_name.as_deref().unwrap_or(""),
1625 prop_pascal
1626 )
1627 } else {
1628 "UnionType".to_string()
1629 };
1630
1631 if let Schema::OneOf {
1633 one_of,
1634 discriminator,
1635 ..
1636 } = schema
1637 {
1638 let oneof_result = self.analyze_oneof_union(
1640 one_of,
1641 discriminator.as_ref(),
1642 &union_name,
1643 dependencies,
1644 )?;
1645
1646 if let SchemaType::Union {
1648 variants: _union_variants,
1649 } = &oneof_result
1650 {
1651 self.resolved_cache.insert(
1653 union_name.clone(),
1654 AnalyzedSchema {
1655 name: union_name.clone(),
1656 original: serde_json::to_value(schema).unwrap_or(Value::Null),
1657 schema_type: oneof_result.clone(),
1658 dependencies: dependencies.clone(),
1659 nullable: false,
1660 description: schema.details().description.clone(),
1661 default: None,
1662 },
1663 );
1664
1665 dependencies.insert(union_name.clone());
1667 return Ok(SchemaType::Reference { target: union_name });
1668 }
1669
1670 return Ok(oneof_result);
1671 } else if let Schema::AnyOf {
1672 any_of,
1673 discriminator,
1674 ..
1675 } = schema
1676 {
1677 let union_analysis = self.analyze_anyof_union(
1679 any_of,
1680 discriminator.as_ref(),
1681 dependencies,
1682 &union_name,
1683 )?;
1684 return Ok(union_analysis);
1685 } else {
1686 let mut union_variants = Vec::new();
1689 for variant in variants {
1690 if let Some(ref_str) = variant.reference() {
1691 if let Some(target) = self.extract_schema_name(ref_str) {
1692 dependencies.insert(target.to_string());
1693 union_variants.push(SchemaRef {
1694 target: target.to_string(),
1695 nullable: false,
1696 });
1697 }
1698 }
1699 }
1700 return Ok(SchemaType::Union {
1701 variants: union_variants,
1702 });
1703 }
1704 }
1705 std::cmp::Ordering::Less => {}
1706 }
1707 }
1708
1709 if let Some(inferred_type) = schema.inferred_type() {
1711 match inferred_type {
1712 OpenApiSchemaType::Object => {
1713 if self.should_use_dynamic_json(schema) {
1715 return Ok(SchemaType::Primitive {
1716 rust_type: "serde_json::Value".to_string(),
1717 });
1718 }
1719 return self.analyze_object_schema(schema, dependencies);
1720 }
1721 OpenApiSchemaType::Array => {
1722 let context_name = if let Some(prop_name) = property_name {
1723 let prop_pascal = self.to_pascal_case(prop_name);
1725 format!(
1726 "{}{}",
1727 self.current_schema_name.as_deref().unwrap_or("Unknown"),
1728 prop_pascal
1729 )
1730 } else {
1731 "ArrayItem".to_string()
1733 };
1734 return self.analyze_array_schema(schema, &context_name, dependencies);
1735 }
1736 OpenApiSchemaType::String => {
1737 if let Some(enum_values) = schema.details().string_enum_values() {
1738 return Ok(SchemaType::StringEnum {
1739 values: enum_values,
1740 });
1741 } else {
1742 return Ok(SchemaType::Primitive {
1743 rust_type: "String".to_string(),
1744 });
1745 }
1746 }
1747 _ => {
1748 let rust_type = self.openapi_type_to_rust_type(inferred_type, schema.details());
1750 return Ok(SchemaType::Primitive { rust_type });
1751 }
1752 }
1753 }
1754
1755 Ok(SchemaType::Primitive {
1756 rust_type: "serde_json::Value".to_string(),
1757 })
1758 }
1759
1760 fn analyze_allof_composition(
1761 &mut self,
1762 all_of_schemas: &[Schema],
1763 dependencies: &mut HashSet<String>,
1764 ) -> Result<SchemaType> {
1765 if all_of_schemas.len() == 1 {
1768 if let Schema::Reference { reference, .. } = &all_of_schemas[0] {
1769 if let Some(target) = self.extract_schema_name(reference) {
1770 dependencies.insert(target.to_string());
1771 return Ok(SchemaType::Reference {
1772 target: target.to_string(),
1773 });
1774 }
1775 }
1776 }
1777
1778 let mut merged_properties = BTreeMap::new();
1780 let mut merged_required = HashSet::new();
1781 let mut descriptions = Vec::new();
1782
1783 let current_context = self.current_schema_name.clone();
1785
1786 for schema in all_of_schemas {
1787 match schema {
1788 Schema::Reference { reference, .. } => {
1789 if let Some(target) = self.extract_schema_name(reference) {
1791 dependencies.insert(target.to_string());
1792
1793 let analyzed_ref = self.analyze_schema(target)?;
1795
1796 match &analyzed_ref.schema_type {
1798 SchemaType::Object {
1799 properties,
1800 required,
1801 ..
1802 } => {
1803 for (prop_name, prop_info) in properties {
1805 merged_properties.insert(prop_name.clone(), prop_info.clone());
1806 }
1807 for req in required {
1809 merged_required.insert(req.clone());
1810 }
1811 }
1812 _ => {
1813 if let Some(ref_schema) = self.schemas.get(target).cloned() {
1815 self.merge_schema_into_properties(
1816 &ref_schema,
1817 &mut merged_properties,
1818 &mut merged_required,
1819 dependencies,
1820 )?;
1821 }
1822 }
1823 }
1824 }
1825 }
1826 Schema::Typed {
1827 schema_type: OpenApiSchemaType::Object,
1828 ..
1829 }
1830 | Schema::Untyped { .. } => {
1831 let saved_context = self.current_schema_name.clone();
1833 self.current_schema_name = current_context.clone();
1834
1835 self.merge_schema_into_properties(
1837 schema,
1838 &mut merged_properties,
1839 &mut merged_required,
1840 dependencies,
1841 )?;
1842
1843 self.current_schema_name = saved_context;
1845 }
1846 _ => {
1847 self.merge_schema_into_properties(
1850 schema,
1851 &mut merged_properties,
1852 &mut merged_required,
1853 dependencies,
1854 )?;
1855 }
1856 }
1857
1858 if let Some(desc) = &schema.details().description {
1860 descriptions.push(desc.clone());
1861 }
1862 }
1863
1864 if !merged_properties.is_empty() {
1866 Ok(SchemaType::Object {
1867 properties: merged_properties,
1868 required: merged_required,
1869 additional_properties: false,
1870 })
1871 } else {
1872 Ok(SchemaType::Composition {
1874 schemas: all_of_schemas
1875 .iter()
1876 .filter_map(|s| {
1877 if let Some(ref_str) = s.reference() {
1878 if let Some(target) = self.extract_schema_name(ref_str) {
1879 dependencies.insert(target.to_string());
1880 Some(SchemaRef {
1881 target: target.to_string(),
1882 nullable: false,
1883 })
1884 } else {
1885 None
1886 }
1887 } else {
1888 None
1889 }
1890 })
1891 .collect(),
1892 })
1893 }
1894 }
1895
1896 fn merge_schema_into_properties(
1897 &mut self,
1898 schema: &Schema,
1899 merged_properties: &mut BTreeMap<String, PropertyInfo>,
1900 merged_required: &mut HashSet<String>,
1901 dependencies: &mut HashSet<String>,
1902 ) -> Result<()> {
1903 let details = schema.details();
1904
1905 if let Some(properties) = &details.properties {
1907 for (prop_name, prop_schema) in properties {
1908 let prop_type = self.analyze_property_schema_with_context(
1909 prop_schema,
1910 Some(prop_name),
1911 dependencies,
1912 )?;
1913 let prop_details = prop_schema.details();
1914
1915 merged_properties.insert(
1916 prop_name.clone(),
1917 PropertyInfo {
1918 schema_type: prop_type,
1919 nullable: prop_details.is_nullable(),
1920 description: prop_details.description.clone(),
1921 default: prop_details.default.clone(),
1922 serde_attrs: Vec::new(),
1923 },
1924 );
1925 }
1926 }
1927
1928 if let Some(required) = &details.required {
1930 for field in required {
1931 merged_required.insert(field.clone());
1932 }
1933 }
1934
1935 Ok(())
1936 }
1937
1938 fn analyze_oneof_union(
1939 &mut self,
1940 one_of_schemas: &[Schema],
1941 discriminator: Option<&crate::openapi::Discriminator>,
1942 parent_name: &str,
1943 dependencies: &mut HashSet<String>,
1944 ) -> Result<SchemaType> {
1945 if one_of_schemas.len() == 2 {
1948 let null_count = one_of_schemas
1949 .iter()
1950 .filter(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
1951 .count();
1952 if null_count == 1 {
1953 if let Some(non_null) = one_of_schemas
1954 .iter()
1955 .find(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
1956 {
1957 return self
1958 .analyze_schema_value(non_null, parent_name)
1959 .map(|a| a.schema_type);
1960 }
1961 }
1962 }
1963
1964 if discriminator.is_none() {
1966 return self.analyze_untagged_oneof_union(one_of_schemas, parent_name, dependencies);
1968 }
1969
1970 let discriminator_field = discriminator
1972 .ok_or_else(|| {
1973 GeneratorError::InvalidDiscriminator(
1974 "expected discriminator after guard check".to_string(),
1975 )
1976 })?
1977 .property_name
1978 .clone();
1979
1980 let mut variants = Vec::new();
1981 let mut used_variant_names = std::collections::HashSet::new();
1982
1983 for variant_schema in one_of_schemas {
1984 let ref_info = if let Some(ref_str) = variant_schema.reference() {
1986 Some((ref_str, false))
1987 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
1988 Some((recursive_ref, true))
1989 } else if let Schema::AllOf { all_of, .. } = variant_schema {
1990 if all_of.len() == 1 {
1992 if let Some(ref_str) = all_of[0].reference() {
1993 Some((ref_str, false))
1994 } else {
1995 all_of[0]
1996 .recursive_reference()
1997 .map(|recursive_ref| (recursive_ref, true))
1998 }
1999 } else {
2000 None
2001 }
2002 } else {
2003 None
2004 };
2005
2006 if let Some((ref_str, is_recursive)) = ref_info {
2007 let schema_name = if is_recursive && ref_str == "#" {
2008 self.find_recursive_anchor_schema()
2010 .or_else(|| self.current_schema_name.clone())
2011 .unwrap_or_else(|| "CompoundFilter".to_string())
2012 } else {
2013 self.extract_schema_name(ref_str)
2014 .map(|s| s.to_string())
2015 .unwrap_or_else(|| "UnknownRef".to_string())
2016 };
2017
2018 if !schema_name.is_empty() {
2019 dependencies.insert(schema_name.clone());
2020
2021 let discriminator_value = if let Some(disc) = discriminator {
2026 if let Some(mappings) = &disc.mapping {
2027 mappings
2030 .iter()
2031 .find(|(_, target_ref)| {
2032 target_ref.as_str() == ref_str
2034 || self
2035 .extract_schema_name(target_ref)
2036 .map(|s| s.to_string())
2037 == Some(schema_name.clone())
2038 })
2039 .map(|(key, _)| key.clone())
2040 .unwrap_or_else(|| {
2041 self.fallback_discriminator_value_for_field(
2042 &schema_name,
2043 &discriminator_field,
2044 )
2045 })
2046 } else {
2047 self.fallback_discriminator_value_for_field(
2048 &schema_name,
2049 &discriminator_field,
2050 )
2051 }
2052 } else {
2053 self.fallback_discriminator_value_for_field(
2054 &schema_name,
2055 &discriminator_field,
2056 )
2057 };
2058
2059 let base_name = self.to_rust_variant_name(&schema_name);
2061 let rust_name =
2062 self.ensure_unique_variant_name(base_name, &mut used_variant_names);
2063
2064 let final_discriminator_value = discriminator_value;
2066
2067 variants.push(UnionVariant {
2068 rust_name,
2069 type_name: schema_name,
2070 discriminator_value: final_discriminator_value,
2071 schema_ref: ref_str.to_string(),
2072 });
2073 }
2074 } else {
2075 let variant_index = variants.len();
2077 let inline_type_name =
2078 self.generate_inline_type_name(variant_schema, variant_index);
2079
2080 let discriminator_value = if let Some(disc) = discriminator {
2082 if let Some(mappings) = &disc.mapping {
2083 mappings
2085 .iter()
2086 .find(|(_, target_ref)| {
2087 target_ref.contains(&format!("variant_{variant_index}"))
2088 })
2089 .map(|(key, _)| key.clone())
2090 .unwrap_or_else(|| {
2091 self.extract_inline_discriminator_value(
2092 variant_schema,
2093 &discriminator_field,
2094 variant_index,
2095 )
2096 })
2097 } else {
2098 self.extract_inline_discriminator_value(
2099 variant_schema,
2100 &discriminator_field,
2101 variant_index,
2102 )
2103 }
2104 } else {
2105 self.extract_inline_discriminator_value(
2106 variant_schema,
2107 &discriminator_field,
2108 variant_index,
2109 )
2110 };
2111
2112 let base_name = if discriminator_value.starts_with("variant_") {
2114 format!("Variant{variant_index}")
2115 } else {
2116 let clean_name = self.discriminator_to_variant_name(&discriminator_value);
2118 self.to_rust_variant_name(&clean_name)
2119 };
2120 let rust_name = self.ensure_unique_variant_name(base_name, &mut used_variant_names);
2121
2122 let final_discriminator_value = discriminator_value;
2124
2125 variants.push(UnionVariant {
2126 rust_name,
2127 type_name: inline_type_name.clone(),
2128 discriminator_value: final_discriminator_value,
2129 schema_ref: format!("inline_{variant_index}"),
2130 });
2131
2132 self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
2134 }
2135 }
2136
2137 if variants.is_empty() {
2138 let mut union_variants = Vec::new();
2141
2142 for (variant_index, variant_schema) in one_of_schemas.iter().enumerate() {
2143 if let Some(ref_str) = variant_schema.reference() {
2145 if let Some(schema_name) = self.extract_schema_name(ref_str) {
2146 dependencies.insert(schema_name.to_string());
2147 union_variants.push(SchemaRef {
2148 target: schema_name.to_string(),
2149 nullable: false,
2150 });
2151 }
2152 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2153 let schema_name = if recursive_ref == "#" {
2154 self.find_recursive_anchor_schema()
2156 .or_else(|| self.current_schema_name.clone())
2157 .unwrap_or_else(|| "CompoundFilter".to_string())
2158 } else {
2159 self.extract_schema_name(recursive_ref)
2160 .map(|s| s.to_string())
2161 .unwrap_or_else(|| "RecursiveType".to_string())
2162 };
2163 dependencies.insert(schema_name.clone());
2164 union_variants.push(SchemaRef {
2165 target: schema_name,
2166 nullable: false,
2167 });
2168 } else {
2169 let inline_name = self.generate_context_aware_name(
2171 parent_name,
2172 "InlineVariant",
2173 variant_index,
2174 Some(variant_schema),
2175 );
2176 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
2177 let variant_type = analyzed.schema_type;
2178
2179 for dep in &analyzed.dependencies {
2181 dependencies.insert(dep.clone());
2182 }
2183
2184 match &variant_type {
2185 SchemaType::Primitive { rust_type } => {
2187 union_variants.push(SchemaRef {
2188 target: rust_type.clone(),
2189 nullable: false,
2190 });
2191 }
2192 SchemaType::Array { item_type } => {
2194 match item_type.as_ref() {
2195 SchemaType::Primitive { rust_type } => {
2196 let type_name = format!("Vec<{rust_type}>");
2197 union_variants.push(SchemaRef {
2198 target: type_name,
2199 nullable: false,
2200 });
2201 }
2202 SchemaType::Reference { target } => {
2203 let type_name = format!("Vec<{target}>");
2204 union_variants.push(SchemaRef {
2205 target: type_name,
2206 nullable: false,
2207 });
2208 }
2209 _ => {
2210 let inline_type_name = self.generate_context_aware_name(
2212 parent_name,
2213 "Variant",
2214 variant_index,
2215 None,
2216 );
2217 self.add_inline_schema(
2218 &inline_type_name,
2219 variant_schema,
2220 dependencies,
2221 )?;
2222 union_variants.push(SchemaRef {
2223 target: inline_type_name,
2224 nullable: false,
2225 });
2226 }
2227 }
2228 }
2229 SchemaType::Reference { target } => {
2231 union_variants.push(SchemaRef {
2232 target: target.clone(),
2233 nullable: false,
2234 });
2235 }
2236 _ => {
2238 let inline_type_name =
2239 format!("{}Variant{}", parent_name, variant_index + 1);
2240 self.add_inline_schema(
2241 &inline_type_name,
2242 variant_schema,
2243 dependencies,
2244 )?;
2245 union_variants.push(SchemaRef {
2246 target: inline_type_name,
2247 nullable: false,
2248 });
2249 }
2250 }
2251 }
2252 }
2253
2254 if !union_variants.is_empty() {
2255 return Ok(SchemaType::Union {
2256 variants: union_variants,
2257 });
2258 }
2259
2260 return Ok(SchemaType::Primitive {
2262 rust_type: "serde_json::Value".to_string(),
2263 });
2264 }
2265
2266 Ok(SchemaType::DiscriminatedUnion {
2267 discriminator_field,
2268 variants,
2269 })
2270 }
2271
2272 fn analyze_untagged_oneof_union(
2273 &mut self,
2274 one_of_schemas: &[Schema],
2275 parent_name: &str,
2276 dependencies: &mut HashSet<String>,
2277 ) -> Result<SchemaType> {
2278 let filtered: Vec<&Schema> = one_of_schemas
2282 .iter()
2283 .filter(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
2284 .collect();
2285
2286 if filtered.len() == 1 {
2288 return self
2289 .analyze_schema_value(filtered[0], parent_name)
2290 .map(|a| a.schema_type);
2291 }
2292
2293 let mut union_variants = Vec::new();
2294
2295 for (variant_index, variant_schema) in filtered.iter().copied().enumerate() {
2296 if let Some(ref_str) = variant_schema.reference() {
2298 if let Some(schema_name) = self.extract_schema_name(ref_str) {
2299 dependencies.insert(schema_name.to_string());
2300 union_variants.push(SchemaRef {
2301 target: schema_name.to_string(),
2302 nullable: false,
2303 });
2304 }
2305 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2306 let schema_name = if recursive_ref == "#" {
2307 self.find_recursive_anchor_schema()
2309 .or_else(|| self.current_schema_name.clone())
2310 .unwrap_or_else(|| "CompoundFilter".to_string())
2311 } else {
2312 self.extract_schema_name(recursive_ref)
2313 .map(|s| s.to_string())
2314 .unwrap_or_else(|| "RecursiveType".to_string())
2315 };
2316 dependencies.insert(schema_name.clone());
2317 union_variants.push(SchemaRef {
2318 target: schema_name,
2319 nullable: false,
2320 });
2321 } else {
2322 let inline_name = self.generate_context_aware_name(
2324 parent_name,
2325 "InlineVariant",
2326 variant_index,
2327 Some(variant_schema),
2328 );
2329 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
2330 let variant_type = analyzed.schema_type;
2331
2332 for dep in &analyzed.dependencies {
2334 dependencies.insert(dep.clone());
2335 }
2336
2337 match &variant_type {
2338 SchemaType::Primitive { rust_type } => {
2340 union_variants.push(SchemaRef {
2341 target: rust_type.clone(),
2342 nullable: false,
2343 });
2344 }
2345 SchemaType::Array { item_type } => {
2347 match item_type.as_ref() {
2348 SchemaType::Primitive { rust_type } => {
2349 let type_name = format!("Vec<{rust_type}>");
2350 union_variants.push(SchemaRef {
2351 target: type_name,
2352 nullable: false,
2353 });
2354 }
2355 SchemaType::Reference { target } => {
2356 let type_name = format!("Vec<{target}>");
2357 union_variants.push(SchemaRef {
2358 target: type_name,
2359 nullable: false,
2360 });
2361 }
2362 SchemaType::Array {
2364 item_type: inner_item_type,
2365 } => {
2366 match inner_item_type.as_ref() {
2367 SchemaType::Primitive { rust_type } => {
2368 let type_name = format!("Vec<Vec<{rust_type}>>");
2369 union_variants.push(SchemaRef {
2370 target: type_name,
2371 nullable: false,
2372 });
2373 }
2374 SchemaType::Reference { target } => {
2375 let type_name = format!("Vec<Vec<{target}>>");
2376 union_variants.push(SchemaRef {
2377 target: type_name,
2378 nullable: false,
2379 });
2380 }
2381 _ => {
2382 let inline_type_name = self.generate_context_aware_name(
2384 parent_name,
2385 "Variant",
2386 variant_index,
2387 None,
2388 );
2389 self.add_inline_schema(
2390 &inline_type_name,
2391 variant_schema,
2392 dependencies,
2393 )?;
2394 union_variants.push(SchemaRef {
2395 target: inline_type_name,
2396 nullable: false,
2397 });
2398 }
2399 }
2400 }
2401 _ => {
2402 let inline_type_name = self.generate_context_aware_name(
2404 parent_name,
2405 "Variant",
2406 variant_index,
2407 None,
2408 );
2409 self.add_inline_schema(
2410 &inline_type_name,
2411 variant_schema,
2412 dependencies,
2413 )?;
2414 union_variants.push(SchemaRef {
2415 target: inline_type_name,
2416 nullable: false,
2417 });
2418 }
2419 }
2420 }
2421 SchemaType::Reference { target } => {
2423 union_variants.push(SchemaRef {
2424 target: target.clone(),
2425 nullable: false,
2426 });
2427 }
2428 _ => {
2430 let inline_type_name = self.generate_context_aware_name(
2431 parent_name,
2432 "Variant",
2433 variant_index,
2434 None,
2435 );
2436 self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
2437 union_variants.push(SchemaRef {
2438 target: inline_type_name,
2439 nullable: false,
2440 });
2441 }
2442 }
2443 }
2444 }
2445
2446 if !union_variants.is_empty() {
2447 return Ok(SchemaType::Union {
2448 variants: union_variants,
2449 });
2450 }
2451
2452 Ok(SchemaType::Primitive {
2454 rust_type: "serde_json::Value".to_string(),
2455 })
2456 }
2457
2458 fn add_inline_schema(
2459 &mut self,
2460 type_name: &str,
2461 schema: &Schema,
2462 dependencies: &mut HashSet<String>,
2463 ) -> Result<()> {
2464 if let Some(schema_type) = schema.schema_type() {
2466 match schema_type {
2467 OpenApiSchemaType::String
2468 | OpenApiSchemaType::Integer
2469 | OpenApiSchemaType::Number
2470 | OpenApiSchemaType::Boolean => {
2471 let rust_type =
2472 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
2473
2474 self.resolved_cache.insert(
2476 type_name.to_string(),
2477 AnalyzedSchema {
2478 name: type_name.to_string(),
2479 original: serde_json::to_value(schema).unwrap_or(Value::Null),
2480 schema_type: SchemaType::Primitive { rust_type },
2481 dependencies: HashSet::new(),
2482 nullable: false,
2483 description: schema.details().description.clone(),
2484 default: None,
2485 },
2486 );
2487 return Ok(());
2488 }
2489 _ => {}
2490 }
2491 }
2492
2493 let previous_schema_name = self.current_schema_name.take();
2497 self.current_schema_name = Some(type_name.to_string());
2498 let analyzed = self.analyze_schema_value(schema, type_name)?;
2499 self.current_schema_name = previous_schema_name;
2500
2501 self.resolved_cache.insert(type_name.to_string(), analyzed);
2503
2504 if let Some(cached) = self.resolved_cache.get(type_name) {
2506 for dep in &cached.dependencies {
2507 dependencies.insert(dep.clone());
2508 }
2509 }
2510
2511 Ok(())
2512 }
2513
2514 fn extract_inline_discriminator_value(
2515 &self,
2516 schema: &Schema,
2517 discriminator_field: &str,
2518 variant_index: usize,
2519 ) -> String {
2520 if let Some(properties) = &schema.details().properties {
2522 if let Some(discriminator_prop) = properties.get(discriminator_field) {
2523 if let Some(enum_values) = &discriminator_prop.details().enum_values {
2525 if enum_values.len() == 1 {
2526 if let Some(value) = enum_values[0].as_str() {
2527 return value.to_string();
2528 }
2529 }
2530 }
2531 if let Some(const_value) = discriminator_prop.details().extra.get("const") {
2533 if let Some(value) = const_value.as_str() {
2534 return value.to_string();
2535 }
2536 }
2537 if let Some(const_value) = &discriminator_prop.details().const_value {
2539 if let Some(value) = const_value.as_str() {
2540 return value.to_string();
2541 }
2542 }
2543 }
2544 }
2545
2546 if let Some(inferred_name) = self.infer_variant_name_from_structure(schema, variant_index) {
2548 return inferred_name;
2549 }
2550
2551 format!("variant_{variant_index}")
2553 }
2554
2555 fn infer_variant_name_from_structure(
2556 &self,
2557 schema: &Schema,
2558 _variant_index: usize,
2559 ) -> Option<String> {
2560 let details = schema.details();
2561
2562 if let Some(properties) = &details.properties {
2564 if properties.contains_key("text") && properties.len() <= 3 {
2566 return Some("text".to_string());
2567 }
2568 if properties.contains_key("image") || properties.contains_key("source") {
2569 return Some("image".to_string());
2570 }
2571 if properties.contains_key("document") {
2572 return Some("document".to_string());
2573 }
2574 if properties.contains_key("tool_use_id") || properties.contains_key("tool_result") {
2575 return Some("tool_result".to_string());
2576 }
2577 if properties.contains_key("content") && properties.contains_key("is_error") {
2578 return Some("tool_result".to_string());
2579 }
2580 if properties.contains_key("partial_json") {
2581 return Some("partial_json".to_string());
2582 }
2583
2584 let property_names: Vec<&String> = properties.keys().collect();
2586
2587 for prop_name in &property_names {
2589 if prop_name.contains("result") {
2590 return Some("result".to_string());
2591 }
2592 if prop_name.contains("error") {
2593 return Some("error".to_string());
2594 }
2595 if prop_name.contains("content") && property_names.len() <= 2 {
2596 return Some("content".to_string());
2597 }
2598 }
2599
2600 let significant_props = property_names
2602 .iter()
2603 .filter(|&name| !["type", "id", "cache_control"].contains(&name.as_str()))
2604 .collect::<Vec<_>>();
2605
2606 if significant_props.len() == 1 {
2607 return Some((*significant_props[0]).clone());
2608 }
2609 }
2610
2611 if let Some(description) = &details.description {
2613 let desc_lower = description.to_lowercase();
2614 if desc_lower.contains("text") && desc_lower.len() < 100 {
2615 return Some("text".to_string());
2616 }
2617 if desc_lower.contains("image") {
2618 return Some("image".to_string());
2619 }
2620 if desc_lower.contains("document") {
2621 return Some("document".to_string());
2622 }
2623 if desc_lower.contains("tool") && desc_lower.contains("result") {
2624 return Some("tool_result".to_string());
2625 }
2626 }
2627
2628 None
2629 }
2630
2631 fn discriminator_to_variant_name(&self, discriminator: &str) -> String {
2632 if discriminator.is_empty() {
2634 return "Variant".to_string();
2635 }
2636
2637 let mut result = String::new();
2638 let mut next_upper = true;
2639
2640 for c in discriminator.chars() {
2641 match c {
2642 'a'..='z' => {
2643 if next_upper {
2644 result.push(c.to_ascii_uppercase());
2645 next_upper = false;
2646 } else {
2647 result.push(c);
2648 }
2649 }
2650 'A'..='Z' => {
2651 result.push(c);
2652 next_upper = false;
2653 }
2654 '0'..='9' => {
2655 result.push(c);
2656 next_upper = false;
2657 }
2658 '_' | '-' | '.' | ' ' | '/' | '\\' => {
2659 next_upper = true;
2661 }
2662 _ => {
2663 next_upper = true;
2665 }
2666 }
2667 }
2668
2669 if result.is_empty() || result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2671 result = format!("Variant{result}");
2672 }
2673
2674 result
2675 }
2676
2677 fn ensure_unique_variant_name(
2678 &self,
2679 base_name: String,
2680 used_names: &mut std::collections::HashSet<String>,
2681 ) -> String {
2682 let mut candidate = base_name.clone();
2683 let mut counter = 1;
2684
2685 while used_names.contains(&candidate) {
2686 counter += 1;
2687 candidate = format!("{base_name}{counter}");
2688 }
2689
2690 used_names.insert(candidate.clone());
2691 candidate
2692 }
2693
2694 fn generate_inline_type_name(&self, schema: &Schema, variant_index: usize) -> String {
2695 if let Some(meaningful_name) = self.infer_type_name_from_structure(schema) {
2697 return meaningful_name;
2698 }
2699
2700 let context = self.current_schema_name.as_deref().unwrap_or("Inline");
2702 self.generate_context_aware_name(context, "Variant", variant_index, Some(schema))
2703 }
2704
2705 fn infer_type_name_from_structure(&self, schema: &Schema) -> Option<String> {
2706 let details = schema.details();
2707
2708 if let Some(description) = &details.description {
2710 if let Some(name_from_desc) = self.extract_type_name_from_description(description) {
2711 return Some(name_from_desc);
2712 }
2713 }
2714
2715 if let Some(properties) = &details.properties {
2717 if let Some(name_from_props) = self.extract_type_name_from_properties(properties) {
2718 return Some(format!("{name_from_props}Block"));
2719 }
2720 }
2721
2722 None
2723 }
2724
2725 fn extract_type_name_from_description(&self, description: &str) -> Option<String> {
2726 if description.len() > 100 || description.contains('\n') {
2728 return None;
2729 }
2730
2731 let words: Vec<&str> = description
2733 .split_whitespace()
2734 .take(2) .filter(|word| {
2736 let w = word.to_lowercase();
2737 word.len() > 2
2738 && ![
2739 "the", "and", "for", "with", "that", "this", "are", "can", "will", "was",
2740 ]
2741 .contains(&w.as_str())
2742 })
2743 .collect();
2744
2745 if words.is_empty() {
2746 return None;
2747 }
2748
2749 let combined = words.join("_");
2751 let pascal_name = self.discriminator_to_variant_name(&combined);
2752
2753 if !pascal_name.ends_with("Content")
2755 && !pascal_name.ends_with("Block")
2756 && !pascal_name.ends_with("Type")
2757 {
2758 Some(format!("{pascal_name}Content"))
2759 } else {
2760 Some(pascal_name)
2761 }
2762 }
2763
2764 fn extract_type_name_from_properties(
2765 &self,
2766 properties: &std::collections::BTreeMap<String, crate::openapi::Schema>,
2767 ) -> Option<String> {
2768 let significant_props: Vec<&String> = properties
2770 .keys()
2771 .filter(|name| !["type", "id", "cache_control"].contains(&name.as_str()))
2772 .collect();
2773
2774 if significant_props.is_empty() {
2775 return None;
2776 }
2777
2778 if significant_props.len() == 1 {
2780 let prop_name = significant_props[0];
2781 return Some(self.discriminator_to_variant_name(prop_name));
2782 }
2783
2784 let mut sorted_props = significant_props.clone();
2787 sorted_props.sort();
2788 if let Some(first_prop) = sorted_props.first() {
2789 return Some(self.discriminator_to_variant_name(first_prop));
2790 }
2791
2792 None
2793 }
2794
2795 fn openapi_type_to_rust_type(
2796 &self,
2797 openapi_type: OpenApiSchemaType,
2798 details: &crate::openapi::SchemaDetails,
2799 ) -> String {
2800 match openapi_type {
2801 OpenApiSchemaType::String => "String".to_string(),
2802 OpenApiSchemaType::Integer => self.get_number_rust_type(openapi_type, details),
2803 OpenApiSchemaType::Number => self.get_number_rust_type(openapi_type, details),
2804 OpenApiSchemaType::Boolean => "bool".to_string(),
2805 OpenApiSchemaType::Array => "Vec<serde_json::Value>".to_string(), OpenApiSchemaType::Object => "serde_json::Value".to_string(), OpenApiSchemaType::Null => "()".to_string(), }
2809 }
2810
2811 #[allow(dead_code)]
2812 fn fallback_discriminator_value(&self, schema_name: &str) -> String {
2813 self.fallback_discriminator_value_for_field(schema_name, "type")
2814 }
2815
2816 fn fallback_discriminator_value_for_field(
2817 &self,
2818 schema_name: &str,
2819 field_name: &str,
2820 ) -> String {
2821 if let Some(ref_schema) = self.schemas.get(schema_name) {
2823 if let Some(extracted) =
2824 self.extract_discriminator_value_for_field(ref_schema, field_name)
2825 {
2826 return extracted;
2827 }
2828 }
2829
2830 self.generate_discriminator_value_from_name(schema_name)
2832 }
2833
2834 fn generate_discriminator_value_from_name(&self, schema_name: &str) -> String {
2835 let mut result = String::new();
2837 let mut chars = schema_name.chars().peekable();
2838 let mut first = true;
2839
2840 while let Some(c) = chars.next() {
2841 if c.is_uppercase()
2842 && !first
2843 && chars
2844 .peek()
2845 .map(|&next| next.is_lowercase())
2846 .unwrap_or(false)
2847 {
2848 result.push('.');
2849 }
2850 result.push(c.to_ascii_lowercase());
2851 first = false;
2852 }
2853
2854 if result.ends_with("event") {
2856 result = result[..result.len() - 5].to_string();
2857 }
2858
2859 if schema_name.starts_with("Response") && !result.starts_with("response.") {
2861 result = format!("response.{}", result.trim_start_matches("response"));
2862 }
2863
2864 result
2865 }
2866
2867 fn to_rust_variant_name(&self, schema_name: &str) -> String {
2868 let mut name = schema_name;
2870
2871 if name.starts_with("Response") && name.len() > 8 {
2873 name = &name[8..]; }
2875
2876 if name.ends_with("Event") && name.len() > 5 {
2878 name = &name[..name.len() - 5]; }
2880
2881 name = name.trim_matches('_');
2883
2884 if name.is_empty() {
2886 schema_name.to_string()
2887 } else {
2888 self.discriminator_to_variant_name(name)
2890 }
2891 }
2892
2893 fn analyze_array_schema(
2894 &mut self,
2895 schema: &Schema,
2896 parent_schema_name: &str,
2897 dependencies: &mut HashSet<String>,
2898 ) -> Result<SchemaType> {
2899 let details = schema.details();
2900
2901 if let Some(items_schema) = &details.items {
2903 let item_type = match items_schema.as_ref() {
2905 Schema::Reference { reference, .. } => {
2906 let target = self
2908 .extract_schema_name(reference)
2909 .ok_or_else(|| GeneratorError::UnresolvedReference(reference.to_string()))?
2910 .to_string();
2911 dependencies.insert(target.clone());
2912 SchemaType::Reference { target }
2913 }
2914 Schema::RecursiveRef { recursive_ref, .. } => {
2915 if recursive_ref == "#" {
2917 let target = self
2919 .find_recursive_anchor_schema()
2920 .unwrap_or_else(|| parent_schema_name.to_string());
2921 dependencies.insert(target.clone());
2922 SchemaType::Reference { target }
2923 } else {
2924 let target = self
2925 .extract_schema_name(recursive_ref)
2926 .unwrap_or("RecursiveType")
2927 .to_string();
2928 dependencies.insert(target.clone());
2929 SchemaType::Reference { target }
2930 }
2931 }
2932 Schema::Typed { schema_type, .. } => {
2933 match schema_type {
2935 OpenApiSchemaType::String => SchemaType::Primitive {
2936 rust_type: "String".to_string(),
2937 },
2938 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
2939 let details = items_schema.details();
2940 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
2941 SchemaType::Primitive { rust_type }
2942 }
2943 OpenApiSchemaType::Boolean => SchemaType::Primitive {
2944 rust_type: "bool".to_string(),
2945 },
2946 OpenApiSchemaType::Object => {
2947 let object_type_name = format!("{parent_schema_name}Item");
2949
2950 let object_type =
2952 self.analyze_object_schema(items_schema, dependencies)?;
2953
2954 let inline_schema = AnalyzedSchema {
2956 name: object_type_name.clone(),
2957 original: serde_json::to_value(items_schema).unwrap_or(Value::Null),
2958 schema_type: object_type,
2959 dependencies: dependencies.clone(),
2960 nullable: false,
2961 description: items_schema.details().description.clone(),
2962 default: None,
2963 };
2964
2965 self.resolved_cache
2967 .insert(object_type_name.clone(), inline_schema);
2968 dependencies.insert(object_type_name.clone());
2969
2970 SchemaType::Reference {
2972 target: object_type_name,
2973 }
2974 }
2975 OpenApiSchemaType::Array => {
2976 self.analyze_array_schema(
2978 items_schema,
2979 parent_schema_name,
2980 dependencies,
2981 )?
2982 }
2983 _ => SchemaType::Primitive {
2984 rust_type: "serde_json::Value".to_string(),
2985 },
2986 }
2987 }
2988 Schema::OneOf { .. } | Schema::AnyOf { .. } => {
2989 let analyzed = self.analyze_schema_value(items_schema, "ArrayItem")?;
2991
2992 match &analyzed.schema_type {
2994 SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => {
2995 let union_name = format!("{parent_schema_name}ItemUnion");
2998
2999 let mut union_schema = analyzed;
3001 union_schema.name = union_name.clone();
3002
3003 self.resolved_cache.insert(union_name.clone(), union_schema);
3005
3006 dependencies.insert(union_name.clone());
3008
3009 SchemaType::Reference { target: union_name }
3011 }
3012 _ => analyzed.schema_type,
3013 }
3014 }
3015 Schema::Untyped { .. } => {
3016 if let Some(inferred) = items_schema.inferred_type() {
3018 match inferred {
3019 OpenApiSchemaType::Object => {
3020 let object_type_name = format!("{parent_schema_name}Item");
3022
3023 let object_type =
3025 self.analyze_object_schema(items_schema, dependencies)?;
3026
3027 let inline_schema = AnalyzedSchema {
3029 name: object_type_name.clone(),
3030 original: serde_json::to_value(items_schema)
3031 .unwrap_or(Value::Null),
3032 schema_type: object_type,
3033 dependencies: dependencies.clone(),
3034 nullable: false,
3035 description: items_schema.details().description.clone(),
3036 default: None,
3037 };
3038
3039 self.resolved_cache
3041 .insert(object_type_name.clone(), inline_schema);
3042 dependencies.insert(object_type_name.clone());
3043
3044 SchemaType::Reference {
3046 target: object_type_name,
3047 }
3048 }
3049 OpenApiSchemaType::String => SchemaType::Primitive {
3050 rust_type: "String".to_string(),
3051 },
3052 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
3053 let details = items_schema.details();
3054 let rust_type = self.get_number_rust_type(inferred, details);
3055 SchemaType::Primitive { rust_type }
3056 }
3057 OpenApiSchemaType::Boolean => SchemaType::Primitive {
3058 rust_type: "bool".to_string(),
3059 },
3060 _ => SchemaType::Primitive {
3061 rust_type: "serde_json::Value".to_string(),
3062 },
3063 }
3064 } else {
3065 SchemaType::Primitive {
3066 rust_type: "serde_json::Value".to_string(),
3067 }
3068 }
3069 }
3070 _ => SchemaType::Primitive {
3071 rust_type: "serde_json::Value".to_string(),
3072 },
3073 };
3074
3075 Ok(SchemaType::Array {
3076 item_type: Box::new(item_type),
3077 })
3078 } else {
3079 Ok(SchemaType::Primitive {
3081 rust_type: "Vec<serde_json::Value>".to_string(),
3082 })
3083 }
3084 }
3085
3086 fn get_number_rust_type(
3087 &self,
3088 schema_type: OpenApiSchemaType,
3089 details: &crate::openapi::SchemaDetails,
3090 ) -> String {
3091 match schema_type {
3092 OpenApiSchemaType::Integer => {
3093 match details.format.as_deref() {
3095 Some("int32") => "i32".to_string(),
3096 Some("int64") => "i64".to_string(),
3097 _ => "i64".to_string(), }
3099 }
3100 OpenApiSchemaType::Number => {
3101 match details.format.as_deref() {
3103 Some("float") => "f32".to_string(),
3104 Some("double") => "f64".to_string(),
3105 _ => "f64".to_string(), }
3107 }
3108 _ => "serde_json::Value".to_string(), }
3110 }
3111
3112 fn analyze_anyof_union(
3113 &mut self,
3114 any_of_schemas: &[Schema],
3115 discriminator: Option<&Discriminator>,
3116 dependencies: &mut HashSet<String>,
3117 context_name: &str,
3118 ) -> Result<SchemaType> {
3119 let filtered_owned: Vec<Schema>;
3124 let any_of_schemas: &[Schema] = if any_of_schemas
3125 .iter()
3126 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
3127 {
3128 filtered_owned = any_of_schemas
3129 .iter()
3130 .filter(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
3131 .cloned()
3132 .collect();
3133 if filtered_owned.is_empty() {
3134 return Ok(SchemaType::Primitive {
3135 rust_type: "serde_json::Value".to_string(),
3136 });
3137 }
3138 if filtered_owned.len() == 1 {
3139 return self
3140 .analyze_schema_value(&filtered_owned[0], context_name)
3141 .map(|a| a.schema_type);
3142 }
3143 &filtered_owned
3144 } else {
3145 any_of_schemas
3146 };
3147
3148 let has_refs = any_of_schemas.iter().any(|s| s.is_reference());
3150 let has_objects = any_of_schemas.iter().any(|s| {
3151 matches!(s.schema_type(), Some(OpenApiSchemaType::Object))
3152 || s.inferred_type() == Some(OpenApiSchemaType::Object)
3153 });
3154 let has_arrays = any_of_schemas
3155 .iter()
3156 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Array)));
3157
3158 let all_string_like = any_of_schemas.iter().all(|s| {
3161 matches!(s.schema_type(), Some(OpenApiSchemaType::String))
3162 || s.details().const_value.is_some()
3163 });
3164
3165 if (has_refs || has_objects || has_arrays || any_of_schemas.len() > 1) && !all_string_like {
3166 if let Some(disc) = discriminator {
3168 return self.analyze_oneof_union(
3170 any_of_schemas,
3171 Some(disc),
3172 context_name,
3173 dependencies,
3174 );
3175 }
3176
3177 if let Some(disc_field) = self.detect_discriminator_field(any_of_schemas) {
3179 return self.analyze_oneof_union(
3180 any_of_schemas,
3181 Some(&Discriminator {
3182 property_name: disc_field,
3183 mapping: None,
3184 extra: BTreeMap::new(),
3185 }),
3186 context_name,
3187 dependencies,
3188 );
3189 }
3190
3191 let mut variants = Vec::new();
3193
3194 for schema in any_of_schemas {
3195 if let Some(ref_str) = schema.reference() {
3196 if let Some(target) = self.extract_schema_name(ref_str) {
3197 dependencies.insert(target.to_string());
3198 variants.push(SchemaRef {
3199 target: target.to_string(),
3200 nullable: false,
3201 });
3202 }
3203 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object))
3204 || schema.inferred_type() == Some(OpenApiSchemaType::Object)
3205 {
3206 let inline_index = variants.len();
3208 let inline_type_name = self.generate_inline_type_name(schema, inline_index);
3209
3210 self.add_inline_schema(&inline_type_name, schema, dependencies)?;
3212
3213 variants.push(SchemaRef {
3214 target: inline_type_name,
3215 nullable: false,
3216 });
3217 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Array)) {
3218 let array_type =
3220 self.analyze_array_schema(schema, context_name, dependencies)?;
3221
3222 let array_type_name = if let Some(items_schema) = &schema.details().items {
3224 if let Some(ref_str) = items_schema.reference() {
3225 if let Some(item_type_name) = self.extract_schema_name(ref_str) {
3226 dependencies.insert(item_type_name.to_string());
3227 format!("{item_type_name}Array")
3228 } else {
3229 self.generate_context_aware_name(
3230 context_name,
3231 "Array",
3232 variants.len(),
3233 Some(schema),
3234 )
3235 }
3236 } else {
3237 self.generate_context_aware_name(
3238 context_name,
3239 "Array",
3240 variants.len(),
3241 Some(schema),
3242 )
3243 }
3244 } else {
3245 self.generate_context_aware_name(
3246 context_name,
3247 "Array",
3248 variants.len(),
3249 Some(schema),
3250 )
3251 };
3252
3253 self.resolved_cache.insert(
3255 array_type_name.clone(),
3256 AnalyzedSchema {
3257 name: array_type_name.clone(),
3258 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3259 schema_type: array_type,
3260 dependencies: HashSet::new(),
3261 nullable: false,
3262 description: Some("Array variant in union".to_string()),
3263 default: None,
3264 },
3265 );
3266
3267 dependencies.insert(array_type_name.clone());
3269
3270 variants.push(SchemaRef {
3271 target: array_type_name,
3272 nullable: false,
3273 });
3274 } else if let Some(schema_type) = schema.schema_type() {
3275 let inline_index = variants.len();
3277
3278 let inline_type_name = match schema_type {
3280 OpenApiSchemaType::String => {
3281 if inline_index == 0 {
3284 format!("{context_name}String")
3285 } else {
3286 format!("{context_name}StringVariant{inline_index}")
3287 }
3288 }
3289 OpenApiSchemaType::Number => {
3290 if inline_index == 0 {
3291 format!("{context_name}Number")
3292 } else {
3293 format!("{context_name}NumberVariant{inline_index}")
3294 }
3295 }
3296 OpenApiSchemaType::Integer => {
3297 if inline_index == 0 {
3298 format!("{context_name}Integer")
3299 } else {
3300 format!("{context_name}IntegerVariant{inline_index}")
3301 }
3302 }
3303 OpenApiSchemaType::Boolean => {
3304 if inline_index == 0 {
3305 format!("{context_name}Boolean")
3306 } else {
3307 format!("{context_name}BooleanVariant{inline_index}")
3308 }
3309 }
3310 _ => format!("{context_name}Variant{inline_index}"),
3311 };
3312
3313 let rust_type =
3314 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
3315
3316 self.resolved_cache.insert(
3318 inline_type_name.clone(),
3319 AnalyzedSchema {
3320 name: inline_type_name.clone(),
3321 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3322 schema_type: SchemaType::Primitive { rust_type },
3323 dependencies: HashSet::new(),
3324 nullable: false,
3325 description: schema.details().description.clone(),
3326 default: None,
3327 },
3328 );
3329
3330 dependencies.insert(inline_type_name.clone());
3332
3333 variants.push(SchemaRef {
3334 target: inline_type_name,
3335 nullable: false,
3336 });
3337 }
3338 }
3339
3340 if !variants.is_empty() {
3341 return Ok(SchemaType::Union { variants });
3342 }
3343 }
3344
3345 let all_strings = any_of_schemas.iter().all(|schema| {
3347 matches!(schema.schema_type(), Some(OpenApiSchemaType::String))
3348 || schema.details().const_value.is_some()
3349 });
3350
3351 if all_strings {
3352 let mut enum_values = Vec::new();
3354 let mut has_open_string = false;
3355
3356 for schema in any_of_schemas {
3357 if let Some(const_val) = &schema.details().const_value {
3358 if let Some(const_str) = const_val.as_str() {
3359 enum_values.push(const_str.to_string());
3360 }
3361 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::String)) {
3362 has_open_string = true;
3363 }
3364 }
3365
3366 if !enum_values.is_empty() {
3367 if has_open_string {
3368 return Ok(SchemaType::ExtensibleEnum {
3371 known_values: enum_values,
3372 });
3373 } else {
3374 return Ok(SchemaType::StringEnum {
3376 values: enum_values,
3377 });
3378 }
3379 }
3380 }
3381
3382 Ok(SchemaType::Primitive {
3384 rust_type: "serde_json::Value".to_string(),
3385 })
3386 }
3387
3388 fn find_recursive_anchor_schema(&self) -> Option<String> {
3390 for (schema_name, schema) in &self.schemas {
3392 let details = schema.details();
3393 if details.recursive_anchor == Some(true) {
3394 return Some(schema_name.clone());
3395 }
3396 }
3397
3398 None
3402 }
3403
3404 fn should_use_dynamic_json(&self, schema: &Schema) -> bool {
3407 if let Schema::AnyOf { any_of, .. } = schema {
3409 if any_of.len() == 2 {
3410 let has_null = any_of
3411 .iter()
3412 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)));
3413 let has_empty_object = any_of.iter().any(|s| self.is_dynamic_object_pattern(s));
3414
3415 if has_null && has_empty_object {
3416 return true;
3417 }
3418 }
3419 }
3420
3421 self.is_dynamic_object_pattern(schema)
3423 }
3424
3425 fn is_dynamic_object_pattern(&self, schema: &Schema) -> bool {
3427 let is_object = match schema.schema_type() {
3429 Some(OpenApiSchemaType::Object) => true,
3430 None => schema.inferred_type() == Some(OpenApiSchemaType::Object),
3431 _ => false,
3432 };
3433
3434 if !is_object {
3435 return false;
3436 }
3437
3438 let details = schema.details();
3439
3440 if self.has_explicit_additional_properties(schema) {
3443 return false;
3444 }
3445
3446 let no_properties = details
3448 .properties
3449 .as_ref()
3450 .map(|props| props.is_empty())
3451 .unwrap_or(true);
3452
3453 if no_properties {
3454 let has_structural_constraints =
3456 details.required.as_ref()
3458 .map(|req| req.iter().any(|r| r != "type"))
3459 .unwrap_or(false)
3460 || details.extra.contains_key("patternProperties")
3462 || details.extra.contains_key("propertyNames")
3464 || details.extra.contains_key("minProperties")
3466 || details.extra.contains_key("maxProperties")
3467 || details.extra.contains_key("dependencies")
3469 || details.extra.contains_key("if")
3471 || details.extra.contains_key("then")
3472 || details.extra.contains_key("else");
3473
3474 return !has_structural_constraints;
3475 }
3476
3477 false
3478 }
3479
3480 fn has_explicit_additional_properties(&self, schema: &Schema) -> bool {
3482 let details = schema.details();
3483
3484 matches!(
3486 &details.additional_properties,
3487 Some(crate::openapi::AdditionalProperties::Boolean(true))
3488 | Some(crate::openapi::AdditionalProperties::Schema(_))
3489 )
3490 }
3491
3492 fn analyze_operations(&mut self, analysis: &mut SchemaAnalysis) -> Result<()> {
3494 let spec: crate::openapi::OpenApiSpec = serde_json::from_value(self.openapi_spec.clone())
3495 .map_err(GeneratorError::ParseError)?;
3496
3497 if let Some(paths) = &spec.paths {
3498 for (path, path_item) in paths {
3499 for (method, operation) in path_item.operations() {
3500 let operation_id = operation
3502 .operation_id
3503 .clone()
3504 .unwrap_or_else(|| Self::generate_operation_id(method, path));
3505
3506 let op_info = self.analyze_single_operation(
3507 &operation_id,
3508 method,
3509 path,
3510 operation,
3511 path_item.parameters.as_ref(),
3512 analysis,
3513 )?;
3514 analysis.operations.insert(operation_id, op_info);
3515 }
3516 }
3517 }
3518 Ok(())
3519 }
3520
3521 fn generate_operation_id(method: &str, path: &str) -> String {
3524 let mut operation_id = method.to_lowercase();
3526
3527 let path_parts: Vec<&str> = path.trim_start_matches('/').split('/').collect();
3529
3530 for part in path_parts {
3531 if part.is_empty() {
3532 continue;
3533 }
3534
3535 let cleaned_part = if part.starts_with('{') && part.ends_with('}') {
3537 &part[1..part.len() - 1]
3538 } else {
3539 part
3540 };
3541
3542 let pascal_case_part = cleaned_part
3544 .split(&['-', '_'][..])
3545 .map(|s| {
3546 let mut chars = s.chars();
3547 match chars.next() {
3548 None => String::new(),
3549 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
3550 }
3551 })
3552 .collect::<String>();
3553
3554 operation_id.push_str(&pascal_case_part);
3555 }
3556
3557 operation_id
3558 }
3559
3560 fn analyze_single_operation(
3562 &mut self,
3563 operation_id: &str,
3564 method: &str,
3565 path: &str,
3566 operation: &crate::openapi::Operation,
3567 path_item_parameters: Option<&Vec<crate::openapi::Parameter>>,
3568 _analysis: &mut SchemaAnalysis,
3569 ) -> Result<OperationInfo> {
3570 let mut op_info = OperationInfo {
3571 operation_id: operation_id.to_string(),
3572 method: method.to_uppercase(),
3573 path: path.to_string(),
3574 summary: operation.summary.clone(),
3575 description: operation.description.clone(),
3576 request_body: None,
3577 response_schemas: BTreeMap::new(),
3578 parameters: Vec::new(),
3579 supports_streaming: false, stream_parameter: None, };
3582
3583 if let Some(request_body) = &operation.request_body
3585 && let Some((content_type, maybe_schema)) = request_body.best_content()
3586 {
3587 use crate::openapi::{is_form_urlencoded_media_type, is_json_media_type};
3588 op_info.request_body = if is_json_media_type(content_type) {
3589 maybe_schema
3590 .map(|s| {
3591 self.resolve_or_inline_schema(s, operation_id, "Request")
3592 .map(|name| RequestBodyContent::Json { schema_name: name })
3593 })
3594 .transpose()?
3595 } else if is_form_urlencoded_media_type(content_type) {
3596 maybe_schema
3597 .map(|s| {
3598 self.resolve_or_inline_schema(s, operation_id, "Request")
3599 .map(|name| RequestBodyContent::FormUrlEncoded { schema_name: name })
3600 })
3601 .transpose()?
3602 } else {
3603 match content_type {
3604 "multipart/form-data" => Some(RequestBodyContent::Multipart),
3605 "application/octet-stream" => Some(RequestBodyContent::OctetStream),
3606 "text/plain" => Some(RequestBodyContent::TextPlain),
3607 _ => None,
3608 }
3609 };
3610 }
3611
3612 if let Some(responses) = &operation.responses {
3614 for (status_code, response) in responses {
3615 if let Some(schema) = response.json_schema() {
3616 if let Some(schema_ref) = schema.reference() {
3617 if let Some(schema_name) = self.extract_schema_name(schema_ref) {
3619 op_info
3620 .response_schemas
3621 .insert(status_code.clone(), schema_name.to_string());
3622 }
3623 } else {
3624 let synthetic_name =
3626 self.generate_inline_response_type_name(operation_id, status_code);
3627
3628 let mut deps = HashSet::new();
3630 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
3631
3632 op_info
3633 .response_schemas
3634 .insert(status_code.clone(), synthetic_name);
3635 }
3636 }
3637 }
3638 }
3639
3640 if let Some(parameters) = &operation.parameters {
3642 for param in parameters {
3643 let resolved = self.resolve_parameter(param);
3644 if let Some(param_info) = self.analyze_parameter(&resolved, operation_id)? {
3645 op_info.parameters.push(param_info);
3646 }
3647 }
3648 }
3649
3650 if let Some(path_params) = path_item_parameters {
3652 let existing_keys: std::collections::HashSet<(String, String)> = op_info
3653 .parameters
3654 .iter()
3655 .map(|p| (p.name.clone(), p.location.clone()))
3656 .collect();
3657 for param in path_params {
3658 let resolved = self.resolve_parameter(param);
3659 if let Some(param_info) = self.analyze_parameter(&resolved, operation_id)? {
3660 if !existing_keys
3661 .contains(&(param_info.name.clone(), param_info.location.clone()))
3662 {
3663 op_info.parameters.push(param_info);
3664 }
3665 }
3666 }
3667 }
3668
3669 Ok(op_info)
3670 }
3671
3672 fn generate_inline_response_type_name(&self, operation_id: &str, status_code: &str) -> String {
3679 use heck::ToPascalCase;
3680 let base_name = operation_id.replace('.', "_").to_pascal_case();
3681 let suffix = Self::status_code_suffix(status_code);
3682 format!("{}Response{}", base_name, suffix)
3683 }
3684
3685 fn status_code_suffix(status_code: &str) -> String {
3692 match status_code {
3693 "" | "200" => String::new(),
3694 "default" | "Default" => "Default".to_string(),
3695 other if other.chars().all(|c| c.is_ascii_digit()) => other.to_string(),
3696 other => other.to_ascii_lowercase(),
3697 }
3698 }
3699
3700 fn generate_inline_request_type_name(&self, operation_id: &str) -> String {
3702 use heck::ToPascalCase;
3703 let base_name = operation_id.replace('.', "_").to_pascal_case();
3707 format!("{}Request", base_name)
3708 }
3709
3710 fn resolve_or_inline_schema(
3713 &mut self,
3714 schema: &crate::openapi::Schema,
3715 operation_id: &str,
3716 suffix: &str,
3717 ) -> Result<String> {
3718 if let Some(schema_ref) = schema.reference()
3719 && let Some(schema_name) = self.extract_schema_name(schema_ref)
3720 {
3721 return Ok(schema_name.to_string());
3722 }
3723 let synthetic_name = if suffix == "Request" {
3725 self.generate_inline_request_type_name(operation_id)
3726 } else {
3727 self.generate_inline_response_type_name(operation_id, "")
3728 };
3729 let mut deps = HashSet::new();
3730 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
3731 Ok(synthetic_name)
3732 }
3733
3734 fn resolve_parameter<'a>(
3737 &'a self,
3738 param: &'a crate::openapi::Parameter,
3739 ) -> std::borrow::Cow<'a, crate::openapi::Parameter> {
3740 if let Some(ref_str) = param.extra.get("$ref").and_then(|v| v.as_str()) {
3741 if let Some(param_name) = ref_str.strip_prefix("#/components/parameters/") {
3742 if let Some(resolved) = self.component_parameters.get(param_name) {
3743 return std::borrow::Cow::Borrowed(resolved);
3744 }
3745 }
3746 }
3747 std::borrow::Cow::Borrowed(param)
3748 }
3749
3750 fn analyze_parameter(
3757 &self,
3758 param: &crate::openapi::Parameter,
3759 operation_id: &str,
3760 ) -> Result<Option<ParameterInfo>> {
3761 use heck::ToPascalCase;
3762
3763 let name = param.name.as_deref().unwrap_or("");
3764 let location = param.location.as_deref().unwrap_or("");
3765 let required = param.required.unwrap_or(false);
3766
3767 let mut rust_type = "String".to_string();
3768 let mut schema_ref = None;
3769 let mut enum_values: Option<Vec<String>> = None;
3770
3771 if let Some(schema) = ¶m.schema {
3772 if let Some(ref_str) = schema.reference() {
3773 schema_ref = self.extract_schema_name(ref_str).map(|s| s.to_string());
3774 } else if let Some(schema_type) = schema.schema_type() {
3775 rust_type = match schema_type {
3776 crate::openapi::SchemaType::Boolean => "bool",
3777 crate::openapi::SchemaType::Integer => "i64",
3778 crate::openapi::SchemaType::Number => "f64",
3779 crate::openapi::SchemaType::String => "String",
3780 _ => "String",
3781 }
3782 .to_string();
3783
3784 if matches!(schema_type, crate::openapi::SchemaType::String) {
3785 let details = schema.details();
3786 if details.is_string_enum() {
3787 if let Some(values) = details.string_enum_values() {
3788 if !values.is_empty() {
3789 let op_pascal = operation_id.replace('.', "_").to_pascal_case();
3790 let param_pascal = name.to_pascal_case();
3791 rust_type = format!("{op_pascal}{param_pascal}");
3792 enum_values = Some(values);
3793 }
3794 }
3795 }
3796 }
3797 }
3798 }
3799
3800 Ok(Some(ParameterInfo {
3801 name: name.to_string(),
3802 location: location.to_string(),
3803 required,
3804 schema_ref,
3805 rust_type,
3806 description: param.description.clone(),
3807 enum_values,
3808 }))
3809 }
3810}