1use crate::openapi::{Discriminator, OpenApiSpec, Schema, SchemaType as OpenApiSchemaType};
2use crate::type_mapping::TypeMapper;
3use crate::{GeneratorError, Result};
4use serde::Deserialize;
5use serde_json::Value;
6use std::collections::{BTreeMap, HashSet};
7use std::path::Path;
8
9fn extract_enum_extensions(
16 original: &Value,
17 enum_value_count: usize,
18 schema_name: &str,
19) -> Option<EnumExtensions> {
20 let obj = original.as_object()?;
21
22 let read_string_array = |key: &str| -> Option<Vec<String>> {
23 let arr = obj.get(key)?.as_array()?;
24 let mut out = Vec::with_capacity(arr.len());
25 for v in arr {
26 out.push(v.as_str()?.to_string());
27 }
28 Some(out)
29 };
30
31 let varnames_raw = read_string_array("x-enum-varnames");
32 let descriptions_raw = read_string_array("x-enum-descriptions");
33
34 if varnames_raw.is_none() && descriptions_raw.is_none() {
35 return None;
36 }
37
38 let validate = |label: &str, vals: Option<Vec<String>>| -> Vec<String> {
39 let Some(vals) = vals else {
40 return Vec::new();
41 };
42 if vals.len() == enum_value_count {
43 vals
44 } else {
45 eprintln!(
46 "⚠️ {schema_name}: dropping {label} (expected {enum_value_count} entries, got {})",
47 vals.len()
48 );
49 Vec::new()
50 }
51 };
52
53 let varnames = validate("x-enum-varnames", varnames_raw);
54 let descriptions = validate("x-enum-descriptions", descriptions_raw);
55
56 if varnames.is_empty() && descriptions.is_empty() {
57 return None;
58 }
59 Some(EnumExtensions {
60 varnames,
61 descriptions,
62 })
63}
64
65#[derive(Debug, Clone)]
66pub struct SchemaAnalysis {
67 pub schemas: BTreeMap<String, AnalyzedSchema>,
69 pub dependencies: DependencyGraph,
71 pub patterns: DetectedPatterns,
73 pub operations: BTreeMap<String, OperationInfo>,
75 pub operation_responses: BTreeMap<String, BTreeMap<String, OperationResponse>>,
79 pub operation_id_aliases: BTreeMap<String, Vec<String>>,
83 pub used_type_features: crate::type_mapping::UsedFeatures,
92 pub enum_extensions: BTreeMap<String, EnumExtensions>,
100 pub validation_context: ValidationContext,
104}
105
106impl SchemaType {
107 pub fn renders_inline(&self) -> bool {
118 match self {
119 Self::Primitive { .. }
120 | Self::Reference { .. }
121 | Self::Array { .. }
122 | Self::Tuple { .. }
123 | Self::Untyped { .. } => true,
124 Self::Object { .. }
125 | Self::StringEnum { .. }
126 | Self::ExtensibleEnum { .. }
127 | Self::DiscriminatedUnion { .. }
128 | Self::Union { .. }
129 | Self::Composition { .. } => false,
130 }
131 }
132}
133
134impl UntypedReason {
135 pub fn inline_drop(schema_type: &SchemaType) -> Option<Self> {
138 match schema_type {
139 SchemaType::Composition { .. } => Some(Self::InlineCompositionDropped),
140 SchemaType::Union { .. } | SchemaType::DiscriminatedUnion { .. } => {
141 Some(Self::InlineUnionDropped)
142 }
143 SchemaType::Object { .. } => Some(Self::InlineObjectDropped),
144 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. } => {
145 Some(Self::InlineEnumDropped)
146 }
147 _ => None,
148 }
149 }
150}
151
152fn schema_type_dependencies(schema_type: &SchemaType) -> HashSet<String> {
162 let mut targets = HashSet::new();
163 collect_type_dependencies(schema_type, &mut targets, 0);
164 targets
165}
166
167fn collect_type_dependencies(
168 schema_type: &SchemaType,
169 targets: &mut HashSet<String>,
170 depth: usize,
171) {
172 if depth > UNTYPED_WALK_DEPTH {
173 return;
174 }
175 match schema_type {
176 SchemaType::Reference { target } => {
177 targets.insert(target.clone());
178 }
179 SchemaType::Array { item_type } => collect_type_dependencies(item_type, targets, depth + 1),
180 SchemaType::Tuple { element_types } => {
181 for element_type in element_types {
182 collect_type_dependencies(element_type, targets, depth + 1);
183 }
184 }
185 SchemaType::Object {
186 properties,
187 additional_properties,
188 ..
189 } => {
190 for property in properties.values() {
191 collect_type_dependencies(&property.schema_type, targets, depth + 1);
192 }
193 if let ObjectAdditionalProperties::Typed { value_type } = additional_properties {
194 collect_type_dependencies(value_type, targets, depth + 1);
195 }
196 }
197 SchemaType::Union { variants } | SchemaType::Composition { schemas: variants } => {
198 for variant in variants {
199 targets.insert(variant.target.clone());
200 }
201 }
202 SchemaType::DiscriminatedUnion { variants, .. } => {
203 for variant in variants {
204 targets.insert(variant.type_name.clone());
205 }
206 }
207 SchemaType::Primitive { .. }
208 | SchemaType::StringEnum { .. }
209 | SchemaType::ExtensibleEnum { .. }
210 | SchemaType::Untyped { .. } => {}
211 }
212}
213
214fn normalize_untyped(schema_type: &mut SchemaType, depth: usize) {
224 if depth > UNTYPED_WALK_DEPTH {
225 return;
226 }
227 match schema_type {
228 SchemaType::Primitive { rust_type, .. } => {
229 let shape = match rust_type.as_str() {
230 "serde_json::Value" => Some(UntypedShape::Value),
231 "Vec<serde_json::Value>" => Some(UntypedShape::ValueArray),
232 _ => None,
233 };
234 if let Some(shape) = shape {
235 *schema_type = SchemaType::Untyped {
236 shape,
237 reason: UntypedReason::Unclassified,
238 };
239 }
240 }
241 SchemaType::Object {
242 properties,
243 additional_properties,
244 ..
245 } => {
246 for property in properties.values_mut() {
247 normalize_untyped(&mut property.schema_type, depth + 1);
248 }
249 if let ObjectAdditionalProperties::Typed { value_type } = additional_properties {
250 normalize_untyped(value_type, depth + 1);
251 }
252 }
253 SchemaType::Array { item_type } => normalize_untyped(item_type, depth + 1),
254 SchemaType::Tuple { element_types } => {
255 for element_type in element_types {
256 normalize_untyped(element_type, depth + 1);
257 }
258 }
259 SchemaType::Untyped { .. }
260 | SchemaType::StringEnum { .. }
261 | SchemaType::ExtensibleEnum { .. }
262 | SchemaType::DiscriminatedUnion { .. }
263 | SchemaType::Union { .. }
264 | SchemaType::Composition { .. }
265 | SchemaType::Reference { .. } => {}
266 }
267}
268
269impl SchemaAnalysis {
270 pub fn untyped_fields(&self) -> Vec<UntypedFinding> {
277 let mut findings = Vec::new();
278 for (name, schema) in &self.schemas {
279 collect_untyped(&schema.schema_type, name, &mut findings, 0);
280 }
281 findings.sort();
282 findings
283 }
284}
285
286const UNTYPED_WALK_DEPTH: usize = 32;
290
291fn collect_untyped(
292 schema_type: &SchemaType,
293 context: &str,
294 findings: &mut Vec<UntypedFinding>,
295 depth: usize,
296) {
297 if depth > UNTYPED_WALK_DEPTH {
298 return;
299 }
300 match schema_type {
301 SchemaType::Untyped { shape, reason } => findings.push(UntypedFinding {
302 context: context.to_string(),
303 shape: *shape,
304 reason: *reason,
305 }),
306 SchemaType::Object {
307 properties,
308 additional_properties,
309 ..
310 } => {
311 for (property_name, property) in properties {
312 let property_context = format!("{context}.{property_name}");
313 if let Some(reason) = UntypedReason::inline_drop(&property.schema_type) {
317 findings.push(UntypedFinding {
318 context: property_context,
319 shape: UntypedShape::Value,
320 reason,
321 });
322 continue;
323 }
324 collect_untyped(
325 &property.schema_type,
326 &property_context,
327 findings,
328 depth + 1,
329 );
330 }
331 match additional_properties {
332 ObjectAdditionalProperties::Untyped => findings.push(UntypedFinding {
333 context: format!("{context}.<additionalProperties>"),
334 shape: UntypedShape::ValueMap,
335 reason: UntypedReason::UntypedAdditionalProperties,
336 }),
337 ObjectAdditionalProperties::Typed { value_type } => collect_untyped(
338 value_type,
339 &format!("{context}.<additionalProperties>"),
340 findings,
341 depth + 1,
342 ),
343 ObjectAdditionalProperties::Forbidden => {}
344 }
345 }
346 SchemaType::Array { item_type } => {
347 let element_context = format!("{context}[]");
348 if let Some(reason) = UntypedReason::inline_drop(item_type) {
349 findings.push(UntypedFinding {
350 context: element_context,
351 shape: UntypedShape::Value,
352 reason,
353 });
354 } else {
355 collect_untyped(item_type, &element_context, findings, depth + 1);
356 }
357 }
358 SchemaType::Tuple { element_types } => {
359 for (index, element_type) in element_types.iter().enumerate() {
360 collect_untyped(
361 element_type,
362 &format!("{context}[{index}]"),
363 findings,
364 depth + 1,
365 );
366 }
367 }
368 SchemaType::Union { variants } | SchemaType::Composition { schemas: variants } => {
371 for (index, variant) in variants.iter().enumerate() {
372 if let Some(shape) = untyped_shape_of(&variant.target) {
373 findings.push(UntypedFinding {
374 context: format!("{context}|{index}"),
375 shape,
376 reason: UntypedReason::UntypedUnionBranch,
377 });
378 }
379 }
380 }
381 SchemaType::Primitive { .. }
382 | SchemaType::StringEnum { .. }
383 | SchemaType::ExtensibleEnum { .. }
384 | SchemaType::DiscriminatedUnion { .. }
385 | SchemaType::Reference { .. } => {}
386 }
387}
388
389fn untyped_shape_of(rust_type: &str) -> Option<UntypedShape> {
391 match rust_type {
392 "serde_json::Value" => Some(UntypedShape::Value),
393 "Vec<serde_json::Value>" => Some(UntypedShape::ValueArray),
394 _ => None,
395 }
396}
397
398#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
401pub struct UntypedFinding {
402 pub context: String,
405 pub shape: UntypedShape,
407 pub reason: UntypedReason,
409}
410
411#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
413#[serde(rename_all = "snake_case")]
414pub enum UntypedShape {
415 Value,
417 ValueArray,
419 ValueMap,
421}
422
423#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
431#[serde(rename_all = "kebab-case")]
432pub enum UntypedReason {
433 AnySchema,
436 OpaqueObject,
439 UntypedAdditionalProperties,
441 ArrayWithoutItems,
443 OpenPositionalItems,
446 UnrepresentableUnion,
449 UnrepresentableComposition,
451 UnsupportedTypeKeyword,
453 UnresolvedReference,
455 InlineCompositionDropped,
460 InlineUnionDropped,
462 InlineObjectDropped,
464 InlineEnumDropped,
466 UntypedUnionBranch,
470 Unclassified,
473}
474
475impl UntypedReason {
476 pub fn verdict(self) -> UntypedVerdict {
479 match self {
480 Self::AnySchema | Self::OpaqueObject | Self::UntypedAdditionalProperties => {
482 UntypedVerdict::Faithful
483 }
484 Self::ArrayWithoutItems | Self::OpenPositionalItems => UntypedVerdict::Faithful,
488 Self::InlineCompositionDropped
491 | Self::InlineUnionDropped
492 | Self::InlineObjectDropped
493 | Self::InlineEnumDropped => UntypedVerdict::Recoverable,
494 Self::UnrepresentableUnion
496 | Self::UnrepresentableComposition
497 | Self::UnsupportedTypeKeyword
498 | Self::UnresolvedReference => UntypedVerdict::Recoverable,
499 Self::UntypedUnionBranch | Self::Unclassified => UntypedVerdict::Unknown,
500 }
501 }
502}
503
504#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
506#[serde(rename_all = "snake_case")]
507pub enum UntypedVerdict {
508 Faithful,
510 Recoverable,
512 Unknown,
514}
515
516#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize)]
518pub struct OperationResponse {
519 pub schema_name: Option<String>,
521 pub media_type: Option<String>,
523 pub body: Option<OperationResponseBody>,
527 pub supports_streaming: bool,
529 pub has_content: bool,
531 pub unsupported_media_types: Vec<String>,
533}
534
535#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
540#[serde(tag = "kind", rename_all = "snake_case")]
541pub enum OperationResponseBody {
542 Json {
543 schema_name: String,
544 media_type: String,
545 },
546 Text {
547 media_type: String,
548 },
549 Binary {
550 media_type: String,
551 wildcard: bool,
552 },
553}
554
555#[derive(Debug, Clone, Default)]
556pub struct ValidationContext {
557 pub openapi_version: String,
558 pub json_schema_dialect: Option<String>,
559 pub component_schemas: BTreeMap<String, Value>,
560}
561
562#[derive(Debug, Clone, Default)]
567pub struct EnumExtensions {
568 pub varnames: Vec<String>,
573 pub descriptions: Vec<String>,
575}
576
577#[derive(Debug, Clone)]
578pub struct AnalyzedSchema {
579 pub name: String,
580 pub original: Value,
581 pub schema_type: SchemaType,
582 pub dependencies: HashSet<String>,
583 pub nullable: bool,
584 pub description: Option<String>,
585 pub default: Option<serde_json::Value>,
586}
587
588#[derive(Debug, Clone)]
589pub enum SchemaType {
590 Primitive {
596 rust_type: String,
597 serde_with: Option<String>,
598 },
599 Object {
601 properties: BTreeMap<String, PropertyInfo>,
602 required: HashSet<String>,
603 additional_properties: ObjectAdditionalProperties,
604 variant: Option<SchemaRef>,
609 },
610 DiscriminatedUnion {
612 discriminator_field: String,
613 variants: Vec<UnionVariant>,
614 },
615 Union { variants: Vec<SchemaRef> },
617 Array { item_type: Box<SchemaType> },
619 Tuple { element_types: Vec<SchemaType> },
625 StringEnum { values: Vec<String> },
627 ExtensibleEnum { known_values: Vec<String> },
629 Composition { schemas: Vec<SchemaRef> },
631 Reference { target: String },
633 Untyped {
640 shape: UntypedShape,
641 reason: UntypedReason,
642 },
643}
644
645#[derive(Debug, Clone)]
650pub enum ObjectAdditionalProperties {
651 Forbidden,
654 Untyped,
657 Typed { value_type: Box<SchemaType> },
660}
661
662impl ObjectAdditionalProperties {
663 pub fn is_open(&self) -> bool {
666 !matches!(self, Self::Forbidden)
667 }
668}
669
670#[derive(Debug, Clone)]
671pub struct PropertyInfo {
672 pub schema_type: SchemaType,
673 pub nullable: bool,
674 pub description: Option<String>,
675 pub default: Option<serde_json::Value>,
676 pub serde_attrs: Vec<String>,
677 pub constraints: PropertyConstraints,
682}
683
684#[derive(Debug, Clone, Default)]
689pub struct PropertyConstraints {
690 pub minimum: Option<f64>,
691 pub maximum: Option<f64>,
692 pub exclusive_minimum: Option<f64>,
693 pub exclusive_maximum: Option<f64>,
694 pub multiple_of: Option<f64>,
695 pub min_length: Option<u64>,
696 pub max_length: Option<u64>,
697 pub pattern: Option<String>,
698 pub min_items: Option<u64>,
699 pub max_items: Option<u64>,
700 pub unique_items: Option<bool>,
701}
702
703impl PropertyConstraints {
704 pub fn is_empty(&self) -> bool {
705 self.minimum.is_none()
706 && self.maximum.is_none()
707 && self.exclusive_minimum.is_none()
708 && self.exclusive_maximum.is_none()
709 && self.multiple_of.is_none()
710 && self.min_length.is_none()
711 && self.max_length.is_none()
712 && self.pattern.is_none()
713 && self.min_items.is_none()
714 && self.max_items.is_none()
715 && self.unique_items.is_none()
716 }
717
718 pub fn from_schema_details(details: &crate::openapi::SchemaDetails) -> Self {
723 use crate::openapi::ExclusiveBound;
724 let exclusive_minimum = match &details.exclusive_minimum {
725 Some(ExclusiveBound::Number(v)) => Some(*v),
726 _ => None,
727 };
728 let exclusive_maximum = match &details.exclusive_maximum {
729 Some(ExclusiveBound::Number(v)) => Some(*v),
730 _ => None,
731 };
732 Self {
733 minimum: details.minimum,
734 maximum: details.maximum,
735 exclusive_minimum,
736 exclusive_maximum,
737 multiple_of: details.multiple_of,
738 min_length: details.min_length,
739 max_length: details.max_length,
740 pattern: details.pattern.clone(),
741 min_items: details.min_items,
742 max_items: details.max_items,
743 unique_items: details.unique_items,
744 }
745 }
746}
747
748#[derive(Debug, Clone)]
749pub struct UnionVariant {
750 pub rust_name: String,
751 pub type_name: String,
752 pub discriminator_value: String,
753 pub schema_ref: String,
754}
755
756#[derive(Debug, Clone)]
757pub struct SchemaRef {
758 pub target: String,
759 pub nullable: bool,
760}
761
762#[derive(Debug, Clone)]
763pub struct DependencyGraph {
764 pub edges: BTreeMap<String, HashSet<String>>,
765 pub recursive_schemas: HashSet<String>,
767}
768
769#[derive(Debug, Clone)]
770pub struct DetectedPatterns {
771 pub tagged_enum_schemas: HashSet<String>,
773 pub untagged_enum_schemas: HashSet<String>,
775 pub type_mappings: BTreeMap<String, BTreeMap<String, String>>,
777}
778
779#[derive(Debug, Clone, Default, serde::Serialize)]
781pub struct OperationInfo {
782 pub operation_id: String,
784 pub method: String,
786 pub path: String,
788 pub summary: Option<String>,
790 pub description: Option<String>,
792 pub request_body: Option<RequestBodyContent>,
794 pub request_body_required: bool,
797 pub response_schemas: BTreeMap<String, String>,
799 pub parameters: Vec<ParameterInfo>,
801 pub supports_streaming: bool,
803 pub stream_parameter: Option<String>,
805 pub tags: Vec<String>,
809}
810
811#[derive(Debug, Clone, serde::Serialize)]
813#[serde(tag = "kind")]
814pub enum RequestBodyContent {
815 Json {
816 schema_name: String,
817 media_type: String,
818 #[serde(skip)]
819 validation_schema: Value,
820 },
821 FormUrlEncoded {
822 schema_name: String,
823 media_type: String,
824 #[serde(skip)]
825 validation_schema: Value,
826 },
827 Multipart {
828 schema_name: String,
829 media_type: String,
830 #[serde(skip)]
831 validation_schema: Value,
832 },
833 OctetStream {
834 media_type: String,
835 },
836 Binary {
837 media_type: String,
838 },
839 TextPlain {
840 media_type: String,
841 },
842 SchemaLess {
846 media_type: String,
847 },
848 Unsupported {
849 media_types: Vec<String>,
850 },
851}
852
853impl RequestBodyContent {
854 pub fn schema_name(&self) -> Option<&str> {
856 match self {
857 Self::Json { schema_name, .. }
858 | Self::FormUrlEncoded { schema_name, .. }
859 | Self::Multipart { schema_name, .. } => Some(schema_name),
860 Self::OctetStream { .. }
861 | Self::Binary { .. }
862 | Self::TextPlain { .. }
863 | Self::SchemaLess { .. }
864 | Self::Unsupported { .. } => None,
865 }
866 }
867}
868
869fn base_param_ident(name: &str) -> String {
873 use heck::ToSnakeCase;
874 let suffix = if name.ends_with("<=") {
875 "_lte"
876 } else if name.ends_with(">=") {
877 "_gte"
878 } else if name.ends_with('<') {
879 "_lt"
880 } else if name.ends_with('>') {
881 "_gt"
882 } else {
883 ""
884 };
885 let stripped = name.trim_end_matches(['<', '>', '=']);
886 let mut snake = stripped.to_snake_case();
887 if snake.is_empty() {
888 snake.push_str("parameter");
889 } else if snake.starts_with(|character: char| character.is_ascii_digit()) {
890 snake.insert(0, '_');
891 }
892 snake.push_str(suffix);
893 snake
894}
895
896#[derive(Debug, Clone, serde::Serialize)]
898pub struct ParameterInfo {
899 pub name: String,
901 pub location: String,
903 pub required: bool,
905 pub schema_ref: Option<String>,
907 pub rust_type: String,
909 pub description: Option<String>,
911 #[serde(skip_serializing_if = "Option::is_none")]
917 pub enum_values: Option<Vec<String>>,
918 #[serde(skip_serializing_if = "Option::is_none")]
924 pub enum_varnames: Option<Vec<String>>,
925 #[serde(skip_serializing_if = "Option::is_none")]
933 pub rust_ident: Option<String>,
934 #[serde(skip_serializing_if = "Option::is_none")]
943 pub query_serialization: Option<QuerySerialization>,
944 #[serde(skip)]
947 pub validation_schema: Option<Value>,
948}
949
950#[derive(Debug, Clone, PartialEq, serde::Serialize)]
953pub enum QuerySerialization {
954 FormExplodedObject,
958 FormExplodedNestedObject {
964 properties: Vec<QueryStructProperty>,
965 },
966 FormObject,
969 DeepObject,
972 FormExplodedArray { item_type: ArrayItemType },
975 FormArray { item_type: ArrayItemType },
978 SimpleHeaderArray { item_type: ArrayItemType },
981 Unsupported { reason: String },
986}
987
988#[derive(Debug, Clone, PartialEq, serde::Serialize)]
995pub enum ArrayItemType {
996 Scalar(String),
998 SchemaRef(String),
1000 FlatStructRef {
1006 schema_name: String,
1007 properties: Vec<QueryStructProperty>,
1008 },
1009 NestedStructRef {
1013 schema_name: String,
1014 properties: Vec<QueryStructProperty>,
1015 },
1016}
1017
1018#[derive(Debug, Clone, PartialEq, serde::Serialize)]
1019pub struct QueryStructProperty {
1020 pub wire_name: String,
1021 pub required: bool,
1022 pub value_type: QueryStructPropertyType,
1023}
1024
1025#[derive(Debug, Clone, PartialEq, serde::Serialize)]
1026pub enum QueryStructPropertyType {
1027 Scalar(QueryScalarType),
1028 Array {
1029 item_type: ArrayItemType,
1030 },
1031 Object {
1032 properties: Vec<QueryStructProperty>,
1033 },
1034}
1035
1036#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
1037pub enum QueryScalarType {
1038 String,
1039 Integer,
1040 Number,
1041 Boolean,
1042}
1043
1044impl Default for DependencyGraph {
1045 fn default() -> Self {
1046 Self::new()
1047 }
1048}
1049
1050impl DependencyGraph {
1051 pub fn new() -> Self {
1052 Self {
1053 edges: BTreeMap::new(),
1054 recursive_schemas: HashSet::new(),
1055 }
1056 }
1057
1058 pub fn add_dependency(&mut self, from: String, to: String) {
1059 self.edges.entry(from).or_default().insert(to);
1060 }
1061
1062 pub fn topological_sort(&mut self) -> Result<Vec<String>> {
1064 self.detect_recursive_schemas();
1066
1067 let mut temp_edges = self.edges.clone();
1069 for (schema, deps) in &mut temp_edges {
1070 deps.remove(schema); }
1072
1073 let mut visited = HashSet::new();
1074 let mut temp_visited = HashSet::new();
1075 let mut result = Vec::new();
1076
1077 let mut all_nodes: Vec<_> = temp_edges.keys().collect();
1079 all_nodes.sort();
1080 for node in all_nodes {
1081 if !visited.contains(node) {
1082 self.visit_node_recursive(
1083 node,
1084 &temp_edges,
1085 &mut visited,
1086 &mut temp_visited,
1087 &mut result,
1088 )?;
1089 }
1090 }
1091
1092 result.reverse();
1093 Ok(result)
1094 }
1095
1096 fn detect_recursive_schemas(&mut self) {
1097 for (schema, deps) in &self.edges {
1098 if deps.contains(schema) {
1099 self.recursive_schemas.insert(schema.clone());
1101 } else {
1102 if self.has_cycle_from(schema, schema, &mut HashSet::new()) {
1104 self.recursive_schemas.insert(schema.clone());
1105 }
1106 }
1107 }
1108
1109 for (schema, deps) in &self.edges {
1111 for dep in deps {
1112 if let Some(dep_deps) = self.edges.get(dep) {
1113 if dep_deps.contains(schema) {
1114 self.recursive_schemas.insert(schema.clone());
1116 self.recursive_schemas.insert(dep.clone());
1117 }
1118 }
1119 }
1120 }
1121 }
1122
1123 fn has_cycle_from(&self, start: &str, current: &str, visited: &mut HashSet<String>) -> bool {
1124 if visited.contains(current) {
1125 return false; }
1127
1128 visited.insert(current.to_string());
1129
1130 if let Some(deps) = self.edges.get(current) {
1131 for dep in deps {
1132 if dep == start {
1133 return true; }
1135 if self.has_cycle_from(start, dep, visited) {
1136 return true;
1137 }
1138 }
1139 }
1140
1141 false
1142 }
1143
1144 #[allow(clippy::only_used_in_recursion)]
1145 fn visit_node_recursive(
1146 &self,
1147 node: &str,
1148 temp_edges: &BTreeMap<String, HashSet<String>>,
1149 visited: &mut HashSet<String>,
1150 temp_visited: &mut HashSet<String>,
1151 result: &mut Vec<String>,
1152 ) -> Result<()> {
1153 if temp_visited.contains(node) {
1154 return Ok(());
1156 }
1157
1158 if visited.contains(node) {
1159 return Ok(());
1160 }
1161
1162 temp_visited.insert(node.to_string());
1163
1164 if let Some(dependencies) = temp_edges.get(node) {
1165 let mut sorted_deps: Vec<_> = dependencies.iter().collect();
1167 sorted_deps.sort();
1168 for dep in sorted_deps {
1169 self.visit_node_recursive(dep, temp_edges, visited, temp_visited, result)?;
1170 }
1171 }
1172
1173 temp_visited.remove(node);
1174 visited.insert(node.to_string());
1175 result.push(node.to_string());
1176
1177 Ok(())
1178 }
1179}
1180
1181pub fn merge_schema_extensions(
1184 main_spec: Value,
1185 extension_paths: &[impl AsRef<Path>],
1186) -> Result<Value> {
1187 let mut result = main_spec;
1188
1189 for path in extension_paths {
1190 let extension = load_extension_file(path.as_ref())?;
1191 result = merge_json_objects_with_replacements(result, extension)?;
1192 }
1193
1194 Ok(result)
1195}
1196
1197fn normalize_operation_path(path: &str) -> String {
1204 match path.split_once('#') {
1205 Some((route, _fragment)) if route.starts_with('/') => route.to_string(),
1206 _ => path.to_string(),
1207 }
1208}
1209
1210fn unwrap_annotation_allof(schema: &crate::openapi::Schema) -> &crate::openapi::Schema {
1215 let crate::openapi::Schema::AllOf { all_of, .. } = schema else {
1216 return schema;
1217 };
1218 let mut references = all_of.iter().filter(|s| s.reference().is_some());
1219 let (Some(first), None) = (references.next(), references.next()) else {
1220 return schema;
1221 };
1222 let others_annotation_only = all_of.iter().all(|member| {
1223 if member.reference().is_some() {
1224 return true;
1225 }
1226 serde_json::to_value(member)
1227 .ok()
1228 .and_then(|value| value.as_object().cloned())
1229 .is_some_and(|object| {
1230 object.keys().all(|key| {
1231 matches!(
1232 key.as_str(),
1233 "title"
1234 | "description"
1235 | "deprecated"
1236 | "readOnly"
1237 | "writeOnly"
1238 | "examples"
1239 | "example"
1240 | "externalDocs"
1241 | "xml"
1242 | "$comment"
1243 ) || key.starts_with("x-")
1244 })
1245 })
1246 });
1247 if others_annotation_only {
1248 first
1249 } else {
1250 schema
1251 }
1252}
1253
1254fn load_extension_file(path: &Path) -> Result<Value> {
1258 let content = std::fs::read_to_string(path).map_err(|e| GeneratorError::FileError {
1259 message: format!("Failed to read file {}: {}", path.display(), e),
1260 })?;
1261
1262 let is_yaml = path
1263 .extension()
1264 .and_then(|extension| extension.to_str())
1265 .is_some_and(|extension| {
1266 extension.eq_ignore_ascii_case("yaml") || extension.eq_ignore_ascii_case("yml")
1267 });
1268
1269 if is_yaml {
1270 crate::spec_source::yaml_to_json_value(&content).map_err(|error| {
1271 GeneratorError::FileError {
1272 message: format!(
1273 "Failed to parse schema extension {} as YAML: {}",
1274 path.display(),
1275 error
1276 ),
1277 }
1278 })
1279 } else {
1280 serde_json::from_str(&content).map_err(|error| GeneratorError::FileError {
1281 message: format!(
1282 "Failed to parse schema extension {} as JSON: {}",
1283 path.display(),
1284 error
1285 ),
1286 })
1287 }
1288}
1289
1290fn merge_json_objects_with_replacements(main: Value, extension: Value) -> Result<Value> {
1292 let replacements = extract_replacement_rules(&extension);
1294
1295 Ok(merge_json_objects_with_rules(
1297 main,
1298 extension,
1299 &replacements,
1300 ))
1301}
1302
1303fn extract_replacement_rules(
1305 extension: &Value,
1306) -> std::collections::HashMap<String, (String, String)> {
1307 let mut rules = std::collections::HashMap::new();
1308
1309 if let Some(x_replacements) = extension.get("x-replacements") {
1310 if let Some(x_replacements_obj) = x_replacements.as_object() {
1311 for (schema_name, replacement_rule) in x_replacements_obj {
1312 if let Some(rule_obj) = replacement_rule.as_object() {
1313 if let (Some(replace), Some(with)) = (
1314 rule_obj.get("replace").and_then(|v| v.as_str()),
1315 rule_obj.get("with").and_then(|v| v.as_str()),
1316 ) {
1317 rules.insert(schema_name.clone(), (replace.to_string(), with.to_string()));
1318 }
1320 }
1321 }
1322 }
1323 }
1324
1325 rules
1326}
1327
1328fn should_replace_variant(
1330 schema_name: &str,
1331 extension_refs: &[String],
1332 replacements: &std::collections::HashMap<String, (String, String)>,
1333) -> bool {
1334 for (replace_schema, with_schema) in replacements.values() {
1336 if schema_name == replace_schema {
1337 let replacement_exists = extension_refs.iter().any(|ext_ref| {
1339 let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
1340 ext_schema_name == with_schema
1341 });
1342
1343 if replacement_exists {
1344 return true;
1345 }
1346 }
1347 }
1348
1349 extension_refs.iter().any(|ext_ref| {
1351 let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
1352 schema_name == ext_schema_name
1353 })
1354}
1355
1356fn merge_json_objects_with_rules(
1361 main: Value,
1362 extension: Value,
1363 replacements: &std::collections::HashMap<String, (String, String)>,
1364) -> Value {
1365 match (main, extension) {
1366 (Value::Object(mut main_obj), Value::Object(ext_obj)) => {
1368 let main_union_keyword = if main_obj.contains_key("oneOf") {
1371 Some("oneOf")
1372 } else if main_obj.contains_key("anyOf") {
1373 Some("anyOf")
1374 } else {
1375 None
1376 };
1377 if let (Some(main_variants), Some(ext_variants)) = (
1378 extract_schema_variants(&Value::Object(main_obj.clone())),
1379 extract_schema_variants(&Value::Object(ext_obj.clone())),
1380 ) {
1381 let union_key = main_union_keyword.unwrap_or("oneOf");
1382 println!(
1383 "🔍 Merging union schemas ({union_key}): {} main variants, {} extension variants",
1384 main_variants.len(),
1385 ext_variants.len()
1386 );
1387 let mut merged_variants = Vec::new();
1390 let extension_refs: Vec<String> = ext_variants
1391 .iter()
1392 .filter_map(|v| v.get("$ref").and_then(|r| r.as_str()))
1393 .map(|s| s.to_string())
1394 .collect();
1395
1396 for main_variant in main_variants {
1398 if let Some(main_ref) = main_variant.get("$ref").and_then(|r| r.as_str()) {
1399 let schema_name = main_ref.split('/').next_back().unwrap_or("");
1401 let should_replace =
1402 should_replace_variant(schema_name, &extension_refs, replacements);
1403
1404 if should_replace {
1405 println!("🔄 REPLACING {} (explicit rule)", schema_name);
1406 }
1407
1408 if !should_replace {
1409 merged_variants.push(main_variant);
1410 }
1411 } else {
1412 merged_variants.push(main_variant);
1414 }
1415 }
1416
1417 for ext_variant in ext_variants {
1419 merged_variants.push(ext_variant);
1420 }
1421
1422 main_obj.remove("oneOf");
1424 main_obj.remove("anyOf");
1425 main_obj.insert(union_key.to_string(), Value::Array(merged_variants));
1426
1427 for (key, ext_value) in ext_obj {
1429 if key != "oneOf" && key != "anyOf" {
1430 match main_obj.get(&key) {
1431 Some(main_value) => {
1432 let merged_value = merge_json_objects_with_rules(
1433 main_value.clone(),
1434 ext_value,
1435 replacements,
1436 );
1437 main_obj.insert(key, merged_value);
1438 }
1439 None => {
1440 main_obj.insert(key, ext_value);
1441 }
1442 }
1443 }
1444 }
1445
1446 return Value::Object(main_obj);
1447 }
1448
1449 for (key, ext_value) in ext_obj {
1451 match main_obj.get(&key) {
1452 Some(main_value) => {
1453 let merged_value = merge_json_objects_with_rules(
1455 main_value.clone(),
1456 ext_value,
1457 replacements,
1458 );
1459 main_obj.insert(key, merged_value);
1460 }
1461 None => {
1462 main_obj.insert(key, ext_value);
1464 }
1465 }
1466 }
1467 Value::Object(main_obj)
1468 }
1469
1470 (Value::Array(mut main_arr), Value::Array(ext_arr)) => {
1472 main_arr.extend(ext_arr);
1473 Value::Array(main_arr)
1474 }
1475
1476 (_, extension) => extension,
1478 }
1479}
1480
1481fn extract_schema_variants(obj: &Value) -> Option<Vec<Value>> {
1483 if let Value::Object(map) = obj {
1484 if let Some(Value::Array(variants)) = map.get("oneOf") {
1485 return Some(variants.clone());
1486 }
1487 if let Some(Value::Array(variants)) = map.get("anyOf") {
1488 return Some(variants.clone());
1489 }
1490 }
1491 None
1492}
1493
1494pub struct SchemaAnalyzer {
1495 schemas: BTreeMap<String, Schema>,
1496 resolved_cache: BTreeMap<String, AnalyzedSchema>,
1497 openapi_spec: Value,
1498 current_schema_name: Option<String>,
1499 component_parameters: BTreeMap<String, crate::openapi::Parameter>,
1500 type_mapper: TypeMapper,
1505 resolving_pointers: HashSet<String>,
1508}
1509
1510impl SchemaAnalyzer {
1511 fn untyped_value(&self, _context: impl Into<String>, reason: UntypedReason) -> SchemaType {
1515 SchemaType::Untyped {
1516 shape: UntypedShape::Value,
1517 reason,
1518 }
1519 }
1520
1521 fn untyped_value_array(
1523 &self,
1524 _context: impl Into<String>,
1525 reason: UntypedReason,
1526 ) -> SchemaType {
1527 SchemaType::Untyped {
1528 shape: UntypedShape::ValueArray,
1529 reason,
1530 }
1531 }
1532
1533 fn untyped_context(&self, detail: &str) -> String {
1535 match (&self.current_schema_name, detail) {
1536 (Some(schema), "") => schema.clone(),
1537 (Some(schema), detail) => format!("{schema}.{detail}"),
1538 (None, "") => "<anonymous>".to_string(),
1539 (None, detail) => detail.to_string(),
1540 }
1541 }
1542
1543 fn uses_aws_query_conventions(&self) -> bool {
1544 self.openapi_spec
1545 .pointer("/info/x-providerName")
1546 .and_then(Value::as_str)
1547 .is_some_and(|provider| provider.eq_ignore_ascii_case("amazonaws.com"))
1548 }
1549
1550 pub fn new(openapi_spec: Value) -> Result<Self> {
1554 Self::with_type_mapper(openapi_spec, TypeMapper::default())
1555 }
1556
1557 pub fn with_type_mapper(mut openapi_spec: Value, type_mapper: TypeMapper) -> Result<Self> {
1561 disambiguate_component_schema_names(&mut openapi_spec);
1562 let spec: OpenApiSpec = parse_spec_document(&openapi_spec)?;
1563 let schemas = Self::extract_schemas(&spec)?;
1564
1565 let component_parameters = spec
1566 .components
1567 .as_ref()
1568 .and_then(|c| c.parameters.as_ref())
1569 .cloned()
1570 .unwrap_or_default();
1571 Ok(Self {
1572 schemas,
1573 resolved_cache: BTreeMap::new(),
1574 openapi_spec,
1575 current_schema_name: None,
1576 component_parameters,
1577 type_mapper,
1578 resolving_pointers: HashSet::new(),
1579 })
1580 }
1581
1582 pub fn new_with_extensions(
1585 openapi_spec: Value,
1586 extension_paths: &[std::path::PathBuf],
1587 ) -> Result<Self> {
1588 let merged_spec = merge_schema_extensions(openapi_spec, extension_paths)?;
1589 Self::new(merged_spec)
1590 }
1591
1592 pub fn new_with_extensions_and_type_mapper(
1595 openapi_spec: Value,
1596 extension_paths: &[std::path::PathBuf],
1597 type_mapper: TypeMapper,
1598 ) -> Result<Self> {
1599 let merged_spec = merge_schema_extensions(openapi_spec, extension_paths)?;
1600 Self::with_type_mapper(merged_spec, type_mapper)
1601 }
1602
1603 pub fn type_mapper(&self) -> &TypeMapper {
1607 &self.type_mapper
1608 }
1609
1610 fn generate_context_aware_name(
1613 &self,
1614 base_context: &str,
1615 type_hint: &str,
1616 index: usize,
1617 schema: Option<&Schema>,
1618 ) -> String {
1619 if let Some(schema) = schema {
1621 if type_hint == "Array"
1623 && matches!(schema.schema_type(), Some(OpenApiSchemaType::Array))
1624 {
1625 if let Some(items_schema) = schema.details().item_schema() {
1626 if let Some(item_type) = items_schema.schema_type() {
1628 match item_type {
1629 OpenApiSchemaType::Object => {
1630 return format!("{base_context}ItemArray");
1631 }
1632 OpenApiSchemaType::String => {
1633 return format!("{base_context}StringArray");
1634 }
1635 _ => {}
1636 }
1637 }
1638 }
1639 }
1640 }
1641
1642 match type_hint {
1644 "Array" => {
1645 format!("{base_context}Array")
1647 }
1648 "Variant" | "InlineVariant" => {
1649 if index == 0 {
1651 format!("{base_context}{type_hint}")
1652 } else {
1653 format!("{}{}{}", base_context, type_hint, index + 1)
1654 }
1655 }
1656 _ => {
1657 format!("{base_context}{type_hint}{index}")
1659 }
1660 }
1661 }
1662
1663 fn to_pascal_case(&self, s: &str) -> String {
1665 s.split(['_', '-'])
1666 .filter(|part| !part.is_empty())
1667 .map(|part| {
1668 let mut chars = part.chars();
1669 match chars.next() {
1670 None => String::new(),
1671 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
1672 }
1673 })
1674 .collect()
1675 }
1676
1677 fn extract_schemas(spec: &OpenApiSpec) -> Result<BTreeMap<String, Schema>> {
1678 let schemas = spec.components.as_ref().and_then(|c| c.schemas.as_ref());
1683 Ok(schemas
1684 .map(|m| {
1685 m.iter()
1686 .map(|(k, v)| (k.clone(), v.clone()))
1687 .collect::<BTreeMap<_, _>>()
1688 })
1689 .unwrap_or_default())
1690 }
1691
1692 pub fn analyze(&mut self) -> Result<SchemaAnalysis> {
1693 let validation_context = ValidationContext {
1694 openapi_version: self
1695 .openapi_spec
1696 .get("openapi")
1697 .and_then(Value::as_str)
1698 .unwrap_or_default()
1699 .to_string(),
1700 json_schema_dialect: self
1701 .openapi_spec
1702 .get("jsonSchemaDialect")
1703 .and_then(Value::as_str)
1704 .map(str::to_string),
1705 component_schemas: self
1706 .openapi_spec
1707 .pointer("/components/schemas")
1708 .and_then(Value::as_object)
1709 .map(|schemas| {
1710 schemas
1711 .iter()
1712 .map(|(name, schema)| (name.clone(), schema.clone()))
1713 .collect()
1714 })
1715 .unwrap_or_default(),
1716 };
1717 let mut analysis = SchemaAnalysis {
1718 schemas: BTreeMap::new(),
1719 dependencies: DependencyGraph::new(),
1720 patterns: DetectedPatterns {
1721 tagged_enum_schemas: HashSet::new(),
1722 untagged_enum_schemas: HashSet::new(),
1723 type_mappings: BTreeMap::new(),
1724 },
1725 operations: BTreeMap::new(),
1726 operation_responses: BTreeMap::new(),
1727 operation_id_aliases: BTreeMap::new(),
1728 used_type_features: crate::type_mapping::UsedFeatures::default(),
1729 enum_extensions: BTreeMap::new(),
1730 validation_context,
1731 };
1732
1733 self.detect_patterns(&mut analysis.patterns)?;
1735
1736 let schema_names: Vec<String> = self.schemas.keys().cloned().collect();
1738 for schema_name in schema_names {
1739 let analyzed = self.analyze_schema(&schema_name)?;
1740
1741 for dep in &analyzed.dependencies {
1743 analysis
1744 .dependencies
1745 .add_dependency(schema_name.clone(), dep.clone());
1746 }
1747
1748 analysis.schemas.insert(schema_name, analyzed);
1749 }
1750
1751 for (inline_name, inline_schema) in &self.resolved_cache {
1754 if !analysis.schemas.contains_key(inline_name) {
1755 analysis
1757 .schemas
1758 .insert(inline_name.clone(), inline_schema.clone());
1759
1760 for dep in &inline_schema.dependencies {
1762 analysis
1763 .dependencies
1764 .add_dependency(inline_name.clone(), dep.clone());
1765 }
1766
1767 let mut schemas_to_update = Vec::new();
1772 for (schema_name, schema) in &analysis.schemas {
1773 if schema_name == inline_name {
1775 continue;
1776 }
1777
1778 if schema.dependencies.contains(inline_name) {
1779 schemas_to_update.push(schema_name.clone());
1781 }
1782 }
1783
1784 for schema_name in schemas_to_update {
1786 analysis
1787 .dependencies
1788 .add_dependency(schema_name, inline_name.clone());
1789 }
1790 }
1791 }
1792
1793 self.analyze_operations(&mut analysis)?;
1795
1796 for (inline_name, inline_schema) in &self.resolved_cache {
1799 if !analysis.schemas.contains_key(inline_name) {
1800 analysis
1801 .schemas
1802 .insert(inline_name.clone(), inline_schema.clone());
1803
1804 for dep in &inline_schema.dependencies {
1806 analysis
1807 .dependencies
1808 .add_dependency(inline_name.clone(), dep.clone());
1809 }
1810 }
1811 }
1812
1813 disambiguate_analyzed_schema_names(&mut analysis, &self.schemas);
1814
1815 analysis.used_type_features = self.type_mapper.used_features();
1819
1820 for (name, analyzed) in &analysis.schemas {
1825 let enum_value_count = match &analyzed.schema_type {
1826 SchemaType::StringEnum { values } => values.len(),
1827 SchemaType::ExtensibleEnum { known_values } => known_values.len(),
1828 _ => continue,
1829 };
1830 if let Some(ext) = extract_enum_extensions(&analyzed.original, enum_value_count, name) {
1831 analysis.enum_extensions.insert(name.clone(), ext);
1832 }
1833 }
1834
1835 for schema in analysis.schemas.values_mut() {
1836 normalize_untyped(&mut schema.schema_type, 0);
1837 }
1838
1839 Ok(analysis)
1840 }
1841
1842 fn detect_patterns(&self, patterns: &mut DetectedPatterns) -> Result<()> {
1843 for (schema_name, schema) in &self.schemas {
1844 if self.is_discriminated_union(schema) {
1846 patterns.tagged_enum_schemas.insert(schema_name.clone());
1847
1848 if let Some(mappings) = self.extract_type_mappings(schema)? {
1850 patterns.type_mappings.insert(schema_name.clone(), mappings);
1851 }
1852 }
1853 else if self.is_simple_union(schema) {
1855 patterns.untagged_enum_schemas.insert(schema_name.clone());
1856 }
1857 }
1858
1859 Ok(())
1860 }
1861
1862 fn is_discriminated_union(&self, schema: &Schema) -> bool {
1863 if schema.is_discriminated_union() {
1865 return true;
1866 }
1867
1868 if let Some(variants) = schema.union_variants() {
1870 return variants.len() > 2 && self.detect_discriminator_field(variants).is_some();
1871 }
1872
1873 false
1874 }
1875
1876 fn all_variants_have_unique_const_values(&self, variants: &[Schema], field_name: &str) -> bool {
1877 let mut values = HashSet::new();
1878
1879 variants.iter().all(|variant| {
1880 let schema = if let Some(ref_str) = variant.reference() {
1881 let Some(schema_name) = self.extract_schema_name(ref_str) else {
1882 return false;
1883 };
1884 let Some(schema) = self.schemas.get(schema_name) else {
1885 return false;
1886 };
1887 schema
1888 } else {
1889 variant
1890 };
1891
1892 self.extract_discriminator_value_for_field(schema, field_name)
1893 .is_some_and(|value| values.insert(value))
1894 })
1895 }
1896
1897 fn branch_resolves_to_object(&self, schema: &Schema) -> bool {
1906 if let Some(ref_str) = schema.reference() {
1908 return match self
1909 .extract_schema_name(ref_str)
1910 .and_then(|n| self.schemas.get(n))
1911 {
1912 Some(target) => self.branch_resolves_to_object(target),
1913 None => false,
1914 };
1915 }
1916 if matches!(
1919 schema,
1920 Schema::AllOf { .. } | Schema::AnyOf { .. } | Schema::OneOf { .. }
1921 ) {
1922 return true;
1923 }
1924 if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object)) {
1925 return true;
1926 }
1927 if schema.inferred_type() == Some(OpenApiSchemaType::Object) {
1928 return true;
1929 }
1930 false
1933 }
1934
1935 fn detect_discriminator_field(&self, variants: &[Schema]) -> Option<String> {
1939 if variants.is_empty() {
1940 return None;
1941 }
1942
1943 let first_variant = &variants[0];
1945 let first_schema = if let Some(ref_str) = first_variant.reference() {
1946 let schema_name = self.extract_schema_name(ref_str)?;
1947 self.schemas.get(schema_name)?
1948 } else {
1949 first_variant
1950 };
1951
1952 let properties = first_schema.details().properties.as_ref()?;
1953 let mut candidates: Vec<String> = Vec::new();
1954
1955 for (field_name, field_schema) in properties {
1956 let details = field_schema.details();
1957 let is_const = details.const_value.is_some()
1958 || details.enum_values.as_ref().is_some_and(|v| v.len() == 1)
1959 || details.extra.contains_key("const");
1960 if is_const {
1961 candidates.push(field_name.clone());
1962 }
1963 }
1964
1965 if candidates.is_empty() {
1966 return None;
1967 }
1968
1969 candidates.sort_by(|a, b| {
1971 if a == "type" {
1972 std::cmp::Ordering::Less
1973 } else if b == "type" {
1974 std::cmp::Ordering::Greater
1975 } else {
1976 a.cmp(b)
1977 }
1978 });
1979
1980 for candidate in &candidates {
1986 if self.all_variants_have_unique_const_values(variants, candidate) {
1987 return Some(candidate.clone());
1988 }
1989 }
1990
1991 None
1992 }
1993
1994 fn is_simple_union(&self, schema: &Schema) -> bool {
1995 if let Some(variants) = schema.union_variants() {
1996 if variants.len() > 1 && !schema.is_nullable_pattern() {
1998 let has_refs = variants.iter().any(|v| v.is_reference());
1999 return has_refs;
2000 }
2001 }
2002 false
2003 }
2004
2005 fn extract_type_mappings(&self, schema: &Schema) -> Result<Option<BTreeMap<String, String>>> {
2006 let variants = schema.union_variants().ok_or_else(|| {
2007 GeneratorError::InvalidSchema("No variants found for discriminated union".to_string())
2008 })?;
2009
2010 let discriminator_field = if let Some(discriminator) = schema.discriminator() {
2012 discriminator.property_name.clone()
2013 } else if let Some(detected) = self.detect_discriminator_field(variants) {
2014 detected
2015 } else {
2016 "type".to_string() };
2018
2019 let mut mappings = BTreeMap::new();
2020
2021 for variant in variants {
2022 if let Some(ref_str) = variant.reference() {
2023 if let Some(type_name) = self.extract_schema_name(ref_str) {
2024 if let Some(variant_schema) = self.schemas.get(type_name) {
2025 if let Some(discriminator_value) = self
2026 .extract_discriminator_value_for_field(
2027 variant_schema,
2028 &discriminator_field,
2029 )
2030 {
2031 mappings.insert(type_name.to_string(), discriminator_value);
2032 }
2033 }
2034 }
2035 }
2036 }
2037
2038 if mappings.is_empty() {
2039 Ok(None)
2040 } else {
2041 Ok(Some(mappings))
2042 }
2043 }
2044
2045 #[allow(dead_code)]
2046 fn extract_discriminator_value(&self, schema: &Schema) -> Option<String> {
2047 self.extract_discriminator_value_for_field(schema, "type")
2048 }
2049
2050 fn extract_discriminator_value_for_field(
2051 &self,
2052 schema: &Schema,
2053 field_name: &str,
2054 ) -> Option<String> {
2055 if let Some(properties) = &schema.details().properties {
2056 if let Some(type_field) = properties.get(field_name) {
2057 if let Some(const_value) = &type_field.details().const_value {
2059 if let Some(value) = const_value.as_str() {
2060 return Some(value.to_string());
2061 }
2062 }
2063 if let Some(enum_values) = &type_field.details().enum_values {
2065 if enum_values.len() == 1 {
2066 return enum_values[0].as_str().map(|s| s.to_string());
2067 }
2068 }
2069 if let Some(const_value) = type_field.details().extra.get("const") {
2071 return const_value.as_str().map(|s| s.to_string());
2072 }
2073 if let Some(stainless_const) = type_field.details().extra.get("x-stainless-const") {
2075 if stainless_const.as_bool() == Some(true) {
2076 if let Some(default_value) = &type_field.details().default {
2077 if let Some(value) = default_value.as_str() {
2078 return Some(value.to_string());
2079 }
2080 }
2081 }
2082 }
2083 }
2084 }
2085 None
2086 }
2087
2088 fn get_any_reference<'a>(&self, schema: &'a Schema) -> Option<&'a str> {
2089 schema.reference().or_else(|| schema.recursive_reference())
2090 }
2091
2092 fn extract_schema_name<'a>(&self, ref_str: &'a str) -> Option<&'a str> {
2093 if ref_str == "#" {
2094 return None; }
2096
2097 let parts: Vec<&str> = ref_str.split('/').collect();
2098
2099 if parts.len() >= 4 && parts[0] == "#" && parts[2] == "schemas" {
2101 return Some(parts[3]);
2102 }
2103
2104 if parts.len() >= 3 && parts[0] == "#" && parts[1] == "definitions" {
2107 return Some(parts[2]);
2108 }
2109
2110 let last = parts.last()?;
2116 if last.is_empty()
2117 || last.chars().all(|c| c.is_ascii_digit())
2118 || matches!(
2119 *last,
2120 "schema" | "properties" | "items" | "additionalProperties"
2121 )
2122 {
2123 return None;
2124 }
2125 let first = last.chars().next().unwrap_or(' ');
2126 if !first.is_ascii_alphabetic() || !first.is_ascii_uppercase() {
2127 return None;
2128 }
2129 Some(last)
2130 }
2131
2132 fn analyze_schema(&mut self, schema_name: &str) -> Result<AnalyzedSchema> {
2133 if let Some(cached) = self.resolved_cache.get(schema_name) {
2135 return Ok(cached.clone());
2136 }
2137
2138 self.current_schema_name = Some(schema_name.to_string());
2140
2141 let schema = self
2142 .schemas
2143 .get(schema_name)
2144 .ok_or_else(|| GeneratorError::UnresolvedReference(schema_name.to_string()))?
2145 .clone();
2146
2147 self.resolved_cache.insert(
2149 schema_name.to_string(),
2150 AnalyzedSchema {
2151 name: schema_name.to_string(),
2152 original: serde_json::to_value(&schema).unwrap_or(Value::Null),
2153 schema_type: SchemaType::Reference {
2154 target: "placeholder".to_string(),
2155 },
2156 dependencies: HashSet::new(),
2157 nullable: false,
2158 description: None,
2159 default: None,
2160 },
2161 );
2162
2163 let analyzed = self.analyze_schema_value(&schema, schema_name)?;
2164
2165 self.resolved_cache
2167 .insert(schema_name.to_string(), analyzed.clone());
2168
2169 Ok(analyzed)
2170 }
2171
2172 fn analyze_schema_value(
2173 &mut self,
2174 schema: &Schema,
2175 schema_name: &str,
2176 ) -> Result<AnalyzedSchema> {
2177 let details = schema.details();
2178 let description = details.description.clone();
2179 let nullable = details.is_nullable() || schema.type_array_contains_null();
2181 let mut dependencies = HashSet::new();
2182
2183 let schema_type = match schema {
2184 Schema::Reference { reference, .. } => {
2185 match self.extract_schema_name(reference) {
2190 Some(name) => {
2191 let target = name.to_string();
2192 dependencies.insert(target.clone());
2193 SchemaType::Reference { target }
2194 }
2195 None => {
2196 let reference = reference.clone();
2197 if let Some(resolved) =
2198 self.resolve_pointer_schema(&reference, &mut dependencies)?
2199 {
2200 resolved
2201 } else {
2202 eprintln!(
2203 "⚠️ unresolvable $ref `{}` — typing as serde_json::Value",
2204 reference
2205 );
2206 self.untyped_value(
2207 format!("$ref {reference}"),
2208 UntypedReason::UnresolvedReference,
2209 )
2210 }
2211 }
2212 }
2213 }
2214 Schema::RecursiveRef { recursive_ref, .. }
2215 | Schema::DynamicRef {
2216 dynamic_ref: recursive_ref,
2217 ..
2218 } => {
2219 if recursive_ref == "#" {
2225 dependencies.insert(schema_name.to_string());
2226 SchemaType::Reference {
2227 target: schema_name.to_string(),
2228 }
2229 } else {
2230 let target = self
2231 .extract_schema_name(recursive_ref)
2232 .unwrap_or(schema_name)
2233 .to_string();
2234 dependencies.insert(target.clone());
2235 SchemaType::Reference { target }
2236 }
2237 }
2238 Schema::Typed { .. } | Schema::TypedMulti { .. } => {
2239 if let Some(non_null_types) = schema.non_null_schema_types() {
2240 let mut variants = Vec::with_capacity(non_null_types.len());
2241 for t in non_null_types {
2242 variants.push(self.build_typed_multi_union_variant(
2243 t,
2244 schema,
2245 schema_name,
2246 &mut dependencies,
2247 )?);
2248 }
2249 SchemaType::Union { variants }
2250 } else {
2251 self.analyze_single_typed_schema(
2252 schema,
2253 schema_name,
2254 details,
2255 &mut dependencies,
2256 )?
2257 }
2258 }
2259 Schema::AnyOf {
2260 any_of,
2261 discriminator,
2262 ..
2263 } => {
2264 if Self::union_only_constrains_requiredness(any_of) {
2265 return Ok(AnalyzedSchema {
2266 name: schema_name.to_string(),
2267 original: serde_json::to_value(schema).unwrap_or(Value::Null),
2268 schema_type: self.analyze_empty_union(schema, &mut dependencies)?,
2269 dependencies,
2270 nullable,
2271 description,
2272 default: details.default.clone(),
2273 });
2274 }
2275 if let Some(schema_type) = self.analyze_object_with_variants(
2276 schema,
2277 any_of,
2278 schema_name,
2279 &mut dependencies,
2280 )? {
2281 return Ok(AnalyzedSchema {
2282 name: schema_name.to_string(),
2283 original: serde_json::to_value(schema).unwrap_or(Value::Null),
2284 schema_type,
2285 dependencies,
2286 nullable,
2287 description,
2288 default: details.default.clone(),
2289 });
2290 }
2291 self.analyze_anyof_union(
2293 any_of,
2294 discriminator.as_ref(),
2295 &mut dependencies,
2296 schema_name,
2297 )?
2298 }
2299 Schema::OneOf {
2300 one_of,
2301 discriminator,
2302 ..
2303 } => {
2304 if one_of.is_empty() {
2305 self.analyze_empty_union(schema, &mut dependencies)?
2306 } else if let Some(schema_type) = self.analyze_object_with_variants(
2307 schema,
2308 one_of,
2309 schema_name,
2310 &mut dependencies,
2311 )? {
2312 schema_type
2313 } else {
2314 self.analyze_oneof_union(
2316 one_of,
2317 discriminator.as_ref(),
2318 schema_name,
2319 &mut dependencies,
2320 )?
2321 }
2322 }
2323 Schema::AllOf { all_of, .. } => {
2324 self.analyze_allof_composition(all_of, &mut dependencies)?
2326 }
2327 Schema::Untyped { .. } => {
2328 if let Some(inferred) = schema.inferred_type() {
2330 match inferred {
2331 OpenApiSchemaType::Object => {
2332 if self.should_use_dynamic_json(schema) {
2333 self.untyped_value(
2334 self.untyped_context(""),
2335 UntypedReason::OpaqueObject,
2336 )
2337 } else {
2338 self.analyze_object_schema(schema, &mut dependencies)?
2339 }
2340 }
2341 OpenApiSchemaType::String if details.is_string_enum() => {
2342 SchemaType::StringEnum {
2343 values: details.string_enum_values().unwrap_or_default(),
2344 }
2345 }
2346 OpenApiSchemaType::Null => SchemaType::Primitive {
2349 rust_type: self.type_mapper.null_unit().rust_type,
2350 serde_with: None,
2351 },
2352 _ => self.untyped_value(
2353 self.untyped_context(""),
2354 UntypedReason::UnsupportedTypeKeyword,
2355 ),
2356 }
2357 } else {
2358 self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema)
2359 }
2360 }
2361 };
2362
2363 Ok(AnalyzedSchema {
2364 name: schema_name.to_string(),
2365 original: serde_json::to_value(schema).unwrap_or(Value::Null), schema_type,
2367 dependencies,
2368 nullable,
2369 description,
2370 default: details.default.clone(),
2371 })
2372 }
2373
2374 fn analyze_single_typed_schema(
2380 &mut self,
2381 schema: &Schema,
2382 schema_name: &str,
2383 details: &crate::openapi::SchemaDetails,
2384 dependencies: &mut HashSet<String>,
2385 ) -> Result<SchemaType> {
2386 let primary = schema
2387 .schema_type()
2388 .cloned()
2389 .unwrap_or(OpenApiSchemaType::Object);
2390 let format = details.format.as_deref();
2391 Ok(match primary {
2392 OpenApiSchemaType::String => {
2393 if let Some(values) = details.string_enum_values() {
2394 SchemaType::StringEnum { values }
2395 } else {
2396 SchemaType::Primitive {
2397 rust_type: self.type_mapper.string_format(format).rust_type,
2398 serde_with: None,
2399 }
2400 }
2401 }
2402 OpenApiSchemaType::Integer => SchemaType::Primitive {
2403 rust_type: self.type_mapper.integer_format(format).rust_type,
2404 serde_with: None,
2405 },
2406 OpenApiSchemaType::Number => SchemaType::Primitive {
2407 rust_type: self.type_mapper.number_format(format).rust_type,
2408 serde_with: None,
2409 },
2410 OpenApiSchemaType::Boolean => SchemaType::Primitive {
2411 rust_type: self.type_mapper.boolean().rust_type,
2412 serde_with: None,
2413 },
2414 OpenApiSchemaType::Array => {
2415 self.analyze_array_schema(schema, schema_name, dependencies)?
2416 }
2417 OpenApiSchemaType::Object => {
2418 if self.should_use_dynamic_json(schema) {
2419 self.untyped_value(self.untyped_context(""), UntypedReason::OpaqueObject)
2420 } else {
2421 self.analyze_object_schema(schema, dependencies)?
2422 }
2423 }
2424 OpenApiSchemaType::Null => SchemaType::Primitive {
2427 rust_type: self.type_mapper.null_unit().rust_type,
2428 serde_with: None,
2429 },
2430 })
2431 }
2432
2433 fn analyze_object_schema(
2434 &mut self,
2435 schema: &Schema,
2436 dependencies: &mut HashSet<String>,
2437 ) -> Result<SchemaType> {
2438 let details = schema.details();
2439 let properties = &details.properties;
2440 let required = details
2441 .required
2442 .as_ref()
2443 .map(|req| req.iter().cloned().collect::<HashSet<String>>())
2444 .unwrap_or_default();
2445
2446 let mut property_info = BTreeMap::new();
2447 let owner_name = self
2450 .current_schema_name
2451 .clone()
2452 .unwrap_or_else(|| "Inline".to_string());
2453
2454 if let Some(props) = properties {
2455 for (prop_name, prop_schema) in props {
2456 let prop_type = if let Schema::AnyOf { any_of, .. } = prop_schema {
2458 if Self::union_only_constrains_requiredness(any_of) {
2463 self.analyze_empty_union(prop_schema, dependencies)?
2464 } else if let Some(with_variants) = self.analyze_object_with_variants(
2465 prop_schema,
2466 any_of,
2467 &format!("{owner_name}{}", self.to_pascal_case(prop_name)),
2468 dependencies,
2469 )? {
2470 with_variants
2471 } else if self.should_use_dynamic_json(prop_schema) {
2472 self.untyped_value(
2474 self.untyped_context(prop_name),
2475 UntypedReason::OpaqueObject,
2476 )
2477 } else if prop_schema.is_nullable_pattern()
2478 && let Some(non_null) = prop_schema.non_null_variant()
2479 {
2480 self.analyze_property_schema_with_context(
2488 non_null,
2489 Some(prop_name),
2490 dependencies,
2491 )?
2492 } else {
2493 let context_name = self
2496 .current_schema_name
2497 .clone()
2498 .unwrap_or_else(|| "Unknown".to_string());
2499
2500 let prop_pascal = self.to_pascal_case(prop_name);
2502 let mut union_type_name = format!("{context_name}{prop_pascal}");
2503
2504 if self.schemas.contains_key(&union_type_name)
2507 || self.resolved_cache.contains_key(&union_type_name)
2508 {
2509 let mut suffix = 2;
2510 loop {
2511 let candidate = format!("{union_type_name}Union{suffix}");
2512 if !self.schemas.contains_key(&candidate)
2513 && !self.resolved_cache.contains_key(&candidate)
2514 {
2515 union_type_name = candidate;
2516 break;
2517 }
2518 suffix += 1;
2519 if suffix > 1000 {
2520 break;
2521 }
2522 }
2523 }
2524
2525 let union_schema_type = self.analyze_anyof_union(
2527 any_of,
2528 prop_schema.discriminator(),
2529 dependencies,
2530 &union_type_name,
2531 )?;
2532
2533 self.resolved_cache.insert(
2535 union_type_name.clone(),
2536 AnalyzedSchema {
2537 name: union_type_name.clone(),
2538 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
2539 dependencies: schema_type_dependencies(&union_schema_type),
2540 schema_type: union_schema_type,
2541 nullable: false,
2542 description: prop_schema.details().description.clone(),
2543 default: None,
2544 },
2545 );
2546
2547 dependencies.insert(union_type_name.clone());
2549 SchemaType::Reference {
2550 target: union_type_name,
2551 }
2552 }
2553 } else if let Schema::OneOf {
2554 one_of,
2555 discriminator,
2556 ..
2557 } = prop_schema
2558 {
2559 if prop_schema.is_nullable_pattern()
2566 && let Some(non_null) = prop_schema.non_null_variant()
2567 {
2568 let unwrapped = self.analyze_property_schema_with_context(
2569 non_null,
2570 Some(prop_name),
2571 dependencies,
2572 )?;
2573 let owner_name = self
2574 .current_schema_name
2575 .clone()
2576 .unwrap_or_else(|| "Inline".to_string());
2577 let unwrapped = self.hoist_inline_property_type(
2578 &owner_name,
2579 prop_name,
2580 unwrapped,
2581 dependencies,
2582 );
2583 let prop_details = prop_schema.details();
2584 let prop_nullable = true;
2585 let prop_description = prop_details.description.clone();
2586 let prop_default = prop_details.default.clone();
2587 property_info.insert(
2588 prop_name.clone(),
2589 PropertyInfo {
2590 schema_type: unwrapped,
2591 nullable: prop_nullable,
2592 description: prop_description,
2593 default: prop_default,
2594 serde_attrs: Vec::new(),
2595 constraints: PropertyConstraints::from_schema_details(prop_details),
2596 },
2597 );
2598 continue;
2599 }
2600
2601 let context_name = self
2603 .current_schema_name
2604 .clone()
2605 .unwrap_or_else(|| "Unknown".to_string());
2606 let prop_pascal = self.to_pascal_case(prop_name);
2607 let mut union_type_name = format!("{context_name}{prop_pascal}");
2608 if self.schemas.contains_key(&union_type_name)
2610 || self.resolved_cache.contains_key(&union_type_name)
2611 {
2612 let mut suffix = 2;
2613 loop {
2614 let candidate = format!("{union_type_name}Union{suffix}");
2615 if !self.schemas.contains_key(&candidate)
2616 && !self.resolved_cache.contains_key(&candidate)
2617 {
2618 union_type_name = candidate;
2619 break;
2620 }
2621 suffix += 1;
2622 if suffix > 1000 {
2623 break;
2624 }
2625 }
2626 }
2627
2628 let union_schema_type = self.analyze_oneof_union(
2630 one_of,
2631 discriminator.as_ref(),
2632 &union_type_name,
2633 dependencies,
2634 )?;
2635
2636 self.resolved_cache.insert(
2638 union_type_name.clone(),
2639 AnalyzedSchema {
2640 name: union_type_name.clone(),
2641 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
2642 schema_type: union_schema_type,
2643 dependencies: HashSet::new(),
2644 nullable: false,
2645 description: prop_schema.details().description.clone(),
2646 default: None,
2647 },
2648 );
2649
2650 dependencies.insert(union_type_name.clone());
2652 SchemaType::Reference {
2653 target: union_type_name,
2654 }
2655 } else {
2656 self.analyze_property_schema_with_context(
2658 prop_schema,
2659 Some(prop_name),
2660 dependencies,
2661 )?
2662 };
2663
2664 let prop_type = self.hoist_inline_property_type(
2665 &owner_name,
2666 prop_name,
2667 prop_type,
2668 dependencies,
2669 );
2670
2671 let prop_details = prop_schema.details();
2672 let prop_nullable = prop_schema.is_nullable_any();
2674 let prop_description = prop_details.description.clone();
2675 let prop_default = prop_details.default.clone();
2676
2677 property_info.insert(
2678 prop_name.clone(),
2679 PropertyInfo {
2680 schema_type: prop_type,
2681 nullable: prop_nullable,
2682 description: prop_description,
2683 default: prop_default,
2684 serde_attrs: Vec::new(),
2685 constraints: PropertyConstraints::from_schema_details(prop_details),
2686 },
2687 );
2688 }
2689 }
2690
2691 let typed_enabled = self
2699 .type_mapper
2700 .config()
2701 .shape
2702 .as_ref()
2703 .and_then(|s| s.additional_properties_typed)
2704 .unwrap_or(true);
2705
2706 let additional_properties = match &details.additional_properties {
2707 Some(crate::openapi::AdditionalProperties::Boolean(true)) => {
2708 ObjectAdditionalProperties::Untyped
2709 }
2710 Some(crate::openapi::AdditionalProperties::Boolean(false)) => {
2711 ObjectAdditionalProperties::Forbidden
2712 }
2713 Some(crate::openapi::AdditionalProperties::Schema(value_schema)) if typed_enabled => {
2714 let analyzed =
2715 self.analyze_property_schema_with_context(value_schema, None, dependencies)?;
2716 ObjectAdditionalProperties::Typed {
2717 value_type: Box::new(analyzed),
2718 }
2719 }
2720 Some(crate::openapi::AdditionalProperties::Schema(_)) => {
2721 ObjectAdditionalProperties::Untyped
2723 }
2724 None => ObjectAdditionalProperties::Forbidden,
2725 };
2726
2727 Ok(SchemaType::Object {
2728 properties: property_info,
2729 variant: None,
2730 required,
2731 additional_properties,
2732 })
2733 }
2734
2735 fn build_typed_multi_union_variant(
2745 &mut self,
2746 member_type: OpenApiSchemaType,
2747 schema: &Schema,
2748 union_type_name: &str,
2749 dependencies: &mut HashSet<String>,
2750 ) -> Result<SchemaRef> {
2751 match member_type {
2752 OpenApiSchemaType::Array => {
2753 let array_type_name = format!("{union_type_name}Array");
2754 let array_type =
2755 self.analyze_array_schema(schema, &array_type_name, dependencies)?;
2756 self.resolved_cache.insert(
2757 array_type_name.clone(),
2758 AnalyzedSchema {
2759 name: array_type_name.clone(),
2760 original: serde_json::to_value(schema).unwrap_or(Value::Null),
2761 schema_type: array_type,
2762 dependencies: HashSet::new(),
2763 nullable: false,
2764 description: Some("Array variant in union".to_string()),
2765 default: None,
2766 },
2767 );
2768 dependencies.insert(array_type_name.clone());
2769 Ok(SchemaRef {
2770 target: array_type_name,
2771 nullable: false,
2772 })
2773 }
2774 OpenApiSchemaType::Object => {
2775 let object_type_name = format!("{union_type_name}Object");
2776 let object_type = self.analyze_object_schema(schema, dependencies)?;
2777 self.resolved_cache.insert(
2778 object_type_name.clone(),
2779 AnalyzedSchema {
2780 name: object_type_name.clone(),
2781 original: serde_json::to_value(schema).unwrap_or(Value::Null),
2782 schema_type: object_type,
2783 dependencies: dependencies.clone(),
2784 nullable: false,
2785 description: schema.details().description.clone(),
2786 default: None,
2787 },
2788 );
2789 dependencies.insert(object_type_name.clone());
2790 Ok(SchemaRef {
2791 target: object_type_name,
2792 nullable: false,
2793 })
2794 }
2795 _ => Ok(SchemaRef {
2796 target: self
2797 .type_mapper
2798 .map(member_type, schema.details())
2799 .rust_type,
2800 nullable: false,
2801 }),
2802 }
2803 }
2804
2805 fn analyze_property_schema_with_context(
2806 &mut self,
2807 schema: &Schema,
2808 property_name: Option<&str>,
2809 dependencies: &mut HashSet<String>,
2810 ) -> Result<SchemaType> {
2811 if let Some(ref_str) = self.get_any_reference(schema) {
2812 let target_opt = if ref_str == "#" {
2813 Some(
2814 self.find_recursive_anchor_schema()
2815 .unwrap_or_else(|| "UnknownRecursive".to_string()),
2816 )
2817 } else {
2818 self.extract_schema_name(ref_str).map(|s| s.to_string())
2819 };
2820 match target_opt {
2821 Some(target) => {
2822 dependencies.insert(target.clone());
2823 return Ok(SchemaType::Reference { target });
2824 }
2825 None => {
2826 if let Some(resolved) = self.resolve_pointer_schema(ref_str, dependencies)? {
2831 return Ok(resolved);
2832 }
2833 eprintln!(
2834 "⚠️ unresolvable $ref `{}` — typing as serde_json::Value",
2835 ref_str
2836 );
2837 return Ok(self.untyped_value(
2838 format!("$ref {ref_str}"),
2839 UntypedReason::UnresolvedReference,
2840 ));
2841 }
2842 }
2843 }
2844
2845 if let Some(non_null_types) = schema.non_null_schema_types() {
2849 let context_name = self
2850 .current_schema_name
2851 .clone()
2852 .unwrap_or_else(|| "Unknown".to_string());
2853 let prop_pascal = property_name
2854 .map(|name| self.to_pascal_case(name))
2855 .unwrap_or_default();
2856 let mut union_type_name = format!("{context_name}{prop_pascal}");
2857 if self.schemas.contains_key(&union_type_name)
2858 || self.resolved_cache.contains_key(&union_type_name)
2859 {
2860 let mut suffix = 2;
2861 loop {
2862 let candidate = format!("{union_type_name}Union{suffix}");
2863 if !self.schemas.contains_key(&candidate)
2864 && !self.resolved_cache.contains_key(&candidate)
2865 {
2866 union_type_name = candidate;
2867 break;
2868 }
2869 suffix += 1;
2870 if suffix > 1000 {
2871 break;
2872 }
2873 }
2874 }
2875
2876 let details = schema.details();
2877 let mut variants = Vec::with_capacity(non_null_types.len());
2878 for t in non_null_types {
2879 variants.push(self.build_typed_multi_union_variant(
2880 t,
2881 schema,
2882 &union_type_name,
2883 dependencies,
2884 )?);
2885 }
2886
2887 self.resolved_cache.insert(
2888 union_type_name.clone(),
2889 AnalyzedSchema {
2890 name: union_type_name.clone(),
2891 original: serde_json::to_value(schema).unwrap_or(Value::Null),
2892 schema_type: SchemaType::Union { variants },
2893 dependencies: HashSet::new(),
2894 nullable: false,
2895 description: details.description.clone(),
2896 default: None,
2897 },
2898 );
2899
2900 dependencies.insert(union_type_name.clone());
2901 return Ok(SchemaType::Reference {
2902 target: union_type_name,
2903 });
2904 }
2905
2906 if let Some(schema_type) = schema.schema_type() {
2907 match schema_type {
2908 OpenApiSchemaType::String => {
2909 if let Some(enum_values) = schema.details().string_enum_values() {
2911 let context_name = self
2914 .current_schema_name
2915 .clone()
2916 .unwrap_or_else(|| "Unknown".to_string());
2917
2918 let primary_name = if let Some(prop_name) = property_name {
2920 let prop_pascal = self.to_pascal_case(prop_name);
2922 format!("{context_name}{prop_pascal}")
2923 } else {
2924 let suffix = if !enum_values.is_empty() {
2927 let first_value = self.to_pascal_case(&enum_values[0]);
2928 format!("{first_value}Enum")
2929 } else {
2930 "StringEnum".to_string()
2931 };
2932 format!("{context_name}{suffix}")
2933 };
2934
2935 return Ok(self.hoist_inline_string_enum(
2936 schema,
2937 enum_values,
2938 primary_name,
2939 dependencies,
2940 ));
2941 } else {
2942 let mapped = self
2948 .type_mapper
2949 .string_format(schema.details().format.as_deref());
2950 return Ok(SchemaType::Primitive {
2951 rust_type: mapped.rust_type,
2952 serde_with: mapped.serde_with,
2953 });
2954 }
2955 }
2956 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
2957 let details = schema.details();
2958 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
2959 return Ok(SchemaType::Primitive {
2960 rust_type,
2961 serde_with: None,
2962 });
2963 }
2964 OpenApiSchemaType::Boolean => {
2965 return Ok(SchemaType::Primitive {
2966 rust_type: "bool".to_string(),
2967 serde_with: None,
2968 });
2969 }
2970 OpenApiSchemaType::Array => {
2971 let context_name = if let Some(prop_name) = property_name {
2973 let prop_pascal = self.to_pascal_case(prop_name);
2975 format!(
2976 "{}{}",
2977 self.current_schema_name.as_deref().unwrap_or("Unknown"),
2978 prop_pascal
2979 )
2980 } else {
2981 "ArrayItem".to_string()
2983 };
2984 return self.analyze_array_schema(schema, &context_name, dependencies);
2985 }
2986 OpenApiSchemaType::Object => {
2987 if self.should_use_dynamic_json(schema) {
2989 return Ok(self
2990 .untyped_value(self.untyped_context(""), UntypedReason::OpaqueObject));
2991 }
2992 let object_type_name = if let Some(prop_name) = property_name {
2994 let prop_pascal = self.to_pascal_case(prop_name);
2996 format!(
2997 "{}{}",
2998 self.current_schema_name.as_deref().unwrap_or("Unknown"),
2999 prop_pascal
3000 )
3001 } else {
3002 format!(
3004 "{}Object",
3005 self.current_schema_name.as_deref().unwrap_or("Unknown")
3006 )
3007 };
3008
3009 let object_type = self.analyze_object_schema(schema, dependencies)?;
3011
3012 let inline_schema = AnalyzedSchema {
3014 name: object_type_name.clone(),
3015 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3016 schema_type: object_type,
3017 dependencies: dependencies.clone(),
3018 nullable: false,
3019 description: schema.details().description.clone(),
3020 default: None,
3021 };
3022
3023 self.resolved_cache
3025 .insert(object_type_name.clone(), inline_schema);
3026 dependencies.insert(object_type_name.clone());
3027
3028 return Ok(SchemaType::Reference {
3030 target: object_type_name,
3031 });
3032 }
3033 OpenApiSchemaType::Null => {
3036 return Ok(SchemaType::Primitive {
3037 rust_type: self.type_mapper.null_unit().rust_type,
3038 serde_with: None,
3039 });
3040 }
3041 }
3042 }
3043
3044 if schema.is_nullable_pattern() {
3046 if let Some(non_null) = schema.non_null_variant() {
3047 return self.analyze_property_schema_with_context(
3048 non_null,
3049 property_name,
3050 dependencies,
3051 );
3052 }
3053 }
3054
3055 if self.should_use_dynamic_json(schema) {
3057 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::OpaqueObject));
3058 }
3059
3060 if let Schema::AllOf { all_of, .. } = schema {
3062 return self.analyze_allof_composition(all_of, dependencies);
3063 }
3064
3065 if let Some(variants) = schema.union_variants() {
3067 match variants.len().cmp(&1) {
3068 std::cmp::Ordering::Equal => {
3069 return self.analyze_property_schema_with_context(
3071 &variants[0],
3072 property_name,
3073 dependencies,
3074 );
3075 }
3076 std::cmp::Ordering::Greater => {
3077 let union_name = if let Some(prop_name) = property_name {
3080 let prop_pascal = self.to_pascal_case(prop_name);
3082 format!(
3083 "{}{}",
3084 self.current_schema_name.as_deref().unwrap_or(""),
3085 prop_pascal
3086 )
3087 } else {
3088 "UnionType".to_string()
3089 };
3090
3091 if let Schema::OneOf {
3093 one_of,
3094 discriminator,
3095 ..
3096 } = schema
3097 {
3098 let oneof_result = self.analyze_oneof_union(
3100 one_of,
3101 discriminator.as_ref(),
3102 &union_name,
3103 dependencies,
3104 )?;
3105
3106 if let SchemaType::Union {
3108 variants: _union_variants,
3109 } = &oneof_result
3110 {
3111 self.resolved_cache.insert(
3113 union_name.clone(),
3114 AnalyzedSchema {
3115 name: union_name.clone(),
3116 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3117 schema_type: oneof_result.clone(),
3118 dependencies: dependencies.clone(),
3119 nullable: false,
3120 description: schema.details().description.clone(),
3121 default: None,
3122 },
3123 );
3124
3125 dependencies.insert(union_name.clone());
3127 return Ok(SchemaType::Reference { target: union_name });
3128 }
3129
3130 return Ok(oneof_result);
3131 } else if let Schema::AnyOf {
3132 any_of,
3133 discriminator,
3134 ..
3135 } = schema
3136 {
3137 let union_analysis = self.analyze_anyof_union(
3139 any_of,
3140 discriminator.as_ref(),
3141 dependencies,
3142 &union_name,
3143 )?;
3144 return Ok(union_analysis);
3145 } else {
3146 let mut union_variants = Vec::new();
3149 for variant in variants {
3150 if let Some(ref_str) = variant.reference() {
3151 if let Some(target) = self.extract_schema_name(ref_str) {
3152 dependencies.insert(target.to_string());
3153 union_variants.push(SchemaRef {
3154 target: target.to_string(),
3155 nullable: false,
3156 });
3157 }
3158 }
3159 }
3160 return Ok(SchemaType::Union {
3161 variants: union_variants,
3162 });
3163 }
3164 }
3165 std::cmp::Ordering::Less => {}
3166 }
3167 }
3168
3169 if let Some(inferred_type) = schema.inferred_type() {
3171 match inferred_type {
3172 OpenApiSchemaType::Object => {
3173 if self.should_use_dynamic_json(schema) {
3175 return Ok(self
3176 .untyped_value(self.untyped_context(""), UntypedReason::OpaqueObject));
3177 }
3178 return self.analyze_object_schema(schema, dependencies);
3179 }
3180 OpenApiSchemaType::Array => {
3181 let context_name = if let Some(prop_name) = property_name {
3182 let prop_pascal = self.to_pascal_case(prop_name);
3184 format!(
3185 "{}{}",
3186 self.current_schema_name.as_deref().unwrap_or("Unknown"),
3187 prop_pascal
3188 )
3189 } else {
3190 "ArrayItem".to_string()
3192 };
3193 return self.analyze_array_schema(schema, &context_name, dependencies);
3194 }
3195 OpenApiSchemaType::String => {
3196 if let Some(enum_values) = schema.details().string_enum_values() {
3197 return Ok(SchemaType::StringEnum {
3198 values: enum_values,
3199 });
3200 } else {
3201 return Ok(SchemaType::Primitive {
3202 rust_type: "String".to_string(),
3203 serde_with: None,
3204 });
3205 }
3206 }
3207 _ => {
3208 let rust_type = self.openapi_type_to_rust_type(inferred_type, schema.details());
3210 return Ok(SchemaType::Primitive {
3211 rust_type,
3212 serde_with: None,
3213 });
3214 }
3215 }
3216 }
3217
3218 Ok(self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema))
3219 }
3220
3221 fn analyze_allof_composition(
3222 &mut self,
3223 all_of_schemas: &[Schema],
3224 dependencies: &mut HashSet<String>,
3225 ) -> Result<SchemaType> {
3226 let referenced_targets = all_of_schemas
3231 .iter()
3232 .filter_map(|schema| schema.reference())
3233 .filter_map(|reference| self.extract_schema_name(reference))
3234 .collect::<Vec<_>>();
3235 let only_reference_and_annotations = all_of_schemas.iter().all(|schema| {
3236 if schema.reference().is_some() {
3237 return true;
3238 }
3239 serde_json::to_value(schema)
3240 .ok()
3241 .and_then(|value| value.as_object().cloned())
3242 .is_some_and(|object| {
3243 object.keys().all(|key| {
3244 matches!(
3245 key.as_str(),
3246 "title"
3247 | "description"
3248 | "deprecated"
3249 | "readOnly"
3250 | "writeOnly"
3251 | "examples"
3252 | "example"
3253 | "externalDocs"
3254 | "xml"
3255 | "$comment"
3256 ) || key.starts_with("x-")
3257 })
3258 })
3259 });
3260 if referenced_targets.len() == 1 && only_reference_and_annotations {
3261 let target = referenced_targets[0];
3262 dependencies.insert(target.to_string());
3263 return Ok(SchemaType::Reference {
3264 target: target.to_string(),
3265 });
3266 }
3267
3268 if let [only] = all_of_schemas
3272 && !matches!(
3273 only.schema_type(),
3274 Some(OpenApiSchemaType::Object) | Some(OpenApiSchemaType::Null)
3275 )
3276 && only.details().properties.is_none()
3277 {
3278 return self.analyze_property_schema_with_context(only, None, dependencies);
3279 }
3280
3281 let mut merged_properties = BTreeMap::new();
3283 let mut merged_required = HashSet::new();
3284 let mut descriptions = Vec::new();
3285
3286 let current_context = self.current_schema_name.clone();
3288
3289 for schema in all_of_schemas {
3290 match schema {
3291 Schema::Reference { reference, .. } => {
3292 if let Some(target) = self.extract_schema_name(reference) {
3294 dependencies.insert(target.to_string());
3295
3296 let analyzed_ref = self.analyze_schema(target)?;
3298
3299 match &analyzed_ref.schema_type {
3301 SchemaType::Object {
3302 properties,
3303 required,
3304 ..
3305 } => {
3306 for (prop_name, prop_info) in properties {
3308 merged_properties.insert(prop_name.clone(), prop_info.clone());
3309 }
3310 for req in required {
3312 merged_required.insert(req.clone());
3313 }
3314 }
3315 _ => {
3316 if let Some(ref_schema) = self.schemas.get(target).cloned() {
3318 self.merge_schema_into_properties(
3319 &ref_schema,
3320 &mut merged_properties,
3321 &mut merged_required,
3322 dependencies,
3323 )?;
3324 }
3325 }
3326 }
3327 }
3328 }
3329 Schema::Typed {
3330 schema_type: OpenApiSchemaType::Object,
3331 ..
3332 }
3333 | Schema::Untyped { .. } => {
3334 let saved_context = self.current_schema_name.clone();
3336 self.current_schema_name = current_context.clone();
3337
3338 self.merge_schema_into_properties(
3340 schema,
3341 &mut merged_properties,
3342 &mut merged_required,
3343 dependencies,
3344 )?;
3345
3346 self.current_schema_name = saved_context;
3348 }
3349 _ => {
3350 self.merge_schema_into_properties(
3353 schema,
3354 &mut merged_properties,
3355 &mut merged_required,
3356 dependencies,
3357 )?;
3358 }
3359 }
3360
3361 if let Some(desc) = &schema.details().description {
3363 descriptions.push(desc.clone());
3364 }
3365 }
3366
3367 if !merged_properties.is_empty() {
3369 Ok(SchemaType::Object {
3370 properties: merged_properties,
3371 required: merged_required,
3372 additional_properties: ObjectAdditionalProperties::Forbidden,
3373 variant: None,
3374 })
3375 } else {
3376 Ok(SchemaType::Composition {
3378 schemas: all_of_schemas
3379 .iter()
3380 .filter_map(|s| {
3381 if let Some(ref_str) = s.reference() {
3382 if let Some(target) = self.extract_schema_name(ref_str) {
3383 dependencies.insert(target.to_string());
3384 Some(SchemaRef {
3385 target: target.to_string(),
3386 nullable: false,
3387 })
3388 } else {
3389 None
3390 }
3391 } else {
3392 None
3393 }
3394 })
3395 .collect(),
3396 })
3397 }
3398 }
3399
3400 fn merge_schema_into_properties(
3401 &mut self,
3402 schema: &Schema,
3403 merged_properties: &mut BTreeMap<String, PropertyInfo>,
3404 merged_required: &mut HashSet<String>,
3405 dependencies: &mut HashSet<String>,
3406 ) -> Result<()> {
3407 let details = schema.details();
3408
3409 if let Some(properties) = &details.properties {
3411 for (prop_name, prop_schema) in properties {
3412 let prop_type = self.analyze_property_schema_with_context(
3413 prop_schema,
3414 Some(prop_name),
3415 dependencies,
3416 )?;
3417 let owner_name = self
3418 .current_schema_name
3419 .clone()
3420 .unwrap_or_else(|| "Inline".to_string());
3421 let prop_type = self.hoist_inline_property_type(
3422 &owner_name,
3423 prop_name,
3424 prop_type,
3425 dependencies,
3426 );
3427 let prop_details = prop_schema.details();
3428
3429 let nullable = prop_schema.is_nullable_any();
3436 merged_properties.insert(
3437 prop_name.clone(),
3438 PropertyInfo {
3439 schema_type: prop_type,
3440 nullable,
3441 description: prop_details.description.clone(),
3442 default: prop_details.default.clone(),
3443 serde_attrs: Vec::new(),
3444 constraints: PropertyConstraints::from_schema_details(prop_details),
3445 },
3446 );
3447 }
3448 }
3449
3450 if let Some(required) = &details.required {
3452 for field in required {
3453 merged_required.insert(field.clone());
3454 }
3455 }
3456
3457 Ok(())
3458 }
3459
3460 fn analyze_oneof_union(
3461 &mut self,
3462 one_of_schemas: &[Schema],
3463 discriminator: Option<&crate::openapi::Discriminator>,
3464 parent_name: &str,
3465 dependencies: &mut HashSet<String>,
3466 ) -> Result<SchemaType> {
3467 let expanded_branches;
3469 let one_of_schemas = match self.expand_pointer_branches(one_of_schemas) {
3470 Some(expanded) => {
3471 expanded_branches = expanded;
3472 expanded_branches.as_slice()
3473 }
3474 None => one_of_schemas,
3475 };
3476
3477 if let [only] = one_of_schemas {
3482 return self
3483 .analyze_schema_value(only, parent_name)
3484 .map(|analyzed| analyzed.schema_type);
3485 }
3486 if let Some(shared) = self.shared_branch_type(one_of_schemas) {
3487 return Ok(shared);
3488 }
3489
3490 if one_of_schemas.len() == 2 {
3493 let null_count = one_of_schemas
3494 .iter()
3495 .filter(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
3496 .count();
3497 if null_count == 1 {
3498 if let Some(non_null) = one_of_schemas
3499 .iter()
3500 .find(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
3501 {
3502 return self
3503 .analyze_schema_value(non_null, parent_name)
3504 .map(|a| a.schema_type);
3505 }
3506 }
3507 }
3508
3509 if discriminator.is_none() {
3511 return self.analyze_untagged_oneof_union(one_of_schemas, parent_name, dependencies);
3513 }
3514
3515 if one_of_schemas
3521 .iter()
3522 .any(|s| !self.branch_resolves_to_object(s))
3523 {
3524 return self.analyze_untagged_oneof_union(one_of_schemas, parent_name, dependencies);
3525 }
3526
3527 let discriminator_field = discriminator
3529 .ok_or_else(|| {
3530 GeneratorError::InvalidDiscriminator(
3531 "expected discriminator after guard check".to_string(),
3532 )
3533 })?
3534 .property_name
3535 .clone();
3536
3537 let mut variants = Vec::new();
3538 let mut used_variant_names = std::collections::HashSet::new();
3539
3540 for variant_schema in one_of_schemas {
3541 let ref_info = if let Some(ref_str) = variant_schema.reference() {
3543 Some((ref_str, false))
3544 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
3545 Some((recursive_ref, true))
3546 } else if let Schema::AllOf { all_of, .. } = variant_schema {
3547 if all_of.len() == 1 {
3549 if let Some(ref_str) = all_of[0].reference() {
3550 Some((ref_str, false))
3551 } else {
3552 all_of[0]
3553 .recursive_reference()
3554 .map(|recursive_ref| (recursive_ref, true))
3555 }
3556 } else {
3557 None
3558 }
3559 } else {
3560 None
3561 };
3562
3563 if let Some((ref_str, is_recursive)) = ref_info {
3564 let schema_name = if is_recursive && ref_str == "#" {
3565 self.find_recursive_anchor_schema()
3567 .or_else(|| self.current_schema_name.clone())
3568 .unwrap_or_else(|| "CompoundFilter".to_string())
3569 } else {
3570 self.extract_schema_name(ref_str)
3571 .map(|s| s.to_string())
3572 .unwrap_or_else(|| "UnknownRef".to_string())
3573 };
3574
3575 if !schema_name.is_empty() {
3576 dependencies.insert(schema_name.clone());
3577
3578 let discriminator_value = if let Some(disc) = discriminator {
3583 if let Some(mappings) = &disc.mapping {
3584 mappings
3587 .iter()
3588 .find(|(_, target_ref)| {
3589 target_ref.as_str() == ref_str
3591 || self
3592 .extract_schema_name(target_ref)
3593 .map(|s| s.to_string())
3594 == Some(schema_name.clone())
3595 })
3596 .map(|(key, _)| key.clone())
3597 .unwrap_or_else(|| {
3598 self.fallback_discriminator_value_for_field(
3599 &schema_name,
3600 &discriminator_field,
3601 )
3602 })
3603 } else {
3604 self.fallback_discriminator_value_for_field(
3605 &schema_name,
3606 &discriminator_field,
3607 )
3608 }
3609 } else {
3610 self.fallback_discriminator_value_for_field(
3611 &schema_name,
3612 &discriminator_field,
3613 )
3614 };
3615
3616 let base_name = self.to_rust_variant_name(&schema_name);
3618 let rust_name =
3619 self.ensure_unique_variant_name(base_name, &mut used_variant_names);
3620
3621 let final_discriminator_value = discriminator_value;
3623
3624 variants.push(UnionVariant {
3625 rust_name,
3626 type_name: schema_name,
3627 discriminator_value: final_discriminator_value,
3628 schema_ref: ref_str.to_string(),
3629 });
3630 }
3631 } else {
3632 let variant_index = variants.len();
3634 let inline_type_name =
3635 self.generate_inline_type_name(variant_schema, variant_index);
3636
3637 let discriminator_value = if let Some(disc) = discriminator {
3639 if let Some(mappings) = &disc.mapping {
3640 mappings
3642 .iter()
3643 .find(|(_, target_ref)| {
3644 target_ref.contains(&format!("variant_{variant_index}"))
3645 })
3646 .map(|(key, _)| key.clone())
3647 .unwrap_or_else(|| {
3648 self.extract_inline_discriminator_value(
3649 variant_schema,
3650 &discriminator_field,
3651 variant_index,
3652 )
3653 })
3654 } else {
3655 self.extract_inline_discriminator_value(
3656 variant_schema,
3657 &discriminator_field,
3658 variant_index,
3659 )
3660 }
3661 } else {
3662 self.extract_inline_discriminator_value(
3663 variant_schema,
3664 &discriminator_field,
3665 variant_index,
3666 )
3667 };
3668
3669 let base_name = if discriminator_value.starts_with("variant_") {
3671 format!("Variant{variant_index}")
3672 } else {
3673 let clean_name = self.discriminator_to_variant_name(&discriminator_value);
3675 self.to_rust_variant_name(&clean_name)
3676 };
3677 let rust_name = self.ensure_unique_variant_name(base_name, &mut used_variant_names);
3678
3679 let final_discriminator_value = discriminator_value;
3681
3682 variants.push(UnionVariant {
3683 rust_name,
3684 type_name: inline_type_name.clone(),
3685 discriminator_value: final_discriminator_value,
3686 schema_ref: format!("inline_{variant_index}"),
3687 });
3688
3689 self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
3691 }
3692 }
3693
3694 if variants.is_empty() {
3695 let mut union_variants = Vec::new();
3698
3699 for (variant_index, variant_schema) in one_of_schemas.iter().enumerate() {
3700 if let Some(ref_str) = variant_schema.reference() {
3702 if let Some(schema_name) = self.extract_schema_name(ref_str) {
3703 dependencies.insert(schema_name.to_string());
3704 union_variants.push(SchemaRef {
3705 target: schema_name.to_string(),
3706 nullable: false,
3707 });
3708 }
3709 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
3710 let schema_name = if recursive_ref == "#" {
3711 self.find_recursive_anchor_schema()
3713 .or_else(|| self.current_schema_name.clone())
3714 .unwrap_or_else(|| "CompoundFilter".to_string())
3715 } else {
3716 self.extract_schema_name(recursive_ref)
3717 .map(|s| s.to_string())
3718 .unwrap_or_else(|| "RecursiveType".to_string())
3719 };
3720 dependencies.insert(schema_name.clone());
3721 union_variants.push(SchemaRef {
3722 target: schema_name,
3723 nullable: false,
3724 });
3725 } else {
3726 let inline_name = self.generate_context_aware_name(
3728 parent_name,
3729 "InlineVariant",
3730 variant_index,
3731 Some(variant_schema),
3732 );
3733 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
3734 let variant_type = analyzed.schema_type;
3735
3736 for dep in &analyzed.dependencies {
3738 dependencies.insert(dep.clone());
3739 }
3740
3741 match &variant_type {
3742 SchemaType::Primitive { rust_type, .. } => {
3744 union_variants.push(SchemaRef {
3745 target: rust_type.clone(),
3746 nullable: false,
3747 });
3748 }
3749 SchemaType::Array { item_type } => {
3751 match item_type.as_ref() {
3752 SchemaType::Primitive { rust_type, .. } => {
3753 let type_name = format!("Vec<{rust_type}>");
3754 union_variants.push(SchemaRef {
3755 target: type_name,
3756 nullable: false,
3757 });
3758 }
3759 SchemaType::Reference { target } => {
3760 let type_name = format!("Vec<{target}>");
3761 union_variants.push(SchemaRef {
3762 target: type_name,
3763 nullable: false,
3764 });
3765 }
3766 _ => {
3767 let inline_type_name = self.generate_context_aware_name(
3769 parent_name,
3770 "Variant",
3771 variant_index,
3772 None,
3773 );
3774 self.add_inline_schema(
3775 &inline_type_name,
3776 variant_schema,
3777 dependencies,
3778 )?;
3779 union_variants.push(SchemaRef {
3780 target: inline_type_name,
3781 nullable: false,
3782 });
3783 }
3784 }
3785 }
3786 SchemaType::Reference { target } => {
3788 union_variants.push(SchemaRef {
3789 target: target.clone(),
3790 nullable: false,
3791 });
3792 }
3793 _ => {
3795 let inline_type_name =
3796 format!("{}Variant{}", parent_name, variant_index + 1);
3797 self.add_inline_schema(
3798 &inline_type_name,
3799 variant_schema,
3800 dependencies,
3801 )?;
3802 union_variants.push(SchemaRef {
3803 target: inline_type_name,
3804 nullable: false,
3805 });
3806 }
3807 }
3808 }
3809 }
3810
3811 if !union_variants.is_empty() {
3812 return Ok(SchemaType::Union {
3813 variants: union_variants,
3814 });
3815 }
3816
3817 return Ok(self.untyped_value(
3819 self.untyped_context(""),
3820 UntypedReason::UnrepresentableUnion,
3821 ));
3822 }
3823
3824 Ok(SchemaType::DiscriminatedUnion {
3825 discriminator_field,
3826 variants,
3827 })
3828 }
3829
3830 fn analyze_untagged_oneof_union(
3831 &mut self,
3832 one_of_schemas: &[Schema],
3833 parent_name: &str,
3834 dependencies: &mut HashSet<String>,
3835 ) -> Result<SchemaType> {
3836 let filtered: Vec<&Schema> = one_of_schemas
3840 .iter()
3841 .filter(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
3842 .collect();
3843
3844 if filtered.len() == 1 {
3846 return self
3847 .analyze_schema_value(filtered[0], parent_name)
3848 .map(|a| a.schema_type);
3849 }
3850
3851 let mut union_variants = Vec::new();
3852
3853 for (variant_index, variant_schema) in filtered.iter().copied().enumerate() {
3854 if let Some(ref_str) = variant_schema.reference() {
3856 if let Some(schema_name) = self.extract_schema_name(ref_str) {
3857 dependencies.insert(schema_name.to_string());
3858 union_variants.push(SchemaRef {
3859 target: schema_name.to_string(),
3860 nullable: false,
3861 });
3862 }
3863 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
3864 let schema_name = if recursive_ref == "#" {
3865 self.find_recursive_anchor_schema()
3867 .or_else(|| self.current_schema_name.clone())
3868 .unwrap_or_else(|| "CompoundFilter".to_string())
3869 } else {
3870 self.extract_schema_name(recursive_ref)
3871 .map(|s| s.to_string())
3872 .unwrap_or_else(|| "RecursiveType".to_string())
3873 };
3874 dependencies.insert(schema_name.clone());
3875 union_variants.push(SchemaRef {
3876 target: schema_name,
3877 nullable: false,
3878 });
3879 } else {
3880 let inline_name = self.generate_context_aware_name(
3882 parent_name,
3883 "InlineVariant",
3884 variant_index,
3885 Some(variant_schema),
3886 );
3887 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
3888 let variant_type = analyzed.schema_type;
3889
3890 for dep in &analyzed.dependencies {
3892 dependencies.insert(dep.clone());
3893 }
3894
3895 match &variant_type {
3896 SchemaType::Primitive { rust_type, .. } => {
3898 union_variants.push(SchemaRef {
3899 target: rust_type.clone(),
3900 nullable: false,
3901 });
3902 }
3903 SchemaType::Array { item_type } => {
3905 match item_type.as_ref() {
3906 SchemaType::Primitive { rust_type, .. } => {
3907 let type_name = format!("Vec<{rust_type}>");
3908 union_variants.push(SchemaRef {
3909 target: type_name,
3910 nullable: false,
3911 });
3912 }
3913 SchemaType::Reference { target } => {
3914 let type_name = format!("Vec<{target}>");
3915 union_variants.push(SchemaRef {
3916 target: type_name,
3917 nullable: false,
3918 });
3919 }
3920 SchemaType::Array {
3922 item_type: inner_item_type,
3923 } => {
3924 match inner_item_type.as_ref() {
3925 SchemaType::Primitive { rust_type, .. } => {
3926 let type_name = format!("Vec<Vec<{rust_type}>>");
3927 union_variants.push(SchemaRef {
3928 target: type_name,
3929 nullable: false,
3930 });
3931 }
3932 SchemaType::Reference { target } => {
3933 let type_name = format!("Vec<Vec<{target}>>");
3934 union_variants.push(SchemaRef {
3935 target: type_name,
3936 nullable: false,
3937 });
3938 }
3939 _ => {
3940 let inline_type_name = self.generate_context_aware_name(
3942 parent_name,
3943 "Variant",
3944 variant_index,
3945 None,
3946 );
3947 self.add_inline_schema(
3948 &inline_type_name,
3949 variant_schema,
3950 dependencies,
3951 )?;
3952 union_variants.push(SchemaRef {
3953 target: inline_type_name,
3954 nullable: false,
3955 });
3956 }
3957 }
3958 }
3959 _ => {
3960 let inline_type_name = self.generate_context_aware_name(
3962 parent_name,
3963 "Variant",
3964 variant_index,
3965 None,
3966 );
3967 self.add_inline_schema(
3968 &inline_type_name,
3969 variant_schema,
3970 dependencies,
3971 )?;
3972 union_variants.push(SchemaRef {
3973 target: inline_type_name,
3974 nullable: false,
3975 });
3976 }
3977 }
3978 }
3979 SchemaType::Reference { target } => {
3981 union_variants.push(SchemaRef {
3982 target: target.clone(),
3983 nullable: false,
3984 });
3985 }
3986 _ => {
3988 let inline_type_name = self.generate_context_aware_name(
3989 parent_name,
3990 "Variant",
3991 variant_index,
3992 None,
3993 );
3994 self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
3995 union_variants.push(SchemaRef {
3996 target: inline_type_name,
3997 nullable: false,
3998 });
3999 }
4000 }
4001 }
4002 }
4003
4004 if !union_variants.is_empty() {
4005 return Ok(SchemaType::Union {
4006 variants: union_variants,
4007 });
4008 }
4009
4010 Ok(self.untyped_value(
4012 self.untyped_context(""),
4013 UntypedReason::UnrepresentableUnion,
4014 ))
4015 }
4016
4017 fn add_inline_schema(
4018 &mut self,
4019 type_name: &str,
4020 schema: &Schema,
4021 dependencies: &mut HashSet<String>,
4022 ) -> Result<()> {
4023 if let Some(schema_type) = schema.schema_type() {
4025 match schema_type {
4026 OpenApiSchemaType::String
4027 | OpenApiSchemaType::Integer
4028 | OpenApiSchemaType::Number
4029 | OpenApiSchemaType::Boolean => {
4030 let rust_type =
4031 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
4032
4033 self.resolved_cache.insert(
4035 type_name.to_string(),
4036 AnalyzedSchema {
4037 name: type_name.to_string(),
4038 original: serde_json::to_value(schema).unwrap_or(Value::Null),
4039 schema_type: SchemaType::Primitive {
4040 rust_type,
4041 serde_with: None,
4042 },
4043 dependencies: HashSet::new(),
4044 nullable: false,
4045 description: schema.details().description.clone(),
4046 default: None,
4047 },
4048 );
4049 return Ok(());
4050 }
4051 _ => {}
4052 }
4053 }
4054
4055 let previous_schema_name = self.current_schema_name.take();
4059 self.current_schema_name = Some(type_name.to_string());
4060 let analyzed = self.analyze_schema_value(schema, type_name)?;
4061 self.current_schema_name = previous_schema_name;
4062
4063 self.resolved_cache.insert(type_name.to_string(), analyzed);
4065
4066 if let Some(cached) = self.resolved_cache.get(type_name) {
4068 for dep in &cached.dependencies {
4069 dependencies.insert(dep.clone());
4070 }
4071 }
4072
4073 Ok(())
4074 }
4075
4076 fn extract_inline_discriminator_value(
4077 &self,
4078 schema: &Schema,
4079 discriminator_field: &str,
4080 variant_index: usize,
4081 ) -> String {
4082 if let Some(properties) = &schema.details().properties {
4084 if let Some(discriminator_prop) = properties.get(discriminator_field) {
4085 if let Some(enum_values) = &discriminator_prop.details().enum_values {
4087 if enum_values.len() == 1 {
4088 if let Some(value) = enum_values[0].as_str() {
4089 return value.to_string();
4090 }
4091 }
4092 }
4093 if let Some(const_value) = discriminator_prop.details().extra.get("const") {
4095 if let Some(value) = const_value.as_str() {
4096 return value.to_string();
4097 }
4098 }
4099 if let Some(const_value) = &discriminator_prop.details().const_value {
4101 if let Some(value) = const_value.as_str() {
4102 return value.to_string();
4103 }
4104 }
4105 }
4106 }
4107
4108 if let Some(inferred_name) = self.infer_variant_name_from_structure(schema, variant_index) {
4110 return inferred_name;
4111 }
4112
4113 format!("variant_{variant_index}")
4115 }
4116
4117 fn infer_variant_name_from_structure(
4118 &self,
4119 schema: &Schema,
4120 _variant_index: usize,
4121 ) -> Option<String> {
4122 let details = schema.details();
4123
4124 if let Some(properties) = &details.properties {
4126 if properties.contains_key("text") && properties.len() <= 3 {
4128 return Some("text".to_string());
4129 }
4130 if properties.contains_key("image") || properties.contains_key("source") {
4131 return Some("image".to_string());
4132 }
4133 if properties.contains_key("document") {
4134 return Some("document".to_string());
4135 }
4136 if properties.contains_key("tool_use_id") || properties.contains_key("tool_result") {
4137 return Some("tool_result".to_string());
4138 }
4139 if properties.contains_key("content") && properties.contains_key("is_error") {
4140 return Some("tool_result".to_string());
4141 }
4142 if properties.contains_key("partial_json") {
4143 return Some("partial_json".to_string());
4144 }
4145
4146 let property_names: Vec<&String> = properties.keys().collect();
4148
4149 for prop_name in &property_names {
4151 if prop_name.contains("result") {
4152 return Some("result".to_string());
4153 }
4154 if prop_name.contains("error") {
4155 return Some("error".to_string());
4156 }
4157 if prop_name.contains("content") && property_names.len() <= 2 {
4158 return Some("content".to_string());
4159 }
4160 }
4161
4162 let significant_props = property_names
4164 .iter()
4165 .filter(|&name| !["type", "id", "cache_control"].contains(&name.as_str()))
4166 .collect::<Vec<_>>();
4167
4168 if significant_props.len() == 1 {
4169 return Some((*significant_props[0]).clone());
4170 }
4171 }
4172
4173 if let Some(description) = &details.description {
4175 let desc_lower = description.to_lowercase();
4176 if desc_lower.contains("text") && desc_lower.len() < 100 {
4177 return Some("text".to_string());
4178 }
4179 if desc_lower.contains("image") {
4180 return Some("image".to_string());
4181 }
4182 if desc_lower.contains("document") {
4183 return Some("document".to_string());
4184 }
4185 if desc_lower.contains("tool") && desc_lower.contains("result") {
4186 return Some("tool_result".to_string());
4187 }
4188 }
4189
4190 None
4191 }
4192
4193 fn discriminator_to_variant_name(&self, discriminator: &str) -> String {
4194 if discriminator.is_empty() {
4196 return "Variant".to_string();
4197 }
4198
4199 let mut result = String::new();
4200 let mut next_upper = true;
4201
4202 for c in discriminator.chars() {
4203 match c {
4204 'a'..='z' => {
4205 if next_upper {
4206 result.push(c.to_ascii_uppercase());
4207 next_upper = false;
4208 } else {
4209 result.push(c);
4210 }
4211 }
4212 'A'..='Z' => {
4213 result.push(c);
4214 next_upper = false;
4215 }
4216 '0'..='9' => {
4217 result.push(c);
4218 next_upper = false;
4219 }
4220 '_' | '-' | '.' | ' ' | '/' | '\\' => {
4221 next_upper = true;
4223 }
4224 _ => {
4225 next_upper = true;
4227 }
4228 }
4229 }
4230
4231 if result.is_empty() || result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
4233 result = format!("Variant{result}");
4234 }
4235
4236 result
4237 }
4238
4239 fn ensure_unique_variant_name(
4240 &self,
4241 base_name: String,
4242 used_names: &mut std::collections::HashSet<String>,
4243 ) -> String {
4244 let mut candidate = base_name.clone();
4245 let mut counter = 1;
4246
4247 while used_names.contains(&candidate) {
4248 counter += 1;
4249 candidate = format!("{base_name}{counter}");
4250 }
4251
4252 used_names.insert(candidate.clone());
4253 candidate
4254 }
4255
4256 fn generate_inline_type_name(&self, schema: &Schema, variant_index: usize) -> String {
4257 if let Some(meaningful_name) = self.infer_type_name_from_structure(schema) {
4259 return meaningful_name;
4260 }
4261
4262 let context = self.current_schema_name.as_deref().unwrap_or("Inline");
4264 self.generate_context_aware_name(context, "Variant", variant_index, Some(schema))
4265 }
4266
4267 fn infer_type_name_from_structure(&self, schema: &Schema) -> Option<String> {
4268 let details = schema.details();
4269
4270 if let Some(description) = &details.description {
4272 if let Some(name_from_desc) = self.extract_type_name_from_description(description) {
4273 return Some(name_from_desc);
4274 }
4275 }
4276
4277 if let Some(properties) = &details.properties {
4279 if let Some(name_from_props) = self.extract_type_name_from_properties(properties) {
4280 return Some(format!("{name_from_props}Block"));
4281 }
4282 }
4283
4284 None
4285 }
4286
4287 fn extract_type_name_from_description(&self, description: &str) -> Option<String> {
4288 if description.len() > 100 || description.contains('\n') {
4290 return None;
4291 }
4292
4293 let words: Vec<&str> = description
4295 .split_whitespace()
4296 .take(2) .filter(|word| {
4298 let w = word.to_lowercase();
4299 word.len() > 2
4300 && ![
4301 "the", "and", "for", "with", "that", "this", "are", "can", "will", "was",
4302 ]
4303 .contains(&w.as_str())
4304 })
4305 .collect();
4306
4307 if words.is_empty() {
4308 return None;
4309 }
4310
4311 let combined = words.join("_");
4313 let pascal_name = self.discriminator_to_variant_name(&combined);
4314
4315 if !pascal_name.ends_with("Content")
4317 && !pascal_name.ends_with("Block")
4318 && !pascal_name.ends_with("Type")
4319 {
4320 Some(format!("{pascal_name}Content"))
4321 } else {
4322 Some(pascal_name)
4323 }
4324 }
4325
4326 fn extract_type_name_from_properties(
4327 &self,
4328 properties: &std::collections::BTreeMap<String, crate::openapi::Schema>,
4329 ) -> Option<String> {
4330 let significant_props: Vec<&String> = properties
4332 .keys()
4333 .filter(|name| !["type", "id", "cache_control"].contains(&name.as_str()))
4334 .collect();
4335
4336 if significant_props.is_empty() {
4337 return None;
4338 }
4339
4340 if significant_props.len() == 1 {
4342 let prop_name = significant_props[0];
4343 return Some(self.discriminator_to_variant_name(prop_name));
4344 }
4345
4346 let mut sorted_props = significant_props.clone();
4349 sorted_props.sort();
4350 if let Some(first_prop) = sorted_props.first() {
4351 return Some(self.discriminator_to_variant_name(first_prop));
4352 }
4353
4354 None
4355 }
4356
4357 fn openapi_type_to_rust_type(
4358 &self,
4359 openapi_type: OpenApiSchemaType,
4360 details: &crate::openapi::SchemaDetails,
4361 ) -> String {
4362 self.type_mapper.map(openapi_type, details).rust_type
4367 }
4368
4369 #[allow(dead_code)]
4370 fn fallback_discriminator_value(&self, schema_name: &str) -> String {
4371 self.fallback_discriminator_value_for_field(schema_name, "type")
4372 }
4373
4374 fn fallback_discriminator_value_for_field(
4375 &self,
4376 schema_name: &str,
4377 field_name: &str,
4378 ) -> String {
4379 if let Some(ref_schema) = self.schemas.get(schema_name) {
4381 if let Some(extracted) =
4382 self.extract_discriminator_value_for_field(ref_schema, field_name)
4383 {
4384 return extracted;
4385 }
4386 }
4387
4388 self.generate_discriminator_value_from_name(schema_name)
4390 }
4391
4392 fn generate_discriminator_value_from_name(&self, schema_name: &str) -> String {
4393 let mut result = String::new();
4395 let mut chars = schema_name.chars().peekable();
4396 let mut first = true;
4397
4398 while let Some(c) = chars.next() {
4399 if c.is_uppercase()
4400 && !first
4401 && chars
4402 .peek()
4403 .map(|&next| next.is_lowercase())
4404 .unwrap_or(false)
4405 {
4406 result.push('.');
4407 }
4408 result.push(c.to_ascii_lowercase());
4409 first = false;
4410 }
4411
4412 if result.ends_with("event") {
4414 result = result[..result.len() - 5].to_string();
4415 }
4416
4417 if schema_name.starts_with("Response") && !result.starts_with("response.") {
4419 result = format!("response.{}", result.trim_start_matches("response"));
4420 }
4421
4422 result
4423 }
4424
4425 fn to_rust_variant_name(&self, schema_name: &str) -> String {
4426 let mut name = schema_name;
4428
4429 if name.starts_with("Response") && name.len() > 8 {
4431 name = &name[8..]; }
4433
4434 if name.ends_with("Event") && name.len() > 5 {
4436 name = &name[..name.len() - 5]; }
4438
4439 name = name.trim_matches('_');
4441
4442 if name.is_empty() {
4444 schema_name.to_string()
4445 } else {
4446 self.discriminator_to_variant_name(name)
4448 }
4449 }
4450
4451 fn hoist_inline_string_enum(
4475 &mut self,
4476 schema: &Schema,
4477 enum_values: Vec<String>,
4478 primary_name: String,
4479 dependencies: &mut HashSet<String>,
4480 ) -> SchemaType {
4481 fn matches_values(existing: &AnalyzedSchema, values: &[String]) -> bool {
4482 matches!(
4483 &existing.schema_type,
4484 SchemaType::StringEnum { values: existing_values }
4485 if existing_values == values
4486 )
4487 }
4488
4489 let mut enum_type_name = primary_name.clone();
4490 let should_insert = match self.resolved_cache.get(&enum_type_name) {
4491 None => true,
4492 Some(existing) if matches_values(existing, &enum_values) => false,
4493 Some(_) => {
4494 let suffix = enum_values
4497 .first()
4498 .map(|v| self.to_pascal_case(v))
4499 .unwrap_or_else(|| "Variant".to_string());
4500 let candidate = format!("{primary_name}{suffix}");
4501
4502 let resolved = match self.resolved_cache.get(&candidate) {
4503 None => Some((candidate.clone(), true)),
4504 Some(existing) if matches_values(existing, &enum_values) => {
4505 Some((candidate.clone(), false))
4506 }
4507 Some(_) => {
4508 let mut found = None;
4511 for n in 2..1000 {
4512 let numbered = format!("{candidate}_{n}");
4513 match self.resolved_cache.get(&numbered) {
4514 None => {
4515 found = Some((numbered, true));
4516 break;
4517 }
4518 Some(existing) if matches_values(existing, &enum_values) => {
4519 found = Some((numbered, false));
4520 break;
4521 }
4522 Some(_) => continue,
4523 }
4524 }
4525 found
4526 }
4527 };
4528
4529 let (resolved_name, insert) = resolved.unwrap_or((candidate, true));
4530 enum_type_name = resolved_name;
4531 insert
4532 }
4533 };
4534
4535 if should_insert {
4538 self.resolved_cache.insert(
4539 enum_type_name.clone(),
4540 AnalyzedSchema {
4541 name: enum_type_name.clone(),
4542 original: serde_json::to_value(schema).unwrap_or(Value::Null),
4543 schema_type: SchemaType::StringEnum {
4544 values: enum_values,
4545 },
4546 dependencies: HashSet::new(),
4547 nullable: false,
4548 description: schema.details().description.clone(),
4549 default: schema.details().default.clone(),
4550 },
4551 );
4552 }
4553
4554 dependencies.insert(enum_type_name.clone());
4556 SchemaType::Reference {
4557 target: enum_type_name,
4558 }
4559 }
4560
4561 fn analyze_array_schema(
4562 &mut self,
4563 schema: &Schema,
4564 parent_schema_name: &str,
4565 dependencies: &mut HashSet<String>,
4566 ) -> Result<SchemaType> {
4567 let details = schema.details();
4568
4569 if let Some(positions) = details.positional_items() {
4573 return self.analyze_positional_items(
4574 positions,
4575 details,
4576 parent_schema_name,
4577 dependencies,
4578 );
4579 }
4580
4581 if let Some(items_schema) = details.item_schema() {
4583 let item_type = self.analyze_item_schema(
4584 items_schema,
4585 parent_schema_name,
4586 &format!("{parent_schema_name}Item"),
4587 dependencies,
4588 )?;
4589 let item_type = self.hoist_inline_property_type(
4590 parent_schema_name,
4591 "Item",
4592 item_type,
4593 dependencies,
4594 );
4595 Ok(SchemaType::Array {
4596 item_type: Box::new(item_type),
4597 })
4598 } else {
4599 Ok(
4601 self.untyped_value_array(
4602 self.untyped_context(""),
4603 UntypedReason::ArrayWithoutItems,
4604 ),
4605 )
4606 }
4607 }
4608
4609 fn shared_branch_type(&self, branches: &[Schema]) -> Option<SchemaType> {
4618 let mut mapped: Option<(String, Option<String>)> = None;
4619 let mut scalar_kind: Option<OpenApiSchemaType> = None;
4620 let mut formats_agree = true;
4621 for branch in branches {
4622 if branch.reference().is_some() {
4623 return None;
4624 }
4625 let details = branch.details();
4626 if details.enum_values.is_some()
4627 || details.const_value.is_some()
4628 || details.properties.is_some()
4629 {
4630 return None;
4631 }
4632 let scalar = match branch.schema_type()? {
4633 scalar @ (OpenApiSchemaType::String
4634 | OpenApiSchemaType::Integer
4635 | OpenApiSchemaType::Number
4636 | OpenApiSchemaType::Boolean) => scalar.clone(),
4637 _ => return None,
4638 };
4639 match &scalar_kind {
4640 Some(existing) if *existing != scalar => return None,
4641 Some(_) => {}
4642 None => scalar_kind = Some(scalar.clone()),
4643 }
4644
4645 let candidate = self.type_mapper.map(scalar, details);
4646 let candidate = (candidate.rust_type, candidate.serde_with);
4647 match &mapped {
4648 Some(existing) if *existing != candidate => formats_agree = false,
4649 Some(_) => {}
4650 None => mapped = Some(candidate),
4651 }
4652 }
4653
4654 if formats_agree {
4655 return mapped.map(|(rust_type, serde_with)| SchemaType::Primitive {
4656 rust_type,
4657 serde_with,
4658 });
4659 }
4660
4661 let scalar = scalar_kind?;
4667 let mapped = self
4668 .type_mapper
4669 .map(scalar, &crate::openapi::SchemaDetails::default());
4670 Some(SchemaType::Primitive {
4671 rust_type: mapped.rust_type,
4672 serde_with: mapped.serde_with,
4673 })
4674 }
4675
4676 fn expand_pointer_branches(&self, branches: &[Schema]) -> Option<Vec<Schema>> {
4685 let mut expanded = Vec::with_capacity(branches.len());
4686 let mut changed = false;
4687 for branch in branches {
4688 let resolved = branch
4689 .reference()
4690 .filter(|reference| self.extract_schema_name(reference).is_none())
4691 .and_then(|reference| reference.strip_prefix('#'))
4692 .filter(|pointer| pointer.starts_with('/'))
4693 .and_then(|pointer| self.openapi_spec.pointer(pointer))
4694 .and_then(|value| Schema::deserialize(value).ok())
4695 .filter(|schema| schema.reference().is_none());
4696 match resolved {
4697 Some(schema) => {
4698 expanded.push(schema);
4699 changed = true;
4700 }
4701 None => expanded.push(branch.clone()),
4702 }
4703 }
4704 changed.then_some(expanded)
4705 }
4706
4707 fn analyze_object_with_variants(
4720 &mut self,
4721 schema: &Schema,
4722 branches: &[Schema],
4723 schema_name: &str,
4724 dependencies: &mut HashSet<String>,
4725 ) -> Result<Option<SchemaType>> {
4726 let details = schema.details();
4727 if details.properties.as_ref().is_none_or(BTreeMap::is_empty) || branches.is_empty() {
4728 return Ok(None);
4729 }
4730 if schema.is_nullable_pattern() || Self::union_only_constrains_requiredness(branches) {
4733 return Ok(None);
4734 }
4735
4736 let base = self.analyze_object_schema(schema, dependencies)?;
4737 let SchemaType::Object {
4738 properties,
4739 required,
4740 additional_properties,
4741 ..
4742 } = base
4743 else {
4744 return Ok(None);
4745 };
4746
4747 let variant_name = self.unique_hoisted_name(schema_name, "Variant");
4748 let variant_type = self.analyze_anyof_union(
4749 branches,
4750 schema.discriminator(),
4751 dependencies,
4752 &variant_name,
4753 )?;
4754 if matches!(variant_type, SchemaType::Untyped { .. }) {
4757 return Ok(Some(SchemaType::Object {
4758 properties,
4759 required,
4760 additional_properties,
4761 variant: None,
4762 }));
4763 }
4764
4765 self.resolved_cache.insert(
4766 variant_name.clone(),
4767 AnalyzedSchema {
4768 name: variant_name.clone(),
4769 original: Value::Null,
4770 dependencies: schema_type_dependencies(&variant_type),
4771 schema_type: variant_type,
4772 nullable: false,
4773 description: None,
4774 default: None,
4775 },
4776 );
4777 dependencies.insert(variant_name.clone());
4778
4779 Ok(Some(SchemaType::Object {
4780 properties,
4781 required,
4782 additional_properties,
4783 variant: Some(SchemaRef {
4784 target: variant_name,
4785 nullable: false,
4786 }),
4787 }))
4788 }
4789
4790 fn union_only_constrains_requiredness(branches: &[Schema]) -> bool {
4799 !branches.is_empty()
4800 && branches.iter().all(|branch| {
4801 let details = branch.details();
4802 branch.schema_type().is_none()
4803 && branch.reference().is_none()
4804 && branch.union_variants().is_none()
4805 && details.properties.is_none()
4806 && details.enum_values.is_none()
4807 && details.const_value.is_none()
4808 && details.items.is_none()
4809 && details.additional_properties.is_none()
4810 && details.required.is_some()
4811 })
4812 }
4813
4814 fn analyze_empty_union(
4821 &mut self,
4822 schema: &Schema,
4823 dependencies: &mut HashSet<String>,
4824 ) -> Result<SchemaType> {
4825 let Some(declared) = schema
4829 .declared_type()
4830 .cloned()
4831 .or_else(|| schema.inferred_type())
4832 .or_else(|| {
4833 schema
4834 .details()
4835 .properties
4836 .is_some()
4837 .then_some(OpenApiSchemaType::Object)
4838 })
4839 else {
4840 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema));
4841 };
4842 match declared {
4843 OpenApiSchemaType::Object => self.analyze_object_schema(schema, dependencies),
4844 OpenApiSchemaType::Array => {
4845 let context = self
4846 .current_schema_name
4847 .clone()
4848 .unwrap_or_else(|| "Inline".to_string());
4849 self.analyze_array_schema(schema, &context, dependencies)
4850 }
4851 scalar => {
4852 let mapped = self.type_mapper.map(scalar, schema.details());
4853 Ok(SchemaType::Primitive {
4854 rust_type: mapped.rust_type,
4855 serde_with: mapped.serde_with,
4856 })
4857 }
4858 }
4859 }
4860
4861 fn resolve_pointer_schema(
4874 &mut self,
4875 reference: &str,
4876 dependencies: &mut HashSet<String>,
4877 ) -> Result<Option<SchemaType>> {
4878 let Some(pointer) = reference.strip_prefix('#') else {
4879 return Ok(None);
4880 };
4881 if pointer.is_empty() || !pointer.starts_with('/') {
4882 return Ok(None);
4883 }
4884 let name = pointer_type_name(pointer);
4885 if name.is_empty() {
4886 return Ok(None);
4887 }
4888 if self.resolved_cache.contains_key(&name) || !self.resolving_pointers.insert(name.clone())
4891 {
4892 dependencies.insert(name.clone());
4893 return Ok(Some(SchemaType::Reference { target: name }));
4894 }
4895
4896 let resolved = (|| {
4897 let value = self.openapi_spec.pointer(pointer)?.clone();
4898 Schema::deserialize(&value).ok()
4899 })();
4900 let Some(schema) = resolved else {
4901 self.resolving_pointers.remove(&name);
4902 return Ok(None);
4903 };
4904
4905 let analyzed = self.analyze_property_schema_with_context(&schema, None, dependencies);
4906 self.resolving_pointers.remove(&name);
4907 let analyzed = analyzed?;
4908 Ok(Some(self.hoist_inline_property_type(
4909 &name,
4910 "",
4911 analyzed,
4912 dependencies,
4913 )))
4914 }
4915
4916 fn hoist_inline_property_type(
4925 &mut self,
4926 schema_name: &str,
4927 property_name: &str,
4928 schema_type: SchemaType,
4929 dependencies: &mut HashSet<String>,
4930 ) -> SchemaType {
4931 if schema_type.renders_inline() {
4932 return schema_type;
4933 }
4934
4935 let hoisted_name = self.unique_hoisted_name(schema_name, property_name);
4936 let hoisted_dependencies = schema_type_dependencies(&schema_type);
4937 self.resolved_cache.insert(
4938 hoisted_name.clone(),
4939 AnalyzedSchema {
4940 name: hoisted_name.clone(),
4941 original: Value::Null,
4942 schema_type,
4943 dependencies: hoisted_dependencies,
4944 nullable: false,
4945 description: None,
4946 default: None,
4947 },
4948 );
4949 dependencies.insert(hoisted_name.clone());
4950 SchemaType::Reference {
4951 target: hoisted_name,
4952 }
4953 }
4954
4955 fn unique_hoisted_name(&self, schema_name: &str, property_name: &str) -> String {
4959 use heck::ToPascalCase;
4960
4961 let base = format!("{schema_name}{}", property_name.to_pascal_case());
4962 if !self.schemas.contains_key(&base) && !self.resolved_cache.contains_key(&base) {
4963 return base;
4964 }
4965 let mut suffix = 2;
4966 loop {
4967 let candidate = format!("{base}{suffix}");
4968 if !self.schemas.contains_key(&candidate)
4969 && !self.resolved_cache.contains_key(&candidate)
4970 {
4971 return candidate;
4972 }
4973 suffix += 1;
4974 }
4975 }
4976
4977 fn analyze_positional_items(
4990 &mut self,
4991 positions: &[Schema],
4992 details: &crate::openapi::SchemaDetails,
4993 parent_schema_name: &str,
4994 dependencies: &mut HashSet<String>,
4995 ) -> Result<SchemaType> {
4996 if details.positional_items_are_exact() && !positions.is_empty() {
4997 let mut element_types = Vec::with_capacity(positions.len());
4998 for (index, position) in positions.iter().enumerate() {
4999 let element_type = self.analyze_item_schema(
5000 position,
5001 parent_schema_name,
5002 &format!("{parent_schema_name}Item{}", index + 1),
5003 dependencies,
5004 )?;
5005 element_types.push(self.hoist_inline_property_type(
5006 parent_schema_name,
5007 &format!("Item{}", index + 1),
5008 element_type,
5009 dependencies,
5010 ));
5011 }
5012 return Ok(SchemaType::Tuple { element_types });
5013 }
5014
5015 if details.positional_items_are_closed()
5019 && let Some(shared) = shared_positional_schema(positions)
5020 {
5021 let item_type = self.analyze_item_schema(
5022 shared,
5023 parent_schema_name,
5024 &format!("{parent_schema_name}Item"),
5025 dependencies,
5026 )?;
5027 return Ok(SchemaType::Array {
5028 item_type: Box::new(item_type),
5029 });
5030 }
5031
5032 Ok(self.untyped_value_array(self.untyped_context(""), UntypedReason::OpenPositionalItems))
5033 }
5034
5035 fn analyze_item_schema(
5042 &mut self,
5043 items_schema: &Schema,
5044 parent_schema_name: &str,
5045 inline_name: &str,
5046 dependencies: &mut HashSet<String>,
5047 ) -> Result<SchemaType> {
5048 let item_type = match items_schema {
5049 Schema::Reference { reference, .. } => {
5050 let target = self
5052 .extract_schema_name(reference)
5053 .ok_or_else(|| GeneratorError::UnresolvedReference(reference.to_string()))?
5054 .to_string();
5055 dependencies.insert(target.clone());
5056 SchemaType::Reference { target }
5057 }
5058 Schema::RecursiveRef { recursive_ref, .. } => {
5059 if recursive_ref == "#" {
5061 let target = self
5063 .find_recursive_anchor_schema()
5064 .unwrap_or_else(|| parent_schema_name.to_string());
5065 dependencies.insert(target.clone());
5066 SchemaType::Reference { target }
5067 } else {
5068 let target = self
5069 .extract_schema_name(recursive_ref)
5070 .unwrap_or("RecursiveType")
5071 .to_string();
5072 dependencies.insert(target.clone());
5073 SchemaType::Reference { target }
5074 }
5075 }
5076 Schema::Typed { schema_type, .. } => {
5077 match schema_type {
5079 OpenApiSchemaType::String => {
5080 match items_schema
5084 .details()
5085 .string_enum_values()
5086 .filter(|values| !values.is_empty())
5087 {
5088 Some(values) => self.hoist_inline_string_enum(
5089 items_schema,
5090 values,
5091 inline_name.to_string(),
5092 dependencies,
5093 ),
5094 None => SchemaType::Primitive {
5095 rust_type: "String".to_string(),
5096 serde_with: None,
5097 },
5098 }
5099 }
5100 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
5101 let details = items_schema.details();
5102 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
5103 SchemaType::Primitive {
5104 rust_type,
5105 serde_with: None,
5106 }
5107 }
5108 OpenApiSchemaType::Boolean => SchemaType::Primitive {
5109 rust_type: "bool".to_string(),
5110 serde_with: None,
5111 },
5112 OpenApiSchemaType::Object => {
5113 let object_type_name = inline_name.to_string();
5115
5116 let object_type = self.analyze_object_schema(items_schema, dependencies)?;
5118
5119 let inline_schema = AnalyzedSchema {
5121 name: object_type_name.clone(),
5122 original: serde_json::to_value(items_schema).unwrap_or(Value::Null),
5123 schema_type: object_type,
5124 dependencies: dependencies.clone(),
5125 nullable: false,
5126 description: items_schema.details().description.clone(),
5127 default: None,
5128 };
5129
5130 self.resolved_cache
5132 .insert(object_type_name.clone(), inline_schema);
5133 dependencies.insert(object_type_name.clone());
5134
5135 SchemaType::Reference {
5137 target: object_type_name,
5138 }
5139 }
5140 OpenApiSchemaType::Array => {
5141 self.analyze_array_schema(items_schema, parent_schema_name, dependencies)?
5143 }
5144 _ => self.untyped_value(
5145 self.untyped_context(""),
5146 UntypedReason::UnsupportedTypeKeyword,
5147 ),
5148 }
5149 }
5150 Schema::OneOf { .. } | Schema::AnyOf { .. } => {
5151 let analyzed = self.analyze_schema_value(items_schema, "ArrayItem")?;
5153
5154 match &analyzed.schema_type {
5156 SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => {
5157 let union_name = format!("{inline_name}Union");
5160
5161 let mut union_schema = analyzed;
5163 union_schema.name = union_name.clone();
5164
5165 self.resolved_cache.insert(union_name.clone(), union_schema);
5167
5168 dependencies.insert(union_name.clone());
5170
5171 SchemaType::Reference { target: union_name }
5173 }
5174 _ => analyzed.schema_type,
5175 }
5176 }
5177 Schema::Untyped { .. } => {
5178 if let Some(inferred) = items_schema.inferred_type() {
5180 match inferred {
5181 OpenApiSchemaType::Object => {
5182 let object_type_name = inline_name.to_string();
5184
5185 let object_type =
5187 self.analyze_object_schema(items_schema, dependencies)?;
5188
5189 let inline_schema = AnalyzedSchema {
5191 name: object_type_name.clone(),
5192 original: serde_json::to_value(items_schema).unwrap_or(Value::Null),
5193 schema_type: object_type,
5194 dependencies: dependencies.clone(),
5195 nullable: false,
5196 description: items_schema.details().description.clone(),
5197 default: None,
5198 };
5199
5200 self.resolved_cache
5202 .insert(object_type_name.clone(), inline_schema);
5203 dependencies.insert(object_type_name.clone());
5204
5205 SchemaType::Reference {
5207 target: object_type_name,
5208 }
5209 }
5210 OpenApiSchemaType::String => {
5211 match items_schema
5214 .details()
5215 .string_enum_values()
5216 .filter(|values| !values.is_empty())
5217 {
5218 Some(values) => self.hoist_inline_string_enum(
5219 items_schema,
5220 values,
5221 inline_name.to_string(),
5222 dependencies,
5223 ),
5224 None => SchemaType::Primitive {
5225 rust_type: "String".to_string(),
5226 serde_with: None,
5227 },
5228 }
5229 }
5230 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
5231 let details = items_schema.details();
5232 let rust_type = self.get_number_rust_type(inferred, details);
5233 SchemaType::Primitive {
5234 rust_type,
5235 serde_with: None,
5236 }
5237 }
5238 OpenApiSchemaType::Boolean => SchemaType::Primitive {
5239 rust_type: "bool".to_string(),
5240 serde_with: None,
5241 },
5242 OpenApiSchemaType::Null => SchemaType::Primitive {
5245 rust_type: self.type_mapper.null_unit().rust_type,
5246 serde_with: None,
5247 },
5248 _ => self.untyped_value(
5249 self.untyped_context(""),
5250 UntypedReason::UnsupportedTypeKeyword,
5251 ),
5252 }
5253 } else {
5254 self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema)
5255 }
5256 }
5257 _ => self.analyze_property_schema_with_context(items_schema, None, dependencies)?,
5262 };
5263
5264 Ok(item_type)
5265 }
5266
5267 fn get_number_rust_type(
5268 &self,
5269 schema_type: OpenApiSchemaType,
5270 details: &crate::openapi::SchemaDetails,
5271 ) -> String {
5272 let format = details.format.as_deref();
5276 match schema_type {
5277 OpenApiSchemaType::Integer => self.type_mapper.integer_format(format).rust_type,
5278 OpenApiSchemaType::Number => self.type_mapper.number_format(format).rust_type,
5279 _ => self.type_mapper.dynamic_json().rust_type,
5280 }
5281 }
5282
5283 fn analyze_anyof_union(
5284 &mut self,
5285 any_of_schemas: &[Schema],
5286 discriminator: Option<&Discriminator>,
5287 dependencies: &mut HashSet<String>,
5288 context_name: &str,
5289 ) -> Result<SchemaType> {
5290 let expanded_branches;
5292 let any_of_schemas = match self.expand_pointer_branches(any_of_schemas) {
5293 Some(expanded) => {
5294 expanded_branches = expanded;
5295 expanded_branches.as_slice()
5296 }
5297 None => any_of_schemas,
5298 };
5299
5300 let filtered_owned: Vec<Schema>;
5305 let any_of_schemas: &[Schema] = if any_of_schemas
5306 .iter()
5307 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
5308 {
5309 filtered_owned = any_of_schemas
5310 .iter()
5311 .filter(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
5312 .cloned()
5313 .collect();
5314 if filtered_owned.is_empty() {
5315 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema));
5316 }
5317 if filtered_owned.len() == 1 {
5318 return self
5319 .analyze_schema_value(&filtered_owned[0], context_name)
5320 .map(|a| a.schema_type);
5321 }
5322 &filtered_owned
5323 } else {
5324 any_of_schemas
5325 };
5326
5327 if let [only] = any_of_schemas {
5330 return self
5331 .analyze_schema_value(only, context_name)
5332 .map(|analyzed| analyzed.schema_type);
5333 }
5334
5335 if let Some(shared) = self.shared_branch_type(any_of_schemas) {
5339 return Ok(shared);
5340 }
5341
5342 let has_refs = any_of_schemas.iter().any(|s| s.is_reference());
5344 let has_objects = any_of_schemas.iter().any(|s| {
5345 matches!(s.schema_type(), Some(OpenApiSchemaType::Object))
5346 || s.inferred_type() == Some(OpenApiSchemaType::Object)
5347 });
5348 let has_arrays = any_of_schemas
5349 .iter()
5350 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Array)));
5351
5352 let all_string_like = any_of_schemas.iter().all(|s| {
5355 matches!(s.schema_type(), Some(OpenApiSchemaType::String))
5356 || s.details().const_value.is_some()
5357 });
5358
5359 if (has_refs || has_objects || has_arrays || any_of_schemas.len() > 1) && !all_string_like {
5360 if let Some(disc) = discriminator {
5362 return self.analyze_oneof_union(
5364 any_of_schemas,
5365 Some(disc),
5366 context_name,
5367 dependencies,
5368 );
5369 }
5370
5371 if let Some(disc_field) = self.detect_discriminator_field(any_of_schemas) {
5373 return self.analyze_oneof_union(
5374 any_of_schemas,
5375 Some(&Discriminator {
5376 property_name: disc_field,
5377 mapping: None,
5378 default_mapping: None,
5379 extensions: crate::extensions::Extensions::default(),
5380 }),
5381 context_name,
5382 dependencies,
5383 );
5384 }
5385
5386 let mut variants = Vec::new();
5388
5389 for schema in any_of_schemas {
5390 if let Some(ref_str) = schema.reference() {
5391 if let Some(target) = self.extract_schema_name(ref_str) {
5392 dependencies.insert(target.to_string());
5393 variants.push(SchemaRef {
5394 target: target.to_string(),
5395 nullable: false,
5396 });
5397 }
5398 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object))
5399 || schema.inferred_type() == Some(OpenApiSchemaType::Object)
5400 {
5401 let inline_index = variants.len();
5403 let inline_type_name = self.generate_inline_type_name(schema, inline_index);
5404
5405 self.add_inline_schema(&inline_type_name, schema, dependencies)?;
5407
5408 variants.push(SchemaRef {
5409 target: inline_type_name,
5410 nullable: false,
5411 });
5412 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Array)) {
5413 let array_type =
5415 self.analyze_array_schema(schema, context_name, dependencies)?;
5416
5417 let array_type_name = if let Some(items_schema) = schema.details().item_schema()
5419 {
5420 if let Some(ref_str) = items_schema.reference() {
5421 if let Some(item_type_name) = self.extract_schema_name(ref_str) {
5422 dependencies.insert(item_type_name.to_string());
5423 format!("{item_type_name}Array")
5424 } else {
5425 self.generate_context_aware_name(
5426 context_name,
5427 "Array",
5428 variants.len(),
5429 Some(schema),
5430 )
5431 }
5432 } else {
5433 self.generate_context_aware_name(
5434 context_name,
5435 "Array",
5436 variants.len(),
5437 Some(schema),
5438 )
5439 }
5440 } else {
5441 self.generate_context_aware_name(
5442 context_name,
5443 "Array",
5444 variants.len(),
5445 Some(schema),
5446 )
5447 };
5448
5449 self.resolved_cache.insert(
5451 array_type_name.clone(),
5452 AnalyzedSchema {
5453 name: array_type_name.clone(),
5454 original: serde_json::to_value(schema).unwrap_or(Value::Null),
5455 schema_type: array_type,
5456 dependencies: HashSet::new(),
5457 nullable: false,
5458 description: Some("Array variant in union".to_string()),
5459 default: None,
5460 },
5461 );
5462
5463 dependencies.insert(array_type_name.clone());
5465
5466 variants.push(SchemaRef {
5467 target: array_type_name,
5468 nullable: false,
5469 });
5470 } else if let Some(schema_type) = schema.schema_type() {
5471 let primitive_unions = self
5481 .type_mapper
5482 .config_shape_primitive_unions()
5483 .unwrap_or(true);
5484
5485 if primitive_unions {
5486 let mapped = self.type_mapper.map(schema_type.clone(), schema.details());
5487 variants.push(SchemaRef {
5488 target: mapped.rust_type,
5489 nullable: false,
5490 });
5491 } else {
5492 let inline_index = variants.len();
5493 let inline_type_name = match schema_type {
5494 OpenApiSchemaType::String => {
5495 if inline_index == 0 {
5496 format!("{context_name}String")
5497 } else {
5498 format!("{context_name}StringVariant{inline_index}")
5499 }
5500 }
5501 OpenApiSchemaType::Number => {
5502 if inline_index == 0 {
5503 format!("{context_name}Number")
5504 } else {
5505 format!("{context_name}NumberVariant{inline_index}")
5506 }
5507 }
5508 OpenApiSchemaType::Integer => {
5509 if inline_index == 0 {
5510 format!("{context_name}Integer")
5511 } else {
5512 format!("{context_name}IntegerVariant{inline_index}")
5513 }
5514 }
5515 OpenApiSchemaType::Boolean => {
5516 if inline_index == 0 {
5517 format!("{context_name}Boolean")
5518 } else {
5519 format!("{context_name}BooleanVariant{inline_index}")
5520 }
5521 }
5522 _ => format!("{context_name}Variant{inline_index}"),
5523 };
5524
5525 let rust_type =
5526 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
5527
5528 self.resolved_cache.insert(
5529 inline_type_name.clone(),
5530 AnalyzedSchema {
5531 name: inline_type_name.clone(),
5532 original: serde_json::to_value(schema).unwrap_or(Value::Null),
5533 schema_type: SchemaType::Primitive {
5534 rust_type,
5535 serde_with: None,
5536 },
5537 dependencies: HashSet::new(),
5538 nullable: false,
5539 description: schema.details().description.clone(),
5540 default: None,
5541 },
5542 );
5543
5544 dependencies.insert(inline_type_name.clone());
5545
5546 variants.push(SchemaRef {
5547 target: inline_type_name,
5548 nullable: false,
5549 });
5550 }
5551 }
5552 }
5553
5554 if !variants.is_empty() {
5555 return Ok(SchemaType::Union { variants });
5556 }
5557 }
5558
5559 let all_strings = any_of_schemas.iter().all(|schema| {
5561 matches!(schema.schema_type(), Some(OpenApiSchemaType::String))
5562 || schema.details().const_value.is_some()
5563 });
5564
5565 if all_strings {
5566 let mut enum_values = Vec::new();
5568 let mut has_open_string = false;
5569
5570 for schema in any_of_schemas {
5571 match schema
5576 .details()
5577 .string_enum_values()
5578 .filter(|values| !values.is_empty())
5579 {
5580 Some(values) => {
5581 for value in values {
5582 if !enum_values.contains(&value) {
5583 enum_values.push(value);
5584 }
5585 }
5586 }
5587 None => {
5588 if matches!(schema.schema_type(), Some(OpenApiSchemaType::String)) {
5589 has_open_string = true;
5590 }
5591 }
5592 }
5593 }
5594
5595 if !enum_values.is_empty() {
5596 if has_open_string {
5597 return Ok(SchemaType::ExtensibleEnum {
5600 known_values: enum_values,
5601 });
5602 } else {
5603 return Ok(SchemaType::StringEnum {
5605 values: enum_values,
5606 });
5607 }
5608 }
5609 }
5610
5611 Ok(self.untyped_value(
5613 self.untyped_context(""),
5614 UntypedReason::UnrepresentableUnion,
5615 ))
5616 }
5617
5618 fn find_recursive_anchor_schema(&self) -> Option<String> {
5620 for (schema_name, schema) in &self.schemas {
5622 let details = schema.details();
5623 if details.recursive_anchor == Some(true) {
5624 return Some(schema_name.clone());
5625 }
5626 }
5627
5628 None
5632 }
5633
5634 fn should_use_dynamic_json(&self, schema: &Schema) -> bool {
5637 if let Schema::AnyOf { any_of, .. } = schema {
5639 if any_of.len() == 2 {
5640 let has_null = any_of
5641 .iter()
5642 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)));
5643 let has_empty_object = any_of.iter().any(|s| self.is_dynamic_object_pattern(s));
5644
5645 if has_null && has_empty_object {
5646 return true;
5647 }
5648 }
5649 }
5650
5651 self.is_dynamic_object_pattern(schema)
5653 }
5654
5655 fn is_dynamic_object_pattern(&self, schema: &Schema) -> bool {
5657 let is_object = match schema.schema_type() {
5659 Some(OpenApiSchemaType::Object) => true,
5660 None => schema.inferred_type() == Some(OpenApiSchemaType::Object),
5661 _ => false,
5662 };
5663
5664 if !is_object {
5665 return false;
5666 }
5667
5668 let details = schema.details();
5669
5670 if self.has_explicit_additional_properties(schema) {
5673 return false;
5674 }
5675
5676 let no_properties = details
5678 .properties
5679 .as_ref()
5680 .map(|props| props.is_empty())
5681 .unwrap_or(true);
5682
5683 if no_properties {
5684 let has_structural_constraints = details
5687 .required
5688 .as_ref()
5689 .map(|req| req.iter().any(|r| r != "type"))
5690 .unwrap_or(false)
5691 || details.pattern_properties.is_some()
5692 || details.property_names.is_some()
5693 || details.min_properties.is_some()
5694 || details.max_properties.is_some()
5695 || details.dependent_required.is_some()
5696 || details.dependent_schemas.is_some()
5697 || details.if_schema.is_some()
5698 || details.then_schema.is_some()
5699 || details.else_schema.is_some();
5700
5701 return !has_structural_constraints;
5702 }
5703
5704 false
5705 }
5706
5707 fn has_explicit_additional_properties(&self, schema: &Schema) -> bool {
5709 let details = schema.details();
5710
5711 matches!(
5713 &details.additional_properties,
5714 Some(crate::openapi::AdditionalProperties::Boolean(true))
5715 | Some(crate::openapi::AdditionalProperties::Schema(_))
5716 )
5717 }
5718
5719 fn analyze_operations(&mut self, analysis: &mut SchemaAnalysis) -> Result<()> {
5721 let spec: crate::openapi::OpenApiSpec = parse_spec_document(&self.openapi_spec)?;
5722 let mut canonical_operation_ids = HashSet::new();
5727
5728 if let Some(paths) = &spec.paths {
5729 for (path, path_item) in paths {
5730 let resolved = self.resolve_path_item(path_item, &spec)?;
5732 let pi: &crate::openapi::PathItem = resolved.as_ref().unwrap_or(path_item);
5733 self.ingest_path_item_operations(path, pi, analysis, &mut canonical_operation_ids)?;
5734 }
5735 }
5736 if let Some(webhooks) = &spec.webhooks {
5743 for (name, path_item) in webhooks {
5744 let synthetic_path = format!("/__webhook__/{name}");
5745 self.ingest_path_item_operations(
5746 &synthetic_path,
5747 path_item,
5748 analysis,
5749 &mut canonical_operation_ids,
5750 )?;
5751 }
5752 }
5753 Ok(())
5754 }
5755
5756 fn resolve_path_item(
5760 &self,
5761 path_item: &crate::openapi::PathItem,
5762 spec: &crate::openapi::OpenApiSpec,
5763 ) -> Result<Option<crate::openapi::PathItem>> {
5764 let Some(reference) = &path_item.reference else {
5765 return Ok(None);
5766 };
5767 let target_name = reference
5768 .strip_prefix("#/components/pathItems/")
5769 .ok_or_else(|| {
5770 GeneratorError::UnresolvedReference(format!(
5771 "Path Item $ref must point at #/components/pathItems/{{name}}, got {reference}"
5772 ))
5773 })?;
5774 let pi = spec
5775 .components
5776 .as_ref()
5777 .and_then(|c| c.path_items.as_ref())
5778 .and_then(|map| map.get(target_name))
5779 .ok_or_else(|| {
5780 GeneratorError::UnresolvedReference(format!(
5781 "Path Item ref {reference} not found in components/pathItems"
5782 ))
5783 })?;
5784 Ok(Some(pi.clone()))
5785 }
5786
5787 fn ingest_path_item_operations(
5788 &mut self,
5789 path: &str,
5790 path_item: &crate::openapi::PathItem,
5791 analysis: &mut SchemaAnalysis,
5792 canonical_operation_ids: &mut HashSet<String>,
5793 ) -> Result<()> {
5794 for (method, operation) in path_item.operations() {
5795 let raw_operation_id = operation
5797 .operation_id
5798 .clone()
5799 .unwrap_or_else(|| Self::generate_operation_id(method, path));
5800
5801 let operation_id = if canonical_operation_ids
5812 .contains(&Self::canonical_operation_id(&raw_operation_id))
5813 {
5814 let method_lower = method.to_lowercase();
5815 let mut candidate = format!("{}_{}", raw_operation_id, method_lower);
5816 let mut suffix = 2;
5817 while canonical_operation_ids.contains(&Self::canonical_operation_id(&candidate)) {
5818 candidate = format!("{}_{}_{}", raw_operation_id, method_lower, suffix);
5819 suffix += 1;
5820 }
5821 eprintln!(
5822 "⚠️ duplicate operationId `{}` at `{} {}` — disambiguated to `{}`",
5823 raw_operation_id, method, path, candidate
5824 );
5825 candidate
5826 } else {
5827 raw_operation_id.clone()
5828 };
5829
5830 let (op_info, responses) = self.analyze_single_operation(
5831 &operation_id,
5832 method,
5833 path,
5834 operation,
5835 path_item.parameters.as_ref(),
5836 analysis,
5837 )?;
5838 analysis
5839 .operation_id_aliases
5840 .entry(raw_operation_id)
5841 .or_default()
5842 .push(operation_id.clone());
5843 canonical_operation_ids.insert(Self::canonical_operation_id(&operation_id));
5844 analysis
5845 .operation_responses
5846 .insert(operation_id.clone(), responses);
5847 analysis.operations.insert(operation_id, op_info);
5848 }
5849 Ok(())
5850 }
5851
5852 fn canonical_operation_id(operation_id: &str) -> String {
5853 use heck::ToPascalCase;
5854 operation_id.replace('.', "_").to_pascal_case()
5855 }
5856
5857 fn generate_operation_id(method: &str, path: &str) -> String {
5860 let mut operation_id = method.to_lowercase();
5862
5863 let path_parts: Vec<&str> = path.trim_start_matches('/').split('/').collect();
5865
5866 for part in path_parts {
5867 if part.is_empty() {
5868 continue;
5869 }
5870
5871 let cleaned_part = if part.starts_with('{') && part.ends_with('}') {
5873 &part[1..part.len() - 1]
5874 } else {
5875 part
5876 };
5877
5878 let pascal_case_part = cleaned_part
5880 .split(&['-', '_'][..])
5881 .map(|s| {
5882 let mut chars = s.chars();
5883 match chars.next() {
5884 None => String::new(),
5885 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
5886 }
5887 })
5888 .collect::<String>();
5889
5890 operation_id.push_str(&pascal_case_part);
5891 }
5892
5893 operation_id
5894 }
5895
5896 fn analyze_single_operation(
5898 &mut self,
5899 operation_id: &str,
5900 method: &str,
5901 path: &str,
5902 operation: &crate::openapi::Operation,
5903 path_item_parameters: Option<&Vec<crate::openapi::Parameter>>,
5904 _analysis: &mut SchemaAnalysis,
5905 ) -> Result<(OperationInfo, BTreeMap<String, OperationResponse>)> {
5906 let raw_path_item = self
5907 .openapi_spec
5908 .get("paths")
5909 .and_then(|paths| paths.get(path))
5910 .cloned();
5911 let raw_operation = raw_path_item
5912 .as_ref()
5913 .and_then(|path_item| path_item.get(method.to_ascii_lowercase()))
5914 .cloned();
5915 let request_body = operation
5916 .request_body
5917 .as_ref()
5918 .map(|request_body| self.resolve_request_body(request_body))
5919 .transpose()?;
5920 let mut op_info = OperationInfo {
5921 operation_id: operation_id.to_string(),
5922 method: method.to_uppercase(),
5923 path: normalize_operation_path(path),
5924 summary: operation.summary.clone(),
5925 description: operation.description.clone(),
5926 request_body: None,
5927 request_body_required: request_body
5929 .as_ref()
5930 .and_then(|rb| rb.required)
5931 .unwrap_or(false),
5932 response_schemas: BTreeMap::new(),
5933 parameters: Vec::new(),
5934 supports_streaming: false, stream_parameter: None, tags: operation.tags.clone().unwrap_or_default(),
5937 };
5938 let mut operation_responses = BTreeMap::new();
5939
5940 if let Some(request_body) = &request_body {
5942 use crate::openapi::{
5943 is_binary_media_type, is_form_urlencoded_media_type, is_json_media_type,
5944 media_type_essence,
5945 };
5946 if let Some((content_type, maybe_schema)) = request_body.best_content() {
5947 op_info.request_body = if is_json_media_type(content_type) {
5948 match maybe_schema {
5949 Some(s) => {
5950 let validation_schema = self
5951 .raw_request_body_schema(raw_operation.as_ref(), content_type)
5952 .unwrap_or(
5953 serde_json::to_value(s).map_err(GeneratorError::ParseError)?,
5954 );
5955 Some(
5956 self.resolve_or_inline_schema(s, operation_id, "Request")
5957 .map(|name| RequestBodyContent::Json {
5958 schema_name: name,
5959 media_type: content_type.to_string(),
5960 validation_schema,
5961 })?,
5962 )
5963 }
5964 None => Some(RequestBodyContent::SchemaLess {
5965 media_type: content_type.to_string(),
5966 }),
5967 }
5968 } else if is_form_urlencoded_media_type(content_type) {
5969 match maybe_schema {
5970 Some(s) => {
5971 let validation_schema = self
5972 .raw_request_body_schema(raw_operation.as_ref(), content_type)
5973 .unwrap_or(
5974 serde_json::to_value(s).map_err(GeneratorError::ParseError)?,
5975 );
5976 Some(
5977 self.resolve_or_inline_schema(s, operation_id, "Request")
5978 .map(|name| RequestBodyContent::FormUrlEncoded {
5979 schema_name: name,
5980 media_type: content_type.to_string(),
5981 validation_schema,
5982 })?,
5983 )
5984 }
5985 None => Some(RequestBodyContent::SchemaLess {
5986 media_type: content_type.to_string(),
5987 }),
5988 }
5989 } else if media_type_essence(content_type)
5990 .eq_ignore_ascii_case("multipart/form-data")
5991 {
5992 match maybe_schema {
5993 Some(schema) => {
5994 let validation_schema = self
5995 .raw_request_body_schema(raw_operation.as_ref(), content_type)
5996 .unwrap_or(
5997 serde_json::to_value(schema)
5998 .map_err(GeneratorError::ParseError)?,
5999 );
6000 Some(
6001 self.resolve_or_inline_schema(schema, operation_id, "Request")
6002 .map(|schema_name| RequestBodyContent::Multipart {
6003 schema_name,
6004 media_type: content_type.to_string(),
6005 validation_schema,
6006 })?,
6007 )
6008 }
6009 None => Some(RequestBodyContent::SchemaLess {
6010 media_type: content_type.to_string(),
6011 }),
6012 }
6013 } else if is_binary_media_type(content_type, maybe_schema) {
6014 if media_type_essence(content_type)
6015 .eq_ignore_ascii_case("application/octet-stream")
6016 {
6017 Some(RequestBodyContent::OctetStream {
6018 media_type: content_type.to_string(),
6019 })
6020 } else {
6021 Some(RequestBodyContent::Binary {
6022 media_type: content_type.to_string(),
6023 })
6024 }
6025 } else if crate::openapi::is_text_media_type(content_type) {
6026 Some(RequestBodyContent::TextPlain {
6031 media_type: content_type.to_string(),
6032 })
6033 } else {
6034 None
6035 };
6036 }
6037 if op_info.request_body.is_none() {
6038 let mut media_types = request_body
6039 .content
6040 .as_ref()
6041 .map(|content| content.keys().cloned().collect::<Vec<_>>())
6042 .unwrap_or_default();
6043 media_types.sort();
6044 if !media_types.is_empty() {
6045 op_info.request_body = Some(RequestBodyContent::Unsupported { media_types });
6046 }
6047 }
6048 }
6049
6050 if let Some(responses) = &operation.responses {
6052 for (status_code, response) in responses {
6053 let response = self.resolve_response(response)?;
6054 let supports_streaming = response.content.as_ref().is_some_and(|content| {
6060 content
6061 .keys()
6062 .any(|ct| crate::openapi::is_event_stream_media_type(ct))
6063 });
6064 if supports_streaming {
6065 op_info.supports_streaming = true;
6066 }
6067
6068 let mut response_info = OperationResponse {
6069 supports_streaming,
6070 has_content: response
6071 .content
6072 .as_ref()
6073 .is_some_and(|content| !content.is_empty()),
6074 ..Default::default()
6075 };
6076 if let Some((media_type, schema)) = response.json_content() {
6077 if let Some(schema_ref) = schema.reference() {
6078 if let Some(schema_name) = self.extract_schema_name(schema_ref) {
6080 op_info
6081 .response_schemas
6082 .insert(status_code.clone(), schema_name.to_string());
6083 response_info.schema_name = Some(schema_name.to_string());
6084 response_info.media_type = Some(media_type.to_string());
6085 response_info.body = Some(OperationResponseBody::Json {
6086 schema_name: schema_name.to_string(),
6087 media_type: media_type.to_string(),
6088 });
6089 }
6090 } else {
6091 let synthetic_name =
6093 self.generate_inline_response_type_name(operation_id, status_code);
6094
6095 let mut deps = HashSet::new();
6097 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
6098
6099 op_info
6100 .response_schemas
6101 .insert(status_code.clone(), synthetic_name.clone());
6102 response_info.body = Some(OperationResponseBody::Json {
6103 schema_name: synthetic_name.clone(),
6104 media_type: media_type.to_string(),
6105 });
6106 response_info.schema_name = Some(synthetic_name);
6107 response_info.media_type = Some(media_type.to_string());
6108 }
6109 }
6110 if response_info.body.is_none()
6111 && let Some(content) = response.content.as_ref()
6112 {
6113 let selected = content
6114 .iter()
6115 .find(|(media_type, media)| {
6116 matches!(
6117 crate::openapi::classify_response_media_type(
6118 media_type,
6119 media.schema.as_ref()
6120 ),
6121 crate::openapi::ResponseMediaKind::Text
6122 )
6123 })
6124 .or_else(|| {
6125 content.iter().find(|(media_type, media)| {
6126 matches!(
6127 crate::openapi::classify_response_media_type(
6128 media_type,
6129 media.schema.as_ref()
6130 ),
6131 crate::openapi::ResponseMediaKind::Binary
6132 ) && !crate::openapi::is_wildcard_media_type(media_type)
6133 })
6134 })
6135 .or_else(|| {
6136 content.iter().find(|(media_type, media)| {
6137 matches!(
6138 crate::openapi::classify_response_media_type(
6139 media_type,
6140 media.schema.as_ref()
6141 ),
6142 crate::openapi::ResponseMediaKind::Binary
6143 )
6144 })
6145 });
6146 if let Some((media_type, media)) = selected {
6147 response_info.body = match crate::openapi::classify_response_media_type(
6148 media_type,
6149 media.schema.as_ref(),
6150 ) {
6151 crate::openapi::ResponseMediaKind::Text => {
6152 Some(OperationResponseBody::Text {
6153 media_type: media_type.clone(),
6154 })
6155 }
6156 crate::openapi::ResponseMediaKind::Binary => {
6157 Some(OperationResponseBody::Binary {
6158 media_type: media_type.clone(),
6159 wildcard: crate::openapi::is_wildcard_media_type(media_type),
6160 })
6161 }
6162 _ => None,
6163 };
6164 }
6165 }
6166 response_info.unsupported_media_types = response
6167 .content
6168 .as_ref()
6169 .into_iter()
6170 .flat_map(|content| content.iter())
6171 .filter(|(media_type, content)| {
6172 match crate::openapi::classify_response_media_type(
6173 media_type,
6174 content.schema.as_ref(),
6175 ) {
6176 crate::openapi::ResponseMediaKind::Json => content.schema.is_none(),
6177 crate::openapi::ResponseMediaKind::Unsupported => true,
6178 crate::openapi::ResponseMediaKind::EventStream
6179 | crate::openapi::ResponseMediaKind::Text
6180 | crate::openapi::ResponseMediaKind::Binary => false,
6181 }
6182 })
6183 .map(|(media_type, _)| media_type.clone())
6184 .collect();
6185 operation_responses.insert(status_code.clone(), response_info);
6186 }
6187 }
6188
6189 if op_info.supports_streaming
6192 && let Some(parameters) = &operation.parameters
6193 {
6194 for param in parameters {
6195 if let Some(name) = param.name.as_deref() {
6196 if name.eq_ignore_ascii_case("stream") {
6197 op_info.stream_parameter = Some(name.to_string());
6198 break;
6199 }
6200 }
6201 }
6202 }
6203
6204 if let Some(parameters) = &operation.parameters {
6206 for (index, param) in parameters.iter().enumerate() {
6207 let resolved = self.resolve_parameter(param).into_owned();
6211 let validation_schema = raw_operation
6212 .as_ref()
6213 .and_then(|operation| operation.get("parameters"))
6214 .and_then(Value::as_array)
6215 .and_then(|parameters| parameters.get(index))
6216 .and_then(|parameter| self.raw_parameter_schema(parameter));
6217 if let Some(param_info) =
6218 self.analyze_parameter(&resolved, operation_id, validation_schema)?
6219 {
6220 op_info.parameters.push(param_info);
6221 }
6222 }
6223 }
6224
6225 if let Some(path_params) = path_item_parameters {
6227 let existing_keys: std::collections::HashSet<(String, String)> = op_info
6228 .parameters
6229 .iter()
6230 .map(|p| (p.name.clone(), p.location.clone()))
6231 .collect();
6232 for (index, param) in path_params.iter().enumerate() {
6233 let resolved = self.resolve_parameter(param).into_owned();
6234 let validation_schema = raw_path_item
6235 .as_ref()
6236 .and_then(|path_item| path_item.get("parameters"))
6237 .and_then(Value::as_array)
6238 .and_then(|parameters| parameters.get(index))
6239 .and_then(|parameter| self.raw_parameter_schema(parameter));
6240 if let Some(param_info) =
6241 self.analyze_parameter(&resolved, operation_id, validation_schema)?
6242 {
6243 if !existing_keys
6244 .contains(&(param_info.name.clone(), param_info.location.clone()))
6245 {
6246 op_info.parameters.push(param_info);
6247 }
6248 }
6249 }
6250 }
6251
6252 let mut declared_path_names: std::collections::HashSet<String> = op_info
6260 .parameters
6261 .iter()
6262 .filter(|p| p.location == "path")
6263 .map(|p| p.name.clone())
6264 .collect();
6265 let bytes = path.as_bytes().iter();
6266 let mut current = String::new();
6267 let mut in_brace = false;
6268 let mut synthesized: Vec<String> = Vec::new();
6269 for b in bytes {
6270 match *b {
6271 b'{' => {
6272 in_brace = true;
6273 current.clear();
6274 }
6275 b'}' if in_brace => {
6276 in_brace = false;
6277 if !current.is_empty() && !declared_path_names.contains(¤t) {
6278 synthesized.push(current.clone());
6279 declared_path_names.insert(current.clone());
6280 }
6281 }
6282 _ if in_brace => current.push(*b as char),
6283 _ => {}
6284 }
6285 }
6286 for name in synthesized {
6287 eprintln!(
6288 "⚠️ path `{}` references `{{{}}}` but the spec doesn't declare it as a parameter — synthesizing as required String",
6289 path, name
6290 );
6291 op_info.parameters.push(ParameterInfo {
6292 name,
6293 location: "path".to_string(),
6294 required: true,
6295 schema_ref: None,
6296 rust_type: "String".to_string(),
6297 description: None,
6298 enum_values: None,
6299 enum_varnames: None,
6300 rust_ident: None,
6301 query_serialization: None,
6302 validation_schema: None,
6303 });
6304 }
6305
6306 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
6314 for p in op_info.parameters.iter_mut() {
6315 let raw = base_param_ident(&p.name);
6316 let mut chosen = raw.clone();
6317 let mut suffix = 2;
6318 while !used.insert(chosen.clone()) {
6319 chosen = format!("{raw}_{suffix}");
6320 suffix += 1;
6321 }
6322 p.rust_ident = Some(chosen);
6323 }
6324
6325 Ok((op_info, operation_responses))
6326 }
6327
6328 fn resolve_request_body(
6330 &self,
6331 request_body: &crate::openapi::RequestBody,
6332 ) -> Result<crate::openapi::RequestBody> {
6333 let mut current = request_body.clone();
6334 let mut visited = HashSet::new();
6335 while let Some(reference) = current.reference.clone() {
6336 if !visited.insert(reference.clone()) {
6337 return Err(GeneratorError::CircularDependency(format!(
6338 "request body reference {reference}"
6339 )));
6340 }
6341
6342 let pointer = reference.strip_prefix('#').ok_or_else(|| {
6343 GeneratorError::UnresolvedReference(format!(
6344 "external request body reference `{reference}` is not supported"
6345 ))
6346 })?;
6347 if !pointer.is_empty() && !pointer.starts_with('/') {
6348 return Err(GeneratorError::UnresolvedReference(format!(
6349 "request body reference `{reference}` is not a local JSON Pointer"
6350 )));
6351 }
6352 let value = self.openapi_spec.pointer(pointer).ok_or_else(|| {
6353 GeneratorError::UnresolvedReference(format!(
6354 "request body reference `{reference}` does not exist"
6355 ))
6356 })?;
6357 let object = value.as_object().ok_or_else(|| {
6358 GeneratorError::InvalidSchema(format!(
6359 "request body reference `{reference}` must target an object"
6360 ))
6361 })?;
6362 if !["$ref", "description", "required", "content"]
6363 .iter()
6364 .any(|field| object.contains_key(*field))
6365 {
6366 return Err(GeneratorError::InvalidSchema(format!(
6367 "request body reference `{reference}` does not target a structurally compatible OpenAPI Request Body Object"
6368 )));
6369 }
6370 current = serde_json::from_value(value.clone()).map_err(|error| {
6371 GeneratorError::InvalidSchema(format!(
6372 "request body reference `{reference}` is not a valid OpenAPI Request Body Object: {error}"
6373 ))
6374 })?;
6375 }
6376 Ok(current)
6377 }
6378
6379 fn resolve_response(
6386 &self,
6387 response: &crate::openapi::Response,
6388 ) -> Result<crate::openapi::Response> {
6389 let mut current = response.clone();
6390 let mut visited = HashSet::new();
6391 while let Some(reference) = current.reference.clone() {
6392 if !visited.insert(reference.clone()) {
6393 return Err(GeneratorError::CircularDependency(format!(
6394 "response reference {reference}"
6395 )));
6396 }
6397
6398 let pointer = reference.strip_prefix('#').ok_or_else(|| {
6399 GeneratorError::UnresolvedReference(format!(
6400 "external response reference `{reference}` is not supported"
6401 ))
6402 })?;
6403 if !pointer.is_empty() && !pointer.starts_with('/') {
6404 return Err(GeneratorError::UnresolvedReference(format!(
6405 "response reference `{reference}` is not a local JSON Pointer"
6406 )));
6407 }
6408 let value = self.openapi_spec.pointer(pointer).ok_or_else(|| {
6409 GeneratorError::UnresolvedReference(format!(
6410 "response reference `{reference}` does not exist"
6411 ))
6412 })?;
6413 let object = value.as_object().ok_or_else(|| {
6414 GeneratorError::InvalidSchema(format!(
6415 "response reference `{reference}` must target an object"
6416 ))
6417 })?;
6418 if !["$ref", "description", "headers", "content", "links"]
6419 .iter()
6420 .any(|field| object.contains_key(*field))
6421 {
6422 return Err(GeneratorError::InvalidSchema(format!(
6423 "response reference `{reference}` does not target a structurally compatible OpenAPI Response Object"
6424 )));
6425 }
6426 current = serde_json::from_value(value.clone()).map_err(|error| {
6427 GeneratorError::InvalidSchema(format!(
6428 "response reference `{reference}` is not a valid OpenAPI Response Object: {error}"
6429 ))
6430 })?;
6431 }
6432 Ok(current)
6433 }
6434
6435 fn generate_inline_response_type_name(&self, operation_id: &str, status_code: &str) -> String {
6442 use heck::ToPascalCase;
6443 let base_name = operation_id.replace('.', "_").to_pascal_case();
6444 let suffix = Self::status_code_suffix(status_code);
6445 format!("{}Response{}", base_name, suffix)
6446 }
6447
6448 fn status_code_suffix(status_code: &str) -> String {
6455 match status_code {
6456 "" | "200" => String::new(),
6457 "default" | "Default" => "Default".to_string(),
6458 other if other.chars().all(|c| c.is_ascii_digit()) => other.to_string(),
6459 other => other.to_ascii_lowercase(),
6460 }
6461 }
6462
6463 fn generate_inline_request_type_name(&self, operation_id: &str) -> String {
6465 use heck::ToPascalCase;
6466 let base_name = operation_id.replace('.', "_").to_pascal_case();
6470 format!("{}Request", base_name)
6471 }
6472
6473 fn resolve_or_inline_schema(
6476 &mut self,
6477 schema: &crate::openapi::Schema,
6478 operation_id: &str,
6479 suffix: &str,
6480 ) -> Result<String> {
6481 if let Some(schema_ref) = schema.reference()
6482 && let Some(schema_name) = self.extract_schema_name(schema_ref)
6483 {
6484 return Ok(schema_name.to_string());
6485 }
6486 let synthetic_name = if suffix == "Request" {
6488 self.generate_inline_request_type_name(operation_id)
6489 } else {
6490 self.generate_inline_response_type_name(operation_id, "")
6491 };
6492 let mut deps = HashSet::new();
6493 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
6494 Ok(synthetic_name)
6495 }
6496
6497 fn resolve_parameter<'a>(
6500 &'a self,
6501 param: &'a crate::openapi::Parameter,
6502 ) -> std::borrow::Cow<'a, crate::openapi::Parameter> {
6503 if let Some(ref_str) = param.reference.as_deref() {
6504 if let Some(param_name) = ref_str.strip_prefix("#/components/parameters/") {
6505 if let Some(resolved) = self.component_parameters.get(param_name) {
6506 return std::borrow::Cow::Borrowed(resolved);
6507 }
6508 }
6509 }
6510 std::borrow::Cow::Borrowed(param)
6511 }
6512
6513 fn referenced_schema_is_string_enum(&self, name: &str) -> bool {
6526 if self.resolve_cached_schema(name).is_some_and(|schema| {
6527 matches!(
6528 schema.schema_type,
6529 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. }
6530 )
6531 }) {
6532 return true;
6533 }
6534 let Some(schema_value) = self
6535 .openapi_spec
6536 .get("components")
6537 .and_then(|c| c.get("schemas"))
6538 .and_then(|s| s.get(name))
6539 else {
6540 return false;
6541 };
6542 let is_string_type = schema_value
6543 .get("type")
6544 .and_then(|v| v.as_str())
6545 .map(|s| s == "string")
6546 .unwrap_or(false);
6547 let has_enum_or_const =
6548 schema_value.get("enum").is_some() || schema_value.get("const").is_some();
6549 is_string_type && has_enum_or_const
6550 }
6551
6552 fn resolve_raw_local_reference(&self, value: &Value) -> Option<Value> {
6553 let Some(reference) = value.get("$ref").and_then(Value::as_str) else {
6554 return Some(value.clone());
6555 };
6556 let pointer = reference.strip_prefix('#')?;
6557 self.openapi_spec.pointer(pointer).cloned()
6558 }
6559
6560 fn raw_request_body_schema(
6561 &self,
6562 operation: Option<&Value>,
6563 content_type: &str,
6564 ) -> Option<Value> {
6565 let request_body = operation?.get("requestBody")?;
6566 self.resolve_raw_local_reference(request_body)?
6567 .get("content")?
6568 .get(content_type)?
6569 .get("schema")
6570 .cloned()
6571 }
6572
6573 fn raw_parameter_schema(&self, parameter: &Value) -> Option<Value> {
6574 self.resolve_raw_local_reference(parameter)?
6575 .get("schema")
6576 .cloned()
6577 }
6578
6579 fn analyze_parameter(
6580 &mut self,
6581 param: &crate::openapi::Parameter,
6582 operation_id: &str,
6583 raw_validation_schema: Option<Value>,
6584 ) -> Result<Option<ParameterInfo>> {
6585 use heck::ToPascalCase;
6586
6587 let name = param.name.as_deref().unwrap_or("");
6588 let location = param.location.as_deref().unwrap_or("");
6589 let required = param.required.unwrap_or(false);
6590 let validation_schema = match raw_validation_schema {
6591 Some(schema) => Some(schema),
6592 None => param
6593 .schema
6594 .as_ref()
6595 .map(serde_json::to_value)
6596 .transpose()
6597 .map_err(GeneratorError::ParseError)?,
6598 };
6599
6600 let mut rust_type = "String".to_string();
6601 let mut schema_ref = None;
6602 let mut enum_values: Option<Vec<String>> = None;
6603 let mut enum_varnames: Option<Vec<String>> = None;
6604 let mut query_serialization: Option<QuerySerialization> = None;
6605
6606 let is_query = location == "query";
6612 let is_simple_header = location == "header"
6613 && matches!(param.style.as_deref(), None | Some("simple"))
6614 && param.explode != Some(true);
6615 let form_style = matches!(param.style.as_deref(), None | Some("form"));
6616 let form_exploded = form_style && param.explode.unwrap_or(true);
6617 let deep_object =
6618 param.style.as_deref() == Some("deepObject") && param.explode != Some(false);
6619
6620 let object_serialization = if !is_query {
6621 None
6622 } else if deep_object {
6623 Some(QuerySerialization::DeepObject)
6624 } else if form_exploded {
6625 Some(QuerySerialization::FormExplodedObject)
6626 } else if form_style {
6627 Some(QuerySerialization::FormObject)
6628 } else {
6629 None
6630 };
6631
6632 if let Some(schema) = ¶m.schema {
6633 if let Some(ref_str) = schema.reference() {
6634 if let Some(name) = self.extract_schema_name(ref_str) {
6640 if self.referenced_schema_is_string_enum(name) {
6641 schema_ref = Some(name.to_string());
6642 } else if object_serialization.is_some()
6643 && self.referenced_schema_is_object(name)
6644 {
6645 schema_ref = Some(name.to_string());
6646 query_serialization = if form_exploded && self.uses_aws_query_conventions()
6647 {
6648 match self.referenced_array_struct_item_type(name, 1) {
6649 Some(ArrayItemType::NestedStructRef { properties, .. }) => {
6650 Some(QuerySerialization::FormExplodedNestedObject {
6651 properties,
6652 })
6653 }
6654 _ => object_serialization.clone(),
6655 }
6656 } else {
6657 object_serialization.clone()
6658 };
6659 } else if (is_query && form_style || is_simple_header)
6660 && let Some(item_type) = self.referenced_array_param_item_type(name)
6661 {
6662 schema_ref = Some(name.to_string());
6668 query_serialization = Some(if is_simple_header {
6669 QuerySerialization::SimpleHeaderArray { item_type }
6670 } else if form_exploded {
6671 QuerySerialization::FormExplodedArray { item_type }
6672 } else {
6673 QuerySerialization::FormArray { item_type }
6674 });
6675 }
6676 }
6677 } else if object_serialization.is_some() && Self::schema_is_inline_object(schema) {
6678 let op_pascal = operation_id.replace('.', "_").to_pascal_case();
6683 let param_pascal = name.to_pascal_case();
6684 let synthetic_name = format!("{op_pascal}{param_pascal}");
6685 let mut deps = HashSet::new();
6686 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
6687 schema_ref = Some(synthetic_name.clone());
6688 query_serialization = if form_exploded && self.uses_aws_query_conventions() {
6689 match self.referenced_array_struct_item_type(&synthetic_name, 1) {
6690 Some(ArrayItemType::NestedStructRef { properties, .. }) => {
6691 Some(QuerySerialization::FormExplodedNestedObject { properties })
6692 }
6693 _ => object_serialization.clone(),
6694 }
6695 } else {
6696 object_serialization.clone()
6697 };
6698 } else if (is_query && form_style || is_simple_header)
6699 && matches!(
6700 schema.schema_type(),
6701 Some(crate::openapi::SchemaType::Array)
6702 )
6703 && let Some(item_type) = self.array_param_item_type(schema)
6704 {
6705 query_serialization = Some(if is_simple_header {
6713 QuerySerialization::SimpleHeaderArray { item_type }
6714 } else if form_exploded {
6715 QuerySerialization::FormExplodedArray { item_type }
6716 } else {
6717 QuerySerialization::FormArray { item_type }
6718 });
6719 } else if let Some(schema_type) = schema.schema_type() {
6720 let format = schema.details().format.clone();
6726 rust_type = match schema_type {
6727 crate::openapi::SchemaType::Boolean => "bool".to_string(),
6728 crate::openapi::SchemaType::Integer => {
6729 self.type_mapper.integer_format(format.as_deref()).rust_type
6730 }
6731 crate::openapi::SchemaType::Number => {
6732 self.type_mapper.number_format(format.as_deref()).rust_type
6733 }
6734 crate::openapi::SchemaType::String => "String".to_string(),
6735 _ => "String".to_string(),
6736 };
6737
6738 if matches!(schema_type, crate::openapi::SchemaType::String) {
6739 let details = schema.details();
6740 if details.is_string_enum() {
6741 if let Some(values) = details.string_enum_values() {
6742 if !values.is_empty() {
6743 let op_pascal = operation_id.replace('.', "_").to_pascal_case();
6744 let param_pascal = name.to_pascal_case();
6745 rust_type = format!("{op_pascal}{param_pascal}");
6746 enum_varnames = details
6751 .extra
6752 .get("x-enum-varnames")
6753 .and_then(Value::as_array)
6754 .map(|raw| {
6755 raw.iter()
6756 .filter_map(Value::as_str)
6757 .map(str::to_owned)
6758 .collect::<Vec<_>>()
6759 })
6760 .filter(|names| names.len() == values.len());
6761 enum_values = Some(values);
6762 }
6763 }
6764 }
6765 }
6766 }
6767
6768 if is_query && query_serialization.is_none() {
6769 let referenced_name = schema
6770 .reference()
6771 .and_then(|reference| self.extract_schema_name(reference));
6772 let is_object = referenced_name
6773 .is_some_and(|name| self.referenced_schema_is_object(name))
6774 || Self::schema_is_inline_object(schema);
6775 let is_array = referenced_name
6776 .is_some_and(|name| self.referenced_schema_is_array(name))
6777 || matches!(
6778 schema.schema_type(),
6779 Some(crate::openapi::SchemaType::Array)
6780 );
6781 let is_composed = referenced_name
6782 .is_some_and(|name| self.referenced_schema_is_composed_query_shape(name));
6783 let reason = if param.style.as_deref() == Some("deepObject")
6784 && param.explode == Some(false)
6785 {
6786 Some("style=deepObject with explode=false is undefined by OpenAPI".to_string())
6787 } else if param.style.as_deref() == Some("deepObject") && !is_object {
6788 Some("style=deepObject is defined only for object query parameters".to_string())
6789 } else if is_object {
6790 Some(format!(
6791 "object query parameters do not support style={}",
6792 param.style.as_deref().unwrap_or("form")
6793 ))
6794 } else if is_array && form_style {
6795 Some(
6796 "form array query parameter exceeds the supported nesting bound or contains a non-scalar leaf; supported shapes are scalar arrays, arrays of flat scalar objects, and one nested scalar-object array"
6797 .to_string(),
6798 )
6799 } else if is_array {
6800 Some(format!(
6801 "array query parameters do not yet support style={}",
6802 param.style.as_deref().unwrap_or("form")
6803 ))
6804 } else if is_composed {
6805 Some(
6806 "composed or union query schemas cannot be projected to an unambiguous flat wire shape"
6807 .to_string(),
6808 )
6809 } else {
6810 None
6811 };
6812 if let Some(reason) = reason {
6813 query_serialization = Some(QuerySerialization::Unsupported { reason });
6814 }
6815 }
6816 }
6817
6818 Ok(Some(ParameterInfo {
6819 name: name.to_string(),
6820 location: location.to_string(),
6821 required,
6822 schema_ref,
6823 rust_type,
6824 description: param.description.clone(),
6825 enum_values,
6826 enum_varnames,
6827 rust_ident: None,
6828 query_serialization,
6829 validation_schema,
6830 }))
6831 }
6832
6833 fn array_param_item_type(&self, schema: &crate::openapi::Schema) -> Option<ArrayItemType> {
6842 let items = schema.details().item_schema()?;
6843 let unwrapped = unwrap_annotation_allof(items);
6847 if let Some(ref_str) = unwrapped.reference() {
6848 let name = self.extract_schema_name(ref_str)?;
6849 return self
6850 .referenced_array_scalar_item_type(name)
6851 .or_else(|| self.referenced_array_struct_item_type(name, 1));
6852 }
6853 let format = unwrapped.details().format.clone();
6854 let scalar = match unwrapped.schema_type()? {
6855 crate::openapi::SchemaType::String => "String".to_string(),
6856 crate::openapi::SchemaType::Integer => {
6857 self.type_mapper.integer_format(format.as_deref()).rust_type
6858 }
6859 crate::openapi::SchemaType::Number => {
6860 self.type_mapper.number_format(format.as_deref()).rust_type
6861 }
6862 crate::openapi::SchemaType::Boolean => "bool".to_string(),
6863 _ => return None,
6864 };
6865 Some(ArrayItemType::Scalar(scalar))
6866 }
6867
6868 fn referenced_array_param_item_type(&self, name: &str) -> Option<ArrayItemType> {
6871 let schema = self.resolve_cached_schema(name)?;
6872 let SchemaType::Array { item_type } = &schema.schema_type else {
6873 return None;
6874 };
6875 self.analyzed_array_item_type(item_type)
6876 }
6877
6878 fn analyzed_array_item_type(&self, item_type: &SchemaType) -> Option<ArrayItemType> {
6879 self.analyzed_array_item_type_at_depth(item_type, 1)
6880 }
6881
6882 fn referenced_array_struct_item_type(
6887 &self,
6888 name: &str,
6889 nested_array_depth: usize,
6890 ) -> Option<ArrayItemType> {
6891 let resolved = self.resolve_cached_schema(name)?;
6892 let SchemaType::Object {
6893 properties,
6894 required,
6895 additional_properties,
6896 ..
6897 } = &resolved.schema_type
6898 else {
6899 return None;
6900 };
6901 if properties.is_empty()
6902 || !matches!(additional_properties, ObjectAdditionalProperties::Forbidden)
6903 {
6904 return None;
6905 }
6906 let mut projected = Vec::with_capacity(properties.len());
6907 let mut has_array = false;
6908 for (wire_name, property) in properties {
6909 let value_type = if let Some(scalar) = self.query_scalar_type(&property.schema_type) {
6910 QueryStructPropertyType::Scalar(scalar)
6911 } else {
6912 if nested_array_depth == 0 {
6913 return None;
6914 }
6915 if let Some(array) = self.resolve_query_array_type(&property.schema_type) {
6916 let item_type =
6917 self.analyzed_array_item_type_at_depth(array, nested_array_depth - 1)?;
6918 if matches!(item_type, ArrayItemType::NestedStructRef { .. }) {
6919 return None;
6920 }
6921 has_array = true;
6922 QueryStructPropertyType::Array { item_type }
6923 } else {
6924 has_array = true;
6925 QueryStructPropertyType::Object {
6926 properties: self.query_flat_object_properties(&property.schema_type)?,
6927 }
6928 }
6929 };
6930 projected.push(QueryStructProperty {
6931 wire_name: wire_name.clone(),
6932 required: required.contains(wire_name),
6933 value_type,
6934 });
6935 }
6936 if has_array {
6937 Some(ArrayItemType::NestedStructRef {
6938 schema_name: name.to_string(),
6939 properties: projected,
6940 })
6941 } else {
6942 Some(ArrayItemType::FlatStructRef {
6943 schema_name: name.to_string(),
6944 properties: projected,
6945 })
6946 }
6947 }
6948
6949 fn analyzed_array_item_type_at_depth(
6950 &self,
6951 item_type: &SchemaType,
6952 nested_array_depth: usize,
6953 ) -> Option<ArrayItemType> {
6954 match item_type {
6955 SchemaType::Primitive { rust_type, .. } => {
6956 Some(ArrayItemType::Scalar(rust_type.clone()))
6957 }
6958 SchemaType::Reference { target } => self
6959 .referenced_array_scalar_item_type(target)
6960 .or_else(|| self.referenced_array_struct_item_type(target, nested_array_depth)),
6961 _ => None,
6962 }
6963 }
6964
6965 fn resolve_query_array_type<'a>(
6966 &'a self,
6967 schema_type: &'a SchemaType,
6968 ) -> Option<&'a SchemaType> {
6969 match schema_type {
6970 SchemaType::Array { item_type } => Some(item_type),
6971 SchemaType::Reference { target } => {
6972 let resolved = self.resolve_cached_schema(target)?;
6973 let SchemaType::Array { item_type } = &resolved.schema_type else {
6974 return None;
6975 };
6976 Some(item_type)
6977 }
6978 _ => None,
6979 }
6980 }
6981
6982 fn query_flat_object_properties(
6983 &self,
6984 schema_type: &SchemaType,
6985 ) -> Option<Vec<QueryStructProperty>> {
6986 let schema_type = match schema_type {
6987 SchemaType::Reference { target } => &self.resolve_cached_schema(target)?.schema_type,
6988 other => other,
6989 };
6990 let SchemaType::Object {
6991 properties,
6992 required,
6993 additional_properties,
6994 ..
6995 } = schema_type
6996 else {
6997 return None;
6998 };
6999 if properties.is_empty()
7000 || !matches!(additional_properties, ObjectAdditionalProperties::Forbidden)
7001 {
7002 return None;
7003 }
7004 properties
7005 .iter()
7006 .map(|(wire_name, property)| {
7007 Some(QueryStructProperty {
7008 wire_name: wire_name.clone(),
7009 required: required.contains(wire_name),
7010 value_type: QueryStructPropertyType::Scalar(
7011 self.query_scalar_type(&property.schema_type)?,
7012 ),
7013 })
7014 })
7015 .collect()
7016 }
7017
7018 fn query_scalar_type(&self, schema_type: &SchemaType) -> Option<QueryScalarType> {
7019 match schema_type {
7020 SchemaType::Primitive { rust_type, .. } => match rust_type.as_str() {
7021 "String" => Some(QueryScalarType::String),
7022 "bool" => Some(QueryScalarType::Boolean),
7023 value if value.starts_with('i') || value.starts_with('u') => {
7024 Some(QueryScalarType::Integer)
7025 }
7026 value if value.starts_with('f') => Some(QueryScalarType::Number),
7027 "serde_json::Value" => None,
7028 _ => Some(QueryScalarType::String),
7029 },
7030 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. } => {
7031 Some(QueryScalarType::String)
7032 }
7033 SchemaType::Reference { target } => {
7034 let resolved = self.resolve_cached_schema(target)?;
7035 self.query_scalar_type(&resolved.schema_type)
7036 }
7037 _ => None,
7038 }
7039 }
7040
7041 fn referenced_array_scalar_item_type(&self, name: &str) -> Option<ArrayItemType> {
7049 let resolved = self.resolve_cached_schema(name)?;
7050 let supported = match &resolved.schema_type {
7051 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. } => true,
7052 SchemaType::Primitive { .. } => resolved
7053 .original
7054 .get("type")
7055 .is_some_and(Self::query_scalar_type_value),
7056 _ => false,
7057 };
7058 supported.then(|| ArrayItemType::SchemaRef(name.to_string()))
7059 }
7060
7061 fn query_scalar_type_value(value: &Value) -> bool {
7062 const SCALARS: [&str; 4] = ["string", "integer", "number", "boolean"];
7063 if let Some(value) = value.as_str() {
7064 return SCALARS.contains(&value);
7065 }
7066 let Some(values) = value.as_array() else {
7067 return false;
7068 };
7069 if !values.iter().all(Value::is_string) {
7070 return false;
7071 }
7072 let mut non_null = values
7073 .iter()
7074 .filter_map(Value::as_str)
7075 .filter(|value| *value != "null");
7076 let Some(scalar) = non_null.next() else {
7077 return false;
7078 };
7079 non_null.next().is_none() && SCALARS.contains(&scalar)
7080 }
7081
7082 fn referenced_schema_is_object(&self, name: &str) -> bool {
7086 self.resolve_cached_schema(name)
7087 .is_some_and(|schema| matches!(schema.schema_type, SchemaType::Object { .. }))
7088 }
7089
7090 fn referenced_schema_is_array(&self, name: &str) -> bool {
7091 self.resolve_cached_schema(name)
7092 .is_some_and(|schema| matches!(schema.schema_type, SchemaType::Array { .. }))
7093 }
7094
7095 fn referenced_schema_is_composed_query_shape(&self, name: &str) -> bool {
7096 self.resolve_cached_schema(name).is_some_and(|schema| {
7097 matches!(
7098 schema.schema_type,
7099 SchemaType::Composition { .. }
7100 | SchemaType::Union { .. }
7101 | SchemaType::DiscriminatedUnion { .. }
7102 )
7103 })
7104 }
7105
7106 fn resolve_cached_schema(&self, name: &str) -> Option<&AnalyzedSchema> {
7107 let mut current = name;
7108 let mut visited = HashSet::new();
7109 loop {
7110 if !visited.insert(current) {
7111 return None;
7112 }
7113 let schema = self.resolved_cache.get(current)?;
7114 if let SchemaType::Reference { target } = &schema.schema_type {
7115 current = target;
7116 } else {
7117 return Some(schema);
7118 }
7119 }
7120 }
7121
7122 fn schema_is_inline_object(schema: &crate::openapi::Schema) -> bool {
7124 match schema.schema_type() {
7125 Some(crate::openapi::SchemaType::Object) => true,
7126 None => schema.details().properties.is_some(),
7127 _ => false,
7128 }
7129 }
7130}
7131
7132fn pointer_type_name(pointer: &str) -> String {
7143 use heck::ToPascalCase;
7144
7145 pointer
7146 .split('/')
7147 .skip(1)
7148 .filter(|segment| !matches!(*segment, "components" | "schemas" | "properties"))
7149 .map(|segment| {
7150 segment
7151 .replace("~1", "/")
7152 .replace("~0", "~")
7153 .to_pascal_case()
7154 })
7155 .collect::<String>()
7156}
7157
7158fn shared_positional_schema(positions: &[Schema]) -> Option<&Schema> {
7163 let first = positions.first()?;
7164 let key = positional_schema_key(first)?;
7165 positions
7166 .iter()
7167 .skip(1)
7168 .all(|position| positional_schema_key(position).as_deref() == Some(key.as_str()))
7169 .then_some(first)
7170}
7171
7172fn positional_schema_key(schema: &Schema) -> Option<String> {
7173 if let Some(reference) = schema.reference() {
7174 return Some(format!("$ref {reference}"));
7175 }
7176 let details = schema.details();
7177 if details.properties.is_some() || details.enum_values.is_some() || details.items.is_some() {
7178 return None;
7179 }
7180 match schema.schema_type()? {
7181 crate::openapi::SchemaType::Object | crate::openapi::SchemaType::Array => None,
7182 scalar => Some(format!(
7183 "{scalar:?} {}",
7184 details.format.as_deref().unwrap_or_default()
7185 )),
7186 }
7187}
7188
7189fn parse_spec_document(openapi_spec: &Value) -> Result<OpenApiSpec> {
7190 serde_path_to_error::deserialize(openapi_spec).map_err(|error| {
7191 let mut pointer = json_pointer(error.path());
7192 pointer.push_str(&refine_schema_failure(openapi_spec, &pointer));
7196 GeneratorError::ParseErrorAt {
7197 pointer,
7198 message: error.into_inner().to_string(),
7199 }
7200 })
7201}
7202
7203const SUBSCHEMA_KEYWORDS: [&str; 11] = [
7205 "items",
7206 "additionalProperties",
7207 "propertyNames",
7208 "unevaluatedProperties",
7209 "unevaluatedItems",
7210 "contains",
7211 "contentSchema",
7212 "if",
7213 "then",
7214 "else",
7215 "not",
7216];
7217
7218const SUBSCHEMA_LIST_KEYWORDS: [&str; 4] = ["oneOf", "anyOf", "allOf", "prefixItems"];
7220
7221const SUBSCHEMA_MAP_KEYWORDS: [&str; 5] = [
7223 "properties",
7224 "patternProperties",
7225 "dependentSchemas",
7226 "$defs",
7227 "definitions",
7228];
7229
7230const REFINE_PARSE_BUDGET: usize = 20_000;
7234
7235fn refine_schema_failure(openapi_spec: &Value, pointer: &str) -> String {
7243 let Some(path) = pointer.strip_prefix('#') else {
7244 return String::new();
7245 };
7246 let Some(node) = openapi_spec.pointer(path) else {
7247 return String::new();
7248 };
7249 let segments = path.split('/').skip(1).collect::<Vec<_>>();
7250 let last = segments.last().copied().unwrap_or_default();
7251 let parent = segments
7252 .len()
7253 .checked_sub(2)
7254 .map(|index| segments[index])
7255 .unwrap_or_default();
7256
7257 if last == "schema" || holds_schemas(parent) {
7258 return if parses_as_schema(node) {
7259 String::new()
7260 } else {
7261 deepest_schema_failure(node)
7262 };
7263 }
7264
7265 let mut budget = REFINE_PARSE_BUDGET;
7266 locate_failing_schema(node, holds_schemas(last), &mut budget).unwrap_or_default()
7267}
7268
7269fn holds_schemas(key: &str) -> bool {
7272 matches!(key, "schemas" | "$defs" | "definitions")
7273}
7274
7275fn locate_failing_schema(
7283 node: &Value,
7284 children_are_schemas: bool,
7285 budget: &mut usize,
7286) -> Option<String> {
7287 for (segment, key, child) in child_nodes(node) {
7288 if *budget == 0 {
7289 return None;
7290 }
7291 *budget -= 1;
7292 if children_are_schemas || key == "schema" {
7293 if !parses_as_schema(child) {
7294 return Some(format!("/{segment}{}", deepest_schema_failure(child)));
7295 }
7296 continue;
7297 }
7298 if let Some(rest) = locate_failing_schema(child, holds_schemas(key), budget) {
7299 return Some(format!("/{segment}{rest}"));
7300 }
7301 }
7302 None
7303}
7304
7305fn parses_as_schema(node: &Value) -> bool {
7306 Schema::deserialize(node).is_ok()
7307}
7308
7309fn deepest_schema_failure(node: &Value) -> String {
7313 let Some(object) = node.as_object() else {
7314 return String::new();
7315 };
7316
7317 let descend = |segment: String, child: &Value| -> Option<String> {
7318 if parses_as_schema(child) {
7319 return None;
7320 }
7321 Some(format!("/{segment}{}", deepest_schema_failure(child)))
7322 };
7323
7324 for keyword in SUBSCHEMA_KEYWORDS {
7325 if let Some(child) = object.get(keyword)
7326 && let Some(suffix) = descend(escape_pointer_segment(keyword), child)
7327 {
7328 return suffix;
7329 }
7330 }
7331 for keyword in SUBSCHEMA_LIST_KEYWORDS {
7332 if let Some(Value::Array(children)) = object.get(keyword) {
7333 for (index, child) in children.iter().enumerate() {
7334 if let Some(suffix) = descend(
7335 format!("{}/{index}", escape_pointer_segment(keyword)),
7336 child,
7337 ) {
7338 return suffix;
7339 }
7340 }
7341 }
7342 }
7343 for keyword in SUBSCHEMA_MAP_KEYWORDS {
7344 if let Some(Value::Object(children)) = object.get(keyword) {
7345 for (name, child) in children {
7346 if let Some(suffix) = descend(
7347 format!(
7348 "{}/{}",
7349 escape_pointer_segment(keyword),
7350 escape_pointer_segment(name)
7351 ),
7352 child,
7353 ) {
7354 return suffix;
7355 }
7356 }
7357 }
7358 }
7359 String::new()
7360}
7361
7362fn child_nodes(node: &Value) -> Vec<(String, &str, &Value)> {
7368 const DATA_KEYWORDS: [&str; 5] = ["example", "examples", "default", "enum", "const"];
7369
7370 match node {
7371 Value::Object(members) => members
7372 .iter()
7373 .filter(|(name, child)| {
7374 (child.is_object() || child.is_array())
7375 && !name.starts_with("x-")
7376 && !DATA_KEYWORDS.contains(&name.as_str())
7377 })
7378 .map(|(name, child)| (escape_pointer_segment(name), name.as_str(), child))
7379 .collect(),
7380 Value::Array(elements) => elements
7381 .iter()
7382 .enumerate()
7383 .filter(|(_, child)| child.is_object() || child.is_array())
7384 .map(|(index, child)| (index.to_string(), "", child))
7385 .collect(),
7386 _ => Vec::new(),
7387 }
7388}
7389
7390fn escape_pointer_segment(segment: &str) -> String {
7391 segment.replace('~', "~0").replace('/', "~1")
7392}
7393
7394fn json_pointer(path: &serde_path_to_error::Path) -> String {
7398 use serde_path_to_error::Segment;
7399
7400 let mut pointer = String::from("#");
7401 for segment in path.iter() {
7402 match segment {
7403 Segment::Seq { index } => {
7404 pointer.push('/');
7405 pointer.push_str(&index.to_string());
7406 }
7407 Segment::Map { key } | Segment::Enum { variant: key } => {
7408 pointer.push('/');
7409 pointer.push_str(&escape_pointer_segment(key));
7410 }
7411 Segment::Unknown => pointer.push_str("/?"),
7412 }
7413 }
7414 pointer
7415}
7416
7417fn disambiguate_component_schema_names(openapi_spec: &mut Value) {
7418 let Some(schemas) = openapi_spec
7419 .pointer_mut("/components/schemas")
7420 .and_then(Value::as_object_mut)
7421 else {
7422 return;
7423 };
7424
7425 let mut names_by_rust_name = BTreeMap::<String, Vec<String>>::new();
7426 for name in schemas.keys() {
7427 names_by_rust_name
7428 .entry(crate::generator::rust_type_name(name))
7429 .or_default()
7430 .push(name.clone());
7431 }
7432
7433 let mut claimed_rust_names = names_by_rust_name.keys().cloned().collect::<HashSet<_>>();
7436 let mut aliases = BTreeMap::<String, String>::new();
7437
7438 for (rust_name, mut names) in names_by_rust_name {
7439 if names.len() < 2 {
7440 continue;
7441 }
7442
7443 names.sort_by_key(|name| (name != &rust_name, name.clone()));
7446 for source_name in names.into_iter().skip(1) {
7447 let mut suffix = 2;
7448 let replacement = loop {
7449 let candidate = format!("{rust_name}{suffix}");
7450 if claimed_rust_names.insert(candidate.clone()) {
7451 break candidate;
7452 }
7453 suffix += 1;
7454 };
7455
7456 eprintln!(
7457 "⚠️ schema `{source_name}` maps to the existing Rust type `{rust_name}` — disambiguated to `{replacement}`"
7458 );
7459 aliases.insert(source_name, replacement);
7460 }
7461 }
7462
7463 if aliases.is_empty() {
7464 return;
7465 }
7466
7467 let original_schemas = std::mem::take(schemas);
7468 for (name, schema) in original_schemas {
7469 schemas.insert(aliases.get(&name).cloned().unwrap_or(name), schema);
7470 }
7471
7472 rewrite_component_schema_references(openapi_spec, &aliases);
7473}
7474
7475fn disambiguate_analyzed_schema_names(
7476 analysis: &mut SchemaAnalysis,
7477 component_schemas: &BTreeMap<String, Schema>,
7478) {
7479 let mut names_by_rust_name = BTreeMap::<String, Vec<String>>::new();
7480 for name in analysis.schemas.keys() {
7481 names_by_rust_name
7482 .entry(crate::generator::rust_type_name(name))
7483 .or_default()
7484 .push(name.clone());
7485 }
7486
7487 let mut claimed_rust_names = names_by_rust_name.keys().cloned().collect::<HashSet<_>>();
7488 let mut aliases = BTreeMap::<String, String>::new();
7489
7490 for (rust_name, mut names) in names_by_rust_name {
7491 if names.len() < 2 {
7492 continue;
7493 }
7494 names.sort_by_key(|name| {
7495 (
7496 !component_schemas.contains_key(name),
7497 name != &rust_name,
7498 name.clone(),
7499 )
7500 });
7501
7502 for source_name in names.into_iter().skip(1) {
7503 let mut suffix = 2;
7504 let replacement = loop {
7505 let candidate = format!("{rust_name}{suffix}");
7506 if claimed_rust_names.insert(candidate.clone()) {
7507 break candidate;
7508 }
7509 suffix += 1;
7510 };
7511 eprintln!(
7512 "⚠️ generated schema `{source_name}` maps to the existing Rust type `{rust_name}` — disambiguated to `{replacement}`"
7513 );
7514 aliases.insert(source_name, replacement);
7515 }
7516 }
7517
7518 if aliases.is_empty() {
7519 return;
7520 }
7521
7522 let original_schemas = std::mem::take(&mut analysis.schemas);
7523 for (name, mut schema) in original_schemas {
7524 schema.name = renamed_schema_name(&schema.name, &aliases);
7525 schema.dependencies = schema
7526 .dependencies
7527 .into_iter()
7528 .map(|name| renamed_schema_name(&name, &aliases))
7529 .collect();
7530 rewrite_schema_type_names(&mut schema.schema_type, &aliases);
7531 analysis
7532 .schemas
7533 .insert(renamed_schema_name(&name, &aliases), schema);
7534 }
7535
7536 let original_edges = std::mem::take(&mut analysis.dependencies.edges);
7537 for (name, dependencies) in original_edges {
7538 analysis.dependencies.edges.insert(
7539 renamed_schema_name(&name, &aliases),
7540 dependencies
7541 .into_iter()
7542 .map(|name| renamed_schema_name(&name, &aliases))
7543 .collect(),
7544 );
7545 }
7546 analysis.dependencies.recursive_schemas = analysis
7547 .dependencies
7548 .recursive_schemas
7549 .iter()
7550 .map(|name| renamed_schema_name(name, &aliases))
7551 .collect();
7552
7553 analysis.patterns.tagged_enum_schemas = analysis
7554 .patterns
7555 .tagged_enum_schemas
7556 .iter()
7557 .map(|name| renamed_schema_name(name, &aliases))
7558 .collect();
7559 analysis.patterns.untagged_enum_schemas = analysis
7560 .patterns
7561 .untagged_enum_schemas
7562 .iter()
7563 .map(|name| renamed_schema_name(name, &aliases))
7564 .collect();
7565 analysis.patterns.type_mappings = std::mem::take(&mut analysis.patterns.type_mappings)
7566 .into_iter()
7567 .map(|(name, mappings)| {
7568 (
7569 renamed_schema_name(&name, &aliases),
7570 mappings
7571 .into_iter()
7572 .map(|(value, schema_name)| {
7573 (value, renamed_schema_name(&schema_name, &aliases))
7574 })
7575 .collect(),
7576 )
7577 })
7578 .collect();
7579
7580 for operation in analysis.operations.values_mut() {
7581 if let Some(request_body) = &mut operation.request_body {
7582 rewrite_request_body_schema_name(request_body, &aliases);
7583 }
7584 for schema_name in operation.response_schemas.values_mut() {
7585 *schema_name = renamed_schema_name(schema_name, &aliases);
7586 }
7587 for parameter in &mut operation.parameters {
7588 if let Some(schema_name) = &mut parameter.schema_ref {
7589 *schema_name = renamed_schema_name(schema_name, &aliases);
7590 }
7591 if let Some(serialization) = &mut parameter.query_serialization {
7592 rewrite_query_serialization_schema_names(serialization, &aliases);
7593 }
7594 }
7595 }
7596
7597 for responses in analysis.operation_responses.values_mut() {
7598 for response in responses.values_mut() {
7599 if let Some(schema_name) = &mut response.schema_name {
7600 *schema_name = renamed_schema_name(schema_name, &aliases);
7601 }
7602 if let Some(OperationResponseBody::Json { schema_name, .. }) = &mut response.body {
7603 *schema_name = renamed_schema_name(schema_name, &aliases);
7604 }
7605 }
7606 }
7607}
7608
7609fn renamed_schema_name(name: &str, aliases: &BTreeMap<String, String>) -> String {
7610 aliases
7611 .get(name)
7612 .cloned()
7613 .unwrap_or_else(|| name.to_string())
7614}
7615
7616fn rewrite_schema_type_names(schema_type: &mut SchemaType, aliases: &BTreeMap<String, String>) {
7617 match schema_type {
7618 SchemaType::Object {
7619 properties,
7620 additional_properties,
7621 ..
7622 } => {
7623 for property in properties.values_mut() {
7624 rewrite_schema_type_names(&mut property.schema_type, aliases);
7625 }
7626 if let ObjectAdditionalProperties::Typed { value_type } = additional_properties {
7627 rewrite_schema_type_names(value_type, aliases);
7628 }
7629 }
7630 SchemaType::DiscriminatedUnion { variants, .. } => {
7631 for variant in variants {
7632 variant.type_name = renamed_schema_name(&variant.type_name, aliases);
7633 variant.schema_ref = renamed_schema_name(&variant.schema_ref, aliases);
7634 }
7635 }
7636 SchemaType::Union { variants } | SchemaType::Composition { schemas: variants } => {
7637 for variant in variants {
7638 variant.target = renamed_schema_name(&variant.target, aliases);
7639 }
7640 }
7641 SchemaType::Array { item_type } => rewrite_schema_type_names(item_type, aliases),
7642 SchemaType::Untyped { .. } => {}
7643 SchemaType::Tuple { element_types } => {
7644 for element_type in element_types {
7645 rewrite_schema_type_names(element_type, aliases);
7646 }
7647 }
7648 SchemaType::Reference { target } => {
7649 *target = renamed_schema_name(target, aliases);
7650 }
7651 SchemaType::Primitive { .. }
7652 | SchemaType::StringEnum { .. }
7653 | SchemaType::ExtensibleEnum { .. } => {}
7654 }
7655}
7656
7657fn rewrite_request_body_schema_name(
7658 request_body: &mut RequestBodyContent,
7659 aliases: &BTreeMap<String, String>,
7660) {
7661 match request_body {
7662 RequestBodyContent::Json { schema_name, .. }
7663 | RequestBodyContent::FormUrlEncoded { schema_name, .. }
7664 | RequestBodyContent::Multipart { schema_name, .. } => {
7665 *schema_name = renamed_schema_name(schema_name, aliases);
7666 }
7667 _ => {}
7668 }
7669}
7670
7671fn rewrite_query_serialization_schema_names(
7672 serialization: &mut QuerySerialization,
7673 aliases: &BTreeMap<String, String>,
7674) {
7675 match serialization {
7676 QuerySerialization::FormExplodedArray { item_type }
7677 | QuerySerialization::FormArray { item_type }
7678 | QuerySerialization::SimpleHeaderArray { item_type } => {
7679 rewrite_array_item_type_schema_names(item_type, aliases);
7680 }
7681 QuerySerialization::FormExplodedNestedObject { properties } => {
7682 for property in properties {
7683 rewrite_query_property_type_schema_names(&mut property.value_type, aliases);
7684 }
7685 }
7686 _ => {}
7687 }
7688}
7689
7690fn rewrite_array_item_type_schema_names(
7691 item_type: &mut ArrayItemType,
7692 aliases: &BTreeMap<String, String>,
7693) {
7694 match item_type {
7695 ArrayItemType::SchemaRef(name) => *name = renamed_schema_name(name, aliases),
7696 ArrayItemType::FlatStructRef {
7697 schema_name,
7698 properties,
7699 }
7700 | ArrayItemType::NestedStructRef {
7701 schema_name,
7702 properties,
7703 } => {
7704 *schema_name = renamed_schema_name(schema_name, aliases);
7705 for property in properties {
7706 rewrite_query_property_type_schema_names(&mut property.value_type, aliases);
7707 }
7708 }
7709 ArrayItemType::Scalar(_) => {}
7710 }
7711}
7712
7713fn rewrite_query_property_type_schema_names(
7714 property_type: &mut QueryStructPropertyType,
7715 aliases: &BTreeMap<String, String>,
7716) {
7717 match property_type {
7718 QueryStructPropertyType::Array { item_type } => {
7719 rewrite_array_item_type_schema_names(item_type, aliases)
7720 }
7721 QueryStructPropertyType::Object { properties } => {
7722 for property in properties {
7723 rewrite_query_property_type_schema_names(&mut property.value_type, aliases);
7724 }
7725 }
7726 QueryStructPropertyType::Scalar(_) => {}
7727 }
7728}
7729
7730fn rewrite_component_schema_references(value: &mut Value, aliases: &BTreeMap<String, String>) {
7731 match value {
7732 Value::Array(values) => {
7733 for value in values {
7734 rewrite_component_schema_references(value, aliases);
7735 }
7736 }
7737 Value::Object(object) => {
7738 if let Some(Value::String(reference)) = object.get_mut("$ref") {
7739 rewrite_component_schema_reference(reference, aliases);
7740 }
7741
7742 if let Some(Value::Object(mapping)) = object.get_mut("mapping") {
7743 for target_value in mapping.values_mut() {
7744 let Some(target) = target_value.as_str() else {
7745 continue;
7746 };
7747 let replacement = aliases.get(target).cloned().or_else(|| {
7748 let mut target = target.to_string();
7749 rewrite_component_schema_reference(&mut target, aliases).then_some(target)
7750 });
7751 if let Some(replacement) = replacement {
7752 *target_value = Value::String(replacement);
7753 }
7754 }
7755 }
7756
7757 for value in object.values_mut() {
7758 rewrite_component_schema_references(value, aliases);
7759 }
7760 }
7761 _ => {}
7762 }
7763}
7764
7765fn rewrite_component_schema_reference(
7766 reference: &mut String,
7767 aliases: &BTreeMap<String, String>,
7768) -> bool {
7769 const PREFIX: &str = "#/components/schemas/";
7770 let Some(encoded_name) = reference.strip_prefix(PREFIX) else {
7771 return false;
7772 };
7773 let encoded_name = encoded_name.split('/').next().unwrap_or(encoded_name);
7774
7775 for (source, replacement) in aliases {
7776 let encoded_source = source.replace('~', "~0").replace('/', "~1");
7777 if encoded_name == encoded_source {
7778 reference.replace_range(
7779 PREFIX.len()..PREFIX.len() + encoded_source.len(),
7780 replacement,
7781 );
7782 return true;
7783 }
7784 }
7785
7786 false
7787}