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}
161
162impl Default for DependencyGraph {
163 fn default() -> Self {
164 Self::new()
165 }
166}
167
168impl DependencyGraph {
169 pub fn new() -> Self {
170 Self {
171 edges: BTreeMap::new(),
172 recursive_schemas: HashSet::new(),
173 }
174 }
175
176 pub fn add_dependency(&mut self, from: String, to: String) {
177 self.edges.entry(from).or_default().insert(to);
178 }
179
180 pub fn topological_sort(&mut self) -> Result<Vec<String>> {
182 self.detect_recursive_schemas();
184
185 let mut temp_edges = self.edges.clone();
187 for (schema, deps) in &mut temp_edges {
188 deps.remove(schema); }
190
191 let mut visited = HashSet::new();
192 let mut temp_visited = HashSet::new();
193 let mut result = Vec::new();
194
195 let mut all_nodes: Vec<_> = temp_edges.keys().collect();
197 all_nodes.sort();
198 for node in all_nodes {
199 if !visited.contains(node) {
200 self.visit_node_recursive(
201 node,
202 &temp_edges,
203 &mut visited,
204 &mut temp_visited,
205 &mut result,
206 )?;
207 }
208 }
209
210 result.reverse();
211 Ok(result)
212 }
213
214 fn detect_recursive_schemas(&mut self) {
215 for (schema, deps) in &self.edges {
216 if deps.contains(schema) {
217 self.recursive_schemas.insert(schema.clone());
219 } else {
220 if self.has_cycle_from(schema, schema, &mut HashSet::new()) {
222 self.recursive_schemas.insert(schema.clone());
223 }
224 }
225 }
226
227 for (schema, deps) in &self.edges {
229 for dep in deps {
230 if let Some(dep_deps) = self.edges.get(dep) {
231 if dep_deps.contains(schema) {
232 self.recursive_schemas.insert(schema.clone());
234 self.recursive_schemas.insert(dep.clone());
235 }
236 }
237 }
238 }
239 }
240
241 fn has_cycle_from(&self, start: &str, current: &str, visited: &mut HashSet<String>) -> bool {
242 if visited.contains(current) {
243 return false; }
245
246 visited.insert(current.to_string());
247
248 if let Some(deps) = self.edges.get(current) {
249 for dep in deps {
250 if dep == start {
251 return true; }
253 if self.has_cycle_from(start, dep, visited) {
254 return true;
255 }
256 }
257 }
258
259 false
260 }
261
262 #[allow(clippy::only_used_in_recursion)]
263 fn visit_node_recursive(
264 &self,
265 node: &str,
266 temp_edges: &BTreeMap<String, HashSet<String>>,
267 visited: &mut HashSet<String>,
268 temp_visited: &mut HashSet<String>,
269 result: &mut Vec<String>,
270 ) -> Result<()> {
271 if temp_visited.contains(node) {
272 return Ok(());
274 }
275
276 if visited.contains(node) {
277 return Ok(());
278 }
279
280 temp_visited.insert(node.to_string());
281
282 if let Some(dependencies) = temp_edges.get(node) {
283 let mut sorted_deps: Vec<_> = dependencies.iter().collect();
285 sorted_deps.sort();
286 for dep in sorted_deps {
287 self.visit_node_recursive(dep, temp_edges, visited, temp_visited, result)?;
288 }
289 }
290
291 temp_visited.remove(node);
292 visited.insert(node.to_string());
293 result.push(node.to_string());
294
295 Ok(())
296 }
297}
298
299pub fn merge_schema_extensions(
302 main_spec: Value,
303 extension_paths: &[impl AsRef<Path>],
304) -> Result<Value> {
305 let mut result = main_spec;
306
307 for path in extension_paths {
308 let extension = load_extension_file(path.as_ref())?;
309 result = merge_json_objects_with_replacements(result, extension)?;
310 }
311
312 Ok(result)
313}
314
315fn load_extension_file(path: &Path) -> Result<Value> {
317 let content = std::fs::read_to_string(path).map_err(|e| GeneratorError::FileError {
318 message: format!("Failed to read file {}: {}", path.display(), e),
319 })?;
320
321 serde_json::from_str(&content).map_err(GeneratorError::ParseError)
322}
323
324fn merge_json_objects_with_replacements(main: Value, extension: Value) -> Result<Value> {
326 let replacements = extract_replacement_rules(&extension);
328
329 Ok(merge_json_objects_with_rules(
331 main,
332 extension,
333 &replacements,
334 ))
335}
336
337fn extract_replacement_rules(
339 extension: &Value,
340) -> std::collections::HashMap<String, (String, String)> {
341 let mut rules = std::collections::HashMap::new();
342
343 if let Some(x_replacements) = extension.get("x-replacements") {
344 if let Some(x_replacements_obj) = x_replacements.as_object() {
345 for (schema_name, replacement_rule) in x_replacements_obj {
346 if let Some(rule_obj) = replacement_rule.as_object() {
347 if let (Some(replace), Some(with)) = (
348 rule_obj.get("replace").and_then(|v| v.as_str()),
349 rule_obj.get("with").and_then(|v| v.as_str()),
350 ) {
351 rules.insert(schema_name.clone(), (replace.to_string(), with.to_string()));
352 }
354 }
355 }
356 }
357 }
358
359 rules
360}
361
362fn should_replace_variant(
364 schema_name: &str,
365 extension_refs: &[String],
366 replacements: &std::collections::HashMap<String, (String, String)>,
367) -> bool {
368 for (replace_schema, with_schema) in replacements.values() {
370 if schema_name == replace_schema {
371 let replacement_exists = extension_refs.iter().any(|ext_ref| {
373 let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
374 ext_schema_name == with_schema
375 });
376
377 if replacement_exists {
378 return true;
379 }
380 }
381 }
382
383 extension_refs.iter().any(|ext_ref| {
385 let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
386 schema_name == ext_schema_name
387 })
388}
389
390fn merge_json_objects_with_rules(
395 main: Value,
396 extension: Value,
397 replacements: &std::collections::HashMap<String, (String, String)>,
398) -> Value {
399 match (main, extension) {
400 (Value::Object(mut main_obj), Value::Object(ext_obj)) => {
402 let main_union_keyword = if main_obj.contains_key("oneOf") {
405 Some("oneOf")
406 } else if main_obj.contains_key("anyOf") {
407 Some("anyOf")
408 } else {
409 None
410 };
411 if let (Some(main_variants), Some(ext_variants)) = (
412 extract_schema_variants(&Value::Object(main_obj.clone())),
413 extract_schema_variants(&Value::Object(ext_obj.clone())),
414 ) {
415 let union_key = main_union_keyword.unwrap_or("oneOf");
416 println!(
417 "🔍 Merging union schemas ({union_key}): {} main variants, {} extension variants",
418 main_variants.len(),
419 ext_variants.len()
420 );
421 let mut merged_variants = Vec::new();
424 let extension_refs: Vec<String> = ext_variants
425 .iter()
426 .filter_map(|v| v.get("$ref").and_then(|r| r.as_str()))
427 .map(|s| s.to_string())
428 .collect();
429
430 for main_variant in main_variants {
432 if let Some(main_ref) = main_variant.get("$ref").and_then(|r| r.as_str()) {
433 let schema_name = main_ref.split('/').next_back().unwrap_or("");
435 let should_replace =
436 should_replace_variant(schema_name, &extension_refs, replacements);
437
438 if should_replace {
439 println!("🔄 REPLACING {} (explicit rule)", schema_name);
440 }
441
442 if !should_replace {
443 merged_variants.push(main_variant);
444 }
445 } else {
446 merged_variants.push(main_variant);
448 }
449 }
450
451 for ext_variant in ext_variants {
453 merged_variants.push(ext_variant);
454 }
455
456 main_obj.remove("oneOf");
458 main_obj.remove("anyOf");
459 main_obj.insert(union_key.to_string(), Value::Array(merged_variants));
460
461 for (key, ext_value) in ext_obj {
463 if key != "oneOf" && key != "anyOf" {
464 match main_obj.get(&key) {
465 Some(main_value) => {
466 let merged_value = merge_json_objects_with_rules(
467 main_value.clone(),
468 ext_value,
469 replacements,
470 );
471 main_obj.insert(key, merged_value);
472 }
473 None => {
474 main_obj.insert(key, ext_value);
475 }
476 }
477 }
478 }
479
480 return Value::Object(main_obj);
481 }
482
483 for (key, ext_value) in ext_obj {
485 match main_obj.get(&key) {
486 Some(main_value) => {
487 let merged_value = merge_json_objects_with_rules(
489 main_value.clone(),
490 ext_value,
491 replacements,
492 );
493 main_obj.insert(key, merged_value);
494 }
495 None => {
496 main_obj.insert(key, ext_value);
498 }
499 }
500 }
501 Value::Object(main_obj)
502 }
503
504 (Value::Array(mut main_arr), Value::Array(ext_arr)) => {
506 main_arr.extend(ext_arr);
507 Value::Array(main_arr)
508 }
509
510 (_, extension) => extension,
512 }
513}
514
515fn extract_schema_variants(obj: &Value) -> Option<Vec<Value>> {
517 if let Value::Object(map) = obj {
518 if let Some(Value::Array(variants)) = map.get("oneOf") {
519 return Some(variants.clone());
520 }
521 if let Some(Value::Array(variants)) = map.get("anyOf") {
522 return Some(variants.clone());
523 }
524 }
525 None
526}
527
528pub struct SchemaAnalyzer {
529 schemas: BTreeMap<String, Schema>,
530 resolved_cache: BTreeMap<String, AnalyzedSchema>,
531 openapi_spec: Value,
532 current_schema_name: Option<String>,
533 component_parameters: BTreeMap<String, crate::openapi::Parameter>,
534}
535
536impl SchemaAnalyzer {
537 pub fn new(openapi_spec: Value) -> Result<Self> {
538 let spec: OpenApiSpec =
539 serde_json::from_value(openapi_spec.clone()).map_err(GeneratorError::ParseError)?;
540 let schemas = Self::extract_schemas(&spec)?;
541
542 let component_parameters = spec
543 .components
544 .as_ref()
545 .and_then(|c| c.parameters.as_ref())
546 .cloned()
547 .unwrap_or_default();
548
549 Ok(Self {
550 schemas,
551 resolved_cache: BTreeMap::new(),
552 openapi_spec,
553 current_schema_name: None,
554 component_parameters,
555 })
556 }
557
558 pub fn new_with_extensions(
560 openapi_spec: Value,
561 extension_paths: &[std::path::PathBuf],
562 ) -> Result<Self> {
563 let merged_spec = merge_schema_extensions(openapi_spec, extension_paths)?;
564 Self::new(merged_spec)
565 }
566
567 fn generate_context_aware_name(
570 &self,
571 base_context: &str,
572 type_hint: &str,
573 index: usize,
574 schema: Option<&Schema>,
575 ) -> String {
576 if let Some(schema) = schema {
578 if type_hint == "Array"
580 && matches!(schema.schema_type(), Some(OpenApiSchemaType::Array))
581 {
582 if let Some(items_schema) = &schema.details().items {
583 if let Some(item_type) = items_schema.schema_type() {
585 match item_type {
586 OpenApiSchemaType::Object => {
587 return format!("{base_context}ItemArray");
588 }
589 OpenApiSchemaType::String => {
590 return format!("{base_context}StringArray");
591 }
592 _ => {}
593 }
594 }
595 }
596 }
597 }
598
599 match type_hint {
601 "Array" => {
602 format!("{base_context}Array")
604 }
605 "Variant" | "InlineVariant" => {
606 if index == 0 {
608 format!("{base_context}{type_hint}")
609 } else {
610 format!("{}{}{}", base_context, type_hint, index + 1)
611 }
612 }
613 _ => {
614 format!("{base_context}{type_hint}{index}")
616 }
617 }
618 }
619
620 fn to_pascal_case(&self, s: &str) -> String {
622 s.split(['_', '-'])
623 .filter(|part| !part.is_empty())
624 .map(|part| {
625 let mut chars = part.chars();
626 match chars.next() {
627 None => String::new(),
628 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
629 }
630 })
631 .collect()
632 }
633
634 fn extract_schemas(spec: &OpenApiSpec) -> Result<BTreeMap<String, Schema>> {
635 let schemas = spec
636 .components
637 .as_ref()
638 .and_then(|c| c.schemas.as_ref())
639 .ok_or_else(|| {
640 GeneratorError::InvalidSchema("No schemas found in OpenAPI spec".to_string())
641 })?;
642
643 Ok(schemas
645 .iter()
646 .map(|(k, v)| (k.clone(), v.clone()))
647 .collect())
648 }
649
650 pub fn analyze(&mut self) -> Result<SchemaAnalysis> {
651 let mut analysis = SchemaAnalysis {
652 schemas: BTreeMap::new(),
653 dependencies: DependencyGraph::new(),
654 patterns: DetectedPatterns {
655 tagged_enum_schemas: HashSet::new(),
656 untagged_enum_schemas: HashSet::new(),
657 type_mappings: BTreeMap::new(),
658 },
659 operations: BTreeMap::new(),
660 };
661
662 self.detect_patterns(&mut analysis.patterns)?;
664
665 let schema_names: Vec<String> = self.schemas.keys().cloned().collect();
667 for schema_name in schema_names {
668 let analyzed = self.analyze_schema(&schema_name)?;
669
670 for dep in &analyzed.dependencies {
672 analysis
673 .dependencies
674 .add_dependency(schema_name.clone(), dep.clone());
675 }
676
677 analysis.schemas.insert(schema_name, analyzed);
678 }
679
680 for (inline_name, inline_schema) in &self.resolved_cache {
683 if !analysis.schemas.contains_key(inline_name) {
684 analysis
686 .schemas
687 .insert(inline_name.clone(), inline_schema.clone());
688
689 for dep in &inline_schema.dependencies {
691 analysis
692 .dependencies
693 .add_dependency(inline_name.clone(), dep.clone());
694 }
695
696 let mut schemas_to_update = Vec::new();
701 for (schema_name, schema) in &analysis.schemas {
702 if schema_name == inline_name {
704 continue;
705 }
706
707 if schema.dependencies.contains(inline_name) {
708 schemas_to_update.push(schema_name.clone());
710 }
711 }
712
713 for schema_name in schemas_to_update {
715 analysis
716 .dependencies
717 .add_dependency(schema_name, inline_name.clone());
718 }
719 }
720 }
721
722 self.analyze_operations(&mut analysis)?;
724
725 for (inline_name, inline_schema) in &self.resolved_cache {
728 if !analysis.schemas.contains_key(inline_name) {
729 analysis
730 .schemas
731 .insert(inline_name.clone(), inline_schema.clone());
732
733 for dep in &inline_schema.dependencies {
735 analysis
736 .dependencies
737 .add_dependency(inline_name.clone(), dep.clone());
738 }
739 }
740 }
741
742 Ok(analysis)
743 }
744
745 fn detect_patterns(&self, patterns: &mut DetectedPatterns) -> Result<()> {
746 for (schema_name, schema) in &self.schemas {
747 if self.is_discriminated_union(schema) {
749 patterns.tagged_enum_schemas.insert(schema_name.clone());
750
751 if let Some(mappings) = self.extract_type_mappings(schema)? {
753 patterns.type_mappings.insert(schema_name.clone(), mappings);
754 }
755 }
756 else if self.is_simple_union(schema) {
758 patterns.untagged_enum_schemas.insert(schema_name.clone());
759 }
760 }
761
762 Ok(())
763 }
764
765 fn is_discriminated_union(&self, schema: &Schema) -> bool {
766 if schema.is_discriminated_union() {
768 return true;
769 }
770
771 if let Some(variants) = schema.union_variants() {
773 return variants.len() > 2 && self.detect_discriminator_field(variants).is_some();
774 }
775
776 false
777 }
778
779 fn all_variants_have_const_field(&self, variants: &[Schema], field_name: &str) -> bool {
780 variants.iter().all(|variant| {
781 if let Some(ref_str) = variant.reference() {
782 if let Some(schema_name) = self.extract_schema_name(ref_str) {
784 if let Some(schema) = self.schemas.get(schema_name) {
785 return self.has_const_discriminator_field(schema, field_name);
786 }
787 }
788 } else {
789 return self.has_const_discriminator_field(variant, field_name);
791 }
792 false
793 })
794 }
795
796 fn detect_discriminator_field(&self, variants: &[Schema]) -> Option<String> {
800 if variants.is_empty() {
801 return None;
802 }
803
804 let first_variant = &variants[0];
806 let first_schema = if let Some(ref_str) = first_variant.reference() {
807 let schema_name = self.extract_schema_name(ref_str)?;
808 self.schemas.get(schema_name)?
809 } else {
810 first_variant
811 };
812
813 let properties = first_schema.details().properties.as_ref()?;
814 let mut candidates: Vec<String> = Vec::new();
815
816 for (field_name, field_schema) in properties {
817 let details = field_schema.details();
818 let is_const = details.const_value.is_some()
819 || details.enum_values.as_ref().is_some_and(|v| v.len() == 1)
820 || details.extra.contains_key("const");
821 if is_const {
822 candidates.push(field_name.clone());
823 }
824 }
825
826 if candidates.is_empty() {
827 return None;
828 }
829
830 candidates.sort_by(|a, b| {
832 if a == "type" {
833 std::cmp::Ordering::Less
834 } else if b == "type" {
835 std::cmp::Ordering::Greater
836 } else {
837 a.cmp(b)
838 }
839 });
840
841 for candidate in &candidates {
843 if self.all_variants_have_const_field(variants, candidate) {
844 return Some(candidate.clone());
845 }
846 }
847
848 None
849 }
850
851 fn has_const_discriminator_field(&self, schema: &Schema, field_name: &str) -> bool {
852 if let Some(properties) = &schema.details().properties {
853 if let Some(field) = properties.get(field_name) {
854 if field.details().const_value.is_some() {
856 return true;
857 }
858 if let Some(enum_vals) = &field.details().enum_values {
860 return enum_vals.len() == 1;
861 }
862 return field.details().extra.contains_key("const");
864 }
865 }
866 false
867 }
868
869 fn is_simple_union(&self, schema: &Schema) -> bool {
870 if let Some(variants) = schema.union_variants() {
871 if variants.len() > 1 && !schema.is_nullable_pattern() {
873 let has_refs = variants.iter().any(|v| v.is_reference());
874 return has_refs;
875 }
876 }
877 false
878 }
879
880 fn extract_type_mappings(&self, schema: &Schema) -> Result<Option<BTreeMap<String, String>>> {
881 let variants = schema.union_variants().ok_or_else(|| {
882 GeneratorError::InvalidSchema("No variants found for discriminated union".to_string())
883 })?;
884
885 let discriminator_field = if let Some(discriminator) = schema.discriminator() {
887 discriminator.property_name.clone()
888 } else if let Some(detected) = self.detect_discriminator_field(variants) {
889 detected
890 } else {
891 "type".to_string() };
893
894 let mut mappings = BTreeMap::new();
895
896 for variant in variants {
897 if let Some(ref_str) = variant.reference() {
898 if let Some(type_name) = self.extract_schema_name(ref_str) {
899 if let Some(variant_schema) = self.schemas.get(type_name) {
900 if let Some(discriminator_value) = self
901 .extract_discriminator_value_for_field(
902 variant_schema,
903 &discriminator_field,
904 )
905 {
906 mappings.insert(type_name.to_string(), discriminator_value);
907 }
908 }
909 }
910 }
911 }
912
913 if mappings.is_empty() {
914 Ok(None)
915 } else {
916 Ok(Some(mappings))
917 }
918 }
919
920 #[allow(dead_code)]
921 fn extract_discriminator_value(&self, schema: &Schema) -> Option<String> {
922 self.extract_discriminator_value_for_field(schema, "type")
923 }
924
925 fn extract_discriminator_value_for_field(
926 &self,
927 schema: &Schema,
928 field_name: &str,
929 ) -> Option<String> {
930 if let Some(properties) = &schema.details().properties {
931 if let Some(type_field) = properties.get(field_name) {
932 if let Some(const_value) = &type_field.details().const_value {
934 if let Some(value) = const_value.as_str() {
935 return Some(value.to_string());
936 }
937 }
938 if let Some(enum_values) = &type_field.details().enum_values {
940 if enum_values.len() == 1 {
941 return enum_values[0].as_str().map(|s| s.to_string());
942 }
943 }
944 if let Some(const_value) = type_field.details().extra.get("const") {
946 return const_value.as_str().map(|s| s.to_string());
947 }
948 if let Some(stainless_const) = type_field.details().extra.get("x-stainless-const") {
950 if stainless_const.as_bool() == Some(true) {
951 if let Some(default_value) = &type_field.details().default {
952 if let Some(value) = default_value.as_str() {
953 return Some(value.to_string());
954 }
955 }
956 }
957 }
958 }
959 }
960 None
961 }
962
963 fn get_any_reference<'a>(&self, schema: &'a Schema) -> Option<&'a str> {
964 schema.reference().or_else(|| schema.recursive_reference())
965 }
966
967 fn extract_schema_name<'a>(&self, ref_str: &'a str) -> Option<&'a str> {
968 if ref_str == "#" {
969 return None; }
971
972 let parts: Vec<&str> = ref_str.split('/').collect();
973
974 if parts.len() >= 4 && parts[0] == "#" && parts[2] == "schemas" {
977 return Some(parts[3]);
978 }
979
980 let last = parts.last()?;
983 if last.is_empty() || last.chars().all(|c| c.is_ascii_digit()) {
984 None
985 } else {
986 Some(last)
987 }
988 }
989
990 fn analyze_schema(&mut self, schema_name: &str) -> Result<AnalyzedSchema> {
991 if let Some(cached) = self.resolved_cache.get(schema_name) {
993 return Ok(cached.clone());
994 }
995
996 self.current_schema_name = Some(schema_name.to_string());
998
999 let schema = self
1000 .schemas
1001 .get(schema_name)
1002 .ok_or_else(|| GeneratorError::UnresolvedReference(schema_name.to_string()))?
1003 .clone();
1004
1005 self.resolved_cache.insert(
1007 schema_name.to_string(),
1008 AnalyzedSchema {
1009 name: schema_name.to_string(),
1010 original: serde_json::to_value(&schema).unwrap_or(Value::Null),
1011 schema_type: SchemaType::Reference {
1012 target: "placeholder".to_string(),
1013 },
1014 dependencies: HashSet::new(),
1015 nullable: false,
1016 description: None,
1017 default: None,
1018 },
1019 );
1020
1021 let analyzed = self.analyze_schema_value(&schema, schema_name)?;
1022
1023 self.resolved_cache
1025 .insert(schema_name.to_string(), analyzed.clone());
1026
1027 Ok(analyzed)
1028 }
1029
1030 fn analyze_schema_value(
1031 &mut self,
1032 schema: &Schema,
1033 schema_name: &str,
1034 ) -> Result<AnalyzedSchema> {
1035 let details = schema.details();
1036 let description = details.description.clone();
1037 let nullable = details.is_nullable();
1038 let mut dependencies = HashSet::new();
1039
1040 let schema_type = match schema {
1041 Schema::Reference { reference, .. } => {
1042 let target = self
1043 .extract_schema_name(reference)
1044 .ok_or_else(|| GeneratorError::UnresolvedReference(reference.to_string()))?
1045 .to_string();
1046 dependencies.insert(target.clone());
1047 SchemaType::Reference { target }
1048 }
1049 Schema::RecursiveRef { recursive_ref, .. } => {
1050 if recursive_ref == "#" {
1052 dependencies.insert(schema_name.to_string());
1054 SchemaType::Reference {
1055 target: schema_name.to_string(),
1056 }
1057 } else {
1058 let target = self
1060 .extract_schema_name(recursive_ref)
1061 .unwrap_or(schema_name)
1062 .to_string();
1063 dependencies.insert(target.clone());
1064 SchemaType::Reference { target }
1065 }
1066 }
1067 Schema::Typed { schema_type, .. } => {
1068 match schema_type {
1069 OpenApiSchemaType::String => {
1070 if let Some(values) = details.string_enum_values() {
1071 SchemaType::StringEnum { values }
1072 } else {
1073 SchemaType::Primitive {
1074 rust_type: "String".to_string(),
1075 }
1076 }
1077 }
1078 OpenApiSchemaType::Integer => {
1079 let rust_type =
1080 self.get_number_rust_type(OpenApiSchemaType::Integer, details);
1081 SchemaType::Primitive { rust_type }
1082 }
1083 OpenApiSchemaType::Number => {
1084 let rust_type =
1085 self.get_number_rust_type(OpenApiSchemaType::Number, details);
1086 SchemaType::Primitive { rust_type }
1087 }
1088 OpenApiSchemaType::Boolean => SchemaType::Primitive {
1089 rust_type: "bool".to_string(),
1090 },
1091 OpenApiSchemaType::Array => {
1092 self.analyze_array_schema(schema, schema_name, &mut dependencies)?
1094 }
1095 OpenApiSchemaType::Object => {
1096 if self.should_use_dynamic_json(schema) {
1098 SchemaType::Primitive {
1099 rust_type: "serde_json::Value".to_string(),
1100 }
1101 } else {
1102 self.analyze_object_schema(schema, &mut dependencies)?
1104 }
1105 }
1106 _ => SchemaType::Primitive {
1107 rust_type: "serde_json::Value".to_string(),
1108 },
1109 }
1110 }
1111 Schema::AnyOf {
1112 any_of,
1113 discriminator,
1114 ..
1115 } => {
1116 self.analyze_anyof_union(
1118 any_of,
1119 discriminator.as_ref(),
1120 &mut dependencies,
1121 schema_name,
1122 )?
1123 }
1124 Schema::OneOf {
1125 one_of,
1126 discriminator,
1127 ..
1128 } => {
1129 self.analyze_oneof_union(
1131 one_of,
1132 discriminator.as_ref(),
1133 schema_name,
1134 &mut dependencies,
1135 )?
1136 }
1137 Schema::AllOf { all_of, .. } => {
1138 self.analyze_allof_composition(all_of, &mut dependencies)?
1140 }
1141 Schema::Untyped { .. } => {
1142 if let Some(inferred) = schema.inferred_type() {
1144 match inferred {
1145 OpenApiSchemaType::Object => {
1146 if self.should_use_dynamic_json(schema) {
1147 SchemaType::Primitive {
1148 rust_type: "serde_json::Value".to_string(),
1149 }
1150 } else {
1151 self.analyze_object_schema(schema, &mut dependencies)?
1152 }
1153 }
1154 OpenApiSchemaType::String if details.is_string_enum() => {
1155 SchemaType::StringEnum {
1156 values: details.string_enum_values().unwrap_or_default(),
1157 }
1158 }
1159 _ => SchemaType::Primitive {
1160 rust_type: "serde_json::Value".to_string(),
1161 },
1162 }
1163 } else {
1164 SchemaType::Primitive {
1165 rust_type: "serde_json::Value".to_string(),
1166 }
1167 }
1168 }
1169 };
1170
1171 Ok(AnalyzedSchema {
1172 name: schema_name.to_string(),
1173 original: serde_json::to_value(schema).unwrap_or(Value::Null), schema_type,
1175 dependencies,
1176 nullable,
1177 description,
1178 default: details.default.clone(),
1179 })
1180 }
1181
1182 fn analyze_object_schema(
1183 &mut self,
1184 schema: &Schema,
1185 dependencies: &mut HashSet<String>,
1186 ) -> Result<SchemaType> {
1187 let details = schema.details();
1188 let properties = &details.properties;
1189 let required = details
1190 .required
1191 .as_ref()
1192 .map(|req| req.iter().cloned().collect::<HashSet<String>>())
1193 .unwrap_or_default();
1194
1195 let mut property_info = BTreeMap::new();
1196
1197 if let Some(props) = properties {
1198 for (prop_name, prop_schema) in props {
1199 let prop_type = if let Schema::AnyOf { any_of, .. } = prop_schema {
1201 if self.should_use_dynamic_json(prop_schema) {
1203 SchemaType::Primitive {
1205 rust_type: "serde_json::Value".to_string(),
1206 }
1207 } else {
1208 let context_name = self
1211 .current_schema_name
1212 .clone()
1213 .unwrap_or_else(|| "Unknown".to_string());
1214
1215 let prop_pascal = self.to_pascal_case(prop_name);
1217 let union_type_name = format!("{context_name}{prop_pascal}");
1218
1219 let union_schema_type = self.analyze_anyof_union(
1221 any_of,
1222 prop_schema.discriminator(),
1223 dependencies,
1224 &union_type_name,
1225 )?;
1226
1227 self.resolved_cache.insert(
1229 union_type_name.clone(),
1230 AnalyzedSchema {
1231 name: union_type_name.clone(),
1232 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
1233 schema_type: union_schema_type,
1234 dependencies: HashSet::new(),
1235 nullable: false,
1236 description: prop_schema.details().description.clone(),
1237 default: None,
1238 },
1239 );
1240
1241 dependencies.insert(union_type_name.clone());
1243 SchemaType::Reference {
1244 target: union_type_name,
1245 }
1246 }
1247 } else if let Schema::OneOf {
1248 one_of,
1249 discriminator,
1250 ..
1251 } = prop_schema
1252 {
1253 let context_name = self
1256 .current_schema_name
1257 .clone()
1258 .unwrap_or_else(|| "Unknown".to_string());
1259 let prop_pascal = self.to_pascal_case(prop_name);
1260 let union_type_name = format!("{context_name}{prop_pascal}");
1261
1262 let union_schema_type = self.analyze_oneof_union(
1264 one_of,
1265 discriminator.as_ref(),
1266 &union_type_name,
1267 dependencies,
1268 )?;
1269
1270 self.resolved_cache.insert(
1272 union_type_name.clone(),
1273 AnalyzedSchema {
1274 name: union_type_name.clone(),
1275 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
1276 schema_type: union_schema_type,
1277 dependencies: HashSet::new(),
1278 nullable: false,
1279 description: prop_schema.details().description.clone(),
1280 default: None,
1281 },
1282 );
1283
1284 dependencies.insert(union_type_name.clone());
1286 SchemaType::Reference {
1287 target: union_type_name,
1288 }
1289 } else {
1290 self.analyze_property_schema_with_context(
1292 prop_schema,
1293 Some(prop_name),
1294 dependencies,
1295 )?
1296 };
1297
1298 let prop_details = prop_schema.details();
1299 let prop_nullable = prop_details.is_nullable() || prop_schema.is_nullable_pattern();
1301 let prop_description = prop_details.description.clone();
1302 let prop_default = prop_details.default.clone();
1303
1304 property_info.insert(
1305 prop_name.clone(),
1306 PropertyInfo {
1307 schema_type: prop_type,
1308 nullable: prop_nullable,
1309 description: prop_description,
1310 default: prop_default,
1311 serde_attrs: Vec::new(),
1312 },
1313 );
1314 }
1315 }
1316
1317 let additional_properties = match &details.additional_properties {
1319 Some(crate::openapi::AdditionalProperties::Boolean(true)) => true,
1320 Some(crate::openapi::AdditionalProperties::Boolean(false)) => false,
1321 Some(crate::openapi::AdditionalProperties::Schema(_)) => {
1322 true
1325 }
1326 None => false, };
1328
1329 Ok(SchemaType::Object {
1330 properties: property_info,
1331 required,
1332 additional_properties,
1333 })
1334 }
1335
1336 fn analyze_property_schema_with_context(
1337 &mut self,
1338 schema: &Schema,
1339 property_name: Option<&str>,
1340 dependencies: &mut HashSet<String>,
1341 ) -> Result<SchemaType> {
1342 if let Some(ref_str) = self.get_any_reference(schema) {
1343 let target = if ref_str == "#" {
1344 self.find_recursive_anchor_schema()
1346 .unwrap_or_else(|| "UnknownRecursive".to_string())
1347 } else {
1348 self.extract_schema_name(ref_str)
1349 .ok_or_else(|| GeneratorError::UnresolvedReference(ref_str.to_string()))?
1350 .to_string()
1351 };
1352 dependencies.insert(target.clone());
1353 return Ok(SchemaType::Reference { target });
1354 }
1355
1356 if let Some(schema_type) = schema.schema_type() {
1357 match schema_type {
1358 OpenApiSchemaType::String => {
1359 if let Some(enum_values) = schema.details().string_enum_values() {
1361 let context_name = self
1364 .current_schema_name
1365 .clone()
1366 .unwrap_or_else(|| "Unknown".to_string());
1367
1368 let primary_name = if let Some(prop_name) = property_name {
1370 let prop_pascal = self.to_pascal_case(prop_name);
1372 format!("{context_name}{prop_pascal}")
1373 } else {
1374 let suffix = if !enum_values.is_empty() {
1377 let first_value = self.to_pascal_case(&enum_values[0]);
1378 format!("{first_value}Enum")
1379 } else {
1380 "StringEnum".to_string()
1381 };
1382 format!("{context_name}{suffix}")
1383 };
1384
1385 fn matches_values(existing: &AnalyzedSchema, values: &[String]) -> bool {
1405 matches!(
1406 &existing.schema_type,
1407 SchemaType::StringEnum { values: existing_values }
1408 if existing_values == values
1409 )
1410 }
1411
1412 let mut enum_type_name = primary_name.clone();
1413 let mut should_insert = match self.resolved_cache.get(&enum_type_name) {
1414 None => true,
1415 Some(existing) if matches_values(existing, &enum_values) => false,
1416 Some(_) => {
1417 let suffix = enum_values
1420 .first()
1421 .map(|v| self.to_pascal_case(v))
1422 .unwrap_or_else(|| "Variant".to_string());
1423 let candidate = format!("{primary_name}{suffix}");
1424
1425 let resolved = match self.resolved_cache.get(&candidate) {
1426 None => Some((candidate.clone(), true)),
1427 Some(existing) if matches_values(existing, &enum_values) => {
1428 Some((candidate.clone(), false))
1429 }
1430 Some(_) => {
1431 let mut found = None;
1434 for n in 2..1000 {
1435 let numbered = format!("{candidate}_{n}");
1436 match self.resolved_cache.get(&numbered) {
1437 None => {
1438 found = Some((numbered, true));
1439 break;
1440 }
1441 Some(existing)
1442 if matches_values(existing, &enum_values) =>
1443 {
1444 found = Some((numbered, false));
1445 break;
1446 }
1447 Some(_) => continue,
1448 }
1449 }
1450 found
1451 }
1452 };
1453
1454 let (resolved_name, insert) = resolved.unwrap_or((candidate, true));
1455 enum_type_name = resolved_name;
1456 insert
1457 }
1458 };
1459
1460 if should_insert {
1463 self.resolved_cache.insert(
1464 enum_type_name.clone(),
1465 AnalyzedSchema {
1466 name: enum_type_name.clone(),
1467 original: serde_json::to_value(schema).unwrap_or(Value::Null),
1468 schema_type: SchemaType::StringEnum {
1469 values: enum_values,
1470 },
1471 dependencies: HashSet::new(),
1472 nullable: false,
1473 description: schema.details().description.clone(),
1474 default: schema.details().default.clone(),
1475 },
1476 );
1477 let _ = &mut should_insert;
1480 }
1481
1482 dependencies.insert(enum_type_name.clone());
1484 return Ok(SchemaType::Reference {
1485 target: enum_type_name,
1486 });
1487 } else {
1488 return Ok(SchemaType::Primitive {
1489 rust_type: "String".to_string(),
1490 });
1491 }
1492 }
1493 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
1494 let details = schema.details();
1495 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
1496 return Ok(SchemaType::Primitive { rust_type });
1497 }
1498 OpenApiSchemaType::Boolean => {
1499 return Ok(SchemaType::Primitive {
1500 rust_type: "bool".to_string(),
1501 });
1502 }
1503 OpenApiSchemaType::Array => {
1504 let context_name = if let Some(prop_name) = property_name {
1506 let prop_pascal = self.to_pascal_case(prop_name);
1508 format!(
1509 "{}{}",
1510 self.current_schema_name.as_deref().unwrap_or("Unknown"),
1511 prop_pascal
1512 )
1513 } else {
1514 "ArrayItem".to_string()
1516 };
1517 return self.analyze_array_schema(schema, &context_name, dependencies);
1518 }
1519 OpenApiSchemaType::Object => {
1520 if self.should_use_dynamic_json(schema) {
1522 return Ok(SchemaType::Primitive {
1523 rust_type: "serde_json::Value".to_string(),
1524 });
1525 }
1526 let object_type_name = if let Some(prop_name) = property_name {
1528 let prop_pascal = self.to_pascal_case(prop_name);
1530 format!(
1531 "{}{}",
1532 self.current_schema_name.as_deref().unwrap_or("Unknown"),
1533 prop_pascal
1534 )
1535 } else {
1536 format!(
1538 "{}Object",
1539 self.current_schema_name.as_deref().unwrap_or("Unknown")
1540 )
1541 };
1542
1543 let object_type = self.analyze_object_schema(schema, dependencies)?;
1545
1546 let inline_schema = AnalyzedSchema {
1548 name: object_type_name.clone(),
1549 original: serde_json::to_value(schema).unwrap_or(Value::Null),
1550 schema_type: object_type,
1551 dependencies: dependencies.clone(),
1552 nullable: false,
1553 description: schema.details().description.clone(),
1554 default: None,
1555 };
1556
1557 self.resolved_cache
1559 .insert(object_type_name.clone(), inline_schema);
1560 dependencies.insert(object_type_name.clone());
1561
1562 return Ok(SchemaType::Reference {
1564 target: object_type_name,
1565 });
1566 }
1567 _ => {
1568 return Ok(SchemaType::Primitive {
1569 rust_type: "serde_json::Value".to_string(),
1570 });
1571 }
1572 }
1573 }
1574
1575 if schema.is_nullable_pattern() {
1577 if let Some(non_null) = schema.non_null_variant() {
1578 return self.analyze_property_schema_with_context(
1579 non_null,
1580 property_name,
1581 dependencies,
1582 );
1583 }
1584 }
1585
1586 if self.should_use_dynamic_json(schema) {
1588 return Ok(SchemaType::Primitive {
1589 rust_type: "serde_json::Value".to_string(),
1590 });
1591 }
1592
1593 if let Schema::AllOf { all_of, .. } = schema {
1595 return self.analyze_allof_composition(all_of, dependencies);
1596 }
1597
1598 if let Some(variants) = schema.union_variants() {
1600 match variants.len().cmp(&1) {
1601 std::cmp::Ordering::Equal => {
1602 return self.analyze_property_schema_with_context(
1604 &variants[0],
1605 property_name,
1606 dependencies,
1607 );
1608 }
1609 std::cmp::Ordering::Greater => {
1610 let union_name = if let Some(prop_name) = property_name {
1613 let prop_pascal = self.to_pascal_case(prop_name);
1615 format!(
1616 "{}{}",
1617 self.current_schema_name.as_deref().unwrap_or(""),
1618 prop_pascal
1619 )
1620 } else {
1621 "UnionType".to_string()
1622 };
1623
1624 if let Schema::OneOf {
1626 one_of,
1627 discriminator,
1628 ..
1629 } = schema
1630 {
1631 let oneof_result = self.analyze_oneof_union(
1633 one_of,
1634 discriminator.as_ref(),
1635 &union_name,
1636 dependencies,
1637 )?;
1638
1639 if let SchemaType::Union {
1641 variants: _union_variants,
1642 } = &oneof_result
1643 {
1644 self.resolved_cache.insert(
1646 union_name.clone(),
1647 AnalyzedSchema {
1648 name: union_name.clone(),
1649 original: serde_json::to_value(schema).unwrap_or(Value::Null),
1650 schema_type: oneof_result.clone(),
1651 dependencies: dependencies.clone(),
1652 nullable: false,
1653 description: schema.details().description.clone(),
1654 default: None,
1655 },
1656 );
1657
1658 dependencies.insert(union_name.clone());
1660 return Ok(SchemaType::Reference { target: union_name });
1661 }
1662
1663 return Ok(oneof_result);
1664 } else if let Schema::AnyOf {
1665 any_of,
1666 discriminator,
1667 ..
1668 } = schema
1669 {
1670 let union_analysis = self.analyze_anyof_union(
1672 any_of,
1673 discriminator.as_ref(),
1674 dependencies,
1675 &union_name,
1676 )?;
1677 return Ok(union_analysis);
1678 } else {
1679 let mut union_variants = Vec::new();
1682 for variant in variants {
1683 if let Some(ref_str) = variant.reference() {
1684 if let Some(target) = self.extract_schema_name(ref_str) {
1685 dependencies.insert(target.to_string());
1686 union_variants.push(SchemaRef {
1687 target: target.to_string(),
1688 nullable: false,
1689 });
1690 }
1691 }
1692 }
1693 return Ok(SchemaType::Union {
1694 variants: union_variants,
1695 });
1696 }
1697 }
1698 std::cmp::Ordering::Less => {}
1699 }
1700 }
1701
1702 if let Some(inferred_type) = schema.inferred_type() {
1704 match inferred_type {
1705 OpenApiSchemaType::Object => {
1706 if self.should_use_dynamic_json(schema) {
1708 return Ok(SchemaType::Primitive {
1709 rust_type: "serde_json::Value".to_string(),
1710 });
1711 }
1712 return self.analyze_object_schema(schema, dependencies);
1713 }
1714 OpenApiSchemaType::Array => {
1715 let context_name = if let Some(prop_name) = property_name {
1716 let prop_pascal = self.to_pascal_case(prop_name);
1718 format!(
1719 "{}{}",
1720 self.current_schema_name.as_deref().unwrap_or("Unknown"),
1721 prop_pascal
1722 )
1723 } else {
1724 "ArrayItem".to_string()
1726 };
1727 return self.analyze_array_schema(schema, &context_name, dependencies);
1728 }
1729 OpenApiSchemaType::String => {
1730 if let Some(enum_values) = schema.details().string_enum_values() {
1731 return Ok(SchemaType::StringEnum {
1732 values: enum_values,
1733 });
1734 } else {
1735 return Ok(SchemaType::Primitive {
1736 rust_type: "String".to_string(),
1737 });
1738 }
1739 }
1740 _ => {
1741 let rust_type = self.openapi_type_to_rust_type(inferred_type, schema.details());
1743 return Ok(SchemaType::Primitive { rust_type });
1744 }
1745 }
1746 }
1747
1748 Ok(SchemaType::Primitive {
1749 rust_type: "serde_json::Value".to_string(),
1750 })
1751 }
1752
1753 fn analyze_allof_composition(
1754 &mut self,
1755 all_of_schemas: &[Schema],
1756 dependencies: &mut HashSet<String>,
1757 ) -> Result<SchemaType> {
1758 if all_of_schemas.len() == 1 {
1761 if let Schema::Reference { reference, .. } = &all_of_schemas[0] {
1762 if let Some(target) = self.extract_schema_name(reference) {
1763 dependencies.insert(target.to_string());
1764 return Ok(SchemaType::Reference {
1765 target: target.to_string(),
1766 });
1767 }
1768 }
1769 }
1770
1771 let mut merged_properties = BTreeMap::new();
1773 let mut merged_required = HashSet::new();
1774 let mut descriptions = Vec::new();
1775
1776 let current_context = self.current_schema_name.clone();
1778
1779 for schema in all_of_schemas {
1780 match schema {
1781 Schema::Reference { reference, .. } => {
1782 if let Some(target) = self.extract_schema_name(reference) {
1784 dependencies.insert(target.to_string());
1785
1786 let analyzed_ref = self.analyze_schema(target)?;
1788
1789 match &analyzed_ref.schema_type {
1791 SchemaType::Object {
1792 properties,
1793 required,
1794 ..
1795 } => {
1796 for (prop_name, prop_info) in properties {
1798 merged_properties.insert(prop_name.clone(), prop_info.clone());
1799 }
1800 for req in required {
1802 merged_required.insert(req.clone());
1803 }
1804 }
1805 _ => {
1806 if let Some(ref_schema) = self.schemas.get(target).cloned() {
1808 self.merge_schema_into_properties(
1809 &ref_schema,
1810 &mut merged_properties,
1811 &mut merged_required,
1812 dependencies,
1813 )?;
1814 }
1815 }
1816 }
1817 }
1818 }
1819 Schema::Typed {
1820 schema_type: OpenApiSchemaType::Object,
1821 ..
1822 }
1823 | Schema::Untyped { .. } => {
1824 let saved_context = self.current_schema_name.clone();
1826 self.current_schema_name = current_context.clone();
1827
1828 self.merge_schema_into_properties(
1830 schema,
1831 &mut merged_properties,
1832 &mut merged_required,
1833 dependencies,
1834 )?;
1835
1836 self.current_schema_name = saved_context;
1838 }
1839 _ => {
1840 self.merge_schema_into_properties(
1843 schema,
1844 &mut merged_properties,
1845 &mut merged_required,
1846 dependencies,
1847 )?;
1848 }
1849 }
1850
1851 if let Some(desc) = &schema.details().description {
1853 descriptions.push(desc.clone());
1854 }
1855 }
1856
1857 if !merged_properties.is_empty() {
1859 Ok(SchemaType::Object {
1860 properties: merged_properties,
1861 required: merged_required,
1862 additional_properties: false,
1863 })
1864 } else {
1865 Ok(SchemaType::Composition {
1867 schemas: all_of_schemas
1868 .iter()
1869 .filter_map(|s| {
1870 if let Some(ref_str) = s.reference() {
1871 if let Some(target) = self.extract_schema_name(ref_str) {
1872 dependencies.insert(target.to_string());
1873 Some(SchemaRef {
1874 target: target.to_string(),
1875 nullable: false,
1876 })
1877 } else {
1878 None
1879 }
1880 } else {
1881 None
1882 }
1883 })
1884 .collect(),
1885 })
1886 }
1887 }
1888
1889 fn merge_schema_into_properties(
1890 &mut self,
1891 schema: &Schema,
1892 merged_properties: &mut BTreeMap<String, PropertyInfo>,
1893 merged_required: &mut HashSet<String>,
1894 dependencies: &mut HashSet<String>,
1895 ) -> Result<()> {
1896 let details = schema.details();
1897
1898 if let Some(properties) = &details.properties {
1900 for (prop_name, prop_schema) in properties {
1901 let prop_type = self.analyze_property_schema_with_context(
1902 prop_schema,
1903 Some(prop_name),
1904 dependencies,
1905 )?;
1906 let prop_details = prop_schema.details();
1907
1908 merged_properties.insert(
1909 prop_name.clone(),
1910 PropertyInfo {
1911 schema_type: prop_type,
1912 nullable: prop_details.is_nullable(),
1913 description: prop_details.description.clone(),
1914 default: prop_details.default.clone(),
1915 serde_attrs: Vec::new(),
1916 },
1917 );
1918 }
1919 }
1920
1921 if let Some(required) = &details.required {
1923 for field in required {
1924 merged_required.insert(field.clone());
1925 }
1926 }
1927
1928 Ok(())
1929 }
1930
1931 fn analyze_oneof_union(
1932 &mut self,
1933 one_of_schemas: &[Schema],
1934 discriminator: Option<&crate::openapi::Discriminator>,
1935 parent_name: &str,
1936 dependencies: &mut HashSet<String>,
1937 ) -> Result<SchemaType> {
1938 if one_of_schemas.len() == 2 {
1941 let null_count = one_of_schemas
1942 .iter()
1943 .filter(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
1944 .count();
1945 if null_count == 1 {
1946 if let Some(non_null) = one_of_schemas
1947 .iter()
1948 .find(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
1949 {
1950 return self
1951 .analyze_schema_value(non_null, parent_name)
1952 .map(|a| a.schema_type);
1953 }
1954 }
1955 }
1956
1957 if discriminator.is_none() {
1959 return self.analyze_untagged_oneof_union(one_of_schemas, parent_name, dependencies);
1961 }
1962
1963 let discriminator_field = discriminator
1965 .ok_or_else(|| {
1966 GeneratorError::InvalidDiscriminator(
1967 "expected discriminator after guard check".to_string(),
1968 )
1969 })?
1970 .property_name
1971 .clone();
1972
1973 let mut variants = Vec::new();
1974 let mut used_variant_names = std::collections::HashSet::new();
1975
1976 for variant_schema in one_of_schemas {
1977 let ref_info = if let Some(ref_str) = variant_schema.reference() {
1979 Some((ref_str, false))
1980 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
1981 Some((recursive_ref, true))
1982 } else if let Schema::AllOf { all_of, .. } = variant_schema {
1983 if all_of.len() == 1 {
1985 if let Some(ref_str) = all_of[0].reference() {
1986 Some((ref_str, false))
1987 } else {
1988 all_of[0]
1989 .recursive_reference()
1990 .map(|recursive_ref| (recursive_ref, true))
1991 }
1992 } else {
1993 None
1994 }
1995 } else {
1996 None
1997 };
1998
1999 if let Some((ref_str, is_recursive)) = ref_info {
2000 let schema_name = if is_recursive && ref_str == "#" {
2001 self.find_recursive_anchor_schema()
2003 .or_else(|| self.current_schema_name.clone())
2004 .unwrap_or_else(|| "CompoundFilter".to_string())
2005 } else {
2006 self.extract_schema_name(ref_str)
2007 .map(|s| s.to_string())
2008 .unwrap_or_else(|| "UnknownRef".to_string())
2009 };
2010
2011 if !schema_name.is_empty() {
2012 dependencies.insert(schema_name.clone());
2013
2014 let discriminator_value = if let Some(disc) = discriminator {
2019 if let Some(mappings) = &disc.mapping {
2020 mappings
2023 .iter()
2024 .find(|(_, target_ref)| {
2025 target_ref.as_str() == ref_str
2027 || self
2028 .extract_schema_name(target_ref)
2029 .map(|s| s.to_string())
2030 == Some(schema_name.clone())
2031 })
2032 .map(|(key, _)| key.clone())
2033 .unwrap_or_else(|| {
2034 self.fallback_discriminator_value_for_field(
2035 &schema_name,
2036 &discriminator_field,
2037 )
2038 })
2039 } else {
2040 self.fallback_discriminator_value_for_field(
2041 &schema_name,
2042 &discriminator_field,
2043 )
2044 }
2045 } else {
2046 self.fallback_discriminator_value_for_field(
2047 &schema_name,
2048 &discriminator_field,
2049 )
2050 };
2051
2052 let base_name = self.to_rust_variant_name(&schema_name);
2054 let rust_name =
2055 self.ensure_unique_variant_name(base_name, &mut used_variant_names);
2056
2057 let final_discriminator_value = discriminator_value;
2059
2060 variants.push(UnionVariant {
2061 rust_name,
2062 type_name: schema_name,
2063 discriminator_value: final_discriminator_value,
2064 schema_ref: ref_str.to_string(),
2065 });
2066 }
2067 } else {
2068 let variant_index = variants.len();
2070 let inline_type_name =
2071 self.generate_inline_type_name(variant_schema, variant_index);
2072
2073 let discriminator_value = if let Some(disc) = discriminator {
2075 if let Some(mappings) = &disc.mapping {
2076 mappings
2078 .iter()
2079 .find(|(_, target_ref)| {
2080 target_ref.contains(&format!("variant_{variant_index}"))
2081 })
2082 .map(|(key, _)| key.clone())
2083 .unwrap_or_else(|| {
2084 self.extract_inline_discriminator_value(
2085 variant_schema,
2086 &discriminator_field,
2087 variant_index,
2088 )
2089 })
2090 } 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
2105 let base_name = if discriminator_value.starts_with("variant_") {
2107 format!("Variant{variant_index}")
2108 } else {
2109 let clean_name = self.discriminator_to_variant_name(&discriminator_value);
2111 self.to_rust_variant_name(&clean_name)
2112 };
2113 let rust_name = self.ensure_unique_variant_name(base_name, &mut used_variant_names);
2114
2115 let final_discriminator_value = discriminator_value;
2117
2118 variants.push(UnionVariant {
2119 rust_name,
2120 type_name: inline_type_name.clone(),
2121 discriminator_value: final_discriminator_value,
2122 schema_ref: format!("inline_{variant_index}"),
2123 });
2124
2125 self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
2127 }
2128 }
2129
2130 if variants.is_empty() {
2131 let mut union_variants = Vec::new();
2134
2135 for (variant_index, variant_schema) in one_of_schemas.iter().enumerate() {
2136 if let Some(ref_str) = variant_schema.reference() {
2138 if let Some(schema_name) = self.extract_schema_name(ref_str) {
2139 dependencies.insert(schema_name.to_string());
2140 union_variants.push(SchemaRef {
2141 target: schema_name.to_string(),
2142 nullable: false,
2143 });
2144 }
2145 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2146 let schema_name = if recursive_ref == "#" {
2147 self.find_recursive_anchor_schema()
2149 .or_else(|| self.current_schema_name.clone())
2150 .unwrap_or_else(|| "CompoundFilter".to_string())
2151 } else {
2152 self.extract_schema_name(recursive_ref)
2153 .map(|s| s.to_string())
2154 .unwrap_or_else(|| "RecursiveType".to_string())
2155 };
2156 dependencies.insert(schema_name.clone());
2157 union_variants.push(SchemaRef {
2158 target: schema_name,
2159 nullable: false,
2160 });
2161 } else {
2162 let inline_name = self.generate_context_aware_name(
2164 parent_name,
2165 "InlineVariant",
2166 variant_index,
2167 Some(variant_schema),
2168 );
2169 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
2170 let variant_type = analyzed.schema_type;
2171
2172 for dep in &analyzed.dependencies {
2174 dependencies.insert(dep.clone());
2175 }
2176
2177 match &variant_type {
2178 SchemaType::Primitive { rust_type } => {
2180 union_variants.push(SchemaRef {
2181 target: rust_type.clone(),
2182 nullable: false,
2183 });
2184 }
2185 SchemaType::Array { item_type } => {
2187 match item_type.as_ref() {
2188 SchemaType::Primitive { rust_type } => {
2189 let type_name = format!("Vec<{rust_type}>");
2190 union_variants.push(SchemaRef {
2191 target: type_name,
2192 nullable: false,
2193 });
2194 }
2195 SchemaType::Reference { target } => {
2196 let type_name = format!("Vec<{target}>");
2197 union_variants.push(SchemaRef {
2198 target: type_name,
2199 nullable: false,
2200 });
2201 }
2202 _ => {
2203 let inline_type_name = self.generate_context_aware_name(
2205 parent_name,
2206 "Variant",
2207 variant_index,
2208 None,
2209 );
2210 self.add_inline_schema(
2211 &inline_type_name,
2212 variant_schema,
2213 dependencies,
2214 )?;
2215 union_variants.push(SchemaRef {
2216 target: inline_type_name,
2217 nullable: false,
2218 });
2219 }
2220 }
2221 }
2222 SchemaType::Reference { target } => {
2224 union_variants.push(SchemaRef {
2225 target: target.clone(),
2226 nullable: false,
2227 });
2228 }
2229 _ => {
2231 let inline_type_name =
2232 format!("{}Variant{}", parent_name, variant_index + 1);
2233 self.add_inline_schema(
2234 &inline_type_name,
2235 variant_schema,
2236 dependencies,
2237 )?;
2238 union_variants.push(SchemaRef {
2239 target: inline_type_name,
2240 nullable: false,
2241 });
2242 }
2243 }
2244 }
2245 }
2246
2247 if !union_variants.is_empty() {
2248 return Ok(SchemaType::Union {
2249 variants: union_variants,
2250 });
2251 }
2252
2253 return Ok(SchemaType::Primitive {
2255 rust_type: "serde_json::Value".to_string(),
2256 });
2257 }
2258
2259 Ok(SchemaType::DiscriminatedUnion {
2260 discriminator_field,
2261 variants,
2262 })
2263 }
2264
2265 fn analyze_untagged_oneof_union(
2266 &mut self,
2267 one_of_schemas: &[Schema],
2268 parent_name: &str,
2269 dependencies: &mut HashSet<String>,
2270 ) -> Result<SchemaType> {
2271 let filtered: Vec<&Schema> = one_of_schemas
2275 .iter()
2276 .filter(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
2277 .collect();
2278
2279 if filtered.len() == 1 {
2281 return self
2282 .analyze_schema_value(filtered[0], parent_name)
2283 .map(|a| a.schema_type);
2284 }
2285
2286 let mut union_variants = Vec::new();
2287
2288 for (variant_index, variant_schema) in filtered.iter().copied().enumerate() {
2289 if let Some(ref_str) = variant_schema.reference() {
2291 if let Some(schema_name) = self.extract_schema_name(ref_str) {
2292 dependencies.insert(schema_name.to_string());
2293 union_variants.push(SchemaRef {
2294 target: schema_name.to_string(),
2295 nullable: false,
2296 });
2297 }
2298 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2299 let schema_name = if recursive_ref == "#" {
2300 self.find_recursive_anchor_schema()
2302 .or_else(|| self.current_schema_name.clone())
2303 .unwrap_or_else(|| "CompoundFilter".to_string())
2304 } else {
2305 self.extract_schema_name(recursive_ref)
2306 .map(|s| s.to_string())
2307 .unwrap_or_else(|| "RecursiveType".to_string())
2308 };
2309 dependencies.insert(schema_name.clone());
2310 union_variants.push(SchemaRef {
2311 target: schema_name,
2312 nullable: false,
2313 });
2314 } else {
2315 let inline_name = self.generate_context_aware_name(
2317 parent_name,
2318 "InlineVariant",
2319 variant_index,
2320 Some(variant_schema),
2321 );
2322 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
2323 let variant_type = analyzed.schema_type;
2324
2325 for dep in &analyzed.dependencies {
2327 dependencies.insert(dep.clone());
2328 }
2329
2330 match &variant_type {
2331 SchemaType::Primitive { rust_type } => {
2333 union_variants.push(SchemaRef {
2334 target: rust_type.clone(),
2335 nullable: false,
2336 });
2337 }
2338 SchemaType::Array { item_type } => {
2340 match item_type.as_ref() {
2341 SchemaType::Primitive { rust_type } => {
2342 let type_name = format!("Vec<{rust_type}>");
2343 union_variants.push(SchemaRef {
2344 target: type_name,
2345 nullable: false,
2346 });
2347 }
2348 SchemaType::Reference { target } => {
2349 let type_name = format!("Vec<{target}>");
2350 union_variants.push(SchemaRef {
2351 target: type_name,
2352 nullable: false,
2353 });
2354 }
2355 SchemaType::Array {
2357 item_type: inner_item_type,
2358 } => {
2359 match inner_item_type.as_ref() {
2360 SchemaType::Primitive { rust_type } => {
2361 let type_name = format!("Vec<Vec<{rust_type}>>");
2362 union_variants.push(SchemaRef {
2363 target: type_name,
2364 nullable: false,
2365 });
2366 }
2367 SchemaType::Reference { target } => {
2368 let type_name = format!("Vec<Vec<{target}>>");
2369 union_variants.push(SchemaRef {
2370 target: type_name,
2371 nullable: false,
2372 });
2373 }
2374 _ => {
2375 let inline_type_name = self.generate_context_aware_name(
2377 parent_name,
2378 "Variant",
2379 variant_index,
2380 None,
2381 );
2382 self.add_inline_schema(
2383 &inline_type_name,
2384 variant_schema,
2385 dependencies,
2386 )?;
2387 union_variants.push(SchemaRef {
2388 target: inline_type_name,
2389 nullable: false,
2390 });
2391 }
2392 }
2393 }
2394 _ => {
2395 let inline_type_name = self.generate_context_aware_name(
2397 parent_name,
2398 "Variant",
2399 variant_index,
2400 None,
2401 );
2402 self.add_inline_schema(
2403 &inline_type_name,
2404 variant_schema,
2405 dependencies,
2406 )?;
2407 union_variants.push(SchemaRef {
2408 target: inline_type_name,
2409 nullable: false,
2410 });
2411 }
2412 }
2413 }
2414 SchemaType::Reference { target } => {
2416 union_variants.push(SchemaRef {
2417 target: target.clone(),
2418 nullable: false,
2419 });
2420 }
2421 _ => {
2423 let inline_type_name = self.generate_context_aware_name(
2424 parent_name,
2425 "Variant",
2426 variant_index,
2427 None,
2428 );
2429 self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
2430 union_variants.push(SchemaRef {
2431 target: inline_type_name,
2432 nullable: false,
2433 });
2434 }
2435 }
2436 }
2437 }
2438
2439 if !union_variants.is_empty() {
2440 return Ok(SchemaType::Union {
2441 variants: union_variants,
2442 });
2443 }
2444
2445 Ok(SchemaType::Primitive {
2447 rust_type: "serde_json::Value".to_string(),
2448 })
2449 }
2450
2451 fn add_inline_schema(
2452 &mut self,
2453 type_name: &str,
2454 schema: &Schema,
2455 dependencies: &mut HashSet<String>,
2456 ) -> Result<()> {
2457 if let Some(schema_type) = schema.schema_type() {
2459 match schema_type {
2460 OpenApiSchemaType::String
2461 | OpenApiSchemaType::Integer
2462 | OpenApiSchemaType::Number
2463 | OpenApiSchemaType::Boolean => {
2464 let rust_type =
2465 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
2466
2467 self.resolved_cache.insert(
2469 type_name.to_string(),
2470 AnalyzedSchema {
2471 name: type_name.to_string(),
2472 original: serde_json::to_value(schema).unwrap_or(Value::Null),
2473 schema_type: SchemaType::Primitive { rust_type },
2474 dependencies: HashSet::new(),
2475 nullable: false,
2476 description: schema.details().description.clone(),
2477 default: None,
2478 },
2479 );
2480 return Ok(());
2481 }
2482 _ => {}
2483 }
2484 }
2485
2486 let previous_schema_name = self.current_schema_name.take();
2490 self.current_schema_name = Some(type_name.to_string());
2491 let analyzed = self.analyze_schema_value(schema, type_name)?;
2492 self.current_schema_name = previous_schema_name;
2493
2494 self.resolved_cache.insert(type_name.to_string(), analyzed);
2496
2497 if let Some(cached) = self.resolved_cache.get(type_name) {
2499 for dep in &cached.dependencies {
2500 dependencies.insert(dep.clone());
2501 }
2502 }
2503
2504 Ok(())
2505 }
2506
2507 fn extract_inline_discriminator_value(
2508 &self,
2509 schema: &Schema,
2510 discriminator_field: &str,
2511 variant_index: usize,
2512 ) -> String {
2513 if let Some(properties) = &schema.details().properties {
2515 if let Some(discriminator_prop) = properties.get(discriminator_field) {
2516 if let Some(enum_values) = &discriminator_prop.details().enum_values {
2518 if enum_values.len() == 1 {
2519 if let Some(value) = enum_values[0].as_str() {
2520 return value.to_string();
2521 }
2522 }
2523 }
2524 if let Some(const_value) = discriminator_prop.details().extra.get("const") {
2526 if let Some(value) = const_value.as_str() {
2527 return value.to_string();
2528 }
2529 }
2530 if let Some(const_value) = &discriminator_prop.details().const_value {
2532 if let Some(value) = const_value.as_str() {
2533 return value.to_string();
2534 }
2535 }
2536 }
2537 }
2538
2539 if let Some(inferred_name) = self.infer_variant_name_from_structure(schema, variant_index) {
2541 return inferred_name;
2542 }
2543
2544 format!("variant_{variant_index}")
2546 }
2547
2548 fn infer_variant_name_from_structure(
2549 &self,
2550 schema: &Schema,
2551 _variant_index: usize,
2552 ) -> Option<String> {
2553 let details = schema.details();
2554
2555 if let Some(properties) = &details.properties {
2557 if properties.contains_key("text") && properties.len() <= 3 {
2559 return Some("text".to_string());
2560 }
2561 if properties.contains_key("image") || properties.contains_key("source") {
2562 return Some("image".to_string());
2563 }
2564 if properties.contains_key("document") {
2565 return Some("document".to_string());
2566 }
2567 if properties.contains_key("tool_use_id") || properties.contains_key("tool_result") {
2568 return Some("tool_result".to_string());
2569 }
2570 if properties.contains_key("content") && properties.contains_key("is_error") {
2571 return Some("tool_result".to_string());
2572 }
2573 if properties.contains_key("partial_json") {
2574 return Some("partial_json".to_string());
2575 }
2576
2577 let property_names: Vec<&String> = properties.keys().collect();
2579
2580 for prop_name in &property_names {
2582 if prop_name.contains("result") {
2583 return Some("result".to_string());
2584 }
2585 if prop_name.contains("error") {
2586 return Some("error".to_string());
2587 }
2588 if prop_name.contains("content") && property_names.len() <= 2 {
2589 return Some("content".to_string());
2590 }
2591 }
2592
2593 let significant_props = property_names
2595 .iter()
2596 .filter(|&name| !["type", "id", "cache_control"].contains(&name.as_str()))
2597 .collect::<Vec<_>>();
2598
2599 if significant_props.len() == 1 {
2600 return Some((*significant_props[0]).clone());
2601 }
2602 }
2603
2604 if let Some(description) = &details.description {
2606 let desc_lower = description.to_lowercase();
2607 if desc_lower.contains("text") && desc_lower.len() < 100 {
2608 return Some("text".to_string());
2609 }
2610 if desc_lower.contains("image") {
2611 return Some("image".to_string());
2612 }
2613 if desc_lower.contains("document") {
2614 return Some("document".to_string());
2615 }
2616 if desc_lower.contains("tool") && desc_lower.contains("result") {
2617 return Some("tool_result".to_string());
2618 }
2619 }
2620
2621 None
2622 }
2623
2624 fn discriminator_to_variant_name(&self, discriminator: &str) -> String {
2625 if discriminator.is_empty() {
2627 return "Variant".to_string();
2628 }
2629
2630 let mut result = String::new();
2631 let mut next_upper = true;
2632
2633 for c in discriminator.chars() {
2634 match c {
2635 'a'..='z' => {
2636 if next_upper {
2637 result.push(c.to_ascii_uppercase());
2638 next_upper = false;
2639 } else {
2640 result.push(c);
2641 }
2642 }
2643 'A'..='Z' => {
2644 result.push(c);
2645 next_upper = false;
2646 }
2647 '0'..='9' => {
2648 result.push(c);
2649 next_upper = false;
2650 }
2651 '_' | '-' | '.' | ' ' | '/' | '\\' => {
2652 next_upper = true;
2654 }
2655 _ => {
2656 next_upper = true;
2658 }
2659 }
2660 }
2661
2662 if result.is_empty() || result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2664 result = format!("Variant{result}");
2665 }
2666
2667 result
2668 }
2669
2670 fn ensure_unique_variant_name(
2671 &self,
2672 base_name: String,
2673 used_names: &mut std::collections::HashSet<String>,
2674 ) -> String {
2675 let mut candidate = base_name.clone();
2676 let mut counter = 1;
2677
2678 while used_names.contains(&candidate) {
2679 counter += 1;
2680 candidate = format!("{base_name}{counter}");
2681 }
2682
2683 used_names.insert(candidate.clone());
2684 candidate
2685 }
2686
2687 fn generate_inline_type_name(&self, schema: &Schema, variant_index: usize) -> String {
2688 if let Some(meaningful_name) = self.infer_type_name_from_structure(schema) {
2690 return meaningful_name;
2691 }
2692
2693 let context = self.current_schema_name.as_deref().unwrap_or("Inline");
2695 self.generate_context_aware_name(context, "Variant", variant_index, Some(schema))
2696 }
2697
2698 fn infer_type_name_from_structure(&self, schema: &Schema) -> Option<String> {
2699 let details = schema.details();
2700
2701 if let Some(description) = &details.description {
2703 if let Some(name_from_desc) = self.extract_type_name_from_description(description) {
2704 return Some(name_from_desc);
2705 }
2706 }
2707
2708 if let Some(properties) = &details.properties {
2710 if let Some(name_from_props) = self.extract_type_name_from_properties(properties) {
2711 return Some(format!("{name_from_props}Block"));
2712 }
2713 }
2714
2715 None
2716 }
2717
2718 fn extract_type_name_from_description(&self, description: &str) -> Option<String> {
2719 if description.len() > 100 || description.contains('\n') {
2721 return None;
2722 }
2723
2724 let words: Vec<&str> = description
2726 .split_whitespace()
2727 .take(2) .filter(|word| {
2729 let w = word.to_lowercase();
2730 word.len() > 2
2731 && ![
2732 "the", "and", "for", "with", "that", "this", "are", "can", "will", "was",
2733 ]
2734 .contains(&w.as_str())
2735 })
2736 .collect();
2737
2738 if words.is_empty() {
2739 return None;
2740 }
2741
2742 let combined = words.join("_");
2744 let pascal_name = self.discriminator_to_variant_name(&combined);
2745
2746 if !pascal_name.ends_with("Content")
2748 && !pascal_name.ends_with("Block")
2749 && !pascal_name.ends_with("Type")
2750 {
2751 Some(format!("{pascal_name}Content"))
2752 } else {
2753 Some(pascal_name)
2754 }
2755 }
2756
2757 fn extract_type_name_from_properties(
2758 &self,
2759 properties: &std::collections::BTreeMap<String, crate::openapi::Schema>,
2760 ) -> Option<String> {
2761 let significant_props: Vec<&String> = properties
2763 .keys()
2764 .filter(|name| !["type", "id", "cache_control"].contains(&name.as_str()))
2765 .collect();
2766
2767 if significant_props.is_empty() {
2768 return None;
2769 }
2770
2771 if significant_props.len() == 1 {
2773 let prop_name = significant_props[0];
2774 return Some(self.discriminator_to_variant_name(prop_name));
2775 }
2776
2777 let mut sorted_props = significant_props.clone();
2780 sorted_props.sort();
2781 if let Some(first_prop) = sorted_props.first() {
2782 return Some(self.discriminator_to_variant_name(first_prop));
2783 }
2784
2785 None
2786 }
2787
2788 fn openapi_type_to_rust_type(
2789 &self,
2790 openapi_type: OpenApiSchemaType,
2791 details: &crate::openapi::SchemaDetails,
2792 ) -> String {
2793 match openapi_type {
2794 OpenApiSchemaType::String => "String".to_string(),
2795 OpenApiSchemaType::Integer => self.get_number_rust_type(openapi_type, details),
2796 OpenApiSchemaType::Number => self.get_number_rust_type(openapi_type, details),
2797 OpenApiSchemaType::Boolean => "bool".to_string(),
2798 OpenApiSchemaType::Array => "Vec<serde_json::Value>".to_string(), OpenApiSchemaType::Object => "serde_json::Value".to_string(), OpenApiSchemaType::Null => "()".to_string(), }
2802 }
2803
2804 #[allow(dead_code)]
2805 fn fallback_discriminator_value(&self, schema_name: &str) -> String {
2806 self.fallback_discriminator_value_for_field(schema_name, "type")
2807 }
2808
2809 fn fallback_discriminator_value_for_field(
2810 &self,
2811 schema_name: &str,
2812 field_name: &str,
2813 ) -> String {
2814 if let Some(ref_schema) = self.schemas.get(schema_name) {
2816 if let Some(extracted) =
2817 self.extract_discriminator_value_for_field(ref_schema, field_name)
2818 {
2819 return extracted;
2820 }
2821 }
2822
2823 self.generate_discriminator_value_from_name(schema_name)
2825 }
2826
2827 fn generate_discriminator_value_from_name(&self, schema_name: &str) -> String {
2828 let mut result = String::new();
2830 let mut chars = schema_name.chars().peekable();
2831 let mut first = true;
2832
2833 while let Some(c) = chars.next() {
2834 if c.is_uppercase()
2835 && !first
2836 && chars
2837 .peek()
2838 .map(|&next| next.is_lowercase())
2839 .unwrap_or(false)
2840 {
2841 result.push('.');
2842 }
2843 result.push(c.to_ascii_lowercase());
2844 first = false;
2845 }
2846
2847 if result.ends_with("event") {
2849 result = result[..result.len() - 5].to_string();
2850 }
2851
2852 if schema_name.starts_with("Response") && !result.starts_with("response.") {
2854 result = format!("response.{}", result.trim_start_matches("response"));
2855 }
2856
2857 result
2858 }
2859
2860 fn to_rust_variant_name(&self, schema_name: &str) -> String {
2861 let mut name = schema_name;
2863
2864 if name.starts_with("Response") && name.len() > 8 {
2866 name = &name[8..]; }
2868
2869 if name.ends_with("Event") && name.len() > 5 {
2871 name = &name[..name.len() - 5]; }
2873
2874 name = name.trim_matches('_');
2876
2877 if name.is_empty() {
2879 schema_name.to_string()
2880 } else {
2881 self.discriminator_to_variant_name(name)
2883 }
2884 }
2885
2886 fn analyze_array_schema(
2887 &mut self,
2888 schema: &Schema,
2889 parent_schema_name: &str,
2890 dependencies: &mut HashSet<String>,
2891 ) -> Result<SchemaType> {
2892 let details = schema.details();
2893
2894 if let Some(items_schema) = &details.items {
2896 let item_type = match items_schema.as_ref() {
2898 Schema::Reference { reference, .. } => {
2899 let target = self
2901 .extract_schema_name(reference)
2902 .ok_or_else(|| GeneratorError::UnresolvedReference(reference.to_string()))?
2903 .to_string();
2904 dependencies.insert(target.clone());
2905 SchemaType::Reference { target }
2906 }
2907 Schema::RecursiveRef { recursive_ref, .. } => {
2908 if recursive_ref == "#" {
2910 let target = self
2912 .find_recursive_anchor_schema()
2913 .unwrap_or_else(|| parent_schema_name.to_string());
2914 dependencies.insert(target.clone());
2915 SchemaType::Reference { target }
2916 } else {
2917 let target = self
2918 .extract_schema_name(recursive_ref)
2919 .unwrap_or("RecursiveType")
2920 .to_string();
2921 dependencies.insert(target.clone());
2922 SchemaType::Reference { target }
2923 }
2924 }
2925 Schema::Typed { schema_type, .. } => {
2926 match schema_type {
2928 OpenApiSchemaType::String => SchemaType::Primitive {
2929 rust_type: "String".to_string(),
2930 },
2931 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
2932 let details = items_schema.details();
2933 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
2934 SchemaType::Primitive { rust_type }
2935 }
2936 OpenApiSchemaType::Boolean => SchemaType::Primitive {
2937 rust_type: "bool".to_string(),
2938 },
2939 OpenApiSchemaType::Object => {
2940 let object_type_name = format!("{parent_schema_name}Item");
2942
2943 let object_type =
2945 self.analyze_object_schema(items_schema, dependencies)?;
2946
2947 let inline_schema = AnalyzedSchema {
2949 name: object_type_name.clone(),
2950 original: serde_json::to_value(items_schema).unwrap_or(Value::Null),
2951 schema_type: object_type,
2952 dependencies: dependencies.clone(),
2953 nullable: false,
2954 description: items_schema.details().description.clone(),
2955 default: None,
2956 };
2957
2958 self.resolved_cache
2960 .insert(object_type_name.clone(), inline_schema);
2961 dependencies.insert(object_type_name.clone());
2962
2963 SchemaType::Reference {
2965 target: object_type_name,
2966 }
2967 }
2968 OpenApiSchemaType::Array => {
2969 self.analyze_array_schema(
2971 items_schema,
2972 parent_schema_name,
2973 dependencies,
2974 )?
2975 }
2976 _ => SchemaType::Primitive {
2977 rust_type: "serde_json::Value".to_string(),
2978 },
2979 }
2980 }
2981 Schema::OneOf { .. } | Schema::AnyOf { .. } => {
2982 let analyzed = self.analyze_schema_value(items_schema, "ArrayItem")?;
2984
2985 match &analyzed.schema_type {
2987 SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => {
2988 let union_name = format!("{parent_schema_name}ItemUnion");
2991
2992 let mut union_schema = analyzed;
2994 union_schema.name = union_name.clone();
2995
2996 self.resolved_cache.insert(union_name.clone(), union_schema);
2998
2999 dependencies.insert(union_name.clone());
3001
3002 SchemaType::Reference { target: union_name }
3004 }
3005 _ => analyzed.schema_type,
3006 }
3007 }
3008 Schema::Untyped { .. } => {
3009 if let Some(inferred) = items_schema.inferred_type() {
3011 match inferred {
3012 OpenApiSchemaType::Object => {
3013 let object_type_name = format!("{parent_schema_name}Item");
3015
3016 let object_type =
3018 self.analyze_object_schema(items_schema, dependencies)?;
3019
3020 let inline_schema = AnalyzedSchema {
3022 name: object_type_name.clone(),
3023 original: serde_json::to_value(items_schema)
3024 .unwrap_or(Value::Null),
3025 schema_type: object_type,
3026 dependencies: dependencies.clone(),
3027 nullable: false,
3028 description: items_schema.details().description.clone(),
3029 default: None,
3030 };
3031
3032 self.resolved_cache
3034 .insert(object_type_name.clone(), inline_schema);
3035 dependencies.insert(object_type_name.clone());
3036
3037 SchemaType::Reference {
3039 target: object_type_name,
3040 }
3041 }
3042 OpenApiSchemaType::String => SchemaType::Primitive {
3043 rust_type: "String".to_string(),
3044 },
3045 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
3046 let details = items_schema.details();
3047 let rust_type = self.get_number_rust_type(inferred, details);
3048 SchemaType::Primitive { rust_type }
3049 }
3050 OpenApiSchemaType::Boolean => SchemaType::Primitive {
3051 rust_type: "bool".to_string(),
3052 },
3053 _ => SchemaType::Primitive {
3054 rust_type: "serde_json::Value".to_string(),
3055 },
3056 }
3057 } else {
3058 SchemaType::Primitive {
3059 rust_type: "serde_json::Value".to_string(),
3060 }
3061 }
3062 }
3063 _ => SchemaType::Primitive {
3064 rust_type: "serde_json::Value".to_string(),
3065 },
3066 };
3067
3068 Ok(SchemaType::Array {
3069 item_type: Box::new(item_type),
3070 })
3071 } else {
3072 Ok(SchemaType::Primitive {
3074 rust_type: "Vec<serde_json::Value>".to_string(),
3075 })
3076 }
3077 }
3078
3079 fn get_number_rust_type(
3080 &self,
3081 schema_type: OpenApiSchemaType,
3082 details: &crate::openapi::SchemaDetails,
3083 ) -> String {
3084 match schema_type {
3085 OpenApiSchemaType::Integer => {
3086 match details.format.as_deref() {
3088 Some("int32") => "i32".to_string(),
3089 Some("int64") => "i64".to_string(),
3090 _ => "i64".to_string(), }
3092 }
3093 OpenApiSchemaType::Number => {
3094 match details.format.as_deref() {
3096 Some("float") => "f32".to_string(),
3097 Some("double") => "f64".to_string(),
3098 _ => "f64".to_string(), }
3100 }
3101 _ => "serde_json::Value".to_string(), }
3103 }
3104
3105 fn analyze_anyof_union(
3106 &mut self,
3107 any_of_schemas: &[Schema],
3108 discriminator: Option<&Discriminator>,
3109 dependencies: &mut HashSet<String>,
3110 context_name: &str,
3111 ) -> Result<SchemaType> {
3112 let filtered_owned: Vec<Schema>;
3117 let any_of_schemas: &[Schema] = if any_of_schemas
3118 .iter()
3119 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
3120 {
3121 filtered_owned = any_of_schemas
3122 .iter()
3123 .filter(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
3124 .cloned()
3125 .collect();
3126 if filtered_owned.is_empty() {
3127 return Ok(SchemaType::Primitive {
3128 rust_type: "serde_json::Value".to_string(),
3129 });
3130 }
3131 if filtered_owned.len() == 1 {
3132 return self
3133 .analyze_schema_value(&filtered_owned[0], context_name)
3134 .map(|a| a.schema_type);
3135 }
3136 &filtered_owned
3137 } else {
3138 any_of_schemas
3139 };
3140
3141 let has_refs = any_of_schemas.iter().any(|s| s.is_reference());
3143 let has_objects = any_of_schemas.iter().any(|s| {
3144 matches!(s.schema_type(), Some(OpenApiSchemaType::Object))
3145 || s.inferred_type() == Some(OpenApiSchemaType::Object)
3146 });
3147 let has_arrays = any_of_schemas
3148 .iter()
3149 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Array)));
3150
3151 let all_string_like = any_of_schemas.iter().all(|s| {
3154 matches!(s.schema_type(), Some(OpenApiSchemaType::String))
3155 || s.details().const_value.is_some()
3156 });
3157
3158 if (has_refs || has_objects || has_arrays || any_of_schemas.len() > 1) && !all_string_like {
3159 if let Some(disc) = discriminator {
3161 return self.analyze_oneof_union(
3163 any_of_schemas,
3164 Some(disc),
3165 context_name,
3166 dependencies,
3167 );
3168 }
3169
3170 if let Some(disc_field) = self.detect_discriminator_field(any_of_schemas) {
3172 return self.analyze_oneof_union(
3173 any_of_schemas,
3174 Some(&Discriminator {
3175 property_name: disc_field,
3176 mapping: None,
3177 extra: BTreeMap::new(),
3178 }),
3179 context_name,
3180 dependencies,
3181 );
3182 }
3183
3184 let mut variants = Vec::new();
3186
3187 for schema in any_of_schemas {
3188 if let Some(ref_str) = schema.reference() {
3189 if let Some(target) = self.extract_schema_name(ref_str) {
3190 dependencies.insert(target.to_string());
3191 variants.push(SchemaRef {
3192 target: target.to_string(),
3193 nullable: false,
3194 });
3195 }
3196 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object))
3197 || schema.inferred_type() == Some(OpenApiSchemaType::Object)
3198 {
3199 let inline_index = variants.len();
3201 let inline_type_name = self.generate_inline_type_name(schema, inline_index);
3202
3203 self.add_inline_schema(&inline_type_name, schema, dependencies)?;
3205
3206 variants.push(SchemaRef {
3207 target: inline_type_name,
3208 nullable: false,
3209 });
3210 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Array)) {
3211 let array_type =
3213 self.analyze_array_schema(schema, context_name, dependencies)?;
3214
3215 let array_type_name = if let Some(items_schema) = &schema.details().items {
3217 if let Some(ref_str) = items_schema.reference() {
3218 if let Some(item_type_name) = self.extract_schema_name(ref_str) {
3219 dependencies.insert(item_type_name.to_string());
3220 format!("{item_type_name}Array")
3221 } else {
3222 self.generate_context_aware_name(
3223 context_name,
3224 "Array",
3225 variants.len(),
3226 Some(schema),
3227 )
3228 }
3229 } else {
3230 self.generate_context_aware_name(
3231 context_name,
3232 "Array",
3233 variants.len(),
3234 Some(schema),
3235 )
3236 }
3237 } else {
3238 self.generate_context_aware_name(
3239 context_name,
3240 "Array",
3241 variants.len(),
3242 Some(schema),
3243 )
3244 };
3245
3246 self.resolved_cache.insert(
3248 array_type_name.clone(),
3249 AnalyzedSchema {
3250 name: array_type_name.clone(),
3251 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3252 schema_type: array_type,
3253 dependencies: HashSet::new(),
3254 nullable: false,
3255 description: Some("Array variant in union".to_string()),
3256 default: None,
3257 },
3258 );
3259
3260 dependencies.insert(array_type_name.clone());
3262
3263 variants.push(SchemaRef {
3264 target: array_type_name,
3265 nullable: false,
3266 });
3267 } else if let Some(schema_type) = schema.schema_type() {
3268 let inline_index = variants.len();
3270
3271 let inline_type_name = match schema_type {
3273 OpenApiSchemaType::String => {
3274 if inline_index == 0 {
3277 format!("{context_name}String")
3278 } else {
3279 format!("{context_name}StringVariant{inline_index}")
3280 }
3281 }
3282 OpenApiSchemaType::Number => {
3283 if inline_index == 0 {
3284 format!("{context_name}Number")
3285 } else {
3286 format!("{context_name}NumberVariant{inline_index}")
3287 }
3288 }
3289 OpenApiSchemaType::Integer => {
3290 if inline_index == 0 {
3291 format!("{context_name}Integer")
3292 } else {
3293 format!("{context_name}IntegerVariant{inline_index}")
3294 }
3295 }
3296 OpenApiSchemaType::Boolean => {
3297 if inline_index == 0 {
3298 format!("{context_name}Boolean")
3299 } else {
3300 format!("{context_name}BooleanVariant{inline_index}")
3301 }
3302 }
3303 _ => format!("{context_name}Variant{inline_index}"),
3304 };
3305
3306 let rust_type =
3307 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
3308
3309 self.resolved_cache.insert(
3311 inline_type_name.clone(),
3312 AnalyzedSchema {
3313 name: inline_type_name.clone(),
3314 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3315 schema_type: SchemaType::Primitive { rust_type },
3316 dependencies: HashSet::new(),
3317 nullable: false,
3318 description: schema.details().description.clone(),
3319 default: None,
3320 },
3321 );
3322
3323 dependencies.insert(inline_type_name.clone());
3325
3326 variants.push(SchemaRef {
3327 target: inline_type_name,
3328 nullable: false,
3329 });
3330 }
3331 }
3332
3333 if !variants.is_empty() {
3334 return Ok(SchemaType::Union { variants });
3335 }
3336 }
3337
3338 let all_strings = any_of_schemas.iter().all(|schema| {
3340 matches!(schema.schema_type(), Some(OpenApiSchemaType::String))
3341 || schema.details().const_value.is_some()
3342 });
3343
3344 if all_strings {
3345 let mut enum_values = Vec::new();
3347 let mut has_open_string = false;
3348
3349 for schema in any_of_schemas {
3350 if let Some(const_val) = &schema.details().const_value {
3351 if let Some(const_str) = const_val.as_str() {
3352 enum_values.push(const_str.to_string());
3353 }
3354 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::String)) {
3355 has_open_string = true;
3356 }
3357 }
3358
3359 if !enum_values.is_empty() {
3360 if has_open_string {
3361 return Ok(SchemaType::ExtensibleEnum {
3364 known_values: enum_values,
3365 });
3366 } else {
3367 return Ok(SchemaType::StringEnum {
3369 values: enum_values,
3370 });
3371 }
3372 }
3373 }
3374
3375 Ok(SchemaType::Primitive {
3377 rust_type: "serde_json::Value".to_string(),
3378 })
3379 }
3380
3381 fn find_recursive_anchor_schema(&self) -> Option<String> {
3383 for (schema_name, schema) in &self.schemas {
3385 let details = schema.details();
3386 if details.recursive_anchor == Some(true) {
3387 return Some(schema_name.clone());
3388 }
3389 }
3390
3391 None
3395 }
3396
3397 fn should_use_dynamic_json(&self, schema: &Schema) -> bool {
3400 if let Schema::AnyOf { any_of, .. } = schema {
3402 if any_of.len() == 2 {
3403 let has_null = any_of
3404 .iter()
3405 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)));
3406 let has_empty_object = any_of.iter().any(|s| self.is_dynamic_object_pattern(s));
3407
3408 if has_null && has_empty_object {
3409 return true;
3410 }
3411 }
3412 }
3413
3414 self.is_dynamic_object_pattern(schema)
3416 }
3417
3418 fn is_dynamic_object_pattern(&self, schema: &Schema) -> bool {
3420 let is_object = match schema.schema_type() {
3422 Some(OpenApiSchemaType::Object) => true,
3423 None => schema.inferred_type() == Some(OpenApiSchemaType::Object),
3424 _ => false,
3425 };
3426
3427 if !is_object {
3428 return false;
3429 }
3430
3431 let details = schema.details();
3432
3433 if self.has_explicit_additional_properties(schema) {
3436 return false;
3437 }
3438
3439 let no_properties = details
3441 .properties
3442 .as_ref()
3443 .map(|props| props.is_empty())
3444 .unwrap_or(true);
3445
3446 if no_properties {
3447 let has_structural_constraints =
3449 details.required.as_ref()
3451 .map(|req| req.iter().any(|r| r != "type"))
3452 .unwrap_or(false)
3453 || details.extra.contains_key("patternProperties")
3455 || details.extra.contains_key("propertyNames")
3457 || details.extra.contains_key("minProperties")
3459 || details.extra.contains_key("maxProperties")
3460 || details.extra.contains_key("dependencies")
3462 || details.extra.contains_key("if")
3464 || details.extra.contains_key("then")
3465 || details.extra.contains_key("else");
3466
3467 return !has_structural_constraints;
3468 }
3469
3470 false
3471 }
3472
3473 fn has_explicit_additional_properties(&self, schema: &Schema) -> bool {
3475 let details = schema.details();
3476
3477 matches!(
3479 &details.additional_properties,
3480 Some(crate::openapi::AdditionalProperties::Boolean(true))
3481 | Some(crate::openapi::AdditionalProperties::Schema(_))
3482 )
3483 }
3484
3485 fn analyze_operations(&mut self, analysis: &mut SchemaAnalysis) -> Result<()> {
3487 let spec: crate::openapi::OpenApiSpec = serde_json::from_value(self.openapi_spec.clone())
3488 .map_err(GeneratorError::ParseError)?;
3489
3490 if let Some(paths) = &spec.paths {
3491 for (path, path_item) in paths {
3492 for (method, operation) in path_item.operations() {
3493 let operation_id = operation
3495 .operation_id
3496 .clone()
3497 .unwrap_or_else(|| Self::generate_operation_id(method, path));
3498
3499 let op_info = self.analyze_single_operation(
3500 &operation_id,
3501 method,
3502 path,
3503 operation,
3504 path_item.parameters.as_ref(),
3505 analysis,
3506 )?;
3507 analysis.operations.insert(operation_id, op_info);
3508 }
3509 }
3510 }
3511 Ok(())
3512 }
3513
3514 fn generate_operation_id(method: &str, path: &str) -> String {
3517 let mut operation_id = method.to_lowercase();
3519
3520 let path_parts: Vec<&str> = path.trim_start_matches('/').split('/').collect();
3522
3523 for part in path_parts {
3524 if part.is_empty() {
3525 continue;
3526 }
3527
3528 let cleaned_part = if part.starts_with('{') && part.ends_with('}') {
3530 &part[1..part.len() - 1]
3531 } else {
3532 part
3533 };
3534
3535 let pascal_case_part = cleaned_part
3537 .split(&['-', '_'][..])
3538 .map(|s| {
3539 let mut chars = s.chars();
3540 match chars.next() {
3541 None => String::new(),
3542 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
3543 }
3544 })
3545 .collect::<String>();
3546
3547 operation_id.push_str(&pascal_case_part);
3548 }
3549
3550 operation_id
3551 }
3552
3553 fn analyze_single_operation(
3555 &mut self,
3556 operation_id: &str,
3557 method: &str,
3558 path: &str,
3559 operation: &crate::openapi::Operation,
3560 path_item_parameters: Option<&Vec<crate::openapi::Parameter>>,
3561 _analysis: &mut SchemaAnalysis,
3562 ) -> Result<OperationInfo> {
3563 let mut op_info = OperationInfo {
3564 operation_id: operation_id.to_string(),
3565 method: method.to_uppercase(),
3566 path: path.to_string(),
3567 summary: operation.summary.clone(),
3568 description: operation.description.clone(),
3569 request_body: None,
3570 response_schemas: BTreeMap::new(),
3571 parameters: Vec::new(),
3572 supports_streaming: false, stream_parameter: None, };
3575
3576 if let Some(request_body) = &operation.request_body
3578 && let Some((content_type, maybe_schema)) = request_body.best_content()
3579 {
3580 use crate::openapi::{is_form_urlencoded_media_type, is_json_media_type};
3581 op_info.request_body = if is_json_media_type(content_type) {
3582 maybe_schema
3583 .map(|s| {
3584 self.resolve_or_inline_schema(s, operation_id, "Request")
3585 .map(|name| RequestBodyContent::Json { schema_name: name })
3586 })
3587 .transpose()?
3588 } else if is_form_urlencoded_media_type(content_type) {
3589 maybe_schema
3590 .map(|s| {
3591 self.resolve_or_inline_schema(s, operation_id, "Request")
3592 .map(|name| RequestBodyContent::FormUrlEncoded { schema_name: name })
3593 })
3594 .transpose()?
3595 } else {
3596 match content_type {
3597 "multipart/form-data" => Some(RequestBodyContent::Multipart),
3598 "application/octet-stream" => Some(RequestBodyContent::OctetStream),
3599 "text/plain" => Some(RequestBodyContent::TextPlain),
3600 _ => None,
3601 }
3602 };
3603 }
3604
3605 if let Some(responses) = &operation.responses {
3607 for (status_code, response) in responses {
3608 if let Some(schema) = response.json_schema() {
3609 if let Some(schema_ref) = schema.reference() {
3610 if let Some(schema_name) = self.extract_schema_name(schema_ref) {
3612 op_info
3613 .response_schemas
3614 .insert(status_code.clone(), schema_name.to_string());
3615 }
3616 } else {
3617 let synthetic_name =
3619 self.generate_inline_response_type_name(operation_id, status_code);
3620
3621 let mut deps = HashSet::new();
3623 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
3624
3625 op_info
3626 .response_schemas
3627 .insert(status_code.clone(), synthetic_name);
3628 }
3629 }
3630 }
3631 }
3632
3633 if let Some(parameters) = &operation.parameters {
3635 for param in parameters {
3636 let resolved = self.resolve_parameter(param);
3637 if let Some(param_info) = self.analyze_parameter(&resolved)? {
3638 op_info.parameters.push(param_info);
3639 }
3640 }
3641 }
3642
3643 if let Some(path_params) = path_item_parameters {
3645 let existing_keys: std::collections::HashSet<(String, String)> = op_info
3646 .parameters
3647 .iter()
3648 .map(|p| (p.name.clone(), p.location.clone()))
3649 .collect();
3650 for param in path_params {
3651 let resolved = self.resolve_parameter(param);
3652 if let Some(param_info) = self.analyze_parameter(&resolved)? {
3653 if !existing_keys
3654 .contains(&(param_info.name.clone(), param_info.location.clone()))
3655 {
3656 op_info.parameters.push(param_info);
3657 }
3658 }
3659 }
3660 }
3661
3662 Ok(op_info)
3663 }
3664
3665 fn generate_inline_response_type_name(&self, operation_id: &str, status_code: &str) -> String {
3672 use heck::ToPascalCase;
3673 let base_name = operation_id.replace('.', "_").to_pascal_case();
3674 let suffix = Self::status_code_suffix(status_code);
3675 format!("{}Response{}", base_name, suffix)
3676 }
3677
3678 fn status_code_suffix(status_code: &str) -> String {
3685 match status_code {
3686 "" | "200" => String::new(),
3687 "default" | "Default" => "Default".to_string(),
3688 other if other.chars().all(|c| c.is_ascii_digit()) => other.to_string(),
3689 other => other.to_ascii_lowercase(),
3690 }
3691 }
3692
3693 fn generate_inline_request_type_name(&self, operation_id: &str) -> String {
3695 use heck::ToPascalCase;
3696 let base_name = operation_id.replace('.', "_").to_pascal_case();
3700 format!("{}Request", base_name)
3701 }
3702
3703 fn resolve_or_inline_schema(
3706 &mut self,
3707 schema: &crate::openapi::Schema,
3708 operation_id: &str,
3709 suffix: &str,
3710 ) -> Result<String> {
3711 if let Some(schema_ref) = schema.reference()
3712 && let Some(schema_name) = self.extract_schema_name(schema_ref)
3713 {
3714 return Ok(schema_name.to_string());
3715 }
3716 let synthetic_name = if suffix == "Request" {
3718 self.generate_inline_request_type_name(operation_id)
3719 } else {
3720 self.generate_inline_response_type_name(operation_id, "")
3721 };
3722 let mut deps = HashSet::new();
3723 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
3724 Ok(synthetic_name)
3725 }
3726
3727 fn resolve_parameter<'a>(
3730 &'a self,
3731 param: &'a crate::openapi::Parameter,
3732 ) -> std::borrow::Cow<'a, crate::openapi::Parameter> {
3733 if let Some(ref_str) = param.extra.get("$ref").and_then(|v| v.as_str()) {
3734 if let Some(param_name) = ref_str.strip_prefix("#/components/parameters/") {
3735 if let Some(resolved) = self.component_parameters.get(param_name) {
3736 return std::borrow::Cow::Borrowed(resolved);
3737 }
3738 }
3739 }
3740 std::borrow::Cow::Borrowed(param)
3741 }
3742
3743 fn analyze_parameter(
3745 &self,
3746 param: &crate::openapi::Parameter,
3747 ) -> Result<Option<ParameterInfo>> {
3748 let name = param.name.as_deref().unwrap_or("");
3749 let location = param.location.as_deref().unwrap_or("");
3750 let required = param.required.unwrap_or(false);
3751
3752 let mut rust_type = "String".to_string();
3753 let mut schema_ref = None;
3754
3755 if let Some(schema) = ¶m.schema {
3756 if let Some(ref_str) = schema.reference() {
3757 schema_ref = self.extract_schema_name(ref_str).map(|s| s.to_string());
3758 } else if let Some(schema_type) = schema.schema_type() {
3759 rust_type = match schema_type {
3760 crate::openapi::SchemaType::Boolean => "bool",
3761 crate::openapi::SchemaType::Integer => "i64",
3762 crate::openapi::SchemaType::Number => "f64",
3763 crate::openapi::SchemaType::String => "String",
3764 _ => "String",
3765 }
3766 .to_string();
3767 }
3768 }
3769
3770 Ok(Some(ParameterInfo {
3771 name: name.to_string(),
3772 location: location.to_string(),
3773 required,
3774 schema_ref,
3775 rust_type,
3776 description: param.description.clone(),
3777 }))
3778 }
3779}