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::Nullable { .. }
124 | Self::Untyped { .. } => true,
125 Self::Object { .. }
126 | Self::StringEnum { .. }
127 | Self::ExtensibleEnum { .. }
128 | Self::DiscriminatedUnion { .. }
129 | Self::Union { .. }
130 | Self::Composition { .. } => false,
131 }
132 }
133}
134
135impl UntypedReason {
136 pub fn inline_drop(schema_type: &SchemaType) -> Option<Self> {
139 match schema_type {
140 SchemaType::Composition { .. } => Some(Self::InlineCompositionDropped),
141 SchemaType::Union { .. } | SchemaType::DiscriminatedUnion { .. } => {
142 Some(Self::InlineUnionDropped)
143 }
144 SchemaType::Object { .. } => Some(Self::InlineObjectDropped),
145 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. } => {
146 Some(Self::InlineEnumDropped)
147 }
148 _ => None,
149 }
150 }
151}
152
153fn schema_type_dependencies(schema_type: &SchemaType) -> HashSet<String> {
163 let mut targets = HashSet::new();
164 collect_type_dependencies(schema_type, &mut targets, 0);
165 targets
166}
167
168fn collect_type_dependencies(
169 schema_type: &SchemaType,
170 targets: &mut HashSet<String>,
171 depth: usize,
172) {
173 if depth > UNTYPED_WALK_DEPTH {
174 return;
175 }
176 match schema_type {
177 SchemaType::Reference { target } => {
178 targets.insert(target.clone());
179 }
180 SchemaType::Array { item_type } => collect_type_dependencies(item_type, targets, depth + 1),
181 SchemaType::Nullable { inner_type } => {
182 collect_type_dependencies(inner_type, targets, depth + 1)
183 }
184 SchemaType::Tuple { element_types } => {
185 for element_type in element_types {
186 collect_type_dependencies(element_type, targets, depth + 1);
187 }
188 }
189 SchemaType::Object {
190 properties,
191 additional_properties,
192 ..
193 } => {
194 for property in properties.values() {
195 collect_type_dependencies(&property.schema_type, targets, depth + 1);
196 }
197 if let ObjectAdditionalProperties::Typed { value_type } = additional_properties {
198 collect_type_dependencies(value_type, targets, depth + 1);
199 }
200 }
201 SchemaType::Union { variants, .. } | SchemaType::Composition { schemas: variants } => {
202 for variant in variants {
203 targets.insert(variant.target.clone());
204 }
205 }
206 SchemaType::DiscriminatedUnion { variants, .. } => {
207 for variant in variants {
208 targets.insert(variant.type_name.clone());
209 }
210 }
211 SchemaType::Primitive { .. }
212 | SchemaType::StringEnum { .. }
213 | SchemaType::ExtensibleEnum { .. }
214 | SchemaType::Untyped { .. } => {}
215 }
216}
217
218fn normalize_untyped(schema_type: &mut SchemaType, depth: usize) {
228 if depth > UNTYPED_WALK_DEPTH {
229 return;
230 }
231 match schema_type {
232 SchemaType::Primitive { rust_type, .. } => {
233 let shape = match rust_type.as_str() {
234 "serde_json::Value" => Some(UntypedShape::Value),
235 "Vec<serde_json::Value>" => Some(UntypedShape::ValueArray),
236 _ => None,
237 };
238 if let Some(shape) = shape {
239 *schema_type = SchemaType::Untyped {
240 shape,
241 reason: UntypedReason::Unclassified,
242 };
243 }
244 }
245 SchemaType::Object {
246 properties,
247 additional_properties,
248 ..
249 } => {
250 for property in properties.values_mut() {
251 normalize_untyped(&mut property.schema_type, depth + 1);
252 }
253 if let ObjectAdditionalProperties::Typed { value_type } = additional_properties {
254 normalize_untyped(value_type, depth + 1);
255 }
256 }
257 SchemaType::Array { item_type } => normalize_untyped(item_type, depth + 1),
258 SchemaType::Nullable { inner_type } => normalize_untyped(inner_type, depth + 1),
259 SchemaType::Tuple { element_types } => {
260 for element_type in element_types {
261 normalize_untyped(element_type, depth + 1);
262 }
263 }
264 SchemaType::Untyped { .. }
265 | SchemaType::StringEnum { .. }
266 | SchemaType::ExtensibleEnum { .. }
267 | SchemaType::DiscriminatedUnion { .. }
268 | SchemaType::Union { .. }
269 | SchemaType::Composition { .. }
270 | SchemaType::Reference { .. } => {}
271 }
272}
273
274impl SchemaAnalysis {
275 pub fn untyped_fields(&self) -> Vec<UntypedFinding> {
282 let mut findings = Vec::new();
283 for (name, schema) in &self.schemas {
284 collect_untyped(&schema.schema_type, name, &mut findings, 0);
285 }
286 findings.sort();
287 findings
288 }
289}
290
291const UNTYPED_WALK_DEPTH: usize = 32;
295
296fn collect_untyped(
297 schema_type: &SchemaType,
298 context: &str,
299 findings: &mut Vec<UntypedFinding>,
300 depth: usize,
301) {
302 if depth > UNTYPED_WALK_DEPTH {
303 return;
304 }
305 match schema_type {
306 SchemaType::Untyped { shape, reason } => findings.push(UntypedFinding {
307 context: context.to_string(),
308 shape: *shape,
309 reason: *reason,
310 }),
311 SchemaType::Object {
312 properties,
313 additional_properties,
314 ..
315 } => {
316 for (property_name, property) in properties {
317 let property_context = format!("{context}.{property_name}");
318 if let Some(reason) = UntypedReason::inline_drop(&property.schema_type) {
322 findings.push(UntypedFinding {
323 context: property_context,
324 shape: UntypedShape::Value,
325 reason,
326 });
327 continue;
328 }
329 collect_untyped(
330 &property.schema_type,
331 &property_context,
332 findings,
333 depth + 1,
334 );
335 }
336 match additional_properties {
337 ObjectAdditionalProperties::Untyped => findings.push(UntypedFinding {
338 context: format!("{context}.<additionalProperties>"),
339 shape: UntypedShape::ValueMap,
340 reason: UntypedReason::UntypedAdditionalProperties,
341 }),
342 ObjectAdditionalProperties::Typed { value_type } => collect_untyped(
343 value_type,
344 &format!("{context}.<additionalProperties>"),
345 findings,
346 depth + 1,
347 ),
348 ObjectAdditionalProperties::Forbidden => {}
349 }
350 }
351 SchemaType::Array { item_type } => {
352 let element_context = format!("{context}[]");
353 if let Some(reason) = UntypedReason::inline_drop(item_type) {
354 findings.push(UntypedFinding {
355 context: element_context,
356 shape: UntypedShape::Value,
357 reason,
358 });
359 } else {
360 collect_untyped(item_type, &element_context, findings, depth + 1);
361 }
362 }
363 SchemaType::Nullable { inner_type } => {
364 collect_untyped(inner_type, context, findings, depth + 1)
365 }
366 SchemaType::Tuple { element_types } => {
367 for (index, element_type) in element_types.iter().enumerate() {
368 collect_untyped(
369 element_type,
370 &format!("{context}[{index}]"),
371 findings,
372 depth + 1,
373 );
374 }
375 }
376 SchemaType::Union { variants, .. } | SchemaType::Composition { schemas: variants } => {
379 for (index, variant) in variants.iter().enumerate() {
380 if let Some(shape) = untyped_shape_of(&variant.target) {
381 findings.push(UntypedFinding {
382 context: format!("{context}|{index}"),
383 shape,
384 reason: UntypedReason::UntypedUnionBranch,
385 });
386 }
387 }
388 }
389 SchemaType::Primitive { .. }
390 | SchemaType::StringEnum { .. }
391 | SchemaType::ExtensibleEnum { .. }
392 | SchemaType::DiscriminatedUnion { .. }
393 | SchemaType::Reference { .. } => {}
394 }
395}
396
397fn untyped_shape_of(rust_type: &str) -> Option<UntypedShape> {
399 match rust_type {
400 "serde_json::Value" => Some(UntypedShape::Value),
401 "Vec<serde_json::Value>" => Some(UntypedShape::ValueArray),
402 _ => None,
403 }
404}
405
406#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
409pub struct UntypedFinding {
410 pub context: String,
413 pub shape: UntypedShape,
415 pub reason: UntypedReason,
417}
418
419#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
421#[serde(rename_all = "snake_case")]
422pub enum UntypedShape {
423 Value,
425 ValueArray,
427 ValueMap,
429}
430
431#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
439#[serde(rename_all = "kebab-case")]
440pub enum UntypedReason {
441 AnySchema,
444 OpaqueObject,
447 UntypedAdditionalProperties,
449 ArrayWithoutItems,
451 OpenPositionalItems,
454 UnrepresentableUnion,
457 UnrepresentableComposition,
459 UnsupportedTypeKeyword,
461 UnresolvedReference,
463 InlineCompositionDropped,
468 InlineUnionDropped,
470 InlineObjectDropped,
472 InlineEnumDropped,
474 UntypedUnionBranch,
478 NeverMatches,
482 Unclassified,
485}
486
487impl UntypedReason {
488 pub fn verdict(self) -> UntypedVerdict {
491 match self {
492 Self::AnySchema | Self::OpaqueObject | Self::UntypedAdditionalProperties => {
494 UntypedVerdict::Faithful
495 }
496 Self::NeverMatches => UntypedVerdict::Faithful,
498 Self::ArrayWithoutItems | Self::OpenPositionalItems => UntypedVerdict::Faithful,
502 Self::InlineCompositionDropped
505 | Self::InlineUnionDropped
506 | Self::InlineObjectDropped
507 | Self::InlineEnumDropped => UntypedVerdict::Recoverable,
508 Self::UnrepresentableUnion
510 | Self::UnrepresentableComposition
511 | Self::UnsupportedTypeKeyword
512 | Self::UnresolvedReference => UntypedVerdict::Recoverable,
513 Self::UntypedUnionBranch | Self::Unclassified => UntypedVerdict::Unknown,
514 }
515 }
516}
517
518#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
520#[serde(rename_all = "snake_case")]
521pub enum UntypedVerdict {
522 Faithful,
524 Recoverable,
526 Unknown,
528}
529
530#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize)]
532pub struct OperationResponse {
533 pub schema_name: Option<String>,
535 pub media_type: Option<String>,
537 pub body: Option<OperationResponseBody>,
541 pub supports_streaming: bool,
543 pub has_content: bool,
545 pub unsupported_media_types: Vec<String>,
547}
548
549#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
554#[serde(tag = "kind", rename_all = "snake_case")]
555pub enum OperationResponseBody {
556 Json {
557 schema_name: String,
558 media_type: String,
559 },
560 Text {
561 media_type: String,
562 },
563 Binary {
564 media_type: String,
565 wildcard: bool,
566 },
567}
568
569#[derive(Debug, Clone, Default)]
570pub struct ValidationContext {
571 pub openapi_version: String,
572 pub json_schema_dialect: Option<String>,
573 pub component_schemas: BTreeMap<String, Value>,
574}
575
576#[derive(Debug, Clone, Default)]
581pub struct EnumExtensions {
582 pub varnames: Vec<String>,
587 pub descriptions: Vec<String>,
589}
590
591#[derive(Debug, Clone)]
592pub struct AnalyzedSchema {
593 pub name: String,
594 pub original: Value,
595 pub schema_type: SchemaType,
596 pub dependencies: HashSet<String>,
597 pub nullable: bool,
598 pub description: Option<String>,
599 pub default: Option<serde_json::Value>,
600}
601
602#[derive(Debug, Clone)]
603pub enum SchemaType {
604 Primitive {
610 rust_type: String,
611 serde_with: Option<String>,
612 },
613 Object {
615 properties: BTreeMap<String, PropertyInfo>,
616 required: HashSet<String>,
617 additional_properties: ObjectAdditionalProperties,
618 variant: Option<SchemaRef>,
623 },
624 DiscriminatedUnion {
626 discriminator_field: String,
627 variants: Vec<UnionVariant>,
628 exclusive: bool,
632 },
633 Union {
637 variants: Vec<SchemaRef>,
638 exclusive: bool,
639 },
640 Array { item_type: Box<SchemaType> },
642 Nullable { inner_type: Box<SchemaType> },
647 Tuple { element_types: Vec<SchemaType> },
653 StringEnum { values: Vec<String> },
655 ExtensibleEnum { known_values: Vec<String> },
657 Composition { schemas: Vec<SchemaRef> },
659 Reference { target: String },
661 Untyped {
668 shape: UntypedShape,
669 reason: UntypedReason,
670 },
671}
672
673#[derive(Debug, Clone)]
678pub enum ObjectAdditionalProperties {
679 Forbidden,
684 Untyped,
687 Typed { value_type: Box<SchemaType> },
690}
691
692impl ObjectAdditionalProperties {
693 pub fn is_open(&self) -> bool {
696 !matches!(self, Self::Forbidden)
697 }
698}
699
700#[derive(Debug, Clone)]
701pub struct PropertyInfo {
702 pub schema_type: SchemaType,
703 pub nullable: bool,
704 pub description: Option<String>,
705 pub default: Option<serde_json::Value>,
706 pub serde_attrs: Vec<String>,
707 pub synthesized_required: bool,
712 pub constraints: PropertyConstraints,
717}
718
719#[derive(Debug, Clone, Default)]
724pub struct PropertyConstraints {
725 pub minimum: Option<f64>,
726 pub maximum: Option<f64>,
727 pub exclusive_minimum: Option<f64>,
728 pub exclusive_maximum: Option<f64>,
729 pub multiple_of: Option<f64>,
730 pub min_length: Option<u64>,
731 pub max_length: Option<u64>,
732 pub pattern: Option<String>,
733 pub min_items: Option<u64>,
734 pub max_items: Option<u64>,
735 pub unique_items: Option<bool>,
736}
737
738impl PropertyConstraints {
739 pub fn is_empty(&self) -> bool {
740 self.minimum.is_none()
741 && self.maximum.is_none()
742 && self.exclusive_minimum.is_none()
743 && self.exclusive_maximum.is_none()
744 && self.multiple_of.is_none()
745 && self.min_length.is_none()
746 && self.max_length.is_none()
747 && self.pattern.is_none()
748 && self.min_items.is_none()
749 && self.max_items.is_none()
750 && self.unique_items.is_none()
751 }
752
753 pub fn from_schema_details(details: &crate::openapi::SchemaDetails) -> Self {
758 use crate::openapi::ExclusiveBound;
759 let exclusive_minimum = match &details.exclusive_minimum {
760 Some(ExclusiveBound::Number(v)) => Some(*v),
761 _ => None,
762 };
763 let exclusive_maximum = match &details.exclusive_maximum {
764 Some(ExclusiveBound::Number(v)) => Some(*v),
765 _ => None,
766 };
767 Self {
768 minimum: details
769 .minimum
770 .as_ref()
771 .and_then(serde_json::Number::as_f64),
772 maximum: details
773 .maximum
774 .as_ref()
775 .and_then(serde_json::Number::as_f64),
776 exclusive_minimum,
777 exclusive_maximum,
778 multiple_of: details.multiple_of,
779 min_length: details.min_length,
780 max_length: details.max_length,
781 pattern: details.pattern.clone(),
782 min_items: details.min_items,
783 max_items: details.max_items,
784 unique_items: details.unique_items,
785 }
786 }
787}
788
789#[derive(Debug, Clone)]
790pub struct UnionVariant {
791 pub rust_name: String,
792 pub type_name: String,
793 pub discriminator_value: String,
797 pub discriminator_values: Vec<String>,
801 pub preferred_discriminator_values: Vec<String>,
806 pub discriminator_field_declared: bool,
810 pub discriminator_field_required: bool,
814 pub schema_ref: String,
815}
816
817#[derive(Debug, Clone)]
818pub struct SchemaRef {
819 pub target: String,
820 pub nullable: bool,
821}
822
823#[derive(Debug, Clone)]
824pub struct DependencyGraph {
825 pub edges: BTreeMap<String, HashSet<String>>,
826 pub recursive_schemas: HashSet<String>,
828}
829
830#[derive(Debug, Clone)]
831pub struct DetectedPatterns {
832 pub tagged_enum_schemas: HashSet<String>,
834 pub untagged_enum_schemas: HashSet<String>,
836 pub type_mappings: BTreeMap<String, BTreeMap<String, String>>,
838}
839
840#[derive(Debug, Clone, Default, serde::Serialize)]
842pub struct OperationInfo {
843 pub operation_id: String,
845 pub method: String,
847 pub path: String,
849 pub summary: Option<String>,
851 pub description: Option<String>,
853 pub request_body: Option<RequestBodyContent>,
855 pub request_body_required: bool,
858 pub response_schemas: BTreeMap<String, String>,
860 pub parameters: Vec<ParameterInfo>,
862 pub supports_streaming: bool,
864 pub stream_parameter: Option<String>,
866 pub tags: Vec<String>,
870}
871
872#[derive(Debug, Clone, serde::Serialize)]
874#[serde(tag = "kind")]
875pub enum RequestBodyContent {
876 Json {
877 schema_name: String,
878 media_type: String,
879 #[serde(skip)]
880 validation_schema: Value,
881 },
882 FormUrlEncoded {
883 schema_name: String,
884 media_type: String,
885 #[serde(skip)]
886 validation_schema: Value,
887 },
888 Multipart {
889 schema_name: String,
890 media_type: String,
891 #[serde(skip)]
892 validation_schema: Value,
893 },
894 OctetStream {
895 media_type: String,
896 },
897 Binary {
898 media_type: String,
899 },
900 TextPlain {
901 media_type: String,
902 },
903 SchemaLess {
907 media_type: String,
908 },
909 Unsupported {
910 media_types: Vec<String>,
911 },
912}
913
914impl RequestBodyContent {
915 pub fn schema_name(&self) -> Option<&str> {
917 match self {
918 Self::Json { schema_name, .. }
919 | Self::FormUrlEncoded { schema_name, .. }
920 | Self::Multipart { schema_name, .. } => Some(schema_name),
921 Self::OctetStream { .. }
922 | Self::Binary { .. }
923 | Self::TextPlain { .. }
924 | Self::SchemaLess { .. }
925 | Self::Unsupported { .. } => None,
926 }
927 }
928}
929
930fn base_param_ident(name: &str) -> String {
934 use heck::ToSnakeCase;
935 let suffix = if name.ends_with("<=") {
936 "_lte"
937 } else if name.ends_with(">=") {
938 "_gte"
939 } else if name.ends_with('<') {
940 "_lt"
941 } else if name.ends_with('>') {
942 "_gt"
943 } else {
944 ""
945 };
946 let stripped = name.trim_end_matches(['<', '>', '=']);
947 let mut snake = stripped.to_snake_case();
948 if snake.is_empty() {
949 snake.push_str("parameter");
950 } else if snake.starts_with(|character: char| character.is_ascii_digit()) {
951 snake.insert(0, '_');
952 }
953 snake.push_str(suffix);
954 snake
955}
956
957#[derive(Debug, Clone, serde::Serialize)]
959pub struct ParameterInfo {
960 pub name: String,
962 pub location: String,
964 pub required: bool,
966 pub schema_ref: Option<String>,
968 pub rust_type: String,
970 pub description: Option<String>,
972 #[serde(skip_serializing_if = "Option::is_none")]
978 pub enum_values: Option<Vec<String>>,
979 #[serde(skip_serializing_if = "Option::is_none")]
985 pub enum_varnames: Option<Vec<String>>,
986 #[serde(skip_serializing_if = "Option::is_none")]
994 pub rust_ident: Option<String>,
995 #[serde(skip_serializing_if = "Option::is_none")]
1004 pub query_serialization: Option<QuerySerialization>,
1005 #[serde(skip)]
1008 pub validation_schema: Option<Value>,
1009}
1010
1011#[derive(Debug, Clone, PartialEq, serde::Serialize)]
1014pub enum QuerySerialization {
1015 FormExplodedObject,
1019 FormExplodedNestedObject {
1025 properties: Vec<QueryStructProperty>,
1026 },
1027 FormObject,
1030 DeepObject,
1033 FormExplodedArray { item_type: ArrayItemType },
1036 FormArray { item_type: ArrayItemType },
1039 SimpleHeaderArray { item_type: ArrayItemType },
1042 Unsupported { reason: String },
1047}
1048
1049#[derive(Debug, Clone, PartialEq, serde::Serialize)]
1056pub enum ArrayItemType {
1057 Scalar(String),
1059 SchemaRef(String),
1061 FlatStructRef {
1067 schema_name: String,
1068 properties: Vec<QueryStructProperty>,
1069 },
1070 NestedStructRef {
1074 schema_name: String,
1075 properties: Vec<QueryStructProperty>,
1076 },
1077}
1078
1079#[derive(Debug, Clone, PartialEq, serde::Serialize)]
1080pub struct QueryStructProperty {
1081 pub wire_name: String,
1082 pub required: bool,
1083 pub value_type: QueryStructPropertyType,
1084}
1085
1086#[derive(Debug, Clone, PartialEq, serde::Serialize)]
1087pub enum QueryStructPropertyType {
1088 Scalar(QueryScalarType),
1089 Array {
1090 item_type: ArrayItemType,
1091 },
1092 Object {
1093 properties: Vec<QueryStructProperty>,
1094 },
1095}
1096
1097#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
1098pub enum QueryScalarType {
1099 String,
1100 Integer,
1101 Number,
1102 Boolean,
1103}
1104
1105impl Default for DependencyGraph {
1106 fn default() -> Self {
1107 Self::new()
1108 }
1109}
1110
1111impl DependencyGraph {
1112 pub fn new() -> Self {
1113 Self {
1114 edges: BTreeMap::new(),
1115 recursive_schemas: HashSet::new(),
1116 }
1117 }
1118
1119 pub fn add_dependency(&mut self, from: String, to: String) {
1120 self.edges.entry(from).or_default().insert(to);
1121 }
1122
1123 pub fn topological_sort(&mut self) -> Result<Vec<String>> {
1125 self.detect_recursive_schemas();
1127
1128 let mut temp_edges = self.edges.clone();
1130 for (schema, deps) in &mut temp_edges {
1131 deps.remove(schema); }
1133
1134 let mut visited = HashSet::new();
1135 let mut temp_visited = HashSet::new();
1136 let mut result = Vec::new();
1137
1138 let mut all_nodes: Vec<_> = temp_edges.keys().collect();
1140 all_nodes.sort();
1141 for node in all_nodes {
1142 if !visited.contains(node) {
1143 self.visit_node_recursive(
1144 node,
1145 &temp_edges,
1146 &mut visited,
1147 &mut temp_visited,
1148 &mut result,
1149 )?;
1150 }
1151 }
1152
1153 result.reverse();
1154 Ok(result)
1155 }
1156
1157 fn detect_recursive_schemas(&mut self) {
1158 for (schema, deps) in &self.edges {
1159 if deps.contains(schema) {
1160 self.recursive_schemas.insert(schema.clone());
1162 } else {
1163 if self.has_cycle_from(schema, schema, &mut HashSet::new()) {
1165 self.recursive_schemas.insert(schema.clone());
1166 }
1167 }
1168 }
1169
1170 for (schema, deps) in &self.edges {
1172 for dep in deps {
1173 if let Some(dep_deps) = self.edges.get(dep) {
1174 if dep_deps.contains(schema) {
1175 self.recursive_schemas.insert(schema.clone());
1177 self.recursive_schemas.insert(dep.clone());
1178 }
1179 }
1180 }
1181 }
1182 }
1183
1184 fn has_cycle_from(&self, start: &str, current: &str, visited: &mut HashSet<String>) -> bool {
1185 if visited.contains(current) {
1186 return false; }
1188
1189 visited.insert(current.to_string());
1190
1191 if let Some(deps) = self.edges.get(current) {
1192 for dep in deps {
1193 if dep == start {
1194 return true; }
1196 if self.has_cycle_from(start, dep, visited) {
1197 return true;
1198 }
1199 }
1200 }
1201
1202 false
1203 }
1204
1205 #[allow(clippy::only_used_in_recursion)]
1206 fn visit_node_recursive(
1207 &self,
1208 node: &str,
1209 temp_edges: &BTreeMap<String, HashSet<String>>,
1210 visited: &mut HashSet<String>,
1211 temp_visited: &mut HashSet<String>,
1212 result: &mut Vec<String>,
1213 ) -> Result<()> {
1214 if temp_visited.contains(node) {
1215 return Ok(());
1217 }
1218
1219 if visited.contains(node) {
1220 return Ok(());
1221 }
1222
1223 temp_visited.insert(node.to_string());
1224
1225 if let Some(dependencies) = temp_edges.get(node) {
1226 let mut sorted_deps: Vec<_> = dependencies.iter().collect();
1228 sorted_deps.sort();
1229 for dep in sorted_deps {
1230 self.visit_node_recursive(dep, temp_edges, visited, temp_visited, result)?;
1231 }
1232 }
1233
1234 temp_visited.remove(node);
1235 visited.insert(node.to_string());
1236 result.push(node.to_string());
1237
1238 Ok(())
1239 }
1240}
1241
1242pub fn merge_schema_extensions(
1245 main_spec: Value,
1246 extension_paths: &[impl AsRef<Path>],
1247) -> Result<Value> {
1248 let mut result = main_spec;
1249
1250 for path in extension_paths {
1251 let extension = load_extension_file(path.as_ref())?;
1252 result = merge_json_objects_with_replacements(result, extension)?;
1253 }
1254
1255 Ok(result)
1256}
1257
1258fn normalize_operation_path(path: &str) -> String {
1265 match path.split_once('#') {
1266 Some((route, _fragment)) if route.starts_with('/') => route.to_string(),
1267 _ => path.to_string(),
1268 }
1269}
1270
1271fn unwrap_annotation_allof(schema: &crate::openapi::Schema) -> &crate::openapi::Schema {
1276 let crate::openapi::Schema::AllOf { all_of, .. } = schema else {
1277 return schema;
1278 };
1279 let mut references = all_of.iter().filter(|s| s.reference().is_some());
1280 let (Some(first), None) = (references.next(), references.next()) else {
1281 return schema;
1282 };
1283 let others_annotation_only = all_of
1284 .iter()
1285 .all(|member| member.reference().is_some() || schema_is_annotation_only(member));
1286 if others_annotation_only {
1287 first
1288 } else {
1289 schema
1290 }
1291}
1292
1293fn schema_is_annotation_only(schema: &crate::openapi::Schema) -> bool {
1298 serde_json::to_value(schema)
1299 .ok()
1300 .and_then(|value| value.as_object().cloned())
1301 .is_some_and(|object| {
1302 object.keys().all(|key| {
1303 matches!(
1304 key.as_str(),
1305 "title"
1306 | "description"
1307 | "deprecated"
1308 | "readOnly"
1309 | "writeOnly"
1310 | "examples"
1311 | "example"
1312 | "default"
1313 | "externalDocs"
1314 | "xml"
1315 | "$comment"
1316 | "nullable"
1317 ) || key.starts_with("x-")
1318 })
1319 })
1320}
1321
1322fn load_extension_file(path: &Path) -> Result<Value> {
1326 let content = std::fs::read_to_string(path).map_err(|e| GeneratorError::FileError {
1327 message: format!("Failed to read file {}: {}", path.display(), e),
1328 })?;
1329
1330 let is_yaml = path
1331 .extension()
1332 .and_then(|extension| extension.to_str())
1333 .is_some_and(|extension| {
1334 extension.eq_ignore_ascii_case("yaml") || extension.eq_ignore_ascii_case("yml")
1335 });
1336
1337 if is_yaml {
1338 crate::spec_source::yaml_to_json_value(&content).map_err(|error| {
1339 GeneratorError::FileError {
1340 message: format!(
1341 "Failed to parse schema extension {} as YAML: {}",
1342 path.display(),
1343 error
1344 ),
1345 }
1346 })
1347 } else {
1348 serde_json::from_str(&content).map_err(|error| GeneratorError::FileError {
1349 message: format!(
1350 "Failed to parse schema extension {} as JSON: {}",
1351 path.display(),
1352 error
1353 ),
1354 })
1355 }
1356}
1357
1358fn merge_json_objects_with_replacements(main: Value, extension: Value) -> Result<Value> {
1360 let replacements = extract_replacement_rules(&extension);
1362
1363 Ok(merge_json_objects_with_rules(
1365 main,
1366 extension,
1367 &replacements,
1368 ))
1369}
1370
1371fn extract_replacement_rules(
1373 extension: &Value,
1374) -> std::collections::HashMap<String, (String, String)> {
1375 let mut rules = std::collections::HashMap::new();
1376
1377 if let Some(x_replacements) = extension.get("x-replacements") {
1378 if let Some(x_replacements_obj) = x_replacements.as_object() {
1379 for (schema_name, replacement_rule) in x_replacements_obj {
1380 if let Some(rule_obj) = replacement_rule.as_object() {
1381 if let (Some(replace), Some(with)) = (
1382 rule_obj.get("replace").and_then(|v| v.as_str()),
1383 rule_obj.get("with").and_then(|v| v.as_str()),
1384 ) {
1385 rules.insert(schema_name.clone(), (replace.to_string(), with.to_string()));
1386 }
1388 }
1389 }
1390 }
1391 }
1392
1393 rules
1394}
1395
1396fn should_replace_variant(
1398 schema_name: &str,
1399 extension_refs: &[String],
1400 replacements: &std::collections::HashMap<String, (String, String)>,
1401) -> bool {
1402 for (replace_schema, with_schema) in replacements.values() {
1404 if schema_name == replace_schema {
1405 let replacement_exists = extension_refs.iter().any(|ext_ref| {
1407 let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
1408 ext_schema_name == with_schema
1409 });
1410
1411 if replacement_exists {
1412 return true;
1413 }
1414 }
1415 }
1416
1417 extension_refs.iter().any(|ext_ref| {
1419 let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
1420 schema_name == ext_schema_name
1421 })
1422}
1423
1424fn merge_json_objects_with_rules(
1429 main: Value,
1430 extension: Value,
1431 replacements: &std::collections::HashMap<String, (String, String)>,
1432) -> Value {
1433 match (main, extension) {
1434 (Value::Object(mut main_obj), Value::Object(ext_obj)) => {
1436 let main_union_keyword = if main_obj.contains_key("oneOf") {
1439 Some("oneOf")
1440 } else if main_obj.contains_key("anyOf") {
1441 Some("anyOf")
1442 } else {
1443 None
1444 };
1445 if let (Some(main_variants), Some(ext_variants)) = (
1446 extract_schema_variants(&Value::Object(main_obj.clone())),
1447 extract_schema_variants(&Value::Object(ext_obj.clone())),
1448 ) {
1449 let union_key = main_union_keyword.unwrap_or("oneOf");
1450 println!(
1451 "🔍 Merging union schemas ({union_key}): {} main variants, {} extension variants",
1452 main_variants.len(),
1453 ext_variants.len()
1454 );
1455 let mut merged_variants = Vec::new();
1458 let extension_refs: Vec<String> = ext_variants
1459 .iter()
1460 .filter_map(|v| v.get("$ref").and_then(|r| r.as_str()))
1461 .map(|s| s.to_string())
1462 .collect();
1463
1464 for main_variant in main_variants {
1466 if let Some(main_ref) = main_variant.get("$ref").and_then(|r| r.as_str()) {
1467 let schema_name = main_ref.split('/').next_back().unwrap_or("");
1469 let should_replace =
1470 should_replace_variant(schema_name, &extension_refs, replacements);
1471
1472 if should_replace {
1473 println!("🔄 REPLACING {} (explicit rule)", schema_name);
1474 }
1475
1476 if !should_replace {
1477 merged_variants.push(main_variant);
1478 }
1479 } else {
1480 merged_variants.push(main_variant);
1482 }
1483 }
1484
1485 for ext_variant in ext_variants {
1487 merged_variants.push(ext_variant);
1488 }
1489
1490 main_obj.remove("oneOf");
1492 main_obj.remove("anyOf");
1493 main_obj.insert(union_key.to_string(), Value::Array(merged_variants));
1494
1495 for (key, ext_value) in ext_obj {
1497 if key != "oneOf" && key != "anyOf" {
1498 match main_obj.get(&key) {
1499 Some(main_value) => {
1500 let merged_value = merge_json_objects_with_rules(
1501 main_value.clone(),
1502 ext_value,
1503 replacements,
1504 );
1505 main_obj.insert(key, merged_value);
1506 }
1507 None => {
1508 main_obj.insert(key, ext_value);
1509 }
1510 }
1511 }
1512 }
1513
1514 return Value::Object(main_obj);
1515 }
1516
1517 for (key, ext_value) in ext_obj {
1519 match main_obj.get(&key) {
1520 Some(main_value) => {
1521 let merged_value = merge_json_objects_with_rules(
1523 main_value.clone(),
1524 ext_value,
1525 replacements,
1526 );
1527 main_obj.insert(key, merged_value);
1528 }
1529 None => {
1530 main_obj.insert(key, ext_value);
1532 }
1533 }
1534 }
1535 Value::Object(main_obj)
1536 }
1537
1538 (Value::Array(mut main_arr), Value::Array(ext_arr)) => {
1540 main_arr.extend(ext_arr);
1541 Value::Array(main_arr)
1542 }
1543
1544 (_, extension) => extension,
1546 }
1547}
1548
1549fn extract_schema_variants(obj: &Value) -> Option<Vec<Value>> {
1551 if let Value::Object(map) = obj {
1552 if let Some(Value::Array(variants)) = map.get("oneOf") {
1553 return Some(variants.clone());
1554 }
1555 if let Some(Value::Array(variants)) = map.get("anyOf") {
1556 return Some(variants.clone());
1557 }
1558 }
1559 None
1560}
1561
1562#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1567enum InlineUnionKind {
1568 OneOf,
1569 AnyOf,
1570}
1571
1572#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
1573enum SchemaIdentity {
1574 Component(String),
1575 Pointer(String),
1576 InlineUnionBranch {
1577 owner_context: String,
1578 union_kind: InlineUnionKind,
1579 original_index: usize,
1580 discriminator: Option<String>,
1581 fingerprint: String,
1582 },
1583 Inline {
1584 context: String,
1585 kind: &'static str,
1586 preferred_name: String,
1587 fingerprint: String,
1588 },
1589}
1590
1591#[derive(Debug, Default)]
1592struct SchemaNameRegistry {
1593 names_by_identity: BTreeMap<SchemaIdentity, String>,
1594 identities_by_name: BTreeMap<String, SchemaIdentity>,
1595}
1596
1597impl SchemaNameRegistry {
1598 fn with_components(component_names: impl IntoIterator<Item = String>) -> Self {
1599 let mut registry = Self::default();
1600 for name in component_names {
1601 let identity = SchemaIdentity::Component(name.clone());
1602 registry
1603 .names_by_identity
1604 .insert(identity.clone(), name.clone());
1605 registry.identities_by_name.insert(name, identity);
1606 }
1607 registry
1608 }
1609
1610 fn component_name(&self, source_name: &str) -> Option<&str> {
1611 self.names_by_identity
1612 .get(&SchemaIdentity::Component(source_name.to_string()))
1613 .map(String::as_str)
1614 }
1615
1616 fn allocate(
1617 &mut self,
1618 identity: SchemaIdentity,
1619 preferred_name: &str,
1620 collision_name: &str,
1621 ) -> String {
1622 if let Some(existing) = self.names_by_identity.get(&identity) {
1623 return existing.clone();
1624 }
1625
1626 let allocated = if !self.identities_by_name.contains_key(preferred_name) {
1627 preferred_name.to_string()
1628 } else if !self.identities_by_name.contains_key(collision_name) {
1629 collision_name.to_string()
1630 } else {
1631 let hash = stable_schema_identity_hash(&identity);
1632 let hashed = format!("{collision_name}{hash:016X}");
1633 if !self.identities_by_name.contains_key(&hashed) {
1634 hashed
1635 } else {
1636 let mut suffix = 2;
1637 loop {
1638 let candidate = format!("{hashed}{suffix}");
1639 if !self.identities_by_name.contains_key(&candidate) {
1640 break candidate;
1641 }
1642 suffix += 1;
1643 }
1644 }
1645 };
1646
1647 self.names_by_identity
1648 .insert(identity.clone(), allocated.clone());
1649 self.identities_by_name.insert(allocated.clone(), identity);
1650 allocated
1651 }
1652}
1653
1654fn stable_schema_identity_hash(identity: &SchemaIdentity) -> u64 {
1658 let bytes = format!("{identity:?}");
1659 bytes
1660 .as_bytes()
1661 .iter()
1662 .fold(0xcbf29ce484222325, |hash, byte| {
1663 (hash ^ u64::from(*byte)).wrapping_mul(0x100000001b3)
1664 })
1665}
1666
1667#[cfg(test)]
1668mod schema_name_registry_tests {
1669 use super::{SchemaIdentity, SchemaNameRegistry};
1670
1671 #[test]
1672 fn component_reservations_are_independent_of_input_traversal_order() {
1673 let component_names = ["ModelApi".to_string(), "OutputFormatContainer".to_string()];
1674 let mut forward = SchemaNameRegistry::with_components(component_names.clone());
1675 let mut reverse = SchemaNameRegistry::with_components(component_names.into_iter().rev());
1676 let inline = SchemaIdentity::Inline {
1677 context: "Model".to_string(),
1678 kind: "property-object",
1679 preferred_name: "ModelApi".to_string(),
1680 fingerprint: r#"{"type":"object"}"#.to_string(),
1681 };
1682
1683 assert_eq!(
1684 forward.allocate(inline.clone(), "ModelApi", "ModelApiInline"),
1685 "ModelApiInline"
1686 );
1687 assert_eq!(
1688 reverse.allocate(inline, "ModelApi", "ModelApiInline"),
1689 "ModelApiInline"
1690 );
1691 assert_eq!(forward.component_name("ModelApi"), Some("ModelApi"));
1692 assert_eq!(reverse.component_name("ModelApi"), Some("ModelApi"));
1693 assert_eq!(
1694 forward.component_name("OutputFormatContainer"),
1695 Some("OutputFormatContainer")
1696 );
1697 assert_eq!(
1698 reverse.component_name("OutputFormatContainer"),
1699 Some("OutputFormatContainer")
1700 );
1701 }
1702
1703 #[test]
1704 fn an_identity_reuses_its_exact_allocated_name() {
1705 let mut registry = SchemaNameRegistry::with_components(["ModelApi".to_string()]);
1706 let inline = SchemaIdentity::Inline {
1707 context: "Model".to_string(),
1708 kind: "property-object",
1709 preferred_name: "ModelApi".to_string(),
1710 fingerprint: r#"{"type":"object"}"#.to_string(),
1711 };
1712
1713 let first = registry.allocate(inline.clone(), "ModelApi", "ModelApiInline");
1714 let repeated = registry.allocate(inline, "Ignored", "IgnoredInline");
1715
1716 assert_eq!(first, "ModelApiInline");
1717 assert_eq!(repeated, first);
1718 assert_eq!(registry.component_name("ModelApi"), Some("ModelApi"));
1719 }
1720}
1721
1722pub struct SchemaAnalyzer {
1723 schemas: BTreeMap<String, Schema>,
1724 resolved_cache: BTreeMap<String, AnalyzedSchema>,
1725 schema_names: SchemaNameRegistry,
1726 openapi_spec: Value,
1727 current_schema_name: Option<String>,
1728 component_parameters: BTreeMap<String, crate::openapi::Parameter>,
1729 type_mapper: TypeMapper,
1734 resolving_pointers: HashSet<String>,
1737}
1738
1739impl SchemaAnalyzer {
1740 fn untyped_value(&self, _context: impl Into<String>, reason: UntypedReason) -> SchemaType {
1744 SchemaType::Untyped {
1745 shape: UntypedShape::Value,
1746 reason,
1747 }
1748 }
1749
1750 fn untyped_value_array(
1752 &self,
1753 _context: impl Into<String>,
1754 reason: UntypedReason,
1755 ) -> SchemaType {
1756 SchemaType::Untyped {
1757 shape: UntypedShape::ValueArray,
1758 reason,
1759 }
1760 }
1761
1762 fn untyped_context(&self, detail: &str) -> String {
1764 match (&self.current_schema_name, detail) {
1765 (Some(schema), "") => schema.clone(),
1766 (Some(schema), detail) => format!("{schema}.{detail}"),
1767 (None, "") => "<anonymous>".to_string(),
1768 (None, detail) => detail.to_string(),
1769 }
1770 }
1771
1772 fn allocate_inline_schema_name(
1773 &mut self,
1774 preferred_name: &str,
1775 collision_name: &str,
1776 kind: &'static str,
1777 schema: &Schema,
1778 ) -> String {
1779 let identity = SchemaIdentity::Inline {
1780 context: self
1781 .current_schema_name
1782 .clone()
1783 .unwrap_or_else(|| "<anonymous>".to_string()),
1784 kind,
1785 preferred_name: preferred_name.to_string(),
1786 fingerprint: serde_json::to_string(schema).unwrap_or_else(|_| format!("{schema:?}")),
1787 };
1788 self.schema_names
1789 .allocate(identity, preferred_name, collision_name)
1790 }
1791
1792 fn with_schema_context<T>(
1797 &mut self,
1798 schema_name: &str,
1799 analyze: impl FnOnce(&mut Self) -> Result<T>,
1800 ) -> Result<T> {
1801 let previous = self.current_schema_name.replace(schema_name.to_string());
1802 let result = analyze(self);
1803 self.current_schema_name = previous;
1804 result
1805 }
1806
1807 fn add_allocated_object_schema(
1808 &mut self,
1809 object_type_name: String,
1810 schema: &Schema,
1811 dependencies: &mut HashSet<String>,
1812 ) -> Result<SchemaType> {
1813 let object_type = self.with_schema_context(&object_type_name, |analyzer| {
1814 analyzer.analyze_object_schema(schema, dependencies)
1815 })?;
1816 self.resolved_cache.insert(
1817 object_type_name.clone(),
1818 AnalyzedSchema {
1819 name: object_type_name.clone(),
1820 original: serde_json::to_value(schema).unwrap_or(Value::Null),
1821 schema_type: object_type,
1822 dependencies: dependencies.clone(),
1823 nullable: false,
1824 description: schema.details().description.clone(),
1825 default: None,
1826 },
1827 );
1828 dependencies.insert(object_type_name.clone());
1829 Ok(SchemaType::Reference {
1830 target: object_type_name,
1831 })
1832 }
1833
1834 fn allocate_inline_union_branch_name(
1835 &mut self,
1836 preferred_name: &str,
1837 owner_context: &str,
1838 union_kind: InlineUnionKind,
1839 original_index: usize,
1840 discriminator: Option<&str>,
1841 schema: &Schema,
1842 ) -> String {
1843 let identity = SchemaIdentity::InlineUnionBranch {
1844 owner_context: owner_context.to_string(),
1845 union_kind,
1846 original_index,
1847 discriminator: discriminator.map(str::to_string),
1848 fingerprint: serde_json::to_string(schema).unwrap_or_else(|_| format!("{schema:?}")),
1849 };
1850 self.schema_names
1851 .allocate(identity, preferred_name, &format!("{preferred_name}Inline"))
1852 }
1853
1854 fn allocate_pointer_schema_name(&mut self, pointer: &str, preferred_name: &str) -> String {
1855 self.schema_names.allocate(
1856 SchemaIdentity::Pointer(pointer.to_string()),
1857 preferred_name,
1858 &format!("{preferred_name}Pointer"),
1859 )
1860 }
1861
1862 fn allocate_synthetic_schema_name(
1863 &mut self,
1864 preferred_name: &str,
1865 collision_name: &str,
1866 kind: &'static str,
1867 fingerprint: String,
1868 ) -> String {
1869 let identity = SchemaIdentity::Inline {
1870 context: self
1871 .current_schema_name
1872 .clone()
1873 .unwrap_or_else(|| "<anonymous>".to_string()),
1874 kind,
1875 preferred_name: preferred_name.to_string(),
1876 fingerprint,
1877 };
1878 self.schema_names
1879 .allocate(identity, preferred_name, collision_name)
1880 }
1881
1882 fn uses_aws_query_conventions(&self) -> bool {
1883 self.openapi_spec
1884 .pointer("/info/x-providerName")
1885 .and_then(Value::as_str)
1886 .is_some_and(|provider| provider.eq_ignore_ascii_case("amazonaws.com"))
1887 }
1888
1889 pub fn new(openapi_spec: Value) -> Result<Self> {
1893 Self::with_type_mapper(openapi_spec, TypeMapper::default())
1894 }
1895
1896 pub fn with_type_mapper(mut openapi_spec: Value, type_mapper: TypeMapper) -> Result<Self> {
1900 disambiguate_component_schema_names(&mut openapi_spec);
1901 let spec: OpenApiSpec = parse_spec_document(&openapi_spec)?;
1902 let schemas = Self::extract_schemas(&spec)?;
1903
1904 let component_parameters = spec
1905 .components
1906 .as_ref()
1907 .and_then(|c| c.parameters.as_ref())
1908 .cloned()
1909 .unwrap_or_default();
1910 let schema_names = SchemaNameRegistry::with_components(schemas.keys().cloned());
1911 Ok(Self {
1912 schemas,
1913 resolved_cache: BTreeMap::new(),
1914 schema_names,
1915 openapi_spec,
1916 current_schema_name: None,
1917 component_parameters,
1918 type_mapper,
1919 resolving_pointers: HashSet::new(),
1920 })
1921 }
1922
1923 pub fn new_with_extensions(
1926 openapi_spec: Value,
1927 extension_paths: &[std::path::PathBuf],
1928 ) -> Result<Self> {
1929 let merged_spec = merge_schema_extensions(openapi_spec, extension_paths)?;
1930 Self::new(merged_spec)
1931 }
1932
1933 pub fn new_with_extensions_and_type_mapper(
1936 openapi_spec: Value,
1937 extension_paths: &[std::path::PathBuf],
1938 type_mapper: TypeMapper,
1939 ) -> Result<Self> {
1940 let merged_spec = merge_schema_extensions(openapi_spec, extension_paths)?;
1941 Self::with_type_mapper(merged_spec, type_mapper)
1942 }
1943
1944 pub fn type_mapper(&self) -> &TypeMapper {
1948 &self.type_mapper
1949 }
1950
1951 fn generate_context_aware_name(
1954 &self,
1955 base_context: &str,
1956 type_hint: &str,
1957 index: usize,
1958 schema: Option<&Schema>,
1959 ) -> String {
1960 if let Some(schema) = schema {
1962 if type_hint == "Array"
1964 && matches!(schema.schema_type(), Some(OpenApiSchemaType::Array))
1965 {
1966 if let Some(items_schema) = schema.details().item_schema() {
1967 if let Some(item_type) = items_schema.schema_type() {
1969 match item_type {
1970 OpenApiSchemaType::Object => {
1971 return format!("{base_context}ItemArray");
1972 }
1973 OpenApiSchemaType::String => {
1974 return format!("{base_context}StringArray");
1975 }
1976 _ => {}
1977 }
1978 }
1979 }
1980 }
1981 }
1982
1983 match type_hint {
1985 "Array" => {
1986 format!("{base_context}Array")
1988 }
1989 "Variant" | "InlineVariant" => {
1990 if index == 0 {
1992 format!("{base_context}{type_hint}")
1993 } else {
1994 format!("{}{}{}", base_context, type_hint, index + 1)
1995 }
1996 }
1997 _ => {
1998 format!("{base_context}{type_hint}{index}")
2000 }
2001 }
2002 }
2003
2004 fn to_pascal_case(&self, s: &str) -> String {
2006 s.split(['_', '-'])
2007 .filter(|part| !part.is_empty())
2008 .map(|part| {
2009 let mut chars = part.chars();
2010 match chars.next() {
2011 None => String::new(),
2012 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
2013 }
2014 })
2015 .collect()
2016 }
2017
2018 fn extract_schemas(spec: &OpenApiSpec) -> Result<BTreeMap<String, Schema>> {
2019 let schemas = spec.components.as_ref().and_then(|c| c.schemas.as_ref());
2024 Ok(schemas
2025 .map(|m| {
2026 m.iter()
2027 .map(|(k, v)| (k.clone(), v.clone()))
2028 .collect::<BTreeMap<_, _>>()
2029 })
2030 .unwrap_or_default())
2031 }
2032
2033 pub fn analyze(&mut self) -> Result<SchemaAnalysis> {
2034 let validation_context = ValidationContext {
2035 openapi_version: self
2036 .openapi_spec
2037 .get("openapi")
2038 .and_then(Value::as_str)
2039 .unwrap_or_default()
2040 .to_string(),
2041 json_schema_dialect: self
2042 .openapi_spec
2043 .get("jsonSchemaDialect")
2044 .and_then(Value::as_str)
2045 .map(str::to_string),
2046 component_schemas: self
2047 .openapi_spec
2048 .pointer("/components/schemas")
2049 .and_then(Value::as_object)
2050 .map(|schemas| {
2051 schemas
2052 .iter()
2053 .map(|(name, schema)| (name.clone(), schema.clone()))
2054 .collect()
2055 })
2056 .unwrap_or_default(),
2057 };
2058 let mut analysis = SchemaAnalysis {
2059 schemas: BTreeMap::new(),
2060 dependencies: DependencyGraph::new(),
2061 patterns: DetectedPatterns {
2062 tagged_enum_schemas: HashSet::new(),
2063 untagged_enum_schemas: HashSet::new(),
2064 type_mappings: BTreeMap::new(),
2065 },
2066 operations: BTreeMap::new(),
2067 operation_responses: BTreeMap::new(),
2068 operation_id_aliases: BTreeMap::new(),
2069 used_type_features: crate::type_mapping::UsedFeatures::default(),
2070 enum_extensions: BTreeMap::new(),
2071 validation_context,
2072 };
2073
2074 self.detect_patterns(&mut analysis.patterns)?;
2076
2077 let schema_names: Vec<String> = self.schemas.keys().cloned().collect();
2079 for schema_name in schema_names {
2080 let analyzed = self.analyze_schema(&schema_name)?;
2081
2082 for dep in &analyzed.dependencies {
2084 analysis
2085 .dependencies
2086 .add_dependency(schema_name.clone(), dep.clone());
2087 }
2088
2089 analysis.schemas.insert(schema_name, analyzed);
2090 }
2091
2092 for (inline_name, inline_schema) in &self.resolved_cache {
2095 if !analysis.schemas.contains_key(inline_name) {
2096 analysis
2098 .schemas
2099 .insert(inline_name.clone(), inline_schema.clone());
2100
2101 for dep in &inline_schema.dependencies {
2103 analysis
2104 .dependencies
2105 .add_dependency(inline_name.clone(), dep.clone());
2106 }
2107
2108 let mut schemas_to_update = Vec::new();
2113 for (schema_name, schema) in &analysis.schemas {
2114 if schema_name == inline_name {
2116 continue;
2117 }
2118
2119 if schema.dependencies.contains(inline_name) {
2120 schemas_to_update.push(schema_name.clone());
2122 }
2123 }
2124
2125 for schema_name in schemas_to_update {
2127 analysis
2128 .dependencies
2129 .add_dependency(schema_name, inline_name.clone());
2130 }
2131 }
2132 }
2133
2134 self.analyze_operations(&mut analysis)?;
2136
2137 for (inline_name, inline_schema) in &self.resolved_cache {
2140 if !analysis.schemas.contains_key(inline_name) {
2141 analysis
2142 .schemas
2143 .insert(inline_name.clone(), inline_schema.clone());
2144
2145 for dep in &inline_schema.dependencies {
2147 analysis
2148 .dependencies
2149 .add_dependency(inline_name.clone(), dep.clone());
2150 }
2151 }
2152 }
2153
2154 disambiguate_analyzed_schema_names(&mut analysis, &self.schemas);
2155
2156 analysis.used_type_features = self.type_mapper.used_features();
2160
2161 for (name, analyzed) in &analysis.schemas {
2166 let enum_value_count = match &analyzed.schema_type {
2167 SchemaType::StringEnum { values } => values.len(),
2168 SchemaType::ExtensibleEnum { known_values } => known_values.len(),
2169 _ => continue,
2170 };
2171 if let Some(ext) = extract_enum_extensions(&analyzed.original, enum_value_count, name) {
2172 analysis.enum_extensions.insert(name.clone(), ext);
2173 }
2174 }
2175
2176 for schema in analysis.schemas.values_mut() {
2177 normalize_untyped(&mut schema.schema_type, 0);
2178 }
2179
2180 Ok(analysis)
2181 }
2182
2183 fn detect_patterns(&self, patterns: &mut DetectedPatterns) -> Result<()> {
2184 for (schema_name, schema) in &self.schemas {
2185 if self.is_discriminated_union(schema) {
2187 patterns.tagged_enum_schemas.insert(schema_name.clone());
2188
2189 if let Some(mappings) = self.extract_type_mappings(schema)? {
2191 patterns.type_mappings.insert(schema_name.clone(), mappings);
2192 }
2193 }
2194 else if self.is_simple_union(schema) {
2196 patterns.untagged_enum_schemas.insert(schema_name.clone());
2197 }
2198 }
2199
2200 Ok(())
2201 }
2202
2203 fn is_discriminated_union(&self, schema: &Schema) -> bool {
2204 if schema.is_discriminated_union() {
2206 return true;
2207 }
2208
2209 if let Some(variants) = schema.union_variants() {
2211 return variants.len() > 2 && self.detect_discriminator_field(variants).is_some();
2212 }
2213
2214 false
2215 }
2216
2217 fn all_variants_have_unique_const_values(&self, variants: &[Schema], field_name: &str) -> bool {
2218 let mut values = HashSet::new();
2219
2220 variants.iter().all(|variant| {
2221 let schema = if let Some(ref_str) = variant.reference() {
2222 let Some(schema_name) = self.extract_schema_name(ref_str) else {
2223 return false;
2224 };
2225 let Some(schema) = self.schemas.get(schema_name) else {
2226 return false;
2227 };
2228 schema
2229 } else {
2230 variant
2231 };
2232
2233 self.extract_discriminator_value_for_field(schema, field_name)
2234 .is_some_and(|value| values.insert(value))
2235 })
2236 }
2237
2238 fn branch_resolves_to_object(&self, schema: &Schema) -> bool {
2247 self.branch_resolves_to_object_inner(schema, &mut HashSet::new())
2248 }
2249
2250 fn branch_resolves_to_object_inner(
2251 &self,
2252 schema: &Schema,
2253 visited_refs: &mut HashSet<String>,
2254 ) -> bool {
2255 if let Some(ref_str) = schema.reference() {
2257 if !visited_refs.insert(ref_str.to_string()) {
2258 return false;
2259 }
2260 let result = match self
2261 .extract_schema_name(ref_str)
2262 .and_then(|n| self.schemas.get(n))
2263 {
2264 Some(target) => self.branch_resolves_to_object_inner(target, visited_refs),
2265 None => false,
2266 };
2267 visited_refs.remove(ref_str);
2268 return result;
2269 }
2270 if let Schema::AllOf { all_of, .. } = schema {
2274 if Self::single_non_object_allof_carrier(all_of).is_some() {
2275 return false;
2276 }
2277 return all_of
2278 .iter()
2279 .filter(|member| !schema_is_annotation_only(member))
2280 .any(|member| self.branch_resolves_to_object_inner(member, visited_refs));
2281 }
2282 if let Some(variants) = schema.union_variants() {
2287 return !variants.is_empty()
2288 && variants
2289 .iter()
2290 .all(|variant| self.branch_resolves_to_object_inner(variant, visited_refs));
2291 }
2292 if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object)) {
2293 return true;
2294 }
2295 if schema.inferred_type() == Some(OpenApiSchemaType::Object) {
2296 return true;
2297 }
2298 false
2301 }
2302
2303 fn detect_discriminator_field(&self, variants: &[Schema]) -> Option<String> {
2307 if variants.is_empty() {
2308 return None;
2309 }
2310
2311 let first_variant = &variants[0];
2313 let first_schema = if let Some(ref_str) = first_variant.reference() {
2314 let schema_name = self.extract_schema_name(ref_str)?;
2315 self.schemas.get(schema_name)?
2316 } else {
2317 first_variant
2318 };
2319
2320 let properties = first_schema.details().properties.as_ref()?;
2321 let mut candidates: Vec<String> = Vec::new();
2322
2323 for (field_name, field_schema) in properties {
2324 let details = field_schema.details();
2325 let is_const = details.const_value.is_some()
2326 || details.enum_values.as_ref().is_some_and(|v| v.len() == 1)
2327 || details.extra.contains_key("const");
2328 if is_const {
2329 candidates.push(field_name.clone());
2330 }
2331 }
2332
2333 if candidates.is_empty() {
2334 return None;
2335 }
2336
2337 candidates.sort_by(|a, b| {
2339 if a == "type" {
2340 std::cmp::Ordering::Less
2341 } else if b == "type" {
2342 std::cmp::Ordering::Greater
2343 } else {
2344 a.cmp(b)
2345 }
2346 });
2347
2348 for candidate in &candidates {
2354 if self.all_variants_have_unique_const_values(variants, candidate) {
2355 return Some(candidate.clone());
2356 }
2357 }
2358
2359 None
2360 }
2361
2362 fn is_simple_union(&self, schema: &Schema) -> bool {
2363 if let Some(variants) = schema.union_variants() {
2364 if variants.len() > 1 && !schema.is_nullable_pattern() {
2366 let has_refs = variants.iter().any(|v| v.is_reference());
2367 return has_refs;
2368 }
2369 }
2370 false
2371 }
2372
2373 fn schema_or_reference_is_nullable(&self, schema: &Schema) -> bool {
2378 let mut current = schema.clone();
2379 let mut visited = HashSet::new();
2380 loop {
2381 if current.is_nullable_any() {
2382 return true;
2383 }
2384 if let Schema::AllOf { all_of, .. } = ¤t
2385 && let Some(carrier) = Self::single_non_object_allof_carrier(all_of)
2386 {
2387 current = carrier.clone();
2388 continue;
2389 }
2390 let Some(reference) = current.reference() else {
2391 return false;
2392 };
2393 if !visited.insert(reference.to_string()) {
2394 return false;
2395 }
2396 let Some(target) = self.reference_target_schema(reference) else {
2397 return false;
2398 };
2399 current = target;
2400 }
2401 }
2402
2403 fn nullable_container_value(&self, schema: &Schema, schema_type: SchemaType) -> SchemaType {
2409 if !self.schema_or_reference_is_nullable(schema)
2410 || matches!(schema_type, SchemaType::Nullable { .. })
2411 || matches!(schema.schema_type(), Some(OpenApiSchemaType::Null))
2412 {
2413 schema_type
2414 } else {
2415 SchemaType::Nullable {
2416 inner_type: Box::new(schema_type),
2417 }
2418 }
2419 }
2420
2421 fn extract_type_mappings(&self, schema: &Schema) -> Result<Option<BTreeMap<String, String>>> {
2422 let variants = schema.union_variants().ok_or_else(|| {
2423 GeneratorError::InvalidSchema("No variants found for discriminated union".to_string())
2424 })?;
2425
2426 let discriminator_field = if let Some(discriminator) = schema.discriminator() {
2428 discriminator.property_name.clone()
2429 } else if let Some(detected) = self.detect_discriminator_field(variants) {
2430 detected
2431 } else {
2432 "type".to_string() };
2434
2435 let mut mappings = BTreeMap::new();
2436
2437 for variant in variants {
2438 if let Some(ref_str) = variant.reference() {
2439 if let Some(type_name) = self.extract_schema_name(ref_str) {
2440 if let Some(variant_schema) = self.schemas.get(type_name) {
2441 if let Some(discriminator_value) = self
2442 .extract_discriminator_value_for_field(
2443 variant_schema,
2444 &discriminator_field,
2445 )
2446 {
2447 mappings.insert(type_name.to_string(), discriminator_value);
2448 }
2449 }
2450 }
2451 }
2452 }
2453
2454 if mappings.is_empty() {
2455 Ok(None)
2456 } else {
2457 Ok(Some(mappings))
2458 }
2459 }
2460
2461 #[allow(dead_code)]
2462 fn extract_discriminator_value(&self, schema: &Schema) -> Option<String> {
2463 self.extract_discriminator_value_for_field(schema, "type")
2464 }
2465
2466 fn extract_discriminator_values_for_field(
2467 &self,
2468 schema: &Schema,
2469 field_name: &str,
2470 ) -> Vec<String> {
2471 self.extract_discriminator_value_domain_for_field(schema, field_name)
2472 .unwrap_or_default()
2473 }
2474
2475 fn extract_discriminator_value_domain_for_field(
2476 &self,
2477 schema: &Schema,
2478 field_name: &str,
2479 ) -> Option<Vec<String>> {
2480 let mut visited_refs = HashSet::new();
2481 self.discriminator_value_domain(schema, field_name, &mut visited_refs)
2482 }
2483
2484 fn discriminator_value_domain(
2491 &self,
2492 schema: &Schema,
2493 field_name: &str,
2494 visited_refs: &mut HashSet<String>,
2495 ) -> Option<Vec<String>> {
2496 if let Some(reference) = schema.reference() {
2497 if !visited_refs.insert(reference.to_string()) {
2498 return None;
2499 }
2500 let result = self
2501 .extract_schema_name(reference)
2502 .and_then(|name| self.schemas.get(name))
2503 .and_then(|target| {
2504 self.discriminator_value_domain(target, field_name, visited_refs)
2505 });
2506 visited_refs.remove(reference);
2507 return result;
2508 }
2509
2510 let own_domain = schema
2511 .details()
2512 .properties
2513 .as_ref()
2514 .and_then(|properties| properties.get(field_name))
2515 .and_then(|property| self.string_constraint_domain(property, visited_refs));
2516
2517 let composition_domain = match schema {
2518 Schema::AllOf { all_of, .. } => {
2519 let mut domain = None;
2520 for member in all_of {
2521 domain = Self::intersect_optional_domains(
2522 domain,
2523 self.discriminator_value_domain(member, field_name, visited_refs),
2524 );
2525 }
2526 domain
2527 }
2528 Schema::AnyOf { any_of, .. } => {
2529 self.union_discriminator_domains(any_of, field_name, visited_refs)
2530 }
2531 Schema::OneOf { one_of, .. } => {
2532 self.union_discriminator_domains(one_of, field_name, visited_refs)
2533 }
2534 Schema::Bool(false) => Some(Vec::new()),
2535 _ => None,
2536 };
2537
2538 Self::intersect_optional_domains(own_domain, composition_domain)
2539 }
2540
2541 fn union_discriminator_domains(
2542 &self,
2543 schemas: &[Schema],
2544 field_name: &str,
2545 visited_refs: &mut HashSet<String>,
2546 ) -> Option<Vec<String>> {
2547 if schemas.is_empty() {
2548 return Some(Vec::new());
2549 }
2550 let mut domain = Vec::new();
2551 for schema in schemas {
2552 let values = self.discriminator_value_domain(schema, field_name, visited_refs)?;
2553 for value in values {
2554 Self::push_unique_string(&mut domain, &value);
2555 }
2556 }
2557 Some(domain)
2558 }
2559
2560 fn string_constraint_domain(
2561 &self,
2562 schema: &Schema,
2563 visited_refs: &mut HashSet<String>,
2564 ) -> Option<Vec<String>> {
2565 let allow_vendor_default =
2566 !self.has_standard_string_constraint(schema, &mut HashSet::new());
2567 self.string_constraint_domain_inner(schema, visited_refs, allow_vendor_default)
2568 }
2569
2570 fn string_constraint_domain_inner(
2571 &self,
2572 schema: &Schema,
2573 visited_refs: &mut HashSet<String>,
2574 allow_vendor_default: bool,
2575 ) -> Option<Vec<String>> {
2576 if let Some(reference) = schema.reference() {
2577 if !visited_refs.insert(reference.to_string()) {
2578 return None;
2579 }
2580 let result = self
2581 .extract_schema_name(reference)
2582 .and_then(|name| self.schemas.get(name))
2583 .and_then(|target| {
2584 self.string_constraint_domain_inner(target, visited_refs, allow_vendor_default)
2585 });
2586 visited_refs.remove(reference);
2587 return result;
2588 }
2589
2590 let details = schema.details();
2591 let mut own_domain = None;
2592 if let Some(value) = details.const_value.as_ref() {
2593 own_domain = Self::intersect_optional_domains(
2594 own_domain,
2595 Some(value.as_str().into_iter().map(str::to_string).collect()),
2596 );
2597 }
2598 if let Some(enum_values) = &details.enum_values {
2599 own_domain = Self::intersect_optional_domains(
2600 own_domain,
2601 Some(
2602 enum_values
2603 .iter()
2604 .filter_map(Value::as_str)
2605 .map(str::to_string)
2606 .collect(),
2607 ),
2608 );
2609 }
2610 if allow_vendor_default
2611 && details
2612 .extra
2613 .get("x-stainless-const")
2614 .and_then(Value::as_bool)
2615 == Some(true)
2616 && let Some(default) = details.default.as_ref().and_then(Value::as_str)
2617 {
2618 own_domain =
2619 Self::intersect_optional_domains(own_domain, Some(vec![default.to_string()]));
2620 }
2621
2622 let composition_domain = match schema {
2623 Schema::AllOf { all_of, .. } => {
2624 let mut domain = None;
2625 for member in all_of {
2626 domain = Self::intersect_optional_domains(
2627 domain,
2628 self.string_constraint_domain_inner(
2629 member,
2630 visited_refs,
2631 allow_vendor_default,
2632 ),
2633 );
2634 }
2635 domain
2636 }
2637 Schema::AnyOf { any_of, .. } => {
2638 self.union_string_constraint_domains(any_of, visited_refs, allow_vendor_default)
2639 }
2640 Schema::OneOf { one_of, .. } => {
2641 self.union_string_constraint_domains(one_of, visited_refs, allow_vendor_default)
2642 }
2643 Schema::Bool(false) => Some(Vec::new()),
2644 _ => None,
2645 };
2646
2647 Self::intersect_optional_domains(own_domain, composition_domain)
2648 }
2649
2650 fn union_string_constraint_domains(
2651 &self,
2652 schemas: &[Schema],
2653 visited_refs: &mut HashSet<String>,
2654 allow_vendor_default: bool,
2655 ) -> Option<Vec<String>> {
2656 if schemas.is_empty() {
2657 return Some(Vec::new());
2658 }
2659 let mut domain = Vec::new();
2660 for schema in schemas {
2661 let values =
2662 self.string_constraint_domain_inner(schema, visited_refs, allow_vendor_default)?;
2663 for value in values {
2664 Self::push_unique_string(&mut domain, &value);
2665 }
2666 }
2667 Some(domain)
2668 }
2669
2670 fn has_standard_string_constraint(
2671 &self,
2672 schema: &Schema,
2673 visited_refs: &mut HashSet<String>,
2674 ) -> bool {
2675 if let Some(reference) = schema.reference() {
2676 if !visited_refs.insert(reference.to_string()) {
2677 return false;
2678 }
2679 let result = self
2680 .extract_schema_name(reference)
2681 .and_then(|name| self.schemas.get(name))
2682 .is_some_and(|target| self.has_standard_string_constraint(target, visited_refs));
2683 visited_refs.remove(reference);
2684 return result;
2685 }
2686
2687 let details = schema.details();
2688 if details.const_value.is_some() || details.enum_values.is_some() {
2689 return true;
2690 }
2691
2692 match schema {
2693 Schema::AllOf { all_of, .. } => all_of
2694 .iter()
2695 .any(|member| self.has_standard_string_constraint(member, visited_refs)),
2696 Schema::AnyOf { any_of, .. } => any_of
2697 .iter()
2698 .any(|member| self.has_standard_string_constraint(member, visited_refs)),
2699 Schema::OneOf { one_of, .. } => one_of
2700 .iter()
2701 .any(|member| self.has_standard_string_constraint(member, visited_refs)),
2702 _ => false,
2703 }
2704 }
2705
2706 fn intersect_optional_domains(
2707 left: Option<Vec<String>>,
2708 right: Option<Vec<String>>,
2709 ) -> Option<Vec<String>> {
2710 match (left, right) {
2711 (None, other) | (other, None) => other,
2712 (Some(left), Some(right)) => Some(
2713 left.into_iter()
2714 .filter(|value| right.contains(value))
2715 .collect(),
2716 ),
2717 }
2718 }
2719
2720 fn push_unique_string(values: &mut Vec<String>, value: &str) {
2721 if !values.iter().any(|existing| existing == value) {
2722 values.push(value.to_string());
2723 }
2724 }
2725
2726 fn discriminator_property_presence(&self, schema: &Schema, field_name: &str) -> (bool, bool) {
2727 self.discriminator_property_presence_inner(schema, field_name, &mut HashSet::new(), 0)
2728 }
2729
2730 fn discriminator_property_presence_inner(
2731 &self,
2732 schema: &Schema,
2733 field_name: &str,
2734 visited_refs: &mut HashSet<String>,
2735 depth: usize,
2736 ) -> (bool, bool) {
2737 if depth > 64 {
2738 return (false, false);
2739 }
2740 if let Some(reference) = schema.reference() {
2741 if !visited_refs.insert(reference.to_string()) {
2742 return (false, false);
2743 }
2744 let result = self
2745 .extract_schema_name(reference)
2746 .and_then(|name| self.schemas.get(name))
2747 .map(|target| {
2748 self.discriminator_property_presence_inner(
2749 target,
2750 field_name,
2751 visited_refs,
2752 depth + 1,
2753 )
2754 })
2755 .unwrap_or((false, false));
2756 visited_refs.remove(reference);
2757 return result;
2758 }
2759
2760 let details = schema.details();
2761 let mut declared = details
2762 .properties
2763 .as_ref()
2764 .is_some_and(|properties| properties.contains_key(field_name));
2765 let mut required = details
2766 .required
2767 .as_ref()
2768 .is_some_and(|names| names.iter().any(|name| name == field_name));
2769
2770 let members = match schema {
2771 Schema::AllOf { all_of, .. } => Some(all_of.as_slice()),
2772 Schema::AnyOf { any_of, .. } => Some(any_of.as_slice()),
2773 Schema::OneOf { one_of, .. } => Some(one_of.as_slice()),
2774 _ => None,
2775 };
2776 if let Some(members) = members {
2777 for member in members {
2778 let (member_declared, member_required) = self
2779 .discriminator_property_presence_inner(
2780 member,
2781 field_name,
2782 visited_refs,
2783 depth + 1,
2784 );
2785 declared |= member_declared;
2786 required |= member_required;
2787 }
2788 }
2789 (declared, required)
2790 }
2791
2792 fn extract_discriminator_value_for_field(
2793 &self,
2794 schema: &Schema,
2795 field_name: &str,
2796 ) -> Option<String> {
2797 self.extract_discriminator_values_for_field(schema, field_name)
2798 .into_iter()
2799 .next()
2800 }
2801
2802 fn get_any_reference<'a>(&self, schema: &'a Schema) -> Option<&'a str> {
2803 schema.reference().or_else(|| schema.recursive_reference())
2804 }
2805
2806 fn extract_schema_name<'a>(&self, ref_str: &'a str) -> Option<&'a str> {
2807 if ref_str == "#" {
2808 return None; }
2810
2811 let parts: Vec<&str> = ref_str.split('/').collect();
2812
2813 if parts.len() == 4 && parts[0] == "#" && parts[2] == "schemas" {
2817 return Some(parts[3]);
2818 }
2819
2820 if parts.len() == 3 && parts[0] == "#" && parts[1] == "definitions" {
2823 return Some(parts[2]);
2824 }
2825
2826 if ref_str.starts_with("#/") {
2830 return None;
2831 }
2832
2833 let last = parts.last()?;
2839 if last.is_empty()
2840 || last.chars().all(|c| c.is_ascii_digit())
2841 || matches!(
2842 *last,
2843 "schema" | "properties" | "items" | "additionalProperties"
2844 )
2845 {
2846 return None;
2847 }
2848 let first = last.chars().next().unwrap_or(' ');
2849 if !first.is_ascii_alphabetic() || !first.is_ascii_uppercase() {
2850 return None;
2851 }
2852 Some(last)
2853 }
2854
2855 fn reference_target_schema(&self, reference: &str) -> Option<Schema> {
2858 if let Some(name) = self.extract_schema_name(reference) {
2859 return self.schemas.get(name).cloned();
2860 }
2861 let pointer = reference.strip_prefix('#')?;
2862 if !pointer.starts_with('/') {
2863 return None;
2864 }
2865 Schema::deserialize(self.openapi_spec.pointer(pointer)?).ok()
2866 }
2867
2868 fn analyze_schema(&mut self, schema_name: &str) -> Result<AnalyzedSchema> {
2869 let emitted_name = self
2873 .schema_names
2874 .component_name(schema_name)
2875 .ok_or_else(|| GeneratorError::UnresolvedReference(schema_name.to_string()))?
2876 .to_string();
2877 if let Some(cached) = self.resolved_cache.get(&emitted_name) {
2878 return Ok(cached.clone());
2879 }
2880
2881 self.current_schema_name = Some(emitted_name.clone());
2883
2884 let schema = self
2885 .schemas
2886 .get(schema_name)
2887 .ok_or_else(|| GeneratorError::UnresolvedReference(schema_name.to_string()))?
2888 .clone();
2889
2890 self.resolved_cache.insert(
2892 emitted_name.clone(),
2893 AnalyzedSchema {
2894 name: emitted_name.clone(),
2895 original: serde_json::to_value(&schema).unwrap_or(Value::Null),
2896 schema_type: SchemaType::Reference {
2897 target: "placeholder".to_string(),
2898 },
2899 dependencies: HashSet::new(),
2900 nullable: false,
2901 description: None,
2902 default: None,
2903 },
2904 );
2905
2906 let analyzed = self.analyze_schema_value(&schema, &emitted_name)?;
2907
2908 self.resolved_cache.insert(emitted_name, analyzed.clone());
2910
2911 Ok(analyzed)
2912 }
2913
2914 fn analyze_schema_value(
2915 &mut self,
2916 schema: &Schema,
2917 schema_name: &str,
2918 ) -> Result<AnalyzedSchema> {
2919 let details = schema.details();
2920 let description = details.description.clone();
2921 let nullable = schema.is_nullable_any();
2925 let mut dependencies = HashSet::new();
2926
2927 let schema_type = match schema {
2928 Schema::Bool(accepts_anything) => self.untyped_value(
2931 self.untyped_context(""),
2932 if *accepts_anything {
2933 UntypedReason::AnySchema
2934 } else {
2935 UntypedReason::NeverMatches
2936 },
2937 ),
2938 Schema::Reference { reference, .. } => {
2939 match self.extract_schema_name(reference) {
2944 Some(name) => {
2945 let target = name.to_string();
2946 dependencies.insert(target.clone());
2947 SchemaType::Reference { target }
2948 }
2949 None => {
2950 let reference = reference.clone();
2951 if let Some(resolved) =
2952 self.resolve_pointer_schema(&reference, &mut dependencies)?
2953 {
2954 resolved
2955 } else {
2956 eprintln!(
2957 "⚠️ unresolvable $ref `{}` — typing as serde_json::Value",
2958 reference
2959 );
2960 self.untyped_value(
2961 format!("$ref {reference}"),
2962 UntypedReason::UnresolvedReference,
2963 )
2964 }
2965 }
2966 }
2967 }
2968 Schema::RecursiveRef { recursive_ref, .. }
2969 | Schema::DynamicRef {
2970 dynamic_ref: recursive_ref,
2971 ..
2972 } => {
2973 if recursive_ref == "#" {
2979 dependencies.insert(schema_name.to_string());
2980 SchemaType::Reference {
2981 target: schema_name.to_string(),
2982 }
2983 } else {
2984 let target = self
2985 .extract_schema_name(recursive_ref)
2986 .unwrap_or(schema_name)
2987 .to_string();
2988 dependencies.insert(target.clone());
2989 SchemaType::Reference { target }
2990 }
2991 }
2992 Schema::Typed { .. } | Schema::TypedMulti { .. } => {
2993 if let Some(non_null_types) = schema.non_null_schema_types() {
2994 let mut variants = Vec::with_capacity(non_null_types.len());
2995 for t in non_null_types {
2996 variants.push(self.build_typed_multi_union_variant(
2997 t,
2998 schema,
2999 schema_name,
3000 &mut dependencies,
3001 )?);
3002 }
3003 SchemaType::Union {
3004 variants,
3005 exclusive: false,
3006 }
3007 } else {
3008 self.analyze_single_typed_schema(
3009 schema,
3010 schema_name,
3011 details,
3012 &mut dependencies,
3013 )?
3014 }
3015 }
3016 Schema::AnyOf {
3017 any_of,
3018 discriminator,
3019 ..
3020 } => {
3021 if Self::union_only_constrains_requiredness(any_of) {
3022 return Ok(AnalyzedSchema {
3023 name: schema_name.to_string(),
3024 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3025 schema_type: self.analyze_empty_union(schema, &mut dependencies)?,
3026 dependencies,
3027 nullable,
3028 description,
3029 default: details.default.clone(),
3030 });
3031 }
3032 if let Some(schema_type) = self.analyze_object_with_variants(
3033 schema,
3034 any_of,
3035 schema_name,
3036 &mut dependencies,
3037 )? {
3038 return Ok(AnalyzedSchema {
3039 name: schema_name.to_string(),
3040 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3041 schema_type,
3042 dependencies,
3043 nullable,
3044 description,
3045 default: details.default.clone(),
3046 });
3047 }
3048 self.analyze_anyof_union(
3050 any_of,
3051 discriminator.as_ref(),
3052 &mut dependencies,
3053 schema_name,
3054 )?
3055 }
3056 Schema::OneOf {
3057 one_of,
3058 discriminator,
3059 ..
3060 } => {
3061 if one_of.is_empty() {
3062 self.analyze_empty_union(schema, &mut dependencies)?
3063 } else if let Some(schema_type) = self.analyze_object_with_variants(
3064 schema,
3065 one_of,
3066 schema_name,
3067 &mut dependencies,
3068 )? {
3069 schema_type
3070 } else {
3071 self.analyze_oneof_union(
3073 one_of,
3074 discriminator.as_ref(),
3075 schema_name,
3076 &mut dependencies,
3077 InlineUnionKind::OneOf,
3078 None,
3079 )?
3080 }
3081 }
3082 Schema::AllOf { all_of, .. } => {
3083 self.analyze_allof_composition(schema, all_of, &mut dependencies)?
3085 }
3086 Schema::Untyped { .. } => {
3087 if let Some(inferred) = schema.inferred_type() {
3089 match inferred {
3090 OpenApiSchemaType::Object => {
3091 if self.should_use_dynamic_json(schema) {
3092 self.untyped_value(
3093 self.untyped_context(""),
3094 UntypedReason::OpaqueObject,
3095 )
3096 } else {
3097 self.analyze_object_schema(schema, &mut dependencies)?
3098 }
3099 }
3100 OpenApiSchemaType::String if details.is_string_enum() => {
3101 SchemaType::StringEnum {
3102 values: details.string_enum_values().unwrap_or_default(),
3103 }
3104 }
3105 OpenApiSchemaType::Null => SchemaType::Primitive {
3108 rust_type: self.type_mapper.null_unit().rust_type,
3109 serde_with: None,
3110 },
3111 _ => self.untyped_value(
3112 self.untyped_context(""),
3113 UntypedReason::UnsupportedTypeKeyword,
3114 ),
3115 }
3116 } else {
3117 self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema)
3118 }
3119 }
3120 };
3121
3122 Ok(AnalyzedSchema {
3123 name: schema_name.to_string(),
3124 original: serde_json::to_value(schema).unwrap_or(Value::Null), schema_type,
3126 dependencies,
3127 nullable,
3128 description,
3129 default: details.default.clone(),
3130 })
3131 }
3132
3133 fn analyze_single_typed_schema(
3139 &mut self,
3140 schema: &Schema,
3141 schema_name: &str,
3142 details: &crate::openapi::SchemaDetails,
3143 dependencies: &mut HashSet<String>,
3144 ) -> Result<SchemaType> {
3145 let primary = schema
3146 .schema_type()
3147 .cloned()
3148 .unwrap_or(OpenApiSchemaType::Object);
3149 let format = details.format.as_deref();
3150 Ok(match primary {
3151 OpenApiSchemaType::String => {
3152 if let Some(values) = details.string_enum_values() {
3153 SchemaType::StringEnum { values }
3154 } else {
3155 let mapped = self.type_mapper.string_format(format);
3156 SchemaType::Primitive {
3157 rust_type: mapped.rust_type,
3158 serde_with: mapped.serde_with,
3159 }
3160 }
3161 }
3162 OpenApiSchemaType::Integer => SchemaType::Primitive {
3163 rust_type: self.integer_rust_type(details),
3164 serde_with: None,
3165 },
3166 OpenApiSchemaType::Number => SchemaType::Primitive {
3167 rust_type: self.type_mapper.number_format(format).rust_type,
3168 serde_with: None,
3169 },
3170 OpenApiSchemaType::Boolean => SchemaType::Primitive {
3171 rust_type: self.type_mapper.boolean().rust_type,
3172 serde_with: None,
3173 },
3174 OpenApiSchemaType::Array => {
3175 self.analyze_array_schema(schema, schema_name, dependencies)?
3176 }
3177 OpenApiSchemaType::Object => {
3178 if self.should_use_dynamic_json(schema) {
3179 self.untyped_value(self.untyped_context(""), UntypedReason::OpaqueObject)
3180 } else {
3181 self.analyze_object_schema(schema, dependencies)?
3182 }
3183 }
3184 OpenApiSchemaType::Null => SchemaType::Primitive {
3187 rust_type: self.type_mapper.null_unit().rust_type,
3188 serde_with: None,
3189 },
3190 })
3191 }
3192
3193 fn analyze_object_schema(
3194 &mut self,
3195 schema: &Schema,
3196 dependencies: &mut HashSet<String>,
3197 ) -> Result<SchemaType> {
3198 let details = schema.details();
3199 let properties = &details.properties;
3200 let required = details
3201 .required
3202 .as_ref()
3203 .map(|req| req.iter().cloned().collect::<HashSet<String>>())
3204 .unwrap_or_default();
3205
3206 let mut property_info = BTreeMap::new();
3207 let owner_name = self
3210 .current_schema_name
3211 .clone()
3212 .unwrap_or_else(|| "Inline".to_string());
3213
3214 if let Some(props) = properties {
3215 for (prop_name, prop_schema) in props {
3216 let prop_type = if let Schema::AnyOf { any_of, .. } = prop_schema {
3218 if Self::union_only_constrains_requiredness(any_of) {
3223 self.analyze_empty_union(prop_schema, dependencies)?
3224 } else if let Some(with_variants) = {
3225 let variant_owner =
3226 format!("{owner_name}{}", self.to_pascal_case(prop_name));
3227 self.with_schema_context(&variant_owner, |analyzer| {
3228 analyzer.analyze_object_with_variants(
3229 prop_schema,
3230 any_of,
3231 &variant_owner,
3232 dependencies,
3233 )
3234 })?
3235 } {
3236 with_variants
3237 } else if self.should_use_dynamic_json(prop_schema) {
3238 self.untyped_value(
3240 self.untyped_context(prop_name),
3241 UntypedReason::OpaqueObject,
3242 )
3243 } else if prop_schema.is_nullable_pattern()
3244 && let Some(non_null) = prop_schema.non_null_variant()
3245 {
3246 self.analyze_property_schema_with_context(
3254 non_null,
3255 Some(prop_name),
3256 dependencies,
3257 )?
3258 } else {
3259 let context_name = self
3262 .current_schema_name
3263 .clone()
3264 .unwrap_or_else(|| "Unknown".to_string());
3265
3266 let prop_pascal = self.to_pascal_case(prop_name);
3268 let preferred_union_name = format!("{context_name}{prop_pascal}");
3269 let union_type_name = self.allocate_inline_schema_name(
3270 &preferred_union_name,
3271 &format!("{preferred_union_name}Union2"),
3272 "property-anyof",
3273 prop_schema,
3274 );
3275
3276 let union_schema_type = self.analyze_anyof_union(
3278 any_of,
3279 prop_schema.discriminator(),
3280 dependencies,
3281 &union_type_name,
3282 )?;
3283
3284 self.resolved_cache.insert(
3286 union_type_name.clone(),
3287 AnalyzedSchema {
3288 name: union_type_name.clone(),
3289 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
3290 dependencies: schema_type_dependencies(&union_schema_type),
3291 schema_type: union_schema_type,
3292 nullable: false,
3293 description: prop_schema.details().description.clone(),
3294 default: None,
3295 },
3296 );
3297
3298 dependencies.insert(union_type_name.clone());
3300 SchemaType::Reference {
3301 target: union_type_name,
3302 }
3303 }
3304 } else if let Schema::OneOf {
3305 one_of,
3306 discriminator,
3307 ..
3308 } = prop_schema
3309 {
3310 if prop_schema.is_nullable_pattern()
3317 && let Some(non_null) = prop_schema.non_null_variant()
3318 {
3319 let unwrapped = self.analyze_property_schema_with_context(
3320 non_null,
3321 Some(prop_name),
3322 dependencies,
3323 )?;
3324 let owner_name = self
3325 .current_schema_name
3326 .clone()
3327 .unwrap_or_else(|| "Inline".to_string());
3328 let unwrapped = self.hoist_inline_property_type(
3329 &owner_name,
3330 prop_name,
3331 unwrapped,
3332 dependencies,
3333 );
3334 let prop_details = prop_schema.details();
3335 let prop_nullable = true;
3336 let prop_description = prop_details.description.clone();
3337 let prop_default = prop_details.default.clone();
3338 property_info.insert(
3339 prop_name.clone(),
3340 PropertyInfo {
3341 schema_type: unwrapped,
3342 nullable: prop_nullable,
3343 description: prop_description,
3344 default: prop_default,
3345 serde_attrs: Vec::new(),
3346 synthesized_required: false,
3347 constraints: PropertyConstraints::from_schema_details(prop_details),
3348 },
3349 );
3350 continue;
3351 }
3352
3353 let context_name = self
3355 .current_schema_name
3356 .clone()
3357 .unwrap_or_else(|| "Unknown".to_string());
3358 let prop_pascal = self.to_pascal_case(prop_name);
3359 let preferred_union_name = format!("{context_name}{prop_pascal}");
3360 let union_type_name = self.allocate_inline_schema_name(
3361 &preferred_union_name,
3362 &format!("{preferred_union_name}Union2"),
3363 "property-oneof",
3364 prop_schema,
3365 );
3366
3367 let union_schema_type = self.analyze_oneof_union(
3369 one_of,
3370 discriminator.as_ref(),
3371 &union_type_name,
3372 dependencies,
3373 InlineUnionKind::OneOf,
3374 None,
3375 )?;
3376
3377 self.resolved_cache.insert(
3379 union_type_name.clone(),
3380 AnalyzedSchema {
3381 name: union_type_name.clone(),
3382 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
3383 schema_type: union_schema_type,
3384 dependencies: HashSet::new(),
3385 nullable: false,
3386 description: prop_schema.details().description.clone(),
3387 default: None,
3388 },
3389 );
3390
3391 dependencies.insert(union_type_name.clone());
3393 SchemaType::Reference {
3394 target: union_type_name,
3395 }
3396 } else {
3397 self.analyze_property_schema_with_context(
3399 prop_schema,
3400 Some(prop_name),
3401 dependencies,
3402 )?
3403 };
3404
3405 let prop_type = self.hoist_inline_property_type(
3406 &owner_name,
3407 prop_name,
3408 prop_type,
3409 dependencies,
3410 );
3411
3412 let prop_details = prop_schema.details();
3413 let prop_nullable = self.schema_or_reference_is_nullable(prop_schema);
3415 let prop_description = prop_details.description.clone();
3416 let prop_default = prop_details.default.clone();
3417
3418 property_info.insert(
3419 prop_name.clone(),
3420 PropertyInfo {
3421 schema_type: prop_type,
3422 nullable: prop_nullable,
3423 description: prop_description,
3424 default: prop_default,
3425 serde_attrs: Vec::new(),
3426 synthesized_required: false,
3427 constraints: PropertyConstraints::from_schema_details(prop_details),
3428 },
3429 );
3430 }
3431 }
3432
3433 let typed_enabled = self
3441 .type_mapper
3442 .config()
3443 .shape
3444 .as_ref()
3445 .and_then(|s| s.additional_properties_typed)
3446 .unwrap_or(true);
3447
3448 let untyped_required_property = || PropertyInfo {
3449 schema_type: SchemaType::Untyped {
3450 shape: UntypedShape::Value,
3451 reason: UntypedReason::AnySchema,
3452 },
3453 nullable: false,
3454 description: None,
3455 default: None,
3456 serde_attrs: Vec::new(),
3457 synthesized_required: true,
3458 constraints: PropertyConstraints::default(),
3459 };
3460 let (mut additional_properties, required_additional_property, explicitly_forbidden) =
3461 match &details.additional_properties {
3462 Some(crate::openapi::AdditionalProperties::Boolean(true)) => (
3463 ObjectAdditionalProperties::Untyped,
3464 Some(untyped_required_property()),
3465 false,
3466 ),
3467 Some(crate::openapi::AdditionalProperties::Boolean(false)) => {
3468 (ObjectAdditionalProperties::Forbidden, None, true)
3469 }
3470 Some(crate::openapi::AdditionalProperties::Schema(value_schema))
3471 if typed_enabled =>
3472 {
3473 let analyzed = self.analyze_property_schema_with_context(
3474 value_schema,
3475 Some("AdditionalProperty"),
3476 dependencies,
3477 )?;
3478 let nullable_value =
3479 self.nullable_container_value(value_schema, analyzed.clone());
3480 let value_details = value_schema.details();
3481 let required_property = PropertyInfo {
3482 schema_type: analyzed.clone(),
3483 nullable: self.schema_or_reference_is_nullable(value_schema),
3484 description: value_details.description.clone(),
3485 default: None,
3488 serde_attrs: Vec::new(),
3489 synthesized_required: true,
3490 constraints: PropertyConstraints::from_schema_details(value_details),
3491 };
3492 (
3493 ObjectAdditionalProperties::Typed {
3494 value_type: Box::new(nullable_value),
3495 },
3496 Some(required_property),
3497 false,
3498 )
3499 }
3500 Some(crate::openapi::AdditionalProperties::Schema(_)) => (
3501 ObjectAdditionalProperties::Untyped,
3504 Some(untyped_required_property()),
3505 false,
3506 ),
3507 None if Self::object_shape_needs_additional_property_carrier(details) => (
3512 ObjectAdditionalProperties::Untyped,
3513 Some(untyped_required_property()),
3514 false,
3515 ),
3516 None => (
3517 ObjectAdditionalProperties::Forbidden,
3518 Some(untyped_required_property()),
3519 false,
3520 ),
3521 };
3522
3523 self.finalize_required_object_members(
3524 &mut property_info,
3525 &required,
3526 &mut additional_properties,
3527 required_additional_property,
3528 explicitly_forbidden,
3529 )?;
3530
3531 Ok(SchemaType::Object {
3532 properties: property_info,
3533 variant: None,
3534 required,
3535 additional_properties,
3536 })
3537 }
3538
3539 fn object_shape_needs_additional_property_carrier(
3545 details: &crate::openapi::SchemaDetails,
3546 ) -> bool {
3547 if details.min_properties.is_some() || details.max_properties.is_some() {
3548 return true;
3549 }
3550
3551 let declared = details.properties.as_ref();
3552 let has_undeclared_key = |value: &Value| {
3553 value.as_object().is_some_and(|object| {
3554 object
3555 .keys()
3556 .any(|key| declared.is_none_or(|properties| !properties.contains_key(key)))
3557 })
3558 };
3559 details.example.as_ref().is_some_and(has_undeclared_key)
3560 || details
3561 .examples
3562 .as_ref()
3563 .is_some_and(|examples| examples.iter().any(has_undeclared_key))
3564 }
3565
3566 fn finalize_required_object_members(
3571 &self,
3572 properties: &mut BTreeMap<String, PropertyInfo>,
3573 required: &HashSet<String>,
3574 additional_properties: &mut ObjectAdditionalProperties,
3575 required_additional_property: Option<PropertyInfo>,
3576 explicitly_forbidden: bool,
3577 ) -> Result<()> {
3578 let mut missing = required
3579 .iter()
3580 .filter(|name| !properties.contains_key(*name))
3581 .cloned()
3582 .collect::<Vec<_>>();
3583 missing.sort();
3584 if missing.is_empty() {
3585 return Ok(());
3586 }
3587
3588 if explicitly_forbidden {
3589 let owner = self
3590 .current_schema_name
3591 .as_deref()
3592 .unwrap_or("<inline object>");
3593 return Err(GeneratorError::InvalidSchema(format!(
3594 "object schema `{owner}` is unsatisfiable: required member(s) {} are not declared in properties while additionalProperties: false",
3595 missing
3596 .iter()
3597 .map(|name| format!("`{name}`"))
3598 .collect::<Vec<_>>()
3599 .join(", ")
3600 )));
3601 }
3602
3603 let required_additional_property = required_additional_property.ok_or_else(|| {
3604 GeneratorError::InvalidSchema(
3605 "undeclared required members have no additional-properties carrier".to_string(),
3606 )
3607 })?;
3608 for name in missing {
3609 properties.insert(name, required_additional_property.clone());
3610 }
3611
3612 if matches!(additional_properties, ObjectAdditionalProperties::Forbidden) {
3613 *additional_properties = ObjectAdditionalProperties::Untyped;
3614 }
3615 Ok(())
3616 }
3617
3618 fn build_typed_multi_union_variant(
3628 &mut self,
3629 member_type: OpenApiSchemaType,
3630 schema: &Schema,
3631 union_type_name: &str,
3632 dependencies: &mut HashSet<String>,
3633 ) -> Result<SchemaRef> {
3634 match member_type {
3635 OpenApiSchemaType::Array => {
3636 let preferred_array_type_name = format!("{union_type_name}Array");
3637 let array_type_name = self.allocate_inline_schema_name(
3638 &preferred_array_type_name,
3639 &format!("{preferred_array_type_name}Inline"),
3640 "typed-multi-array",
3641 schema,
3642 );
3643 let array_type =
3644 self.analyze_array_schema(schema, &array_type_name, dependencies)?;
3645 self.resolved_cache.insert(
3646 array_type_name.clone(),
3647 AnalyzedSchema {
3648 name: array_type_name.clone(),
3649 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3650 schema_type: array_type,
3651 dependencies: HashSet::new(),
3652 nullable: false,
3653 description: Some("Array variant in union".to_string()),
3654 default: None,
3655 },
3656 );
3657 dependencies.insert(array_type_name.clone());
3658 Ok(SchemaRef {
3659 target: array_type_name,
3660 nullable: false,
3661 })
3662 }
3663 OpenApiSchemaType::Object => {
3664 let preferred_object_type_name = format!("{union_type_name}Object");
3665 let object_type_name = self.allocate_inline_schema_name(
3666 &preferred_object_type_name,
3667 &format!("{preferred_object_type_name}Inline"),
3668 "typed-multi-object",
3669 schema,
3670 );
3671 let object_type = self.add_allocated_object_schema(
3672 object_type_name.clone(),
3673 schema,
3674 dependencies,
3675 )?;
3676 let SchemaType::Reference { target } = object_type else {
3677 unreachable!("allocated object schemas always return a reference");
3678 };
3679 Ok(SchemaRef {
3680 target,
3681 nullable: false,
3682 })
3683 }
3684 _ => Ok(SchemaRef {
3685 target: self.openapi_type_to_rust_type(member_type, schema.details()),
3686 nullable: false,
3687 }),
3688 }
3689 }
3690
3691 fn analyze_property_schema_with_context(
3692 &mut self,
3693 schema: &Schema,
3694 property_name: Option<&str>,
3695 dependencies: &mut HashSet<String>,
3696 ) -> Result<SchemaType> {
3697 if let Schema::Bool(accepts_anything) = schema {
3699 let reason = if *accepts_anything {
3700 UntypedReason::AnySchema
3701 } else {
3702 UntypedReason::NeverMatches
3703 };
3704 return Ok(self.untyped_value(
3705 self.untyped_context(property_name.unwrap_or_default()),
3706 reason,
3707 ));
3708 }
3709
3710 if let Some(ref_str) = self.get_any_reference(schema) {
3711 let target_opt = if ref_str == "#" {
3712 Some(
3713 self.find_recursive_anchor_schema()
3714 .unwrap_or_else(|| "UnknownRecursive".to_string()),
3715 )
3716 } else {
3717 self.extract_schema_name(ref_str).map(|s| s.to_string())
3718 };
3719 match target_opt {
3720 Some(target) => {
3721 dependencies.insert(target.clone());
3722 return Ok(SchemaType::Reference { target });
3723 }
3724 None => {
3725 if let Some(resolved) = self.resolve_pointer_schema(ref_str, dependencies)? {
3730 return Ok(resolved);
3731 }
3732 eprintln!(
3733 "⚠️ unresolvable $ref `{}` — typing as serde_json::Value",
3734 ref_str
3735 );
3736 return Ok(self.untyped_value(
3737 format!("$ref {ref_str}"),
3738 UntypedReason::UnresolvedReference,
3739 ));
3740 }
3741 }
3742 }
3743
3744 if let Some(non_null_types) = schema.non_null_schema_types() {
3748 let context_name = self
3749 .current_schema_name
3750 .clone()
3751 .unwrap_or_else(|| "Unknown".to_string());
3752 let prop_pascal = property_name
3753 .map(|name| self.to_pascal_case(name))
3754 .unwrap_or_default();
3755 let preferred_union_name = format!("{context_name}{prop_pascal}");
3756 let union_type_name = self.allocate_inline_schema_name(
3757 &preferred_union_name,
3758 &format!("{preferred_union_name}Union2"),
3759 "property-typed-multi",
3760 schema,
3761 );
3762
3763 let details = schema.details();
3764 let mut variants = Vec::with_capacity(non_null_types.len());
3765 for t in non_null_types {
3766 variants.push(self.build_typed_multi_union_variant(
3767 t,
3768 schema,
3769 &union_type_name,
3770 dependencies,
3771 )?);
3772 }
3773
3774 self.resolved_cache.insert(
3775 union_type_name.clone(),
3776 AnalyzedSchema {
3777 name: union_type_name.clone(),
3778 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3779 schema_type: SchemaType::Union {
3780 variants,
3781 exclusive: false,
3782 },
3783 dependencies: HashSet::new(),
3784 nullable: false,
3785 description: details.description.clone(),
3786 default: None,
3787 },
3788 );
3789
3790 dependencies.insert(union_type_name.clone());
3791 return Ok(SchemaType::Reference {
3792 target: union_type_name,
3793 });
3794 }
3795
3796 if let Some(schema_type) = schema.schema_type() {
3797 match schema_type {
3798 OpenApiSchemaType::String => {
3799 if let Some(enum_values) = schema.details().string_enum_values() {
3801 let context_name = self
3804 .current_schema_name
3805 .clone()
3806 .unwrap_or_else(|| "Unknown".to_string());
3807
3808 let primary_name = if let Some(prop_name) = property_name {
3810 let prop_pascal = self.to_pascal_case(prop_name);
3812 format!("{context_name}{prop_pascal}")
3813 } else {
3814 let suffix = if !enum_values.is_empty() {
3817 let first_value = self.to_pascal_case(&enum_values[0]);
3818 format!("{first_value}Enum")
3819 } else {
3820 "StringEnum".to_string()
3821 };
3822 format!("{context_name}{suffix}")
3823 };
3824
3825 return Ok(self.hoist_inline_string_enum(
3826 schema,
3827 enum_values,
3828 primary_name,
3829 dependencies,
3830 ));
3831 } else {
3832 let mapped = self
3838 .type_mapper
3839 .string_format(schema.details().format.as_deref());
3840 return Ok(SchemaType::Primitive {
3841 rust_type: mapped.rust_type,
3842 serde_with: mapped.serde_with,
3843 });
3844 }
3845 }
3846 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
3847 let details = schema.details();
3848 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
3849 return Ok(SchemaType::Primitive {
3850 rust_type,
3851 serde_with: None,
3852 });
3853 }
3854 OpenApiSchemaType::Boolean => {
3855 return Ok(SchemaType::Primitive {
3856 rust_type: "bool".to_string(),
3857 serde_with: None,
3858 });
3859 }
3860 OpenApiSchemaType::Array => {
3861 let context_name = if let Some(prop_name) = property_name {
3863 let prop_pascal = self.to_pascal_case(prop_name);
3865 format!(
3866 "{}{}",
3867 self.current_schema_name.as_deref().unwrap_or("Unknown"),
3868 prop_pascal
3869 )
3870 } else {
3871 "ArrayItem".to_string()
3873 };
3874 return self.analyze_array_schema(schema, &context_name, dependencies);
3875 }
3876 OpenApiSchemaType::Object => {
3877 if self.should_use_dynamic_json(schema) {
3879 return Ok(self
3880 .untyped_value(self.untyped_context(""), UntypedReason::OpaqueObject));
3881 }
3882 let preferred_object_type_name = if let Some(prop_name) = property_name {
3884 let prop_pascal = self.to_pascal_case(prop_name);
3886 format!(
3887 "{}{}",
3888 self.current_schema_name.as_deref().unwrap_or("Unknown"),
3889 prop_pascal
3890 )
3891 } else {
3892 format!(
3894 "{}Object",
3895 self.current_schema_name.as_deref().unwrap_or("Unknown")
3896 )
3897 };
3898 let object_type_name = self.allocate_inline_schema_name(
3899 &preferred_object_type_name,
3900 &format!("{preferred_object_type_name}Inline"),
3901 "property-object",
3902 schema,
3903 );
3904
3905 return self.add_allocated_object_schema(
3906 object_type_name,
3907 schema,
3908 dependencies,
3909 );
3910 }
3911 OpenApiSchemaType::Null => {
3914 return Ok(SchemaType::Primitive {
3915 rust_type: self.type_mapper.null_unit().rust_type,
3916 serde_with: None,
3917 });
3918 }
3919 }
3920 }
3921
3922 if schema.is_nullable_pattern() {
3924 if let Some(non_null) = schema.non_null_variant() {
3925 return self.analyze_property_schema_with_context(
3926 non_null,
3927 property_name,
3928 dependencies,
3929 );
3930 }
3931 }
3932
3933 if self.should_use_dynamic_json(schema) {
3935 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::OpaqueObject));
3936 }
3937
3938 if let Schema::AllOf { all_of, .. } = schema {
3940 if let Some(property_name) = property_name {
3941 let owner = self
3942 .current_schema_name
3943 .clone()
3944 .unwrap_or_else(|| "Inline".to_string());
3945 let composition_name = format!("{owner}{}", self.to_pascal_case(property_name));
3946 return self.with_schema_context(&composition_name, |analyzer| {
3947 analyzer.analyze_allof_composition(schema, all_of, dependencies)
3948 });
3949 }
3950 return self.analyze_allof_composition(schema, all_of, dependencies);
3951 }
3952
3953 if let Some(variants) = schema.union_variants() {
3955 match variants.len().cmp(&1) {
3956 std::cmp::Ordering::Equal => {
3957 return self.analyze_property_schema_with_context(
3959 &variants[0],
3960 property_name,
3961 dependencies,
3962 );
3963 }
3964 std::cmp::Ordering::Greater => {
3965 let union_name = if let Some(prop_name) = property_name {
3968 let prop_pascal = self.to_pascal_case(prop_name);
3970 format!(
3971 "{}{}",
3972 self.current_schema_name.as_deref().unwrap_or(""),
3973 prop_pascal
3974 )
3975 } else {
3976 "UnionType".to_string()
3977 };
3978
3979 if let Schema::OneOf {
3981 one_of,
3982 discriminator,
3983 ..
3984 } = schema
3985 {
3986 let union_name = self.allocate_inline_schema_name(
3987 &union_name,
3988 &format!("{union_name}Union2"),
3989 "fallback-property-oneof",
3990 schema,
3991 );
3992 let oneof_result = self.analyze_oneof_union(
3994 one_of,
3995 discriminator.as_ref(),
3996 &union_name,
3997 dependencies,
3998 InlineUnionKind::OneOf,
3999 None,
4000 )?;
4001
4002 if let SchemaType::Union { .. } = &oneof_result {
4004 self.resolved_cache.insert(
4006 union_name.clone(),
4007 AnalyzedSchema {
4008 name: union_name.clone(),
4009 original: serde_json::to_value(schema).unwrap_or(Value::Null),
4010 schema_type: oneof_result.clone(),
4011 dependencies: dependencies.clone(),
4012 nullable: false,
4013 description: schema.details().description.clone(),
4014 default: None,
4015 },
4016 );
4017
4018 dependencies.insert(union_name.clone());
4020 return Ok(SchemaType::Reference { target: union_name });
4021 }
4022
4023 return Ok(oneof_result);
4024 } else if let Schema::AnyOf {
4025 any_of,
4026 discriminator,
4027 ..
4028 } = schema
4029 {
4030 let union_analysis = self.analyze_anyof_union(
4032 any_of,
4033 discriminator.as_ref(),
4034 dependencies,
4035 &union_name,
4036 )?;
4037 return Ok(union_analysis);
4038 } else {
4039 let mut union_variants = Vec::new();
4042 for variant in variants {
4043 if let Some(ref_str) = variant.reference() {
4044 if let Some(target) = self.extract_schema_name(ref_str) {
4045 dependencies.insert(target.to_string());
4046 union_variants.push(SchemaRef {
4047 target: target.to_string(),
4048 nullable: false,
4049 });
4050 }
4051 }
4052 }
4053 return Ok(SchemaType::Union {
4054 variants: union_variants,
4055 exclusive: false,
4056 });
4057 }
4058 }
4059 std::cmp::Ordering::Less => {}
4060 }
4061 }
4062
4063 if let Some(inferred_type) = schema.inferred_type() {
4065 match inferred_type {
4066 OpenApiSchemaType::Object => {
4067 if self.should_use_dynamic_json(schema) {
4069 return Ok(self
4070 .untyped_value(self.untyped_context(""), UntypedReason::OpaqueObject));
4071 }
4072 let owner = self
4073 .current_schema_name
4074 .clone()
4075 .unwrap_or_else(|| "Unknown".to_string());
4076 let preferred_name = property_name.map_or_else(
4077 || format!("{owner}Object"),
4078 |property_name| format!("{owner}{}", self.to_pascal_case(property_name)),
4079 );
4080 let object_type_name = self.allocate_inline_schema_name(
4081 &preferred_name,
4082 &format!("{preferred_name}Inline"),
4083 "property-object",
4084 schema,
4085 );
4086 return self.add_allocated_object_schema(
4087 object_type_name,
4088 schema,
4089 dependencies,
4090 );
4091 }
4092 OpenApiSchemaType::Array => {
4093 let context_name = if let Some(prop_name) = property_name {
4094 let prop_pascal = self.to_pascal_case(prop_name);
4096 format!(
4097 "{}{}",
4098 self.current_schema_name.as_deref().unwrap_or("Unknown"),
4099 prop_pascal
4100 )
4101 } else {
4102 "ArrayItem".to_string()
4104 };
4105 return self.analyze_array_schema(schema, &context_name, dependencies);
4106 }
4107 OpenApiSchemaType::String => {
4108 if let Some(enum_values) = schema.details().string_enum_values() {
4109 return Ok(SchemaType::StringEnum {
4110 values: enum_values,
4111 });
4112 } else {
4113 return Ok(SchemaType::Primitive {
4114 rust_type: "String".to_string(),
4115 serde_with: None,
4116 });
4117 }
4118 }
4119 _ => {
4120 let rust_type = self.openapi_type_to_rust_type(inferred_type, schema.details());
4122 return Ok(SchemaType::Primitive {
4123 rust_type,
4124 serde_with: None,
4125 });
4126 }
4127 }
4128 }
4129
4130 Ok(self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema))
4131 }
4132
4133 fn analyze_allof_composition(
4134 &mut self,
4135 owner_schema: &Schema,
4136 all_of_schemas: &[Schema],
4137 dependencies: &mut HashSet<String>,
4138 ) -> Result<SchemaType> {
4139 if let Some(carrier) = Self::single_non_object_allof_carrier(all_of_schemas) {
4144 return self.analyze_property_schema_with_context(carrier, None, dependencies);
4145 }
4146
4147 let referenced_targets = all_of_schemas
4152 .iter()
4153 .filter_map(|schema| schema.reference())
4154 .filter_map(|reference| self.extract_schema_name(reference))
4155 .collect::<Vec<_>>();
4156 let only_reference_and_annotations = all_of_schemas
4157 .iter()
4158 .all(|schema| schema.reference().is_some() || schema_is_annotation_only(schema));
4159 if referenced_targets.len() == 1 && only_reference_and_annotations {
4160 let target = referenced_targets[0];
4161 dependencies.insert(target.to_string());
4162 return Ok(SchemaType::Reference {
4163 target: target.to_string(),
4164 });
4165 }
4166
4167 if let [only] = all_of_schemas
4171 && !matches!(
4172 only.schema_type(),
4173 Some(OpenApiSchemaType::Object) | Some(OpenApiSchemaType::Null)
4174 )
4175 && only.details().properties.is_none()
4176 {
4177 return self.analyze_property_schema_with_context(only, None, dependencies);
4178 }
4179
4180 let mut merged_properties = BTreeMap::new();
4182 let mut merged_required = HashSet::new();
4183 let mut merged_variant = None;
4184 let mut descriptions = Vec::new();
4185
4186 let current_context = self.current_schema_name.clone();
4188 let owner_name = current_context.as_deref().unwrap_or("InlineComposition");
4189
4190 self.merge_schema_into_properties(
4194 owner_schema,
4195 &mut merged_properties,
4196 &mut merged_required,
4197 dependencies,
4198 )?;
4199
4200 for (member_index, schema) in all_of_schemas.iter().enumerate() {
4201 match schema {
4202 Schema::Reference { reference, .. } => {
4203 let (analyzed_type, analyzed_name, raw_target) =
4204 if let Some(target) = self.extract_schema_name(reference) {
4205 dependencies.insert(target.to_string());
4206 let analyzed_ref = self.analyze_schema(target)?;
4207 (
4208 Some(analyzed_ref.schema_type),
4209 Some(target.to_string()),
4210 self.schemas.get(target).cloned(),
4211 )
4212 } else {
4213 (
4214 self.resolve_pointer_schema(reference, dependencies)?,
4215 None,
4216 self.reference_target_schema(reference),
4217 )
4218 };
4219
4220 let merged = if let Some(analyzed_type) = analyzed_type {
4221 self.merge_analyzed_object_properties(
4222 &analyzed_type,
4223 analyzed_name.as_deref(),
4224 &mut merged_properties,
4225 &mut merged_required,
4226 &mut merged_variant,
4227 owner_name,
4228 )?
4229 } else {
4230 false
4231 };
4232 if !merged && let Some(raw_target) = raw_target {
4233 self.merge_schema_into_properties(
4234 &raw_target,
4235 &mut merged_properties,
4236 &mut merged_required,
4237 dependencies,
4238 )?;
4239 }
4240 }
4241 Schema::AnyOf { any_of, .. }
4242 if Self::union_only_constrains_requiredness(any_of) => {}
4243 Schema::OneOf { one_of, .. }
4244 if Self::union_only_constrains_requiredness(one_of) => {}
4245 Schema::AnyOf { .. } | Schema::OneOf { .. } => {
4246 let preferred_name = format!("{owner_name}AllOfVariant{}", member_index + 1);
4247 let variant_name =
4248 self.add_inline_schema(&preferred_name, schema, dependencies)?;
4249 dependencies.insert(variant_name.clone());
4250 let analyzed_type = self
4251 .resolved_cache
4252 .get(&variant_name)
4253 .map(|analyzed| analyzed.schema_type.clone())
4254 .ok_or_else(|| {
4255 GeneratorError::InvalidSchema(format!(
4256 "allOf union member `{variant_name}` was not analyzed"
4257 ))
4258 })?;
4259 if !self.merge_analyzed_object_properties(
4260 &analyzed_type,
4261 Some(&variant_name),
4262 &mut merged_properties,
4263 &mut merged_required,
4264 &mut merged_variant,
4265 owner_name,
4266 )? {
4267 return Ok(self.untyped_value(
4268 self.untyped_context(""),
4269 UntypedReason::UnrepresentableComposition,
4270 ));
4271 }
4272 }
4273 Schema::Typed {
4274 schema_type: OpenApiSchemaType::Object,
4275 ..
4276 }
4277 | Schema::Untyped { .. } => {
4278 let saved_context = self.current_schema_name.clone();
4280 self.current_schema_name = current_context.clone();
4281
4282 self.merge_schema_into_properties(
4284 schema,
4285 &mut merged_properties,
4286 &mut merged_required,
4287 dependencies,
4288 )?;
4289
4290 self.current_schema_name = saved_context;
4292 }
4293 _ => {
4294 self.merge_schema_into_properties(
4297 schema,
4298 &mut merged_properties,
4299 &mut merged_required,
4300 dependencies,
4301 )?;
4302 }
4303 }
4304
4305 if let Some(desc) = &schema.details().description {
4307 descriptions.push(desc.clone());
4308 }
4309 }
4310
4311 if !merged_properties.is_empty() || !merged_required.is_empty() || merged_variant.is_some()
4316 {
4317 let mut additional_properties = if merged_properties
4318 .values()
4319 .any(|property| property.synthesized_required)
4320 {
4321 ObjectAdditionalProperties::Untyped
4322 } else {
4323 ObjectAdditionalProperties::Forbidden
4324 };
4325 self.finalize_required_object_members(
4326 &mut merged_properties,
4327 &merged_required,
4328 &mut additional_properties,
4329 Some(PropertyInfo {
4330 schema_type: SchemaType::Untyped {
4331 shape: UntypedShape::Value,
4332 reason: UntypedReason::AnySchema,
4333 },
4334 nullable: false,
4335 description: None,
4336 default: None,
4337 serde_attrs: Vec::new(),
4338 synthesized_required: true,
4339 constraints: PropertyConstraints::default(),
4340 }),
4341 false,
4342 )?;
4343 Ok(SchemaType::Object {
4344 properties: merged_properties,
4345 required: merged_required,
4346 additional_properties,
4347 variant: merged_variant,
4348 })
4349 } else {
4350 let schemas = all_of_schemas
4351 .iter()
4352 .filter_map(|schema| {
4353 let reference = schema.reference()?;
4354 let target = self.extract_schema_name(reference)?;
4355 dependencies.insert(target.to_string());
4356 Some(SchemaRef {
4357 target: target.to_string(),
4358 nullable: false,
4359 })
4360 })
4361 .collect::<Vec<_>>();
4362 let references_are_objects = all_of_schemas
4368 .iter()
4369 .filter(|schema| schema.reference().is_some())
4370 .all(|schema| self.branch_resolves_to_object(schema));
4371 if schemas.is_empty() || !references_are_objects {
4372 Ok(self.untyped_value(
4373 self.untyped_context(""),
4374 UntypedReason::UnrepresentableComposition,
4375 ))
4376 } else {
4377 Ok(SchemaType::Composition { schemas })
4378 }
4379 }
4380 }
4381
4382 fn single_non_object_allof_carrier(all_of_schemas: &[Schema]) -> Option<&Schema> {
4387 let mut meaningful = all_of_schemas
4388 .iter()
4389 .filter(|schema| !schema_is_annotation_only(schema));
4390 let carrier = meaningful.next()?;
4391 if meaningful.next().is_some() {
4392 return None;
4393 }
4394
4395 match carrier {
4396 Schema::Typed {
4397 schema_type:
4398 OpenApiSchemaType::String
4399 | OpenApiSchemaType::Integer
4400 | OpenApiSchemaType::Number
4401 | OpenApiSchemaType::Boolean
4402 | OpenApiSchemaType::Array,
4403 ..
4404 } => Some(carrier),
4405 Schema::TypedMulti { schema_types, .. } => {
4406 let mut non_null = schema_types
4407 .iter()
4408 .filter(|schema_type| **schema_type != OpenApiSchemaType::Null);
4409 let only = non_null.next()?;
4410 (non_null.next().is_none()
4411 && matches!(
4412 only,
4413 OpenApiSchemaType::String
4414 | OpenApiSchemaType::Integer
4415 | OpenApiSchemaType::Number
4416 | OpenApiSchemaType::Boolean
4417 | OpenApiSchemaType::Array
4418 ))
4419 .then_some(carrier)
4420 }
4421 _ => None,
4422 }
4423 }
4424
4425 fn merge_analyzed_object_properties(
4430 &mut self,
4431 schema_type: &SchemaType,
4432 named_target: Option<&str>,
4433 merged_properties: &mut BTreeMap<String, PropertyInfo>,
4434 merged_required: &mut HashSet<String>,
4435 merged_variant: &mut Option<SchemaRef>,
4436 owner_name: &str,
4437 ) -> Result<bool> {
4438 let mut current = schema_type.clone();
4439 let mut visited = HashSet::new();
4440 let mut variant_target = named_target.map(str::to_string);
4441 loop {
4442 match current {
4443 SchemaType::Object {
4444 properties,
4445 required,
4446 variant,
4447 ..
4448 } => {
4449 for (name, property) in properties {
4450 let keep_declared_sibling =
4451 merged_properties.get(&name).is_some_and(|existing| {
4452 !existing.synthesized_required && property.synthesized_required
4453 });
4454 if !keep_declared_sibling {
4455 merged_properties.insert(name, property);
4456 }
4457 }
4458 merged_required.extend(required);
4459 if let Some(variant) = variant {
4460 Self::merge_allof_variant(merged_variant, variant, owner_name)?;
4461 }
4462 return Ok(true);
4463 }
4464 SchemaType::Reference { target } => {
4465 if !visited.insert(target.clone()) {
4466 return Ok(false);
4467 }
4468 if variant_target.is_none() {
4469 variant_target = Some(target.clone());
4470 }
4471 current = if let Some(analyzed) = self.resolved_cache.get(&target) {
4472 analyzed.schema_type.clone()
4473 } else if self.schemas.contains_key(&target) {
4474 self.analyze_schema(&target)?.schema_type
4475 } else {
4476 return Ok(false);
4477 };
4478 }
4479 SchemaType::Union { .. } | SchemaType::DiscriminatedUnion { .. } => {
4480 let Some(target) = variant_target else {
4481 return Ok(false);
4482 };
4483 Self::merge_allof_variant(
4484 merged_variant,
4485 SchemaRef {
4486 target,
4487 nullable: false,
4488 },
4489 owner_name,
4490 )?;
4491 return Ok(true);
4492 }
4493 _ => return Ok(false),
4494 }
4495 }
4496 }
4497
4498 fn merge_allof_variant(
4499 merged_variant: &mut Option<SchemaRef>,
4500 candidate: SchemaRef,
4501 owner_name: &str,
4502 ) -> Result<()> {
4503 match merged_variant {
4504 Some(existing) if existing.target == candidate.target => Ok(()),
4505 Some(existing) => Err(GeneratorError::InvalidSchema(format!(
4506 "allOf object `{owner_name}` intersects multiple union members (`{}` and `{}`), which cannot be represented by one flattened variant",
4507 existing.target, candidate.target
4508 ))),
4509 None => {
4510 *merged_variant = Some(candidate);
4511 Ok(())
4512 }
4513 }
4514 }
4515
4516 fn merge_schema_into_properties(
4517 &mut self,
4518 schema: &Schema,
4519 merged_properties: &mut BTreeMap<String, PropertyInfo>,
4520 merged_required: &mut HashSet<String>,
4521 dependencies: &mut HashSet<String>,
4522 ) -> Result<()> {
4523 let details = schema.details();
4524
4525 if let Some(properties) = &details.properties {
4527 for (prop_name, prop_schema) in properties {
4528 let prop_type = self.analyze_property_schema_with_context(
4529 prop_schema,
4530 Some(prop_name),
4531 dependencies,
4532 )?;
4533 let owner_name = self
4534 .current_schema_name
4535 .clone()
4536 .unwrap_or_else(|| "Inline".to_string());
4537 let prop_type = self.hoist_inline_property_type(
4538 &owner_name,
4539 prop_name,
4540 prop_type,
4541 dependencies,
4542 );
4543 let prop_details = prop_schema.details();
4544
4545 let nullable = self.schema_or_reference_is_nullable(prop_schema);
4552 merged_properties.insert(
4553 prop_name.clone(),
4554 PropertyInfo {
4555 schema_type: prop_type,
4556 nullable,
4557 description: prop_details.description.clone(),
4558 default: prop_details.default.clone(),
4559 serde_attrs: Vec::new(),
4560 synthesized_required: false,
4561 constraints: PropertyConstraints::from_schema_details(prop_details),
4562 },
4563 );
4564 }
4565 }
4566
4567 if let Some(required) = &details.required {
4569 for field in required {
4570 merged_required.insert(field.clone());
4571 }
4572 }
4573
4574 Ok(())
4575 }
4576
4577 fn analyze_oneof_union(
4578 &mut self,
4579 one_of_schemas: &[Schema],
4580 discriminator: Option<&crate::openapi::Discriminator>,
4581 parent_name: &str,
4582 dependencies: &mut HashSet<String>,
4583 union_kind: InlineUnionKind,
4584 source_indices: Option<&[usize]>,
4585 ) -> Result<SchemaType> {
4586 let default_indices = (0..one_of_schemas.len()).collect::<Vec<_>>();
4587 let source_indices = source_indices
4588 .filter(|indices| indices.len() == one_of_schemas.len())
4589 .unwrap_or(&default_indices);
4590
4591 let expanded_branches;
4593 let one_of_schemas = match self.expand_pointer_branches(one_of_schemas) {
4594 Some(expanded) => {
4595 expanded_branches = expanded;
4596 expanded_branches.as_slice()
4597 }
4598 None => one_of_schemas,
4599 };
4600
4601 let boolean_resolved;
4603 let boolean_resolved_indices;
4604 let one_of_schemas = match Self::resolve_boolean_branches(one_of_schemas) {
4605 Ok(resolved) => {
4606 boolean_resolved_indices = one_of_schemas
4607 .iter()
4608 .zip(source_indices.iter().copied())
4609 .filter_map(|(branch, index)| {
4610 (!matches!(branch, Schema::Bool(false))).then_some(index)
4611 })
4612 .collect::<Vec<_>>();
4613 boolean_resolved = resolved;
4614 boolean_resolved.as_slice()
4615 }
4616 Err(()) => {
4617 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema));
4618 }
4619 };
4620 let source_indices = boolean_resolved_indices.as_slice();
4621 if one_of_schemas.is_empty() {
4622 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::NeverMatches));
4623 }
4624
4625 if let [only] = one_of_schemas {
4630 return self
4631 .analyze_schema_value(only, parent_name)
4632 .map(|analyzed| analyzed.schema_type);
4633 }
4634 if let Some(shared) = self.shared_branch_type(one_of_schemas) {
4635 return Ok(shared);
4636 }
4637
4638 if one_of_schemas.len() == 2 {
4641 let null_count = one_of_schemas
4642 .iter()
4643 .filter(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
4644 .count();
4645 if null_count == 1 {
4646 if let Some(non_null) = one_of_schemas
4647 .iter()
4648 .find(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
4649 {
4650 return self
4651 .analyze_schema_value(non_null, parent_name)
4652 .map(|a| a.schema_type);
4653 }
4654 }
4655 }
4656
4657 if discriminator.is_none()
4661 || one_of_schemas
4662 .iter()
4663 .any(|schema| self.schema_or_reference_is_nullable(schema))
4664 {
4665 return self.analyze_untagged_oneof_union(
4667 one_of_schemas,
4668 parent_name,
4669 dependencies,
4670 union_kind,
4671 source_indices,
4672 None,
4673 );
4674 }
4675
4676 if one_of_schemas
4682 .iter()
4683 .any(|s| !self.branch_resolves_to_object(s))
4684 {
4685 return self.analyze_untagged_oneof_union(
4686 one_of_schemas,
4687 parent_name,
4688 dependencies,
4689 union_kind,
4690 source_indices,
4691 discriminator,
4692 );
4693 }
4694
4695 let discriminator_field = discriminator
4697 .ok_or_else(|| {
4698 GeneratorError::InvalidDiscriminator(
4699 "expected discriminator after guard check".to_string(),
4700 )
4701 })?
4702 .property_name
4703 .clone();
4704
4705 if one_of_schemas.iter().any(|branch| {
4709 self.extract_discriminator_value_domain_for_field(branch, &discriminator_field)
4710 .is_some_and(|values| values.is_empty())
4711 }) {
4712 eprintln!(
4713 "⚠️ discriminated union `{parent_name}` has a branch with contradictory `{discriminator_field}` constraints; using structural union fallback"
4714 );
4715 return self.analyze_untagged_oneof_union(
4716 one_of_schemas,
4717 parent_name,
4718 dependencies,
4719 union_kind,
4720 source_indices,
4721 discriminator,
4722 );
4723 }
4724
4725 let mut variants = Vec::new();
4726 let mut used_variant_names = std::collections::HashSet::new();
4727
4728 for (variant_schema, original_index) in
4729 one_of_schemas.iter().zip(source_indices.iter().copied())
4730 {
4731 let ref_info = if let Some(ref_str) = variant_schema.reference() {
4733 Some((ref_str, false))
4734 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
4735 Some((recursive_ref, true))
4736 } else if let Schema::AllOf { all_of, .. } = variant_schema {
4737 if all_of.len() == 1 {
4739 if let Some(ref_str) = all_of[0].reference() {
4740 Some((ref_str, false))
4741 } else {
4742 all_of[0]
4743 .recursive_reference()
4744 .map(|recursive_ref| (recursive_ref, true))
4745 }
4746 } else {
4747 None
4748 }
4749 } else {
4750 None
4751 };
4752
4753 if let Some((ref_str, is_recursive)) = ref_info {
4754 let schema_name = if is_recursive && ref_str == "#" {
4755 self.find_recursive_anchor_schema()
4757 .or_else(|| self.current_schema_name.clone())
4758 .unwrap_or_else(|| "CompoundFilter".to_string())
4759 } else {
4760 self.extract_schema_name(ref_str)
4761 .map(|s| s.to_string())
4762 .unwrap_or_else(|| "UnknownRef".to_string())
4763 };
4764
4765 if !schema_name.is_empty() {
4766 dependencies.insert(schema_name.clone());
4767
4768 let mut discriminator_values = Vec::new();
4772 let allowed_domain = self.schemas.get(&schema_name).and_then(|ref_schema| {
4773 self.extract_discriminator_value_domain_for_field(
4774 ref_schema,
4775 &discriminator_field,
4776 )
4777 });
4778 if let Some(mappings) = discriminator.and_then(|disc| disc.mapping.as_ref()) {
4779 for (key, target_ref) in mappings {
4780 if target_ref == ref_str
4781 || self.extract_schema_name(target_ref)
4782 == Some(schema_name.as_str())
4783 {
4784 if allowed_domain
4785 .as_ref()
4786 .is_some_and(|allowed| !allowed.contains(key))
4787 {
4788 let allowed = allowed_domain
4789 .as_ref()
4790 .map(|values| values.join("`, `"))
4791 .unwrap_or_default();
4792 eprintln!(
4793 "⚠️ discriminator mapping conflict in union `{parent_name}`: key `{key}` targets `{schema_name}` but branch allows `{allowed}`; ignoring mapping key"
4794 );
4795 } else {
4796 Self::push_unique_string(&mut discriminator_values, key);
4797 }
4798 }
4799 }
4800 }
4801 if let Some(allowed_domain) = allowed_domain {
4802 for value in allowed_domain {
4803 Self::push_unique_string(&mut discriminator_values, &value);
4804 }
4805 }
4806 if discriminator_values.is_empty() {
4807 discriminator_values
4808 .push(self.generate_discriminator_value_from_name(&schema_name));
4809 }
4810 let discriminator_value = discriminator_values[0].clone();
4811 let (discriminator_field_declared, discriminator_field_required) = self
4812 .schemas
4813 .get(&schema_name)
4814 .map(|schema| {
4815 self.discriminator_property_presence(schema, &discriminator_field)
4816 })
4817 .unwrap_or((false, false));
4818
4819 let base_name = self.to_rust_variant_name(&schema_name);
4821 let rust_name =
4822 self.ensure_unique_variant_name(base_name, &mut used_variant_names);
4823
4824 let final_discriminator_value = discriminator_value;
4826
4827 variants.push(UnionVariant {
4828 rust_name,
4829 type_name: schema_name,
4830 discriminator_value: final_discriminator_value,
4831 preferred_discriminator_values: discriminator_values.clone(),
4832 discriminator_values,
4833 discriminator_field_declared,
4834 discriminator_field_required,
4835 schema_ref: ref_str.to_string(),
4836 });
4837 }
4838 } else {
4839 let variant_index = original_index;
4841 let inline_type_name =
4842 self.generate_inline_type_name(variant_schema, variant_index);
4843
4844 let mut discriminator_values = Vec::new();
4847 let allowed_domain = self.extract_discriminator_value_domain_for_field(
4848 variant_schema,
4849 &discriminator_field,
4850 );
4851 if let Some(mappings) = discriminator.and_then(|disc| disc.mapping.as_ref()) {
4852 for (key, target_ref) in mappings {
4853 if target_ref.contains(&format!("variant_{variant_index}")) {
4854 if allowed_domain
4855 .as_ref()
4856 .is_some_and(|allowed| !allowed.contains(key))
4857 {
4858 let allowed = allowed_domain
4859 .as_ref()
4860 .map(|values| values.join("`, `"))
4861 .unwrap_or_default();
4862 eprintln!(
4863 "⚠️ discriminator mapping conflict in union `{parent_name}`: key `{key}` targets `{inline_type_name}` but branch allows `{allowed}`; ignoring mapping key"
4864 );
4865 } else {
4866 Self::push_unique_string(&mut discriminator_values, key);
4867 }
4868 }
4869 }
4870 }
4871 if let Some(allowed_domain) = allowed_domain {
4872 for value in allowed_domain {
4873 Self::push_unique_string(&mut discriminator_values, &value);
4874 }
4875 }
4876 if discriminator_values.is_empty() {
4877 discriminator_values.push(format!("variant_{variant_index}"));
4878 }
4879 let discriminator_value = discriminator_values[0].clone();
4880 let (discriminator_field_declared, discriminator_field_required) =
4881 self.discriminator_property_presence(variant_schema, &discriminator_field);
4882
4883 let base_name = if discriminator_value.starts_with("variant_") {
4885 format!("Variant{variant_index}")
4886 } else {
4887 let clean_name = self.discriminator_to_variant_name(&discriminator_value);
4889 self.to_rust_variant_name(&clean_name)
4890 };
4891 let rust_name = self.ensure_unique_variant_name(base_name, &mut used_variant_names);
4892
4893 let final_discriminator_value = discriminator_value;
4895
4896 let inline_type_name = self.add_inline_union_branch_schema(
4899 &inline_type_name,
4900 variant_schema,
4901 dependencies,
4902 parent_name,
4903 union_kind,
4904 original_index,
4905 Some(&final_discriminator_value),
4906 )?;
4907
4908 variants.push(UnionVariant {
4909 rust_name,
4910 type_name: inline_type_name.clone(),
4911 discriminator_value: final_discriminator_value,
4912 preferred_discriminator_values: discriminator_values.clone(),
4913 discriminator_values,
4914 discriminator_field_declared,
4915 discriminator_field_required,
4916 schema_ref: format!("inline_{variant_index}"),
4917 });
4918 }
4919 }
4920
4921 self.disambiguate_shared_discriminator_values(&mut variants, discriminator);
4922
4923 if variants.is_empty() {
4924 let mut union_variants = Vec::new();
4927
4928 for (variant_schema, original_index) in
4929 one_of_schemas.iter().zip(source_indices.iter().copied())
4930 {
4931 if let Some(ref_str) = variant_schema.reference() {
4933 if let Some(schema_name) = self.extract_schema_name(ref_str) {
4934 dependencies.insert(schema_name.to_string());
4935 union_variants.push(SchemaRef {
4936 target: schema_name.to_string(),
4937 nullable: false,
4938 });
4939 }
4940 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
4941 let schema_name = if recursive_ref == "#" {
4942 self.find_recursive_anchor_schema()
4944 .or_else(|| self.current_schema_name.clone())
4945 .unwrap_or_else(|| "CompoundFilter".to_string())
4946 } else {
4947 self.extract_schema_name(recursive_ref)
4948 .map(|s| s.to_string())
4949 .unwrap_or_else(|| "RecursiveType".to_string())
4950 };
4951 dependencies.insert(schema_name.clone());
4952 union_variants.push(SchemaRef {
4953 target: schema_name,
4954 nullable: false,
4955 });
4956 } else {
4957 let branch_discriminator = self.inline_union_branch_discriminator_value(
4958 variant_schema,
4959 discriminator,
4960 original_index,
4961 );
4962 let inline_name = self.generate_context_aware_name(
4964 parent_name,
4965 "InlineVariant",
4966 original_index,
4967 Some(variant_schema),
4968 );
4969 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
4970 let variant_type = analyzed.schema_type;
4971
4972 for dep in &analyzed.dependencies {
4974 dependencies.insert(dep.clone());
4975 }
4976
4977 match &variant_type {
4978 SchemaType::Primitive { rust_type, .. } => {
4980 union_variants.push(SchemaRef {
4981 target: rust_type.clone(),
4982 nullable: false,
4983 });
4984 }
4985 SchemaType::Array { item_type } => {
4987 match item_type.as_ref() {
4988 SchemaType::Primitive { rust_type, .. } => {
4989 let type_name = format!("Vec<{rust_type}>");
4990 union_variants.push(SchemaRef {
4991 target: type_name,
4992 nullable: false,
4993 });
4994 }
4995 SchemaType::Reference { target } => {
4996 let type_name = format!("Vec<{target}>");
4997 union_variants.push(SchemaRef {
4998 target: type_name,
4999 nullable: false,
5000 });
5001 }
5002 _ => {
5003 let inline_type_name = self.generate_context_aware_name(
5005 parent_name,
5006 "Variant",
5007 original_index,
5008 None,
5009 );
5010 let inline_type_name = self.add_inline_union_branch_schema(
5011 &inline_type_name,
5012 variant_schema,
5013 dependencies,
5014 parent_name,
5015 union_kind,
5016 original_index,
5017 branch_discriminator.as_deref(),
5018 )?;
5019 union_variants.push(SchemaRef {
5020 target: inline_type_name,
5021 nullable: false,
5022 });
5023 }
5024 }
5025 }
5026 SchemaType::Reference { target } => {
5028 union_variants.push(SchemaRef {
5029 target: target.clone(),
5030 nullable: false,
5031 });
5032 }
5033 _ => {
5035 let inline_type_name =
5036 format!("{}Variant{}", parent_name, original_index + 1);
5037 let inline_type_name = self.add_inline_union_branch_schema(
5038 &inline_type_name,
5039 variant_schema,
5040 dependencies,
5041 parent_name,
5042 union_kind,
5043 original_index,
5044 branch_discriminator.as_deref(),
5045 )?;
5046 union_variants.push(SchemaRef {
5047 target: inline_type_name,
5048 nullable: false,
5049 });
5050 }
5051 }
5052 }
5053 }
5054
5055 if !union_variants.is_empty() {
5056 return Ok(SchemaType::Union {
5057 variants: union_variants,
5058 exclusive: matches!(union_kind, InlineUnionKind::OneOf),
5059 });
5060 }
5061
5062 return Ok(self.untyped_value(
5064 self.untyped_context(""),
5065 UntypedReason::UnrepresentableUnion,
5066 ));
5067 }
5068
5069 Ok(SchemaType::DiscriminatedUnion {
5070 discriminator_field,
5071 variants,
5072 exclusive: matches!(union_kind, InlineUnionKind::OneOf),
5073 })
5074 }
5075
5076 fn analyze_untagged_oneof_union(
5077 &mut self,
5078 one_of_schemas: &[Schema],
5079 parent_name: &str,
5080 dependencies: &mut HashSet<String>,
5081 union_kind: InlineUnionKind,
5082 source_indices: &[usize],
5083 discriminator: Option<&Discriminator>,
5084 ) -> Result<SchemaType> {
5085 let filtered: Vec<(usize, &Schema)> = one_of_schemas
5090 .iter()
5091 .zip(source_indices.iter().copied())
5092 .filter_map(|(schema, original_index)| {
5093 (!schema.is_explicit_null_only()).then_some((original_index, schema))
5094 })
5095 .collect();
5096
5097 if filtered.len() == 1 {
5099 return self
5100 .analyze_schema_value(filtered[0].1, parent_name)
5101 .map(|a| a.schema_type);
5102 }
5103
5104 let exclusive_object_union = matches!(union_kind, InlineUnionKind::OneOf)
5110 && filtered
5111 .iter()
5112 .all(|(_, schema)| self.branch_resolves_to_object(schema));
5113
5114 let mut union_variants = Vec::new();
5115
5116 for (original_index, variant_schema) in filtered {
5117 if let Some(ref_str) = variant_schema.reference() {
5119 if let Some(schema_name) = self.extract_schema_name(ref_str) {
5120 dependencies.insert(schema_name.to_string());
5121 union_variants.push(SchemaRef {
5122 target: schema_name.to_string(),
5123 nullable: self.schema_or_reference_is_nullable(variant_schema),
5124 });
5125 }
5126 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
5127 let schema_name = if recursive_ref == "#" {
5128 self.find_recursive_anchor_schema()
5130 .or_else(|| self.current_schema_name.clone())
5131 .unwrap_or_else(|| "CompoundFilter".to_string())
5132 } else {
5133 self.extract_schema_name(recursive_ref)
5134 .map(|s| s.to_string())
5135 .unwrap_or_else(|| "RecursiveType".to_string())
5136 };
5137 dependencies.insert(schema_name.clone());
5138 union_variants.push(SchemaRef {
5139 target: schema_name,
5140 nullable: false,
5141 });
5142 } else {
5143 let branch_discriminator = self.inline_union_branch_discriminator_value(
5144 variant_schema,
5145 discriminator,
5146 original_index,
5147 );
5148 let inline_name = self.generate_context_aware_name(
5150 parent_name,
5151 "InlineVariant",
5152 original_index,
5153 Some(variant_schema),
5154 );
5155 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
5156 let variant_type = analyzed.schema_type;
5157
5158 for dep in &analyzed.dependencies {
5160 dependencies.insert(dep.clone());
5161 }
5162
5163 match &variant_type {
5164 SchemaType::Primitive { rust_type, .. } => {
5166 union_variants.push(SchemaRef {
5167 target: rust_type.clone(),
5168 nullable: false,
5169 });
5170 }
5171 SchemaType::Array { item_type } => {
5173 match item_type.as_ref() {
5174 SchemaType::Primitive { rust_type, .. } => {
5175 let type_name = format!("Vec<{rust_type}>");
5176 union_variants.push(SchemaRef {
5177 target: type_name,
5178 nullable: false,
5179 });
5180 }
5181 SchemaType::Reference { target } => {
5182 let type_name = format!("Vec<{target}>");
5183 union_variants.push(SchemaRef {
5184 target: type_name,
5185 nullable: false,
5186 });
5187 }
5188 SchemaType::Array {
5190 item_type: inner_item_type,
5191 } => {
5192 match inner_item_type.as_ref() {
5193 SchemaType::Primitive { rust_type, .. } => {
5194 let type_name = format!("Vec<Vec<{rust_type}>>");
5195 union_variants.push(SchemaRef {
5196 target: type_name,
5197 nullable: false,
5198 });
5199 }
5200 SchemaType::Reference { target } => {
5201 let type_name = format!("Vec<Vec<{target}>>");
5202 union_variants.push(SchemaRef {
5203 target: type_name,
5204 nullable: false,
5205 });
5206 }
5207 _ => {
5208 let inline_type_name = self.generate_context_aware_name(
5210 parent_name,
5211 "Variant",
5212 original_index,
5213 None,
5214 );
5215 let inline_type_name = self
5216 .add_inline_union_branch_schema(
5217 &inline_type_name,
5218 variant_schema,
5219 dependencies,
5220 parent_name,
5221 union_kind,
5222 original_index,
5223 branch_discriminator.as_deref(),
5224 )?;
5225 union_variants.push(SchemaRef {
5226 target: inline_type_name,
5227 nullable: false,
5228 });
5229 }
5230 }
5231 }
5232 _ => {
5233 let inline_type_name = self.generate_context_aware_name(
5235 parent_name,
5236 "Variant",
5237 original_index,
5238 None,
5239 );
5240 let inline_type_name = self.add_inline_union_branch_schema(
5241 &inline_type_name,
5242 variant_schema,
5243 dependencies,
5244 parent_name,
5245 union_kind,
5246 original_index,
5247 branch_discriminator.as_deref(),
5248 )?;
5249 union_variants.push(SchemaRef {
5250 target: inline_type_name,
5251 nullable: false,
5252 });
5253 }
5254 }
5255 }
5256 SchemaType::Reference { target } => {
5258 union_variants.push(SchemaRef {
5259 target: target.clone(),
5260 nullable: false,
5261 });
5262 }
5263 _ => {
5265 let inline_type_name = self.generate_context_aware_name(
5266 parent_name,
5267 "Variant",
5268 original_index,
5269 None,
5270 );
5271 let inline_type_name = self.add_inline_union_branch_schema(
5272 &inline_type_name,
5273 variant_schema,
5274 dependencies,
5275 parent_name,
5276 union_kind,
5277 original_index,
5278 branch_discriminator.as_deref(),
5279 )?;
5280 union_variants.push(SchemaRef {
5281 target: inline_type_name,
5282 nullable: false,
5283 });
5284 }
5285 }
5286 }
5287 }
5288
5289 if !union_variants.is_empty() {
5290 return Ok(SchemaType::Union {
5291 variants: union_variants,
5292 exclusive: exclusive_object_union,
5293 });
5294 }
5295
5296 Ok(self.untyped_value(
5298 self.untyped_context(""),
5299 UntypedReason::UnrepresentableUnion,
5300 ))
5301 }
5302
5303 fn add_inline_schema(
5304 &mut self,
5305 type_name: &str,
5306 schema: &Schema,
5307 dependencies: &mut HashSet<String>,
5308 ) -> Result<String> {
5309 let allocated_name = self.allocate_inline_schema_name(
5310 type_name,
5311 &format!("{type_name}Inline"),
5312 "inline-schema",
5313 schema,
5314 );
5315 self.add_allocated_inline_schema(allocated_name, schema, dependencies)
5316 }
5317
5318 #[allow(clippy::too_many_arguments)]
5319 fn add_inline_union_branch_schema(
5320 &mut self,
5321 type_name: &str,
5322 schema: &Schema,
5323 dependencies: &mut HashSet<String>,
5324 owner_context: &str,
5325 union_kind: InlineUnionKind,
5326 original_index: usize,
5327 discriminator: Option<&str>,
5328 ) -> Result<String> {
5329 let allocated_name = self.allocate_inline_union_branch_name(
5330 type_name,
5331 owner_context,
5332 union_kind,
5333 original_index,
5334 discriminator,
5335 schema,
5336 );
5337 self.add_allocated_inline_schema(allocated_name, schema, dependencies)
5338 }
5339
5340 fn add_allocated_inline_schema(
5341 &mut self,
5342 allocated_name: String,
5343 schema: &Schema,
5344 dependencies: &mut HashSet<String>,
5345 ) -> Result<String> {
5346 if let Some(schema_type) = schema.schema_type() {
5348 match schema_type {
5349 OpenApiSchemaType::String
5350 | OpenApiSchemaType::Integer
5351 | OpenApiSchemaType::Number
5352 | OpenApiSchemaType::Boolean => {
5353 let rust_type =
5354 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
5355
5356 self.resolved_cache.insert(
5358 allocated_name.clone(),
5359 AnalyzedSchema {
5360 name: allocated_name.clone(),
5361 original: serde_json::to_value(schema).unwrap_or(Value::Null),
5362 schema_type: SchemaType::Primitive {
5363 rust_type,
5364 serde_with: None,
5365 },
5366 dependencies: HashSet::new(),
5367 nullable: false,
5368 description: schema.details().description.clone(),
5369 default: None,
5370 },
5371 );
5372 return Ok(allocated_name);
5373 }
5374 _ => {}
5375 }
5376 }
5377
5378 let analyzed = self.with_schema_context(&allocated_name, |analyzer| {
5382 analyzer.analyze_schema_value(schema, &allocated_name)
5383 })?;
5384
5385 self.resolved_cache.insert(allocated_name.clone(), analyzed);
5387
5388 if let Some(cached) = self.resolved_cache.get(&allocated_name) {
5390 for dep in &cached.dependencies {
5391 dependencies.insert(dep.clone());
5392 }
5393 }
5394
5395 Ok(allocated_name)
5396 }
5397
5398 fn inline_union_branch_discriminator_value(
5399 &self,
5400 schema: &Schema,
5401 discriminator: Option<&Discriminator>,
5402 original_index: usize,
5403 ) -> Option<String> {
5404 let discriminator = discriminator?;
5405 discriminator
5406 .mapping
5407 .as_ref()
5408 .and_then(|mappings| {
5409 mappings
5410 .iter()
5411 .find(|(_, target_ref)| {
5412 target_ref.contains(&format!("variant_{original_index}"))
5413 })
5414 .map(|(key, _)| key.clone())
5415 })
5416 .or_else(|| {
5417 Some(self.extract_inline_discriminator_value(
5418 schema,
5419 &discriminator.property_name,
5420 original_index,
5421 ))
5422 })
5423 }
5424
5425 fn extract_inline_discriminator_value(
5426 &self,
5427 schema: &Schema,
5428 discriminator_field: &str,
5429 variant_index: usize,
5430 ) -> String {
5431 if let Some(properties) = &schema.details().properties {
5433 if let Some(discriminator_prop) = properties.get(discriminator_field) {
5434 if let Some(enum_values) = &discriminator_prop.details().enum_values {
5436 if enum_values.len() == 1 {
5437 if let Some(value) = enum_values[0].as_str() {
5438 return value.to_string();
5439 }
5440 }
5441 }
5442 if let Some(const_value) = discriminator_prop.details().extra.get("const") {
5444 if let Some(value) = const_value.as_str() {
5445 return value.to_string();
5446 }
5447 }
5448 if let Some(const_value) = &discriminator_prop.details().const_value {
5450 if let Some(value) = const_value.as_str() {
5451 return value.to_string();
5452 }
5453 }
5454 }
5455 }
5456
5457 if let Some(inferred_name) = self.infer_variant_name_from_structure(schema, variant_index) {
5459 return inferred_name;
5460 }
5461
5462 format!("variant_{variant_index}")
5464 }
5465
5466 fn infer_variant_name_from_structure(
5467 &self,
5468 schema: &Schema,
5469 _variant_index: usize,
5470 ) -> Option<String> {
5471 let details = schema.details();
5472
5473 if let Some(properties) = &details.properties {
5475 if properties.contains_key("text") && properties.len() <= 3 {
5477 return Some("text".to_string());
5478 }
5479 if properties.contains_key("image") || properties.contains_key("source") {
5480 return Some("image".to_string());
5481 }
5482 if properties.contains_key("document") {
5483 return Some("document".to_string());
5484 }
5485 if properties.contains_key("tool_use_id") || properties.contains_key("tool_result") {
5486 return Some("tool_result".to_string());
5487 }
5488 if properties.contains_key("content") && properties.contains_key("is_error") {
5489 return Some("tool_result".to_string());
5490 }
5491 if properties.contains_key("partial_json") {
5492 return Some("partial_json".to_string());
5493 }
5494
5495 let property_names: Vec<&String> = properties.keys().collect();
5497
5498 for prop_name in &property_names {
5500 if prop_name.contains("result") {
5501 return Some("result".to_string());
5502 }
5503 if prop_name.contains("error") {
5504 return Some("error".to_string());
5505 }
5506 if prop_name.contains("content") && property_names.len() <= 2 {
5507 return Some("content".to_string());
5508 }
5509 }
5510
5511 let significant_props = property_names
5513 .iter()
5514 .filter(|&name| !["type", "id", "cache_control"].contains(&name.as_str()))
5515 .collect::<Vec<_>>();
5516
5517 if significant_props.len() == 1 {
5518 return Some((*significant_props[0]).clone());
5519 }
5520 }
5521
5522 if let Some(description) = &details.description {
5524 let desc_lower = description.to_lowercase();
5525 if desc_lower.contains("text") && desc_lower.len() < 100 {
5526 return Some("text".to_string());
5527 }
5528 if desc_lower.contains("image") {
5529 return Some("image".to_string());
5530 }
5531 if desc_lower.contains("document") {
5532 return Some("document".to_string());
5533 }
5534 if desc_lower.contains("tool") && desc_lower.contains("result") {
5535 return Some("tool_result".to_string());
5536 }
5537 }
5538
5539 None
5540 }
5541
5542 fn discriminator_to_variant_name(&self, discriminator: &str) -> String {
5543 if discriminator.is_empty() {
5545 return "Variant".to_string();
5546 }
5547
5548 let mut result = String::new();
5549 let mut next_upper = true;
5550
5551 for c in discriminator.chars() {
5552 match c {
5553 'a'..='z' => {
5554 if next_upper {
5555 result.push(c.to_ascii_uppercase());
5556 next_upper = false;
5557 } else {
5558 result.push(c);
5559 }
5560 }
5561 'A'..='Z' => {
5562 result.push(c);
5563 next_upper = false;
5564 }
5565 '0'..='9' => {
5566 result.push(c);
5567 next_upper = false;
5568 }
5569 '_' | '-' | '.' | ' ' | '/' | '\\' => {
5570 next_upper = true;
5572 }
5573 _ => {
5574 next_upper = true;
5576 }
5577 }
5578 }
5579
5580 if result.is_empty() || result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
5582 result = format!("Variant{result}");
5583 }
5584
5585 result
5586 }
5587
5588 fn ensure_unique_variant_name(
5589 &self,
5590 base_name: String,
5591 used_names: &mut std::collections::HashSet<String>,
5592 ) -> String {
5593 let mut candidate = base_name.clone();
5594 let mut counter = 1;
5595
5596 while used_names.contains(&candidate) {
5597 counter += 1;
5598 candidate = format!("{base_name}{counter}");
5599 }
5600
5601 used_names.insert(candidate.clone());
5602 candidate
5603 }
5604
5605 fn generate_inline_type_name(&self, schema: &Schema, variant_index: usize) -> String {
5606 if let Some(meaningful_name) = self.infer_type_name_from_structure(schema) {
5608 return meaningful_name;
5609 }
5610
5611 let context = self.current_schema_name.as_deref().unwrap_or("Inline");
5613 self.generate_context_aware_name(context, "Variant", variant_index, Some(schema))
5614 }
5615
5616 fn infer_type_name_from_structure(&self, schema: &Schema) -> Option<String> {
5617 let details = schema.details();
5618
5619 if let Some(description) = &details.description {
5621 if let Some(name_from_desc) = self.extract_type_name_from_description(description) {
5622 return Some(name_from_desc);
5623 }
5624 }
5625
5626 if let Some(properties) = &details.properties {
5628 if let Some(name_from_props) = self.extract_type_name_from_properties(properties) {
5629 return Some(format!("{name_from_props}Block"));
5630 }
5631 }
5632
5633 None
5634 }
5635
5636 fn extract_type_name_from_description(&self, description: &str) -> Option<String> {
5637 if description.len() > 100 || description.contains('\n') {
5639 return None;
5640 }
5641
5642 let words: Vec<&str> = description
5644 .split_whitespace()
5645 .take(2) .filter(|word| {
5647 let w = word.to_lowercase();
5648 word.len() > 2
5649 && ![
5650 "the", "and", "for", "with", "that", "this", "are", "can", "will", "was",
5651 ]
5652 .contains(&w.as_str())
5653 })
5654 .collect();
5655
5656 if words.is_empty() {
5657 return None;
5658 }
5659
5660 let combined = words.join("_");
5662 let pascal_name = self.discriminator_to_variant_name(&combined);
5663
5664 if !pascal_name.ends_with("Content")
5666 && !pascal_name.ends_with("Block")
5667 && !pascal_name.ends_with("Type")
5668 {
5669 Some(format!("{pascal_name}Content"))
5670 } else {
5671 Some(pascal_name)
5672 }
5673 }
5674
5675 fn extract_type_name_from_properties(
5676 &self,
5677 properties: &std::collections::BTreeMap<String, crate::openapi::Schema>,
5678 ) -> Option<String> {
5679 let significant_props: Vec<&String> = properties
5681 .keys()
5682 .filter(|name| !["type", "id", "cache_control"].contains(&name.as_str()))
5683 .collect();
5684
5685 if significant_props.is_empty() {
5686 return None;
5687 }
5688
5689 if significant_props.len() == 1 {
5691 let prop_name = significant_props[0];
5692 return Some(self.discriminator_to_variant_name(prop_name));
5693 }
5694
5695 let mut sorted_props = significant_props.clone();
5698 sorted_props.sort();
5699 if let Some(first_prop) = sorted_props.first() {
5700 return Some(self.discriminator_to_variant_name(first_prop));
5701 }
5702
5703 None
5704 }
5705
5706 fn openapi_type_to_rust_type(
5707 &self,
5708 openapi_type: OpenApiSchemaType,
5709 details: &crate::openapi::SchemaDetails,
5710 ) -> String {
5711 if openapi_type == OpenApiSchemaType::Integer {
5716 self.integer_rust_type(details)
5717 } else {
5718 self.type_mapper.map(openapi_type, details).rust_type
5719 }
5720 }
5721
5722 #[allow(dead_code)]
5723 fn fallback_discriminator_value(&self, schema_name: &str) -> String {
5724 self.fallback_discriminator_value_for_field(schema_name, "type")
5725 }
5726
5727 fn fallback_discriminator_value_for_field(
5728 &self,
5729 schema_name: &str,
5730 field_name: &str,
5731 ) -> String {
5732 if let Some(ref_schema) = self.schemas.get(schema_name) {
5734 if let Some(extracted) =
5735 self.extract_discriminator_value_for_field(ref_schema, field_name)
5736 {
5737 return extracted;
5738 }
5739 }
5740
5741 self.generate_discriminator_value_from_name(schema_name)
5743 }
5744
5745 fn disambiguate_shared_discriminator_values(
5746 &self,
5747 variants: &mut [UnionVariant],
5748 discriminator: Option<&Discriminator>,
5749 ) {
5750 let mut owners_by_value: BTreeMap<String, Vec<usize>> = BTreeMap::new();
5751 for (index, variant) in variants.iter().enumerate() {
5752 for value in &variant.discriminator_values {
5753 owners_by_value
5754 .entry(value.clone())
5755 .or_default()
5756 .push(index);
5757 }
5758 }
5759
5760 let mut removals: Vec<(usize, String)> = Vec::new();
5761 for (value, candidate_owners) in owners_by_value {
5762 if candidate_owners.len() < 2 {
5763 continue;
5764 }
5765
5766 let explicitly_mapped_owners: Vec<usize> = discriminator
5767 .and_then(|disc| disc.mapping.as_ref())
5768 .and_then(|mappings| mappings.get(&value))
5769 .map(|target_ref| {
5770 candidate_owners
5771 .iter()
5772 .copied()
5773 .filter(|index| {
5774 let variant = &variants[*index];
5775 target_ref == &variant.schema_ref
5776 || self.extract_schema_name(target_ref)
5777 == Some(variant.type_name.as_str())
5778 || (variant.schema_ref.starts_with("inline_")
5779 && target_ref.contains(&variant.schema_ref))
5780 })
5781 .collect()
5782 })
5783 .unwrap_or_default();
5784
5785 let winner = if explicitly_mapped_owners.len() == 1 {
5786 explicitly_mapped_owners.first().copied()
5787 } else {
5788 let mut scored: Vec<(usize, usize)> = candidate_owners
5789 .iter()
5790 .map(|index| {
5791 (
5792 *index,
5793 Self::discriminator_name_affinity(&variants[*index].type_name, &value),
5794 )
5795 })
5796 .collect();
5797 scored.sort_by(|left, right| right.1.cmp(&left.1).then(left.0.cmp(&right.0)));
5798 match scored.as_slice() {
5799 [(index, score), rest @ ..]
5800 if *score > 0 && rest.first().is_none_or(|(_, next)| score > next) =>
5801 {
5802 Some(*index)
5803 }
5804 _ => None,
5805 }
5806 };
5807
5808 if let Some(winner) = winner {
5809 for index in candidate_owners {
5810 if index != winner && variants[index].preferred_discriminator_values.len() > 1 {
5811 removals.push((index, value.clone()));
5812 }
5813 }
5814 }
5815 }
5816
5817 for (index, value) in removals {
5818 variants[index]
5819 .preferred_discriminator_values
5820 .retain(|candidate| candidate != &value);
5821 }
5822 for variant in variants {
5823 if let Some(canonical) = variant.preferred_discriminator_values.first() {
5824 variant.discriminator_value.clone_from(canonical);
5825 }
5826 }
5827 }
5828
5829 fn discriminator_name_affinity(schema_name: &str, value: &str) -> usize {
5830 let schema_compact: String = schema_name
5831 .chars()
5832 .filter(|character| character.is_ascii_alphanumeric())
5833 .flat_map(char::to_lowercase)
5834 .collect();
5835 let value_compact: String = value
5836 .chars()
5837 .filter(|character| character.is_ascii_alphanumeric())
5838 .flat_map(char::to_lowercase)
5839 .collect();
5840 let exact_bonus = usize::from(
5841 !value_compact.is_empty() && schema_compact.contains(value_compact.as_str()),
5842 ) * 10;
5843 let token_matches = value
5844 .split(|character: char| !character.is_ascii_alphanumeric())
5845 .filter(|token| !token.is_empty())
5846 .filter(|token| schema_compact.contains(&token.to_ascii_lowercase()))
5847 .count();
5848 exact_bonus + token_matches
5849 }
5850
5851 fn generate_discriminator_value_from_name(&self, schema_name: &str) -> String {
5852 let mut result = String::new();
5854 let mut chars = schema_name.chars().peekable();
5855 let mut first = true;
5856
5857 while let Some(c) = chars.next() {
5858 if c.is_uppercase()
5859 && !first
5860 && chars
5861 .peek()
5862 .map(|&next| next.is_lowercase())
5863 .unwrap_or(false)
5864 {
5865 result.push('.');
5866 }
5867 result.push(c.to_ascii_lowercase());
5868 first = false;
5869 }
5870
5871 if result.ends_with("event") {
5873 result = result[..result.len() - 5].to_string();
5874 }
5875
5876 if schema_name.starts_with("Response") && !result.starts_with("response.") {
5878 result = format!("response.{}", result.trim_start_matches("response"));
5879 }
5880
5881 result
5882 }
5883
5884 fn to_rust_variant_name(&self, schema_name: &str) -> String {
5885 let mut name = schema_name;
5887
5888 if name.starts_with("Response") && name.len() > 8 {
5890 name = &name[8..]; }
5892
5893 if name.ends_with("Event") && name.len() > 5 {
5895 name = &name[..name.len() - 5]; }
5897
5898 name = name.trim_matches('_');
5900
5901 if name.is_empty() {
5903 schema_name.to_string()
5904 } else {
5905 self.discriminator_to_variant_name(name)
5907 }
5908 }
5909
5910 fn hoist_inline_string_enum(
5934 &mut self,
5935 schema: &Schema,
5936 enum_values: Vec<String>,
5937 primary_name: String,
5938 dependencies: &mut HashSet<String>,
5939 ) -> SchemaType {
5940 let suffix = enum_values
5941 .first()
5942 .map(|value| self.to_pascal_case(value))
5943 .unwrap_or_else(|| "Variant".to_string());
5944 let collision_name = format!("{primary_name}{suffix}");
5945 let enum_type_name =
5946 self.allocate_inline_schema_name(&primary_name, &collision_name, "string-enum", schema);
5947 let should_insert = !self.resolved_cache.contains_key(&enum_type_name);
5948
5949 if should_insert {
5952 self.resolved_cache.insert(
5953 enum_type_name.clone(),
5954 AnalyzedSchema {
5955 name: enum_type_name.clone(),
5956 original: serde_json::to_value(schema).unwrap_or(Value::Null),
5957 schema_type: SchemaType::StringEnum {
5958 values: enum_values,
5959 },
5960 dependencies: HashSet::new(),
5961 nullable: false,
5962 description: schema.details().description.clone(),
5963 default: schema.details().default.clone(),
5964 },
5965 );
5966 }
5967
5968 dependencies.insert(enum_type_name.clone());
5970 SchemaType::Reference {
5971 target: enum_type_name,
5972 }
5973 }
5974
5975 fn analyze_array_schema(
5976 &mut self,
5977 schema: &Schema,
5978 parent_schema_name: &str,
5979 dependencies: &mut HashSet<String>,
5980 ) -> Result<SchemaType> {
5981 let details = schema.details();
5982
5983 if let Some(positions) = details.positional_items() {
5987 return self.analyze_positional_items(
5988 positions,
5989 details,
5990 parent_schema_name,
5991 dependencies,
5992 );
5993 }
5994
5995 if let Some(items_schema) = details.item_schema() {
5997 let item_type = self.analyze_item_schema(
5998 items_schema,
5999 parent_schema_name,
6000 &format!("{parent_schema_name}Item"),
6001 dependencies,
6002 )?;
6003 let item_type = self.hoist_inline_property_type(
6004 parent_schema_name,
6005 "Item",
6006 item_type,
6007 dependencies,
6008 );
6009 Ok(SchemaType::Array {
6010 item_type: Box::new(item_type),
6011 })
6012 } else {
6013 Ok(
6015 self.untyped_value_array(
6016 self.untyped_context(""),
6017 UntypedReason::ArrayWithoutItems,
6018 ),
6019 )
6020 }
6021 }
6022
6023 fn shared_branch_type(&self, branches: &[Schema]) -> Option<SchemaType> {
6032 let mut mapped: Option<(String, Option<String>)> = None;
6033 let mut scalar_kind: Option<OpenApiSchemaType> = None;
6034 let mut formats_agree = true;
6035 for branch in branches {
6036 if branch.reference().is_some() {
6037 return None;
6038 }
6039 let details = branch.details();
6040 if details.enum_values.is_some()
6041 || details.const_value.is_some()
6042 || details.properties.is_some()
6043 {
6044 return None;
6045 }
6046 let scalar = match branch.schema_type()? {
6047 scalar @ (OpenApiSchemaType::String
6048 | OpenApiSchemaType::Integer
6049 | OpenApiSchemaType::Number
6050 | OpenApiSchemaType::Boolean) => scalar.clone(),
6051 _ => return None,
6052 };
6053 match &scalar_kind {
6054 Some(existing) if *existing != scalar => return None,
6055 Some(_) => {}
6056 None => scalar_kind = Some(scalar.clone()),
6057 }
6058
6059 let candidate = self.type_mapper.map(scalar, details);
6060 let candidate = (candidate.rust_type, candidate.serde_with);
6061 match &mapped {
6062 Some(existing) if *existing != candidate => formats_agree = false,
6063 Some(_) => {}
6064 None => mapped = Some(candidate),
6065 }
6066 }
6067
6068 if formats_agree {
6069 return mapped.map(|(rust_type, serde_with)| SchemaType::Primitive {
6070 rust_type,
6071 serde_with,
6072 });
6073 }
6074
6075 let scalar = scalar_kind?;
6081 let mapped = self
6082 .type_mapper
6083 .map(scalar, &crate::openapi::SchemaDetails::default());
6084 Some(SchemaType::Primitive {
6085 rust_type: mapped.rust_type,
6086 serde_with: mapped.serde_with,
6087 })
6088 }
6089
6090 fn expand_pointer_branches(&self, branches: &[Schema]) -> Option<Vec<Schema>> {
6099 let mut expanded = Vec::with_capacity(branches.len());
6100 let mut changed = false;
6101 for branch in branches {
6102 let resolved = branch
6103 .reference()
6104 .filter(|reference| self.extract_schema_name(reference).is_none())
6105 .and_then(|reference| reference.strip_prefix('#'))
6106 .filter(|pointer| pointer.starts_with('/'))
6107 .and_then(|pointer| self.openapi_spec.pointer(pointer))
6108 .and_then(|value| Schema::deserialize(value).ok())
6109 .filter(|schema| schema.reference().is_none());
6110 match resolved {
6111 Some(schema) => {
6112 expanded.push(schema);
6113 changed = true;
6114 }
6115 None => expanded.push(branch.clone()),
6116 }
6117 }
6118 changed.then_some(expanded)
6119 }
6120
6121 fn analyze_object_with_variants(
6134 &mut self,
6135 schema: &Schema,
6136 branches: &[Schema],
6137 schema_name: &str,
6138 dependencies: &mut HashSet<String>,
6139 ) -> Result<Option<SchemaType>> {
6140 let details = schema.details();
6141 if details.properties.as_ref().is_none_or(BTreeMap::is_empty) || branches.is_empty() {
6142 return Ok(None);
6143 }
6144 if schema.is_nullable_pattern() || Self::union_only_constrains_requiredness(branches) {
6147 return Ok(None);
6148 }
6149
6150 let base = self.analyze_object_schema(schema, dependencies)?;
6151 let SchemaType::Object {
6152 properties,
6153 required,
6154 additional_properties,
6155 ..
6156 } = base
6157 else {
6158 return Ok(None);
6159 };
6160
6161 let preferred_variant_name = format!("{schema_name}Variant");
6162 let variant_name = self.allocate_inline_schema_name(
6163 &preferred_variant_name,
6164 &format!("{preferred_variant_name}Inline"),
6165 "object-variant",
6166 schema,
6167 );
6168 let variant_type = self.analyze_anyof_union(
6169 branches,
6170 schema.discriminator(),
6171 dependencies,
6172 &variant_name,
6173 )?;
6174 if matches!(variant_type, SchemaType::Untyped { .. }) {
6177 return Ok(Some(SchemaType::Object {
6178 properties,
6179 required,
6180 additional_properties,
6181 variant: None,
6182 }));
6183 }
6184
6185 self.resolved_cache.insert(
6186 variant_name.clone(),
6187 AnalyzedSchema {
6188 name: variant_name.clone(),
6189 original: Value::Null,
6190 dependencies: schema_type_dependencies(&variant_type),
6191 schema_type: variant_type,
6192 nullable: false,
6193 description: None,
6194 default: None,
6195 },
6196 );
6197 dependencies.insert(variant_name.clone());
6198
6199 Ok(Some(SchemaType::Object {
6200 properties,
6201 required,
6202 additional_properties,
6203 variant: Some(SchemaRef {
6204 target: variant_name,
6205 nullable: false,
6206 }),
6207 }))
6208 }
6209
6210 #[allow(clippy::result_unit_err)]
6218 fn resolve_boolean_branches(branches: &[Schema]) -> std::result::Result<Vec<Schema>, ()> {
6219 if branches
6220 .iter()
6221 .any(|branch| matches!(branch, Schema::Bool(true)))
6222 {
6223 return Err(());
6224 }
6225 Ok(branches
6226 .iter()
6227 .filter(|branch| !matches!(branch, Schema::Bool(false)))
6228 .cloned()
6229 .collect())
6230 }
6231
6232 fn union_only_constrains_requiredness(branches: &[Schema]) -> bool {
6241 !branches.is_empty()
6242 && branches
6243 .iter()
6244 .all(Self::schema_only_constrains_requiredness)
6245 }
6246
6247 fn schema_only_constrains_requiredness(schema: &Schema) -> bool {
6251 let keys_are_requiredness_or_annotations = serde_json::to_value(schema)
6252 .ok()
6253 .and_then(|value| value.as_object().cloned())
6254 .is_some_and(|object| {
6255 object.keys().all(|key| {
6256 matches!(
6257 key.as_str(),
6258 "required"
6259 | "not"
6260 | "anyOf"
6261 | "oneOf"
6262 | "title"
6263 | "description"
6264 | "deprecated"
6265 | "readOnly"
6266 | "writeOnly"
6267 | "examples"
6268 | "example"
6269 | "default"
6270 | "externalDocs"
6271 | "xml"
6272 | "$comment"
6273 ) || key.starts_with("x-")
6274 })
6275 });
6276 if !keys_are_requiredness_or_annotations {
6277 return false;
6278 }
6279
6280 match schema {
6281 Schema::AnyOf { any_of, .. } => {
6282 !any_of.is_empty() && any_of.iter().all(Self::schema_only_constrains_requiredness)
6283 }
6284 Schema::OneOf { one_of, .. } => {
6285 !one_of.is_empty() && one_of.iter().all(Self::schema_only_constrains_requiredness)
6286 }
6287 other => {
6288 let details = other.details();
6289 details
6290 .required
6291 .as_ref()
6292 .is_some_and(|required| !required.is_empty())
6293 || details
6294 .not
6295 .as_deref()
6296 .is_some_and(Self::schema_only_constrains_requiredness)
6297 }
6298 }
6299 }
6300
6301 fn analyze_empty_union(
6308 &mut self,
6309 schema: &Schema,
6310 dependencies: &mut HashSet<String>,
6311 ) -> Result<SchemaType> {
6312 let Some(declared) = schema
6316 .declared_type()
6317 .cloned()
6318 .or_else(|| schema.inferred_type())
6319 .or_else(|| {
6320 schema
6321 .details()
6322 .properties
6323 .is_some()
6324 .then_some(OpenApiSchemaType::Object)
6325 })
6326 else {
6327 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema));
6328 };
6329 match declared {
6330 OpenApiSchemaType::Object => self.analyze_object_schema(schema, dependencies),
6331 OpenApiSchemaType::Array => {
6332 let context = self
6333 .current_schema_name
6334 .clone()
6335 .unwrap_or_else(|| "Inline".to_string());
6336 self.analyze_array_schema(schema, &context, dependencies)
6337 }
6338 scalar => {
6339 let mapped = self.type_mapper.map(scalar, schema.details());
6340 Ok(SchemaType::Primitive {
6341 rust_type: mapped.rust_type,
6342 serde_with: mapped.serde_with,
6343 })
6344 }
6345 }
6346 }
6347
6348 fn resolve_pointer_schema(
6361 &mut self,
6362 reference: &str,
6363 dependencies: &mut HashSet<String>,
6364 ) -> Result<Option<SchemaType>> {
6365 let Some(pointer) = reference.strip_prefix('#') else {
6366 return Ok(None);
6367 };
6368 if pointer.is_empty() || !pointer.starts_with('/') {
6369 return Ok(None);
6370 }
6371 let preferred_name = pointer_type_name(pointer);
6372 if preferred_name.is_empty() {
6373 return Ok(None);
6374 }
6375 let name = self.allocate_pointer_schema_name(pointer, &preferred_name);
6376 if self.resolved_cache.contains_key(&name) || !self.resolving_pointers.insert(name.clone())
6379 {
6380 dependencies.insert(name.clone());
6381 return Ok(Some(SchemaType::Reference { target: name }));
6382 }
6383
6384 let resolved = (|| {
6385 let value = self.openapi_spec.pointer(pointer)?.clone();
6386 Schema::deserialize(&value).ok()
6387 })();
6388 let Some(schema) = resolved else {
6389 self.resolving_pointers.remove(&name);
6390 return Ok(None);
6391 };
6392
6393 let saved_context = self.current_schema_name.clone();
6398 self.current_schema_name = Some(name.clone());
6399 let analyzed = self.analyze_schema_value(&schema, &name);
6400 self.current_schema_name = saved_context;
6401 self.resolving_pointers.remove(&name);
6402 let analyzed = analyzed?;
6403 dependencies.extend(analyzed.dependencies.iter().cloned());
6404 if analyzed.schema_type.renders_inline() {
6405 return Ok(Some(analyzed.schema_type));
6406 }
6407
6408 self.resolved_cache.insert(name.clone(), analyzed);
6409 dependencies.insert(name.clone());
6410 Ok(Some(SchemaType::Reference { target: name }))
6411 }
6412
6413 fn hoist_inline_property_type(
6422 &mut self,
6423 schema_name: &str,
6424 property_name: &str,
6425 schema_type: SchemaType,
6426 dependencies: &mut HashSet<String>,
6427 ) -> SchemaType {
6428 if schema_type.renders_inline() {
6429 return schema_type;
6430 }
6431
6432 use heck::ToPascalCase;
6433
6434 let preferred_name = format!("{schema_name}{}", property_name.to_pascal_case());
6435 let hoisted_name = self.allocate_synthetic_schema_name(
6436 &preferred_name,
6437 &format!("{preferred_name}Inline"),
6438 "hoisted-property",
6439 format!("{schema_type:?}"),
6440 );
6441 let hoisted_dependencies = schema_type_dependencies(&schema_type);
6442 self.resolved_cache.insert(
6443 hoisted_name.clone(),
6444 AnalyzedSchema {
6445 name: hoisted_name.clone(),
6446 original: Value::Null,
6447 schema_type,
6448 dependencies: hoisted_dependencies,
6449 nullable: false,
6450 description: None,
6451 default: None,
6452 },
6453 );
6454 dependencies.insert(hoisted_name.clone());
6455 SchemaType::Reference {
6456 target: hoisted_name,
6457 }
6458 }
6459
6460 fn analyze_positional_items(
6473 &mut self,
6474 positions: &[Schema],
6475 details: &crate::openapi::SchemaDetails,
6476 parent_schema_name: &str,
6477 dependencies: &mut HashSet<String>,
6478 ) -> Result<SchemaType> {
6479 if details.positional_items_are_exact() && !positions.is_empty() {
6480 let mut element_types = Vec::with_capacity(positions.len());
6481 for (index, position) in positions.iter().enumerate() {
6482 let element_type = self.analyze_item_schema(
6483 position,
6484 parent_schema_name,
6485 &format!("{parent_schema_name}Item{}", index + 1),
6486 dependencies,
6487 )?;
6488 element_types.push(self.hoist_inline_property_type(
6489 parent_schema_name,
6490 &format!("Item{}", index + 1),
6491 element_type,
6492 dependencies,
6493 ));
6494 }
6495 return Ok(SchemaType::Tuple { element_types });
6496 }
6497
6498 if details.positional_items_are_closed()
6502 && let Some(shared) = shared_positional_schema(positions)
6503 {
6504 let item_type = self.analyze_item_schema(
6505 shared,
6506 parent_schema_name,
6507 &format!("{parent_schema_name}Item"),
6508 dependencies,
6509 )?;
6510 return Ok(SchemaType::Array {
6511 item_type: Box::new(item_type),
6512 });
6513 }
6514
6515 Ok(self.untyped_value_array(self.untyped_context(""), UntypedReason::OpenPositionalItems))
6516 }
6517
6518 fn analyze_item_schema(
6525 &mut self,
6526 items_schema: &Schema,
6527 parent_schema_name: &str,
6528 inline_name: &str,
6529 dependencies: &mut HashSet<String>,
6530 ) -> Result<SchemaType> {
6531 let item_type = match items_schema {
6532 Schema::Bool(accepts_anything) => self.untyped_value(
6533 self.untyped_context(""),
6534 if *accepts_anything {
6535 UntypedReason::AnySchema
6536 } else {
6537 UntypedReason::NeverMatches
6538 },
6539 ),
6540 Schema::Reference { reference, .. } => {
6541 if let Some(target) = self.extract_schema_name(reference) {
6543 let target = target.to_string();
6544 dependencies.insert(target.clone());
6545 SchemaType::Reference { target }
6546 } else {
6547 self.resolve_pointer_schema(reference, dependencies)?
6548 .ok_or_else(|| GeneratorError::UnresolvedReference(reference.to_string()))?
6549 }
6550 }
6551 Schema::RecursiveRef { recursive_ref, .. } => {
6552 if recursive_ref == "#" {
6554 let target = self
6556 .find_recursive_anchor_schema()
6557 .unwrap_or_else(|| parent_schema_name.to_string());
6558 dependencies.insert(target.clone());
6559 SchemaType::Reference { target }
6560 } else {
6561 let target = self
6562 .extract_schema_name(recursive_ref)
6563 .unwrap_or("RecursiveType")
6564 .to_string();
6565 dependencies.insert(target.clone());
6566 SchemaType::Reference { target }
6567 }
6568 }
6569 Schema::Typed { schema_type, .. } => {
6570 match schema_type {
6572 OpenApiSchemaType::String => {
6573 match items_schema
6577 .details()
6578 .string_enum_values()
6579 .filter(|values| !values.is_empty())
6580 {
6581 Some(values) => self.hoist_inline_string_enum(
6582 items_schema,
6583 values,
6584 inline_name.to_string(),
6585 dependencies,
6586 ),
6587 None => SchemaType::Primitive {
6588 rust_type: "String".to_string(),
6589 serde_with: None,
6590 },
6591 }
6592 }
6593 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
6594 let details = items_schema.details();
6595 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
6596 SchemaType::Primitive {
6597 rust_type,
6598 serde_with: None,
6599 }
6600 }
6601 OpenApiSchemaType::Boolean => SchemaType::Primitive {
6602 rust_type: "bool".to_string(),
6603 serde_with: None,
6604 },
6605 OpenApiSchemaType::Object => {
6606 let preferred_object_type_name = inline_name.to_string();
6608 let object_type_name = self.allocate_inline_schema_name(
6609 &preferred_object_type_name,
6610 &format!("{preferred_object_type_name}Inline"),
6611 "array-item-object",
6612 items_schema,
6613 );
6614
6615 self.add_allocated_object_schema(
6616 object_type_name,
6617 items_schema,
6618 dependencies,
6619 )?
6620 }
6621 OpenApiSchemaType::Array => {
6622 self.analyze_array_schema(items_schema, parent_schema_name, dependencies)?
6624 }
6625 _ => self.untyped_value(
6626 self.untyped_context(""),
6627 UntypedReason::UnsupportedTypeKeyword,
6628 ),
6629 }
6630 }
6631 Schema::OneOf { .. } | Schema::AnyOf { .. } => {
6632 let analyzed = self.analyze_schema_value(items_schema, "ArrayItem")?;
6634
6635 match &analyzed.schema_type {
6637 SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => {
6638 let preferred_union_name = format!("{inline_name}Union");
6641 let union_name = self.allocate_inline_schema_name(
6642 &preferred_union_name,
6643 &format!("{preferred_union_name}Inline"),
6644 "array-item-union",
6645 items_schema,
6646 );
6647
6648 let mut union_schema = analyzed;
6650 union_schema.name = union_name.clone();
6651
6652 self.resolved_cache.insert(union_name.clone(), union_schema);
6654
6655 dependencies.insert(union_name.clone());
6657
6658 SchemaType::Reference { target: union_name }
6660 }
6661 _ => analyzed.schema_type,
6662 }
6663 }
6664 Schema::Untyped { .. } => {
6665 if let Some(inferred) = items_schema.inferred_type() {
6667 match inferred {
6668 OpenApiSchemaType::Object => {
6669 let preferred_object_type_name = inline_name.to_string();
6671 let object_type_name = self.allocate_inline_schema_name(
6672 &preferred_object_type_name,
6673 &format!("{preferred_object_type_name}Inline"),
6674 "array-item-object",
6675 items_schema,
6676 );
6677
6678 self.add_allocated_object_schema(
6679 object_type_name,
6680 items_schema,
6681 dependencies,
6682 )?
6683 }
6684 OpenApiSchemaType::String => {
6685 match items_schema
6688 .details()
6689 .string_enum_values()
6690 .filter(|values| !values.is_empty())
6691 {
6692 Some(values) => self.hoist_inline_string_enum(
6693 items_schema,
6694 values,
6695 inline_name.to_string(),
6696 dependencies,
6697 ),
6698 None => SchemaType::Primitive {
6699 rust_type: "String".to_string(),
6700 serde_with: None,
6701 },
6702 }
6703 }
6704 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
6705 let details = items_schema.details();
6706 let rust_type = self.get_number_rust_type(inferred, details);
6707 SchemaType::Primitive {
6708 rust_type,
6709 serde_with: None,
6710 }
6711 }
6712 OpenApiSchemaType::Boolean => SchemaType::Primitive {
6713 rust_type: "bool".to_string(),
6714 serde_with: None,
6715 },
6716 OpenApiSchemaType::Null => SchemaType::Primitive {
6719 rust_type: self.type_mapper.null_unit().rust_type,
6720 serde_with: None,
6721 },
6722 _ => self.untyped_value(
6723 self.untyped_context(""),
6724 UntypedReason::UnsupportedTypeKeyword,
6725 ),
6726 }
6727 } else {
6728 self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema)
6729 }
6730 }
6731 _ => self.analyze_property_schema_with_context(items_schema, None, dependencies)?,
6736 };
6737
6738 Ok(self.nullable_container_value(items_schema, item_type))
6739 }
6740
6741 fn get_number_rust_type(
6742 &self,
6743 schema_type: OpenApiSchemaType,
6744 details: &crate::openapi::SchemaDetails,
6745 ) -> String {
6746 let format = details.format.as_deref();
6750 match schema_type {
6751 OpenApiSchemaType::Integer => self.integer_rust_type(details),
6752 OpenApiSchemaType::Number => self.type_mapper.number_format(format).rust_type,
6753 _ => self.type_mapper.dynamic_json().rust_type,
6754 }
6755 }
6756
6757 fn integer_rust_type(&self, details: &crate::openapi::SchemaDetails) -> String {
6763 fn integer_value(number: &serde_json::Number) -> Option<i128> {
6764 number
6765 .as_i64()
6766 .map(i128::from)
6767 .or_else(|| number.as_u64().map(i128::from))
6768 .or_else(|| {
6769 number.as_f64().and_then(|value| {
6770 (value.fract() == 0.0
6771 && value >= i128::MIN as f64
6772 && value <= i128::MAX as f64)
6773 .then_some(value as i128)
6774 })
6775 })
6776 }
6777
6778 fn below(number: &serde_json::Number, boundary: i128) -> bool {
6779 integer_value(number).is_some_and(|value| value < boundary)
6780 }
6781
6782 fn above(number: &serde_json::Number, boundary: i128) -> bool {
6783 integer_value(number).is_some_and(|value| value > boundary)
6784 }
6785
6786 let explicitly_nonnegative = details
6787 .minimum
6788 .as_ref()
6789 .and_then(integer_value)
6790 .is_some_and(|value| value >= 0)
6791 || matches!(
6792 details.exclusive_minimum,
6793 Some(crate::openapi::ExclusiveBound::Number(value)) if value >= 0.0
6794 );
6795
6796 let annotated_numbers = details
6797 .const_value
6798 .iter()
6799 .chain(details.default.iter())
6800 .chain(details.example.iter())
6801 .chain(details.enum_values.iter().flatten())
6802 .chain(details.examples.iter().flatten())
6803 .filter_map(Value::as_number)
6804 .collect::<Vec<_>>();
6805 let below_i32 = details
6806 .minimum
6807 .as_ref()
6808 .is_some_and(|number| below(number, i128::from(i32::MIN)))
6809 || annotated_numbers
6810 .iter()
6811 .any(|number| below(number, i128::from(i32::MIN)))
6812 || matches!(
6813 details.exclusive_minimum,
6814 Some(crate::openapi::ExclusiveBound::Number(value)) if value < i32::MIN as f64
6815 );
6816 let above_i32 = details
6817 .maximum
6818 .as_ref()
6819 .is_some_and(|number| above(number, i128::from(i32::MAX)))
6820 || annotated_numbers
6821 .iter()
6822 .any(|number| above(number, i128::from(i32::MAX)))
6823 || matches!(
6824 details.exclusive_maximum,
6825 Some(crate::openapi::ExclusiveBound::Number(value)) if value > i32::MAX as f64
6826 );
6827 let below_i64 = details
6828 .minimum
6829 .as_ref()
6830 .is_some_and(|number| below(number, i128::from(i64::MIN)))
6831 || annotated_numbers
6832 .iter()
6833 .any(|number| below(number, i128::from(i64::MIN)))
6834 || matches!(
6835 details.exclusive_minimum,
6836 Some(crate::openapi::ExclusiveBound::Number(value)) if value < i64::MIN as f64
6837 );
6838 let above_i64 = details
6839 .maximum
6840 .as_ref()
6841 .is_some_and(|number| above(number, i128::from(i64::MAX)))
6842 || annotated_numbers
6843 .iter()
6844 .any(|number| above(number, i128::from(i64::MAX)))
6845 || matches!(
6846 details.exclusive_maximum,
6847 Some(crate::openapi::ExclusiveBound::Number(value)) if value >= 9_223_372_036_854_775_808.0
6848 );
6849 let below_zero = details
6850 .minimum
6851 .as_ref()
6852 .and_then(integer_value)
6853 .is_some_and(|value| value < 0)
6854 || annotated_numbers
6855 .iter()
6856 .filter_map(|number| integer_value(number))
6857 .any(|value| value < 0)
6858 || matches!(
6859 details.exclusive_minimum,
6860 Some(crate::openapi::ExclusiveBound::Number(value)) if value < 0.0
6861 );
6862 let above_u32 = details
6863 .maximum
6864 .as_ref()
6865 .is_some_and(|number| above(number, i128::from(u32::MAX)))
6866 || annotated_numbers
6867 .iter()
6868 .any(|number| above(number, i128::from(u32::MAX)))
6869 || matches!(
6870 details.exclusive_maximum,
6871 Some(crate::openapi::ExclusiveBound::Number(value)) if value > u32::MAX as f64
6872 );
6873
6874 let configured = self
6875 .type_mapper
6876 .integer_format(details.format.as_deref())
6877 .rust_type;
6878 match configured.as_str() {
6879 "i32" if below_i32 || above_i32 => {
6880 if below_i64 || above_i64 {
6881 "i128".to_string()
6882 } else {
6883 "i64".to_string()
6884 }
6885 }
6886 "i64" if below_i64 => "i128".to_string(),
6887 "i64" if above_i64 && explicitly_nonnegative => "u64".to_string(),
6888 "i64" if above_i64 => "i128".to_string(),
6889 "u32" if below_zero => {
6890 if below_i64 || above_i64 {
6891 "i128".to_string()
6892 } else {
6893 "i64".to_string()
6894 }
6895 }
6896 "u32" if above_u32 => "u64".to_string(),
6897 "u64" if below_zero => "i128".to_string(),
6898 configured => configured.to_string(),
6899 }
6900 }
6901
6902 fn analyze_anyof_union(
6903 &mut self,
6904 any_of_schemas: &[Schema],
6905 discriminator: Option<&Discriminator>,
6906 dependencies: &mut HashSet<String>,
6907 context_name: &str,
6908 ) -> Result<SchemaType> {
6909 let original_indices = (0..any_of_schemas.len()).collect::<Vec<_>>();
6910
6911 let expanded_branches;
6913 let any_of_schemas = match self.expand_pointer_branches(any_of_schemas) {
6914 Some(expanded) => {
6915 expanded_branches = expanded;
6916 expanded_branches.as_slice()
6917 }
6918 None => any_of_schemas,
6919 };
6920
6921 let boolean_resolved;
6923 let boolean_resolved_indices;
6924 let any_of_schemas = match Self::resolve_boolean_branches(any_of_schemas) {
6925 Ok(resolved) => {
6926 boolean_resolved_indices = any_of_schemas
6927 .iter()
6928 .zip(original_indices.iter().copied())
6929 .filter_map(|(branch, index)| {
6930 (!matches!(branch, Schema::Bool(false))).then_some(index)
6931 })
6932 .collect::<Vec<_>>();
6933 boolean_resolved = resolved;
6934 boolean_resolved.as_slice()
6935 }
6936 Err(()) => {
6937 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema));
6938 }
6939 };
6940 let source_indices = boolean_resolved_indices.as_slice();
6941 if any_of_schemas.is_empty() {
6942 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::NeverMatches));
6943 }
6944
6945 let filtered_owned: Vec<Schema>;
6951 let filtered_indices: Vec<usize>;
6952 let (any_of_schemas, source_indices): (&[Schema], &[usize]) = if any_of_schemas
6953 .iter()
6954 .any(Schema::is_explicit_null_only)
6955 {
6956 filtered_owned = any_of_schemas
6957 .iter()
6958 .filter(|s| !s.is_explicit_null_only())
6959 .cloned()
6960 .collect();
6961 filtered_indices = any_of_schemas
6962 .iter()
6963 .zip(source_indices.iter().copied())
6964 .filter_map(|(schema, index)| (!schema.is_explicit_null_only()).then_some(index))
6965 .collect();
6966 if filtered_owned.is_empty() {
6967 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema));
6968 }
6969 if filtered_owned.len() == 1 {
6970 return self
6971 .analyze_schema_value(&filtered_owned[0], context_name)
6972 .map(|a| a.schema_type);
6973 }
6974 (&filtered_owned, &filtered_indices)
6975 } else {
6976 (any_of_schemas, source_indices)
6977 };
6978
6979 if let [only] = any_of_schemas {
6982 return self
6983 .analyze_schema_value(only, context_name)
6984 .map(|analyzed| analyzed.schema_type);
6985 }
6986
6987 if let Some(shared) = self.shared_branch_type(any_of_schemas) {
6991 return Ok(shared);
6992 }
6993
6994 let has_refs = any_of_schemas.iter().any(|s| s.is_reference());
6996 let has_objects = any_of_schemas.iter().any(|s| {
6997 matches!(s.schema_type(), Some(OpenApiSchemaType::Object))
6998 || s.inferred_type() == Some(OpenApiSchemaType::Object)
6999 });
7000 let has_arrays = any_of_schemas
7001 .iter()
7002 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Array)));
7003
7004 let all_string_like = any_of_schemas.iter().all(|s| {
7007 matches!(s.schema_type(), Some(OpenApiSchemaType::String))
7008 || s.details().const_value.is_some()
7009 });
7010
7011 if (has_refs || has_objects || has_arrays || any_of_schemas.len() > 1) && !all_string_like {
7012 if let Some(disc) = discriminator {
7014 return self.analyze_oneof_union(
7016 any_of_schemas,
7017 Some(disc),
7018 context_name,
7019 dependencies,
7020 InlineUnionKind::AnyOf,
7021 Some(source_indices),
7022 );
7023 }
7024
7025 if let Some(disc_field) = self.detect_discriminator_field(any_of_schemas) {
7027 return self.analyze_oneof_union(
7028 any_of_schemas,
7029 Some(&Discriminator {
7030 property_name: disc_field,
7031 mapping: None,
7032 default_mapping: None,
7033 extensions: crate::extensions::Extensions::default(),
7034 }),
7035 context_name,
7036 dependencies,
7037 InlineUnionKind::AnyOf,
7038 Some(source_indices),
7039 );
7040 }
7041
7042 let mut variants = Vec::new();
7044
7045 for (schema, original_index) in
7046 any_of_schemas.iter().zip(source_indices.iter().copied())
7047 {
7048 if let Some(ref_str) = schema.reference() {
7049 if let Some(target) = self.extract_schema_name(ref_str) {
7050 dependencies.insert(target.to_string());
7051 variants.push(SchemaRef {
7052 target: target.to_string(),
7053 nullable: self.schema_or_reference_is_nullable(schema),
7054 });
7055 }
7056 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object))
7057 || schema.inferred_type() == Some(OpenApiSchemaType::Object)
7058 {
7059 let inline_type_name = self.generate_inline_type_name(schema, original_index);
7061
7062 let inline_type_name = self.add_inline_union_branch_schema(
7064 &inline_type_name,
7065 schema,
7066 dependencies,
7067 context_name,
7068 InlineUnionKind::AnyOf,
7069 original_index,
7070 None,
7071 )?;
7072
7073 variants.push(SchemaRef {
7074 target: inline_type_name,
7075 nullable: false,
7076 });
7077 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Array)) {
7078 let preferred_array_type_name =
7080 if let Some(items_schema) = schema.details().item_schema() {
7081 if let Some(ref_str) = items_schema.reference() {
7082 if let Some(item_type_name) = self.extract_schema_name(ref_str) {
7083 dependencies.insert(item_type_name.to_string());
7084 format!("{item_type_name}Array")
7085 } else {
7086 self.generate_context_aware_name(
7087 context_name,
7088 "Array",
7089 original_index,
7090 Some(schema),
7091 )
7092 }
7093 } else {
7094 self.generate_context_aware_name(
7095 context_name,
7096 "Array",
7097 original_index,
7098 Some(schema),
7099 )
7100 }
7101 } else {
7102 self.generate_context_aware_name(
7103 context_name,
7104 "Array",
7105 original_index,
7106 Some(schema),
7107 )
7108 };
7109 let array_type_name = self.allocate_inline_union_branch_name(
7110 &preferred_array_type_name,
7111 context_name,
7112 InlineUnionKind::AnyOf,
7113 original_index,
7114 None,
7115 schema,
7116 );
7117 let array_type =
7119 self.analyze_array_schema(schema, context_name, dependencies)?;
7120
7121 self.resolved_cache.insert(
7123 array_type_name.clone(),
7124 AnalyzedSchema {
7125 name: array_type_name.clone(),
7126 original: serde_json::to_value(schema).unwrap_or(Value::Null),
7127 schema_type: array_type,
7128 dependencies: HashSet::new(),
7129 nullable: false,
7130 description: Some("Array variant in union".to_string()),
7131 default: None,
7132 },
7133 );
7134
7135 dependencies.insert(array_type_name.clone());
7137
7138 variants.push(SchemaRef {
7139 target: array_type_name,
7140 nullable: false,
7141 });
7142 } else if let Some(schema_type) = schema.schema_type() {
7143 let primitive_unions = self
7153 .type_mapper
7154 .config_shape_primitive_unions()
7155 .unwrap_or(true);
7156
7157 if primitive_unions {
7158 variants.push(SchemaRef {
7159 target: self
7160 .openapi_type_to_rust_type(schema_type.clone(), schema.details()),
7161 nullable: false,
7162 });
7163 } else {
7164 let preferred_inline_type_name = match schema_type {
7165 OpenApiSchemaType::String => {
7166 if original_index == 0 {
7167 format!("{context_name}String")
7168 } else {
7169 format!("{context_name}StringVariant{original_index}")
7170 }
7171 }
7172 OpenApiSchemaType::Number => {
7173 if original_index == 0 {
7174 format!("{context_name}Number")
7175 } else {
7176 format!("{context_name}NumberVariant{original_index}")
7177 }
7178 }
7179 OpenApiSchemaType::Integer => {
7180 if original_index == 0 {
7181 format!("{context_name}Integer")
7182 } else {
7183 format!("{context_name}IntegerVariant{original_index}")
7184 }
7185 }
7186 OpenApiSchemaType::Boolean => {
7187 if original_index == 0 {
7188 format!("{context_name}Boolean")
7189 } else {
7190 format!("{context_name}BooleanVariant{original_index}")
7191 }
7192 }
7193 _ => format!("{context_name}Variant{original_index}"),
7194 };
7195 let inline_type_name = self.allocate_inline_union_branch_name(
7196 &preferred_inline_type_name,
7197 context_name,
7198 InlineUnionKind::AnyOf,
7199 original_index,
7200 None,
7201 schema,
7202 );
7203
7204 let rust_type =
7205 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
7206
7207 self.resolved_cache.insert(
7208 inline_type_name.clone(),
7209 AnalyzedSchema {
7210 name: inline_type_name.clone(),
7211 original: serde_json::to_value(schema).unwrap_or(Value::Null),
7212 schema_type: SchemaType::Primitive {
7213 rust_type,
7214 serde_with: None,
7215 },
7216 dependencies: HashSet::new(),
7217 nullable: false,
7218 description: schema.details().description.clone(),
7219 default: None,
7220 },
7221 );
7222
7223 dependencies.insert(inline_type_name.clone());
7224
7225 variants.push(SchemaRef {
7226 target: inline_type_name,
7227 nullable: false,
7228 });
7229 }
7230 } else {
7231 let inline_type_name = self.generate_context_aware_name(
7239 context_name,
7240 "InlineVariant",
7241 original_index,
7242 Some(schema),
7243 );
7244 let inline_type_name = self.add_inline_union_branch_schema(
7245 &inline_type_name,
7246 schema,
7247 dependencies,
7248 context_name,
7249 InlineUnionKind::AnyOf,
7250 original_index,
7251 None,
7252 )?;
7253 dependencies.insert(inline_type_name.clone());
7254 variants.push(SchemaRef {
7255 target: inline_type_name,
7256 nullable: false,
7257 });
7258 }
7259 }
7260
7261 if !variants.is_empty() {
7262 return Ok(SchemaType::Union {
7263 variants,
7264 exclusive: false,
7265 });
7266 }
7267 }
7268
7269 let all_strings = any_of_schemas.iter().all(|schema| {
7271 matches!(schema.schema_type(), Some(OpenApiSchemaType::String))
7272 || schema.details().const_value.is_some()
7273 });
7274
7275 if all_strings {
7276 let mut enum_values = Vec::new();
7278 let mut has_open_string = false;
7279
7280 for schema in any_of_schemas {
7281 match schema
7286 .details()
7287 .string_enum_values()
7288 .filter(|values| !values.is_empty())
7289 {
7290 Some(values) => {
7291 for value in values {
7292 if !enum_values.contains(&value) {
7293 enum_values.push(value);
7294 }
7295 }
7296 }
7297 None => {
7298 if matches!(schema.schema_type(), Some(OpenApiSchemaType::String)) {
7299 has_open_string = true;
7300 }
7301 }
7302 }
7303 }
7304
7305 if !enum_values.is_empty() {
7306 if has_open_string {
7307 return Ok(SchemaType::ExtensibleEnum {
7310 known_values: enum_values,
7311 });
7312 } else {
7313 return Ok(SchemaType::StringEnum {
7315 values: enum_values,
7316 });
7317 }
7318 }
7319 }
7320
7321 Ok(self.untyped_value(
7323 self.untyped_context(""),
7324 UntypedReason::UnrepresentableUnion,
7325 ))
7326 }
7327
7328 fn find_recursive_anchor_schema(&self) -> Option<String> {
7330 for (schema_name, schema) in &self.schemas {
7332 let details = schema.details();
7333 if details.recursive_anchor == Some(true) {
7334 return Some(schema_name.clone());
7335 }
7336 }
7337
7338 None
7342 }
7343
7344 fn should_use_dynamic_json(&self, schema: &Schema) -> bool {
7347 if let Schema::AnyOf { any_of, .. } = schema {
7349 if any_of.len() == 2 {
7350 let has_null = any_of
7351 .iter()
7352 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)));
7353 let has_empty_object = any_of.iter().any(|s| self.is_dynamic_object_pattern(s));
7354
7355 if has_null && has_empty_object {
7356 return true;
7357 }
7358 }
7359 }
7360
7361 self.is_dynamic_object_pattern(schema)
7363 }
7364
7365 fn is_dynamic_object_pattern(&self, schema: &Schema) -> bool {
7367 let is_object = match schema.schema_type() {
7369 Some(OpenApiSchemaType::Object) => true,
7370 None => schema.inferred_type() == Some(OpenApiSchemaType::Object),
7371 _ => false,
7372 };
7373
7374 if !is_object {
7375 return false;
7376 }
7377
7378 let details = schema.details();
7379
7380 if self.has_explicit_additional_properties(schema) {
7385 return false;
7386 }
7387
7388 let no_properties = details
7390 .properties
7391 .as_ref()
7392 .map(|props| props.is_empty())
7393 .unwrap_or(true);
7394
7395 if no_properties {
7396 let has_structural_constraints = details
7399 .required
7400 .as_ref()
7401 .map(|req| !req.is_empty())
7402 .unwrap_or(false)
7403 || details.pattern_properties.is_some()
7404 || details.property_names.is_some()
7405 || details.min_properties.is_some()
7406 || details.max_properties.is_some()
7407 || details.dependent_required.is_some()
7408 || details.dependent_schemas.is_some()
7409 || details.if_schema.is_some()
7410 || details.then_schema.is_some()
7411 || details.else_schema.is_some();
7412
7413 return !has_structural_constraints;
7414 }
7415
7416 false
7417 }
7418
7419 fn has_explicit_additional_properties(&self, schema: &Schema) -> bool {
7421 let details = schema.details();
7422 details.additional_properties.is_some()
7423 }
7424
7425 fn analyze_operations(&mut self, analysis: &mut SchemaAnalysis) -> Result<()> {
7427 let spec: crate::openapi::OpenApiSpec = parse_spec_document(&self.openapi_spec)?;
7428 let mut canonical_operation_ids = HashSet::new();
7433
7434 if let Some(paths) = &spec.paths {
7435 for (path, path_item) in paths {
7436 let resolved = self.resolve_path_item(path_item, &spec)?;
7438 let pi: &crate::openapi::PathItem = resolved.as_ref().unwrap_or(path_item);
7439 self.ingest_path_item_operations(path, pi, analysis, &mut canonical_operation_ids)?;
7440 }
7441 }
7442 if let Some(webhooks) = &spec.webhooks {
7449 for (name, path_item) in webhooks {
7450 let synthetic_path = format!("/__webhook__/{name}");
7451 self.ingest_path_item_operations(
7452 &synthetic_path,
7453 path_item,
7454 analysis,
7455 &mut canonical_operation_ids,
7456 )?;
7457 }
7458 }
7459 Ok(())
7460 }
7461
7462 fn resolve_path_item(
7466 &self,
7467 path_item: &crate::openapi::PathItem,
7468 spec: &crate::openapi::OpenApiSpec,
7469 ) -> Result<Option<crate::openapi::PathItem>> {
7470 let Some(reference) = &path_item.reference else {
7471 return Ok(None);
7472 };
7473 let target_name = reference
7474 .strip_prefix("#/components/pathItems/")
7475 .ok_or_else(|| {
7476 GeneratorError::UnresolvedReference(format!(
7477 "Path Item $ref must point at #/components/pathItems/{{name}}, got {reference}"
7478 ))
7479 })?;
7480 let pi = spec
7481 .components
7482 .as_ref()
7483 .and_then(|c| c.path_items.as_ref())
7484 .and_then(|map| map.get(target_name))
7485 .ok_or_else(|| {
7486 GeneratorError::UnresolvedReference(format!(
7487 "Path Item ref {reference} not found in components/pathItems"
7488 ))
7489 })?;
7490 Ok(Some(pi.clone()))
7491 }
7492
7493 fn ingest_path_item_operations(
7494 &mut self,
7495 path: &str,
7496 path_item: &crate::openapi::PathItem,
7497 analysis: &mut SchemaAnalysis,
7498 canonical_operation_ids: &mut HashSet<String>,
7499 ) -> Result<()> {
7500 for (method, operation) in path_item.operations() {
7501 let raw_operation_id = operation
7503 .operation_id
7504 .clone()
7505 .unwrap_or_else(|| Self::generate_operation_id(method, path));
7506
7507 let operation_id = if canonical_operation_ids
7518 .contains(&Self::canonical_operation_id(&raw_operation_id))
7519 {
7520 let method_lower = method.to_lowercase();
7521 let mut candidate = format!("{}_{}", raw_operation_id, method_lower);
7522 let mut suffix = 2;
7523 while canonical_operation_ids.contains(&Self::canonical_operation_id(&candidate)) {
7524 candidate = format!("{}_{}_{}", raw_operation_id, method_lower, suffix);
7525 suffix += 1;
7526 }
7527 eprintln!(
7528 "⚠️ duplicate operationId `{}` at `{} {}` — disambiguated to `{}`",
7529 raw_operation_id, method, path, candidate
7530 );
7531 candidate
7532 } else {
7533 raw_operation_id.clone()
7534 };
7535
7536 let (op_info, responses) = self.analyze_single_operation(
7537 &operation_id,
7538 method,
7539 path,
7540 operation,
7541 path_item.parameters.as_ref(),
7542 analysis,
7543 )?;
7544 analysis
7545 .operation_id_aliases
7546 .entry(raw_operation_id)
7547 .or_default()
7548 .push(operation_id.clone());
7549 canonical_operation_ids.insert(Self::canonical_operation_id(&operation_id));
7550 analysis
7551 .operation_responses
7552 .insert(operation_id.clone(), responses);
7553 analysis.operations.insert(operation_id, op_info);
7554 }
7555 Ok(())
7556 }
7557
7558 fn canonical_operation_id(operation_id: &str) -> String {
7559 use heck::ToPascalCase;
7560 operation_id.replace('.', "_").to_pascal_case()
7561 }
7562
7563 fn generate_operation_id(method: &str, path: &str) -> String {
7566 let mut operation_id = method.to_lowercase();
7568
7569 let path_parts: Vec<&str> = path.trim_start_matches('/').split('/').collect();
7571
7572 for part in path_parts {
7573 if part.is_empty() {
7574 continue;
7575 }
7576
7577 let cleaned_part = if part.starts_with('{') && part.ends_with('}') {
7579 &part[1..part.len() - 1]
7580 } else {
7581 part
7582 };
7583
7584 let pascal_case_part = cleaned_part
7586 .split(&['-', '_'][..])
7587 .map(|s| {
7588 let mut chars = s.chars();
7589 match chars.next() {
7590 None => String::new(),
7591 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
7592 }
7593 })
7594 .collect::<String>();
7595
7596 operation_id.push_str(&pascal_case_part);
7597 }
7598
7599 operation_id
7600 }
7601
7602 fn analyze_single_operation(
7604 &mut self,
7605 operation_id: &str,
7606 method: &str,
7607 path: &str,
7608 operation: &crate::openapi::Operation,
7609 path_item_parameters: Option<&Vec<crate::openapi::Parameter>>,
7610 _analysis: &mut SchemaAnalysis,
7611 ) -> Result<(OperationInfo, BTreeMap<String, OperationResponse>)> {
7612 let raw_path_item = self
7613 .openapi_spec
7614 .get("paths")
7615 .and_then(|paths| paths.get(path))
7616 .cloned();
7617 let raw_operation = raw_path_item
7618 .as_ref()
7619 .and_then(|path_item| path_item.get(method.to_ascii_lowercase()))
7620 .cloned();
7621 let request_body = operation
7622 .request_body
7623 .as_ref()
7624 .map(|request_body| self.resolve_request_body(request_body))
7625 .transpose()?;
7626 let mut op_info = OperationInfo {
7627 operation_id: operation_id.to_string(),
7628 method: method.to_uppercase(),
7629 path: normalize_operation_path(path),
7630 summary: operation.summary.clone(),
7631 description: operation.description.clone(),
7632 request_body: None,
7633 request_body_required: request_body
7635 .as_ref()
7636 .and_then(|rb| rb.required)
7637 .unwrap_or(false),
7638 response_schemas: BTreeMap::new(),
7639 parameters: Vec::new(),
7640 supports_streaming: false, stream_parameter: None, tags: operation.tags.clone().unwrap_or_default(),
7643 };
7644 let mut operation_responses = BTreeMap::new();
7645
7646 if let Some(request_body) = &request_body {
7648 use crate::openapi::{
7649 is_binary_media_type, is_form_urlencoded_media_type, is_json_media_type,
7650 media_type_essence,
7651 };
7652 if let Some((content_type, maybe_schema)) = request_body.best_content() {
7653 op_info.request_body = if is_json_media_type(content_type) {
7654 match maybe_schema {
7655 Some(s) => {
7656 let validation_schema = self
7657 .raw_request_body_schema(raw_operation.as_ref(), content_type)
7658 .unwrap_or(
7659 serde_json::to_value(s).map_err(GeneratorError::ParseError)?,
7660 );
7661 Some(
7662 self.resolve_or_inline_schema(s, operation_id, "Request")
7663 .map(|name| RequestBodyContent::Json {
7664 schema_name: name,
7665 media_type: content_type.to_string(),
7666 validation_schema,
7667 })?,
7668 )
7669 }
7670 None => Some(RequestBodyContent::SchemaLess {
7671 media_type: content_type.to_string(),
7672 }),
7673 }
7674 } else if is_form_urlencoded_media_type(content_type) {
7675 match maybe_schema {
7676 Some(s) => {
7677 let validation_schema = self
7678 .raw_request_body_schema(raw_operation.as_ref(), content_type)
7679 .unwrap_or(
7680 serde_json::to_value(s).map_err(GeneratorError::ParseError)?,
7681 );
7682 Some(
7683 self.resolve_or_inline_schema(s, operation_id, "Request")
7684 .map(|name| RequestBodyContent::FormUrlEncoded {
7685 schema_name: name,
7686 media_type: content_type.to_string(),
7687 validation_schema,
7688 })?,
7689 )
7690 }
7691 None => Some(RequestBodyContent::SchemaLess {
7692 media_type: content_type.to_string(),
7693 }),
7694 }
7695 } else if media_type_essence(content_type)
7696 .eq_ignore_ascii_case("multipart/form-data")
7697 {
7698 match maybe_schema {
7699 Some(schema) => {
7700 let validation_schema = self
7701 .raw_request_body_schema(raw_operation.as_ref(), content_type)
7702 .unwrap_or(
7703 serde_json::to_value(schema)
7704 .map_err(GeneratorError::ParseError)?,
7705 );
7706 Some(
7707 self.resolve_or_inline_schema(schema, operation_id, "Request")
7708 .map(|schema_name| RequestBodyContent::Multipart {
7709 schema_name,
7710 media_type: content_type.to_string(),
7711 validation_schema,
7712 })?,
7713 )
7714 }
7715 None => Some(RequestBodyContent::SchemaLess {
7716 media_type: content_type.to_string(),
7717 }),
7718 }
7719 } else if is_binary_media_type(content_type, maybe_schema) {
7720 if media_type_essence(content_type)
7721 .eq_ignore_ascii_case("application/octet-stream")
7722 {
7723 Some(RequestBodyContent::OctetStream {
7724 media_type: content_type.to_string(),
7725 })
7726 } else {
7727 Some(RequestBodyContent::Binary {
7728 media_type: content_type.to_string(),
7729 })
7730 }
7731 } else if crate::openapi::is_text_media_type(content_type) {
7732 Some(RequestBodyContent::TextPlain {
7737 media_type: content_type.to_string(),
7738 })
7739 } else {
7740 None
7741 };
7742 }
7743 if op_info.request_body.is_none() {
7744 let mut media_types = request_body
7745 .content
7746 .as_ref()
7747 .map(|content| content.keys().cloned().collect::<Vec<_>>())
7748 .unwrap_or_default();
7749 media_types.sort();
7750 if !media_types.is_empty() {
7751 op_info.request_body = Some(RequestBodyContent::Unsupported { media_types });
7752 }
7753 }
7754 }
7755
7756 if let Some(responses) = &operation.responses {
7758 for (status_code, response) in responses {
7759 let response = self.resolve_response(response)?;
7760 let supports_streaming = response.content.as_ref().is_some_and(|content| {
7766 content
7767 .keys()
7768 .any(|ct| crate::openapi::is_event_stream_media_type(ct))
7769 });
7770 if supports_streaming {
7771 op_info.supports_streaming = true;
7772 }
7773
7774 let mut response_info = OperationResponse {
7775 supports_streaming,
7776 has_content: response
7777 .content
7778 .as_ref()
7779 .is_some_and(|content| !content.is_empty()),
7780 ..Default::default()
7781 };
7782 if let Some((media_type, schema)) = response.json_content() {
7783 if let Some(schema_ref) = schema.reference() {
7784 if let Some(schema_name) = self.extract_schema_name(schema_ref) {
7786 op_info
7787 .response_schemas
7788 .insert(status_code.clone(), schema_name.to_string());
7789 response_info.schema_name = Some(schema_name.to_string());
7790 response_info.media_type = Some(media_type.to_string());
7791 response_info.body = Some(OperationResponseBody::Json {
7792 schema_name: schema_name.to_string(),
7793 media_type: media_type.to_string(),
7794 });
7795 }
7796 } else {
7797 let synthetic_name =
7799 self.generate_inline_response_type_name(operation_id, status_code);
7800
7801 let mut deps = HashSet::new();
7803 let synthetic_name =
7804 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
7805
7806 op_info
7807 .response_schemas
7808 .insert(status_code.clone(), synthetic_name.clone());
7809 response_info.body = Some(OperationResponseBody::Json {
7810 schema_name: synthetic_name.clone(),
7811 media_type: media_type.to_string(),
7812 });
7813 response_info.schema_name = Some(synthetic_name);
7814 response_info.media_type = Some(media_type.to_string());
7815 }
7816 }
7817 if response_info.body.is_none()
7818 && let Some(content) = response.content.as_ref()
7819 {
7820 let selected = content
7821 .iter()
7822 .find(|(media_type, media)| {
7823 matches!(
7824 crate::openapi::classify_response_media_type(
7825 media_type,
7826 media.schema.as_ref()
7827 ),
7828 crate::openapi::ResponseMediaKind::Text
7829 )
7830 })
7831 .or_else(|| {
7832 content.iter().find(|(media_type, media)| {
7833 matches!(
7834 crate::openapi::classify_response_media_type(
7835 media_type,
7836 media.schema.as_ref()
7837 ),
7838 crate::openapi::ResponseMediaKind::Binary
7839 ) && !crate::openapi::is_wildcard_media_type(media_type)
7840 })
7841 })
7842 .or_else(|| {
7843 content.iter().find(|(media_type, media)| {
7844 matches!(
7845 crate::openapi::classify_response_media_type(
7846 media_type,
7847 media.schema.as_ref()
7848 ),
7849 crate::openapi::ResponseMediaKind::Binary
7850 )
7851 })
7852 });
7853 if let Some((media_type, media)) = selected {
7854 response_info.body = match crate::openapi::classify_response_media_type(
7855 media_type,
7856 media.schema.as_ref(),
7857 ) {
7858 crate::openapi::ResponseMediaKind::Text => {
7859 Some(OperationResponseBody::Text {
7860 media_type: media_type.clone(),
7861 })
7862 }
7863 crate::openapi::ResponseMediaKind::Binary => {
7864 Some(OperationResponseBody::Binary {
7865 media_type: media_type.clone(),
7866 wildcard: crate::openapi::is_wildcard_media_type(media_type),
7867 })
7868 }
7869 _ => None,
7870 };
7871 }
7872 }
7873 response_info.unsupported_media_types = response
7874 .content
7875 .as_ref()
7876 .into_iter()
7877 .flat_map(|content| content.iter())
7878 .filter(|(media_type, content)| {
7879 match crate::openapi::classify_response_media_type(
7880 media_type,
7881 content.schema.as_ref(),
7882 ) {
7883 crate::openapi::ResponseMediaKind::Json => content.schema.is_none(),
7884 crate::openapi::ResponseMediaKind::Unsupported => true,
7885 crate::openapi::ResponseMediaKind::EventStream
7886 | crate::openapi::ResponseMediaKind::Text
7887 | crate::openapi::ResponseMediaKind::Binary => false,
7888 }
7889 })
7890 .map(|(media_type, _)| media_type.clone())
7891 .collect();
7892 operation_responses.insert(status_code.clone(), response_info);
7893 }
7894 }
7895
7896 if op_info.supports_streaming
7899 && let Some(parameters) = &operation.parameters
7900 {
7901 for param in parameters {
7902 if let Some(name) = param.name.as_deref() {
7903 if name.eq_ignore_ascii_case("stream") {
7904 op_info.stream_parameter = Some(name.to_string());
7905 break;
7906 }
7907 }
7908 }
7909 }
7910
7911 if let Some(parameters) = &operation.parameters {
7913 for (index, param) in parameters.iter().enumerate() {
7914 let resolved = self.resolve_parameter(param).into_owned();
7918 let validation_schema = raw_operation
7919 .as_ref()
7920 .and_then(|operation| operation.get("parameters"))
7921 .and_then(Value::as_array)
7922 .and_then(|parameters| parameters.get(index))
7923 .and_then(|parameter| self.raw_parameter_schema(parameter));
7924 if let Some(param_info) =
7925 self.analyze_parameter(&resolved, operation_id, validation_schema)?
7926 {
7927 op_info.parameters.push(param_info);
7928 }
7929 }
7930 }
7931
7932 if let Some(path_params) = path_item_parameters {
7934 let existing_keys: std::collections::HashSet<(String, String)> = op_info
7935 .parameters
7936 .iter()
7937 .map(|p| (p.name.clone(), p.location.clone()))
7938 .collect();
7939 for (index, param) in path_params.iter().enumerate() {
7940 let resolved = self.resolve_parameter(param).into_owned();
7941 let validation_schema = raw_path_item
7942 .as_ref()
7943 .and_then(|path_item| path_item.get("parameters"))
7944 .and_then(Value::as_array)
7945 .and_then(|parameters| parameters.get(index))
7946 .and_then(|parameter| self.raw_parameter_schema(parameter));
7947 if let Some(param_info) =
7948 self.analyze_parameter(&resolved, operation_id, validation_schema)?
7949 {
7950 if !existing_keys
7951 .contains(&(param_info.name.clone(), param_info.location.clone()))
7952 {
7953 op_info.parameters.push(param_info);
7954 }
7955 }
7956 }
7957 }
7958
7959 let mut declared_path_names: std::collections::HashSet<String> = op_info
7967 .parameters
7968 .iter()
7969 .filter(|p| p.location == "path")
7970 .map(|p| p.name.clone())
7971 .collect();
7972 let bytes = path.as_bytes().iter();
7973 let mut current = String::new();
7974 let mut in_brace = false;
7975 let mut synthesized: Vec<String> = Vec::new();
7976 for b in bytes {
7977 match *b {
7978 b'{' => {
7979 in_brace = true;
7980 current.clear();
7981 }
7982 b'}' if in_brace => {
7983 in_brace = false;
7984 if !current.is_empty() && !declared_path_names.contains(¤t) {
7985 synthesized.push(current.clone());
7986 declared_path_names.insert(current.clone());
7987 }
7988 }
7989 _ if in_brace => current.push(*b as char),
7990 _ => {}
7991 }
7992 }
7993 for name in synthesized {
7994 eprintln!(
7995 "⚠️ path `{}` references `{{{}}}` but the spec doesn't declare it as a parameter — synthesizing as required String",
7996 path, name
7997 );
7998 op_info.parameters.push(ParameterInfo {
7999 name,
8000 location: "path".to_string(),
8001 required: true,
8002 schema_ref: None,
8003 rust_type: "String".to_string(),
8004 description: None,
8005 enum_values: None,
8006 enum_varnames: None,
8007 rust_ident: None,
8008 query_serialization: None,
8009 validation_schema: None,
8010 });
8011 }
8012
8013 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
8021 for p in op_info.parameters.iter_mut() {
8022 let raw = base_param_ident(&p.name);
8023 let mut chosen = raw.clone();
8024 let mut suffix = 2;
8025 while !used.insert(chosen.clone()) {
8026 chosen = format!("{raw}_{suffix}");
8027 suffix += 1;
8028 }
8029 p.rust_ident = Some(chosen);
8030 }
8031
8032 Ok((op_info, operation_responses))
8033 }
8034
8035 fn resolve_request_body(
8037 &self,
8038 request_body: &crate::openapi::RequestBody,
8039 ) -> Result<crate::openapi::RequestBody> {
8040 let mut current = request_body.clone();
8041 let mut visited = HashSet::new();
8042 while let Some(reference) = current.reference.clone() {
8043 if !visited.insert(reference.clone()) {
8044 return Err(GeneratorError::CircularDependency(format!(
8045 "request body reference {reference}"
8046 )));
8047 }
8048
8049 let pointer = reference.strip_prefix('#').ok_or_else(|| {
8050 GeneratorError::UnresolvedReference(format!(
8051 "external request body reference `{reference}` is not supported"
8052 ))
8053 })?;
8054 if !pointer.is_empty() && !pointer.starts_with('/') {
8055 return Err(GeneratorError::UnresolvedReference(format!(
8056 "request body reference `{reference}` is not a local JSON Pointer"
8057 )));
8058 }
8059 let value = self.openapi_spec.pointer(pointer).ok_or_else(|| {
8060 GeneratorError::UnresolvedReference(format!(
8061 "request body reference `{reference}` does not exist"
8062 ))
8063 })?;
8064 let object = value.as_object().ok_or_else(|| {
8065 GeneratorError::InvalidSchema(format!(
8066 "request body reference `{reference}` must target an object"
8067 ))
8068 })?;
8069 if !["$ref", "description", "required", "content"]
8070 .iter()
8071 .any(|field| object.contains_key(*field))
8072 {
8073 return Err(GeneratorError::InvalidSchema(format!(
8074 "request body reference `{reference}` does not target a structurally compatible OpenAPI Request Body Object"
8075 )));
8076 }
8077 current = serde_json::from_value(value.clone()).map_err(|error| {
8078 GeneratorError::InvalidSchema(format!(
8079 "request body reference `{reference}` is not a valid OpenAPI Request Body Object: {error}"
8080 ))
8081 })?;
8082 }
8083 Ok(current)
8084 }
8085
8086 fn resolve_response(
8093 &self,
8094 response: &crate::openapi::Response,
8095 ) -> Result<crate::openapi::Response> {
8096 let mut current = response.clone();
8097 let mut visited = HashSet::new();
8098 while let Some(reference) = current.reference.clone() {
8099 if !visited.insert(reference.clone()) {
8100 return Err(GeneratorError::CircularDependency(format!(
8101 "response reference {reference}"
8102 )));
8103 }
8104
8105 let pointer = reference.strip_prefix('#').ok_or_else(|| {
8106 GeneratorError::UnresolvedReference(format!(
8107 "external response reference `{reference}` is not supported"
8108 ))
8109 })?;
8110 if !pointer.is_empty() && !pointer.starts_with('/') {
8111 return Err(GeneratorError::UnresolvedReference(format!(
8112 "response reference `{reference}` is not a local JSON Pointer"
8113 )));
8114 }
8115 let value = self.openapi_spec.pointer(pointer).ok_or_else(|| {
8116 GeneratorError::UnresolvedReference(format!(
8117 "response reference `{reference}` does not exist"
8118 ))
8119 })?;
8120 let object = value.as_object().ok_or_else(|| {
8121 GeneratorError::InvalidSchema(format!(
8122 "response reference `{reference}` must target an object"
8123 ))
8124 })?;
8125 if !["$ref", "description", "headers", "content", "links"]
8126 .iter()
8127 .any(|field| object.contains_key(*field))
8128 {
8129 return Err(GeneratorError::InvalidSchema(format!(
8130 "response reference `{reference}` does not target a structurally compatible OpenAPI Response Object"
8131 )));
8132 }
8133 current = serde_json::from_value(value.clone()).map_err(|error| {
8134 GeneratorError::InvalidSchema(format!(
8135 "response reference `{reference}` is not a valid OpenAPI Response Object: {error}"
8136 ))
8137 })?;
8138 }
8139 Ok(current)
8140 }
8141
8142 fn generate_inline_response_type_name(&self, operation_id: &str, status_code: &str) -> String {
8149 use heck::ToPascalCase;
8150 let base_name = operation_id.replace('.', "_").to_pascal_case();
8151 let suffix = Self::status_code_suffix(status_code);
8152 format!("{}Response{}", base_name, suffix)
8153 }
8154
8155 fn status_code_suffix(status_code: &str) -> String {
8162 match status_code {
8163 "" | "200" => String::new(),
8164 "default" | "Default" => "Default".to_string(),
8165 other if other.chars().all(|c| c.is_ascii_digit()) => other.to_string(),
8166 other => other.to_ascii_lowercase(),
8167 }
8168 }
8169
8170 fn generate_inline_request_type_name(&self, operation_id: &str) -> String {
8172 use heck::ToPascalCase;
8173 let base_name = operation_id.replace('.', "_").to_pascal_case();
8177 format!("{}Request", base_name)
8178 }
8179
8180 fn resolve_or_inline_schema(
8183 &mut self,
8184 schema: &crate::openapi::Schema,
8185 operation_id: &str,
8186 suffix: &str,
8187 ) -> Result<String> {
8188 if let Some(schema_ref) = schema.reference()
8189 && let Some(schema_name) = self.extract_schema_name(schema_ref)
8190 {
8191 return Ok(schema_name.to_string());
8192 }
8193 let synthetic_name = if suffix == "Request" {
8195 self.generate_inline_request_type_name(operation_id)
8196 } else {
8197 self.generate_inline_response_type_name(operation_id, "")
8198 };
8199 let mut deps = HashSet::new();
8200 self.add_inline_schema(&synthetic_name, schema, &mut deps)
8201 }
8202
8203 fn resolve_parameter<'a>(
8206 &'a self,
8207 param: &'a crate::openapi::Parameter,
8208 ) -> std::borrow::Cow<'a, crate::openapi::Parameter> {
8209 if let Some(ref_str) = param.reference.as_deref() {
8210 if let Some(param_name) = ref_str.strip_prefix("#/components/parameters/") {
8211 if let Some(resolved) = self.component_parameters.get(param_name) {
8212 return std::borrow::Cow::Borrowed(resolved);
8213 }
8214 }
8215 }
8216 std::borrow::Cow::Borrowed(param)
8217 }
8218
8219 fn referenced_schema_is_string_enum(&self, name: &str) -> bool {
8232 if self.resolve_cached_schema(name).is_some_and(|schema| {
8233 matches!(
8234 schema.schema_type,
8235 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. }
8236 )
8237 }) {
8238 return true;
8239 }
8240 let Some(schema_value) = self
8241 .openapi_spec
8242 .get("components")
8243 .and_then(|c| c.get("schemas"))
8244 .and_then(|s| s.get(name))
8245 else {
8246 return false;
8247 };
8248 let is_string_type = schema_value
8249 .get("type")
8250 .and_then(|v| v.as_str())
8251 .map(|s| s == "string")
8252 .unwrap_or(false);
8253 let has_enum_or_const =
8254 schema_value.get("enum").is_some() || schema_value.get("const").is_some();
8255 is_string_type && has_enum_or_const
8256 }
8257
8258 fn resolve_raw_local_reference(&self, value: &Value) -> Option<Value> {
8259 let Some(reference) = value.get("$ref").and_then(Value::as_str) else {
8260 return Some(value.clone());
8261 };
8262 let pointer = reference.strip_prefix('#')?;
8263 self.openapi_spec.pointer(pointer).cloned()
8264 }
8265
8266 fn raw_request_body_schema(
8267 &self,
8268 operation: Option<&Value>,
8269 content_type: &str,
8270 ) -> Option<Value> {
8271 let request_body = operation?.get("requestBody")?;
8272 self.resolve_raw_local_reference(request_body)?
8273 .get("content")?
8274 .get(content_type)?
8275 .get("schema")
8276 .cloned()
8277 }
8278
8279 fn raw_parameter_schema(&self, parameter: &Value) -> Option<Value> {
8280 self.resolve_raw_local_reference(parameter)?
8281 .get("schema")
8282 .cloned()
8283 }
8284
8285 fn analyze_parameter(
8286 &mut self,
8287 param: &crate::openapi::Parameter,
8288 operation_id: &str,
8289 raw_validation_schema: Option<Value>,
8290 ) -> Result<Option<ParameterInfo>> {
8291 use heck::ToPascalCase;
8292
8293 let name = param.name.as_deref().unwrap_or("");
8294 let location = param.location.as_deref().unwrap_or("");
8295 let required = param.required.unwrap_or(false);
8296 let validation_schema = match raw_validation_schema {
8297 Some(schema) => Some(schema),
8298 None => param
8299 .schema
8300 .as_ref()
8301 .map(serde_json::to_value)
8302 .transpose()
8303 .map_err(GeneratorError::ParseError)?,
8304 };
8305
8306 let mut rust_type = "String".to_string();
8307 let mut schema_ref = None;
8308 let mut enum_values: Option<Vec<String>> = None;
8309 let mut enum_varnames: Option<Vec<String>> = None;
8310 let mut query_serialization: Option<QuerySerialization> = None;
8311
8312 let is_query = location == "query";
8318 let is_simple_header = location == "header"
8319 && matches!(param.style.as_deref(), None | Some("simple"))
8320 && param.explode != Some(true);
8321 let form_style = matches!(param.style.as_deref(), None | Some("form"));
8322 let form_exploded = form_style && param.explode.unwrap_or(true);
8323 let deep_object =
8324 param.style.as_deref() == Some("deepObject") && param.explode != Some(false);
8325
8326 let object_serialization = if !is_query {
8327 None
8328 } else if deep_object {
8329 Some(QuerySerialization::DeepObject)
8330 } else if form_exploded {
8331 Some(QuerySerialization::FormExplodedObject)
8332 } else if form_style {
8333 Some(QuerySerialization::FormObject)
8334 } else {
8335 None
8336 };
8337
8338 if let Some(schema) = ¶m.schema {
8339 if let Some(ref_str) = schema.reference() {
8340 if let Some(name) = self.extract_schema_name(ref_str) {
8346 if self.referenced_schema_is_string_enum(name) {
8347 schema_ref = Some(name.to_string());
8348 } else if object_serialization.is_some()
8349 && self.referenced_schema_is_object(name)
8350 {
8351 schema_ref = Some(name.to_string());
8352 query_serialization = if form_exploded && self.uses_aws_query_conventions()
8353 {
8354 match self.referenced_array_struct_item_type(name, 1) {
8355 Some(ArrayItemType::NestedStructRef { properties, .. }) => {
8356 Some(QuerySerialization::FormExplodedNestedObject {
8357 properties,
8358 })
8359 }
8360 _ => object_serialization.clone(),
8361 }
8362 } else {
8363 object_serialization.clone()
8364 };
8365 } else if (is_query && form_style || is_simple_header)
8366 && let Some(item_type) = self.referenced_array_param_item_type(name)
8367 {
8368 schema_ref = Some(name.to_string());
8374 query_serialization = Some(if is_simple_header {
8375 QuerySerialization::SimpleHeaderArray { item_type }
8376 } else if form_exploded {
8377 QuerySerialization::FormExplodedArray { item_type }
8378 } else {
8379 QuerySerialization::FormArray { item_type }
8380 });
8381 }
8382 }
8383 } else if object_serialization.is_some() && Self::schema_is_inline_object(schema) {
8384 let op_pascal = operation_id.replace('.', "_").to_pascal_case();
8389 let param_pascal = name.to_pascal_case();
8390 let synthetic_name = format!("{op_pascal}{param_pascal}");
8391 let mut deps = HashSet::new();
8392 let synthetic_name = self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
8393 schema_ref = Some(synthetic_name.clone());
8394 query_serialization = if form_exploded && self.uses_aws_query_conventions() {
8395 match self.referenced_array_struct_item_type(&synthetic_name, 1) {
8396 Some(ArrayItemType::NestedStructRef { properties, .. }) => {
8397 Some(QuerySerialization::FormExplodedNestedObject { properties })
8398 }
8399 _ => object_serialization.clone(),
8400 }
8401 } else {
8402 object_serialization.clone()
8403 };
8404 } else if (is_query && form_style || is_simple_header)
8405 && matches!(
8406 schema.schema_type(),
8407 Some(crate::openapi::SchemaType::Array)
8408 )
8409 && let Some(item_type) = self.array_param_item_type(schema)
8410 {
8411 query_serialization = Some(if is_simple_header {
8419 QuerySerialization::SimpleHeaderArray { item_type }
8420 } else if form_exploded {
8421 QuerySerialization::FormExplodedArray { item_type }
8422 } else {
8423 QuerySerialization::FormArray { item_type }
8424 });
8425 } else if let Some(schema_type) = schema.schema_type() {
8426 let format = schema.details().format.clone();
8432 rust_type = match schema_type {
8433 crate::openapi::SchemaType::Boolean => "bool".to_string(),
8434 crate::openapi::SchemaType::Integer => self.integer_rust_type(schema.details()),
8435 crate::openapi::SchemaType::Number => {
8436 self.type_mapper.number_format(format.as_deref()).rust_type
8437 }
8438 crate::openapi::SchemaType::String => "String".to_string(),
8439 _ => "String".to_string(),
8440 };
8441
8442 if matches!(schema_type, crate::openapi::SchemaType::String) {
8443 let details = schema.details();
8444 if details.is_string_enum() {
8445 if let Some(values) = details.string_enum_values() {
8446 if !values.is_empty() {
8447 let op_pascal = operation_id.replace('.', "_").to_pascal_case();
8448 let param_pascal = name.to_pascal_case();
8449 rust_type = format!("{op_pascal}{param_pascal}");
8450 enum_varnames = details
8455 .extra
8456 .get("x-enum-varnames")
8457 .and_then(Value::as_array)
8458 .map(|raw| {
8459 raw.iter()
8460 .filter_map(Value::as_str)
8461 .map(str::to_owned)
8462 .collect::<Vec<_>>()
8463 })
8464 .filter(|names| names.len() == values.len());
8465 enum_values = Some(values);
8466 }
8467 }
8468 }
8469 }
8470 }
8471
8472 if is_query && query_serialization.is_none() {
8473 let referenced_name = schema
8474 .reference()
8475 .and_then(|reference| self.extract_schema_name(reference));
8476 let is_object = referenced_name
8477 .is_some_and(|name| self.referenced_schema_is_object(name))
8478 || Self::schema_is_inline_object(schema);
8479 let is_array = referenced_name
8480 .is_some_and(|name| self.referenced_schema_is_array(name))
8481 || matches!(
8482 schema.schema_type(),
8483 Some(crate::openapi::SchemaType::Array)
8484 );
8485 let is_composed = referenced_name
8486 .is_some_and(|name| self.referenced_schema_is_composed_query_shape(name));
8487 let reason = if param.style.as_deref() == Some("deepObject")
8488 && param.explode == Some(false)
8489 {
8490 Some("style=deepObject with explode=false is undefined by OpenAPI".to_string())
8491 } else if param.style.as_deref() == Some("deepObject") && !is_object {
8492 Some("style=deepObject is defined only for object query parameters".to_string())
8493 } else if is_object {
8494 Some(format!(
8495 "object query parameters do not support style={}",
8496 param.style.as_deref().unwrap_or("form")
8497 ))
8498 } else if is_array && form_style {
8499 Some(
8500 "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"
8501 .to_string(),
8502 )
8503 } else if is_array {
8504 Some(format!(
8505 "array query parameters do not yet support style={}",
8506 param.style.as_deref().unwrap_or("form")
8507 ))
8508 } else if is_composed {
8509 Some(
8510 "composed or union query schemas cannot be projected to an unambiguous flat wire shape"
8511 .to_string(),
8512 )
8513 } else {
8514 None
8515 };
8516 if let Some(reason) = reason {
8517 query_serialization = Some(QuerySerialization::Unsupported { reason });
8518 }
8519 }
8520 }
8521
8522 Ok(Some(ParameterInfo {
8523 name: name.to_string(),
8524 location: location.to_string(),
8525 required,
8526 schema_ref,
8527 rust_type,
8528 description: param.description.clone(),
8529 enum_values,
8530 enum_varnames,
8531 rust_ident: None,
8532 query_serialization,
8533 validation_schema,
8534 }))
8535 }
8536
8537 fn array_param_item_type(&self, schema: &crate::openapi::Schema) -> Option<ArrayItemType> {
8546 let items = schema.details().item_schema()?;
8547 let unwrapped = unwrap_annotation_allof(items);
8551 if let Some(ref_str) = unwrapped.reference() {
8552 let name = self.extract_schema_name(ref_str)?;
8553 return self
8554 .referenced_array_scalar_item_type(name)
8555 .or_else(|| self.referenced_array_struct_item_type(name, 1));
8556 }
8557 let format = unwrapped.details().format.clone();
8558 let scalar = match unwrapped.schema_type()? {
8559 crate::openapi::SchemaType::String => "String".to_string(),
8560 crate::openapi::SchemaType::Integer => self.integer_rust_type(unwrapped.details()),
8561 crate::openapi::SchemaType::Number => {
8562 self.type_mapper.number_format(format.as_deref()).rust_type
8563 }
8564 crate::openapi::SchemaType::Boolean => "bool".to_string(),
8565 _ => return None,
8566 };
8567 Some(ArrayItemType::Scalar(scalar))
8568 }
8569
8570 fn referenced_array_param_item_type(&self, name: &str) -> Option<ArrayItemType> {
8573 let schema = self.resolve_cached_schema(name)?;
8574 let SchemaType::Array { item_type } = &schema.schema_type else {
8575 return None;
8576 };
8577 self.analyzed_array_item_type(item_type)
8578 }
8579
8580 fn analyzed_array_item_type(&self, item_type: &SchemaType) -> Option<ArrayItemType> {
8581 self.analyzed_array_item_type_at_depth(item_type, 1)
8582 }
8583
8584 fn referenced_array_struct_item_type(
8589 &self,
8590 name: &str,
8591 nested_array_depth: usize,
8592 ) -> Option<ArrayItemType> {
8593 let resolved = self.resolve_cached_schema(name)?;
8594 let SchemaType::Object {
8595 properties,
8596 required,
8597 additional_properties,
8598 ..
8599 } = &resolved.schema_type
8600 else {
8601 return None;
8602 };
8603 if properties.is_empty()
8604 || !matches!(additional_properties, ObjectAdditionalProperties::Forbidden)
8605 {
8606 return None;
8607 }
8608 let mut projected = Vec::with_capacity(properties.len());
8609 let mut has_array = false;
8610 for (wire_name, property) in properties {
8611 let value_type = if let Some(scalar) = self.query_scalar_type(&property.schema_type) {
8612 QueryStructPropertyType::Scalar(scalar)
8613 } else {
8614 if nested_array_depth == 0 {
8615 return None;
8616 }
8617 if let Some(array) = self.resolve_query_array_type(&property.schema_type) {
8618 let item_type =
8619 self.analyzed_array_item_type_at_depth(array, nested_array_depth - 1)?;
8620 if matches!(item_type, ArrayItemType::NestedStructRef { .. }) {
8621 return None;
8622 }
8623 has_array = true;
8624 QueryStructPropertyType::Array { item_type }
8625 } else {
8626 has_array = true;
8627 QueryStructPropertyType::Object {
8628 properties: self.query_flat_object_properties(&property.schema_type)?,
8629 }
8630 }
8631 };
8632 projected.push(QueryStructProperty {
8633 wire_name: wire_name.clone(),
8634 required: required.contains(wire_name),
8635 value_type,
8636 });
8637 }
8638 if has_array {
8639 Some(ArrayItemType::NestedStructRef {
8640 schema_name: name.to_string(),
8641 properties: projected,
8642 })
8643 } else {
8644 Some(ArrayItemType::FlatStructRef {
8645 schema_name: name.to_string(),
8646 properties: projected,
8647 })
8648 }
8649 }
8650
8651 fn analyzed_array_item_type_at_depth(
8652 &self,
8653 item_type: &SchemaType,
8654 nested_array_depth: usize,
8655 ) -> Option<ArrayItemType> {
8656 match item_type {
8657 SchemaType::Primitive { rust_type, .. } => {
8658 Some(ArrayItemType::Scalar(rust_type.clone()))
8659 }
8660 SchemaType::Reference { target } => self
8661 .referenced_array_scalar_item_type(target)
8662 .or_else(|| self.referenced_array_struct_item_type(target, nested_array_depth)),
8663 _ => None,
8664 }
8665 }
8666
8667 fn resolve_query_array_type<'a>(
8668 &'a self,
8669 schema_type: &'a SchemaType,
8670 ) -> Option<&'a SchemaType> {
8671 match schema_type {
8672 SchemaType::Array { item_type } => Some(item_type),
8673 SchemaType::Reference { target } => {
8674 let resolved = self.resolve_cached_schema(target)?;
8675 let SchemaType::Array { item_type } = &resolved.schema_type else {
8676 return None;
8677 };
8678 Some(item_type)
8679 }
8680 _ => None,
8681 }
8682 }
8683
8684 fn query_flat_object_properties(
8685 &self,
8686 schema_type: &SchemaType,
8687 ) -> Option<Vec<QueryStructProperty>> {
8688 let schema_type = match schema_type {
8689 SchemaType::Reference { target } => &self.resolve_cached_schema(target)?.schema_type,
8690 other => other,
8691 };
8692 let SchemaType::Object {
8693 properties,
8694 required,
8695 additional_properties,
8696 ..
8697 } = schema_type
8698 else {
8699 return None;
8700 };
8701 if properties.is_empty()
8702 || !matches!(additional_properties, ObjectAdditionalProperties::Forbidden)
8703 {
8704 return None;
8705 }
8706 properties
8707 .iter()
8708 .map(|(wire_name, property)| {
8709 Some(QueryStructProperty {
8710 wire_name: wire_name.clone(),
8711 required: required.contains(wire_name),
8712 value_type: QueryStructPropertyType::Scalar(
8713 self.query_scalar_type(&property.schema_type)?,
8714 ),
8715 })
8716 })
8717 .collect()
8718 }
8719
8720 fn query_scalar_type(&self, schema_type: &SchemaType) -> Option<QueryScalarType> {
8721 match schema_type {
8722 SchemaType::Primitive { rust_type, .. } => match rust_type.as_str() {
8723 "String" => Some(QueryScalarType::String),
8724 "bool" => Some(QueryScalarType::Boolean),
8725 value if value.starts_with('i') || value.starts_with('u') => {
8726 Some(QueryScalarType::Integer)
8727 }
8728 value if value.starts_with('f') => Some(QueryScalarType::Number),
8729 "serde_json::Value" => None,
8730 _ => Some(QueryScalarType::String),
8731 },
8732 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. } => {
8733 Some(QueryScalarType::String)
8734 }
8735 SchemaType::Reference { target } => {
8736 let resolved = self.resolve_cached_schema(target)?;
8737 self.query_scalar_type(&resolved.schema_type)
8738 }
8739 _ => None,
8740 }
8741 }
8742
8743 fn referenced_array_scalar_item_type(&self, name: &str) -> Option<ArrayItemType> {
8751 let resolved = self.resolve_cached_schema(name)?;
8752 let supported = match &resolved.schema_type {
8753 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. } => true,
8754 SchemaType::Primitive { .. } => resolved
8755 .original
8756 .get("type")
8757 .is_some_and(Self::query_scalar_type_value),
8758 _ => false,
8759 };
8760 supported.then(|| ArrayItemType::SchemaRef(name.to_string()))
8761 }
8762
8763 fn query_scalar_type_value(value: &Value) -> bool {
8764 const SCALARS: [&str; 4] = ["string", "integer", "number", "boolean"];
8765 if let Some(value) = value.as_str() {
8766 return SCALARS.contains(&value);
8767 }
8768 let Some(values) = value.as_array() else {
8769 return false;
8770 };
8771 if !values.iter().all(Value::is_string) {
8772 return false;
8773 }
8774 let mut non_null = values
8775 .iter()
8776 .filter_map(Value::as_str)
8777 .filter(|value| *value != "null");
8778 let Some(scalar) = non_null.next() else {
8779 return false;
8780 };
8781 non_null.next().is_none() && SCALARS.contains(&scalar)
8782 }
8783
8784 fn referenced_schema_is_object(&self, name: &str) -> bool {
8788 self.resolve_cached_schema(name)
8789 .is_some_and(|schema| matches!(schema.schema_type, SchemaType::Object { .. }))
8790 }
8791
8792 fn referenced_schema_is_array(&self, name: &str) -> bool {
8793 self.resolve_cached_schema(name)
8794 .is_some_and(|schema| matches!(schema.schema_type, SchemaType::Array { .. }))
8795 }
8796
8797 fn referenced_schema_is_composed_query_shape(&self, name: &str) -> bool {
8798 self.resolve_cached_schema(name).is_some_and(|schema| {
8799 matches!(
8800 schema.schema_type,
8801 SchemaType::Composition { .. }
8802 | SchemaType::Union { .. }
8803 | SchemaType::DiscriminatedUnion { .. }
8804 )
8805 })
8806 }
8807
8808 fn resolve_cached_schema(&self, name: &str) -> Option<&AnalyzedSchema> {
8809 let mut current = name;
8810 let mut visited = HashSet::new();
8811 loop {
8812 if !visited.insert(current) {
8813 return None;
8814 }
8815 let schema = self.resolved_cache.get(current)?;
8816 if let SchemaType::Reference { target } = &schema.schema_type {
8817 current = target;
8818 } else {
8819 return Some(schema);
8820 }
8821 }
8822 }
8823
8824 fn schema_is_inline_object(schema: &crate::openapi::Schema) -> bool {
8826 match schema.schema_type() {
8827 Some(crate::openapi::SchemaType::Object) => true,
8828 None => schema.details().properties.is_some(),
8829 _ => false,
8830 }
8831 }
8832}
8833
8834fn pointer_type_name(pointer: &str) -> String {
8845 use heck::ToPascalCase;
8846
8847 pointer
8848 .split('/')
8849 .skip(1)
8850 .filter(|segment| !matches!(*segment, "components" | "schemas" | "properties"))
8851 .map(|segment| {
8852 segment
8853 .replace("~1", "/")
8854 .replace("~0", "~")
8855 .to_pascal_case()
8856 })
8857 .collect::<String>()
8858}
8859
8860fn shared_positional_schema(positions: &[Schema]) -> Option<&Schema> {
8865 let first = positions.first()?;
8866 let key = positional_schema_key(first)?;
8867 positions
8868 .iter()
8869 .skip(1)
8870 .all(|position| positional_schema_key(position).as_deref() == Some(key.as_str()))
8871 .then_some(first)
8872}
8873
8874fn positional_schema_key(schema: &Schema) -> Option<String> {
8875 if let Some(reference) = schema.reference() {
8876 return Some(format!("$ref {reference}"));
8877 }
8878 let details = schema.details();
8879 if details.properties.is_some() || details.enum_values.is_some() || details.items.is_some() {
8880 return None;
8881 }
8882 match schema.schema_type()? {
8883 crate::openapi::SchemaType::Object | crate::openapi::SchemaType::Array => None,
8884 scalar => Some(format!(
8885 "{scalar:?} {}",
8886 details.format.as_deref().unwrap_or_default()
8887 )),
8888 }
8889}
8890
8891fn parse_spec_document(openapi_spec: &Value) -> Result<OpenApiSpec> {
8892 serde_path_to_error::deserialize(openapi_spec).map_err(|error| {
8893 let mut pointer = json_pointer(error.path());
8894 pointer.push_str(&refine_schema_failure(openapi_spec, &pointer));
8898 GeneratorError::ParseErrorAt {
8899 pointer,
8900 message: error.into_inner().to_string(),
8901 }
8902 })
8903}
8904
8905const SUBSCHEMA_KEYWORDS: [&str; 11] = [
8907 "items",
8908 "additionalProperties",
8909 "propertyNames",
8910 "unevaluatedProperties",
8911 "unevaluatedItems",
8912 "contains",
8913 "contentSchema",
8914 "if",
8915 "then",
8916 "else",
8917 "not",
8918];
8919
8920const SUBSCHEMA_LIST_KEYWORDS: [&str; 4] = ["oneOf", "anyOf", "allOf", "prefixItems"];
8922
8923const SUBSCHEMA_MAP_KEYWORDS: [&str; 5] = [
8925 "properties",
8926 "patternProperties",
8927 "dependentSchemas",
8928 "$defs",
8929 "definitions",
8930];
8931
8932const REFINE_PARSE_BUDGET: usize = 20_000;
8936
8937fn refine_schema_failure(openapi_spec: &Value, pointer: &str) -> String {
8945 let Some(path) = pointer.strip_prefix('#') else {
8946 return String::new();
8947 };
8948 let Some(node) = openapi_spec.pointer(path) else {
8949 return String::new();
8950 };
8951 let segments = path.split('/').skip(1).collect::<Vec<_>>();
8952 let last = segments.last().copied().unwrap_or_default();
8953 let parent = segments
8954 .len()
8955 .checked_sub(2)
8956 .map(|index| segments[index])
8957 .unwrap_or_default();
8958
8959 if last == "schema" || holds_schemas(parent) {
8960 return if parses_as_schema(node) {
8961 String::new()
8962 } else {
8963 deepest_schema_failure(node)
8964 };
8965 }
8966
8967 let mut budget = REFINE_PARSE_BUDGET;
8968 locate_failing_schema(node, holds_schemas(last), &mut budget).unwrap_or_default()
8969}
8970
8971fn holds_schemas(key: &str) -> bool {
8974 matches!(key, "schemas" | "$defs" | "definitions")
8975}
8976
8977fn locate_failing_schema(
8985 node: &Value,
8986 children_are_schemas: bool,
8987 budget: &mut usize,
8988) -> Option<String> {
8989 for (segment, key, child) in child_nodes(node) {
8990 if *budget == 0 {
8991 return None;
8992 }
8993 *budget -= 1;
8994 if children_are_schemas || key == "schema" {
8995 if !parses_as_schema(child) {
8996 return Some(format!("/{segment}{}", deepest_schema_failure(child)));
8997 }
8998 continue;
8999 }
9000 if let Some(rest) = locate_failing_schema(child, holds_schemas(key), budget) {
9001 return Some(format!("/{segment}{rest}"));
9002 }
9003 }
9004 None
9005}
9006
9007fn parses_as_schema(node: &Value) -> bool {
9008 Schema::deserialize(node).is_ok()
9009}
9010
9011fn deepest_schema_failure(node: &Value) -> String {
9015 let Some(object) = node.as_object() else {
9016 return String::new();
9017 };
9018
9019 let descend = |segment: String, child: &Value| -> Option<String> {
9020 if parses_as_schema(child) {
9021 return None;
9022 }
9023 Some(format!("/{segment}{}", deepest_schema_failure(child)))
9024 };
9025
9026 for keyword in SUBSCHEMA_KEYWORDS {
9027 if let Some(child) = object.get(keyword)
9028 && let Some(suffix) = descend(escape_pointer_segment(keyword), child)
9029 {
9030 return suffix;
9031 }
9032 }
9033 for keyword in SUBSCHEMA_LIST_KEYWORDS {
9034 if let Some(Value::Array(children)) = object.get(keyword) {
9035 for (index, child) in children.iter().enumerate() {
9036 if let Some(suffix) = descend(
9037 format!("{}/{index}", escape_pointer_segment(keyword)),
9038 child,
9039 ) {
9040 return suffix;
9041 }
9042 }
9043 }
9044 }
9045 for keyword in SUBSCHEMA_MAP_KEYWORDS {
9046 if let Some(Value::Object(children)) = object.get(keyword) {
9047 for (name, child) in children {
9048 if let Some(suffix) = descend(
9049 format!(
9050 "{}/{}",
9051 escape_pointer_segment(keyword),
9052 escape_pointer_segment(name)
9053 ),
9054 child,
9055 ) {
9056 return suffix;
9057 }
9058 }
9059 }
9060 }
9061 String::new()
9062}
9063
9064fn child_nodes(node: &Value) -> Vec<(String, &str, &Value)> {
9070 const DATA_KEYWORDS: [&str; 5] = ["example", "examples", "default", "enum", "const"];
9071
9072 match node {
9073 Value::Object(members) => members
9074 .iter()
9075 .filter(|(name, child)| {
9076 (child.is_object() || child.is_array())
9077 && !name.starts_with("x-")
9078 && !DATA_KEYWORDS.contains(&name.as_str())
9079 })
9080 .map(|(name, child)| (escape_pointer_segment(name), name.as_str(), child))
9081 .collect(),
9082 Value::Array(elements) => elements
9083 .iter()
9084 .enumerate()
9085 .filter(|(_, child)| child.is_object() || child.is_array())
9086 .map(|(index, child)| (index.to_string(), "", child))
9087 .collect(),
9088 _ => Vec::new(),
9089 }
9090}
9091
9092fn escape_pointer_segment(segment: &str) -> String {
9093 segment.replace('~', "~0").replace('/', "~1")
9094}
9095
9096fn json_pointer(path: &serde_path_to_error::Path) -> String {
9100 use serde_path_to_error::Segment;
9101
9102 let mut pointer = String::from("#");
9103 for segment in path.iter() {
9104 match segment {
9105 Segment::Seq { index } => {
9106 pointer.push('/');
9107 pointer.push_str(&index.to_string());
9108 }
9109 Segment::Map { key } | Segment::Enum { variant: key } => {
9110 pointer.push('/');
9111 pointer.push_str(&escape_pointer_segment(key));
9112 }
9113 Segment::Unknown => pointer.push_str("/?"),
9114 }
9115 }
9116 pointer
9117}
9118
9119pub(crate) fn component_schema_name_aliases(openapi_spec: &Value) -> BTreeMap<String, String> {
9120 let Some(schemas) = openapi_spec
9121 .pointer("/components/schemas")
9122 .and_then(Value::as_object)
9123 else {
9124 return BTreeMap::new();
9125 };
9126
9127 let mut names_by_rust_name = BTreeMap::<String, Vec<String>>::new();
9128 for name in schemas.keys() {
9129 names_by_rust_name
9130 .entry(crate::generator::rust_type_name(name))
9131 .or_default()
9132 .push(name.clone());
9133 }
9134
9135 let mut claimed_rust_names = names_by_rust_name.keys().cloned().collect::<HashSet<_>>();
9138 let mut aliases = BTreeMap::new();
9139
9140 for (rust_name, mut names) in names_by_rust_name {
9141 if names.len() < 2 {
9142 continue;
9143 }
9144
9145 names.sort_by_key(|name| (name != &rust_name, name.clone()));
9148 for source_name in names.into_iter().skip(1) {
9149 let mut suffix = 2;
9150 let replacement = loop {
9151 let candidate = format!("{rust_name}{suffix}");
9152 if claimed_rust_names.insert(candidate.clone()) {
9153 break candidate;
9154 }
9155 suffix += 1;
9156 };
9157
9158 aliases.insert(source_name, replacement);
9159 }
9160 }
9161
9162 aliases
9163}
9164
9165fn disambiguate_component_schema_names(openapi_spec: &mut Value) {
9166 let aliases = component_schema_name_aliases(openapi_spec);
9167 if aliases.is_empty() {
9168 return;
9169 }
9170
9171 for (source_name, replacement) in &aliases {
9172 let rust_name = crate::generator::rust_type_name(source_name);
9173 eprintln!(
9174 "⚠️ schema `{source_name}` maps to the existing Rust type `{rust_name}` — disambiguated to `{replacement}`"
9175 );
9176 }
9177
9178 let Some(schemas) = openapi_spec
9179 .pointer_mut("/components/schemas")
9180 .and_then(Value::as_object_mut)
9181 else {
9182 return;
9183 };
9184
9185 let original_schemas = std::mem::take(schemas);
9186 for (name, schema) in original_schemas {
9187 schemas.insert(aliases.get(&name).cloned().unwrap_or(name), schema);
9188 }
9189
9190 rewrite_component_schema_references(openapi_spec, &aliases);
9191}
9192
9193fn disambiguate_analyzed_schema_names(
9194 analysis: &mut SchemaAnalysis,
9195 component_schemas: &BTreeMap<String, Schema>,
9196) {
9197 let mut names_by_rust_name = BTreeMap::<String, Vec<String>>::new();
9198 for name in analysis.schemas.keys() {
9199 names_by_rust_name
9200 .entry(crate::generator::rust_type_name(name))
9201 .or_default()
9202 .push(name.clone());
9203 }
9204
9205 let mut claimed_rust_names = names_by_rust_name.keys().cloned().collect::<HashSet<_>>();
9206 let mut aliases = BTreeMap::<String, String>::new();
9207
9208 for (rust_name, mut names) in names_by_rust_name {
9209 if names.len() < 2 {
9210 continue;
9211 }
9212 names.sort_by_key(|name| {
9213 (
9214 !component_schemas.contains_key(name),
9215 name != &rust_name,
9216 name.clone(),
9217 )
9218 });
9219
9220 for source_name in names.into_iter().skip(1) {
9221 let mut suffix = 2;
9222 let replacement = loop {
9223 let candidate = format!("{rust_name}{suffix}");
9224 if claimed_rust_names.insert(candidate.clone()) {
9225 break candidate;
9226 }
9227 suffix += 1;
9228 };
9229 eprintln!(
9230 "⚠️ generated schema `{source_name}` maps to the existing Rust type `{rust_name}` — disambiguated to `{replacement}`"
9231 );
9232 aliases.insert(source_name, replacement);
9233 }
9234 }
9235
9236 if aliases.is_empty() {
9237 return;
9238 }
9239
9240 let original_schemas = std::mem::take(&mut analysis.schemas);
9241 for (name, mut schema) in original_schemas {
9242 schema.name = renamed_schema_name(&schema.name, &aliases);
9243 schema.dependencies = schema
9244 .dependencies
9245 .into_iter()
9246 .map(|name| renamed_schema_name(&name, &aliases))
9247 .collect();
9248 rewrite_schema_type_names(&mut schema.schema_type, &aliases);
9249 analysis
9250 .schemas
9251 .insert(renamed_schema_name(&name, &aliases), schema);
9252 }
9253
9254 let original_edges = std::mem::take(&mut analysis.dependencies.edges);
9255 for (name, dependencies) in original_edges {
9256 analysis.dependencies.edges.insert(
9257 renamed_schema_name(&name, &aliases),
9258 dependencies
9259 .into_iter()
9260 .map(|name| renamed_schema_name(&name, &aliases))
9261 .collect(),
9262 );
9263 }
9264 analysis.dependencies.recursive_schemas = analysis
9265 .dependencies
9266 .recursive_schemas
9267 .iter()
9268 .map(|name| renamed_schema_name(name, &aliases))
9269 .collect();
9270
9271 analysis.patterns.tagged_enum_schemas = analysis
9272 .patterns
9273 .tagged_enum_schemas
9274 .iter()
9275 .map(|name| renamed_schema_name(name, &aliases))
9276 .collect();
9277 analysis.patterns.untagged_enum_schemas = analysis
9278 .patterns
9279 .untagged_enum_schemas
9280 .iter()
9281 .map(|name| renamed_schema_name(name, &aliases))
9282 .collect();
9283 analysis.patterns.type_mappings = std::mem::take(&mut analysis.patterns.type_mappings)
9284 .into_iter()
9285 .map(|(name, mappings)| {
9286 (
9287 renamed_schema_name(&name, &aliases),
9288 mappings
9289 .into_iter()
9290 .map(|(value, schema_name)| {
9291 (value, renamed_schema_name(&schema_name, &aliases))
9292 })
9293 .collect(),
9294 )
9295 })
9296 .collect();
9297
9298 for operation in analysis.operations.values_mut() {
9299 if let Some(request_body) = &mut operation.request_body {
9300 rewrite_request_body_schema_name(request_body, &aliases);
9301 }
9302 for schema_name in operation.response_schemas.values_mut() {
9303 *schema_name = renamed_schema_name(schema_name, &aliases);
9304 }
9305 for parameter in &mut operation.parameters {
9306 if let Some(schema_name) = &mut parameter.schema_ref {
9307 *schema_name = renamed_schema_name(schema_name, &aliases);
9308 }
9309 if let Some(serialization) = &mut parameter.query_serialization {
9310 rewrite_query_serialization_schema_names(serialization, &aliases);
9311 }
9312 }
9313 }
9314
9315 for responses in analysis.operation_responses.values_mut() {
9316 for response in responses.values_mut() {
9317 if let Some(schema_name) = &mut response.schema_name {
9318 *schema_name = renamed_schema_name(schema_name, &aliases);
9319 }
9320 if let Some(OperationResponseBody::Json { schema_name, .. }) = &mut response.body {
9321 *schema_name = renamed_schema_name(schema_name, &aliases);
9322 }
9323 }
9324 }
9325}
9326
9327fn renamed_schema_name(name: &str, aliases: &BTreeMap<String, String>) -> String {
9328 aliases
9329 .get(name)
9330 .cloned()
9331 .unwrap_or_else(|| name.to_string())
9332}
9333
9334fn rewrite_schema_type_names(schema_type: &mut SchemaType, aliases: &BTreeMap<String, String>) {
9335 match schema_type {
9336 SchemaType::Object {
9337 properties,
9338 additional_properties,
9339 ..
9340 } => {
9341 for property in properties.values_mut() {
9342 rewrite_schema_type_names(&mut property.schema_type, aliases);
9343 }
9344 if let ObjectAdditionalProperties::Typed { value_type } = additional_properties {
9345 rewrite_schema_type_names(value_type, aliases);
9346 }
9347 }
9348 SchemaType::DiscriminatedUnion { variants, .. } => {
9349 for variant in variants {
9350 variant.type_name = renamed_schema_name(&variant.type_name, aliases);
9351 variant.schema_ref = renamed_schema_name(&variant.schema_ref, aliases);
9352 }
9353 }
9354 SchemaType::Union { variants, .. } | SchemaType::Composition { schemas: variants } => {
9355 for variant in variants {
9356 variant.target = renamed_schema_name(&variant.target, aliases);
9357 }
9358 }
9359 SchemaType::Array { item_type } => rewrite_schema_type_names(item_type, aliases),
9360 SchemaType::Nullable { inner_type } => rewrite_schema_type_names(inner_type, aliases),
9361 SchemaType::Untyped { .. } => {}
9362 SchemaType::Tuple { element_types } => {
9363 for element_type in element_types {
9364 rewrite_schema_type_names(element_type, aliases);
9365 }
9366 }
9367 SchemaType::Reference { target } => {
9368 *target = renamed_schema_name(target, aliases);
9369 }
9370 SchemaType::Primitive { .. }
9371 | SchemaType::StringEnum { .. }
9372 | SchemaType::ExtensibleEnum { .. } => {}
9373 }
9374}
9375
9376fn rewrite_request_body_schema_name(
9377 request_body: &mut RequestBodyContent,
9378 aliases: &BTreeMap<String, String>,
9379) {
9380 match request_body {
9381 RequestBodyContent::Json { schema_name, .. }
9382 | RequestBodyContent::FormUrlEncoded { schema_name, .. }
9383 | RequestBodyContent::Multipart { schema_name, .. } => {
9384 *schema_name = renamed_schema_name(schema_name, aliases);
9385 }
9386 _ => {}
9387 }
9388}
9389
9390fn rewrite_query_serialization_schema_names(
9391 serialization: &mut QuerySerialization,
9392 aliases: &BTreeMap<String, String>,
9393) {
9394 match serialization {
9395 QuerySerialization::FormExplodedArray { item_type }
9396 | QuerySerialization::FormArray { item_type }
9397 | QuerySerialization::SimpleHeaderArray { item_type } => {
9398 rewrite_array_item_type_schema_names(item_type, aliases);
9399 }
9400 QuerySerialization::FormExplodedNestedObject { properties } => {
9401 for property in properties {
9402 rewrite_query_property_type_schema_names(&mut property.value_type, aliases);
9403 }
9404 }
9405 _ => {}
9406 }
9407}
9408
9409fn rewrite_array_item_type_schema_names(
9410 item_type: &mut ArrayItemType,
9411 aliases: &BTreeMap<String, String>,
9412) {
9413 match item_type {
9414 ArrayItemType::SchemaRef(name) => *name = renamed_schema_name(name, aliases),
9415 ArrayItemType::FlatStructRef {
9416 schema_name,
9417 properties,
9418 }
9419 | ArrayItemType::NestedStructRef {
9420 schema_name,
9421 properties,
9422 } => {
9423 *schema_name = renamed_schema_name(schema_name, aliases);
9424 for property in properties {
9425 rewrite_query_property_type_schema_names(&mut property.value_type, aliases);
9426 }
9427 }
9428 ArrayItemType::Scalar(_) => {}
9429 }
9430}
9431
9432fn rewrite_query_property_type_schema_names(
9433 property_type: &mut QueryStructPropertyType,
9434 aliases: &BTreeMap<String, String>,
9435) {
9436 match property_type {
9437 QueryStructPropertyType::Array { item_type } => {
9438 rewrite_array_item_type_schema_names(item_type, aliases)
9439 }
9440 QueryStructPropertyType::Object { properties } => {
9441 for property in properties {
9442 rewrite_query_property_type_schema_names(&mut property.value_type, aliases);
9443 }
9444 }
9445 QueryStructPropertyType::Scalar(_) => {}
9446 }
9447}
9448
9449fn rewrite_component_schema_references(value: &mut Value, aliases: &BTreeMap<String, String>) {
9450 match value {
9451 Value::Array(values) => {
9452 for value in values {
9453 rewrite_component_schema_references(value, aliases);
9454 }
9455 }
9456 Value::Object(object) => {
9457 if let Some(Value::String(reference)) = object.get_mut("$ref") {
9458 rewrite_component_schema_reference(reference, aliases);
9459 }
9460
9461 if let Some(Value::Object(mapping)) = object.get_mut("mapping") {
9462 for target_value in mapping.values_mut() {
9463 let Some(target) = target_value.as_str() else {
9464 continue;
9465 };
9466 let replacement = aliases.get(target).cloned().or_else(|| {
9467 let mut target = target.to_string();
9468 rewrite_component_schema_reference(&mut target, aliases).then_some(target)
9469 });
9470 if let Some(replacement) = replacement {
9471 *target_value = Value::String(replacement);
9472 }
9473 }
9474 }
9475
9476 for value in object.values_mut() {
9477 rewrite_component_schema_references(value, aliases);
9478 }
9479 }
9480 _ => {}
9481 }
9482}
9483
9484fn rewrite_component_schema_reference(
9485 reference: &mut String,
9486 aliases: &BTreeMap<String, String>,
9487) -> bool {
9488 const PREFIX: &str = "#/components/schemas/";
9489 let Some(encoded_name) = reference.strip_prefix(PREFIX) else {
9490 return false;
9491 };
9492 let encoded_name = encoded_name.split('/').next().unwrap_or(encoded_name);
9493
9494 for (source, replacement) in aliases {
9495 let encoded_source = source.replace('~', "~0").replace('/', "~1");
9496 if encoded_name == encoded_source {
9497 reference.replace_range(
9498 PREFIX.len()..PREFIX.len() + encoded_source.len(),
9499 replacement,
9500 );
9501 return true;
9502 }
9503 }
9504
9505 false
9506}