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