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(one_of, discriminator.as_ref(), None, &mut dependencies)?
1131 }
1132 Schema::AllOf { all_of, .. } => {
1133 self.analyze_allof_composition(all_of, &mut dependencies)?
1135 }
1136 Schema::Untyped { .. } => {
1137 if let Some(inferred) = schema.inferred_type() {
1139 match inferred {
1140 OpenApiSchemaType::Object => {
1141 if self.should_use_dynamic_json(schema) {
1142 SchemaType::Primitive {
1143 rust_type: "serde_json::Value".to_string(),
1144 }
1145 } else {
1146 self.analyze_object_schema(schema, &mut dependencies)?
1147 }
1148 }
1149 OpenApiSchemaType::String if details.is_string_enum() => {
1150 SchemaType::StringEnum {
1151 values: details.string_enum_values().unwrap_or_default(),
1152 }
1153 }
1154 _ => SchemaType::Primitive {
1155 rust_type: "serde_json::Value".to_string(),
1156 },
1157 }
1158 } else {
1159 SchemaType::Primitive {
1160 rust_type: "serde_json::Value".to_string(),
1161 }
1162 }
1163 }
1164 };
1165
1166 Ok(AnalyzedSchema {
1167 name: schema_name.to_string(),
1168 original: serde_json::to_value(schema).unwrap_or(Value::Null), schema_type,
1170 dependencies,
1171 nullable,
1172 description,
1173 default: details.default.clone(),
1174 })
1175 }
1176
1177 fn analyze_object_schema(
1178 &mut self,
1179 schema: &Schema,
1180 dependencies: &mut HashSet<String>,
1181 ) -> Result<SchemaType> {
1182 let details = schema.details();
1183 let properties = &details.properties;
1184 let required = details
1185 .required
1186 .as_ref()
1187 .map(|req| req.iter().cloned().collect::<HashSet<String>>())
1188 .unwrap_or_default();
1189
1190 let mut property_info = BTreeMap::new();
1191
1192 if let Some(props) = properties {
1193 for (prop_name, prop_schema) in props {
1194 let prop_type = if let Schema::AnyOf { any_of, .. } = prop_schema {
1196 if self.should_use_dynamic_json(prop_schema) {
1198 SchemaType::Primitive {
1200 rust_type: "serde_json::Value".to_string(),
1201 }
1202 } else {
1203 let context_name = self
1206 .current_schema_name
1207 .clone()
1208 .unwrap_or_else(|| "Unknown".to_string());
1209
1210 let prop_pascal = self.to_pascal_case(prop_name);
1212 let union_type_name = format!("{context_name}{prop_pascal}");
1213
1214 let union_schema_type = self.analyze_anyof_union(
1216 any_of,
1217 prop_schema.discriminator(),
1218 dependencies,
1219 &union_type_name,
1220 )?;
1221
1222 self.resolved_cache.insert(
1224 union_type_name.clone(),
1225 AnalyzedSchema {
1226 name: union_type_name.clone(),
1227 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
1228 schema_type: union_schema_type,
1229 dependencies: HashSet::new(),
1230 nullable: false,
1231 description: prop_schema.details().description.clone(),
1232 default: None,
1233 },
1234 );
1235
1236 dependencies.insert(union_type_name.clone());
1238 SchemaType::Reference {
1239 target: union_type_name,
1240 }
1241 }
1242 } else if let Schema::OneOf {
1243 one_of,
1244 discriminator,
1245 ..
1246 } = prop_schema
1247 {
1248 let context_name = self
1251 .current_schema_name
1252 .clone()
1253 .unwrap_or_else(|| "Unknown".to_string());
1254 let prop_pascal = self.to_pascal_case(prop_name);
1255 let union_type_name = format!("{context_name}{prop_pascal}");
1256
1257 let union_schema_type = self.analyze_oneof_union(
1259 one_of,
1260 discriminator.as_ref(),
1261 Some(&union_type_name),
1262 dependencies,
1263 )?;
1264
1265 self.resolved_cache.insert(
1267 union_type_name.clone(),
1268 AnalyzedSchema {
1269 name: union_type_name.clone(),
1270 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
1271 schema_type: union_schema_type,
1272 dependencies: HashSet::new(),
1273 nullable: false,
1274 description: prop_schema.details().description.clone(),
1275 default: None,
1276 },
1277 );
1278
1279 dependencies.insert(union_type_name.clone());
1281 SchemaType::Reference {
1282 target: union_type_name,
1283 }
1284 } else {
1285 self.analyze_property_schema_with_context(
1287 prop_schema,
1288 Some(prop_name),
1289 dependencies,
1290 )?
1291 };
1292
1293 let prop_details = prop_schema.details();
1294 let prop_nullable = prop_details.is_nullable() || prop_schema.is_nullable_pattern();
1296 let prop_description = prop_details.description.clone();
1297 let prop_default = prop_details.default.clone();
1298
1299 property_info.insert(
1300 prop_name.clone(),
1301 PropertyInfo {
1302 schema_type: prop_type,
1303 nullable: prop_nullable,
1304 description: prop_description,
1305 default: prop_default,
1306 serde_attrs: Vec::new(),
1307 },
1308 );
1309 }
1310 }
1311
1312 let additional_properties = match &details.additional_properties {
1314 Some(crate::openapi::AdditionalProperties::Boolean(true)) => true,
1315 Some(crate::openapi::AdditionalProperties::Boolean(false)) => false,
1316 Some(crate::openapi::AdditionalProperties::Schema(_)) => {
1317 true
1320 }
1321 None => false, };
1323
1324 Ok(SchemaType::Object {
1325 properties: property_info,
1326 required,
1327 additional_properties,
1328 })
1329 }
1330
1331 fn analyze_property_schema_with_context(
1332 &mut self,
1333 schema: &Schema,
1334 property_name: Option<&str>,
1335 dependencies: &mut HashSet<String>,
1336 ) -> Result<SchemaType> {
1337 if let Some(ref_str) = self.get_any_reference(schema) {
1338 let target = if ref_str == "#" {
1339 self.find_recursive_anchor_schema()
1341 .unwrap_or_else(|| "UnknownRecursive".to_string())
1342 } else {
1343 self.extract_schema_name(ref_str)
1344 .ok_or_else(|| GeneratorError::UnresolvedReference(ref_str.to_string()))?
1345 .to_string()
1346 };
1347 dependencies.insert(target.clone());
1348 return Ok(SchemaType::Reference { target });
1349 }
1350
1351 if let Some(schema_type) = schema.schema_type() {
1352 match schema_type {
1353 OpenApiSchemaType::String => {
1354 if let Some(enum_values) = schema.details().string_enum_values() {
1356 let context_name = self
1359 .current_schema_name
1360 .clone()
1361 .unwrap_or_else(|| "Unknown".to_string());
1362
1363 let primary_name = if let Some(prop_name) = property_name {
1365 let prop_pascal = self.to_pascal_case(prop_name);
1367 format!("{context_name}{prop_pascal}")
1368 } else {
1369 let suffix = if !enum_values.is_empty() {
1372 let first_value = self.to_pascal_case(&enum_values[0]);
1373 format!("{first_value}Enum")
1374 } else {
1375 "StringEnum".to_string()
1376 };
1377 format!("{context_name}{suffix}")
1378 };
1379
1380 fn matches_values(
1400 existing: &AnalyzedSchema,
1401 values: &[String],
1402 ) -> bool {
1403 matches!(
1404 &existing.schema_type,
1405 SchemaType::StringEnum { values: existing_values }
1406 if existing_values == values
1407 )
1408 }
1409
1410 let mut enum_type_name = primary_name.clone();
1411 let mut should_insert = match self.resolved_cache.get(&enum_type_name) {
1412 None => true,
1413 Some(existing) if matches_values(existing, &enum_values) => false,
1414 Some(_) => {
1415 let suffix = enum_values
1418 .first()
1419 .map(|v| self.to_pascal_case(v))
1420 .unwrap_or_else(|| "Variant".to_string());
1421 let candidate = format!("{primary_name}{suffix}");
1422
1423 let resolved = match self.resolved_cache.get(&candidate) {
1424 None => Some((candidate.clone(), true)),
1425 Some(existing) if matches_values(existing, &enum_values) => {
1426 Some((candidate.clone(), false))
1427 }
1428 Some(_) => {
1429 let mut found = None;
1432 for n in 2..1000 {
1433 let numbered = format!("{candidate}_{n}");
1434 match self.resolved_cache.get(&numbered) {
1435 None => {
1436 found = Some((numbered, true));
1437 break;
1438 }
1439 Some(existing)
1440 if matches_values(existing, &enum_values) =>
1441 {
1442 found = Some((numbered, false));
1443 break;
1444 }
1445 Some(_) => continue,
1446 }
1447 }
1448 found
1449 }
1450 };
1451
1452 let (resolved_name, insert) = resolved.unwrap_or((candidate, true));
1453 enum_type_name = resolved_name;
1454 insert
1455 }
1456 };
1457
1458 if should_insert {
1461 self.resolved_cache.insert(
1462 enum_type_name.clone(),
1463 AnalyzedSchema {
1464 name: enum_type_name.clone(),
1465 original: serde_json::to_value(schema).unwrap_or(Value::Null),
1466 schema_type: SchemaType::StringEnum {
1467 values: enum_values,
1468 },
1469 dependencies: HashSet::new(),
1470 nullable: false,
1471 description: schema.details().description.clone(),
1472 default: schema.details().default.clone(),
1473 },
1474 );
1475 let _ = &mut should_insert;
1478 }
1479
1480 dependencies.insert(enum_type_name.clone());
1482 return Ok(SchemaType::Reference {
1483 target: enum_type_name,
1484 });
1485 } else {
1486 return Ok(SchemaType::Primitive {
1487 rust_type: "String".to_string(),
1488 });
1489 }
1490 }
1491 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
1492 let details = schema.details();
1493 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
1494 return Ok(SchemaType::Primitive { rust_type });
1495 }
1496 OpenApiSchemaType::Boolean => {
1497 return Ok(SchemaType::Primitive {
1498 rust_type: "bool".to_string(),
1499 });
1500 }
1501 OpenApiSchemaType::Array => {
1502 let context_name = if let Some(prop_name) = property_name {
1504 let prop_pascal = self.to_pascal_case(prop_name);
1506 format!(
1507 "{}{}",
1508 self.current_schema_name.as_deref().unwrap_or("Unknown"),
1509 prop_pascal
1510 )
1511 } else {
1512 "ArrayItem".to_string()
1514 };
1515 return self.analyze_array_schema(schema, &context_name, dependencies);
1516 }
1517 OpenApiSchemaType::Object => {
1518 if self.should_use_dynamic_json(schema) {
1520 return Ok(SchemaType::Primitive {
1521 rust_type: "serde_json::Value".to_string(),
1522 });
1523 }
1524 let object_type_name = if let Some(prop_name) = property_name {
1526 let prop_pascal = self.to_pascal_case(prop_name);
1528 format!(
1529 "{}{}",
1530 self.current_schema_name.as_deref().unwrap_or("Unknown"),
1531 prop_pascal
1532 )
1533 } else {
1534 format!(
1536 "{}Object",
1537 self.current_schema_name.as_deref().unwrap_or("Unknown")
1538 )
1539 };
1540
1541 let object_type = self.analyze_object_schema(schema, dependencies)?;
1543
1544 let inline_schema = AnalyzedSchema {
1546 name: object_type_name.clone(),
1547 original: serde_json::to_value(schema).unwrap_or(Value::Null),
1548 schema_type: object_type,
1549 dependencies: dependencies.clone(),
1550 nullable: false,
1551 description: schema.details().description.clone(),
1552 default: None,
1553 };
1554
1555 self.resolved_cache
1557 .insert(object_type_name.clone(), inline_schema);
1558 dependencies.insert(object_type_name.clone());
1559
1560 return Ok(SchemaType::Reference {
1562 target: object_type_name,
1563 });
1564 }
1565 _ => {
1566 return Ok(SchemaType::Primitive {
1567 rust_type: "serde_json::Value".to_string(),
1568 });
1569 }
1570 }
1571 }
1572
1573 if schema.is_nullable_pattern() {
1575 if let Some(non_null) = schema.non_null_variant() {
1576 return self.analyze_property_schema_with_context(
1577 non_null,
1578 property_name,
1579 dependencies,
1580 );
1581 }
1582 }
1583
1584 if self.should_use_dynamic_json(schema) {
1586 return Ok(SchemaType::Primitive {
1587 rust_type: "serde_json::Value".to_string(),
1588 });
1589 }
1590
1591 if let Schema::AllOf { all_of, .. } = schema {
1593 return self.analyze_allof_composition(all_of, dependencies);
1594 }
1595
1596 if let Some(variants) = schema.union_variants() {
1598 match variants.len().cmp(&1) {
1599 std::cmp::Ordering::Equal => {
1600 return self.analyze_property_schema_with_context(
1602 &variants[0],
1603 property_name,
1604 dependencies,
1605 );
1606 }
1607 std::cmp::Ordering::Greater => {
1608 let union_name = if let Some(prop_name) = property_name {
1611 let prop_pascal = self.to_pascal_case(prop_name);
1613 format!(
1614 "{}{}",
1615 self.current_schema_name.as_deref().unwrap_or(""),
1616 prop_pascal
1617 )
1618 } else {
1619 "UnionType".to_string()
1620 };
1621
1622 if let Schema::OneOf {
1624 one_of,
1625 discriminator,
1626 ..
1627 } = schema
1628 {
1629 let oneof_result = self.analyze_oneof_union(
1631 one_of,
1632 discriminator.as_ref(),
1633 Some(&union_name),
1634 dependencies,
1635 )?;
1636
1637 if let SchemaType::Union {
1639 variants: _union_variants,
1640 } = &oneof_result
1641 {
1642 self.resolved_cache.insert(
1644 union_name.clone(),
1645 AnalyzedSchema {
1646 name: union_name.clone(),
1647 original: serde_json::to_value(schema).unwrap_or(Value::Null),
1648 schema_type: oneof_result.clone(),
1649 dependencies: dependencies.clone(),
1650 nullable: false,
1651 description: schema.details().description.clone(),
1652 default: None,
1653 },
1654 );
1655
1656 dependencies.insert(union_name.clone());
1658 return Ok(SchemaType::Reference { target: union_name });
1659 }
1660
1661 return Ok(oneof_result);
1662 } else if let Schema::AnyOf {
1663 any_of,
1664 discriminator,
1665 ..
1666 } = schema
1667 {
1668 let union_analysis = self.analyze_anyof_union(
1670 any_of,
1671 discriminator.as_ref(),
1672 dependencies,
1673 &union_name,
1674 )?;
1675 return Ok(union_analysis);
1676 } else {
1677 let mut union_variants = Vec::new();
1680 for variant in variants {
1681 if let Some(ref_str) = variant.reference() {
1682 if let Some(target) = self.extract_schema_name(ref_str) {
1683 dependencies.insert(target.to_string());
1684 union_variants.push(SchemaRef {
1685 target: target.to_string(),
1686 nullable: false,
1687 });
1688 }
1689 }
1690 }
1691 return Ok(SchemaType::Union {
1692 variants: union_variants,
1693 });
1694 }
1695 }
1696 std::cmp::Ordering::Less => {}
1697 }
1698 }
1699
1700 if let Some(inferred_type) = schema.inferred_type() {
1702 match inferred_type {
1703 OpenApiSchemaType::Object => {
1704 if self.should_use_dynamic_json(schema) {
1706 return Ok(SchemaType::Primitive {
1707 rust_type: "serde_json::Value".to_string(),
1708 });
1709 }
1710 return self.analyze_object_schema(schema, dependencies);
1711 }
1712 OpenApiSchemaType::Array => {
1713 let context_name = if let Some(prop_name) = property_name {
1714 let prop_pascal = self.to_pascal_case(prop_name);
1716 format!(
1717 "{}{}",
1718 self.current_schema_name.as_deref().unwrap_or("Unknown"),
1719 prop_pascal
1720 )
1721 } else {
1722 "ArrayItem".to_string()
1724 };
1725 return self.analyze_array_schema(schema, &context_name, dependencies);
1726 }
1727 OpenApiSchemaType::String => {
1728 if let Some(enum_values) = schema.details().string_enum_values() {
1729 return Ok(SchemaType::StringEnum {
1730 values: enum_values,
1731 });
1732 } else {
1733 return Ok(SchemaType::Primitive {
1734 rust_type: "String".to_string(),
1735 });
1736 }
1737 }
1738 _ => {
1739 let rust_type = self.openapi_type_to_rust_type(inferred_type, schema.details());
1741 return Ok(SchemaType::Primitive { rust_type });
1742 }
1743 }
1744 }
1745
1746 Ok(SchemaType::Primitive {
1747 rust_type: "serde_json::Value".to_string(),
1748 })
1749 }
1750
1751 fn analyze_allof_composition(
1752 &mut self,
1753 all_of_schemas: &[Schema],
1754 dependencies: &mut HashSet<String>,
1755 ) -> Result<SchemaType> {
1756 if all_of_schemas.len() == 1 {
1759 if let Schema::Reference { reference, .. } = &all_of_schemas[0] {
1760 if let Some(target) = self.extract_schema_name(reference) {
1761 dependencies.insert(target.to_string());
1762 return Ok(SchemaType::Reference {
1763 target: target.to_string(),
1764 });
1765 }
1766 }
1767 }
1768
1769 let mut merged_properties = BTreeMap::new();
1771 let mut merged_required = HashSet::new();
1772 let mut descriptions = Vec::new();
1773
1774 let current_context = self.current_schema_name.clone();
1776
1777 for schema in all_of_schemas {
1778 match schema {
1779 Schema::Reference { reference, .. } => {
1780 if let Some(target) = self.extract_schema_name(reference) {
1782 dependencies.insert(target.to_string());
1783
1784 let analyzed_ref = self.analyze_schema(target)?;
1786
1787 match &analyzed_ref.schema_type {
1789 SchemaType::Object {
1790 properties,
1791 required,
1792 ..
1793 } => {
1794 for (prop_name, prop_info) in properties {
1796 merged_properties.insert(prop_name.clone(), prop_info.clone());
1797 }
1798 for req in required {
1800 merged_required.insert(req.clone());
1801 }
1802 }
1803 _ => {
1804 if let Some(ref_schema) = self.schemas.get(target).cloned() {
1806 self.merge_schema_into_properties(
1807 &ref_schema,
1808 &mut merged_properties,
1809 &mut merged_required,
1810 dependencies,
1811 )?;
1812 }
1813 }
1814 }
1815 }
1816 }
1817 Schema::Typed {
1818 schema_type: OpenApiSchemaType::Object,
1819 ..
1820 }
1821 | Schema::Untyped { .. } => {
1822 let saved_context = self.current_schema_name.clone();
1824 self.current_schema_name = current_context.clone();
1825
1826 self.merge_schema_into_properties(
1828 schema,
1829 &mut merged_properties,
1830 &mut merged_required,
1831 dependencies,
1832 )?;
1833
1834 self.current_schema_name = saved_context;
1836 }
1837 _ => {
1838 self.merge_schema_into_properties(
1841 schema,
1842 &mut merged_properties,
1843 &mut merged_required,
1844 dependencies,
1845 )?;
1846 }
1847 }
1848
1849 if let Some(desc) = &schema.details().description {
1851 descriptions.push(desc.clone());
1852 }
1853 }
1854
1855 if !merged_properties.is_empty() {
1857 Ok(SchemaType::Object {
1858 properties: merged_properties,
1859 required: merged_required,
1860 additional_properties: false,
1861 })
1862 } else {
1863 Ok(SchemaType::Composition {
1865 schemas: all_of_schemas
1866 .iter()
1867 .filter_map(|s| {
1868 if let Some(ref_str) = s.reference() {
1869 if let Some(target) = self.extract_schema_name(ref_str) {
1870 dependencies.insert(target.to_string());
1871 Some(SchemaRef {
1872 target: target.to_string(),
1873 nullable: false,
1874 })
1875 } else {
1876 None
1877 }
1878 } else {
1879 None
1880 }
1881 })
1882 .collect(),
1883 })
1884 }
1885 }
1886
1887 fn merge_schema_into_properties(
1888 &mut self,
1889 schema: &Schema,
1890 merged_properties: &mut BTreeMap<String, PropertyInfo>,
1891 merged_required: &mut HashSet<String>,
1892 dependencies: &mut HashSet<String>,
1893 ) -> Result<()> {
1894 let details = schema.details();
1895
1896 if let Some(properties) = &details.properties {
1898 for (prop_name, prop_schema) in properties {
1899 let prop_type = self.analyze_property_schema_with_context(
1900 prop_schema,
1901 Some(prop_name),
1902 dependencies,
1903 )?;
1904 let prop_details = prop_schema.details();
1905
1906 merged_properties.insert(
1907 prop_name.clone(),
1908 PropertyInfo {
1909 schema_type: prop_type,
1910 nullable: prop_details.is_nullable(),
1911 description: prop_details.description.clone(),
1912 default: prop_details.default.clone(),
1913 serde_attrs: Vec::new(),
1914 },
1915 );
1916 }
1917 }
1918
1919 if let Some(required) = &details.required {
1921 for field in required {
1922 merged_required.insert(field.clone());
1923 }
1924 }
1925
1926 Ok(())
1927 }
1928
1929 fn analyze_oneof_union(
1930 &mut self,
1931 one_of_schemas: &[Schema],
1932 discriminator: Option<&crate::openapi::Discriminator>,
1933 parent_name: Option<&str>,
1934 dependencies: &mut HashSet<String>,
1935 ) -> Result<SchemaType> {
1936 if discriminator.is_none() {
1938 return self.analyze_untagged_oneof_union(one_of_schemas, parent_name, dependencies);
1940 }
1941
1942 let discriminator_field = discriminator
1944 .ok_or_else(|| {
1945 GeneratorError::InvalidDiscriminator(
1946 "expected discriminator after guard check".to_string(),
1947 )
1948 })?
1949 .property_name
1950 .clone();
1951
1952 let mut variants = Vec::new();
1953 let mut used_variant_names = std::collections::HashSet::new();
1954
1955 for variant_schema in one_of_schemas {
1956 let ref_info = if let Some(ref_str) = variant_schema.reference() {
1958 Some((ref_str, false))
1959 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
1960 Some((recursive_ref, true))
1961 } else if let Schema::AllOf { all_of, .. } = variant_schema {
1962 if all_of.len() == 1 {
1964 if let Some(ref_str) = all_of[0].reference() {
1965 Some((ref_str, false))
1966 } else {
1967 all_of[0]
1968 .recursive_reference()
1969 .map(|recursive_ref| (recursive_ref, true))
1970 }
1971 } else {
1972 None
1973 }
1974 } else {
1975 None
1976 };
1977
1978 if let Some((ref_str, is_recursive)) = ref_info {
1979 let schema_name = if is_recursive && ref_str == "#" {
1980 self.find_recursive_anchor_schema()
1982 .or_else(|| self.current_schema_name.clone())
1983 .unwrap_or_else(|| "CompoundFilter".to_string())
1984 } else {
1985 self.extract_schema_name(ref_str)
1986 .map(|s| s.to_string())
1987 .unwrap_or_else(|| "UnknownRef".to_string())
1988 };
1989
1990 if !schema_name.is_empty() {
1991 dependencies.insert(schema_name.clone());
1992
1993 let discriminator_value = if let Some(disc) = discriminator {
1998 if let Some(mappings) = &disc.mapping {
1999 mappings
2002 .iter()
2003 .find(|(_, target_ref)| {
2004 target_ref.as_str() == ref_str
2006 || self
2007 .extract_schema_name(target_ref)
2008 .map(|s| s.to_string())
2009 == Some(schema_name.clone())
2010 })
2011 .map(|(key, _)| key.clone())
2012 .unwrap_or_else(|| {
2013 self.fallback_discriminator_value_for_field(
2014 &schema_name,
2015 &discriminator_field,
2016 )
2017 })
2018 } else {
2019 self.fallback_discriminator_value_for_field(
2020 &schema_name,
2021 &discriminator_field,
2022 )
2023 }
2024 } else {
2025 self.fallback_discriminator_value_for_field(
2026 &schema_name,
2027 &discriminator_field,
2028 )
2029 };
2030
2031 let base_name = self.to_rust_variant_name(&schema_name);
2033 let rust_name =
2034 self.ensure_unique_variant_name(base_name, &mut used_variant_names);
2035
2036 let final_discriminator_value = discriminator_value;
2038
2039 variants.push(UnionVariant {
2040 rust_name,
2041 type_name: schema_name,
2042 discriminator_value: final_discriminator_value,
2043 schema_ref: ref_str.to_string(),
2044 });
2045 }
2046 } else {
2047 let variant_index = variants.len();
2049 let inline_type_name =
2050 self.generate_inline_type_name(variant_schema, variant_index);
2051
2052 let discriminator_value = if let Some(disc) = discriminator {
2054 if let Some(mappings) = &disc.mapping {
2055 mappings
2057 .iter()
2058 .find(|(_, target_ref)| {
2059 target_ref.contains(&format!("variant_{variant_index}"))
2060 })
2061 .map(|(key, _)| key.clone())
2062 .unwrap_or_else(|| {
2063 self.extract_inline_discriminator_value(
2064 variant_schema,
2065 &discriminator_field,
2066 variant_index,
2067 )
2068 })
2069 } else {
2070 self.extract_inline_discriminator_value(
2071 variant_schema,
2072 &discriminator_field,
2073 variant_index,
2074 )
2075 }
2076 } else {
2077 self.extract_inline_discriminator_value(
2078 variant_schema,
2079 &discriminator_field,
2080 variant_index,
2081 )
2082 };
2083
2084 let base_name = if discriminator_value.starts_with("variant_") {
2086 format!("Variant{variant_index}")
2087 } else {
2088 let clean_name = self.discriminator_to_variant_name(&discriminator_value);
2090 self.to_rust_variant_name(&clean_name)
2091 };
2092 let rust_name = self.ensure_unique_variant_name(base_name, &mut used_variant_names);
2093
2094 let final_discriminator_value = discriminator_value;
2096
2097 variants.push(UnionVariant {
2098 rust_name,
2099 type_name: inline_type_name.clone(),
2100 discriminator_value: final_discriminator_value,
2101 schema_ref: format!("inline_{variant_index}"),
2102 });
2103
2104 self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
2106 }
2107 }
2108
2109 if variants.is_empty() {
2110 let mut union_variants = Vec::new();
2113
2114 for (variant_index, variant_schema) in one_of_schemas.iter().enumerate() {
2115 if let Some(ref_str) = variant_schema.reference() {
2117 if let Some(schema_name) = self.extract_schema_name(ref_str) {
2118 dependencies.insert(schema_name.to_string());
2119 union_variants.push(SchemaRef {
2120 target: schema_name.to_string(),
2121 nullable: false,
2122 });
2123 }
2124 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2125 let schema_name = if recursive_ref == "#" {
2126 self.find_recursive_anchor_schema()
2128 .or_else(|| self.current_schema_name.clone())
2129 .unwrap_or_else(|| "CompoundFilter".to_string())
2130 } else {
2131 self.extract_schema_name(recursive_ref)
2132 .map(|s| s.to_string())
2133 .unwrap_or_else(|| "RecursiveType".to_string())
2134 };
2135 dependencies.insert(schema_name.clone());
2136 union_variants.push(SchemaRef {
2137 target: schema_name,
2138 nullable: false,
2139 });
2140 } else {
2141 let context = parent_name.unwrap_or("Union");
2143 let inline_name = self.generate_context_aware_name(
2144 context,
2145 "InlineVariant",
2146 variant_index,
2147 Some(variant_schema),
2148 );
2149 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
2150 let variant_type = analyzed.schema_type;
2151
2152 for dep in &analyzed.dependencies {
2154 dependencies.insert(dep.clone());
2155 }
2156
2157 match &variant_type {
2158 SchemaType::Primitive { rust_type } => {
2160 union_variants.push(SchemaRef {
2161 target: rust_type.clone(),
2162 nullable: false,
2163 });
2164 }
2165 SchemaType::Array { item_type } => {
2167 match item_type.as_ref() {
2168 SchemaType::Primitive { rust_type } => {
2169 let type_name = format!("Vec<{rust_type}>");
2170 union_variants.push(SchemaRef {
2171 target: type_name,
2172 nullable: false,
2173 });
2174 }
2175 SchemaType::Reference { target } => {
2176 let type_name = format!("Vec<{target}>");
2177 union_variants.push(SchemaRef {
2178 target: type_name,
2179 nullable: false,
2180 });
2181 }
2182 _ => {
2183 let context = parent_name.unwrap_or("Inline");
2185 let inline_type_name = self.generate_context_aware_name(
2186 context,
2187 "Variant",
2188 variant_index,
2189 None,
2190 );
2191 self.add_inline_schema(
2192 &inline_type_name,
2193 variant_schema,
2194 dependencies,
2195 )?;
2196 union_variants.push(SchemaRef {
2197 target: inline_type_name,
2198 nullable: false,
2199 });
2200 }
2201 }
2202 }
2203 SchemaType::Reference { target } => {
2205 union_variants.push(SchemaRef {
2206 target: target.clone(),
2207 nullable: false,
2208 });
2209 }
2210 _ => {
2212 let inline_type_name = format!(
2213 "{}Variant{}",
2214 parent_name.unwrap_or("Inline"),
2215 variant_index + 1
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 }
2230
2231 if !union_variants.is_empty() {
2232 return Ok(SchemaType::Union {
2233 variants: union_variants,
2234 });
2235 }
2236
2237 return Ok(SchemaType::Primitive {
2239 rust_type: "serde_json::Value".to_string(),
2240 });
2241 }
2242
2243 Ok(SchemaType::DiscriminatedUnion {
2244 discriminator_field,
2245 variants,
2246 })
2247 }
2248
2249 fn analyze_untagged_oneof_union(
2250 &mut self,
2251 one_of_schemas: &[Schema],
2252 parent_name: Option<&str>,
2253 dependencies: &mut HashSet<String>,
2254 ) -> Result<SchemaType> {
2255 let mut union_variants = Vec::new();
2256
2257 for (variant_index, variant_schema) in one_of_schemas.iter().enumerate() {
2258 if let Some(ref_str) = variant_schema.reference() {
2260 if let Some(schema_name) = self.extract_schema_name(ref_str) {
2261 dependencies.insert(schema_name.to_string());
2262 union_variants.push(SchemaRef {
2263 target: schema_name.to_string(),
2264 nullable: false,
2265 });
2266 }
2267 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2268 let schema_name = if recursive_ref == "#" {
2269 self.find_recursive_anchor_schema()
2271 .or_else(|| self.current_schema_name.clone())
2272 .unwrap_or_else(|| "CompoundFilter".to_string())
2273 } else {
2274 self.extract_schema_name(recursive_ref)
2275 .map(|s| s.to_string())
2276 .unwrap_or_else(|| "RecursiveType".to_string())
2277 };
2278 dependencies.insert(schema_name.clone());
2279 union_variants.push(SchemaRef {
2280 target: schema_name,
2281 nullable: false,
2282 });
2283 } else {
2284 let context = parent_name.unwrap_or("Union");
2286 let inline_name = self.generate_context_aware_name(
2287 context,
2288 "InlineVariant",
2289 variant_index,
2290 Some(variant_schema),
2291 );
2292 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
2293 let variant_type = analyzed.schema_type;
2294
2295 for dep in &analyzed.dependencies {
2297 dependencies.insert(dep.clone());
2298 }
2299
2300 match &variant_type {
2301 SchemaType::Primitive { rust_type } => {
2303 union_variants.push(SchemaRef {
2304 target: rust_type.clone(),
2305 nullable: false,
2306 });
2307 }
2308 SchemaType::Array { item_type } => {
2310 match item_type.as_ref() {
2311 SchemaType::Primitive { rust_type } => {
2312 let type_name = format!("Vec<{rust_type}>");
2313 union_variants.push(SchemaRef {
2314 target: type_name,
2315 nullable: false,
2316 });
2317 }
2318 SchemaType::Reference { target } => {
2319 let type_name = format!("Vec<{target}>");
2320 union_variants.push(SchemaRef {
2321 target: type_name,
2322 nullable: false,
2323 });
2324 }
2325 SchemaType::Array {
2327 item_type: inner_item_type,
2328 } => {
2329 match inner_item_type.as_ref() {
2330 SchemaType::Primitive { rust_type } => {
2331 let type_name = format!("Vec<Vec<{rust_type}>>");
2332 union_variants.push(SchemaRef {
2333 target: type_name,
2334 nullable: false,
2335 });
2336 }
2337 SchemaType::Reference { target } => {
2338 let type_name = format!("Vec<Vec<{target}>>");
2339 union_variants.push(SchemaRef {
2340 target: type_name,
2341 nullable: false,
2342 });
2343 }
2344 _ => {
2345 let context = parent_name.unwrap_or("Inline");
2347 let inline_type_name = self.generate_context_aware_name(
2348 context,
2349 "Variant",
2350 variant_index,
2351 None,
2352 );
2353 self.add_inline_schema(
2354 &inline_type_name,
2355 variant_schema,
2356 dependencies,
2357 )?;
2358 union_variants.push(SchemaRef {
2359 target: inline_type_name,
2360 nullable: false,
2361 });
2362 }
2363 }
2364 }
2365 _ => {
2366 let context = parent_name.unwrap_or("Inline");
2368 let inline_type_name = self.generate_context_aware_name(
2369 context,
2370 "Variant",
2371 variant_index,
2372 None,
2373 );
2374 self.add_inline_schema(
2375 &inline_type_name,
2376 variant_schema,
2377 dependencies,
2378 )?;
2379 union_variants.push(SchemaRef {
2380 target: inline_type_name,
2381 nullable: false,
2382 });
2383 }
2384 }
2385 }
2386 SchemaType::Reference { target } => {
2388 union_variants.push(SchemaRef {
2389 target: target.clone(),
2390 nullable: false,
2391 });
2392 }
2393 _ => {
2395 let context = parent_name.unwrap_or("Inline");
2396 let inline_type_name = self.generate_context_aware_name(
2397 context,
2398 "Variant",
2399 variant_index,
2400 None,
2401 );
2402 self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
2403 union_variants.push(SchemaRef {
2404 target: inline_type_name,
2405 nullable: false,
2406 });
2407 }
2408 }
2409 }
2410 }
2411
2412 if !union_variants.is_empty() {
2413 return Ok(SchemaType::Union {
2414 variants: union_variants,
2415 });
2416 }
2417
2418 Ok(SchemaType::Primitive {
2420 rust_type: "serde_json::Value".to_string(),
2421 })
2422 }
2423
2424 fn add_inline_schema(
2425 &mut self,
2426 type_name: &str,
2427 schema: &Schema,
2428 dependencies: &mut HashSet<String>,
2429 ) -> Result<()> {
2430 if let Some(schema_type) = schema.schema_type() {
2432 match schema_type {
2433 OpenApiSchemaType::String
2434 | OpenApiSchemaType::Integer
2435 | OpenApiSchemaType::Number
2436 | OpenApiSchemaType::Boolean => {
2437 let rust_type =
2438 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
2439
2440 self.resolved_cache.insert(
2442 type_name.to_string(),
2443 AnalyzedSchema {
2444 name: type_name.to_string(),
2445 original: serde_json::to_value(schema).unwrap_or(Value::Null),
2446 schema_type: SchemaType::Primitive { rust_type },
2447 dependencies: HashSet::new(),
2448 nullable: false,
2449 description: schema.details().description.clone(),
2450 default: None,
2451 },
2452 );
2453 return Ok(());
2454 }
2455 _ => {}
2456 }
2457 }
2458
2459 let previous_schema_name = self.current_schema_name.take();
2463 self.current_schema_name = Some(type_name.to_string());
2464 let analyzed = self.analyze_schema_value(schema, type_name)?;
2465 self.current_schema_name = previous_schema_name;
2466
2467 self.resolved_cache.insert(type_name.to_string(), analyzed);
2469
2470 if let Some(cached) = self.resolved_cache.get(type_name) {
2472 for dep in &cached.dependencies {
2473 dependencies.insert(dep.clone());
2474 }
2475 }
2476
2477 Ok(())
2478 }
2479
2480 fn extract_inline_discriminator_value(
2481 &self,
2482 schema: &Schema,
2483 discriminator_field: &str,
2484 variant_index: usize,
2485 ) -> String {
2486 if let Some(properties) = &schema.details().properties {
2488 if let Some(discriminator_prop) = properties.get(discriminator_field) {
2489 if let Some(enum_values) = &discriminator_prop.details().enum_values {
2491 if enum_values.len() == 1 {
2492 if let Some(value) = enum_values[0].as_str() {
2493 return value.to_string();
2494 }
2495 }
2496 }
2497 if let Some(const_value) = discriminator_prop.details().extra.get("const") {
2499 if let Some(value) = const_value.as_str() {
2500 return value.to_string();
2501 }
2502 }
2503 if let Some(const_value) = &discriminator_prop.details().const_value {
2505 if let Some(value) = const_value.as_str() {
2506 return value.to_string();
2507 }
2508 }
2509 }
2510 }
2511
2512 if let Some(inferred_name) = self.infer_variant_name_from_structure(schema, variant_index) {
2514 return inferred_name;
2515 }
2516
2517 format!("variant_{variant_index}")
2519 }
2520
2521 fn infer_variant_name_from_structure(
2522 &self,
2523 schema: &Schema,
2524 _variant_index: usize,
2525 ) -> Option<String> {
2526 let details = schema.details();
2527
2528 if let Some(properties) = &details.properties {
2530 if properties.contains_key("text") && properties.len() <= 3 {
2532 return Some("text".to_string());
2533 }
2534 if properties.contains_key("image") || properties.contains_key("source") {
2535 return Some("image".to_string());
2536 }
2537 if properties.contains_key("document") {
2538 return Some("document".to_string());
2539 }
2540 if properties.contains_key("tool_use_id") || properties.contains_key("tool_result") {
2541 return Some("tool_result".to_string());
2542 }
2543 if properties.contains_key("content") && properties.contains_key("is_error") {
2544 return Some("tool_result".to_string());
2545 }
2546 if properties.contains_key("partial_json") {
2547 return Some("partial_json".to_string());
2548 }
2549
2550 let property_names: Vec<&String> = properties.keys().collect();
2552
2553 for prop_name in &property_names {
2555 if prop_name.contains("result") {
2556 return Some("result".to_string());
2557 }
2558 if prop_name.contains("error") {
2559 return Some("error".to_string());
2560 }
2561 if prop_name.contains("content") && property_names.len() <= 2 {
2562 return Some("content".to_string());
2563 }
2564 }
2565
2566 let significant_props = property_names
2568 .iter()
2569 .filter(|&name| !["type", "id", "cache_control"].contains(&name.as_str()))
2570 .collect::<Vec<_>>();
2571
2572 if significant_props.len() == 1 {
2573 return Some((*significant_props[0]).clone());
2574 }
2575 }
2576
2577 if let Some(description) = &details.description {
2579 let desc_lower = description.to_lowercase();
2580 if desc_lower.contains("text") && desc_lower.len() < 100 {
2581 return Some("text".to_string());
2582 }
2583 if desc_lower.contains("image") {
2584 return Some("image".to_string());
2585 }
2586 if desc_lower.contains("document") {
2587 return Some("document".to_string());
2588 }
2589 if desc_lower.contains("tool") && desc_lower.contains("result") {
2590 return Some("tool_result".to_string());
2591 }
2592 }
2593
2594 None
2595 }
2596
2597 fn discriminator_to_variant_name(&self, discriminator: &str) -> String {
2598 if discriminator.is_empty() {
2600 return "Variant".to_string();
2601 }
2602
2603 let mut result = String::new();
2604 let mut next_upper = true;
2605
2606 for c in discriminator.chars() {
2607 match c {
2608 'a'..='z' => {
2609 if next_upper {
2610 result.push(c.to_ascii_uppercase());
2611 next_upper = false;
2612 } else {
2613 result.push(c);
2614 }
2615 }
2616 'A'..='Z' => {
2617 result.push(c);
2618 next_upper = false;
2619 }
2620 '0'..='9' => {
2621 result.push(c);
2622 next_upper = false;
2623 }
2624 '_' | '-' | '.' | ' ' | '/' | '\\' => {
2625 next_upper = true;
2627 }
2628 _ => {
2629 next_upper = true;
2631 }
2632 }
2633 }
2634
2635 if result.is_empty() || result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2637 result = format!("Variant{result}");
2638 }
2639
2640 result
2641 }
2642
2643 fn ensure_unique_variant_name(
2644 &self,
2645 base_name: String,
2646 used_names: &mut std::collections::HashSet<String>,
2647 ) -> String {
2648 let mut candidate = base_name.clone();
2649 let mut counter = 1;
2650
2651 while used_names.contains(&candidate) {
2652 counter += 1;
2653 candidate = format!("{base_name}{counter}");
2654 }
2655
2656 used_names.insert(candidate.clone());
2657 candidate
2658 }
2659
2660 fn generate_inline_type_name(&self, schema: &Schema, variant_index: usize) -> String {
2661 if let Some(meaningful_name) = self.infer_type_name_from_structure(schema) {
2663 return meaningful_name;
2664 }
2665
2666 let context = self.current_schema_name.as_deref().unwrap_or("Inline");
2668 self.generate_context_aware_name(context, "Variant", variant_index, Some(schema))
2669 }
2670
2671 fn infer_type_name_from_structure(&self, schema: &Schema) -> Option<String> {
2672 let details = schema.details();
2673
2674 if let Some(description) = &details.description {
2676 if let Some(name_from_desc) = self.extract_type_name_from_description(description) {
2677 return Some(name_from_desc);
2678 }
2679 }
2680
2681 if let Some(properties) = &details.properties {
2683 if let Some(name_from_props) = self.extract_type_name_from_properties(properties) {
2684 return Some(format!("{name_from_props}Block"));
2685 }
2686 }
2687
2688 None
2689 }
2690
2691 fn extract_type_name_from_description(&self, description: &str) -> Option<String> {
2692 if description.len() > 100 || description.contains('\n') {
2694 return None;
2695 }
2696
2697 let words: Vec<&str> = description
2699 .split_whitespace()
2700 .take(2) .filter(|word| {
2702 let w = word.to_lowercase();
2703 word.len() > 2
2704 && ![
2705 "the", "and", "for", "with", "that", "this", "are", "can", "will", "was",
2706 ]
2707 .contains(&w.as_str())
2708 })
2709 .collect();
2710
2711 if words.is_empty() {
2712 return None;
2713 }
2714
2715 let combined = words.join("_");
2717 let pascal_name = self.discriminator_to_variant_name(&combined);
2718
2719 if !pascal_name.ends_with("Content")
2721 && !pascal_name.ends_with("Block")
2722 && !pascal_name.ends_with("Type")
2723 {
2724 Some(format!("{pascal_name}Content"))
2725 } else {
2726 Some(pascal_name)
2727 }
2728 }
2729
2730 fn extract_type_name_from_properties(
2731 &self,
2732 properties: &std::collections::BTreeMap<String, crate::openapi::Schema>,
2733 ) -> Option<String> {
2734 let significant_props: Vec<&String> = properties
2736 .keys()
2737 .filter(|name| !["type", "id", "cache_control"].contains(&name.as_str()))
2738 .collect();
2739
2740 if significant_props.is_empty() {
2741 return None;
2742 }
2743
2744 if significant_props.len() == 1 {
2746 let prop_name = significant_props[0];
2747 return Some(self.discriminator_to_variant_name(prop_name));
2748 }
2749
2750 let mut sorted_props = significant_props.clone();
2753 sorted_props.sort();
2754 if let Some(first_prop) = sorted_props.first() {
2755 return Some(self.discriminator_to_variant_name(first_prop));
2756 }
2757
2758 None
2759 }
2760
2761 fn openapi_type_to_rust_type(
2762 &self,
2763 openapi_type: OpenApiSchemaType,
2764 details: &crate::openapi::SchemaDetails,
2765 ) -> String {
2766 match openapi_type {
2767 OpenApiSchemaType::String => "String".to_string(),
2768 OpenApiSchemaType::Integer => self.get_number_rust_type(openapi_type, details),
2769 OpenApiSchemaType::Number => self.get_number_rust_type(openapi_type, details),
2770 OpenApiSchemaType::Boolean => "bool".to_string(),
2771 OpenApiSchemaType::Array => "Vec<serde_json::Value>".to_string(), OpenApiSchemaType::Object => "serde_json::Value".to_string(), OpenApiSchemaType::Null => "()".to_string(), }
2775 }
2776
2777 #[allow(dead_code)]
2778 fn fallback_discriminator_value(&self, schema_name: &str) -> String {
2779 self.fallback_discriminator_value_for_field(schema_name, "type")
2780 }
2781
2782 fn fallback_discriminator_value_for_field(
2783 &self,
2784 schema_name: &str,
2785 field_name: &str,
2786 ) -> String {
2787 if let Some(ref_schema) = self.schemas.get(schema_name) {
2789 if let Some(extracted) =
2790 self.extract_discriminator_value_for_field(ref_schema, field_name)
2791 {
2792 return extracted;
2793 }
2794 }
2795
2796 self.generate_discriminator_value_from_name(schema_name)
2798 }
2799
2800 fn generate_discriminator_value_from_name(&self, schema_name: &str) -> String {
2801 let mut result = String::new();
2803 let mut chars = schema_name.chars().peekable();
2804 let mut first = true;
2805
2806 while let Some(c) = chars.next() {
2807 if c.is_uppercase()
2808 && !first
2809 && chars
2810 .peek()
2811 .map(|&next| next.is_lowercase())
2812 .unwrap_or(false)
2813 {
2814 result.push('.');
2815 }
2816 result.push(c.to_ascii_lowercase());
2817 first = false;
2818 }
2819
2820 if result.ends_with("event") {
2822 result = result[..result.len() - 5].to_string();
2823 }
2824
2825 if schema_name.starts_with("Response") && !result.starts_with("response.") {
2827 result = format!("response.{}", result.trim_start_matches("response"));
2828 }
2829
2830 result
2831 }
2832
2833 fn to_rust_variant_name(&self, schema_name: &str) -> String {
2834 let mut name = schema_name;
2836
2837 if name.starts_with("Response") && name.len() > 8 {
2839 name = &name[8..]; }
2841
2842 if name.ends_with("Event") && name.len() > 5 {
2844 name = &name[..name.len() - 5]; }
2846
2847 name = name.trim_matches('_');
2849
2850 if name.is_empty() {
2852 schema_name.to_string()
2853 } else {
2854 self.discriminator_to_variant_name(name)
2856 }
2857 }
2858
2859 fn analyze_array_schema(
2860 &mut self,
2861 schema: &Schema,
2862 parent_schema_name: &str,
2863 dependencies: &mut HashSet<String>,
2864 ) -> Result<SchemaType> {
2865 let details = schema.details();
2866
2867 if let Some(items_schema) = &details.items {
2869 let item_type = match items_schema.as_ref() {
2871 Schema::Reference { reference, .. } => {
2872 let target = self
2874 .extract_schema_name(reference)
2875 .ok_or_else(|| GeneratorError::UnresolvedReference(reference.to_string()))?
2876 .to_string();
2877 dependencies.insert(target.clone());
2878 SchemaType::Reference { target }
2879 }
2880 Schema::RecursiveRef { recursive_ref, .. } => {
2881 if recursive_ref == "#" {
2883 let target = self
2885 .find_recursive_anchor_schema()
2886 .unwrap_or_else(|| parent_schema_name.to_string());
2887 dependencies.insert(target.clone());
2888 SchemaType::Reference { target }
2889 } else {
2890 let target = self
2891 .extract_schema_name(recursive_ref)
2892 .unwrap_or("RecursiveType")
2893 .to_string();
2894 dependencies.insert(target.clone());
2895 SchemaType::Reference { target }
2896 }
2897 }
2898 Schema::Typed { schema_type, .. } => {
2899 match schema_type {
2901 OpenApiSchemaType::String => SchemaType::Primitive {
2902 rust_type: "String".to_string(),
2903 },
2904 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
2905 let details = items_schema.details();
2906 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
2907 SchemaType::Primitive { rust_type }
2908 }
2909 OpenApiSchemaType::Boolean => SchemaType::Primitive {
2910 rust_type: "bool".to_string(),
2911 },
2912 OpenApiSchemaType::Object => {
2913 let object_type_name = format!("{parent_schema_name}Item");
2915
2916 let object_type =
2918 self.analyze_object_schema(items_schema, dependencies)?;
2919
2920 let inline_schema = AnalyzedSchema {
2922 name: object_type_name.clone(),
2923 original: serde_json::to_value(items_schema).unwrap_or(Value::Null),
2924 schema_type: object_type,
2925 dependencies: dependencies.clone(),
2926 nullable: false,
2927 description: items_schema.details().description.clone(),
2928 default: None,
2929 };
2930
2931 self.resolved_cache
2933 .insert(object_type_name.clone(), inline_schema);
2934 dependencies.insert(object_type_name.clone());
2935
2936 SchemaType::Reference {
2938 target: object_type_name,
2939 }
2940 }
2941 OpenApiSchemaType::Array => {
2942 self.analyze_array_schema(
2944 items_schema,
2945 parent_schema_name,
2946 dependencies,
2947 )?
2948 }
2949 _ => SchemaType::Primitive {
2950 rust_type: "serde_json::Value".to_string(),
2951 },
2952 }
2953 }
2954 Schema::OneOf { .. } | Schema::AnyOf { .. } => {
2955 let analyzed = self.analyze_schema_value(items_schema, "ArrayItem")?;
2957
2958 match &analyzed.schema_type {
2960 SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => {
2961 let union_name = format!("{parent_schema_name}ItemUnion");
2964
2965 let mut union_schema = analyzed;
2967 union_schema.name = union_name.clone();
2968
2969 self.resolved_cache.insert(union_name.clone(), union_schema);
2971
2972 dependencies.insert(union_name.clone());
2974
2975 SchemaType::Reference { target: union_name }
2977 }
2978 _ => analyzed.schema_type,
2979 }
2980 }
2981 Schema::Untyped { .. } => {
2982 if let Some(inferred) = items_schema.inferred_type() {
2984 match inferred {
2985 OpenApiSchemaType::Object => {
2986 let object_type_name = format!("{parent_schema_name}Item");
2988
2989 let object_type =
2991 self.analyze_object_schema(items_schema, dependencies)?;
2992
2993 let inline_schema = AnalyzedSchema {
2995 name: object_type_name.clone(),
2996 original: serde_json::to_value(items_schema)
2997 .unwrap_or(Value::Null),
2998 schema_type: object_type,
2999 dependencies: dependencies.clone(),
3000 nullable: false,
3001 description: items_schema.details().description.clone(),
3002 default: None,
3003 };
3004
3005 self.resolved_cache
3007 .insert(object_type_name.clone(), inline_schema);
3008 dependencies.insert(object_type_name.clone());
3009
3010 SchemaType::Reference {
3012 target: object_type_name,
3013 }
3014 }
3015 OpenApiSchemaType::String => SchemaType::Primitive {
3016 rust_type: "String".to_string(),
3017 },
3018 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
3019 let details = items_schema.details();
3020 let rust_type = self.get_number_rust_type(inferred, details);
3021 SchemaType::Primitive { rust_type }
3022 }
3023 OpenApiSchemaType::Boolean => SchemaType::Primitive {
3024 rust_type: "bool".to_string(),
3025 },
3026 _ => SchemaType::Primitive {
3027 rust_type: "serde_json::Value".to_string(),
3028 },
3029 }
3030 } else {
3031 SchemaType::Primitive {
3032 rust_type: "serde_json::Value".to_string(),
3033 }
3034 }
3035 }
3036 _ => SchemaType::Primitive {
3037 rust_type: "serde_json::Value".to_string(),
3038 },
3039 };
3040
3041 Ok(SchemaType::Array {
3042 item_type: Box::new(item_type),
3043 })
3044 } else {
3045 Ok(SchemaType::Primitive {
3047 rust_type: "Vec<serde_json::Value>".to_string(),
3048 })
3049 }
3050 }
3051
3052 fn get_number_rust_type(
3053 &self,
3054 schema_type: OpenApiSchemaType,
3055 details: &crate::openapi::SchemaDetails,
3056 ) -> String {
3057 match schema_type {
3058 OpenApiSchemaType::Integer => {
3059 match details.format.as_deref() {
3061 Some("int32") => "i32".to_string(),
3062 Some("int64") => "i64".to_string(),
3063 _ => "i64".to_string(), }
3065 }
3066 OpenApiSchemaType::Number => {
3067 match details.format.as_deref() {
3069 Some("float") => "f32".to_string(),
3070 Some("double") => "f64".to_string(),
3071 _ => "f64".to_string(), }
3073 }
3074 _ => "serde_json::Value".to_string(), }
3076 }
3077
3078 fn analyze_anyof_union(
3079 &mut self,
3080 any_of_schemas: &[Schema],
3081 discriminator: Option<&Discriminator>,
3082 dependencies: &mut HashSet<String>,
3083 context_name: &str,
3084 ) -> Result<SchemaType> {
3085 if any_of_schemas.len() == 2 {
3089 let null_count = any_of_schemas
3090 .iter()
3091 .filter(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
3092 .count();
3093 if null_count == 1 {
3094 for schema in any_of_schemas {
3096 if !matches!(schema.schema_type(), Some(OpenApiSchemaType::Null)) {
3097 return self
3100 .analyze_schema_value(schema, context_name)
3101 .map(|a| a.schema_type);
3102 }
3103 }
3104 }
3105 }
3106
3107 let has_refs = any_of_schemas.iter().any(|s| s.is_reference());
3109 let has_objects = any_of_schemas.iter().any(|s| {
3110 matches!(s.schema_type(), Some(OpenApiSchemaType::Object))
3111 || s.inferred_type() == Some(OpenApiSchemaType::Object)
3112 });
3113 let has_arrays = any_of_schemas
3114 .iter()
3115 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Array)));
3116
3117 let all_string_like = any_of_schemas.iter().all(|s| {
3120 matches!(s.schema_type(), Some(OpenApiSchemaType::String))
3121 || s.details().const_value.is_some()
3122 });
3123
3124 if (has_refs || has_objects || has_arrays || any_of_schemas.len() > 1) && !all_string_like {
3125 if let Some(disc) = discriminator {
3127 return self.analyze_oneof_union(any_of_schemas, Some(disc), None, dependencies);
3129 }
3130
3131 if let Some(disc_field) = self.detect_discriminator_field(any_of_schemas) {
3133 return self.analyze_oneof_union(
3134 any_of_schemas,
3135 Some(&Discriminator {
3136 property_name: disc_field,
3137 mapping: None,
3138 extra: BTreeMap::new(),
3139 }),
3140 None,
3141 dependencies,
3142 );
3143 }
3144
3145 let mut variants = Vec::new();
3147
3148 for schema in any_of_schemas {
3149 if let Some(ref_str) = schema.reference() {
3150 if let Some(target) = self.extract_schema_name(ref_str) {
3151 dependencies.insert(target.to_string());
3152 variants.push(SchemaRef {
3153 target: target.to_string(),
3154 nullable: false,
3155 });
3156 }
3157 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object))
3158 || schema.inferred_type() == Some(OpenApiSchemaType::Object)
3159 {
3160 let inline_index = variants.len();
3162 let inline_type_name = self.generate_inline_type_name(schema, inline_index);
3163
3164 self.add_inline_schema(&inline_type_name, schema, dependencies)?;
3166
3167 variants.push(SchemaRef {
3168 target: inline_type_name,
3169 nullable: false,
3170 });
3171 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Array)) {
3172 let array_type =
3174 self.analyze_array_schema(schema, context_name, dependencies)?;
3175
3176 let array_type_name = if let Some(items_schema) = &schema.details().items {
3178 if let Some(ref_str) = items_schema.reference() {
3179 if let Some(item_type_name) = self.extract_schema_name(ref_str) {
3180 dependencies.insert(item_type_name.to_string());
3181 format!("{item_type_name}Array")
3182 } else {
3183 self.generate_context_aware_name(
3184 context_name,
3185 "Array",
3186 variants.len(),
3187 Some(schema),
3188 )
3189 }
3190 } else {
3191 self.generate_context_aware_name(
3192 context_name,
3193 "Array",
3194 variants.len(),
3195 Some(schema),
3196 )
3197 }
3198 } else {
3199 self.generate_context_aware_name(
3200 context_name,
3201 "Array",
3202 variants.len(),
3203 Some(schema),
3204 )
3205 };
3206
3207 self.resolved_cache.insert(
3209 array_type_name.clone(),
3210 AnalyzedSchema {
3211 name: array_type_name.clone(),
3212 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3213 schema_type: array_type,
3214 dependencies: HashSet::new(),
3215 nullable: false,
3216 description: Some("Array variant in union".to_string()),
3217 default: None,
3218 },
3219 );
3220
3221 dependencies.insert(array_type_name.clone());
3223
3224 variants.push(SchemaRef {
3225 target: array_type_name,
3226 nullable: false,
3227 });
3228 } else if let Some(schema_type) = schema.schema_type() {
3229 let inline_index = variants.len();
3231
3232 let inline_type_name = match schema_type {
3234 OpenApiSchemaType::String => {
3235 if inline_index == 0 {
3238 format!("{context_name}String")
3239 } else {
3240 format!("{context_name}StringVariant{inline_index}")
3241 }
3242 }
3243 OpenApiSchemaType::Number => {
3244 if inline_index == 0 {
3245 format!("{context_name}Number")
3246 } else {
3247 format!("{context_name}NumberVariant{inline_index}")
3248 }
3249 }
3250 OpenApiSchemaType::Integer => {
3251 if inline_index == 0 {
3252 format!("{context_name}Integer")
3253 } else {
3254 format!("{context_name}IntegerVariant{inline_index}")
3255 }
3256 }
3257 OpenApiSchemaType::Boolean => {
3258 if inline_index == 0 {
3259 format!("{context_name}Boolean")
3260 } else {
3261 format!("{context_name}BooleanVariant{inline_index}")
3262 }
3263 }
3264 _ => format!("{context_name}Variant{inline_index}"),
3265 };
3266
3267 let rust_type =
3268 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
3269
3270 self.resolved_cache.insert(
3272 inline_type_name.clone(),
3273 AnalyzedSchema {
3274 name: inline_type_name.clone(),
3275 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3276 schema_type: SchemaType::Primitive { rust_type },
3277 dependencies: HashSet::new(),
3278 nullable: false,
3279 description: schema.details().description.clone(),
3280 default: None,
3281 },
3282 );
3283
3284 dependencies.insert(inline_type_name.clone());
3286
3287 variants.push(SchemaRef {
3288 target: inline_type_name,
3289 nullable: false,
3290 });
3291 }
3292 }
3293
3294 if !variants.is_empty() {
3295 return Ok(SchemaType::Union { variants });
3296 }
3297 }
3298
3299 let all_strings = any_of_schemas.iter().all(|schema| {
3301 matches!(schema.schema_type(), Some(OpenApiSchemaType::String))
3302 || schema.details().const_value.is_some()
3303 });
3304
3305 if all_strings {
3306 let mut enum_values = Vec::new();
3308 let mut has_open_string = false;
3309
3310 for schema in any_of_schemas {
3311 if let Some(const_val) = &schema.details().const_value {
3312 if let Some(const_str) = const_val.as_str() {
3313 enum_values.push(const_str.to_string());
3314 }
3315 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::String)) {
3316 has_open_string = true;
3317 }
3318 }
3319
3320 if !enum_values.is_empty() {
3321 if has_open_string {
3322 return Ok(SchemaType::ExtensibleEnum {
3325 known_values: enum_values,
3326 });
3327 } else {
3328 return Ok(SchemaType::StringEnum {
3330 values: enum_values,
3331 });
3332 }
3333 }
3334 }
3335
3336 Ok(SchemaType::Primitive {
3338 rust_type: "serde_json::Value".to_string(),
3339 })
3340 }
3341
3342 fn find_recursive_anchor_schema(&self) -> Option<String> {
3344 for (schema_name, schema) in &self.schemas {
3346 let details = schema.details();
3347 if details.recursive_anchor == Some(true) {
3348 return Some(schema_name.clone());
3349 }
3350 }
3351
3352 None
3356 }
3357
3358 fn should_use_dynamic_json(&self, schema: &Schema) -> bool {
3361 if let Schema::AnyOf { any_of, .. } = schema {
3363 if any_of.len() == 2 {
3364 let has_null = any_of
3365 .iter()
3366 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)));
3367 let has_empty_object = any_of.iter().any(|s| self.is_dynamic_object_pattern(s));
3368
3369 if has_null && has_empty_object {
3370 return true;
3371 }
3372 }
3373 }
3374
3375 self.is_dynamic_object_pattern(schema)
3377 }
3378
3379 fn is_dynamic_object_pattern(&self, schema: &Schema) -> bool {
3381 let is_object = match schema.schema_type() {
3383 Some(OpenApiSchemaType::Object) => true,
3384 None => schema.inferred_type() == Some(OpenApiSchemaType::Object),
3385 _ => false,
3386 };
3387
3388 if !is_object {
3389 return false;
3390 }
3391
3392 let details = schema.details();
3393
3394 if self.has_explicit_additional_properties(schema) {
3397 return false;
3398 }
3399
3400 let no_properties = details
3402 .properties
3403 .as_ref()
3404 .map(|props| props.is_empty())
3405 .unwrap_or(true);
3406
3407 if no_properties {
3408 let has_structural_constraints =
3410 details.required.as_ref()
3412 .map(|req| req.iter().any(|r| r != "type"))
3413 .unwrap_or(false)
3414 || details.extra.contains_key("patternProperties")
3416 || details.extra.contains_key("propertyNames")
3418 || details.extra.contains_key("minProperties")
3420 || details.extra.contains_key("maxProperties")
3421 || details.extra.contains_key("dependencies")
3423 || details.extra.contains_key("if")
3425 || details.extra.contains_key("then")
3426 || details.extra.contains_key("else");
3427
3428 return !has_structural_constraints;
3429 }
3430
3431 false
3432 }
3433
3434 fn has_explicit_additional_properties(&self, schema: &Schema) -> bool {
3436 let details = schema.details();
3437
3438 matches!(
3440 &details.additional_properties,
3441 Some(crate::openapi::AdditionalProperties::Boolean(true))
3442 | Some(crate::openapi::AdditionalProperties::Schema(_))
3443 )
3444 }
3445
3446 fn analyze_operations(&mut self, analysis: &mut SchemaAnalysis) -> Result<()> {
3448 let spec: crate::openapi::OpenApiSpec = serde_json::from_value(self.openapi_spec.clone())
3449 .map_err(GeneratorError::ParseError)?;
3450
3451 if let Some(paths) = &spec.paths {
3452 for (path, path_item) in paths {
3453 for (method, operation) in path_item.operations() {
3454 let operation_id = operation
3456 .operation_id
3457 .clone()
3458 .unwrap_or_else(|| Self::generate_operation_id(method, path));
3459
3460 let op_info = self.analyze_single_operation(
3461 &operation_id,
3462 method,
3463 path,
3464 operation,
3465 path_item.parameters.as_ref(),
3466 analysis,
3467 )?;
3468 analysis.operations.insert(operation_id, op_info);
3469 }
3470 }
3471 }
3472 Ok(())
3473 }
3474
3475 fn generate_operation_id(method: &str, path: &str) -> String {
3478 let mut operation_id = method.to_lowercase();
3480
3481 let path_parts: Vec<&str> = path.trim_start_matches('/').split('/').collect();
3483
3484 for part in path_parts {
3485 if part.is_empty() {
3486 continue;
3487 }
3488
3489 let cleaned_part = if part.starts_with('{') && part.ends_with('}') {
3491 &part[1..part.len() - 1]
3492 } else {
3493 part
3494 };
3495
3496 let pascal_case_part = cleaned_part
3498 .split(&['-', '_'][..])
3499 .map(|s| {
3500 let mut chars = s.chars();
3501 match chars.next() {
3502 None => String::new(),
3503 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
3504 }
3505 })
3506 .collect::<String>();
3507
3508 operation_id.push_str(&pascal_case_part);
3509 }
3510
3511 operation_id
3512 }
3513
3514 fn analyze_single_operation(
3516 &mut self,
3517 operation_id: &str,
3518 method: &str,
3519 path: &str,
3520 operation: &crate::openapi::Operation,
3521 path_item_parameters: Option<&Vec<crate::openapi::Parameter>>,
3522 _analysis: &mut SchemaAnalysis,
3523 ) -> Result<OperationInfo> {
3524 let mut op_info = OperationInfo {
3525 operation_id: operation_id.to_string(),
3526 method: method.to_uppercase(),
3527 path: path.to_string(),
3528 summary: operation.summary.clone(),
3529 description: operation.description.clone(),
3530 request_body: None,
3531 response_schemas: BTreeMap::new(),
3532 parameters: Vec::new(),
3533 supports_streaming: false, stream_parameter: None, };
3536
3537 if let Some(request_body) = &operation.request_body
3539 && let Some((content_type, maybe_schema)) = request_body.best_content()
3540 {
3541 use crate::openapi::{is_form_urlencoded_media_type, is_json_media_type};
3542 op_info.request_body = if is_json_media_type(content_type) {
3543 maybe_schema
3544 .map(|s| {
3545 self.resolve_or_inline_schema(s, operation_id, "Request")
3546 .map(|name| RequestBodyContent::Json { schema_name: name })
3547 })
3548 .transpose()?
3549 } else if is_form_urlencoded_media_type(content_type) {
3550 maybe_schema
3551 .map(|s| {
3552 self.resolve_or_inline_schema(s, operation_id, "Request")
3553 .map(|name| RequestBodyContent::FormUrlEncoded { schema_name: name })
3554 })
3555 .transpose()?
3556 } else {
3557 match content_type {
3558 "multipart/form-data" => Some(RequestBodyContent::Multipart),
3559 "application/octet-stream" => Some(RequestBodyContent::OctetStream),
3560 "text/plain" => Some(RequestBodyContent::TextPlain),
3561 _ => None,
3562 }
3563 };
3564 }
3565
3566 if let Some(responses) = &operation.responses {
3568 for (status_code, response) in responses {
3569 if let Some(schema) = response.json_schema() {
3570 if let Some(schema_ref) = schema.reference() {
3571 if let Some(schema_name) = self.extract_schema_name(schema_ref) {
3573 op_info
3574 .response_schemas
3575 .insert(status_code.clone(), schema_name.to_string());
3576 }
3577 } else {
3578 let synthetic_name =
3580 self.generate_inline_response_type_name(operation_id, status_code);
3581
3582 let mut deps = HashSet::new();
3584 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
3585
3586 op_info
3587 .response_schemas
3588 .insert(status_code.clone(), synthetic_name);
3589 }
3590 }
3591 }
3592 }
3593
3594 if let Some(parameters) = &operation.parameters {
3596 for param in parameters {
3597 let resolved = self.resolve_parameter(param);
3598 if let Some(param_info) = self.analyze_parameter(&resolved)? {
3599 op_info.parameters.push(param_info);
3600 }
3601 }
3602 }
3603
3604 if let Some(path_params) = path_item_parameters {
3606 let existing_keys: std::collections::HashSet<(String, String)> = op_info
3607 .parameters
3608 .iter()
3609 .map(|p| (p.name.clone(), p.location.clone()))
3610 .collect();
3611 for param in path_params {
3612 let resolved = self.resolve_parameter(param);
3613 if let Some(param_info) = self.analyze_parameter(&resolved)? {
3614 if !existing_keys
3615 .contains(&(param_info.name.clone(), param_info.location.clone()))
3616 {
3617 op_info.parameters.push(param_info);
3618 }
3619 }
3620 }
3621 }
3622
3623 Ok(op_info)
3624 }
3625
3626 fn generate_inline_response_type_name(&self, operation_id: &str, _status_code: &str) -> String {
3628 use heck::ToPascalCase;
3629 let base_name = operation_id.replace('.', "_").to_pascal_case();
3633 format!("{}Response", base_name)
3634 }
3635
3636 fn generate_inline_request_type_name(&self, operation_id: &str) -> String {
3638 use heck::ToPascalCase;
3639 let base_name = operation_id.replace('.', "_").to_pascal_case();
3643 format!("{}Request", base_name)
3644 }
3645
3646 fn resolve_or_inline_schema(
3649 &mut self,
3650 schema: &crate::openapi::Schema,
3651 operation_id: &str,
3652 suffix: &str,
3653 ) -> Result<String> {
3654 if let Some(schema_ref) = schema.reference()
3655 && let Some(schema_name) = self.extract_schema_name(schema_ref)
3656 {
3657 return Ok(schema_name.to_string());
3658 }
3659 let synthetic_name = if suffix == "Request" {
3661 self.generate_inline_request_type_name(operation_id)
3662 } else {
3663 self.generate_inline_response_type_name(operation_id, "")
3664 };
3665 let mut deps = HashSet::new();
3666 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
3667 Ok(synthetic_name)
3668 }
3669
3670 fn resolve_parameter<'a>(
3673 &'a self,
3674 param: &'a crate::openapi::Parameter,
3675 ) -> std::borrow::Cow<'a, crate::openapi::Parameter> {
3676 if let Some(ref_str) = param.extra.get("$ref").and_then(|v| v.as_str()) {
3677 if let Some(param_name) = ref_str.strip_prefix("#/components/parameters/") {
3678 if let Some(resolved) = self.component_parameters.get(param_name) {
3679 return std::borrow::Cow::Borrowed(resolved);
3680 }
3681 }
3682 }
3683 std::borrow::Cow::Borrowed(param)
3684 }
3685
3686 fn analyze_parameter(
3688 &self,
3689 param: &crate::openapi::Parameter,
3690 ) -> Result<Option<ParameterInfo>> {
3691 let name = param.name.as_deref().unwrap_or("");
3692 let location = param.location.as_deref().unwrap_or("");
3693 let required = param.required.unwrap_or(false);
3694
3695 let mut rust_type = "String".to_string();
3696 let mut schema_ref = None;
3697
3698 if let Some(schema) = ¶m.schema {
3699 if let Some(ref_str) = schema.reference() {
3700 schema_ref = self.extract_schema_name(ref_str).map(|s| s.to_string());
3701 } else if let Some(schema_type) = schema.schema_type() {
3702 rust_type = match schema_type {
3703 crate::openapi::SchemaType::Boolean => "bool",
3704 crate::openapi::SchemaType::Integer => "i64",
3705 crate::openapi::SchemaType::Number => "f64",
3706 crate::openapi::SchemaType::String => "String",
3707 _ => "String",
3708 }
3709 .to_string();
3710 }
3711 }
3712
3713 Ok(Some(ParameterInfo {
3714 name: name.to_string(),
3715 location: location.to_string(),
3716 required,
3717 schema_ref,
3718 rust_type,
3719 description: param.description.clone(),
3720 }))
3721 }
3722}