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(existing: &AnalyzedSchema, values: &[String]) -> bool {
1400 matches!(
1401 &existing.schema_type,
1402 SchemaType::StringEnum { values: existing_values }
1403 if existing_values == values
1404 )
1405 }
1406
1407 let mut enum_type_name = primary_name.clone();
1408 let mut should_insert = match self.resolved_cache.get(&enum_type_name) {
1409 None => true,
1410 Some(existing) if matches_values(existing, &enum_values) => false,
1411 Some(_) => {
1412 let suffix = enum_values
1415 .first()
1416 .map(|v| self.to_pascal_case(v))
1417 .unwrap_or_else(|| "Variant".to_string());
1418 let candidate = format!("{primary_name}{suffix}");
1419
1420 let resolved = match self.resolved_cache.get(&candidate) {
1421 None => Some((candidate.clone(), true)),
1422 Some(existing) if matches_values(existing, &enum_values) => {
1423 Some((candidate.clone(), false))
1424 }
1425 Some(_) => {
1426 let mut found = None;
1429 for n in 2..1000 {
1430 let numbered = format!("{candidate}_{n}");
1431 match self.resolved_cache.get(&numbered) {
1432 None => {
1433 found = Some((numbered, true));
1434 break;
1435 }
1436 Some(existing)
1437 if matches_values(existing, &enum_values) =>
1438 {
1439 found = Some((numbered, false));
1440 break;
1441 }
1442 Some(_) => continue,
1443 }
1444 }
1445 found
1446 }
1447 };
1448
1449 let (resolved_name, insert) = resolved.unwrap_or((candidate, true));
1450 enum_type_name = resolved_name;
1451 insert
1452 }
1453 };
1454
1455 if should_insert {
1458 self.resolved_cache.insert(
1459 enum_type_name.clone(),
1460 AnalyzedSchema {
1461 name: enum_type_name.clone(),
1462 original: serde_json::to_value(schema).unwrap_or(Value::Null),
1463 schema_type: SchemaType::StringEnum {
1464 values: enum_values,
1465 },
1466 dependencies: HashSet::new(),
1467 nullable: false,
1468 description: schema.details().description.clone(),
1469 default: schema.details().default.clone(),
1470 },
1471 );
1472 let _ = &mut should_insert;
1475 }
1476
1477 dependencies.insert(enum_type_name.clone());
1479 return Ok(SchemaType::Reference {
1480 target: enum_type_name,
1481 });
1482 } else {
1483 return Ok(SchemaType::Primitive {
1484 rust_type: "String".to_string(),
1485 });
1486 }
1487 }
1488 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
1489 let details = schema.details();
1490 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
1491 return Ok(SchemaType::Primitive { rust_type });
1492 }
1493 OpenApiSchemaType::Boolean => {
1494 return Ok(SchemaType::Primitive {
1495 rust_type: "bool".to_string(),
1496 });
1497 }
1498 OpenApiSchemaType::Array => {
1499 let context_name = if let Some(prop_name) = property_name {
1501 let prop_pascal = self.to_pascal_case(prop_name);
1503 format!(
1504 "{}{}",
1505 self.current_schema_name.as_deref().unwrap_or("Unknown"),
1506 prop_pascal
1507 )
1508 } else {
1509 "ArrayItem".to_string()
1511 };
1512 return self.analyze_array_schema(schema, &context_name, dependencies);
1513 }
1514 OpenApiSchemaType::Object => {
1515 if self.should_use_dynamic_json(schema) {
1517 return Ok(SchemaType::Primitive {
1518 rust_type: "serde_json::Value".to_string(),
1519 });
1520 }
1521 let object_type_name = if let Some(prop_name) = property_name {
1523 let prop_pascal = self.to_pascal_case(prop_name);
1525 format!(
1526 "{}{}",
1527 self.current_schema_name.as_deref().unwrap_or("Unknown"),
1528 prop_pascal
1529 )
1530 } else {
1531 format!(
1533 "{}Object",
1534 self.current_schema_name.as_deref().unwrap_or("Unknown")
1535 )
1536 };
1537
1538 let object_type = self.analyze_object_schema(schema, dependencies)?;
1540
1541 let inline_schema = AnalyzedSchema {
1543 name: object_type_name.clone(),
1544 original: serde_json::to_value(schema).unwrap_or(Value::Null),
1545 schema_type: object_type,
1546 dependencies: dependencies.clone(),
1547 nullable: false,
1548 description: schema.details().description.clone(),
1549 default: None,
1550 };
1551
1552 self.resolved_cache
1554 .insert(object_type_name.clone(), inline_schema);
1555 dependencies.insert(object_type_name.clone());
1556
1557 return Ok(SchemaType::Reference {
1559 target: object_type_name,
1560 });
1561 }
1562 _ => {
1563 return Ok(SchemaType::Primitive {
1564 rust_type: "serde_json::Value".to_string(),
1565 });
1566 }
1567 }
1568 }
1569
1570 if schema.is_nullable_pattern() {
1572 if let Some(non_null) = schema.non_null_variant() {
1573 return self.analyze_property_schema_with_context(
1574 non_null,
1575 property_name,
1576 dependencies,
1577 );
1578 }
1579 }
1580
1581 if self.should_use_dynamic_json(schema) {
1583 return Ok(SchemaType::Primitive {
1584 rust_type: "serde_json::Value".to_string(),
1585 });
1586 }
1587
1588 if let Schema::AllOf { all_of, .. } = schema {
1590 return self.analyze_allof_composition(all_of, dependencies);
1591 }
1592
1593 if let Some(variants) = schema.union_variants() {
1595 match variants.len().cmp(&1) {
1596 std::cmp::Ordering::Equal => {
1597 return self.analyze_property_schema_with_context(
1599 &variants[0],
1600 property_name,
1601 dependencies,
1602 );
1603 }
1604 std::cmp::Ordering::Greater => {
1605 let union_name = if let Some(prop_name) = property_name {
1608 let prop_pascal = self.to_pascal_case(prop_name);
1610 format!(
1611 "{}{}",
1612 self.current_schema_name.as_deref().unwrap_or(""),
1613 prop_pascal
1614 )
1615 } else {
1616 "UnionType".to_string()
1617 };
1618
1619 if let Schema::OneOf {
1621 one_of,
1622 discriminator,
1623 ..
1624 } = schema
1625 {
1626 let oneof_result = self.analyze_oneof_union(
1628 one_of,
1629 discriminator.as_ref(),
1630 Some(&union_name),
1631 dependencies,
1632 )?;
1633
1634 if let SchemaType::Union {
1636 variants: _union_variants,
1637 } = &oneof_result
1638 {
1639 self.resolved_cache.insert(
1641 union_name.clone(),
1642 AnalyzedSchema {
1643 name: union_name.clone(),
1644 original: serde_json::to_value(schema).unwrap_or(Value::Null),
1645 schema_type: oneof_result.clone(),
1646 dependencies: dependencies.clone(),
1647 nullable: false,
1648 description: schema.details().description.clone(),
1649 default: None,
1650 },
1651 );
1652
1653 dependencies.insert(union_name.clone());
1655 return Ok(SchemaType::Reference { target: union_name });
1656 }
1657
1658 return Ok(oneof_result);
1659 } else if let Schema::AnyOf {
1660 any_of,
1661 discriminator,
1662 ..
1663 } = schema
1664 {
1665 let union_analysis = self.analyze_anyof_union(
1667 any_of,
1668 discriminator.as_ref(),
1669 dependencies,
1670 &union_name,
1671 )?;
1672 return Ok(union_analysis);
1673 } else {
1674 let mut union_variants = Vec::new();
1677 for variant in variants {
1678 if let Some(ref_str) = variant.reference() {
1679 if let Some(target) = self.extract_schema_name(ref_str) {
1680 dependencies.insert(target.to_string());
1681 union_variants.push(SchemaRef {
1682 target: target.to_string(),
1683 nullable: false,
1684 });
1685 }
1686 }
1687 }
1688 return Ok(SchemaType::Union {
1689 variants: union_variants,
1690 });
1691 }
1692 }
1693 std::cmp::Ordering::Less => {}
1694 }
1695 }
1696
1697 if let Some(inferred_type) = schema.inferred_type() {
1699 match inferred_type {
1700 OpenApiSchemaType::Object => {
1701 if self.should_use_dynamic_json(schema) {
1703 return Ok(SchemaType::Primitive {
1704 rust_type: "serde_json::Value".to_string(),
1705 });
1706 }
1707 return self.analyze_object_schema(schema, dependencies);
1708 }
1709 OpenApiSchemaType::Array => {
1710 let context_name = if let Some(prop_name) = property_name {
1711 let prop_pascal = self.to_pascal_case(prop_name);
1713 format!(
1714 "{}{}",
1715 self.current_schema_name.as_deref().unwrap_or("Unknown"),
1716 prop_pascal
1717 )
1718 } else {
1719 "ArrayItem".to_string()
1721 };
1722 return self.analyze_array_schema(schema, &context_name, dependencies);
1723 }
1724 OpenApiSchemaType::String => {
1725 if let Some(enum_values) = schema.details().string_enum_values() {
1726 return Ok(SchemaType::StringEnum {
1727 values: enum_values,
1728 });
1729 } else {
1730 return Ok(SchemaType::Primitive {
1731 rust_type: "String".to_string(),
1732 });
1733 }
1734 }
1735 _ => {
1736 let rust_type = self.openapi_type_to_rust_type(inferred_type, schema.details());
1738 return Ok(SchemaType::Primitive { rust_type });
1739 }
1740 }
1741 }
1742
1743 Ok(SchemaType::Primitive {
1744 rust_type: "serde_json::Value".to_string(),
1745 })
1746 }
1747
1748 fn analyze_allof_composition(
1749 &mut self,
1750 all_of_schemas: &[Schema],
1751 dependencies: &mut HashSet<String>,
1752 ) -> Result<SchemaType> {
1753 if all_of_schemas.len() == 1 {
1756 if let Schema::Reference { reference, .. } = &all_of_schemas[0] {
1757 if let Some(target) = self.extract_schema_name(reference) {
1758 dependencies.insert(target.to_string());
1759 return Ok(SchemaType::Reference {
1760 target: target.to_string(),
1761 });
1762 }
1763 }
1764 }
1765
1766 let mut merged_properties = BTreeMap::new();
1768 let mut merged_required = HashSet::new();
1769 let mut descriptions = Vec::new();
1770
1771 let current_context = self.current_schema_name.clone();
1773
1774 for schema in all_of_schemas {
1775 match schema {
1776 Schema::Reference { reference, .. } => {
1777 if let Some(target) = self.extract_schema_name(reference) {
1779 dependencies.insert(target.to_string());
1780
1781 let analyzed_ref = self.analyze_schema(target)?;
1783
1784 match &analyzed_ref.schema_type {
1786 SchemaType::Object {
1787 properties,
1788 required,
1789 ..
1790 } => {
1791 for (prop_name, prop_info) in properties {
1793 merged_properties.insert(prop_name.clone(), prop_info.clone());
1794 }
1795 for req in required {
1797 merged_required.insert(req.clone());
1798 }
1799 }
1800 _ => {
1801 if let Some(ref_schema) = self.schemas.get(target).cloned() {
1803 self.merge_schema_into_properties(
1804 &ref_schema,
1805 &mut merged_properties,
1806 &mut merged_required,
1807 dependencies,
1808 )?;
1809 }
1810 }
1811 }
1812 }
1813 }
1814 Schema::Typed {
1815 schema_type: OpenApiSchemaType::Object,
1816 ..
1817 }
1818 | Schema::Untyped { .. } => {
1819 let saved_context = self.current_schema_name.clone();
1821 self.current_schema_name = current_context.clone();
1822
1823 self.merge_schema_into_properties(
1825 schema,
1826 &mut merged_properties,
1827 &mut merged_required,
1828 dependencies,
1829 )?;
1830
1831 self.current_schema_name = saved_context;
1833 }
1834 _ => {
1835 self.merge_schema_into_properties(
1838 schema,
1839 &mut merged_properties,
1840 &mut merged_required,
1841 dependencies,
1842 )?;
1843 }
1844 }
1845
1846 if let Some(desc) = &schema.details().description {
1848 descriptions.push(desc.clone());
1849 }
1850 }
1851
1852 if !merged_properties.is_empty() {
1854 Ok(SchemaType::Object {
1855 properties: merged_properties,
1856 required: merged_required,
1857 additional_properties: false,
1858 })
1859 } else {
1860 Ok(SchemaType::Composition {
1862 schemas: all_of_schemas
1863 .iter()
1864 .filter_map(|s| {
1865 if let Some(ref_str) = s.reference() {
1866 if let Some(target) = self.extract_schema_name(ref_str) {
1867 dependencies.insert(target.to_string());
1868 Some(SchemaRef {
1869 target: target.to_string(),
1870 nullable: false,
1871 })
1872 } else {
1873 None
1874 }
1875 } else {
1876 None
1877 }
1878 })
1879 .collect(),
1880 })
1881 }
1882 }
1883
1884 fn merge_schema_into_properties(
1885 &mut self,
1886 schema: &Schema,
1887 merged_properties: &mut BTreeMap<String, PropertyInfo>,
1888 merged_required: &mut HashSet<String>,
1889 dependencies: &mut HashSet<String>,
1890 ) -> Result<()> {
1891 let details = schema.details();
1892
1893 if let Some(properties) = &details.properties {
1895 for (prop_name, prop_schema) in properties {
1896 let prop_type = self.analyze_property_schema_with_context(
1897 prop_schema,
1898 Some(prop_name),
1899 dependencies,
1900 )?;
1901 let prop_details = prop_schema.details();
1902
1903 merged_properties.insert(
1904 prop_name.clone(),
1905 PropertyInfo {
1906 schema_type: prop_type,
1907 nullable: prop_details.is_nullable(),
1908 description: prop_details.description.clone(),
1909 default: prop_details.default.clone(),
1910 serde_attrs: Vec::new(),
1911 },
1912 );
1913 }
1914 }
1915
1916 if let Some(required) = &details.required {
1918 for field in required {
1919 merged_required.insert(field.clone());
1920 }
1921 }
1922
1923 Ok(())
1924 }
1925
1926 fn analyze_oneof_union(
1927 &mut self,
1928 one_of_schemas: &[Schema],
1929 discriminator: Option<&crate::openapi::Discriminator>,
1930 parent_name: Option<&str>,
1931 dependencies: &mut HashSet<String>,
1932 ) -> Result<SchemaType> {
1933 if discriminator.is_none() {
1935 return self.analyze_untagged_oneof_union(one_of_schemas, parent_name, dependencies);
1937 }
1938
1939 let discriminator_field = discriminator
1941 .ok_or_else(|| {
1942 GeneratorError::InvalidDiscriminator(
1943 "expected discriminator after guard check".to_string(),
1944 )
1945 })?
1946 .property_name
1947 .clone();
1948
1949 let mut variants = Vec::new();
1950 let mut used_variant_names = std::collections::HashSet::new();
1951
1952 for variant_schema in one_of_schemas {
1953 let ref_info = if let Some(ref_str) = variant_schema.reference() {
1955 Some((ref_str, false))
1956 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
1957 Some((recursive_ref, true))
1958 } else if let Schema::AllOf { all_of, .. } = variant_schema {
1959 if all_of.len() == 1 {
1961 if let Some(ref_str) = all_of[0].reference() {
1962 Some((ref_str, false))
1963 } else {
1964 all_of[0]
1965 .recursive_reference()
1966 .map(|recursive_ref| (recursive_ref, true))
1967 }
1968 } else {
1969 None
1970 }
1971 } else {
1972 None
1973 };
1974
1975 if let Some((ref_str, is_recursive)) = ref_info {
1976 let schema_name = if is_recursive && ref_str == "#" {
1977 self.find_recursive_anchor_schema()
1979 .or_else(|| self.current_schema_name.clone())
1980 .unwrap_or_else(|| "CompoundFilter".to_string())
1981 } else {
1982 self.extract_schema_name(ref_str)
1983 .map(|s| s.to_string())
1984 .unwrap_or_else(|| "UnknownRef".to_string())
1985 };
1986
1987 if !schema_name.is_empty() {
1988 dependencies.insert(schema_name.clone());
1989
1990 let discriminator_value = if let Some(disc) = discriminator {
1995 if let Some(mappings) = &disc.mapping {
1996 mappings
1999 .iter()
2000 .find(|(_, target_ref)| {
2001 target_ref.as_str() == ref_str
2003 || self
2004 .extract_schema_name(target_ref)
2005 .map(|s| s.to_string())
2006 == Some(schema_name.clone())
2007 })
2008 .map(|(key, _)| key.clone())
2009 .unwrap_or_else(|| {
2010 self.fallback_discriminator_value_for_field(
2011 &schema_name,
2012 &discriminator_field,
2013 )
2014 })
2015 } else {
2016 self.fallback_discriminator_value_for_field(
2017 &schema_name,
2018 &discriminator_field,
2019 )
2020 }
2021 } else {
2022 self.fallback_discriminator_value_for_field(
2023 &schema_name,
2024 &discriminator_field,
2025 )
2026 };
2027
2028 let base_name = self.to_rust_variant_name(&schema_name);
2030 let rust_name =
2031 self.ensure_unique_variant_name(base_name, &mut used_variant_names);
2032
2033 let final_discriminator_value = discriminator_value;
2035
2036 variants.push(UnionVariant {
2037 rust_name,
2038 type_name: schema_name,
2039 discriminator_value: final_discriminator_value,
2040 schema_ref: ref_str.to_string(),
2041 });
2042 }
2043 } else {
2044 let variant_index = variants.len();
2046 let inline_type_name =
2047 self.generate_inline_type_name(variant_schema, variant_index);
2048
2049 let discriminator_value = if let Some(disc) = discriminator {
2051 if let Some(mappings) = &disc.mapping {
2052 mappings
2054 .iter()
2055 .find(|(_, target_ref)| {
2056 target_ref.contains(&format!("variant_{variant_index}"))
2057 })
2058 .map(|(key, _)| key.clone())
2059 .unwrap_or_else(|| {
2060 self.extract_inline_discriminator_value(
2061 variant_schema,
2062 &discriminator_field,
2063 variant_index,
2064 )
2065 })
2066 } else {
2067 self.extract_inline_discriminator_value(
2068 variant_schema,
2069 &discriminator_field,
2070 variant_index,
2071 )
2072 }
2073 } else {
2074 self.extract_inline_discriminator_value(
2075 variant_schema,
2076 &discriminator_field,
2077 variant_index,
2078 )
2079 };
2080
2081 let base_name = if discriminator_value.starts_with("variant_") {
2083 format!("Variant{variant_index}")
2084 } else {
2085 let clean_name = self.discriminator_to_variant_name(&discriminator_value);
2087 self.to_rust_variant_name(&clean_name)
2088 };
2089 let rust_name = self.ensure_unique_variant_name(base_name, &mut used_variant_names);
2090
2091 let final_discriminator_value = discriminator_value;
2093
2094 variants.push(UnionVariant {
2095 rust_name,
2096 type_name: inline_type_name.clone(),
2097 discriminator_value: final_discriminator_value,
2098 schema_ref: format!("inline_{variant_index}"),
2099 });
2100
2101 self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
2103 }
2104 }
2105
2106 if variants.is_empty() {
2107 let mut union_variants = Vec::new();
2110
2111 for (variant_index, variant_schema) in one_of_schemas.iter().enumerate() {
2112 if let Some(ref_str) = variant_schema.reference() {
2114 if let Some(schema_name) = self.extract_schema_name(ref_str) {
2115 dependencies.insert(schema_name.to_string());
2116 union_variants.push(SchemaRef {
2117 target: schema_name.to_string(),
2118 nullable: false,
2119 });
2120 }
2121 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2122 let schema_name = if recursive_ref == "#" {
2123 self.find_recursive_anchor_schema()
2125 .or_else(|| self.current_schema_name.clone())
2126 .unwrap_or_else(|| "CompoundFilter".to_string())
2127 } else {
2128 self.extract_schema_name(recursive_ref)
2129 .map(|s| s.to_string())
2130 .unwrap_or_else(|| "RecursiveType".to_string())
2131 };
2132 dependencies.insert(schema_name.clone());
2133 union_variants.push(SchemaRef {
2134 target: schema_name,
2135 nullable: false,
2136 });
2137 } else {
2138 let context = parent_name.unwrap_or("Union");
2140 let inline_name = self.generate_context_aware_name(
2141 context,
2142 "InlineVariant",
2143 variant_index,
2144 Some(variant_schema),
2145 );
2146 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
2147 let variant_type = analyzed.schema_type;
2148
2149 for dep in &analyzed.dependencies {
2151 dependencies.insert(dep.clone());
2152 }
2153
2154 match &variant_type {
2155 SchemaType::Primitive { rust_type } => {
2157 union_variants.push(SchemaRef {
2158 target: rust_type.clone(),
2159 nullable: false,
2160 });
2161 }
2162 SchemaType::Array { item_type } => {
2164 match item_type.as_ref() {
2165 SchemaType::Primitive { rust_type } => {
2166 let type_name = format!("Vec<{rust_type}>");
2167 union_variants.push(SchemaRef {
2168 target: type_name,
2169 nullable: false,
2170 });
2171 }
2172 SchemaType::Reference { target } => {
2173 let type_name = format!("Vec<{target}>");
2174 union_variants.push(SchemaRef {
2175 target: type_name,
2176 nullable: false,
2177 });
2178 }
2179 _ => {
2180 let context = parent_name.unwrap_or("Inline");
2182 let inline_type_name = self.generate_context_aware_name(
2183 context,
2184 "Variant",
2185 variant_index,
2186 None,
2187 );
2188 self.add_inline_schema(
2189 &inline_type_name,
2190 variant_schema,
2191 dependencies,
2192 )?;
2193 union_variants.push(SchemaRef {
2194 target: inline_type_name,
2195 nullable: false,
2196 });
2197 }
2198 }
2199 }
2200 SchemaType::Reference { target } => {
2202 union_variants.push(SchemaRef {
2203 target: target.clone(),
2204 nullable: false,
2205 });
2206 }
2207 _ => {
2209 let inline_type_name = format!(
2210 "{}Variant{}",
2211 parent_name.unwrap_or("Inline"),
2212 variant_index + 1
2213 );
2214 self.add_inline_schema(
2215 &inline_type_name,
2216 variant_schema,
2217 dependencies,
2218 )?;
2219 union_variants.push(SchemaRef {
2220 target: inline_type_name,
2221 nullable: false,
2222 });
2223 }
2224 }
2225 }
2226 }
2227
2228 if !union_variants.is_empty() {
2229 return Ok(SchemaType::Union {
2230 variants: union_variants,
2231 });
2232 }
2233
2234 return Ok(SchemaType::Primitive {
2236 rust_type: "serde_json::Value".to_string(),
2237 });
2238 }
2239
2240 Ok(SchemaType::DiscriminatedUnion {
2241 discriminator_field,
2242 variants,
2243 })
2244 }
2245
2246 fn analyze_untagged_oneof_union(
2247 &mut self,
2248 one_of_schemas: &[Schema],
2249 parent_name: Option<&str>,
2250 dependencies: &mut HashSet<String>,
2251 ) -> Result<SchemaType> {
2252 let mut union_variants = Vec::new();
2253
2254 for (variant_index, variant_schema) in one_of_schemas.iter().enumerate() {
2255 if let Some(ref_str) = variant_schema.reference() {
2257 if let Some(schema_name) = self.extract_schema_name(ref_str) {
2258 dependencies.insert(schema_name.to_string());
2259 union_variants.push(SchemaRef {
2260 target: schema_name.to_string(),
2261 nullable: false,
2262 });
2263 }
2264 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2265 let schema_name = if recursive_ref == "#" {
2266 self.find_recursive_anchor_schema()
2268 .or_else(|| self.current_schema_name.clone())
2269 .unwrap_or_else(|| "CompoundFilter".to_string())
2270 } else {
2271 self.extract_schema_name(recursive_ref)
2272 .map(|s| s.to_string())
2273 .unwrap_or_else(|| "RecursiveType".to_string())
2274 };
2275 dependencies.insert(schema_name.clone());
2276 union_variants.push(SchemaRef {
2277 target: schema_name,
2278 nullable: false,
2279 });
2280 } else {
2281 let context = parent_name.unwrap_or("Union");
2283 let inline_name = self.generate_context_aware_name(
2284 context,
2285 "InlineVariant",
2286 variant_index,
2287 Some(variant_schema),
2288 );
2289 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
2290 let variant_type = analyzed.schema_type;
2291
2292 for dep in &analyzed.dependencies {
2294 dependencies.insert(dep.clone());
2295 }
2296
2297 match &variant_type {
2298 SchemaType::Primitive { rust_type } => {
2300 union_variants.push(SchemaRef {
2301 target: rust_type.clone(),
2302 nullable: false,
2303 });
2304 }
2305 SchemaType::Array { item_type } => {
2307 match item_type.as_ref() {
2308 SchemaType::Primitive { rust_type } => {
2309 let type_name = format!("Vec<{rust_type}>");
2310 union_variants.push(SchemaRef {
2311 target: type_name,
2312 nullable: false,
2313 });
2314 }
2315 SchemaType::Reference { target } => {
2316 let type_name = format!("Vec<{target}>");
2317 union_variants.push(SchemaRef {
2318 target: type_name,
2319 nullable: false,
2320 });
2321 }
2322 SchemaType::Array {
2324 item_type: inner_item_type,
2325 } => {
2326 match inner_item_type.as_ref() {
2327 SchemaType::Primitive { rust_type } => {
2328 let type_name = format!("Vec<Vec<{rust_type}>>");
2329 union_variants.push(SchemaRef {
2330 target: type_name,
2331 nullable: false,
2332 });
2333 }
2334 SchemaType::Reference { target } => {
2335 let type_name = format!("Vec<Vec<{target}>>");
2336 union_variants.push(SchemaRef {
2337 target: type_name,
2338 nullable: false,
2339 });
2340 }
2341 _ => {
2342 let context = parent_name.unwrap_or("Inline");
2344 let inline_type_name = self.generate_context_aware_name(
2345 context,
2346 "Variant",
2347 variant_index,
2348 None,
2349 );
2350 self.add_inline_schema(
2351 &inline_type_name,
2352 variant_schema,
2353 dependencies,
2354 )?;
2355 union_variants.push(SchemaRef {
2356 target: inline_type_name,
2357 nullable: false,
2358 });
2359 }
2360 }
2361 }
2362 _ => {
2363 let context = parent_name.unwrap_or("Inline");
2365 let inline_type_name = self.generate_context_aware_name(
2366 context,
2367 "Variant",
2368 variant_index,
2369 None,
2370 );
2371 self.add_inline_schema(
2372 &inline_type_name,
2373 variant_schema,
2374 dependencies,
2375 )?;
2376 union_variants.push(SchemaRef {
2377 target: inline_type_name,
2378 nullable: false,
2379 });
2380 }
2381 }
2382 }
2383 SchemaType::Reference { target } => {
2385 union_variants.push(SchemaRef {
2386 target: target.clone(),
2387 nullable: false,
2388 });
2389 }
2390 _ => {
2392 let context = parent_name.unwrap_or("Inline");
2393 let inline_type_name = self.generate_context_aware_name(
2394 context,
2395 "Variant",
2396 variant_index,
2397 None,
2398 );
2399 self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
2400 union_variants.push(SchemaRef {
2401 target: inline_type_name,
2402 nullable: false,
2403 });
2404 }
2405 }
2406 }
2407 }
2408
2409 if !union_variants.is_empty() {
2410 return Ok(SchemaType::Union {
2411 variants: union_variants,
2412 });
2413 }
2414
2415 Ok(SchemaType::Primitive {
2417 rust_type: "serde_json::Value".to_string(),
2418 })
2419 }
2420
2421 fn add_inline_schema(
2422 &mut self,
2423 type_name: &str,
2424 schema: &Schema,
2425 dependencies: &mut HashSet<String>,
2426 ) -> Result<()> {
2427 if let Some(schema_type) = schema.schema_type() {
2429 match schema_type {
2430 OpenApiSchemaType::String
2431 | OpenApiSchemaType::Integer
2432 | OpenApiSchemaType::Number
2433 | OpenApiSchemaType::Boolean => {
2434 let rust_type =
2435 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
2436
2437 self.resolved_cache.insert(
2439 type_name.to_string(),
2440 AnalyzedSchema {
2441 name: type_name.to_string(),
2442 original: serde_json::to_value(schema).unwrap_or(Value::Null),
2443 schema_type: SchemaType::Primitive { rust_type },
2444 dependencies: HashSet::new(),
2445 nullable: false,
2446 description: schema.details().description.clone(),
2447 default: None,
2448 },
2449 );
2450 return Ok(());
2451 }
2452 _ => {}
2453 }
2454 }
2455
2456 let previous_schema_name = self.current_schema_name.take();
2460 self.current_schema_name = Some(type_name.to_string());
2461 let analyzed = self.analyze_schema_value(schema, type_name)?;
2462 self.current_schema_name = previous_schema_name;
2463
2464 self.resolved_cache.insert(type_name.to_string(), analyzed);
2466
2467 if let Some(cached) = self.resolved_cache.get(type_name) {
2469 for dep in &cached.dependencies {
2470 dependencies.insert(dep.clone());
2471 }
2472 }
2473
2474 Ok(())
2475 }
2476
2477 fn extract_inline_discriminator_value(
2478 &self,
2479 schema: &Schema,
2480 discriminator_field: &str,
2481 variant_index: usize,
2482 ) -> String {
2483 if let Some(properties) = &schema.details().properties {
2485 if let Some(discriminator_prop) = properties.get(discriminator_field) {
2486 if let Some(enum_values) = &discriminator_prop.details().enum_values {
2488 if enum_values.len() == 1 {
2489 if let Some(value) = enum_values[0].as_str() {
2490 return value.to_string();
2491 }
2492 }
2493 }
2494 if let Some(const_value) = discriminator_prop.details().extra.get("const") {
2496 if let Some(value) = const_value.as_str() {
2497 return value.to_string();
2498 }
2499 }
2500 if let Some(const_value) = &discriminator_prop.details().const_value {
2502 if let Some(value) = const_value.as_str() {
2503 return value.to_string();
2504 }
2505 }
2506 }
2507 }
2508
2509 if let Some(inferred_name) = self.infer_variant_name_from_structure(schema, variant_index) {
2511 return inferred_name;
2512 }
2513
2514 format!("variant_{variant_index}")
2516 }
2517
2518 fn infer_variant_name_from_structure(
2519 &self,
2520 schema: &Schema,
2521 _variant_index: usize,
2522 ) -> Option<String> {
2523 let details = schema.details();
2524
2525 if let Some(properties) = &details.properties {
2527 if properties.contains_key("text") && properties.len() <= 3 {
2529 return Some("text".to_string());
2530 }
2531 if properties.contains_key("image") || properties.contains_key("source") {
2532 return Some("image".to_string());
2533 }
2534 if properties.contains_key("document") {
2535 return Some("document".to_string());
2536 }
2537 if properties.contains_key("tool_use_id") || properties.contains_key("tool_result") {
2538 return Some("tool_result".to_string());
2539 }
2540 if properties.contains_key("content") && properties.contains_key("is_error") {
2541 return Some("tool_result".to_string());
2542 }
2543 if properties.contains_key("partial_json") {
2544 return Some("partial_json".to_string());
2545 }
2546
2547 let property_names: Vec<&String> = properties.keys().collect();
2549
2550 for prop_name in &property_names {
2552 if prop_name.contains("result") {
2553 return Some("result".to_string());
2554 }
2555 if prop_name.contains("error") {
2556 return Some("error".to_string());
2557 }
2558 if prop_name.contains("content") && property_names.len() <= 2 {
2559 return Some("content".to_string());
2560 }
2561 }
2562
2563 let significant_props = property_names
2565 .iter()
2566 .filter(|&name| !["type", "id", "cache_control"].contains(&name.as_str()))
2567 .collect::<Vec<_>>();
2568
2569 if significant_props.len() == 1 {
2570 return Some((*significant_props[0]).clone());
2571 }
2572 }
2573
2574 if let Some(description) = &details.description {
2576 let desc_lower = description.to_lowercase();
2577 if desc_lower.contains("text") && desc_lower.len() < 100 {
2578 return Some("text".to_string());
2579 }
2580 if desc_lower.contains("image") {
2581 return Some("image".to_string());
2582 }
2583 if desc_lower.contains("document") {
2584 return Some("document".to_string());
2585 }
2586 if desc_lower.contains("tool") && desc_lower.contains("result") {
2587 return Some("tool_result".to_string());
2588 }
2589 }
2590
2591 None
2592 }
2593
2594 fn discriminator_to_variant_name(&self, discriminator: &str) -> String {
2595 if discriminator.is_empty() {
2597 return "Variant".to_string();
2598 }
2599
2600 let mut result = String::new();
2601 let mut next_upper = true;
2602
2603 for c in discriminator.chars() {
2604 match c {
2605 'a'..='z' => {
2606 if next_upper {
2607 result.push(c.to_ascii_uppercase());
2608 next_upper = false;
2609 } else {
2610 result.push(c);
2611 }
2612 }
2613 'A'..='Z' => {
2614 result.push(c);
2615 next_upper = false;
2616 }
2617 '0'..='9' => {
2618 result.push(c);
2619 next_upper = false;
2620 }
2621 '_' | '-' | '.' | ' ' | '/' | '\\' => {
2622 next_upper = true;
2624 }
2625 _ => {
2626 next_upper = true;
2628 }
2629 }
2630 }
2631
2632 if result.is_empty() || result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2634 result = format!("Variant{result}");
2635 }
2636
2637 result
2638 }
2639
2640 fn ensure_unique_variant_name(
2641 &self,
2642 base_name: String,
2643 used_names: &mut std::collections::HashSet<String>,
2644 ) -> String {
2645 let mut candidate = base_name.clone();
2646 let mut counter = 1;
2647
2648 while used_names.contains(&candidate) {
2649 counter += 1;
2650 candidate = format!("{base_name}{counter}");
2651 }
2652
2653 used_names.insert(candidate.clone());
2654 candidate
2655 }
2656
2657 fn generate_inline_type_name(&self, schema: &Schema, variant_index: usize) -> String {
2658 if let Some(meaningful_name) = self.infer_type_name_from_structure(schema) {
2660 return meaningful_name;
2661 }
2662
2663 let context = self.current_schema_name.as_deref().unwrap_or("Inline");
2665 self.generate_context_aware_name(context, "Variant", variant_index, Some(schema))
2666 }
2667
2668 fn infer_type_name_from_structure(&self, schema: &Schema) -> Option<String> {
2669 let details = schema.details();
2670
2671 if let Some(description) = &details.description {
2673 if let Some(name_from_desc) = self.extract_type_name_from_description(description) {
2674 return Some(name_from_desc);
2675 }
2676 }
2677
2678 if let Some(properties) = &details.properties {
2680 if let Some(name_from_props) = self.extract_type_name_from_properties(properties) {
2681 return Some(format!("{name_from_props}Block"));
2682 }
2683 }
2684
2685 None
2686 }
2687
2688 fn extract_type_name_from_description(&self, description: &str) -> Option<String> {
2689 if description.len() > 100 || description.contains('\n') {
2691 return None;
2692 }
2693
2694 let words: Vec<&str> = description
2696 .split_whitespace()
2697 .take(2) .filter(|word| {
2699 let w = word.to_lowercase();
2700 word.len() > 2
2701 && ![
2702 "the", "and", "for", "with", "that", "this", "are", "can", "will", "was",
2703 ]
2704 .contains(&w.as_str())
2705 })
2706 .collect();
2707
2708 if words.is_empty() {
2709 return None;
2710 }
2711
2712 let combined = words.join("_");
2714 let pascal_name = self.discriminator_to_variant_name(&combined);
2715
2716 if !pascal_name.ends_with("Content")
2718 && !pascal_name.ends_with("Block")
2719 && !pascal_name.ends_with("Type")
2720 {
2721 Some(format!("{pascal_name}Content"))
2722 } else {
2723 Some(pascal_name)
2724 }
2725 }
2726
2727 fn extract_type_name_from_properties(
2728 &self,
2729 properties: &std::collections::BTreeMap<String, crate::openapi::Schema>,
2730 ) -> Option<String> {
2731 let significant_props: Vec<&String> = properties
2733 .keys()
2734 .filter(|name| !["type", "id", "cache_control"].contains(&name.as_str()))
2735 .collect();
2736
2737 if significant_props.is_empty() {
2738 return None;
2739 }
2740
2741 if significant_props.len() == 1 {
2743 let prop_name = significant_props[0];
2744 return Some(self.discriminator_to_variant_name(prop_name));
2745 }
2746
2747 let mut sorted_props = significant_props.clone();
2750 sorted_props.sort();
2751 if let Some(first_prop) = sorted_props.first() {
2752 return Some(self.discriminator_to_variant_name(first_prop));
2753 }
2754
2755 None
2756 }
2757
2758 fn openapi_type_to_rust_type(
2759 &self,
2760 openapi_type: OpenApiSchemaType,
2761 details: &crate::openapi::SchemaDetails,
2762 ) -> String {
2763 match openapi_type {
2764 OpenApiSchemaType::String => "String".to_string(),
2765 OpenApiSchemaType::Integer => self.get_number_rust_type(openapi_type, details),
2766 OpenApiSchemaType::Number => self.get_number_rust_type(openapi_type, details),
2767 OpenApiSchemaType::Boolean => "bool".to_string(),
2768 OpenApiSchemaType::Array => "Vec<serde_json::Value>".to_string(), OpenApiSchemaType::Object => "serde_json::Value".to_string(), OpenApiSchemaType::Null => "()".to_string(), }
2772 }
2773
2774 #[allow(dead_code)]
2775 fn fallback_discriminator_value(&self, schema_name: &str) -> String {
2776 self.fallback_discriminator_value_for_field(schema_name, "type")
2777 }
2778
2779 fn fallback_discriminator_value_for_field(
2780 &self,
2781 schema_name: &str,
2782 field_name: &str,
2783 ) -> String {
2784 if let Some(ref_schema) = self.schemas.get(schema_name) {
2786 if let Some(extracted) =
2787 self.extract_discriminator_value_for_field(ref_schema, field_name)
2788 {
2789 return extracted;
2790 }
2791 }
2792
2793 self.generate_discriminator_value_from_name(schema_name)
2795 }
2796
2797 fn generate_discriminator_value_from_name(&self, schema_name: &str) -> String {
2798 let mut result = String::new();
2800 let mut chars = schema_name.chars().peekable();
2801 let mut first = true;
2802
2803 while let Some(c) = chars.next() {
2804 if c.is_uppercase()
2805 && !first
2806 && chars
2807 .peek()
2808 .map(|&next| next.is_lowercase())
2809 .unwrap_or(false)
2810 {
2811 result.push('.');
2812 }
2813 result.push(c.to_ascii_lowercase());
2814 first = false;
2815 }
2816
2817 if result.ends_with("event") {
2819 result = result[..result.len() - 5].to_string();
2820 }
2821
2822 if schema_name.starts_with("Response") && !result.starts_with("response.") {
2824 result = format!("response.{}", result.trim_start_matches("response"));
2825 }
2826
2827 result
2828 }
2829
2830 fn to_rust_variant_name(&self, schema_name: &str) -> String {
2831 let mut name = schema_name;
2833
2834 if name.starts_with("Response") && name.len() > 8 {
2836 name = &name[8..]; }
2838
2839 if name.ends_with("Event") && name.len() > 5 {
2841 name = &name[..name.len() - 5]; }
2843
2844 name = name.trim_matches('_');
2846
2847 if name.is_empty() {
2849 schema_name.to_string()
2850 } else {
2851 self.discriminator_to_variant_name(name)
2853 }
2854 }
2855
2856 fn analyze_array_schema(
2857 &mut self,
2858 schema: &Schema,
2859 parent_schema_name: &str,
2860 dependencies: &mut HashSet<String>,
2861 ) -> Result<SchemaType> {
2862 let details = schema.details();
2863
2864 if let Some(items_schema) = &details.items {
2866 let item_type = match items_schema.as_ref() {
2868 Schema::Reference { reference, .. } => {
2869 let target = self
2871 .extract_schema_name(reference)
2872 .ok_or_else(|| GeneratorError::UnresolvedReference(reference.to_string()))?
2873 .to_string();
2874 dependencies.insert(target.clone());
2875 SchemaType::Reference { target }
2876 }
2877 Schema::RecursiveRef { recursive_ref, .. } => {
2878 if recursive_ref == "#" {
2880 let target = self
2882 .find_recursive_anchor_schema()
2883 .unwrap_or_else(|| parent_schema_name.to_string());
2884 dependencies.insert(target.clone());
2885 SchemaType::Reference { target }
2886 } else {
2887 let target = self
2888 .extract_schema_name(recursive_ref)
2889 .unwrap_or("RecursiveType")
2890 .to_string();
2891 dependencies.insert(target.clone());
2892 SchemaType::Reference { target }
2893 }
2894 }
2895 Schema::Typed { schema_type, .. } => {
2896 match schema_type {
2898 OpenApiSchemaType::String => SchemaType::Primitive {
2899 rust_type: "String".to_string(),
2900 },
2901 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
2902 let details = items_schema.details();
2903 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
2904 SchemaType::Primitive { rust_type }
2905 }
2906 OpenApiSchemaType::Boolean => SchemaType::Primitive {
2907 rust_type: "bool".to_string(),
2908 },
2909 OpenApiSchemaType::Object => {
2910 let object_type_name = format!("{parent_schema_name}Item");
2912
2913 let object_type =
2915 self.analyze_object_schema(items_schema, dependencies)?;
2916
2917 let inline_schema = AnalyzedSchema {
2919 name: object_type_name.clone(),
2920 original: serde_json::to_value(items_schema).unwrap_or(Value::Null),
2921 schema_type: object_type,
2922 dependencies: dependencies.clone(),
2923 nullable: false,
2924 description: items_schema.details().description.clone(),
2925 default: None,
2926 };
2927
2928 self.resolved_cache
2930 .insert(object_type_name.clone(), inline_schema);
2931 dependencies.insert(object_type_name.clone());
2932
2933 SchemaType::Reference {
2935 target: object_type_name,
2936 }
2937 }
2938 OpenApiSchemaType::Array => {
2939 self.analyze_array_schema(
2941 items_schema,
2942 parent_schema_name,
2943 dependencies,
2944 )?
2945 }
2946 _ => SchemaType::Primitive {
2947 rust_type: "serde_json::Value".to_string(),
2948 },
2949 }
2950 }
2951 Schema::OneOf { .. } | Schema::AnyOf { .. } => {
2952 let analyzed = self.analyze_schema_value(items_schema, "ArrayItem")?;
2954
2955 match &analyzed.schema_type {
2957 SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => {
2958 let union_name = format!("{parent_schema_name}ItemUnion");
2961
2962 let mut union_schema = analyzed;
2964 union_schema.name = union_name.clone();
2965
2966 self.resolved_cache.insert(union_name.clone(), union_schema);
2968
2969 dependencies.insert(union_name.clone());
2971
2972 SchemaType::Reference { target: union_name }
2974 }
2975 _ => analyzed.schema_type,
2976 }
2977 }
2978 Schema::Untyped { .. } => {
2979 if let Some(inferred) = items_schema.inferred_type() {
2981 match inferred {
2982 OpenApiSchemaType::Object => {
2983 let object_type_name = format!("{parent_schema_name}Item");
2985
2986 let object_type =
2988 self.analyze_object_schema(items_schema, dependencies)?;
2989
2990 let inline_schema = AnalyzedSchema {
2992 name: object_type_name.clone(),
2993 original: serde_json::to_value(items_schema)
2994 .unwrap_or(Value::Null),
2995 schema_type: object_type,
2996 dependencies: dependencies.clone(),
2997 nullable: false,
2998 description: items_schema.details().description.clone(),
2999 default: None,
3000 };
3001
3002 self.resolved_cache
3004 .insert(object_type_name.clone(), inline_schema);
3005 dependencies.insert(object_type_name.clone());
3006
3007 SchemaType::Reference {
3009 target: object_type_name,
3010 }
3011 }
3012 OpenApiSchemaType::String => SchemaType::Primitive {
3013 rust_type: "String".to_string(),
3014 },
3015 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
3016 let details = items_schema.details();
3017 let rust_type = self.get_number_rust_type(inferred, details);
3018 SchemaType::Primitive { rust_type }
3019 }
3020 OpenApiSchemaType::Boolean => SchemaType::Primitive {
3021 rust_type: "bool".to_string(),
3022 },
3023 _ => SchemaType::Primitive {
3024 rust_type: "serde_json::Value".to_string(),
3025 },
3026 }
3027 } else {
3028 SchemaType::Primitive {
3029 rust_type: "serde_json::Value".to_string(),
3030 }
3031 }
3032 }
3033 _ => SchemaType::Primitive {
3034 rust_type: "serde_json::Value".to_string(),
3035 },
3036 };
3037
3038 Ok(SchemaType::Array {
3039 item_type: Box::new(item_type),
3040 })
3041 } else {
3042 Ok(SchemaType::Primitive {
3044 rust_type: "Vec<serde_json::Value>".to_string(),
3045 })
3046 }
3047 }
3048
3049 fn get_number_rust_type(
3050 &self,
3051 schema_type: OpenApiSchemaType,
3052 details: &crate::openapi::SchemaDetails,
3053 ) -> String {
3054 match schema_type {
3055 OpenApiSchemaType::Integer => {
3056 match details.format.as_deref() {
3058 Some("int32") => "i32".to_string(),
3059 Some("int64") => "i64".to_string(),
3060 _ => "i64".to_string(), }
3062 }
3063 OpenApiSchemaType::Number => {
3064 match details.format.as_deref() {
3066 Some("float") => "f32".to_string(),
3067 Some("double") => "f64".to_string(),
3068 _ => "f64".to_string(), }
3070 }
3071 _ => "serde_json::Value".to_string(), }
3073 }
3074
3075 fn analyze_anyof_union(
3076 &mut self,
3077 any_of_schemas: &[Schema],
3078 discriminator: Option<&Discriminator>,
3079 dependencies: &mut HashSet<String>,
3080 context_name: &str,
3081 ) -> Result<SchemaType> {
3082 if any_of_schemas.len() == 2 {
3086 let null_count = any_of_schemas
3087 .iter()
3088 .filter(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
3089 .count();
3090 if null_count == 1 {
3091 for schema in any_of_schemas {
3093 if !matches!(schema.schema_type(), Some(OpenApiSchemaType::Null)) {
3094 return self
3097 .analyze_schema_value(schema, context_name)
3098 .map(|a| a.schema_type);
3099 }
3100 }
3101 }
3102 }
3103
3104 let has_refs = any_of_schemas.iter().any(|s| s.is_reference());
3106 let has_objects = any_of_schemas.iter().any(|s| {
3107 matches!(s.schema_type(), Some(OpenApiSchemaType::Object))
3108 || s.inferred_type() == Some(OpenApiSchemaType::Object)
3109 });
3110 let has_arrays = any_of_schemas
3111 .iter()
3112 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Array)));
3113
3114 let all_string_like = any_of_schemas.iter().all(|s| {
3117 matches!(s.schema_type(), Some(OpenApiSchemaType::String))
3118 || s.details().const_value.is_some()
3119 });
3120
3121 if (has_refs || has_objects || has_arrays || any_of_schemas.len() > 1) && !all_string_like {
3122 if let Some(disc) = discriminator {
3124 return self.analyze_oneof_union(any_of_schemas, Some(disc), None, dependencies);
3126 }
3127
3128 if let Some(disc_field) = self.detect_discriminator_field(any_of_schemas) {
3130 return self.analyze_oneof_union(
3131 any_of_schemas,
3132 Some(&Discriminator {
3133 property_name: disc_field,
3134 mapping: None,
3135 extra: BTreeMap::new(),
3136 }),
3137 None,
3138 dependencies,
3139 );
3140 }
3141
3142 let mut variants = Vec::new();
3144
3145 for schema in any_of_schemas {
3146 if let Some(ref_str) = schema.reference() {
3147 if let Some(target) = self.extract_schema_name(ref_str) {
3148 dependencies.insert(target.to_string());
3149 variants.push(SchemaRef {
3150 target: target.to_string(),
3151 nullable: false,
3152 });
3153 }
3154 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object))
3155 || schema.inferred_type() == Some(OpenApiSchemaType::Object)
3156 {
3157 let inline_index = variants.len();
3159 let inline_type_name = self.generate_inline_type_name(schema, inline_index);
3160
3161 self.add_inline_schema(&inline_type_name, schema, dependencies)?;
3163
3164 variants.push(SchemaRef {
3165 target: inline_type_name,
3166 nullable: false,
3167 });
3168 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Array)) {
3169 let array_type =
3171 self.analyze_array_schema(schema, context_name, dependencies)?;
3172
3173 let array_type_name = if let Some(items_schema) = &schema.details().items {
3175 if let Some(ref_str) = items_schema.reference() {
3176 if let Some(item_type_name) = self.extract_schema_name(ref_str) {
3177 dependencies.insert(item_type_name.to_string());
3178 format!("{item_type_name}Array")
3179 } else {
3180 self.generate_context_aware_name(
3181 context_name,
3182 "Array",
3183 variants.len(),
3184 Some(schema),
3185 )
3186 }
3187 } else {
3188 self.generate_context_aware_name(
3189 context_name,
3190 "Array",
3191 variants.len(),
3192 Some(schema),
3193 )
3194 }
3195 } else {
3196 self.generate_context_aware_name(
3197 context_name,
3198 "Array",
3199 variants.len(),
3200 Some(schema),
3201 )
3202 };
3203
3204 self.resolved_cache.insert(
3206 array_type_name.clone(),
3207 AnalyzedSchema {
3208 name: array_type_name.clone(),
3209 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3210 schema_type: array_type,
3211 dependencies: HashSet::new(),
3212 nullable: false,
3213 description: Some("Array variant in union".to_string()),
3214 default: None,
3215 },
3216 );
3217
3218 dependencies.insert(array_type_name.clone());
3220
3221 variants.push(SchemaRef {
3222 target: array_type_name,
3223 nullable: false,
3224 });
3225 } else if let Some(schema_type) = schema.schema_type() {
3226 let inline_index = variants.len();
3228
3229 let inline_type_name = match schema_type {
3231 OpenApiSchemaType::String => {
3232 if inline_index == 0 {
3235 format!("{context_name}String")
3236 } else {
3237 format!("{context_name}StringVariant{inline_index}")
3238 }
3239 }
3240 OpenApiSchemaType::Number => {
3241 if inline_index == 0 {
3242 format!("{context_name}Number")
3243 } else {
3244 format!("{context_name}NumberVariant{inline_index}")
3245 }
3246 }
3247 OpenApiSchemaType::Integer => {
3248 if inline_index == 0 {
3249 format!("{context_name}Integer")
3250 } else {
3251 format!("{context_name}IntegerVariant{inline_index}")
3252 }
3253 }
3254 OpenApiSchemaType::Boolean => {
3255 if inline_index == 0 {
3256 format!("{context_name}Boolean")
3257 } else {
3258 format!("{context_name}BooleanVariant{inline_index}")
3259 }
3260 }
3261 _ => format!("{context_name}Variant{inline_index}"),
3262 };
3263
3264 let rust_type =
3265 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
3266
3267 self.resolved_cache.insert(
3269 inline_type_name.clone(),
3270 AnalyzedSchema {
3271 name: inline_type_name.clone(),
3272 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3273 schema_type: SchemaType::Primitive { rust_type },
3274 dependencies: HashSet::new(),
3275 nullable: false,
3276 description: schema.details().description.clone(),
3277 default: None,
3278 },
3279 );
3280
3281 dependencies.insert(inline_type_name.clone());
3283
3284 variants.push(SchemaRef {
3285 target: inline_type_name,
3286 nullable: false,
3287 });
3288 }
3289 }
3290
3291 if !variants.is_empty() {
3292 return Ok(SchemaType::Union { variants });
3293 }
3294 }
3295
3296 let all_strings = any_of_schemas.iter().all(|schema| {
3298 matches!(schema.schema_type(), Some(OpenApiSchemaType::String))
3299 || schema.details().const_value.is_some()
3300 });
3301
3302 if all_strings {
3303 let mut enum_values = Vec::new();
3305 let mut has_open_string = false;
3306
3307 for schema in any_of_schemas {
3308 if let Some(const_val) = &schema.details().const_value {
3309 if let Some(const_str) = const_val.as_str() {
3310 enum_values.push(const_str.to_string());
3311 }
3312 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::String)) {
3313 has_open_string = true;
3314 }
3315 }
3316
3317 if !enum_values.is_empty() {
3318 if has_open_string {
3319 return Ok(SchemaType::ExtensibleEnum {
3322 known_values: enum_values,
3323 });
3324 } else {
3325 return Ok(SchemaType::StringEnum {
3327 values: enum_values,
3328 });
3329 }
3330 }
3331 }
3332
3333 Ok(SchemaType::Primitive {
3335 rust_type: "serde_json::Value".to_string(),
3336 })
3337 }
3338
3339 fn find_recursive_anchor_schema(&self) -> Option<String> {
3341 for (schema_name, schema) in &self.schemas {
3343 let details = schema.details();
3344 if details.recursive_anchor == Some(true) {
3345 return Some(schema_name.clone());
3346 }
3347 }
3348
3349 None
3353 }
3354
3355 fn should_use_dynamic_json(&self, schema: &Schema) -> bool {
3358 if let Schema::AnyOf { any_of, .. } = schema {
3360 if any_of.len() == 2 {
3361 let has_null = any_of
3362 .iter()
3363 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)));
3364 let has_empty_object = any_of.iter().any(|s| self.is_dynamic_object_pattern(s));
3365
3366 if has_null && has_empty_object {
3367 return true;
3368 }
3369 }
3370 }
3371
3372 self.is_dynamic_object_pattern(schema)
3374 }
3375
3376 fn is_dynamic_object_pattern(&self, schema: &Schema) -> bool {
3378 let is_object = match schema.schema_type() {
3380 Some(OpenApiSchemaType::Object) => true,
3381 None => schema.inferred_type() == Some(OpenApiSchemaType::Object),
3382 _ => false,
3383 };
3384
3385 if !is_object {
3386 return false;
3387 }
3388
3389 let details = schema.details();
3390
3391 if self.has_explicit_additional_properties(schema) {
3394 return false;
3395 }
3396
3397 let no_properties = details
3399 .properties
3400 .as_ref()
3401 .map(|props| props.is_empty())
3402 .unwrap_or(true);
3403
3404 if no_properties {
3405 let has_structural_constraints =
3407 details.required.as_ref()
3409 .map(|req| req.iter().any(|r| r != "type"))
3410 .unwrap_or(false)
3411 || details.extra.contains_key("patternProperties")
3413 || details.extra.contains_key("propertyNames")
3415 || details.extra.contains_key("minProperties")
3417 || details.extra.contains_key("maxProperties")
3418 || details.extra.contains_key("dependencies")
3420 || details.extra.contains_key("if")
3422 || details.extra.contains_key("then")
3423 || details.extra.contains_key("else");
3424
3425 return !has_structural_constraints;
3426 }
3427
3428 false
3429 }
3430
3431 fn has_explicit_additional_properties(&self, schema: &Schema) -> bool {
3433 let details = schema.details();
3434
3435 matches!(
3437 &details.additional_properties,
3438 Some(crate::openapi::AdditionalProperties::Boolean(true))
3439 | Some(crate::openapi::AdditionalProperties::Schema(_))
3440 )
3441 }
3442
3443 fn analyze_operations(&mut self, analysis: &mut SchemaAnalysis) -> Result<()> {
3445 let spec: crate::openapi::OpenApiSpec = serde_json::from_value(self.openapi_spec.clone())
3446 .map_err(GeneratorError::ParseError)?;
3447
3448 if let Some(paths) = &spec.paths {
3449 for (path, path_item) in paths {
3450 for (method, operation) in path_item.operations() {
3451 let operation_id = operation
3453 .operation_id
3454 .clone()
3455 .unwrap_or_else(|| Self::generate_operation_id(method, path));
3456
3457 let op_info = self.analyze_single_operation(
3458 &operation_id,
3459 method,
3460 path,
3461 operation,
3462 path_item.parameters.as_ref(),
3463 analysis,
3464 )?;
3465 analysis.operations.insert(operation_id, op_info);
3466 }
3467 }
3468 }
3469 Ok(())
3470 }
3471
3472 fn generate_operation_id(method: &str, path: &str) -> String {
3475 let mut operation_id = method.to_lowercase();
3477
3478 let path_parts: Vec<&str> = path.trim_start_matches('/').split('/').collect();
3480
3481 for part in path_parts {
3482 if part.is_empty() {
3483 continue;
3484 }
3485
3486 let cleaned_part = if part.starts_with('{') && part.ends_with('}') {
3488 &part[1..part.len() - 1]
3489 } else {
3490 part
3491 };
3492
3493 let pascal_case_part = cleaned_part
3495 .split(&['-', '_'][..])
3496 .map(|s| {
3497 let mut chars = s.chars();
3498 match chars.next() {
3499 None => String::new(),
3500 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
3501 }
3502 })
3503 .collect::<String>();
3504
3505 operation_id.push_str(&pascal_case_part);
3506 }
3507
3508 operation_id
3509 }
3510
3511 fn analyze_single_operation(
3513 &mut self,
3514 operation_id: &str,
3515 method: &str,
3516 path: &str,
3517 operation: &crate::openapi::Operation,
3518 path_item_parameters: Option<&Vec<crate::openapi::Parameter>>,
3519 _analysis: &mut SchemaAnalysis,
3520 ) -> Result<OperationInfo> {
3521 let mut op_info = OperationInfo {
3522 operation_id: operation_id.to_string(),
3523 method: method.to_uppercase(),
3524 path: path.to_string(),
3525 summary: operation.summary.clone(),
3526 description: operation.description.clone(),
3527 request_body: None,
3528 response_schemas: BTreeMap::new(),
3529 parameters: Vec::new(),
3530 supports_streaming: false, stream_parameter: None, };
3533
3534 if let Some(request_body) = &operation.request_body
3536 && let Some((content_type, maybe_schema)) = request_body.best_content()
3537 {
3538 use crate::openapi::{is_form_urlencoded_media_type, is_json_media_type};
3539 op_info.request_body = if is_json_media_type(content_type) {
3540 maybe_schema
3541 .map(|s| {
3542 self.resolve_or_inline_schema(s, operation_id, "Request")
3543 .map(|name| RequestBodyContent::Json { schema_name: name })
3544 })
3545 .transpose()?
3546 } else if is_form_urlencoded_media_type(content_type) {
3547 maybe_schema
3548 .map(|s| {
3549 self.resolve_or_inline_schema(s, operation_id, "Request")
3550 .map(|name| RequestBodyContent::FormUrlEncoded { schema_name: name })
3551 })
3552 .transpose()?
3553 } else {
3554 match content_type {
3555 "multipart/form-data" => Some(RequestBodyContent::Multipart),
3556 "application/octet-stream" => Some(RequestBodyContent::OctetStream),
3557 "text/plain" => Some(RequestBodyContent::TextPlain),
3558 _ => None,
3559 }
3560 };
3561 }
3562
3563 if let Some(responses) = &operation.responses {
3565 for (status_code, response) in responses {
3566 if let Some(schema) = response.json_schema() {
3567 if let Some(schema_ref) = schema.reference() {
3568 if let Some(schema_name) = self.extract_schema_name(schema_ref) {
3570 op_info
3571 .response_schemas
3572 .insert(status_code.clone(), schema_name.to_string());
3573 }
3574 } else {
3575 let synthetic_name =
3577 self.generate_inline_response_type_name(operation_id, status_code);
3578
3579 let mut deps = HashSet::new();
3581 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
3582
3583 op_info
3584 .response_schemas
3585 .insert(status_code.clone(), synthetic_name);
3586 }
3587 }
3588 }
3589 }
3590
3591 if let Some(parameters) = &operation.parameters {
3593 for param in parameters {
3594 let resolved = self.resolve_parameter(param);
3595 if let Some(param_info) = self.analyze_parameter(&resolved)? {
3596 op_info.parameters.push(param_info);
3597 }
3598 }
3599 }
3600
3601 if let Some(path_params) = path_item_parameters {
3603 let existing_keys: std::collections::HashSet<(String, String)> = op_info
3604 .parameters
3605 .iter()
3606 .map(|p| (p.name.clone(), p.location.clone()))
3607 .collect();
3608 for param in path_params {
3609 let resolved = self.resolve_parameter(param);
3610 if let Some(param_info) = self.analyze_parameter(&resolved)? {
3611 if !existing_keys
3612 .contains(&(param_info.name.clone(), param_info.location.clone()))
3613 {
3614 op_info.parameters.push(param_info);
3615 }
3616 }
3617 }
3618 }
3619
3620 Ok(op_info)
3621 }
3622
3623 fn generate_inline_response_type_name(&self, operation_id: &str, _status_code: &str) -> String {
3625 use heck::ToPascalCase;
3626 let base_name = operation_id.replace('.', "_").to_pascal_case();
3630 format!("{}Response", base_name)
3631 }
3632
3633 fn generate_inline_request_type_name(&self, operation_id: &str) -> String {
3635 use heck::ToPascalCase;
3636 let base_name = operation_id.replace('.', "_").to_pascal_case();
3640 format!("{}Request", base_name)
3641 }
3642
3643 fn resolve_or_inline_schema(
3646 &mut self,
3647 schema: &crate::openapi::Schema,
3648 operation_id: &str,
3649 suffix: &str,
3650 ) -> Result<String> {
3651 if let Some(schema_ref) = schema.reference()
3652 && let Some(schema_name) = self.extract_schema_name(schema_ref)
3653 {
3654 return Ok(schema_name.to_string());
3655 }
3656 let synthetic_name = if suffix == "Request" {
3658 self.generate_inline_request_type_name(operation_id)
3659 } else {
3660 self.generate_inline_response_type_name(operation_id, "")
3661 };
3662 let mut deps = HashSet::new();
3663 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
3664 Ok(synthetic_name)
3665 }
3666
3667 fn resolve_parameter<'a>(
3670 &'a self,
3671 param: &'a crate::openapi::Parameter,
3672 ) -> std::borrow::Cow<'a, crate::openapi::Parameter> {
3673 if let Some(ref_str) = param.extra.get("$ref").and_then(|v| v.as_str()) {
3674 if let Some(param_name) = ref_str.strip_prefix("#/components/parameters/") {
3675 if let Some(resolved) = self.component_parameters.get(param_name) {
3676 return std::borrow::Cow::Borrowed(resolved);
3677 }
3678 }
3679 }
3680 std::borrow::Cow::Borrowed(param)
3681 }
3682
3683 fn analyze_parameter(
3685 &self,
3686 param: &crate::openapi::Parameter,
3687 ) -> Result<Option<ParameterInfo>> {
3688 let name = param.name.as_deref().unwrap_or("");
3689 let location = param.location.as_deref().unwrap_or("");
3690 let required = param.required.unwrap_or(false);
3691
3692 let mut rust_type = "String".to_string();
3693 let mut schema_ref = None;
3694
3695 if let Some(schema) = ¶m.schema {
3696 if let Some(ref_str) = schema.reference() {
3697 schema_ref = self.extract_schema_name(ref_str).map(|s| s.to_string());
3698 } else if let Some(schema_type) = schema.schema_type() {
3699 rust_type = match schema_type {
3700 crate::openapi::SchemaType::Boolean => "bool",
3701 crate::openapi::SchemaType::Integer => "i64",
3702 crate::openapi::SchemaType::Number => "f64",
3703 crate::openapi::SchemaType::String => "String",
3704 _ => "String",
3705 }
3706 .to_string();
3707 }
3708 }
3709
3710 Ok(Some(ParameterInfo {
3711 name: name.to_string(),
3712 location: location.to_string(),
3713 required,
3714 schema_ref,
3715 rust_type,
3716 description: param.description.clone(),
3717 }))
3718 }
3719}