1use crate::openapi::{Discriminator, OpenApiSpec, Schema, SchemaType as OpenApiSchemaType};
2use crate::type_mapping::TypeMapper;
3use crate::{GeneratorError, Result};
4use serde_json::Value;
5use std::collections::{BTreeMap, HashSet};
6use std::path::Path;
7
8fn extract_enum_extensions(
15 original: &Value,
16 enum_value_count: usize,
17 schema_name: &str,
18) -> Option<EnumExtensions> {
19 let obj = original.as_object()?;
20
21 let read_string_array = |key: &str| -> Option<Vec<String>> {
22 let arr = obj.get(key)?.as_array()?;
23 let mut out = Vec::with_capacity(arr.len());
24 for v in arr {
25 out.push(v.as_str()?.to_string());
26 }
27 Some(out)
28 };
29
30 let varnames_raw = read_string_array("x-enum-varnames");
31 let descriptions_raw = read_string_array("x-enum-descriptions");
32
33 if varnames_raw.is_none() && descriptions_raw.is_none() {
34 return None;
35 }
36
37 let validate = |label: &str, vals: Option<Vec<String>>| -> Vec<String> {
38 let Some(vals) = vals else {
39 return Vec::new();
40 };
41 if vals.len() == enum_value_count {
42 vals
43 } else {
44 eprintln!(
45 "⚠️ {schema_name}: dropping {label} (expected {enum_value_count} entries, got {})",
46 vals.len()
47 );
48 Vec::new()
49 }
50 };
51
52 let varnames = validate("x-enum-varnames", varnames_raw);
53 let descriptions = validate("x-enum-descriptions", descriptions_raw);
54
55 if varnames.is_empty() && descriptions.is_empty() {
56 return None;
57 }
58 Some(EnumExtensions {
59 varnames,
60 descriptions,
61 })
62}
63
64#[derive(Debug, Clone)]
65pub struct SchemaAnalysis {
66 pub schemas: BTreeMap<String, AnalyzedSchema>,
68 pub dependencies: DependencyGraph,
70 pub patterns: DetectedPatterns,
72 pub operations: BTreeMap<String, OperationInfo>,
74 pub operation_id_aliases: BTreeMap<String, Vec<String>>,
78 pub used_type_features: crate::type_mapping::UsedFeatures,
87 pub enum_extensions: BTreeMap<String, EnumExtensions>,
95}
96
97#[derive(Debug, Clone, Default)]
102pub struct EnumExtensions {
103 pub varnames: Vec<String>,
108 pub descriptions: Vec<String>,
110}
111
112#[derive(Debug, Clone)]
113pub struct AnalyzedSchema {
114 pub name: String,
115 pub original: Value,
116 pub schema_type: SchemaType,
117 pub dependencies: HashSet<String>,
118 pub nullable: bool,
119 pub description: Option<String>,
120 pub default: Option<serde_json::Value>,
121}
122
123#[derive(Debug, Clone)]
124pub enum SchemaType {
125 Primitive {
131 rust_type: String,
132 serde_with: Option<String>,
133 },
134 Object {
136 properties: BTreeMap<String, PropertyInfo>,
137 required: HashSet<String>,
138 additional_properties: ObjectAdditionalProperties,
139 },
140 DiscriminatedUnion {
142 discriminator_field: String,
143 variants: Vec<UnionVariant>,
144 },
145 Union { variants: Vec<SchemaRef> },
147 Array { item_type: Box<SchemaType> },
149 StringEnum { values: Vec<String> },
151 ExtensibleEnum { known_values: Vec<String> },
153 Composition { schemas: Vec<SchemaRef> },
155 Reference { target: String },
157}
158
159#[derive(Debug, Clone)]
164pub enum ObjectAdditionalProperties {
165 Forbidden,
168 Untyped,
171 Typed { value_type: Box<SchemaType> },
174}
175
176impl ObjectAdditionalProperties {
177 pub fn is_open(&self) -> bool {
180 !matches!(self, Self::Forbidden)
181 }
182}
183
184#[derive(Debug, Clone)]
185pub struct PropertyInfo {
186 pub schema_type: SchemaType,
187 pub nullable: bool,
188 pub description: Option<String>,
189 pub default: Option<serde_json::Value>,
190 pub serde_attrs: Vec<String>,
191 pub constraints: PropertyConstraints,
196}
197
198#[derive(Debug, Clone, Default)]
203pub struct PropertyConstraints {
204 pub minimum: Option<f64>,
205 pub maximum: Option<f64>,
206 pub exclusive_minimum: Option<f64>,
207 pub exclusive_maximum: Option<f64>,
208 pub multiple_of: Option<f64>,
209 pub min_length: Option<u64>,
210 pub max_length: Option<u64>,
211 pub pattern: Option<String>,
212 pub min_items: Option<u64>,
213 pub max_items: Option<u64>,
214 pub unique_items: Option<bool>,
215}
216
217impl PropertyConstraints {
218 pub fn is_empty(&self) -> bool {
219 self.minimum.is_none()
220 && self.maximum.is_none()
221 && self.exclusive_minimum.is_none()
222 && self.exclusive_maximum.is_none()
223 && self.multiple_of.is_none()
224 && self.min_length.is_none()
225 && self.max_length.is_none()
226 && self.pattern.is_none()
227 && self.min_items.is_none()
228 && self.max_items.is_none()
229 && self.unique_items.is_none()
230 }
231
232 pub fn from_schema_details(details: &crate::openapi::SchemaDetails) -> Self {
237 use crate::openapi::ExclusiveBound;
238 let exclusive_minimum = match &details.exclusive_minimum {
239 Some(ExclusiveBound::Number(v)) => Some(*v),
240 _ => None,
241 };
242 let exclusive_maximum = match &details.exclusive_maximum {
243 Some(ExclusiveBound::Number(v)) => Some(*v),
244 _ => None,
245 };
246 Self {
247 minimum: details.minimum,
248 maximum: details.maximum,
249 exclusive_minimum,
250 exclusive_maximum,
251 multiple_of: details.multiple_of,
252 min_length: details.min_length,
253 max_length: details.max_length,
254 pattern: details.pattern.clone(),
255 min_items: details.min_items,
256 max_items: details.max_items,
257 unique_items: details.unique_items,
258 }
259 }
260}
261
262#[derive(Debug, Clone)]
263pub struct UnionVariant {
264 pub rust_name: String,
265 pub type_name: String,
266 pub discriminator_value: String,
267 pub schema_ref: String,
268}
269
270#[derive(Debug, Clone)]
271pub struct SchemaRef {
272 pub target: String,
273 pub nullable: bool,
274}
275
276#[derive(Debug, Clone)]
277pub struct DependencyGraph {
278 pub edges: BTreeMap<String, HashSet<String>>,
279 pub recursive_schemas: HashSet<String>,
281}
282
283#[derive(Debug, Clone)]
284pub struct DetectedPatterns {
285 pub tagged_enum_schemas: HashSet<String>,
287 pub untagged_enum_schemas: HashSet<String>,
289 pub type_mappings: BTreeMap<String, BTreeMap<String, String>>,
291}
292
293#[derive(Debug, Clone, Default, serde::Serialize)]
295pub struct OperationInfo {
296 pub operation_id: String,
298 pub method: String,
300 pub path: String,
302 pub summary: Option<String>,
304 pub description: Option<String>,
306 pub request_body: Option<RequestBodyContent>,
308 pub request_body_required: bool,
311 pub response_schemas: BTreeMap<String, String>,
313 pub parameters: Vec<ParameterInfo>,
315 pub supports_streaming: bool,
317 pub stream_parameter: Option<String>,
319 pub tags: Vec<String>,
323}
324
325#[derive(Debug, Clone, serde::Serialize)]
327#[serde(tag = "kind")]
328pub enum RequestBodyContent {
329 Json { schema_name: String },
330 FormUrlEncoded { schema_name: String },
331 Multipart,
332 OctetStream,
333 TextPlain,
334}
335
336impl RequestBodyContent {
337 pub fn schema_name(&self) -> Option<&str> {
339 match self {
340 Self::Json { schema_name } | Self::FormUrlEncoded { schema_name } => Some(schema_name),
341 _ => None,
342 }
343 }
344}
345
346fn base_param_ident(name: &str) -> String {
350 use heck::ToSnakeCase;
351 let suffix = if name.ends_with("<=") {
352 "_lte"
353 } else if name.ends_with(">=") {
354 "_gte"
355 } else if name.ends_with('<') {
356 "_lt"
357 } else if name.ends_with('>') {
358 "_gt"
359 } else {
360 ""
361 };
362 let stripped = name.trim_end_matches(['<', '>', '=']);
363 let mut snake = stripped.to_snake_case();
364 snake.push_str(suffix);
365 snake
366}
367
368#[derive(Debug, Clone, serde::Serialize)]
370pub struct ParameterInfo {
371 pub name: String,
373 pub location: String,
375 pub required: bool,
377 pub schema_ref: Option<String>,
379 pub rust_type: String,
381 pub description: Option<String>,
383 #[serde(skip_serializing_if = "Option::is_none")]
389 pub enum_values: Option<Vec<String>>,
390 #[serde(skip_serializing_if = "Option::is_none")]
398 pub rust_ident: Option<String>,
399 #[serde(skip_serializing_if = "Option::is_none")]
408 pub query_serialization: Option<QuerySerialization>,
409}
410
411#[derive(Debug, Clone, PartialEq, serde::Serialize)]
414pub enum QuerySerialization {
415 FormExplodedObject,
419 FormObject,
422 DeepObject,
425 FormExplodedArray { item_type: ArrayItemType },
428 FormArray { item_type: ArrayItemType },
431 Unsupported { reason: String },
436}
437
438#[derive(Debug, Clone, PartialEq, serde::Serialize)]
445pub enum ArrayItemType {
446 Scalar(String),
448 EnumRef(String),
450}
451
452impl Default for DependencyGraph {
453 fn default() -> Self {
454 Self::new()
455 }
456}
457
458impl DependencyGraph {
459 pub fn new() -> Self {
460 Self {
461 edges: BTreeMap::new(),
462 recursive_schemas: HashSet::new(),
463 }
464 }
465
466 pub fn add_dependency(&mut self, from: String, to: String) {
467 self.edges.entry(from).or_default().insert(to);
468 }
469
470 pub fn topological_sort(&mut self) -> Result<Vec<String>> {
472 self.detect_recursive_schemas();
474
475 let mut temp_edges = self.edges.clone();
477 for (schema, deps) in &mut temp_edges {
478 deps.remove(schema); }
480
481 let mut visited = HashSet::new();
482 let mut temp_visited = HashSet::new();
483 let mut result = Vec::new();
484
485 let mut all_nodes: Vec<_> = temp_edges.keys().collect();
487 all_nodes.sort();
488 for node in all_nodes {
489 if !visited.contains(node) {
490 self.visit_node_recursive(
491 node,
492 &temp_edges,
493 &mut visited,
494 &mut temp_visited,
495 &mut result,
496 )?;
497 }
498 }
499
500 result.reverse();
501 Ok(result)
502 }
503
504 fn detect_recursive_schemas(&mut self) {
505 for (schema, deps) in &self.edges {
506 if deps.contains(schema) {
507 self.recursive_schemas.insert(schema.clone());
509 } else {
510 if self.has_cycle_from(schema, schema, &mut HashSet::new()) {
512 self.recursive_schemas.insert(schema.clone());
513 }
514 }
515 }
516
517 for (schema, deps) in &self.edges {
519 for dep in deps {
520 if let Some(dep_deps) = self.edges.get(dep) {
521 if dep_deps.contains(schema) {
522 self.recursive_schemas.insert(schema.clone());
524 self.recursive_schemas.insert(dep.clone());
525 }
526 }
527 }
528 }
529 }
530
531 fn has_cycle_from(&self, start: &str, current: &str, visited: &mut HashSet<String>) -> bool {
532 if visited.contains(current) {
533 return false; }
535
536 visited.insert(current.to_string());
537
538 if let Some(deps) = self.edges.get(current) {
539 for dep in deps {
540 if dep == start {
541 return true; }
543 if self.has_cycle_from(start, dep, visited) {
544 return true;
545 }
546 }
547 }
548
549 false
550 }
551
552 #[allow(clippy::only_used_in_recursion)]
553 fn visit_node_recursive(
554 &self,
555 node: &str,
556 temp_edges: &BTreeMap<String, HashSet<String>>,
557 visited: &mut HashSet<String>,
558 temp_visited: &mut HashSet<String>,
559 result: &mut Vec<String>,
560 ) -> Result<()> {
561 if temp_visited.contains(node) {
562 return Ok(());
564 }
565
566 if visited.contains(node) {
567 return Ok(());
568 }
569
570 temp_visited.insert(node.to_string());
571
572 if let Some(dependencies) = temp_edges.get(node) {
573 let mut sorted_deps: Vec<_> = dependencies.iter().collect();
575 sorted_deps.sort();
576 for dep in sorted_deps {
577 self.visit_node_recursive(dep, temp_edges, visited, temp_visited, result)?;
578 }
579 }
580
581 temp_visited.remove(node);
582 visited.insert(node.to_string());
583 result.push(node.to_string());
584
585 Ok(())
586 }
587}
588
589pub fn merge_schema_extensions(
592 main_spec: Value,
593 extension_paths: &[impl AsRef<Path>],
594) -> Result<Value> {
595 let mut result = main_spec;
596
597 for path in extension_paths {
598 let extension = load_extension_file(path.as_ref())?;
599 result = merge_json_objects_with_replacements(result, extension)?;
600 }
601
602 Ok(result)
603}
604
605fn load_extension_file(path: &Path) -> Result<Value> {
607 let content = std::fs::read_to_string(path).map_err(|e| GeneratorError::FileError {
608 message: format!("Failed to read file {}: {}", path.display(), e),
609 })?;
610
611 serde_json::from_str(&content).map_err(GeneratorError::ParseError)
612}
613
614fn merge_json_objects_with_replacements(main: Value, extension: Value) -> Result<Value> {
616 let replacements = extract_replacement_rules(&extension);
618
619 Ok(merge_json_objects_with_rules(
621 main,
622 extension,
623 &replacements,
624 ))
625}
626
627fn extract_replacement_rules(
629 extension: &Value,
630) -> std::collections::HashMap<String, (String, String)> {
631 let mut rules = std::collections::HashMap::new();
632
633 if let Some(x_replacements) = extension.get("x-replacements") {
634 if let Some(x_replacements_obj) = x_replacements.as_object() {
635 for (schema_name, replacement_rule) in x_replacements_obj {
636 if let Some(rule_obj) = replacement_rule.as_object() {
637 if let (Some(replace), Some(with)) = (
638 rule_obj.get("replace").and_then(|v| v.as_str()),
639 rule_obj.get("with").and_then(|v| v.as_str()),
640 ) {
641 rules.insert(schema_name.clone(), (replace.to_string(), with.to_string()));
642 }
644 }
645 }
646 }
647 }
648
649 rules
650}
651
652fn should_replace_variant(
654 schema_name: &str,
655 extension_refs: &[String],
656 replacements: &std::collections::HashMap<String, (String, String)>,
657) -> bool {
658 for (replace_schema, with_schema) in replacements.values() {
660 if schema_name == replace_schema {
661 let replacement_exists = extension_refs.iter().any(|ext_ref| {
663 let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
664 ext_schema_name == with_schema
665 });
666
667 if replacement_exists {
668 return true;
669 }
670 }
671 }
672
673 extension_refs.iter().any(|ext_ref| {
675 let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
676 schema_name == ext_schema_name
677 })
678}
679
680fn merge_json_objects_with_rules(
685 main: Value,
686 extension: Value,
687 replacements: &std::collections::HashMap<String, (String, String)>,
688) -> Value {
689 match (main, extension) {
690 (Value::Object(mut main_obj), Value::Object(ext_obj)) => {
692 let main_union_keyword = if main_obj.contains_key("oneOf") {
695 Some("oneOf")
696 } else if main_obj.contains_key("anyOf") {
697 Some("anyOf")
698 } else {
699 None
700 };
701 if let (Some(main_variants), Some(ext_variants)) = (
702 extract_schema_variants(&Value::Object(main_obj.clone())),
703 extract_schema_variants(&Value::Object(ext_obj.clone())),
704 ) {
705 let union_key = main_union_keyword.unwrap_or("oneOf");
706 println!(
707 "🔍 Merging union schemas ({union_key}): {} main variants, {} extension variants",
708 main_variants.len(),
709 ext_variants.len()
710 );
711 let mut merged_variants = Vec::new();
714 let extension_refs: Vec<String> = ext_variants
715 .iter()
716 .filter_map(|v| v.get("$ref").and_then(|r| r.as_str()))
717 .map(|s| s.to_string())
718 .collect();
719
720 for main_variant in main_variants {
722 if let Some(main_ref) = main_variant.get("$ref").and_then(|r| r.as_str()) {
723 let schema_name = main_ref.split('/').next_back().unwrap_or("");
725 let should_replace =
726 should_replace_variant(schema_name, &extension_refs, replacements);
727
728 if should_replace {
729 println!("🔄 REPLACING {} (explicit rule)", schema_name);
730 }
731
732 if !should_replace {
733 merged_variants.push(main_variant);
734 }
735 } else {
736 merged_variants.push(main_variant);
738 }
739 }
740
741 for ext_variant in ext_variants {
743 merged_variants.push(ext_variant);
744 }
745
746 main_obj.remove("oneOf");
748 main_obj.remove("anyOf");
749 main_obj.insert(union_key.to_string(), Value::Array(merged_variants));
750
751 for (key, ext_value) in ext_obj {
753 if key != "oneOf" && key != "anyOf" {
754 match main_obj.get(&key) {
755 Some(main_value) => {
756 let merged_value = merge_json_objects_with_rules(
757 main_value.clone(),
758 ext_value,
759 replacements,
760 );
761 main_obj.insert(key, merged_value);
762 }
763 None => {
764 main_obj.insert(key, ext_value);
765 }
766 }
767 }
768 }
769
770 return Value::Object(main_obj);
771 }
772
773 for (key, ext_value) in ext_obj {
775 match main_obj.get(&key) {
776 Some(main_value) => {
777 let merged_value = merge_json_objects_with_rules(
779 main_value.clone(),
780 ext_value,
781 replacements,
782 );
783 main_obj.insert(key, merged_value);
784 }
785 None => {
786 main_obj.insert(key, ext_value);
788 }
789 }
790 }
791 Value::Object(main_obj)
792 }
793
794 (Value::Array(mut main_arr), Value::Array(ext_arr)) => {
796 main_arr.extend(ext_arr);
797 Value::Array(main_arr)
798 }
799
800 (_, extension) => extension,
802 }
803}
804
805fn extract_schema_variants(obj: &Value) -> Option<Vec<Value>> {
807 if let Value::Object(map) = obj {
808 if let Some(Value::Array(variants)) = map.get("oneOf") {
809 return Some(variants.clone());
810 }
811 if let Some(Value::Array(variants)) = map.get("anyOf") {
812 return Some(variants.clone());
813 }
814 }
815 None
816}
817
818pub struct SchemaAnalyzer {
819 schemas: BTreeMap<String, Schema>,
820 resolved_cache: BTreeMap<String, AnalyzedSchema>,
821 openapi_spec: Value,
822 current_schema_name: Option<String>,
823 component_parameters: BTreeMap<String, crate::openapi::Parameter>,
824 type_mapper: TypeMapper,
829}
830
831impl SchemaAnalyzer {
832 pub fn new(openapi_spec: Value) -> Result<Self> {
836 Self::with_type_mapper(openapi_spec, TypeMapper::default())
837 }
838
839 pub fn with_type_mapper(openapi_spec: Value, type_mapper: TypeMapper) -> Result<Self> {
843 let spec: OpenApiSpec =
844 serde_json::from_value(openapi_spec.clone()).map_err(GeneratorError::ParseError)?;
845 let schemas = Self::extract_schemas(&spec)?;
846
847 let component_parameters = spec
848 .components
849 .as_ref()
850 .and_then(|c| c.parameters.as_ref())
851 .cloned()
852 .unwrap_or_default();
853
854 Ok(Self {
855 schemas,
856 resolved_cache: BTreeMap::new(),
857 openapi_spec,
858 current_schema_name: None,
859 component_parameters,
860 type_mapper,
861 })
862 }
863
864 pub fn new_with_extensions(
867 openapi_spec: Value,
868 extension_paths: &[std::path::PathBuf],
869 ) -> Result<Self> {
870 let merged_spec = merge_schema_extensions(openapi_spec, extension_paths)?;
871 Self::new(merged_spec)
872 }
873
874 pub fn new_with_extensions_and_type_mapper(
877 openapi_spec: Value,
878 extension_paths: &[std::path::PathBuf],
879 type_mapper: TypeMapper,
880 ) -> Result<Self> {
881 let merged_spec = merge_schema_extensions(openapi_spec, extension_paths)?;
882 Self::with_type_mapper(merged_spec, type_mapper)
883 }
884
885 pub fn type_mapper(&self) -> &TypeMapper {
889 &self.type_mapper
890 }
891
892 fn generate_context_aware_name(
895 &self,
896 base_context: &str,
897 type_hint: &str,
898 index: usize,
899 schema: Option<&Schema>,
900 ) -> String {
901 if let Some(schema) = schema {
903 if type_hint == "Array"
905 && matches!(schema.schema_type(), Some(OpenApiSchemaType::Array))
906 {
907 if let Some(items_schema) = &schema.details().items {
908 if let Some(item_type) = items_schema.schema_type() {
910 match item_type {
911 OpenApiSchemaType::Object => {
912 return format!("{base_context}ItemArray");
913 }
914 OpenApiSchemaType::String => {
915 return format!("{base_context}StringArray");
916 }
917 _ => {}
918 }
919 }
920 }
921 }
922 }
923
924 match type_hint {
926 "Array" => {
927 format!("{base_context}Array")
929 }
930 "Variant" | "InlineVariant" => {
931 if index == 0 {
933 format!("{base_context}{type_hint}")
934 } else {
935 format!("{}{}{}", base_context, type_hint, index + 1)
936 }
937 }
938 _ => {
939 format!("{base_context}{type_hint}{index}")
941 }
942 }
943 }
944
945 fn to_pascal_case(&self, s: &str) -> String {
947 s.split(['_', '-'])
948 .filter(|part| !part.is_empty())
949 .map(|part| {
950 let mut chars = part.chars();
951 match chars.next() {
952 None => String::new(),
953 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
954 }
955 })
956 .collect()
957 }
958
959 fn extract_schemas(spec: &OpenApiSpec) -> Result<BTreeMap<String, Schema>> {
960 let schemas = spec.components.as_ref().and_then(|c| c.schemas.as_ref());
965 Ok(schemas
966 .map(|m| {
967 m.iter()
968 .map(|(k, v)| (k.clone(), v.clone()))
969 .collect::<BTreeMap<_, _>>()
970 })
971 .unwrap_or_default())
972 }
973
974 pub fn analyze(&mut self) -> Result<SchemaAnalysis> {
975 let mut analysis = SchemaAnalysis {
976 schemas: BTreeMap::new(),
977 dependencies: DependencyGraph::new(),
978 patterns: DetectedPatterns {
979 tagged_enum_schemas: HashSet::new(),
980 untagged_enum_schemas: HashSet::new(),
981 type_mappings: BTreeMap::new(),
982 },
983 operations: BTreeMap::new(),
984 operation_id_aliases: BTreeMap::new(),
985 used_type_features: crate::type_mapping::UsedFeatures::default(),
986 enum_extensions: BTreeMap::new(),
987 };
988
989 self.detect_patterns(&mut analysis.patterns)?;
991
992 let schema_names: Vec<String> = self.schemas.keys().cloned().collect();
994 for schema_name in schema_names {
995 let analyzed = self.analyze_schema(&schema_name)?;
996
997 for dep in &analyzed.dependencies {
999 analysis
1000 .dependencies
1001 .add_dependency(schema_name.clone(), dep.clone());
1002 }
1003
1004 analysis.schemas.insert(schema_name, analyzed);
1005 }
1006
1007 for (inline_name, inline_schema) in &self.resolved_cache {
1010 if !analysis.schemas.contains_key(inline_name) {
1011 analysis
1013 .schemas
1014 .insert(inline_name.clone(), inline_schema.clone());
1015
1016 for dep in &inline_schema.dependencies {
1018 analysis
1019 .dependencies
1020 .add_dependency(inline_name.clone(), dep.clone());
1021 }
1022
1023 let mut schemas_to_update = Vec::new();
1028 for (schema_name, schema) in &analysis.schemas {
1029 if schema_name == inline_name {
1031 continue;
1032 }
1033
1034 if schema.dependencies.contains(inline_name) {
1035 schemas_to_update.push(schema_name.clone());
1037 }
1038 }
1039
1040 for schema_name in schemas_to_update {
1042 analysis
1043 .dependencies
1044 .add_dependency(schema_name, inline_name.clone());
1045 }
1046 }
1047 }
1048
1049 self.analyze_operations(&mut analysis)?;
1051
1052 for (inline_name, inline_schema) in &self.resolved_cache {
1055 if !analysis.schemas.contains_key(inline_name) {
1056 analysis
1057 .schemas
1058 .insert(inline_name.clone(), inline_schema.clone());
1059
1060 for dep in &inline_schema.dependencies {
1062 analysis
1063 .dependencies
1064 .add_dependency(inline_name.clone(), dep.clone());
1065 }
1066 }
1067 }
1068
1069 analysis.used_type_features = self.type_mapper.used_features();
1073
1074 for (name, analyzed) in &analysis.schemas {
1079 let enum_value_count = match &analyzed.schema_type {
1080 SchemaType::StringEnum { values } => values.len(),
1081 SchemaType::ExtensibleEnum { known_values } => known_values.len(),
1082 _ => continue,
1083 };
1084 if let Some(ext) = extract_enum_extensions(&analyzed.original, enum_value_count, name) {
1085 analysis.enum_extensions.insert(name.clone(), ext);
1086 }
1087 }
1088
1089 Ok(analysis)
1090 }
1091
1092 fn detect_patterns(&self, patterns: &mut DetectedPatterns) -> Result<()> {
1093 for (schema_name, schema) in &self.schemas {
1094 if self.is_discriminated_union(schema) {
1096 patterns.tagged_enum_schemas.insert(schema_name.clone());
1097
1098 if let Some(mappings) = self.extract_type_mappings(schema)? {
1100 patterns.type_mappings.insert(schema_name.clone(), mappings);
1101 }
1102 }
1103 else if self.is_simple_union(schema) {
1105 patterns.untagged_enum_schemas.insert(schema_name.clone());
1106 }
1107 }
1108
1109 Ok(())
1110 }
1111
1112 fn is_discriminated_union(&self, schema: &Schema) -> bool {
1113 if schema.is_discriminated_union() {
1115 return true;
1116 }
1117
1118 if let Some(variants) = schema.union_variants() {
1120 return variants.len() > 2 && self.detect_discriminator_field(variants).is_some();
1121 }
1122
1123 false
1124 }
1125
1126 fn all_variants_have_const_field(&self, variants: &[Schema], field_name: &str) -> bool {
1127 variants.iter().all(|variant| {
1128 if let Some(ref_str) = variant.reference() {
1129 if let Some(schema_name) = self.extract_schema_name(ref_str) {
1131 if let Some(schema) = self.schemas.get(schema_name) {
1132 return self.has_const_discriminator_field(schema, field_name);
1133 }
1134 }
1135 } else {
1136 return self.has_const_discriminator_field(variant, field_name);
1138 }
1139 false
1140 })
1141 }
1142
1143 fn branch_resolves_to_object(&self, schema: &Schema) -> bool {
1152 if let Some(ref_str) = schema.reference() {
1154 return match self
1155 .extract_schema_name(ref_str)
1156 .and_then(|n| self.schemas.get(n))
1157 {
1158 Some(target) => self.branch_resolves_to_object(target),
1159 None => false,
1160 };
1161 }
1162 if matches!(
1165 schema,
1166 Schema::AllOf { .. } | Schema::AnyOf { .. } | Schema::OneOf { .. }
1167 ) {
1168 return true;
1169 }
1170 if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object)) {
1171 return true;
1172 }
1173 if schema.inferred_type() == Some(OpenApiSchemaType::Object) {
1174 return true;
1175 }
1176 false
1179 }
1180
1181 fn detect_discriminator_field(&self, variants: &[Schema]) -> Option<String> {
1185 if variants.is_empty() {
1186 return None;
1187 }
1188
1189 let first_variant = &variants[0];
1191 let first_schema = if let Some(ref_str) = first_variant.reference() {
1192 let schema_name = self.extract_schema_name(ref_str)?;
1193 self.schemas.get(schema_name)?
1194 } else {
1195 first_variant
1196 };
1197
1198 let properties = first_schema.details().properties.as_ref()?;
1199 let mut candidates: Vec<String> = Vec::new();
1200
1201 for (field_name, field_schema) in properties {
1202 let details = field_schema.details();
1203 let is_const = details.const_value.is_some()
1204 || details.enum_values.as_ref().is_some_and(|v| v.len() == 1)
1205 || details.extra.contains_key("const");
1206 if is_const {
1207 candidates.push(field_name.clone());
1208 }
1209 }
1210
1211 if candidates.is_empty() {
1212 return None;
1213 }
1214
1215 candidates.sort_by(|a, b| {
1217 if a == "type" {
1218 std::cmp::Ordering::Less
1219 } else if b == "type" {
1220 std::cmp::Ordering::Greater
1221 } else {
1222 a.cmp(b)
1223 }
1224 });
1225
1226 for candidate in &candidates {
1228 if self.all_variants_have_const_field(variants, candidate) {
1229 return Some(candidate.clone());
1230 }
1231 }
1232
1233 None
1234 }
1235
1236 fn has_const_discriminator_field(&self, schema: &Schema, field_name: &str) -> bool {
1237 if let Some(properties) = &schema.details().properties {
1238 if let Some(field) = properties.get(field_name) {
1239 if field.details().const_value.is_some() {
1241 return true;
1242 }
1243 if let Some(enum_vals) = &field.details().enum_values {
1245 return enum_vals.len() == 1;
1246 }
1247 return field.details().extra.contains_key("const");
1249 }
1250 }
1251 false
1252 }
1253
1254 fn is_simple_union(&self, schema: &Schema) -> bool {
1255 if let Some(variants) = schema.union_variants() {
1256 if variants.len() > 1 && !schema.is_nullable_pattern() {
1258 let has_refs = variants.iter().any(|v| v.is_reference());
1259 return has_refs;
1260 }
1261 }
1262 false
1263 }
1264
1265 fn extract_type_mappings(&self, schema: &Schema) -> Result<Option<BTreeMap<String, String>>> {
1266 let variants = schema.union_variants().ok_or_else(|| {
1267 GeneratorError::InvalidSchema("No variants found for discriminated union".to_string())
1268 })?;
1269
1270 let discriminator_field = if let Some(discriminator) = schema.discriminator() {
1272 discriminator.property_name.clone()
1273 } else if let Some(detected) = self.detect_discriminator_field(variants) {
1274 detected
1275 } else {
1276 "type".to_string() };
1278
1279 let mut mappings = BTreeMap::new();
1280
1281 for variant in variants {
1282 if let Some(ref_str) = variant.reference() {
1283 if let Some(type_name) = self.extract_schema_name(ref_str) {
1284 if let Some(variant_schema) = self.schemas.get(type_name) {
1285 if let Some(discriminator_value) = self
1286 .extract_discriminator_value_for_field(
1287 variant_schema,
1288 &discriminator_field,
1289 )
1290 {
1291 mappings.insert(type_name.to_string(), discriminator_value);
1292 }
1293 }
1294 }
1295 }
1296 }
1297
1298 if mappings.is_empty() {
1299 Ok(None)
1300 } else {
1301 Ok(Some(mappings))
1302 }
1303 }
1304
1305 #[allow(dead_code)]
1306 fn extract_discriminator_value(&self, schema: &Schema) -> Option<String> {
1307 self.extract_discriminator_value_for_field(schema, "type")
1308 }
1309
1310 fn extract_discriminator_value_for_field(
1311 &self,
1312 schema: &Schema,
1313 field_name: &str,
1314 ) -> Option<String> {
1315 if let Some(properties) = &schema.details().properties {
1316 if let Some(type_field) = properties.get(field_name) {
1317 if let Some(const_value) = &type_field.details().const_value {
1319 if let Some(value) = const_value.as_str() {
1320 return Some(value.to_string());
1321 }
1322 }
1323 if let Some(enum_values) = &type_field.details().enum_values {
1325 if enum_values.len() == 1 {
1326 return enum_values[0].as_str().map(|s| s.to_string());
1327 }
1328 }
1329 if let Some(const_value) = type_field.details().extra.get("const") {
1331 return const_value.as_str().map(|s| s.to_string());
1332 }
1333 if let Some(stainless_const) = type_field.details().extra.get("x-stainless-const") {
1335 if stainless_const.as_bool() == Some(true) {
1336 if let Some(default_value) = &type_field.details().default {
1337 if let Some(value) = default_value.as_str() {
1338 return Some(value.to_string());
1339 }
1340 }
1341 }
1342 }
1343 }
1344 }
1345 None
1346 }
1347
1348 fn get_any_reference<'a>(&self, schema: &'a Schema) -> Option<&'a str> {
1349 schema.reference().or_else(|| schema.recursive_reference())
1350 }
1351
1352 fn extract_schema_name<'a>(&self, ref_str: &'a str) -> Option<&'a str> {
1353 if ref_str == "#" {
1354 return None; }
1356
1357 let parts: Vec<&str> = ref_str.split('/').collect();
1358
1359 if parts.len() >= 4 && parts[0] == "#" && parts[2] == "schemas" {
1361 return Some(parts[3]);
1362 }
1363
1364 if parts.len() >= 3 && parts[0] == "#" && parts[1] == "definitions" {
1367 return Some(parts[2]);
1368 }
1369
1370 let last = parts.last()?;
1376 if last.is_empty()
1377 || last.chars().all(|c| c.is_ascii_digit())
1378 || matches!(
1379 *last,
1380 "schema" | "properties" | "items" | "additionalProperties"
1381 )
1382 {
1383 return None;
1384 }
1385 let first = last.chars().next().unwrap_or(' ');
1386 if !first.is_ascii_alphabetic() || !first.is_ascii_uppercase() {
1387 return None;
1388 }
1389 Some(last)
1390 }
1391
1392 fn analyze_schema(&mut self, schema_name: &str) -> Result<AnalyzedSchema> {
1393 if let Some(cached) = self.resolved_cache.get(schema_name) {
1395 return Ok(cached.clone());
1396 }
1397
1398 self.current_schema_name = Some(schema_name.to_string());
1400
1401 let schema = self
1402 .schemas
1403 .get(schema_name)
1404 .ok_or_else(|| GeneratorError::UnresolvedReference(schema_name.to_string()))?
1405 .clone();
1406
1407 self.resolved_cache.insert(
1409 schema_name.to_string(),
1410 AnalyzedSchema {
1411 name: schema_name.to_string(),
1412 original: serde_json::to_value(&schema).unwrap_or(Value::Null),
1413 schema_type: SchemaType::Reference {
1414 target: "placeholder".to_string(),
1415 },
1416 dependencies: HashSet::new(),
1417 nullable: false,
1418 description: None,
1419 default: None,
1420 },
1421 );
1422
1423 let analyzed = self.analyze_schema_value(&schema, schema_name)?;
1424
1425 self.resolved_cache
1427 .insert(schema_name.to_string(), analyzed.clone());
1428
1429 Ok(analyzed)
1430 }
1431
1432 fn analyze_schema_value(
1433 &mut self,
1434 schema: &Schema,
1435 schema_name: &str,
1436 ) -> Result<AnalyzedSchema> {
1437 let details = schema.details();
1438 let description = details.description.clone();
1439 let nullable = details.is_nullable() || schema.type_array_contains_null();
1441 let mut dependencies = HashSet::new();
1442
1443 let schema_type = match schema {
1444 Schema::Reference { reference, .. } => {
1445 match self.extract_schema_name(reference) {
1450 Some(name) => {
1451 let target = name.to_string();
1452 dependencies.insert(target.clone());
1453 SchemaType::Reference { target }
1454 }
1455 None => {
1456 eprintln!(
1457 "⚠️ unresolvable $ref `{}` — typing as serde_json::Value",
1458 reference
1459 );
1460 SchemaType::Primitive {
1461 rust_type: "serde_json::Value".to_string(),
1462 serde_with: None,
1463 }
1464 }
1465 }
1466 }
1467 Schema::RecursiveRef { recursive_ref, .. }
1468 | Schema::DynamicRef {
1469 dynamic_ref: recursive_ref,
1470 ..
1471 } => {
1472 if recursive_ref == "#" {
1478 dependencies.insert(schema_name.to_string());
1479 SchemaType::Reference {
1480 target: schema_name.to_string(),
1481 }
1482 } else {
1483 let target = self
1484 .extract_schema_name(recursive_ref)
1485 .unwrap_or(schema_name)
1486 .to_string();
1487 dependencies.insert(target.clone());
1488 SchemaType::Reference { target }
1489 }
1490 }
1491 Schema::Typed { .. } | Schema::TypedMulti { .. } => {
1492 let primary = schema
1493 .schema_type()
1494 .cloned()
1495 .unwrap_or(OpenApiSchemaType::Object);
1496 let format = details.format.as_deref();
1497 match primary {
1498 OpenApiSchemaType::String => {
1499 if let Some(values) = details.string_enum_values() {
1500 SchemaType::StringEnum { values }
1501 } else {
1502 SchemaType::Primitive {
1503 rust_type: self.type_mapper.string_format(format).rust_type,
1504 serde_with: None,
1505 }
1506 }
1507 }
1508 OpenApiSchemaType::Integer => SchemaType::Primitive {
1509 rust_type: self.type_mapper.integer_format(format).rust_type,
1510 serde_with: None,
1511 },
1512 OpenApiSchemaType::Number => SchemaType::Primitive {
1513 rust_type: self.type_mapper.number_format(format).rust_type,
1514 serde_with: None,
1515 },
1516 OpenApiSchemaType::Boolean => SchemaType::Primitive {
1517 rust_type: self.type_mapper.boolean().rust_type,
1518 serde_with: None,
1519 },
1520 OpenApiSchemaType::Array => {
1521 self.analyze_array_schema(schema, schema_name, &mut dependencies)?
1523 }
1524 OpenApiSchemaType::Object => {
1525 if self.should_use_dynamic_json(schema) {
1527 SchemaType::Primitive {
1528 rust_type: self.type_mapper.dynamic_json().rust_type,
1529 serde_with: None,
1530 }
1531 } else {
1532 self.analyze_object_schema(schema, &mut dependencies)?
1534 }
1535 }
1536 _ => SchemaType::Primitive {
1537 rust_type: self.type_mapper.dynamic_json().rust_type,
1538 serde_with: None,
1539 },
1540 }
1541 }
1542 Schema::AnyOf {
1543 any_of,
1544 discriminator,
1545 ..
1546 } => {
1547 self.analyze_anyof_union(
1549 any_of,
1550 discriminator.as_ref(),
1551 &mut dependencies,
1552 schema_name,
1553 )?
1554 }
1555 Schema::OneOf {
1556 one_of,
1557 discriminator,
1558 ..
1559 } => {
1560 self.analyze_oneof_union(
1562 one_of,
1563 discriminator.as_ref(),
1564 schema_name,
1565 &mut dependencies,
1566 )?
1567 }
1568 Schema::AllOf { all_of, .. } => {
1569 self.analyze_allof_composition(all_of, &mut dependencies)?
1571 }
1572 Schema::Untyped { .. } => {
1573 if let Some(inferred) = schema.inferred_type() {
1575 match inferred {
1576 OpenApiSchemaType::Object => {
1577 if self.should_use_dynamic_json(schema) {
1578 SchemaType::Primitive {
1579 rust_type: "serde_json::Value".to_string(),
1580 serde_with: None,
1581 }
1582 } else {
1583 self.analyze_object_schema(schema, &mut dependencies)?
1584 }
1585 }
1586 OpenApiSchemaType::String if details.is_string_enum() => {
1587 SchemaType::StringEnum {
1588 values: details.string_enum_values().unwrap_or_default(),
1589 }
1590 }
1591 _ => SchemaType::Primitive {
1592 rust_type: "serde_json::Value".to_string(),
1593 serde_with: None,
1594 },
1595 }
1596 } else {
1597 SchemaType::Primitive {
1598 rust_type: "serde_json::Value".to_string(),
1599 serde_with: None,
1600 }
1601 }
1602 }
1603 };
1604
1605 Ok(AnalyzedSchema {
1606 name: schema_name.to_string(),
1607 original: serde_json::to_value(schema).unwrap_or(Value::Null), schema_type,
1609 dependencies,
1610 nullable,
1611 description,
1612 default: details.default.clone(),
1613 })
1614 }
1615
1616 fn analyze_object_schema(
1617 &mut self,
1618 schema: &Schema,
1619 dependencies: &mut HashSet<String>,
1620 ) -> Result<SchemaType> {
1621 let details = schema.details();
1622 let properties = &details.properties;
1623 let required = details
1624 .required
1625 .as_ref()
1626 .map(|req| req.iter().cloned().collect::<HashSet<String>>())
1627 .unwrap_or_default();
1628
1629 let mut property_info = BTreeMap::new();
1630
1631 if let Some(props) = properties {
1632 for (prop_name, prop_schema) in props {
1633 let prop_type = if let Schema::AnyOf { any_of, .. } = prop_schema {
1635 if self.should_use_dynamic_json(prop_schema) {
1637 SchemaType::Primitive {
1639 rust_type: "serde_json::Value".to_string(),
1640 serde_with: None,
1641 }
1642 } else if prop_schema.is_nullable_pattern()
1643 && let Some(non_null) = prop_schema.non_null_variant()
1644 {
1645 self.analyze_property_schema_with_context(
1653 non_null,
1654 Some(prop_name),
1655 dependencies,
1656 )?
1657 } else {
1658 let context_name = self
1661 .current_schema_name
1662 .clone()
1663 .unwrap_or_else(|| "Unknown".to_string());
1664
1665 let prop_pascal = self.to_pascal_case(prop_name);
1667 let mut union_type_name = format!("{context_name}{prop_pascal}");
1668
1669 if self.schemas.contains_key(&union_type_name)
1672 || self.resolved_cache.contains_key(&union_type_name)
1673 {
1674 let mut suffix = 2;
1675 loop {
1676 let candidate = format!("{union_type_name}Union{suffix}");
1677 if !self.schemas.contains_key(&candidate)
1678 && !self.resolved_cache.contains_key(&candidate)
1679 {
1680 union_type_name = candidate;
1681 break;
1682 }
1683 suffix += 1;
1684 if suffix > 1000 {
1685 break;
1686 }
1687 }
1688 }
1689
1690 let union_schema_type = self.analyze_anyof_union(
1692 any_of,
1693 prop_schema.discriminator(),
1694 dependencies,
1695 &union_type_name,
1696 )?;
1697
1698 self.resolved_cache.insert(
1700 union_type_name.clone(),
1701 AnalyzedSchema {
1702 name: union_type_name.clone(),
1703 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
1704 schema_type: union_schema_type,
1705 dependencies: HashSet::new(),
1706 nullable: false,
1707 description: prop_schema.details().description.clone(),
1708 default: None,
1709 },
1710 );
1711
1712 dependencies.insert(union_type_name.clone());
1714 SchemaType::Reference {
1715 target: union_type_name,
1716 }
1717 }
1718 } else if let Schema::OneOf {
1719 one_of,
1720 discriminator,
1721 ..
1722 } = prop_schema
1723 {
1724 if prop_schema.is_nullable_pattern()
1731 && let Some(non_null) = prop_schema.non_null_variant()
1732 {
1733 let unwrapped = self.analyze_property_schema_with_context(
1734 non_null,
1735 Some(prop_name),
1736 dependencies,
1737 )?;
1738 let prop_details = prop_schema.details();
1739 let prop_nullable = true;
1740 let prop_description = prop_details.description.clone();
1741 let prop_default = prop_details.default.clone();
1742 property_info.insert(
1743 prop_name.clone(),
1744 PropertyInfo {
1745 schema_type: unwrapped,
1746 nullable: prop_nullable,
1747 description: prop_description,
1748 default: prop_default,
1749 serde_attrs: Vec::new(),
1750 constraints: PropertyConstraints::from_schema_details(prop_details),
1751 },
1752 );
1753 continue;
1754 }
1755
1756 let context_name = self
1758 .current_schema_name
1759 .clone()
1760 .unwrap_or_else(|| "Unknown".to_string());
1761 let prop_pascal = self.to_pascal_case(prop_name);
1762 let mut union_type_name = format!("{context_name}{prop_pascal}");
1763 if self.schemas.contains_key(&union_type_name)
1765 || self.resolved_cache.contains_key(&union_type_name)
1766 {
1767 let mut suffix = 2;
1768 loop {
1769 let candidate = format!("{union_type_name}Union{suffix}");
1770 if !self.schemas.contains_key(&candidate)
1771 && !self.resolved_cache.contains_key(&candidate)
1772 {
1773 union_type_name = candidate;
1774 break;
1775 }
1776 suffix += 1;
1777 if suffix > 1000 {
1778 break;
1779 }
1780 }
1781 }
1782
1783 let union_schema_type = self.analyze_oneof_union(
1785 one_of,
1786 discriminator.as_ref(),
1787 &union_type_name,
1788 dependencies,
1789 )?;
1790
1791 self.resolved_cache.insert(
1793 union_type_name.clone(),
1794 AnalyzedSchema {
1795 name: union_type_name.clone(),
1796 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
1797 schema_type: union_schema_type,
1798 dependencies: HashSet::new(),
1799 nullable: false,
1800 description: prop_schema.details().description.clone(),
1801 default: None,
1802 },
1803 );
1804
1805 dependencies.insert(union_type_name.clone());
1807 SchemaType::Reference {
1808 target: union_type_name,
1809 }
1810 } else {
1811 self.analyze_property_schema_with_context(
1813 prop_schema,
1814 Some(prop_name),
1815 dependencies,
1816 )?
1817 };
1818
1819 let prop_details = prop_schema.details();
1820 let prop_nullable = prop_details.is_nullable() || prop_schema.is_nullable_pattern();
1822 let prop_description = prop_details.description.clone();
1823 let prop_default = prop_details.default.clone();
1824
1825 property_info.insert(
1826 prop_name.clone(),
1827 PropertyInfo {
1828 schema_type: prop_type,
1829 nullable: prop_nullable,
1830 description: prop_description,
1831 default: prop_default,
1832 serde_attrs: Vec::new(),
1833 constraints: PropertyConstraints::from_schema_details(prop_details),
1834 },
1835 );
1836 }
1837 }
1838
1839 let typed_enabled = self
1847 .type_mapper
1848 .config()
1849 .shape
1850 .as_ref()
1851 .and_then(|s| s.additional_properties_typed)
1852 .unwrap_or(true);
1853
1854 let additional_properties = match &details.additional_properties {
1855 Some(crate::openapi::AdditionalProperties::Boolean(true)) => {
1856 ObjectAdditionalProperties::Untyped
1857 }
1858 Some(crate::openapi::AdditionalProperties::Boolean(false)) => {
1859 ObjectAdditionalProperties::Forbidden
1860 }
1861 Some(crate::openapi::AdditionalProperties::Schema(value_schema)) if typed_enabled => {
1862 let analyzed =
1863 self.analyze_property_schema_with_context(value_schema, None, dependencies)?;
1864 ObjectAdditionalProperties::Typed {
1865 value_type: Box::new(analyzed),
1866 }
1867 }
1868 Some(crate::openapi::AdditionalProperties::Schema(_)) => {
1869 ObjectAdditionalProperties::Untyped
1871 }
1872 None => ObjectAdditionalProperties::Forbidden,
1873 };
1874
1875 Ok(SchemaType::Object {
1876 properties: property_info,
1877 required,
1878 additional_properties,
1879 })
1880 }
1881
1882 fn analyze_property_schema_with_context(
1883 &mut self,
1884 schema: &Schema,
1885 property_name: Option<&str>,
1886 dependencies: &mut HashSet<String>,
1887 ) -> Result<SchemaType> {
1888 if let Some(ref_str) = self.get_any_reference(schema) {
1889 let target_opt = if ref_str == "#" {
1890 Some(
1891 self.find_recursive_anchor_schema()
1892 .unwrap_or_else(|| "UnknownRecursive".to_string()),
1893 )
1894 } else {
1895 self.extract_schema_name(ref_str).map(|s| s.to_string())
1896 };
1897 match target_opt {
1898 Some(target) => {
1899 dependencies.insert(target.clone());
1900 return Ok(SchemaType::Reference { target });
1901 }
1902 None => {
1903 eprintln!(
1904 "⚠️ unresolvable $ref `{}` — typing as serde_json::Value",
1905 ref_str
1906 );
1907 return Ok(SchemaType::Primitive {
1908 rust_type: "serde_json::Value".to_string(),
1909 serde_with: None,
1910 });
1911 }
1912 }
1913 }
1914
1915 if let Some(schema_type) = schema.schema_type() {
1916 match schema_type {
1917 OpenApiSchemaType::String => {
1918 if let Some(enum_values) = schema.details().string_enum_values() {
1920 let context_name = self
1923 .current_schema_name
1924 .clone()
1925 .unwrap_or_else(|| "Unknown".to_string());
1926
1927 let primary_name = if let Some(prop_name) = property_name {
1929 let prop_pascal = self.to_pascal_case(prop_name);
1931 format!("{context_name}{prop_pascal}")
1932 } else {
1933 let suffix = if !enum_values.is_empty() {
1936 let first_value = self.to_pascal_case(&enum_values[0]);
1937 format!("{first_value}Enum")
1938 } else {
1939 "StringEnum".to_string()
1940 };
1941 format!("{context_name}{suffix}")
1942 };
1943
1944 return Ok(self.hoist_inline_string_enum(
1945 schema,
1946 enum_values,
1947 primary_name,
1948 dependencies,
1949 ));
1950 } else {
1951 let mapped = self
1957 .type_mapper
1958 .string_format(schema.details().format.as_deref());
1959 return Ok(SchemaType::Primitive {
1960 rust_type: mapped.rust_type,
1961 serde_with: mapped.serde_with,
1962 });
1963 }
1964 }
1965 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
1966 let details = schema.details();
1967 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
1968 return Ok(SchemaType::Primitive {
1969 rust_type,
1970 serde_with: None,
1971 });
1972 }
1973 OpenApiSchemaType::Boolean => {
1974 return Ok(SchemaType::Primitive {
1975 rust_type: "bool".to_string(),
1976 serde_with: None,
1977 });
1978 }
1979 OpenApiSchemaType::Array => {
1980 let context_name = if let Some(prop_name) = property_name {
1982 let prop_pascal = self.to_pascal_case(prop_name);
1984 format!(
1985 "{}{}",
1986 self.current_schema_name.as_deref().unwrap_or("Unknown"),
1987 prop_pascal
1988 )
1989 } else {
1990 "ArrayItem".to_string()
1992 };
1993 return self.analyze_array_schema(schema, &context_name, dependencies);
1994 }
1995 OpenApiSchemaType::Object => {
1996 if self.should_use_dynamic_json(schema) {
1998 return Ok(SchemaType::Primitive {
1999 rust_type: "serde_json::Value".to_string(),
2000 serde_with: None,
2001 });
2002 }
2003 let object_type_name = if let Some(prop_name) = property_name {
2005 let prop_pascal = self.to_pascal_case(prop_name);
2007 format!(
2008 "{}{}",
2009 self.current_schema_name.as_deref().unwrap_or("Unknown"),
2010 prop_pascal
2011 )
2012 } else {
2013 format!(
2015 "{}Object",
2016 self.current_schema_name.as_deref().unwrap_or("Unknown")
2017 )
2018 };
2019
2020 let object_type = self.analyze_object_schema(schema, dependencies)?;
2022
2023 let inline_schema = AnalyzedSchema {
2025 name: object_type_name.clone(),
2026 original: serde_json::to_value(schema).unwrap_or(Value::Null),
2027 schema_type: object_type,
2028 dependencies: dependencies.clone(),
2029 nullable: false,
2030 description: schema.details().description.clone(),
2031 default: None,
2032 };
2033
2034 self.resolved_cache
2036 .insert(object_type_name.clone(), inline_schema);
2037 dependencies.insert(object_type_name.clone());
2038
2039 return Ok(SchemaType::Reference {
2041 target: object_type_name,
2042 });
2043 }
2044 _ => {
2045 return Ok(SchemaType::Primitive {
2046 rust_type: "serde_json::Value".to_string(),
2047 serde_with: None,
2048 });
2049 }
2050 }
2051 }
2052
2053 if schema.is_nullable_pattern() {
2055 if let Some(non_null) = schema.non_null_variant() {
2056 return self.analyze_property_schema_with_context(
2057 non_null,
2058 property_name,
2059 dependencies,
2060 );
2061 }
2062 }
2063
2064 if self.should_use_dynamic_json(schema) {
2066 return Ok(SchemaType::Primitive {
2067 rust_type: "serde_json::Value".to_string(),
2068 serde_with: None,
2069 });
2070 }
2071
2072 if let Schema::AllOf { all_of, .. } = schema {
2074 return self.analyze_allof_composition(all_of, dependencies);
2075 }
2076
2077 if let Some(variants) = schema.union_variants() {
2079 match variants.len().cmp(&1) {
2080 std::cmp::Ordering::Equal => {
2081 return self.analyze_property_schema_with_context(
2083 &variants[0],
2084 property_name,
2085 dependencies,
2086 );
2087 }
2088 std::cmp::Ordering::Greater => {
2089 let union_name = if let Some(prop_name) = property_name {
2092 let prop_pascal = self.to_pascal_case(prop_name);
2094 format!(
2095 "{}{}",
2096 self.current_schema_name.as_deref().unwrap_or(""),
2097 prop_pascal
2098 )
2099 } else {
2100 "UnionType".to_string()
2101 };
2102
2103 if let Schema::OneOf {
2105 one_of,
2106 discriminator,
2107 ..
2108 } = schema
2109 {
2110 let oneof_result = self.analyze_oneof_union(
2112 one_of,
2113 discriminator.as_ref(),
2114 &union_name,
2115 dependencies,
2116 )?;
2117
2118 if let SchemaType::Union {
2120 variants: _union_variants,
2121 } = &oneof_result
2122 {
2123 self.resolved_cache.insert(
2125 union_name.clone(),
2126 AnalyzedSchema {
2127 name: union_name.clone(),
2128 original: serde_json::to_value(schema).unwrap_or(Value::Null),
2129 schema_type: oneof_result.clone(),
2130 dependencies: dependencies.clone(),
2131 nullable: false,
2132 description: schema.details().description.clone(),
2133 default: None,
2134 },
2135 );
2136
2137 dependencies.insert(union_name.clone());
2139 return Ok(SchemaType::Reference { target: union_name });
2140 }
2141
2142 return Ok(oneof_result);
2143 } else if let Schema::AnyOf {
2144 any_of,
2145 discriminator,
2146 ..
2147 } = schema
2148 {
2149 let union_analysis = self.analyze_anyof_union(
2151 any_of,
2152 discriminator.as_ref(),
2153 dependencies,
2154 &union_name,
2155 )?;
2156 return Ok(union_analysis);
2157 } else {
2158 let mut union_variants = Vec::new();
2161 for variant in variants {
2162 if let Some(ref_str) = variant.reference() {
2163 if let Some(target) = self.extract_schema_name(ref_str) {
2164 dependencies.insert(target.to_string());
2165 union_variants.push(SchemaRef {
2166 target: target.to_string(),
2167 nullable: false,
2168 });
2169 }
2170 }
2171 }
2172 return Ok(SchemaType::Union {
2173 variants: union_variants,
2174 });
2175 }
2176 }
2177 std::cmp::Ordering::Less => {}
2178 }
2179 }
2180
2181 if let Some(inferred_type) = schema.inferred_type() {
2183 match inferred_type {
2184 OpenApiSchemaType::Object => {
2185 if self.should_use_dynamic_json(schema) {
2187 return Ok(SchemaType::Primitive {
2188 rust_type: "serde_json::Value".to_string(),
2189 serde_with: None,
2190 });
2191 }
2192 return self.analyze_object_schema(schema, dependencies);
2193 }
2194 OpenApiSchemaType::Array => {
2195 let context_name = if let Some(prop_name) = property_name {
2196 let prop_pascal = self.to_pascal_case(prop_name);
2198 format!(
2199 "{}{}",
2200 self.current_schema_name.as_deref().unwrap_or("Unknown"),
2201 prop_pascal
2202 )
2203 } else {
2204 "ArrayItem".to_string()
2206 };
2207 return self.analyze_array_schema(schema, &context_name, dependencies);
2208 }
2209 OpenApiSchemaType::String => {
2210 if let Some(enum_values) = schema.details().string_enum_values() {
2211 return Ok(SchemaType::StringEnum {
2212 values: enum_values,
2213 });
2214 } else {
2215 return Ok(SchemaType::Primitive {
2216 rust_type: "String".to_string(),
2217 serde_with: None,
2218 });
2219 }
2220 }
2221 _ => {
2222 let rust_type = self.openapi_type_to_rust_type(inferred_type, schema.details());
2224 return Ok(SchemaType::Primitive {
2225 rust_type,
2226 serde_with: None,
2227 });
2228 }
2229 }
2230 }
2231
2232 Ok(SchemaType::Primitive {
2233 rust_type: "serde_json::Value".to_string(),
2234 serde_with: None,
2235 })
2236 }
2237
2238 fn analyze_allof_composition(
2239 &mut self,
2240 all_of_schemas: &[Schema],
2241 dependencies: &mut HashSet<String>,
2242 ) -> Result<SchemaType> {
2243 if all_of_schemas.len() == 1 {
2246 if let Schema::Reference { reference, .. } = &all_of_schemas[0] {
2247 if let Some(target) = self.extract_schema_name(reference) {
2248 dependencies.insert(target.to_string());
2249 return Ok(SchemaType::Reference {
2250 target: target.to_string(),
2251 });
2252 }
2253 }
2254 }
2255
2256 let mut merged_properties = BTreeMap::new();
2258 let mut merged_required = HashSet::new();
2259 let mut descriptions = Vec::new();
2260
2261 let current_context = self.current_schema_name.clone();
2263
2264 for schema in all_of_schemas {
2265 match schema {
2266 Schema::Reference { reference, .. } => {
2267 if let Some(target) = self.extract_schema_name(reference) {
2269 dependencies.insert(target.to_string());
2270
2271 let analyzed_ref = self.analyze_schema(target)?;
2273
2274 match &analyzed_ref.schema_type {
2276 SchemaType::Object {
2277 properties,
2278 required,
2279 ..
2280 } => {
2281 for (prop_name, prop_info) in properties {
2283 merged_properties.insert(prop_name.clone(), prop_info.clone());
2284 }
2285 for req in required {
2287 merged_required.insert(req.clone());
2288 }
2289 }
2290 _ => {
2291 if let Some(ref_schema) = self.schemas.get(target).cloned() {
2293 self.merge_schema_into_properties(
2294 &ref_schema,
2295 &mut merged_properties,
2296 &mut merged_required,
2297 dependencies,
2298 )?;
2299 }
2300 }
2301 }
2302 }
2303 }
2304 Schema::Typed {
2305 schema_type: OpenApiSchemaType::Object,
2306 ..
2307 }
2308 | Schema::Untyped { .. } => {
2309 let saved_context = self.current_schema_name.clone();
2311 self.current_schema_name = current_context.clone();
2312
2313 self.merge_schema_into_properties(
2315 schema,
2316 &mut merged_properties,
2317 &mut merged_required,
2318 dependencies,
2319 )?;
2320
2321 self.current_schema_name = saved_context;
2323 }
2324 _ => {
2325 self.merge_schema_into_properties(
2328 schema,
2329 &mut merged_properties,
2330 &mut merged_required,
2331 dependencies,
2332 )?;
2333 }
2334 }
2335
2336 if let Some(desc) = &schema.details().description {
2338 descriptions.push(desc.clone());
2339 }
2340 }
2341
2342 if !merged_properties.is_empty() {
2344 Ok(SchemaType::Object {
2345 properties: merged_properties,
2346 required: merged_required,
2347 additional_properties: ObjectAdditionalProperties::Forbidden,
2348 })
2349 } else {
2350 Ok(SchemaType::Composition {
2352 schemas: all_of_schemas
2353 .iter()
2354 .filter_map(|s| {
2355 if let Some(ref_str) = s.reference() {
2356 if let Some(target) = self.extract_schema_name(ref_str) {
2357 dependencies.insert(target.to_string());
2358 Some(SchemaRef {
2359 target: target.to_string(),
2360 nullable: false,
2361 })
2362 } else {
2363 None
2364 }
2365 } else {
2366 None
2367 }
2368 })
2369 .collect(),
2370 })
2371 }
2372 }
2373
2374 fn merge_schema_into_properties(
2375 &mut self,
2376 schema: &Schema,
2377 merged_properties: &mut BTreeMap<String, PropertyInfo>,
2378 merged_required: &mut HashSet<String>,
2379 dependencies: &mut HashSet<String>,
2380 ) -> Result<()> {
2381 let details = schema.details();
2382
2383 if let Some(properties) = &details.properties {
2385 for (prop_name, prop_schema) in properties {
2386 let prop_type = self.analyze_property_schema_with_context(
2387 prop_schema,
2388 Some(prop_name),
2389 dependencies,
2390 )?;
2391 let prop_details = prop_schema.details();
2392
2393 let nullable = prop_details.is_nullable() || prop_schema.is_nullable_pattern();
2399 merged_properties.insert(
2400 prop_name.clone(),
2401 PropertyInfo {
2402 schema_type: prop_type,
2403 nullable,
2404 description: prop_details.description.clone(),
2405 default: prop_details.default.clone(),
2406 serde_attrs: Vec::new(),
2407 constraints: PropertyConstraints::from_schema_details(prop_details),
2408 },
2409 );
2410 }
2411 }
2412
2413 if let Some(required) = &details.required {
2415 for field in required {
2416 merged_required.insert(field.clone());
2417 }
2418 }
2419
2420 Ok(())
2421 }
2422
2423 fn analyze_oneof_union(
2424 &mut self,
2425 one_of_schemas: &[Schema],
2426 discriminator: Option<&crate::openapi::Discriminator>,
2427 parent_name: &str,
2428 dependencies: &mut HashSet<String>,
2429 ) -> Result<SchemaType> {
2430 if one_of_schemas.len() == 2 {
2433 let null_count = one_of_schemas
2434 .iter()
2435 .filter(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
2436 .count();
2437 if null_count == 1 {
2438 if let Some(non_null) = one_of_schemas
2439 .iter()
2440 .find(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
2441 {
2442 return self
2443 .analyze_schema_value(non_null, parent_name)
2444 .map(|a| a.schema_type);
2445 }
2446 }
2447 }
2448
2449 if discriminator.is_none() {
2451 return self.analyze_untagged_oneof_union(one_of_schemas, parent_name, dependencies);
2453 }
2454
2455 if one_of_schemas
2461 .iter()
2462 .any(|s| !self.branch_resolves_to_object(s))
2463 {
2464 return self.analyze_untagged_oneof_union(one_of_schemas, parent_name, dependencies);
2465 }
2466
2467 let discriminator_field = discriminator
2469 .ok_or_else(|| {
2470 GeneratorError::InvalidDiscriminator(
2471 "expected discriminator after guard check".to_string(),
2472 )
2473 })?
2474 .property_name
2475 .clone();
2476
2477 let mut variants = Vec::new();
2478 let mut used_variant_names = std::collections::HashSet::new();
2479
2480 for variant_schema in one_of_schemas {
2481 let ref_info = if let Some(ref_str) = variant_schema.reference() {
2483 Some((ref_str, false))
2484 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2485 Some((recursive_ref, true))
2486 } else if let Schema::AllOf { all_of, .. } = variant_schema {
2487 if all_of.len() == 1 {
2489 if let Some(ref_str) = all_of[0].reference() {
2490 Some((ref_str, false))
2491 } else {
2492 all_of[0]
2493 .recursive_reference()
2494 .map(|recursive_ref| (recursive_ref, true))
2495 }
2496 } else {
2497 None
2498 }
2499 } else {
2500 None
2501 };
2502
2503 if let Some((ref_str, is_recursive)) = ref_info {
2504 let schema_name = if is_recursive && ref_str == "#" {
2505 self.find_recursive_anchor_schema()
2507 .or_else(|| self.current_schema_name.clone())
2508 .unwrap_or_else(|| "CompoundFilter".to_string())
2509 } else {
2510 self.extract_schema_name(ref_str)
2511 .map(|s| s.to_string())
2512 .unwrap_or_else(|| "UnknownRef".to_string())
2513 };
2514
2515 if !schema_name.is_empty() {
2516 dependencies.insert(schema_name.clone());
2517
2518 let discriminator_value = if let Some(disc) = discriminator {
2523 if let Some(mappings) = &disc.mapping {
2524 mappings
2527 .iter()
2528 .find(|(_, target_ref)| {
2529 target_ref.as_str() == ref_str
2531 || self
2532 .extract_schema_name(target_ref)
2533 .map(|s| s.to_string())
2534 == Some(schema_name.clone())
2535 })
2536 .map(|(key, _)| key.clone())
2537 .unwrap_or_else(|| {
2538 self.fallback_discriminator_value_for_field(
2539 &schema_name,
2540 &discriminator_field,
2541 )
2542 })
2543 } else {
2544 self.fallback_discriminator_value_for_field(
2545 &schema_name,
2546 &discriminator_field,
2547 )
2548 }
2549 } else {
2550 self.fallback_discriminator_value_for_field(
2551 &schema_name,
2552 &discriminator_field,
2553 )
2554 };
2555
2556 let base_name = self.to_rust_variant_name(&schema_name);
2558 let rust_name =
2559 self.ensure_unique_variant_name(base_name, &mut used_variant_names);
2560
2561 let final_discriminator_value = discriminator_value;
2563
2564 variants.push(UnionVariant {
2565 rust_name,
2566 type_name: schema_name,
2567 discriminator_value: final_discriminator_value,
2568 schema_ref: ref_str.to_string(),
2569 });
2570 }
2571 } else {
2572 let variant_index = variants.len();
2574 let inline_type_name =
2575 self.generate_inline_type_name(variant_schema, variant_index);
2576
2577 let discriminator_value = if let Some(disc) = discriminator {
2579 if let Some(mappings) = &disc.mapping {
2580 mappings
2582 .iter()
2583 .find(|(_, target_ref)| {
2584 target_ref.contains(&format!("variant_{variant_index}"))
2585 })
2586 .map(|(key, _)| key.clone())
2587 .unwrap_or_else(|| {
2588 self.extract_inline_discriminator_value(
2589 variant_schema,
2590 &discriminator_field,
2591 variant_index,
2592 )
2593 })
2594 } else {
2595 self.extract_inline_discriminator_value(
2596 variant_schema,
2597 &discriminator_field,
2598 variant_index,
2599 )
2600 }
2601 } else {
2602 self.extract_inline_discriminator_value(
2603 variant_schema,
2604 &discriminator_field,
2605 variant_index,
2606 )
2607 };
2608
2609 let base_name = if discriminator_value.starts_with("variant_") {
2611 format!("Variant{variant_index}")
2612 } else {
2613 let clean_name = self.discriminator_to_variant_name(&discriminator_value);
2615 self.to_rust_variant_name(&clean_name)
2616 };
2617 let rust_name = self.ensure_unique_variant_name(base_name, &mut used_variant_names);
2618
2619 let final_discriminator_value = discriminator_value;
2621
2622 variants.push(UnionVariant {
2623 rust_name,
2624 type_name: inline_type_name.clone(),
2625 discriminator_value: final_discriminator_value,
2626 schema_ref: format!("inline_{variant_index}"),
2627 });
2628
2629 self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
2631 }
2632 }
2633
2634 if variants.is_empty() {
2635 let mut union_variants = Vec::new();
2638
2639 for (variant_index, variant_schema) in one_of_schemas.iter().enumerate() {
2640 if let Some(ref_str) = variant_schema.reference() {
2642 if let Some(schema_name) = self.extract_schema_name(ref_str) {
2643 dependencies.insert(schema_name.to_string());
2644 union_variants.push(SchemaRef {
2645 target: schema_name.to_string(),
2646 nullable: false,
2647 });
2648 }
2649 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2650 let schema_name = if recursive_ref == "#" {
2651 self.find_recursive_anchor_schema()
2653 .or_else(|| self.current_schema_name.clone())
2654 .unwrap_or_else(|| "CompoundFilter".to_string())
2655 } else {
2656 self.extract_schema_name(recursive_ref)
2657 .map(|s| s.to_string())
2658 .unwrap_or_else(|| "RecursiveType".to_string())
2659 };
2660 dependencies.insert(schema_name.clone());
2661 union_variants.push(SchemaRef {
2662 target: schema_name,
2663 nullable: false,
2664 });
2665 } else {
2666 let inline_name = self.generate_context_aware_name(
2668 parent_name,
2669 "InlineVariant",
2670 variant_index,
2671 Some(variant_schema),
2672 );
2673 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
2674 let variant_type = analyzed.schema_type;
2675
2676 for dep in &analyzed.dependencies {
2678 dependencies.insert(dep.clone());
2679 }
2680
2681 match &variant_type {
2682 SchemaType::Primitive { rust_type, .. } => {
2684 union_variants.push(SchemaRef {
2685 target: rust_type.clone(),
2686 nullable: false,
2687 });
2688 }
2689 SchemaType::Array { item_type } => {
2691 match item_type.as_ref() {
2692 SchemaType::Primitive { rust_type, .. } => {
2693 let type_name = format!("Vec<{rust_type}>");
2694 union_variants.push(SchemaRef {
2695 target: type_name,
2696 nullable: false,
2697 });
2698 }
2699 SchemaType::Reference { target } => {
2700 let type_name = format!("Vec<{target}>");
2701 union_variants.push(SchemaRef {
2702 target: type_name,
2703 nullable: false,
2704 });
2705 }
2706 _ => {
2707 let inline_type_name = self.generate_context_aware_name(
2709 parent_name,
2710 "Variant",
2711 variant_index,
2712 None,
2713 );
2714 self.add_inline_schema(
2715 &inline_type_name,
2716 variant_schema,
2717 dependencies,
2718 )?;
2719 union_variants.push(SchemaRef {
2720 target: inline_type_name,
2721 nullable: false,
2722 });
2723 }
2724 }
2725 }
2726 SchemaType::Reference { target } => {
2728 union_variants.push(SchemaRef {
2729 target: target.clone(),
2730 nullable: false,
2731 });
2732 }
2733 _ => {
2735 let inline_type_name =
2736 format!("{}Variant{}", parent_name, variant_index + 1);
2737 self.add_inline_schema(
2738 &inline_type_name,
2739 variant_schema,
2740 dependencies,
2741 )?;
2742 union_variants.push(SchemaRef {
2743 target: inline_type_name,
2744 nullable: false,
2745 });
2746 }
2747 }
2748 }
2749 }
2750
2751 if !union_variants.is_empty() {
2752 return Ok(SchemaType::Union {
2753 variants: union_variants,
2754 });
2755 }
2756
2757 return Ok(SchemaType::Primitive {
2759 rust_type: "serde_json::Value".to_string(),
2760 serde_with: None,
2761 });
2762 }
2763
2764 Ok(SchemaType::DiscriminatedUnion {
2765 discriminator_field,
2766 variants,
2767 })
2768 }
2769
2770 fn analyze_untagged_oneof_union(
2771 &mut self,
2772 one_of_schemas: &[Schema],
2773 parent_name: &str,
2774 dependencies: &mut HashSet<String>,
2775 ) -> Result<SchemaType> {
2776 let filtered: Vec<&Schema> = one_of_schemas
2780 .iter()
2781 .filter(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
2782 .collect();
2783
2784 if filtered.len() == 1 {
2786 return self
2787 .analyze_schema_value(filtered[0], parent_name)
2788 .map(|a| a.schema_type);
2789 }
2790
2791 let mut union_variants = Vec::new();
2792
2793 for (variant_index, variant_schema) in filtered.iter().copied().enumerate() {
2794 if let Some(ref_str) = variant_schema.reference() {
2796 if let Some(schema_name) = self.extract_schema_name(ref_str) {
2797 dependencies.insert(schema_name.to_string());
2798 union_variants.push(SchemaRef {
2799 target: schema_name.to_string(),
2800 nullable: false,
2801 });
2802 }
2803 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2804 let schema_name = if recursive_ref == "#" {
2805 self.find_recursive_anchor_schema()
2807 .or_else(|| self.current_schema_name.clone())
2808 .unwrap_or_else(|| "CompoundFilter".to_string())
2809 } else {
2810 self.extract_schema_name(recursive_ref)
2811 .map(|s| s.to_string())
2812 .unwrap_or_else(|| "RecursiveType".to_string())
2813 };
2814 dependencies.insert(schema_name.clone());
2815 union_variants.push(SchemaRef {
2816 target: schema_name,
2817 nullable: false,
2818 });
2819 } else {
2820 let inline_name = self.generate_context_aware_name(
2822 parent_name,
2823 "InlineVariant",
2824 variant_index,
2825 Some(variant_schema),
2826 );
2827 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
2828 let variant_type = analyzed.schema_type;
2829
2830 for dep in &analyzed.dependencies {
2832 dependencies.insert(dep.clone());
2833 }
2834
2835 match &variant_type {
2836 SchemaType::Primitive { rust_type, .. } => {
2838 union_variants.push(SchemaRef {
2839 target: rust_type.clone(),
2840 nullable: false,
2841 });
2842 }
2843 SchemaType::Array { item_type } => {
2845 match item_type.as_ref() {
2846 SchemaType::Primitive { rust_type, .. } => {
2847 let type_name = format!("Vec<{rust_type}>");
2848 union_variants.push(SchemaRef {
2849 target: type_name,
2850 nullable: false,
2851 });
2852 }
2853 SchemaType::Reference { target } => {
2854 let type_name = format!("Vec<{target}>");
2855 union_variants.push(SchemaRef {
2856 target: type_name,
2857 nullable: false,
2858 });
2859 }
2860 SchemaType::Array {
2862 item_type: inner_item_type,
2863 } => {
2864 match inner_item_type.as_ref() {
2865 SchemaType::Primitive { rust_type, .. } => {
2866 let type_name = format!("Vec<Vec<{rust_type}>>");
2867 union_variants.push(SchemaRef {
2868 target: type_name,
2869 nullable: false,
2870 });
2871 }
2872 SchemaType::Reference { target } => {
2873 let type_name = format!("Vec<Vec<{target}>>");
2874 union_variants.push(SchemaRef {
2875 target: type_name,
2876 nullable: false,
2877 });
2878 }
2879 _ => {
2880 let inline_type_name = self.generate_context_aware_name(
2882 parent_name,
2883 "Variant",
2884 variant_index,
2885 None,
2886 );
2887 self.add_inline_schema(
2888 &inline_type_name,
2889 variant_schema,
2890 dependencies,
2891 )?;
2892 union_variants.push(SchemaRef {
2893 target: inline_type_name,
2894 nullable: false,
2895 });
2896 }
2897 }
2898 }
2899 _ => {
2900 let inline_type_name = self.generate_context_aware_name(
2902 parent_name,
2903 "Variant",
2904 variant_index,
2905 None,
2906 );
2907 self.add_inline_schema(
2908 &inline_type_name,
2909 variant_schema,
2910 dependencies,
2911 )?;
2912 union_variants.push(SchemaRef {
2913 target: inline_type_name,
2914 nullable: false,
2915 });
2916 }
2917 }
2918 }
2919 SchemaType::Reference { target } => {
2921 union_variants.push(SchemaRef {
2922 target: target.clone(),
2923 nullable: false,
2924 });
2925 }
2926 _ => {
2928 let inline_type_name = self.generate_context_aware_name(
2929 parent_name,
2930 "Variant",
2931 variant_index,
2932 None,
2933 );
2934 self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
2935 union_variants.push(SchemaRef {
2936 target: inline_type_name,
2937 nullable: false,
2938 });
2939 }
2940 }
2941 }
2942 }
2943
2944 if !union_variants.is_empty() {
2945 return Ok(SchemaType::Union {
2946 variants: union_variants,
2947 });
2948 }
2949
2950 Ok(SchemaType::Primitive {
2952 rust_type: "serde_json::Value".to_string(),
2953 serde_with: None,
2954 })
2955 }
2956
2957 fn add_inline_schema(
2958 &mut self,
2959 type_name: &str,
2960 schema: &Schema,
2961 dependencies: &mut HashSet<String>,
2962 ) -> Result<()> {
2963 if let Some(schema_type) = schema.schema_type() {
2965 match schema_type {
2966 OpenApiSchemaType::String
2967 | OpenApiSchemaType::Integer
2968 | OpenApiSchemaType::Number
2969 | OpenApiSchemaType::Boolean => {
2970 let rust_type =
2971 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
2972
2973 self.resolved_cache.insert(
2975 type_name.to_string(),
2976 AnalyzedSchema {
2977 name: type_name.to_string(),
2978 original: serde_json::to_value(schema).unwrap_or(Value::Null),
2979 schema_type: SchemaType::Primitive {
2980 rust_type,
2981 serde_with: None,
2982 },
2983 dependencies: HashSet::new(),
2984 nullable: false,
2985 description: schema.details().description.clone(),
2986 default: None,
2987 },
2988 );
2989 return Ok(());
2990 }
2991 _ => {}
2992 }
2993 }
2994
2995 let previous_schema_name = self.current_schema_name.take();
2999 self.current_schema_name = Some(type_name.to_string());
3000 let analyzed = self.analyze_schema_value(schema, type_name)?;
3001 self.current_schema_name = previous_schema_name;
3002
3003 self.resolved_cache.insert(type_name.to_string(), analyzed);
3005
3006 if let Some(cached) = self.resolved_cache.get(type_name) {
3008 for dep in &cached.dependencies {
3009 dependencies.insert(dep.clone());
3010 }
3011 }
3012
3013 Ok(())
3014 }
3015
3016 fn extract_inline_discriminator_value(
3017 &self,
3018 schema: &Schema,
3019 discriminator_field: &str,
3020 variant_index: usize,
3021 ) -> String {
3022 if let Some(properties) = &schema.details().properties {
3024 if let Some(discriminator_prop) = properties.get(discriminator_field) {
3025 if let Some(enum_values) = &discriminator_prop.details().enum_values {
3027 if enum_values.len() == 1 {
3028 if let Some(value) = enum_values[0].as_str() {
3029 return value.to_string();
3030 }
3031 }
3032 }
3033 if let Some(const_value) = discriminator_prop.details().extra.get("const") {
3035 if let Some(value) = const_value.as_str() {
3036 return value.to_string();
3037 }
3038 }
3039 if let Some(const_value) = &discriminator_prop.details().const_value {
3041 if let Some(value) = const_value.as_str() {
3042 return value.to_string();
3043 }
3044 }
3045 }
3046 }
3047
3048 if let Some(inferred_name) = self.infer_variant_name_from_structure(schema, variant_index) {
3050 return inferred_name;
3051 }
3052
3053 format!("variant_{variant_index}")
3055 }
3056
3057 fn infer_variant_name_from_structure(
3058 &self,
3059 schema: &Schema,
3060 _variant_index: usize,
3061 ) -> Option<String> {
3062 let details = schema.details();
3063
3064 if let Some(properties) = &details.properties {
3066 if properties.contains_key("text") && properties.len() <= 3 {
3068 return Some("text".to_string());
3069 }
3070 if properties.contains_key("image") || properties.contains_key("source") {
3071 return Some("image".to_string());
3072 }
3073 if properties.contains_key("document") {
3074 return Some("document".to_string());
3075 }
3076 if properties.contains_key("tool_use_id") || properties.contains_key("tool_result") {
3077 return Some("tool_result".to_string());
3078 }
3079 if properties.contains_key("content") && properties.contains_key("is_error") {
3080 return Some("tool_result".to_string());
3081 }
3082 if properties.contains_key("partial_json") {
3083 return Some("partial_json".to_string());
3084 }
3085
3086 let property_names: Vec<&String> = properties.keys().collect();
3088
3089 for prop_name in &property_names {
3091 if prop_name.contains("result") {
3092 return Some("result".to_string());
3093 }
3094 if prop_name.contains("error") {
3095 return Some("error".to_string());
3096 }
3097 if prop_name.contains("content") && property_names.len() <= 2 {
3098 return Some("content".to_string());
3099 }
3100 }
3101
3102 let significant_props = property_names
3104 .iter()
3105 .filter(|&name| !["type", "id", "cache_control"].contains(&name.as_str()))
3106 .collect::<Vec<_>>();
3107
3108 if significant_props.len() == 1 {
3109 return Some((*significant_props[0]).clone());
3110 }
3111 }
3112
3113 if let Some(description) = &details.description {
3115 let desc_lower = description.to_lowercase();
3116 if desc_lower.contains("text") && desc_lower.len() < 100 {
3117 return Some("text".to_string());
3118 }
3119 if desc_lower.contains("image") {
3120 return Some("image".to_string());
3121 }
3122 if desc_lower.contains("document") {
3123 return Some("document".to_string());
3124 }
3125 if desc_lower.contains("tool") && desc_lower.contains("result") {
3126 return Some("tool_result".to_string());
3127 }
3128 }
3129
3130 None
3131 }
3132
3133 fn discriminator_to_variant_name(&self, discriminator: &str) -> String {
3134 if discriminator.is_empty() {
3136 return "Variant".to_string();
3137 }
3138
3139 let mut result = String::new();
3140 let mut next_upper = true;
3141
3142 for c in discriminator.chars() {
3143 match c {
3144 'a'..='z' => {
3145 if next_upper {
3146 result.push(c.to_ascii_uppercase());
3147 next_upper = false;
3148 } else {
3149 result.push(c);
3150 }
3151 }
3152 'A'..='Z' => {
3153 result.push(c);
3154 next_upper = false;
3155 }
3156 '0'..='9' => {
3157 result.push(c);
3158 next_upper = false;
3159 }
3160 '_' | '-' | '.' | ' ' | '/' | '\\' => {
3161 next_upper = true;
3163 }
3164 _ => {
3165 next_upper = true;
3167 }
3168 }
3169 }
3170
3171 if result.is_empty() || result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
3173 result = format!("Variant{result}");
3174 }
3175
3176 result
3177 }
3178
3179 fn ensure_unique_variant_name(
3180 &self,
3181 base_name: String,
3182 used_names: &mut std::collections::HashSet<String>,
3183 ) -> String {
3184 let mut candidate = base_name.clone();
3185 let mut counter = 1;
3186
3187 while used_names.contains(&candidate) {
3188 counter += 1;
3189 candidate = format!("{base_name}{counter}");
3190 }
3191
3192 used_names.insert(candidate.clone());
3193 candidate
3194 }
3195
3196 fn generate_inline_type_name(&self, schema: &Schema, variant_index: usize) -> String {
3197 if let Some(meaningful_name) = self.infer_type_name_from_structure(schema) {
3199 return meaningful_name;
3200 }
3201
3202 let context = self.current_schema_name.as_deref().unwrap_or("Inline");
3204 self.generate_context_aware_name(context, "Variant", variant_index, Some(schema))
3205 }
3206
3207 fn infer_type_name_from_structure(&self, schema: &Schema) -> Option<String> {
3208 let details = schema.details();
3209
3210 if let Some(description) = &details.description {
3212 if let Some(name_from_desc) = self.extract_type_name_from_description(description) {
3213 return Some(name_from_desc);
3214 }
3215 }
3216
3217 if let Some(properties) = &details.properties {
3219 if let Some(name_from_props) = self.extract_type_name_from_properties(properties) {
3220 return Some(format!("{name_from_props}Block"));
3221 }
3222 }
3223
3224 None
3225 }
3226
3227 fn extract_type_name_from_description(&self, description: &str) -> Option<String> {
3228 if description.len() > 100 || description.contains('\n') {
3230 return None;
3231 }
3232
3233 let words: Vec<&str> = description
3235 .split_whitespace()
3236 .take(2) .filter(|word| {
3238 let w = word.to_lowercase();
3239 word.len() > 2
3240 && ![
3241 "the", "and", "for", "with", "that", "this", "are", "can", "will", "was",
3242 ]
3243 .contains(&w.as_str())
3244 })
3245 .collect();
3246
3247 if words.is_empty() {
3248 return None;
3249 }
3250
3251 let combined = words.join("_");
3253 let pascal_name = self.discriminator_to_variant_name(&combined);
3254
3255 if !pascal_name.ends_with("Content")
3257 && !pascal_name.ends_with("Block")
3258 && !pascal_name.ends_with("Type")
3259 {
3260 Some(format!("{pascal_name}Content"))
3261 } else {
3262 Some(pascal_name)
3263 }
3264 }
3265
3266 fn extract_type_name_from_properties(
3267 &self,
3268 properties: &std::collections::BTreeMap<String, crate::openapi::Schema>,
3269 ) -> Option<String> {
3270 let significant_props: Vec<&String> = properties
3272 .keys()
3273 .filter(|name| !["type", "id", "cache_control"].contains(&name.as_str()))
3274 .collect();
3275
3276 if significant_props.is_empty() {
3277 return None;
3278 }
3279
3280 if significant_props.len() == 1 {
3282 let prop_name = significant_props[0];
3283 return Some(self.discriminator_to_variant_name(prop_name));
3284 }
3285
3286 let mut sorted_props = significant_props.clone();
3289 sorted_props.sort();
3290 if let Some(first_prop) = sorted_props.first() {
3291 return Some(self.discriminator_to_variant_name(first_prop));
3292 }
3293
3294 None
3295 }
3296
3297 fn openapi_type_to_rust_type(
3298 &self,
3299 openapi_type: OpenApiSchemaType,
3300 details: &crate::openapi::SchemaDetails,
3301 ) -> String {
3302 self.type_mapper.map(openapi_type, details).rust_type
3307 }
3308
3309 #[allow(dead_code)]
3310 fn fallback_discriminator_value(&self, schema_name: &str) -> String {
3311 self.fallback_discriminator_value_for_field(schema_name, "type")
3312 }
3313
3314 fn fallback_discriminator_value_for_field(
3315 &self,
3316 schema_name: &str,
3317 field_name: &str,
3318 ) -> String {
3319 if let Some(ref_schema) = self.schemas.get(schema_name) {
3321 if let Some(extracted) =
3322 self.extract_discriminator_value_for_field(ref_schema, field_name)
3323 {
3324 return extracted;
3325 }
3326 }
3327
3328 self.generate_discriminator_value_from_name(schema_name)
3330 }
3331
3332 fn generate_discriminator_value_from_name(&self, schema_name: &str) -> String {
3333 let mut result = String::new();
3335 let mut chars = schema_name.chars().peekable();
3336 let mut first = true;
3337
3338 while let Some(c) = chars.next() {
3339 if c.is_uppercase()
3340 && !first
3341 && chars
3342 .peek()
3343 .map(|&next| next.is_lowercase())
3344 .unwrap_or(false)
3345 {
3346 result.push('.');
3347 }
3348 result.push(c.to_ascii_lowercase());
3349 first = false;
3350 }
3351
3352 if result.ends_with("event") {
3354 result = result[..result.len() - 5].to_string();
3355 }
3356
3357 if schema_name.starts_with("Response") && !result.starts_with("response.") {
3359 result = format!("response.{}", result.trim_start_matches("response"));
3360 }
3361
3362 result
3363 }
3364
3365 fn to_rust_variant_name(&self, schema_name: &str) -> String {
3366 let mut name = schema_name;
3368
3369 if name.starts_with("Response") && name.len() > 8 {
3371 name = &name[8..]; }
3373
3374 if name.ends_with("Event") && name.len() > 5 {
3376 name = &name[..name.len() - 5]; }
3378
3379 name = name.trim_matches('_');
3381
3382 if name.is_empty() {
3384 schema_name.to_string()
3385 } else {
3386 self.discriminator_to_variant_name(name)
3388 }
3389 }
3390
3391 fn hoist_inline_string_enum(
3415 &mut self,
3416 schema: &Schema,
3417 enum_values: Vec<String>,
3418 primary_name: String,
3419 dependencies: &mut HashSet<String>,
3420 ) -> SchemaType {
3421 fn matches_values(existing: &AnalyzedSchema, values: &[String]) -> bool {
3422 matches!(
3423 &existing.schema_type,
3424 SchemaType::StringEnum { values: existing_values }
3425 if existing_values == values
3426 )
3427 }
3428
3429 let mut enum_type_name = primary_name.clone();
3430 let should_insert = match self.resolved_cache.get(&enum_type_name) {
3431 None => true,
3432 Some(existing) if matches_values(existing, &enum_values) => false,
3433 Some(_) => {
3434 let suffix = enum_values
3437 .first()
3438 .map(|v| self.to_pascal_case(v))
3439 .unwrap_or_else(|| "Variant".to_string());
3440 let candidate = format!("{primary_name}{suffix}");
3441
3442 let resolved = match self.resolved_cache.get(&candidate) {
3443 None => Some((candidate.clone(), true)),
3444 Some(existing) if matches_values(existing, &enum_values) => {
3445 Some((candidate.clone(), false))
3446 }
3447 Some(_) => {
3448 let mut found = None;
3451 for n in 2..1000 {
3452 let numbered = format!("{candidate}_{n}");
3453 match self.resolved_cache.get(&numbered) {
3454 None => {
3455 found = Some((numbered, true));
3456 break;
3457 }
3458 Some(existing) if matches_values(existing, &enum_values) => {
3459 found = Some((numbered, false));
3460 break;
3461 }
3462 Some(_) => continue,
3463 }
3464 }
3465 found
3466 }
3467 };
3468
3469 let (resolved_name, insert) = resolved.unwrap_or((candidate, true));
3470 enum_type_name = resolved_name;
3471 insert
3472 }
3473 };
3474
3475 if should_insert {
3478 self.resolved_cache.insert(
3479 enum_type_name.clone(),
3480 AnalyzedSchema {
3481 name: enum_type_name.clone(),
3482 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3483 schema_type: SchemaType::StringEnum {
3484 values: enum_values,
3485 },
3486 dependencies: HashSet::new(),
3487 nullable: false,
3488 description: schema.details().description.clone(),
3489 default: schema.details().default.clone(),
3490 },
3491 );
3492 }
3493
3494 dependencies.insert(enum_type_name.clone());
3496 SchemaType::Reference {
3497 target: enum_type_name,
3498 }
3499 }
3500
3501 fn analyze_array_schema(
3502 &mut self,
3503 schema: &Schema,
3504 parent_schema_name: &str,
3505 dependencies: &mut HashSet<String>,
3506 ) -> Result<SchemaType> {
3507 let details = schema.details();
3508
3509 if let Some(items_schema) = &details.items {
3511 let item_type = match items_schema.as_ref() {
3513 Schema::Reference { reference, .. } => {
3514 let target = self
3516 .extract_schema_name(reference)
3517 .ok_or_else(|| GeneratorError::UnresolvedReference(reference.to_string()))?
3518 .to_string();
3519 dependencies.insert(target.clone());
3520 SchemaType::Reference { target }
3521 }
3522 Schema::RecursiveRef { recursive_ref, .. } => {
3523 if recursive_ref == "#" {
3525 let target = self
3527 .find_recursive_anchor_schema()
3528 .unwrap_or_else(|| parent_schema_name.to_string());
3529 dependencies.insert(target.clone());
3530 SchemaType::Reference { target }
3531 } else {
3532 let target = self
3533 .extract_schema_name(recursive_ref)
3534 .unwrap_or("RecursiveType")
3535 .to_string();
3536 dependencies.insert(target.clone());
3537 SchemaType::Reference { target }
3538 }
3539 }
3540 Schema::Typed { schema_type, .. } => {
3541 match schema_type {
3543 OpenApiSchemaType::String => {
3544 match items_schema
3548 .details()
3549 .string_enum_values()
3550 .filter(|values| !values.is_empty())
3551 {
3552 Some(values) => self.hoist_inline_string_enum(
3553 items_schema,
3554 values,
3555 format!("{parent_schema_name}Item"),
3556 dependencies,
3557 ),
3558 None => SchemaType::Primitive {
3559 rust_type: "String".to_string(),
3560 serde_with: None,
3561 },
3562 }
3563 }
3564 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
3565 let details = items_schema.details();
3566 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
3567 SchemaType::Primitive {
3568 rust_type,
3569 serde_with: None,
3570 }
3571 }
3572 OpenApiSchemaType::Boolean => SchemaType::Primitive {
3573 rust_type: "bool".to_string(),
3574 serde_with: None,
3575 },
3576 OpenApiSchemaType::Object => {
3577 let object_type_name = format!("{parent_schema_name}Item");
3579
3580 let object_type =
3582 self.analyze_object_schema(items_schema, dependencies)?;
3583
3584 let inline_schema = AnalyzedSchema {
3586 name: object_type_name.clone(),
3587 original: serde_json::to_value(items_schema).unwrap_or(Value::Null),
3588 schema_type: object_type,
3589 dependencies: dependencies.clone(),
3590 nullable: false,
3591 description: items_schema.details().description.clone(),
3592 default: None,
3593 };
3594
3595 self.resolved_cache
3597 .insert(object_type_name.clone(), inline_schema);
3598 dependencies.insert(object_type_name.clone());
3599
3600 SchemaType::Reference {
3602 target: object_type_name,
3603 }
3604 }
3605 OpenApiSchemaType::Array => {
3606 self.analyze_array_schema(
3608 items_schema,
3609 parent_schema_name,
3610 dependencies,
3611 )?
3612 }
3613 _ => SchemaType::Primitive {
3614 rust_type: "serde_json::Value".to_string(),
3615 serde_with: None,
3616 },
3617 }
3618 }
3619 Schema::OneOf { .. } | Schema::AnyOf { .. } => {
3620 let analyzed = self.analyze_schema_value(items_schema, "ArrayItem")?;
3622
3623 match &analyzed.schema_type {
3625 SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => {
3626 let union_name = format!("{parent_schema_name}ItemUnion");
3629
3630 let mut union_schema = analyzed;
3632 union_schema.name = union_name.clone();
3633
3634 self.resolved_cache.insert(union_name.clone(), union_schema);
3636
3637 dependencies.insert(union_name.clone());
3639
3640 SchemaType::Reference { target: union_name }
3642 }
3643 _ => analyzed.schema_type,
3644 }
3645 }
3646 Schema::Untyped { .. } => {
3647 if let Some(inferred) = items_schema.inferred_type() {
3649 match inferred {
3650 OpenApiSchemaType::Object => {
3651 let object_type_name = format!("{parent_schema_name}Item");
3653
3654 let object_type =
3656 self.analyze_object_schema(items_schema, dependencies)?;
3657
3658 let inline_schema = AnalyzedSchema {
3660 name: object_type_name.clone(),
3661 original: serde_json::to_value(items_schema)
3662 .unwrap_or(Value::Null),
3663 schema_type: object_type,
3664 dependencies: dependencies.clone(),
3665 nullable: false,
3666 description: items_schema.details().description.clone(),
3667 default: None,
3668 };
3669
3670 self.resolved_cache
3672 .insert(object_type_name.clone(), inline_schema);
3673 dependencies.insert(object_type_name.clone());
3674
3675 SchemaType::Reference {
3677 target: object_type_name,
3678 }
3679 }
3680 OpenApiSchemaType::String => {
3681 match items_schema
3684 .details()
3685 .string_enum_values()
3686 .filter(|values| !values.is_empty())
3687 {
3688 Some(values) => self.hoist_inline_string_enum(
3689 items_schema,
3690 values,
3691 format!("{parent_schema_name}Item"),
3692 dependencies,
3693 ),
3694 None => SchemaType::Primitive {
3695 rust_type: "String".to_string(),
3696 serde_with: None,
3697 },
3698 }
3699 }
3700 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
3701 let details = items_schema.details();
3702 let rust_type = self.get_number_rust_type(inferred, details);
3703 SchemaType::Primitive {
3704 rust_type,
3705 serde_with: None,
3706 }
3707 }
3708 OpenApiSchemaType::Boolean => SchemaType::Primitive {
3709 rust_type: "bool".to_string(),
3710 serde_with: None,
3711 },
3712 _ => SchemaType::Primitive {
3713 rust_type: "serde_json::Value".to_string(),
3714 serde_with: None,
3715 },
3716 }
3717 } else {
3718 SchemaType::Primitive {
3719 rust_type: "serde_json::Value".to_string(),
3720 serde_with: None,
3721 }
3722 }
3723 }
3724 _ => SchemaType::Primitive {
3725 rust_type: "serde_json::Value".to_string(),
3726 serde_with: None,
3727 },
3728 };
3729
3730 Ok(SchemaType::Array {
3731 item_type: Box::new(item_type),
3732 })
3733 } else {
3734 Ok(SchemaType::Primitive {
3736 rust_type: "Vec<serde_json::Value>".to_string(),
3737 serde_with: None,
3738 })
3739 }
3740 }
3741
3742 fn get_number_rust_type(
3743 &self,
3744 schema_type: OpenApiSchemaType,
3745 details: &crate::openapi::SchemaDetails,
3746 ) -> String {
3747 let format = details.format.as_deref();
3751 match schema_type {
3752 OpenApiSchemaType::Integer => self.type_mapper.integer_format(format).rust_type,
3753 OpenApiSchemaType::Number => self.type_mapper.number_format(format).rust_type,
3754 _ => self.type_mapper.dynamic_json().rust_type,
3755 }
3756 }
3757
3758 fn analyze_anyof_union(
3759 &mut self,
3760 any_of_schemas: &[Schema],
3761 discriminator: Option<&Discriminator>,
3762 dependencies: &mut HashSet<String>,
3763 context_name: &str,
3764 ) -> Result<SchemaType> {
3765 let filtered_owned: Vec<Schema>;
3770 let any_of_schemas: &[Schema] = if any_of_schemas
3771 .iter()
3772 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
3773 {
3774 filtered_owned = any_of_schemas
3775 .iter()
3776 .filter(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
3777 .cloned()
3778 .collect();
3779 if filtered_owned.is_empty() {
3780 return Ok(SchemaType::Primitive {
3781 rust_type: "serde_json::Value".to_string(),
3782 serde_with: None,
3783 });
3784 }
3785 if filtered_owned.len() == 1 {
3786 return self
3787 .analyze_schema_value(&filtered_owned[0], context_name)
3788 .map(|a| a.schema_type);
3789 }
3790 &filtered_owned
3791 } else {
3792 any_of_schemas
3793 };
3794
3795 let has_refs = any_of_schemas.iter().any(|s| s.is_reference());
3797 let has_objects = any_of_schemas.iter().any(|s| {
3798 matches!(s.schema_type(), Some(OpenApiSchemaType::Object))
3799 || s.inferred_type() == Some(OpenApiSchemaType::Object)
3800 });
3801 let has_arrays = any_of_schemas
3802 .iter()
3803 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Array)));
3804
3805 let all_string_like = any_of_schemas.iter().all(|s| {
3808 matches!(s.schema_type(), Some(OpenApiSchemaType::String))
3809 || s.details().const_value.is_some()
3810 });
3811
3812 if (has_refs || has_objects || has_arrays || any_of_schemas.len() > 1) && !all_string_like {
3813 if let Some(disc) = discriminator {
3815 return self.analyze_oneof_union(
3817 any_of_schemas,
3818 Some(disc),
3819 context_name,
3820 dependencies,
3821 );
3822 }
3823
3824 if let Some(disc_field) = self.detect_discriminator_field(any_of_schemas) {
3826 return self.analyze_oneof_union(
3827 any_of_schemas,
3828 Some(&Discriminator {
3829 property_name: disc_field,
3830 mapping: None,
3831 default_mapping: None,
3832 extensions: crate::extensions::Extensions::default(),
3833 }),
3834 context_name,
3835 dependencies,
3836 );
3837 }
3838
3839 let mut variants = Vec::new();
3841
3842 for schema in any_of_schemas {
3843 if let Some(ref_str) = schema.reference() {
3844 if let Some(target) = self.extract_schema_name(ref_str) {
3845 dependencies.insert(target.to_string());
3846 variants.push(SchemaRef {
3847 target: target.to_string(),
3848 nullable: false,
3849 });
3850 }
3851 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object))
3852 || schema.inferred_type() == Some(OpenApiSchemaType::Object)
3853 {
3854 let inline_index = variants.len();
3856 let inline_type_name = self.generate_inline_type_name(schema, inline_index);
3857
3858 self.add_inline_schema(&inline_type_name, schema, dependencies)?;
3860
3861 variants.push(SchemaRef {
3862 target: inline_type_name,
3863 nullable: false,
3864 });
3865 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Array)) {
3866 let array_type =
3868 self.analyze_array_schema(schema, context_name, dependencies)?;
3869
3870 let array_type_name = if let Some(items_schema) = &schema.details().items {
3872 if let Some(ref_str) = items_schema.reference() {
3873 if let Some(item_type_name) = self.extract_schema_name(ref_str) {
3874 dependencies.insert(item_type_name.to_string());
3875 format!("{item_type_name}Array")
3876 } else {
3877 self.generate_context_aware_name(
3878 context_name,
3879 "Array",
3880 variants.len(),
3881 Some(schema),
3882 )
3883 }
3884 } else {
3885 self.generate_context_aware_name(
3886 context_name,
3887 "Array",
3888 variants.len(),
3889 Some(schema),
3890 )
3891 }
3892 } else {
3893 self.generate_context_aware_name(
3894 context_name,
3895 "Array",
3896 variants.len(),
3897 Some(schema),
3898 )
3899 };
3900
3901 self.resolved_cache.insert(
3903 array_type_name.clone(),
3904 AnalyzedSchema {
3905 name: array_type_name.clone(),
3906 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3907 schema_type: array_type,
3908 dependencies: HashSet::new(),
3909 nullable: false,
3910 description: Some("Array variant in union".to_string()),
3911 default: None,
3912 },
3913 );
3914
3915 dependencies.insert(array_type_name.clone());
3917
3918 variants.push(SchemaRef {
3919 target: array_type_name,
3920 nullable: false,
3921 });
3922 } else if let Some(schema_type) = schema.schema_type() {
3923 let primitive_unions = self
3933 .type_mapper
3934 .config_shape_primitive_unions()
3935 .unwrap_or(true);
3936
3937 if primitive_unions {
3938 let mapped = self.type_mapper.map(schema_type.clone(), schema.details());
3939 variants.push(SchemaRef {
3940 target: mapped.rust_type,
3941 nullable: false,
3942 });
3943 } else {
3944 let inline_index = variants.len();
3945 let inline_type_name = match schema_type {
3946 OpenApiSchemaType::String => {
3947 if inline_index == 0 {
3948 format!("{context_name}String")
3949 } else {
3950 format!("{context_name}StringVariant{inline_index}")
3951 }
3952 }
3953 OpenApiSchemaType::Number => {
3954 if inline_index == 0 {
3955 format!("{context_name}Number")
3956 } else {
3957 format!("{context_name}NumberVariant{inline_index}")
3958 }
3959 }
3960 OpenApiSchemaType::Integer => {
3961 if inline_index == 0 {
3962 format!("{context_name}Integer")
3963 } else {
3964 format!("{context_name}IntegerVariant{inline_index}")
3965 }
3966 }
3967 OpenApiSchemaType::Boolean => {
3968 if inline_index == 0 {
3969 format!("{context_name}Boolean")
3970 } else {
3971 format!("{context_name}BooleanVariant{inline_index}")
3972 }
3973 }
3974 _ => format!("{context_name}Variant{inline_index}"),
3975 };
3976
3977 let rust_type =
3978 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
3979
3980 self.resolved_cache.insert(
3981 inline_type_name.clone(),
3982 AnalyzedSchema {
3983 name: inline_type_name.clone(),
3984 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3985 schema_type: SchemaType::Primitive {
3986 rust_type,
3987 serde_with: None,
3988 },
3989 dependencies: HashSet::new(),
3990 nullable: false,
3991 description: schema.details().description.clone(),
3992 default: None,
3993 },
3994 );
3995
3996 dependencies.insert(inline_type_name.clone());
3997
3998 variants.push(SchemaRef {
3999 target: inline_type_name,
4000 nullable: false,
4001 });
4002 }
4003 }
4004 }
4005
4006 if !variants.is_empty() {
4007 return Ok(SchemaType::Union { variants });
4008 }
4009 }
4010
4011 let all_strings = any_of_schemas.iter().all(|schema| {
4013 matches!(schema.schema_type(), Some(OpenApiSchemaType::String))
4014 || schema.details().const_value.is_some()
4015 });
4016
4017 if all_strings {
4018 let mut enum_values = Vec::new();
4020 let mut has_open_string = false;
4021
4022 for schema in any_of_schemas {
4023 if let Some(const_val) = &schema.details().const_value {
4024 if let Some(const_str) = const_val.as_str() {
4025 enum_values.push(const_str.to_string());
4026 }
4027 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::String)) {
4028 has_open_string = true;
4029 }
4030 }
4031
4032 if !enum_values.is_empty() {
4033 if has_open_string {
4034 return Ok(SchemaType::ExtensibleEnum {
4037 known_values: enum_values,
4038 });
4039 } else {
4040 return Ok(SchemaType::StringEnum {
4042 values: enum_values,
4043 });
4044 }
4045 }
4046 }
4047
4048 Ok(SchemaType::Primitive {
4050 rust_type: "serde_json::Value".to_string(),
4051 serde_with: None,
4052 })
4053 }
4054
4055 fn find_recursive_anchor_schema(&self) -> Option<String> {
4057 for (schema_name, schema) in &self.schemas {
4059 let details = schema.details();
4060 if details.recursive_anchor == Some(true) {
4061 return Some(schema_name.clone());
4062 }
4063 }
4064
4065 None
4069 }
4070
4071 fn should_use_dynamic_json(&self, schema: &Schema) -> bool {
4074 if let Schema::AnyOf { any_of, .. } = schema {
4076 if any_of.len() == 2 {
4077 let has_null = any_of
4078 .iter()
4079 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)));
4080 let has_empty_object = any_of.iter().any(|s| self.is_dynamic_object_pattern(s));
4081
4082 if has_null && has_empty_object {
4083 return true;
4084 }
4085 }
4086 }
4087
4088 self.is_dynamic_object_pattern(schema)
4090 }
4091
4092 fn is_dynamic_object_pattern(&self, schema: &Schema) -> bool {
4094 let is_object = match schema.schema_type() {
4096 Some(OpenApiSchemaType::Object) => true,
4097 None => schema.inferred_type() == Some(OpenApiSchemaType::Object),
4098 _ => false,
4099 };
4100
4101 if !is_object {
4102 return false;
4103 }
4104
4105 let details = schema.details();
4106
4107 if self.has_explicit_additional_properties(schema) {
4110 return false;
4111 }
4112
4113 let no_properties = details
4115 .properties
4116 .as_ref()
4117 .map(|props| props.is_empty())
4118 .unwrap_or(true);
4119
4120 if no_properties {
4121 let has_structural_constraints = details
4124 .required
4125 .as_ref()
4126 .map(|req| req.iter().any(|r| r != "type"))
4127 .unwrap_or(false)
4128 || details.pattern_properties.is_some()
4129 || details.property_names.is_some()
4130 || details.min_properties.is_some()
4131 || details.max_properties.is_some()
4132 || details.dependent_required.is_some()
4133 || details.dependent_schemas.is_some()
4134 || details.if_schema.is_some()
4135 || details.then_schema.is_some()
4136 || details.else_schema.is_some();
4137
4138 return !has_structural_constraints;
4139 }
4140
4141 false
4142 }
4143
4144 fn has_explicit_additional_properties(&self, schema: &Schema) -> bool {
4146 let details = schema.details();
4147
4148 matches!(
4150 &details.additional_properties,
4151 Some(crate::openapi::AdditionalProperties::Boolean(true))
4152 | Some(crate::openapi::AdditionalProperties::Schema(_))
4153 )
4154 }
4155
4156 fn analyze_operations(&mut self, analysis: &mut SchemaAnalysis) -> Result<()> {
4158 let spec: crate::openapi::OpenApiSpec = serde_json::from_value(self.openapi_spec.clone())
4159 .map_err(GeneratorError::ParseError)?;
4160 let mut canonical_operation_ids = HashSet::new();
4165
4166 if let Some(paths) = &spec.paths {
4167 for (path, path_item) in paths {
4168 let resolved = self.resolve_path_item(path_item, &spec)?;
4170 let pi: &crate::openapi::PathItem = resolved.as_ref().unwrap_or(path_item);
4171 self.ingest_path_item_operations(path, pi, analysis, &mut canonical_operation_ids)?;
4172 }
4173 }
4174 if let Some(webhooks) = &spec.webhooks {
4181 for (name, path_item) in webhooks {
4182 let synthetic_path = format!("__webhook__/{name}");
4183 self.ingest_path_item_operations(
4184 &synthetic_path,
4185 path_item,
4186 analysis,
4187 &mut canonical_operation_ids,
4188 )?;
4189 }
4190 }
4191 Ok(())
4192 }
4193
4194 fn resolve_path_item(
4198 &self,
4199 path_item: &crate::openapi::PathItem,
4200 spec: &crate::openapi::OpenApiSpec,
4201 ) -> Result<Option<crate::openapi::PathItem>> {
4202 let Some(reference) = &path_item.reference else {
4203 return Ok(None);
4204 };
4205 let target_name = reference
4206 .strip_prefix("#/components/pathItems/")
4207 .ok_or_else(|| {
4208 GeneratorError::UnresolvedReference(format!(
4209 "Path Item $ref must point at #/components/pathItems/{{name}}, got {reference}"
4210 ))
4211 })?;
4212 let pi = spec
4213 .components
4214 .as_ref()
4215 .and_then(|c| c.path_items.as_ref())
4216 .and_then(|map| map.get(target_name))
4217 .ok_or_else(|| {
4218 GeneratorError::UnresolvedReference(format!(
4219 "Path Item ref {reference} not found in components/pathItems"
4220 ))
4221 })?;
4222 Ok(Some(pi.clone()))
4223 }
4224
4225 fn ingest_path_item_operations(
4226 &mut self,
4227 path: &str,
4228 path_item: &crate::openapi::PathItem,
4229 analysis: &mut SchemaAnalysis,
4230 canonical_operation_ids: &mut HashSet<String>,
4231 ) -> Result<()> {
4232 for (method, operation) in path_item.operations() {
4233 let raw_operation_id = operation
4235 .operation_id
4236 .clone()
4237 .unwrap_or_else(|| Self::generate_operation_id(method, path));
4238
4239 let operation_id = if canonical_operation_ids
4250 .contains(&Self::canonical_operation_id(&raw_operation_id))
4251 {
4252 let method_lower = method.to_lowercase();
4253 let mut candidate = format!("{}_{}", raw_operation_id, method_lower);
4254 let mut suffix = 2;
4255 while canonical_operation_ids.contains(&Self::canonical_operation_id(&candidate)) {
4256 candidate = format!("{}_{}_{}", raw_operation_id, method_lower, suffix);
4257 suffix += 1;
4258 }
4259 eprintln!(
4260 "⚠️ duplicate operationId `{}` at `{} {}` — disambiguated to `{}`",
4261 raw_operation_id, method, path, candidate
4262 );
4263 candidate
4264 } else {
4265 raw_operation_id.clone()
4266 };
4267
4268 let op_info = self.analyze_single_operation(
4269 &operation_id,
4270 method,
4271 path,
4272 operation,
4273 path_item.parameters.as_ref(),
4274 analysis,
4275 )?;
4276 analysis
4277 .operation_id_aliases
4278 .entry(raw_operation_id)
4279 .or_default()
4280 .push(operation_id.clone());
4281 canonical_operation_ids.insert(Self::canonical_operation_id(&operation_id));
4282 analysis.operations.insert(operation_id, op_info);
4283 }
4284 Ok(())
4285 }
4286
4287 fn canonical_operation_id(operation_id: &str) -> String {
4288 use heck::ToPascalCase;
4289 operation_id.replace('.', "_").to_pascal_case()
4290 }
4291
4292 fn generate_operation_id(method: &str, path: &str) -> String {
4295 let mut operation_id = method.to_lowercase();
4297
4298 let path_parts: Vec<&str> = path.trim_start_matches('/').split('/').collect();
4300
4301 for part in path_parts {
4302 if part.is_empty() {
4303 continue;
4304 }
4305
4306 let cleaned_part = if part.starts_with('{') && part.ends_with('}') {
4308 &part[1..part.len() - 1]
4309 } else {
4310 part
4311 };
4312
4313 let pascal_case_part = cleaned_part
4315 .split(&['-', '_'][..])
4316 .map(|s| {
4317 let mut chars = s.chars();
4318 match chars.next() {
4319 None => String::new(),
4320 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
4321 }
4322 })
4323 .collect::<String>();
4324
4325 operation_id.push_str(&pascal_case_part);
4326 }
4327
4328 operation_id
4329 }
4330
4331 fn analyze_single_operation(
4333 &mut self,
4334 operation_id: &str,
4335 method: &str,
4336 path: &str,
4337 operation: &crate::openapi::Operation,
4338 path_item_parameters: Option<&Vec<crate::openapi::Parameter>>,
4339 _analysis: &mut SchemaAnalysis,
4340 ) -> Result<OperationInfo> {
4341 let mut op_info = OperationInfo {
4342 operation_id: operation_id.to_string(),
4343 method: method.to_uppercase(),
4344 path: path.to_string(),
4345 summary: operation.summary.clone(),
4346 description: operation.description.clone(),
4347 request_body: None,
4348 request_body_required: operation
4350 .request_body
4351 .as_ref()
4352 .and_then(|rb| rb.required)
4353 .unwrap_or(false),
4354 response_schemas: BTreeMap::new(),
4355 parameters: Vec::new(),
4356 supports_streaming: false, stream_parameter: None, tags: operation.tags.clone().unwrap_or_default(),
4359 };
4360
4361 if let Some(request_body) = &operation.request_body
4363 && let Some((content_type, maybe_schema)) = request_body.best_content()
4364 {
4365 use crate::openapi::{is_form_urlencoded_media_type, is_json_media_type};
4366 op_info.request_body = if is_json_media_type(content_type) {
4367 maybe_schema
4368 .map(|s| {
4369 self.resolve_or_inline_schema(s, operation_id, "Request")
4370 .map(|name| RequestBodyContent::Json { schema_name: name })
4371 })
4372 .transpose()?
4373 } else if is_form_urlencoded_media_type(content_type) {
4374 maybe_schema
4375 .map(|s| {
4376 self.resolve_or_inline_schema(s, operation_id, "Request")
4377 .map(|name| RequestBodyContent::FormUrlEncoded { schema_name: name })
4378 })
4379 .transpose()?
4380 } else {
4381 match content_type {
4382 "multipart/form-data" => Some(RequestBodyContent::Multipart),
4383 "application/octet-stream" => Some(RequestBodyContent::OctetStream),
4384 "text/plain" => Some(RequestBodyContent::TextPlain),
4385 _ => None,
4386 }
4387 };
4388 }
4389
4390 if let Some(responses) = &operation.responses {
4392 for (status_code, response) in responses {
4393 if let Some(content) = response.content.as_ref() {
4399 if content.keys().any(|ct| ct.starts_with("text/event-stream")) {
4400 op_info.supports_streaming = true;
4401 }
4402 }
4403
4404 if let Some(schema) = response.json_schema() {
4405 if let Some(schema_ref) = schema.reference() {
4406 if let Some(schema_name) = self.extract_schema_name(schema_ref) {
4408 op_info
4409 .response_schemas
4410 .insert(status_code.clone(), schema_name.to_string());
4411 }
4412 } else {
4413 let synthetic_name =
4415 self.generate_inline_response_type_name(operation_id, status_code);
4416
4417 let mut deps = HashSet::new();
4419 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
4420
4421 op_info
4422 .response_schemas
4423 .insert(status_code.clone(), synthetic_name);
4424 }
4425 }
4426 }
4427 }
4428
4429 if op_info.supports_streaming
4432 && let Some(parameters) = &operation.parameters
4433 {
4434 for param in parameters {
4435 if let Some(name) = param.name.as_deref() {
4436 if name.eq_ignore_ascii_case("stream") {
4437 op_info.stream_parameter = Some(name.to_string());
4438 break;
4439 }
4440 }
4441 }
4442 }
4443
4444 if let Some(parameters) = &operation.parameters {
4446 for param in parameters {
4447 let resolved = self.resolve_parameter(param).into_owned();
4451 if let Some(param_info) = self.analyze_parameter(&resolved, operation_id)? {
4452 op_info.parameters.push(param_info);
4453 }
4454 }
4455 }
4456
4457 if let Some(path_params) = path_item_parameters {
4459 let existing_keys: std::collections::HashSet<(String, String)> = op_info
4460 .parameters
4461 .iter()
4462 .map(|p| (p.name.clone(), p.location.clone()))
4463 .collect();
4464 for param in path_params {
4465 let resolved = self.resolve_parameter(param).into_owned();
4466 if let Some(param_info) = self.analyze_parameter(&resolved, operation_id)? {
4467 if !existing_keys
4468 .contains(&(param_info.name.clone(), param_info.location.clone()))
4469 {
4470 op_info.parameters.push(param_info);
4471 }
4472 }
4473 }
4474 }
4475
4476 let mut declared_path_names: std::collections::HashSet<String> = op_info
4484 .parameters
4485 .iter()
4486 .filter(|p| p.location == "path")
4487 .map(|p| p.name.clone())
4488 .collect();
4489 let bytes = path.as_bytes().iter();
4490 let mut current = String::new();
4491 let mut in_brace = false;
4492 let mut synthesized: Vec<String> = Vec::new();
4493 for b in bytes {
4494 match *b {
4495 b'{' => {
4496 in_brace = true;
4497 current.clear();
4498 }
4499 b'}' if in_brace => {
4500 in_brace = false;
4501 if !current.is_empty() && !declared_path_names.contains(¤t) {
4502 synthesized.push(current.clone());
4503 declared_path_names.insert(current.clone());
4504 }
4505 }
4506 _ if in_brace => current.push(*b as char),
4507 _ => {}
4508 }
4509 }
4510 for name in synthesized {
4511 eprintln!(
4512 "⚠️ path `{}` references `{{{}}}` but the spec doesn't declare it as a parameter — synthesizing as required String",
4513 path, name
4514 );
4515 op_info.parameters.push(ParameterInfo {
4516 name,
4517 location: "path".to_string(),
4518 required: true,
4519 schema_ref: None,
4520 rust_type: "String".to_string(),
4521 description: None,
4522 enum_values: None,
4523 rust_ident: None,
4524 query_serialization: None,
4525 });
4526 }
4527
4528 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
4536 for p in op_info.parameters.iter_mut() {
4537 let raw = base_param_ident(&p.name);
4538 let mut chosen = raw.clone();
4539 let mut suffix = 2;
4540 while !used.insert(chosen.clone()) {
4541 chosen = format!("{raw}_{suffix}");
4542 suffix += 1;
4543 }
4544 p.rust_ident = Some(chosen);
4545 }
4546
4547 Ok(op_info)
4548 }
4549
4550 fn generate_inline_response_type_name(&self, operation_id: &str, status_code: &str) -> String {
4557 use heck::ToPascalCase;
4558 let base_name = operation_id.replace('.', "_").to_pascal_case();
4559 let suffix = Self::status_code_suffix(status_code);
4560 format!("{}Response{}", base_name, suffix)
4561 }
4562
4563 fn status_code_suffix(status_code: &str) -> String {
4570 match status_code {
4571 "" | "200" => String::new(),
4572 "default" | "Default" => "Default".to_string(),
4573 other if other.chars().all(|c| c.is_ascii_digit()) => other.to_string(),
4574 other => other.to_ascii_lowercase(),
4575 }
4576 }
4577
4578 fn generate_inline_request_type_name(&self, operation_id: &str) -> String {
4580 use heck::ToPascalCase;
4581 let base_name = operation_id.replace('.', "_").to_pascal_case();
4585 format!("{}Request", base_name)
4586 }
4587
4588 fn resolve_or_inline_schema(
4591 &mut self,
4592 schema: &crate::openapi::Schema,
4593 operation_id: &str,
4594 suffix: &str,
4595 ) -> Result<String> {
4596 if let Some(schema_ref) = schema.reference()
4597 && let Some(schema_name) = self.extract_schema_name(schema_ref)
4598 {
4599 return Ok(schema_name.to_string());
4600 }
4601 let synthetic_name = if suffix == "Request" {
4603 self.generate_inline_request_type_name(operation_id)
4604 } else {
4605 self.generate_inline_response_type_name(operation_id, "")
4606 };
4607 let mut deps = HashSet::new();
4608 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
4609 Ok(synthetic_name)
4610 }
4611
4612 fn resolve_parameter<'a>(
4615 &'a self,
4616 param: &'a crate::openapi::Parameter,
4617 ) -> std::borrow::Cow<'a, crate::openapi::Parameter> {
4618 if let Some(ref_str) = param.reference.as_deref() {
4619 if let Some(param_name) = ref_str.strip_prefix("#/components/parameters/") {
4620 if let Some(resolved) = self.component_parameters.get(param_name) {
4621 return std::borrow::Cow::Borrowed(resolved);
4622 }
4623 }
4624 }
4625 std::borrow::Cow::Borrowed(param)
4626 }
4627
4628 fn referenced_schema_is_string_enum(&self, name: &str) -> bool {
4641 if self.resolve_cached_schema(name).is_some_and(|schema| {
4642 matches!(
4643 schema.schema_type,
4644 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. }
4645 )
4646 }) {
4647 return true;
4648 }
4649 let Some(schema_value) = self
4650 .openapi_spec
4651 .get("components")
4652 .and_then(|c| c.get("schemas"))
4653 .and_then(|s| s.get(name))
4654 else {
4655 return false;
4656 };
4657 let is_string_type = schema_value
4658 .get("type")
4659 .and_then(|v| v.as_str())
4660 .map(|s| s == "string")
4661 .unwrap_or(false);
4662 let has_enum_or_const =
4663 schema_value.get("enum").is_some() || schema_value.get("const").is_some();
4664 is_string_type && has_enum_or_const
4665 }
4666
4667 fn analyze_parameter(
4668 &mut self,
4669 param: &crate::openapi::Parameter,
4670 operation_id: &str,
4671 ) -> Result<Option<ParameterInfo>> {
4672 use heck::ToPascalCase;
4673
4674 let name = param.name.as_deref().unwrap_or("");
4675 let location = param.location.as_deref().unwrap_or("");
4676 let required = param.required.unwrap_or(false);
4677
4678 let mut rust_type = "String".to_string();
4679 let mut schema_ref = None;
4680 let mut enum_values: Option<Vec<String>> = None;
4681 let mut query_serialization: Option<QuerySerialization> = None;
4682
4683 let is_query = location == "query";
4689 let form_style = matches!(param.style.as_deref(), None | Some("form"));
4690 let form_exploded = form_style && param.explode.unwrap_or(true);
4691 let deep_object =
4692 param.style.as_deref() == Some("deepObject") && param.explode != Some(false);
4693
4694 let object_serialization = if !is_query {
4695 None
4696 } else if deep_object {
4697 Some(QuerySerialization::DeepObject)
4698 } else if form_exploded {
4699 Some(QuerySerialization::FormExplodedObject)
4700 } else if form_style {
4701 Some(QuerySerialization::FormObject)
4702 } else {
4703 None
4704 };
4705
4706 if let Some(schema) = ¶m.schema {
4707 if let Some(ref_str) = schema.reference() {
4708 if let Some(name) = self.extract_schema_name(ref_str) {
4714 if self.referenced_schema_is_string_enum(name) {
4715 schema_ref = Some(name.to_string());
4716 } else if object_serialization.is_some()
4717 && self.referenced_schema_is_object(name)
4718 {
4719 schema_ref = Some(name.to_string());
4720 query_serialization = object_serialization.clone();
4721 } else if is_query
4722 && form_style
4723 && let Some(item_type) = self.referenced_array_param_item_type(name)
4724 {
4725 schema_ref = Some(name.to_string());
4731 query_serialization = Some(if form_exploded {
4732 QuerySerialization::FormExplodedArray { item_type }
4733 } else {
4734 QuerySerialization::FormArray { item_type }
4735 });
4736 }
4737 }
4738 } else if object_serialization.is_some() && Self::schema_is_inline_object(schema) {
4739 let op_pascal = operation_id.replace('.', "_").to_pascal_case();
4744 let param_pascal = name.to_pascal_case();
4745 let synthetic_name = format!("{op_pascal}{param_pascal}");
4746 let mut deps = HashSet::new();
4747 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
4748 schema_ref = Some(synthetic_name);
4749 query_serialization = object_serialization.clone();
4750 } else if is_query
4751 && form_style
4752 && matches!(
4753 schema.schema_type(),
4754 Some(crate::openapi::SchemaType::Array)
4755 )
4756 && let Some(item_type) = self.array_param_item_type(schema)
4757 {
4758 query_serialization = Some(if form_exploded {
4766 QuerySerialization::FormExplodedArray { item_type }
4767 } else {
4768 QuerySerialization::FormArray { item_type }
4769 });
4770 } else if let Some(schema_type) = schema.schema_type() {
4771 let format = schema.details().format.clone();
4777 rust_type = match schema_type {
4778 crate::openapi::SchemaType::Boolean => "bool".to_string(),
4779 crate::openapi::SchemaType::Integer => {
4780 self.type_mapper.integer_format(format.as_deref()).rust_type
4781 }
4782 crate::openapi::SchemaType::Number => {
4783 self.type_mapper.number_format(format.as_deref()).rust_type
4784 }
4785 crate::openapi::SchemaType::String => "String".to_string(),
4786 _ => "String".to_string(),
4787 };
4788
4789 if matches!(schema_type, crate::openapi::SchemaType::String) {
4790 let details = schema.details();
4791 if details.is_string_enum() {
4792 if let Some(values) = details.string_enum_values() {
4793 if !values.is_empty() {
4794 let op_pascal = operation_id.replace('.', "_").to_pascal_case();
4795 let param_pascal = name.to_pascal_case();
4796 rust_type = format!("{op_pascal}{param_pascal}");
4797 enum_values = Some(values);
4798 }
4799 }
4800 }
4801 }
4802 }
4803
4804 if is_query && query_serialization.is_none() {
4805 let referenced_name = schema
4806 .reference()
4807 .and_then(|reference| self.extract_schema_name(reference));
4808 let is_object = referenced_name
4809 .is_some_and(|name| self.referenced_schema_is_object(name))
4810 || Self::schema_is_inline_object(schema);
4811 let is_array = referenced_name
4812 .is_some_and(|name| self.referenced_schema_is_array(name))
4813 || matches!(
4814 schema.schema_type(),
4815 Some(crate::openapi::SchemaType::Array)
4816 );
4817 let is_composed = referenced_name
4818 .is_some_and(|name| self.referenced_schema_is_composed_query_shape(name));
4819 let reason = if param.style.as_deref() == Some("deepObject")
4820 && param.explode == Some(false)
4821 {
4822 Some("style=deepObject with explode=false is undefined by OpenAPI".to_string())
4823 } else if param.style.as_deref() == Some("deepObject") && !is_object {
4824 Some("style=deepObject is defined only for object query parameters".to_string())
4825 } else if is_object {
4826 Some(format!(
4827 "object query parameters do not support style={}",
4828 param.style.as_deref().unwrap_or("form")
4829 ))
4830 } else if is_array && form_style {
4831 Some(
4832 "form array query parameters require scalar or string-enum items"
4833 .to_string(),
4834 )
4835 } else if is_array {
4836 Some(format!(
4837 "array query parameters do not yet support style={}",
4838 param.style.as_deref().unwrap_or("form")
4839 ))
4840 } else if is_composed {
4841 Some(
4842 "composed or union query schemas cannot be projected to an unambiguous flat wire shape"
4843 .to_string(),
4844 )
4845 } else {
4846 None
4847 };
4848 if let Some(reason) = reason {
4849 query_serialization = Some(QuerySerialization::Unsupported { reason });
4850 }
4851 }
4852 }
4853
4854 Ok(Some(ParameterInfo {
4855 name: name.to_string(),
4856 location: location.to_string(),
4857 required,
4858 schema_ref,
4859 rust_type,
4860 description: param.description.clone(),
4861 enum_values,
4862 rust_ident: None,
4863 query_serialization,
4864 }))
4865 }
4866
4867 fn array_param_item_type(&self, schema: &crate::openapi::Schema) -> Option<ArrayItemType> {
4876 let items = schema.details().items.as_deref()?;
4877 if let Some(ref_str) = items.reference() {
4878 let name = self.extract_schema_name(ref_str)?;
4879 return self
4880 .referenced_schema_is_string_enum(name)
4881 .then(|| ArrayItemType::EnumRef(name.to_string()));
4882 }
4883 let format = items.details().format.clone();
4884 let scalar = match items.schema_type()? {
4885 crate::openapi::SchemaType::String => "String".to_string(),
4886 crate::openapi::SchemaType::Integer => {
4887 self.type_mapper.integer_format(format.as_deref()).rust_type
4888 }
4889 crate::openapi::SchemaType::Number => {
4890 self.type_mapper.number_format(format.as_deref()).rust_type
4891 }
4892 crate::openapi::SchemaType::Boolean => "bool".to_string(),
4893 _ => return None,
4894 };
4895 Some(ArrayItemType::Scalar(scalar))
4896 }
4897
4898 fn referenced_array_param_item_type(&self, name: &str) -> Option<ArrayItemType> {
4901 let schema = self.resolve_cached_schema(name)?;
4902 let SchemaType::Array { item_type } = &schema.schema_type else {
4903 return None;
4904 };
4905 self.analyzed_array_item_type(item_type)
4906 }
4907
4908 fn analyzed_array_item_type(&self, item_type: &SchemaType) -> Option<ArrayItemType> {
4909 match item_type {
4910 SchemaType::Primitive { rust_type, .. } => {
4911 Some(ArrayItemType::Scalar(rust_type.clone()))
4912 }
4913 SchemaType::Reference { target } => {
4914 let resolved = self.resolve_cached_schema(target)?;
4915 matches!(
4916 resolved.schema_type,
4917 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. }
4918 )
4919 .then(|| ArrayItemType::EnumRef(target.clone()))
4920 }
4921 _ => None,
4922 }
4923 }
4924
4925 fn referenced_schema_is_object(&self, name: &str) -> bool {
4929 self.resolve_cached_schema(name)
4930 .is_some_and(|schema| matches!(schema.schema_type, SchemaType::Object { .. }))
4931 }
4932
4933 fn referenced_schema_is_array(&self, name: &str) -> bool {
4934 self.resolve_cached_schema(name)
4935 .is_some_and(|schema| matches!(schema.schema_type, SchemaType::Array { .. }))
4936 }
4937
4938 fn referenced_schema_is_composed_query_shape(&self, name: &str) -> bool {
4939 self.resolve_cached_schema(name).is_some_and(|schema| {
4940 matches!(
4941 schema.schema_type,
4942 SchemaType::Composition { .. }
4943 | SchemaType::Union { .. }
4944 | SchemaType::DiscriminatedUnion { .. }
4945 )
4946 })
4947 }
4948
4949 fn resolve_cached_schema(&self, name: &str) -> Option<&AnalyzedSchema> {
4950 let mut current = name;
4951 let mut visited = HashSet::new();
4952 loop {
4953 if !visited.insert(current) {
4954 return None;
4955 }
4956 let schema = self.resolved_cache.get(current)?;
4957 if let SchemaType::Reference { target } = &schema.schema_type {
4958 current = target;
4959 } else {
4960 return Some(schema);
4961 }
4962 }
4963 }
4964
4965 fn schema_is_inline_object(schema: &crate::openapi::Schema) -> bool {
4967 match schema.schema_type() {
4968 Some(crate::openapi::SchemaType::Object) => true,
4969 None => schema.details().properties.is_some(),
4970 _ => false,
4971 }
4972 }
4973}