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, BTreeSet, 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 pub untagged_union_branches: BTreeSet<String>,
117}
118
119fn collect_untagged_union_branches(analysis: &SchemaAnalysis) -> BTreeSet<String> {
126 let mut branches = BTreeSet::new();
127 for schema in analysis.schemas.values() {
128 let SchemaType::Union { variants, .. } = &schema.schema_type else {
129 continue;
130 };
131 for variant in variants {
132 let mut target = variant.target.clone();
133 let mut seen = BTreeSet::new();
136 while seen.insert(target.clone()) {
137 branches.insert(target.clone());
138 match analysis.schemas.get(&target).map(|s| &s.schema_type) {
139 Some(SchemaType::Reference { target: next }) => target = next.clone(),
140 _ => break,
141 }
142 }
143 }
144 }
145 branches
146}
147
148impl SchemaType {
149 pub fn renders_inline(&self) -> bool {
160 match self {
161 Self::Primitive { .. }
162 | Self::Reference { .. }
163 | Self::Array { .. }
164 | Self::Tuple { .. }
165 | Self::Nullable { .. }
166 | Self::Untyped { .. } => true,
167 Self::Object { .. }
168 | Self::StringEnum { .. }
169 | Self::ExtensibleEnum { .. }
170 | Self::DiscriminatedUnion { .. }
171 | Self::Union { .. }
172 | Self::Composition { .. } => false,
173 }
174 }
175}
176
177impl UntypedReason {
178 pub fn inline_drop(schema_type: &SchemaType) -> Option<Self> {
181 match schema_type {
182 SchemaType::Composition { .. } => Some(Self::InlineCompositionDropped),
183 SchemaType::Union { .. } | SchemaType::DiscriminatedUnion { .. } => {
184 Some(Self::InlineUnionDropped)
185 }
186 SchemaType::Object { .. } => Some(Self::InlineObjectDropped),
187 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. } => {
188 Some(Self::InlineEnumDropped)
189 }
190 _ => None,
191 }
192 }
193}
194
195fn schema_type_dependencies(schema_type: &SchemaType) -> HashSet<String> {
205 let mut targets = HashSet::new();
206 collect_type_dependencies(schema_type, &mut targets, 0);
207 targets
208}
209
210fn collect_type_dependencies(
211 schema_type: &SchemaType,
212 targets: &mut HashSet<String>,
213 depth: usize,
214) {
215 if depth > UNTYPED_WALK_DEPTH {
216 return;
217 }
218 match schema_type {
219 SchemaType::Reference { target } => {
220 targets.insert(target.clone());
221 }
222 SchemaType::Array { item_type } => collect_type_dependencies(item_type, targets, depth + 1),
223 SchemaType::Nullable { inner_type } => {
224 collect_type_dependencies(inner_type, targets, depth + 1)
225 }
226 SchemaType::Tuple { element_types } => {
227 for element_type in element_types {
228 collect_type_dependencies(element_type, targets, depth + 1);
229 }
230 }
231 SchemaType::Object {
232 properties,
233 additional_properties,
234 ..
235 } => {
236 for property in properties.values() {
237 collect_type_dependencies(&property.schema_type, targets, depth + 1);
238 }
239 if let ObjectAdditionalProperties::Typed { value_type } = additional_properties {
240 collect_type_dependencies(value_type, targets, depth + 1);
241 }
242 }
243 SchemaType::Union { variants, .. } | SchemaType::Composition { schemas: variants } => {
244 for variant in variants {
245 targets.insert(variant.target.clone());
246 }
247 }
248 SchemaType::DiscriminatedUnion { variants, .. } => {
249 for variant in variants {
250 targets.insert(variant.type_name.clone());
251 }
252 }
253 SchemaType::Primitive { .. }
254 | SchemaType::StringEnum { .. }
255 | SchemaType::ExtensibleEnum { .. }
256 | SchemaType::Untyped { .. } => {}
257 }
258}
259
260fn normalize_untyped(schema_type: &mut SchemaType, depth: usize) {
270 if depth > UNTYPED_WALK_DEPTH {
271 return;
272 }
273 match schema_type {
274 SchemaType::Primitive { rust_type, .. } => {
275 let shape = match rust_type.as_str() {
276 "serde_json::Value" => Some(UntypedShape::Value),
277 "Vec<serde_json::Value>" => Some(UntypedShape::ValueArray),
278 _ => None,
279 };
280 if let Some(shape) = shape {
281 *schema_type = SchemaType::Untyped {
282 shape,
283 reason: UntypedReason::Unclassified,
284 };
285 }
286 }
287 SchemaType::Object {
288 properties,
289 additional_properties,
290 ..
291 } => {
292 for property in properties.values_mut() {
293 normalize_untyped(&mut property.schema_type, depth + 1);
294 }
295 if let ObjectAdditionalProperties::Typed { value_type } = additional_properties {
296 normalize_untyped(value_type, depth + 1);
297 }
298 }
299 SchemaType::Array { item_type } => normalize_untyped(item_type, depth + 1),
300 SchemaType::Nullable { inner_type } => normalize_untyped(inner_type, depth + 1),
301 SchemaType::Tuple { element_types } => {
302 for element_type in element_types {
303 normalize_untyped(element_type, depth + 1);
304 }
305 }
306 SchemaType::Untyped { .. }
307 | SchemaType::StringEnum { .. }
308 | SchemaType::ExtensibleEnum { .. }
309 | SchemaType::DiscriminatedUnion { .. }
310 | SchemaType::Union { .. }
311 | SchemaType::Composition { .. }
312 | SchemaType::Reference { .. } => {}
313 }
314}
315
316impl SchemaAnalysis {
317 pub fn untyped_fields(&self) -> Vec<UntypedFinding> {
324 let mut findings = Vec::new();
325 for (name, schema) in &self.schemas {
326 collect_untyped(&schema.schema_type, name, &mut findings, 0);
327 }
328 findings.sort();
329 findings
330 }
331}
332
333const UNTYPED_WALK_DEPTH: usize = 32;
337
338fn collect_untyped(
339 schema_type: &SchemaType,
340 context: &str,
341 findings: &mut Vec<UntypedFinding>,
342 depth: usize,
343) {
344 if depth > UNTYPED_WALK_DEPTH {
345 return;
346 }
347 match schema_type {
348 SchemaType::Untyped { shape, reason } => findings.push(UntypedFinding {
349 context: context.to_string(),
350 shape: *shape,
351 reason: *reason,
352 }),
353 SchemaType::Object {
354 properties,
355 additional_properties,
356 ..
357 } => {
358 for (property_name, property) in properties {
359 let property_context = format!("{context}.{property_name}");
360 if let Some(reason) = UntypedReason::inline_drop(&property.schema_type) {
364 findings.push(UntypedFinding {
365 context: property_context,
366 shape: UntypedShape::Value,
367 reason,
368 });
369 continue;
370 }
371 collect_untyped(
372 &property.schema_type,
373 &property_context,
374 findings,
375 depth + 1,
376 );
377 }
378 match additional_properties {
379 ObjectAdditionalProperties::Untyped => findings.push(UntypedFinding {
380 context: format!("{context}.<additionalProperties>"),
381 shape: UntypedShape::ValueMap,
382 reason: UntypedReason::UntypedAdditionalProperties,
383 }),
384 ObjectAdditionalProperties::Typed { value_type } => collect_untyped(
385 value_type,
386 &format!("{context}.<additionalProperties>"),
387 findings,
388 depth + 1,
389 ),
390 ObjectAdditionalProperties::Denied | ObjectAdditionalProperties::Closed => {}
391 }
392 }
393 SchemaType::Array { item_type } => {
394 let element_context = format!("{context}[]");
395 if let Some(reason) = UntypedReason::inline_drop(item_type) {
396 findings.push(UntypedFinding {
397 context: element_context,
398 shape: UntypedShape::Value,
399 reason,
400 });
401 } else {
402 collect_untyped(item_type, &element_context, findings, depth + 1);
403 }
404 }
405 SchemaType::Nullable { inner_type } => {
406 collect_untyped(inner_type, context, findings, depth + 1)
407 }
408 SchemaType::Tuple { element_types } => {
409 for (index, element_type) in element_types.iter().enumerate() {
410 collect_untyped(
411 element_type,
412 &format!("{context}[{index}]"),
413 findings,
414 depth + 1,
415 );
416 }
417 }
418 SchemaType::Union { variants, .. } | SchemaType::Composition { schemas: variants } => {
421 for (index, variant) in variants.iter().enumerate() {
422 if let Some(shape) = untyped_shape_of(&variant.target) {
423 findings.push(UntypedFinding {
424 context: format!("{context}|{index}"),
425 shape,
426 reason: UntypedReason::UntypedUnionBranch,
427 });
428 }
429 }
430 }
431 SchemaType::Primitive { .. }
432 | SchemaType::StringEnum { .. }
433 | SchemaType::ExtensibleEnum { .. }
434 | SchemaType::DiscriminatedUnion { .. }
435 | SchemaType::Reference { .. } => {}
436 }
437}
438
439fn untyped_shape_of(rust_type: &str) -> Option<UntypedShape> {
441 match rust_type {
442 "serde_json::Value" => Some(UntypedShape::Value),
443 "Vec<serde_json::Value>" => Some(UntypedShape::ValueArray),
444 _ => None,
445 }
446}
447
448#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
451pub struct UntypedFinding {
452 pub context: String,
455 pub shape: UntypedShape,
457 pub reason: UntypedReason,
459}
460
461#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
463#[serde(rename_all = "snake_case")]
464pub enum UntypedShape {
465 Value,
467 ValueArray,
469 ValueMap,
471}
472
473#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
481#[serde(rename_all = "kebab-case")]
482pub enum UntypedReason {
483 AnySchema,
486 OpaqueObject,
489 UntypedAdditionalProperties,
491 ArrayWithoutItems,
493 OpenPositionalItems,
496 UnrepresentableUnion,
499 UnrepresentableComposition,
501 UnsupportedTypeKeyword,
503 UnresolvedReference,
505 InlineCompositionDropped,
510 InlineUnionDropped,
512 InlineObjectDropped,
514 InlineEnumDropped,
516 UntypedUnionBranch,
520 NeverMatches,
524 Unclassified,
527}
528
529impl UntypedReason {
530 pub fn verdict(self) -> UntypedVerdict {
533 match self {
534 Self::AnySchema | Self::OpaqueObject | Self::UntypedAdditionalProperties => {
536 UntypedVerdict::Faithful
537 }
538 Self::NeverMatches => UntypedVerdict::Faithful,
540 Self::ArrayWithoutItems | Self::OpenPositionalItems => UntypedVerdict::Faithful,
544 Self::InlineCompositionDropped
547 | Self::InlineUnionDropped
548 | Self::InlineObjectDropped
549 | Self::InlineEnumDropped => UntypedVerdict::Recoverable,
550 Self::UnrepresentableUnion
552 | Self::UnrepresentableComposition
553 | Self::UnsupportedTypeKeyword
554 | Self::UnresolvedReference => UntypedVerdict::Recoverable,
555 Self::UntypedUnionBranch | Self::Unclassified => UntypedVerdict::Unknown,
556 }
557 }
558}
559
560#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
562#[serde(rename_all = "snake_case")]
563pub enum UntypedVerdict {
564 Faithful,
566 Recoverable,
568 Unknown,
570}
571
572#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize)]
574pub struct OperationResponse {
575 pub schema_name: Option<String>,
577 pub media_type: Option<String>,
579 pub body: Option<OperationResponseBody>,
583 pub supports_streaming: bool,
585 pub has_content: bool,
587 pub unsupported_media_types: Vec<String>,
589}
590
591#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
596#[serde(tag = "kind", rename_all = "snake_case")]
597pub enum OperationResponseBody {
598 Json {
599 schema_name: String,
600 media_type: String,
601 },
602 Text {
603 media_type: String,
604 },
605 Binary {
606 media_type: String,
607 wildcard: bool,
608 },
609}
610
611#[derive(Debug, Clone, Default)]
612pub struct ValidationContext {
613 pub openapi_version: String,
614 pub json_schema_dialect: Option<String>,
615 pub component_schemas: BTreeMap<String, Value>,
616}
617
618#[derive(Debug, Clone, Default)]
623pub struct EnumExtensions {
624 pub varnames: Vec<String>,
629 pub descriptions: Vec<String>,
631}
632
633#[derive(Debug, Clone)]
634pub struct AnalyzedSchema {
635 pub name: String,
636 pub original: Value,
637 pub schema_type: SchemaType,
638 pub dependencies: HashSet<String>,
639 pub nullable: bool,
640 pub description: Option<String>,
641 pub default: Option<serde_json::Value>,
642}
643
644#[derive(Debug, Clone)]
645pub enum SchemaType {
646 Primitive {
652 rust_type: String,
653 serde_with: Option<String>,
654 },
655 Object {
657 properties: BTreeMap<String, PropertyInfo>,
658 required: HashSet<String>,
659 additional_properties: ObjectAdditionalProperties,
660 variant: Option<SchemaRef>,
665 },
666 DiscriminatedUnion {
668 discriminator_field: String,
669 variants: Vec<UnionVariant>,
670 exclusive: bool,
674 },
675 Union {
679 variants: Vec<SchemaRef>,
680 exclusive: bool,
681 },
682 Array { item_type: Box<SchemaType> },
684 Nullable { inner_type: Box<SchemaType> },
689 Tuple { element_types: Vec<SchemaType> },
695 StringEnum { values: Vec<String> },
697 ExtensibleEnum { known_values: Vec<String> },
699 Composition { schemas: Vec<SchemaRef> },
701 Reference { target: String },
703 Untyped {
710 shape: UntypedShape,
711 reason: UntypedReason,
712 },
713}
714
715#[derive(Debug, Clone)]
720pub enum ObjectAdditionalProperties {
721 Denied,
726 Closed,
732 Untyped,
735 Typed { value_type: Box<SchemaType> },
738}
739
740impl ObjectAdditionalProperties {
741 pub fn is_open(&self) -> bool {
744 !matches!(self, Self::Denied | Self::Closed)
745 }
746
747 pub fn denies_unknown_keys(&self) -> bool {
751 matches!(self, Self::Denied)
752 }
753}
754
755#[derive(Debug, Clone)]
756pub struct PropertyInfo {
757 pub schema_type: SchemaType,
758 pub nullable: bool,
759 pub description: Option<String>,
760 pub default: Option<serde_json::Value>,
761 pub serde_attrs: Vec<String>,
762 pub synthesized_required: bool,
767 pub constraints: PropertyConstraints,
772}
773
774#[derive(Debug, Clone, Default)]
779pub struct PropertyConstraints {
780 pub minimum: Option<f64>,
781 pub maximum: Option<f64>,
782 pub exclusive_minimum: Option<f64>,
783 pub exclusive_maximum: Option<f64>,
784 pub multiple_of: Option<f64>,
785 pub min_length: Option<u64>,
786 pub max_length: Option<u64>,
787 pub pattern: Option<String>,
788 pub min_items: Option<u64>,
789 pub max_items: Option<u64>,
790 pub unique_items: Option<bool>,
791}
792
793impl PropertyConstraints {
794 pub fn is_empty(&self) -> bool {
795 self.minimum.is_none()
796 && self.maximum.is_none()
797 && self.exclusive_minimum.is_none()
798 && self.exclusive_maximum.is_none()
799 && self.multiple_of.is_none()
800 && self.min_length.is_none()
801 && self.max_length.is_none()
802 && self.pattern.is_none()
803 && self.min_items.is_none()
804 && self.max_items.is_none()
805 && self.unique_items.is_none()
806 }
807
808 pub fn from_schema_details(details: &crate::openapi::SchemaDetails) -> Self {
813 use crate::openapi::ExclusiveBound;
814 let exclusive_minimum = match &details.exclusive_minimum {
815 Some(ExclusiveBound::Number(v)) => Some(*v),
816 _ => None,
817 };
818 let exclusive_maximum = match &details.exclusive_maximum {
819 Some(ExclusiveBound::Number(v)) => Some(*v),
820 _ => None,
821 };
822 Self {
823 minimum: details
824 .minimum
825 .as_ref()
826 .and_then(serde_json::Number::as_f64),
827 maximum: details
828 .maximum
829 .as_ref()
830 .and_then(serde_json::Number::as_f64),
831 exclusive_minimum,
832 exclusive_maximum,
833 multiple_of: details.multiple_of,
834 min_length: details.min_length,
835 max_length: details.max_length,
836 pattern: details.pattern.clone(),
837 min_items: details.min_items,
838 max_items: details.max_items,
839 unique_items: details.unique_items,
840 }
841 }
842}
843
844#[derive(Debug, Clone)]
845pub struct UnionVariant {
846 pub rust_name: String,
847 pub type_name: String,
848 pub discriminator_value: String,
852 pub discriminator_values: Vec<String>,
856 pub preferred_discriminator_values: Vec<String>,
861 pub discriminator_field_declared: bool,
865 pub discriminator_field_required: bool,
869 pub schema_ref: String,
870}
871
872#[derive(Debug, Clone)]
873pub struct SchemaRef {
874 pub target: String,
875 pub nullable: bool,
876}
877
878#[derive(Debug, Clone)]
879pub struct DependencyGraph {
880 pub edges: BTreeMap<String, HashSet<String>>,
881 pub recursive_schemas: HashSet<String>,
883}
884
885#[derive(Debug, Clone)]
886pub struct DetectedPatterns {
887 pub tagged_enum_schemas: HashSet<String>,
889 pub untagged_enum_schemas: HashSet<String>,
891 pub type_mappings: BTreeMap<String, BTreeMap<String, String>>,
893}
894
895#[derive(Debug, Clone, Default, serde::Serialize)]
897pub struct OperationInfo {
898 pub operation_id: String,
900 pub method: String,
902 pub path: String,
904 pub summary: Option<String>,
906 pub description: Option<String>,
908 pub request_body: Option<RequestBodyContent>,
910 pub request_body_required: bool,
913 pub response_schemas: BTreeMap<String, String>,
915 pub parameters: Vec<ParameterInfo>,
917 pub supports_streaming: bool,
919 pub stream_parameter: Option<String>,
921 pub tags: Vec<String>,
925}
926
927#[derive(Debug, Clone, serde::Serialize)]
929#[serde(tag = "kind")]
930pub enum RequestBodyContent {
931 Json {
932 schema_name: String,
933 media_type: String,
934 #[serde(skip)]
935 validation_schema: Value,
936 },
937 FormUrlEncoded {
938 schema_name: String,
939 media_type: String,
940 #[serde(skip)]
941 validation_schema: Value,
942 },
943 Multipart {
944 schema_name: String,
945 media_type: String,
946 #[serde(skip)]
947 validation_schema: Value,
948 },
949 OctetStream {
950 media_type: String,
951 },
952 Binary {
953 media_type: String,
954 },
955 TextPlain {
956 media_type: String,
957 },
958 SchemaLess {
962 media_type: String,
963 },
964 Unsupported {
965 media_types: Vec<String>,
966 },
967}
968
969impl RequestBodyContent {
970 pub fn schema_name(&self) -> Option<&str> {
972 match self {
973 Self::Json { schema_name, .. }
974 | Self::FormUrlEncoded { schema_name, .. }
975 | Self::Multipart { schema_name, .. } => Some(schema_name),
976 Self::OctetStream { .. }
977 | Self::Binary { .. }
978 | Self::TextPlain { .. }
979 | Self::SchemaLess { .. }
980 | Self::Unsupported { .. } => None,
981 }
982 }
983}
984
985fn base_param_ident(name: &str) -> String {
989 use heck::ToSnakeCase;
990 let suffix = if name.ends_with("<=") {
991 "_lte"
992 } else if name.ends_with(">=") {
993 "_gte"
994 } else if name.ends_with('<') {
995 "_lt"
996 } else if name.ends_with('>') {
997 "_gt"
998 } else {
999 ""
1000 };
1001 let stripped = name.trim_end_matches(['<', '>', '=']);
1002 let mut snake = stripped.to_snake_case();
1003 if snake.is_empty() {
1004 snake.push_str("parameter");
1005 } else if snake.starts_with(|character: char| character.is_ascii_digit()) {
1006 snake.insert(0, '_');
1007 }
1008 snake.push_str(suffix);
1009 snake
1010}
1011
1012#[derive(Debug, Clone, serde::Serialize)]
1014pub struct ParameterInfo {
1015 pub name: String,
1017 pub location: String,
1019 pub required: bool,
1021 pub schema_ref: Option<String>,
1023 pub rust_type: String,
1025 pub description: Option<String>,
1027 #[serde(skip_serializing_if = "Option::is_none")]
1033 pub enum_values: Option<Vec<String>>,
1034 #[serde(skip_serializing_if = "Option::is_none")]
1040 pub enum_varnames: Option<Vec<String>>,
1041 #[serde(skip_serializing_if = "Option::is_none")]
1049 pub rust_ident: Option<String>,
1050 #[serde(skip_serializing_if = "Option::is_none")]
1059 pub query_serialization: Option<QuerySerialization>,
1060 #[serde(skip)]
1063 pub validation_schema: Option<Value>,
1064}
1065
1066#[derive(Debug, Clone, PartialEq, serde::Serialize)]
1069pub enum QuerySerialization {
1070 FormExplodedObject,
1074 FormExplodedNestedObject {
1080 properties: Vec<QueryStructProperty>,
1081 },
1082 FormObject,
1085 DeepObject,
1088 FormExplodedArray { item_type: ArrayItemType },
1091 FormArray { item_type: ArrayItemType },
1094 SimpleHeaderArray { item_type: ArrayItemType },
1097 Unsupported { reason: String },
1102}
1103
1104#[derive(Debug, Clone, PartialEq, serde::Serialize)]
1111pub enum ArrayItemType {
1112 Scalar(String),
1114 SchemaRef(String),
1116 FlatStructRef {
1122 schema_name: String,
1123 properties: Vec<QueryStructProperty>,
1124 },
1125 NestedStructRef {
1129 schema_name: String,
1130 properties: Vec<QueryStructProperty>,
1131 },
1132}
1133
1134#[derive(Debug, Clone, PartialEq, serde::Serialize)]
1135pub struct QueryStructProperty {
1136 pub wire_name: String,
1137 pub required: bool,
1138 pub value_type: QueryStructPropertyType,
1139}
1140
1141#[derive(Debug, Clone, PartialEq, serde::Serialize)]
1142pub enum QueryStructPropertyType {
1143 Scalar(QueryScalarType),
1144 Array {
1145 item_type: ArrayItemType,
1146 },
1147 Object {
1148 properties: Vec<QueryStructProperty>,
1149 },
1150}
1151
1152#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
1153pub enum QueryScalarType {
1154 String,
1155 Integer,
1156 Number,
1157 Boolean,
1158}
1159
1160impl Default for DependencyGraph {
1161 fn default() -> Self {
1162 Self::new()
1163 }
1164}
1165
1166impl DependencyGraph {
1167 pub fn new() -> Self {
1168 Self {
1169 edges: BTreeMap::new(),
1170 recursive_schemas: HashSet::new(),
1171 }
1172 }
1173
1174 pub fn add_dependency(&mut self, from: String, to: String) {
1175 self.edges.entry(from).or_default().insert(to);
1176 }
1177
1178 pub fn topological_sort(&mut self) -> Result<Vec<String>> {
1180 self.detect_recursive_schemas();
1182
1183 let mut temp_edges = self.edges.clone();
1185 for (schema, deps) in &mut temp_edges {
1186 deps.remove(schema); }
1188
1189 let mut visited = HashSet::new();
1190 let mut temp_visited = HashSet::new();
1191 let mut result = Vec::new();
1192
1193 let mut all_nodes: Vec<_> = temp_edges.keys().collect();
1195 all_nodes.sort();
1196 for node in all_nodes {
1197 if !visited.contains(node) {
1198 self.visit_node_recursive(
1199 node,
1200 &temp_edges,
1201 &mut visited,
1202 &mut temp_visited,
1203 &mut result,
1204 )?;
1205 }
1206 }
1207
1208 result.reverse();
1209 Ok(result)
1210 }
1211
1212 fn detect_recursive_schemas(&mut self) {
1213 for (schema, deps) in &self.edges {
1214 if deps.contains(schema) {
1215 self.recursive_schemas.insert(schema.clone());
1217 } else {
1218 if self.has_cycle_from(schema, schema, &mut HashSet::new()) {
1220 self.recursive_schemas.insert(schema.clone());
1221 }
1222 }
1223 }
1224
1225 for (schema, deps) in &self.edges {
1227 for dep in deps {
1228 if let Some(dep_deps) = self.edges.get(dep) {
1229 if dep_deps.contains(schema) {
1230 self.recursive_schemas.insert(schema.clone());
1232 self.recursive_schemas.insert(dep.clone());
1233 }
1234 }
1235 }
1236 }
1237 }
1238
1239 fn has_cycle_from(&self, start: &str, current: &str, visited: &mut HashSet<String>) -> bool {
1240 if visited.contains(current) {
1241 return false; }
1243
1244 visited.insert(current.to_string());
1245
1246 if let Some(deps) = self.edges.get(current) {
1247 for dep in deps {
1248 if dep == start {
1249 return true; }
1251 if self.has_cycle_from(start, dep, visited) {
1252 return true;
1253 }
1254 }
1255 }
1256
1257 false
1258 }
1259
1260 #[allow(clippy::only_used_in_recursion)]
1261 fn visit_node_recursive(
1262 &self,
1263 node: &str,
1264 temp_edges: &BTreeMap<String, HashSet<String>>,
1265 visited: &mut HashSet<String>,
1266 temp_visited: &mut HashSet<String>,
1267 result: &mut Vec<String>,
1268 ) -> Result<()> {
1269 if temp_visited.contains(node) {
1270 return Ok(());
1272 }
1273
1274 if visited.contains(node) {
1275 return Ok(());
1276 }
1277
1278 temp_visited.insert(node.to_string());
1279
1280 if let Some(dependencies) = temp_edges.get(node) {
1281 let mut sorted_deps: Vec<_> = dependencies.iter().collect();
1283 sorted_deps.sort();
1284 for dep in sorted_deps {
1285 self.visit_node_recursive(dep, temp_edges, visited, temp_visited, result)?;
1286 }
1287 }
1288
1289 temp_visited.remove(node);
1290 visited.insert(node.to_string());
1291 result.push(node.to_string());
1292
1293 Ok(())
1294 }
1295}
1296
1297pub fn merge_schema_extensions(
1300 main_spec: Value,
1301 extension_paths: &[impl AsRef<Path>],
1302) -> Result<Value> {
1303 let mut result = main_spec;
1304
1305 for path in extension_paths {
1306 let extension = load_extension_file(path.as_ref())?;
1307 result = merge_json_objects_with_replacements(result, extension)?;
1308 }
1309
1310 Ok(result)
1311}
1312
1313fn normalize_operation_path(path: &str) -> String {
1320 match path.split_once('#') {
1321 Some((route, _fragment)) if route.starts_with('/') => route.to_string(),
1322 _ => path.to_string(),
1323 }
1324}
1325
1326fn unwrap_annotation_allof(schema: &crate::openapi::Schema) -> &crate::openapi::Schema {
1331 let crate::openapi::Schema::AllOf { all_of, .. } = schema else {
1332 return schema;
1333 };
1334 let mut references = all_of.iter().filter(|s| s.reference().is_some());
1335 let (Some(first), None) = (references.next(), references.next()) else {
1336 return schema;
1337 };
1338 let others_annotation_only = all_of
1339 .iter()
1340 .all(|member| member.reference().is_some() || schema_is_annotation_only(member));
1341 if others_annotation_only {
1342 first
1343 } else {
1344 schema
1345 }
1346}
1347
1348fn schema_is_annotation_only(schema: &crate::openapi::Schema) -> bool {
1353 serde_json::to_value(schema)
1354 .ok()
1355 .and_then(|value| value.as_object().cloned())
1356 .is_some_and(|object| {
1357 object.keys().all(|key| {
1358 matches!(
1359 key.as_str(),
1360 "title"
1361 | "description"
1362 | "deprecated"
1363 | "readOnly"
1364 | "writeOnly"
1365 | "examples"
1366 | "example"
1367 | "default"
1368 | "externalDocs"
1369 | "xml"
1370 | "$comment"
1371 | "nullable"
1372 ) || key.starts_with("x-")
1373 })
1374 })
1375}
1376
1377fn load_extension_file(path: &Path) -> Result<Value> {
1381 let content = std::fs::read_to_string(path).map_err(|e| GeneratorError::FileError {
1382 message: format!("Failed to read file {}: {}", path.display(), e),
1383 })?;
1384
1385 let is_yaml = path
1386 .extension()
1387 .and_then(|extension| extension.to_str())
1388 .is_some_and(|extension| {
1389 extension.eq_ignore_ascii_case("yaml") || extension.eq_ignore_ascii_case("yml")
1390 });
1391
1392 if is_yaml {
1393 crate::spec_source::yaml_to_json_value(&content).map_err(|error| {
1394 GeneratorError::FileError {
1395 message: format!(
1396 "Failed to parse schema extension {} as YAML: {}",
1397 path.display(),
1398 error
1399 ),
1400 }
1401 })
1402 } else {
1403 serde_json::from_str(&content).map_err(|error| GeneratorError::FileError {
1404 message: format!(
1405 "Failed to parse schema extension {} as JSON: {}",
1406 path.display(),
1407 error
1408 ),
1409 })
1410 }
1411}
1412
1413fn merge_json_objects_with_replacements(main: Value, extension: Value) -> Result<Value> {
1415 let replacements = extract_replacement_rules(&extension);
1417
1418 Ok(merge_json_objects_with_rules(
1420 main,
1421 extension,
1422 &replacements,
1423 ))
1424}
1425
1426fn extract_replacement_rules(
1428 extension: &Value,
1429) -> std::collections::HashMap<String, (String, String)> {
1430 let mut rules = std::collections::HashMap::new();
1431
1432 if let Some(x_replacements) = extension.get("x-replacements") {
1433 if let Some(x_replacements_obj) = x_replacements.as_object() {
1434 for (schema_name, replacement_rule) in x_replacements_obj {
1435 if let Some(rule_obj) = replacement_rule.as_object() {
1436 if let (Some(replace), Some(with)) = (
1437 rule_obj.get("replace").and_then(|v| v.as_str()),
1438 rule_obj.get("with").and_then(|v| v.as_str()),
1439 ) {
1440 rules.insert(schema_name.clone(), (replace.to_string(), with.to_string()));
1441 }
1443 }
1444 }
1445 }
1446 }
1447
1448 rules
1449}
1450
1451fn should_replace_variant(
1453 schema_name: &str,
1454 extension_refs: &[String],
1455 replacements: &std::collections::HashMap<String, (String, String)>,
1456) -> bool {
1457 for (replace_schema, with_schema) in replacements.values() {
1459 if schema_name == replace_schema {
1460 let replacement_exists = extension_refs.iter().any(|ext_ref| {
1462 let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
1463 ext_schema_name == with_schema
1464 });
1465
1466 if replacement_exists {
1467 return true;
1468 }
1469 }
1470 }
1471
1472 extension_refs.iter().any(|ext_ref| {
1474 let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
1475 schema_name == ext_schema_name
1476 })
1477}
1478
1479fn merge_json_objects_with_rules(
1484 main: Value,
1485 extension: Value,
1486 replacements: &std::collections::HashMap<String, (String, String)>,
1487) -> Value {
1488 match (main, extension) {
1489 (Value::Object(mut main_obj), Value::Object(ext_obj)) => {
1491 let main_union_keyword = if main_obj.contains_key("oneOf") {
1494 Some("oneOf")
1495 } else if main_obj.contains_key("anyOf") {
1496 Some("anyOf")
1497 } else {
1498 None
1499 };
1500 if let (Some(main_variants), Some(ext_variants)) = (
1501 extract_schema_variants(&Value::Object(main_obj.clone())),
1502 extract_schema_variants(&Value::Object(ext_obj.clone())),
1503 ) {
1504 let union_key = main_union_keyword.unwrap_or("oneOf");
1505 println!(
1506 "🔍 Merging union schemas ({union_key}): {} main variants, {} extension variants",
1507 main_variants.len(),
1508 ext_variants.len()
1509 );
1510 let mut merged_variants = Vec::new();
1513 let extension_refs: Vec<String> = ext_variants
1514 .iter()
1515 .filter_map(|v| v.get("$ref").and_then(|r| r.as_str()))
1516 .map(|s| s.to_string())
1517 .collect();
1518
1519 for main_variant in main_variants {
1521 if let Some(main_ref) = main_variant.get("$ref").and_then(|r| r.as_str()) {
1522 let schema_name = main_ref.split('/').next_back().unwrap_or("");
1524 let should_replace =
1525 should_replace_variant(schema_name, &extension_refs, replacements);
1526
1527 if should_replace {
1528 println!("🔄 REPLACING {} (explicit rule)", schema_name);
1529 }
1530
1531 if !should_replace {
1532 merged_variants.push(main_variant);
1533 }
1534 } else {
1535 merged_variants.push(main_variant);
1537 }
1538 }
1539
1540 for ext_variant in ext_variants {
1542 merged_variants.push(ext_variant);
1543 }
1544
1545 main_obj.remove("oneOf");
1547 main_obj.remove("anyOf");
1548 main_obj.insert(union_key.to_string(), Value::Array(merged_variants));
1549
1550 for (key, ext_value) in ext_obj {
1552 if key != "oneOf" && key != "anyOf" {
1553 match main_obj.get(&key) {
1554 Some(main_value) => {
1555 let merged_value = merge_json_objects_with_rules(
1556 main_value.clone(),
1557 ext_value,
1558 replacements,
1559 );
1560 main_obj.insert(key, merged_value);
1561 }
1562 None => {
1563 main_obj.insert(key, ext_value);
1564 }
1565 }
1566 }
1567 }
1568
1569 return Value::Object(main_obj);
1570 }
1571
1572 for (key, ext_value) in ext_obj {
1574 match main_obj.get(&key) {
1575 Some(main_value) => {
1576 let merged_value = merge_json_objects_with_rules(
1578 main_value.clone(),
1579 ext_value,
1580 replacements,
1581 );
1582 main_obj.insert(key, merged_value);
1583 }
1584 None => {
1585 main_obj.insert(key, ext_value);
1587 }
1588 }
1589 }
1590 Value::Object(main_obj)
1591 }
1592
1593 (Value::Array(mut main_arr), Value::Array(ext_arr)) => {
1595 main_arr.extend(ext_arr);
1596 Value::Array(main_arr)
1597 }
1598
1599 (_, extension) => extension,
1601 }
1602}
1603
1604fn extract_schema_variants(obj: &Value) -> Option<Vec<Value>> {
1606 if let Value::Object(map) = obj {
1607 if let Some(Value::Array(variants)) = map.get("oneOf") {
1608 return Some(variants.clone());
1609 }
1610 if let Some(Value::Array(variants)) = map.get("anyOf") {
1611 return Some(variants.clone());
1612 }
1613 }
1614 None
1615}
1616
1617#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1622enum InlineUnionKind {
1623 OneOf,
1624 AnyOf,
1625}
1626
1627#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
1628enum SchemaIdentity {
1629 Component(String),
1630 Pointer(String),
1631 InlineUnionBranch {
1632 owner_context: String,
1633 union_kind: InlineUnionKind,
1634 original_index: usize,
1635 discriminator: Option<String>,
1636 fingerprint: String,
1637 },
1638 Inline {
1639 context: String,
1640 kind: &'static str,
1641 preferred_name: String,
1642 fingerprint: String,
1643 },
1644}
1645
1646#[derive(Debug, Default)]
1647struct SchemaNameRegistry {
1648 names_by_identity: BTreeMap<SchemaIdentity, String>,
1649 identities_by_name: BTreeMap<String, SchemaIdentity>,
1650}
1651
1652impl SchemaNameRegistry {
1653 fn with_components(component_names: impl IntoIterator<Item = String>) -> Self {
1654 let mut registry = Self::default();
1655 for name in component_names {
1656 let identity = SchemaIdentity::Component(name.clone());
1657 registry
1658 .names_by_identity
1659 .insert(identity.clone(), name.clone());
1660 registry.identities_by_name.insert(name, identity);
1661 }
1662 registry
1663 }
1664
1665 fn component_name(&self, source_name: &str) -> Option<&str> {
1666 self.names_by_identity
1667 .get(&SchemaIdentity::Component(source_name.to_string()))
1668 .map(String::as_str)
1669 }
1670
1671 fn allocate(
1672 &mut self,
1673 identity: SchemaIdentity,
1674 preferred_name: &str,
1675 collision_name: &str,
1676 ) -> String {
1677 if let Some(existing) = self.names_by_identity.get(&identity) {
1678 return existing.clone();
1679 }
1680
1681 let allocated = if !self.identities_by_name.contains_key(preferred_name) {
1682 preferred_name.to_string()
1683 } else if !self.identities_by_name.contains_key(collision_name) {
1684 collision_name.to_string()
1685 } else {
1686 let hash = stable_schema_identity_hash(&identity);
1687 let hashed = format!("{collision_name}{hash:016X}");
1688 if !self.identities_by_name.contains_key(&hashed) {
1689 hashed
1690 } else {
1691 let mut suffix = 2;
1692 loop {
1693 let candidate = format!("{hashed}{suffix}");
1694 if !self.identities_by_name.contains_key(&candidate) {
1695 break candidate;
1696 }
1697 suffix += 1;
1698 }
1699 }
1700 };
1701
1702 self.names_by_identity
1703 .insert(identity.clone(), allocated.clone());
1704 self.identities_by_name.insert(allocated.clone(), identity);
1705 allocated
1706 }
1707}
1708
1709fn stable_schema_identity_hash(identity: &SchemaIdentity) -> u64 {
1713 let bytes = format!("{identity:?}");
1714 bytes
1715 .as_bytes()
1716 .iter()
1717 .fold(0xcbf29ce484222325, |hash, byte| {
1718 (hash ^ u64::from(*byte)).wrapping_mul(0x100000001b3)
1719 })
1720}
1721
1722#[cfg(test)]
1723mod schema_name_registry_tests {
1724 use super::{SchemaIdentity, SchemaNameRegistry};
1725
1726 #[test]
1727 fn component_reservations_are_independent_of_input_traversal_order() {
1728 let component_names = ["ModelApi".to_string(), "OutputFormatContainer".to_string()];
1729 let mut forward = SchemaNameRegistry::with_components(component_names.clone());
1730 let mut reverse = SchemaNameRegistry::with_components(component_names.into_iter().rev());
1731 let inline = SchemaIdentity::Inline {
1732 context: "Model".to_string(),
1733 kind: "property-object",
1734 preferred_name: "ModelApi".to_string(),
1735 fingerprint: r#"{"type":"object"}"#.to_string(),
1736 };
1737
1738 assert_eq!(
1739 forward.allocate(inline.clone(), "ModelApi", "ModelApiInline"),
1740 "ModelApiInline"
1741 );
1742 assert_eq!(
1743 reverse.allocate(inline, "ModelApi", "ModelApiInline"),
1744 "ModelApiInline"
1745 );
1746 assert_eq!(forward.component_name("ModelApi"), Some("ModelApi"));
1747 assert_eq!(reverse.component_name("ModelApi"), Some("ModelApi"));
1748 assert_eq!(
1749 forward.component_name("OutputFormatContainer"),
1750 Some("OutputFormatContainer")
1751 );
1752 assert_eq!(
1753 reverse.component_name("OutputFormatContainer"),
1754 Some("OutputFormatContainer")
1755 );
1756 }
1757
1758 #[test]
1759 fn an_identity_reuses_its_exact_allocated_name() {
1760 let mut registry = SchemaNameRegistry::with_components(["ModelApi".to_string()]);
1761 let inline = SchemaIdentity::Inline {
1762 context: "Model".to_string(),
1763 kind: "property-object",
1764 preferred_name: "ModelApi".to_string(),
1765 fingerprint: r#"{"type":"object"}"#.to_string(),
1766 };
1767
1768 let first = registry.allocate(inline.clone(), "ModelApi", "ModelApiInline");
1769 let repeated = registry.allocate(inline, "Ignored", "IgnoredInline");
1770
1771 assert_eq!(first, "ModelApiInline");
1772 assert_eq!(repeated, first);
1773 assert_eq!(registry.component_name("ModelApi"), Some("ModelApi"));
1774 }
1775}
1776
1777pub struct SchemaAnalyzer {
1778 schemas: BTreeMap<String, Schema>,
1779 resolved_cache: BTreeMap<String, AnalyzedSchema>,
1780 schema_names: SchemaNameRegistry,
1781 openapi_spec: Value,
1782 current_schema_name: Option<String>,
1783 component_parameters: BTreeMap<String, crate::openapi::Parameter>,
1784 type_mapper: TypeMapper,
1789 resolving_pointers: HashSet<String>,
1792}
1793
1794impl SchemaAnalyzer {
1795 fn untyped_value(&self, _context: impl Into<String>, reason: UntypedReason) -> SchemaType {
1799 SchemaType::Untyped {
1800 shape: UntypedShape::Value,
1801 reason,
1802 }
1803 }
1804
1805 fn untyped_value_array(
1807 &self,
1808 _context: impl Into<String>,
1809 reason: UntypedReason,
1810 ) -> SchemaType {
1811 SchemaType::Untyped {
1812 shape: UntypedShape::ValueArray,
1813 reason,
1814 }
1815 }
1816
1817 fn untyped_value_map(&self, _context: impl Into<String>, reason: UntypedReason) -> SchemaType {
1821 SchemaType::Untyped {
1822 shape: UntypedShape::ValueMap,
1823 reason,
1824 }
1825 }
1826
1827 fn untyped_context(&self, detail: &str) -> String {
1829 match (&self.current_schema_name, detail) {
1830 (Some(schema), "") => schema.clone(),
1831 (Some(schema), detail) => format!("{schema}.{detail}"),
1832 (None, "") => "<anonymous>".to_string(),
1833 (None, detail) => detail.to_string(),
1834 }
1835 }
1836
1837 fn allocate_inline_schema_name(
1838 &mut self,
1839 preferred_name: &str,
1840 collision_name: &str,
1841 kind: &'static str,
1842 schema: &Schema,
1843 ) -> String {
1844 let identity = SchemaIdentity::Inline {
1845 context: self
1846 .current_schema_name
1847 .clone()
1848 .unwrap_or_else(|| "<anonymous>".to_string()),
1849 kind,
1850 preferred_name: preferred_name.to_string(),
1851 fingerprint: serde_json::to_string(schema).unwrap_or_else(|_| format!("{schema:?}")),
1852 };
1853 self.schema_names
1854 .allocate(identity, preferred_name, collision_name)
1855 }
1856
1857 fn with_schema_context<T>(
1862 &mut self,
1863 schema_name: &str,
1864 analyze: impl FnOnce(&mut Self) -> Result<T>,
1865 ) -> Result<T> {
1866 let previous = self.current_schema_name.replace(schema_name.to_string());
1867 let result = analyze(self);
1868 self.current_schema_name = previous;
1869 result
1870 }
1871
1872 fn add_allocated_object_schema(
1873 &mut self,
1874 object_type_name: String,
1875 schema: &Schema,
1876 dependencies: &mut HashSet<String>,
1877 ) -> Result<SchemaType> {
1878 let object_type = self.with_schema_context(&object_type_name, |analyzer| {
1879 analyzer.analyze_object_schema(schema, dependencies)
1880 })?;
1881 self.resolved_cache.insert(
1882 object_type_name.clone(),
1883 AnalyzedSchema {
1884 name: object_type_name.clone(),
1885 original: serde_json::to_value(schema).unwrap_or(Value::Null),
1886 schema_type: object_type,
1887 dependencies: dependencies.clone(),
1888 nullable: false,
1889 description: schema.details().description.clone(),
1890 default: None,
1891 },
1892 );
1893 dependencies.insert(object_type_name.clone());
1894 Ok(SchemaType::Reference {
1895 target: object_type_name,
1896 })
1897 }
1898
1899 fn allocate_inline_union_branch_name(
1900 &mut self,
1901 preferred_name: &str,
1902 owner_context: &str,
1903 union_kind: InlineUnionKind,
1904 original_index: usize,
1905 discriminator: Option<&str>,
1906 schema: &Schema,
1907 ) -> String {
1908 let identity = SchemaIdentity::InlineUnionBranch {
1909 owner_context: owner_context.to_string(),
1910 union_kind,
1911 original_index,
1912 discriminator: discriminator.map(str::to_string),
1913 fingerprint: serde_json::to_string(schema).unwrap_or_else(|_| format!("{schema:?}")),
1914 };
1915 self.schema_names
1916 .allocate(identity, preferred_name, &format!("{preferred_name}Inline"))
1917 }
1918
1919 fn allocate_pointer_schema_name(&mut self, pointer: &str, preferred_name: &str) -> String {
1920 self.schema_names.allocate(
1921 SchemaIdentity::Pointer(pointer.to_string()),
1922 preferred_name,
1923 &format!("{preferred_name}Pointer"),
1924 )
1925 }
1926
1927 fn allocate_synthetic_schema_name(
1928 &mut self,
1929 preferred_name: &str,
1930 collision_name: &str,
1931 kind: &'static str,
1932 fingerprint: String,
1933 ) -> String {
1934 let identity = SchemaIdentity::Inline {
1935 context: self
1936 .current_schema_name
1937 .clone()
1938 .unwrap_or_else(|| "<anonymous>".to_string()),
1939 kind,
1940 preferred_name: preferred_name.to_string(),
1941 fingerprint,
1942 };
1943 self.schema_names
1944 .allocate(identity, preferred_name, collision_name)
1945 }
1946
1947 fn uses_aws_query_conventions(&self) -> bool {
1948 self.openapi_spec
1949 .pointer("/info/x-providerName")
1950 .and_then(Value::as_str)
1951 .is_some_and(|provider| provider.eq_ignore_ascii_case("amazonaws.com"))
1952 }
1953
1954 pub fn new(openapi_spec: Value) -> Result<Self> {
1958 Self::with_type_mapper(openapi_spec, TypeMapper::default())
1959 }
1960
1961 pub fn with_type_mapper(mut openapi_spec: Value, type_mapper: TypeMapper) -> Result<Self> {
1965 disambiguate_component_schema_names(&mut openapi_spec);
1966 let spec: OpenApiSpec = parse_spec_document(&openapi_spec)?;
1967 let schemas = Self::extract_schemas(&spec)?;
1968
1969 let component_parameters = spec
1970 .components
1971 .as_ref()
1972 .and_then(|c| c.parameters.as_ref())
1973 .cloned()
1974 .unwrap_or_default();
1975 let schema_names = SchemaNameRegistry::with_components(schemas.keys().cloned());
1976 Ok(Self {
1977 schemas,
1978 resolved_cache: BTreeMap::new(),
1979 schema_names,
1980 openapi_spec,
1981 current_schema_name: None,
1982 component_parameters,
1983 type_mapper,
1984 resolving_pointers: HashSet::new(),
1985 })
1986 }
1987
1988 pub fn new_with_extensions(
1991 openapi_spec: Value,
1992 extension_paths: &[std::path::PathBuf],
1993 ) -> Result<Self> {
1994 let merged_spec = merge_schema_extensions(openapi_spec, extension_paths)?;
1995 Self::new(merged_spec)
1996 }
1997
1998 pub fn new_with_extensions_and_type_mapper(
2001 openapi_spec: Value,
2002 extension_paths: &[std::path::PathBuf],
2003 type_mapper: TypeMapper,
2004 ) -> Result<Self> {
2005 let merged_spec = merge_schema_extensions(openapi_spec, extension_paths)?;
2006 Self::with_type_mapper(merged_spec, type_mapper)
2007 }
2008
2009 pub fn type_mapper(&self) -> &TypeMapper {
2013 &self.type_mapper
2014 }
2015
2016 fn generate_context_aware_name(
2019 &self,
2020 base_context: &str,
2021 type_hint: &str,
2022 index: usize,
2023 schema: Option<&Schema>,
2024 ) -> String {
2025 if let Some(schema) = schema {
2027 if type_hint == "Array"
2029 && matches!(schema.schema_type(), Some(OpenApiSchemaType::Array))
2030 {
2031 if let Some(items_schema) = schema.details().item_schema() {
2032 if let Some(item_type) = items_schema.schema_type() {
2034 match item_type {
2035 OpenApiSchemaType::Object => {
2036 return format!("{base_context}ItemArray");
2037 }
2038 OpenApiSchemaType::String => {
2039 return format!("{base_context}StringArray");
2040 }
2041 _ => {}
2042 }
2043 }
2044 }
2045 }
2046 }
2047
2048 match type_hint {
2050 "Array" => {
2051 format!("{base_context}Array")
2053 }
2054 "Variant" | "InlineVariant" => {
2055 if index == 0 {
2057 format!("{base_context}{type_hint}")
2058 } else {
2059 format!("{}{}{}", base_context, type_hint, index + 1)
2060 }
2061 }
2062 _ => {
2063 format!("{base_context}{type_hint}{index}")
2065 }
2066 }
2067 }
2068
2069 fn to_pascal_case(&self, s: &str) -> String {
2071 s.split(['_', '-'])
2072 .filter(|part| !part.is_empty())
2073 .map(|part| {
2074 let mut chars = part.chars();
2075 match chars.next() {
2076 None => String::new(),
2077 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
2078 }
2079 })
2080 .collect()
2081 }
2082
2083 fn extract_schemas(spec: &OpenApiSpec) -> Result<BTreeMap<String, Schema>> {
2084 let schemas = spec.components.as_ref().and_then(|c| c.schemas.as_ref());
2089 Ok(schemas
2090 .map(|m| {
2091 m.iter()
2092 .map(|(k, v)| (k.clone(), v.clone()))
2093 .collect::<BTreeMap<_, _>>()
2094 })
2095 .unwrap_or_default())
2096 }
2097
2098 pub fn analyze(&mut self) -> Result<SchemaAnalysis> {
2099 let validation_context = ValidationContext {
2100 openapi_version: self
2101 .openapi_spec
2102 .get("openapi")
2103 .and_then(Value::as_str)
2104 .unwrap_or_default()
2105 .to_string(),
2106 json_schema_dialect: self
2107 .openapi_spec
2108 .get("jsonSchemaDialect")
2109 .and_then(Value::as_str)
2110 .map(str::to_string),
2111 component_schemas: self
2112 .openapi_spec
2113 .pointer("/components/schemas")
2114 .and_then(Value::as_object)
2115 .map(|schemas| {
2116 schemas
2117 .iter()
2118 .map(|(name, schema)| (name.clone(), schema.clone()))
2119 .collect()
2120 })
2121 .unwrap_or_default(),
2122 };
2123 let mut analysis = SchemaAnalysis {
2124 schemas: BTreeMap::new(),
2125 dependencies: DependencyGraph::new(),
2126 patterns: DetectedPatterns {
2127 tagged_enum_schemas: HashSet::new(),
2128 untagged_enum_schemas: HashSet::new(),
2129 type_mappings: BTreeMap::new(),
2130 },
2131 operations: BTreeMap::new(),
2132 operation_responses: BTreeMap::new(),
2133 operation_id_aliases: BTreeMap::new(),
2134 used_type_features: crate::type_mapping::UsedFeatures::default(),
2135 enum_extensions: BTreeMap::new(),
2136 validation_context,
2137 untagged_union_branches: BTreeSet::new(),
2138 };
2139
2140 self.detect_patterns(&mut analysis.patterns)?;
2142
2143 let schema_names: Vec<String> = self.schemas.keys().cloned().collect();
2145 for schema_name in schema_names {
2146 let analyzed = self.analyze_schema(&schema_name)?;
2147
2148 for dep in &analyzed.dependencies {
2150 analysis
2151 .dependencies
2152 .add_dependency(schema_name.clone(), dep.clone());
2153 }
2154
2155 analysis.schemas.insert(schema_name, analyzed);
2156 }
2157
2158 for (inline_name, inline_schema) in &self.resolved_cache {
2161 if !analysis.schemas.contains_key(inline_name) {
2162 analysis
2164 .schemas
2165 .insert(inline_name.clone(), inline_schema.clone());
2166
2167 for dep in &inline_schema.dependencies {
2169 analysis
2170 .dependencies
2171 .add_dependency(inline_name.clone(), dep.clone());
2172 }
2173
2174 let mut schemas_to_update = Vec::new();
2179 for (schema_name, schema) in &analysis.schemas {
2180 if schema_name == inline_name {
2182 continue;
2183 }
2184
2185 if schema.dependencies.contains(inline_name) {
2186 schemas_to_update.push(schema_name.clone());
2188 }
2189 }
2190
2191 for schema_name in schemas_to_update {
2193 analysis
2194 .dependencies
2195 .add_dependency(schema_name, inline_name.clone());
2196 }
2197 }
2198 }
2199
2200 self.analyze_operations(&mut analysis)?;
2202
2203 for (inline_name, inline_schema) in &self.resolved_cache {
2206 if !analysis.schemas.contains_key(inline_name) {
2207 analysis
2208 .schemas
2209 .insert(inline_name.clone(), inline_schema.clone());
2210
2211 for dep in &inline_schema.dependencies {
2213 analysis
2214 .dependencies
2215 .add_dependency(inline_name.clone(), dep.clone());
2216 }
2217 }
2218 }
2219
2220 disambiguate_analyzed_schema_names(&mut analysis, &self.schemas);
2221
2222 analysis.used_type_features = self.type_mapper.used_features();
2226
2227 for (name, analyzed) in &analysis.schemas {
2232 let enum_value_count = match &analyzed.schema_type {
2233 SchemaType::StringEnum { values } => values.len(),
2234 SchemaType::ExtensibleEnum { known_values } => known_values.len(),
2235 _ => continue,
2236 };
2237 if let Some(ext) = extract_enum_extensions(&analyzed.original, enum_value_count, name) {
2238 analysis.enum_extensions.insert(name.clone(), ext);
2239 }
2240 }
2241
2242 analysis.untagged_union_branches = collect_untagged_union_branches(&analysis);
2243
2244 for schema in analysis.schemas.values_mut() {
2245 normalize_untyped(&mut schema.schema_type, 0);
2246 }
2247
2248 Ok(analysis)
2249 }
2250
2251 fn detect_patterns(&self, patterns: &mut DetectedPatterns) -> Result<()> {
2252 for (schema_name, schema) in &self.schemas {
2253 if self.is_discriminated_union(schema) {
2255 patterns.tagged_enum_schemas.insert(schema_name.clone());
2256
2257 if let Some(mappings) = self.extract_type_mappings(schema)? {
2259 patterns.type_mappings.insert(schema_name.clone(), mappings);
2260 }
2261 }
2262 else if self.is_simple_union(schema) {
2264 patterns.untagged_enum_schemas.insert(schema_name.clone());
2265 }
2266 }
2267
2268 Ok(())
2269 }
2270
2271 fn is_discriminated_union(&self, schema: &Schema) -> bool {
2272 if schema.is_discriminated_union() {
2274 return true;
2275 }
2276
2277 if let Some(variants) = schema.union_variants() {
2279 return variants.len() > 2 && self.detect_discriminator_field(variants).is_some();
2280 }
2281
2282 false
2283 }
2284
2285 fn all_variants_have_unique_const_values(&self, variants: &[Schema], field_name: &str) -> bool {
2286 let mut values = HashSet::new();
2287
2288 variants.iter().all(|variant| {
2289 let schema = if let Some(ref_str) = variant.reference() {
2290 let Some(schema_name) = self.extract_schema_name(ref_str) else {
2291 return false;
2292 };
2293 let Some(schema) = self.schemas.get(schema_name) else {
2294 return false;
2295 };
2296 schema
2297 } else {
2298 variant
2299 };
2300
2301 self.extract_discriminator_value_for_field(schema, field_name)
2302 .is_some_and(|value| values.insert(value))
2303 })
2304 }
2305
2306 fn branch_resolves_to_object(&self, schema: &Schema) -> bool {
2315 self.branch_resolves_to_object_inner(schema, &mut HashSet::new())
2316 }
2317
2318 fn branch_resolves_to_object_inner(
2319 &self,
2320 schema: &Schema,
2321 visited_refs: &mut HashSet<String>,
2322 ) -> bool {
2323 if let Some(ref_str) = schema.reference() {
2325 if !visited_refs.insert(ref_str.to_string()) {
2326 return false;
2327 }
2328 let result = match self
2329 .extract_schema_name(ref_str)
2330 .and_then(|n| self.schemas.get(n))
2331 {
2332 Some(target) => self.branch_resolves_to_object_inner(target, visited_refs),
2333 None => false,
2334 };
2335 visited_refs.remove(ref_str);
2336 return result;
2337 }
2338 if let Schema::AllOf { all_of, .. } = schema {
2342 if Self::single_non_object_allof_carrier(all_of).is_some() {
2343 return false;
2344 }
2345 return all_of
2346 .iter()
2347 .filter(|member| !schema_is_annotation_only(member))
2348 .any(|member| self.branch_resolves_to_object_inner(member, visited_refs));
2349 }
2350 if let Some(variants) = schema.union_variants() {
2355 return !variants.is_empty()
2356 && variants
2357 .iter()
2358 .all(|variant| self.branch_resolves_to_object_inner(variant, visited_refs));
2359 }
2360 if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object)) {
2361 return true;
2362 }
2363 if schema.inferred_type() == Some(OpenApiSchemaType::Object) {
2364 return true;
2365 }
2366 false
2369 }
2370
2371 fn detect_discriminator_field(&self, variants: &[Schema]) -> Option<String> {
2375 if variants.is_empty() {
2376 return None;
2377 }
2378
2379 let first_variant = &variants[0];
2381 let first_schema = if let Some(ref_str) = first_variant.reference() {
2382 let schema_name = self.extract_schema_name(ref_str)?;
2383 self.schemas.get(schema_name)?
2384 } else {
2385 first_variant
2386 };
2387
2388 let properties = first_schema.details().properties.as_ref()?;
2389 let mut candidates: Vec<String> = Vec::new();
2390
2391 for (field_name, field_schema) in properties {
2392 let details = field_schema.details();
2393 let is_const = details.const_value.is_some()
2394 || details.enum_values.as_ref().is_some_and(|v| v.len() == 1)
2395 || details.extra.contains_key("const");
2396 if is_const {
2397 candidates.push(field_name.clone());
2398 }
2399 }
2400
2401 if candidates.is_empty() {
2402 return None;
2403 }
2404
2405 candidates.sort_by(|a, b| {
2407 if a == "type" {
2408 std::cmp::Ordering::Less
2409 } else if b == "type" {
2410 std::cmp::Ordering::Greater
2411 } else {
2412 a.cmp(b)
2413 }
2414 });
2415
2416 for candidate in &candidates {
2422 if self.all_variants_have_unique_const_values(variants, candidate) {
2423 return Some(candidate.clone());
2424 }
2425 }
2426
2427 None
2428 }
2429
2430 fn is_simple_union(&self, schema: &Schema) -> bool {
2431 if let Some(variants) = schema.union_variants() {
2432 if variants.len() > 1 && !schema.is_nullable_pattern() {
2434 let has_refs = variants.iter().any(|v| v.is_reference());
2435 return has_refs;
2436 }
2437 }
2438 false
2439 }
2440
2441 fn schema_or_reference_is_nullable(&self, schema: &Schema) -> bool {
2446 let mut current = schema.clone();
2447 let mut visited = HashSet::new();
2448 loop {
2449 if current.is_nullable_any() {
2450 return true;
2451 }
2452 if let Schema::AllOf { all_of, .. } = ¤t
2453 && let Some(carrier) = Self::single_non_object_allof_carrier(all_of)
2454 {
2455 current = carrier.clone();
2456 continue;
2457 }
2458 let Some(reference) = current.reference() else {
2459 return false;
2460 };
2461 if !visited.insert(reference.to_string()) {
2462 return false;
2463 }
2464 let Some(target) = self.reference_target_schema(reference) else {
2465 return false;
2466 };
2467 current = target;
2468 }
2469 }
2470
2471 fn nullable_container_value(&self, schema: &Schema, schema_type: SchemaType) -> SchemaType {
2477 if !self.schema_or_reference_is_nullable(schema)
2478 || matches!(schema_type, SchemaType::Nullable { .. })
2479 || matches!(schema.schema_type(), Some(OpenApiSchemaType::Null))
2480 {
2481 schema_type
2482 } else {
2483 SchemaType::Nullable {
2484 inner_type: Box::new(schema_type),
2485 }
2486 }
2487 }
2488
2489 fn extract_type_mappings(&self, schema: &Schema) -> Result<Option<BTreeMap<String, String>>> {
2490 let variants = schema.union_variants().ok_or_else(|| {
2491 GeneratorError::InvalidSchema("No variants found for discriminated union".to_string())
2492 })?;
2493
2494 let discriminator_field = if let Some(discriminator) = schema.discriminator() {
2496 discriminator.property_name.clone()
2497 } else if let Some(detected) = self.detect_discriminator_field(variants) {
2498 detected
2499 } else {
2500 "type".to_string() };
2502
2503 let mut mappings = BTreeMap::new();
2504
2505 for variant in variants {
2506 if let Some(ref_str) = variant.reference() {
2507 if let Some(type_name) = self.extract_schema_name(ref_str) {
2508 if let Some(variant_schema) = self.schemas.get(type_name) {
2509 if let Some(discriminator_value) = self
2510 .extract_discriminator_value_for_field(
2511 variant_schema,
2512 &discriminator_field,
2513 )
2514 {
2515 mappings.insert(type_name.to_string(), discriminator_value);
2516 }
2517 }
2518 }
2519 }
2520 }
2521
2522 if mappings.is_empty() {
2523 Ok(None)
2524 } else {
2525 Ok(Some(mappings))
2526 }
2527 }
2528
2529 #[allow(dead_code)]
2530 fn extract_discriminator_value(&self, schema: &Schema) -> Option<String> {
2531 self.extract_discriminator_value_for_field(schema, "type")
2532 }
2533
2534 fn extract_discriminator_values_for_field(
2535 &self,
2536 schema: &Schema,
2537 field_name: &str,
2538 ) -> Vec<String> {
2539 self.extract_discriminator_value_domain_for_field(schema, field_name)
2540 .unwrap_or_default()
2541 }
2542
2543 fn extract_discriminator_value_domain_for_field(
2544 &self,
2545 schema: &Schema,
2546 field_name: &str,
2547 ) -> Option<Vec<String>> {
2548 let mut visited_refs = HashSet::new();
2549 self.discriminator_value_domain(schema, field_name, &mut visited_refs)
2550 }
2551
2552 fn discriminator_value_domain(
2559 &self,
2560 schema: &Schema,
2561 field_name: &str,
2562 visited_refs: &mut HashSet<String>,
2563 ) -> Option<Vec<String>> {
2564 if let Some(reference) = schema.reference() {
2565 if !visited_refs.insert(reference.to_string()) {
2566 return None;
2567 }
2568 let result = self
2569 .extract_schema_name(reference)
2570 .and_then(|name| self.schemas.get(name))
2571 .and_then(|target| {
2572 self.discriminator_value_domain(target, field_name, visited_refs)
2573 });
2574 visited_refs.remove(reference);
2575 return result;
2576 }
2577
2578 let own_domain = schema
2579 .details()
2580 .properties
2581 .as_ref()
2582 .and_then(|properties| properties.get(field_name))
2583 .and_then(|property| self.string_constraint_domain(property, visited_refs));
2584
2585 let composition_domain = match schema {
2586 Schema::AllOf { all_of, .. } => {
2587 let mut domain = None;
2588 for member in all_of {
2589 domain = Self::intersect_optional_domains(
2590 domain,
2591 self.discriminator_value_domain(member, field_name, visited_refs),
2592 );
2593 }
2594 domain
2595 }
2596 Schema::AnyOf { any_of, .. } => {
2597 self.union_discriminator_domains(any_of, field_name, visited_refs)
2598 }
2599 Schema::OneOf { one_of, .. } => {
2600 self.union_discriminator_domains(one_of, field_name, visited_refs)
2601 }
2602 Schema::Bool(false) => Some(Vec::new()),
2603 _ => None,
2604 };
2605
2606 Self::intersect_optional_domains(own_domain, composition_domain)
2607 }
2608
2609 fn union_discriminator_domains(
2610 &self,
2611 schemas: &[Schema],
2612 field_name: &str,
2613 visited_refs: &mut HashSet<String>,
2614 ) -> Option<Vec<String>> {
2615 if schemas.is_empty() {
2616 return Some(Vec::new());
2617 }
2618 let mut domain = Vec::new();
2619 for schema in schemas {
2620 let values = self.discriminator_value_domain(schema, field_name, visited_refs)?;
2621 for value in values {
2622 Self::push_unique_string(&mut domain, &value);
2623 }
2624 }
2625 Some(domain)
2626 }
2627
2628 fn string_constraint_domain(
2629 &self,
2630 schema: &Schema,
2631 visited_refs: &mut HashSet<String>,
2632 ) -> Option<Vec<String>> {
2633 let allow_vendor_default =
2634 !self.has_standard_string_constraint(schema, &mut HashSet::new());
2635 self.string_constraint_domain_inner(schema, visited_refs, allow_vendor_default)
2636 }
2637
2638 fn string_constraint_domain_inner(
2639 &self,
2640 schema: &Schema,
2641 visited_refs: &mut HashSet<String>,
2642 allow_vendor_default: bool,
2643 ) -> Option<Vec<String>> {
2644 if let Some(reference) = schema.reference() {
2645 if !visited_refs.insert(reference.to_string()) {
2646 return None;
2647 }
2648 let result = self
2649 .extract_schema_name(reference)
2650 .and_then(|name| self.schemas.get(name))
2651 .and_then(|target| {
2652 self.string_constraint_domain_inner(target, visited_refs, allow_vendor_default)
2653 });
2654 visited_refs.remove(reference);
2655 return result;
2656 }
2657
2658 let details = schema.details();
2659 let mut own_domain = None;
2660 if let Some(value) = details.const_value.as_ref() {
2661 own_domain = Self::intersect_optional_domains(
2662 own_domain,
2663 Some(value.as_str().into_iter().map(str::to_string).collect()),
2664 );
2665 }
2666 if let Some(enum_values) = &details.enum_values {
2667 own_domain = Self::intersect_optional_domains(
2668 own_domain,
2669 Some(
2670 enum_values
2671 .iter()
2672 .filter_map(Value::as_str)
2673 .map(str::to_string)
2674 .collect(),
2675 ),
2676 );
2677 }
2678 if allow_vendor_default
2679 && details
2680 .extra
2681 .get("x-stainless-const")
2682 .and_then(Value::as_bool)
2683 == Some(true)
2684 && let Some(default) = details.default.as_ref().and_then(Value::as_str)
2685 {
2686 own_domain =
2687 Self::intersect_optional_domains(own_domain, Some(vec![default.to_string()]));
2688 }
2689
2690 let composition_domain = match schema {
2691 Schema::AllOf { all_of, .. } => {
2692 let mut domain = None;
2693 for member in all_of {
2694 domain = Self::intersect_optional_domains(
2695 domain,
2696 self.string_constraint_domain_inner(
2697 member,
2698 visited_refs,
2699 allow_vendor_default,
2700 ),
2701 );
2702 }
2703 domain
2704 }
2705 Schema::AnyOf { any_of, .. } => {
2706 self.union_string_constraint_domains(any_of, visited_refs, allow_vendor_default)
2707 }
2708 Schema::OneOf { one_of, .. } => {
2709 self.union_string_constraint_domains(one_of, visited_refs, allow_vendor_default)
2710 }
2711 Schema::Bool(false) => Some(Vec::new()),
2712 _ => None,
2713 };
2714
2715 Self::intersect_optional_domains(own_domain, composition_domain)
2716 }
2717
2718 fn union_string_constraint_domains(
2719 &self,
2720 schemas: &[Schema],
2721 visited_refs: &mut HashSet<String>,
2722 allow_vendor_default: bool,
2723 ) -> Option<Vec<String>> {
2724 if schemas.is_empty() {
2725 return Some(Vec::new());
2726 }
2727 let mut domain = Vec::new();
2728 for schema in schemas {
2729 let values =
2730 self.string_constraint_domain_inner(schema, visited_refs, allow_vendor_default)?;
2731 for value in values {
2732 Self::push_unique_string(&mut domain, &value);
2733 }
2734 }
2735 Some(domain)
2736 }
2737
2738 fn has_standard_string_constraint(
2739 &self,
2740 schema: &Schema,
2741 visited_refs: &mut HashSet<String>,
2742 ) -> bool {
2743 if let Some(reference) = schema.reference() {
2744 if !visited_refs.insert(reference.to_string()) {
2745 return false;
2746 }
2747 let result = self
2748 .extract_schema_name(reference)
2749 .and_then(|name| self.schemas.get(name))
2750 .is_some_and(|target| self.has_standard_string_constraint(target, visited_refs));
2751 visited_refs.remove(reference);
2752 return result;
2753 }
2754
2755 let details = schema.details();
2756 if details.const_value.is_some() || details.enum_values.is_some() {
2757 return true;
2758 }
2759
2760 match schema {
2761 Schema::AllOf { all_of, .. } => all_of
2762 .iter()
2763 .any(|member| self.has_standard_string_constraint(member, visited_refs)),
2764 Schema::AnyOf { any_of, .. } => any_of
2765 .iter()
2766 .any(|member| self.has_standard_string_constraint(member, visited_refs)),
2767 Schema::OneOf { one_of, .. } => one_of
2768 .iter()
2769 .any(|member| self.has_standard_string_constraint(member, visited_refs)),
2770 _ => false,
2771 }
2772 }
2773
2774 fn intersect_optional_domains(
2775 left: Option<Vec<String>>,
2776 right: Option<Vec<String>>,
2777 ) -> Option<Vec<String>> {
2778 match (left, right) {
2779 (None, other) | (other, None) => other,
2780 (Some(left), Some(right)) => Some(
2781 left.into_iter()
2782 .filter(|value| right.contains(value))
2783 .collect(),
2784 ),
2785 }
2786 }
2787
2788 fn push_unique_string(values: &mut Vec<String>, value: &str) {
2789 if !values.iter().any(|existing| existing == value) {
2790 values.push(value.to_string());
2791 }
2792 }
2793
2794 fn discriminator_property_presence(&self, schema: &Schema, field_name: &str) -> (bool, bool) {
2795 self.discriminator_property_presence_inner(schema, field_name, &mut HashSet::new(), 0)
2796 }
2797
2798 fn discriminator_property_presence_inner(
2799 &self,
2800 schema: &Schema,
2801 field_name: &str,
2802 visited_refs: &mut HashSet<String>,
2803 depth: usize,
2804 ) -> (bool, bool) {
2805 if depth > 64 {
2806 return (false, false);
2807 }
2808 if let Some(reference) = schema.reference() {
2809 if !visited_refs.insert(reference.to_string()) {
2810 return (false, false);
2811 }
2812 let result = self
2813 .extract_schema_name(reference)
2814 .and_then(|name| self.schemas.get(name))
2815 .map(|target| {
2816 self.discriminator_property_presence_inner(
2817 target,
2818 field_name,
2819 visited_refs,
2820 depth + 1,
2821 )
2822 })
2823 .unwrap_or((false, false));
2824 visited_refs.remove(reference);
2825 return result;
2826 }
2827
2828 let details = schema.details();
2829 let mut declared = details
2830 .properties
2831 .as_ref()
2832 .is_some_and(|properties| properties.contains_key(field_name));
2833 let mut required = details
2834 .required
2835 .as_ref()
2836 .is_some_and(|names| names.iter().any(|name| name == field_name));
2837
2838 let members = match schema {
2839 Schema::AllOf { all_of, .. } => Some(all_of.as_slice()),
2840 Schema::AnyOf { any_of, .. } => Some(any_of.as_slice()),
2841 Schema::OneOf { one_of, .. } => Some(one_of.as_slice()),
2842 _ => None,
2843 };
2844 if let Some(members) = members {
2845 for member in members {
2846 let (member_declared, member_required) = self
2847 .discriminator_property_presence_inner(
2848 member,
2849 field_name,
2850 visited_refs,
2851 depth + 1,
2852 );
2853 declared |= member_declared;
2854 required |= member_required;
2855 }
2856 }
2857 (declared, required)
2858 }
2859
2860 fn extract_discriminator_value_for_field(
2861 &self,
2862 schema: &Schema,
2863 field_name: &str,
2864 ) -> Option<String> {
2865 self.extract_discriminator_values_for_field(schema, field_name)
2866 .into_iter()
2867 .next()
2868 }
2869
2870 fn get_any_reference<'a>(&self, schema: &'a Schema) -> Option<&'a str> {
2871 schema.reference().or_else(|| schema.recursive_reference())
2872 }
2873
2874 fn extract_schema_name<'a>(&self, ref_str: &'a str) -> Option<&'a str> {
2875 if ref_str == "#" {
2876 return None; }
2878
2879 let parts: Vec<&str> = ref_str.split('/').collect();
2880
2881 if parts.len() == 4 && parts[0] == "#" && parts[2] == "schemas" {
2885 return Some(parts[3]);
2886 }
2887
2888 if parts.len() == 3 && parts[0] == "#" && parts[1] == "definitions" {
2891 return Some(parts[2]);
2892 }
2893
2894 if ref_str.starts_with("#/") {
2898 return None;
2899 }
2900
2901 let last = parts.last()?;
2907 if last.is_empty()
2908 || last.chars().all(|c| c.is_ascii_digit())
2909 || matches!(
2910 *last,
2911 "schema" | "properties" | "items" | "additionalProperties"
2912 )
2913 {
2914 return None;
2915 }
2916 let first = last.chars().next().unwrap_or(' ');
2917 if !first.is_ascii_alphabetic() || !first.is_ascii_uppercase() {
2918 return None;
2919 }
2920 Some(last)
2921 }
2922
2923 fn reference_target_schema(&self, reference: &str) -> Option<Schema> {
2926 if let Some(name) = self.extract_schema_name(reference) {
2927 return self.schemas.get(name).cloned();
2928 }
2929 let pointer = reference.strip_prefix('#')?;
2930 if !pointer.starts_with('/') {
2931 return None;
2932 }
2933 Schema::deserialize(self.openapi_spec.pointer(pointer)?).ok()
2934 }
2935
2936 fn analyze_schema(&mut self, schema_name: &str) -> Result<AnalyzedSchema> {
2937 let emitted_name = self
2941 .schema_names
2942 .component_name(schema_name)
2943 .ok_or_else(|| GeneratorError::UnresolvedReference(schema_name.to_string()))?
2944 .to_string();
2945 if let Some(cached) = self.resolved_cache.get(&emitted_name) {
2946 return Ok(cached.clone());
2947 }
2948
2949 self.current_schema_name = Some(emitted_name.clone());
2951
2952 let schema = self
2953 .schemas
2954 .get(schema_name)
2955 .ok_or_else(|| GeneratorError::UnresolvedReference(schema_name.to_string()))?
2956 .clone();
2957
2958 self.resolved_cache.insert(
2960 emitted_name.clone(),
2961 AnalyzedSchema {
2962 name: emitted_name.clone(),
2963 original: serde_json::to_value(&schema).unwrap_or(Value::Null),
2964 schema_type: SchemaType::Reference {
2965 target: "placeholder".to_string(),
2966 },
2967 dependencies: HashSet::new(),
2968 nullable: false,
2969 description: None,
2970 default: None,
2971 },
2972 );
2973
2974 let analyzed = self.analyze_schema_value(&schema, &emitted_name)?;
2975
2976 self.resolved_cache.insert(emitted_name, analyzed.clone());
2978
2979 Ok(analyzed)
2980 }
2981
2982 fn analyze_schema_value(
2983 &mut self,
2984 schema: &Schema,
2985 schema_name: &str,
2986 ) -> Result<AnalyzedSchema> {
2987 let details = schema.details();
2988 let description = details.description.clone();
2989 let nullable = schema.is_nullable_any();
2993 let mut dependencies = HashSet::new();
2994
2995 let schema_type = match schema {
2996 Schema::Bool(accepts_anything) => self.untyped_value(
2999 self.untyped_context(""),
3000 if *accepts_anything {
3001 UntypedReason::AnySchema
3002 } else {
3003 UntypedReason::NeverMatches
3004 },
3005 ),
3006 Schema::Reference { reference, .. } => {
3007 match self.extract_schema_name(reference) {
3012 Some(name) => {
3013 let target = name.to_string();
3014 dependencies.insert(target.clone());
3015 SchemaType::Reference { target }
3016 }
3017 None => {
3018 let reference = reference.clone();
3019 if let Some(resolved) =
3020 self.resolve_pointer_schema(&reference, &mut dependencies)?
3021 {
3022 resolved
3023 } else {
3024 eprintln!(
3025 "⚠️ unresolvable $ref `{}` — typing as serde_json::Value",
3026 reference
3027 );
3028 self.untyped_value(
3029 format!("$ref {reference}"),
3030 UntypedReason::UnresolvedReference,
3031 )
3032 }
3033 }
3034 }
3035 }
3036 Schema::RecursiveRef { recursive_ref, .. }
3037 | Schema::DynamicRef {
3038 dynamic_ref: recursive_ref,
3039 ..
3040 } => {
3041 if recursive_ref == "#" {
3047 dependencies.insert(schema_name.to_string());
3048 SchemaType::Reference {
3049 target: schema_name.to_string(),
3050 }
3051 } else {
3052 let target = self
3053 .extract_schema_name(recursive_ref)
3054 .unwrap_or(schema_name)
3055 .to_string();
3056 dependencies.insert(target.clone());
3057 SchemaType::Reference { target }
3058 }
3059 }
3060 Schema::Typed { .. } | Schema::TypedMulti { .. } => {
3061 if let Some(non_null_types) = schema.non_null_schema_types() {
3062 let mut variants = Vec::with_capacity(non_null_types.len());
3063 for t in non_null_types {
3064 variants.push(self.build_typed_multi_union_variant(
3065 t,
3066 schema,
3067 schema_name,
3068 &mut dependencies,
3069 )?);
3070 }
3071 SchemaType::Union {
3072 variants,
3073 exclusive: false,
3074 }
3075 } else {
3076 self.analyze_single_typed_schema(
3077 schema,
3078 schema_name,
3079 details,
3080 &mut dependencies,
3081 )?
3082 }
3083 }
3084 Schema::AnyOf {
3085 any_of,
3086 discriminator,
3087 ..
3088 } => {
3089 if Self::union_only_constrains_requiredness(any_of) {
3090 return Ok(AnalyzedSchema {
3091 name: schema_name.to_string(),
3092 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3093 schema_type: self.analyze_empty_union(schema, &mut dependencies)?,
3094 dependencies,
3095 nullable,
3096 description,
3097 default: details.default.clone(),
3098 });
3099 }
3100 if let Some(schema_type) = self.analyze_object_with_variants(
3101 schema,
3102 any_of,
3103 schema_name,
3104 &mut dependencies,
3105 )? {
3106 return Ok(AnalyzedSchema {
3107 name: schema_name.to_string(),
3108 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3109 schema_type,
3110 dependencies,
3111 nullable,
3112 description,
3113 default: details.default.clone(),
3114 });
3115 }
3116 self.analyze_anyof_union(
3118 any_of,
3119 discriminator.as_ref(),
3120 &mut dependencies,
3121 schema_name,
3122 )?
3123 }
3124 Schema::OneOf {
3125 one_of,
3126 discriminator,
3127 ..
3128 } => {
3129 if one_of.is_empty() {
3130 self.analyze_empty_union(schema, &mut dependencies)?
3131 } else if let Some(schema_type) = self.analyze_object_with_variants(
3132 schema,
3133 one_of,
3134 schema_name,
3135 &mut dependencies,
3136 )? {
3137 schema_type
3138 } else {
3139 self.analyze_oneof_union(
3141 one_of,
3142 discriminator.as_ref(),
3143 schema_name,
3144 &mut dependencies,
3145 InlineUnionKind::OneOf,
3146 None,
3147 )?
3148 }
3149 }
3150 Schema::AllOf { all_of, .. } => {
3151 self.analyze_allof_composition(schema, all_of, &mut dependencies)?
3153 }
3154 Schema::Untyped { .. } => {
3155 if let Some(inferred) = schema.inferred_type() {
3157 match inferred {
3158 OpenApiSchemaType::Object => {
3159 if self.should_use_dynamic_json(schema) {
3160 self.untyped_value(
3161 self.untyped_context(""),
3162 UntypedReason::OpaqueObject,
3163 )
3164 } else {
3165 self.analyze_object_schema(schema, &mut dependencies)?
3166 }
3167 }
3168 OpenApiSchemaType::String if details.is_string_enum() => {
3169 SchemaType::StringEnum {
3170 values: details.string_enum_values().unwrap_or_default(),
3171 }
3172 }
3173 OpenApiSchemaType::Null => SchemaType::Primitive {
3176 rust_type: self.type_mapper.null_unit().rust_type,
3177 serde_with: None,
3178 },
3179 _ => self.untyped_value(
3180 self.untyped_context(""),
3181 UntypedReason::UnsupportedTypeKeyword,
3182 ),
3183 }
3184 } else {
3185 self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema)
3186 }
3187 }
3188 };
3189
3190 Ok(AnalyzedSchema {
3191 name: schema_name.to_string(),
3192 original: serde_json::to_value(schema).unwrap_or(Value::Null), schema_type,
3194 dependencies,
3195 nullable,
3196 description,
3197 default: details.default.clone(),
3198 })
3199 }
3200
3201 fn analyze_single_typed_schema(
3207 &mut self,
3208 schema: &Schema,
3209 schema_name: &str,
3210 details: &crate::openapi::SchemaDetails,
3211 dependencies: &mut HashSet<String>,
3212 ) -> Result<SchemaType> {
3213 let primary = schema
3214 .schema_type()
3215 .cloned()
3216 .unwrap_or(OpenApiSchemaType::Object);
3217 let format = details.format.as_deref();
3218 Ok(match primary {
3219 OpenApiSchemaType::String => {
3220 if let Some(values) = details.string_enum_values() {
3221 SchemaType::StringEnum { values }
3222 } else {
3223 let mapped = self.type_mapper.string_format(format);
3224 SchemaType::Primitive {
3225 rust_type: mapped.rust_type,
3226 serde_with: mapped.serde_with,
3227 }
3228 }
3229 }
3230 OpenApiSchemaType::Integer => SchemaType::Primitive {
3231 rust_type: self.integer_rust_type(details),
3232 serde_with: None,
3233 },
3234 OpenApiSchemaType::Number => SchemaType::Primitive {
3235 rust_type: self.type_mapper.number_format(format).rust_type,
3236 serde_with: None,
3237 },
3238 OpenApiSchemaType::Boolean => SchemaType::Primitive {
3239 rust_type: self.type_mapper.boolean().rust_type,
3240 serde_with: None,
3241 },
3242 OpenApiSchemaType::Array => {
3243 self.analyze_array_schema(schema, schema_name, dependencies)?
3244 }
3245 OpenApiSchemaType::Object => {
3246 if self.should_use_dynamic_json(schema) {
3247 self.untyped_value(self.untyped_context(""), UntypedReason::OpaqueObject)
3248 } else {
3249 self.analyze_object_schema(schema, dependencies)?
3250 }
3251 }
3252 OpenApiSchemaType::Null => SchemaType::Primitive {
3255 rust_type: self.type_mapper.null_unit().rust_type,
3256 serde_with: None,
3257 },
3258 })
3259 }
3260
3261 fn analyze_object_schema(
3262 &mut self,
3263 schema: &Schema,
3264 dependencies: &mut HashSet<String>,
3265 ) -> Result<SchemaType> {
3266 let details = schema.details();
3267 let properties = &details.properties;
3268 let required = details
3269 .required
3270 .as_ref()
3271 .map(|req| req.iter().cloned().collect::<HashSet<String>>())
3272 .unwrap_or_default();
3273
3274 let mut property_info = BTreeMap::new();
3275 let owner_name = self
3278 .current_schema_name
3279 .clone()
3280 .unwrap_or_else(|| "Inline".to_string());
3281
3282 if let Some(props) = properties {
3283 for (prop_name, prop_schema) in props {
3284 let prop_type = if let Schema::AnyOf { any_of, .. } = prop_schema {
3286 if Self::union_only_constrains_requiredness(any_of) {
3291 self.analyze_empty_union(prop_schema, dependencies)?
3292 } else if let Some(with_variants) = {
3293 let variant_owner =
3294 format!("{owner_name}{}", self.to_pascal_case(prop_name));
3295 self.with_schema_context(&variant_owner, |analyzer| {
3296 analyzer.analyze_object_with_variants(
3297 prop_schema,
3298 any_of,
3299 &variant_owner,
3300 dependencies,
3301 )
3302 })?
3303 } {
3304 with_variants
3305 } else if self.should_use_dynamic_json(prop_schema) {
3306 self.untyped_value(
3308 self.untyped_context(prop_name),
3309 UntypedReason::OpaqueObject,
3310 )
3311 } else if prop_schema.is_nullable_pattern()
3312 && let Some(non_null) = prop_schema.non_null_variant()
3313 {
3314 self.analyze_property_schema_with_context(
3322 non_null,
3323 Some(prop_name),
3324 dependencies,
3325 )?
3326 } else {
3327 let context_name = self
3330 .current_schema_name
3331 .clone()
3332 .unwrap_or_else(|| "Unknown".to_string());
3333
3334 let prop_pascal = self.to_pascal_case(prop_name);
3336 let preferred_union_name = format!("{context_name}{prop_pascal}");
3337 let union_type_name = self.allocate_inline_schema_name(
3338 &preferred_union_name,
3339 &format!("{preferred_union_name}Union2"),
3340 "property-anyof",
3341 prop_schema,
3342 );
3343
3344 let union_schema_type = self.analyze_anyof_union(
3346 any_of,
3347 prop_schema.discriminator(),
3348 dependencies,
3349 &union_type_name,
3350 )?;
3351
3352 self.resolved_cache.insert(
3354 union_type_name.clone(),
3355 AnalyzedSchema {
3356 name: union_type_name.clone(),
3357 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
3358 dependencies: schema_type_dependencies(&union_schema_type),
3359 schema_type: union_schema_type,
3360 nullable: false,
3361 description: prop_schema.details().description.clone(),
3362 default: None,
3363 },
3364 );
3365
3366 dependencies.insert(union_type_name.clone());
3368 SchemaType::Reference {
3369 target: union_type_name,
3370 }
3371 }
3372 } else if let Schema::OneOf {
3373 one_of,
3374 discriminator,
3375 ..
3376 } = prop_schema
3377 {
3378 if prop_schema.is_nullable_pattern()
3385 && let Some(non_null) = prop_schema.non_null_variant()
3386 {
3387 let unwrapped = self.analyze_property_schema_with_context(
3388 non_null,
3389 Some(prop_name),
3390 dependencies,
3391 )?;
3392 let owner_name = self
3393 .current_schema_name
3394 .clone()
3395 .unwrap_or_else(|| "Inline".to_string());
3396 let unwrapped = self.hoist_inline_property_type(
3397 &owner_name,
3398 prop_name,
3399 unwrapped,
3400 dependencies,
3401 );
3402 let prop_details = prop_schema.details();
3403 let prop_nullable = true;
3404 let prop_description = prop_details.description.clone();
3405 let prop_default = prop_details.default.clone();
3406 property_info.insert(
3407 prop_name.clone(),
3408 PropertyInfo {
3409 schema_type: unwrapped,
3410 nullable: prop_nullable,
3411 description: prop_description,
3412 default: prop_default,
3413 serde_attrs: Vec::new(),
3414 synthesized_required: false,
3415 constraints: PropertyConstraints::from_schema_details(prop_details),
3416 },
3417 );
3418 continue;
3419 }
3420
3421 let context_name = self
3423 .current_schema_name
3424 .clone()
3425 .unwrap_or_else(|| "Unknown".to_string());
3426 let prop_pascal = self.to_pascal_case(prop_name);
3427 let preferred_union_name = format!("{context_name}{prop_pascal}");
3428 let union_type_name = self.allocate_inline_schema_name(
3429 &preferred_union_name,
3430 &format!("{preferred_union_name}Union2"),
3431 "property-oneof",
3432 prop_schema,
3433 );
3434
3435 let union_schema_type = self.analyze_oneof_union(
3437 one_of,
3438 discriminator.as_ref(),
3439 &union_type_name,
3440 dependencies,
3441 InlineUnionKind::OneOf,
3442 None,
3443 )?;
3444
3445 self.resolved_cache.insert(
3447 union_type_name.clone(),
3448 AnalyzedSchema {
3449 name: union_type_name.clone(),
3450 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
3451 schema_type: union_schema_type,
3452 dependencies: HashSet::new(),
3453 nullable: false,
3454 description: prop_schema.details().description.clone(),
3455 default: None,
3456 },
3457 );
3458
3459 dependencies.insert(union_type_name.clone());
3461 SchemaType::Reference {
3462 target: union_type_name,
3463 }
3464 } else {
3465 self.analyze_property_schema_with_context(
3467 prop_schema,
3468 Some(prop_name),
3469 dependencies,
3470 )?
3471 };
3472
3473 let prop_type = self.hoist_inline_property_type(
3474 &owner_name,
3475 prop_name,
3476 prop_type,
3477 dependencies,
3478 );
3479
3480 let prop_details = prop_schema.details();
3481 let prop_nullable = self.schema_or_reference_is_nullable(prop_schema);
3483 let prop_description = prop_details.description.clone();
3484 let prop_default = prop_details.default.clone();
3485
3486 property_info.insert(
3487 prop_name.clone(),
3488 PropertyInfo {
3489 schema_type: prop_type,
3490 nullable: prop_nullable,
3491 description: prop_description,
3492 default: prop_default,
3493 serde_attrs: Vec::new(),
3494 synthesized_required: false,
3495 constraints: PropertyConstraints::from_schema_details(prop_details),
3496 },
3497 );
3498 }
3499 }
3500
3501 let typed_enabled = self
3509 .type_mapper
3510 .config()
3511 .shape
3512 .as_ref()
3513 .and_then(|s| s.additional_properties_typed)
3514 .unwrap_or(true);
3515
3516 let untyped_required_property = || PropertyInfo {
3517 schema_type: SchemaType::Untyped {
3518 shape: UntypedShape::Value,
3519 reason: UntypedReason::AnySchema,
3520 },
3521 nullable: false,
3522 description: None,
3523 default: None,
3524 serde_attrs: Vec::new(),
3525 synthesized_required: true,
3526 constraints: PropertyConstraints::default(),
3527 };
3528 let (mut additional_properties, required_additional_property, explicitly_forbidden) =
3529 match &details.additional_properties {
3530 Some(crate::openapi::AdditionalProperties::Boolean(true)) => (
3531 ObjectAdditionalProperties::Untyped,
3532 Some(untyped_required_property()),
3533 false,
3534 ),
3535 Some(crate::openapi::AdditionalProperties::Boolean(false)) => {
3536 (ObjectAdditionalProperties::Denied, None, true)
3537 }
3538 Some(crate::openapi::AdditionalProperties::Schema(value_schema))
3539 if typed_enabled =>
3540 {
3541 let analyzed = self.analyze_property_schema_with_context(
3542 value_schema,
3543 Some("AdditionalProperty"),
3544 dependencies,
3545 )?;
3546 let nullable_value =
3547 self.nullable_container_value(value_schema, analyzed.clone());
3548 let value_details = value_schema.details();
3549 let required_property = PropertyInfo {
3550 schema_type: analyzed.clone(),
3551 nullable: self.schema_or_reference_is_nullable(value_schema),
3552 description: value_details.description.clone(),
3553 default: None,
3556 serde_attrs: Vec::new(),
3557 synthesized_required: true,
3558 constraints: PropertyConstraints::from_schema_details(value_details),
3559 };
3560 (
3561 ObjectAdditionalProperties::Typed {
3562 value_type: Box::new(nullable_value),
3563 },
3564 Some(required_property),
3565 false,
3566 )
3567 }
3568 Some(crate::openapi::AdditionalProperties::Schema(_)) => (
3569 ObjectAdditionalProperties::Untyped,
3572 Some(untyped_required_property()),
3573 false,
3574 ),
3575 None if Self::object_shape_needs_additional_property_carrier(details) => (
3580 ObjectAdditionalProperties::Untyped,
3581 Some(untyped_required_property()),
3582 false,
3583 ),
3584 None => (
3585 ObjectAdditionalProperties::Closed,
3586 Some(untyped_required_property()),
3587 false,
3588 ),
3589 };
3590
3591 self.finalize_required_object_members(
3592 &mut property_info,
3593 &required,
3594 &mut additional_properties,
3595 required_additional_property,
3596 explicitly_forbidden,
3597 )?;
3598
3599 Ok(SchemaType::Object {
3600 properties: property_info,
3601 variant: None,
3602 required,
3603 additional_properties,
3604 })
3605 }
3606
3607 fn object_shape_needs_additional_property_carrier(
3613 details: &crate::openapi::SchemaDetails,
3614 ) -> bool {
3615 if details.min_properties.is_some() || details.max_properties.is_some() {
3616 return true;
3617 }
3618
3619 let declared = details.properties.as_ref();
3620 let has_undeclared_key = |value: &Value| {
3621 value.as_object().is_some_and(|object| {
3622 object
3623 .keys()
3624 .any(|key| declared.is_none_or(|properties| !properties.contains_key(key)))
3625 })
3626 };
3627 details.example.as_ref().is_some_and(has_undeclared_key)
3628 || details
3629 .examples
3630 .as_ref()
3631 .is_some_and(|examples| examples.iter().any(has_undeclared_key))
3632 }
3633
3634 fn finalize_required_object_members(
3639 &self,
3640 properties: &mut BTreeMap<String, PropertyInfo>,
3641 required: &HashSet<String>,
3642 additional_properties: &mut ObjectAdditionalProperties,
3643 required_additional_property: Option<PropertyInfo>,
3644 explicitly_forbidden: bool,
3645 ) -> Result<()> {
3646 let mut missing = required
3647 .iter()
3648 .filter(|name| !properties.contains_key(*name))
3649 .cloned()
3650 .collect::<Vec<_>>();
3651 missing.sort();
3652 if missing.is_empty() {
3653 return Ok(());
3654 }
3655
3656 if explicitly_forbidden {
3657 let owner = self
3658 .current_schema_name
3659 .as_deref()
3660 .unwrap_or("<inline object>");
3661 return Err(GeneratorError::InvalidSchema(format!(
3662 "object schema `{owner}` is unsatisfiable: required member(s) {} are not declared in properties while additionalProperties: false",
3663 missing
3664 .iter()
3665 .map(|name| format!("`{name}`"))
3666 .collect::<Vec<_>>()
3667 .join(", ")
3668 )));
3669 }
3670
3671 let required_additional_property = required_additional_property.ok_or_else(|| {
3672 GeneratorError::InvalidSchema(
3673 "undeclared required members have no additional-properties carrier".to_string(),
3674 )
3675 })?;
3676 for name in missing {
3677 properties.insert(name, required_additional_property.clone());
3678 }
3679
3680 if !additional_properties.is_open() {
3681 *additional_properties = ObjectAdditionalProperties::Untyped;
3682 }
3683 Ok(())
3684 }
3685
3686 fn build_typed_multi_union_variant(
3696 &mut self,
3697 member_type: OpenApiSchemaType,
3698 schema: &Schema,
3699 union_type_name: &str,
3700 dependencies: &mut HashSet<String>,
3701 ) -> Result<SchemaRef> {
3702 match member_type {
3703 OpenApiSchemaType::Array => {
3704 let preferred_array_type_name = format!("{union_type_name}Array");
3705 let array_type_name = self.allocate_inline_schema_name(
3706 &preferred_array_type_name,
3707 &format!("{preferred_array_type_name}Inline"),
3708 "typed-multi-array",
3709 schema,
3710 );
3711 let array_type =
3712 self.analyze_array_schema(schema, &array_type_name, dependencies)?;
3713 self.resolved_cache.insert(
3714 array_type_name.clone(),
3715 AnalyzedSchema {
3716 name: array_type_name.clone(),
3717 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3718 schema_type: array_type,
3719 dependencies: HashSet::new(),
3720 nullable: false,
3721 description: Some("Array variant in union".to_string()),
3722 default: None,
3723 },
3724 );
3725 dependencies.insert(array_type_name.clone());
3726 Ok(SchemaRef {
3727 target: array_type_name,
3728 nullable: false,
3729 })
3730 }
3731 OpenApiSchemaType::Object => {
3732 let preferred_object_type_name = format!("{union_type_name}Object");
3733 let object_type_name = self.allocate_inline_schema_name(
3734 &preferred_object_type_name,
3735 &format!("{preferred_object_type_name}Inline"),
3736 "typed-multi-object",
3737 schema,
3738 );
3739 if Self::object_shape_is_unconstrained(schema.details()) {
3747 let schema_type = self.untyped_value_map(
3748 self.untyped_context(&object_type_name),
3749 UntypedReason::OpaqueObject,
3750 );
3751 self.resolved_cache.insert(
3752 object_type_name.clone(),
3753 AnalyzedSchema {
3754 name: object_type_name.clone(),
3755 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3756 schema_type,
3757 dependencies: HashSet::new(),
3758 nullable: false,
3759 description: Some("Object variant in union".to_string()),
3760 default: None,
3761 },
3762 );
3763 dependencies.insert(object_type_name.clone());
3764 return Ok(SchemaRef {
3765 target: object_type_name,
3766 nullable: false,
3767 });
3768 }
3769 let object_type = self.add_allocated_object_schema(
3770 object_type_name.clone(),
3771 schema,
3772 dependencies,
3773 )?;
3774 let SchemaType::Reference { target } = object_type else {
3775 unreachable!("allocated object schemas always return a reference");
3776 };
3777 Ok(SchemaRef {
3778 target,
3779 nullable: false,
3780 })
3781 }
3782 _ => Ok(SchemaRef {
3783 target: self.openapi_type_to_rust_type(member_type, schema.details()),
3784 nullable: false,
3785 }),
3786 }
3787 }
3788
3789 fn analyze_property_schema_with_context(
3790 &mut self,
3791 schema: &Schema,
3792 property_name: Option<&str>,
3793 dependencies: &mut HashSet<String>,
3794 ) -> Result<SchemaType> {
3795 if let Schema::Bool(accepts_anything) = schema {
3797 let reason = if *accepts_anything {
3798 UntypedReason::AnySchema
3799 } else {
3800 UntypedReason::NeverMatches
3801 };
3802 return Ok(self.untyped_value(
3803 self.untyped_context(property_name.unwrap_or_default()),
3804 reason,
3805 ));
3806 }
3807
3808 if let Some(ref_str) = self.get_any_reference(schema) {
3809 let target_opt = if ref_str == "#" {
3810 Some(
3811 self.find_recursive_anchor_schema()
3812 .unwrap_or_else(|| "UnknownRecursive".to_string()),
3813 )
3814 } else {
3815 self.extract_schema_name(ref_str).map(|s| s.to_string())
3816 };
3817 match target_opt {
3818 Some(target) => {
3819 dependencies.insert(target.clone());
3820 return Ok(SchemaType::Reference { target });
3821 }
3822 None => {
3823 if let Some(resolved) = self.resolve_pointer_schema(ref_str, dependencies)? {
3828 return Ok(resolved);
3829 }
3830 eprintln!(
3831 "⚠️ unresolvable $ref `{}` — typing as serde_json::Value",
3832 ref_str
3833 );
3834 return Ok(self.untyped_value(
3835 format!("$ref {ref_str}"),
3836 UntypedReason::UnresolvedReference,
3837 ));
3838 }
3839 }
3840 }
3841
3842 if let Some(non_null_types) = schema.non_null_schema_types() {
3846 let context_name = self
3847 .current_schema_name
3848 .clone()
3849 .unwrap_or_else(|| "Unknown".to_string());
3850 let prop_pascal = property_name
3851 .map(|name| self.to_pascal_case(name))
3852 .unwrap_or_default();
3853 let preferred_union_name = format!("{context_name}{prop_pascal}");
3854 let union_type_name = self.allocate_inline_schema_name(
3855 &preferred_union_name,
3856 &format!("{preferred_union_name}Union2"),
3857 "property-typed-multi",
3858 schema,
3859 );
3860
3861 let details = schema.details();
3862 let mut variants = Vec::with_capacity(non_null_types.len());
3863 for t in non_null_types {
3864 variants.push(self.build_typed_multi_union_variant(
3865 t,
3866 schema,
3867 &union_type_name,
3868 dependencies,
3869 )?);
3870 }
3871
3872 self.resolved_cache.insert(
3873 union_type_name.clone(),
3874 AnalyzedSchema {
3875 name: union_type_name.clone(),
3876 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3877 schema_type: SchemaType::Union {
3878 variants,
3879 exclusive: false,
3880 },
3881 dependencies: HashSet::new(),
3882 nullable: false,
3883 description: details.description.clone(),
3884 default: None,
3885 },
3886 );
3887
3888 dependencies.insert(union_type_name.clone());
3889 return Ok(SchemaType::Reference {
3890 target: union_type_name,
3891 });
3892 }
3893
3894 if let Some(schema_type) = schema.schema_type() {
3895 match schema_type {
3896 OpenApiSchemaType::String => {
3897 if let Some(enum_values) = schema.details().string_enum_values() {
3899 let context_name = self
3902 .current_schema_name
3903 .clone()
3904 .unwrap_or_else(|| "Unknown".to_string());
3905
3906 let primary_name = if let Some(prop_name) = property_name {
3908 let prop_pascal = self.to_pascal_case(prop_name);
3910 format!("{context_name}{prop_pascal}")
3911 } else {
3912 let suffix = if !enum_values.is_empty() {
3915 let first_value = self.to_pascal_case(&enum_values[0]);
3916 format!("{first_value}Enum")
3917 } else {
3918 "StringEnum".to_string()
3919 };
3920 format!("{context_name}{suffix}")
3921 };
3922
3923 return Ok(self.hoist_inline_string_enum(
3924 schema,
3925 enum_values,
3926 primary_name,
3927 dependencies,
3928 ));
3929 } else {
3930 let mapped = self
3936 .type_mapper
3937 .string_format(schema.details().format.as_deref());
3938 return Ok(SchemaType::Primitive {
3939 rust_type: mapped.rust_type,
3940 serde_with: mapped.serde_with,
3941 });
3942 }
3943 }
3944 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
3945 let details = schema.details();
3946 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
3947 return Ok(SchemaType::Primitive {
3948 rust_type,
3949 serde_with: None,
3950 });
3951 }
3952 OpenApiSchemaType::Boolean => {
3953 return Ok(SchemaType::Primitive {
3954 rust_type: "bool".to_string(),
3955 serde_with: None,
3956 });
3957 }
3958 OpenApiSchemaType::Array => {
3959 let context_name = if let Some(prop_name) = property_name {
3961 let prop_pascal = self.to_pascal_case(prop_name);
3963 format!(
3964 "{}{}",
3965 self.current_schema_name.as_deref().unwrap_or("Unknown"),
3966 prop_pascal
3967 )
3968 } else {
3969 "ArrayItem".to_string()
3971 };
3972 return self.analyze_array_schema(schema, &context_name, dependencies);
3973 }
3974 OpenApiSchemaType::Object => {
3975 if self.should_use_dynamic_json(schema) {
3977 return Ok(self
3978 .untyped_value(self.untyped_context(""), UntypedReason::OpaqueObject));
3979 }
3980 let preferred_object_type_name = if let Some(prop_name) = property_name {
3982 let prop_pascal = self.to_pascal_case(prop_name);
3984 format!(
3985 "{}{}",
3986 self.current_schema_name.as_deref().unwrap_or("Unknown"),
3987 prop_pascal
3988 )
3989 } else {
3990 format!(
3992 "{}Object",
3993 self.current_schema_name.as_deref().unwrap_or("Unknown")
3994 )
3995 };
3996 let object_type_name = self.allocate_inline_schema_name(
3997 &preferred_object_type_name,
3998 &format!("{preferred_object_type_name}Inline"),
3999 "property-object",
4000 schema,
4001 );
4002
4003 return self.add_allocated_object_schema(
4004 object_type_name,
4005 schema,
4006 dependencies,
4007 );
4008 }
4009 OpenApiSchemaType::Null => {
4012 return Ok(SchemaType::Primitive {
4013 rust_type: self.type_mapper.null_unit().rust_type,
4014 serde_with: None,
4015 });
4016 }
4017 }
4018 }
4019
4020 if schema.is_nullable_pattern() {
4022 if let Some(non_null) = schema.non_null_variant() {
4023 return self.analyze_property_schema_with_context(
4024 non_null,
4025 property_name,
4026 dependencies,
4027 );
4028 }
4029 }
4030
4031 if self.should_use_dynamic_json(schema) {
4033 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::OpaqueObject));
4034 }
4035
4036 if let Schema::AllOf { all_of, .. } = schema {
4038 if let Some(property_name) = property_name {
4039 let owner = self
4040 .current_schema_name
4041 .clone()
4042 .unwrap_or_else(|| "Inline".to_string());
4043 let composition_name = format!("{owner}{}", self.to_pascal_case(property_name));
4044 return self.with_schema_context(&composition_name, |analyzer| {
4045 analyzer.analyze_allof_composition(schema, all_of, dependencies)
4046 });
4047 }
4048 return self.analyze_allof_composition(schema, all_of, dependencies);
4049 }
4050
4051 if let Some(variants) = schema.union_variants() {
4053 match variants.len().cmp(&1) {
4054 std::cmp::Ordering::Equal => {
4055 return self.analyze_property_schema_with_context(
4057 &variants[0],
4058 property_name,
4059 dependencies,
4060 );
4061 }
4062 std::cmp::Ordering::Greater => {
4063 let union_name = if let Some(prop_name) = property_name {
4066 let prop_pascal = self.to_pascal_case(prop_name);
4068 format!(
4069 "{}{}",
4070 self.current_schema_name.as_deref().unwrap_or(""),
4071 prop_pascal
4072 )
4073 } else {
4074 "UnionType".to_string()
4075 };
4076
4077 if let Schema::OneOf {
4079 one_of,
4080 discriminator,
4081 ..
4082 } = schema
4083 {
4084 let union_name = self.allocate_inline_schema_name(
4085 &union_name,
4086 &format!("{union_name}Union2"),
4087 "fallback-property-oneof",
4088 schema,
4089 );
4090 let oneof_result = self.analyze_oneof_union(
4092 one_of,
4093 discriminator.as_ref(),
4094 &union_name,
4095 dependencies,
4096 InlineUnionKind::OneOf,
4097 None,
4098 )?;
4099
4100 if let SchemaType::Union { .. } = &oneof_result {
4102 self.resolved_cache.insert(
4104 union_name.clone(),
4105 AnalyzedSchema {
4106 name: union_name.clone(),
4107 original: serde_json::to_value(schema).unwrap_or(Value::Null),
4108 schema_type: oneof_result.clone(),
4109 dependencies: dependencies.clone(),
4110 nullable: false,
4111 description: schema.details().description.clone(),
4112 default: None,
4113 },
4114 );
4115
4116 dependencies.insert(union_name.clone());
4118 return Ok(SchemaType::Reference { target: union_name });
4119 }
4120
4121 return Ok(oneof_result);
4122 } else if let Schema::AnyOf {
4123 any_of,
4124 discriminator,
4125 ..
4126 } = schema
4127 {
4128 let union_analysis = self.analyze_anyof_union(
4130 any_of,
4131 discriminator.as_ref(),
4132 dependencies,
4133 &union_name,
4134 )?;
4135 return Ok(union_analysis);
4136 } else {
4137 let mut union_variants = Vec::new();
4140 for variant in variants {
4141 if let Some(ref_str) = variant.reference() {
4142 if let Some(target) = self.extract_schema_name(ref_str) {
4143 dependencies.insert(target.to_string());
4144 union_variants.push(SchemaRef {
4145 target: target.to_string(),
4146 nullable: false,
4147 });
4148 }
4149 }
4150 }
4151 return Ok(SchemaType::Union {
4152 variants: union_variants,
4153 exclusive: false,
4154 });
4155 }
4156 }
4157 std::cmp::Ordering::Less => {}
4158 }
4159 }
4160
4161 if let Some(inferred_type) = schema.inferred_type() {
4163 match inferred_type {
4164 OpenApiSchemaType::Object => {
4165 if self.should_use_dynamic_json(schema) {
4167 return Ok(self
4168 .untyped_value(self.untyped_context(""), UntypedReason::OpaqueObject));
4169 }
4170 let owner = self
4171 .current_schema_name
4172 .clone()
4173 .unwrap_or_else(|| "Unknown".to_string());
4174 let preferred_name = property_name.map_or_else(
4175 || format!("{owner}Object"),
4176 |property_name| format!("{owner}{}", self.to_pascal_case(property_name)),
4177 );
4178 let object_type_name = self.allocate_inline_schema_name(
4179 &preferred_name,
4180 &format!("{preferred_name}Inline"),
4181 "property-object",
4182 schema,
4183 );
4184 return self.add_allocated_object_schema(
4185 object_type_name,
4186 schema,
4187 dependencies,
4188 );
4189 }
4190 OpenApiSchemaType::Array => {
4191 let context_name = if let Some(prop_name) = property_name {
4192 let prop_pascal = self.to_pascal_case(prop_name);
4194 format!(
4195 "{}{}",
4196 self.current_schema_name.as_deref().unwrap_or("Unknown"),
4197 prop_pascal
4198 )
4199 } else {
4200 "ArrayItem".to_string()
4202 };
4203 return self.analyze_array_schema(schema, &context_name, dependencies);
4204 }
4205 OpenApiSchemaType::String => {
4206 if let Some(enum_values) = schema.details().string_enum_values() {
4207 return Ok(SchemaType::StringEnum {
4208 values: enum_values,
4209 });
4210 } else {
4211 return Ok(SchemaType::Primitive {
4212 rust_type: "String".to_string(),
4213 serde_with: None,
4214 });
4215 }
4216 }
4217 _ => {
4218 let rust_type = self.openapi_type_to_rust_type(inferred_type, schema.details());
4220 return Ok(SchemaType::Primitive {
4221 rust_type,
4222 serde_with: None,
4223 });
4224 }
4225 }
4226 }
4227
4228 Ok(self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema))
4229 }
4230
4231 fn analyze_allof_composition(
4232 &mut self,
4233 owner_schema: &Schema,
4234 all_of_schemas: &[Schema],
4235 dependencies: &mut HashSet<String>,
4236 ) -> Result<SchemaType> {
4237 if let Some(carrier) = Self::single_non_object_allof_carrier(all_of_schemas) {
4242 return self.analyze_property_schema_with_context(carrier, None, dependencies);
4243 }
4244
4245 let referenced_targets = all_of_schemas
4250 .iter()
4251 .filter_map(|schema| schema.reference())
4252 .filter_map(|reference| self.extract_schema_name(reference))
4253 .collect::<Vec<_>>();
4254 let only_reference_and_annotations = all_of_schemas
4255 .iter()
4256 .all(|schema| schema.reference().is_some() || schema_is_annotation_only(schema));
4257 if referenced_targets.len() == 1 && only_reference_and_annotations {
4258 let target = referenced_targets[0];
4259 dependencies.insert(target.to_string());
4260 return Ok(SchemaType::Reference {
4261 target: target.to_string(),
4262 });
4263 }
4264
4265 if let [only] = all_of_schemas
4269 && !matches!(
4270 only.schema_type(),
4271 Some(OpenApiSchemaType::Object) | Some(OpenApiSchemaType::Null)
4272 )
4273 && only.details().properties.is_none()
4274 {
4275 return self.analyze_property_schema_with_context(only, None, dependencies);
4276 }
4277
4278 let mut merged_properties = BTreeMap::new();
4280 let mut merged_required = HashSet::new();
4281 let mut merged_variant = None;
4282 let mut descriptions = Vec::new();
4283
4284 let current_context = self.current_schema_name.clone();
4286 let owner_name = current_context.as_deref().unwrap_or("InlineComposition");
4287
4288 self.merge_schema_into_properties(
4292 owner_schema,
4293 &mut merged_properties,
4294 &mut merged_required,
4295 dependencies,
4296 )?;
4297
4298 for (member_index, schema) in all_of_schemas.iter().enumerate() {
4299 match schema {
4300 Schema::Reference { reference, .. } => {
4301 let (analyzed_type, analyzed_name, raw_target) =
4302 if let Some(target) = self.extract_schema_name(reference) {
4303 dependencies.insert(target.to_string());
4304 let analyzed_ref = self.analyze_schema(target)?;
4305 (
4306 Some(analyzed_ref.schema_type),
4307 Some(target.to_string()),
4308 self.schemas.get(target).cloned(),
4309 )
4310 } else {
4311 (
4312 self.resolve_pointer_schema(reference, dependencies)?,
4313 None,
4314 self.reference_target_schema(reference),
4315 )
4316 };
4317
4318 let merged = if let Some(analyzed_type) = analyzed_type {
4319 self.merge_analyzed_object_properties(
4320 &analyzed_type,
4321 analyzed_name.as_deref(),
4322 &mut merged_properties,
4323 &mut merged_required,
4324 &mut merged_variant,
4325 owner_name,
4326 )?
4327 } else {
4328 false
4329 };
4330 if !merged && let Some(raw_target) = raw_target {
4331 self.merge_schema_into_properties(
4332 &raw_target,
4333 &mut merged_properties,
4334 &mut merged_required,
4335 dependencies,
4336 )?;
4337 }
4338 }
4339 Schema::AnyOf { any_of, .. }
4340 if Self::union_only_constrains_requiredness(any_of) => {}
4341 Schema::OneOf { one_of, .. }
4342 if Self::union_only_constrains_requiredness(one_of) => {}
4343 Schema::AnyOf { .. } | Schema::OneOf { .. } => {
4344 let preferred_name = format!("{owner_name}AllOfVariant{}", member_index + 1);
4345 let variant_name =
4346 self.add_inline_schema(&preferred_name, schema, dependencies)?;
4347 dependencies.insert(variant_name.clone());
4348 let analyzed_type = self
4349 .resolved_cache
4350 .get(&variant_name)
4351 .map(|analyzed| analyzed.schema_type.clone())
4352 .ok_or_else(|| {
4353 GeneratorError::InvalidSchema(format!(
4354 "allOf union member `{variant_name}` was not analyzed"
4355 ))
4356 })?;
4357 if !self.merge_analyzed_object_properties(
4358 &analyzed_type,
4359 Some(&variant_name),
4360 &mut merged_properties,
4361 &mut merged_required,
4362 &mut merged_variant,
4363 owner_name,
4364 )? {
4365 return Ok(self.untyped_value(
4366 self.untyped_context(""),
4367 UntypedReason::UnrepresentableComposition,
4368 ));
4369 }
4370 }
4371 Schema::Typed {
4372 schema_type: OpenApiSchemaType::Object,
4373 ..
4374 }
4375 | Schema::Untyped { .. } => {
4376 let saved_context = self.current_schema_name.clone();
4378 self.current_schema_name = current_context.clone();
4379
4380 self.merge_schema_into_properties(
4382 schema,
4383 &mut merged_properties,
4384 &mut merged_required,
4385 dependencies,
4386 )?;
4387
4388 self.current_schema_name = saved_context;
4390 }
4391 _ => {
4392 self.merge_schema_into_properties(
4395 schema,
4396 &mut merged_properties,
4397 &mut merged_required,
4398 dependencies,
4399 )?;
4400 }
4401 }
4402
4403 if let Some(desc) = &schema.details().description {
4405 descriptions.push(desc.clone());
4406 }
4407 }
4408
4409 if !merged_properties.is_empty() || !merged_required.is_empty() || merged_variant.is_some()
4414 {
4415 let mut additional_properties = if merged_properties
4416 .values()
4417 .any(|property| property.synthesized_required)
4418 {
4419 ObjectAdditionalProperties::Untyped
4420 } else {
4421 ObjectAdditionalProperties::Closed
4422 };
4423 self.finalize_required_object_members(
4424 &mut merged_properties,
4425 &merged_required,
4426 &mut additional_properties,
4427 Some(PropertyInfo {
4428 schema_type: SchemaType::Untyped {
4429 shape: UntypedShape::Value,
4430 reason: UntypedReason::AnySchema,
4431 },
4432 nullable: false,
4433 description: None,
4434 default: None,
4435 serde_attrs: Vec::new(),
4436 synthesized_required: true,
4437 constraints: PropertyConstraints::default(),
4438 }),
4439 false,
4440 )?;
4441 Ok(SchemaType::Object {
4442 properties: merged_properties,
4443 required: merged_required,
4444 additional_properties,
4445 variant: merged_variant,
4446 })
4447 } else {
4448 let schemas = all_of_schemas
4449 .iter()
4450 .filter_map(|schema| {
4451 let reference = schema.reference()?;
4452 let target = self.extract_schema_name(reference)?;
4453 dependencies.insert(target.to_string());
4454 Some(SchemaRef {
4455 target: target.to_string(),
4456 nullable: false,
4457 })
4458 })
4459 .collect::<Vec<_>>();
4460 let references_are_objects = all_of_schemas
4466 .iter()
4467 .filter(|schema| schema.reference().is_some())
4468 .all(|schema| self.branch_resolves_to_object(schema));
4469 if schemas.is_empty() || !references_are_objects {
4470 Ok(self.untyped_value(
4471 self.untyped_context(""),
4472 UntypedReason::UnrepresentableComposition,
4473 ))
4474 } else {
4475 Ok(SchemaType::Composition { schemas })
4476 }
4477 }
4478 }
4479
4480 fn single_non_object_allof_carrier(all_of_schemas: &[Schema]) -> Option<&Schema> {
4485 let mut meaningful = all_of_schemas
4486 .iter()
4487 .filter(|schema| !schema_is_annotation_only(schema));
4488 let carrier = meaningful.next()?;
4489 if meaningful.next().is_some() {
4490 return None;
4491 }
4492
4493 match carrier {
4494 Schema::Typed {
4495 schema_type:
4496 OpenApiSchemaType::String
4497 | OpenApiSchemaType::Integer
4498 | OpenApiSchemaType::Number
4499 | OpenApiSchemaType::Boolean
4500 | OpenApiSchemaType::Array,
4501 ..
4502 } => Some(carrier),
4503 Schema::TypedMulti { schema_types, .. } => {
4504 let mut non_null = schema_types
4505 .iter()
4506 .filter(|schema_type| **schema_type != OpenApiSchemaType::Null);
4507 let only = non_null.next()?;
4508 (non_null.next().is_none()
4509 && matches!(
4510 only,
4511 OpenApiSchemaType::String
4512 | OpenApiSchemaType::Integer
4513 | OpenApiSchemaType::Number
4514 | OpenApiSchemaType::Boolean
4515 | OpenApiSchemaType::Array
4516 ))
4517 .then_some(carrier)
4518 }
4519 _ => None,
4520 }
4521 }
4522
4523 fn merge_analyzed_object_properties(
4528 &mut self,
4529 schema_type: &SchemaType,
4530 named_target: Option<&str>,
4531 merged_properties: &mut BTreeMap<String, PropertyInfo>,
4532 merged_required: &mut HashSet<String>,
4533 merged_variant: &mut Option<SchemaRef>,
4534 owner_name: &str,
4535 ) -> Result<bool> {
4536 let mut current = schema_type.clone();
4537 let mut visited = HashSet::new();
4538 let mut variant_target = named_target.map(str::to_string);
4539 loop {
4540 match current {
4541 SchemaType::Object {
4542 properties,
4543 required,
4544 variant,
4545 ..
4546 } => {
4547 for (name, property) in properties {
4548 let keep_declared_sibling =
4549 merged_properties.get(&name).is_some_and(|existing| {
4550 !existing.synthesized_required && property.synthesized_required
4551 });
4552 if !keep_declared_sibling {
4553 merged_properties.insert(name, property);
4554 }
4555 }
4556 merged_required.extend(required);
4557 if let Some(variant) = variant {
4558 Self::merge_allof_variant(merged_variant, variant, owner_name)?;
4559 }
4560 return Ok(true);
4561 }
4562 SchemaType::Reference { target } => {
4563 if !visited.insert(target.clone()) {
4564 return Ok(false);
4565 }
4566 if variant_target.is_none() {
4567 variant_target = Some(target.clone());
4568 }
4569 current = if let Some(analyzed) = self.resolved_cache.get(&target) {
4570 analyzed.schema_type.clone()
4571 } else if self.schemas.contains_key(&target) {
4572 self.analyze_schema(&target)?.schema_type
4573 } else {
4574 return Ok(false);
4575 };
4576 }
4577 SchemaType::Union { .. } | SchemaType::DiscriminatedUnion { .. } => {
4578 let Some(target) = variant_target else {
4579 return Ok(false);
4580 };
4581 Self::merge_allof_variant(
4582 merged_variant,
4583 SchemaRef {
4584 target,
4585 nullable: false,
4586 },
4587 owner_name,
4588 )?;
4589 return Ok(true);
4590 }
4591 _ => return Ok(false),
4592 }
4593 }
4594 }
4595
4596 fn merge_allof_variant(
4597 merged_variant: &mut Option<SchemaRef>,
4598 candidate: SchemaRef,
4599 owner_name: &str,
4600 ) -> Result<()> {
4601 match merged_variant {
4602 Some(existing) if existing.target == candidate.target => Ok(()),
4603 Some(existing) => Err(GeneratorError::InvalidSchema(format!(
4604 "allOf object `{owner_name}` intersects multiple union members (`{}` and `{}`), which cannot be represented by one flattened variant",
4605 existing.target, candidate.target
4606 ))),
4607 None => {
4608 *merged_variant = Some(candidate);
4609 Ok(())
4610 }
4611 }
4612 }
4613
4614 fn merge_schema_into_properties(
4615 &mut self,
4616 schema: &Schema,
4617 merged_properties: &mut BTreeMap<String, PropertyInfo>,
4618 merged_required: &mut HashSet<String>,
4619 dependencies: &mut HashSet<String>,
4620 ) -> Result<()> {
4621 let details = schema.details();
4622
4623 if let Some(properties) = &details.properties {
4625 for (prop_name, prop_schema) in properties {
4626 let prop_type = self.analyze_property_schema_with_context(
4627 prop_schema,
4628 Some(prop_name),
4629 dependencies,
4630 )?;
4631 let owner_name = self
4632 .current_schema_name
4633 .clone()
4634 .unwrap_or_else(|| "Inline".to_string());
4635 let prop_type = self.hoist_inline_property_type(
4636 &owner_name,
4637 prop_name,
4638 prop_type,
4639 dependencies,
4640 );
4641 let prop_details = prop_schema.details();
4642
4643 let nullable = self.schema_or_reference_is_nullable(prop_schema);
4650 merged_properties.insert(
4651 prop_name.clone(),
4652 PropertyInfo {
4653 schema_type: prop_type,
4654 nullable,
4655 description: prop_details.description.clone(),
4656 default: prop_details.default.clone(),
4657 serde_attrs: Vec::new(),
4658 synthesized_required: false,
4659 constraints: PropertyConstraints::from_schema_details(prop_details),
4660 },
4661 );
4662 }
4663 }
4664
4665 if let Some(required) = &details.required {
4667 for field in required {
4668 merged_required.insert(field.clone());
4669 }
4670 }
4671
4672 Ok(())
4673 }
4674
4675 fn analyze_oneof_union(
4676 &mut self,
4677 one_of_schemas: &[Schema],
4678 discriminator: Option<&crate::openapi::Discriminator>,
4679 parent_name: &str,
4680 dependencies: &mut HashSet<String>,
4681 union_kind: InlineUnionKind,
4682 source_indices: Option<&[usize]>,
4683 ) -> Result<SchemaType> {
4684 let default_indices = (0..one_of_schemas.len()).collect::<Vec<_>>();
4685 let source_indices = source_indices
4686 .filter(|indices| indices.len() == one_of_schemas.len())
4687 .unwrap_or(&default_indices);
4688
4689 let expanded_branches;
4691 let one_of_schemas = match self.expand_pointer_branches(one_of_schemas) {
4692 Some(expanded) => {
4693 expanded_branches = expanded;
4694 expanded_branches.as_slice()
4695 }
4696 None => one_of_schemas,
4697 };
4698
4699 let boolean_resolved;
4701 let boolean_resolved_indices;
4702 let one_of_schemas = match Self::resolve_boolean_branches(one_of_schemas) {
4703 Ok(resolved) => {
4704 boolean_resolved_indices = one_of_schemas
4705 .iter()
4706 .zip(source_indices.iter().copied())
4707 .filter_map(|(branch, index)| {
4708 (!matches!(branch, Schema::Bool(false))).then_some(index)
4709 })
4710 .collect::<Vec<_>>();
4711 boolean_resolved = resolved;
4712 boolean_resolved.as_slice()
4713 }
4714 Err(()) => {
4715 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema));
4716 }
4717 };
4718 let source_indices = boolean_resolved_indices.as_slice();
4719 if one_of_schemas.is_empty() {
4720 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::NeverMatches));
4721 }
4722
4723 if let [only] = one_of_schemas {
4728 return self
4729 .analyze_schema_value(only, parent_name)
4730 .map(|analyzed| analyzed.schema_type);
4731 }
4732 if let Some(shared) = self.shared_branch_type(one_of_schemas) {
4733 return Ok(shared);
4734 }
4735
4736 if one_of_schemas.len() == 2 {
4739 let null_count = one_of_schemas
4740 .iter()
4741 .filter(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
4742 .count();
4743 if null_count == 1 {
4744 if let Some(non_null) = one_of_schemas
4745 .iter()
4746 .find(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
4747 {
4748 return self
4749 .analyze_schema_value(non_null, parent_name)
4750 .map(|a| a.schema_type);
4751 }
4752 }
4753 }
4754
4755 if discriminator.is_none()
4759 || one_of_schemas
4760 .iter()
4761 .any(|schema| self.schema_or_reference_is_nullable(schema))
4762 {
4763 return self.analyze_untagged_oneof_union(
4765 one_of_schemas,
4766 parent_name,
4767 dependencies,
4768 union_kind,
4769 source_indices,
4770 None,
4771 );
4772 }
4773
4774 if one_of_schemas
4780 .iter()
4781 .any(|s| !self.branch_resolves_to_object(s))
4782 {
4783 return self.analyze_untagged_oneof_union(
4784 one_of_schemas,
4785 parent_name,
4786 dependencies,
4787 union_kind,
4788 source_indices,
4789 discriminator,
4790 );
4791 }
4792
4793 let discriminator_field = discriminator
4795 .ok_or_else(|| {
4796 GeneratorError::InvalidDiscriminator(
4797 "expected discriminator after guard check".to_string(),
4798 )
4799 })?
4800 .property_name
4801 .clone();
4802
4803 if one_of_schemas.iter().any(|branch| {
4807 self.extract_discriminator_value_domain_for_field(branch, &discriminator_field)
4808 .is_some_and(|values| values.is_empty())
4809 }) {
4810 eprintln!(
4811 "⚠️ discriminated union `{parent_name}` has a branch with contradictory `{discriminator_field}` constraints; using structural union fallback"
4812 );
4813 return self.analyze_untagged_oneof_union(
4814 one_of_schemas,
4815 parent_name,
4816 dependencies,
4817 union_kind,
4818 source_indices,
4819 discriminator,
4820 );
4821 }
4822
4823 let mut variants = Vec::new();
4824 let mut used_variant_names = std::collections::HashSet::new();
4825
4826 for (variant_schema, original_index) in
4827 one_of_schemas.iter().zip(source_indices.iter().copied())
4828 {
4829 let ref_info = if let Some(ref_str) = variant_schema.reference() {
4831 Some((ref_str, false))
4832 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
4833 Some((recursive_ref, true))
4834 } else if let Schema::AllOf { all_of, .. } = variant_schema {
4835 if all_of.len() == 1 {
4837 if let Some(ref_str) = all_of[0].reference() {
4838 Some((ref_str, false))
4839 } else {
4840 all_of[0]
4841 .recursive_reference()
4842 .map(|recursive_ref| (recursive_ref, true))
4843 }
4844 } else {
4845 None
4846 }
4847 } else {
4848 None
4849 };
4850
4851 if let Some((ref_str, is_recursive)) = ref_info {
4852 let schema_name = if is_recursive && ref_str == "#" {
4853 self.find_recursive_anchor_schema()
4855 .or_else(|| self.current_schema_name.clone())
4856 .unwrap_or_else(|| "CompoundFilter".to_string())
4857 } else {
4858 self.extract_schema_name(ref_str)
4859 .map(|s| s.to_string())
4860 .unwrap_or_else(|| "UnknownRef".to_string())
4861 };
4862
4863 if !schema_name.is_empty() {
4864 dependencies.insert(schema_name.clone());
4865
4866 let mut discriminator_values = Vec::new();
4870 let allowed_domain = self.schemas.get(&schema_name).and_then(|ref_schema| {
4871 self.extract_discriminator_value_domain_for_field(
4872 ref_schema,
4873 &discriminator_field,
4874 )
4875 });
4876 if let Some(mappings) = discriminator.and_then(|disc| disc.mapping.as_ref()) {
4877 for (key, target_ref) in mappings {
4878 if target_ref == ref_str
4879 || self.extract_schema_name(target_ref)
4880 == Some(schema_name.as_str())
4881 {
4882 if allowed_domain
4883 .as_ref()
4884 .is_some_and(|allowed| !allowed.contains(key))
4885 {
4886 let allowed = allowed_domain
4887 .as_ref()
4888 .map(|values| values.join("`, `"))
4889 .unwrap_or_default();
4890 eprintln!(
4891 "⚠️ discriminator mapping conflict in union `{parent_name}`: key `{key}` targets `{schema_name}` but branch allows `{allowed}`; ignoring mapping key"
4892 );
4893 } else {
4894 Self::push_unique_string(&mut discriminator_values, key);
4895 }
4896 }
4897 }
4898 }
4899 if let Some(allowed_domain) = allowed_domain {
4900 for value in allowed_domain {
4901 Self::push_unique_string(&mut discriminator_values, &value);
4902 }
4903 }
4904 if discriminator_values.is_empty() {
4905 discriminator_values
4906 .push(self.generate_discriminator_value_from_name(&schema_name));
4907 }
4908 let discriminator_value = discriminator_values[0].clone();
4909 let (discriminator_field_declared, discriminator_field_required) = self
4910 .schemas
4911 .get(&schema_name)
4912 .map(|schema| {
4913 self.discriminator_property_presence(schema, &discriminator_field)
4914 })
4915 .unwrap_or((false, false));
4916
4917 let base_name = self.to_rust_variant_name(&schema_name);
4919 let rust_name =
4920 self.ensure_unique_variant_name(base_name, &mut used_variant_names);
4921
4922 let final_discriminator_value = discriminator_value;
4924
4925 variants.push(UnionVariant {
4926 rust_name,
4927 type_name: schema_name,
4928 discriminator_value: final_discriminator_value,
4929 preferred_discriminator_values: discriminator_values.clone(),
4930 discriminator_values,
4931 discriminator_field_declared,
4932 discriminator_field_required,
4933 schema_ref: ref_str.to_string(),
4934 });
4935 }
4936 } else {
4937 let variant_index = original_index;
4939 let inline_type_name =
4940 self.generate_inline_type_name(variant_schema, variant_index);
4941
4942 let mut discriminator_values = Vec::new();
4945 let allowed_domain = self.extract_discriminator_value_domain_for_field(
4946 variant_schema,
4947 &discriminator_field,
4948 );
4949 if let Some(mappings) = discriminator.and_then(|disc| disc.mapping.as_ref()) {
4950 for (key, target_ref) in mappings {
4951 if target_ref.contains(&format!("variant_{variant_index}")) {
4952 if allowed_domain
4953 .as_ref()
4954 .is_some_and(|allowed| !allowed.contains(key))
4955 {
4956 let allowed = allowed_domain
4957 .as_ref()
4958 .map(|values| values.join("`, `"))
4959 .unwrap_or_default();
4960 eprintln!(
4961 "⚠️ discriminator mapping conflict in union `{parent_name}`: key `{key}` targets `{inline_type_name}` but branch allows `{allowed}`; ignoring mapping key"
4962 );
4963 } else {
4964 Self::push_unique_string(&mut discriminator_values, key);
4965 }
4966 }
4967 }
4968 }
4969 if let Some(allowed_domain) = allowed_domain {
4970 for value in allowed_domain {
4971 Self::push_unique_string(&mut discriminator_values, &value);
4972 }
4973 }
4974 if discriminator_values.is_empty() {
4975 discriminator_values.push(format!("variant_{variant_index}"));
4976 }
4977 let discriminator_value = discriminator_values[0].clone();
4978 let (discriminator_field_declared, discriminator_field_required) =
4979 self.discriminator_property_presence(variant_schema, &discriminator_field);
4980
4981 let base_name = if discriminator_value.starts_with("variant_") {
4983 format!("Variant{variant_index}")
4984 } else {
4985 let clean_name = self.discriminator_to_variant_name(&discriminator_value);
4987 self.to_rust_variant_name(&clean_name)
4988 };
4989 let rust_name = self.ensure_unique_variant_name(base_name, &mut used_variant_names);
4990
4991 let final_discriminator_value = discriminator_value;
4993
4994 let inline_type_name = self.add_inline_union_branch_schema(
4997 &inline_type_name,
4998 variant_schema,
4999 dependencies,
5000 parent_name,
5001 union_kind,
5002 original_index,
5003 Some(&final_discriminator_value),
5004 )?;
5005
5006 variants.push(UnionVariant {
5007 rust_name,
5008 type_name: inline_type_name.clone(),
5009 discriminator_value: final_discriminator_value,
5010 preferred_discriminator_values: discriminator_values.clone(),
5011 discriminator_values,
5012 discriminator_field_declared,
5013 discriminator_field_required,
5014 schema_ref: format!("inline_{variant_index}"),
5015 });
5016 }
5017 }
5018
5019 self.disambiguate_shared_discriminator_values(&mut variants, discriminator);
5020
5021 if variants.is_empty() {
5022 let mut union_variants = Vec::new();
5025
5026 for (variant_schema, original_index) in
5027 one_of_schemas.iter().zip(source_indices.iter().copied())
5028 {
5029 if let Some(ref_str) = variant_schema.reference() {
5031 if let Some(schema_name) = self.extract_schema_name(ref_str) {
5032 dependencies.insert(schema_name.to_string());
5033 union_variants.push(SchemaRef {
5034 target: schema_name.to_string(),
5035 nullable: false,
5036 });
5037 }
5038 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
5039 let schema_name = if recursive_ref == "#" {
5040 self.find_recursive_anchor_schema()
5042 .or_else(|| self.current_schema_name.clone())
5043 .unwrap_or_else(|| "CompoundFilter".to_string())
5044 } else {
5045 self.extract_schema_name(recursive_ref)
5046 .map(|s| s.to_string())
5047 .unwrap_or_else(|| "RecursiveType".to_string())
5048 };
5049 dependencies.insert(schema_name.clone());
5050 union_variants.push(SchemaRef {
5051 target: schema_name,
5052 nullable: false,
5053 });
5054 } else {
5055 let branch_discriminator = self.inline_union_branch_discriminator_value(
5056 variant_schema,
5057 discriminator,
5058 original_index,
5059 );
5060 let inline_name = self.generate_context_aware_name(
5062 parent_name,
5063 "InlineVariant",
5064 original_index,
5065 Some(variant_schema),
5066 );
5067 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
5068 let variant_type = analyzed.schema_type;
5069
5070 for dep in &analyzed.dependencies {
5072 dependencies.insert(dep.clone());
5073 }
5074
5075 match &variant_type {
5076 SchemaType::Primitive { rust_type, .. } => {
5078 union_variants.push(SchemaRef {
5079 target: rust_type.clone(),
5080 nullable: false,
5081 });
5082 }
5083 SchemaType::Array { item_type } => {
5085 match item_type.as_ref() {
5086 SchemaType::Primitive { rust_type, .. } => {
5087 let type_name = format!("Vec<{rust_type}>");
5088 union_variants.push(SchemaRef {
5089 target: type_name,
5090 nullable: false,
5091 });
5092 }
5093 SchemaType::Reference { target } => {
5094 let type_name = format!("Vec<{target}>");
5095 union_variants.push(SchemaRef {
5096 target: type_name,
5097 nullable: false,
5098 });
5099 }
5100 _ => {
5101 let inline_type_name = self.generate_context_aware_name(
5103 parent_name,
5104 "Variant",
5105 original_index,
5106 None,
5107 );
5108 let inline_type_name = self.add_inline_union_branch_schema(
5109 &inline_type_name,
5110 variant_schema,
5111 dependencies,
5112 parent_name,
5113 union_kind,
5114 original_index,
5115 branch_discriminator.as_deref(),
5116 )?;
5117 union_variants.push(SchemaRef {
5118 target: inline_type_name,
5119 nullable: false,
5120 });
5121 }
5122 }
5123 }
5124 SchemaType::Reference { target } => {
5126 union_variants.push(SchemaRef {
5127 target: target.clone(),
5128 nullable: false,
5129 });
5130 }
5131 _ => {
5133 let inline_type_name =
5134 format!("{}Variant{}", parent_name, original_index + 1);
5135 let inline_type_name = self.add_inline_union_branch_schema(
5136 &inline_type_name,
5137 variant_schema,
5138 dependencies,
5139 parent_name,
5140 union_kind,
5141 original_index,
5142 branch_discriminator.as_deref(),
5143 )?;
5144 union_variants.push(SchemaRef {
5145 target: inline_type_name,
5146 nullable: false,
5147 });
5148 }
5149 }
5150 }
5151 }
5152
5153 if !union_variants.is_empty() {
5154 return Ok(SchemaType::Union {
5155 variants: union_variants,
5156 exclusive: matches!(union_kind, InlineUnionKind::OneOf),
5157 });
5158 }
5159
5160 return Ok(self.untyped_value(
5162 self.untyped_context(""),
5163 UntypedReason::UnrepresentableUnion,
5164 ));
5165 }
5166
5167 Ok(SchemaType::DiscriminatedUnion {
5168 discriminator_field,
5169 variants,
5170 exclusive: matches!(union_kind, InlineUnionKind::OneOf),
5171 })
5172 }
5173
5174 fn analyze_untagged_oneof_union(
5175 &mut self,
5176 one_of_schemas: &[Schema],
5177 parent_name: &str,
5178 dependencies: &mut HashSet<String>,
5179 union_kind: InlineUnionKind,
5180 source_indices: &[usize],
5181 discriminator: Option<&Discriminator>,
5182 ) -> Result<SchemaType> {
5183 let filtered: Vec<(usize, &Schema)> = one_of_schemas
5188 .iter()
5189 .zip(source_indices.iter().copied())
5190 .filter_map(|(schema, original_index)| {
5191 (!schema.is_explicit_null_only()).then_some((original_index, schema))
5192 })
5193 .collect();
5194
5195 if filtered.len() == 1 {
5197 return self
5198 .analyze_schema_value(filtered[0].1, parent_name)
5199 .map(|a| a.schema_type);
5200 }
5201
5202 let exclusive_object_union = matches!(union_kind, InlineUnionKind::OneOf)
5208 && filtered
5209 .iter()
5210 .all(|(_, schema)| self.branch_resolves_to_object(schema));
5211
5212 let mut union_variants = Vec::new();
5213
5214 for (original_index, variant_schema) in filtered {
5215 if let Some(ref_str) = variant_schema.reference() {
5217 if let Some(schema_name) = self.extract_schema_name(ref_str) {
5218 dependencies.insert(schema_name.to_string());
5219 union_variants.push(SchemaRef {
5220 target: schema_name.to_string(),
5221 nullable: self.schema_or_reference_is_nullable(variant_schema),
5222 });
5223 }
5224 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
5225 let schema_name = if recursive_ref == "#" {
5226 self.find_recursive_anchor_schema()
5228 .or_else(|| self.current_schema_name.clone())
5229 .unwrap_or_else(|| "CompoundFilter".to_string())
5230 } else {
5231 self.extract_schema_name(recursive_ref)
5232 .map(|s| s.to_string())
5233 .unwrap_or_else(|| "RecursiveType".to_string())
5234 };
5235 dependencies.insert(schema_name.clone());
5236 union_variants.push(SchemaRef {
5237 target: schema_name,
5238 nullable: false,
5239 });
5240 } else {
5241 let branch_discriminator = self.inline_union_branch_discriminator_value(
5242 variant_schema,
5243 discriminator,
5244 original_index,
5245 );
5246 let inline_name = self.generate_context_aware_name(
5248 parent_name,
5249 "InlineVariant",
5250 original_index,
5251 Some(variant_schema),
5252 );
5253 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
5254 let variant_type = analyzed.schema_type;
5255
5256 for dep in &analyzed.dependencies {
5258 dependencies.insert(dep.clone());
5259 }
5260
5261 match &variant_type {
5262 SchemaType::Primitive { rust_type, .. } => {
5264 union_variants.push(SchemaRef {
5265 target: rust_type.clone(),
5266 nullable: false,
5267 });
5268 }
5269 SchemaType::Array { item_type } => {
5271 match item_type.as_ref() {
5272 SchemaType::Primitive { rust_type, .. } => {
5273 let type_name = format!("Vec<{rust_type}>");
5274 union_variants.push(SchemaRef {
5275 target: type_name,
5276 nullable: false,
5277 });
5278 }
5279 SchemaType::Reference { target } => {
5280 let type_name = format!("Vec<{target}>");
5281 union_variants.push(SchemaRef {
5282 target: type_name,
5283 nullable: false,
5284 });
5285 }
5286 SchemaType::Array {
5288 item_type: inner_item_type,
5289 } => {
5290 match inner_item_type.as_ref() {
5291 SchemaType::Primitive { rust_type, .. } => {
5292 let type_name = format!("Vec<Vec<{rust_type}>>");
5293 union_variants.push(SchemaRef {
5294 target: type_name,
5295 nullable: false,
5296 });
5297 }
5298 SchemaType::Reference { target } => {
5299 let type_name = format!("Vec<Vec<{target}>>");
5300 union_variants.push(SchemaRef {
5301 target: type_name,
5302 nullable: false,
5303 });
5304 }
5305 _ => {
5306 let inline_type_name = self.generate_context_aware_name(
5308 parent_name,
5309 "Variant",
5310 original_index,
5311 None,
5312 );
5313 let inline_type_name = self
5314 .add_inline_union_branch_schema(
5315 &inline_type_name,
5316 variant_schema,
5317 dependencies,
5318 parent_name,
5319 union_kind,
5320 original_index,
5321 branch_discriminator.as_deref(),
5322 )?;
5323 union_variants.push(SchemaRef {
5324 target: inline_type_name,
5325 nullable: false,
5326 });
5327 }
5328 }
5329 }
5330 _ => {
5331 let inline_type_name = self.generate_context_aware_name(
5333 parent_name,
5334 "Variant",
5335 original_index,
5336 None,
5337 );
5338 let inline_type_name = self.add_inline_union_branch_schema(
5339 &inline_type_name,
5340 variant_schema,
5341 dependencies,
5342 parent_name,
5343 union_kind,
5344 original_index,
5345 branch_discriminator.as_deref(),
5346 )?;
5347 union_variants.push(SchemaRef {
5348 target: inline_type_name,
5349 nullable: false,
5350 });
5351 }
5352 }
5353 }
5354 SchemaType::Reference { target } => {
5356 union_variants.push(SchemaRef {
5357 target: target.clone(),
5358 nullable: false,
5359 });
5360 }
5361 _ => {
5363 let inline_type_name = self.generate_context_aware_name(
5364 parent_name,
5365 "Variant",
5366 original_index,
5367 None,
5368 );
5369 let inline_type_name = self.add_inline_union_branch_schema(
5370 &inline_type_name,
5371 variant_schema,
5372 dependencies,
5373 parent_name,
5374 union_kind,
5375 original_index,
5376 branch_discriminator.as_deref(),
5377 )?;
5378 union_variants.push(SchemaRef {
5379 target: inline_type_name,
5380 nullable: false,
5381 });
5382 }
5383 }
5384 }
5385 }
5386
5387 if !union_variants.is_empty() {
5388 return Ok(SchemaType::Union {
5389 variants: union_variants,
5390 exclusive: exclusive_object_union,
5391 });
5392 }
5393
5394 Ok(self.untyped_value(
5396 self.untyped_context(""),
5397 UntypedReason::UnrepresentableUnion,
5398 ))
5399 }
5400
5401 fn add_inline_schema(
5402 &mut self,
5403 type_name: &str,
5404 schema: &Schema,
5405 dependencies: &mut HashSet<String>,
5406 ) -> Result<String> {
5407 let allocated_name = self.allocate_inline_schema_name(
5408 type_name,
5409 &format!("{type_name}Inline"),
5410 "inline-schema",
5411 schema,
5412 );
5413 self.add_allocated_inline_schema(allocated_name, schema, dependencies)
5414 }
5415
5416 #[allow(clippy::too_many_arguments)]
5417 fn add_inline_union_branch_schema(
5418 &mut self,
5419 type_name: &str,
5420 schema: &Schema,
5421 dependencies: &mut HashSet<String>,
5422 owner_context: &str,
5423 union_kind: InlineUnionKind,
5424 original_index: usize,
5425 discriminator: Option<&str>,
5426 ) -> Result<String> {
5427 let allocated_name = self.allocate_inline_union_branch_name(
5428 type_name,
5429 owner_context,
5430 union_kind,
5431 original_index,
5432 discriminator,
5433 schema,
5434 );
5435 let branch_name = self.add_allocated_inline_schema(allocated_name, schema, dependencies)?;
5436 self.narrow_opaque_object_branch(&branch_name);
5437 Ok(branch_name)
5438 }
5439
5440 fn narrow_opaque_object_branch(&mut self, branch_name: &str) {
5451 if let Some(cached) = self.resolved_cache.get_mut(branch_name)
5452 && let SchemaType::Untyped {
5453 shape: shape @ UntypedShape::Value,
5454 reason: UntypedReason::OpaqueObject,
5455 } = &mut cached.schema_type
5456 {
5457 *shape = UntypedShape::ValueMap;
5458 }
5459 }
5460
5461 fn add_allocated_inline_schema(
5462 &mut self,
5463 allocated_name: String,
5464 schema: &Schema,
5465 dependencies: &mut HashSet<String>,
5466 ) -> Result<String> {
5467 if let Some(schema_type) = schema.schema_type() {
5469 match schema_type {
5470 OpenApiSchemaType::String
5471 | OpenApiSchemaType::Integer
5472 | OpenApiSchemaType::Number
5473 | OpenApiSchemaType::Boolean => {
5474 let rust_type =
5475 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
5476
5477 self.resolved_cache.insert(
5479 allocated_name.clone(),
5480 AnalyzedSchema {
5481 name: allocated_name.clone(),
5482 original: serde_json::to_value(schema).unwrap_or(Value::Null),
5483 schema_type: SchemaType::Primitive {
5484 rust_type,
5485 serde_with: None,
5486 },
5487 dependencies: HashSet::new(),
5488 nullable: false,
5489 description: schema.details().description.clone(),
5490 default: None,
5491 },
5492 );
5493 return Ok(allocated_name);
5494 }
5495 _ => {}
5496 }
5497 }
5498
5499 let analyzed = self.with_schema_context(&allocated_name, |analyzer| {
5503 analyzer.analyze_schema_value(schema, &allocated_name)
5504 })?;
5505
5506 self.resolved_cache.insert(allocated_name.clone(), analyzed);
5508
5509 if let Some(cached) = self.resolved_cache.get(&allocated_name) {
5511 for dep in &cached.dependencies {
5512 dependencies.insert(dep.clone());
5513 }
5514 }
5515
5516 Ok(allocated_name)
5517 }
5518
5519 fn inline_union_branch_discriminator_value(
5520 &self,
5521 schema: &Schema,
5522 discriminator: Option<&Discriminator>,
5523 original_index: usize,
5524 ) -> Option<String> {
5525 let discriminator = discriminator?;
5526 discriminator
5527 .mapping
5528 .as_ref()
5529 .and_then(|mappings| {
5530 mappings
5531 .iter()
5532 .find(|(_, target_ref)| {
5533 target_ref.contains(&format!("variant_{original_index}"))
5534 })
5535 .map(|(key, _)| key.clone())
5536 })
5537 .or_else(|| {
5538 Some(self.extract_inline_discriminator_value(
5539 schema,
5540 &discriminator.property_name,
5541 original_index,
5542 ))
5543 })
5544 }
5545
5546 fn extract_inline_discriminator_value(
5547 &self,
5548 schema: &Schema,
5549 discriminator_field: &str,
5550 variant_index: usize,
5551 ) -> String {
5552 if let Some(properties) = &schema.details().properties {
5554 if let Some(discriminator_prop) = properties.get(discriminator_field) {
5555 if let Some(enum_values) = &discriminator_prop.details().enum_values {
5557 if enum_values.len() == 1 {
5558 if let Some(value) = enum_values[0].as_str() {
5559 return value.to_string();
5560 }
5561 }
5562 }
5563 if let Some(const_value) = discriminator_prop.details().extra.get("const") {
5565 if let Some(value) = const_value.as_str() {
5566 return value.to_string();
5567 }
5568 }
5569 if let Some(const_value) = &discriminator_prop.details().const_value {
5571 if let Some(value) = const_value.as_str() {
5572 return value.to_string();
5573 }
5574 }
5575 }
5576 }
5577
5578 if let Some(inferred_name) = self.infer_variant_name_from_structure(schema, variant_index) {
5580 return inferred_name;
5581 }
5582
5583 format!("variant_{variant_index}")
5585 }
5586
5587 fn infer_variant_name_from_structure(
5588 &self,
5589 schema: &Schema,
5590 _variant_index: usize,
5591 ) -> Option<String> {
5592 let details = schema.details();
5593
5594 if let Some(properties) = &details.properties {
5596 if properties.contains_key("text") && properties.len() <= 3 {
5598 return Some("text".to_string());
5599 }
5600 if properties.contains_key("image") || properties.contains_key("source") {
5601 return Some("image".to_string());
5602 }
5603 if properties.contains_key("document") {
5604 return Some("document".to_string());
5605 }
5606 if properties.contains_key("tool_use_id") || properties.contains_key("tool_result") {
5607 return Some("tool_result".to_string());
5608 }
5609 if properties.contains_key("content") && properties.contains_key("is_error") {
5610 return Some("tool_result".to_string());
5611 }
5612 if properties.contains_key("partial_json") {
5613 return Some("partial_json".to_string());
5614 }
5615
5616 let property_names: Vec<&String> = properties.keys().collect();
5618
5619 for prop_name in &property_names {
5621 if prop_name.contains("result") {
5622 return Some("result".to_string());
5623 }
5624 if prop_name.contains("error") {
5625 return Some("error".to_string());
5626 }
5627 if prop_name.contains("content") && property_names.len() <= 2 {
5628 return Some("content".to_string());
5629 }
5630 }
5631
5632 let significant_props = property_names
5634 .iter()
5635 .filter(|&name| !["type", "id", "cache_control"].contains(&name.as_str()))
5636 .collect::<Vec<_>>();
5637
5638 if significant_props.len() == 1 {
5639 return Some((*significant_props[0]).clone());
5640 }
5641 }
5642
5643 if let Some(description) = &details.description {
5645 let desc_lower = description.to_lowercase();
5646 if desc_lower.contains("text") && desc_lower.len() < 100 {
5647 return Some("text".to_string());
5648 }
5649 if desc_lower.contains("image") {
5650 return Some("image".to_string());
5651 }
5652 if desc_lower.contains("document") {
5653 return Some("document".to_string());
5654 }
5655 if desc_lower.contains("tool") && desc_lower.contains("result") {
5656 return Some("tool_result".to_string());
5657 }
5658 }
5659
5660 None
5661 }
5662
5663 fn discriminator_to_variant_name(&self, discriminator: &str) -> String {
5664 if discriminator.is_empty() {
5666 return "Variant".to_string();
5667 }
5668
5669 let mut result = String::new();
5670 let mut next_upper = true;
5671
5672 for c in discriminator.chars() {
5673 match c {
5674 'a'..='z' => {
5675 if next_upper {
5676 result.push(c.to_ascii_uppercase());
5677 next_upper = false;
5678 } else {
5679 result.push(c);
5680 }
5681 }
5682 'A'..='Z' => {
5683 result.push(c);
5684 next_upper = false;
5685 }
5686 '0'..='9' => {
5687 result.push(c);
5688 next_upper = false;
5689 }
5690 '_' | '-' | '.' | ' ' | '/' | '\\' => {
5691 next_upper = true;
5693 }
5694 _ => {
5695 next_upper = true;
5697 }
5698 }
5699 }
5700
5701 if result.is_empty() || result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
5703 result = format!("Variant{result}");
5704 }
5705
5706 result
5707 }
5708
5709 fn ensure_unique_variant_name(
5710 &self,
5711 base_name: String,
5712 used_names: &mut std::collections::HashSet<String>,
5713 ) -> String {
5714 let mut candidate = base_name.clone();
5715 let mut counter = 1;
5716
5717 while used_names.contains(&candidate) {
5718 counter += 1;
5719 candidate = format!("{base_name}{counter}");
5720 }
5721
5722 used_names.insert(candidate.clone());
5723 candidate
5724 }
5725
5726 fn generate_inline_type_name(&self, schema: &Schema, variant_index: usize) -> String {
5727 if let Some(meaningful_name) = self.infer_type_name_from_structure(schema) {
5729 return meaningful_name;
5730 }
5731
5732 let context = self.current_schema_name.as_deref().unwrap_or("Inline");
5734 self.generate_context_aware_name(context, "Variant", variant_index, Some(schema))
5735 }
5736
5737 fn infer_type_name_from_structure(&self, schema: &Schema) -> Option<String> {
5738 let details = schema.details();
5739
5740 if let Some(description) = &details.description {
5742 if let Some(name_from_desc) = self.extract_type_name_from_description(description) {
5743 return Some(name_from_desc);
5744 }
5745 }
5746
5747 if let Some(properties) = &details.properties {
5749 if let Some(name_from_props) = self.extract_type_name_from_properties(properties) {
5750 return Some(format!("{name_from_props}Block"));
5751 }
5752 }
5753
5754 None
5755 }
5756
5757 fn extract_type_name_from_description(&self, description: &str) -> Option<String> {
5758 if description.len() > 100 || description.contains('\n') {
5760 return None;
5761 }
5762
5763 let words: Vec<&str> = description
5765 .split_whitespace()
5766 .take(2) .filter(|word| {
5768 let w = word.to_lowercase();
5769 word.len() > 2
5770 && ![
5771 "the", "and", "for", "with", "that", "this", "are", "can", "will", "was",
5772 ]
5773 .contains(&w.as_str())
5774 })
5775 .collect();
5776
5777 if words.is_empty() {
5778 return None;
5779 }
5780
5781 let combined = words.join("_");
5783 let pascal_name = self.discriminator_to_variant_name(&combined);
5784
5785 if !pascal_name.ends_with("Content")
5787 && !pascal_name.ends_with("Block")
5788 && !pascal_name.ends_with("Type")
5789 {
5790 Some(format!("{pascal_name}Content"))
5791 } else {
5792 Some(pascal_name)
5793 }
5794 }
5795
5796 fn extract_type_name_from_properties(
5797 &self,
5798 properties: &std::collections::BTreeMap<String, crate::openapi::Schema>,
5799 ) -> Option<String> {
5800 let significant_props: Vec<&String> = properties
5802 .keys()
5803 .filter(|name| !["type", "id", "cache_control"].contains(&name.as_str()))
5804 .collect();
5805
5806 if significant_props.is_empty() {
5807 return None;
5808 }
5809
5810 if significant_props.len() == 1 {
5812 let prop_name = significant_props[0];
5813 return Some(self.discriminator_to_variant_name(prop_name));
5814 }
5815
5816 let mut sorted_props = significant_props.clone();
5819 sorted_props.sort();
5820 if let Some(first_prop) = sorted_props.first() {
5821 return Some(self.discriminator_to_variant_name(first_prop));
5822 }
5823
5824 None
5825 }
5826
5827 fn openapi_type_to_rust_type(
5828 &self,
5829 openapi_type: OpenApiSchemaType,
5830 details: &crate::openapi::SchemaDetails,
5831 ) -> String {
5832 if openapi_type == OpenApiSchemaType::Integer {
5837 self.integer_rust_type(details)
5838 } else {
5839 self.type_mapper.map(openapi_type, details).rust_type
5840 }
5841 }
5842
5843 #[allow(dead_code)]
5844 fn fallback_discriminator_value(&self, schema_name: &str) -> String {
5845 self.fallback_discriminator_value_for_field(schema_name, "type")
5846 }
5847
5848 fn fallback_discriminator_value_for_field(
5849 &self,
5850 schema_name: &str,
5851 field_name: &str,
5852 ) -> String {
5853 if let Some(ref_schema) = self.schemas.get(schema_name) {
5855 if let Some(extracted) =
5856 self.extract_discriminator_value_for_field(ref_schema, field_name)
5857 {
5858 return extracted;
5859 }
5860 }
5861
5862 self.generate_discriminator_value_from_name(schema_name)
5864 }
5865
5866 fn disambiguate_shared_discriminator_values(
5867 &self,
5868 variants: &mut [UnionVariant],
5869 discriminator: Option<&Discriminator>,
5870 ) {
5871 let mut owners_by_value: BTreeMap<String, Vec<usize>> = BTreeMap::new();
5872 for (index, variant) in variants.iter().enumerate() {
5873 for value in &variant.discriminator_values {
5874 owners_by_value
5875 .entry(value.clone())
5876 .or_default()
5877 .push(index);
5878 }
5879 }
5880
5881 let mut removals: Vec<(usize, String)> = Vec::new();
5882 for (value, candidate_owners) in owners_by_value {
5883 if candidate_owners.len() < 2 {
5884 continue;
5885 }
5886
5887 let explicitly_mapped_owners: Vec<usize> = discriminator
5888 .and_then(|disc| disc.mapping.as_ref())
5889 .and_then(|mappings| mappings.get(&value))
5890 .map(|target_ref| {
5891 candidate_owners
5892 .iter()
5893 .copied()
5894 .filter(|index| {
5895 let variant = &variants[*index];
5896 target_ref == &variant.schema_ref
5897 || self.extract_schema_name(target_ref)
5898 == Some(variant.type_name.as_str())
5899 || (variant.schema_ref.starts_with("inline_")
5900 && target_ref.contains(&variant.schema_ref))
5901 })
5902 .collect()
5903 })
5904 .unwrap_or_default();
5905
5906 let winner = if explicitly_mapped_owners.len() == 1 {
5907 explicitly_mapped_owners.first().copied()
5908 } else {
5909 let mut scored: Vec<(usize, usize)> = candidate_owners
5910 .iter()
5911 .map(|index| {
5912 (
5913 *index,
5914 Self::discriminator_name_affinity(&variants[*index].type_name, &value),
5915 )
5916 })
5917 .collect();
5918 scored.sort_by(|left, right| right.1.cmp(&left.1).then(left.0.cmp(&right.0)));
5919 match scored.as_slice() {
5920 [(index, score), rest @ ..]
5921 if *score > 0 && rest.first().is_none_or(|(_, next)| score > next) =>
5922 {
5923 Some(*index)
5924 }
5925 _ => None,
5926 }
5927 };
5928
5929 if let Some(winner) = winner {
5930 for index in candidate_owners {
5931 if index != winner && variants[index].preferred_discriminator_values.len() > 1 {
5932 removals.push((index, value.clone()));
5933 }
5934 }
5935 }
5936 }
5937
5938 for (index, value) in removals {
5939 variants[index]
5940 .preferred_discriminator_values
5941 .retain(|candidate| candidate != &value);
5942 }
5943 for variant in variants {
5944 if let Some(canonical) = variant.preferred_discriminator_values.first() {
5945 variant.discriminator_value.clone_from(canonical);
5946 }
5947 }
5948 }
5949
5950 fn discriminator_name_affinity(schema_name: &str, value: &str) -> usize {
5951 let schema_compact: String = schema_name
5952 .chars()
5953 .filter(|character| character.is_ascii_alphanumeric())
5954 .flat_map(char::to_lowercase)
5955 .collect();
5956 let value_compact: String = value
5957 .chars()
5958 .filter(|character| character.is_ascii_alphanumeric())
5959 .flat_map(char::to_lowercase)
5960 .collect();
5961 let exact_bonus = usize::from(
5962 !value_compact.is_empty() && schema_compact.contains(value_compact.as_str()),
5963 ) * 10;
5964 let token_matches = value
5965 .split(|character: char| !character.is_ascii_alphanumeric())
5966 .filter(|token| !token.is_empty())
5967 .filter(|token| schema_compact.contains(&token.to_ascii_lowercase()))
5968 .count();
5969 exact_bonus + token_matches
5970 }
5971
5972 fn generate_discriminator_value_from_name(&self, schema_name: &str) -> String {
5973 let mut result = String::new();
5975 let mut chars = schema_name.chars().peekable();
5976 let mut first = true;
5977
5978 while let Some(c) = chars.next() {
5979 if c.is_uppercase()
5980 && !first
5981 && chars
5982 .peek()
5983 .map(|&next| next.is_lowercase())
5984 .unwrap_or(false)
5985 {
5986 result.push('.');
5987 }
5988 result.push(c.to_ascii_lowercase());
5989 first = false;
5990 }
5991
5992 if result.ends_with("event") {
5994 result = result[..result.len() - 5].to_string();
5995 }
5996
5997 if schema_name.starts_with("Response") && !result.starts_with("response.") {
5999 result = format!("response.{}", result.trim_start_matches("response"));
6000 }
6001
6002 result
6003 }
6004
6005 fn to_rust_variant_name(&self, schema_name: &str) -> String {
6006 let mut name = schema_name;
6008
6009 if name.starts_with("Response") && name.len() > 8 {
6011 name = &name[8..]; }
6013
6014 if name.ends_with("Event") && name.len() > 5 {
6016 name = &name[..name.len() - 5]; }
6018
6019 name = name.trim_matches('_');
6021
6022 if name.is_empty() {
6024 schema_name.to_string()
6025 } else {
6026 self.discriminator_to_variant_name(name)
6028 }
6029 }
6030
6031 fn hoist_inline_string_enum(
6055 &mut self,
6056 schema: &Schema,
6057 enum_values: Vec<String>,
6058 primary_name: String,
6059 dependencies: &mut HashSet<String>,
6060 ) -> SchemaType {
6061 let suffix = enum_values
6062 .first()
6063 .map(|value| self.to_pascal_case(value))
6064 .unwrap_or_else(|| "Variant".to_string());
6065 let collision_name = format!("{primary_name}{suffix}");
6066 let enum_type_name =
6067 self.allocate_inline_schema_name(&primary_name, &collision_name, "string-enum", schema);
6068 let should_insert = !self.resolved_cache.contains_key(&enum_type_name);
6069
6070 if should_insert {
6073 self.resolved_cache.insert(
6074 enum_type_name.clone(),
6075 AnalyzedSchema {
6076 name: enum_type_name.clone(),
6077 original: serde_json::to_value(schema).unwrap_or(Value::Null),
6078 schema_type: SchemaType::StringEnum {
6079 values: enum_values,
6080 },
6081 dependencies: HashSet::new(),
6082 nullable: false,
6083 description: schema.details().description.clone(),
6084 default: schema.details().default.clone(),
6085 },
6086 );
6087 }
6088
6089 dependencies.insert(enum_type_name.clone());
6091 SchemaType::Reference {
6092 target: enum_type_name,
6093 }
6094 }
6095
6096 fn analyze_array_schema(
6097 &mut self,
6098 schema: &Schema,
6099 parent_schema_name: &str,
6100 dependencies: &mut HashSet<String>,
6101 ) -> Result<SchemaType> {
6102 let details = schema.details();
6103
6104 if let Some(positions) = details.positional_items() {
6108 return self.analyze_positional_items(
6109 positions,
6110 details,
6111 parent_schema_name,
6112 dependencies,
6113 );
6114 }
6115
6116 if let Some(items_schema) = details.item_schema() {
6118 let item_type = self.analyze_item_schema(
6119 items_schema,
6120 parent_schema_name,
6121 &format!("{parent_schema_name}Item"),
6122 dependencies,
6123 )?;
6124 let item_type = self.hoist_inline_property_type(
6125 parent_schema_name,
6126 "Item",
6127 item_type,
6128 dependencies,
6129 );
6130 Ok(SchemaType::Array {
6131 item_type: Box::new(item_type),
6132 })
6133 } else {
6134 Ok(
6136 self.untyped_value_array(
6137 self.untyped_context(""),
6138 UntypedReason::ArrayWithoutItems,
6139 ),
6140 )
6141 }
6142 }
6143
6144 fn shared_branch_type(&self, branches: &[Schema]) -> Option<SchemaType> {
6153 let mut mapped: Option<(String, Option<String>)> = None;
6154 let mut scalar_kind: Option<OpenApiSchemaType> = None;
6155 let mut formats_agree = true;
6156 for branch in branches {
6157 if branch.reference().is_some() {
6158 return None;
6159 }
6160 let details = branch.details();
6161 if details.enum_values.is_some()
6162 || details.const_value.is_some()
6163 || details.properties.is_some()
6164 {
6165 return None;
6166 }
6167 let scalar = match branch.schema_type()? {
6168 scalar @ (OpenApiSchemaType::String
6169 | OpenApiSchemaType::Integer
6170 | OpenApiSchemaType::Number
6171 | OpenApiSchemaType::Boolean) => scalar.clone(),
6172 _ => return None,
6173 };
6174 match &scalar_kind {
6175 Some(existing) if *existing != scalar => return None,
6176 Some(_) => {}
6177 None => scalar_kind = Some(scalar.clone()),
6178 }
6179
6180 let candidate = self.type_mapper.map(scalar, details);
6181 let candidate = (candidate.rust_type, candidate.serde_with);
6182 match &mapped {
6183 Some(existing) if *existing != candidate => formats_agree = false,
6184 Some(_) => {}
6185 None => mapped = Some(candidate),
6186 }
6187 }
6188
6189 if formats_agree {
6190 return mapped.map(|(rust_type, serde_with)| SchemaType::Primitive {
6191 rust_type,
6192 serde_with,
6193 });
6194 }
6195
6196 let scalar = scalar_kind?;
6202 let mapped = self
6203 .type_mapper
6204 .map(scalar, &crate::openapi::SchemaDetails::default());
6205 Some(SchemaType::Primitive {
6206 rust_type: mapped.rust_type,
6207 serde_with: mapped.serde_with,
6208 })
6209 }
6210
6211 fn expand_pointer_branches(&self, branches: &[Schema]) -> Option<Vec<Schema>> {
6220 let mut expanded = Vec::with_capacity(branches.len());
6221 let mut changed = false;
6222 for branch in branches {
6223 let resolved = branch
6224 .reference()
6225 .filter(|reference| self.extract_schema_name(reference).is_none())
6226 .and_then(|reference| reference.strip_prefix('#'))
6227 .filter(|pointer| pointer.starts_with('/'))
6228 .and_then(|pointer| self.openapi_spec.pointer(pointer))
6229 .and_then(|value| Schema::deserialize(value).ok())
6230 .filter(|schema| schema.reference().is_none());
6231 match resolved {
6232 Some(schema) => {
6233 expanded.push(schema);
6234 changed = true;
6235 }
6236 None => expanded.push(branch.clone()),
6237 }
6238 }
6239 changed.then_some(expanded)
6240 }
6241
6242 fn analyze_object_with_variants(
6255 &mut self,
6256 schema: &Schema,
6257 branches: &[Schema],
6258 schema_name: &str,
6259 dependencies: &mut HashSet<String>,
6260 ) -> Result<Option<SchemaType>> {
6261 let details = schema.details();
6262 if details.properties.as_ref().is_none_or(BTreeMap::is_empty) || branches.is_empty() {
6263 return Ok(None);
6264 }
6265 if schema.is_nullable_pattern() || Self::union_only_constrains_requiredness(branches) {
6268 return Ok(None);
6269 }
6270
6271 let base = self.analyze_object_schema(schema, dependencies)?;
6272 let SchemaType::Object {
6273 properties,
6274 required,
6275 additional_properties,
6276 ..
6277 } = base
6278 else {
6279 return Ok(None);
6280 };
6281
6282 let preferred_variant_name = format!("{schema_name}Variant");
6283 let variant_name = self.allocate_inline_schema_name(
6284 &preferred_variant_name,
6285 &format!("{preferred_variant_name}Inline"),
6286 "object-variant",
6287 schema,
6288 );
6289 let variant_type = self.analyze_anyof_union(
6290 branches,
6291 schema.discriminator(),
6292 dependencies,
6293 &variant_name,
6294 )?;
6295 if matches!(variant_type, SchemaType::Untyped { .. }) {
6298 return Ok(Some(SchemaType::Object {
6299 properties,
6300 required,
6301 additional_properties,
6302 variant: None,
6303 }));
6304 }
6305
6306 self.resolved_cache.insert(
6307 variant_name.clone(),
6308 AnalyzedSchema {
6309 name: variant_name.clone(),
6310 original: Value::Null,
6311 dependencies: schema_type_dependencies(&variant_type),
6312 schema_type: variant_type,
6313 nullable: false,
6314 description: None,
6315 default: None,
6316 },
6317 );
6318 dependencies.insert(variant_name.clone());
6319
6320 Ok(Some(SchemaType::Object {
6321 properties,
6322 required,
6323 additional_properties,
6324 variant: Some(SchemaRef {
6325 target: variant_name,
6326 nullable: false,
6327 }),
6328 }))
6329 }
6330
6331 #[allow(clippy::result_unit_err)]
6339 fn resolve_boolean_branches(branches: &[Schema]) -> std::result::Result<Vec<Schema>, ()> {
6340 if branches
6341 .iter()
6342 .any(|branch| matches!(branch, Schema::Bool(true)))
6343 {
6344 return Err(());
6345 }
6346 Ok(branches
6347 .iter()
6348 .filter(|branch| !matches!(branch, Schema::Bool(false)))
6349 .cloned()
6350 .collect())
6351 }
6352
6353 fn union_only_constrains_requiredness(branches: &[Schema]) -> bool {
6362 !branches.is_empty()
6363 && branches
6364 .iter()
6365 .all(Self::schema_only_constrains_requiredness)
6366 }
6367
6368 fn schema_only_constrains_requiredness(schema: &Schema) -> bool {
6372 let keys_are_requiredness_or_annotations = serde_json::to_value(schema)
6373 .ok()
6374 .and_then(|value| value.as_object().cloned())
6375 .is_some_and(|object| {
6376 object.keys().all(|key| {
6377 matches!(
6378 key.as_str(),
6379 "required"
6380 | "not"
6381 | "anyOf"
6382 | "oneOf"
6383 | "title"
6384 | "description"
6385 | "deprecated"
6386 | "readOnly"
6387 | "writeOnly"
6388 | "examples"
6389 | "example"
6390 | "default"
6391 | "externalDocs"
6392 | "xml"
6393 | "$comment"
6394 ) || key.starts_with("x-")
6395 })
6396 });
6397 if !keys_are_requiredness_or_annotations {
6398 return false;
6399 }
6400
6401 match schema {
6402 Schema::AnyOf { any_of, .. } => {
6403 !any_of.is_empty() && any_of.iter().all(Self::schema_only_constrains_requiredness)
6404 }
6405 Schema::OneOf { one_of, .. } => {
6406 !one_of.is_empty() && one_of.iter().all(Self::schema_only_constrains_requiredness)
6407 }
6408 other => {
6409 let details = other.details();
6410 details
6411 .required
6412 .as_ref()
6413 .is_some_and(|required| !required.is_empty())
6414 || details
6415 .not
6416 .as_deref()
6417 .is_some_and(Self::schema_only_constrains_requiredness)
6418 }
6419 }
6420 }
6421
6422 fn analyze_empty_union(
6429 &mut self,
6430 schema: &Schema,
6431 dependencies: &mut HashSet<String>,
6432 ) -> Result<SchemaType> {
6433 let Some(declared) = schema
6437 .declared_type()
6438 .cloned()
6439 .or_else(|| schema.inferred_type())
6440 .or_else(|| {
6441 schema
6442 .details()
6443 .properties
6444 .is_some()
6445 .then_some(OpenApiSchemaType::Object)
6446 })
6447 else {
6448 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema));
6449 };
6450 match declared {
6451 OpenApiSchemaType::Object => self.analyze_object_schema(schema, dependencies),
6452 OpenApiSchemaType::Array => {
6453 let context = self
6454 .current_schema_name
6455 .clone()
6456 .unwrap_or_else(|| "Inline".to_string());
6457 self.analyze_array_schema(schema, &context, dependencies)
6458 }
6459 scalar => {
6460 let mapped = self.type_mapper.map(scalar, schema.details());
6461 Ok(SchemaType::Primitive {
6462 rust_type: mapped.rust_type,
6463 serde_with: mapped.serde_with,
6464 })
6465 }
6466 }
6467 }
6468
6469 fn resolve_pointer_schema(
6482 &mut self,
6483 reference: &str,
6484 dependencies: &mut HashSet<String>,
6485 ) -> Result<Option<SchemaType>> {
6486 let Some(pointer) = reference.strip_prefix('#') else {
6487 return Ok(None);
6488 };
6489 if pointer.is_empty() || !pointer.starts_with('/') {
6490 return Ok(None);
6491 }
6492 let preferred_name = pointer_type_name(pointer);
6493 if preferred_name.is_empty() {
6494 return Ok(None);
6495 }
6496 let name = self.allocate_pointer_schema_name(pointer, &preferred_name);
6497 if self.resolved_cache.contains_key(&name) || !self.resolving_pointers.insert(name.clone())
6500 {
6501 dependencies.insert(name.clone());
6502 return Ok(Some(SchemaType::Reference { target: name }));
6503 }
6504
6505 let resolved = (|| {
6506 let value = self.openapi_spec.pointer(pointer)?.clone();
6507 Schema::deserialize(&value).ok()
6508 })();
6509 let Some(schema) = resolved else {
6510 self.resolving_pointers.remove(&name);
6511 return Ok(None);
6512 };
6513
6514 let saved_context = self.current_schema_name.clone();
6519 self.current_schema_name = Some(name.clone());
6520 let analyzed = self.analyze_schema_value(&schema, &name);
6521 self.current_schema_name = saved_context;
6522 self.resolving_pointers.remove(&name);
6523 let analyzed = analyzed?;
6524 dependencies.extend(analyzed.dependencies.iter().cloned());
6525 if analyzed.schema_type.renders_inline() {
6526 return Ok(Some(analyzed.schema_type));
6527 }
6528
6529 self.resolved_cache.insert(name.clone(), analyzed);
6530 dependencies.insert(name.clone());
6531 Ok(Some(SchemaType::Reference { target: name }))
6532 }
6533
6534 fn hoist_inline_property_type(
6543 &mut self,
6544 schema_name: &str,
6545 property_name: &str,
6546 schema_type: SchemaType,
6547 dependencies: &mut HashSet<String>,
6548 ) -> SchemaType {
6549 if schema_type.renders_inline() {
6550 return schema_type;
6551 }
6552
6553 use heck::ToPascalCase;
6554
6555 let preferred_name = format!("{schema_name}{}", property_name.to_pascal_case());
6556 let hoisted_name = self.allocate_synthetic_schema_name(
6557 &preferred_name,
6558 &format!("{preferred_name}Inline"),
6559 "hoisted-property",
6560 format!("{schema_type:?}"),
6561 );
6562 let hoisted_dependencies = schema_type_dependencies(&schema_type);
6563 self.resolved_cache.insert(
6564 hoisted_name.clone(),
6565 AnalyzedSchema {
6566 name: hoisted_name.clone(),
6567 original: Value::Null,
6568 schema_type,
6569 dependencies: hoisted_dependencies,
6570 nullable: false,
6571 description: None,
6572 default: None,
6573 },
6574 );
6575 dependencies.insert(hoisted_name.clone());
6576 SchemaType::Reference {
6577 target: hoisted_name,
6578 }
6579 }
6580
6581 fn analyze_positional_items(
6594 &mut self,
6595 positions: &[Schema],
6596 details: &crate::openapi::SchemaDetails,
6597 parent_schema_name: &str,
6598 dependencies: &mut HashSet<String>,
6599 ) -> Result<SchemaType> {
6600 if details.positional_items_are_exact() && !positions.is_empty() {
6601 let mut element_types = Vec::with_capacity(positions.len());
6602 for (index, position) in positions.iter().enumerate() {
6603 let element_type = self.analyze_item_schema(
6604 position,
6605 parent_schema_name,
6606 &format!("{parent_schema_name}Item{}", index + 1),
6607 dependencies,
6608 )?;
6609 element_types.push(self.hoist_inline_property_type(
6610 parent_schema_name,
6611 &format!("Item{}", index + 1),
6612 element_type,
6613 dependencies,
6614 ));
6615 }
6616 return Ok(SchemaType::Tuple { element_types });
6617 }
6618
6619 if details.positional_items_are_closed()
6623 && let Some(shared) = shared_positional_schema(positions)
6624 {
6625 let item_type = self.analyze_item_schema(
6626 shared,
6627 parent_schema_name,
6628 &format!("{parent_schema_name}Item"),
6629 dependencies,
6630 )?;
6631 return Ok(SchemaType::Array {
6632 item_type: Box::new(item_type),
6633 });
6634 }
6635
6636 Ok(self.untyped_value_array(self.untyped_context(""), UntypedReason::OpenPositionalItems))
6637 }
6638
6639 fn analyze_item_schema(
6646 &mut self,
6647 items_schema: &Schema,
6648 parent_schema_name: &str,
6649 inline_name: &str,
6650 dependencies: &mut HashSet<String>,
6651 ) -> Result<SchemaType> {
6652 let item_type = match items_schema {
6653 Schema::Bool(accepts_anything) => self.untyped_value(
6654 self.untyped_context(""),
6655 if *accepts_anything {
6656 UntypedReason::AnySchema
6657 } else {
6658 UntypedReason::NeverMatches
6659 },
6660 ),
6661 Schema::Reference { reference, .. } => {
6662 if let Some(target) = self.extract_schema_name(reference) {
6664 let target = target.to_string();
6665 dependencies.insert(target.clone());
6666 SchemaType::Reference { target }
6667 } else {
6668 self.resolve_pointer_schema(reference, dependencies)?
6669 .ok_or_else(|| GeneratorError::UnresolvedReference(reference.to_string()))?
6670 }
6671 }
6672 Schema::RecursiveRef { recursive_ref, .. } => {
6673 if recursive_ref == "#" {
6675 let target = self
6677 .find_recursive_anchor_schema()
6678 .unwrap_or_else(|| parent_schema_name.to_string());
6679 dependencies.insert(target.clone());
6680 SchemaType::Reference { target }
6681 } else {
6682 let target = self
6683 .extract_schema_name(recursive_ref)
6684 .unwrap_or("RecursiveType")
6685 .to_string();
6686 dependencies.insert(target.clone());
6687 SchemaType::Reference { target }
6688 }
6689 }
6690 Schema::Typed { schema_type, .. } => {
6691 match schema_type {
6693 OpenApiSchemaType::String => {
6694 match items_schema
6698 .details()
6699 .string_enum_values()
6700 .filter(|values| !values.is_empty())
6701 {
6702 Some(values) => self.hoist_inline_string_enum(
6703 items_schema,
6704 values,
6705 inline_name.to_string(),
6706 dependencies,
6707 ),
6708 None => SchemaType::Primitive {
6709 rust_type: "String".to_string(),
6710 serde_with: None,
6711 },
6712 }
6713 }
6714 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
6715 let details = items_schema.details();
6716 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
6717 SchemaType::Primitive {
6718 rust_type,
6719 serde_with: None,
6720 }
6721 }
6722 OpenApiSchemaType::Boolean => SchemaType::Primitive {
6723 rust_type: "bool".to_string(),
6724 serde_with: None,
6725 },
6726 OpenApiSchemaType::Object => {
6727 let preferred_object_type_name = inline_name.to_string();
6729 let object_type_name = self.allocate_inline_schema_name(
6730 &preferred_object_type_name,
6731 &format!("{preferred_object_type_name}Inline"),
6732 "array-item-object",
6733 items_schema,
6734 );
6735
6736 self.add_allocated_object_schema(
6737 object_type_name,
6738 items_schema,
6739 dependencies,
6740 )?
6741 }
6742 OpenApiSchemaType::Array => {
6743 self.analyze_array_schema(items_schema, parent_schema_name, dependencies)?
6745 }
6746 _ => self.untyped_value(
6747 self.untyped_context(""),
6748 UntypedReason::UnsupportedTypeKeyword,
6749 ),
6750 }
6751 }
6752 Schema::OneOf { .. } | Schema::AnyOf { .. } => {
6753 let analyzed = self.analyze_schema_value(items_schema, "ArrayItem")?;
6755
6756 match &analyzed.schema_type {
6758 SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => {
6759 let preferred_union_name = format!("{inline_name}Union");
6762 let union_name = self.allocate_inline_schema_name(
6763 &preferred_union_name,
6764 &format!("{preferred_union_name}Inline"),
6765 "array-item-union",
6766 items_schema,
6767 );
6768
6769 let mut union_schema = analyzed;
6771 union_schema.name = union_name.clone();
6772
6773 self.resolved_cache.insert(union_name.clone(), union_schema);
6775
6776 dependencies.insert(union_name.clone());
6778
6779 SchemaType::Reference { target: union_name }
6781 }
6782 _ => analyzed.schema_type,
6783 }
6784 }
6785 Schema::Untyped { .. } => {
6786 if let Some(inferred) = items_schema.inferred_type() {
6788 match inferred {
6789 OpenApiSchemaType::Object => {
6790 let preferred_object_type_name = inline_name.to_string();
6792 let object_type_name = self.allocate_inline_schema_name(
6793 &preferred_object_type_name,
6794 &format!("{preferred_object_type_name}Inline"),
6795 "array-item-object",
6796 items_schema,
6797 );
6798
6799 self.add_allocated_object_schema(
6800 object_type_name,
6801 items_schema,
6802 dependencies,
6803 )?
6804 }
6805 OpenApiSchemaType::String => {
6806 match items_schema
6809 .details()
6810 .string_enum_values()
6811 .filter(|values| !values.is_empty())
6812 {
6813 Some(values) => self.hoist_inline_string_enum(
6814 items_schema,
6815 values,
6816 inline_name.to_string(),
6817 dependencies,
6818 ),
6819 None => SchemaType::Primitive {
6820 rust_type: "String".to_string(),
6821 serde_with: None,
6822 },
6823 }
6824 }
6825 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
6826 let details = items_schema.details();
6827 let rust_type = self.get_number_rust_type(inferred, details);
6828 SchemaType::Primitive {
6829 rust_type,
6830 serde_with: None,
6831 }
6832 }
6833 OpenApiSchemaType::Boolean => SchemaType::Primitive {
6834 rust_type: "bool".to_string(),
6835 serde_with: None,
6836 },
6837 OpenApiSchemaType::Null => SchemaType::Primitive {
6840 rust_type: self.type_mapper.null_unit().rust_type,
6841 serde_with: None,
6842 },
6843 _ => self.untyped_value(
6844 self.untyped_context(""),
6845 UntypedReason::UnsupportedTypeKeyword,
6846 ),
6847 }
6848 } else {
6849 self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema)
6850 }
6851 }
6852 _ => self.analyze_property_schema_with_context(items_schema, None, dependencies)?,
6857 };
6858
6859 Ok(self.nullable_container_value(items_schema, item_type))
6860 }
6861
6862 fn get_number_rust_type(
6863 &self,
6864 schema_type: OpenApiSchemaType,
6865 details: &crate::openapi::SchemaDetails,
6866 ) -> String {
6867 let format = details.format.as_deref();
6871 match schema_type {
6872 OpenApiSchemaType::Integer => self.integer_rust_type(details),
6873 OpenApiSchemaType::Number => self.type_mapper.number_format(format).rust_type,
6874 _ => self.type_mapper.dynamic_json().rust_type,
6875 }
6876 }
6877
6878 fn integer_rust_type(&self, details: &crate::openapi::SchemaDetails) -> String {
6884 fn integer_value(number: &serde_json::Number) -> Option<i128> {
6885 number
6886 .as_i64()
6887 .map(i128::from)
6888 .or_else(|| number.as_u64().map(i128::from))
6889 .or_else(|| {
6890 number.as_f64().and_then(|value| {
6891 (value.fract() == 0.0
6892 && value >= i128::MIN as f64
6893 && value <= i128::MAX as f64)
6894 .then_some(value as i128)
6895 })
6896 })
6897 }
6898
6899 fn below(number: &serde_json::Number, boundary: i128) -> bool {
6900 integer_value(number).is_some_and(|value| value < boundary)
6901 }
6902
6903 fn above(number: &serde_json::Number, boundary: i128) -> bool {
6904 integer_value(number).is_some_and(|value| value > boundary)
6905 }
6906
6907 let explicitly_nonnegative = details
6908 .minimum
6909 .as_ref()
6910 .and_then(integer_value)
6911 .is_some_and(|value| value >= 0)
6912 || matches!(
6913 details.exclusive_minimum,
6914 Some(crate::openapi::ExclusiveBound::Number(value)) if value >= 0.0
6915 );
6916
6917 let annotated_numbers = details
6918 .const_value
6919 .iter()
6920 .chain(details.default.iter())
6921 .chain(details.example.iter())
6922 .chain(details.enum_values.iter().flatten())
6923 .chain(details.examples.iter().flatten())
6924 .filter_map(Value::as_number)
6925 .collect::<Vec<_>>();
6926 let below_i32 = details
6927 .minimum
6928 .as_ref()
6929 .is_some_and(|number| below(number, i128::from(i32::MIN)))
6930 || annotated_numbers
6931 .iter()
6932 .any(|number| below(number, i128::from(i32::MIN)))
6933 || matches!(
6934 details.exclusive_minimum,
6935 Some(crate::openapi::ExclusiveBound::Number(value)) if value < i32::MIN as f64
6936 );
6937 let above_i32 = details
6938 .maximum
6939 .as_ref()
6940 .is_some_and(|number| above(number, i128::from(i32::MAX)))
6941 || annotated_numbers
6942 .iter()
6943 .any(|number| above(number, i128::from(i32::MAX)))
6944 || matches!(
6945 details.exclusive_maximum,
6946 Some(crate::openapi::ExclusiveBound::Number(value)) if value > i32::MAX as f64
6947 );
6948 let below_i64 = details
6949 .minimum
6950 .as_ref()
6951 .is_some_and(|number| below(number, i128::from(i64::MIN)))
6952 || annotated_numbers
6953 .iter()
6954 .any(|number| below(number, i128::from(i64::MIN)))
6955 || matches!(
6956 details.exclusive_minimum,
6957 Some(crate::openapi::ExclusiveBound::Number(value)) if value < i64::MIN as f64
6958 );
6959 let above_i64 = details
6960 .maximum
6961 .as_ref()
6962 .is_some_and(|number| above(number, i128::from(i64::MAX)))
6963 || annotated_numbers
6964 .iter()
6965 .any(|number| above(number, i128::from(i64::MAX)))
6966 || matches!(
6967 details.exclusive_maximum,
6968 Some(crate::openapi::ExclusiveBound::Number(value)) if value >= 9_223_372_036_854_775_808.0
6969 );
6970 let below_zero = details
6971 .minimum
6972 .as_ref()
6973 .and_then(integer_value)
6974 .is_some_and(|value| value < 0)
6975 || annotated_numbers
6976 .iter()
6977 .filter_map(|number| integer_value(number))
6978 .any(|value| value < 0)
6979 || matches!(
6980 details.exclusive_minimum,
6981 Some(crate::openapi::ExclusiveBound::Number(value)) if value < 0.0
6982 );
6983 let above_u32 = details
6984 .maximum
6985 .as_ref()
6986 .is_some_and(|number| above(number, i128::from(u32::MAX)))
6987 || annotated_numbers
6988 .iter()
6989 .any(|number| above(number, i128::from(u32::MAX)))
6990 || matches!(
6991 details.exclusive_maximum,
6992 Some(crate::openapi::ExclusiveBound::Number(value)) if value > u32::MAX as f64
6993 );
6994
6995 let configured = self
6996 .type_mapper
6997 .integer_format(details.format.as_deref())
6998 .rust_type;
6999 match configured.as_str() {
7000 "i32" if below_i32 || above_i32 => {
7001 if below_i64 || above_i64 {
7002 "i128".to_string()
7003 } else {
7004 "i64".to_string()
7005 }
7006 }
7007 "i64" if below_i64 => "i128".to_string(),
7008 "i64" if above_i64 && explicitly_nonnegative => "u64".to_string(),
7009 "i64" if above_i64 => "i128".to_string(),
7010 "u32" if below_zero => {
7011 if below_i64 || above_i64 {
7012 "i128".to_string()
7013 } else {
7014 "i64".to_string()
7015 }
7016 }
7017 "u32" if above_u32 => "u64".to_string(),
7018 "u64" if below_zero => "i128".to_string(),
7019 configured => configured.to_string(),
7020 }
7021 }
7022
7023 fn analyze_anyof_union(
7024 &mut self,
7025 any_of_schemas: &[Schema],
7026 discriminator: Option<&Discriminator>,
7027 dependencies: &mut HashSet<String>,
7028 context_name: &str,
7029 ) -> Result<SchemaType> {
7030 let original_indices = (0..any_of_schemas.len()).collect::<Vec<_>>();
7031
7032 let expanded_branches;
7034 let any_of_schemas = match self.expand_pointer_branches(any_of_schemas) {
7035 Some(expanded) => {
7036 expanded_branches = expanded;
7037 expanded_branches.as_slice()
7038 }
7039 None => any_of_schemas,
7040 };
7041
7042 let boolean_resolved;
7044 let boolean_resolved_indices;
7045 let any_of_schemas = match Self::resolve_boolean_branches(any_of_schemas) {
7046 Ok(resolved) => {
7047 boolean_resolved_indices = any_of_schemas
7048 .iter()
7049 .zip(original_indices.iter().copied())
7050 .filter_map(|(branch, index)| {
7051 (!matches!(branch, Schema::Bool(false))).then_some(index)
7052 })
7053 .collect::<Vec<_>>();
7054 boolean_resolved = resolved;
7055 boolean_resolved.as_slice()
7056 }
7057 Err(()) => {
7058 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema));
7059 }
7060 };
7061 let source_indices = boolean_resolved_indices.as_slice();
7062 if any_of_schemas.is_empty() {
7063 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::NeverMatches));
7064 }
7065
7066 let filtered_owned: Vec<Schema>;
7072 let filtered_indices: Vec<usize>;
7073 let (any_of_schemas, source_indices): (&[Schema], &[usize]) = if any_of_schemas
7074 .iter()
7075 .any(Schema::is_explicit_null_only)
7076 {
7077 filtered_owned = any_of_schemas
7078 .iter()
7079 .filter(|s| !s.is_explicit_null_only())
7080 .cloned()
7081 .collect();
7082 filtered_indices = any_of_schemas
7083 .iter()
7084 .zip(source_indices.iter().copied())
7085 .filter_map(|(schema, index)| (!schema.is_explicit_null_only()).then_some(index))
7086 .collect();
7087 if filtered_owned.is_empty() {
7088 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema));
7089 }
7090 if filtered_owned.len() == 1 {
7091 return self
7092 .analyze_schema_value(&filtered_owned[0], context_name)
7093 .map(|a| a.schema_type);
7094 }
7095 (&filtered_owned, &filtered_indices)
7096 } else {
7097 (any_of_schemas, source_indices)
7098 };
7099
7100 if let [only] = any_of_schemas {
7103 return self
7104 .analyze_schema_value(only, context_name)
7105 .map(|analyzed| analyzed.schema_type);
7106 }
7107
7108 if let Some(shared) = self.shared_branch_type(any_of_schemas) {
7112 return Ok(shared);
7113 }
7114
7115 let has_refs = any_of_schemas.iter().any(|s| s.is_reference());
7117 let has_objects = any_of_schemas.iter().any(|s| {
7118 matches!(s.schema_type(), Some(OpenApiSchemaType::Object))
7119 || s.inferred_type() == Some(OpenApiSchemaType::Object)
7120 });
7121 let has_arrays = any_of_schemas
7122 .iter()
7123 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Array)));
7124
7125 let all_string_like = any_of_schemas.iter().all(|s| {
7128 matches!(s.schema_type(), Some(OpenApiSchemaType::String))
7129 || s.details().const_value.is_some()
7130 });
7131
7132 if (has_refs || has_objects || has_arrays || any_of_schemas.len() > 1) && !all_string_like {
7133 if let Some(disc) = discriminator {
7135 return self.analyze_oneof_union(
7137 any_of_schemas,
7138 Some(disc),
7139 context_name,
7140 dependencies,
7141 InlineUnionKind::AnyOf,
7142 Some(source_indices),
7143 );
7144 }
7145
7146 if let Some(disc_field) = self.detect_discriminator_field(any_of_schemas) {
7148 return self.analyze_oneof_union(
7149 any_of_schemas,
7150 Some(&Discriminator {
7151 property_name: disc_field,
7152 mapping: None,
7153 default_mapping: None,
7154 extensions: crate::extensions::Extensions::default(),
7155 }),
7156 context_name,
7157 dependencies,
7158 InlineUnionKind::AnyOf,
7159 Some(source_indices),
7160 );
7161 }
7162
7163 let mut variants = Vec::new();
7165
7166 for (schema, original_index) in
7167 any_of_schemas.iter().zip(source_indices.iter().copied())
7168 {
7169 if let Some(ref_str) = schema.reference() {
7170 if let Some(target) = self.extract_schema_name(ref_str) {
7171 dependencies.insert(target.to_string());
7172 variants.push(SchemaRef {
7173 target: target.to_string(),
7174 nullable: self.schema_or_reference_is_nullable(schema),
7175 });
7176 }
7177 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object))
7178 || schema.inferred_type() == Some(OpenApiSchemaType::Object)
7179 {
7180 let inline_type_name = self.generate_inline_type_name(schema, original_index);
7182
7183 let inline_type_name = self.add_inline_union_branch_schema(
7185 &inline_type_name,
7186 schema,
7187 dependencies,
7188 context_name,
7189 InlineUnionKind::AnyOf,
7190 original_index,
7191 None,
7192 )?;
7193
7194 variants.push(SchemaRef {
7195 target: inline_type_name,
7196 nullable: false,
7197 });
7198 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Array)) {
7199 let preferred_array_type_name =
7201 if let Some(items_schema) = schema.details().item_schema() {
7202 if let Some(ref_str) = items_schema.reference() {
7203 if let Some(item_type_name) = self.extract_schema_name(ref_str) {
7204 dependencies.insert(item_type_name.to_string());
7205 format!("{item_type_name}Array")
7206 } else {
7207 self.generate_context_aware_name(
7208 context_name,
7209 "Array",
7210 original_index,
7211 Some(schema),
7212 )
7213 }
7214 } else {
7215 self.generate_context_aware_name(
7216 context_name,
7217 "Array",
7218 original_index,
7219 Some(schema),
7220 )
7221 }
7222 } else {
7223 self.generate_context_aware_name(
7224 context_name,
7225 "Array",
7226 original_index,
7227 Some(schema),
7228 )
7229 };
7230 let array_type_name = self.allocate_inline_union_branch_name(
7231 &preferred_array_type_name,
7232 context_name,
7233 InlineUnionKind::AnyOf,
7234 original_index,
7235 None,
7236 schema,
7237 );
7238 let array_type =
7240 self.analyze_array_schema(schema, context_name, dependencies)?;
7241
7242 self.resolved_cache.insert(
7244 array_type_name.clone(),
7245 AnalyzedSchema {
7246 name: array_type_name.clone(),
7247 original: serde_json::to_value(schema).unwrap_or(Value::Null),
7248 schema_type: array_type,
7249 dependencies: HashSet::new(),
7250 nullable: false,
7251 description: Some("Array variant in union".to_string()),
7252 default: None,
7253 },
7254 );
7255
7256 dependencies.insert(array_type_name.clone());
7258
7259 variants.push(SchemaRef {
7260 target: array_type_name,
7261 nullable: false,
7262 });
7263 } else if let Some(schema_type) = schema.schema_type() {
7264 let primitive_unions = self
7274 .type_mapper
7275 .config_shape_primitive_unions()
7276 .unwrap_or(true);
7277
7278 if primitive_unions {
7279 variants.push(SchemaRef {
7280 target: self
7281 .openapi_type_to_rust_type(schema_type.clone(), schema.details()),
7282 nullable: false,
7283 });
7284 } else {
7285 let preferred_inline_type_name = match schema_type {
7286 OpenApiSchemaType::String => {
7287 if original_index == 0 {
7288 format!("{context_name}String")
7289 } else {
7290 format!("{context_name}StringVariant{original_index}")
7291 }
7292 }
7293 OpenApiSchemaType::Number => {
7294 if original_index == 0 {
7295 format!("{context_name}Number")
7296 } else {
7297 format!("{context_name}NumberVariant{original_index}")
7298 }
7299 }
7300 OpenApiSchemaType::Integer => {
7301 if original_index == 0 {
7302 format!("{context_name}Integer")
7303 } else {
7304 format!("{context_name}IntegerVariant{original_index}")
7305 }
7306 }
7307 OpenApiSchemaType::Boolean => {
7308 if original_index == 0 {
7309 format!("{context_name}Boolean")
7310 } else {
7311 format!("{context_name}BooleanVariant{original_index}")
7312 }
7313 }
7314 _ => format!("{context_name}Variant{original_index}"),
7315 };
7316 let inline_type_name = self.allocate_inline_union_branch_name(
7317 &preferred_inline_type_name,
7318 context_name,
7319 InlineUnionKind::AnyOf,
7320 original_index,
7321 None,
7322 schema,
7323 );
7324
7325 let rust_type =
7326 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
7327
7328 self.resolved_cache.insert(
7329 inline_type_name.clone(),
7330 AnalyzedSchema {
7331 name: inline_type_name.clone(),
7332 original: serde_json::to_value(schema).unwrap_or(Value::Null),
7333 schema_type: SchemaType::Primitive {
7334 rust_type,
7335 serde_with: None,
7336 },
7337 dependencies: HashSet::new(),
7338 nullable: false,
7339 description: schema.details().description.clone(),
7340 default: None,
7341 },
7342 );
7343
7344 dependencies.insert(inline_type_name.clone());
7345
7346 variants.push(SchemaRef {
7347 target: inline_type_name,
7348 nullable: false,
7349 });
7350 }
7351 } else {
7352 let inline_type_name = self.generate_context_aware_name(
7360 context_name,
7361 "InlineVariant",
7362 original_index,
7363 Some(schema),
7364 );
7365 let inline_type_name = self.add_inline_union_branch_schema(
7366 &inline_type_name,
7367 schema,
7368 dependencies,
7369 context_name,
7370 InlineUnionKind::AnyOf,
7371 original_index,
7372 None,
7373 )?;
7374 dependencies.insert(inline_type_name.clone());
7375 variants.push(SchemaRef {
7376 target: inline_type_name,
7377 nullable: false,
7378 });
7379 }
7380 }
7381
7382 if !variants.is_empty() {
7383 return Ok(SchemaType::Union {
7384 variants,
7385 exclusive: false,
7386 });
7387 }
7388 }
7389
7390 let all_strings = any_of_schemas.iter().all(|schema| {
7392 matches!(schema.schema_type(), Some(OpenApiSchemaType::String))
7393 || schema.details().const_value.is_some()
7394 });
7395
7396 if all_strings {
7397 let mut enum_values = Vec::new();
7399 let mut has_open_string = false;
7400
7401 for schema in any_of_schemas {
7402 match schema
7407 .details()
7408 .string_enum_values()
7409 .filter(|values| !values.is_empty())
7410 {
7411 Some(values) => {
7412 for value in values {
7413 if !enum_values.contains(&value) {
7414 enum_values.push(value);
7415 }
7416 }
7417 }
7418 None => {
7419 if matches!(schema.schema_type(), Some(OpenApiSchemaType::String)) {
7420 has_open_string = true;
7421 }
7422 }
7423 }
7424 }
7425
7426 if !enum_values.is_empty() {
7427 if has_open_string {
7428 return Ok(SchemaType::ExtensibleEnum {
7431 known_values: enum_values,
7432 });
7433 } else {
7434 return Ok(SchemaType::StringEnum {
7436 values: enum_values,
7437 });
7438 }
7439 }
7440 }
7441
7442 Ok(self.untyped_value(
7444 self.untyped_context(""),
7445 UntypedReason::UnrepresentableUnion,
7446 ))
7447 }
7448
7449 fn find_recursive_anchor_schema(&self) -> Option<String> {
7451 for (schema_name, schema) in &self.schemas {
7453 let details = schema.details();
7454 if details.recursive_anchor == Some(true) {
7455 return Some(schema_name.clone());
7456 }
7457 }
7458
7459 None
7463 }
7464
7465 fn should_use_dynamic_json(&self, schema: &Schema) -> bool {
7468 if let Schema::AnyOf { any_of, .. } = schema {
7470 if any_of.len() == 2 {
7471 let has_null = any_of
7472 .iter()
7473 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)));
7474 let has_empty_object = any_of.iter().any(|s| self.is_dynamic_object_pattern(s));
7475
7476 if has_null && has_empty_object {
7477 return true;
7478 }
7479 }
7480 }
7481
7482 self.is_dynamic_object_pattern(schema)
7484 }
7485
7486 fn is_dynamic_object_pattern(&self, schema: &Schema) -> bool {
7488 let is_object = match schema.schema_type() {
7490 Some(OpenApiSchemaType::Object) => true,
7491 None => schema.inferred_type() == Some(OpenApiSchemaType::Object),
7492 _ => false,
7493 };
7494
7495 if !is_object {
7496 return false;
7497 }
7498
7499 Self::object_shape_is_unconstrained(schema.details())
7500 }
7501
7502 fn object_shape_is_unconstrained(details: &crate::openapi::SchemaDetails) -> bool {
7510 if details.additional_properties.is_some() {
7515 return false;
7516 }
7517
7518 let no_properties = details
7520 .properties
7521 .as_ref()
7522 .map(|props| props.is_empty())
7523 .unwrap_or(true);
7524
7525 if no_properties {
7526 let has_structural_constraints = details
7529 .required
7530 .as_ref()
7531 .map(|req| !req.is_empty())
7532 .unwrap_or(false)
7533 || details.pattern_properties.is_some()
7534 || details.property_names.is_some()
7535 || details.min_properties.is_some()
7536 || details.max_properties.is_some()
7537 || details.dependent_required.is_some()
7538 || details.dependent_schemas.is_some()
7539 || details.if_schema.is_some()
7540 || details.then_schema.is_some()
7541 || details.else_schema.is_some();
7542
7543 return !has_structural_constraints;
7544 }
7545
7546 false
7547 }
7548
7549 fn analyze_operations(&mut self, analysis: &mut SchemaAnalysis) -> Result<()> {
7551 let spec: crate::openapi::OpenApiSpec = parse_spec_document(&self.openapi_spec)?;
7552 let mut canonical_operation_ids = HashSet::new();
7557
7558 if let Some(paths) = &spec.paths {
7559 for (path, path_item) in paths {
7560 let resolved = self.resolve_path_item(path_item, &spec)?;
7562 let pi: &crate::openapi::PathItem = resolved.as_ref().unwrap_or(path_item);
7563 self.ingest_path_item_operations(path, pi, analysis, &mut canonical_operation_ids)?;
7564 }
7565 }
7566 if let Some(webhooks) = &spec.webhooks {
7573 for (name, path_item) in webhooks {
7574 let synthetic_path = format!("/__webhook__/{name}");
7575 self.ingest_path_item_operations(
7576 &synthetic_path,
7577 path_item,
7578 analysis,
7579 &mut canonical_operation_ids,
7580 )?;
7581 }
7582 }
7583 Ok(())
7584 }
7585
7586 fn resolve_path_item(
7590 &self,
7591 path_item: &crate::openapi::PathItem,
7592 spec: &crate::openapi::OpenApiSpec,
7593 ) -> Result<Option<crate::openapi::PathItem>> {
7594 let Some(reference) = &path_item.reference else {
7595 return Ok(None);
7596 };
7597 let target_name = reference
7598 .strip_prefix("#/components/pathItems/")
7599 .ok_or_else(|| {
7600 GeneratorError::UnresolvedReference(format!(
7601 "Path Item $ref must point at #/components/pathItems/{{name}}, got {reference}"
7602 ))
7603 })?;
7604 let pi = spec
7605 .components
7606 .as_ref()
7607 .and_then(|c| c.path_items.as_ref())
7608 .and_then(|map| map.get(target_name))
7609 .ok_or_else(|| {
7610 GeneratorError::UnresolvedReference(format!(
7611 "Path Item ref {reference} not found in components/pathItems"
7612 ))
7613 })?;
7614 Ok(Some(pi.clone()))
7615 }
7616
7617 fn ingest_path_item_operations(
7618 &mut self,
7619 path: &str,
7620 path_item: &crate::openapi::PathItem,
7621 analysis: &mut SchemaAnalysis,
7622 canonical_operation_ids: &mut HashSet<String>,
7623 ) -> Result<()> {
7624 for (method, operation) in path_item.operations() {
7625 let raw_operation_id = operation
7627 .operation_id
7628 .clone()
7629 .unwrap_or_else(|| Self::generate_operation_id(method, path));
7630
7631 let operation_id = if canonical_operation_ids
7642 .contains(&Self::canonical_operation_id(&raw_operation_id))
7643 {
7644 let method_lower = method.to_lowercase();
7645 let mut candidate = format!("{}_{}", raw_operation_id, method_lower);
7646 let mut suffix = 2;
7647 while canonical_operation_ids.contains(&Self::canonical_operation_id(&candidate)) {
7648 candidate = format!("{}_{}_{}", raw_operation_id, method_lower, suffix);
7649 suffix += 1;
7650 }
7651 eprintln!(
7652 "⚠️ duplicate operationId `{}` at `{} {}` — disambiguated to `{}`",
7653 raw_operation_id, method, path, candidate
7654 );
7655 candidate
7656 } else {
7657 raw_operation_id.clone()
7658 };
7659
7660 let (op_info, responses) = self.analyze_single_operation(
7661 &operation_id,
7662 method,
7663 path,
7664 operation,
7665 path_item.parameters.as_ref(),
7666 analysis,
7667 )?;
7668 analysis
7669 .operation_id_aliases
7670 .entry(raw_operation_id)
7671 .or_default()
7672 .push(operation_id.clone());
7673 canonical_operation_ids.insert(Self::canonical_operation_id(&operation_id));
7674 analysis
7675 .operation_responses
7676 .insert(operation_id.clone(), responses);
7677 analysis.operations.insert(operation_id, op_info);
7678 }
7679 Ok(())
7680 }
7681
7682 fn canonical_operation_id(operation_id: &str) -> String {
7683 use heck::ToPascalCase;
7684 operation_id.replace('.', "_").to_pascal_case()
7685 }
7686
7687 fn generate_operation_id(method: &str, path: &str) -> String {
7690 let mut operation_id = method.to_lowercase();
7692
7693 let path_parts: Vec<&str> = path.trim_start_matches('/').split('/').collect();
7695
7696 for part in path_parts {
7697 if part.is_empty() {
7698 continue;
7699 }
7700
7701 let cleaned_part = if part.starts_with('{') && part.ends_with('}') {
7703 &part[1..part.len() - 1]
7704 } else {
7705 part
7706 };
7707
7708 let pascal_case_part = cleaned_part
7710 .split(&['-', '_'][..])
7711 .map(|s| {
7712 let mut chars = s.chars();
7713 match chars.next() {
7714 None => String::new(),
7715 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
7716 }
7717 })
7718 .collect::<String>();
7719
7720 operation_id.push_str(&pascal_case_part);
7721 }
7722
7723 operation_id
7724 }
7725
7726 fn analyze_single_operation(
7728 &mut self,
7729 operation_id: &str,
7730 method: &str,
7731 path: &str,
7732 operation: &crate::openapi::Operation,
7733 path_item_parameters: Option<&Vec<crate::openapi::Parameter>>,
7734 _analysis: &mut SchemaAnalysis,
7735 ) -> Result<(OperationInfo, BTreeMap<String, OperationResponse>)> {
7736 let raw_path_item = self
7737 .openapi_spec
7738 .get("paths")
7739 .and_then(|paths| paths.get(path))
7740 .cloned();
7741 let raw_operation = raw_path_item
7742 .as_ref()
7743 .and_then(|path_item| path_item.get(method.to_ascii_lowercase()))
7744 .cloned();
7745 let request_body = operation
7746 .request_body
7747 .as_ref()
7748 .map(|request_body| self.resolve_request_body(request_body))
7749 .transpose()?;
7750 let mut op_info = OperationInfo {
7751 operation_id: operation_id.to_string(),
7752 method: method.to_uppercase(),
7753 path: normalize_operation_path(path),
7754 summary: operation.summary.clone(),
7755 description: operation.description.clone(),
7756 request_body: None,
7757 request_body_required: request_body
7759 .as_ref()
7760 .and_then(|rb| rb.required)
7761 .unwrap_or(false),
7762 response_schemas: BTreeMap::new(),
7763 parameters: Vec::new(),
7764 supports_streaming: false, stream_parameter: None, tags: operation.tags.clone().unwrap_or_default(),
7767 };
7768 let mut operation_responses = BTreeMap::new();
7769
7770 if let Some(request_body) = &request_body {
7772 use crate::openapi::{
7773 is_binary_media_type, is_form_urlencoded_media_type, is_json_media_type,
7774 media_type_essence,
7775 };
7776 if let Some((content_type, maybe_schema)) = request_body.best_content() {
7777 op_info.request_body = if is_json_media_type(content_type) {
7778 match maybe_schema {
7779 Some(s) => {
7780 let validation_schema = self
7781 .raw_request_body_schema(raw_operation.as_ref(), content_type)
7782 .unwrap_or(
7783 serde_json::to_value(s).map_err(GeneratorError::ParseError)?,
7784 );
7785 Some(
7786 self.resolve_or_inline_schema(s, operation_id, "Request")
7787 .map(|name| RequestBodyContent::Json {
7788 schema_name: name,
7789 media_type: content_type.to_string(),
7790 validation_schema,
7791 })?,
7792 )
7793 }
7794 None => Some(RequestBodyContent::SchemaLess {
7795 media_type: content_type.to_string(),
7796 }),
7797 }
7798 } else if is_form_urlencoded_media_type(content_type) {
7799 match maybe_schema {
7800 Some(s) => {
7801 let validation_schema = self
7802 .raw_request_body_schema(raw_operation.as_ref(), content_type)
7803 .unwrap_or(
7804 serde_json::to_value(s).map_err(GeneratorError::ParseError)?,
7805 );
7806 Some(
7807 self.resolve_or_inline_schema(s, operation_id, "Request")
7808 .map(|name| RequestBodyContent::FormUrlEncoded {
7809 schema_name: name,
7810 media_type: content_type.to_string(),
7811 validation_schema,
7812 })?,
7813 )
7814 }
7815 None => Some(RequestBodyContent::SchemaLess {
7816 media_type: content_type.to_string(),
7817 }),
7818 }
7819 } else if media_type_essence(content_type)
7820 .eq_ignore_ascii_case("multipart/form-data")
7821 {
7822 match maybe_schema {
7823 Some(schema) => {
7824 let validation_schema = self
7825 .raw_request_body_schema(raw_operation.as_ref(), content_type)
7826 .unwrap_or(
7827 serde_json::to_value(schema)
7828 .map_err(GeneratorError::ParseError)?,
7829 );
7830 Some(
7831 self.resolve_or_inline_schema(schema, operation_id, "Request")
7832 .map(|schema_name| RequestBodyContent::Multipart {
7833 schema_name,
7834 media_type: content_type.to_string(),
7835 validation_schema,
7836 })?,
7837 )
7838 }
7839 None => Some(RequestBodyContent::SchemaLess {
7840 media_type: content_type.to_string(),
7841 }),
7842 }
7843 } else if is_binary_media_type(content_type, maybe_schema) {
7844 if media_type_essence(content_type)
7845 .eq_ignore_ascii_case("application/octet-stream")
7846 {
7847 Some(RequestBodyContent::OctetStream {
7848 media_type: content_type.to_string(),
7849 })
7850 } else {
7851 Some(RequestBodyContent::Binary {
7852 media_type: content_type.to_string(),
7853 })
7854 }
7855 } else if crate::openapi::is_text_media_type(content_type) {
7856 Some(RequestBodyContent::TextPlain {
7861 media_type: content_type.to_string(),
7862 })
7863 } else {
7864 None
7865 };
7866 }
7867 if op_info.request_body.is_none() {
7868 let mut media_types = request_body
7869 .content
7870 .as_ref()
7871 .map(|content| content.keys().cloned().collect::<Vec<_>>())
7872 .unwrap_or_default();
7873 media_types.sort();
7874 if !media_types.is_empty() {
7875 op_info.request_body = Some(RequestBodyContent::Unsupported { media_types });
7876 }
7877 }
7878 }
7879
7880 if let Some(responses) = &operation.responses {
7882 for (status_code, response) in responses {
7883 let response = self.resolve_response(response)?;
7884 let supports_streaming = response.content.as_ref().is_some_and(|content| {
7890 content
7891 .keys()
7892 .any(|ct| crate::openapi::is_event_stream_media_type(ct))
7893 });
7894 if supports_streaming {
7895 op_info.supports_streaming = true;
7896 }
7897
7898 let mut response_info = OperationResponse {
7899 supports_streaming,
7900 has_content: response
7901 .content
7902 .as_ref()
7903 .is_some_and(|content| !content.is_empty()),
7904 ..Default::default()
7905 };
7906 if let Some((media_type, schema)) = response.json_content() {
7907 if let Some(schema_ref) = schema.reference() {
7908 if let Some(schema_name) = self.extract_schema_name(schema_ref) {
7910 op_info
7911 .response_schemas
7912 .insert(status_code.clone(), schema_name.to_string());
7913 response_info.schema_name = Some(schema_name.to_string());
7914 response_info.media_type = Some(media_type.to_string());
7915 response_info.body = Some(OperationResponseBody::Json {
7916 schema_name: schema_name.to_string(),
7917 media_type: media_type.to_string(),
7918 });
7919 }
7920 } else {
7921 let synthetic_name =
7923 self.generate_inline_response_type_name(operation_id, status_code);
7924
7925 let mut deps = HashSet::new();
7927 let synthetic_name =
7928 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
7929
7930 op_info
7931 .response_schemas
7932 .insert(status_code.clone(), synthetic_name.clone());
7933 response_info.body = Some(OperationResponseBody::Json {
7934 schema_name: synthetic_name.clone(),
7935 media_type: media_type.to_string(),
7936 });
7937 response_info.schema_name = Some(synthetic_name);
7938 response_info.media_type = Some(media_type.to_string());
7939 }
7940 }
7941 if response_info.body.is_none()
7942 && let Some(content) = response.content.as_ref()
7943 {
7944 let selected = content
7945 .iter()
7946 .find(|(media_type, media)| {
7947 matches!(
7948 crate::openapi::classify_response_media_type(
7949 media_type,
7950 media.schema.as_ref()
7951 ),
7952 crate::openapi::ResponseMediaKind::Text
7953 )
7954 })
7955 .or_else(|| {
7956 content.iter().find(|(media_type, media)| {
7957 matches!(
7958 crate::openapi::classify_response_media_type(
7959 media_type,
7960 media.schema.as_ref()
7961 ),
7962 crate::openapi::ResponseMediaKind::Binary
7963 ) && !crate::openapi::is_wildcard_media_type(media_type)
7964 })
7965 })
7966 .or_else(|| {
7967 content.iter().find(|(media_type, media)| {
7968 matches!(
7969 crate::openapi::classify_response_media_type(
7970 media_type,
7971 media.schema.as_ref()
7972 ),
7973 crate::openapi::ResponseMediaKind::Binary
7974 )
7975 })
7976 });
7977 if let Some((media_type, media)) = selected {
7978 response_info.body = match crate::openapi::classify_response_media_type(
7979 media_type,
7980 media.schema.as_ref(),
7981 ) {
7982 crate::openapi::ResponseMediaKind::Text => {
7983 Some(OperationResponseBody::Text {
7984 media_type: media_type.clone(),
7985 })
7986 }
7987 crate::openapi::ResponseMediaKind::Binary => {
7988 Some(OperationResponseBody::Binary {
7989 media_type: media_type.clone(),
7990 wildcard: crate::openapi::is_wildcard_media_type(media_type),
7991 })
7992 }
7993 _ => None,
7994 };
7995 }
7996 }
7997 response_info.unsupported_media_types = response
7998 .content
7999 .as_ref()
8000 .into_iter()
8001 .flat_map(|content| content.iter())
8002 .filter(|(media_type, content)| {
8003 match crate::openapi::classify_response_media_type(
8004 media_type,
8005 content.schema.as_ref(),
8006 ) {
8007 crate::openapi::ResponseMediaKind::Json => content.schema.is_none(),
8008 crate::openapi::ResponseMediaKind::Unsupported => true,
8009 crate::openapi::ResponseMediaKind::EventStream
8010 | crate::openapi::ResponseMediaKind::Text
8011 | crate::openapi::ResponseMediaKind::Binary => false,
8012 }
8013 })
8014 .map(|(media_type, _)| media_type.clone())
8015 .collect();
8016 operation_responses.insert(status_code.clone(), response_info);
8017 }
8018 }
8019
8020 if op_info.supports_streaming
8023 && let Some(parameters) = &operation.parameters
8024 {
8025 for param in parameters {
8026 if let Some(name) = param.name.as_deref() {
8027 if name.eq_ignore_ascii_case("stream") {
8028 op_info.stream_parameter = Some(name.to_string());
8029 break;
8030 }
8031 }
8032 }
8033 }
8034
8035 if let Some(parameters) = &operation.parameters {
8037 for (index, param) in parameters.iter().enumerate() {
8038 let resolved = self.resolve_parameter(param).into_owned();
8042 let validation_schema = raw_operation
8043 .as_ref()
8044 .and_then(|operation| operation.get("parameters"))
8045 .and_then(Value::as_array)
8046 .and_then(|parameters| parameters.get(index))
8047 .and_then(|parameter| self.raw_parameter_schema(parameter));
8048 if let Some(param_info) =
8049 self.analyze_parameter(&resolved, operation_id, validation_schema)?
8050 {
8051 op_info.parameters.push(param_info);
8052 }
8053 }
8054 }
8055
8056 if let Some(path_params) = path_item_parameters {
8058 let existing_keys: std::collections::HashSet<(String, String)> = op_info
8059 .parameters
8060 .iter()
8061 .map(|p| (p.name.clone(), p.location.clone()))
8062 .collect();
8063 for (index, param) in path_params.iter().enumerate() {
8064 let resolved = self.resolve_parameter(param).into_owned();
8065 let validation_schema = raw_path_item
8066 .as_ref()
8067 .and_then(|path_item| path_item.get("parameters"))
8068 .and_then(Value::as_array)
8069 .and_then(|parameters| parameters.get(index))
8070 .and_then(|parameter| self.raw_parameter_schema(parameter));
8071 if let Some(param_info) =
8072 self.analyze_parameter(&resolved, operation_id, validation_schema)?
8073 {
8074 if !existing_keys
8075 .contains(&(param_info.name.clone(), param_info.location.clone()))
8076 {
8077 op_info.parameters.push(param_info);
8078 }
8079 }
8080 }
8081 }
8082
8083 let mut declared_path_names: std::collections::HashSet<String> = op_info
8091 .parameters
8092 .iter()
8093 .filter(|p| p.location == "path")
8094 .map(|p| p.name.clone())
8095 .collect();
8096 let bytes = path.as_bytes().iter();
8097 let mut current = String::new();
8098 let mut in_brace = false;
8099 let mut synthesized: Vec<String> = Vec::new();
8100 for b in bytes {
8101 match *b {
8102 b'{' => {
8103 in_brace = true;
8104 current.clear();
8105 }
8106 b'}' if in_brace => {
8107 in_brace = false;
8108 if !current.is_empty() && !declared_path_names.contains(¤t) {
8109 synthesized.push(current.clone());
8110 declared_path_names.insert(current.clone());
8111 }
8112 }
8113 _ if in_brace => current.push(*b as char),
8114 _ => {}
8115 }
8116 }
8117 for name in synthesized {
8118 eprintln!(
8119 "⚠️ path `{}` references `{{{}}}` but the spec doesn't declare it as a parameter — synthesizing as required String",
8120 path, name
8121 );
8122 op_info.parameters.push(ParameterInfo {
8123 name,
8124 location: "path".to_string(),
8125 required: true,
8126 schema_ref: None,
8127 rust_type: "String".to_string(),
8128 description: None,
8129 enum_values: None,
8130 enum_varnames: None,
8131 rust_ident: None,
8132 query_serialization: None,
8133 validation_schema: None,
8134 });
8135 }
8136
8137 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
8145 for p in op_info.parameters.iter_mut() {
8146 let raw = base_param_ident(&p.name);
8147 let mut chosen = raw.clone();
8148 let mut suffix = 2;
8149 while !used.insert(chosen.clone()) {
8150 chosen = format!("{raw}_{suffix}");
8151 suffix += 1;
8152 }
8153 p.rust_ident = Some(chosen);
8154 }
8155
8156 Ok((op_info, operation_responses))
8157 }
8158
8159 fn resolve_request_body(
8161 &self,
8162 request_body: &crate::openapi::RequestBody,
8163 ) -> Result<crate::openapi::RequestBody> {
8164 let mut current = request_body.clone();
8165 let mut visited = HashSet::new();
8166 while let Some(reference) = current.reference.clone() {
8167 if !visited.insert(reference.clone()) {
8168 return Err(GeneratorError::CircularDependency(format!(
8169 "request body reference {reference}"
8170 )));
8171 }
8172
8173 let pointer = reference.strip_prefix('#').ok_or_else(|| {
8174 GeneratorError::UnresolvedReference(format!(
8175 "external request body reference `{reference}` is not supported"
8176 ))
8177 })?;
8178 if !pointer.is_empty() && !pointer.starts_with('/') {
8179 return Err(GeneratorError::UnresolvedReference(format!(
8180 "request body reference `{reference}` is not a local JSON Pointer"
8181 )));
8182 }
8183 let value = self.openapi_spec.pointer(pointer).ok_or_else(|| {
8184 GeneratorError::UnresolvedReference(format!(
8185 "request body reference `{reference}` does not exist"
8186 ))
8187 })?;
8188 let object = value.as_object().ok_or_else(|| {
8189 GeneratorError::InvalidSchema(format!(
8190 "request body reference `{reference}` must target an object"
8191 ))
8192 })?;
8193 if !["$ref", "description", "required", "content"]
8194 .iter()
8195 .any(|field| object.contains_key(*field))
8196 {
8197 return Err(GeneratorError::InvalidSchema(format!(
8198 "request body reference `{reference}` does not target a structurally compatible OpenAPI Request Body Object"
8199 )));
8200 }
8201 current = serde_json::from_value(value.clone()).map_err(|error| {
8202 GeneratorError::InvalidSchema(format!(
8203 "request body reference `{reference}` is not a valid OpenAPI Request Body Object: {error}"
8204 ))
8205 })?;
8206 }
8207 Ok(current)
8208 }
8209
8210 fn resolve_response(
8217 &self,
8218 response: &crate::openapi::Response,
8219 ) -> Result<crate::openapi::Response> {
8220 let mut current = response.clone();
8221 let mut visited = HashSet::new();
8222 while let Some(reference) = current.reference.clone() {
8223 if !visited.insert(reference.clone()) {
8224 return Err(GeneratorError::CircularDependency(format!(
8225 "response reference {reference}"
8226 )));
8227 }
8228
8229 let pointer = reference.strip_prefix('#').ok_or_else(|| {
8230 GeneratorError::UnresolvedReference(format!(
8231 "external response reference `{reference}` is not supported"
8232 ))
8233 })?;
8234 if !pointer.is_empty() && !pointer.starts_with('/') {
8235 return Err(GeneratorError::UnresolvedReference(format!(
8236 "response reference `{reference}` is not a local JSON Pointer"
8237 )));
8238 }
8239 let value = self.openapi_spec.pointer(pointer).ok_or_else(|| {
8240 GeneratorError::UnresolvedReference(format!(
8241 "response reference `{reference}` does not exist"
8242 ))
8243 })?;
8244 let object = value.as_object().ok_or_else(|| {
8245 GeneratorError::InvalidSchema(format!(
8246 "response reference `{reference}` must target an object"
8247 ))
8248 })?;
8249 if !["$ref", "description", "headers", "content", "links"]
8250 .iter()
8251 .any(|field| object.contains_key(*field))
8252 {
8253 return Err(GeneratorError::InvalidSchema(format!(
8254 "response reference `{reference}` does not target a structurally compatible OpenAPI Response Object"
8255 )));
8256 }
8257 current = serde_json::from_value(value.clone()).map_err(|error| {
8258 GeneratorError::InvalidSchema(format!(
8259 "response reference `{reference}` is not a valid OpenAPI Response Object: {error}"
8260 ))
8261 })?;
8262 }
8263 Ok(current)
8264 }
8265
8266 fn generate_inline_response_type_name(&self, operation_id: &str, status_code: &str) -> String {
8273 use heck::ToPascalCase;
8274 let base_name = operation_id.replace('.', "_").to_pascal_case();
8275 let suffix = Self::status_code_suffix(status_code);
8276 format!("{}Response{}", base_name, suffix)
8277 }
8278
8279 fn status_code_suffix(status_code: &str) -> String {
8286 match status_code {
8287 "" | "200" => String::new(),
8288 "default" | "Default" => "Default".to_string(),
8289 other if other.chars().all(|c| c.is_ascii_digit()) => other.to_string(),
8290 other => other.to_ascii_lowercase(),
8291 }
8292 }
8293
8294 fn generate_inline_request_type_name(&self, operation_id: &str) -> String {
8296 use heck::ToPascalCase;
8297 let base_name = operation_id.replace('.', "_").to_pascal_case();
8301 format!("{}Request", base_name)
8302 }
8303
8304 fn resolve_or_inline_schema(
8307 &mut self,
8308 schema: &crate::openapi::Schema,
8309 operation_id: &str,
8310 suffix: &str,
8311 ) -> Result<String> {
8312 if let Some(schema_ref) = schema.reference()
8313 && let Some(schema_name) = self.extract_schema_name(schema_ref)
8314 {
8315 return Ok(schema_name.to_string());
8316 }
8317 let synthetic_name = if suffix == "Request" {
8319 self.generate_inline_request_type_name(operation_id)
8320 } else {
8321 self.generate_inline_response_type_name(operation_id, "")
8322 };
8323 let mut deps = HashSet::new();
8324 self.add_inline_schema(&synthetic_name, schema, &mut deps)
8325 }
8326
8327 fn resolve_parameter<'a>(
8330 &'a self,
8331 param: &'a crate::openapi::Parameter,
8332 ) -> std::borrow::Cow<'a, crate::openapi::Parameter> {
8333 if let Some(ref_str) = param.reference.as_deref() {
8334 if let Some(param_name) = ref_str.strip_prefix("#/components/parameters/") {
8335 if let Some(resolved) = self.component_parameters.get(param_name) {
8336 return std::borrow::Cow::Borrowed(resolved);
8337 }
8338 }
8339 }
8340 std::borrow::Cow::Borrowed(param)
8341 }
8342
8343 fn referenced_schema_is_string_enum(&self, name: &str) -> bool {
8356 if self.resolve_cached_schema(name).is_some_and(|schema| {
8357 matches!(
8358 schema.schema_type,
8359 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. }
8360 )
8361 }) {
8362 return true;
8363 }
8364 let Some(schema_value) = self
8365 .openapi_spec
8366 .get("components")
8367 .and_then(|c| c.get("schemas"))
8368 .and_then(|s| s.get(name))
8369 else {
8370 return false;
8371 };
8372 let is_string_type = schema_value
8373 .get("type")
8374 .and_then(|v| v.as_str())
8375 .map(|s| s == "string")
8376 .unwrap_or(false);
8377 let has_enum_or_const =
8378 schema_value.get("enum").is_some() || schema_value.get("const").is_some();
8379 is_string_type && has_enum_or_const
8380 }
8381
8382 fn resolve_raw_local_reference(&self, value: &Value) -> Option<Value> {
8383 let Some(reference) = value.get("$ref").and_then(Value::as_str) else {
8384 return Some(value.clone());
8385 };
8386 let pointer = reference.strip_prefix('#')?;
8387 self.openapi_spec.pointer(pointer).cloned()
8388 }
8389
8390 fn raw_request_body_schema(
8391 &self,
8392 operation: Option<&Value>,
8393 content_type: &str,
8394 ) -> Option<Value> {
8395 let request_body = operation?.get("requestBody")?;
8396 self.resolve_raw_local_reference(request_body)?
8397 .get("content")?
8398 .get(content_type)?
8399 .get("schema")
8400 .cloned()
8401 }
8402
8403 fn raw_parameter_schema(&self, parameter: &Value) -> Option<Value> {
8404 self.resolve_raw_local_reference(parameter)?
8405 .get("schema")
8406 .cloned()
8407 }
8408
8409 fn analyze_parameter(
8410 &mut self,
8411 param: &crate::openapi::Parameter,
8412 operation_id: &str,
8413 raw_validation_schema: Option<Value>,
8414 ) -> Result<Option<ParameterInfo>> {
8415 use heck::ToPascalCase;
8416
8417 let name = param.name.as_deref().unwrap_or("");
8418 let location = param.location.as_deref().unwrap_or("");
8419 let required = param.required.unwrap_or(false);
8420 let validation_schema = match raw_validation_schema {
8421 Some(schema) => Some(schema),
8422 None => param
8423 .schema
8424 .as_ref()
8425 .map(serde_json::to_value)
8426 .transpose()
8427 .map_err(GeneratorError::ParseError)?,
8428 };
8429
8430 let mut rust_type = "String".to_string();
8431 let mut schema_ref = None;
8432 let mut enum_values: Option<Vec<String>> = None;
8433 let mut enum_varnames: Option<Vec<String>> = None;
8434 let mut query_serialization: Option<QuerySerialization> = None;
8435
8436 let is_query = location == "query";
8442 let is_simple_header = location == "header"
8443 && matches!(param.style.as_deref(), None | Some("simple"))
8444 && param.explode != Some(true);
8445 let form_style = matches!(param.style.as_deref(), None | Some("form"));
8446 let form_exploded = form_style && param.explode.unwrap_or(true);
8447 let deep_object =
8448 param.style.as_deref() == Some("deepObject") && param.explode != Some(false);
8449
8450 let object_serialization = if !is_query {
8451 None
8452 } else if deep_object {
8453 Some(QuerySerialization::DeepObject)
8454 } else if form_exploded {
8455 Some(QuerySerialization::FormExplodedObject)
8456 } else if form_style {
8457 Some(QuerySerialization::FormObject)
8458 } else {
8459 None
8460 };
8461
8462 if let Some(schema) = ¶m.schema {
8463 if let Some(ref_str) = schema.reference() {
8464 if let Some(name) = self.extract_schema_name(ref_str) {
8470 if self.referenced_schema_is_string_enum(name) {
8471 schema_ref = Some(name.to_string());
8472 } else if object_serialization.is_some()
8473 && self.referenced_schema_is_object(name)
8474 {
8475 schema_ref = Some(name.to_string());
8476 query_serialization = if form_exploded && self.uses_aws_query_conventions()
8477 {
8478 match self.referenced_array_struct_item_type(name, 1) {
8479 Some(ArrayItemType::NestedStructRef { properties, .. }) => {
8480 Some(QuerySerialization::FormExplodedNestedObject {
8481 properties,
8482 })
8483 }
8484 _ => object_serialization.clone(),
8485 }
8486 } else {
8487 object_serialization.clone()
8488 };
8489 } else if (is_query && form_style || is_simple_header)
8490 && let Some(item_type) = self.referenced_array_param_item_type(name)
8491 {
8492 schema_ref = Some(name.to_string());
8498 query_serialization = Some(if is_simple_header {
8499 QuerySerialization::SimpleHeaderArray { item_type }
8500 } else if form_exploded {
8501 QuerySerialization::FormExplodedArray { item_type }
8502 } else {
8503 QuerySerialization::FormArray { item_type }
8504 });
8505 }
8506 }
8507 } else if object_serialization.is_some() && Self::schema_is_inline_object(schema) {
8508 let op_pascal = operation_id.replace('.', "_").to_pascal_case();
8513 let param_pascal = name.to_pascal_case();
8514 let synthetic_name = format!("{op_pascal}{param_pascal}");
8515 let mut deps = HashSet::new();
8516 let synthetic_name = self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
8517 schema_ref = Some(synthetic_name.clone());
8518 query_serialization = if form_exploded && self.uses_aws_query_conventions() {
8519 match self.referenced_array_struct_item_type(&synthetic_name, 1) {
8520 Some(ArrayItemType::NestedStructRef { properties, .. }) => {
8521 Some(QuerySerialization::FormExplodedNestedObject { properties })
8522 }
8523 _ => object_serialization.clone(),
8524 }
8525 } else {
8526 object_serialization.clone()
8527 };
8528 } else if (is_query && form_style || is_simple_header)
8529 && matches!(
8530 schema.schema_type(),
8531 Some(crate::openapi::SchemaType::Array)
8532 )
8533 && let Some(item_type) = self.array_param_item_type(schema)
8534 {
8535 query_serialization = Some(if is_simple_header {
8543 QuerySerialization::SimpleHeaderArray { item_type }
8544 } else if form_exploded {
8545 QuerySerialization::FormExplodedArray { item_type }
8546 } else {
8547 QuerySerialization::FormArray { item_type }
8548 });
8549 } else if let Some(schema_type) = schema.schema_type() {
8550 let format = schema.details().format.clone();
8556 rust_type = match schema_type {
8557 crate::openapi::SchemaType::Boolean => "bool".to_string(),
8558 crate::openapi::SchemaType::Integer => self.integer_rust_type(schema.details()),
8559 crate::openapi::SchemaType::Number => {
8560 self.type_mapper.number_format(format.as_deref()).rust_type
8561 }
8562 crate::openapi::SchemaType::String => "String".to_string(),
8563 _ => "String".to_string(),
8564 };
8565
8566 if matches!(schema_type, crate::openapi::SchemaType::String) {
8567 let details = schema.details();
8568 if details.is_string_enum() {
8569 if let Some(values) = details.string_enum_values() {
8570 if !values.is_empty() {
8571 let op_pascal = operation_id.replace('.', "_").to_pascal_case();
8572 let param_pascal = name.to_pascal_case();
8573 rust_type = format!("{op_pascal}{param_pascal}");
8574 enum_varnames = details
8579 .extra
8580 .get("x-enum-varnames")
8581 .and_then(Value::as_array)
8582 .map(|raw| {
8583 raw.iter()
8584 .filter_map(Value::as_str)
8585 .map(str::to_owned)
8586 .collect::<Vec<_>>()
8587 })
8588 .filter(|names| names.len() == values.len());
8589 enum_values = Some(values);
8590 }
8591 }
8592 }
8593 }
8594 }
8595
8596 if is_query && query_serialization.is_none() {
8597 let referenced_name = schema
8598 .reference()
8599 .and_then(|reference| self.extract_schema_name(reference));
8600 let is_object = referenced_name
8601 .is_some_and(|name| self.referenced_schema_is_object(name))
8602 || Self::schema_is_inline_object(schema);
8603 let is_array = referenced_name
8604 .is_some_and(|name| self.referenced_schema_is_array(name))
8605 || matches!(
8606 schema.schema_type(),
8607 Some(crate::openapi::SchemaType::Array)
8608 );
8609 let is_composed = referenced_name
8610 .is_some_and(|name| self.referenced_schema_is_composed_query_shape(name));
8611 let reason = if param.style.as_deref() == Some("deepObject")
8612 && param.explode == Some(false)
8613 {
8614 Some("style=deepObject with explode=false is undefined by OpenAPI".to_string())
8615 } else if param.style.as_deref() == Some("deepObject") && !is_object {
8616 Some("style=deepObject is defined only for object query parameters".to_string())
8617 } else if is_object {
8618 Some(format!(
8619 "object query parameters do not support style={}",
8620 param.style.as_deref().unwrap_or("form")
8621 ))
8622 } else if is_array && form_style {
8623 Some(
8624 "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"
8625 .to_string(),
8626 )
8627 } else if is_array {
8628 Some(format!(
8629 "array query parameters do not yet support style={}",
8630 param.style.as_deref().unwrap_or("form")
8631 ))
8632 } else if is_composed {
8633 Some(
8634 "composed or union query schemas cannot be projected to an unambiguous flat wire shape"
8635 .to_string(),
8636 )
8637 } else {
8638 None
8639 };
8640 if let Some(reason) = reason {
8641 query_serialization = Some(QuerySerialization::Unsupported { reason });
8642 }
8643 }
8644 }
8645
8646 Ok(Some(ParameterInfo {
8647 name: name.to_string(),
8648 location: location.to_string(),
8649 required,
8650 schema_ref,
8651 rust_type,
8652 description: param.description.clone(),
8653 enum_values,
8654 enum_varnames,
8655 rust_ident: None,
8656 query_serialization,
8657 validation_schema,
8658 }))
8659 }
8660
8661 fn array_param_item_type(&self, schema: &crate::openapi::Schema) -> Option<ArrayItemType> {
8670 let items = schema.details().item_schema()?;
8671 let unwrapped = unwrap_annotation_allof(items);
8675 if let Some(ref_str) = unwrapped.reference() {
8676 let name = self.extract_schema_name(ref_str)?;
8677 return self
8678 .referenced_array_scalar_item_type(name)
8679 .or_else(|| self.referenced_array_struct_item_type(name, 1));
8680 }
8681 let format = unwrapped.details().format.clone();
8682 let scalar = match unwrapped.schema_type()? {
8683 crate::openapi::SchemaType::String => "String".to_string(),
8684 crate::openapi::SchemaType::Integer => self.integer_rust_type(unwrapped.details()),
8685 crate::openapi::SchemaType::Number => {
8686 self.type_mapper.number_format(format.as_deref()).rust_type
8687 }
8688 crate::openapi::SchemaType::Boolean => "bool".to_string(),
8689 _ => return None,
8690 };
8691 Some(ArrayItemType::Scalar(scalar))
8692 }
8693
8694 fn referenced_array_param_item_type(&self, name: &str) -> Option<ArrayItemType> {
8697 let schema = self.resolve_cached_schema(name)?;
8698 let SchemaType::Array { item_type } = &schema.schema_type else {
8699 return None;
8700 };
8701 self.analyzed_array_item_type(item_type)
8702 }
8703
8704 fn analyzed_array_item_type(&self, item_type: &SchemaType) -> Option<ArrayItemType> {
8705 self.analyzed_array_item_type_at_depth(item_type, 1)
8706 }
8707
8708 fn referenced_array_struct_item_type(
8713 &self,
8714 name: &str,
8715 nested_array_depth: usize,
8716 ) -> Option<ArrayItemType> {
8717 let resolved = self.resolve_cached_schema(name)?;
8718 let SchemaType::Object {
8719 properties,
8720 required,
8721 additional_properties,
8722 ..
8723 } = &resolved.schema_type
8724 else {
8725 return None;
8726 };
8727 if properties.is_empty() || additional_properties.is_open() {
8728 return None;
8729 }
8730 let mut projected = Vec::with_capacity(properties.len());
8731 let mut has_array = false;
8732 for (wire_name, property) in properties {
8733 let value_type = if let Some(scalar) = self.query_scalar_type(&property.schema_type) {
8734 QueryStructPropertyType::Scalar(scalar)
8735 } else {
8736 if nested_array_depth == 0 {
8737 return None;
8738 }
8739 if let Some(array) = self.resolve_query_array_type(&property.schema_type) {
8740 let item_type =
8741 self.analyzed_array_item_type_at_depth(array, nested_array_depth - 1)?;
8742 if matches!(item_type, ArrayItemType::NestedStructRef { .. }) {
8743 return None;
8744 }
8745 has_array = true;
8746 QueryStructPropertyType::Array { item_type }
8747 } else {
8748 has_array = true;
8749 QueryStructPropertyType::Object {
8750 properties: self.query_flat_object_properties(&property.schema_type)?,
8751 }
8752 }
8753 };
8754 projected.push(QueryStructProperty {
8755 wire_name: wire_name.clone(),
8756 required: required.contains(wire_name),
8757 value_type,
8758 });
8759 }
8760 if has_array {
8761 Some(ArrayItemType::NestedStructRef {
8762 schema_name: name.to_string(),
8763 properties: projected,
8764 })
8765 } else {
8766 Some(ArrayItemType::FlatStructRef {
8767 schema_name: name.to_string(),
8768 properties: projected,
8769 })
8770 }
8771 }
8772
8773 fn analyzed_array_item_type_at_depth(
8774 &self,
8775 item_type: &SchemaType,
8776 nested_array_depth: usize,
8777 ) -> Option<ArrayItemType> {
8778 match item_type {
8779 SchemaType::Primitive { rust_type, .. } => {
8780 Some(ArrayItemType::Scalar(rust_type.clone()))
8781 }
8782 SchemaType::Reference { target } => self
8783 .referenced_array_scalar_item_type(target)
8784 .or_else(|| self.referenced_array_struct_item_type(target, nested_array_depth)),
8785 _ => None,
8786 }
8787 }
8788
8789 fn resolve_query_array_type<'a>(
8790 &'a self,
8791 schema_type: &'a SchemaType,
8792 ) -> Option<&'a SchemaType> {
8793 match schema_type {
8794 SchemaType::Array { item_type } => Some(item_type),
8795 SchemaType::Reference { target } => {
8796 let resolved = self.resolve_cached_schema(target)?;
8797 let SchemaType::Array { item_type } = &resolved.schema_type else {
8798 return None;
8799 };
8800 Some(item_type)
8801 }
8802 _ => None,
8803 }
8804 }
8805
8806 fn query_flat_object_properties(
8807 &self,
8808 schema_type: &SchemaType,
8809 ) -> Option<Vec<QueryStructProperty>> {
8810 let schema_type = match schema_type {
8811 SchemaType::Reference { target } => &self.resolve_cached_schema(target)?.schema_type,
8812 other => other,
8813 };
8814 let SchemaType::Object {
8815 properties,
8816 required,
8817 additional_properties,
8818 ..
8819 } = schema_type
8820 else {
8821 return None;
8822 };
8823 if properties.is_empty() || additional_properties.is_open() {
8824 return None;
8825 }
8826 properties
8827 .iter()
8828 .map(|(wire_name, property)| {
8829 Some(QueryStructProperty {
8830 wire_name: wire_name.clone(),
8831 required: required.contains(wire_name),
8832 value_type: QueryStructPropertyType::Scalar(
8833 self.query_scalar_type(&property.schema_type)?,
8834 ),
8835 })
8836 })
8837 .collect()
8838 }
8839
8840 fn query_scalar_type(&self, schema_type: &SchemaType) -> Option<QueryScalarType> {
8841 match schema_type {
8842 SchemaType::Primitive { rust_type, .. } => match rust_type.as_str() {
8843 "String" => Some(QueryScalarType::String),
8844 "bool" => Some(QueryScalarType::Boolean),
8845 value if value.starts_with('i') || value.starts_with('u') => {
8846 Some(QueryScalarType::Integer)
8847 }
8848 value if value.starts_with('f') => Some(QueryScalarType::Number),
8849 "serde_json::Value" => None,
8850 _ => Some(QueryScalarType::String),
8851 },
8852 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. } => {
8853 Some(QueryScalarType::String)
8854 }
8855 SchemaType::Reference { target } => {
8856 let resolved = self.resolve_cached_schema(target)?;
8857 self.query_scalar_type(&resolved.schema_type)
8858 }
8859 _ => None,
8860 }
8861 }
8862
8863 fn referenced_array_scalar_item_type(&self, name: &str) -> Option<ArrayItemType> {
8871 let resolved = self.resolve_cached_schema(name)?;
8872 let supported = match &resolved.schema_type {
8873 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. } => true,
8874 SchemaType::Primitive { .. } => resolved
8875 .original
8876 .get("type")
8877 .is_some_and(Self::query_scalar_type_value),
8878 _ => false,
8879 };
8880 supported.then(|| ArrayItemType::SchemaRef(name.to_string()))
8881 }
8882
8883 fn query_scalar_type_value(value: &Value) -> bool {
8884 const SCALARS: [&str; 4] = ["string", "integer", "number", "boolean"];
8885 if let Some(value) = value.as_str() {
8886 return SCALARS.contains(&value);
8887 }
8888 let Some(values) = value.as_array() else {
8889 return false;
8890 };
8891 if !values.iter().all(Value::is_string) {
8892 return false;
8893 }
8894 let mut non_null = values
8895 .iter()
8896 .filter_map(Value::as_str)
8897 .filter(|value| *value != "null");
8898 let Some(scalar) = non_null.next() else {
8899 return false;
8900 };
8901 non_null.next().is_none() && SCALARS.contains(&scalar)
8902 }
8903
8904 fn referenced_schema_is_object(&self, name: &str) -> bool {
8908 self.resolve_cached_schema(name)
8909 .is_some_and(|schema| matches!(schema.schema_type, SchemaType::Object { .. }))
8910 }
8911
8912 fn referenced_schema_is_array(&self, name: &str) -> bool {
8913 self.resolve_cached_schema(name)
8914 .is_some_and(|schema| matches!(schema.schema_type, SchemaType::Array { .. }))
8915 }
8916
8917 fn referenced_schema_is_composed_query_shape(&self, name: &str) -> bool {
8918 self.resolve_cached_schema(name).is_some_and(|schema| {
8919 matches!(
8920 schema.schema_type,
8921 SchemaType::Composition { .. }
8922 | SchemaType::Union { .. }
8923 | SchemaType::DiscriminatedUnion { .. }
8924 )
8925 })
8926 }
8927
8928 fn resolve_cached_schema(&self, name: &str) -> Option<&AnalyzedSchema> {
8929 let mut current = name;
8930 let mut visited = HashSet::new();
8931 loop {
8932 if !visited.insert(current) {
8933 return None;
8934 }
8935 let schema = self.resolved_cache.get(current)?;
8936 if let SchemaType::Reference { target } = &schema.schema_type {
8937 current = target;
8938 } else {
8939 return Some(schema);
8940 }
8941 }
8942 }
8943
8944 fn schema_is_inline_object(schema: &crate::openapi::Schema) -> bool {
8946 match schema.schema_type() {
8947 Some(crate::openapi::SchemaType::Object) => true,
8948 None => schema.details().properties.is_some(),
8949 _ => false,
8950 }
8951 }
8952}
8953
8954fn pointer_type_name(pointer: &str) -> String {
8965 use heck::ToPascalCase;
8966
8967 pointer
8968 .split('/')
8969 .skip(1)
8970 .filter(|segment| !matches!(*segment, "components" | "schemas" | "properties"))
8971 .map(|segment| {
8972 segment
8973 .replace("~1", "/")
8974 .replace("~0", "~")
8975 .to_pascal_case()
8976 })
8977 .collect::<String>()
8978}
8979
8980fn shared_positional_schema(positions: &[Schema]) -> Option<&Schema> {
8985 let first = positions.first()?;
8986 let key = positional_schema_key(first)?;
8987 positions
8988 .iter()
8989 .skip(1)
8990 .all(|position| positional_schema_key(position).as_deref() == Some(key.as_str()))
8991 .then_some(first)
8992}
8993
8994fn positional_schema_key(schema: &Schema) -> Option<String> {
8995 if let Some(reference) = schema.reference() {
8996 return Some(format!("$ref {reference}"));
8997 }
8998 let details = schema.details();
8999 if details.properties.is_some() || details.enum_values.is_some() || details.items.is_some() {
9000 return None;
9001 }
9002 match schema.schema_type()? {
9003 crate::openapi::SchemaType::Object | crate::openapi::SchemaType::Array => None,
9004 scalar => Some(format!(
9005 "{scalar:?} {}",
9006 details.format.as_deref().unwrap_or_default()
9007 )),
9008 }
9009}
9010
9011fn parse_spec_document(openapi_spec: &Value) -> Result<OpenApiSpec> {
9012 serde_path_to_error::deserialize(openapi_spec).map_err(|error| {
9013 let mut pointer = json_pointer(error.path());
9014 pointer.push_str(&refine_schema_failure(openapi_spec, &pointer));
9018 GeneratorError::ParseErrorAt {
9019 pointer,
9020 message: error.into_inner().to_string(),
9021 }
9022 })
9023}
9024
9025const SUBSCHEMA_KEYWORDS: [&str; 11] = [
9027 "items",
9028 "additionalProperties",
9029 "propertyNames",
9030 "unevaluatedProperties",
9031 "unevaluatedItems",
9032 "contains",
9033 "contentSchema",
9034 "if",
9035 "then",
9036 "else",
9037 "not",
9038];
9039
9040const SUBSCHEMA_LIST_KEYWORDS: [&str; 4] = ["oneOf", "anyOf", "allOf", "prefixItems"];
9042
9043const SUBSCHEMA_MAP_KEYWORDS: [&str; 5] = [
9045 "properties",
9046 "patternProperties",
9047 "dependentSchemas",
9048 "$defs",
9049 "definitions",
9050];
9051
9052const REFINE_PARSE_BUDGET: usize = 20_000;
9056
9057fn refine_schema_failure(openapi_spec: &Value, pointer: &str) -> String {
9065 let Some(path) = pointer.strip_prefix('#') else {
9066 return String::new();
9067 };
9068 let Some(node) = openapi_spec.pointer(path) else {
9069 return String::new();
9070 };
9071 let segments = path.split('/').skip(1).collect::<Vec<_>>();
9072 let last = segments.last().copied().unwrap_or_default();
9073 let parent = segments
9074 .len()
9075 .checked_sub(2)
9076 .map(|index| segments[index])
9077 .unwrap_or_default();
9078
9079 if last == "schema" || holds_schemas(parent) {
9080 return if parses_as_schema(node) {
9081 String::new()
9082 } else {
9083 deepest_schema_failure(node)
9084 };
9085 }
9086
9087 let mut budget = REFINE_PARSE_BUDGET;
9088 locate_failing_schema(node, holds_schemas(last), &mut budget).unwrap_or_default()
9089}
9090
9091fn holds_schemas(key: &str) -> bool {
9094 matches!(key, "schemas" | "$defs" | "definitions")
9095}
9096
9097fn locate_failing_schema(
9105 node: &Value,
9106 children_are_schemas: bool,
9107 budget: &mut usize,
9108) -> Option<String> {
9109 for (segment, key, child) in child_nodes(node) {
9110 if *budget == 0 {
9111 return None;
9112 }
9113 *budget -= 1;
9114 if children_are_schemas || key == "schema" {
9115 if !parses_as_schema(child) {
9116 return Some(format!("/{segment}{}", deepest_schema_failure(child)));
9117 }
9118 continue;
9119 }
9120 if let Some(rest) = locate_failing_schema(child, holds_schemas(key), budget) {
9121 return Some(format!("/{segment}{rest}"));
9122 }
9123 }
9124 None
9125}
9126
9127fn parses_as_schema(node: &Value) -> bool {
9128 Schema::deserialize(node).is_ok()
9129}
9130
9131fn deepest_schema_failure(node: &Value) -> String {
9135 let Some(object) = node.as_object() else {
9136 return String::new();
9137 };
9138
9139 let descend = |segment: String, child: &Value| -> Option<String> {
9140 if parses_as_schema(child) {
9141 return None;
9142 }
9143 Some(format!("/{segment}{}", deepest_schema_failure(child)))
9144 };
9145
9146 for keyword in SUBSCHEMA_KEYWORDS {
9147 if let Some(child) = object.get(keyword)
9148 && let Some(suffix) = descend(escape_pointer_segment(keyword), child)
9149 {
9150 return suffix;
9151 }
9152 }
9153 for keyword in SUBSCHEMA_LIST_KEYWORDS {
9154 if let Some(Value::Array(children)) = object.get(keyword) {
9155 for (index, child) in children.iter().enumerate() {
9156 if let Some(suffix) = descend(
9157 format!("{}/{index}", escape_pointer_segment(keyword)),
9158 child,
9159 ) {
9160 return suffix;
9161 }
9162 }
9163 }
9164 }
9165 for keyword in SUBSCHEMA_MAP_KEYWORDS {
9166 if let Some(Value::Object(children)) = object.get(keyword) {
9167 for (name, child) in children {
9168 if let Some(suffix) = descend(
9169 format!(
9170 "{}/{}",
9171 escape_pointer_segment(keyword),
9172 escape_pointer_segment(name)
9173 ),
9174 child,
9175 ) {
9176 return suffix;
9177 }
9178 }
9179 }
9180 }
9181 String::new()
9182}
9183
9184fn child_nodes(node: &Value) -> Vec<(String, &str, &Value)> {
9190 const DATA_KEYWORDS: [&str; 5] = ["example", "examples", "default", "enum", "const"];
9191
9192 match node {
9193 Value::Object(members) => members
9194 .iter()
9195 .filter(|(name, child)| {
9196 (child.is_object() || child.is_array())
9197 && !name.starts_with("x-")
9198 && !DATA_KEYWORDS.contains(&name.as_str())
9199 })
9200 .map(|(name, child)| (escape_pointer_segment(name), name.as_str(), child))
9201 .collect(),
9202 Value::Array(elements) => elements
9203 .iter()
9204 .enumerate()
9205 .filter(|(_, child)| child.is_object() || child.is_array())
9206 .map(|(index, child)| (index.to_string(), "", child))
9207 .collect(),
9208 _ => Vec::new(),
9209 }
9210}
9211
9212fn escape_pointer_segment(segment: &str) -> String {
9213 segment.replace('~', "~0").replace('/', "~1")
9214}
9215
9216fn json_pointer(path: &serde_path_to_error::Path) -> String {
9220 use serde_path_to_error::Segment;
9221
9222 let mut pointer = String::from("#");
9223 for segment in path.iter() {
9224 match segment {
9225 Segment::Seq { index } => {
9226 pointer.push('/');
9227 pointer.push_str(&index.to_string());
9228 }
9229 Segment::Map { key } | Segment::Enum { variant: key } => {
9230 pointer.push('/');
9231 pointer.push_str(&escape_pointer_segment(key));
9232 }
9233 Segment::Unknown => pointer.push_str("/?"),
9234 }
9235 }
9236 pointer
9237}
9238
9239pub(crate) fn component_schema_name_aliases(openapi_spec: &Value) -> BTreeMap<String, String> {
9240 let Some(schemas) = openapi_spec
9241 .pointer("/components/schemas")
9242 .and_then(Value::as_object)
9243 else {
9244 return BTreeMap::new();
9245 };
9246
9247 let mut names_by_rust_name = BTreeMap::<String, Vec<String>>::new();
9248 for name in schemas.keys() {
9249 names_by_rust_name
9250 .entry(crate::generator::rust_type_name(name))
9251 .or_default()
9252 .push(name.clone());
9253 }
9254
9255 let mut claimed_rust_names = names_by_rust_name.keys().cloned().collect::<HashSet<_>>();
9258 let mut aliases = BTreeMap::new();
9259
9260 for (rust_name, mut names) in names_by_rust_name {
9261 if names.len() < 2 {
9262 continue;
9263 }
9264
9265 names.sort_by_key(|name| (name != &rust_name, name.clone()));
9268 for source_name in names.into_iter().skip(1) {
9269 let mut suffix = 2;
9270 let replacement = loop {
9271 let candidate = format!("{rust_name}{suffix}");
9272 if claimed_rust_names.insert(candidate.clone()) {
9273 break candidate;
9274 }
9275 suffix += 1;
9276 };
9277
9278 aliases.insert(source_name, replacement);
9279 }
9280 }
9281
9282 aliases
9283}
9284
9285fn disambiguate_component_schema_names(openapi_spec: &mut Value) {
9286 let aliases = component_schema_name_aliases(openapi_spec);
9287 if aliases.is_empty() {
9288 return;
9289 }
9290
9291 for (source_name, replacement) in &aliases {
9292 let rust_name = crate::generator::rust_type_name(source_name);
9293 eprintln!(
9294 "⚠️ schema `{source_name}` maps to the existing Rust type `{rust_name}` — disambiguated to `{replacement}`"
9295 );
9296 }
9297
9298 let Some(schemas) = openapi_spec
9299 .pointer_mut("/components/schemas")
9300 .and_then(Value::as_object_mut)
9301 else {
9302 return;
9303 };
9304
9305 let original_schemas = std::mem::take(schemas);
9306 for (name, schema) in original_schemas {
9307 schemas.insert(aliases.get(&name).cloned().unwrap_or(name), schema);
9308 }
9309
9310 rewrite_component_schema_references(openapi_spec, &aliases);
9311}
9312
9313fn disambiguate_analyzed_schema_names(
9314 analysis: &mut SchemaAnalysis,
9315 component_schemas: &BTreeMap<String, Schema>,
9316) {
9317 let mut names_by_rust_name = BTreeMap::<String, Vec<String>>::new();
9318 for name in analysis.schemas.keys() {
9319 names_by_rust_name
9320 .entry(crate::generator::rust_type_name(name))
9321 .or_default()
9322 .push(name.clone());
9323 }
9324
9325 let mut claimed_rust_names = names_by_rust_name.keys().cloned().collect::<HashSet<_>>();
9326 let mut aliases = BTreeMap::<String, String>::new();
9327
9328 for (rust_name, mut names) in names_by_rust_name {
9329 if names.len() < 2 {
9330 continue;
9331 }
9332 names.sort_by_key(|name| {
9333 (
9334 !component_schemas.contains_key(name),
9335 name != &rust_name,
9336 name.clone(),
9337 )
9338 });
9339
9340 for source_name in names.into_iter().skip(1) {
9341 let mut suffix = 2;
9342 let replacement = loop {
9343 let candidate = format!("{rust_name}{suffix}");
9344 if claimed_rust_names.insert(candidate.clone()) {
9345 break candidate;
9346 }
9347 suffix += 1;
9348 };
9349 eprintln!(
9350 "⚠️ generated schema `{source_name}` maps to the existing Rust type `{rust_name}` — disambiguated to `{replacement}`"
9351 );
9352 aliases.insert(source_name, replacement);
9353 }
9354 }
9355
9356 if aliases.is_empty() {
9357 return;
9358 }
9359
9360 let original_schemas = std::mem::take(&mut analysis.schemas);
9361 for (name, mut schema) in original_schemas {
9362 schema.name = renamed_schema_name(&schema.name, &aliases);
9363 schema.dependencies = schema
9364 .dependencies
9365 .into_iter()
9366 .map(|name| renamed_schema_name(&name, &aliases))
9367 .collect();
9368 rewrite_schema_type_names(&mut schema.schema_type, &aliases);
9369 analysis
9370 .schemas
9371 .insert(renamed_schema_name(&name, &aliases), schema);
9372 }
9373
9374 let original_edges = std::mem::take(&mut analysis.dependencies.edges);
9375 for (name, dependencies) in original_edges {
9376 analysis.dependencies.edges.insert(
9377 renamed_schema_name(&name, &aliases),
9378 dependencies
9379 .into_iter()
9380 .map(|name| renamed_schema_name(&name, &aliases))
9381 .collect(),
9382 );
9383 }
9384 analysis.dependencies.recursive_schemas = analysis
9385 .dependencies
9386 .recursive_schemas
9387 .iter()
9388 .map(|name| renamed_schema_name(name, &aliases))
9389 .collect();
9390
9391 analysis.patterns.tagged_enum_schemas = analysis
9392 .patterns
9393 .tagged_enum_schemas
9394 .iter()
9395 .map(|name| renamed_schema_name(name, &aliases))
9396 .collect();
9397 analysis.patterns.untagged_enum_schemas = analysis
9398 .patterns
9399 .untagged_enum_schemas
9400 .iter()
9401 .map(|name| renamed_schema_name(name, &aliases))
9402 .collect();
9403 analysis.patterns.type_mappings = std::mem::take(&mut analysis.patterns.type_mappings)
9404 .into_iter()
9405 .map(|(name, mappings)| {
9406 (
9407 renamed_schema_name(&name, &aliases),
9408 mappings
9409 .into_iter()
9410 .map(|(value, schema_name)| {
9411 (value, renamed_schema_name(&schema_name, &aliases))
9412 })
9413 .collect(),
9414 )
9415 })
9416 .collect();
9417
9418 for operation in analysis.operations.values_mut() {
9419 if let Some(request_body) = &mut operation.request_body {
9420 rewrite_request_body_schema_name(request_body, &aliases);
9421 }
9422 for schema_name in operation.response_schemas.values_mut() {
9423 *schema_name = renamed_schema_name(schema_name, &aliases);
9424 }
9425 for parameter in &mut operation.parameters {
9426 if let Some(schema_name) = &mut parameter.schema_ref {
9427 *schema_name = renamed_schema_name(schema_name, &aliases);
9428 }
9429 if let Some(serialization) = &mut parameter.query_serialization {
9430 rewrite_query_serialization_schema_names(serialization, &aliases);
9431 }
9432 }
9433 }
9434
9435 for responses in analysis.operation_responses.values_mut() {
9436 for response in responses.values_mut() {
9437 if let Some(schema_name) = &mut response.schema_name {
9438 *schema_name = renamed_schema_name(schema_name, &aliases);
9439 }
9440 if let Some(OperationResponseBody::Json { schema_name, .. }) = &mut response.body {
9441 *schema_name = renamed_schema_name(schema_name, &aliases);
9442 }
9443 }
9444 }
9445}
9446
9447fn renamed_schema_name(name: &str, aliases: &BTreeMap<String, String>) -> String {
9448 aliases
9449 .get(name)
9450 .cloned()
9451 .unwrap_or_else(|| name.to_string())
9452}
9453
9454fn rewrite_schema_type_names(schema_type: &mut SchemaType, aliases: &BTreeMap<String, String>) {
9455 match schema_type {
9456 SchemaType::Object {
9457 properties,
9458 additional_properties,
9459 ..
9460 } => {
9461 for property in properties.values_mut() {
9462 rewrite_schema_type_names(&mut property.schema_type, aliases);
9463 }
9464 if let ObjectAdditionalProperties::Typed { value_type } = additional_properties {
9465 rewrite_schema_type_names(value_type, aliases);
9466 }
9467 }
9468 SchemaType::DiscriminatedUnion { variants, .. } => {
9469 for variant in variants {
9470 variant.type_name = renamed_schema_name(&variant.type_name, aliases);
9471 variant.schema_ref = renamed_schema_name(&variant.schema_ref, aliases);
9472 }
9473 }
9474 SchemaType::Union { variants, .. } | SchemaType::Composition { schemas: variants } => {
9475 for variant in variants {
9476 variant.target = renamed_schema_name(&variant.target, aliases);
9477 }
9478 }
9479 SchemaType::Array { item_type } => rewrite_schema_type_names(item_type, aliases),
9480 SchemaType::Nullable { inner_type } => rewrite_schema_type_names(inner_type, aliases),
9481 SchemaType::Untyped { .. } => {}
9482 SchemaType::Tuple { element_types } => {
9483 for element_type in element_types {
9484 rewrite_schema_type_names(element_type, aliases);
9485 }
9486 }
9487 SchemaType::Reference { target } => {
9488 *target = renamed_schema_name(target, aliases);
9489 }
9490 SchemaType::Primitive { .. }
9491 | SchemaType::StringEnum { .. }
9492 | SchemaType::ExtensibleEnum { .. } => {}
9493 }
9494}
9495
9496fn rewrite_request_body_schema_name(
9497 request_body: &mut RequestBodyContent,
9498 aliases: &BTreeMap<String, String>,
9499) {
9500 match request_body {
9501 RequestBodyContent::Json { schema_name, .. }
9502 | RequestBodyContent::FormUrlEncoded { schema_name, .. }
9503 | RequestBodyContent::Multipart { schema_name, .. } => {
9504 *schema_name = renamed_schema_name(schema_name, aliases);
9505 }
9506 _ => {}
9507 }
9508}
9509
9510fn rewrite_query_serialization_schema_names(
9511 serialization: &mut QuerySerialization,
9512 aliases: &BTreeMap<String, String>,
9513) {
9514 match serialization {
9515 QuerySerialization::FormExplodedArray { item_type }
9516 | QuerySerialization::FormArray { item_type }
9517 | QuerySerialization::SimpleHeaderArray { item_type } => {
9518 rewrite_array_item_type_schema_names(item_type, aliases);
9519 }
9520 QuerySerialization::FormExplodedNestedObject { properties } => {
9521 for property in properties {
9522 rewrite_query_property_type_schema_names(&mut property.value_type, aliases);
9523 }
9524 }
9525 _ => {}
9526 }
9527}
9528
9529fn rewrite_array_item_type_schema_names(
9530 item_type: &mut ArrayItemType,
9531 aliases: &BTreeMap<String, String>,
9532) {
9533 match item_type {
9534 ArrayItemType::SchemaRef(name) => *name = renamed_schema_name(name, aliases),
9535 ArrayItemType::FlatStructRef {
9536 schema_name,
9537 properties,
9538 }
9539 | ArrayItemType::NestedStructRef {
9540 schema_name,
9541 properties,
9542 } => {
9543 *schema_name = renamed_schema_name(schema_name, aliases);
9544 for property in properties {
9545 rewrite_query_property_type_schema_names(&mut property.value_type, aliases);
9546 }
9547 }
9548 ArrayItemType::Scalar(_) => {}
9549 }
9550}
9551
9552fn rewrite_query_property_type_schema_names(
9553 property_type: &mut QueryStructPropertyType,
9554 aliases: &BTreeMap<String, String>,
9555) {
9556 match property_type {
9557 QueryStructPropertyType::Array { item_type } => {
9558 rewrite_array_item_type_schema_names(item_type, aliases)
9559 }
9560 QueryStructPropertyType::Object { properties } => {
9561 for property in properties {
9562 rewrite_query_property_type_schema_names(&mut property.value_type, aliases);
9563 }
9564 }
9565 QueryStructPropertyType::Scalar(_) => {}
9566 }
9567}
9568
9569fn rewrite_component_schema_references(value: &mut Value, aliases: &BTreeMap<String, String>) {
9570 match value {
9571 Value::Array(values) => {
9572 for value in values {
9573 rewrite_component_schema_references(value, aliases);
9574 }
9575 }
9576 Value::Object(object) => {
9577 if let Some(Value::String(reference)) = object.get_mut("$ref") {
9578 rewrite_component_schema_reference(reference, aliases);
9579 }
9580
9581 if let Some(Value::Object(mapping)) = object.get_mut("mapping") {
9582 for target_value in mapping.values_mut() {
9583 let Some(target) = target_value.as_str() else {
9584 continue;
9585 };
9586 let replacement = aliases.get(target).cloned().or_else(|| {
9587 let mut target = target.to_string();
9588 rewrite_component_schema_reference(&mut target, aliases).then_some(target)
9589 });
9590 if let Some(replacement) = replacement {
9591 *target_value = Value::String(replacement);
9592 }
9593 }
9594 }
9595
9596 for value in object.values_mut() {
9597 rewrite_component_schema_references(value, aliases);
9598 }
9599 }
9600 _ => {}
9601 }
9602}
9603
9604fn rewrite_component_schema_reference(
9605 reference: &mut String,
9606 aliases: &BTreeMap<String, String>,
9607) -> bool {
9608 const PREFIX: &str = "#/components/schemas/";
9609 let Some(encoded_name) = reference.strip_prefix(PREFIX) else {
9610 return false;
9611 };
9612 let encoded_name = encoded_name.split('/').next().unwrap_or(encoded_name);
9613
9614 for (source, replacement) in aliases {
9615 let encoded_source = source.replace('~', "~0").replace('/', "~1");
9616 if encoded_name == encoded_source {
9617 reference.replace_range(
9618 PREFIX.len()..PREFIX.len() + encoded_source.len(),
9619 replacement,
9620 );
9621 return true;
9622 }
9623 }
9624
9625 false
9626}