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_sources: BTreeMap<String, OperationSource>,
81 pub operation_id_aliases: BTreeMap<String, Vec<String>>,
85 pub used_type_features: crate::type_mapping::UsedFeatures,
94 pub enum_extensions: BTreeMap<String, EnumExtensions>,
102 pub validation_context: ValidationContext,
106 pub untagged_union_branches: BTreeSet<String>,
119}
120
121fn collect_untagged_union_branches(analysis: &SchemaAnalysis) -> BTreeSet<String> {
128 let mut branches = BTreeSet::new();
129 for schema in analysis.schemas.values() {
130 let SchemaType::Union { variants, .. } = &schema.schema_type else {
131 continue;
132 };
133 for variant in variants {
134 let mut target = variant.target.clone();
135 let mut seen = BTreeSet::new();
138 while seen.insert(target.clone()) {
139 branches.insert(target.clone());
140 match analysis.schemas.get(&target).map(|s| &s.schema_type) {
141 Some(SchemaType::Reference { target: next }) => target = next.clone(),
142 _ => break,
143 }
144 }
145 }
146 }
147 branches
148}
149
150impl SchemaType {
151 pub fn renders_inline(&self) -> bool {
162 match self {
163 Self::Primitive { .. }
164 | Self::Reference { .. }
165 | Self::Array { .. }
166 | Self::Tuple { .. }
167 | Self::Nullable { .. }
168 | Self::Untyped { .. } => true,
169 Self::Object { .. }
170 | Self::StringEnum { .. }
171 | Self::ExtensibleEnum { .. }
172 | Self::DiscriminatedUnion { .. }
173 | Self::Union { .. }
174 | Self::Composition { .. } => false,
175 }
176 }
177}
178
179impl UntypedReason {
180 pub fn inline_drop(schema_type: &SchemaType) -> Option<Self> {
183 match schema_type {
184 SchemaType::Composition { .. } => Some(Self::InlineCompositionDropped),
185 SchemaType::Union { .. } | SchemaType::DiscriminatedUnion { .. } => {
186 Some(Self::InlineUnionDropped)
187 }
188 SchemaType::Object { .. } => Some(Self::InlineObjectDropped),
189 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. } => {
190 Some(Self::InlineEnumDropped)
191 }
192 _ => None,
193 }
194 }
195}
196
197fn schema_type_dependencies(schema_type: &SchemaType) -> HashSet<String> {
207 let mut targets = HashSet::new();
208 collect_type_dependencies(schema_type, &mut targets, 0);
209 targets
210}
211
212fn collect_type_dependencies(
213 schema_type: &SchemaType,
214 targets: &mut HashSet<String>,
215 depth: usize,
216) {
217 if depth > UNTYPED_WALK_DEPTH {
218 return;
219 }
220 match schema_type {
221 SchemaType::Reference { target } => {
222 targets.insert(target.clone());
223 }
224 SchemaType::Array { item_type } => collect_type_dependencies(item_type, targets, depth + 1),
225 SchemaType::Nullable { inner_type } => {
226 collect_type_dependencies(inner_type, targets, depth + 1)
227 }
228 SchemaType::Tuple { element_types } => {
229 for element_type in element_types {
230 collect_type_dependencies(element_type, targets, depth + 1);
231 }
232 }
233 SchemaType::Object {
234 properties,
235 additional_properties,
236 ..
237 } => {
238 for property in properties.values() {
239 collect_type_dependencies(&property.schema_type, targets, depth + 1);
240 }
241 if let ObjectAdditionalProperties::Typed { value_type } = additional_properties {
242 collect_type_dependencies(value_type, targets, depth + 1);
243 }
244 }
245 SchemaType::Union { variants, .. } | SchemaType::Composition { schemas: variants } => {
246 for variant in variants {
247 targets.insert(variant.target.clone());
248 }
249 }
250 SchemaType::DiscriminatedUnion { variants, .. } => {
251 for variant in variants {
252 targets.insert(variant.type_name.clone());
253 }
254 }
255 SchemaType::Primitive { .. }
256 | SchemaType::StringEnum { .. }
257 | SchemaType::ExtensibleEnum { .. }
258 | SchemaType::Untyped { .. } => {}
259 }
260}
261
262fn normalize_untyped(schema_type: &mut SchemaType, depth: usize) {
272 if depth > UNTYPED_WALK_DEPTH {
273 return;
274 }
275 match schema_type {
276 SchemaType::Primitive { rust_type, .. } => {
277 let shape = match rust_type.as_str() {
278 "serde_json::Value" => Some(UntypedShape::Value),
279 "Vec<serde_json::Value>" => Some(UntypedShape::ValueArray),
280 _ => None,
281 };
282 if let Some(shape) = shape {
283 *schema_type = SchemaType::Untyped {
284 shape,
285 reason: UntypedReason::Unclassified,
286 };
287 }
288 }
289 SchemaType::Object {
290 properties,
291 additional_properties,
292 ..
293 } => {
294 for property in properties.values_mut() {
295 normalize_untyped(&mut property.schema_type, depth + 1);
296 }
297 if let ObjectAdditionalProperties::Typed { value_type } = additional_properties {
298 normalize_untyped(value_type, depth + 1);
299 }
300 }
301 SchemaType::Array { item_type } => normalize_untyped(item_type, depth + 1),
302 SchemaType::Nullable { inner_type } => normalize_untyped(inner_type, depth + 1),
303 SchemaType::Tuple { element_types } => {
304 for element_type in element_types {
305 normalize_untyped(element_type, depth + 1);
306 }
307 }
308 SchemaType::Untyped { .. }
309 | SchemaType::StringEnum { .. }
310 | SchemaType::ExtensibleEnum { .. }
311 | SchemaType::DiscriminatedUnion { .. }
312 | SchemaType::Union { .. }
313 | SchemaType::Composition { .. }
314 | SchemaType::Reference { .. } => {}
315 }
316}
317
318impl SchemaAnalysis {
319 pub fn untyped_fields(&self) -> Vec<UntypedFinding> {
326 let mut findings = Vec::new();
327 for (name, schema) in &self.schemas {
328 collect_untyped(&schema.schema_type, name, &mut findings, 0);
329 }
330 findings.sort();
331 findings
332 }
333}
334
335const UNTYPED_WALK_DEPTH: usize = 32;
339
340fn collect_untyped(
341 schema_type: &SchemaType,
342 context: &str,
343 findings: &mut Vec<UntypedFinding>,
344 depth: usize,
345) {
346 if depth > UNTYPED_WALK_DEPTH {
347 return;
348 }
349 match schema_type {
350 SchemaType::Untyped { shape, reason } => findings.push(UntypedFinding {
351 context: context.to_string(),
352 shape: *shape,
353 reason: *reason,
354 }),
355 SchemaType::Object {
356 properties,
357 additional_properties,
358 ..
359 } => {
360 for (property_name, property) in properties {
361 let property_context = format!("{context}.{property_name}");
362 if let Some(reason) = UntypedReason::inline_drop(&property.schema_type) {
366 findings.push(UntypedFinding {
367 context: property_context,
368 shape: UntypedShape::Value,
369 reason,
370 });
371 continue;
372 }
373 collect_untyped(
374 &property.schema_type,
375 &property_context,
376 findings,
377 depth + 1,
378 );
379 }
380 match additional_properties {
381 ObjectAdditionalProperties::Untyped => findings.push(UntypedFinding {
382 context: format!("{context}.<additionalProperties>"),
383 shape: UntypedShape::ValueMap,
384 reason: UntypedReason::UntypedAdditionalProperties,
385 }),
386 ObjectAdditionalProperties::Typed { value_type } => collect_untyped(
387 value_type,
388 &format!("{context}.<additionalProperties>"),
389 findings,
390 depth + 1,
391 ),
392 ObjectAdditionalProperties::Denied | ObjectAdditionalProperties::Closed => {}
393 }
394 }
395 SchemaType::Array { item_type } => {
396 let element_context = format!("{context}[]");
397 if let Some(reason) = UntypedReason::inline_drop(item_type) {
398 findings.push(UntypedFinding {
399 context: element_context,
400 shape: UntypedShape::Value,
401 reason,
402 });
403 } else {
404 collect_untyped(item_type, &element_context, findings, depth + 1);
405 }
406 }
407 SchemaType::Nullable { inner_type } => {
408 collect_untyped(inner_type, context, findings, depth + 1)
409 }
410 SchemaType::Tuple { element_types } => {
411 for (index, element_type) in element_types.iter().enumerate() {
412 collect_untyped(
413 element_type,
414 &format!("{context}[{index}]"),
415 findings,
416 depth + 1,
417 );
418 }
419 }
420 SchemaType::Union { variants, .. } | SchemaType::Composition { schemas: variants } => {
423 for (index, variant) in variants.iter().enumerate() {
424 if let Some(shape) = untyped_shape_of(&variant.target) {
425 findings.push(UntypedFinding {
426 context: format!("{context}|{index}"),
427 shape,
428 reason: UntypedReason::UntypedUnionBranch,
429 });
430 }
431 }
432 }
433 SchemaType::Primitive { .. }
434 | SchemaType::StringEnum { .. }
435 | SchemaType::ExtensibleEnum { .. }
436 | SchemaType::DiscriminatedUnion { .. }
437 | SchemaType::Reference { .. } => {}
438 }
439}
440
441fn untyped_shape_of(rust_type: &str) -> Option<UntypedShape> {
443 match rust_type {
444 "serde_json::Value" => Some(UntypedShape::Value),
445 "Vec<serde_json::Value>" => Some(UntypedShape::ValueArray),
446 _ => None,
447 }
448}
449
450#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
453pub struct UntypedFinding {
454 pub context: String,
457 pub shape: UntypedShape,
459 pub reason: UntypedReason,
461}
462
463#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
465#[serde(rename_all = "snake_case")]
466pub enum UntypedShape {
467 Value,
469 ValueArray,
471 ValueMap,
473}
474
475#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
483#[serde(rename_all = "kebab-case")]
484pub enum UntypedReason {
485 AnySchema,
488 OpaqueObject,
491 UntypedAdditionalProperties,
493 ArrayWithoutItems,
495 OpenPositionalItems,
498 UnrepresentableUnion,
501 UnrepresentableComposition,
503 UnsupportedTypeKeyword,
505 UnresolvedReference,
507 InlineCompositionDropped,
512 InlineUnionDropped,
514 InlineObjectDropped,
516 InlineEnumDropped,
518 UntypedUnionBranch,
522 NeverMatches,
526 Unclassified,
529}
530
531impl UntypedReason {
532 pub fn verdict(self) -> UntypedVerdict {
535 match self {
536 Self::AnySchema | Self::OpaqueObject | Self::UntypedAdditionalProperties => {
538 UntypedVerdict::Faithful
539 }
540 Self::NeverMatches => UntypedVerdict::Faithful,
542 Self::ArrayWithoutItems | Self::OpenPositionalItems => UntypedVerdict::Faithful,
546 Self::InlineCompositionDropped
549 | Self::InlineUnionDropped
550 | Self::InlineObjectDropped
551 | Self::InlineEnumDropped => UntypedVerdict::Recoverable,
552 Self::UnrepresentableUnion
554 | Self::UnrepresentableComposition
555 | Self::UnsupportedTypeKeyword
556 | Self::UnresolvedReference => UntypedVerdict::Recoverable,
557 Self::UntypedUnionBranch | Self::Unclassified => UntypedVerdict::Unknown,
558 }
559 }
560}
561
562#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
564#[serde(rename_all = "snake_case")]
565pub enum UntypedVerdict {
566 Faithful,
568 Recoverable,
570 Unknown,
572}
573
574#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize)]
576pub struct OperationResponse {
577 pub schema_name: Option<String>,
579 pub media_type: Option<String>,
581 pub body: Option<OperationResponseBody>,
585 pub supports_streaming: bool,
587 pub has_content: bool,
589 pub unsupported_media_types: Vec<String>,
591 pub representations: BTreeMap<String, OperationRepresentation>,
593}
594
595#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
597pub struct OperationSource {
598 pub json_pointer: String,
599 pub method: String,
600 pub path: String,
601 pub operation_id: Option<String>,
602 pub webhook: bool,
603}
604
605#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
606#[serde(tag = "kind", rename_all = "snake_case")]
607pub enum OperationRepresentation {
608 Json { schema_name: String },
609 Text,
610 Binary { wildcard: bool },
611 EventStream,
612}
613
614#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
619#[serde(tag = "kind", rename_all = "snake_case")]
620pub enum OperationResponseBody {
621 Json {
622 schema_name: String,
623 media_type: String,
624 },
625 Text {
626 media_type: String,
627 },
628 Binary {
629 media_type: String,
630 wildcard: bool,
631 },
632}
633
634#[derive(Debug, Clone, Default)]
635pub struct ValidationContext {
636 pub openapi_version: String,
637 pub json_schema_dialect: Option<String>,
638 pub component_schemas: BTreeMap<String, Value>,
639}
640
641#[derive(Debug, Clone, Default)]
646pub struct EnumExtensions {
647 pub varnames: Vec<String>,
652 pub descriptions: Vec<String>,
654}
655
656#[derive(Debug, Clone)]
657pub struct AnalyzedSchema {
658 pub name: String,
659 pub original: Value,
660 pub schema_type: SchemaType,
661 pub dependencies: HashSet<String>,
662 pub nullable: bool,
663 pub description: Option<String>,
664 pub default: Option<serde_json::Value>,
665}
666
667#[derive(Debug, Clone)]
668pub enum SchemaType {
669 Primitive {
675 rust_type: String,
676 serde_with: Option<String>,
677 },
678 Object {
680 properties: BTreeMap<String, PropertyInfo>,
681 required: HashSet<String>,
682 additional_properties: ObjectAdditionalProperties,
683 variant: Option<SchemaRef>,
688 },
689 DiscriminatedUnion {
691 discriminator_field: String,
692 variants: Vec<UnionVariant>,
693 exclusive: bool,
697 },
698 Union {
702 variants: Vec<SchemaRef>,
703 exclusive: bool,
704 },
705 Array { item_type: Box<SchemaType> },
707 Nullable { inner_type: Box<SchemaType> },
712 Tuple { element_types: Vec<SchemaType> },
718 StringEnum { values: Vec<String> },
720 ExtensibleEnum { known_values: Vec<String> },
722 Composition { schemas: Vec<SchemaRef> },
724 Reference { target: String },
726 Untyped {
733 shape: UntypedShape,
734 reason: UntypedReason,
735 },
736}
737
738#[derive(Debug, Clone)]
743pub enum ObjectAdditionalProperties {
744 Denied,
749 Closed,
755 Untyped,
758 Typed { value_type: Box<SchemaType> },
761}
762
763impl ObjectAdditionalProperties {
764 pub fn is_open(&self) -> bool {
767 !matches!(self, Self::Denied | Self::Closed)
768 }
769
770 pub fn denies_unknown_keys(&self) -> bool {
774 matches!(self, Self::Denied)
775 }
776}
777
778#[derive(Debug, Clone)]
779pub struct PropertyInfo {
780 pub schema_type: SchemaType,
781 pub nullable: bool,
782 pub description: Option<String>,
783 pub default: Option<serde_json::Value>,
784 pub serde_attrs: Vec<String>,
785 pub synthesized_required: bool,
790 pub constraints: PropertyConstraints,
795}
796
797#[derive(Debug, Clone, Default)]
802pub struct PropertyConstraints {
803 pub minimum: Option<f64>,
804 pub maximum: Option<f64>,
805 pub exclusive_minimum: Option<f64>,
806 pub exclusive_maximum: Option<f64>,
807 pub multiple_of: Option<f64>,
808 pub min_length: Option<u64>,
809 pub max_length: Option<u64>,
810 pub pattern: Option<String>,
811 pub min_items: Option<u64>,
812 pub max_items: Option<u64>,
813 pub unique_items: Option<bool>,
814}
815
816impl PropertyConstraints {
817 pub fn is_empty(&self) -> bool {
818 self.minimum.is_none()
819 && self.maximum.is_none()
820 && self.exclusive_minimum.is_none()
821 && self.exclusive_maximum.is_none()
822 && self.multiple_of.is_none()
823 && self.min_length.is_none()
824 && self.max_length.is_none()
825 && self.pattern.is_none()
826 && self.min_items.is_none()
827 && self.max_items.is_none()
828 && self.unique_items.is_none()
829 }
830
831 pub fn from_schema_details(details: &crate::openapi::SchemaDetails) -> Self {
836 use crate::openapi::ExclusiveBound;
837 let exclusive_minimum = match &details.exclusive_minimum {
838 Some(ExclusiveBound::Number(v)) => Some(*v),
839 _ => None,
840 };
841 let exclusive_maximum = match &details.exclusive_maximum {
842 Some(ExclusiveBound::Number(v)) => Some(*v),
843 _ => None,
844 };
845 Self {
846 minimum: details
847 .minimum
848 .as_ref()
849 .and_then(serde_json::Number::as_f64),
850 maximum: details
851 .maximum
852 .as_ref()
853 .and_then(serde_json::Number::as_f64),
854 exclusive_minimum,
855 exclusive_maximum,
856 multiple_of: details.multiple_of,
857 min_length: details.min_length,
858 max_length: details.max_length,
859 pattern: details.pattern.clone(),
860 min_items: details.min_items,
861 max_items: details.max_items,
862 unique_items: details.unique_items,
863 }
864 }
865}
866
867#[derive(Debug, Clone)]
868pub struct UnionVariant {
869 pub rust_name: String,
870 pub type_name: String,
871 pub discriminator_value: String,
875 pub discriminator_values: Vec<String>,
879 pub preferred_discriminator_values: Vec<String>,
884 pub discriminator_field_declared: bool,
888 pub discriminator_field_required: bool,
892 pub schema_ref: String,
893}
894
895#[derive(Debug, Clone)]
896pub struct SchemaRef {
897 pub target: String,
898 pub nullable: bool,
899}
900
901#[derive(Debug, Clone)]
902pub struct DependencyGraph {
903 pub edges: BTreeMap<String, HashSet<String>>,
904 pub recursive_schemas: HashSet<String>,
906}
907
908#[derive(Debug, Clone)]
909pub struct DetectedPatterns {
910 pub tagged_enum_schemas: HashSet<String>,
912 pub untagged_enum_schemas: HashSet<String>,
914 pub type_mappings: BTreeMap<String, BTreeMap<String, String>>,
916}
917
918#[derive(Debug, Clone, Default, serde::Serialize)]
920pub struct OperationInfo {
921 pub operation_id: String,
923 pub method: String,
925 pub path: String,
927 pub summary: Option<String>,
929 pub description: Option<String>,
931 pub request_body: Option<RequestBodyContent>,
933 pub request_body_required: bool,
936 pub response_schemas: BTreeMap<String, String>,
938 pub parameters: Vec<ParameterInfo>,
940 pub supports_streaming: bool,
942 pub stream_parameter: Option<String>,
944 pub tags: Vec<String>,
948}
949
950#[derive(Debug, Clone, serde::Serialize)]
952#[serde(tag = "kind")]
953pub enum RequestBodyContent {
954 Json {
955 schema_name: String,
956 media_type: String,
957 #[serde(skip)]
958 validation_schema: Value,
959 },
960 FormUrlEncoded {
961 schema_name: String,
962 media_type: String,
963 #[serde(skip)]
964 validation_schema: Value,
965 },
966 Multipart {
967 schema_name: String,
968 media_type: String,
969 #[serde(skip)]
970 validation_schema: Value,
971 },
972 OctetStream {
973 media_type: String,
974 },
975 Binary {
976 media_type: String,
977 },
978 TextPlain {
979 media_type: String,
980 },
981 SchemaLess {
985 media_type: String,
986 },
987 Unsupported {
988 media_types: Vec<String>,
989 },
990}
991
992impl RequestBodyContent {
993 pub fn schema_name(&self) -> Option<&str> {
995 match self {
996 Self::Json { schema_name, .. }
997 | Self::FormUrlEncoded { schema_name, .. }
998 | Self::Multipart { schema_name, .. } => Some(schema_name),
999 Self::OctetStream { .. }
1000 | Self::Binary { .. }
1001 | Self::TextPlain { .. }
1002 | Self::SchemaLess { .. }
1003 | Self::Unsupported { .. } => None,
1004 }
1005 }
1006}
1007
1008fn base_param_ident(name: &str) -> String {
1012 use heck::ToSnakeCase;
1013 let suffix = if name.ends_with("<=") {
1014 "_lte"
1015 } else if name.ends_with(">=") {
1016 "_gte"
1017 } else if name.ends_with('<') {
1018 "_lt"
1019 } else if name.ends_with('>') {
1020 "_gt"
1021 } else {
1022 ""
1023 };
1024 let stripped = name.trim_end_matches(['<', '>', '=']);
1025 let mut snake = stripped.to_snake_case();
1026 if snake.is_empty() {
1027 snake.push_str("parameter");
1028 } else if snake.starts_with(|character: char| character.is_ascii_digit()) {
1029 snake.insert(0, '_');
1030 }
1031 snake.push_str(suffix);
1032 snake
1033}
1034
1035#[derive(Debug, Clone, serde::Serialize)]
1037pub struct ParameterInfo {
1038 pub name: String,
1040 pub location: String,
1042 pub required: bool,
1044 pub schema_ref: Option<String>,
1046 pub rust_type: String,
1048 pub description: Option<String>,
1050 #[serde(skip_serializing_if = "Option::is_none")]
1056 pub enum_values: Option<Vec<String>>,
1057 #[serde(skip_serializing_if = "Option::is_none")]
1063 pub enum_varnames: Option<Vec<String>>,
1064 #[serde(skip_serializing_if = "Option::is_none")]
1072 pub rust_ident: Option<String>,
1073 #[serde(skip_serializing_if = "Option::is_none")]
1082 pub query_serialization: Option<QuerySerialization>,
1083 #[serde(skip)]
1086 pub validation_schema: Option<Value>,
1087}
1088
1089#[derive(Debug, Clone, PartialEq, serde::Serialize)]
1092pub enum QuerySerialization {
1093 FormExplodedObject,
1097 FormExplodedNestedObject {
1103 properties: Vec<QueryStructProperty>,
1104 },
1105 FormObject,
1108 DeepObject,
1111 FormExplodedArray { item_type: ArrayItemType },
1114 FormArray { item_type: ArrayItemType },
1117 SimpleHeaderArray { item_type: ArrayItemType },
1120 Unsupported { reason: String },
1125}
1126
1127#[derive(Debug, Clone, PartialEq, serde::Serialize)]
1134pub enum ArrayItemType {
1135 Scalar(String),
1137 SchemaRef(String),
1139 FlatStructRef {
1145 schema_name: String,
1146 properties: Vec<QueryStructProperty>,
1147 },
1148 NestedStructRef {
1152 schema_name: String,
1153 properties: Vec<QueryStructProperty>,
1154 },
1155}
1156
1157#[derive(Debug, Clone, PartialEq, serde::Serialize)]
1158pub struct QueryStructProperty {
1159 pub wire_name: String,
1160 pub required: bool,
1161 pub value_type: QueryStructPropertyType,
1162}
1163
1164#[derive(Debug, Clone, PartialEq, serde::Serialize)]
1165pub enum QueryStructPropertyType {
1166 Scalar(QueryScalarType),
1167 Array {
1168 item_type: ArrayItemType,
1169 },
1170 Object {
1171 properties: Vec<QueryStructProperty>,
1172 },
1173}
1174
1175#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
1176pub enum QueryScalarType {
1177 String,
1178 Integer,
1179 Number,
1180 Boolean,
1181}
1182
1183impl Default for DependencyGraph {
1184 fn default() -> Self {
1185 Self::new()
1186 }
1187}
1188
1189impl DependencyGraph {
1190 pub fn new() -> Self {
1191 Self {
1192 edges: BTreeMap::new(),
1193 recursive_schemas: HashSet::new(),
1194 }
1195 }
1196
1197 pub fn add_dependency(&mut self, from: String, to: String) {
1198 self.edges.entry(from).or_default().insert(to);
1199 }
1200
1201 pub fn topological_sort(&mut self) -> Result<Vec<String>> {
1203 self.detect_recursive_schemas();
1205
1206 let mut temp_edges = self.edges.clone();
1208 for (schema, deps) in &mut temp_edges {
1209 deps.remove(schema); }
1211
1212 let mut visited = HashSet::new();
1213 let mut temp_visited = HashSet::new();
1214 let mut result = Vec::new();
1215
1216 let mut all_nodes: Vec<_> = temp_edges.keys().collect();
1218 all_nodes.sort();
1219 for node in all_nodes {
1220 if !visited.contains(node) {
1221 self.visit_node_recursive(
1222 node,
1223 &temp_edges,
1224 &mut visited,
1225 &mut temp_visited,
1226 &mut result,
1227 )?;
1228 }
1229 }
1230
1231 result.reverse();
1232 Ok(result)
1233 }
1234
1235 fn detect_recursive_schemas(&mut self) {
1236 for (schema, deps) in &self.edges {
1237 if deps.contains(schema) {
1238 self.recursive_schemas.insert(schema.clone());
1240 } else {
1241 if self.has_cycle_from(schema, schema, &mut HashSet::new()) {
1243 self.recursive_schemas.insert(schema.clone());
1244 }
1245 }
1246 }
1247
1248 for (schema, deps) in &self.edges {
1250 for dep in deps {
1251 if let Some(dep_deps) = self.edges.get(dep) {
1252 if dep_deps.contains(schema) {
1253 self.recursive_schemas.insert(schema.clone());
1255 self.recursive_schemas.insert(dep.clone());
1256 }
1257 }
1258 }
1259 }
1260 }
1261
1262 fn has_cycle_from(&self, start: &str, current: &str, visited: &mut HashSet<String>) -> bool {
1263 if visited.contains(current) {
1264 return false; }
1266
1267 visited.insert(current.to_string());
1268
1269 if let Some(deps) = self.edges.get(current) {
1270 for dep in deps {
1271 if dep == start {
1272 return true; }
1274 if self.has_cycle_from(start, dep, visited) {
1275 return true;
1276 }
1277 }
1278 }
1279
1280 false
1281 }
1282
1283 #[allow(clippy::only_used_in_recursion)]
1284 fn visit_node_recursive(
1285 &self,
1286 node: &str,
1287 temp_edges: &BTreeMap<String, HashSet<String>>,
1288 visited: &mut HashSet<String>,
1289 temp_visited: &mut HashSet<String>,
1290 result: &mut Vec<String>,
1291 ) -> Result<()> {
1292 if temp_visited.contains(node) {
1293 return Ok(());
1295 }
1296
1297 if visited.contains(node) {
1298 return Ok(());
1299 }
1300
1301 temp_visited.insert(node.to_string());
1302
1303 if let Some(dependencies) = temp_edges.get(node) {
1304 let mut sorted_deps: Vec<_> = dependencies.iter().collect();
1306 sorted_deps.sort();
1307 for dep in sorted_deps {
1308 self.visit_node_recursive(dep, temp_edges, visited, temp_visited, result)?;
1309 }
1310 }
1311
1312 temp_visited.remove(node);
1313 visited.insert(node.to_string());
1314 result.push(node.to_string());
1315
1316 Ok(())
1317 }
1318}
1319
1320pub fn merge_schema_extensions(
1323 main_spec: Value,
1324 extension_paths: &[impl AsRef<Path>],
1325) -> Result<Value> {
1326 let mut result = main_spec;
1327
1328 for path in extension_paths {
1329 let extension = load_extension_file(path.as_ref())?;
1330 result = merge_json_objects_with_replacements(result, extension)?;
1331 }
1332
1333 Ok(result)
1334}
1335
1336fn normalize_operation_path(path: &str) -> String {
1343 match path.split_once('#') {
1344 Some((route, _fragment)) if route.starts_with('/') => route.to_string(),
1345 _ => path.to_string(),
1346 }
1347}
1348
1349fn unwrap_annotation_allof(schema: &crate::openapi::Schema) -> &crate::openapi::Schema {
1354 let crate::openapi::Schema::AllOf { all_of, .. } = schema else {
1355 return schema;
1356 };
1357 let mut references = all_of.iter().filter(|s| s.reference().is_some());
1358 let (Some(first), None) = (references.next(), references.next()) else {
1359 return schema;
1360 };
1361 let others_annotation_only = all_of
1362 .iter()
1363 .all(|member| member.reference().is_some() || schema_is_annotation_only(member));
1364 if others_annotation_only {
1365 first
1366 } else {
1367 schema
1368 }
1369}
1370
1371fn schema_is_annotation_only(schema: &crate::openapi::Schema) -> bool {
1376 serde_json::to_value(schema)
1377 .ok()
1378 .and_then(|value| value.as_object().cloned())
1379 .is_some_and(|object| {
1380 object.keys().all(|key| {
1381 matches!(
1382 key.as_str(),
1383 "title"
1384 | "description"
1385 | "deprecated"
1386 | "readOnly"
1387 | "writeOnly"
1388 | "examples"
1389 | "example"
1390 | "default"
1391 | "externalDocs"
1392 | "xml"
1393 | "$comment"
1394 | "nullable"
1395 ) || key.starts_with("x-")
1396 })
1397 })
1398}
1399
1400fn load_extension_file(path: &Path) -> Result<Value> {
1404 let content = std::fs::read_to_string(path).map_err(|e| GeneratorError::FileError {
1405 message: format!("Failed to read file {}: {}", path.display(), e),
1406 })?;
1407
1408 let is_yaml = path
1409 .extension()
1410 .and_then(|extension| extension.to_str())
1411 .is_some_and(|extension| {
1412 extension.eq_ignore_ascii_case("yaml") || extension.eq_ignore_ascii_case("yml")
1413 });
1414
1415 if is_yaml {
1416 crate::spec_source::yaml_to_json_value(&content).map_err(|error| {
1417 GeneratorError::FileError {
1418 message: format!(
1419 "Failed to parse schema extension {} as YAML: {}",
1420 path.display(),
1421 error
1422 ),
1423 }
1424 })
1425 } else {
1426 serde_json::from_str(&content).map_err(|error| GeneratorError::FileError {
1427 message: format!(
1428 "Failed to parse schema extension {} as JSON: {}",
1429 path.display(),
1430 error
1431 ),
1432 })
1433 }
1434}
1435
1436fn merge_json_objects_with_replacements(main: Value, extension: Value) -> Result<Value> {
1438 let replacements = extract_replacement_rules(&extension);
1440
1441 Ok(merge_json_objects_with_rules(
1443 main,
1444 extension,
1445 &replacements,
1446 ))
1447}
1448
1449fn extract_replacement_rules(
1451 extension: &Value,
1452) -> std::collections::HashMap<String, (String, String)> {
1453 let mut rules = std::collections::HashMap::new();
1454
1455 if let Some(x_replacements) = extension.get("x-replacements") {
1456 if let Some(x_replacements_obj) = x_replacements.as_object() {
1457 for (schema_name, replacement_rule) in x_replacements_obj {
1458 if let Some(rule_obj) = replacement_rule.as_object() {
1459 if let (Some(replace), Some(with)) = (
1460 rule_obj.get("replace").and_then(|v| v.as_str()),
1461 rule_obj.get("with").and_then(|v| v.as_str()),
1462 ) {
1463 rules.insert(schema_name.clone(), (replace.to_string(), with.to_string()));
1464 }
1466 }
1467 }
1468 }
1469 }
1470
1471 rules
1472}
1473
1474fn should_replace_variant(
1476 schema_name: &str,
1477 extension_refs: &[String],
1478 replacements: &std::collections::HashMap<String, (String, String)>,
1479) -> bool {
1480 for (replace_schema, with_schema) in replacements.values() {
1482 if schema_name == replace_schema {
1483 let replacement_exists = extension_refs.iter().any(|ext_ref| {
1485 let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
1486 ext_schema_name == with_schema
1487 });
1488
1489 if replacement_exists {
1490 return true;
1491 }
1492 }
1493 }
1494
1495 extension_refs.iter().any(|ext_ref| {
1497 let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
1498 schema_name == ext_schema_name
1499 })
1500}
1501
1502fn merge_json_objects_with_rules(
1507 main: Value,
1508 extension: Value,
1509 replacements: &std::collections::HashMap<String, (String, String)>,
1510) -> Value {
1511 match (main, extension) {
1512 (Value::Object(mut main_obj), Value::Object(ext_obj)) => {
1514 let main_union_keyword = if main_obj.contains_key("oneOf") {
1517 Some("oneOf")
1518 } else if main_obj.contains_key("anyOf") {
1519 Some("anyOf")
1520 } else {
1521 None
1522 };
1523 if let (Some(main_variants), Some(ext_variants)) = (
1524 extract_schema_variants(&Value::Object(main_obj.clone())),
1525 extract_schema_variants(&Value::Object(ext_obj.clone())),
1526 ) {
1527 let union_key = main_union_keyword.unwrap_or("oneOf");
1528 println!(
1529 "🔍 Merging union schemas ({union_key}): {} main variants, {} extension variants",
1530 main_variants.len(),
1531 ext_variants.len()
1532 );
1533 let mut merged_variants = Vec::new();
1536 let extension_refs: Vec<String> = ext_variants
1537 .iter()
1538 .filter_map(|v| v.get("$ref").and_then(|r| r.as_str()))
1539 .map(|s| s.to_string())
1540 .collect();
1541
1542 for main_variant in main_variants {
1544 if let Some(main_ref) = main_variant.get("$ref").and_then(|r| r.as_str()) {
1545 let schema_name = main_ref.split('/').next_back().unwrap_or("");
1547 let should_replace =
1548 should_replace_variant(schema_name, &extension_refs, replacements);
1549
1550 if should_replace {
1551 println!("🔄 REPLACING {} (explicit rule)", schema_name);
1552 }
1553
1554 if !should_replace {
1555 merged_variants.push(main_variant);
1556 }
1557 } else {
1558 merged_variants.push(main_variant);
1560 }
1561 }
1562
1563 for ext_variant in ext_variants {
1565 merged_variants.push(ext_variant);
1566 }
1567
1568 main_obj.remove("oneOf");
1570 main_obj.remove("anyOf");
1571 main_obj.insert(union_key.to_string(), Value::Array(merged_variants));
1572
1573 for (key, ext_value) in ext_obj {
1575 if key != "oneOf" && key != "anyOf" {
1576 match main_obj.get(&key) {
1577 Some(main_value) => {
1578 let merged_value = merge_json_objects_with_rules(
1579 main_value.clone(),
1580 ext_value,
1581 replacements,
1582 );
1583 main_obj.insert(key, merged_value);
1584 }
1585 None => {
1586 main_obj.insert(key, ext_value);
1587 }
1588 }
1589 }
1590 }
1591
1592 return Value::Object(main_obj);
1593 }
1594
1595 for (key, ext_value) in ext_obj {
1597 match main_obj.get(&key) {
1598 Some(main_value) => {
1599 let merged_value = merge_json_objects_with_rules(
1601 main_value.clone(),
1602 ext_value,
1603 replacements,
1604 );
1605 main_obj.insert(key, merged_value);
1606 }
1607 None => {
1608 main_obj.insert(key, ext_value);
1610 }
1611 }
1612 }
1613 Value::Object(main_obj)
1614 }
1615
1616 (Value::Array(mut main_arr), Value::Array(ext_arr)) => {
1618 main_arr.extend(ext_arr);
1619 Value::Array(main_arr)
1620 }
1621
1622 (_, extension) => extension,
1624 }
1625}
1626
1627fn extract_schema_variants(obj: &Value) -> Option<Vec<Value>> {
1629 if let Value::Object(map) = obj {
1630 if let Some(Value::Array(variants)) = map.get("oneOf") {
1631 return Some(variants.clone());
1632 }
1633 if let Some(Value::Array(variants)) = map.get("anyOf") {
1634 return Some(variants.clone());
1635 }
1636 }
1637 None
1638}
1639
1640#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1645enum InlineUnionKind {
1646 OneOf,
1647 AnyOf,
1648}
1649
1650#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
1651enum SchemaIdentity {
1652 Component(String),
1653 Pointer(String),
1654 InlineUnionBranch {
1655 owner_context: String,
1656 union_kind: InlineUnionKind,
1657 original_index: usize,
1658 discriminator: Option<String>,
1659 fingerprint: String,
1660 },
1661 Inline {
1662 context: String,
1663 kind: &'static str,
1664 preferred_name: String,
1665 fingerprint: String,
1666 },
1667}
1668
1669#[derive(Debug, Default)]
1670struct SchemaNameRegistry {
1671 names_by_identity: BTreeMap<SchemaIdentity, String>,
1672 identities_by_name: BTreeMap<String, SchemaIdentity>,
1673}
1674
1675impl SchemaNameRegistry {
1676 fn with_components(component_names: impl IntoIterator<Item = String>) -> Self {
1677 let mut registry = Self::default();
1678 for name in component_names {
1679 let identity = SchemaIdentity::Component(name.clone());
1680 registry
1681 .names_by_identity
1682 .insert(identity.clone(), name.clone());
1683 registry.identities_by_name.insert(name, identity);
1684 }
1685 registry
1686 }
1687
1688 fn component_name(&self, source_name: &str) -> Option<&str> {
1689 self.names_by_identity
1690 .get(&SchemaIdentity::Component(source_name.to_string()))
1691 .map(String::as_str)
1692 }
1693
1694 fn allocate(
1695 &mut self,
1696 identity: SchemaIdentity,
1697 preferred_name: &str,
1698 collision_name: &str,
1699 ) -> String {
1700 if let Some(existing) = self.names_by_identity.get(&identity) {
1701 return existing.clone();
1702 }
1703
1704 let allocated = if !self.identities_by_name.contains_key(preferred_name) {
1705 preferred_name.to_string()
1706 } else if !self.identities_by_name.contains_key(collision_name) {
1707 collision_name.to_string()
1708 } else {
1709 let hash = stable_schema_identity_hash(&identity);
1710 let hashed = format!("{collision_name}{hash:016X}");
1711 if !self.identities_by_name.contains_key(&hashed) {
1712 hashed
1713 } else {
1714 let mut suffix = 2;
1715 loop {
1716 let candidate = format!("{hashed}{suffix}");
1717 if !self.identities_by_name.contains_key(&candidate) {
1718 break candidate;
1719 }
1720 suffix += 1;
1721 }
1722 }
1723 };
1724
1725 self.names_by_identity
1726 .insert(identity.clone(), allocated.clone());
1727 self.identities_by_name.insert(allocated.clone(), identity);
1728 allocated
1729 }
1730}
1731
1732fn stable_schema_identity_hash(identity: &SchemaIdentity) -> u64 {
1736 let bytes = format!("{identity:?}");
1737 bytes
1738 .as_bytes()
1739 .iter()
1740 .fold(0xcbf29ce484222325, |hash, byte| {
1741 (hash ^ u64::from(*byte)).wrapping_mul(0x100000001b3)
1742 })
1743}
1744
1745#[cfg(test)]
1746mod schema_name_registry_tests {
1747 use super::{SchemaIdentity, SchemaNameRegistry};
1748
1749 #[test]
1750 fn component_reservations_are_independent_of_input_traversal_order() {
1751 let component_names = ["ModelApi".to_string(), "OutputFormatContainer".to_string()];
1752 let mut forward = SchemaNameRegistry::with_components(component_names.clone());
1753 let mut reverse = SchemaNameRegistry::with_components(component_names.into_iter().rev());
1754 let inline = SchemaIdentity::Inline {
1755 context: "Model".to_string(),
1756 kind: "property-object",
1757 preferred_name: "ModelApi".to_string(),
1758 fingerprint: r#"{"type":"object"}"#.to_string(),
1759 };
1760
1761 assert_eq!(
1762 forward.allocate(inline.clone(), "ModelApi", "ModelApiInline"),
1763 "ModelApiInline"
1764 );
1765 assert_eq!(
1766 reverse.allocate(inline, "ModelApi", "ModelApiInline"),
1767 "ModelApiInline"
1768 );
1769 assert_eq!(forward.component_name("ModelApi"), Some("ModelApi"));
1770 assert_eq!(reverse.component_name("ModelApi"), Some("ModelApi"));
1771 assert_eq!(
1772 forward.component_name("OutputFormatContainer"),
1773 Some("OutputFormatContainer")
1774 );
1775 assert_eq!(
1776 reverse.component_name("OutputFormatContainer"),
1777 Some("OutputFormatContainer")
1778 );
1779 }
1780
1781 #[test]
1782 fn an_identity_reuses_its_exact_allocated_name() {
1783 let mut registry = SchemaNameRegistry::with_components(["ModelApi".to_string()]);
1784 let inline = SchemaIdentity::Inline {
1785 context: "Model".to_string(),
1786 kind: "property-object",
1787 preferred_name: "ModelApi".to_string(),
1788 fingerprint: r#"{"type":"object"}"#.to_string(),
1789 };
1790
1791 let first = registry.allocate(inline.clone(), "ModelApi", "ModelApiInline");
1792 let repeated = registry.allocate(inline, "Ignored", "IgnoredInline");
1793
1794 assert_eq!(first, "ModelApiInline");
1795 assert_eq!(repeated, first);
1796 assert_eq!(registry.component_name("ModelApi"), Some("ModelApi"));
1797 }
1798}
1799
1800pub struct SchemaAnalyzer {
1801 schemas: BTreeMap<String, Schema>,
1802 resolved_cache: BTreeMap<String, AnalyzedSchema>,
1803 schema_names: SchemaNameRegistry,
1804 openapi_spec: Value,
1805 current_schema_name: Option<String>,
1806 component_parameters: BTreeMap<String, crate::openapi::Parameter>,
1807 type_mapper: TypeMapper,
1812 resolving_pointers: HashSet<String>,
1815}
1816
1817impl SchemaAnalyzer {
1818 fn untyped_value(&self, _context: impl Into<String>, reason: UntypedReason) -> SchemaType {
1822 SchemaType::Untyped {
1823 shape: UntypedShape::Value,
1824 reason,
1825 }
1826 }
1827
1828 fn untyped_value_array(
1830 &self,
1831 _context: impl Into<String>,
1832 reason: UntypedReason,
1833 ) -> SchemaType {
1834 SchemaType::Untyped {
1835 shape: UntypedShape::ValueArray,
1836 reason,
1837 }
1838 }
1839
1840 fn untyped_value_map(&self, _context: impl Into<String>, reason: UntypedReason) -> SchemaType {
1844 SchemaType::Untyped {
1845 shape: UntypedShape::ValueMap,
1846 reason,
1847 }
1848 }
1849
1850 fn untyped_context(&self, detail: &str) -> String {
1852 match (&self.current_schema_name, detail) {
1853 (Some(schema), "") => schema.clone(),
1854 (Some(schema), detail) => format!("{schema}.{detail}"),
1855 (None, "") => "<anonymous>".to_string(),
1856 (None, detail) => detail.to_string(),
1857 }
1858 }
1859
1860 fn allocate_inline_schema_name(
1861 &mut self,
1862 preferred_name: &str,
1863 collision_name: &str,
1864 kind: &'static str,
1865 schema: &Schema,
1866 ) -> String {
1867 let identity = SchemaIdentity::Inline {
1868 context: self
1869 .current_schema_name
1870 .clone()
1871 .unwrap_or_else(|| "<anonymous>".to_string()),
1872 kind,
1873 preferred_name: preferred_name.to_string(),
1874 fingerprint: serde_json::to_string(schema).unwrap_or_else(|_| format!("{schema:?}")),
1875 };
1876 self.schema_names
1877 .allocate(identity, preferred_name, collision_name)
1878 }
1879
1880 fn with_schema_context<T>(
1885 &mut self,
1886 schema_name: &str,
1887 analyze: impl FnOnce(&mut Self) -> Result<T>,
1888 ) -> Result<T> {
1889 let previous = self.current_schema_name.replace(schema_name.to_string());
1890 let result = analyze(self);
1891 self.current_schema_name = previous;
1892 result
1893 }
1894
1895 fn add_allocated_object_schema(
1896 &mut self,
1897 object_type_name: String,
1898 schema: &Schema,
1899 dependencies: &mut HashSet<String>,
1900 ) -> Result<SchemaType> {
1901 let object_type = self.with_schema_context(&object_type_name, |analyzer| {
1902 analyzer.analyze_object_schema(schema, dependencies)
1903 })?;
1904 self.resolved_cache.insert(
1905 object_type_name.clone(),
1906 AnalyzedSchema {
1907 name: object_type_name.clone(),
1908 original: serde_json::to_value(schema).unwrap_or(Value::Null),
1909 schema_type: object_type,
1910 dependencies: dependencies.clone(),
1911 nullable: false,
1912 description: schema.details().description.clone(),
1913 default: None,
1914 },
1915 );
1916 dependencies.insert(object_type_name.clone());
1917 Ok(SchemaType::Reference {
1918 target: object_type_name,
1919 })
1920 }
1921
1922 fn allocate_inline_union_branch_name(
1923 &mut self,
1924 preferred_name: &str,
1925 owner_context: &str,
1926 union_kind: InlineUnionKind,
1927 original_index: usize,
1928 discriminator: Option<&str>,
1929 schema: &Schema,
1930 ) -> String {
1931 let identity = SchemaIdentity::InlineUnionBranch {
1932 owner_context: owner_context.to_string(),
1933 union_kind,
1934 original_index,
1935 discriminator: discriminator.map(str::to_string),
1936 fingerprint: serde_json::to_string(schema).unwrap_or_else(|_| format!("{schema:?}")),
1937 };
1938 self.schema_names
1939 .allocate(identity, preferred_name, &format!("{preferred_name}Inline"))
1940 }
1941
1942 fn allocate_pointer_schema_name(&mut self, pointer: &str, preferred_name: &str) -> String {
1943 self.schema_names.allocate(
1944 SchemaIdentity::Pointer(pointer.to_string()),
1945 preferred_name,
1946 &format!("{preferred_name}Pointer"),
1947 )
1948 }
1949
1950 fn allocate_synthetic_schema_name(
1951 &mut self,
1952 preferred_name: &str,
1953 collision_name: &str,
1954 kind: &'static str,
1955 fingerprint: String,
1956 ) -> String {
1957 let identity = SchemaIdentity::Inline {
1958 context: self
1959 .current_schema_name
1960 .clone()
1961 .unwrap_or_else(|| "<anonymous>".to_string()),
1962 kind,
1963 preferred_name: preferred_name.to_string(),
1964 fingerprint,
1965 };
1966 self.schema_names
1967 .allocate(identity, preferred_name, collision_name)
1968 }
1969
1970 fn uses_aws_query_conventions(&self) -> bool {
1971 self.openapi_spec
1972 .pointer("/info/x-providerName")
1973 .and_then(Value::as_str)
1974 .is_some_and(|provider| provider.eq_ignore_ascii_case("amazonaws.com"))
1975 }
1976
1977 pub fn new(openapi_spec: Value) -> Result<Self> {
1981 Self::with_type_mapper(openapi_spec, TypeMapper::default())
1982 }
1983
1984 pub fn with_type_mapper(mut openapi_spec: Value, type_mapper: TypeMapper) -> Result<Self> {
1988 disambiguate_component_schema_names(&mut openapi_spec);
1989 let spec: OpenApiSpec = parse_spec_document(&openapi_spec)?;
1990 let schemas = Self::extract_schemas(&spec)?;
1991
1992 let component_parameters = spec
1993 .components
1994 .as_ref()
1995 .and_then(|c| c.parameters.as_ref())
1996 .cloned()
1997 .unwrap_or_default();
1998 let schema_names = SchemaNameRegistry::with_components(schemas.keys().cloned());
1999 Ok(Self {
2000 schemas,
2001 resolved_cache: BTreeMap::new(),
2002 schema_names,
2003 openapi_spec,
2004 current_schema_name: None,
2005 component_parameters,
2006 type_mapper,
2007 resolving_pointers: HashSet::new(),
2008 })
2009 }
2010
2011 pub fn new_with_extensions(
2014 openapi_spec: Value,
2015 extension_paths: &[std::path::PathBuf],
2016 ) -> Result<Self> {
2017 let merged_spec = merge_schema_extensions(openapi_spec, extension_paths)?;
2018 Self::new(merged_spec)
2019 }
2020
2021 pub fn new_with_extensions_and_type_mapper(
2024 openapi_spec: Value,
2025 extension_paths: &[std::path::PathBuf],
2026 type_mapper: TypeMapper,
2027 ) -> Result<Self> {
2028 let merged_spec = merge_schema_extensions(openapi_spec, extension_paths)?;
2029 Self::with_type_mapper(merged_spec, type_mapper)
2030 }
2031
2032 pub fn type_mapper(&self) -> &TypeMapper {
2036 &self.type_mapper
2037 }
2038
2039 fn generate_context_aware_name(
2042 &self,
2043 base_context: &str,
2044 type_hint: &str,
2045 index: usize,
2046 schema: Option<&Schema>,
2047 ) -> String {
2048 if let Some(schema) = schema {
2050 if type_hint == "Array"
2052 && matches!(schema.schema_type(), Some(OpenApiSchemaType::Array))
2053 {
2054 if let Some(items_schema) = schema.details().item_schema() {
2055 if let Some(item_type) = items_schema.schema_type() {
2057 match item_type {
2058 OpenApiSchemaType::Object => {
2059 return format!("{base_context}ItemArray");
2060 }
2061 OpenApiSchemaType::String => {
2062 return format!("{base_context}StringArray");
2063 }
2064 _ => {}
2065 }
2066 }
2067 }
2068 }
2069 }
2070
2071 match type_hint {
2073 "Array" => {
2074 format!("{base_context}Array")
2076 }
2077 "Variant" | "InlineVariant" => {
2078 if index == 0 {
2080 format!("{base_context}{type_hint}")
2081 } else {
2082 format!("{}{}{}", base_context, type_hint, index + 1)
2083 }
2084 }
2085 _ => {
2086 format!("{base_context}{type_hint}{index}")
2088 }
2089 }
2090 }
2091
2092 fn to_pascal_case(&self, s: &str) -> String {
2094 s.split(['_', '-'])
2095 .filter(|part| !part.is_empty())
2096 .map(|part| {
2097 let mut chars = part.chars();
2098 match chars.next() {
2099 None => String::new(),
2100 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
2101 }
2102 })
2103 .collect()
2104 }
2105
2106 fn extract_schemas(spec: &OpenApiSpec) -> Result<BTreeMap<String, Schema>> {
2107 let schemas = spec.components.as_ref().and_then(|c| c.schemas.as_ref());
2112 Ok(schemas
2113 .map(|m| {
2114 m.iter()
2115 .map(|(k, v)| (k.clone(), v.clone()))
2116 .collect::<BTreeMap<_, _>>()
2117 })
2118 .unwrap_or_default())
2119 }
2120
2121 pub fn analyze(&mut self) -> Result<SchemaAnalysis> {
2122 let validation_context = ValidationContext {
2123 openapi_version: self
2124 .openapi_spec
2125 .get("openapi")
2126 .and_then(Value::as_str)
2127 .unwrap_or_default()
2128 .to_string(),
2129 json_schema_dialect: self
2130 .openapi_spec
2131 .get("jsonSchemaDialect")
2132 .and_then(Value::as_str)
2133 .map(str::to_string),
2134 component_schemas: self
2135 .openapi_spec
2136 .pointer("/components/schemas")
2137 .and_then(Value::as_object)
2138 .map(|schemas| {
2139 schemas
2140 .iter()
2141 .map(|(name, schema)| (name.clone(), schema.clone()))
2142 .collect()
2143 })
2144 .unwrap_or_default(),
2145 };
2146 let mut analysis = SchemaAnalysis {
2147 schemas: BTreeMap::new(),
2148 dependencies: DependencyGraph::new(),
2149 patterns: DetectedPatterns {
2150 tagged_enum_schemas: HashSet::new(),
2151 untagged_enum_schemas: HashSet::new(),
2152 type_mappings: BTreeMap::new(),
2153 },
2154 operations: BTreeMap::new(),
2155 operation_responses: BTreeMap::new(),
2156 operation_sources: BTreeMap::new(),
2157 operation_id_aliases: BTreeMap::new(),
2158 used_type_features: crate::type_mapping::UsedFeatures::default(),
2159 enum_extensions: BTreeMap::new(),
2160 validation_context,
2161 untagged_union_branches: BTreeSet::new(),
2162 };
2163
2164 self.detect_patterns(&mut analysis.patterns)?;
2166
2167 let schema_names: Vec<String> = self.schemas.keys().cloned().collect();
2169 for schema_name in schema_names {
2170 let analyzed = self.analyze_schema(&schema_name)?;
2171
2172 for dep in &analyzed.dependencies {
2174 analysis
2175 .dependencies
2176 .add_dependency(schema_name.clone(), dep.clone());
2177 }
2178
2179 analysis.schemas.insert(schema_name, analyzed);
2180 }
2181
2182 for (inline_name, inline_schema) in &self.resolved_cache {
2185 if !analysis.schemas.contains_key(inline_name) {
2186 analysis
2188 .schemas
2189 .insert(inline_name.clone(), inline_schema.clone());
2190
2191 for dep in &inline_schema.dependencies {
2193 analysis
2194 .dependencies
2195 .add_dependency(inline_name.clone(), dep.clone());
2196 }
2197
2198 let mut schemas_to_update = Vec::new();
2203 for (schema_name, schema) in &analysis.schemas {
2204 if schema_name == inline_name {
2206 continue;
2207 }
2208
2209 if schema.dependencies.contains(inline_name) {
2210 schemas_to_update.push(schema_name.clone());
2212 }
2213 }
2214
2215 for schema_name in schemas_to_update {
2217 analysis
2218 .dependencies
2219 .add_dependency(schema_name, inline_name.clone());
2220 }
2221 }
2222 }
2223
2224 self.analyze_operations(&mut analysis)?;
2226
2227 for (inline_name, inline_schema) in &self.resolved_cache {
2230 if !analysis.schemas.contains_key(inline_name) {
2231 analysis
2232 .schemas
2233 .insert(inline_name.clone(), inline_schema.clone());
2234
2235 for dep in &inline_schema.dependencies {
2237 analysis
2238 .dependencies
2239 .add_dependency(inline_name.clone(), dep.clone());
2240 }
2241 }
2242 }
2243
2244 disambiguate_analyzed_schema_names(&mut analysis, &self.schemas);
2245
2246 analysis.used_type_features = self.type_mapper.used_features();
2250
2251 for (name, analyzed) in &analysis.schemas {
2256 let enum_value_count = match &analyzed.schema_type {
2257 SchemaType::StringEnum { values } => values.len(),
2258 SchemaType::ExtensibleEnum { known_values } => known_values.len(),
2259 _ => continue,
2260 };
2261 if let Some(ext) = extract_enum_extensions(&analyzed.original, enum_value_count, name) {
2262 analysis.enum_extensions.insert(name.clone(), ext);
2263 }
2264 }
2265
2266 analysis.untagged_union_branches = collect_untagged_union_branches(&analysis);
2267
2268 for schema in analysis.schemas.values_mut() {
2269 normalize_untyped(&mut schema.schema_type, 0);
2270 }
2271
2272 Ok(analysis)
2273 }
2274
2275 fn detect_patterns(&self, patterns: &mut DetectedPatterns) -> Result<()> {
2276 for (schema_name, schema) in &self.schemas {
2277 if self.is_discriminated_union(schema) {
2279 patterns.tagged_enum_schemas.insert(schema_name.clone());
2280
2281 if let Some(mappings) = self.extract_type_mappings(schema)? {
2283 patterns.type_mappings.insert(schema_name.clone(), mappings);
2284 }
2285 }
2286 else if self.is_simple_union(schema) {
2288 patterns.untagged_enum_schemas.insert(schema_name.clone());
2289 }
2290 }
2291
2292 Ok(())
2293 }
2294
2295 fn is_discriminated_union(&self, schema: &Schema) -> bool {
2296 if schema.is_discriminated_union() {
2298 return true;
2299 }
2300
2301 if let Some(variants) = schema.union_variants() {
2303 return variants.len() > 2 && self.detect_discriminator_field(variants).is_some();
2304 }
2305
2306 false
2307 }
2308
2309 fn all_variants_have_unique_const_values(&self, variants: &[Schema], field_name: &str) -> bool {
2310 let mut values = HashSet::new();
2311
2312 variants.iter().all(|variant| {
2313 let schema = if let Some(ref_str) = variant.reference() {
2314 let Some(schema_name) = self.extract_schema_name(ref_str) else {
2315 return false;
2316 };
2317 let Some(schema) = self.schemas.get(schema_name) else {
2318 return false;
2319 };
2320 schema
2321 } else {
2322 variant
2323 };
2324
2325 self.extract_discriminator_value_for_field(schema, field_name)
2326 .is_some_and(|value| values.insert(value))
2327 })
2328 }
2329
2330 fn branch_resolves_to_object(&self, schema: &Schema) -> bool {
2339 self.branch_resolves_to_object_inner(schema, &mut HashSet::new())
2340 }
2341
2342 fn branch_resolves_to_object_inner(
2343 &self,
2344 schema: &Schema,
2345 visited_refs: &mut HashSet<String>,
2346 ) -> bool {
2347 if let Some(ref_str) = schema.reference() {
2349 if !visited_refs.insert(ref_str.to_string()) {
2350 return false;
2351 }
2352 let result = match self
2353 .extract_schema_name(ref_str)
2354 .and_then(|n| self.schemas.get(n))
2355 {
2356 Some(target) => self.branch_resolves_to_object_inner(target, visited_refs),
2357 None => false,
2358 };
2359 visited_refs.remove(ref_str);
2360 return result;
2361 }
2362 if let Schema::AllOf { all_of, .. } = schema {
2366 if Self::single_non_object_allof_carrier(all_of).is_some() {
2367 return false;
2368 }
2369 return all_of
2370 .iter()
2371 .filter(|member| !schema_is_annotation_only(member))
2372 .any(|member| self.branch_resolves_to_object_inner(member, visited_refs));
2373 }
2374 if let Some(variants) = schema.union_variants() {
2379 return !variants.is_empty()
2380 && variants
2381 .iter()
2382 .all(|variant| self.branch_resolves_to_object_inner(variant, visited_refs));
2383 }
2384 if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object)) {
2385 return true;
2386 }
2387 if schema.inferred_type() == Some(OpenApiSchemaType::Object) {
2388 return true;
2389 }
2390 false
2393 }
2394
2395 fn detect_discriminator_field(&self, variants: &[Schema]) -> Option<String> {
2399 if variants.is_empty() {
2400 return None;
2401 }
2402
2403 let first_variant = &variants[0];
2405 let first_schema = if let Some(ref_str) = first_variant.reference() {
2406 let schema_name = self.extract_schema_name(ref_str)?;
2407 self.schemas.get(schema_name)?
2408 } else {
2409 first_variant
2410 };
2411
2412 let properties = first_schema.details().properties.as_ref()?;
2413 let mut candidates: Vec<String> = Vec::new();
2414
2415 for (field_name, field_schema) in properties {
2416 let details = field_schema.details();
2417 let is_const = details.const_value.is_some()
2418 || details.enum_values.as_ref().is_some_and(|v| v.len() == 1)
2419 || details.extra.contains_key("const");
2420 if is_const {
2421 candidates.push(field_name.clone());
2422 }
2423 }
2424
2425 if candidates.is_empty() {
2426 return None;
2427 }
2428
2429 candidates.sort_by(|a, b| {
2431 if a == "type" {
2432 std::cmp::Ordering::Less
2433 } else if b == "type" {
2434 std::cmp::Ordering::Greater
2435 } else {
2436 a.cmp(b)
2437 }
2438 });
2439
2440 for candidate in &candidates {
2446 if self.all_variants_have_unique_const_values(variants, candidate) {
2447 return Some(candidate.clone());
2448 }
2449 }
2450
2451 None
2452 }
2453
2454 fn is_simple_union(&self, schema: &Schema) -> bool {
2455 if let Some(variants) = schema.union_variants() {
2456 if variants.len() > 1 && !schema.is_nullable_pattern() {
2458 let has_refs = variants.iter().any(|v| v.is_reference());
2459 return has_refs;
2460 }
2461 }
2462 false
2463 }
2464
2465 fn schema_or_reference_is_nullable(&self, schema: &Schema) -> bool {
2470 let mut current = schema.clone();
2471 let mut visited = HashSet::new();
2472 loop {
2473 if current.is_nullable_any() {
2474 return true;
2475 }
2476 if let Schema::AllOf { all_of, .. } = ¤t
2477 && let Some(carrier) = Self::single_non_object_allof_carrier(all_of)
2478 {
2479 current = carrier.clone();
2480 continue;
2481 }
2482 let Some(reference) = current.reference() else {
2483 return false;
2484 };
2485 if !visited.insert(reference.to_string()) {
2486 return false;
2487 }
2488 let Some(target) = self.reference_target_schema(reference) else {
2489 return false;
2490 };
2491 current = target;
2492 }
2493 }
2494
2495 fn nullable_container_value(&self, schema: &Schema, schema_type: SchemaType) -> SchemaType {
2501 if !self.schema_or_reference_is_nullable(schema)
2502 || matches!(schema_type, SchemaType::Nullable { .. })
2503 || matches!(schema.schema_type(), Some(OpenApiSchemaType::Null))
2504 {
2505 schema_type
2506 } else {
2507 SchemaType::Nullable {
2508 inner_type: Box::new(schema_type),
2509 }
2510 }
2511 }
2512
2513 fn extract_type_mappings(&self, schema: &Schema) -> Result<Option<BTreeMap<String, String>>> {
2514 let variants = schema.union_variants().ok_or_else(|| {
2515 GeneratorError::InvalidSchema("No variants found for discriminated union".to_string())
2516 })?;
2517
2518 let discriminator_field = if let Some(discriminator) = schema.discriminator() {
2520 discriminator.property_name.clone()
2521 } else if let Some(detected) = self.detect_discriminator_field(variants) {
2522 detected
2523 } else {
2524 "type".to_string() };
2526
2527 let mut mappings = BTreeMap::new();
2528
2529 for variant in variants {
2530 if let Some(ref_str) = variant.reference() {
2531 if let Some(type_name) = self.extract_schema_name(ref_str) {
2532 if let Some(variant_schema) = self.schemas.get(type_name) {
2533 if let Some(discriminator_value) = self
2534 .extract_discriminator_value_for_field(
2535 variant_schema,
2536 &discriminator_field,
2537 )
2538 {
2539 mappings.insert(type_name.to_string(), discriminator_value);
2540 }
2541 }
2542 }
2543 }
2544 }
2545
2546 if mappings.is_empty() {
2547 Ok(None)
2548 } else {
2549 Ok(Some(mappings))
2550 }
2551 }
2552
2553 #[allow(dead_code)]
2554 fn extract_discriminator_value(&self, schema: &Schema) -> Option<String> {
2555 self.extract_discriminator_value_for_field(schema, "type")
2556 }
2557
2558 fn extract_discriminator_values_for_field(
2559 &self,
2560 schema: &Schema,
2561 field_name: &str,
2562 ) -> Vec<String> {
2563 self.extract_discriminator_value_domain_for_field(schema, field_name)
2564 .unwrap_or_default()
2565 }
2566
2567 fn extract_discriminator_value_domain_for_field(
2568 &self,
2569 schema: &Schema,
2570 field_name: &str,
2571 ) -> Option<Vec<String>> {
2572 let mut visited_refs = HashSet::new();
2573 self.discriminator_value_domain(schema, field_name, &mut visited_refs)
2574 }
2575
2576 fn discriminator_value_domain(
2583 &self,
2584 schema: &Schema,
2585 field_name: &str,
2586 visited_refs: &mut HashSet<String>,
2587 ) -> Option<Vec<String>> {
2588 if let Some(reference) = schema.reference() {
2589 if !visited_refs.insert(reference.to_string()) {
2590 return None;
2591 }
2592 let result = self
2593 .extract_schema_name(reference)
2594 .and_then(|name| self.schemas.get(name))
2595 .and_then(|target| {
2596 self.discriminator_value_domain(target, field_name, visited_refs)
2597 });
2598 visited_refs.remove(reference);
2599 return result;
2600 }
2601
2602 let own_domain = schema
2603 .details()
2604 .properties
2605 .as_ref()
2606 .and_then(|properties| properties.get(field_name))
2607 .and_then(|property| self.string_constraint_domain(property, visited_refs));
2608
2609 let composition_domain = match schema {
2610 Schema::AllOf { all_of, .. } => {
2611 let mut domain = None;
2612 for member in all_of {
2613 domain = Self::intersect_optional_domains(
2614 domain,
2615 self.discriminator_value_domain(member, field_name, visited_refs),
2616 );
2617 }
2618 domain
2619 }
2620 Schema::AnyOf { any_of, .. } => {
2621 self.union_discriminator_domains(any_of, field_name, visited_refs)
2622 }
2623 Schema::OneOf { one_of, .. } => {
2624 self.union_discriminator_domains(one_of, field_name, visited_refs)
2625 }
2626 Schema::Bool(false) => Some(Vec::new()),
2627 _ => None,
2628 };
2629
2630 Self::intersect_optional_domains(own_domain, composition_domain)
2631 }
2632
2633 fn union_discriminator_domains(
2634 &self,
2635 schemas: &[Schema],
2636 field_name: &str,
2637 visited_refs: &mut HashSet<String>,
2638 ) -> Option<Vec<String>> {
2639 if schemas.is_empty() {
2640 return Some(Vec::new());
2641 }
2642 let mut domain = Vec::new();
2643 for schema in schemas {
2644 let values = self.discriminator_value_domain(schema, field_name, visited_refs)?;
2645 for value in values {
2646 Self::push_unique_string(&mut domain, &value);
2647 }
2648 }
2649 Some(domain)
2650 }
2651
2652 fn string_constraint_domain(
2653 &self,
2654 schema: &Schema,
2655 visited_refs: &mut HashSet<String>,
2656 ) -> Option<Vec<String>> {
2657 let allow_vendor_default =
2658 !self.has_standard_string_constraint(schema, &mut HashSet::new());
2659 self.string_constraint_domain_inner(schema, visited_refs, allow_vendor_default)
2660 }
2661
2662 fn string_constraint_domain_inner(
2663 &self,
2664 schema: &Schema,
2665 visited_refs: &mut HashSet<String>,
2666 allow_vendor_default: bool,
2667 ) -> Option<Vec<String>> {
2668 if let Some(reference) = schema.reference() {
2669 if !visited_refs.insert(reference.to_string()) {
2670 return None;
2671 }
2672 let result = self
2673 .extract_schema_name(reference)
2674 .and_then(|name| self.schemas.get(name))
2675 .and_then(|target| {
2676 self.string_constraint_domain_inner(target, visited_refs, allow_vendor_default)
2677 });
2678 visited_refs.remove(reference);
2679 return result;
2680 }
2681
2682 let details = schema.details();
2683 let mut own_domain = None;
2684 if let Some(value) = details.const_value.as_ref() {
2685 own_domain = Self::intersect_optional_domains(
2686 own_domain,
2687 Some(value.as_str().into_iter().map(str::to_string).collect()),
2688 );
2689 }
2690 if let Some(enum_values) = &details.enum_values {
2691 own_domain = Self::intersect_optional_domains(
2692 own_domain,
2693 Some(
2694 enum_values
2695 .iter()
2696 .filter_map(Value::as_str)
2697 .map(str::to_string)
2698 .collect(),
2699 ),
2700 );
2701 }
2702 if allow_vendor_default
2703 && details
2704 .extra
2705 .get("x-stainless-const")
2706 .and_then(Value::as_bool)
2707 == Some(true)
2708 && let Some(default) = details.default.as_ref().and_then(Value::as_str)
2709 {
2710 own_domain =
2711 Self::intersect_optional_domains(own_domain, Some(vec![default.to_string()]));
2712 }
2713
2714 let composition_domain = match schema {
2715 Schema::AllOf { all_of, .. } => {
2716 let mut domain = None;
2717 for member in all_of {
2718 domain = Self::intersect_optional_domains(
2719 domain,
2720 self.string_constraint_domain_inner(
2721 member,
2722 visited_refs,
2723 allow_vendor_default,
2724 ),
2725 );
2726 }
2727 domain
2728 }
2729 Schema::AnyOf { any_of, .. } => {
2730 self.union_string_constraint_domains(any_of, visited_refs, allow_vendor_default)
2731 }
2732 Schema::OneOf { one_of, .. } => {
2733 self.union_string_constraint_domains(one_of, visited_refs, allow_vendor_default)
2734 }
2735 Schema::Bool(false) => Some(Vec::new()),
2736 _ => None,
2737 };
2738
2739 Self::intersect_optional_domains(own_domain, composition_domain)
2740 }
2741
2742 fn union_string_constraint_domains(
2743 &self,
2744 schemas: &[Schema],
2745 visited_refs: &mut HashSet<String>,
2746 allow_vendor_default: bool,
2747 ) -> Option<Vec<String>> {
2748 if schemas.is_empty() {
2749 return Some(Vec::new());
2750 }
2751 let mut domain = Vec::new();
2752 for schema in schemas {
2753 let values =
2754 self.string_constraint_domain_inner(schema, visited_refs, allow_vendor_default)?;
2755 for value in values {
2756 Self::push_unique_string(&mut domain, &value);
2757 }
2758 }
2759 Some(domain)
2760 }
2761
2762 fn has_standard_string_constraint(
2763 &self,
2764 schema: &Schema,
2765 visited_refs: &mut HashSet<String>,
2766 ) -> bool {
2767 if let Some(reference) = schema.reference() {
2768 if !visited_refs.insert(reference.to_string()) {
2769 return false;
2770 }
2771 let result = self
2772 .extract_schema_name(reference)
2773 .and_then(|name| self.schemas.get(name))
2774 .is_some_and(|target| self.has_standard_string_constraint(target, visited_refs));
2775 visited_refs.remove(reference);
2776 return result;
2777 }
2778
2779 let details = schema.details();
2780 if details.const_value.is_some() || details.enum_values.is_some() {
2781 return true;
2782 }
2783
2784 match schema {
2785 Schema::AllOf { all_of, .. } => all_of
2786 .iter()
2787 .any(|member| self.has_standard_string_constraint(member, visited_refs)),
2788 Schema::AnyOf { any_of, .. } => any_of
2789 .iter()
2790 .any(|member| self.has_standard_string_constraint(member, visited_refs)),
2791 Schema::OneOf { one_of, .. } => one_of
2792 .iter()
2793 .any(|member| self.has_standard_string_constraint(member, visited_refs)),
2794 _ => false,
2795 }
2796 }
2797
2798 fn intersect_optional_domains(
2799 left: Option<Vec<String>>,
2800 right: Option<Vec<String>>,
2801 ) -> Option<Vec<String>> {
2802 match (left, right) {
2803 (None, other) | (other, None) => other,
2804 (Some(left), Some(right)) => Some(
2805 left.into_iter()
2806 .filter(|value| right.contains(value))
2807 .collect(),
2808 ),
2809 }
2810 }
2811
2812 fn push_unique_string(values: &mut Vec<String>, value: &str) {
2813 if !values.iter().any(|existing| existing == value) {
2814 values.push(value.to_string());
2815 }
2816 }
2817
2818 fn discriminator_property_presence(&self, schema: &Schema, field_name: &str) -> (bool, bool) {
2819 self.discriminator_property_presence_inner(schema, field_name, &mut HashSet::new(), 0)
2820 }
2821
2822 fn discriminator_property_presence_inner(
2823 &self,
2824 schema: &Schema,
2825 field_name: &str,
2826 visited_refs: &mut HashSet<String>,
2827 depth: usize,
2828 ) -> (bool, bool) {
2829 if depth > 64 {
2830 return (false, false);
2831 }
2832 if let Some(reference) = schema.reference() {
2833 if !visited_refs.insert(reference.to_string()) {
2834 return (false, false);
2835 }
2836 let result = self
2837 .extract_schema_name(reference)
2838 .and_then(|name| self.schemas.get(name))
2839 .map(|target| {
2840 self.discriminator_property_presence_inner(
2841 target,
2842 field_name,
2843 visited_refs,
2844 depth + 1,
2845 )
2846 })
2847 .unwrap_or((false, false));
2848 visited_refs.remove(reference);
2849 return result;
2850 }
2851
2852 let details = schema.details();
2853 let mut declared = details
2854 .properties
2855 .as_ref()
2856 .is_some_and(|properties| properties.contains_key(field_name));
2857 let mut required = details
2858 .required
2859 .as_ref()
2860 .is_some_and(|names| names.iter().any(|name| name == field_name));
2861
2862 let members = match schema {
2863 Schema::AllOf { all_of, .. } => Some(all_of.as_slice()),
2864 Schema::AnyOf { any_of, .. } => Some(any_of.as_slice()),
2865 Schema::OneOf { one_of, .. } => Some(one_of.as_slice()),
2866 _ => None,
2867 };
2868 if let Some(members) = members {
2869 for member in members {
2870 let (member_declared, member_required) = self
2871 .discriminator_property_presence_inner(
2872 member,
2873 field_name,
2874 visited_refs,
2875 depth + 1,
2876 );
2877 declared |= member_declared;
2878 required |= member_required;
2879 }
2880 }
2881 (declared, required)
2882 }
2883
2884 fn extract_discriminator_value_for_field(
2885 &self,
2886 schema: &Schema,
2887 field_name: &str,
2888 ) -> Option<String> {
2889 self.extract_discriminator_values_for_field(schema, field_name)
2890 .into_iter()
2891 .next()
2892 }
2893
2894 fn get_any_reference<'a>(&self, schema: &'a Schema) -> Option<&'a str> {
2895 schema.reference().or_else(|| schema.recursive_reference())
2896 }
2897
2898 fn extract_schema_name<'a>(&self, ref_str: &'a str) -> Option<&'a str> {
2899 if ref_str == "#" {
2900 return None; }
2902
2903 let parts: Vec<&str> = ref_str.split('/').collect();
2904
2905 if parts.len() == 4 && parts[0] == "#" && parts[2] == "schemas" {
2909 return Some(parts[3]);
2910 }
2911
2912 if parts.len() == 3 && parts[0] == "#" && parts[1] == "definitions" {
2915 return Some(parts[2]);
2916 }
2917
2918 if ref_str.starts_with("#/") {
2922 return None;
2923 }
2924
2925 let last = parts.last()?;
2931 if last.is_empty()
2932 || last.chars().all(|c| c.is_ascii_digit())
2933 || matches!(
2934 *last,
2935 "schema" | "properties" | "items" | "additionalProperties"
2936 )
2937 {
2938 return None;
2939 }
2940 let first = last.chars().next().unwrap_or(' ');
2941 if !first.is_ascii_alphabetic() || !first.is_ascii_uppercase() {
2942 return None;
2943 }
2944 Some(last)
2945 }
2946
2947 fn reference_target_schema(&self, reference: &str) -> Option<Schema> {
2950 if let Some(name) = self.extract_schema_name(reference) {
2951 return self.schemas.get(name).cloned();
2952 }
2953 let pointer = reference.strip_prefix('#')?;
2954 if !pointer.starts_with('/') {
2955 return None;
2956 }
2957 Schema::deserialize(self.openapi_spec.pointer(pointer)?).ok()
2958 }
2959
2960 fn analyze_schema(&mut self, schema_name: &str) -> Result<AnalyzedSchema> {
2961 let emitted_name = self
2965 .schema_names
2966 .component_name(schema_name)
2967 .ok_or_else(|| GeneratorError::UnresolvedReference(schema_name.to_string()))?
2968 .to_string();
2969 if let Some(cached) = self.resolved_cache.get(&emitted_name) {
2970 return Ok(cached.clone());
2971 }
2972
2973 self.current_schema_name = Some(emitted_name.clone());
2975
2976 let schema = self
2977 .schemas
2978 .get(schema_name)
2979 .ok_or_else(|| GeneratorError::UnresolvedReference(schema_name.to_string()))?
2980 .clone();
2981
2982 self.resolved_cache.insert(
2984 emitted_name.clone(),
2985 AnalyzedSchema {
2986 name: emitted_name.clone(),
2987 original: serde_json::to_value(&schema).unwrap_or(Value::Null),
2988 schema_type: SchemaType::Reference {
2989 target: "placeholder".to_string(),
2990 },
2991 dependencies: HashSet::new(),
2992 nullable: false,
2993 description: None,
2994 default: None,
2995 },
2996 );
2997
2998 let analyzed = self.analyze_schema_value(&schema, &emitted_name)?;
2999
3000 self.resolved_cache.insert(emitted_name, analyzed.clone());
3002
3003 Ok(analyzed)
3004 }
3005
3006 fn analyze_schema_value(
3007 &mut self,
3008 schema: &Schema,
3009 schema_name: &str,
3010 ) -> Result<AnalyzedSchema> {
3011 let details = schema.details();
3012 let description = details.description.clone();
3013 let nullable = schema.is_nullable_any();
3017 let mut dependencies = HashSet::new();
3018
3019 let schema_type = match schema {
3020 Schema::Bool(accepts_anything) => self.untyped_value(
3023 self.untyped_context(""),
3024 if *accepts_anything {
3025 UntypedReason::AnySchema
3026 } else {
3027 UntypedReason::NeverMatches
3028 },
3029 ),
3030 Schema::Reference { reference, .. } => {
3031 match self.extract_schema_name(reference) {
3036 Some(name) => {
3037 let target = name.to_string();
3038 dependencies.insert(target.clone());
3039 SchemaType::Reference { target }
3040 }
3041 None => {
3042 let reference = reference.clone();
3043 if let Some(resolved) =
3044 self.resolve_pointer_schema(&reference, &mut dependencies)?
3045 {
3046 resolved
3047 } else {
3048 eprintln!(
3049 "⚠️ unresolvable $ref `{}` — typing as serde_json::Value",
3050 reference
3051 );
3052 self.untyped_value(
3053 format!("$ref {reference}"),
3054 UntypedReason::UnresolvedReference,
3055 )
3056 }
3057 }
3058 }
3059 }
3060 Schema::RecursiveRef { recursive_ref, .. }
3061 | Schema::DynamicRef {
3062 dynamic_ref: recursive_ref,
3063 ..
3064 } => {
3065 if recursive_ref == "#" {
3071 dependencies.insert(schema_name.to_string());
3072 SchemaType::Reference {
3073 target: schema_name.to_string(),
3074 }
3075 } else {
3076 let target = self
3077 .extract_schema_name(recursive_ref)
3078 .unwrap_or(schema_name)
3079 .to_string();
3080 dependencies.insert(target.clone());
3081 SchemaType::Reference { target }
3082 }
3083 }
3084 Schema::Typed { .. } | Schema::TypedMulti { .. } => {
3085 if let Some(non_null_types) = schema.non_null_schema_types() {
3086 let mut variants = Vec::with_capacity(non_null_types.len());
3087 for t in non_null_types {
3088 variants.push(self.build_typed_multi_union_variant(
3089 t,
3090 schema,
3091 schema_name,
3092 &mut dependencies,
3093 )?);
3094 }
3095 SchemaType::Union {
3096 variants,
3097 exclusive: false,
3098 }
3099 } else {
3100 self.analyze_single_typed_schema(
3101 schema,
3102 schema_name,
3103 details,
3104 &mut dependencies,
3105 )?
3106 }
3107 }
3108 Schema::AnyOf {
3109 any_of,
3110 discriminator,
3111 ..
3112 } => {
3113 if Self::union_only_constrains_requiredness(any_of) {
3114 return Ok(AnalyzedSchema {
3115 name: schema_name.to_string(),
3116 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3117 schema_type: self.analyze_empty_union(schema, &mut dependencies)?,
3118 dependencies,
3119 nullable,
3120 description,
3121 default: details.default.clone(),
3122 });
3123 }
3124 if let Some(schema_type) = self.analyze_object_with_variants(
3125 schema,
3126 any_of,
3127 schema_name,
3128 &mut dependencies,
3129 )? {
3130 return Ok(AnalyzedSchema {
3131 name: schema_name.to_string(),
3132 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3133 schema_type,
3134 dependencies,
3135 nullable,
3136 description,
3137 default: details.default.clone(),
3138 });
3139 }
3140 self.analyze_anyof_union(
3142 any_of,
3143 discriminator.as_ref(),
3144 &mut dependencies,
3145 schema_name,
3146 )?
3147 }
3148 Schema::OneOf {
3149 one_of,
3150 discriminator,
3151 ..
3152 } => {
3153 if one_of.is_empty() {
3154 self.analyze_empty_union(schema, &mut dependencies)?
3155 } else if let Some(schema_type) = self.analyze_object_with_variants(
3156 schema,
3157 one_of,
3158 schema_name,
3159 &mut dependencies,
3160 )? {
3161 schema_type
3162 } else {
3163 self.analyze_oneof_union(
3165 one_of,
3166 discriminator.as_ref(),
3167 schema_name,
3168 &mut dependencies,
3169 InlineUnionKind::OneOf,
3170 None,
3171 )?
3172 }
3173 }
3174 Schema::AllOf { all_of, .. } => {
3175 self.analyze_allof_composition(schema, all_of, &mut dependencies)?
3177 }
3178 Schema::Untyped { .. } => {
3179 if let Some(inferred) = schema.inferred_type() {
3181 match inferred {
3182 OpenApiSchemaType::Object => {
3183 if self.should_use_dynamic_json(schema) {
3184 self.untyped_value(
3185 self.untyped_context(""),
3186 UntypedReason::OpaqueObject,
3187 )
3188 } else {
3189 self.analyze_object_schema(schema, &mut dependencies)?
3190 }
3191 }
3192 OpenApiSchemaType::String if details.is_string_enum() => {
3193 SchemaType::StringEnum {
3194 values: details.string_enum_values().unwrap_or_default(),
3195 }
3196 }
3197 OpenApiSchemaType::Null => SchemaType::Primitive {
3200 rust_type: self.type_mapper.null_unit().rust_type,
3201 serde_with: None,
3202 },
3203 _ => self.untyped_value(
3204 self.untyped_context(""),
3205 UntypedReason::UnsupportedTypeKeyword,
3206 ),
3207 }
3208 } else {
3209 self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema)
3210 }
3211 }
3212 };
3213
3214 Ok(AnalyzedSchema {
3215 name: schema_name.to_string(),
3216 original: serde_json::to_value(schema).unwrap_or(Value::Null), schema_type,
3218 dependencies,
3219 nullable,
3220 description,
3221 default: details.default.clone(),
3222 })
3223 }
3224
3225 fn analyze_single_typed_schema(
3231 &mut self,
3232 schema: &Schema,
3233 schema_name: &str,
3234 details: &crate::openapi::SchemaDetails,
3235 dependencies: &mut HashSet<String>,
3236 ) -> Result<SchemaType> {
3237 let primary = schema
3238 .schema_type()
3239 .cloned()
3240 .unwrap_or(OpenApiSchemaType::Object);
3241 let format = details.format.as_deref();
3242 Ok(match primary {
3243 OpenApiSchemaType::String => {
3244 if let Some(values) = details.string_enum_values() {
3245 SchemaType::StringEnum { values }
3246 } else {
3247 let mapped = self.type_mapper.string_format(format);
3248 SchemaType::Primitive {
3249 rust_type: mapped.rust_type,
3250 serde_with: mapped.serde_with,
3251 }
3252 }
3253 }
3254 OpenApiSchemaType::Integer => SchemaType::Primitive {
3255 rust_type: self.integer_rust_type(details),
3256 serde_with: None,
3257 },
3258 OpenApiSchemaType::Number => SchemaType::Primitive {
3259 rust_type: self.type_mapper.number_format(format).rust_type,
3260 serde_with: None,
3261 },
3262 OpenApiSchemaType::Boolean => SchemaType::Primitive {
3263 rust_type: self.type_mapper.boolean().rust_type,
3264 serde_with: None,
3265 },
3266 OpenApiSchemaType::Array => {
3267 self.analyze_array_schema(schema, schema_name, dependencies)?
3268 }
3269 OpenApiSchemaType::Object => {
3270 if self.should_use_dynamic_json(schema) {
3271 self.untyped_value(self.untyped_context(""), UntypedReason::OpaqueObject)
3272 } else {
3273 self.analyze_object_schema(schema, dependencies)?
3274 }
3275 }
3276 OpenApiSchemaType::Null => SchemaType::Primitive {
3279 rust_type: self.type_mapper.null_unit().rust_type,
3280 serde_with: None,
3281 },
3282 })
3283 }
3284
3285 fn analyze_object_schema(
3286 &mut self,
3287 schema: &Schema,
3288 dependencies: &mut HashSet<String>,
3289 ) -> Result<SchemaType> {
3290 let details = schema.details();
3291 let properties = &details.properties;
3292 let required = details
3293 .required
3294 .as_ref()
3295 .map(|req| req.iter().cloned().collect::<HashSet<String>>())
3296 .unwrap_or_default();
3297
3298 let mut property_info = BTreeMap::new();
3299 let owner_name = self
3302 .current_schema_name
3303 .clone()
3304 .unwrap_or_else(|| "Inline".to_string());
3305
3306 if let Some(props) = properties {
3307 for (prop_name, prop_schema) in props {
3308 let prop_type = if let Schema::AnyOf { any_of, .. } = prop_schema {
3310 if Self::union_only_constrains_requiredness(any_of) {
3315 self.analyze_empty_union(prop_schema, dependencies)?
3316 } else if let Some(with_variants) = {
3317 let variant_owner =
3318 format!("{owner_name}{}", self.to_pascal_case(prop_name));
3319 self.with_schema_context(&variant_owner, |analyzer| {
3320 analyzer.analyze_object_with_variants(
3321 prop_schema,
3322 any_of,
3323 &variant_owner,
3324 dependencies,
3325 )
3326 })?
3327 } {
3328 with_variants
3329 } else if self.should_use_dynamic_json(prop_schema) {
3330 self.untyped_value(
3332 self.untyped_context(prop_name),
3333 UntypedReason::OpaqueObject,
3334 )
3335 } else if prop_schema.is_nullable_pattern()
3336 && let Some(non_null) = prop_schema.non_null_variant()
3337 {
3338 self.analyze_property_schema_with_context(
3346 non_null,
3347 Some(prop_name),
3348 dependencies,
3349 )?
3350 } else {
3351 let context_name = self
3354 .current_schema_name
3355 .clone()
3356 .unwrap_or_else(|| "Unknown".to_string());
3357
3358 let prop_pascal = self.to_pascal_case(prop_name);
3360 let preferred_union_name = format!("{context_name}{prop_pascal}");
3361 let union_type_name = self.allocate_inline_schema_name(
3362 &preferred_union_name,
3363 &format!("{preferred_union_name}Union2"),
3364 "property-anyof",
3365 prop_schema,
3366 );
3367
3368 let union_schema_type = self.analyze_anyof_union(
3370 any_of,
3371 prop_schema.discriminator(),
3372 dependencies,
3373 &union_type_name,
3374 )?;
3375
3376 self.resolved_cache.insert(
3378 union_type_name.clone(),
3379 AnalyzedSchema {
3380 name: union_type_name.clone(),
3381 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
3382 dependencies: schema_type_dependencies(&union_schema_type),
3383 schema_type: union_schema_type,
3384 nullable: false,
3385 description: prop_schema.details().description.clone(),
3386 default: None,
3387 },
3388 );
3389
3390 dependencies.insert(union_type_name.clone());
3392 SchemaType::Reference {
3393 target: union_type_name,
3394 }
3395 }
3396 } else if let Schema::OneOf {
3397 one_of,
3398 discriminator,
3399 ..
3400 } = prop_schema
3401 {
3402 if prop_schema.is_nullable_pattern()
3409 && let Some(non_null) = prop_schema.non_null_variant()
3410 {
3411 let unwrapped = self.analyze_property_schema_with_context(
3412 non_null,
3413 Some(prop_name),
3414 dependencies,
3415 )?;
3416 let owner_name = self
3417 .current_schema_name
3418 .clone()
3419 .unwrap_or_else(|| "Inline".to_string());
3420 let unwrapped = self.hoist_inline_property_type(
3421 &owner_name,
3422 prop_name,
3423 unwrapped,
3424 dependencies,
3425 );
3426 let prop_details = prop_schema.details();
3427 let prop_nullable = true;
3428 let prop_description = prop_details.description.clone();
3429 let prop_default = prop_details.default.clone();
3430 property_info.insert(
3431 prop_name.clone(),
3432 PropertyInfo {
3433 schema_type: unwrapped,
3434 nullable: prop_nullable,
3435 description: prop_description,
3436 default: prop_default,
3437 serde_attrs: Vec::new(),
3438 synthesized_required: false,
3439 constraints: PropertyConstraints::from_schema_details(prop_details),
3440 },
3441 );
3442 continue;
3443 }
3444
3445 let context_name = self
3447 .current_schema_name
3448 .clone()
3449 .unwrap_or_else(|| "Unknown".to_string());
3450 let prop_pascal = self.to_pascal_case(prop_name);
3451 let preferred_union_name = format!("{context_name}{prop_pascal}");
3452 let union_type_name = self.allocate_inline_schema_name(
3453 &preferred_union_name,
3454 &format!("{preferred_union_name}Union2"),
3455 "property-oneof",
3456 prop_schema,
3457 );
3458
3459 let union_schema_type = self.analyze_oneof_union(
3461 one_of,
3462 discriminator.as_ref(),
3463 &union_type_name,
3464 dependencies,
3465 InlineUnionKind::OneOf,
3466 None,
3467 )?;
3468
3469 self.resolved_cache.insert(
3471 union_type_name.clone(),
3472 AnalyzedSchema {
3473 name: union_type_name.clone(),
3474 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
3475 schema_type: union_schema_type,
3476 dependencies: HashSet::new(),
3477 nullable: false,
3478 description: prop_schema.details().description.clone(),
3479 default: None,
3480 },
3481 );
3482
3483 dependencies.insert(union_type_name.clone());
3485 SchemaType::Reference {
3486 target: union_type_name,
3487 }
3488 } else {
3489 self.analyze_property_schema_with_context(
3491 prop_schema,
3492 Some(prop_name),
3493 dependencies,
3494 )?
3495 };
3496
3497 let prop_type = self.hoist_inline_property_type(
3498 &owner_name,
3499 prop_name,
3500 prop_type,
3501 dependencies,
3502 );
3503
3504 let prop_details = prop_schema.details();
3505 let prop_nullable = self.schema_or_reference_is_nullable(prop_schema);
3507 let prop_description = prop_details.description.clone();
3508 let prop_default = prop_details.default.clone();
3509
3510 property_info.insert(
3511 prop_name.clone(),
3512 PropertyInfo {
3513 schema_type: prop_type,
3514 nullable: prop_nullable,
3515 description: prop_description,
3516 default: prop_default,
3517 serde_attrs: Vec::new(),
3518 synthesized_required: false,
3519 constraints: PropertyConstraints::from_schema_details(prop_details),
3520 },
3521 );
3522 }
3523 }
3524
3525 let typed_enabled = self
3533 .type_mapper
3534 .config()
3535 .shape
3536 .as_ref()
3537 .and_then(|s| s.additional_properties_typed)
3538 .unwrap_or(true);
3539
3540 let untyped_required_property = || PropertyInfo {
3541 schema_type: SchemaType::Untyped {
3542 shape: UntypedShape::Value,
3543 reason: UntypedReason::AnySchema,
3544 },
3545 nullable: false,
3546 description: None,
3547 default: None,
3548 serde_attrs: Vec::new(),
3549 synthesized_required: true,
3550 constraints: PropertyConstraints::default(),
3551 };
3552 let (mut additional_properties, required_additional_property, explicitly_forbidden) =
3553 match &details.additional_properties {
3554 Some(crate::openapi::AdditionalProperties::Boolean(true)) => (
3555 ObjectAdditionalProperties::Untyped,
3556 Some(untyped_required_property()),
3557 false,
3558 ),
3559 Some(crate::openapi::AdditionalProperties::Boolean(false)) => {
3560 (ObjectAdditionalProperties::Denied, None, true)
3561 }
3562 Some(crate::openapi::AdditionalProperties::Schema(value_schema))
3563 if typed_enabled =>
3564 {
3565 let analyzed = self.analyze_property_schema_with_context(
3566 value_schema,
3567 Some("AdditionalProperty"),
3568 dependencies,
3569 )?;
3570 let nullable_value =
3571 self.nullable_container_value(value_schema, analyzed.clone());
3572 let value_details = value_schema.details();
3573 let required_property = PropertyInfo {
3574 schema_type: analyzed.clone(),
3575 nullable: self.schema_or_reference_is_nullable(value_schema),
3576 description: value_details.description.clone(),
3577 default: None,
3580 serde_attrs: Vec::new(),
3581 synthesized_required: true,
3582 constraints: PropertyConstraints::from_schema_details(value_details),
3583 };
3584 (
3585 ObjectAdditionalProperties::Typed {
3586 value_type: Box::new(nullable_value),
3587 },
3588 Some(required_property),
3589 false,
3590 )
3591 }
3592 Some(crate::openapi::AdditionalProperties::Schema(_)) => (
3593 ObjectAdditionalProperties::Untyped,
3596 Some(untyped_required_property()),
3597 false,
3598 ),
3599 None if Self::object_shape_needs_additional_property_carrier(details) => (
3604 ObjectAdditionalProperties::Untyped,
3605 Some(untyped_required_property()),
3606 false,
3607 ),
3608 None => (
3609 ObjectAdditionalProperties::Closed,
3610 Some(untyped_required_property()),
3611 false,
3612 ),
3613 };
3614
3615 self.finalize_required_object_members(
3616 &mut property_info,
3617 &required,
3618 &mut additional_properties,
3619 required_additional_property,
3620 explicitly_forbidden,
3621 )?;
3622
3623 Ok(SchemaType::Object {
3624 properties: property_info,
3625 variant: None,
3626 required,
3627 additional_properties,
3628 })
3629 }
3630
3631 fn object_shape_needs_additional_property_carrier(
3637 details: &crate::openapi::SchemaDetails,
3638 ) -> bool {
3639 if details.min_properties.is_some() || details.max_properties.is_some() {
3640 return true;
3641 }
3642
3643 let declared = details.properties.as_ref();
3644 let has_undeclared_key = |value: &Value| {
3645 value.as_object().is_some_and(|object| {
3646 object
3647 .keys()
3648 .any(|key| declared.is_none_or(|properties| !properties.contains_key(key)))
3649 })
3650 };
3651 details.example.as_ref().is_some_and(has_undeclared_key)
3652 || details
3653 .examples
3654 .as_ref()
3655 .is_some_and(|examples| examples.iter().any(has_undeclared_key))
3656 }
3657
3658 fn finalize_required_object_members(
3663 &self,
3664 properties: &mut BTreeMap<String, PropertyInfo>,
3665 required: &HashSet<String>,
3666 additional_properties: &mut ObjectAdditionalProperties,
3667 required_additional_property: Option<PropertyInfo>,
3668 explicitly_forbidden: bool,
3669 ) -> Result<()> {
3670 let mut missing = required
3671 .iter()
3672 .filter(|name| !properties.contains_key(*name))
3673 .cloned()
3674 .collect::<Vec<_>>();
3675 missing.sort();
3676 if missing.is_empty() {
3677 return Ok(());
3678 }
3679
3680 if explicitly_forbidden {
3681 let owner = self
3682 .current_schema_name
3683 .as_deref()
3684 .unwrap_or("<inline object>");
3685 return Err(GeneratorError::InvalidSchema(format!(
3686 "object schema `{owner}` is unsatisfiable: required member(s) {} are not declared in properties while additionalProperties: false",
3687 missing
3688 .iter()
3689 .map(|name| format!("`{name}`"))
3690 .collect::<Vec<_>>()
3691 .join(", ")
3692 )));
3693 }
3694
3695 let required_additional_property = required_additional_property.ok_or_else(|| {
3696 GeneratorError::InvalidSchema(
3697 "undeclared required members have no additional-properties carrier".to_string(),
3698 )
3699 })?;
3700 for name in missing {
3701 properties.insert(name, required_additional_property.clone());
3702 }
3703
3704 if !additional_properties.is_open() {
3705 *additional_properties = ObjectAdditionalProperties::Untyped;
3706 }
3707 Ok(())
3708 }
3709
3710 fn build_typed_multi_union_variant(
3720 &mut self,
3721 member_type: OpenApiSchemaType,
3722 schema: &Schema,
3723 union_type_name: &str,
3724 dependencies: &mut HashSet<String>,
3725 ) -> Result<SchemaRef> {
3726 match member_type {
3727 OpenApiSchemaType::Array => {
3728 let preferred_array_type_name = format!("{union_type_name}Array");
3729 let array_type_name = self.allocate_inline_schema_name(
3730 &preferred_array_type_name,
3731 &format!("{preferred_array_type_name}Inline"),
3732 "typed-multi-array",
3733 schema,
3734 );
3735 let array_type =
3736 self.analyze_array_schema(schema, &array_type_name, dependencies)?;
3737 self.resolved_cache.insert(
3738 array_type_name.clone(),
3739 AnalyzedSchema {
3740 name: array_type_name.clone(),
3741 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3742 schema_type: array_type,
3743 dependencies: HashSet::new(),
3744 nullable: false,
3745 description: Some("Array variant in union".to_string()),
3746 default: None,
3747 },
3748 );
3749 dependencies.insert(array_type_name.clone());
3750 Ok(SchemaRef {
3751 target: array_type_name,
3752 nullable: false,
3753 })
3754 }
3755 OpenApiSchemaType::Object => {
3756 let preferred_object_type_name = format!("{union_type_name}Object");
3757 let object_type_name = self.allocate_inline_schema_name(
3758 &preferred_object_type_name,
3759 &format!("{preferred_object_type_name}Inline"),
3760 "typed-multi-object",
3761 schema,
3762 );
3763 if Self::object_shape_is_unconstrained(schema.details()) {
3771 let schema_type = self.untyped_value_map(
3772 self.untyped_context(&object_type_name),
3773 UntypedReason::OpaqueObject,
3774 );
3775 self.resolved_cache.insert(
3776 object_type_name.clone(),
3777 AnalyzedSchema {
3778 name: object_type_name.clone(),
3779 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3780 schema_type,
3781 dependencies: HashSet::new(),
3782 nullable: false,
3783 description: Some("Object variant in union".to_string()),
3784 default: None,
3785 },
3786 );
3787 dependencies.insert(object_type_name.clone());
3788 return Ok(SchemaRef {
3789 target: object_type_name,
3790 nullable: false,
3791 });
3792 }
3793 let object_type = self.add_allocated_object_schema(
3794 object_type_name.clone(),
3795 schema,
3796 dependencies,
3797 )?;
3798 let SchemaType::Reference { target } = object_type else {
3799 unreachable!("allocated object schemas always return a reference");
3800 };
3801 Ok(SchemaRef {
3802 target,
3803 nullable: false,
3804 })
3805 }
3806 _ => Ok(SchemaRef {
3807 target: self.openapi_type_to_rust_type(member_type, schema.details()),
3808 nullable: false,
3809 }),
3810 }
3811 }
3812
3813 fn analyze_property_schema_with_context(
3814 &mut self,
3815 schema: &Schema,
3816 property_name: Option<&str>,
3817 dependencies: &mut HashSet<String>,
3818 ) -> Result<SchemaType> {
3819 if let Schema::Bool(accepts_anything) = schema {
3821 let reason = if *accepts_anything {
3822 UntypedReason::AnySchema
3823 } else {
3824 UntypedReason::NeverMatches
3825 };
3826 return Ok(self.untyped_value(
3827 self.untyped_context(property_name.unwrap_or_default()),
3828 reason,
3829 ));
3830 }
3831
3832 if let Some(ref_str) = self.get_any_reference(schema) {
3833 let target_opt = if ref_str == "#" {
3834 Some(
3835 self.find_recursive_anchor_schema()
3836 .unwrap_or_else(|| "UnknownRecursive".to_string()),
3837 )
3838 } else {
3839 self.extract_schema_name(ref_str).map(|s| s.to_string())
3840 };
3841 match target_opt {
3842 Some(target) => {
3843 dependencies.insert(target.clone());
3844 return Ok(SchemaType::Reference { target });
3845 }
3846 None => {
3847 if let Some(resolved) = self.resolve_pointer_schema(ref_str, dependencies)? {
3852 return Ok(resolved);
3853 }
3854 eprintln!(
3855 "⚠️ unresolvable $ref `{}` — typing as serde_json::Value",
3856 ref_str
3857 );
3858 return Ok(self.untyped_value(
3859 format!("$ref {ref_str}"),
3860 UntypedReason::UnresolvedReference,
3861 ));
3862 }
3863 }
3864 }
3865
3866 if let Some(non_null_types) = schema.non_null_schema_types() {
3870 let context_name = self
3871 .current_schema_name
3872 .clone()
3873 .unwrap_or_else(|| "Unknown".to_string());
3874 let prop_pascal = property_name
3875 .map(|name| self.to_pascal_case(name))
3876 .unwrap_or_default();
3877 let preferred_union_name = format!("{context_name}{prop_pascal}");
3878 let union_type_name = self.allocate_inline_schema_name(
3879 &preferred_union_name,
3880 &format!("{preferred_union_name}Union2"),
3881 "property-typed-multi",
3882 schema,
3883 );
3884
3885 let details = schema.details();
3886 let mut variants = Vec::with_capacity(non_null_types.len());
3887 for t in non_null_types {
3888 variants.push(self.build_typed_multi_union_variant(
3889 t,
3890 schema,
3891 &union_type_name,
3892 dependencies,
3893 )?);
3894 }
3895
3896 self.resolved_cache.insert(
3897 union_type_name.clone(),
3898 AnalyzedSchema {
3899 name: union_type_name.clone(),
3900 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3901 schema_type: SchemaType::Union {
3902 variants,
3903 exclusive: false,
3904 },
3905 dependencies: HashSet::new(),
3906 nullable: false,
3907 description: details.description.clone(),
3908 default: None,
3909 },
3910 );
3911
3912 dependencies.insert(union_type_name.clone());
3913 return Ok(SchemaType::Reference {
3914 target: union_type_name,
3915 });
3916 }
3917
3918 if let Some(schema_type) = schema.schema_type() {
3919 match schema_type {
3920 OpenApiSchemaType::String => {
3921 if let Some(enum_values) = schema.details().string_enum_values() {
3923 let context_name = self
3926 .current_schema_name
3927 .clone()
3928 .unwrap_or_else(|| "Unknown".to_string());
3929
3930 let primary_name = if let Some(prop_name) = property_name {
3932 let prop_pascal = self.to_pascal_case(prop_name);
3934 format!("{context_name}{prop_pascal}")
3935 } else {
3936 let suffix = if !enum_values.is_empty() {
3939 let first_value = self.to_pascal_case(&enum_values[0]);
3940 format!("{first_value}Enum")
3941 } else {
3942 "StringEnum".to_string()
3943 };
3944 format!("{context_name}{suffix}")
3945 };
3946
3947 return Ok(self.hoist_inline_string_enum(
3948 schema,
3949 enum_values,
3950 primary_name,
3951 dependencies,
3952 ));
3953 } else {
3954 let mapped = self
3960 .type_mapper
3961 .string_format(schema.details().format.as_deref());
3962 return Ok(SchemaType::Primitive {
3963 rust_type: mapped.rust_type,
3964 serde_with: mapped.serde_with,
3965 });
3966 }
3967 }
3968 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
3969 let details = schema.details();
3970 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
3971 return Ok(SchemaType::Primitive {
3972 rust_type,
3973 serde_with: None,
3974 });
3975 }
3976 OpenApiSchemaType::Boolean => {
3977 return Ok(SchemaType::Primitive {
3978 rust_type: "bool".to_string(),
3979 serde_with: None,
3980 });
3981 }
3982 OpenApiSchemaType::Array => {
3983 let context_name = if let Some(prop_name) = property_name {
3985 let prop_pascal = self.to_pascal_case(prop_name);
3987 format!(
3988 "{}{}",
3989 self.current_schema_name.as_deref().unwrap_or("Unknown"),
3990 prop_pascal
3991 )
3992 } else {
3993 "ArrayItem".to_string()
3995 };
3996 return self.analyze_array_schema(schema, &context_name, dependencies);
3997 }
3998 OpenApiSchemaType::Object => {
3999 if self.should_use_dynamic_json(schema) {
4001 return Ok(self
4002 .untyped_value(self.untyped_context(""), UntypedReason::OpaqueObject));
4003 }
4004 let preferred_object_type_name = if let Some(prop_name) = property_name {
4006 let prop_pascal = self.to_pascal_case(prop_name);
4008 format!(
4009 "{}{}",
4010 self.current_schema_name.as_deref().unwrap_or("Unknown"),
4011 prop_pascal
4012 )
4013 } else {
4014 format!(
4016 "{}Object",
4017 self.current_schema_name.as_deref().unwrap_or("Unknown")
4018 )
4019 };
4020 let object_type_name = self.allocate_inline_schema_name(
4021 &preferred_object_type_name,
4022 &format!("{preferred_object_type_name}Inline"),
4023 "property-object",
4024 schema,
4025 );
4026
4027 return self.add_allocated_object_schema(
4028 object_type_name,
4029 schema,
4030 dependencies,
4031 );
4032 }
4033 OpenApiSchemaType::Null => {
4036 return Ok(SchemaType::Primitive {
4037 rust_type: self.type_mapper.null_unit().rust_type,
4038 serde_with: None,
4039 });
4040 }
4041 }
4042 }
4043
4044 if schema.is_nullable_pattern() {
4046 if let Some(non_null) = schema.non_null_variant() {
4047 return self.analyze_property_schema_with_context(
4048 non_null,
4049 property_name,
4050 dependencies,
4051 );
4052 }
4053 }
4054
4055 if self.should_use_dynamic_json(schema) {
4057 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::OpaqueObject));
4058 }
4059
4060 if let Schema::AllOf { all_of, .. } = schema {
4062 if let Some(property_name) = property_name {
4063 let owner = self
4064 .current_schema_name
4065 .clone()
4066 .unwrap_or_else(|| "Inline".to_string());
4067 let composition_name = format!("{owner}{}", self.to_pascal_case(property_name));
4068 return self.with_schema_context(&composition_name, |analyzer| {
4069 analyzer.analyze_allof_composition(schema, all_of, dependencies)
4070 });
4071 }
4072 return self.analyze_allof_composition(schema, all_of, dependencies);
4073 }
4074
4075 if let Some(variants) = schema.union_variants() {
4077 match variants.len().cmp(&1) {
4078 std::cmp::Ordering::Equal => {
4079 return self.analyze_property_schema_with_context(
4081 &variants[0],
4082 property_name,
4083 dependencies,
4084 );
4085 }
4086 std::cmp::Ordering::Greater => {
4087 let union_name = if let Some(prop_name) = property_name {
4090 let prop_pascal = self.to_pascal_case(prop_name);
4092 format!(
4093 "{}{}",
4094 self.current_schema_name.as_deref().unwrap_or(""),
4095 prop_pascal
4096 )
4097 } else {
4098 "UnionType".to_string()
4099 };
4100
4101 if let Schema::OneOf {
4103 one_of,
4104 discriminator,
4105 ..
4106 } = schema
4107 {
4108 let union_name = self.allocate_inline_schema_name(
4109 &union_name,
4110 &format!("{union_name}Union2"),
4111 "fallback-property-oneof",
4112 schema,
4113 );
4114 let oneof_result = self.analyze_oneof_union(
4116 one_of,
4117 discriminator.as_ref(),
4118 &union_name,
4119 dependencies,
4120 InlineUnionKind::OneOf,
4121 None,
4122 )?;
4123
4124 if let SchemaType::Union { .. } = &oneof_result {
4126 self.resolved_cache.insert(
4128 union_name.clone(),
4129 AnalyzedSchema {
4130 name: union_name.clone(),
4131 original: serde_json::to_value(schema).unwrap_or(Value::Null),
4132 schema_type: oneof_result.clone(),
4133 dependencies: dependencies.clone(),
4134 nullable: false,
4135 description: schema.details().description.clone(),
4136 default: None,
4137 },
4138 );
4139
4140 dependencies.insert(union_name.clone());
4142 return Ok(SchemaType::Reference { target: union_name });
4143 }
4144
4145 return Ok(oneof_result);
4146 } else if let Schema::AnyOf {
4147 any_of,
4148 discriminator,
4149 ..
4150 } = schema
4151 {
4152 let union_analysis = self.analyze_anyof_union(
4154 any_of,
4155 discriminator.as_ref(),
4156 dependencies,
4157 &union_name,
4158 )?;
4159 return Ok(union_analysis);
4160 } else {
4161 let mut union_variants = Vec::new();
4164 for variant in variants {
4165 if let Some(ref_str) = variant.reference() {
4166 if let Some(target) = self.extract_schema_name(ref_str) {
4167 dependencies.insert(target.to_string());
4168 union_variants.push(SchemaRef {
4169 target: target.to_string(),
4170 nullable: false,
4171 });
4172 }
4173 }
4174 }
4175 return Ok(SchemaType::Union {
4176 variants: union_variants,
4177 exclusive: false,
4178 });
4179 }
4180 }
4181 std::cmp::Ordering::Less => {}
4182 }
4183 }
4184
4185 if let Some(inferred_type) = schema.inferred_type() {
4187 match inferred_type {
4188 OpenApiSchemaType::Object => {
4189 if self.should_use_dynamic_json(schema) {
4191 return Ok(self
4192 .untyped_value(self.untyped_context(""), UntypedReason::OpaqueObject));
4193 }
4194 let owner = self
4195 .current_schema_name
4196 .clone()
4197 .unwrap_or_else(|| "Unknown".to_string());
4198 let preferred_name = property_name.map_or_else(
4199 || format!("{owner}Object"),
4200 |property_name| format!("{owner}{}", self.to_pascal_case(property_name)),
4201 );
4202 let object_type_name = self.allocate_inline_schema_name(
4203 &preferred_name,
4204 &format!("{preferred_name}Inline"),
4205 "property-object",
4206 schema,
4207 );
4208 return self.add_allocated_object_schema(
4209 object_type_name,
4210 schema,
4211 dependencies,
4212 );
4213 }
4214 OpenApiSchemaType::Array => {
4215 let context_name = if let Some(prop_name) = property_name {
4216 let prop_pascal = self.to_pascal_case(prop_name);
4218 format!(
4219 "{}{}",
4220 self.current_schema_name.as_deref().unwrap_or("Unknown"),
4221 prop_pascal
4222 )
4223 } else {
4224 "ArrayItem".to_string()
4226 };
4227 return self.analyze_array_schema(schema, &context_name, dependencies);
4228 }
4229 OpenApiSchemaType::String => {
4230 if let Some(enum_values) = schema.details().string_enum_values() {
4231 return Ok(SchemaType::StringEnum {
4232 values: enum_values,
4233 });
4234 } else {
4235 return Ok(SchemaType::Primitive {
4236 rust_type: "String".to_string(),
4237 serde_with: None,
4238 });
4239 }
4240 }
4241 _ => {
4242 let rust_type = self.openapi_type_to_rust_type(inferred_type, schema.details());
4244 return Ok(SchemaType::Primitive {
4245 rust_type,
4246 serde_with: None,
4247 });
4248 }
4249 }
4250 }
4251
4252 Ok(self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema))
4253 }
4254
4255 fn analyze_allof_composition(
4256 &mut self,
4257 owner_schema: &Schema,
4258 all_of_schemas: &[Schema],
4259 dependencies: &mut HashSet<String>,
4260 ) -> Result<SchemaType> {
4261 if let Some(carrier) = Self::single_non_object_allof_carrier(all_of_schemas) {
4266 return self.analyze_property_schema_with_context(carrier, None, dependencies);
4267 }
4268
4269 let referenced_targets = all_of_schemas
4274 .iter()
4275 .filter_map(|schema| schema.reference())
4276 .filter_map(|reference| self.extract_schema_name(reference))
4277 .collect::<Vec<_>>();
4278 let only_reference_and_annotations = all_of_schemas
4279 .iter()
4280 .all(|schema| schema.reference().is_some() || schema_is_annotation_only(schema));
4281 if referenced_targets.len() == 1 && only_reference_and_annotations {
4282 let target = referenced_targets[0];
4283 dependencies.insert(target.to_string());
4284 return Ok(SchemaType::Reference {
4285 target: target.to_string(),
4286 });
4287 }
4288
4289 if let [only] = all_of_schemas
4293 && !matches!(
4294 only.schema_type(),
4295 Some(OpenApiSchemaType::Object) | Some(OpenApiSchemaType::Null)
4296 )
4297 && only.details().properties.is_none()
4298 {
4299 return self.analyze_property_schema_with_context(only, None, dependencies);
4300 }
4301
4302 let mut merged_properties = BTreeMap::new();
4304 let mut merged_required = HashSet::new();
4305 let mut merged_variant = None;
4306 let mut descriptions = Vec::new();
4307
4308 let current_context = self.current_schema_name.clone();
4310 let owner_name = current_context.as_deref().unwrap_or("InlineComposition");
4311
4312 self.merge_schema_into_properties(
4316 owner_schema,
4317 &mut merged_properties,
4318 &mut merged_required,
4319 dependencies,
4320 )?;
4321
4322 for (member_index, schema) in all_of_schemas.iter().enumerate() {
4323 match schema {
4324 Schema::Reference { reference, .. } => {
4325 let (analyzed_type, analyzed_name, raw_target) =
4326 if let Some(target) = self.extract_schema_name(reference) {
4327 dependencies.insert(target.to_string());
4328 let analyzed_ref = self.analyze_schema(target)?;
4329 (
4330 Some(analyzed_ref.schema_type),
4331 Some(target.to_string()),
4332 self.schemas.get(target).cloned(),
4333 )
4334 } else {
4335 (
4336 self.resolve_pointer_schema(reference, dependencies)?,
4337 None,
4338 self.reference_target_schema(reference),
4339 )
4340 };
4341
4342 let merged = if let Some(analyzed_type) = analyzed_type {
4343 self.merge_analyzed_object_properties(
4344 &analyzed_type,
4345 analyzed_name.as_deref(),
4346 &mut merged_properties,
4347 &mut merged_required,
4348 &mut merged_variant,
4349 owner_name,
4350 )?
4351 } else {
4352 false
4353 };
4354 if !merged && let Some(raw_target) = raw_target {
4355 self.merge_schema_into_properties(
4356 &raw_target,
4357 &mut merged_properties,
4358 &mut merged_required,
4359 dependencies,
4360 )?;
4361 }
4362 }
4363 Schema::AnyOf { any_of, .. }
4364 if Self::union_only_constrains_requiredness(any_of) => {}
4365 Schema::OneOf { one_of, .. }
4366 if Self::union_only_constrains_requiredness(one_of) => {}
4367 Schema::AnyOf { .. } | Schema::OneOf { .. } => {
4368 let preferred_name = format!("{owner_name}AllOfVariant{}", member_index + 1);
4369 let variant_name =
4370 self.add_inline_schema(&preferred_name, schema, dependencies)?;
4371 dependencies.insert(variant_name.clone());
4372 let analyzed_type = self
4373 .resolved_cache
4374 .get(&variant_name)
4375 .map(|analyzed| analyzed.schema_type.clone())
4376 .ok_or_else(|| {
4377 GeneratorError::InvalidSchema(format!(
4378 "allOf union member `{variant_name}` was not analyzed"
4379 ))
4380 })?;
4381 if !self.merge_analyzed_object_properties(
4382 &analyzed_type,
4383 Some(&variant_name),
4384 &mut merged_properties,
4385 &mut merged_required,
4386 &mut merged_variant,
4387 owner_name,
4388 )? {
4389 return Ok(self.untyped_value(
4390 self.untyped_context(""),
4391 UntypedReason::UnrepresentableComposition,
4392 ));
4393 }
4394 }
4395 Schema::Typed {
4396 schema_type: OpenApiSchemaType::Object,
4397 ..
4398 }
4399 | Schema::Untyped { .. } => {
4400 let saved_context = self.current_schema_name.clone();
4402 self.current_schema_name = current_context.clone();
4403
4404 self.merge_schema_into_properties(
4406 schema,
4407 &mut merged_properties,
4408 &mut merged_required,
4409 dependencies,
4410 )?;
4411
4412 self.current_schema_name = saved_context;
4414 }
4415 _ => {
4416 self.merge_schema_into_properties(
4419 schema,
4420 &mut merged_properties,
4421 &mut merged_required,
4422 dependencies,
4423 )?;
4424 }
4425 }
4426
4427 if let Some(desc) = &schema.details().description {
4429 descriptions.push(desc.clone());
4430 }
4431 }
4432
4433 if !merged_properties.is_empty() || !merged_required.is_empty() || merged_variant.is_some()
4438 {
4439 let mut additional_properties = if merged_properties
4440 .values()
4441 .any(|property| property.synthesized_required)
4442 {
4443 ObjectAdditionalProperties::Untyped
4444 } else {
4445 ObjectAdditionalProperties::Closed
4446 };
4447 self.finalize_required_object_members(
4448 &mut merged_properties,
4449 &merged_required,
4450 &mut additional_properties,
4451 Some(PropertyInfo {
4452 schema_type: SchemaType::Untyped {
4453 shape: UntypedShape::Value,
4454 reason: UntypedReason::AnySchema,
4455 },
4456 nullable: false,
4457 description: None,
4458 default: None,
4459 serde_attrs: Vec::new(),
4460 synthesized_required: true,
4461 constraints: PropertyConstraints::default(),
4462 }),
4463 false,
4464 )?;
4465 Ok(SchemaType::Object {
4466 properties: merged_properties,
4467 required: merged_required,
4468 additional_properties,
4469 variant: merged_variant,
4470 })
4471 } else {
4472 let schemas = all_of_schemas
4473 .iter()
4474 .filter_map(|schema| {
4475 let reference = schema.reference()?;
4476 let target = self.extract_schema_name(reference)?;
4477 dependencies.insert(target.to_string());
4478 Some(SchemaRef {
4479 target: target.to_string(),
4480 nullable: false,
4481 })
4482 })
4483 .collect::<Vec<_>>();
4484 let references_are_objects = all_of_schemas
4490 .iter()
4491 .filter(|schema| schema.reference().is_some())
4492 .all(|schema| self.branch_resolves_to_object(schema));
4493 if schemas.is_empty() || !references_are_objects {
4494 Ok(self.untyped_value(
4495 self.untyped_context(""),
4496 UntypedReason::UnrepresentableComposition,
4497 ))
4498 } else {
4499 Ok(SchemaType::Composition { schemas })
4500 }
4501 }
4502 }
4503
4504 fn single_non_object_allof_carrier(all_of_schemas: &[Schema]) -> Option<&Schema> {
4509 let mut meaningful = all_of_schemas
4510 .iter()
4511 .filter(|schema| !schema_is_annotation_only(schema));
4512 let carrier = meaningful.next()?;
4513 if meaningful.next().is_some() {
4514 return None;
4515 }
4516
4517 match carrier {
4518 Schema::Typed {
4519 schema_type:
4520 OpenApiSchemaType::String
4521 | OpenApiSchemaType::Integer
4522 | OpenApiSchemaType::Number
4523 | OpenApiSchemaType::Boolean
4524 | OpenApiSchemaType::Array,
4525 ..
4526 } => Some(carrier),
4527 Schema::TypedMulti { schema_types, .. } => {
4528 let mut non_null = schema_types
4529 .iter()
4530 .filter(|schema_type| **schema_type != OpenApiSchemaType::Null);
4531 let only = non_null.next()?;
4532 (non_null.next().is_none()
4533 && matches!(
4534 only,
4535 OpenApiSchemaType::String
4536 | OpenApiSchemaType::Integer
4537 | OpenApiSchemaType::Number
4538 | OpenApiSchemaType::Boolean
4539 | OpenApiSchemaType::Array
4540 ))
4541 .then_some(carrier)
4542 }
4543 _ => None,
4544 }
4545 }
4546
4547 fn merge_analyzed_object_properties(
4552 &mut self,
4553 schema_type: &SchemaType,
4554 named_target: Option<&str>,
4555 merged_properties: &mut BTreeMap<String, PropertyInfo>,
4556 merged_required: &mut HashSet<String>,
4557 merged_variant: &mut Option<SchemaRef>,
4558 owner_name: &str,
4559 ) -> Result<bool> {
4560 let mut current = schema_type.clone();
4561 let mut visited = HashSet::new();
4562 let mut variant_target = named_target.map(str::to_string);
4563 loop {
4564 match current {
4565 SchemaType::Object {
4566 properties,
4567 required,
4568 variant,
4569 ..
4570 } => {
4571 for (name, property) in properties {
4572 let keep_declared_sibling =
4573 merged_properties.get(&name).is_some_and(|existing| {
4574 !existing.synthesized_required && property.synthesized_required
4575 });
4576 if !keep_declared_sibling {
4577 merged_properties.insert(name, property);
4578 }
4579 }
4580 merged_required.extend(required);
4581 if let Some(variant) = variant {
4582 Self::merge_allof_variant(merged_variant, variant, owner_name)?;
4583 }
4584 return Ok(true);
4585 }
4586 SchemaType::Reference { target } => {
4587 if !visited.insert(target.clone()) {
4588 return Ok(false);
4589 }
4590 if variant_target.is_none() {
4591 variant_target = Some(target.clone());
4592 }
4593 current = if let Some(analyzed) = self.resolved_cache.get(&target) {
4594 analyzed.schema_type.clone()
4595 } else if self.schemas.contains_key(&target) {
4596 self.analyze_schema(&target)?.schema_type
4597 } else {
4598 return Ok(false);
4599 };
4600 }
4601 SchemaType::Union { .. } | SchemaType::DiscriminatedUnion { .. } => {
4602 let Some(target) = variant_target else {
4603 return Ok(false);
4604 };
4605 Self::merge_allof_variant(
4606 merged_variant,
4607 SchemaRef {
4608 target,
4609 nullable: false,
4610 },
4611 owner_name,
4612 )?;
4613 return Ok(true);
4614 }
4615 _ => return Ok(false),
4616 }
4617 }
4618 }
4619
4620 fn merge_allof_variant(
4621 merged_variant: &mut Option<SchemaRef>,
4622 candidate: SchemaRef,
4623 owner_name: &str,
4624 ) -> Result<()> {
4625 match merged_variant {
4626 Some(existing) if existing.target == candidate.target => Ok(()),
4627 Some(existing) => Err(GeneratorError::InvalidSchema(format!(
4628 "allOf object `{owner_name}` intersects multiple union members (`{}` and `{}`), which cannot be represented by one flattened variant",
4629 existing.target, candidate.target
4630 ))),
4631 None => {
4632 *merged_variant = Some(candidate);
4633 Ok(())
4634 }
4635 }
4636 }
4637
4638 fn merge_schema_into_properties(
4639 &mut self,
4640 schema: &Schema,
4641 merged_properties: &mut BTreeMap<String, PropertyInfo>,
4642 merged_required: &mut HashSet<String>,
4643 dependencies: &mut HashSet<String>,
4644 ) -> Result<()> {
4645 let details = schema.details();
4646
4647 if let Some(properties) = &details.properties {
4649 for (prop_name, prop_schema) in properties {
4650 let prop_type = self.analyze_property_schema_with_context(
4651 prop_schema,
4652 Some(prop_name),
4653 dependencies,
4654 )?;
4655 let owner_name = self
4656 .current_schema_name
4657 .clone()
4658 .unwrap_or_else(|| "Inline".to_string());
4659 let prop_type = self.hoist_inline_property_type(
4660 &owner_name,
4661 prop_name,
4662 prop_type,
4663 dependencies,
4664 );
4665 let prop_details = prop_schema.details();
4666
4667 let nullable = self.schema_or_reference_is_nullable(prop_schema);
4674 merged_properties.insert(
4675 prop_name.clone(),
4676 PropertyInfo {
4677 schema_type: prop_type,
4678 nullable,
4679 description: prop_details.description.clone(),
4680 default: prop_details.default.clone(),
4681 serde_attrs: Vec::new(),
4682 synthesized_required: false,
4683 constraints: PropertyConstraints::from_schema_details(prop_details),
4684 },
4685 );
4686 }
4687 }
4688
4689 if let Some(required) = &details.required {
4691 for field in required {
4692 merged_required.insert(field.clone());
4693 }
4694 }
4695
4696 Ok(())
4697 }
4698
4699 fn analyze_oneof_union(
4700 &mut self,
4701 one_of_schemas: &[Schema],
4702 discriminator: Option<&crate::openapi::Discriminator>,
4703 parent_name: &str,
4704 dependencies: &mut HashSet<String>,
4705 union_kind: InlineUnionKind,
4706 source_indices: Option<&[usize]>,
4707 ) -> Result<SchemaType> {
4708 let default_indices = (0..one_of_schemas.len()).collect::<Vec<_>>();
4709 let source_indices = source_indices
4710 .filter(|indices| indices.len() == one_of_schemas.len())
4711 .unwrap_or(&default_indices);
4712
4713 let expanded_branches;
4715 let one_of_schemas = match self.expand_pointer_branches(one_of_schemas) {
4716 Some(expanded) => {
4717 expanded_branches = expanded;
4718 expanded_branches.as_slice()
4719 }
4720 None => one_of_schemas,
4721 };
4722
4723 let boolean_resolved;
4725 let boolean_resolved_indices;
4726 let one_of_schemas = match Self::resolve_boolean_branches(one_of_schemas) {
4727 Ok(resolved) => {
4728 boolean_resolved_indices = one_of_schemas
4729 .iter()
4730 .zip(source_indices.iter().copied())
4731 .filter_map(|(branch, index)| {
4732 (!matches!(branch, Schema::Bool(false))).then_some(index)
4733 })
4734 .collect::<Vec<_>>();
4735 boolean_resolved = resolved;
4736 boolean_resolved.as_slice()
4737 }
4738 Err(()) => {
4739 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema));
4740 }
4741 };
4742 let source_indices = boolean_resolved_indices.as_slice();
4743 if one_of_schemas.is_empty() {
4744 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::NeverMatches));
4745 }
4746
4747 if let [only] = one_of_schemas {
4752 return self
4753 .analyze_schema_value(only, parent_name)
4754 .map(|analyzed| analyzed.schema_type);
4755 }
4756 if let Some(shared) = self.shared_branch_type(one_of_schemas) {
4757 return Ok(shared);
4758 }
4759
4760 if one_of_schemas.len() == 2 {
4763 let null_count = one_of_schemas
4764 .iter()
4765 .filter(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
4766 .count();
4767 if null_count == 1 {
4768 if let Some(non_null) = one_of_schemas
4769 .iter()
4770 .find(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
4771 {
4772 return self
4773 .analyze_schema_value(non_null, parent_name)
4774 .map(|a| a.schema_type);
4775 }
4776 }
4777 }
4778
4779 if discriminator.is_none()
4783 || one_of_schemas
4784 .iter()
4785 .any(|schema| self.schema_or_reference_is_nullable(schema))
4786 {
4787 return self.analyze_untagged_oneof_union(
4789 one_of_schemas,
4790 parent_name,
4791 dependencies,
4792 union_kind,
4793 source_indices,
4794 None,
4795 );
4796 }
4797
4798 if one_of_schemas
4804 .iter()
4805 .any(|s| !self.branch_resolves_to_object(s))
4806 {
4807 return self.analyze_untagged_oneof_union(
4808 one_of_schemas,
4809 parent_name,
4810 dependencies,
4811 union_kind,
4812 source_indices,
4813 discriminator,
4814 );
4815 }
4816
4817 let discriminator_field = discriminator
4819 .ok_or_else(|| {
4820 GeneratorError::InvalidDiscriminator(
4821 "expected discriminator after guard check".to_string(),
4822 )
4823 })?
4824 .property_name
4825 .clone();
4826
4827 if one_of_schemas.iter().any(|branch| {
4831 self.extract_discriminator_value_domain_for_field(branch, &discriminator_field)
4832 .is_some_and(|values| values.is_empty())
4833 }) {
4834 eprintln!(
4835 "⚠️ discriminated union `{parent_name}` has a branch with contradictory `{discriminator_field}` constraints; using structural union fallback"
4836 );
4837 return self.analyze_untagged_oneof_union(
4838 one_of_schemas,
4839 parent_name,
4840 dependencies,
4841 union_kind,
4842 source_indices,
4843 discriminator,
4844 );
4845 }
4846
4847 let mut variants = Vec::new();
4848 let mut used_variant_names = std::collections::HashSet::new();
4849
4850 for (variant_schema, original_index) in
4851 one_of_schemas.iter().zip(source_indices.iter().copied())
4852 {
4853 let ref_info = if let Some(ref_str) = variant_schema.reference() {
4855 Some((ref_str, false))
4856 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
4857 Some((recursive_ref, true))
4858 } else if let Schema::AllOf { all_of, .. } = variant_schema {
4859 if all_of.len() == 1 {
4861 if let Some(ref_str) = all_of[0].reference() {
4862 Some((ref_str, false))
4863 } else {
4864 all_of[0]
4865 .recursive_reference()
4866 .map(|recursive_ref| (recursive_ref, true))
4867 }
4868 } else {
4869 None
4870 }
4871 } else {
4872 None
4873 };
4874
4875 if let Some((ref_str, is_recursive)) = ref_info {
4876 let schema_name = if is_recursive && ref_str == "#" {
4877 self.find_recursive_anchor_schema()
4879 .or_else(|| self.current_schema_name.clone())
4880 .unwrap_or_else(|| "CompoundFilter".to_string())
4881 } else {
4882 self.extract_schema_name(ref_str)
4883 .map(|s| s.to_string())
4884 .unwrap_or_else(|| "UnknownRef".to_string())
4885 };
4886
4887 if !schema_name.is_empty() {
4888 dependencies.insert(schema_name.clone());
4889
4890 let mut discriminator_values = Vec::new();
4894 let allowed_domain = self.schemas.get(&schema_name).and_then(|ref_schema| {
4895 self.extract_discriminator_value_domain_for_field(
4896 ref_schema,
4897 &discriminator_field,
4898 )
4899 });
4900 if let Some(mappings) = discriminator.and_then(|disc| disc.mapping.as_ref()) {
4901 for (key, target_ref) in mappings {
4902 if target_ref == ref_str
4903 || self.extract_schema_name(target_ref)
4904 == Some(schema_name.as_str())
4905 {
4906 if allowed_domain
4907 .as_ref()
4908 .is_some_and(|allowed| !allowed.contains(key))
4909 {
4910 let allowed = allowed_domain
4911 .as_ref()
4912 .map(|values| values.join("`, `"))
4913 .unwrap_or_default();
4914 eprintln!(
4915 "⚠️ discriminator mapping conflict in union `{parent_name}`: key `{key}` targets `{schema_name}` but branch allows `{allowed}`; ignoring mapping key"
4916 );
4917 } else {
4918 Self::push_unique_string(&mut discriminator_values, key);
4919 }
4920 }
4921 }
4922 }
4923 if let Some(allowed_domain) = allowed_domain {
4924 for value in allowed_domain {
4925 Self::push_unique_string(&mut discriminator_values, &value);
4926 }
4927 }
4928 if discriminator_values.is_empty() {
4929 discriminator_values
4930 .push(self.generate_discriminator_value_from_name(&schema_name));
4931 }
4932 let discriminator_value = discriminator_values[0].clone();
4933 let (discriminator_field_declared, discriminator_field_required) = self
4934 .schemas
4935 .get(&schema_name)
4936 .map(|schema| {
4937 self.discriminator_property_presence(schema, &discriminator_field)
4938 })
4939 .unwrap_or((false, false));
4940
4941 let base_name = self.to_rust_variant_name(&schema_name);
4943 let rust_name =
4944 self.ensure_unique_variant_name(base_name, &mut used_variant_names);
4945
4946 let final_discriminator_value = discriminator_value;
4948
4949 variants.push(UnionVariant {
4950 rust_name,
4951 type_name: schema_name,
4952 discriminator_value: final_discriminator_value,
4953 preferred_discriminator_values: discriminator_values.clone(),
4954 discriminator_values,
4955 discriminator_field_declared,
4956 discriminator_field_required,
4957 schema_ref: ref_str.to_string(),
4958 });
4959 }
4960 } else {
4961 let variant_index = original_index;
4963 let inline_type_name =
4964 self.generate_inline_type_name(variant_schema, variant_index);
4965
4966 let mut discriminator_values = Vec::new();
4969 let allowed_domain = self.extract_discriminator_value_domain_for_field(
4970 variant_schema,
4971 &discriminator_field,
4972 );
4973 if let Some(mappings) = discriminator.and_then(|disc| disc.mapping.as_ref()) {
4974 for (key, target_ref) in mappings {
4975 if target_ref.contains(&format!("variant_{variant_index}")) {
4976 if allowed_domain
4977 .as_ref()
4978 .is_some_and(|allowed| !allowed.contains(key))
4979 {
4980 let allowed = allowed_domain
4981 .as_ref()
4982 .map(|values| values.join("`, `"))
4983 .unwrap_or_default();
4984 eprintln!(
4985 "⚠️ discriminator mapping conflict in union `{parent_name}`: key `{key}` targets `{inline_type_name}` but branch allows `{allowed}`; ignoring mapping key"
4986 );
4987 } else {
4988 Self::push_unique_string(&mut discriminator_values, key);
4989 }
4990 }
4991 }
4992 }
4993 if let Some(allowed_domain) = allowed_domain {
4994 for value in allowed_domain {
4995 Self::push_unique_string(&mut discriminator_values, &value);
4996 }
4997 }
4998 if discriminator_values.is_empty() {
4999 discriminator_values.push(format!("variant_{variant_index}"));
5000 }
5001 let discriminator_value = discriminator_values[0].clone();
5002 let (discriminator_field_declared, discriminator_field_required) =
5003 self.discriminator_property_presence(variant_schema, &discriminator_field);
5004
5005 let base_name = if discriminator_value.starts_with("variant_") {
5007 format!("Variant{variant_index}")
5008 } else {
5009 let clean_name = self.discriminator_to_variant_name(&discriminator_value);
5011 self.to_rust_variant_name(&clean_name)
5012 };
5013 let rust_name = self.ensure_unique_variant_name(base_name, &mut used_variant_names);
5014
5015 let final_discriminator_value = discriminator_value;
5017
5018 let inline_type_name = self.add_inline_union_branch_schema(
5021 &inline_type_name,
5022 variant_schema,
5023 dependencies,
5024 parent_name,
5025 union_kind,
5026 original_index,
5027 Some(&final_discriminator_value),
5028 )?;
5029
5030 variants.push(UnionVariant {
5031 rust_name,
5032 type_name: inline_type_name.clone(),
5033 discriminator_value: final_discriminator_value,
5034 preferred_discriminator_values: discriminator_values.clone(),
5035 discriminator_values,
5036 discriminator_field_declared,
5037 discriminator_field_required,
5038 schema_ref: format!("inline_{variant_index}"),
5039 });
5040 }
5041 }
5042
5043 self.disambiguate_shared_discriminator_values(&mut variants, discriminator);
5044
5045 if variants.is_empty() {
5046 let mut union_variants = Vec::new();
5049
5050 for (variant_schema, original_index) in
5051 one_of_schemas.iter().zip(source_indices.iter().copied())
5052 {
5053 if let Some(ref_str) = variant_schema.reference() {
5055 if let Some(schema_name) = self.extract_schema_name(ref_str) {
5056 dependencies.insert(schema_name.to_string());
5057 union_variants.push(SchemaRef {
5058 target: schema_name.to_string(),
5059 nullable: false,
5060 });
5061 }
5062 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
5063 let schema_name = if recursive_ref == "#" {
5064 self.find_recursive_anchor_schema()
5066 .or_else(|| self.current_schema_name.clone())
5067 .unwrap_or_else(|| "CompoundFilter".to_string())
5068 } else {
5069 self.extract_schema_name(recursive_ref)
5070 .map(|s| s.to_string())
5071 .unwrap_or_else(|| "RecursiveType".to_string())
5072 };
5073 dependencies.insert(schema_name.clone());
5074 union_variants.push(SchemaRef {
5075 target: schema_name,
5076 nullable: false,
5077 });
5078 } else {
5079 let branch_discriminator = self.inline_union_branch_discriminator_value(
5080 variant_schema,
5081 discriminator,
5082 original_index,
5083 );
5084 let inline_name = self.generate_context_aware_name(
5086 parent_name,
5087 "InlineVariant",
5088 original_index,
5089 Some(variant_schema),
5090 );
5091 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
5092 let variant_type = analyzed.schema_type;
5093
5094 for dep in &analyzed.dependencies {
5096 dependencies.insert(dep.clone());
5097 }
5098
5099 match &variant_type {
5100 SchemaType::Primitive { rust_type, .. } => {
5102 union_variants.push(SchemaRef {
5103 target: rust_type.clone(),
5104 nullable: false,
5105 });
5106 }
5107 SchemaType::Array { item_type } => {
5109 match item_type.as_ref() {
5110 SchemaType::Primitive { rust_type, .. } => {
5111 let type_name = format!("Vec<{rust_type}>");
5112 union_variants.push(SchemaRef {
5113 target: type_name,
5114 nullable: false,
5115 });
5116 }
5117 SchemaType::Reference { target } => {
5118 let type_name = format!("Vec<{target}>");
5119 union_variants.push(SchemaRef {
5120 target: type_name,
5121 nullable: false,
5122 });
5123 }
5124 _ => {
5125 let inline_type_name = self.generate_context_aware_name(
5127 parent_name,
5128 "Variant",
5129 original_index,
5130 None,
5131 );
5132 let inline_type_name = self.add_inline_union_branch_schema(
5133 &inline_type_name,
5134 variant_schema,
5135 dependencies,
5136 parent_name,
5137 union_kind,
5138 original_index,
5139 branch_discriminator.as_deref(),
5140 )?;
5141 union_variants.push(SchemaRef {
5142 target: inline_type_name,
5143 nullable: false,
5144 });
5145 }
5146 }
5147 }
5148 SchemaType::Reference { target } => {
5150 union_variants.push(SchemaRef {
5151 target: target.clone(),
5152 nullable: false,
5153 });
5154 }
5155 _ => {
5157 let inline_type_name =
5158 format!("{}Variant{}", parent_name, original_index + 1);
5159 let inline_type_name = self.add_inline_union_branch_schema(
5160 &inline_type_name,
5161 variant_schema,
5162 dependencies,
5163 parent_name,
5164 union_kind,
5165 original_index,
5166 branch_discriminator.as_deref(),
5167 )?;
5168 union_variants.push(SchemaRef {
5169 target: inline_type_name,
5170 nullable: false,
5171 });
5172 }
5173 }
5174 }
5175 }
5176
5177 if !union_variants.is_empty() {
5178 return Ok(SchemaType::Union {
5179 variants: union_variants,
5180 exclusive: matches!(union_kind, InlineUnionKind::OneOf),
5181 });
5182 }
5183
5184 return Ok(self.untyped_value(
5186 self.untyped_context(""),
5187 UntypedReason::UnrepresentableUnion,
5188 ));
5189 }
5190
5191 Ok(SchemaType::DiscriminatedUnion {
5192 discriminator_field,
5193 variants,
5194 exclusive: matches!(union_kind, InlineUnionKind::OneOf),
5195 })
5196 }
5197
5198 fn analyze_untagged_oneof_union(
5199 &mut self,
5200 one_of_schemas: &[Schema],
5201 parent_name: &str,
5202 dependencies: &mut HashSet<String>,
5203 union_kind: InlineUnionKind,
5204 source_indices: &[usize],
5205 discriminator: Option<&Discriminator>,
5206 ) -> Result<SchemaType> {
5207 let filtered: Vec<(usize, &Schema)> = one_of_schemas
5212 .iter()
5213 .zip(source_indices.iter().copied())
5214 .filter_map(|(schema, original_index)| {
5215 (!schema.is_explicit_null_only()).then_some((original_index, schema))
5216 })
5217 .collect();
5218
5219 if filtered.len() == 1 {
5221 return self
5222 .analyze_schema_value(filtered[0].1, parent_name)
5223 .map(|a| a.schema_type);
5224 }
5225
5226 let exclusive_object_union = matches!(union_kind, InlineUnionKind::OneOf)
5232 && filtered
5233 .iter()
5234 .all(|(_, schema)| self.branch_resolves_to_object(schema));
5235
5236 let mut union_variants = Vec::new();
5237
5238 for (original_index, variant_schema) in filtered {
5239 if let Some(ref_str) = variant_schema.reference() {
5241 if let Some(schema_name) = self.extract_schema_name(ref_str) {
5242 dependencies.insert(schema_name.to_string());
5243 union_variants.push(SchemaRef {
5244 target: schema_name.to_string(),
5245 nullable: self.schema_or_reference_is_nullable(variant_schema),
5246 });
5247 }
5248 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
5249 let schema_name = if recursive_ref == "#" {
5250 self.find_recursive_anchor_schema()
5252 .or_else(|| self.current_schema_name.clone())
5253 .unwrap_or_else(|| "CompoundFilter".to_string())
5254 } else {
5255 self.extract_schema_name(recursive_ref)
5256 .map(|s| s.to_string())
5257 .unwrap_or_else(|| "RecursiveType".to_string())
5258 };
5259 dependencies.insert(schema_name.clone());
5260 union_variants.push(SchemaRef {
5261 target: schema_name,
5262 nullable: false,
5263 });
5264 } else {
5265 let branch_discriminator = self.inline_union_branch_discriminator_value(
5266 variant_schema,
5267 discriminator,
5268 original_index,
5269 );
5270 let inline_name = self.generate_context_aware_name(
5272 parent_name,
5273 "InlineVariant",
5274 original_index,
5275 Some(variant_schema),
5276 );
5277 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
5278 let variant_type = analyzed.schema_type;
5279
5280 for dep in &analyzed.dependencies {
5282 dependencies.insert(dep.clone());
5283 }
5284
5285 match &variant_type {
5286 SchemaType::Primitive { rust_type, .. } => {
5288 union_variants.push(SchemaRef {
5289 target: rust_type.clone(),
5290 nullable: false,
5291 });
5292 }
5293 SchemaType::Array { item_type } => {
5295 match item_type.as_ref() {
5296 SchemaType::Primitive { rust_type, .. } => {
5297 let type_name = format!("Vec<{rust_type}>");
5298 union_variants.push(SchemaRef {
5299 target: type_name,
5300 nullable: false,
5301 });
5302 }
5303 SchemaType::Reference { target } => {
5304 let type_name = format!("Vec<{target}>");
5305 union_variants.push(SchemaRef {
5306 target: type_name,
5307 nullable: false,
5308 });
5309 }
5310 SchemaType::Array {
5312 item_type: inner_item_type,
5313 } => {
5314 match inner_item_type.as_ref() {
5315 SchemaType::Primitive { rust_type, .. } => {
5316 let type_name = format!("Vec<Vec<{rust_type}>>");
5317 union_variants.push(SchemaRef {
5318 target: type_name,
5319 nullable: false,
5320 });
5321 }
5322 SchemaType::Reference { target } => {
5323 let type_name = format!("Vec<Vec<{target}>>");
5324 union_variants.push(SchemaRef {
5325 target: type_name,
5326 nullable: false,
5327 });
5328 }
5329 _ => {
5330 let inline_type_name = self.generate_context_aware_name(
5332 parent_name,
5333 "Variant",
5334 original_index,
5335 None,
5336 );
5337 let inline_type_name = self
5338 .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 _ => {
5355 let inline_type_name = self.generate_context_aware_name(
5357 parent_name,
5358 "Variant",
5359 original_index,
5360 None,
5361 );
5362 let inline_type_name = self.add_inline_union_branch_schema(
5363 &inline_type_name,
5364 variant_schema,
5365 dependencies,
5366 parent_name,
5367 union_kind,
5368 original_index,
5369 branch_discriminator.as_deref(),
5370 )?;
5371 union_variants.push(SchemaRef {
5372 target: inline_type_name,
5373 nullable: false,
5374 });
5375 }
5376 }
5377 }
5378 SchemaType::Reference { target } => {
5380 union_variants.push(SchemaRef {
5381 target: target.clone(),
5382 nullable: false,
5383 });
5384 }
5385 _ => {
5387 let inline_type_name = self.generate_context_aware_name(
5388 parent_name,
5389 "Variant",
5390 original_index,
5391 None,
5392 );
5393 let inline_type_name = self.add_inline_union_branch_schema(
5394 &inline_type_name,
5395 variant_schema,
5396 dependencies,
5397 parent_name,
5398 union_kind,
5399 original_index,
5400 branch_discriminator.as_deref(),
5401 )?;
5402 union_variants.push(SchemaRef {
5403 target: inline_type_name,
5404 nullable: false,
5405 });
5406 }
5407 }
5408 }
5409 }
5410
5411 if !union_variants.is_empty() {
5412 return Ok(SchemaType::Union {
5413 variants: union_variants,
5414 exclusive: exclusive_object_union,
5415 });
5416 }
5417
5418 Ok(self.untyped_value(
5420 self.untyped_context(""),
5421 UntypedReason::UnrepresentableUnion,
5422 ))
5423 }
5424
5425 fn add_inline_schema(
5426 &mut self,
5427 type_name: &str,
5428 schema: &Schema,
5429 dependencies: &mut HashSet<String>,
5430 ) -> Result<String> {
5431 let allocated_name = self.allocate_inline_schema_name(
5432 type_name,
5433 &format!("{type_name}Inline"),
5434 "inline-schema",
5435 schema,
5436 );
5437 self.add_allocated_inline_schema(allocated_name, schema, dependencies)
5438 }
5439
5440 #[allow(clippy::too_many_arguments)]
5441 fn add_inline_union_branch_schema(
5442 &mut self,
5443 type_name: &str,
5444 schema: &Schema,
5445 dependencies: &mut HashSet<String>,
5446 owner_context: &str,
5447 union_kind: InlineUnionKind,
5448 original_index: usize,
5449 discriminator: Option<&str>,
5450 ) -> Result<String> {
5451 let allocated_name = self.allocate_inline_union_branch_name(
5452 type_name,
5453 owner_context,
5454 union_kind,
5455 original_index,
5456 discriminator,
5457 schema,
5458 );
5459 let branch_name = self.add_allocated_inline_schema(allocated_name, schema, dependencies)?;
5460 self.narrow_opaque_object_branch(&branch_name);
5461 Ok(branch_name)
5462 }
5463
5464 fn narrow_opaque_object_branch(&mut self, branch_name: &str) {
5475 if let Some(cached) = self.resolved_cache.get_mut(branch_name)
5476 && let SchemaType::Untyped {
5477 shape: shape @ UntypedShape::Value,
5478 reason: UntypedReason::OpaqueObject,
5479 } = &mut cached.schema_type
5480 {
5481 *shape = UntypedShape::ValueMap;
5482 }
5483 }
5484
5485 fn add_allocated_inline_schema(
5486 &mut self,
5487 allocated_name: String,
5488 schema: &Schema,
5489 dependencies: &mut HashSet<String>,
5490 ) -> Result<String> {
5491 if let Some(schema_type) = schema.schema_type() {
5493 match schema_type {
5494 OpenApiSchemaType::String
5495 | OpenApiSchemaType::Integer
5496 | OpenApiSchemaType::Number
5497 | OpenApiSchemaType::Boolean => {
5498 let rust_type =
5499 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
5500
5501 self.resolved_cache.insert(
5503 allocated_name.clone(),
5504 AnalyzedSchema {
5505 name: allocated_name.clone(),
5506 original: serde_json::to_value(schema).unwrap_or(Value::Null),
5507 schema_type: SchemaType::Primitive {
5508 rust_type,
5509 serde_with: None,
5510 },
5511 dependencies: HashSet::new(),
5512 nullable: false,
5513 description: schema.details().description.clone(),
5514 default: None,
5515 },
5516 );
5517 return Ok(allocated_name);
5518 }
5519 _ => {}
5520 }
5521 }
5522
5523 let analyzed = self.with_schema_context(&allocated_name, |analyzer| {
5527 analyzer.analyze_schema_value(schema, &allocated_name)
5528 })?;
5529
5530 self.resolved_cache.insert(allocated_name.clone(), analyzed);
5532
5533 if let Some(cached) = self.resolved_cache.get(&allocated_name) {
5535 for dep in &cached.dependencies {
5536 dependencies.insert(dep.clone());
5537 }
5538 }
5539
5540 Ok(allocated_name)
5541 }
5542
5543 fn inline_union_branch_discriminator_value(
5544 &self,
5545 schema: &Schema,
5546 discriminator: Option<&Discriminator>,
5547 original_index: usize,
5548 ) -> Option<String> {
5549 let discriminator = discriminator?;
5550 discriminator
5551 .mapping
5552 .as_ref()
5553 .and_then(|mappings| {
5554 mappings
5555 .iter()
5556 .find(|(_, target_ref)| {
5557 target_ref.contains(&format!("variant_{original_index}"))
5558 })
5559 .map(|(key, _)| key.clone())
5560 })
5561 .or_else(|| {
5562 Some(self.extract_inline_discriminator_value(
5563 schema,
5564 &discriminator.property_name,
5565 original_index,
5566 ))
5567 })
5568 }
5569
5570 fn extract_inline_discriminator_value(
5571 &self,
5572 schema: &Schema,
5573 discriminator_field: &str,
5574 variant_index: usize,
5575 ) -> String {
5576 if let Some(properties) = &schema.details().properties {
5578 if let Some(discriminator_prop) = properties.get(discriminator_field) {
5579 if let Some(enum_values) = &discriminator_prop.details().enum_values {
5581 if enum_values.len() == 1 {
5582 if let Some(value) = enum_values[0].as_str() {
5583 return value.to_string();
5584 }
5585 }
5586 }
5587 if let Some(const_value) = discriminator_prop.details().extra.get("const") {
5589 if let Some(value) = const_value.as_str() {
5590 return value.to_string();
5591 }
5592 }
5593 if let Some(const_value) = &discriminator_prop.details().const_value {
5595 if let Some(value) = const_value.as_str() {
5596 return value.to_string();
5597 }
5598 }
5599 }
5600 }
5601
5602 if let Some(inferred_name) = self.infer_variant_name_from_structure(schema, variant_index) {
5604 return inferred_name;
5605 }
5606
5607 format!("variant_{variant_index}")
5609 }
5610
5611 fn infer_variant_name_from_structure(
5612 &self,
5613 schema: &Schema,
5614 _variant_index: usize,
5615 ) -> Option<String> {
5616 let details = schema.details();
5617
5618 if let Some(properties) = &details.properties {
5620 if properties.contains_key("text") && properties.len() <= 3 {
5622 return Some("text".to_string());
5623 }
5624 if properties.contains_key("image") || properties.contains_key("source") {
5625 return Some("image".to_string());
5626 }
5627 if properties.contains_key("document") {
5628 return Some("document".to_string());
5629 }
5630 if properties.contains_key("tool_use_id") || properties.contains_key("tool_result") {
5631 return Some("tool_result".to_string());
5632 }
5633 if properties.contains_key("content") && properties.contains_key("is_error") {
5634 return Some("tool_result".to_string());
5635 }
5636 if properties.contains_key("partial_json") {
5637 return Some("partial_json".to_string());
5638 }
5639
5640 let property_names: Vec<&String> = properties.keys().collect();
5642
5643 for prop_name in &property_names {
5645 if prop_name.contains("result") {
5646 return Some("result".to_string());
5647 }
5648 if prop_name.contains("error") {
5649 return Some("error".to_string());
5650 }
5651 if prop_name.contains("content") && property_names.len() <= 2 {
5652 return Some("content".to_string());
5653 }
5654 }
5655
5656 let significant_props = property_names
5658 .iter()
5659 .filter(|&name| !["type", "id", "cache_control"].contains(&name.as_str()))
5660 .collect::<Vec<_>>();
5661
5662 if significant_props.len() == 1 {
5663 return Some((*significant_props[0]).clone());
5664 }
5665 }
5666
5667 if let Some(description) = &details.description {
5669 let desc_lower = description.to_lowercase();
5670 if desc_lower.contains("text") && desc_lower.len() < 100 {
5671 return Some("text".to_string());
5672 }
5673 if desc_lower.contains("image") {
5674 return Some("image".to_string());
5675 }
5676 if desc_lower.contains("document") {
5677 return Some("document".to_string());
5678 }
5679 if desc_lower.contains("tool") && desc_lower.contains("result") {
5680 return Some("tool_result".to_string());
5681 }
5682 }
5683
5684 None
5685 }
5686
5687 fn discriminator_to_variant_name(&self, discriminator: &str) -> String {
5688 if discriminator.is_empty() {
5690 return "Variant".to_string();
5691 }
5692
5693 let mut result = String::new();
5694 let mut next_upper = true;
5695
5696 for c in discriminator.chars() {
5697 match c {
5698 'a'..='z' => {
5699 if next_upper {
5700 result.push(c.to_ascii_uppercase());
5701 next_upper = false;
5702 } else {
5703 result.push(c);
5704 }
5705 }
5706 'A'..='Z' => {
5707 result.push(c);
5708 next_upper = false;
5709 }
5710 '0'..='9' => {
5711 result.push(c);
5712 next_upper = false;
5713 }
5714 '_' | '-' | '.' | ' ' | '/' | '\\' => {
5715 next_upper = true;
5717 }
5718 _ => {
5719 next_upper = true;
5721 }
5722 }
5723 }
5724
5725 if result.is_empty() || result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
5727 result = format!("Variant{result}");
5728 }
5729
5730 result
5731 }
5732
5733 fn ensure_unique_variant_name(
5734 &self,
5735 base_name: String,
5736 used_names: &mut std::collections::HashSet<String>,
5737 ) -> String {
5738 let mut candidate = base_name.clone();
5739 let mut counter = 1;
5740
5741 while used_names.contains(&candidate) {
5742 counter += 1;
5743 candidate = format!("{base_name}{counter}");
5744 }
5745
5746 used_names.insert(candidate.clone());
5747 candidate
5748 }
5749
5750 fn generate_inline_type_name(&self, schema: &Schema, variant_index: usize) -> String {
5751 if let Some(meaningful_name) = self.infer_type_name_from_structure(schema) {
5753 return meaningful_name;
5754 }
5755
5756 let context = self.current_schema_name.as_deref().unwrap_or("Inline");
5758 self.generate_context_aware_name(context, "Variant", variant_index, Some(schema))
5759 }
5760
5761 fn infer_type_name_from_structure(&self, schema: &Schema) -> Option<String> {
5762 let details = schema.details();
5763
5764 if let Some(description) = &details.description {
5766 if let Some(name_from_desc) = self.extract_type_name_from_description(description) {
5767 return Some(name_from_desc);
5768 }
5769 }
5770
5771 if let Some(properties) = &details.properties {
5773 if let Some(name_from_props) = self.extract_type_name_from_properties(properties) {
5774 return Some(format!("{name_from_props}Block"));
5775 }
5776 }
5777
5778 None
5779 }
5780
5781 fn extract_type_name_from_description(&self, description: &str) -> Option<String> {
5782 if description.len() > 100 || description.contains('\n') {
5784 return None;
5785 }
5786
5787 let words: Vec<&str> = description
5789 .split_whitespace()
5790 .take(2) .filter(|word| {
5792 let w = word.to_lowercase();
5793 word.len() > 2
5794 && ![
5795 "the", "and", "for", "with", "that", "this", "are", "can", "will", "was",
5796 ]
5797 .contains(&w.as_str())
5798 })
5799 .collect();
5800
5801 if words.is_empty() {
5802 return None;
5803 }
5804
5805 let combined = words.join("_");
5807 let pascal_name = self.discriminator_to_variant_name(&combined);
5808
5809 if !pascal_name.ends_with("Content")
5811 && !pascal_name.ends_with("Block")
5812 && !pascal_name.ends_with("Type")
5813 {
5814 Some(format!("{pascal_name}Content"))
5815 } else {
5816 Some(pascal_name)
5817 }
5818 }
5819
5820 fn extract_type_name_from_properties(
5821 &self,
5822 properties: &std::collections::BTreeMap<String, crate::openapi::Schema>,
5823 ) -> Option<String> {
5824 let significant_props: Vec<&String> = properties
5826 .keys()
5827 .filter(|name| !["type", "id", "cache_control"].contains(&name.as_str()))
5828 .collect();
5829
5830 if significant_props.is_empty() {
5831 return None;
5832 }
5833
5834 if significant_props.len() == 1 {
5836 let prop_name = significant_props[0];
5837 return Some(self.discriminator_to_variant_name(prop_name));
5838 }
5839
5840 let mut sorted_props = significant_props.clone();
5843 sorted_props.sort();
5844 if let Some(first_prop) = sorted_props.first() {
5845 return Some(self.discriminator_to_variant_name(first_prop));
5846 }
5847
5848 None
5849 }
5850
5851 fn openapi_type_to_rust_type(
5852 &self,
5853 openapi_type: OpenApiSchemaType,
5854 details: &crate::openapi::SchemaDetails,
5855 ) -> String {
5856 if openapi_type == OpenApiSchemaType::Integer {
5861 self.integer_rust_type(details)
5862 } else {
5863 self.type_mapper.map(openapi_type, details).rust_type
5864 }
5865 }
5866
5867 #[allow(dead_code)]
5868 fn fallback_discriminator_value(&self, schema_name: &str) -> String {
5869 self.fallback_discriminator_value_for_field(schema_name, "type")
5870 }
5871
5872 fn fallback_discriminator_value_for_field(
5873 &self,
5874 schema_name: &str,
5875 field_name: &str,
5876 ) -> String {
5877 if let Some(ref_schema) = self.schemas.get(schema_name) {
5879 if let Some(extracted) =
5880 self.extract_discriminator_value_for_field(ref_schema, field_name)
5881 {
5882 return extracted;
5883 }
5884 }
5885
5886 self.generate_discriminator_value_from_name(schema_name)
5888 }
5889
5890 fn disambiguate_shared_discriminator_values(
5891 &self,
5892 variants: &mut [UnionVariant],
5893 discriminator: Option<&Discriminator>,
5894 ) {
5895 let mut owners_by_value: BTreeMap<String, Vec<usize>> = BTreeMap::new();
5896 for (index, variant) in variants.iter().enumerate() {
5897 for value in &variant.discriminator_values {
5898 owners_by_value
5899 .entry(value.clone())
5900 .or_default()
5901 .push(index);
5902 }
5903 }
5904
5905 let mut removals: Vec<(usize, String)> = Vec::new();
5906 for (value, candidate_owners) in owners_by_value {
5907 if candidate_owners.len() < 2 {
5908 continue;
5909 }
5910
5911 let explicitly_mapped_owners: Vec<usize> = discriminator
5912 .and_then(|disc| disc.mapping.as_ref())
5913 .and_then(|mappings| mappings.get(&value))
5914 .map(|target_ref| {
5915 candidate_owners
5916 .iter()
5917 .copied()
5918 .filter(|index| {
5919 let variant = &variants[*index];
5920 target_ref == &variant.schema_ref
5921 || self.extract_schema_name(target_ref)
5922 == Some(variant.type_name.as_str())
5923 || (variant.schema_ref.starts_with("inline_")
5924 && target_ref.contains(&variant.schema_ref))
5925 })
5926 .collect()
5927 })
5928 .unwrap_or_default();
5929
5930 let winner = if explicitly_mapped_owners.len() == 1 {
5931 explicitly_mapped_owners.first().copied()
5932 } else {
5933 let mut scored: Vec<(usize, usize)> = candidate_owners
5934 .iter()
5935 .map(|index| {
5936 (
5937 *index,
5938 Self::discriminator_name_affinity(&variants[*index].type_name, &value),
5939 )
5940 })
5941 .collect();
5942 scored.sort_by(|left, right| right.1.cmp(&left.1).then(left.0.cmp(&right.0)));
5943 match scored.as_slice() {
5944 [(index, score), rest @ ..]
5945 if *score > 0 && rest.first().is_none_or(|(_, next)| score > next) =>
5946 {
5947 Some(*index)
5948 }
5949 _ => None,
5950 }
5951 };
5952
5953 if let Some(winner) = winner {
5954 for index in candidate_owners {
5955 if index != winner && variants[index].preferred_discriminator_values.len() > 1 {
5956 removals.push((index, value.clone()));
5957 }
5958 }
5959 }
5960 }
5961
5962 for (index, value) in removals {
5963 variants[index]
5964 .preferred_discriminator_values
5965 .retain(|candidate| candidate != &value);
5966 }
5967 for variant in variants {
5968 if let Some(canonical) = variant.preferred_discriminator_values.first() {
5969 variant.discriminator_value.clone_from(canonical);
5970 }
5971 }
5972 }
5973
5974 fn discriminator_name_affinity(schema_name: &str, value: &str) -> usize {
5975 let schema_compact: String = schema_name
5976 .chars()
5977 .filter(|character| character.is_ascii_alphanumeric())
5978 .flat_map(char::to_lowercase)
5979 .collect();
5980 let value_compact: String = value
5981 .chars()
5982 .filter(|character| character.is_ascii_alphanumeric())
5983 .flat_map(char::to_lowercase)
5984 .collect();
5985 let exact_bonus = usize::from(
5986 !value_compact.is_empty() && schema_compact.contains(value_compact.as_str()),
5987 ) * 10;
5988 let token_matches = value
5989 .split(|character: char| !character.is_ascii_alphanumeric())
5990 .filter(|token| !token.is_empty())
5991 .filter(|token| schema_compact.contains(&token.to_ascii_lowercase()))
5992 .count();
5993 exact_bonus + token_matches
5994 }
5995
5996 fn generate_discriminator_value_from_name(&self, schema_name: &str) -> String {
5997 let mut result = String::new();
5999 let mut chars = schema_name.chars().peekable();
6000 let mut first = true;
6001
6002 while let Some(c) = chars.next() {
6003 if c.is_uppercase()
6004 && !first
6005 && chars
6006 .peek()
6007 .map(|&next| next.is_lowercase())
6008 .unwrap_or(false)
6009 {
6010 result.push('.');
6011 }
6012 result.push(c.to_ascii_lowercase());
6013 first = false;
6014 }
6015
6016 if result.ends_with("event") {
6018 result = result[..result.len() - 5].to_string();
6019 }
6020
6021 if schema_name.starts_with("Response") && !result.starts_with("response.") {
6023 result = format!("response.{}", result.trim_start_matches("response"));
6024 }
6025
6026 result
6027 }
6028
6029 fn to_rust_variant_name(&self, schema_name: &str) -> String {
6030 let mut name = schema_name;
6032
6033 if name.starts_with("Response") && name.len() > 8 {
6035 name = &name[8..]; }
6037
6038 if name.ends_with("Event") && name.len() > 5 {
6040 name = &name[..name.len() - 5]; }
6042
6043 name = name.trim_matches('_');
6045
6046 if name.is_empty() {
6048 schema_name.to_string()
6049 } else {
6050 self.discriminator_to_variant_name(name)
6052 }
6053 }
6054
6055 fn hoist_inline_string_enum(
6079 &mut self,
6080 schema: &Schema,
6081 enum_values: Vec<String>,
6082 primary_name: String,
6083 dependencies: &mut HashSet<String>,
6084 ) -> SchemaType {
6085 let suffix = enum_values
6086 .first()
6087 .map(|value| self.to_pascal_case(value))
6088 .unwrap_or_else(|| "Variant".to_string());
6089 let collision_name = format!("{primary_name}{suffix}");
6090 let enum_type_name =
6091 self.allocate_inline_schema_name(&primary_name, &collision_name, "string-enum", schema);
6092 let should_insert = !self.resolved_cache.contains_key(&enum_type_name);
6093
6094 if should_insert {
6097 self.resolved_cache.insert(
6098 enum_type_name.clone(),
6099 AnalyzedSchema {
6100 name: enum_type_name.clone(),
6101 original: serde_json::to_value(schema).unwrap_or(Value::Null),
6102 schema_type: SchemaType::StringEnum {
6103 values: enum_values,
6104 },
6105 dependencies: HashSet::new(),
6106 nullable: false,
6107 description: schema.details().description.clone(),
6108 default: schema.details().default.clone(),
6109 },
6110 );
6111 }
6112
6113 dependencies.insert(enum_type_name.clone());
6115 SchemaType::Reference {
6116 target: enum_type_name,
6117 }
6118 }
6119
6120 fn analyze_array_schema(
6121 &mut self,
6122 schema: &Schema,
6123 parent_schema_name: &str,
6124 dependencies: &mut HashSet<String>,
6125 ) -> Result<SchemaType> {
6126 let details = schema.details();
6127
6128 if let Some(positions) = details.positional_items() {
6132 return self.analyze_positional_items(
6133 positions,
6134 details,
6135 parent_schema_name,
6136 dependencies,
6137 );
6138 }
6139
6140 if let Some(items_schema) = details.item_schema() {
6142 let item_type = self.analyze_item_schema(
6143 items_schema,
6144 parent_schema_name,
6145 &format!("{parent_schema_name}Item"),
6146 dependencies,
6147 )?;
6148 let item_type = self.hoist_inline_property_type(
6149 parent_schema_name,
6150 "Item",
6151 item_type,
6152 dependencies,
6153 );
6154 Ok(SchemaType::Array {
6155 item_type: Box::new(item_type),
6156 })
6157 } else {
6158 Ok(
6160 self.untyped_value_array(
6161 self.untyped_context(""),
6162 UntypedReason::ArrayWithoutItems,
6163 ),
6164 )
6165 }
6166 }
6167
6168 fn shared_branch_type(&self, branches: &[Schema]) -> Option<SchemaType> {
6177 let mut mapped: Option<(String, Option<String>)> = None;
6178 let mut scalar_kind: Option<OpenApiSchemaType> = None;
6179 let mut formats_agree = true;
6180 for branch in branches {
6181 if branch.reference().is_some() {
6182 return None;
6183 }
6184 let details = branch.details();
6185 if details.enum_values.is_some()
6186 || details.const_value.is_some()
6187 || details.properties.is_some()
6188 {
6189 return None;
6190 }
6191 let scalar = match branch.schema_type()? {
6192 scalar @ (OpenApiSchemaType::String
6193 | OpenApiSchemaType::Integer
6194 | OpenApiSchemaType::Number
6195 | OpenApiSchemaType::Boolean) => scalar.clone(),
6196 _ => return None,
6197 };
6198 match &scalar_kind {
6199 Some(existing) if *existing != scalar => return None,
6200 Some(_) => {}
6201 None => scalar_kind = Some(scalar.clone()),
6202 }
6203
6204 let candidate = self.type_mapper.map(scalar, details);
6205 let candidate = (candidate.rust_type, candidate.serde_with);
6206 match &mapped {
6207 Some(existing) if *existing != candidate => formats_agree = false,
6208 Some(_) => {}
6209 None => mapped = Some(candidate),
6210 }
6211 }
6212
6213 if formats_agree {
6214 return mapped.map(|(rust_type, serde_with)| SchemaType::Primitive {
6215 rust_type,
6216 serde_with,
6217 });
6218 }
6219
6220 let scalar = scalar_kind?;
6226 let mapped = self
6227 .type_mapper
6228 .map(scalar, &crate::openapi::SchemaDetails::default());
6229 Some(SchemaType::Primitive {
6230 rust_type: mapped.rust_type,
6231 serde_with: mapped.serde_with,
6232 })
6233 }
6234
6235 fn expand_pointer_branches(&self, branches: &[Schema]) -> Option<Vec<Schema>> {
6244 let mut expanded = Vec::with_capacity(branches.len());
6245 let mut changed = false;
6246 for branch in branches {
6247 let resolved = branch
6248 .reference()
6249 .filter(|reference| self.extract_schema_name(reference).is_none())
6250 .and_then(|reference| reference.strip_prefix('#'))
6251 .filter(|pointer| pointer.starts_with('/'))
6252 .and_then(|pointer| self.openapi_spec.pointer(pointer))
6253 .and_then(|value| Schema::deserialize(value).ok())
6254 .filter(|schema| schema.reference().is_none());
6255 match resolved {
6256 Some(schema) => {
6257 expanded.push(schema);
6258 changed = true;
6259 }
6260 None => expanded.push(branch.clone()),
6261 }
6262 }
6263 changed.then_some(expanded)
6264 }
6265
6266 fn analyze_object_with_variants(
6279 &mut self,
6280 schema: &Schema,
6281 branches: &[Schema],
6282 schema_name: &str,
6283 dependencies: &mut HashSet<String>,
6284 ) -> Result<Option<SchemaType>> {
6285 let details = schema.details();
6286 if details.properties.as_ref().is_none_or(BTreeMap::is_empty) || branches.is_empty() {
6287 return Ok(None);
6288 }
6289 if schema.is_nullable_pattern() || Self::union_only_constrains_requiredness(branches) {
6292 return Ok(None);
6293 }
6294
6295 let base = self.analyze_object_schema(schema, dependencies)?;
6296 let SchemaType::Object {
6297 properties,
6298 required,
6299 additional_properties,
6300 ..
6301 } = base
6302 else {
6303 return Ok(None);
6304 };
6305
6306 let preferred_variant_name = format!("{schema_name}Variant");
6307 let variant_name = self.allocate_inline_schema_name(
6308 &preferred_variant_name,
6309 &format!("{preferred_variant_name}Inline"),
6310 "object-variant",
6311 schema,
6312 );
6313 let variant_type = self.analyze_anyof_union(
6314 branches,
6315 schema.discriminator(),
6316 dependencies,
6317 &variant_name,
6318 )?;
6319 if matches!(variant_type, SchemaType::Untyped { .. }) {
6322 return Ok(Some(SchemaType::Object {
6323 properties,
6324 required,
6325 additional_properties,
6326 variant: None,
6327 }));
6328 }
6329
6330 self.resolved_cache.insert(
6331 variant_name.clone(),
6332 AnalyzedSchema {
6333 name: variant_name.clone(),
6334 original: Value::Null,
6335 dependencies: schema_type_dependencies(&variant_type),
6336 schema_type: variant_type,
6337 nullable: false,
6338 description: None,
6339 default: None,
6340 },
6341 );
6342 dependencies.insert(variant_name.clone());
6343
6344 Ok(Some(SchemaType::Object {
6345 properties,
6346 required,
6347 additional_properties,
6348 variant: Some(SchemaRef {
6349 target: variant_name,
6350 nullable: false,
6351 }),
6352 }))
6353 }
6354
6355 #[allow(clippy::result_unit_err)]
6363 fn resolve_boolean_branches(branches: &[Schema]) -> std::result::Result<Vec<Schema>, ()> {
6364 if branches
6365 .iter()
6366 .any(|branch| matches!(branch, Schema::Bool(true)))
6367 {
6368 return Err(());
6369 }
6370 Ok(branches
6371 .iter()
6372 .filter(|branch| !matches!(branch, Schema::Bool(false)))
6373 .cloned()
6374 .collect())
6375 }
6376
6377 fn union_only_constrains_requiredness(branches: &[Schema]) -> bool {
6386 !branches.is_empty()
6387 && branches
6388 .iter()
6389 .all(Self::schema_only_constrains_requiredness)
6390 }
6391
6392 fn schema_only_constrains_requiredness(schema: &Schema) -> bool {
6396 let keys_are_requiredness_or_annotations = serde_json::to_value(schema)
6397 .ok()
6398 .and_then(|value| value.as_object().cloned())
6399 .is_some_and(|object| {
6400 object.keys().all(|key| {
6401 matches!(
6402 key.as_str(),
6403 "required"
6404 | "not"
6405 | "anyOf"
6406 | "oneOf"
6407 | "title"
6408 | "description"
6409 | "deprecated"
6410 | "readOnly"
6411 | "writeOnly"
6412 | "examples"
6413 | "example"
6414 | "default"
6415 | "externalDocs"
6416 | "xml"
6417 | "$comment"
6418 ) || key.starts_with("x-")
6419 })
6420 });
6421 if !keys_are_requiredness_or_annotations {
6422 return false;
6423 }
6424
6425 match schema {
6426 Schema::AnyOf { any_of, .. } => {
6427 !any_of.is_empty() && any_of.iter().all(Self::schema_only_constrains_requiredness)
6428 }
6429 Schema::OneOf { one_of, .. } => {
6430 !one_of.is_empty() && one_of.iter().all(Self::schema_only_constrains_requiredness)
6431 }
6432 other => {
6433 let details = other.details();
6434 details
6435 .required
6436 .as_ref()
6437 .is_some_and(|required| !required.is_empty())
6438 || details
6439 .not
6440 .as_deref()
6441 .is_some_and(Self::schema_only_constrains_requiredness)
6442 }
6443 }
6444 }
6445
6446 fn analyze_empty_union(
6453 &mut self,
6454 schema: &Schema,
6455 dependencies: &mut HashSet<String>,
6456 ) -> Result<SchemaType> {
6457 let Some(declared) = schema
6461 .declared_type()
6462 .cloned()
6463 .or_else(|| schema.inferred_type())
6464 .or_else(|| {
6465 schema
6466 .details()
6467 .properties
6468 .is_some()
6469 .then_some(OpenApiSchemaType::Object)
6470 })
6471 else {
6472 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema));
6473 };
6474 match declared {
6475 OpenApiSchemaType::Object => self.analyze_object_schema(schema, dependencies),
6476 OpenApiSchemaType::Array => {
6477 let context = self
6478 .current_schema_name
6479 .clone()
6480 .unwrap_or_else(|| "Inline".to_string());
6481 self.analyze_array_schema(schema, &context, dependencies)
6482 }
6483 scalar => {
6484 let mapped = self.type_mapper.map(scalar, schema.details());
6485 Ok(SchemaType::Primitive {
6486 rust_type: mapped.rust_type,
6487 serde_with: mapped.serde_with,
6488 })
6489 }
6490 }
6491 }
6492
6493 fn resolve_pointer_schema(
6506 &mut self,
6507 reference: &str,
6508 dependencies: &mut HashSet<String>,
6509 ) -> Result<Option<SchemaType>> {
6510 let Some(pointer) = reference.strip_prefix('#') else {
6511 return Ok(None);
6512 };
6513 if pointer.is_empty() || !pointer.starts_with('/') {
6514 return Ok(None);
6515 }
6516 let preferred_name = pointer_type_name(pointer);
6517 if preferred_name.is_empty() {
6518 return Ok(None);
6519 }
6520 let name = self.allocate_pointer_schema_name(pointer, &preferred_name);
6521 if self.resolved_cache.contains_key(&name) || !self.resolving_pointers.insert(name.clone())
6524 {
6525 dependencies.insert(name.clone());
6526 return Ok(Some(SchemaType::Reference { target: name }));
6527 }
6528
6529 let resolved = (|| {
6530 let value = self.openapi_spec.pointer(pointer)?.clone();
6531 Schema::deserialize(&value).ok()
6532 })();
6533 let Some(schema) = resolved else {
6534 self.resolving_pointers.remove(&name);
6535 return Ok(None);
6536 };
6537
6538 let saved_context = self.current_schema_name.clone();
6543 self.current_schema_name = Some(name.clone());
6544 let analyzed = self.analyze_schema_value(&schema, &name);
6545 self.current_schema_name = saved_context;
6546 self.resolving_pointers.remove(&name);
6547 let analyzed = analyzed?;
6548 dependencies.extend(analyzed.dependencies.iter().cloned());
6549 if analyzed.schema_type.renders_inline() {
6550 return Ok(Some(analyzed.schema_type));
6551 }
6552
6553 self.resolved_cache.insert(name.clone(), analyzed);
6554 dependencies.insert(name.clone());
6555 Ok(Some(SchemaType::Reference { target: name }))
6556 }
6557
6558 fn hoist_inline_property_type(
6567 &mut self,
6568 schema_name: &str,
6569 property_name: &str,
6570 schema_type: SchemaType,
6571 dependencies: &mut HashSet<String>,
6572 ) -> SchemaType {
6573 if schema_type.renders_inline() {
6574 return schema_type;
6575 }
6576
6577 use heck::ToPascalCase;
6578
6579 let preferred_name = format!("{schema_name}{}", property_name.to_pascal_case());
6580 let hoisted_name = self.allocate_synthetic_schema_name(
6581 &preferred_name,
6582 &format!("{preferred_name}Inline"),
6583 "hoisted-property",
6584 format!("{schema_type:?}"),
6585 );
6586 let hoisted_dependencies = schema_type_dependencies(&schema_type);
6587 self.resolved_cache.insert(
6588 hoisted_name.clone(),
6589 AnalyzedSchema {
6590 name: hoisted_name.clone(),
6591 original: Value::Null,
6592 schema_type,
6593 dependencies: hoisted_dependencies,
6594 nullable: false,
6595 description: None,
6596 default: None,
6597 },
6598 );
6599 dependencies.insert(hoisted_name.clone());
6600 SchemaType::Reference {
6601 target: hoisted_name,
6602 }
6603 }
6604
6605 fn analyze_positional_items(
6618 &mut self,
6619 positions: &[Schema],
6620 details: &crate::openapi::SchemaDetails,
6621 parent_schema_name: &str,
6622 dependencies: &mut HashSet<String>,
6623 ) -> Result<SchemaType> {
6624 if details.positional_items_are_exact() && !positions.is_empty() {
6625 let mut element_types = Vec::with_capacity(positions.len());
6626 for (index, position) in positions.iter().enumerate() {
6627 let element_type = self.analyze_item_schema(
6628 position,
6629 parent_schema_name,
6630 &format!("{parent_schema_name}Item{}", index + 1),
6631 dependencies,
6632 )?;
6633 element_types.push(self.hoist_inline_property_type(
6634 parent_schema_name,
6635 &format!("Item{}", index + 1),
6636 element_type,
6637 dependencies,
6638 ));
6639 }
6640 return Ok(SchemaType::Tuple { element_types });
6641 }
6642
6643 if details.positional_items_are_closed()
6647 && let Some(shared) = shared_positional_schema(positions)
6648 {
6649 let item_type = self.analyze_item_schema(
6650 shared,
6651 parent_schema_name,
6652 &format!("{parent_schema_name}Item"),
6653 dependencies,
6654 )?;
6655 return Ok(SchemaType::Array {
6656 item_type: Box::new(item_type),
6657 });
6658 }
6659
6660 Ok(self.untyped_value_array(self.untyped_context(""), UntypedReason::OpenPositionalItems))
6661 }
6662
6663 fn analyze_item_schema(
6670 &mut self,
6671 items_schema: &Schema,
6672 parent_schema_name: &str,
6673 inline_name: &str,
6674 dependencies: &mut HashSet<String>,
6675 ) -> Result<SchemaType> {
6676 let item_type = match items_schema {
6677 Schema::Bool(accepts_anything) => self.untyped_value(
6678 self.untyped_context(""),
6679 if *accepts_anything {
6680 UntypedReason::AnySchema
6681 } else {
6682 UntypedReason::NeverMatches
6683 },
6684 ),
6685 Schema::Reference { reference, .. } => {
6686 if let Some(target) = self.extract_schema_name(reference) {
6688 let target = target.to_string();
6689 dependencies.insert(target.clone());
6690 SchemaType::Reference { target }
6691 } else {
6692 self.resolve_pointer_schema(reference, dependencies)?
6693 .ok_or_else(|| GeneratorError::UnresolvedReference(reference.to_string()))?
6694 }
6695 }
6696 Schema::RecursiveRef { recursive_ref, .. } => {
6697 if recursive_ref == "#" {
6699 let target = self
6701 .find_recursive_anchor_schema()
6702 .unwrap_or_else(|| parent_schema_name.to_string());
6703 dependencies.insert(target.clone());
6704 SchemaType::Reference { target }
6705 } else {
6706 let target = self
6707 .extract_schema_name(recursive_ref)
6708 .unwrap_or("RecursiveType")
6709 .to_string();
6710 dependencies.insert(target.clone());
6711 SchemaType::Reference { target }
6712 }
6713 }
6714 Schema::Typed { schema_type, .. } => {
6715 match schema_type {
6717 OpenApiSchemaType::String => {
6718 match items_schema
6722 .details()
6723 .string_enum_values()
6724 .filter(|values| !values.is_empty())
6725 {
6726 Some(values) => self.hoist_inline_string_enum(
6727 items_schema,
6728 values,
6729 inline_name.to_string(),
6730 dependencies,
6731 ),
6732 None => SchemaType::Primitive {
6733 rust_type: "String".to_string(),
6734 serde_with: None,
6735 },
6736 }
6737 }
6738 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
6739 let details = items_schema.details();
6740 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
6741 SchemaType::Primitive {
6742 rust_type,
6743 serde_with: None,
6744 }
6745 }
6746 OpenApiSchemaType::Boolean => SchemaType::Primitive {
6747 rust_type: "bool".to_string(),
6748 serde_with: None,
6749 },
6750 OpenApiSchemaType::Object => {
6751 let preferred_object_type_name = inline_name.to_string();
6753 let object_type_name = self.allocate_inline_schema_name(
6754 &preferred_object_type_name,
6755 &format!("{preferred_object_type_name}Inline"),
6756 "array-item-object",
6757 items_schema,
6758 );
6759
6760 self.add_allocated_object_schema(
6761 object_type_name,
6762 items_schema,
6763 dependencies,
6764 )?
6765 }
6766 OpenApiSchemaType::Array => {
6767 self.analyze_array_schema(items_schema, parent_schema_name, dependencies)?
6769 }
6770 _ => self.untyped_value(
6771 self.untyped_context(""),
6772 UntypedReason::UnsupportedTypeKeyword,
6773 ),
6774 }
6775 }
6776 Schema::OneOf { .. } | Schema::AnyOf { .. } => {
6777 let analyzed = self.analyze_schema_value(items_schema, "ArrayItem")?;
6779
6780 match &analyzed.schema_type {
6782 SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => {
6783 let preferred_union_name = format!("{inline_name}Union");
6786 let union_name = self.allocate_inline_schema_name(
6787 &preferred_union_name,
6788 &format!("{preferred_union_name}Inline"),
6789 "array-item-union",
6790 items_schema,
6791 );
6792
6793 let mut union_schema = analyzed;
6795 union_schema.name = union_name.clone();
6796
6797 self.resolved_cache.insert(union_name.clone(), union_schema);
6799
6800 dependencies.insert(union_name.clone());
6802
6803 SchemaType::Reference { target: union_name }
6805 }
6806 _ => analyzed.schema_type,
6807 }
6808 }
6809 Schema::Untyped { .. } => {
6810 if let Some(inferred) = items_schema.inferred_type() {
6812 match inferred {
6813 OpenApiSchemaType::Object => {
6814 let preferred_object_type_name = inline_name.to_string();
6816 let object_type_name = self.allocate_inline_schema_name(
6817 &preferred_object_type_name,
6818 &format!("{preferred_object_type_name}Inline"),
6819 "array-item-object",
6820 items_schema,
6821 );
6822
6823 self.add_allocated_object_schema(
6824 object_type_name,
6825 items_schema,
6826 dependencies,
6827 )?
6828 }
6829 OpenApiSchemaType::String => {
6830 match items_schema
6833 .details()
6834 .string_enum_values()
6835 .filter(|values| !values.is_empty())
6836 {
6837 Some(values) => self.hoist_inline_string_enum(
6838 items_schema,
6839 values,
6840 inline_name.to_string(),
6841 dependencies,
6842 ),
6843 None => SchemaType::Primitive {
6844 rust_type: "String".to_string(),
6845 serde_with: None,
6846 },
6847 }
6848 }
6849 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
6850 let details = items_schema.details();
6851 let rust_type = self.get_number_rust_type(inferred, details);
6852 SchemaType::Primitive {
6853 rust_type,
6854 serde_with: None,
6855 }
6856 }
6857 OpenApiSchemaType::Boolean => SchemaType::Primitive {
6858 rust_type: "bool".to_string(),
6859 serde_with: None,
6860 },
6861 OpenApiSchemaType::Null => SchemaType::Primitive {
6864 rust_type: self.type_mapper.null_unit().rust_type,
6865 serde_with: None,
6866 },
6867 _ => self.untyped_value(
6868 self.untyped_context(""),
6869 UntypedReason::UnsupportedTypeKeyword,
6870 ),
6871 }
6872 } else {
6873 self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema)
6874 }
6875 }
6876 _ => self.analyze_property_schema_with_context(items_schema, None, dependencies)?,
6881 };
6882
6883 Ok(self.nullable_container_value(items_schema, item_type))
6884 }
6885
6886 fn get_number_rust_type(
6887 &self,
6888 schema_type: OpenApiSchemaType,
6889 details: &crate::openapi::SchemaDetails,
6890 ) -> String {
6891 let format = details.format.as_deref();
6895 match schema_type {
6896 OpenApiSchemaType::Integer => self.integer_rust_type(details),
6897 OpenApiSchemaType::Number => self.type_mapper.number_format(format).rust_type,
6898 _ => self.type_mapper.dynamic_json().rust_type,
6899 }
6900 }
6901
6902 fn integer_rust_type(&self, details: &crate::openapi::SchemaDetails) -> String {
6908 fn integer_value(number: &serde_json::Number) -> Option<i128> {
6909 number
6910 .as_i64()
6911 .map(i128::from)
6912 .or_else(|| number.as_u64().map(i128::from))
6913 .or_else(|| {
6914 number.as_f64().and_then(|value| {
6915 (value.fract() == 0.0
6916 && value >= i128::MIN as f64
6917 && value <= i128::MAX as f64)
6918 .then_some(value as i128)
6919 })
6920 })
6921 }
6922
6923 fn below(number: &serde_json::Number, boundary: i128) -> bool {
6924 integer_value(number).is_some_and(|value| value < boundary)
6925 }
6926
6927 fn above(number: &serde_json::Number, boundary: i128) -> bool {
6928 integer_value(number).is_some_and(|value| value > boundary)
6929 }
6930
6931 let explicitly_nonnegative = details
6932 .minimum
6933 .as_ref()
6934 .and_then(integer_value)
6935 .is_some_and(|value| value >= 0)
6936 || matches!(
6937 details.exclusive_minimum,
6938 Some(crate::openapi::ExclusiveBound::Number(value)) if value >= 0.0
6939 );
6940
6941 let annotated_numbers = details
6942 .const_value
6943 .iter()
6944 .chain(details.default.iter())
6945 .chain(details.example.iter())
6946 .chain(details.enum_values.iter().flatten())
6947 .chain(details.examples.iter().flatten())
6948 .filter_map(Value::as_number)
6949 .collect::<Vec<_>>();
6950 let below_i32 = details
6951 .minimum
6952 .as_ref()
6953 .is_some_and(|number| below(number, i128::from(i32::MIN)))
6954 || annotated_numbers
6955 .iter()
6956 .any(|number| below(number, i128::from(i32::MIN)))
6957 || matches!(
6958 details.exclusive_minimum,
6959 Some(crate::openapi::ExclusiveBound::Number(value)) if value < i32::MIN as f64
6960 );
6961 let above_i32 = details
6962 .maximum
6963 .as_ref()
6964 .is_some_and(|number| above(number, i128::from(i32::MAX)))
6965 || annotated_numbers
6966 .iter()
6967 .any(|number| above(number, i128::from(i32::MAX)))
6968 || matches!(
6969 details.exclusive_maximum,
6970 Some(crate::openapi::ExclusiveBound::Number(value)) if value > i32::MAX as f64
6971 );
6972 let below_i64 = details
6973 .minimum
6974 .as_ref()
6975 .is_some_and(|number| below(number, i128::from(i64::MIN)))
6976 || annotated_numbers
6977 .iter()
6978 .any(|number| below(number, i128::from(i64::MIN)))
6979 || matches!(
6980 details.exclusive_minimum,
6981 Some(crate::openapi::ExclusiveBound::Number(value)) if value < i64::MIN as f64
6982 );
6983 let above_i64 = details
6984 .maximum
6985 .as_ref()
6986 .is_some_and(|number| above(number, i128::from(i64::MAX)))
6987 || annotated_numbers
6988 .iter()
6989 .any(|number| above(number, i128::from(i64::MAX)))
6990 || matches!(
6991 details.exclusive_maximum,
6992 Some(crate::openapi::ExclusiveBound::Number(value)) if value >= 9_223_372_036_854_775_808.0
6993 );
6994 let below_zero = details
6995 .minimum
6996 .as_ref()
6997 .and_then(integer_value)
6998 .is_some_and(|value| value < 0)
6999 || annotated_numbers
7000 .iter()
7001 .filter_map(|number| integer_value(number))
7002 .any(|value| value < 0)
7003 || matches!(
7004 details.exclusive_minimum,
7005 Some(crate::openapi::ExclusiveBound::Number(value)) if value < 0.0
7006 );
7007 let above_u32 = details
7008 .maximum
7009 .as_ref()
7010 .is_some_and(|number| above(number, i128::from(u32::MAX)))
7011 || annotated_numbers
7012 .iter()
7013 .any(|number| above(number, i128::from(u32::MAX)))
7014 || matches!(
7015 details.exclusive_maximum,
7016 Some(crate::openapi::ExclusiveBound::Number(value)) if value > u32::MAX as f64
7017 );
7018
7019 let configured = self
7020 .type_mapper
7021 .integer_format(details.format.as_deref())
7022 .rust_type;
7023 match configured.as_str() {
7024 "i32" if below_i32 || above_i32 => {
7025 if below_i64 || above_i64 {
7026 "i128".to_string()
7027 } else {
7028 "i64".to_string()
7029 }
7030 }
7031 "i64" if below_i64 => "i128".to_string(),
7032 "i64" if above_i64 && explicitly_nonnegative => "u64".to_string(),
7033 "i64" if above_i64 => "i128".to_string(),
7034 "u32" if below_zero => {
7035 if below_i64 || above_i64 {
7036 "i128".to_string()
7037 } else {
7038 "i64".to_string()
7039 }
7040 }
7041 "u32" if above_u32 => "u64".to_string(),
7042 "u64" if below_zero => "i128".to_string(),
7043 configured => configured.to_string(),
7044 }
7045 }
7046
7047 fn analyze_anyof_union(
7048 &mut self,
7049 any_of_schemas: &[Schema],
7050 discriminator: Option<&Discriminator>,
7051 dependencies: &mut HashSet<String>,
7052 context_name: &str,
7053 ) -> Result<SchemaType> {
7054 let original_indices = (0..any_of_schemas.len()).collect::<Vec<_>>();
7055
7056 let expanded_branches;
7058 let any_of_schemas = match self.expand_pointer_branches(any_of_schemas) {
7059 Some(expanded) => {
7060 expanded_branches = expanded;
7061 expanded_branches.as_slice()
7062 }
7063 None => any_of_schemas,
7064 };
7065
7066 let boolean_resolved;
7068 let boolean_resolved_indices;
7069 let any_of_schemas = match Self::resolve_boolean_branches(any_of_schemas) {
7070 Ok(resolved) => {
7071 boolean_resolved_indices = any_of_schemas
7072 .iter()
7073 .zip(original_indices.iter().copied())
7074 .filter_map(|(branch, index)| {
7075 (!matches!(branch, Schema::Bool(false))).then_some(index)
7076 })
7077 .collect::<Vec<_>>();
7078 boolean_resolved = resolved;
7079 boolean_resolved.as_slice()
7080 }
7081 Err(()) => {
7082 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema));
7083 }
7084 };
7085 let source_indices = boolean_resolved_indices.as_slice();
7086 if any_of_schemas.is_empty() {
7087 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::NeverMatches));
7088 }
7089
7090 let filtered_owned: Vec<Schema>;
7096 let filtered_indices: Vec<usize>;
7097 let (any_of_schemas, source_indices): (&[Schema], &[usize]) = if any_of_schemas
7098 .iter()
7099 .any(Schema::is_explicit_null_only)
7100 {
7101 filtered_owned = any_of_schemas
7102 .iter()
7103 .filter(|s| !s.is_explicit_null_only())
7104 .cloned()
7105 .collect();
7106 filtered_indices = any_of_schemas
7107 .iter()
7108 .zip(source_indices.iter().copied())
7109 .filter_map(|(schema, index)| (!schema.is_explicit_null_only()).then_some(index))
7110 .collect();
7111 if filtered_owned.is_empty() {
7112 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema));
7113 }
7114 if filtered_owned.len() == 1 {
7115 return self
7116 .analyze_schema_value(&filtered_owned[0], context_name)
7117 .map(|a| a.schema_type);
7118 }
7119 (&filtered_owned, &filtered_indices)
7120 } else {
7121 (any_of_schemas, source_indices)
7122 };
7123
7124 if let [only] = any_of_schemas {
7127 return self
7128 .analyze_schema_value(only, context_name)
7129 .map(|analyzed| analyzed.schema_type);
7130 }
7131
7132 if let Some(shared) = self.shared_branch_type(any_of_schemas) {
7136 return Ok(shared);
7137 }
7138
7139 let has_refs = any_of_schemas.iter().any(|s| s.is_reference());
7141 let has_objects = any_of_schemas.iter().any(|s| {
7142 matches!(s.schema_type(), Some(OpenApiSchemaType::Object))
7143 || s.inferred_type() == Some(OpenApiSchemaType::Object)
7144 });
7145 let has_arrays = any_of_schemas
7146 .iter()
7147 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Array)));
7148
7149 let all_string_like = any_of_schemas.iter().all(|s| {
7152 matches!(s.schema_type(), Some(OpenApiSchemaType::String))
7153 || s.details().const_value.is_some()
7154 });
7155
7156 if (has_refs || has_objects || has_arrays || any_of_schemas.len() > 1) && !all_string_like {
7157 if let Some(disc) = discriminator {
7159 return self.analyze_oneof_union(
7161 any_of_schemas,
7162 Some(disc),
7163 context_name,
7164 dependencies,
7165 InlineUnionKind::AnyOf,
7166 Some(source_indices),
7167 );
7168 }
7169
7170 if let Some(disc_field) = self.detect_discriminator_field(any_of_schemas) {
7172 return self.analyze_oneof_union(
7173 any_of_schemas,
7174 Some(&Discriminator {
7175 property_name: disc_field,
7176 mapping: None,
7177 default_mapping: None,
7178 extensions: crate::extensions::Extensions::default(),
7179 }),
7180 context_name,
7181 dependencies,
7182 InlineUnionKind::AnyOf,
7183 Some(source_indices),
7184 );
7185 }
7186
7187 let mut variants = Vec::new();
7189
7190 for (schema, original_index) in
7191 any_of_schemas.iter().zip(source_indices.iter().copied())
7192 {
7193 if let Some(ref_str) = schema.reference() {
7194 if let Some(target) = self.extract_schema_name(ref_str) {
7195 dependencies.insert(target.to_string());
7196 variants.push(SchemaRef {
7197 target: target.to_string(),
7198 nullable: self.schema_or_reference_is_nullable(schema),
7199 });
7200 }
7201 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object))
7202 || schema.inferred_type() == Some(OpenApiSchemaType::Object)
7203 {
7204 let inline_type_name = self.generate_inline_type_name(schema, original_index);
7206
7207 let inline_type_name = self.add_inline_union_branch_schema(
7209 &inline_type_name,
7210 schema,
7211 dependencies,
7212 context_name,
7213 InlineUnionKind::AnyOf,
7214 original_index,
7215 None,
7216 )?;
7217
7218 variants.push(SchemaRef {
7219 target: inline_type_name,
7220 nullable: false,
7221 });
7222 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Array)) {
7223 let preferred_array_type_name =
7225 if let Some(items_schema) = schema.details().item_schema() {
7226 if let Some(ref_str) = items_schema.reference() {
7227 if let Some(item_type_name) = self.extract_schema_name(ref_str) {
7228 dependencies.insert(item_type_name.to_string());
7229 format!("{item_type_name}Array")
7230 } else {
7231 self.generate_context_aware_name(
7232 context_name,
7233 "Array",
7234 original_index,
7235 Some(schema),
7236 )
7237 }
7238 } else {
7239 self.generate_context_aware_name(
7240 context_name,
7241 "Array",
7242 original_index,
7243 Some(schema),
7244 )
7245 }
7246 } else {
7247 self.generate_context_aware_name(
7248 context_name,
7249 "Array",
7250 original_index,
7251 Some(schema),
7252 )
7253 };
7254 let array_type_name = self.allocate_inline_union_branch_name(
7255 &preferred_array_type_name,
7256 context_name,
7257 InlineUnionKind::AnyOf,
7258 original_index,
7259 None,
7260 schema,
7261 );
7262 let array_type =
7264 self.analyze_array_schema(schema, context_name, dependencies)?;
7265
7266 self.resolved_cache.insert(
7268 array_type_name.clone(),
7269 AnalyzedSchema {
7270 name: array_type_name.clone(),
7271 original: serde_json::to_value(schema).unwrap_or(Value::Null),
7272 schema_type: array_type,
7273 dependencies: HashSet::new(),
7274 nullable: false,
7275 description: Some("Array variant in union".to_string()),
7276 default: None,
7277 },
7278 );
7279
7280 dependencies.insert(array_type_name.clone());
7282
7283 variants.push(SchemaRef {
7284 target: array_type_name,
7285 nullable: false,
7286 });
7287 } else if let Some(schema_type) = schema.schema_type() {
7288 let primitive_unions = self
7298 .type_mapper
7299 .config_shape_primitive_unions()
7300 .unwrap_or(true);
7301
7302 if primitive_unions {
7303 variants.push(SchemaRef {
7304 target: self
7305 .openapi_type_to_rust_type(schema_type.clone(), schema.details()),
7306 nullable: false,
7307 });
7308 } else {
7309 let preferred_inline_type_name = match schema_type {
7310 OpenApiSchemaType::String => {
7311 if original_index == 0 {
7312 format!("{context_name}String")
7313 } else {
7314 format!("{context_name}StringVariant{original_index}")
7315 }
7316 }
7317 OpenApiSchemaType::Number => {
7318 if original_index == 0 {
7319 format!("{context_name}Number")
7320 } else {
7321 format!("{context_name}NumberVariant{original_index}")
7322 }
7323 }
7324 OpenApiSchemaType::Integer => {
7325 if original_index == 0 {
7326 format!("{context_name}Integer")
7327 } else {
7328 format!("{context_name}IntegerVariant{original_index}")
7329 }
7330 }
7331 OpenApiSchemaType::Boolean => {
7332 if original_index == 0 {
7333 format!("{context_name}Boolean")
7334 } else {
7335 format!("{context_name}BooleanVariant{original_index}")
7336 }
7337 }
7338 _ => format!("{context_name}Variant{original_index}"),
7339 };
7340 let inline_type_name = self.allocate_inline_union_branch_name(
7341 &preferred_inline_type_name,
7342 context_name,
7343 InlineUnionKind::AnyOf,
7344 original_index,
7345 None,
7346 schema,
7347 );
7348
7349 let rust_type =
7350 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
7351
7352 self.resolved_cache.insert(
7353 inline_type_name.clone(),
7354 AnalyzedSchema {
7355 name: inline_type_name.clone(),
7356 original: serde_json::to_value(schema).unwrap_or(Value::Null),
7357 schema_type: SchemaType::Primitive {
7358 rust_type,
7359 serde_with: None,
7360 },
7361 dependencies: HashSet::new(),
7362 nullable: false,
7363 description: schema.details().description.clone(),
7364 default: None,
7365 },
7366 );
7367
7368 dependencies.insert(inline_type_name.clone());
7369
7370 variants.push(SchemaRef {
7371 target: inline_type_name,
7372 nullable: false,
7373 });
7374 }
7375 } else {
7376 let inline_type_name = self.generate_context_aware_name(
7384 context_name,
7385 "InlineVariant",
7386 original_index,
7387 Some(schema),
7388 );
7389 let inline_type_name = self.add_inline_union_branch_schema(
7390 &inline_type_name,
7391 schema,
7392 dependencies,
7393 context_name,
7394 InlineUnionKind::AnyOf,
7395 original_index,
7396 None,
7397 )?;
7398 dependencies.insert(inline_type_name.clone());
7399 variants.push(SchemaRef {
7400 target: inline_type_name,
7401 nullable: false,
7402 });
7403 }
7404 }
7405
7406 if !variants.is_empty() {
7407 return Ok(SchemaType::Union {
7408 variants,
7409 exclusive: false,
7410 });
7411 }
7412 }
7413
7414 let all_strings = any_of_schemas.iter().all(|schema| {
7416 matches!(schema.schema_type(), Some(OpenApiSchemaType::String))
7417 || schema.details().const_value.is_some()
7418 });
7419
7420 if all_strings {
7421 let mut enum_values = Vec::new();
7423 let mut has_open_string = false;
7424
7425 for schema in any_of_schemas {
7426 match schema
7431 .details()
7432 .string_enum_values()
7433 .filter(|values| !values.is_empty())
7434 {
7435 Some(values) => {
7436 for value in values {
7437 if !enum_values.contains(&value) {
7438 enum_values.push(value);
7439 }
7440 }
7441 }
7442 None => {
7443 if matches!(schema.schema_type(), Some(OpenApiSchemaType::String)) {
7444 has_open_string = true;
7445 }
7446 }
7447 }
7448 }
7449
7450 if !enum_values.is_empty() {
7451 if has_open_string {
7452 return Ok(SchemaType::ExtensibleEnum {
7455 known_values: enum_values,
7456 });
7457 } else {
7458 return Ok(SchemaType::StringEnum {
7460 values: enum_values,
7461 });
7462 }
7463 }
7464 }
7465
7466 Ok(self.untyped_value(
7468 self.untyped_context(""),
7469 UntypedReason::UnrepresentableUnion,
7470 ))
7471 }
7472
7473 fn find_recursive_anchor_schema(&self) -> Option<String> {
7475 for (schema_name, schema) in &self.schemas {
7477 let details = schema.details();
7478 if details.recursive_anchor == Some(true) {
7479 return Some(schema_name.clone());
7480 }
7481 }
7482
7483 None
7487 }
7488
7489 fn should_use_dynamic_json(&self, schema: &Schema) -> bool {
7492 if let Schema::AnyOf { any_of, .. } = schema {
7494 if any_of.len() == 2 {
7495 let has_null = any_of
7496 .iter()
7497 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)));
7498 let has_empty_object = any_of.iter().any(|s| self.is_dynamic_object_pattern(s));
7499
7500 if has_null && has_empty_object {
7501 return true;
7502 }
7503 }
7504 }
7505
7506 self.is_dynamic_object_pattern(schema)
7508 }
7509
7510 fn is_dynamic_object_pattern(&self, schema: &Schema) -> bool {
7512 let is_object = match schema.schema_type() {
7514 Some(OpenApiSchemaType::Object) => true,
7515 None => schema.inferred_type() == Some(OpenApiSchemaType::Object),
7516 _ => false,
7517 };
7518
7519 if !is_object {
7520 return false;
7521 }
7522
7523 Self::object_shape_is_unconstrained(schema.details())
7524 }
7525
7526 fn object_shape_is_unconstrained(details: &crate::openapi::SchemaDetails) -> bool {
7534 if details.additional_properties.is_some() {
7539 return false;
7540 }
7541
7542 let no_properties = details
7544 .properties
7545 .as_ref()
7546 .map(|props| props.is_empty())
7547 .unwrap_or(true);
7548
7549 if no_properties {
7550 let has_structural_constraints = details
7553 .required
7554 .as_ref()
7555 .map(|req| !req.is_empty())
7556 .unwrap_or(false)
7557 || details.pattern_properties.is_some()
7558 || details.property_names.is_some()
7559 || details.min_properties.is_some()
7560 || details.max_properties.is_some()
7561 || details.dependent_required.is_some()
7562 || details.dependent_schemas.is_some()
7563 || details.if_schema.is_some()
7564 || details.then_schema.is_some()
7565 || details.else_schema.is_some();
7566
7567 return !has_structural_constraints;
7568 }
7569
7570 false
7571 }
7572
7573 fn analyze_operations(&mut self, analysis: &mut SchemaAnalysis) -> Result<()> {
7575 let spec: crate::openapi::OpenApiSpec = parse_spec_document(&self.openapi_spec)?;
7576 let mut canonical_operation_ids = HashSet::new();
7581
7582 if let Some(paths) = &spec.paths {
7583 for (path, path_item) in paths {
7584 let resolved = self.resolve_path_item(path_item, &spec)?;
7586 let pi: &crate::openapi::PathItem = resolved.as_ref().unwrap_or(path_item);
7587 self.ingest_path_item_operations(
7588 path,
7589 pi,
7590 analysis,
7591 &mut canonical_operation_ids,
7592 None,
7593 path_item.reference.as_deref(),
7594 )?;
7595 }
7596 }
7597 if let Some(webhooks) = &spec.webhooks {
7604 for (name, path_item) in webhooks {
7605 let synthetic_path = format!("/__webhook__/{name}");
7606 let resolved = self.resolve_path_item(path_item, &spec)?;
7607 let pi = resolved.as_ref().unwrap_or(path_item);
7608 self.ingest_path_item_operations(
7609 &synthetic_path,
7610 pi,
7611 analysis,
7612 &mut canonical_operation_ids,
7613 Some(name),
7614 path_item.reference.as_deref(),
7615 )?;
7616 }
7617 }
7618 Ok(())
7619 }
7620
7621 fn resolve_path_item(
7625 &self,
7626 path_item: &crate::openapi::PathItem,
7627 spec: &crate::openapi::OpenApiSpec,
7628 ) -> Result<Option<crate::openapi::PathItem>> {
7629 let Some(reference) = &path_item.reference else {
7630 return Ok(None);
7631 };
7632 let target_name = reference
7633 .strip_prefix("#/components/pathItems/")
7634 .ok_or_else(|| {
7635 GeneratorError::UnresolvedReference(format!(
7636 "Path Item $ref must point at #/components/pathItems/{{name}}, got {reference}"
7637 ))
7638 })?;
7639 let pi = spec
7640 .components
7641 .as_ref()
7642 .and_then(|c| c.path_items.as_ref())
7643 .and_then(|map| map.get(target_name))
7644 .ok_or_else(|| {
7645 GeneratorError::UnresolvedReference(format!(
7646 "Path Item ref {reference} not found in components/pathItems"
7647 ))
7648 })?;
7649 Ok(Some(pi.clone()))
7650 }
7651
7652 fn ingest_path_item_operations(
7653 &mut self,
7654 path: &str,
7655 path_item: &crate::openapi::PathItem,
7656 analysis: &mut SchemaAnalysis,
7657 canonical_operation_ids: &mut HashSet<String>,
7658 webhook_name: Option<&str>,
7659 path_item_reference: Option<&str>,
7660 ) -> Result<()> {
7661 for (method, operation) in path_item.operations() {
7662 let raw_operation_id = operation
7664 .operation_id
7665 .clone()
7666 .unwrap_or_else(|| Self::generate_operation_id(method, path));
7667
7668 let operation_id = if canonical_operation_ids
7679 .contains(&Self::canonical_operation_id(&raw_operation_id))
7680 {
7681 let method_lower = method.to_lowercase();
7682 let mut candidate = format!("{}_{}", raw_operation_id, method_lower);
7683 let mut suffix = 2;
7684 while canonical_operation_ids.contains(&Self::canonical_operation_id(&candidate)) {
7685 candidate = format!("{}_{}_{}", raw_operation_id, method_lower, suffix);
7686 suffix += 1;
7687 }
7688 eprintln!(
7689 "⚠️ duplicate operationId `{}` at `{} {}` — disambiguated to `{}`",
7690 raw_operation_id, method, path, candidate
7691 );
7692 candidate
7693 } else {
7694 raw_operation_id.clone()
7695 };
7696
7697 let (op_info, responses) = self.analyze_single_operation(
7698 &operation_id,
7699 method,
7700 path,
7701 operation,
7702 path_item.parameters.as_ref(),
7703 analysis,
7704 )?;
7705 analysis
7706 .operation_id_aliases
7707 .entry(raw_operation_id)
7708 .or_default()
7709 .push(operation_id.clone());
7710 canonical_operation_ids.insert(Self::canonical_operation_id(&operation_id));
7711 analysis
7712 .operation_responses
7713 .insert(operation_id.clone(), responses);
7714 let source_path = webhook_name.unwrap_or(path);
7715 let escaped = source_path.replace('~', "~0").replace('/', "~1");
7716 let collection = if webhook_name.is_some() {
7717 "webhooks"
7718 } else {
7719 "paths"
7720 };
7721 let base_pointer = path_item_reference
7722 .and_then(|reference| reference.strip_prefix('#'))
7723 .map(str::to_owned)
7724 .unwrap_or_else(|| format!("/{collection}/{escaped}"));
7725 let operation_pointer = if path_item.additional_operations.as_ref().is_some_and(|map| {
7726 map.get(method)
7727 .is_some_and(|candidate| std::ptr::eq(candidate, operation))
7728 }) {
7729 format!(
7730 "{base_pointer}/additionalOperations/{}",
7731 method.replace('~', "~0").replace('/', "~1")
7732 )
7733 } else {
7734 format!("{base_pointer}/{}", method.to_ascii_lowercase())
7735 };
7736 analysis.operation_sources.insert(
7737 operation_id.clone(),
7738 OperationSource {
7739 json_pointer: operation_pointer,
7740 method: method.to_ascii_uppercase(),
7741 path: source_path.to_string(),
7742 operation_id: operation.operation_id.clone(),
7743 webhook: webhook_name.is_some(),
7744 },
7745 );
7746 analysis.operations.insert(operation_id, op_info);
7747 }
7748 Ok(())
7749 }
7750
7751 fn canonical_operation_id(operation_id: &str) -> String {
7752 use heck::ToPascalCase;
7753 operation_id.replace('.', "_").to_pascal_case()
7754 }
7755
7756 fn generate_operation_id(method: &str, path: &str) -> String {
7759 let mut operation_id = method.to_lowercase();
7761
7762 let path_parts: Vec<&str> = path.trim_start_matches('/').split('/').collect();
7764
7765 for part in path_parts {
7766 if part.is_empty() {
7767 continue;
7768 }
7769
7770 let cleaned_part = if part.starts_with('{') && part.ends_with('}') {
7772 &part[1..part.len() - 1]
7773 } else {
7774 part
7775 };
7776
7777 let pascal_case_part = cleaned_part
7779 .split(&['-', '_'][..])
7780 .map(|s| {
7781 let mut chars = s.chars();
7782 match chars.next() {
7783 None => String::new(),
7784 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
7785 }
7786 })
7787 .collect::<String>();
7788
7789 operation_id.push_str(&pascal_case_part);
7790 }
7791
7792 operation_id
7793 }
7794
7795 fn analyze_single_operation(
7797 &mut self,
7798 operation_id: &str,
7799 method: &str,
7800 path: &str,
7801 operation: &crate::openapi::Operation,
7802 path_item_parameters: Option<&Vec<crate::openapi::Parameter>>,
7803 _analysis: &mut SchemaAnalysis,
7804 ) -> Result<(OperationInfo, BTreeMap<String, OperationResponse>)> {
7805 let raw_path_item = self
7806 .openapi_spec
7807 .get("paths")
7808 .and_then(|paths| paths.get(path))
7809 .cloned();
7810 let raw_operation = raw_path_item
7811 .as_ref()
7812 .and_then(|path_item| path_item.get(method.to_ascii_lowercase()))
7813 .cloned();
7814 let request_body = operation
7815 .request_body
7816 .as_ref()
7817 .map(|request_body| self.resolve_request_body(request_body))
7818 .transpose()?;
7819 let mut op_info = OperationInfo {
7820 operation_id: operation_id.to_string(),
7821 method: method.to_uppercase(),
7822 path: normalize_operation_path(path),
7823 summary: operation.summary.clone(),
7824 description: operation.description.clone(),
7825 request_body: None,
7826 request_body_required: request_body
7828 .as_ref()
7829 .and_then(|rb| rb.required)
7830 .unwrap_or(false),
7831 response_schemas: BTreeMap::new(),
7832 parameters: Vec::new(),
7833 supports_streaming: false, stream_parameter: None, tags: operation.tags.clone().unwrap_or_default(),
7836 };
7837 let mut operation_responses = BTreeMap::new();
7838
7839 if let Some(request_body) = &request_body {
7841 use crate::openapi::{
7842 is_binary_media_type, is_form_urlencoded_media_type, is_json_media_type,
7843 media_type_essence,
7844 };
7845 if let Some((content_type, maybe_schema)) = request_body.best_content() {
7846 op_info.request_body = if is_json_media_type(content_type) {
7847 match maybe_schema {
7848 Some(s) => {
7849 let validation_schema = self
7850 .raw_request_body_schema(raw_operation.as_ref(), content_type)
7851 .unwrap_or(
7852 serde_json::to_value(s).map_err(GeneratorError::ParseError)?,
7853 );
7854 Some(
7855 self.resolve_or_inline_schema(s, operation_id, "Request")
7856 .map(|name| RequestBodyContent::Json {
7857 schema_name: name,
7858 media_type: content_type.to_string(),
7859 validation_schema,
7860 })?,
7861 )
7862 }
7863 None => Some(RequestBodyContent::SchemaLess {
7864 media_type: content_type.to_string(),
7865 }),
7866 }
7867 } else if is_form_urlencoded_media_type(content_type) {
7868 match maybe_schema {
7869 Some(s) => {
7870 let validation_schema = self
7871 .raw_request_body_schema(raw_operation.as_ref(), content_type)
7872 .unwrap_or(
7873 serde_json::to_value(s).map_err(GeneratorError::ParseError)?,
7874 );
7875 Some(
7876 self.resolve_or_inline_schema(s, operation_id, "Request")
7877 .map(|name| RequestBodyContent::FormUrlEncoded {
7878 schema_name: name,
7879 media_type: content_type.to_string(),
7880 validation_schema,
7881 })?,
7882 )
7883 }
7884 None => Some(RequestBodyContent::SchemaLess {
7885 media_type: content_type.to_string(),
7886 }),
7887 }
7888 } else if media_type_essence(content_type)
7889 .eq_ignore_ascii_case("multipart/form-data")
7890 {
7891 match maybe_schema {
7892 Some(schema) => {
7893 let validation_schema = self
7894 .raw_request_body_schema(raw_operation.as_ref(), content_type)
7895 .unwrap_or(
7896 serde_json::to_value(schema)
7897 .map_err(GeneratorError::ParseError)?,
7898 );
7899 Some(
7900 self.resolve_or_inline_schema(schema, operation_id, "Request")
7901 .map(|schema_name| RequestBodyContent::Multipart {
7902 schema_name,
7903 media_type: content_type.to_string(),
7904 validation_schema,
7905 })?,
7906 )
7907 }
7908 None => Some(RequestBodyContent::SchemaLess {
7909 media_type: content_type.to_string(),
7910 }),
7911 }
7912 } else if is_binary_media_type(content_type, maybe_schema) {
7913 if media_type_essence(content_type)
7914 .eq_ignore_ascii_case("application/octet-stream")
7915 {
7916 Some(RequestBodyContent::OctetStream {
7917 media_type: content_type.to_string(),
7918 })
7919 } else {
7920 Some(RequestBodyContent::Binary {
7921 media_type: content_type.to_string(),
7922 })
7923 }
7924 } else if crate::openapi::is_text_media_type(content_type) {
7925 Some(RequestBodyContent::TextPlain {
7930 media_type: content_type.to_string(),
7931 })
7932 } else {
7933 None
7934 };
7935 }
7936 if op_info.request_body.is_none() {
7937 let mut media_types = request_body
7938 .content
7939 .as_ref()
7940 .map(|content| content.keys().cloned().collect::<Vec<_>>())
7941 .unwrap_or_default();
7942 media_types.sort();
7943 if !media_types.is_empty() {
7944 op_info.request_body = Some(RequestBodyContent::Unsupported { media_types });
7945 }
7946 }
7947 }
7948
7949 if let Some(responses) = &operation.responses {
7951 for (status_code, response) in responses {
7952 let response = self.resolve_response(response)?;
7953 let supports_streaming = response.content.as_ref().is_some_and(|content| {
7959 content
7960 .keys()
7961 .any(|ct| crate::openapi::is_event_stream_media_type(ct))
7962 });
7963 if supports_streaming {
7964 op_info.supports_streaming = true;
7965 }
7966
7967 let mut response_info = OperationResponse {
7968 supports_streaming,
7969 has_content: response
7970 .content
7971 .as_ref()
7972 .is_some_and(|content| !content.is_empty()),
7973 ..Default::default()
7974 };
7975 if let Some((media_type, schema)) = response.json_content() {
7976 if let Some(schema_ref) = schema.reference() {
7977 if let Some(schema_name) = self.extract_schema_name(schema_ref) {
7979 op_info
7980 .response_schemas
7981 .insert(status_code.clone(), schema_name.to_string());
7982 response_info.schema_name = Some(schema_name.to_string());
7983 response_info.media_type = Some(media_type.to_string());
7984 response_info.body = Some(OperationResponseBody::Json {
7985 schema_name: schema_name.to_string(),
7986 media_type: media_type.to_string(),
7987 });
7988 }
7989 } else {
7990 let synthetic_name =
7992 self.generate_inline_response_type_name(operation_id, status_code);
7993
7994 let mut deps = HashSet::new();
7996 let synthetic_name =
7997 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
7998
7999 op_info
8000 .response_schemas
8001 .insert(status_code.clone(), synthetic_name.clone());
8002 response_info.body = Some(OperationResponseBody::Json {
8003 schema_name: synthetic_name.clone(),
8004 media_type: media_type.to_string(),
8005 });
8006 response_info.schema_name = Some(synthetic_name);
8007 response_info.media_type = Some(media_type.to_string());
8008 }
8009 }
8010 if response_info.body.is_none()
8011 && let Some(content) = response.content.as_ref()
8012 {
8013 let selected = content
8014 .iter()
8015 .find(|(media_type, media)| {
8016 matches!(
8017 crate::openapi::classify_response_media_type(
8018 media_type,
8019 media.schema.as_ref()
8020 ),
8021 crate::openapi::ResponseMediaKind::Text
8022 )
8023 })
8024 .or_else(|| {
8025 content.iter().find(|(media_type, media)| {
8026 matches!(
8027 crate::openapi::classify_response_media_type(
8028 media_type,
8029 media.schema.as_ref()
8030 ),
8031 crate::openapi::ResponseMediaKind::Binary
8032 ) && !crate::openapi::is_wildcard_media_type(media_type)
8033 })
8034 })
8035 .or_else(|| {
8036 content.iter().find(|(media_type, media)| {
8037 matches!(
8038 crate::openapi::classify_response_media_type(
8039 media_type,
8040 media.schema.as_ref()
8041 ),
8042 crate::openapi::ResponseMediaKind::Binary
8043 )
8044 })
8045 });
8046 if let Some((media_type, media)) = selected {
8047 response_info.body = match crate::openapi::classify_response_media_type(
8048 media_type,
8049 media.schema.as_ref(),
8050 ) {
8051 crate::openapi::ResponseMediaKind::Text => {
8052 Some(OperationResponseBody::Text {
8053 media_type: media_type.clone(),
8054 })
8055 }
8056 crate::openapi::ResponseMediaKind::Binary => {
8057 Some(OperationResponseBody::Binary {
8058 media_type: media_type.clone(),
8059 wildcard: crate::openapi::is_wildcard_media_type(media_type),
8060 })
8061 }
8062 _ => None,
8063 };
8064 }
8065 }
8066 if let Some(content) = &response.content {
8067 for (media_type, media) in content {
8068 let representation = match crate::openapi::classify_response_media_type(
8069 media_type,
8070 media.schema.as_ref(),
8071 ) {
8072 crate::openapi::ResponseMediaKind::Json => {
8073 let Some(schema) = media.schema.as_ref() else {
8074 continue;
8075 };
8076 let schema_name =
8077 if response_info.media_type.as_ref() == Some(media_type) {
8078 response_info.schema_name.clone()
8079 } else if let Some(reference) = schema.reference() {
8080 self.extract_schema_name(reference).map(str::to_owned)
8081 } else {
8082 let preferred = format!(
8083 "{}{}",
8084 self.generate_inline_response_type_name(
8085 operation_id,
8086 status_code
8087 ),
8088 base_param_ident(media_type)
8089 );
8090 Some(self.add_inline_schema(
8091 &preferred,
8092 schema,
8093 &mut HashSet::new(),
8094 )?)
8095 };
8096 let Some(schema_name) = schema_name else {
8097 continue;
8098 };
8099 OperationRepresentation::Json { schema_name }
8100 }
8101 crate::openapi::ResponseMediaKind::Text => {
8102 OperationRepresentation::Text
8103 }
8104 crate::openapi::ResponseMediaKind::Binary => {
8105 OperationRepresentation::Binary {
8106 wildcard: crate::openapi::is_wildcard_media_type(media_type),
8107 }
8108 }
8109 crate::openapi::ResponseMediaKind::EventStream => {
8110 OperationRepresentation::EventStream
8111 }
8112 crate::openapi::ResponseMediaKind::Unsupported => continue,
8113 };
8114 response_info
8115 .representations
8116 .insert(media_type.clone(), representation);
8117 }
8118 }
8119 response_info.unsupported_media_types = response
8120 .content
8121 .as_ref()
8122 .into_iter()
8123 .flat_map(|content| content.iter())
8124 .filter(|(media_type, content)| {
8125 match crate::openapi::classify_response_media_type(
8126 media_type,
8127 content.schema.as_ref(),
8128 ) {
8129 crate::openapi::ResponseMediaKind::Json => content.schema.is_none(),
8130 crate::openapi::ResponseMediaKind::Unsupported => true,
8131 crate::openapi::ResponseMediaKind::EventStream
8132 | crate::openapi::ResponseMediaKind::Text
8133 | crate::openapi::ResponseMediaKind::Binary => false,
8134 }
8135 })
8136 .map(|(media_type, _)| media_type.clone())
8137 .collect();
8138 operation_responses.insert(status_code.clone(), response_info);
8139 }
8140 }
8141
8142 if op_info.supports_streaming
8145 && let Some(parameters) = &operation.parameters
8146 {
8147 for param in parameters {
8148 if let Some(name) = param.name.as_deref() {
8149 if name.eq_ignore_ascii_case("stream") {
8150 op_info.stream_parameter = Some(name.to_string());
8151 break;
8152 }
8153 }
8154 }
8155 }
8156
8157 if let Some(parameters) = &operation.parameters {
8159 for (index, param) in parameters.iter().enumerate() {
8160 let resolved = self.resolve_parameter(param).into_owned();
8164 let validation_schema = raw_operation
8165 .as_ref()
8166 .and_then(|operation| operation.get("parameters"))
8167 .and_then(Value::as_array)
8168 .and_then(|parameters| parameters.get(index))
8169 .and_then(|parameter| self.raw_parameter_schema(parameter));
8170 if let Some(param_info) =
8171 self.analyze_parameter(&resolved, operation_id, validation_schema)?
8172 {
8173 op_info.parameters.push(param_info);
8174 }
8175 }
8176 }
8177
8178 if let Some(path_params) = path_item_parameters {
8180 let existing_keys: std::collections::HashSet<(String, String)> = op_info
8181 .parameters
8182 .iter()
8183 .map(|p| (p.name.clone(), p.location.clone()))
8184 .collect();
8185 for (index, param) in path_params.iter().enumerate() {
8186 let resolved = self.resolve_parameter(param).into_owned();
8187 let validation_schema = raw_path_item
8188 .as_ref()
8189 .and_then(|path_item| path_item.get("parameters"))
8190 .and_then(Value::as_array)
8191 .and_then(|parameters| parameters.get(index))
8192 .and_then(|parameter| self.raw_parameter_schema(parameter));
8193 if let Some(param_info) =
8194 self.analyze_parameter(&resolved, operation_id, validation_schema)?
8195 {
8196 if !existing_keys
8197 .contains(&(param_info.name.clone(), param_info.location.clone()))
8198 {
8199 op_info.parameters.push(param_info);
8200 }
8201 }
8202 }
8203 }
8204
8205 let mut declared_path_names: std::collections::HashSet<String> = op_info
8213 .parameters
8214 .iter()
8215 .filter(|p| p.location == "path")
8216 .map(|p| p.name.clone())
8217 .collect();
8218 let bytes = path.as_bytes().iter();
8219 let mut current = String::new();
8220 let mut in_brace = false;
8221 let mut synthesized: Vec<String> = Vec::new();
8222 for b in bytes {
8223 match *b {
8224 b'{' => {
8225 in_brace = true;
8226 current.clear();
8227 }
8228 b'}' if in_brace => {
8229 in_brace = false;
8230 if !current.is_empty() && !declared_path_names.contains(¤t) {
8231 synthesized.push(current.clone());
8232 declared_path_names.insert(current.clone());
8233 }
8234 }
8235 _ if in_brace => current.push(*b as char),
8236 _ => {}
8237 }
8238 }
8239 for name in synthesized {
8240 eprintln!(
8241 "⚠️ path `{}` references `{{{}}}` but the spec doesn't declare it as a parameter — synthesizing as required String",
8242 path, name
8243 );
8244 op_info.parameters.push(ParameterInfo {
8245 name,
8246 location: "path".to_string(),
8247 required: true,
8248 schema_ref: None,
8249 rust_type: "String".to_string(),
8250 description: None,
8251 enum_values: None,
8252 enum_varnames: None,
8253 rust_ident: None,
8254 query_serialization: None,
8255 validation_schema: None,
8256 });
8257 }
8258
8259 let mut used: std::collections::HashSet<String> =
8267 ["request", "body", "req", "request_url", "client", "form"]
8268 .into_iter()
8269 .map(str::to_owned)
8270 .collect();
8271 for p in op_info.parameters.iter_mut() {
8272 let raw = base_param_ident(&p.name);
8273 let raw = if matches!(raw.as_str(), "self" | "super" | "crate") {
8274 format!("{raw}_param")
8275 } else {
8276 raw
8277 };
8278 let mut chosen = raw.clone();
8279 let mut suffix = 2;
8280 while !used.insert(chosen.clone()) {
8281 chosen = format!("{raw}_{suffix}");
8282 suffix += 1;
8283 }
8284 p.rust_ident = Some(chosen);
8285 }
8286
8287 Ok((op_info, operation_responses))
8288 }
8289
8290 fn resolve_request_body(
8292 &self,
8293 request_body: &crate::openapi::RequestBody,
8294 ) -> Result<crate::openapi::RequestBody> {
8295 let mut current = request_body.clone();
8296 let mut visited = HashSet::new();
8297 while let Some(reference) = current.reference.clone() {
8298 if !visited.insert(reference.clone()) {
8299 return Err(GeneratorError::CircularDependency(format!(
8300 "request body reference {reference}"
8301 )));
8302 }
8303
8304 let pointer = reference.strip_prefix('#').ok_or_else(|| {
8305 GeneratorError::UnresolvedReference(format!(
8306 "external request body reference `{reference}` is not supported"
8307 ))
8308 })?;
8309 if !pointer.is_empty() && !pointer.starts_with('/') {
8310 return Err(GeneratorError::UnresolvedReference(format!(
8311 "request body reference `{reference}` is not a local JSON Pointer"
8312 )));
8313 }
8314 let value = self.openapi_spec.pointer(pointer).ok_or_else(|| {
8315 GeneratorError::UnresolvedReference(format!(
8316 "request body reference `{reference}` does not exist"
8317 ))
8318 })?;
8319 let object = value.as_object().ok_or_else(|| {
8320 GeneratorError::InvalidSchema(format!(
8321 "request body reference `{reference}` must target an object"
8322 ))
8323 })?;
8324 if !["$ref", "description", "required", "content"]
8325 .iter()
8326 .any(|field| object.contains_key(*field))
8327 {
8328 return Err(GeneratorError::InvalidSchema(format!(
8329 "request body reference `{reference}` does not target a structurally compatible OpenAPI Request Body Object"
8330 )));
8331 }
8332 current = serde_json::from_value(value.clone()).map_err(|error| {
8333 GeneratorError::InvalidSchema(format!(
8334 "request body reference `{reference}` is not a valid OpenAPI Request Body Object: {error}"
8335 ))
8336 })?;
8337 }
8338 Ok(current)
8339 }
8340
8341 fn resolve_response(
8348 &self,
8349 response: &crate::openapi::Response,
8350 ) -> Result<crate::openapi::Response> {
8351 let mut current = response.clone();
8352 let mut visited = HashSet::new();
8353 while let Some(reference) = current.reference.clone() {
8354 if !visited.insert(reference.clone()) {
8355 return Err(GeneratorError::CircularDependency(format!(
8356 "response reference {reference}"
8357 )));
8358 }
8359
8360 let pointer = reference.strip_prefix('#').ok_or_else(|| {
8361 GeneratorError::UnresolvedReference(format!(
8362 "external response reference `{reference}` is not supported"
8363 ))
8364 })?;
8365 if !pointer.is_empty() && !pointer.starts_with('/') {
8366 return Err(GeneratorError::UnresolvedReference(format!(
8367 "response reference `{reference}` is not a local JSON Pointer"
8368 )));
8369 }
8370 let value = self.openapi_spec.pointer(pointer).ok_or_else(|| {
8371 GeneratorError::UnresolvedReference(format!(
8372 "response reference `{reference}` does not exist"
8373 ))
8374 })?;
8375 let object = value.as_object().ok_or_else(|| {
8376 GeneratorError::InvalidSchema(format!(
8377 "response reference `{reference}` must target an object"
8378 ))
8379 })?;
8380 if !["$ref", "description", "headers", "content", "links"]
8381 .iter()
8382 .any(|field| object.contains_key(*field))
8383 {
8384 return Err(GeneratorError::InvalidSchema(format!(
8385 "response reference `{reference}` does not target a structurally compatible OpenAPI Response Object"
8386 )));
8387 }
8388 current = serde_json::from_value(value.clone()).map_err(|error| {
8389 GeneratorError::InvalidSchema(format!(
8390 "response reference `{reference}` is not a valid OpenAPI Response Object: {error}"
8391 ))
8392 })?;
8393 }
8394 Ok(current)
8395 }
8396
8397 fn generate_inline_response_type_name(&self, operation_id: &str, status_code: &str) -> String {
8404 use heck::ToPascalCase;
8405 let base_name = operation_id.replace('.', "_").to_pascal_case();
8406 let suffix = Self::status_code_suffix(status_code);
8407 format!("{}Response{}", base_name, suffix)
8408 }
8409
8410 fn status_code_suffix(status_code: &str) -> String {
8417 match status_code {
8418 "" | "200" => String::new(),
8419 "default" | "Default" => "Default".to_string(),
8420 other if other.chars().all(|c| c.is_ascii_digit()) => other.to_string(),
8421 other => other.to_ascii_lowercase(),
8422 }
8423 }
8424
8425 fn generate_inline_request_type_name(&self, operation_id: &str) -> String {
8427 use heck::ToPascalCase;
8428 let base_name = operation_id.replace('.', "_").to_pascal_case();
8432 format!("{}Request", base_name)
8433 }
8434
8435 fn resolve_or_inline_schema(
8438 &mut self,
8439 schema: &crate::openapi::Schema,
8440 operation_id: &str,
8441 suffix: &str,
8442 ) -> Result<String> {
8443 if let Some(schema_ref) = schema.reference()
8444 && let Some(schema_name) = self.extract_schema_name(schema_ref)
8445 {
8446 return Ok(schema_name.to_string());
8447 }
8448 let synthetic_name = if suffix == "Request" {
8450 self.generate_inline_request_type_name(operation_id)
8451 } else {
8452 self.generate_inline_response_type_name(operation_id, "")
8453 };
8454 let mut deps = HashSet::new();
8455 self.add_inline_schema(&synthetic_name, schema, &mut deps)
8456 }
8457
8458 fn resolve_parameter<'a>(
8461 &'a self,
8462 param: &'a crate::openapi::Parameter,
8463 ) -> std::borrow::Cow<'a, crate::openapi::Parameter> {
8464 if let Some(ref_str) = param.reference.as_deref() {
8465 if let Some(param_name) = ref_str.strip_prefix("#/components/parameters/") {
8466 if let Some(resolved) = self.component_parameters.get(param_name) {
8467 return std::borrow::Cow::Borrowed(resolved);
8468 }
8469 }
8470 }
8471 std::borrow::Cow::Borrowed(param)
8472 }
8473
8474 fn referenced_schema_is_string_enum(&self, name: &str) -> bool {
8487 if self.resolve_cached_schema(name).is_some_and(|schema| {
8488 matches!(
8489 schema.schema_type,
8490 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. }
8491 )
8492 }) {
8493 return true;
8494 }
8495 let Some(schema_value) = self
8496 .openapi_spec
8497 .get("components")
8498 .and_then(|c| c.get("schemas"))
8499 .and_then(|s| s.get(name))
8500 else {
8501 return false;
8502 };
8503 let is_string_type = schema_value
8504 .get("type")
8505 .and_then(|v| v.as_str())
8506 .map(|s| s == "string")
8507 .unwrap_or(false);
8508 let has_enum_or_const =
8509 schema_value.get("enum").is_some() || schema_value.get("const").is_some();
8510 is_string_type && has_enum_or_const
8511 }
8512
8513 fn resolve_raw_local_reference(&self, value: &Value) -> Option<Value> {
8514 let Some(reference) = value.get("$ref").and_then(Value::as_str) else {
8515 return Some(value.clone());
8516 };
8517 let pointer = reference.strip_prefix('#')?;
8518 self.openapi_spec.pointer(pointer).cloned()
8519 }
8520
8521 fn raw_request_body_schema(
8522 &self,
8523 operation: Option<&Value>,
8524 content_type: &str,
8525 ) -> Option<Value> {
8526 let request_body = operation?.get("requestBody")?;
8527 self.resolve_raw_local_reference(request_body)?
8528 .get("content")?
8529 .get(content_type)?
8530 .get("schema")
8531 .cloned()
8532 }
8533
8534 fn raw_parameter_schema(&self, parameter: &Value) -> Option<Value> {
8535 self.resolve_raw_local_reference(parameter)?
8536 .get("schema")
8537 .cloned()
8538 }
8539
8540 fn analyze_parameter(
8541 &mut self,
8542 param: &crate::openapi::Parameter,
8543 operation_id: &str,
8544 raw_validation_schema: Option<Value>,
8545 ) -> Result<Option<ParameterInfo>> {
8546 use heck::ToPascalCase;
8547
8548 let name = param.name.as_deref().unwrap_or("");
8549 let location = param.location.as_deref().unwrap_or("");
8550 let required = param.required.unwrap_or(false);
8551 let validation_schema = match raw_validation_schema {
8552 Some(schema) => Some(schema),
8553 None => param
8554 .schema
8555 .as_ref()
8556 .map(serde_json::to_value)
8557 .transpose()
8558 .map_err(GeneratorError::ParseError)?,
8559 };
8560
8561 let mut rust_type = "String".to_string();
8562 let mut schema_ref = None;
8563 let mut enum_values: Option<Vec<String>> = None;
8564 let mut enum_varnames: Option<Vec<String>> = None;
8565 let mut query_serialization: Option<QuerySerialization> = None;
8566
8567 let is_query = location == "query";
8573 let is_simple_header = location == "header"
8574 && matches!(param.style.as_deref(), None | Some("simple"))
8575 && param.explode != Some(true);
8576 let form_style = matches!(param.style.as_deref(), None | Some("form"));
8577 let form_exploded = form_style && param.explode.unwrap_or(true);
8578 let deep_object =
8579 param.style.as_deref() == Some("deepObject") && param.explode != Some(false);
8580
8581 let object_serialization = if !is_query {
8582 None
8583 } else if deep_object {
8584 Some(QuerySerialization::DeepObject)
8585 } else if form_exploded {
8586 Some(QuerySerialization::FormExplodedObject)
8587 } else if form_style {
8588 Some(QuerySerialization::FormObject)
8589 } else {
8590 None
8591 };
8592
8593 if let Some(schema) = ¶m.schema {
8594 if let Some(ref_str) = schema.reference() {
8595 if let Some(name) = self.extract_schema_name(ref_str) {
8601 if self.referenced_schema_is_string_enum(name) {
8602 schema_ref = Some(name.to_string());
8603 } else if object_serialization.is_some()
8604 && self.referenced_schema_is_object(name)
8605 {
8606 schema_ref = Some(name.to_string());
8607 query_serialization = if form_exploded && self.uses_aws_query_conventions()
8608 {
8609 match self.referenced_array_struct_item_type(name, 1) {
8610 Some(ArrayItemType::NestedStructRef { properties, .. }) => {
8611 Some(QuerySerialization::FormExplodedNestedObject {
8612 properties,
8613 })
8614 }
8615 _ => object_serialization.clone(),
8616 }
8617 } else {
8618 object_serialization.clone()
8619 };
8620 } else if (is_query && form_style || is_simple_header)
8621 && let Some(item_type) = self.referenced_array_param_item_type(name)
8622 {
8623 schema_ref = Some(name.to_string());
8629 query_serialization = Some(if is_simple_header {
8630 QuerySerialization::SimpleHeaderArray { item_type }
8631 } else if form_exploded {
8632 QuerySerialization::FormExplodedArray { item_type }
8633 } else {
8634 QuerySerialization::FormArray { item_type }
8635 });
8636 }
8637 }
8638 } else if object_serialization.is_some() && Self::schema_is_inline_object(schema) {
8639 let op_pascal = operation_id.replace('.', "_").to_pascal_case();
8644 let param_pascal = name.to_pascal_case();
8645 let synthetic_name = format!("{op_pascal}{param_pascal}");
8646 let mut deps = HashSet::new();
8647 let synthetic_name = self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
8648 schema_ref = Some(synthetic_name.clone());
8649 query_serialization = if form_exploded && self.uses_aws_query_conventions() {
8650 match self.referenced_array_struct_item_type(&synthetic_name, 1) {
8651 Some(ArrayItemType::NestedStructRef { properties, .. }) => {
8652 Some(QuerySerialization::FormExplodedNestedObject { properties })
8653 }
8654 _ => object_serialization.clone(),
8655 }
8656 } else {
8657 object_serialization.clone()
8658 };
8659 } else if (is_query && form_style || is_simple_header)
8660 && matches!(
8661 schema.schema_type(),
8662 Some(crate::openapi::SchemaType::Array)
8663 )
8664 && let Some(item_type) = self.array_param_item_type(schema)
8665 {
8666 query_serialization = Some(if is_simple_header {
8674 QuerySerialization::SimpleHeaderArray { item_type }
8675 } else if form_exploded {
8676 QuerySerialization::FormExplodedArray { item_type }
8677 } else {
8678 QuerySerialization::FormArray { item_type }
8679 });
8680 } else if let Some(schema_type) = schema.schema_type() {
8681 let format = schema.details().format.clone();
8687 rust_type = match schema_type {
8688 crate::openapi::SchemaType::Boolean => "bool".to_string(),
8689 crate::openapi::SchemaType::Integer => self.integer_rust_type(schema.details()),
8690 crate::openapi::SchemaType::Number => {
8691 self.type_mapper.number_format(format.as_deref()).rust_type
8692 }
8693 crate::openapi::SchemaType::String => "String".to_string(),
8694 _ => "String".to_string(),
8695 };
8696
8697 if matches!(schema_type, crate::openapi::SchemaType::String) {
8698 let details = schema.details();
8699 if details.is_string_enum() {
8700 if let Some(values) = details.string_enum_values() {
8701 if !values.is_empty() {
8702 let op_pascal = operation_id.replace('.', "_").to_pascal_case();
8703 let param_pascal = name.to_pascal_case();
8704 rust_type = format!("{op_pascal}{param_pascal}");
8705 enum_varnames = details
8710 .extra
8711 .get("x-enum-varnames")
8712 .and_then(Value::as_array)
8713 .map(|raw| {
8714 raw.iter()
8715 .filter_map(Value::as_str)
8716 .map(str::to_owned)
8717 .collect::<Vec<_>>()
8718 })
8719 .filter(|names| names.len() == values.len());
8720 enum_values = Some(values);
8721 }
8722 }
8723 }
8724 }
8725 }
8726
8727 if is_query && query_serialization.is_none() {
8728 let referenced_name = schema
8729 .reference()
8730 .and_then(|reference| self.extract_schema_name(reference));
8731 let is_object = referenced_name
8732 .is_some_and(|name| self.referenced_schema_is_object(name))
8733 || Self::schema_is_inline_object(schema);
8734 let is_array = referenced_name
8735 .is_some_and(|name| self.referenced_schema_is_array(name))
8736 || matches!(
8737 schema.schema_type(),
8738 Some(crate::openapi::SchemaType::Array)
8739 );
8740 let is_composed = referenced_name
8741 .is_some_and(|name| self.referenced_schema_is_composed_query_shape(name));
8742 let reason = if param.style.as_deref() == Some("deepObject")
8743 && param.explode == Some(false)
8744 {
8745 Some("style=deepObject with explode=false is undefined by OpenAPI".to_string())
8746 } else if param.style.as_deref() == Some("deepObject") && !is_object {
8747 Some("style=deepObject is defined only for object query parameters".to_string())
8748 } else if is_object {
8749 Some(format!(
8750 "object query parameters do not support style={}",
8751 param.style.as_deref().unwrap_or("form")
8752 ))
8753 } else if is_array && form_style {
8754 Some(
8755 "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"
8756 .to_string(),
8757 )
8758 } else if is_array {
8759 Some(format!(
8760 "array query parameters do not yet support style={}",
8761 param.style.as_deref().unwrap_or("form")
8762 ))
8763 } else if is_composed {
8764 Some(
8765 "composed or union query schemas cannot be projected to an unambiguous flat wire shape"
8766 .to_string(),
8767 )
8768 } else {
8769 None
8770 };
8771 if let Some(reason) = reason {
8772 query_serialization = Some(QuerySerialization::Unsupported { reason });
8773 }
8774 }
8775 }
8776
8777 Ok(Some(ParameterInfo {
8778 name: name.to_string(),
8779 location: location.to_string(),
8780 required,
8781 schema_ref,
8782 rust_type,
8783 description: param.description.clone(),
8784 enum_values,
8785 enum_varnames,
8786 rust_ident: None,
8787 query_serialization,
8788 validation_schema,
8789 }))
8790 }
8791
8792 fn array_param_item_type(&self, schema: &crate::openapi::Schema) -> Option<ArrayItemType> {
8801 let items = schema.details().item_schema()?;
8802 let unwrapped = unwrap_annotation_allof(items);
8806 if let Some(ref_str) = unwrapped.reference() {
8807 let name = self.extract_schema_name(ref_str)?;
8808 return self
8809 .referenced_array_scalar_item_type(name)
8810 .or_else(|| self.referenced_array_struct_item_type(name, 1));
8811 }
8812 let format = unwrapped.details().format.clone();
8813 let scalar = match unwrapped.schema_type()? {
8814 crate::openapi::SchemaType::String => "String".to_string(),
8815 crate::openapi::SchemaType::Integer => self.integer_rust_type(unwrapped.details()),
8816 crate::openapi::SchemaType::Number => {
8817 self.type_mapper.number_format(format.as_deref()).rust_type
8818 }
8819 crate::openapi::SchemaType::Boolean => "bool".to_string(),
8820 _ => return None,
8821 };
8822 Some(ArrayItemType::Scalar(scalar))
8823 }
8824
8825 fn referenced_array_param_item_type(&self, name: &str) -> Option<ArrayItemType> {
8828 let schema = self.resolve_cached_schema(name)?;
8829 let SchemaType::Array { item_type } = &schema.schema_type else {
8830 return None;
8831 };
8832 self.analyzed_array_item_type(item_type)
8833 }
8834
8835 fn analyzed_array_item_type(&self, item_type: &SchemaType) -> Option<ArrayItemType> {
8836 self.analyzed_array_item_type_at_depth(item_type, 1)
8837 }
8838
8839 fn referenced_array_struct_item_type(
8844 &self,
8845 name: &str,
8846 nested_array_depth: usize,
8847 ) -> Option<ArrayItemType> {
8848 let resolved = self.resolve_cached_schema(name)?;
8849 let SchemaType::Object {
8850 properties,
8851 required,
8852 additional_properties,
8853 ..
8854 } = &resolved.schema_type
8855 else {
8856 return None;
8857 };
8858 if properties.is_empty() || additional_properties.is_open() {
8859 return None;
8860 }
8861 let mut projected = Vec::with_capacity(properties.len());
8862 let mut has_array = false;
8863 for (wire_name, property) in properties {
8864 let value_type = if let Some(scalar) = self.query_scalar_type(&property.schema_type) {
8865 QueryStructPropertyType::Scalar(scalar)
8866 } else {
8867 if nested_array_depth == 0 {
8868 return None;
8869 }
8870 if let Some(array) = self.resolve_query_array_type(&property.schema_type) {
8871 let item_type =
8872 self.analyzed_array_item_type_at_depth(array, nested_array_depth - 1)?;
8873 if matches!(item_type, ArrayItemType::NestedStructRef { .. }) {
8874 return None;
8875 }
8876 has_array = true;
8877 QueryStructPropertyType::Array { item_type }
8878 } else {
8879 has_array = true;
8880 QueryStructPropertyType::Object {
8881 properties: self.query_flat_object_properties(&property.schema_type)?,
8882 }
8883 }
8884 };
8885 projected.push(QueryStructProperty {
8886 wire_name: wire_name.clone(),
8887 required: required.contains(wire_name),
8888 value_type,
8889 });
8890 }
8891 if has_array {
8892 Some(ArrayItemType::NestedStructRef {
8893 schema_name: name.to_string(),
8894 properties: projected,
8895 })
8896 } else {
8897 Some(ArrayItemType::FlatStructRef {
8898 schema_name: name.to_string(),
8899 properties: projected,
8900 })
8901 }
8902 }
8903
8904 fn analyzed_array_item_type_at_depth(
8905 &self,
8906 item_type: &SchemaType,
8907 nested_array_depth: usize,
8908 ) -> Option<ArrayItemType> {
8909 match item_type {
8910 SchemaType::Primitive { rust_type, .. } => {
8911 Some(ArrayItemType::Scalar(rust_type.clone()))
8912 }
8913 SchemaType::Reference { target } => self
8914 .referenced_array_scalar_item_type(target)
8915 .or_else(|| self.referenced_array_struct_item_type(target, nested_array_depth)),
8916 _ => None,
8917 }
8918 }
8919
8920 fn resolve_query_array_type<'a>(
8921 &'a self,
8922 schema_type: &'a SchemaType,
8923 ) -> Option<&'a SchemaType> {
8924 match schema_type {
8925 SchemaType::Array { item_type } => Some(item_type),
8926 SchemaType::Reference { target } => {
8927 let resolved = self.resolve_cached_schema(target)?;
8928 let SchemaType::Array { item_type } = &resolved.schema_type else {
8929 return None;
8930 };
8931 Some(item_type)
8932 }
8933 _ => None,
8934 }
8935 }
8936
8937 fn query_flat_object_properties(
8938 &self,
8939 schema_type: &SchemaType,
8940 ) -> Option<Vec<QueryStructProperty>> {
8941 let schema_type = match schema_type {
8942 SchemaType::Reference { target } => &self.resolve_cached_schema(target)?.schema_type,
8943 other => other,
8944 };
8945 let SchemaType::Object {
8946 properties,
8947 required,
8948 additional_properties,
8949 ..
8950 } = schema_type
8951 else {
8952 return None;
8953 };
8954 if properties.is_empty() || additional_properties.is_open() {
8955 return None;
8956 }
8957 properties
8958 .iter()
8959 .map(|(wire_name, property)| {
8960 Some(QueryStructProperty {
8961 wire_name: wire_name.clone(),
8962 required: required.contains(wire_name),
8963 value_type: QueryStructPropertyType::Scalar(
8964 self.query_scalar_type(&property.schema_type)?,
8965 ),
8966 })
8967 })
8968 .collect()
8969 }
8970
8971 fn query_scalar_type(&self, schema_type: &SchemaType) -> Option<QueryScalarType> {
8972 match schema_type {
8973 SchemaType::Primitive { rust_type, .. } => match rust_type.as_str() {
8974 "String" => Some(QueryScalarType::String),
8975 "bool" => Some(QueryScalarType::Boolean),
8976 value if value.starts_with('i') || value.starts_with('u') => {
8977 Some(QueryScalarType::Integer)
8978 }
8979 value if value.starts_with('f') => Some(QueryScalarType::Number),
8980 "serde_json::Value" => None,
8981 _ => Some(QueryScalarType::String),
8982 },
8983 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. } => {
8984 Some(QueryScalarType::String)
8985 }
8986 SchemaType::Reference { target } => {
8987 let resolved = self.resolve_cached_schema(target)?;
8988 self.query_scalar_type(&resolved.schema_type)
8989 }
8990 _ => None,
8991 }
8992 }
8993
8994 fn referenced_array_scalar_item_type(&self, name: &str) -> Option<ArrayItemType> {
9002 let resolved = self.resolve_cached_schema(name)?;
9003 let supported = match &resolved.schema_type {
9004 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. } => true,
9005 SchemaType::Primitive { .. } => resolved
9006 .original
9007 .get("type")
9008 .is_some_and(Self::query_scalar_type_value),
9009 _ => false,
9010 };
9011 supported.then(|| ArrayItemType::SchemaRef(name.to_string()))
9012 }
9013
9014 fn query_scalar_type_value(value: &Value) -> bool {
9015 const SCALARS: [&str; 4] = ["string", "integer", "number", "boolean"];
9016 if let Some(value) = value.as_str() {
9017 return SCALARS.contains(&value);
9018 }
9019 let Some(values) = value.as_array() else {
9020 return false;
9021 };
9022 if !values.iter().all(Value::is_string) {
9023 return false;
9024 }
9025 let mut non_null = values
9026 .iter()
9027 .filter_map(Value::as_str)
9028 .filter(|value| *value != "null");
9029 let Some(scalar) = non_null.next() else {
9030 return false;
9031 };
9032 non_null.next().is_none() && SCALARS.contains(&scalar)
9033 }
9034
9035 fn referenced_schema_is_object(&self, name: &str) -> bool {
9039 self.resolve_cached_schema(name)
9040 .is_some_and(|schema| matches!(schema.schema_type, SchemaType::Object { .. }))
9041 }
9042
9043 fn referenced_schema_is_array(&self, name: &str) -> bool {
9044 self.resolve_cached_schema(name)
9045 .is_some_and(|schema| matches!(schema.schema_type, SchemaType::Array { .. }))
9046 }
9047
9048 fn referenced_schema_is_composed_query_shape(&self, name: &str) -> bool {
9049 self.resolve_cached_schema(name).is_some_and(|schema| {
9050 matches!(
9051 schema.schema_type,
9052 SchemaType::Composition { .. }
9053 | SchemaType::Union { .. }
9054 | SchemaType::DiscriminatedUnion { .. }
9055 )
9056 })
9057 }
9058
9059 fn resolve_cached_schema(&self, name: &str) -> Option<&AnalyzedSchema> {
9060 let mut current = name;
9061 let mut visited = HashSet::new();
9062 loop {
9063 if !visited.insert(current) {
9064 return None;
9065 }
9066 let schema = self.resolved_cache.get(current)?;
9067 if let SchemaType::Reference { target } = &schema.schema_type {
9068 current = target;
9069 } else {
9070 return Some(schema);
9071 }
9072 }
9073 }
9074
9075 fn schema_is_inline_object(schema: &crate::openapi::Schema) -> bool {
9077 match schema.schema_type() {
9078 Some(crate::openapi::SchemaType::Object) => true,
9079 None => schema.details().properties.is_some(),
9080 _ => false,
9081 }
9082 }
9083}
9084
9085fn pointer_type_name(pointer: &str) -> String {
9096 use heck::ToPascalCase;
9097
9098 pointer
9099 .split('/')
9100 .skip(1)
9101 .filter(|segment| !matches!(*segment, "components" | "schemas" | "properties"))
9102 .map(|segment| {
9103 segment
9104 .replace("~1", "/")
9105 .replace("~0", "~")
9106 .to_pascal_case()
9107 })
9108 .collect::<String>()
9109}
9110
9111fn shared_positional_schema(positions: &[Schema]) -> Option<&Schema> {
9116 let first = positions.first()?;
9117 let key = positional_schema_key(first)?;
9118 positions
9119 .iter()
9120 .skip(1)
9121 .all(|position| positional_schema_key(position).as_deref() == Some(key.as_str()))
9122 .then_some(first)
9123}
9124
9125fn positional_schema_key(schema: &Schema) -> Option<String> {
9126 if let Some(reference) = schema.reference() {
9127 return Some(format!("$ref {reference}"));
9128 }
9129 let details = schema.details();
9130 if details.properties.is_some() || details.enum_values.is_some() || details.items.is_some() {
9131 return None;
9132 }
9133 match schema.schema_type()? {
9134 crate::openapi::SchemaType::Object | crate::openapi::SchemaType::Array => None,
9135 scalar => Some(format!(
9136 "{scalar:?} {}",
9137 details.format.as_deref().unwrap_or_default()
9138 )),
9139 }
9140}
9141
9142fn parse_spec_document(openapi_spec: &Value) -> Result<OpenApiSpec> {
9143 serde_path_to_error::deserialize(openapi_spec).map_err(|error| {
9144 let mut pointer = json_pointer(error.path());
9145 pointer.push_str(&refine_schema_failure(openapi_spec, &pointer));
9149 GeneratorError::ParseErrorAt {
9150 pointer,
9151 message: error.into_inner().to_string(),
9152 }
9153 })
9154}
9155
9156const SUBSCHEMA_KEYWORDS: [&str; 11] = [
9158 "items",
9159 "additionalProperties",
9160 "propertyNames",
9161 "unevaluatedProperties",
9162 "unevaluatedItems",
9163 "contains",
9164 "contentSchema",
9165 "if",
9166 "then",
9167 "else",
9168 "not",
9169];
9170
9171const SUBSCHEMA_LIST_KEYWORDS: [&str; 4] = ["oneOf", "anyOf", "allOf", "prefixItems"];
9173
9174const SUBSCHEMA_MAP_KEYWORDS: [&str; 5] = [
9176 "properties",
9177 "patternProperties",
9178 "dependentSchemas",
9179 "$defs",
9180 "definitions",
9181];
9182
9183const REFINE_PARSE_BUDGET: usize = 20_000;
9187
9188fn refine_schema_failure(openapi_spec: &Value, pointer: &str) -> String {
9196 let Some(path) = pointer.strip_prefix('#') else {
9197 return String::new();
9198 };
9199 let Some(node) = openapi_spec.pointer(path) else {
9200 return String::new();
9201 };
9202 let segments = path.split('/').skip(1).collect::<Vec<_>>();
9203 let last = segments.last().copied().unwrap_or_default();
9204 let parent = segments
9205 .len()
9206 .checked_sub(2)
9207 .map(|index| segments[index])
9208 .unwrap_or_default();
9209
9210 if last == "schema" || holds_schemas(parent) {
9211 return if parses_as_schema(node) {
9212 String::new()
9213 } else {
9214 deepest_schema_failure(node)
9215 };
9216 }
9217
9218 let mut budget = REFINE_PARSE_BUDGET;
9219 locate_failing_schema(node, holds_schemas(last), &mut budget).unwrap_or_default()
9220}
9221
9222fn holds_schemas(key: &str) -> bool {
9225 matches!(key, "schemas" | "$defs" | "definitions")
9226}
9227
9228fn locate_failing_schema(
9236 node: &Value,
9237 children_are_schemas: bool,
9238 budget: &mut usize,
9239) -> Option<String> {
9240 for (segment, key, child) in child_nodes(node) {
9241 if *budget == 0 {
9242 return None;
9243 }
9244 *budget -= 1;
9245 if children_are_schemas || key == "schema" {
9246 if !parses_as_schema(child) {
9247 return Some(format!("/{segment}{}", deepest_schema_failure(child)));
9248 }
9249 continue;
9250 }
9251 if let Some(rest) = locate_failing_schema(child, holds_schemas(key), budget) {
9252 return Some(format!("/{segment}{rest}"));
9253 }
9254 }
9255 None
9256}
9257
9258fn parses_as_schema(node: &Value) -> bool {
9259 Schema::deserialize(node).is_ok()
9260}
9261
9262fn deepest_schema_failure(node: &Value) -> String {
9266 let Some(object) = node.as_object() else {
9267 return String::new();
9268 };
9269
9270 let descend = |segment: String, child: &Value| -> Option<String> {
9271 if parses_as_schema(child) {
9272 return None;
9273 }
9274 Some(format!("/{segment}{}", deepest_schema_failure(child)))
9275 };
9276
9277 for keyword in SUBSCHEMA_KEYWORDS {
9278 if let Some(child) = object.get(keyword)
9279 && let Some(suffix) = descend(escape_pointer_segment(keyword), child)
9280 {
9281 return suffix;
9282 }
9283 }
9284 for keyword in SUBSCHEMA_LIST_KEYWORDS {
9285 if let Some(Value::Array(children)) = object.get(keyword) {
9286 for (index, child) in children.iter().enumerate() {
9287 if let Some(suffix) = descend(
9288 format!("{}/{index}", escape_pointer_segment(keyword)),
9289 child,
9290 ) {
9291 return suffix;
9292 }
9293 }
9294 }
9295 }
9296 for keyword in SUBSCHEMA_MAP_KEYWORDS {
9297 if let Some(Value::Object(children)) = object.get(keyword) {
9298 for (name, child) in children {
9299 if let Some(suffix) = descend(
9300 format!(
9301 "{}/{}",
9302 escape_pointer_segment(keyword),
9303 escape_pointer_segment(name)
9304 ),
9305 child,
9306 ) {
9307 return suffix;
9308 }
9309 }
9310 }
9311 }
9312 String::new()
9313}
9314
9315fn child_nodes(node: &Value) -> Vec<(String, &str, &Value)> {
9321 const DATA_KEYWORDS: [&str; 5] = ["example", "examples", "default", "enum", "const"];
9322
9323 match node {
9324 Value::Object(members) => members
9325 .iter()
9326 .filter(|(name, child)| {
9327 (child.is_object() || child.is_array())
9328 && !name.starts_with("x-")
9329 && !DATA_KEYWORDS.contains(&name.as_str())
9330 })
9331 .map(|(name, child)| (escape_pointer_segment(name), name.as_str(), child))
9332 .collect(),
9333 Value::Array(elements) => elements
9334 .iter()
9335 .enumerate()
9336 .filter(|(_, child)| child.is_object() || child.is_array())
9337 .map(|(index, child)| (index.to_string(), "", child))
9338 .collect(),
9339 _ => Vec::new(),
9340 }
9341}
9342
9343fn escape_pointer_segment(segment: &str) -> String {
9344 segment.replace('~', "~0").replace('/', "~1")
9345}
9346
9347fn json_pointer(path: &serde_path_to_error::Path) -> String {
9351 use serde_path_to_error::Segment;
9352
9353 let mut pointer = String::from("#");
9354 for segment in path.iter() {
9355 match segment {
9356 Segment::Seq { index } => {
9357 pointer.push('/');
9358 pointer.push_str(&index.to_string());
9359 }
9360 Segment::Map { key } | Segment::Enum { variant: key } => {
9361 pointer.push('/');
9362 pointer.push_str(&escape_pointer_segment(key));
9363 }
9364 Segment::Unknown => pointer.push_str("/?"),
9365 }
9366 }
9367 pointer
9368}
9369
9370pub(crate) fn component_schema_name_aliases(openapi_spec: &Value) -> BTreeMap<String, String> {
9371 let Some(schemas) = openapi_spec
9372 .pointer("/components/schemas")
9373 .and_then(Value::as_object)
9374 else {
9375 return BTreeMap::new();
9376 };
9377
9378 let mut names_by_rust_name = BTreeMap::<String, Vec<String>>::new();
9379 for name in schemas.keys() {
9380 names_by_rust_name
9381 .entry(crate::generator::rust_type_name(name))
9382 .or_default()
9383 .push(name.clone());
9384 }
9385
9386 let mut claimed_rust_names = names_by_rust_name.keys().cloned().collect::<HashSet<_>>();
9389 let mut aliases = BTreeMap::new();
9390
9391 for (rust_name, mut names) in names_by_rust_name {
9392 if names.len() < 2 {
9393 continue;
9394 }
9395
9396 names.sort_by_key(|name| (name != &rust_name, name.clone()));
9399 for source_name in names.into_iter().skip(1) {
9400 let mut suffix = 2;
9401 let replacement = loop {
9402 let candidate = format!("{rust_name}{suffix}");
9403 if claimed_rust_names.insert(candidate.clone()) {
9404 break candidate;
9405 }
9406 suffix += 1;
9407 };
9408
9409 aliases.insert(source_name, replacement);
9410 }
9411 }
9412
9413 aliases
9414}
9415
9416fn disambiguate_component_schema_names(openapi_spec: &mut Value) {
9417 let aliases = component_schema_name_aliases(openapi_spec);
9418 if aliases.is_empty() {
9419 return;
9420 }
9421
9422 for (source_name, replacement) in &aliases {
9423 let rust_name = crate::generator::rust_type_name(source_name);
9424 eprintln!(
9425 "⚠️ schema `{source_name}` maps to the existing Rust type `{rust_name}` — disambiguated to `{replacement}`"
9426 );
9427 }
9428
9429 let Some(schemas) = openapi_spec
9430 .pointer_mut("/components/schemas")
9431 .and_then(Value::as_object_mut)
9432 else {
9433 return;
9434 };
9435
9436 let original_schemas = std::mem::take(schemas);
9437 for (name, schema) in original_schemas {
9438 schemas.insert(aliases.get(&name).cloned().unwrap_or(name), schema);
9439 }
9440
9441 rewrite_component_schema_references(openapi_spec, &aliases);
9442}
9443
9444fn disambiguate_analyzed_schema_names(
9445 analysis: &mut SchemaAnalysis,
9446 component_schemas: &BTreeMap<String, Schema>,
9447) {
9448 let mut names_by_rust_name = BTreeMap::<String, Vec<String>>::new();
9449 for name in analysis.schemas.keys() {
9450 names_by_rust_name
9451 .entry(crate::generator::rust_type_name(name))
9452 .or_default()
9453 .push(name.clone());
9454 }
9455
9456 let mut claimed_rust_names = names_by_rust_name.keys().cloned().collect::<HashSet<_>>();
9457 let mut aliases = BTreeMap::<String, String>::new();
9458
9459 for (rust_name, mut names) in names_by_rust_name {
9460 if names.len() < 2 {
9461 continue;
9462 }
9463 names.sort_by_key(|name| {
9464 (
9465 !component_schemas.contains_key(name),
9466 name != &rust_name,
9467 name.clone(),
9468 )
9469 });
9470
9471 for source_name in names.into_iter().skip(1) {
9472 let mut suffix = 2;
9473 let replacement = loop {
9474 let candidate = format!("{rust_name}{suffix}");
9475 if claimed_rust_names.insert(candidate.clone()) {
9476 break candidate;
9477 }
9478 suffix += 1;
9479 };
9480 eprintln!(
9481 "⚠️ generated schema `{source_name}` maps to the existing Rust type `{rust_name}` — disambiguated to `{replacement}`"
9482 );
9483 aliases.insert(source_name, replacement);
9484 }
9485 }
9486
9487 if aliases.is_empty() {
9488 return;
9489 }
9490
9491 let original_schemas = std::mem::take(&mut analysis.schemas);
9492 for (name, mut schema) in original_schemas {
9493 schema.name = renamed_schema_name(&schema.name, &aliases);
9494 schema.dependencies = schema
9495 .dependencies
9496 .into_iter()
9497 .map(|name| renamed_schema_name(&name, &aliases))
9498 .collect();
9499 rewrite_schema_type_names(&mut schema.schema_type, &aliases);
9500 analysis
9501 .schemas
9502 .insert(renamed_schema_name(&name, &aliases), schema);
9503 }
9504
9505 let original_edges = std::mem::take(&mut analysis.dependencies.edges);
9506 for (name, dependencies) in original_edges {
9507 analysis.dependencies.edges.insert(
9508 renamed_schema_name(&name, &aliases),
9509 dependencies
9510 .into_iter()
9511 .map(|name| renamed_schema_name(&name, &aliases))
9512 .collect(),
9513 );
9514 }
9515 analysis.dependencies.recursive_schemas = analysis
9516 .dependencies
9517 .recursive_schemas
9518 .iter()
9519 .map(|name| renamed_schema_name(name, &aliases))
9520 .collect();
9521
9522 analysis.patterns.tagged_enum_schemas = analysis
9523 .patterns
9524 .tagged_enum_schemas
9525 .iter()
9526 .map(|name| renamed_schema_name(name, &aliases))
9527 .collect();
9528 analysis.patterns.untagged_enum_schemas = analysis
9529 .patterns
9530 .untagged_enum_schemas
9531 .iter()
9532 .map(|name| renamed_schema_name(name, &aliases))
9533 .collect();
9534 analysis.patterns.type_mappings = std::mem::take(&mut analysis.patterns.type_mappings)
9535 .into_iter()
9536 .map(|(name, mappings)| {
9537 (
9538 renamed_schema_name(&name, &aliases),
9539 mappings
9540 .into_iter()
9541 .map(|(value, schema_name)| {
9542 (value, renamed_schema_name(&schema_name, &aliases))
9543 })
9544 .collect(),
9545 )
9546 })
9547 .collect();
9548
9549 for operation in analysis.operations.values_mut() {
9550 if let Some(request_body) = &mut operation.request_body {
9551 rewrite_request_body_schema_name(request_body, &aliases);
9552 }
9553 for schema_name in operation.response_schemas.values_mut() {
9554 *schema_name = renamed_schema_name(schema_name, &aliases);
9555 }
9556 for parameter in &mut operation.parameters {
9557 if let Some(schema_name) = &mut parameter.schema_ref {
9558 *schema_name = renamed_schema_name(schema_name, &aliases);
9559 }
9560 if let Some(serialization) = &mut parameter.query_serialization {
9561 rewrite_query_serialization_schema_names(serialization, &aliases);
9562 }
9563 }
9564 }
9565
9566 for responses in analysis.operation_responses.values_mut() {
9567 for response in responses.values_mut() {
9568 if let Some(schema_name) = &mut response.schema_name {
9569 *schema_name = renamed_schema_name(schema_name, &aliases);
9570 }
9571 for representation in response.representations.values_mut() {
9572 if let OperationRepresentation::Json { schema_name } = representation {
9573 *schema_name = renamed_schema_name(schema_name, &aliases);
9574 }
9575 }
9576 if let Some(OperationResponseBody::Json { schema_name, .. }) = &mut response.body {
9577 *schema_name = renamed_schema_name(schema_name, &aliases);
9578 }
9579 }
9580 }
9581}
9582
9583fn renamed_schema_name(name: &str, aliases: &BTreeMap<String, String>) -> String {
9584 aliases
9585 .get(name)
9586 .cloned()
9587 .unwrap_or_else(|| name.to_string())
9588}
9589
9590fn rewrite_schema_type_names(schema_type: &mut SchemaType, aliases: &BTreeMap<String, String>) {
9591 match schema_type {
9592 SchemaType::Object {
9593 properties,
9594 additional_properties,
9595 ..
9596 } => {
9597 for property in properties.values_mut() {
9598 rewrite_schema_type_names(&mut property.schema_type, aliases);
9599 }
9600 if let ObjectAdditionalProperties::Typed { value_type } = additional_properties {
9601 rewrite_schema_type_names(value_type, aliases);
9602 }
9603 }
9604 SchemaType::DiscriminatedUnion { variants, .. } => {
9605 for variant in variants {
9606 variant.type_name = renamed_schema_name(&variant.type_name, aliases);
9607 variant.schema_ref = renamed_schema_name(&variant.schema_ref, aliases);
9608 }
9609 }
9610 SchemaType::Union { variants, .. } | SchemaType::Composition { schemas: variants } => {
9611 for variant in variants {
9612 variant.target = renamed_schema_name(&variant.target, aliases);
9613 }
9614 }
9615 SchemaType::Array { item_type } => rewrite_schema_type_names(item_type, aliases),
9616 SchemaType::Nullable { inner_type } => rewrite_schema_type_names(inner_type, aliases),
9617 SchemaType::Untyped { .. } => {}
9618 SchemaType::Tuple { element_types } => {
9619 for element_type in element_types {
9620 rewrite_schema_type_names(element_type, aliases);
9621 }
9622 }
9623 SchemaType::Reference { target } => {
9624 *target = renamed_schema_name(target, aliases);
9625 }
9626 SchemaType::Primitive { .. }
9627 | SchemaType::StringEnum { .. }
9628 | SchemaType::ExtensibleEnum { .. } => {}
9629 }
9630}
9631
9632fn rewrite_request_body_schema_name(
9633 request_body: &mut RequestBodyContent,
9634 aliases: &BTreeMap<String, String>,
9635) {
9636 match request_body {
9637 RequestBodyContent::Json { schema_name, .. }
9638 | RequestBodyContent::FormUrlEncoded { schema_name, .. }
9639 | RequestBodyContent::Multipart { schema_name, .. } => {
9640 *schema_name = renamed_schema_name(schema_name, aliases);
9641 }
9642 _ => {}
9643 }
9644}
9645
9646fn rewrite_query_serialization_schema_names(
9647 serialization: &mut QuerySerialization,
9648 aliases: &BTreeMap<String, String>,
9649) {
9650 match serialization {
9651 QuerySerialization::FormExplodedArray { item_type }
9652 | QuerySerialization::FormArray { item_type }
9653 | QuerySerialization::SimpleHeaderArray { item_type } => {
9654 rewrite_array_item_type_schema_names(item_type, aliases);
9655 }
9656 QuerySerialization::FormExplodedNestedObject { properties } => {
9657 for property in properties {
9658 rewrite_query_property_type_schema_names(&mut property.value_type, aliases);
9659 }
9660 }
9661 _ => {}
9662 }
9663}
9664
9665fn rewrite_array_item_type_schema_names(
9666 item_type: &mut ArrayItemType,
9667 aliases: &BTreeMap<String, String>,
9668) {
9669 match item_type {
9670 ArrayItemType::SchemaRef(name) => *name = renamed_schema_name(name, aliases),
9671 ArrayItemType::FlatStructRef {
9672 schema_name,
9673 properties,
9674 }
9675 | ArrayItemType::NestedStructRef {
9676 schema_name,
9677 properties,
9678 } => {
9679 *schema_name = renamed_schema_name(schema_name, aliases);
9680 for property in properties {
9681 rewrite_query_property_type_schema_names(&mut property.value_type, aliases);
9682 }
9683 }
9684 ArrayItemType::Scalar(_) => {}
9685 }
9686}
9687
9688fn rewrite_query_property_type_schema_names(
9689 property_type: &mut QueryStructPropertyType,
9690 aliases: &BTreeMap<String, String>,
9691) {
9692 match property_type {
9693 QueryStructPropertyType::Array { item_type } => {
9694 rewrite_array_item_type_schema_names(item_type, aliases)
9695 }
9696 QueryStructPropertyType::Object { properties } => {
9697 for property in properties {
9698 rewrite_query_property_type_schema_names(&mut property.value_type, aliases);
9699 }
9700 }
9701 QueryStructPropertyType::Scalar(_) => {}
9702 }
9703}
9704
9705fn rewrite_component_schema_references(value: &mut Value, aliases: &BTreeMap<String, String>) {
9706 match value {
9707 Value::Array(values) => {
9708 for value in values {
9709 rewrite_component_schema_references(value, aliases);
9710 }
9711 }
9712 Value::Object(object) => {
9713 if let Some(Value::String(reference)) = object.get_mut("$ref") {
9714 rewrite_component_schema_reference(reference, aliases);
9715 }
9716
9717 if let Some(Value::Object(mapping)) = object.get_mut("mapping") {
9718 for target_value in mapping.values_mut() {
9719 let Some(target) = target_value.as_str() else {
9720 continue;
9721 };
9722 let replacement = aliases.get(target).cloned().or_else(|| {
9723 let mut target = target.to_string();
9724 rewrite_component_schema_reference(&mut target, aliases).then_some(target)
9725 });
9726 if let Some(replacement) = replacement {
9727 *target_value = Value::String(replacement);
9728 }
9729 }
9730 }
9731
9732 for value in object.values_mut() {
9733 rewrite_component_schema_references(value, aliases);
9734 }
9735 }
9736 _ => {}
9737 }
9738}
9739
9740fn rewrite_component_schema_reference(
9741 reference: &mut String,
9742 aliases: &BTreeMap<String, String>,
9743) -> bool {
9744 const PREFIX: &str = "#/components/schemas/";
9745 let Some(encoded_name) = reference.strip_prefix(PREFIX) else {
9746 return false;
9747 };
9748 let encoded_name = encoded_name.split('/').next().unwrap_or(encoded_name);
9749
9750 for (source, replacement) in aliases {
9751 let encoded_source = source.replace('~', "~0").replace('/', "~1");
9752 if encoded_name == encoded_source {
9753 reference.replace_range(
9754 PREFIX.len()..PREFIX.len() + encoded_source.len(),
9755 replacement,
9756 );
9757 return true;
9758 }
9759 }
9760
9761 false
9762}