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 pub multipart_file_type: Option<String>,
124}
125
126fn collect_untagged_union_branches(analysis: &SchemaAnalysis) -> BTreeSet<String> {
133 let mut branches = BTreeSet::new();
134 for schema in analysis.schemas.values() {
135 let SchemaType::Union { variants, .. } = &schema.schema_type else {
136 continue;
137 };
138 for variant in variants {
139 let mut target = variant.target.clone();
140 let mut seen = BTreeSet::new();
143 while seen.insert(target.clone()) {
144 branches.insert(target.clone());
145 match analysis.schemas.get(&target).map(|s| &s.schema_type) {
146 Some(SchemaType::Reference { target: next }) => target = next.clone(),
147 _ => break,
148 }
149 }
150 }
151 }
152 branches
153}
154
155impl SchemaType {
156 pub fn renders_inline(&self) -> bool {
167 match self {
168 Self::Primitive { .. }
169 | Self::Reference { .. }
170 | Self::Array { .. }
171 | Self::Tuple { .. }
172 | Self::Nullable { .. }
173 | Self::Untyped { .. } => true,
174 Self::Object { .. }
175 | Self::StringEnum { .. }
176 | Self::ExtensibleEnum { .. }
177 | Self::DiscriminatedUnion { .. }
178 | Self::Union { .. }
179 | Self::Composition { .. } => false,
180 }
181 }
182}
183
184impl UntypedReason {
185 pub fn inline_drop(schema_type: &SchemaType) -> Option<Self> {
188 match schema_type {
189 SchemaType::Composition { .. } => Some(Self::InlineCompositionDropped),
190 SchemaType::Union { .. } | SchemaType::DiscriminatedUnion { .. } => {
191 Some(Self::InlineUnionDropped)
192 }
193 SchemaType::Object { .. } => Some(Self::InlineObjectDropped),
194 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. } => {
195 Some(Self::InlineEnumDropped)
196 }
197 _ => None,
198 }
199 }
200}
201
202fn schema_type_dependencies(schema_type: &SchemaType) -> HashSet<String> {
212 let mut targets = HashSet::new();
213 collect_type_dependencies(schema_type, &mut targets, 0);
214 targets
215}
216
217fn collect_type_dependencies(
218 schema_type: &SchemaType,
219 targets: &mut HashSet<String>,
220 depth: usize,
221) {
222 if depth > UNTYPED_WALK_DEPTH {
223 return;
224 }
225 match schema_type {
226 SchemaType::Reference { target } => {
227 targets.insert(target.clone());
228 }
229 SchemaType::Array { item_type } => collect_type_dependencies(item_type, targets, depth + 1),
230 SchemaType::Nullable { inner_type } => {
231 collect_type_dependencies(inner_type, targets, depth + 1)
232 }
233 SchemaType::Tuple { element_types } => {
234 for element_type in element_types {
235 collect_type_dependencies(element_type, targets, depth + 1);
236 }
237 }
238 SchemaType::Object {
239 properties,
240 additional_properties,
241 ..
242 } => {
243 for property in properties.values() {
244 collect_type_dependencies(&property.schema_type, targets, depth + 1);
245 }
246 if let ObjectAdditionalProperties::Typed { value_type } = additional_properties {
247 collect_type_dependencies(value_type, targets, depth + 1);
248 }
249 }
250 SchemaType::Union { variants, .. } | SchemaType::Composition { schemas: variants } => {
251 for variant in variants {
252 targets.insert(variant.target.clone());
253 }
254 }
255 SchemaType::DiscriminatedUnion { variants, .. } => {
256 for variant in variants {
257 targets.insert(variant.type_name.clone());
258 }
259 }
260 SchemaType::Primitive { .. }
261 | SchemaType::StringEnum { .. }
262 | SchemaType::ExtensibleEnum { .. }
263 | SchemaType::Untyped { .. } => {}
264 }
265}
266
267fn normalize_untyped(schema_type: &mut SchemaType, depth: usize) {
277 if depth > UNTYPED_WALK_DEPTH {
278 return;
279 }
280 match schema_type {
281 SchemaType::Primitive { rust_type, .. } => {
282 let shape = match rust_type.as_str() {
283 "serde_json::Value" => Some(UntypedShape::Value),
284 "Vec<serde_json::Value>" => Some(UntypedShape::ValueArray),
285 _ => None,
286 };
287 if let Some(shape) = shape {
288 *schema_type = SchemaType::Untyped {
289 shape,
290 reason: UntypedReason::Unclassified,
291 };
292 }
293 }
294 SchemaType::Object {
295 properties,
296 additional_properties,
297 ..
298 } => {
299 for property in properties.values_mut() {
300 normalize_untyped(&mut property.schema_type, depth + 1);
301 }
302 if let ObjectAdditionalProperties::Typed { value_type } = additional_properties {
303 normalize_untyped(value_type, depth + 1);
304 }
305 }
306 SchemaType::Array { item_type } => normalize_untyped(item_type, depth + 1),
307 SchemaType::Nullable { inner_type } => normalize_untyped(inner_type, depth + 1),
308 SchemaType::Tuple { element_types } => {
309 for element_type in element_types {
310 normalize_untyped(element_type, depth + 1);
311 }
312 }
313 SchemaType::Untyped { .. }
314 | SchemaType::StringEnum { .. }
315 | SchemaType::ExtensibleEnum { .. }
316 | SchemaType::DiscriminatedUnion { .. }
317 | SchemaType::Union { .. }
318 | SchemaType::Composition { .. }
319 | SchemaType::Reference { .. } => {}
320 }
321}
322
323impl SchemaAnalysis {
324 pub fn untyped_fields(&self) -> Vec<UntypedFinding> {
331 let mut findings = Vec::new();
332 for (name, schema) in &self.schemas {
333 collect_untyped(&schema.schema_type, name, &mut findings, 0);
334 }
335 findings.sort();
336 findings
337 }
338}
339
340const UNTYPED_WALK_DEPTH: usize = 32;
344
345fn collect_untyped(
346 schema_type: &SchemaType,
347 context: &str,
348 findings: &mut Vec<UntypedFinding>,
349 depth: usize,
350) {
351 if depth > UNTYPED_WALK_DEPTH {
352 return;
353 }
354 match schema_type {
355 SchemaType::Untyped { shape, reason } => findings.push(UntypedFinding {
356 context: context.to_string(),
357 shape: *shape,
358 reason: *reason,
359 }),
360 SchemaType::Object {
361 properties,
362 additional_properties,
363 ..
364 } => {
365 for (property_name, property) in properties {
366 let property_context = format!("{context}.{property_name}");
367 if let Some(reason) = UntypedReason::inline_drop(&property.schema_type) {
371 findings.push(UntypedFinding {
372 context: property_context,
373 shape: UntypedShape::Value,
374 reason,
375 });
376 continue;
377 }
378 collect_untyped(
379 &property.schema_type,
380 &property_context,
381 findings,
382 depth + 1,
383 );
384 }
385 match additional_properties {
386 ObjectAdditionalProperties::Untyped => findings.push(UntypedFinding {
387 context: format!("{context}.<additionalProperties>"),
388 shape: UntypedShape::ValueMap,
389 reason: UntypedReason::UntypedAdditionalProperties,
390 }),
391 ObjectAdditionalProperties::Typed { value_type } => collect_untyped(
392 value_type,
393 &format!("{context}.<additionalProperties>"),
394 findings,
395 depth + 1,
396 ),
397 ObjectAdditionalProperties::Denied | ObjectAdditionalProperties::Closed => {}
398 }
399 }
400 SchemaType::Array { item_type } => {
401 let element_context = format!("{context}[]");
402 if let Some(reason) = UntypedReason::inline_drop(item_type) {
403 findings.push(UntypedFinding {
404 context: element_context,
405 shape: UntypedShape::Value,
406 reason,
407 });
408 } else {
409 collect_untyped(item_type, &element_context, findings, depth + 1);
410 }
411 }
412 SchemaType::Nullable { inner_type } => {
413 collect_untyped(inner_type, context, findings, depth + 1)
414 }
415 SchemaType::Tuple { element_types } => {
416 for (index, element_type) in element_types.iter().enumerate() {
417 collect_untyped(
418 element_type,
419 &format!("{context}[{index}]"),
420 findings,
421 depth + 1,
422 );
423 }
424 }
425 SchemaType::Union { variants, .. } | SchemaType::Composition { schemas: variants } => {
428 for (index, variant) in variants.iter().enumerate() {
429 if let Some(shape) = untyped_shape_of(&variant.target) {
430 findings.push(UntypedFinding {
431 context: format!("{context}|{index}"),
432 shape,
433 reason: UntypedReason::UntypedUnionBranch,
434 });
435 }
436 }
437 }
438 SchemaType::Primitive { .. }
439 | SchemaType::StringEnum { .. }
440 | SchemaType::ExtensibleEnum { .. }
441 | SchemaType::DiscriminatedUnion { .. }
442 | SchemaType::Reference { .. } => {}
443 }
444}
445
446fn untyped_shape_of(rust_type: &str) -> Option<UntypedShape> {
448 match rust_type {
449 "serde_json::Value" => Some(UntypedShape::Value),
450 "Vec<serde_json::Value>" => Some(UntypedShape::ValueArray),
451 _ => None,
452 }
453}
454
455#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
458pub struct UntypedFinding {
459 pub context: String,
462 pub shape: UntypedShape,
464 pub reason: UntypedReason,
466}
467
468#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
470#[serde(rename_all = "snake_case")]
471pub enum UntypedShape {
472 Value,
474 ValueArray,
476 ValueMap,
478}
479
480#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
488#[serde(rename_all = "kebab-case")]
489pub enum UntypedReason {
490 AnySchema,
493 OpaqueObject,
496 UntypedAdditionalProperties,
498 ArrayWithoutItems,
500 OpenPositionalItems,
503 UnrepresentableUnion,
506 UnrepresentableComposition,
508 UnsupportedTypeKeyword,
510 UnresolvedReference,
512 InlineCompositionDropped,
517 InlineUnionDropped,
519 InlineObjectDropped,
521 InlineEnumDropped,
523 UntypedUnionBranch,
527 NeverMatches,
531 Unclassified,
534}
535
536impl UntypedReason {
537 pub fn verdict(self) -> UntypedVerdict {
540 match self {
541 Self::AnySchema | Self::OpaqueObject | Self::UntypedAdditionalProperties => {
543 UntypedVerdict::Faithful
544 }
545 Self::NeverMatches => UntypedVerdict::Faithful,
547 Self::ArrayWithoutItems | Self::OpenPositionalItems => UntypedVerdict::Faithful,
551 Self::InlineCompositionDropped
554 | Self::InlineUnionDropped
555 | Self::InlineObjectDropped
556 | Self::InlineEnumDropped => UntypedVerdict::Recoverable,
557 Self::UnrepresentableUnion
559 | Self::UnrepresentableComposition
560 | Self::UnsupportedTypeKeyword
561 | Self::UnresolvedReference => UntypedVerdict::Recoverable,
562 Self::UntypedUnionBranch | Self::Unclassified => UntypedVerdict::Unknown,
563 }
564 }
565}
566
567#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
569#[serde(rename_all = "snake_case")]
570pub enum UntypedVerdict {
571 Faithful,
573 Recoverable,
575 Unknown,
577}
578
579#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize)]
581pub struct OperationResponse {
582 pub schema_name: Option<String>,
584 pub media_type: Option<String>,
586 pub body: Option<OperationResponseBody>,
590 pub supports_streaming: bool,
592 pub has_content: bool,
594 pub unsupported_media_types: Vec<String>,
596 pub representations: BTreeMap<String, OperationRepresentation>,
598}
599
600#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
602pub struct OperationSource {
603 pub json_pointer: String,
604 pub method: String,
605 pub path: String,
606 pub operation_id: Option<String>,
607 pub webhook: bool,
608}
609
610#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
611#[serde(tag = "kind", rename_all = "snake_case")]
612pub enum OperationRepresentation {
613 Json { schema_name: String },
614 Text,
615 Binary { wildcard: bool },
616 EventStream,
617}
618
619#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
624#[serde(tag = "kind", rename_all = "snake_case")]
625pub enum OperationResponseBody {
626 Json {
627 schema_name: String,
628 media_type: String,
629 },
630 Text {
631 media_type: String,
632 },
633 Binary {
634 media_type: String,
635 wildcard: bool,
636 },
637}
638
639#[derive(Debug, Clone, Default)]
640pub struct ValidationContext {
641 pub openapi_version: String,
642 pub json_schema_dialect: Option<String>,
643 pub component_schemas: BTreeMap<String, Value>,
644}
645
646#[derive(Debug, Clone, Default)]
651pub struct EnumExtensions {
652 pub varnames: Vec<String>,
657 pub descriptions: Vec<String>,
659}
660
661#[derive(Debug, Clone)]
662pub struct AnalyzedSchema {
663 pub name: String,
664 pub original: Value,
665 pub schema_type: SchemaType,
666 pub dependencies: HashSet<String>,
667 pub nullable: bool,
668 pub description: Option<String>,
669 pub default: Option<serde_json::Value>,
670}
671
672#[derive(Debug, Clone)]
673pub enum SchemaType {
674 Primitive {
680 rust_type: String,
681 serde_with: Option<String>,
682 },
683 Object {
685 properties: BTreeMap<String, PropertyInfo>,
686 required: HashSet<String>,
687 additional_properties: ObjectAdditionalProperties,
688 variant: Option<SchemaRef>,
693 },
694 DiscriminatedUnion {
696 discriminator_field: String,
697 variants: Vec<UnionVariant>,
698 exclusive: bool,
702 },
703 Union {
707 variants: Vec<SchemaRef>,
708 exclusive: bool,
709 },
710 Array { item_type: Box<SchemaType> },
712 Nullable { inner_type: Box<SchemaType> },
717 Tuple { element_types: Vec<SchemaType> },
723 StringEnum { values: Vec<String> },
725 ExtensibleEnum { known_values: Vec<String> },
727 Composition { schemas: Vec<SchemaRef> },
729 Reference { target: String },
731 Untyped {
738 shape: UntypedShape,
739 reason: UntypedReason,
740 },
741}
742
743#[derive(Debug, Clone)]
748pub enum ObjectAdditionalProperties {
749 Denied,
754 Closed,
760 Untyped,
763 Typed { value_type: Box<SchemaType> },
766}
767
768impl ObjectAdditionalProperties {
769 pub fn is_open(&self) -> bool {
772 !matches!(self, Self::Denied | Self::Closed)
773 }
774
775 pub fn denies_unknown_keys(&self) -> bool {
779 matches!(self, Self::Denied)
780 }
781}
782
783#[derive(Debug, Clone)]
784pub struct PropertyInfo {
785 pub schema_type: SchemaType,
786 pub nullable: bool,
787 pub description: Option<String>,
788 pub default: Option<serde_json::Value>,
789 pub serde_attrs: Vec<String>,
790 pub synthesized_required: bool,
795 pub constraints: PropertyConstraints,
800}
801
802#[derive(Debug, Clone, Default)]
807pub struct PropertyConstraints {
808 pub minimum: Option<f64>,
809 pub maximum: Option<f64>,
810 pub exclusive_minimum: Option<f64>,
811 pub exclusive_maximum: Option<f64>,
812 pub multiple_of: Option<f64>,
813 pub min_length: Option<u64>,
814 pub max_length: Option<u64>,
815 pub pattern: Option<String>,
816 pub min_items: Option<u64>,
817 pub max_items: Option<u64>,
818 pub unique_items: Option<bool>,
819}
820
821impl PropertyConstraints {
822 pub fn is_empty(&self) -> bool {
823 self.minimum.is_none()
824 && self.maximum.is_none()
825 && self.exclusive_minimum.is_none()
826 && self.exclusive_maximum.is_none()
827 && self.multiple_of.is_none()
828 && self.min_length.is_none()
829 && self.max_length.is_none()
830 && self.pattern.is_none()
831 && self.min_items.is_none()
832 && self.max_items.is_none()
833 && self.unique_items.is_none()
834 }
835
836 pub fn from_schema_details(details: &crate::openapi::SchemaDetails) -> Self {
841 use crate::openapi::ExclusiveBound;
842 let exclusive_minimum = match &details.exclusive_minimum {
843 Some(ExclusiveBound::Number(v)) => Some(*v),
844 _ => None,
845 };
846 let exclusive_maximum = match &details.exclusive_maximum {
847 Some(ExclusiveBound::Number(v)) => Some(*v),
848 _ => None,
849 };
850 Self {
851 minimum: details
852 .minimum
853 .as_ref()
854 .and_then(serde_json::Number::as_f64),
855 maximum: details
856 .maximum
857 .as_ref()
858 .and_then(serde_json::Number::as_f64),
859 exclusive_minimum,
860 exclusive_maximum,
861 multiple_of: details.multiple_of,
862 min_length: details.min_length,
863 max_length: details.max_length,
864 pattern: details.pattern.clone(),
865 min_items: details.min_items,
866 max_items: details.max_items,
867 unique_items: details.unique_items,
868 }
869 }
870}
871
872#[derive(Debug, Clone)]
873pub struct UnionVariant {
874 pub rust_name: String,
875 pub type_name: String,
876 pub discriminator_value: String,
880 pub discriminator_values: Vec<String>,
884 pub preferred_discriminator_values: Vec<String>,
889 pub discriminator_field_declared: bool,
893 pub discriminator_field_required: bool,
897 pub schema_ref: String,
898}
899
900#[derive(Debug, Clone)]
901pub struct SchemaRef {
902 pub target: String,
903 pub nullable: bool,
904}
905
906#[derive(Debug, Clone)]
907pub struct DependencyGraph {
908 pub edges: BTreeMap<String, HashSet<String>>,
909 pub recursive_schemas: HashSet<String>,
911}
912
913#[derive(Debug, Clone)]
914pub struct DetectedPatterns {
915 pub tagged_enum_schemas: HashSet<String>,
917 pub untagged_enum_schemas: HashSet<String>,
919 pub type_mappings: BTreeMap<String, BTreeMap<String, String>>,
921}
922
923#[derive(Debug, Clone, Default, serde::Serialize)]
925pub struct OperationInfo {
926 pub operation_id: String,
928 pub method: String,
930 pub path: String,
932 pub summary: Option<String>,
934 pub description: Option<String>,
936 pub request_body: Option<RequestBodyContent>,
938 pub request_body_required: bool,
941 pub response_schemas: BTreeMap<String, String>,
943 pub parameters: Vec<ParameterInfo>,
945 pub supports_streaming: bool,
947 pub stream_parameter: Option<String>,
949 pub tags: Vec<String>,
953 pub deprecated: bool,
956}
957
958#[derive(Debug, Clone, serde::Serialize)]
960#[serde(tag = "kind")]
961pub enum RequestBodyContent {
962 Json {
963 schema_name: String,
964 media_type: String,
965 #[serde(skip)]
966 validation_schema: Value,
967 },
968 FormUrlEncoded {
969 schema_name: String,
970 media_type: String,
971 #[serde(skip)]
972 validation_schema: Value,
973 },
974 Multipart {
975 schema_name: String,
976 media_type: String,
977 #[serde(skip)]
978 validation_schema: Value,
979 #[serde(skip)]
983 encoding: BTreeMap<String, String>,
984 },
985 OctetStream {
986 media_type: String,
987 },
988 Binary {
989 media_type: String,
990 },
991 TextPlain {
992 media_type: String,
993 },
994 SchemaLess {
998 media_type: String,
999 },
1000 Unsupported {
1001 media_types: Vec<String>,
1002 },
1003}
1004
1005impl RequestBodyContent {
1006 pub fn schema_name(&self) -> Option<&str> {
1008 match self {
1009 Self::Json { schema_name, .. }
1010 | Self::FormUrlEncoded { schema_name, .. }
1011 | Self::Multipart { schema_name, .. } => Some(schema_name),
1012 Self::OctetStream { .. }
1013 | Self::Binary { .. }
1014 | Self::TextPlain { .. }
1015 | Self::SchemaLess { .. }
1016 | Self::Unsupported { .. } => None,
1017 }
1018 }
1019}
1020
1021fn base_param_ident(name: &str) -> String {
1025 use heck::ToSnakeCase;
1026 let suffix = if name.ends_with("<=") {
1027 "_lte"
1028 } else if name.ends_with(">=") {
1029 "_gte"
1030 } else if name.ends_with('<') {
1031 "_lt"
1032 } else if name.ends_with('>') {
1033 "_gt"
1034 } else {
1035 ""
1036 };
1037 let stripped = name.trim_end_matches(['<', '>', '=']);
1038 let mut snake = stripped.to_snake_case();
1039 if snake.is_empty() {
1040 snake.push_str("parameter");
1041 } else if snake.starts_with(|character: char| character.is_ascii_digit()) {
1042 snake.insert(0, '_');
1043 }
1044 snake.push_str(suffix);
1045 snake
1046}
1047
1048#[derive(Debug, Clone, serde::Serialize)]
1050pub struct ParameterInfo {
1051 pub name: String,
1053 pub location: String,
1055 pub required: bool,
1057 pub schema_ref: Option<String>,
1059 pub rust_type: String,
1061 pub description: Option<String>,
1063 #[serde(skip_serializing_if = "Option::is_none")]
1069 pub enum_values: Option<Vec<String>>,
1070 #[serde(skip_serializing_if = "Option::is_none")]
1076 pub enum_varnames: Option<Vec<String>>,
1077 #[serde(skip_serializing_if = "Option::is_none")]
1085 pub rust_ident: Option<String>,
1086 #[serde(skip_serializing_if = "Option::is_none")]
1095 pub query_serialization: Option<QuerySerialization>,
1096 #[serde(skip_serializing_if = "std::ops::Not::not")]
1100 pub allow_reserved: bool,
1101 #[serde(skip)]
1104 pub validation_schema: Option<Value>,
1105}
1106
1107#[derive(Debug, Clone, PartialEq, serde::Serialize)]
1110pub enum QuerySerialization {
1111 FormExplodedObject,
1115 FormExplodedNestedObject {
1121 properties: Vec<QueryStructProperty>,
1122 },
1123 FormObject,
1126 DeepObject,
1129 FormExplodedArray { item_type: ArrayItemType },
1132 FormArray { item_type: ArrayItemType },
1135 SimpleHeaderArray { item_type: ArrayItemType },
1138 Unsupported { reason: String },
1143}
1144
1145#[derive(Debug, Clone, PartialEq, serde::Serialize)]
1152pub enum ArrayItemType {
1153 Scalar(String),
1155 SchemaRef(String),
1157 FlatStructRef {
1163 schema_name: String,
1164 properties: Vec<QueryStructProperty>,
1165 },
1166 NestedStructRef {
1170 schema_name: String,
1171 properties: Vec<QueryStructProperty>,
1172 },
1173}
1174
1175#[derive(Debug, Clone, PartialEq, serde::Serialize)]
1176pub struct QueryStructProperty {
1177 pub wire_name: String,
1178 pub required: bool,
1179 pub value_type: QueryStructPropertyType,
1180}
1181
1182#[derive(Debug, Clone, PartialEq, serde::Serialize)]
1183pub enum QueryStructPropertyType {
1184 Scalar(QueryScalarType),
1185 Array {
1186 item_type: ArrayItemType,
1187 },
1188 Object {
1189 properties: Vec<QueryStructProperty>,
1190 },
1191}
1192
1193#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
1194pub enum QueryScalarType {
1195 String,
1196 Integer,
1197 Number,
1198 Boolean,
1199}
1200
1201impl Default for DependencyGraph {
1202 fn default() -> Self {
1203 Self::new()
1204 }
1205}
1206
1207impl DependencyGraph {
1208 pub fn new() -> Self {
1209 Self {
1210 edges: BTreeMap::new(),
1211 recursive_schemas: HashSet::new(),
1212 }
1213 }
1214
1215 pub fn add_dependency(&mut self, from: String, to: String) {
1216 self.edges.entry(from).or_default().insert(to);
1217 }
1218
1219 pub fn topological_sort(&mut self) -> Result<Vec<String>> {
1221 self.detect_recursive_schemas();
1223
1224 let mut temp_edges = self.edges.clone();
1226 for (schema, deps) in &mut temp_edges {
1227 deps.remove(schema); }
1229
1230 let mut visited = HashSet::new();
1231 let mut temp_visited = HashSet::new();
1232 let mut result = Vec::new();
1233
1234 let mut all_nodes: Vec<_> = temp_edges.keys().collect();
1236 all_nodes.sort();
1237 for node in all_nodes {
1238 if !visited.contains(node) {
1239 self.visit_node_recursive(
1240 node,
1241 &temp_edges,
1242 &mut visited,
1243 &mut temp_visited,
1244 &mut result,
1245 )?;
1246 }
1247 }
1248
1249 result.reverse();
1250 Ok(result)
1251 }
1252
1253 fn detect_recursive_schemas(&mut self) {
1254 for (schema, deps) in &self.edges {
1255 if deps.contains(schema) {
1256 self.recursive_schemas.insert(schema.clone());
1258 } else {
1259 if self.has_cycle_from(schema, schema, &mut HashSet::new()) {
1261 self.recursive_schemas.insert(schema.clone());
1262 }
1263 }
1264 }
1265
1266 for (schema, deps) in &self.edges {
1268 for dep in deps {
1269 if let Some(dep_deps) = self.edges.get(dep) {
1270 if dep_deps.contains(schema) {
1271 self.recursive_schemas.insert(schema.clone());
1273 self.recursive_schemas.insert(dep.clone());
1274 }
1275 }
1276 }
1277 }
1278 }
1279
1280 fn has_cycle_from(&self, start: &str, current: &str, visited: &mut HashSet<String>) -> bool {
1281 if visited.contains(current) {
1282 return false; }
1284
1285 visited.insert(current.to_string());
1286
1287 if let Some(deps) = self.edges.get(current) {
1288 for dep in deps {
1289 if dep == start {
1290 return true; }
1292 if self.has_cycle_from(start, dep, visited) {
1293 return true;
1294 }
1295 }
1296 }
1297
1298 false
1299 }
1300
1301 #[allow(clippy::only_used_in_recursion)]
1302 fn visit_node_recursive(
1303 &self,
1304 node: &str,
1305 temp_edges: &BTreeMap<String, HashSet<String>>,
1306 visited: &mut HashSet<String>,
1307 temp_visited: &mut HashSet<String>,
1308 result: &mut Vec<String>,
1309 ) -> Result<()> {
1310 if temp_visited.contains(node) {
1311 return Ok(());
1313 }
1314
1315 if visited.contains(node) {
1316 return Ok(());
1317 }
1318
1319 temp_visited.insert(node.to_string());
1320
1321 if let Some(dependencies) = temp_edges.get(node) {
1322 let mut sorted_deps: Vec<_> = dependencies.iter().collect();
1324 sorted_deps.sort();
1325 for dep in sorted_deps {
1326 self.visit_node_recursive(dep, temp_edges, visited, temp_visited, result)?;
1327 }
1328 }
1329
1330 temp_visited.remove(node);
1331 visited.insert(node.to_string());
1332 result.push(node.to_string());
1333
1334 Ok(())
1335 }
1336}
1337
1338pub fn merge_schema_extensions(
1341 main_spec: Value,
1342 extension_paths: &[impl AsRef<Path>],
1343) -> Result<Value> {
1344 let mut result = main_spec;
1345
1346 for path in extension_paths {
1347 let extension = load_extension_file(path.as_ref())?;
1348 result = merge_json_objects_with_replacements(result, extension)?;
1349 }
1350
1351 Ok(result)
1352}
1353
1354fn normalize_operation_path(path: &str) -> String {
1361 match path.split_once('#') {
1362 Some((route, _fragment)) if route.starts_with('/') => route.to_string(),
1363 _ => path.to_string(),
1364 }
1365}
1366
1367fn unwrap_annotation_allof(schema: &crate::openapi::Schema) -> &crate::openapi::Schema {
1372 let crate::openapi::Schema::AllOf { all_of, .. } = schema else {
1373 return schema;
1374 };
1375 let mut references = all_of.iter().filter(|s| s.reference().is_some());
1376 let (Some(first), None) = (references.next(), references.next()) else {
1377 return schema;
1378 };
1379 let others_annotation_only = all_of
1380 .iter()
1381 .all(|member| member.reference().is_some() || schema_is_annotation_only(member));
1382 if others_annotation_only {
1383 first
1384 } else {
1385 schema
1386 }
1387}
1388
1389fn schema_is_annotation_only(schema: &crate::openapi::Schema) -> bool {
1394 serde_json::to_value(schema)
1395 .ok()
1396 .and_then(|value| value.as_object().cloned())
1397 .is_some_and(|object| {
1398 object.keys().all(|key| {
1399 matches!(
1400 key.as_str(),
1401 "title"
1402 | "description"
1403 | "deprecated"
1404 | "readOnly"
1405 | "writeOnly"
1406 | "examples"
1407 | "example"
1408 | "default"
1409 | "externalDocs"
1410 | "xml"
1411 | "$comment"
1412 | "nullable"
1413 ) || key.starts_with("x-")
1414 })
1415 })
1416}
1417
1418fn load_extension_file(path: &Path) -> Result<Value> {
1422 let content = std::fs::read_to_string(path).map_err(|e| GeneratorError::FileError {
1423 message: format!("Failed to read file {}: {}", path.display(), e),
1424 })?;
1425
1426 let is_yaml = path
1427 .extension()
1428 .and_then(|extension| extension.to_str())
1429 .is_some_and(|extension| {
1430 extension.eq_ignore_ascii_case("yaml") || extension.eq_ignore_ascii_case("yml")
1431 });
1432
1433 if is_yaml {
1434 crate::spec_source::yaml_to_json_value(&content).map_err(|error| {
1435 GeneratorError::FileError {
1436 message: format!(
1437 "Failed to parse schema extension {} as YAML: {}",
1438 path.display(),
1439 error
1440 ),
1441 }
1442 })
1443 } else {
1444 serde_json::from_str(&content).map_err(|error| GeneratorError::FileError {
1445 message: format!(
1446 "Failed to parse schema extension {} as JSON: {}",
1447 path.display(),
1448 error
1449 ),
1450 })
1451 }
1452}
1453
1454fn merge_json_objects_with_replacements(main: Value, extension: Value) -> Result<Value> {
1456 let replacements = extract_replacement_rules(&extension);
1458
1459 Ok(merge_json_objects_with_rules(
1461 main,
1462 extension,
1463 &replacements,
1464 ))
1465}
1466
1467fn extract_replacement_rules(
1469 extension: &Value,
1470) -> std::collections::HashMap<String, (String, String)> {
1471 let mut rules = std::collections::HashMap::new();
1472
1473 if let Some(x_replacements) = extension.get("x-replacements") {
1474 if let Some(x_replacements_obj) = x_replacements.as_object() {
1475 for (schema_name, replacement_rule) in x_replacements_obj {
1476 if let Some(rule_obj) = replacement_rule.as_object() {
1477 if let (Some(replace), Some(with)) = (
1478 rule_obj.get("replace").and_then(|v| v.as_str()),
1479 rule_obj.get("with").and_then(|v| v.as_str()),
1480 ) {
1481 rules.insert(schema_name.clone(), (replace.to_string(), with.to_string()));
1482 }
1484 }
1485 }
1486 }
1487 }
1488
1489 rules
1490}
1491
1492fn should_replace_variant(
1494 schema_name: &str,
1495 extension_refs: &[String],
1496 replacements: &std::collections::HashMap<String, (String, String)>,
1497) -> bool {
1498 for (replace_schema, with_schema) in replacements.values() {
1500 if schema_name == replace_schema {
1501 let replacement_exists = extension_refs.iter().any(|ext_ref| {
1503 let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
1504 ext_schema_name == with_schema
1505 });
1506
1507 if replacement_exists {
1508 return true;
1509 }
1510 }
1511 }
1512
1513 extension_refs.iter().any(|ext_ref| {
1515 let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
1516 schema_name == ext_schema_name
1517 })
1518}
1519
1520fn merge_json_objects_with_rules(
1525 main: Value,
1526 extension: Value,
1527 replacements: &std::collections::HashMap<String, (String, String)>,
1528) -> Value {
1529 match (main, extension) {
1530 (Value::Object(mut main_obj), Value::Object(ext_obj)) => {
1532 let main_union_keyword = if main_obj.contains_key("oneOf") {
1535 Some("oneOf")
1536 } else if main_obj.contains_key("anyOf") {
1537 Some("anyOf")
1538 } else {
1539 None
1540 };
1541 if let (Some(main_variants), Some(ext_variants)) = (
1542 extract_schema_variants(&Value::Object(main_obj.clone())),
1543 extract_schema_variants(&Value::Object(ext_obj.clone())),
1544 ) {
1545 let union_key = main_union_keyword.unwrap_or("oneOf");
1546 println!(
1547 "🔍 Merging union schemas ({union_key}): {} main variants, {} extension variants",
1548 main_variants.len(),
1549 ext_variants.len()
1550 );
1551 let mut merged_variants = Vec::new();
1554 let extension_refs: Vec<String> = ext_variants
1555 .iter()
1556 .filter_map(|v| v.get("$ref").and_then(|r| r.as_str()))
1557 .map(|s| s.to_string())
1558 .collect();
1559
1560 for main_variant in main_variants {
1562 if let Some(main_ref) = main_variant.get("$ref").and_then(|r| r.as_str()) {
1563 let schema_name = main_ref.split('/').next_back().unwrap_or("");
1565 let should_replace =
1566 should_replace_variant(schema_name, &extension_refs, replacements);
1567
1568 if should_replace {
1569 println!("🔄 REPLACING {} (explicit rule)", schema_name);
1570 }
1571
1572 if !should_replace {
1573 merged_variants.push(main_variant);
1574 }
1575 } else {
1576 merged_variants.push(main_variant);
1578 }
1579 }
1580
1581 for ext_variant in ext_variants {
1583 merged_variants.push(ext_variant);
1584 }
1585
1586 main_obj.remove("oneOf");
1588 main_obj.remove("anyOf");
1589 main_obj.insert(union_key.to_string(), Value::Array(merged_variants));
1590
1591 for (key, ext_value) in ext_obj {
1593 if key != "oneOf" && key != "anyOf" {
1594 match main_obj.get(&key) {
1595 Some(main_value) => {
1596 let merged_value = merge_json_objects_with_rules(
1597 main_value.clone(),
1598 ext_value,
1599 replacements,
1600 );
1601 main_obj.insert(key, merged_value);
1602 }
1603 None => {
1604 main_obj.insert(key, ext_value);
1605 }
1606 }
1607 }
1608 }
1609
1610 return Value::Object(main_obj);
1611 }
1612
1613 for (key, ext_value) in ext_obj {
1615 match main_obj.get(&key) {
1616 Some(main_value) => {
1617 let merged_value = merge_json_objects_with_rules(
1619 main_value.clone(),
1620 ext_value,
1621 replacements,
1622 );
1623 main_obj.insert(key, merged_value);
1624 }
1625 None => {
1626 main_obj.insert(key, ext_value);
1628 }
1629 }
1630 }
1631 Value::Object(main_obj)
1632 }
1633
1634 (Value::Array(mut main_arr), Value::Array(ext_arr)) => {
1636 main_arr.extend(ext_arr);
1637 Value::Array(main_arr)
1638 }
1639
1640 (_, extension) => extension,
1642 }
1643}
1644
1645fn extract_schema_variants(obj: &Value) -> Option<Vec<Value>> {
1647 if let Value::Object(map) = obj {
1648 if let Some(Value::Array(variants)) = map.get("oneOf") {
1649 return Some(variants.clone());
1650 }
1651 if let Some(Value::Array(variants)) = map.get("anyOf") {
1652 return Some(variants.clone());
1653 }
1654 }
1655 None
1656}
1657
1658#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1663enum InlineUnionKind {
1664 OneOf,
1665 AnyOf,
1666}
1667
1668#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
1669enum SchemaIdentity {
1670 Component(String),
1671 Pointer(String),
1672 InlineUnionBranch {
1673 owner_context: String,
1674 union_kind: InlineUnionKind,
1675 original_index: usize,
1676 discriminator: Option<String>,
1677 fingerprint: String,
1678 },
1679 Inline {
1680 context: String,
1681 kind: &'static str,
1682 preferred_name: String,
1683 fingerprint: String,
1684 },
1685}
1686
1687#[derive(Debug, Default)]
1688struct SchemaNameRegistry {
1689 names_by_identity: BTreeMap<SchemaIdentity, String>,
1690 identities_by_name: BTreeMap<String, SchemaIdentity>,
1691}
1692
1693impl SchemaNameRegistry {
1694 fn with_components(component_names: impl IntoIterator<Item = String>) -> Self {
1695 let mut registry = Self::default();
1696 for name in component_names {
1697 let identity = SchemaIdentity::Component(name.clone());
1698 registry
1699 .names_by_identity
1700 .insert(identity.clone(), name.clone());
1701 registry.identities_by_name.insert(name, identity);
1702 }
1703 registry
1704 }
1705
1706 fn component_name(&self, source_name: &str) -> Option<&str> {
1707 self.names_by_identity
1708 .get(&SchemaIdentity::Component(source_name.to_string()))
1709 .map(String::as_str)
1710 }
1711
1712 fn allocate(
1713 &mut self,
1714 identity: SchemaIdentity,
1715 preferred_name: &str,
1716 collision_name: &str,
1717 ) -> String {
1718 if let Some(existing) = self.names_by_identity.get(&identity) {
1719 return existing.clone();
1720 }
1721
1722 let allocated = if !self.identities_by_name.contains_key(preferred_name) {
1723 preferred_name.to_string()
1724 } else if !self.identities_by_name.contains_key(collision_name) {
1725 collision_name.to_string()
1726 } else {
1727 let hash = stable_schema_identity_hash(&identity);
1728 let hashed = format!("{collision_name}{hash:016X}");
1729 if !self.identities_by_name.contains_key(&hashed) {
1730 hashed
1731 } else {
1732 let mut suffix = 2;
1733 loop {
1734 let candidate = format!("{hashed}{suffix}");
1735 if !self.identities_by_name.contains_key(&candidate) {
1736 break candidate;
1737 }
1738 suffix += 1;
1739 }
1740 }
1741 };
1742
1743 self.names_by_identity
1744 .insert(identity.clone(), allocated.clone());
1745 self.identities_by_name.insert(allocated.clone(), identity);
1746 allocated
1747 }
1748}
1749
1750fn stable_schema_identity_hash(identity: &SchemaIdentity) -> u64 {
1754 let bytes = format!("{identity:?}");
1755 bytes
1756 .as_bytes()
1757 .iter()
1758 .fold(0xcbf29ce484222325, |hash, byte| {
1759 (hash ^ u64::from(*byte)).wrapping_mul(0x100000001b3)
1760 })
1761}
1762
1763#[cfg(test)]
1764mod schema_name_registry_tests {
1765 use super::{SchemaIdentity, SchemaNameRegistry};
1766
1767 #[test]
1768 fn component_reservations_are_independent_of_input_traversal_order() {
1769 let component_names = ["ModelApi".to_string(), "OutputFormatContainer".to_string()];
1770 let mut forward = SchemaNameRegistry::with_components(component_names.clone());
1771 let mut reverse = SchemaNameRegistry::with_components(component_names.into_iter().rev());
1772 let inline = SchemaIdentity::Inline {
1773 context: "Model".to_string(),
1774 kind: "property-object",
1775 preferred_name: "ModelApi".to_string(),
1776 fingerprint: r#"{"type":"object"}"#.to_string(),
1777 };
1778
1779 assert_eq!(
1780 forward.allocate(inline.clone(), "ModelApi", "ModelApiInline"),
1781 "ModelApiInline"
1782 );
1783 assert_eq!(
1784 reverse.allocate(inline, "ModelApi", "ModelApiInline"),
1785 "ModelApiInline"
1786 );
1787 assert_eq!(forward.component_name("ModelApi"), Some("ModelApi"));
1788 assert_eq!(reverse.component_name("ModelApi"), Some("ModelApi"));
1789 assert_eq!(
1790 forward.component_name("OutputFormatContainer"),
1791 Some("OutputFormatContainer")
1792 );
1793 assert_eq!(
1794 reverse.component_name("OutputFormatContainer"),
1795 Some("OutputFormatContainer")
1796 );
1797 }
1798
1799 #[test]
1800 fn an_identity_reuses_its_exact_allocated_name() {
1801 let mut registry = SchemaNameRegistry::with_components(["ModelApi".to_string()]);
1802 let inline = SchemaIdentity::Inline {
1803 context: "Model".to_string(),
1804 kind: "property-object",
1805 preferred_name: "ModelApi".to_string(),
1806 fingerprint: r#"{"type":"object"}"#.to_string(),
1807 };
1808
1809 let first = registry.allocate(inline.clone(), "ModelApi", "ModelApiInline");
1810 let repeated = registry.allocate(inline, "Ignored", "IgnoredInline");
1811
1812 assert_eq!(first, "ModelApiInline");
1813 assert_eq!(repeated, first);
1814 assert_eq!(registry.component_name("ModelApi"), Some("ModelApi"));
1815 }
1816}
1817
1818pub struct SchemaAnalyzer {
1819 schemas: BTreeMap<String, Schema>,
1820 resolved_cache: BTreeMap<String, AnalyzedSchema>,
1821 schema_names: SchemaNameRegistry,
1822 openapi_spec: Value,
1823 current_schema_name: Option<String>,
1824 component_parameters: BTreeMap<String, crate::openapi::Parameter>,
1825 type_mapper: TypeMapper,
1830 resolving_pointers: HashSet<String>,
1833}
1834
1835impl SchemaAnalyzer {
1836 fn untyped_value(&self, _context: impl Into<String>, reason: UntypedReason) -> SchemaType {
1840 SchemaType::Untyped {
1841 shape: UntypedShape::Value,
1842 reason,
1843 }
1844 }
1845
1846 fn untyped_value_array(
1848 &self,
1849 _context: impl Into<String>,
1850 reason: UntypedReason,
1851 ) -> SchemaType {
1852 SchemaType::Untyped {
1853 shape: UntypedShape::ValueArray,
1854 reason,
1855 }
1856 }
1857
1858 fn untyped_value_map(&self, _context: impl Into<String>, reason: UntypedReason) -> SchemaType {
1862 SchemaType::Untyped {
1863 shape: UntypedShape::ValueMap,
1864 reason,
1865 }
1866 }
1867
1868 fn untyped_context(&self, detail: &str) -> String {
1870 match (&self.current_schema_name, detail) {
1871 (Some(schema), "") => schema.clone(),
1872 (Some(schema), detail) => format!("{schema}.{detail}"),
1873 (None, "") => "<anonymous>".to_string(),
1874 (None, detail) => detail.to_string(),
1875 }
1876 }
1877
1878 fn allocate_inline_schema_name(
1879 &mut self,
1880 preferred_name: &str,
1881 collision_name: &str,
1882 kind: &'static str,
1883 schema: &Schema,
1884 ) -> String {
1885 let identity = SchemaIdentity::Inline {
1886 context: self
1887 .current_schema_name
1888 .clone()
1889 .unwrap_or_else(|| "<anonymous>".to_string()),
1890 kind,
1891 preferred_name: preferred_name.to_string(),
1892 fingerprint: serde_json::to_string(schema).unwrap_or_else(|_| format!("{schema:?}")),
1893 };
1894 self.schema_names
1895 .allocate(identity, preferred_name, collision_name)
1896 }
1897
1898 fn with_schema_context<T>(
1903 &mut self,
1904 schema_name: &str,
1905 analyze: impl FnOnce(&mut Self) -> Result<T>,
1906 ) -> Result<T> {
1907 let previous = self.current_schema_name.replace(schema_name.to_string());
1908 let result = analyze(self);
1909 self.current_schema_name = previous;
1910 result
1911 }
1912
1913 fn add_allocated_object_schema(
1914 &mut self,
1915 object_type_name: String,
1916 schema: &Schema,
1917 dependencies: &mut HashSet<String>,
1918 ) -> Result<SchemaType> {
1919 let object_type = self.with_schema_context(&object_type_name, |analyzer| {
1920 analyzer.analyze_object_schema(schema, dependencies)
1921 })?;
1922 self.resolved_cache.insert(
1923 object_type_name.clone(),
1924 AnalyzedSchema {
1925 name: object_type_name.clone(),
1926 original: serde_json::to_value(schema).unwrap_or(Value::Null),
1927 schema_type: object_type,
1928 dependencies: dependencies.clone(),
1929 nullable: false,
1930 description: schema.details().description.clone(),
1931 default: None,
1932 },
1933 );
1934 dependencies.insert(object_type_name.clone());
1935 Ok(SchemaType::Reference {
1936 target: object_type_name,
1937 })
1938 }
1939
1940 fn allocate_inline_union_branch_name(
1941 &mut self,
1942 preferred_name: &str,
1943 owner_context: &str,
1944 union_kind: InlineUnionKind,
1945 original_index: usize,
1946 discriminator: Option<&str>,
1947 schema: &Schema,
1948 ) -> String {
1949 let identity = SchemaIdentity::InlineUnionBranch {
1950 owner_context: owner_context.to_string(),
1951 union_kind,
1952 original_index,
1953 discriminator: discriminator.map(str::to_string),
1954 fingerprint: serde_json::to_string(schema).unwrap_or_else(|_| format!("{schema:?}")),
1955 };
1956 self.schema_names
1957 .allocate(identity, preferred_name, &format!("{preferred_name}Inline"))
1958 }
1959
1960 fn allocate_pointer_schema_name(&mut self, pointer: &str, preferred_name: &str) -> String {
1961 self.schema_names.allocate(
1962 SchemaIdentity::Pointer(pointer.to_string()),
1963 preferred_name,
1964 &format!("{preferred_name}Pointer"),
1965 )
1966 }
1967
1968 fn allocate_synthetic_schema_name(
1969 &mut self,
1970 preferred_name: &str,
1971 collision_name: &str,
1972 kind: &'static str,
1973 fingerprint: String,
1974 ) -> String {
1975 let identity = SchemaIdentity::Inline {
1976 context: self
1977 .current_schema_name
1978 .clone()
1979 .unwrap_or_else(|| "<anonymous>".to_string()),
1980 kind,
1981 preferred_name: preferred_name.to_string(),
1982 fingerprint,
1983 };
1984 self.schema_names
1985 .allocate(identity, preferred_name, collision_name)
1986 }
1987
1988 fn uses_aws_query_conventions(&self) -> bool {
1989 self.openapi_spec
1990 .pointer("/info/x-providerName")
1991 .and_then(Value::as_str)
1992 .is_some_and(|provider| provider.eq_ignore_ascii_case("amazonaws.com"))
1993 }
1994
1995 pub fn new(openapi_spec: Value) -> Result<Self> {
1999 Self::with_type_mapper(openapi_spec, TypeMapper::default())
2000 }
2001
2002 pub fn with_type_mapper(mut openapi_spec: Value, type_mapper: TypeMapper) -> Result<Self> {
2006 disambiguate_component_schema_names(&mut openapi_spec);
2007 let spec: OpenApiSpec = parse_spec_document(&openapi_spec)?;
2008 let schemas = Self::extract_schemas(&spec)?;
2009
2010 let component_parameters = spec
2011 .components
2012 .as_ref()
2013 .and_then(|c| c.parameters.as_ref())
2014 .cloned()
2015 .unwrap_or_default();
2016 let schema_names = SchemaNameRegistry::with_components(schemas.keys().cloned());
2017 Ok(Self {
2018 schemas,
2019 resolved_cache: BTreeMap::new(),
2020 schema_names,
2021 openapi_spec,
2022 current_schema_name: None,
2023 component_parameters,
2024 type_mapper,
2025 resolving_pointers: HashSet::new(),
2026 })
2027 }
2028
2029 pub fn new_with_extensions(
2032 openapi_spec: Value,
2033 extension_paths: &[std::path::PathBuf],
2034 ) -> Result<Self> {
2035 let merged_spec = merge_schema_extensions(openapi_spec, extension_paths)?;
2036 Self::new(merged_spec)
2037 }
2038
2039 pub fn new_with_extensions_and_type_mapper(
2042 openapi_spec: Value,
2043 extension_paths: &[std::path::PathBuf],
2044 type_mapper: TypeMapper,
2045 ) -> Result<Self> {
2046 let merged_spec = merge_schema_extensions(openapi_spec, extension_paths)?;
2047 Self::with_type_mapper(merged_spec, type_mapper)
2048 }
2049
2050 pub fn type_mapper(&self) -> &TypeMapper {
2054 &self.type_mapper
2055 }
2056
2057 fn generate_context_aware_name(
2060 &self,
2061 base_context: &str,
2062 type_hint: &str,
2063 index: usize,
2064 schema: Option<&Schema>,
2065 ) -> String {
2066 if let Some(schema) = schema {
2068 if type_hint == "Array"
2070 && matches!(schema.schema_type(), Some(OpenApiSchemaType::Array))
2071 {
2072 if let Some(items_schema) = schema.details().item_schema() {
2073 if let Some(item_type) = items_schema.schema_type() {
2075 match item_type {
2076 OpenApiSchemaType::Object => {
2077 return format!("{base_context}ItemArray");
2078 }
2079 OpenApiSchemaType::String => {
2080 return format!("{base_context}StringArray");
2081 }
2082 _ => {}
2083 }
2084 }
2085 }
2086 }
2087 }
2088
2089 match type_hint {
2091 "Array" => {
2092 format!("{base_context}Array")
2094 }
2095 "Variant" | "InlineVariant" => {
2096 if index == 0 {
2098 format!("{base_context}{type_hint}")
2099 } else {
2100 format!("{}{}{}", base_context, type_hint, index + 1)
2101 }
2102 }
2103 _ => {
2104 format!("{base_context}{type_hint}{index}")
2106 }
2107 }
2108 }
2109
2110 fn to_pascal_case(&self, s: &str) -> String {
2112 s.split(['_', '-'])
2113 .filter(|part| !part.is_empty())
2114 .map(|part| {
2115 let mut chars = part.chars();
2116 match chars.next() {
2117 None => String::new(),
2118 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
2119 }
2120 })
2121 .collect()
2122 }
2123
2124 fn extract_schemas(spec: &OpenApiSpec) -> Result<BTreeMap<String, Schema>> {
2125 let schemas = spec.components.as_ref().and_then(|c| c.schemas.as_ref());
2130 Ok(schemas
2131 .map(|m| {
2132 m.iter()
2133 .map(|(k, v)| (k.clone(), v.clone()))
2134 .collect::<BTreeMap<_, _>>()
2135 })
2136 .unwrap_or_default())
2137 }
2138
2139 pub fn analyze(&mut self) -> Result<SchemaAnalysis> {
2140 let validation_context = ValidationContext {
2141 openapi_version: self
2142 .openapi_spec
2143 .get("openapi")
2144 .and_then(Value::as_str)
2145 .unwrap_or_default()
2146 .to_string(),
2147 json_schema_dialect: self
2148 .openapi_spec
2149 .get("jsonSchemaDialect")
2150 .and_then(Value::as_str)
2151 .map(str::to_string),
2152 component_schemas: self
2153 .openapi_spec
2154 .pointer("/components/schemas")
2155 .and_then(Value::as_object)
2156 .map(|schemas| {
2157 schemas
2158 .iter()
2159 .map(|(name, schema)| (name.clone(), schema.clone()))
2160 .collect()
2161 })
2162 .unwrap_or_default(),
2163 };
2164 let mut analysis = SchemaAnalysis {
2165 schemas: BTreeMap::new(),
2166 dependencies: DependencyGraph::new(),
2167 patterns: DetectedPatterns {
2168 tagged_enum_schemas: HashSet::new(),
2169 untagged_enum_schemas: HashSet::new(),
2170 type_mappings: BTreeMap::new(),
2171 },
2172 operations: BTreeMap::new(),
2173 operation_responses: BTreeMap::new(),
2174 operation_sources: BTreeMap::new(),
2175 operation_id_aliases: BTreeMap::new(),
2176 used_type_features: crate::type_mapping::UsedFeatures::default(),
2177 enum_extensions: BTreeMap::new(),
2178 validation_context,
2179 untagged_union_branches: BTreeSet::new(),
2180 multipart_file_type: None,
2181 };
2182
2183 self.detect_patterns(&mut analysis.patterns)?;
2185
2186 let schema_names: Vec<String> = self.schemas.keys().cloned().collect();
2188 for schema_name in schema_names {
2189 let analyzed = self.analyze_schema(&schema_name)?;
2190
2191 for dep in &analyzed.dependencies {
2193 analysis
2194 .dependencies
2195 .add_dependency(schema_name.clone(), dep.clone());
2196 }
2197
2198 analysis.schemas.insert(schema_name, analyzed);
2199 }
2200
2201 for (inline_name, inline_schema) in &self.resolved_cache {
2204 if !analysis.schemas.contains_key(inline_name) {
2205 analysis
2207 .schemas
2208 .insert(inline_name.clone(), inline_schema.clone());
2209
2210 for dep in &inline_schema.dependencies {
2212 analysis
2213 .dependencies
2214 .add_dependency(inline_name.clone(), dep.clone());
2215 }
2216
2217 let mut schemas_to_update = Vec::new();
2222 for (schema_name, schema) in &analysis.schemas {
2223 if schema_name == inline_name {
2225 continue;
2226 }
2227
2228 if schema.dependencies.contains(inline_name) {
2229 schemas_to_update.push(schema_name.clone());
2231 }
2232 }
2233
2234 for schema_name in schemas_to_update {
2236 analysis
2237 .dependencies
2238 .add_dependency(schema_name, inline_name.clone());
2239 }
2240 }
2241 }
2242
2243 self.analyze_operations(&mut analysis)?;
2245
2246 for (inline_name, inline_schema) in &self.resolved_cache {
2249 if !analysis.schemas.contains_key(inline_name) {
2250 analysis
2251 .schemas
2252 .insert(inline_name.clone(), inline_schema.clone());
2253
2254 for dep in &inline_schema.dependencies {
2256 analysis
2257 .dependencies
2258 .add_dependency(inline_name.clone(), dep.clone());
2259 }
2260 }
2261 }
2262
2263 disambiguate_analyzed_schema_names(&mut analysis, &self.schemas);
2264 retype_multipart_file_fields(&mut analysis);
2265
2266 analysis.used_type_features = self.type_mapper.used_features();
2270
2271 for (name, analyzed) in &analysis.schemas {
2276 let enum_value_count = match &analyzed.schema_type {
2277 SchemaType::StringEnum { values } => values.len(),
2278 SchemaType::ExtensibleEnum { known_values } => known_values.len(),
2279 _ => continue,
2280 };
2281 if let Some(ext) = extract_enum_extensions(&analyzed.original, enum_value_count, name) {
2282 analysis.enum_extensions.insert(name.clone(), ext);
2283 }
2284 }
2285
2286 analysis.untagged_union_branches = collect_untagged_union_branches(&analysis);
2287
2288 for schema in analysis.schemas.values_mut() {
2289 normalize_untyped(&mut schema.schema_type, 0);
2290 }
2291
2292 Ok(analysis)
2293 }
2294
2295 fn detect_patterns(&self, patterns: &mut DetectedPatterns) -> Result<()> {
2296 for (schema_name, schema) in &self.schemas {
2297 if self.is_discriminated_union(schema) {
2299 patterns.tagged_enum_schemas.insert(schema_name.clone());
2300
2301 if let Some(mappings) = self.extract_type_mappings(schema)? {
2303 patterns.type_mappings.insert(schema_name.clone(), mappings);
2304 }
2305 }
2306 else if self.is_simple_union(schema) {
2308 patterns.untagged_enum_schemas.insert(schema_name.clone());
2309 }
2310 }
2311
2312 Ok(())
2313 }
2314
2315 fn is_discriminated_union(&self, schema: &Schema) -> bool {
2316 if schema.is_discriminated_union() {
2318 return true;
2319 }
2320
2321 if let Some(variants) = schema.union_variants() {
2323 return variants.len() > 2 && self.detect_discriminator_field(variants).is_some();
2324 }
2325
2326 false
2327 }
2328
2329 fn all_variants_have_unique_const_values(&self, variants: &[Schema], field_name: &str) -> bool {
2330 let mut values = HashSet::new();
2331
2332 variants.iter().all(|variant| {
2333 let schema = if let Some(ref_str) = variant.reference() {
2334 let Some(schema_name) = self.extract_schema_name(ref_str) else {
2335 return false;
2336 };
2337 let Some(schema) = self.schemas.get(schema_name) else {
2338 return false;
2339 };
2340 schema
2341 } else {
2342 variant
2343 };
2344
2345 self.extract_discriminator_value_for_field(schema, field_name)
2346 .is_some_and(|value| values.insert(value))
2347 })
2348 }
2349
2350 fn branch_resolves_to_object(&self, schema: &Schema) -> bool {
2359 self.branch_resolves_to_object_inner(schema, &mut HashSet::new())
2360 }
2361
2362 fn branch_resolves_to_object_inner(
2363 &self,
2364 schema: &Schema,
2365 visited_refs: &mut HashSet<String>,
2366 ) -> bool {
2367 if let Some(ref_str) = schema.reference() {
2369 if !visited_refs.insert(ref_str.to_string()) {
2370 return false;
2371 }
2372 let result = match self
2373 .extract_schema_name(ref_str)
2374 .and_then(|n| self.schemas.get(n))
2375 {
2376 Some(target) => self.branch_resolves_to_object_inner(target, visited_refs),
2377 None => false,
2378 };
2379 visited_refs.remove(ref_str);
2380 return result;
2381 }
2382 if let Schema::AllOf { all_of, .. } = schema {
2386 if Self::single_non_object_allof_carrier(all_of).is_some() {
2387 return false;
2388 }
2389 return all_of
2390 .iter()
2391 .filter(|member| !schema_is_annotation_only(member))
2392 .any(|member| self.branch_resolves_to_object_inner(member, visited_refs));
2393 }
2394 if let Some(variants) = schema.union_variants() {
2399 return !variants.is_empty()
2400 && variants
2401 .iter()
2402 .all(|variant| self.branch_resolves_to_object_inner(variant, visited_refs));
2403 }
2404 if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object)) {
2405 return true;
2406 }
2407 if schema.inferred_type() == Some(OpenApiSchemaType::Object) {
2408 return true;
2409 }
2410 false
2413 }
2414
2415 fn detect_discriminator_field(&self, variants: &[Schema]) -> Option<String> {
2419 if variants.is_empty() {
2420 return None;
2421 }
2422
2423 let first_variant = &variants[0];
2425 let first_schema = if let Some(ref_str) = first_variant.reference() {
2426 let schema_name = self.extract_schema_name(ref_str)?;
2427 self.schemas.get(schema_name)?
2428 } else {
2429 first_variant
2430 };
2431
2432 let properties = first_schema.details().properties.as_ref()?;
2433 let mut candidates: Vec<String> = Vec::new();
2434
2435 for (field_name, field_schema) in properties {
2436 let details = field_schema.details();
2437 let is_const = details.const_value.is_some()
2438 || details.enum_values.as_ref().is_some_and(|v| v.len() == 1)
2439 || details.extra.contains_key("const");
2440 if is_const {
2441 candidates.push(field_name.clone());
2442 }
2443 }
2444
2445 if candidates.is_empty() {
2446 return None;
2447 }
2448
2449 candidates.sort_by(|a, b| {
2451 if a == "type" {
2452 std::cmp::Ordering::Less
2453 } else if b == "type" {
2454 std::cmp::Ordering::Greater
2455 } else {
2456 a.cmp(b)
2457 }
2458 });
2459
2460 for candidate in &candidates {
2466 if self.all_variants_have_unique_const_values(variants, candidate) {
2467 return Some(candidate.clone());
2468 }
2469 }
2470
2471 None
2472 }
2473
2474 fn is_simple_union(&self, schema: &Schema) -> bool {
2475 if let Some(variants) = schema.union_variants() {
2476 if variants.len() > 1 && !schema.is_nullable_pattern() {
2478 let has_refs = variants.iter().any(|v| v.is_reference());
2479 return has_refs;
2480 }
2481 }
2482 false
2483 }
2484
2485 fn schema_or_reference_is_nullable(&self, schema: &Schema) -> bool {
2490 let mut current = schema.clone();
2491 let mut visited = HashSet::new();
2492 loop {
2493 if current.is_nullable_any() {
2494 return true;
2495 }
2496 if let Schema::AllOf { all_of, .. } = ¤t
2497 && let Some(carrier) = Self::single_non_object_allof_carrier(all_of)
2498 {
2499 current = carrier.clone();
2500 continue;
2501 }
2502 let Some(reference) = current.reference() else {
2503 return false;
2504 };
2505 if !visited.insert(reference.to_string()) {
2506 return false;
2507 }
2508 let Some(target) = self.reference_target_schema(reference) else {
2509 return false;
2510 };
2511 current = target;
2512 }
2513 }
2514
2515 fn nullable_container_value(&self, schema: &Schema, schema_type: SchemaType) -> SchemaType {
2521 if !self.schema_or_reference_is_nullable(schema)
2522 || matches!(schema_type, SchemaType::Nullable { .. })
2523 || matches!(schema.schema_type(), Some(OpenApiSchemaType::Null))
2524 {
2525 schema_type
2526 } else {
2527 SchemaType::Nullable {
2528 inner_type: Box::new(schema_type),
2529 }
2530 }
2531 }
2532
2533 fn extract_type_mappings(&self, schema: &Schema) -> Result<Option<BTreeMap<String, String>>> {
2534 let variants = schema.union_variants().ok_or_else(|| {
2535 GeneratorError::InvalidSchema("No variants found for discriminated union".to_string())
2536 })?;
2537
2538 let discriminator_field = if let Some(discriminator) = schema.discriminator() {
2540 discriminator.property_name.clone()
2541 } else if let Some(detected) = self.detect_discriminator_field(variants) {
2542 detected
2543 } else {
2544 "type".to_string() };
2546
2547 let mut mappings = BTreeMap::new();
2548
2549 for variant in variants {
2550 if let Some(ref_str) = variant.reference() {
2551 if let Some(type_name) = self.extract_schema_name(ref_str) {
2552 if let Some(variant_schema) = self.schemas.get(type_name) {
2553 if let Some(discriminator_value) = self
2554 .extract_discriminator_value_for_field(
2555 variant_schema,
2556 &discriminator_field,
2557 )
2558 {
2559 mappings.insert(type_name.to_string(), discriminator_value);
2560 }
2561 }
2562 }
2563 }
2564 }
2565
2566 if mappings.is_empty() {
2567 Ok(None)
2568 } else {
2569 Ok(Some(mappings))
2570 }
2571 }
2572
2573 #[allow(dead_code)]
2574 fn extract_discriminator_value(&self, schema: &Schema) -> Option<String> {
2575 self.extract_discriminator_value_for_field(schema, "type")
2576 }
2577
2578 fn extract_discriminator_values_for_field(
2579 &self,
2580 schema: &Schema,
2581 field_name: &str,
2582 ) -> Vec<String> {
2583 self.extract_discriminator_value_domain_for_field(schema, field_name)
2584 .unwrap_or_default()
2585 }
2586
2587 fn extract_discriminator_value_domain_for_field(
2588 &self,
2589 schema: &Schema,
2590 field_name: &str,
2591 ) -> Option<Vec<String>> {
2592 let mut visited_refs = HashSet::new();
2593 self.discriminator_value_domain(schema, field_name, &mut visited_refs)
2594 }
2595
2596 fn discriminator_value_domain(
2603 &self,
2604 schema: &Schema,
2605 field_name: &str,
2606 visited_refs: &mut HashSet<String>,
2607 ) -> Option<Vec<String>> {
2608 if let Some(reference) = schema.reference() {
2609 if !visited_refs.insert(reference.to_string()) {
2610 return None;
2611 }
2612 let result = self
2613 .extract_schema_name(reference)
2614 .and_then(|name| self.schemas.get(name))
2615 .and_then(|target| {
2616 self.discriminator_value_domain(target, field_name, visited_refs)
2617 });
2618 visited_refs.remove(reference);
2619 return result;
2620 }
2621
2622 let own_domain = schema
2623 .details()
2624 .properties
2625 .as_ref()
2626 .and_then(|properties| properties.get(field_name))
2627 .and_then(|property| self.string_constraint_domain(property, visited_refs));
2628
2629 let composition_domain = match schema {
2630 Schema::AllOf { all_of, .. } => {
2631 let mut domain = None;
2632 for member in all_of {
2633 domain = Self::intersect_optional_domains(
2634 domain,
2635 self.discriminator_value_domain(member, field_name, visited_refs),
2636 );
2637 }
2638 domain
2639 }
2640 Schema::AnyOf { any_of, .. } => {
2641 self.union_discriminator_domains(any_of, field_name, visited_refs)
2642 }
2643 Schema::OneOf { one_of, .. } => {
2644 self.union_discriminator_domains(one_of, field_name, visited_refs)
2645 }
2646 Schema::Bool(false) => Some(Vec::new()),
2647 _ => None,
2648 };
2649
2650 Self::intersect_optional_domains(own_domain, composition_domain)
2651 }
2652
2653 fn union_discriminator_domains(
2654 &self,
2655 schemas: &[Schema],
2656 field_name: &str,
2657 visited_refs: &mut HashSet<String>,
2658 ) -> Option<Vec<String>> {
2659 if schemas.is_empty() {
2660 return Some(Vec::new());
2661 }
2662 let mut domain = Vec::new();
2663 for schema in schemas {
2664 let values = self.discriminator_value_domain(schema, field_name, visited_refs)?;
2665 for value in values {
2666 Self::push_unique_string(&mut domain, &value);
2667 }
2668 }
2669 Some(domain)
2670 }
2671
2672 fn string_constraint_domain(
2673 &self,
2674 schema: &Schema,
2675 visited_refs: &mut HashSet<String>,
2676 ) -> Option<Vec<String>> {
2677 let allow_vendor_default =
2678 !self.has_standard_string_constraint(schema, &mut HashSet::new());
2679 self.string_constraint_domain_inner(schema, visited_refs, allow_vendor_default)
2680 }
2681
2682 fn string_constraint_domain_inner(
2683 &self,
2684 schema: &Schema,
2685 visited_refs: &mut HashSet<String>,
2686 allow_vendor_default: bool,
2687 ) -> Option<Vec<String>> {
2688 if let Some(reference) = schema.reference() {
2689 if !visited_refs.insert(reference.to_string()) {
2690 return None;
2691 }
2692 let result = self
2693 .extract_schema_name(reference)
2694 .and_then(|name| self.schemas.get(name))
2695 .and_then(|target| {
2696 self.string_constraint_domain_inner(target, visited_refs, allow_vendor_default)
2697 });
2698 visited_refs.remove(reference);
2699 return result;
2700 }
2701
2702 let details = schema.details();
2703 let mut own_domain = None;
2704 if let Some(value) = details.const_value.as_ref() {
2705 own_domain = Self::intersect_optional_domains(
2706 own_domain,
2707 Some(value.as_str().into_iter().map(str::to_string).collect()),
2708 );
2709 }
2710 if let Some(enum_values) = &details.enum_values {
2711 own_domain = Self::intersect_optional_domains(
2712 own_domain,
2713 Some(
2714 enum_values
2715 .iter()
2716 .filter_map(Value::as_str)
2717 .map(str::to_string)
2718 .collect(),
2719 ),
2720 );
2721 }
2722 if allow_vendor_default
2723 && details
2724 .extra
2725 .get("x-stainless-const")
2726 .and_then(Value::as_bool)
2727 == Some(true)
2728 && let Some(default) = details.default.as_ref().and_then(Value::as_str)
2729 {
2730 own_domain =
2731 Self::intersect_optional_domains(own_domain, Some(vec![default.to_string()]));
2732 }
2733
2734 let composition_domain = match schema {
2735 Schema::AllOf { all_of, .. } => {
2736 let mut domain = None;
2737 for member in all_of {
2738 domain = Self::intersect_optional_domains(
2739 domain,
2740 self.string_constraint_domain_inner(
2741 member,
2742 visited_refs,
2743 allow_vendor_default,
2744 ),
2745 );
2746 }
2747 domain
2748 }
2749 Schema::AnyOf { any_of, .. } => {
2750 self.union_string_constraint_domains(any_of, visited_refs, allow_vendor_default)
2751 }
2752 Schema::OneOf { one_of, .. } => {
2753 self.union_string_constraint_domains(one_of, visited_refs, allow_vendor_default)
2754 }
2755 Schema::Bool(false) => Some(Vec::new()),
2756 _ => None,
2757 };
2758
2759 Self::intersect_optional_domains(own_domain, composition_domain)
2760 }
2761
2762 fn union_string_constraint_domains(
2763 &self,
2764 schemas: &[Schema],
2765 visited_refs: &mut HashSet<String>,
2766 allow_vendor_default: bool,
2767 ) -> Option<Vec<String>> {
2768 if schemas.is_empty() {
2769 return Some(Vec::new());
2770 }
2771 let mut domain = Vec::new();
2772 for schema in schemas {
2773 let values =
2774 self.string_constraint_domain_inner(schema, visited_refs, allow_vendor_default)?;
2775 for value in values {
2776 Self::push_unique_string(&mut domain, &value);
2777 }
2778 }
2779 Some(domain)
2780 }
2781
2782 fn has_standard_string_constraint(
2783 &self,
2784 schema: &Schema,
2785 visited_refs: &mut HashSet<String>,
2786 ) -> bool {
2787 if let Some(reference) = schema.reference() {
2788 if !visited_refs.insert(reference.to_string()) {
2789 return false;
2790 }
2791 let result = self
2792 .extract_schema_name(reference)
2793 .and_then(|name| self.schemas.get(name))
2794 .is_some_and(|target| self.has_standard_string_constraint(target, visited_refs));
2795 visited_refs.remove(reference);
2796 return result;
2797 }
2798
2799 let details = schema.details();
2800 if details.const_value.is_some() || details.enum_values.is_some() {
2801 return true;
2802 }
2803
2804 match schema {
2805 Schema::AllOf { all_of, .. } => all_of
2806 .iter()
2807 .any(|member| self.has_standard_string_constraint(member, visited_refs)),
2808 Schema::AnyOf { any_of, .. } => any_of
2809 .iter()
2810 .any(|member| self.has_standard_string_constraint(member, visited_refs)),
2811 Schema::OneOf { one_of, .. } => one_of
2812 .iter()
2813 .any(|member| self.has_standard_string_constraint(member, visited_refs)),
2814 _ => false,
2815 }
2816 }
2817
2818 fn intersect_optional_domains(
2819 left: Option<Vec<String>>,
2820 right: Option<Vec<String>>,
2821 ) -> Option<Vec<String>> {
2822 match (left, right) {
2823 (None, other) | (other, None) => other,
2824 (Some(left), Some(right)) => Some(
2825 left.into_iter()
2826 .filter(|value| right.contains(value))
2827 .collect(),
2828 ),
2829 }
2830 }
2831
2832 fn push_unique_string(values: &mut Vec<String>, value: &str) {
2833 if !values.iter().any(|existing| existing == value) {
2834 values.push(value.to_string());
2835 }
2836 }
2837
2838 fn discriminator_property_presence(&self, schema: &Schema, field_name: &str) -> (bool, bool) {
2839 self.discriminator_property_presence_inner(schema, field_name, &mut HashSet::new(), 0)
2840 }
2841
2842 fn discriminator_property_presence_inner(
2843 &self,
2844 schema: &Schema,
2845 field_name: &str,
2846 visited_refs: &mut HashSet<String>,
2847 depth: usize,
2848 ) -> (bool, bool) {
2849 if depth > 64 {
2850 return (false, false);
2851 }
2852 if let Some(reference) = schema.reference() {
2853 if !visited_refs.insert(reference.to_string()) {
2854 return (false, false);
2855 }
2856 let result = self
2857 .extract_schema_name(reference)
2858 .and_then(|name| self.schemas.get(name))
2859 .map(|target| {
2860 self.discriminator_property_presence_inner(
2861 target,
2862 field_name,
2863 visited_refs,
2864 depth + 1,
2865 )
2866 })
2867 .unwrap_or((false, false));
2868 visited_refs.remove(reference);
2869 return result;
2870 }
2871
2872 let details = schema.details();
2873 let mut declared = details
2874 .properties
2875 .as_ref()
2876 .is_some_and(|properties| properties.contains_key(field_name));
2877 let mut required = details
2878 .required
2879 .as_ref()
2880 .is_some_and(|names| names.iter().any(|name| name == field_name));
2881
2882 let members = match schema {
2883 Schema::AllOf { all_of, .. } => Some(all_of.as_slice()),
2884 Schema::AnyOf { any_of, .. } => Some(any_of.as_slice()),
2885 Schema::OneOf { one_of, .. } => Some(one_of.as_slice()),
2886 _ => None,
2887 };
2888 if let Some(members) = members {
2889 for member in members {
2890 let (member_declared, member_required) = self
2891 .discriminator_property_presence_inner(
2892 member,
2893 field_name,
2894 visited_refs,
2895 depth + 1,
2896 );
2897 declared |= member_declared;
2898 required |= member_required;
2899 }
2900 }
2901 (declared, required)
2902 }
2903
2904 fn extract_discriminator_value_for_field(
2905 &self,
2906 schema: &Schema,
2907 field_name: &str,
2908 ) -> Option<String> {
2909 self.extract_discriminator_values_for_field(schema, field_name)
2910 .into_iter()
2911 .next()
2912 }
2913
2914 fn get_any_reference<'a>(&self, schema: &'a Schema) -> Option<&'a str> {
2915 schema.reference().or_else(|| schema.recursive_reference())
2916 }
2917
2918 fn extract_schema_name<'a>(&self, ref_str: &'a str) -> Option<&'a str> {
2919 if ref_str == "#" {
2920 return None; }
2922
2923 let parts: Vec<&str> = ref_str.split('/').collect();
2924
2925 if parts.len() == 4 && parts[0] == "#" && parts[2] == "schemas" {
2929 return Some(parts[3]);
2930 }
2931
2932 if parts.len() == 3 && parts[0] == "#" && parts[1] == "definitions" {
2935 return Some(parts[2]);
2936 }
2937
2938 if ref_str.starts_with("#/") {
2942 return None;
2943 }
2944
2945 let last = parts.last()?;
2951 if last.is_empty()
2952 || last.chars().all(|c| c.is_ascii_digit())
2953 || matches!(
2954 *last,
2955 "schema" | "properties" | "items" | "additionalProperties"
2956 )
2957 {
2958 return None;
2959 }
2960 let first = last.chars().next().unwrap_or(' ');
2961 if !first.is_ascii_alphabetic() || !first.is_ascii_uppercase() {
2962 return None;
2963 }
2964 Some(last)
2965 }
2966
2967 fn reference_target_schema(&self, reference: &str) -> Option<Schema> {
2970 if let Some(name) = self.extract_schema_name(reference) {
2971 return self.schemas.get(name).cloned();
2972 }
2973 let pointer = reference.strip_prefix('#')?;
2974 if !pointer.starts_with('/') {
2975 return None;
2976 }
2977 Schema::deserialize(self.openapi_spec.pointer(pointer)?).ok()
2978 }
2979
2980 fn analyze_schema(&mut self, schema_name: &str) -> Result<AnalyzedSchema> {
2981 let emitted_name = self
2985 .schema_names
2986 .component_name(schema_name)
2987 .ok_or_else(|| GeneratorError::UnresolvedReference(schema_name.to_string()))?
2988 .to_string();
2989 if let Some(cached) = self.resolved_cache.get(&emitted_name) {
2990 return Ok(cached.clone());
2991 }
2992
2993 self.current_schema_name = Some(emitted_name.clone());
2995
2996 let schema = self
2997 .schemas
2998 .get(schema_name)
2999 .ok_or_else(|| GeneratorError::UnresolvedReference(schema_name.to_string()))?
3000 .clone();
3001
3002 self.resolved_cache.insert(
3004 emitted_name.clone(),
3005 AnalyzedSchema {
3006 name: emitted_name.clone(),
3007 original: serde_json::to_value(&schema).unwrap_or(Value::Null),
3008 schema_type: SchemaType::Reference {
3009 target: "placeholder".to_string(),
3010 },
3011 dependencies: HashSet::new(),
3012 nullable: false,
3013 description: None,
3014 default: None,
3015 },
3016 );
3017
3018 let analyzed = self.analyze_schema_value(&schema, &emitted_name)?;
3019
3020 self.resolved_cache.insert(emitted_name, analyzed.clone());
3022
3023 Ok(analyzed)
3024 }
3025
3026 fn analyze_schema_value(
3027 &mut self,
3028 schema: &Schema,
3029 schema_name: &str,
3030 ) -> Result<AnalyzedSchema> {
3031 let details = schema.details();
3032 let description = details.description.clone();
3033 let nullable = schema.is_nullable_any();
3037 let mut dependencies = HashSet::new();
3038
3039 let schema_type = match schema {
3040 Schema::Bool(accepts_anything) => self.untyped_value(
3043 self.untyped_context(""),
3044 if *accepts_anything {
3045 UntypedReason::AnySchema
3046 } else {
3047 UntypedReason::NeverMatches
3048 },
3049 ),
3050 Schema::Reference { reference, .. } => {
3051 match self.extract_schema_name(reference) {
3056 Some(name) => {
3057 let target = name.to_string();
3058 dependencies.insert(target.clone());
3059 SchemaType::Reference { target }
3060 }
3061 None => {
3062 let reference = reference.clone();
3063 if let Some(resolved) =
3064 self.resolve_pointer_schema(&reference, &mut dependencies)?
3065 {
3066 resolved
3067 } else {
3068 eprintln!(
3069 "⚠️ unresolvable $ref `{}` — typing as serde_json::Value",
3070 reference
3071 );
3072 self.untyped_value(
3073 format!("$ref {reference}"),
3074 UntypedReason::UnresolvedReference,
3075 )
3076 }
3077 }
3078 }
3079 }
3080 Schema::RecursiveRef { recursive_ref, .. }
3081 | Schema::DynamicRef {
3082 dynamic_ref: recursive_ref,
3083 ..
3084 } => {
3085 if recursive_ref == "#" {
3091 dependencies.insert(schema_name.to_string());
3092 SchemaType::Reference {
3093 target: schema_name.to_string(),
3094 }
3095 } else {
3096 let target = self
3097 .extract_schema_name(recursive_ref)
3098 .unwrap_or(schema_name)
3099 .to_string();
3100 dependencies.insert(target.clone());
3101 SchemaType::Reference { target }
3102 }
3103 }
3104 Schema::Typed { .. } | Schema::TypedMulti { .. } => {
3105 if let Some(non_null_types) = schema.non_null_schema_types() {
3106 let mut variants = Vec::with_capacity(non_null_types.len());
3107 for t in non_null_types {
3108 variants.push(self.build_typed_multi_union_variant(
3109 t,
3110 schema,
3111 schema_name,
3112 &mut dependencies,
3113 )?);
3114 }
3115 SchemaType::Union {
3116 variants,
3117 exclusive: false,
3118 }
3119 } else {
3120 self.analyze_single_typed_schema(
3121 schema,
3122 schema_name,
3123 details,
3124 &mut dependencies,
3125 )?
3126 }
3127 }
3128 Schema::AnyOf {
3129 any_of,
3130 discriminator,
3131 ..
3132 } => {
3133 if Self::union_only_constrains_requiredness(any_of) {
3134 return Ok(AnalyzedSchema {
3135 name: schema_name.to_string(),
3136 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3137 schema_type: self.analyze_empty_union(schema, &mut dependencies)?,
3138 dependencies,
3139 nullable,
3140 description,
3141 default: details.default.clone(),
3142 });
3143 }
3144 if let Some(schema_type) = self.analyze_object_with_variants(
3145 schema,
3146 any_of,
3147 schema_name,
3148 &mut dependencies,
3149 )? {
3150 return Ok(AnalyzedSchema {
3151 name: schema_name.to_string(),
3152 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3153 schema_type,
3154 dependencies,
3155 nullable,
3156 description,
3157 default: details.default.clone(),
3158 });
3159 }
3160 self.analyze_anyof_union(
3162 any_of,
3163 discriminator.as_ref(),
3164 &mut dependencies,
3165 schema_name,
3166 )?
3167 }
3168 Schema::OneOf {
3169 one_of,
3170 discriminator,
3171 ..
3172 } => {
3173 if one_of.is_empty() {
3174 self.analyze_empty_union(schema, &mut dependencies)?
3175 } else if let Some(schema_type) = self.analyze_object_with_variants(
3176 schema,
3177 one_of,
3178 schema_name,
3179 &mut dependencies,
3180 )? {
3181 schema_type
3182 } else {
3183 self.analyze_oneof_union(
3185 one_of,
3186 discriminator.as_ref(),
3187 schema_name,
3188 &mut dependencies,
3189 InlineUnionKind::OneOf,
3190 None,
3191 )?
3192 }
3193 }
3194 Schema::AllOf { all_of, .. } => {
3195 self.analyze_allof_composition(schema, all_of, &mut dependencies)?
3197 }
3198 Schema::Untyped { .. } => {
3199 if let Some(inferred) = schema.inferred_type() {
3201 match inferred {
3202 OpenApiSchemaType::Object => {
3203 if self.should_use_dynamic_json(schema) {
3204 self.untyped_value(
3205 self.untyped_context(""),
3206 UntypedReason::OpaqueObject,
3207 )
3208 } else {
3209 self.analyze_object_schema(schema, &mut dependencies)?
3210 }
3211 }
3212 OpenApiSchemaType::String if details.is_string_enum() => {
3213 SchemaType::StringEnum {
3214 values: details.string_enum_values().unwrap_or_default(),
3215 }
3216 }
3217 OpenApiSchemaType::Null => SchemaType::Primitive {
3220 rust_type: self.type_mapper.null_unit().rust_type,
3221 serde_with: None,
3222 },
3223 _ => self.untyped_value(
3224 self.untyped_context(""),
3225 UntypedReason::UnsupportedTypeKeyword,
3226 ),
3227 }
3228 } else {
3229 self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema)
3230 }
3231 }
3232 };
3233
3234 Ok(AnalyzedSchema {
3235 name: schema_name.to_string(),
3236 original: serde_json::to_value(schema).unwrap_or(Value::Null), schema_type,
3238 dependencies,
3239 nullable,
3240 description,
3241 default: details.default.clone(),
3242 })
3243 }
3244
3245 fn analyze_single_typed_schema(
3251 &mut self,
3252 schema: &Schema,
3253 schema_name: &str,
3254 details: &crate::openapi::SchemaDetails,
3255 dependencies: &mut HashSet<String>,
3256 ) -> Result<SchemaType> {
3257 let primary = schema
3258 .schema_type()
3259 .cloned()
3260 .unwrap_or(OpenApiSchemaType::Object);
3261 let format = details.format.as_deref();
3262 Ok(match primary {
3263 OpenApiSchemaType::String => {
3264 if let Some(values) = details.string_enum_values() {
3265 SchemaType::StringEnum { values }
3266 } else {
3267 let mapped = self.type_mapper.string_format(format);
3268 SchemaType::Primitive {
3269 rust_type: mapped.rust_type,
3270 serde_with: mapped.serde_with,
3271 }
3272 }
3273 }
3274 OpenApiSchemaType::Integer => SchemaType::Primitive {
3275 rust_type: self.integer_rust_type(details),
3276 serde_with: None,
3277 },
3278 OpenApiSchemaType::Number => SchemaType::Primitive {
3279 rust_type: self.type_mapper.number_format(format).rust_type,
3280 serde_with: None,
3281 },
3282 OpenApiSchemaType::Boolean => SchemaType::Primitive {
3283 rust_type: self.type_mapper.boolean().rust_type,
3284 serde_with: None,
3285 },
3286 OpenApiSchemaType::Array => {
3287 self.analyze_array_schema(schema, schema_name, dependencies)?
3288 }
3289 OpenApiSchemaType::Object => {
3290 if self.should_use_dynamic_json(schema) {
3291 self.untyped_value(self.untyped_context(""), UntypedReason::OpaqueObject)
3292 } else {
3293 self.analyze_object_schema(schema, dependencies)?
3294 }
3295 }
3296 OpenApiSchemaType::Null => SchemaType::Primitive {
3299 rust_type: self.type_mapper.null_unit().rust_type,
3300 serde_with: None,
3301 },
3302 })
3303 }
3304
3305 fn analyze_object_schema(
3306 &mut self,
3307 schema: &Schema,
3308 dependencies: &mut HashSet<String>,
3309 ) -> Result<SchemaType> {
3310 let details = schema.details();
3311 let properties = &details.properties;
3312 let required = details
3313 .required
3314 .as_ref()
3315 .map(|req| req.iter().cloned().collect::<HashSet<String>>())
3316 .unwrap_or_default();
3317
3318 let mut property_info = BTreeMap::new();
3319 let owner_name = self
3322 .current_schema_name
3323 .clone()
3324 .unwrap_or_else(|| "Inline".to_string());
3325
3326 if let Some(props) = properties {
3327 for (prop_name, prop_schema) in props {
3328 let prop_type = if let Schema::AnyOf { any_of, .. } = prop_schema {
3330 if Self::union_only_constrains_requiredness(any_of) {
3335 self.analyze_empty_union(prop_schema, dependencies)?
3336 } else if let Some(with_variants) = {
3337 let variant_owner =
3338 format!("{owner_name}{}", self.to_pascal_case(prop_name));
3339 self.with_schema_context(&variant_owner, |analyzer| {
3340 analyzer.analyze_object_with_variants(
3341 prop_schema,
3342 any_of,
3343 &variant_owner,
3344 dependencies,
3345 )
3346 })?
3347 } {
3348 with_variants
3349 } else if self.should_use_dynamic_json(prop_schema) {
3350 self.untyped_value(
3352 self.untyped_context(prop_name),
3353 UntypedReason::OpaqueObject,
3354 )
3355 } else if prop_schema.is_nullable_pattern()
3356 && let Some(non_null) = prop_schema.non_null_variant()
3357 {
3358 self.analyze_property_schema_with_context(
3366 non_null,
3367 Some(prop_name),
3368 dependencies,
3369 )?
3370 } else {
3371 let context_name = self
3374 .current_schema_name
3375 .clone()
3376 .unwrap_or_else(|| "Unknown".to_string());
3377
3378 let prop_pascal = self.to_pascal_case(prop_name);
3380 let preferred_union_name = format!("{context_name}{prop_pascal}");
3381 let union_type_name = self.allocate_inline_schema_name(
3382 &preferred_union_name,
3383 &format!("{preferred_union_name}Union2"),
3384 "property-anyof",
3385 prop_schema,
3386 );
3387
3388 let union_schema_type = self.analyze_anyof_union(
3390 any_of,
3391 prop_schema.discriminator(),
3392 dependencies,
3393 &union_type_name,
3394 )?;
3395
3396 self.resolved_cache.insert(
3398 union_type_name.clone(),
3399 AnalyzedSchema {
3400 name: union_type_name.clone(),
3401 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
3402 dependencies: schema_type_dependencies(&union_schema_type),
3403 schema_type: union_schema_type,
3404 nullable: false,
3405 description: prop_schema.details().description.clone(),
3406 default: None,
3407 },
3408 );
3409
3410 dependencies.insert(union_type_name.clone());
3412 SchemaType::Reference {
3413 target: union_type_name,
3414 }
3415 }
3416 } else if let Schema::OneOf {
3417 one_of,
3418 discriminator,
3419 ..
3420 } = prop_schema
3421 {
3422 if prop_schema.is_nullable_pattern()
3429 && let Some(non_null) = prop_schema.non_null_variant()
3430 {
3431 let unwrapped = self.analyze_property_schema_with_context(
3432 non_null,
3433 Some(prop_name),
3434 dependencies,
3435 )?;
3436 let owner_name = self
3437 .current_schema_name
3438 .clone()
3439 .unwrap_or_else(|| "Inline".to_string());
3440 let unwrapped = self.hoist_inline_property_type(
3441 &owner_name,
3442 prop_name,
3443 unwrapped,
3444 dependencies,
3445 );
3446 let prop_details = prop_schema.details();
3447 let prop_nullable = true;
3448 let prop_description = prop_details.description.clone();
3449 let prop_default = prop_details.default.clone();
3450 property_info.insert(
3451 prop_name.clone(),
3452 PropertyInfo {
3453 schema_type: unwrapped,
3454 nullable: prop_nullable,
3455 description: prop_description,
3456 default: prop_default,
3457 serde_attrs: Vec::new(),
3458 synthesized_required: false,
3459 constraints: PropertyConstraints::from_schema_details(prop_details),
3460 },
3461 );
3462 continue;
3463 }
3464
3465 let context_name = self
3467 .current_schema_name
3468 .clone()
3469 .unwrap_or_else(|| "Unknown".to_string());
3470 let prop_pascal = self.to_pascal_case(prop_name);
3471 let preferred_union_name = format!("{context_name}{prop_pascal}");
3472 let union_type_name = self.allocate_inline_schema_name(
3473 &preferred_union_name,
3474 &format!("{preferred_union_name}Union2"),
3475 "property-oneof",
3476 prop_schema,
3477 );
3478
3479 let union_schema_type = self.analyze_oneof_union(
3481 one_of,
3482 discriminator.as_ref(),
3483 &union_type_name,
3484 dependencies,
3485 InlineUnionKind::OneOf,
3486 None,
3487 )?;
3488
3489 self.resolved_cache.insert(
3491 union_type_name.clone(),
3492 AnalyzedSchema {
3493 name: union_type_name.clone(),
3494 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
3495 schema_type: union_schema_type,
3496 dependencies: HashSet::new(),
3497 nullable: false,
3498 description: prop_schema.details().description.clone(),
3499 default: None,
3500 },
3501 );
3502
3503 dependencies.insert(union_type_name.clone());
3505 SchemaType::Reference {
3506 target: union_type_name,
3507 }
3508 } else {
3509 self.analyze_property_schema_with_context(
3511 prop_schema,
3512 Some(prop_name),
3513 dependencies,
3514 )?
3515 };
3516
3517 let prop_type = self.hoist_inline_property_type(
3518 &owner_name,
3519 prop_name,
3520 prop_type,
3521 dependencies,
3522 );
3523
3524 let prop_details = prop_schema.details();
3525 let prop_nullable = self.schema_or_reference_is_nullable(prop_schema);
3527 let prop_description = prop_details.description.clone();
3528 let prop_default = prop_details.default.clone();
3529
3530 property_info.insert(
3531 prop_name.clone(),
3532 PropertyInfo {
3533 schema_type: prop_type,
3534 nullable: prop_nullable,
3535 description: prop_description,
3536 default: prop_default,
3537 serde_attrs: Vec::new(),
3538 synthesized_required: false,
3539 constraints: PropertyConstraints::from_schema_details(prop_details),
3540 },
3541 );
3542 }
3543 }
3544
3545 let typed_enabled = self
3553 .type_mapper
3554 .config()
3555 .shape
3556 .as_ref()
3557 .and_then(|s| s.additional_properties_typed)
3558 .unwrap_or(true);
3559
3560 let untyped_required_property = || PropertyInfo {
3561 schema_type: SchemaType::Untyped {
3562 shape: UntypedShape::Value,
3563 reason: UntypedReason::AnySchema,
3564 },
3565 nullable: false,
3566 description: None,
3567 default: None,
3568 serde_attrs: Vec::new(),
3569 synthesized_required: true,
3570 constraints: PropertyConstraints::default(),
3571 };
3572 let (mut additional_properties, required_additional_property, explicitly_forbidden) =
3573 match &details.additional_properties {
3574 Some(crate::openapi::AdditionalProperties::Boolean(true)) => (
3575 ObjectAdditionalProperties::Untyped,
3576 Some(untyped_required_property()),
3577 false,
3578 ),
3579 Some(crate::openapi::AdditionalProperties::Boolean(false)) => {
3580 (ObjectAdditionalProperties::Denied, None, true)
3581 }
3582 Some(crate::openapi::AdditionalProperties::Schema(value_schema))
3583 if typed_enabled =>
3584 {
3585 let analyzed = self.analyze_property_schema_with_context(
3586 value_schema,
3587 Some("AdditionalProperty"),
3588 dependencies,
3589 )?;
3590 let nullable_value =
3591 self.nullable_container_value(value_schema, analyzed.clone());
3592 let value_details = value_schema.details();
3593 let required_property = PropertyInfo {
3594 schema_type: analyzed.clone(),
3595 nullable: self.schema_or_reference_is_nullable(value_schema),
3596 description: value_details.description.clone(),
3597 default: None,
3600 serde_attrs: Vec::new(),
3601 synthesized_required: true,
3602 constraints: PropertyConstraints::from_schema_details(value_details),
3603 };
3604 (
3605 ObjectAdditionalProperties::Typed {
3606 value_type: Box::new(nullable_value),
3607 },
3608 Some(required_property),
3609 false,
3610 )
3611 }
3612 Some(crate::openapi::AdditionalProperties::Schema(_)) => (
3613 ObjectAdditionalProperties::Untyped,
3616 Some(untyped_required_property()),
3617 false,
3618 ),
3619 None if Self::object_shape_needs_additional_property_carrier(details) => (
3624 ObjectAdditionalProperties::Untyped,
3625 Some(untyped_required_property()),
3626 false,
3627 ),
3628 None => (
3629 ObjectAdditionalProperties::Closed,
3630 Some(untyped_required_property()),
3631 false,
3632 ),
3633 };
3634
3635 self.finalize_required_object_members(
3636 &mut property_info,
3637 &required,
3638 &mut additional_properties,
3639 required_additional_property,
3640 explicitly_forbidden,
3641 )?;
3642
3643 Ok(SchemaType::Object {
3644 properties: property_info,
3645 variant: None,
3646 required,
3647 additional_properties,
3648 })
3649 }
3650
3651 fn object_shape_needs_additional_property_carrier(
3657 details: &crate::openapi::SchemaDetails,
3658 ) -> bool {
3659 if details.min_properties.is_some() || details.max_properties.is_some() {
3660 return true;
3661 }
3662
3663 let declared = details.properties.as_ref();
3664 let has_undeclared_key = |value: &Value| {
3665 value.as_object().is_some_and(|object| {
3666 object
3667 .keys()
3668 .any(|key| declared.is_none_or(|properties| !properties.contains_key(key)))
3669 })
3670 };
3671 details.example.as_ref().is_some_and(has_undeclared_key)
3672 || details
3673 .examples
3674 .as_ref()
3675 .is_some_and(|examples| examples.iter().any(has_undeclared_key))
3676 }
3677
3678 fn finalize_required_object_members(
3683 &self,
3684 properties: &mut BTreeMap<String, PropertyInfo>,
3685 required: &HashSet<String>,
3686 additional_properties: &mut ObjectAdditionalProperties,
3687 required_additional_property: Option<PropertyInfo>,
3688 explicitly_forbidden: bool,
3689 ) -> Result<()> {
3690 let mut missing = required
3691 .iter()
3692 .filter(|name| !properties.contains_key(*name))
3693 .cloned()
3694 .collect::<Vec<_>>();
3695 missing.sort();
3696 if missing.is_empty() {
3697 return Ok(());
3698 }
3699
3700 if explicitly_forbidden {
3701 let owner = self
3702 .current_schema_name
3703 .as_deref()
3704 .unwrap_or("<inline object>");
3705 return Err(GeneratorError::InvalidSchema(format!(
3706 "object schema `{owner}` is unsatisfiable: required member(s) {} are not declared in properties while additionalProperties: false",
3707 missing
3708 .iter()
3709 .map(|name| format!("`{name}`"))
3710 .collect::<Vec<_>>()
3711 .join(", ")
3712 )));
3713 }
3714
3715 let required_additional_property = required_additional_property.ok_or_else(|| {
3716 GeneratorError::InvalidSchema(
3717 "undeclared required members have no additional-properties carrier".to_string(),
3718 )
3719 })?;
3720 for name in missing {
3721 properties.insert(name, required_additional_property.clone());
3722 }
3723
3724 if !additional_properties.is_open() {
3725 *additional_properties = ObjectAdditionalProperties::Untyped;
3726 }
3727 Ok(())
3728 }
3729
3730 fn build_typed_multi_union_variant(
3740 &mut self,
3741 member_type: OpenApiSchemaType,
3742 schema: &Schema,
3743 union_type_name: &str,
3744 dependencies: &mut HashSet<String>,
3745 ) -> Result<SchemaRef> {
3746 match member_type {
3747 OpenApiSchemaType::Array => {
3748 let preferred_array_type_name = format!("{union_type_name}Array");
3749 let array_type_name = self.allocate_inline_schema_name(
3750 &preferred_array_type_name,
3751 &format!("{preferred_array_type_name}Inline"),
3752 "typed-multi-array",
3753 schema,
3754 );
3755 let array_type =
3756 self.analyze_array_schema(schema, &array_type_name, dependencies)?;
3757 self.resolved_cache.insert(
3758 array_type_name.clone(),
3759 AnalyzedSchema {
3760 name: array_type_name.clone(),
3761 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3762 schema_type: array_type,
3763 dependencies: HashSet::new(),
3764 nullable: false,
3765 description: Some("Array variant in union".to_string()),
3766 default: None,
3767 },
3768 );
3769 dependencies.insert(array_type_name.clone());
3770 Ok(SchemaRef {
3771 target: array_type_name,
3772 nullable: false,
3773 })
3774 }
3775 OpenApiSchemaType::Object => {
3776 let preferred_object_type_name = format!("{union_type_name}Object");
3777 let object_type_name = self.allocate_inline_schema_name(
3778 &preferred_object_type_name,
3779 &format!("{preferred_object_type_name}Inline"),
3780 "typed-multi-object",
3781 schema,
3782 );
3783 if Self::object_shape_is_unconstrained(schema.details()) {
3791 let schema_type = self.untyped_value_map(
3792 self.untyped_context(&object_type_name),
3793 UntypedReason::OpaqueObject,
3794 );
3795 self.resolved_cache.insert(
3796 object_type_name.clone(),
3797 AnalyzedSchema {
3798 name: object_type_name.clone(),
3799 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3800 schema_type,
3801 dependencies: HashSet::new(),
3802 nullable: false,
3803 description: Some("Object variant in union".to_string()),
3804 default: None,
3805 },
3806 );
3807 dependencies.insert(object_type_name.clone());
3808 return Ok(SchemaRef {
3809 target: object_type_name,
3810 nullable: false,
3811 });
3812 }
3813 let object_type = self.add_allocated_object_schema(
3814 object_type_name.clone(),
3815 schema,
3816 dependencies,
3817 )?;
3818 let SchemaType::Reference { target } = object_type else {
3819 unreachable!("allocated object schemas always return a reference");
3820 };
3821 Ok(SchemaRef {
3822 target,
3823 nullable: false,
3824 })
3825 }
3826 _ => Ok(SchemaRef {
3827 target: self.openapi_type_to_rust_type(member_type, schema.details()),
3828 nullable: false,
3829 }),
3830 }
3831 }
3832
3833 fn analyze_property_schema_with_context(
3834 &mut self,
3835 schema: &Schema,
3836 property_name: Option<&str>,
3837 dependencies: &mut HashSet<String>,
3838 ) -> Result<SchemaType> {
3839 if let Schema::Bool(accepts_anything) = schema {
3841 let reason = if *accepts_anything {
3842 UntypedReason::AnySchema
3843 } else {
3844 UntypedReason::NeverMatches
3845 };
3846 return Ok(self.untyped_value(
3847 self.untyped_context(property_name.unwrap_or_default()),
3848 reason,
3849 ));
3850 }
3851
3852 if let Some(ref_str) = self.get_any_reference(schema) {
3853 let target_opt = if ref_str == "#" {
3854 Some(
3855 self.find_recursive_anchor_schema()
3856 .unwrap_or_else(|| "UnknownRecursive".to_string()),
3857 )
3858 } else {
3859 self.extract_schema_name(ref_str).map(|s| s.to_string())
3860 };
3861 match target_opt {
3862 Some(target) => {
3863 dependencies.insert(target.clone());
3864 return Ok(SchemaType::Reference { target });
3865 }
3866 None => {
3867 if let Some(resolved) = self.resolve_pointer_schema(ref_str, dependencies)? {
3872 return Ok(resolved);
3873 }
3874 eprintln!(
3875 "⚠️ unresolvable $ref `{}` — typing as serde_json::Value",
3876 ref_str
3877 );
3878 return Ok(self.untyped_value(
3879 format!("$ref {ref_str}"),
3880 UntypedReason::UnresolvedReference,
3881 ));
3882 }
3883 }
3884 }
3885
3886 if let Some(non_null_types) = schema.non_null_schema_types() {
3890 let context_name = self
3891 .current_schema_name
3892 .clone()
3893 .unwrap_or_else(|| "Unknown".to_string());
3894 let prop_pascal = property_name
3895 .map(|name| self.to_pascal_case(name))
3896 .unwrap_or_default();
3897 let preferred_union_name = format!("{context_name}{prop_pascal}");
3898 let union_type_name = self.allocate_inline_schema_name(
3899 &preferred_union_name,
3900 &format!("{preferred_union_name}Union2"),
3901 "property-typed-multi",
3902 schema,
3903 );
3904
3905 let details = schema.details();
3906 let mut variants = Vec::with_capacity(non_null_types.len());
3907 for t in non_null_types {
3908 variants.push(self.build_typed_multi_union_variant(
3909 t,
3910 schema,
3911 &union_type_name,
3912 dependencies,
3913 )?);
3914 }
3915
3916 self.resolved_cache.insert(
3917 union_type_name.clone(),
3918 AnalyzedSchema {
3919 name: union_type_name.clone(),
3920 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3921 schema_type: SchemaType::Union {
3922 variants,
3923 exclusive: false,
3924 },
3925 dependencies: HashSet::new(),
3926 nullable: false,
3927 description: details.description.clone(),
3928 default: None,
3929 },
3930 );
3931
3932 dependencies.insert(union_type_name.clone());
3933 return Ok(SchemaType::Reference {
3934 target: union_type_name,
3935 });
3936 }
3937
3938 if let Some(schema_type) = schema.schema_type() {
3939 match schema_type {
3940 OpenApiSchemaType::String => {
3941 if let Some(enum_values) = schema.details().string_enum_values() {
3943 let context_name = self
3946 .current_schema_name
3947 .clone()
3948 .unwrap_or_else(|| "Unknown".to_string());
3949
3950 let primary_name = if let Some(prop_name) = property_name {
3952 let prop_pascal = self.to_pascal_case(prop_name);
3954 format!("{context_name}{prop_pascal}")
3955 } else {
3956 let suffix = if !enum_values.is_empty() {
3959 let first_value = self.to_pascal_case(&enum_values[0]);
3960 format!("{first_value}Enum")
3961 } else {
3962 "StringEnum".to_string()
3963 };
3964 format!("{context_name}{suffix}")
3965 };
3966
3967 return Ok(self.hoist_inline_string_enum(
3968 schema,
3969 enum_values,
3970 primary_name,
3971 dependencies,
3972 ));
3973 } else {
3974 let mapped = self
3980 .type_mapper
3981 .string_format(schema.details().format.as_deref());
3982 return Ok(SchemaType::Primitive {
3983 rust_type: mapped.rust_type,
3984 serde_with: mapped.serde_with,
3985 });
3986 }
3987 }
3988 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
3989 let details = schema.details();
3990 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
3991 return Ok(SchemaType::Primitive {
3992 rust_type,
3993 serde_with: None,
3994 });
3995 }
3996 OpenApiSchemaType::Boolean => {
3997 return Ok(SchemaType::Primitive {
3998 rust_type: "bool".to_string(),
3999 serde_with: None,
4000 });
4001 }
4002 OpenApiSchemaType::Array => {
4003 let context_name = if let Some(prop_name) = property_name {
4005 let prop_pascal = self.to_pascal_case(prop_name);
4007 format!(
4008 "{}{}",
4009 self.current_schema_name.as_deref().unwrap_or("Unknown"),
4010 prop_pascal
4011 )
4012 } else {
4013 "ArrayItem".to_string()
4015 };
4016 return self.analyze_array_schema(schema, &context_name, dependencies);
4017 }
4018 OpenApiSchemaType::Object => {
4019 if self.should_use_dynamic_json(schema) {
4021 return Ok(self
4022 .untyped_value(self.untyped_context(""), UntypedReason::OpaqueObject));
4023 }
4024 let preferred_object_type_name = if let Some(prop_name) = property_name {
4026 let prop_pascal = self.to_pascal_case(prop_name);
4028 format!(
4029 "{}{}",
4030 self.current_schema_name.as_deref().unwrap_or("Unknown"),
4031 prop_pascal
4032 )
4033 } else {
4034 format!(
4036 "{}Object",
4037 self.current_schema_name.as_deref().unwrap_or("Unknown")
4038 )
4039 };
4040 let object_type_name = self.allocate_inline_schema_name(
4041 &preferred_object_type_name,
4042 &format!("{preferred_object_type_name}Inline"),
4043 "property-object",
4044 schema,
4045 );
4046
4047 return self.add_allocated_object_schema(
4048 object_type_name,
4049 schema,
4050 dependencies,
4051 );
4052 }
4053 OpenApiSchemaType::Null => {
4056 return Ok(SchemaType::Primitive {
4057 rust_type: self.type_mapper.null_unit().rust_type,
4058 serde_with: None,
4059 });
4060 }
4061 }
4062 }
4063
4064 if schema.is_nullable_pattern() {
4066 if let Some(non_null) = schema.non_null_variant() {
4067 return self.analyze_property_schema_with_context(
4068 non_null,
4069 property_name,
4070 dependencies,
4071 );
4072 }
4073 }
4074
4075 if self.should_use_dynamic_json(schema) {
4077 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::OpaqueObject));
4078 }
4079
4080 if let Schema::AllOf { all_of, .. } = schema {
4082 if let Some(property_name) = property_name {
4083 let owner = self
4084 .current_schema_name
4085 .clone()
4086 .unwrap_or_else(|| "Inline".to_string());
4087 let composition_name = format!("{owner}{}", self.to_pascal_case(property_name));
4088 return self.with_schema_context(&composition_name, |analyzer| {
4089 analyzer.analyze_allof_composition(schema, all_of, dependencies)
4090 });
4091 }
4092 return self.analyze_allof_composition(schema, all_of, dependencies);
4093 }
4094
4095 if let Some(variants) = schema.union_variants() {
4097 match variants.len().cmp(&1) {
4098 std::cmp::Ordering::Equal => {
4099 return self.analyze_property_schema_with_context(
4101 &variants[0],
4102 property_name,
4103 dependencies,
4104 );
4105 }
4106 std::cmp::Ordering::Greater => {
4107 let union_name = if let Some(prop_name) = property_name {
4110 let prop_pascal = self.to_pascal_case(prop_name);
4112 format!(
4113 "{}{}",
4114 self.current_schema_name.as_deref().unwrap_or(""),
4115 prop_pascal
4116 )
4117 } else {
4118 "UnionType".to_string()
4119 };
4120
4121 if let Schema::OneOf {
4123 one_of,
4124 discriminator,
4125 ..
4126 } = schema
4127 {
4128 let union_name = self.allocate_inline_schema_name(
4129 &union_name,
4130 &format!("{union_name}Union2"),
4131 "fallback-property-oneof",
4132 schema,
4133 );
4134 let oneof_result = self.analyze_oneof_union(
4136 one_of,
4137 discriminator.as_ref(),
4138 &union_name,
4139 dependencies,
4140 InlineUnionKind::OneOf,
4141 None,
4142 )?;
4143
4144 if let SchemaType::Union { .. } = &oneof_result {
4146 self.resolved_cache.insert(
4148 union_name.clone(),
4149 AnalyzedSchema {
4150 name: union_name.clone(),
4151 original: serde_json::to_value(schema).unwrap_or(Value::Null),
4152 schema_type: oneof_result.clone(),
4153 dependencies: dependencies.clone(),
4154 nullable: false,
4155 description: schema.details().description.clone(),
4156 default: None,
4157 },
4158 );
4159
4160 dependencies.insert(union_name.clone());
4162 return Ok(SchemaType::Reference { target: union_name });
4163 }
4164
4165 return Ok(oneof_result);
4166 } else if let Schema::AnyOf {
4167 any_of,
4168 discriminator,
4169 ..
4170 } = schema
4171 {
4172 let union_analysis = self.analyze_anyof_union(
4174 any_of,
4175 discriminator.as_ref(),
4176 dependencies,
4177 &union_name,
4178 )?;
4179 return Ok(union_analysis);
4180 } else {
4181 let mut union_variants = Vec::new();
4184 for variant in variants {
4185 if let Some(ref_str) = variant.reference() {
4186 if let Some(target) = self.extract_schema_name(ref_str) {
4187 dependencies.insert(target.to_string());
4188 union_variants.push(SchemaRef {
4189 target: target.to_string(),
4190 nullable: false,
4191 });
4192 }
4193 }
4194 }
4195 return Ok(SchemaType::Union {
4196 variants: union_variants,
4197 exclusive: false,
4198 });
4199 }
4200 }
4201 std::cmp::Ordering::Less => {}
4202 }
4203 }
4204
4205 if let Some(inferred_type) = schema.inferred_type() {
4207 match inferred_type {
4208 OpenApiSchemaType::Object => {
4209 if self.should_use_dynamic_json(schema) {
4211 return Ok(self
4212 .untyped_value(self.untyped_context(""), UntypedReason::OpaqueObject));
4213 }
4214 let owner = self
4215 .current_schema_name
4216 .clone()
4217 .unwrap_or_else(|| "Unknown".to_string());
4218 let preferred_name = property_name.map_or_else(
4219 || format!("{owner}Object"),
4220 |property_name| format!("{owner}{}", self.to_pascal_case(property_name)),
4221 );
4222 let object_type_name = self.allocate_inline_schema_name(
4223 &preferred_name,
4224 &format!("{preferred_name}Inline"),
4225 "property-object",
4226 schema,
4227 );
4228 return self.add_allocated_object_schema(
4229 object_type_name,
4230 schema,
4231 dependencies,
4232 );
4233 }
4234 OpenApiSchemaType::Array => {
4235 let context_name = if let Some(prop_name) = property_name {
4236 let prop_pascal = self.to_pascal_case(prop_name);
4238 format!(
4239 "{}{}",
4240 self.current_schema_name.as_deref().unwrap_or("Unknown"),
4241 prop_pascal
4242 )
4243 } else {
4244 "ArrayItem".to_string()
4246 };
4247 return self.analyze_array_schema(schema, &context_name, dependencies);
4248 }
4249 OpenApiSchemaType::String => {
4250 if let Some(enum_values) = schema.details().string_enum_values() {
4251 return Ok(SchemaType::StringEnum {
4252 values: enum_values,
4253 });
4254 } else {
4255 return Ok(SchemaType::Primitive {
4256 rust_type: "String".to_string(),
4257 serde_with: None,
4258 });
4259 }
4260 }
4261 _ => {
4262 let rust_type = self.openapi_type_to_rust_type(inferred_type, schema.details());
4264 return Ok(SchemaType::Primitive {
4265 rust_type,
4266 serde_with: None,
4267 });
4268 }
4269 }
4270 }
4271
4272 Ok(self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema))
4273 }
4274
4275 fn analyze_allof_composition(
4276 &mut self,
4277 owner_schema: &Schema,
4278 all_of_schemas: &[Schema],
4279 dependencies: &mut HashSet<String>,
4280 ) -> Result<SchemaType> {
4281 if let Some(carrier) = Self::single_non_object_allof_carrier(all_of_schemas) {
4286 return self.analyze_property_schema_with_context(carrier, None, dependencies);
4287 }
4288
4289 let referenced_targets = all_of_schemas
4294 .iter()
4295 .filter_map(|schema| schema.reference())
4296 .filter_map(|reference| self.extract_schema_name(reference))
4297 .collect::<Vec<_>>();
4298 let only_reference_and_annotations = all_of_schemas
4299 .iter()
4300 .all(|schema| schema.reference().is_some() || schema_is_annotation_only(schema));
4301 if referenced_targets.len() == 1 && only_reference_and_annotations {
4302 let target = referenced_targets[0];
4303 dependencies.insert(target.to_string());
4304 return Ok(SchemaType::Reference {
4305 target: target.to_string(),
4306 });
4307 }
4308
4309 if let [only] = all_of_schemas
4313 && !matches!(
4314 only.schema_type(),
4315 Some(OpenApiSchemaType::Object) | Some(OpenApiSchemaType::Null)
4316 )
4317 && only.details().properties.is_none()
4318 {
4319 return self.analyze_property_schema_with_context(only, None, dependencies);
4320 }
4321
4322 let mut merged_properties = BTreeMap::new();
4324 let mut merged_required = HashSet::new();
4325 let mut merged_variant = None;
4326 let mut descriptions = Vec::new();
4327
4328 let current_context = self.current_schema_name.clone();
4330 let owner_name = current_context.as_deref().unwrap_or("InlineComposition");
4331
4332 self.merge_schema_into_properties(
4336 owner_schema,
4337 &mut merged_properties,
4338 &mut merged_required,
4339 dependencies,
4340 )?;
4341
4342 for (member_index, schema) in all_of_schemas.iter().enumerate() {
4343 match schema {
4344 Schema::Reference { reference, .. } => {
4345 let (analyzed_type, analyzed_name, raw_target) =
4346 if let Some(target) = self.extract_schema_name(reference) {
4347 dependencies.insert(target.to_string());
4348 let analyzed_ref = self.analyze_schema(target)?;
4349 (
4350 Some(analyzed_ref.schema_type),
4351 Some(target.to_string()),
4352 self.schemas.get(target).cloned(),
4353 )
4354 } else {
4355 (
4356 self.resolve_pointer_schema(reference, dependencies)?,
4357 None,
4358 self.reference_target_schema(reference),
4359 )
4360 };
4361
4362 let merged = if let Some(analyzed_type) = analyzed_type {
4363 self.merge_analyzed_object_properties(
4364 &analyzed_type,
4365 analyzed_name.as_deref(),
4366 &mut merged_properties,
4367 &mut merged_required,
4368 &mut merged_variant,
4369 owner_name,
4370 )?
4371 } else {
4372 false
4373 };
4374 if !merged && let Some(raw_target) = raw_target {
4375 self.merge_schema_into_properties(
4376 &raw_target,
4377 &mut merged_properties,
4378 &mut merged_required,
4379 dependencies,
4380 )?;
4381 }
4382 }
4383 Schema::AnyOf { any_of, .. }
4384 if Self::union_only_constrains_requiredness(any_of) => {}
4385 Schema::OneOf { one_of, .. }
4386 if Self::union_only_constrains_requiredness(one_of) => {}
4387 Schema::AnyOf { .. } | Schema::OneOf { .. } => {
4388 let preferred_name = format!("{owner_name}AllOfVariant{}", member_index + 1);
4389 let variant_name =
4390 self.add_inline_schema(&preferred_name, schema, dependencies)?;
4391 dependencies.insert(variant_name.clone());
4392 let analyzed_type = self
4393 .resolved_cache
4394 .get(&variant_name)
4395 .map(|analyzed| analyzed.schema_type.clone())
4396 .ok_or_else(|| {
4397 GeneratorError::InvalidSchema(format!(
4398 "allOf union member `{variant_name}` was not analyzed"
4399 ))
4400 })?;
4401 if !self.merge_analyzed_object_properties(
4402 &analyzed_type,
4403 Some(&variant_name),
4404 &mut merged_properties,
4405 &mut merged_required,
4406 &mut merged_variant,
4407 owner_name,
4408 )? {
4409 return Ok(self.untyped_value(
4410 self.untyped_context(""),
4411 UntypedReason::UnrepresentableComposition,
4412 ));
4413 }
4414 }
4415 Schema::Typed {
4416 schema_type: OpenApiSchemaType::Object,
4417 ..
4418 }
4419 | Schema::Untyped { .. } => {
4420 let saved_context = self.current_schema_name.clone();
4422 self.current_schema_name = current_context.clone();
4423
4424 self.merge_schema_into_properties(
4426 schema,
4427 &mut merged_properties,
4428 &mut merged_required,
4429 dependencies,
4430 )?;
4431
4432 self.current_schema_name = saved_context;
4434 }
4435 _ => {
4436 self.merge_schema_into_properties(
4439 schema,
4440 &mut merged_properties,
4441 &mut merged_required,
4442 dependencies,
4443 )?;
4444 }
4445 }
4446
4447 if let Some(desc) = &schema.details().description {
4449 descriptions.push(desc.clone());
4450 }
4451 }
4452
4453 if !merged_properties.is_empty() || !merged_required.is_empty() || merged_variant.is_some()
4458 {
4459 let mut additional_properties = if merged_properties
4460 .values()
4461 .any(|property| property.synthesized_required)
4462 {
4463 ObjectAdditionalProperties::Untyped
4464 } else {
4465 ObjectAdditionalProperties::Closed
4466 };
4467 self.finalize_required_object_members(
4468 &mut merged_properties,
4469 &merged_required,
4470 &mut additional_properties,
4471 Some(PropertyInfo {
4472 schema_type: SchemaType::Untyped {
4473 shape: UntypedShape::Value,
4474 reason: UntypedReason::AnySchema,
4475 },
4476 nullable: false,
4477 description: None,
4478 default: None,
4479 serde_attrs: Vec::new(),
4480 synthesized_required: true,
4481 constraints: PropertyConstraints::default(),
4482 }),
4483 false,
4484 )?;
4485 Ok(SchemaType::Object {
4486 properties: merged_properties,
4487 required: merged_required,
4488 additional_properties,
4489 variant: merged_variant,
4490 })
4491 } else {
4492 let schemas = all_of_schemas
4493 .iter()
4494 .filter_map(|schema| {
4495 let reference = schema.reference()?;
4496 let target = self.extract_schema_name(reference)?;
4497 dependencies.insert(target.to_string());
4498 Some(SchemaRef {
4499 target: target.to_string(),
4500 nullable: false,
4501 })
4502 })
4503 .collect::<Vec<_>>();
4504 let references_are_objects = all_of_schemas
4510 .iter()
4511 .filter(|schema| schema.reference().is_some())
4512 .all(|schema| self.branch_resolves_to_object(schema));
4513 if schemas.is_empty() || !references_are_objects {
4514 Ok(self.untyped_value(
4515 self.untyped_context(""),
4516 UntypedReason::UnrepresentableComposition,
4517 ))
4518 } else {
4519 Ok(SchemaType::Composition { schemas })
4520 }
4521 }
4522 }
4523
4524 fn single_non_object_allof_carrier(all_of_schemas: &[Schema]) -> Option<&Schema> {
4529 let mut meaningful = all_of_schemas
4530 .iter()
4531 .filter(|schema| !schema_is_annotation_only(schema));
4532 let carrier = meaningful.next()?;
4533 if meaningful.next().is_some() {
4534 return None;
4535 }
4536
4537 match carrier {
4538 Schema::Typed {
4539 schema_type:
4540 OpenApiSchemaType::String
4541 | OpenApiSchemaType::Integer
4542 | OpenApiSchemaType::Number
4543 | OpenApiSchemaType::Boolean
4544 | OpenApiSchemaType::Array,
4545 ..
4546 } => Some(carrier),
4547 Schema::TypedMulti { schema_types, .. } => {
4548 let mut non_null = schema_types
4549 .iter()
4550 .filter(|schema_type| **schema_type != OpenApiSchemaType::Null);
4551 let only = non_null.next()?;
4552 (non_null.next().is_none()
4553 && matches!(
4554 only,
4555 OpenApiSchemaType::String
4556 | OpenApiSchemaType::Integer
4557 | OpenApiSchemaType::Number
4558 | OpenApiSchemaType::Boolean
4559 | OpenApiSchemaType::Array
4560 ))
4561 .then_some(carrier)
4562 }
4563 _ => None,
4564 }
4565 }
4566
4567 fn merge_analyzed_object_properties(
4572 &mut self,
4573 schema_type: &SchemaType,
4574 named_target: Option<&str>,
4575 merged_properties: &mut BTreeMap<String, PropertyInfo>,
4576 merged_required: &mut HashSet<String>,
4577 merged_variant: &mut Option<SchemaRef>,
4578 owner_name: &str,
4579 ) -> Result<bool> {
4580 let mut current = schema_type.clone();
4581 let mut visited = HashSet::new();
4582 let mut variant_target = named_target.map(str::to_string);
4583 loop {
4584 match current {
4585 SchemaType::Object {
4586 properties,
4587 required,
4588 variant,
4589 ..
4590 } => {
4591 for (name, property) in properties {
4592 let keep_declared_sibling =
4593 merged_properties.get(&name).is_some_and(|existing| {
4594 !existing.synthesized_required && property.synthesized_required
4595 });
4596 if !keep_declared_sibling {
4597 merged_properties.insert(name, property);
4598 }
4599 }
4600 merged_required.extend(required);
4601 if let Some(variant) = variant {
4602 Self::merge_allof_variant(merged_variant, variant, owner_name)?;
4603 }
4604 return Ok(true);
4605 }
4606 SchemaType::Reference { target } => {
4607 if !visited.insert(target.clone()) {
4608 return Ok(false);
4609 }
4610 if variant_target.is_none() {
4611 variant_target = Some(target.clone());
4612 }
4613 current = if let Some(analyzed) = self.resolved_cache.get(&target) {
4614 analyzed.schema_type.clone()
4615 } else if self.schemas.contains_key(&target) {
4616 self.analyze_schema(&target)?.schema_type
4617 } else {
4618 return Ok(false);
4619 };
4620 }
4621 SchemaType::Union { .. } | SchemaType::DiscriminatedUnion { .. } => {
4622 let Some(target) = variant_target else {
4623 return Ok(false);
4624 };
4625 Self::merge_allof_variant(
4626 merged_variant,
4627 SchemaRef {
4628 target,
4629 nullable: false,
4630 },
4631 owner_name,
4632 )?;
4633 return Ok(true);
4634 }
4635 _ => return Ok(false),
4636 }
4637 }
4638 }
4639
4640 fn merge_allof_variant(
4641 merged_variant: &mut Option<SchemaRef>,
4642 candidate: SchemaRef,
4643 owner_name: &str,
4644 ) -> Result<()> {
4645 match merged_variant {
4646 Some(existing) if existing.target == candidate.target => Ok(()),
4647 Some(existing) => Err(GeneratorError::InvalidSchema(format!(
4648 "allOf object `{owner_name}` intersects multiple union members (`{}` and `{}`), which cannot be represented by one flattened variant",
4649 existing.target, candidate.target
4650 ))),
4651 None => {
4652 *merged_variant = Some(candidate);
4653 Ok(())
4654 }
4655 }
4656 }
4657
4658 fn merge_schema_into_properties(
4659 &mut self,
4660 schema: &Schema,
4661 merged_properties: &mut BTreeMap<String, PropertyInfo>,
4662 merged_required: &mut HashSet<String>,
4663 dependencies: &mut HashSet<String>,
4664 ) -> Result<()> {
4665 let details = schema.details();
4666
4667 if let Some(properties) = &details.properties {
4669 for (prop_name, prop_schema) in properties {
4670 let prop_type = self.analyze_property_schema_with_context(
4671 prop_schema,
4672 Some(prop_name),
4673 dependencies,
4674 )?;
4675 let owner_name = self
4676 .current_schema_name
4677 .clone()
4678 .unwrap_or_else(|| "Inline".to_string());
4679 let prop_type = self.hoist_inline_property_type(
4680 &owner_name,
4681 prop_name,
4682 prop_type,
4683 dependencies,
4684 );
4685 let prop_details = prop_schema.details();
4686
4687 let nullable = self.schema_or_reference_is_nullable(prop_schema);
4694 merged_properties.insert(
4695 prop_name.clone(),
4696 PropertyInfo {
4697 schema_type: prop_type,
4698 nullable,
4699 description: prop_details.description.clone(),
4700 default: prop_details.default.clone(),
4701 serde_attrs: Vec::new(),
4702 synthesized_required: false,
4703 constraints: PropertyConstraints::from_schema_details(prop_details),
4704 },
4705 );
4706 }
4707 }
4708
4709 if let Some(required) = &details.required {
4711 for field in required {
4712 merged_required.insert(field.clone());
4713 }
4714 }
4715
4716 Ok(())
4717 }
4718
4719 fn analyze_oneof_union(
4720 &mut self,
4721 one_of_schemas: &[Schema],
4722 discriminator: Option<&crate::openapi::Discriminator>,
4723 parent_name: &str,
4724 dependencies: &mut HashSet<String>,
4725 union_kind: InlineUnionKind,
4726 source_indices: Option<&[usize]>,
4727 ) -> Result<SchemaType> {
4728 let default_indices = (0..one_of_schemas.len()).collect::<Vec<_>>();
4729 let source_indices = source_indices
4730 .filter(|indices| indices.len() == one_of_schemas.len())
4731 .unwrap_or(&default_indices);
4732
4733 let expanded_branches;
4735 let one_of_schemas = match self.expand_pointer_branches(one_of_schemas) {
4736 Some(expanded) => {
4737 expanded_branches = expanded;
4738 expanded_branches.as_slice()
4739 }
4740 None => one_of_schemas,
4741 };
4742
4743 let boolean_resolved;
4745 let boolean_resolved_indices;
4746 let one_of_schemas = match Self::resolve_boolean_branches(one_of_schemas) {
4747 Ok(resolved) => {
4748 boolean_resolved_indices = one_of_schemas
4749 .iter()
4750 .zip(source_indices.iter().copied())
4751 .filter_map(|(branch, index)| {
4752 (!matches!(branch, Schema::Bool(false))).then_some(index)
4753 })
4754 .collect::<Vec<_>>();
4755 boolean_resolved = resolved;
4756 boolean_resolved.as_slice()
4757 }
4758 Err(()) => {
4759 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema));
4760 }
4761 };
4762 let source_indices = boolean_resolved_indices.as_slice();
4763 if one_of_schemas.is_empty() {
4764 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::NeverMatches));
4765 }
4766
4767 if let [only] = one_of_schemas {
4772 return self
4773 .analyze_schema_value(only, parent_name)
4774 .map(|analyzed| analyzed.schema_type);
4775 }
4776 if let Some(shared) = self.shared_branch_type(one_of_schemas) {
4777 return Ok(shared);
4778 }
4779
4780 if one_of_schemas.len() == 2 {
4783 let null_count = one_of_schemas
4784 .iter()
4785 .filter(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
4786 .count();
4787 if null_count == 1 {
4788 if let Some(non_null) = one_of_schemas
4789 .iter()
4790 .find(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
4791 {
4792 return self
4793 .analyze_schema_value(non_null, parent_name)
4794 .map(|a| a.schema_type);
4795 }
4796 }
4797 }
4798
4799 if discriminator.is_none()
4803 || one_of_schemas
4804 .iter()
4805 .any(|schema| self.schema_or_reference_is_nullable(schema))
4806 {
4807 return self.analyze_untagged_oneof_union(
4809 one_of_schemas,
4810 parent_name,
4811 dependencies,
4812 union_kind,
4813 source_indices,
4814 None,
4815 );
4816 }
4817
4818 if one_of_schemas
4824 .iter()
4825 .any(|s| !self.branch_resolves_to_object(s))
4826 {
4827 return self.analyze_untagged_oneof_union(
4828 one_of_schemas,
4829 parent_name,
4830 dependencies,
4831 union_kind,
4832 source_indices,
4833 discriminator,
4834 );
4835 }
4836
4837 let discriminator_field = discriminator
4839 .ok_or_else(|| {
4840 GeneratorError::InvalidDiscriminator(
4841 "expected discriminator after guard check".to_string(),
4842 )
4843 })?
4844 .property_name
4845 .clone();
4846
4847 if one_of_schemas.iter().any(|branch| {
4851 self.extract_discriminator_value_domain_for_field(branch, &discriminator_field)
4852 .is_some_and(|values| values.is_empty())
4853 }) {
4854 eprintln!(
4855 "⚠️ discriminated union `{parent_name}` has a branch with contradictory `{discriminator_field}` constraints; using structural union fallback"
4856 );
4857 return self.analyze_untagged_oneof_union(
4858 one_of_schemas,
4859 parent_name,
4860 dependencies,
4861 union_kind,
4862 source_indices,
4863 discriminator,
4864 );
4865 }
4866
4867 let mut variants = Vec::new();
4868 let mut used_variant_names = std::collections::HashSet::new();
4869
4870 for (variant_schema, original_index) in
4871 one_of_schemas.iter().zip(source_indices.iter().copied())
4872 {
4873 let ref_info = if let Some(ref_str) = variant_schema.reference() {
4875 Some((ref_str, false))
4876 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
4877 Some((recursive_ref, true))
4878 } else if let Schema::AllOf { all_of, .. } = variant_schema {
4879 if all_of.len() == 1 {
4881 if let Some(ref_str) = all_of[0].reference() {
4882 Some((ref_str, false))
4883 } else {
4884 all_of[0]
4885 .recursive_reference()
4886 .map(|recursive_ref| (recursive_ref, true))
4887 }
4888 } else {
4889 None
4890 }
4891 } else {
4892 None
4893 };
4894
4895 if let Some((ref_str, is_recursive)) = ref_info {
4896 let schema_name = if is_recursive && ref_str == "#" {
4897 self.find_recursive_anchor_schema()
4899 .or_else(|| self.current_schema_name.clone())
4900 .unwrap_or_else(|| "CompoundFilter".to_string())
4901 } else {
4902 self.extract_schema_name(ref_str)
4903 .map(|s| s.to_string())
4904 .unwrap_or_else(|| "UnknownRef".to_string())
4905 };
4906
4907 if !schema_name.is_empty() {
4908 dependencies.insert(schema_name.clone());
4909
4910 let mut discriminator_values = Vec::new();
4914 let allowed_domain = self.schemas.get(&schema_name).and_then(|ref_schema| {
4915 self.extract_discriminator_value_domain_for_field(
4916 ref_schema,
4917 &discriminator_field,
4918 )
4919 });
4920 if let Some(mappings) = discriminator.and_then(|disc| disc.mapping.as_ref()) {
4921 for (key, target_ref) in mappings {
4922 if target_ref == ref_str
4923 || self.extract_schema_name(target_ref)
4924 == Some(schema_name.as_str())
4925 {
4926 if allowed_domain
4927 .as_ref()
4928 .is_some_and(|allowed| !allowed.contains(key))
4929 {
4930 let allowed = allowed_domain
4931 .as_ref()
4932 .map(|values| values.join("`, `"))
4933 .unwrap_or_default();
4934 eprintln!(
4935 "⚠️ discriminator mapping conflict in union `{parent_name}`: key `{key}` targets `{schema_name}` but branch allows `{allowed}`; ignoring mapping key"
4936 );
4937 } else {
4938 Self::push_unique_string(&mut discriminator_values, key);
4939 }
4940 }
4941 }
4942 }
4943 if let Some(allowed_domain) = allowed_domain {
4944 for value in allowed_domain {
4945 Self::push_unique_string(&mut discriminator_values, &value);
4946 }
4947 }
4948 if discriminator_values.is_empty() {
4949 discriminator_values
4950 .push(self.generate_discriminator_value_from_name(&schema_name));
4951 }
4952 let discriminator_value = discriminator_values[0].clone();
4953 let (discriminator_field_declared, discriminator_field_required) = self
4954 .schemas
4955 .get(&schema_name)
4956 .map(|schema| {
4957 self.discriminator_property_presence(schema, &discriminator_field)
4958 })
4959 .unwrap_or((false, false));
4960
4961 let base_name = self.to_rust_variant_name(&schema_name);
4963 let rust_name =
4964 self.ensure_unique_variant_name(base_name, &mut used_variant_names);
4965
4966 let final_discriminator_value = discriminator_value;
4968
4969 variants.push(UnionVariant {
4970 rust_name,
4971 type_name: schema_name,
4972 discriminator_value: final_discriminator_value,
4973 preferred_discriminator_values: discriminator_values.clone(),
4974 discriminator_values,
4975 discriminator_field_declared,
4976 discriminator_field_required,
4977 schema_ref: ref_str.to_string(),
4978 });
4979 }
4980 } else {
4981 let variant_index = original_index;
4983 let inline_type_name =
4984 self.generate_inline_type_name(variant_schema, variant_index);
4985
4986 let mut discriminator_values = Vec::new();
4989 let allowed_domain = self.extract_discriminator_value_domain_for_field(
4990 variant_schema,
4991 &discriminator_field,
4992 );
4993 if let Some(mappings) = discriminator.and_then(|disc| disc.mapping.as_ref()) {
4994 for (key, target_ref) in mappings {
4995 if target_ref.contains(&format!("variant_{variant_index}")) {
4996 if allowed_domain
4997 .as_ref()
4998 .is_some_and(|allowed| !allowed.contains(key))
4999 {
5000 let allowed = allowed_domain
5001 .as_ref()
5002 .map(|values| values.join("`, `"))
5003 .unwrap_or_default();
5004 eprintln!(
5005 "⚠️ discriminator mapping conflict in union `{parent_name}`: key `{key}` targets `{inline_type_name}` but branch allows `{allowed}`; ignoring mapping key"
5006 );
5007 } else {
5008 Self::push_unique_string(&mut discriminator_values, key);
5009 }
5010 }
5011 }
5012 }
5013 if let Some(allowed_domain) = allowed_domain {
5014 for value in allowed_domain {
5015 Self::push_unique_string(&mut discriminator_values, &value);
5016 }
5017 }
5018 if discriminator_values.is_empty() {
5019 discriminator_values.push(format!("variant_{variant_index}"));
5020 }
5021 let discriminator_value = discriminator_values[0].clone();
5022 let (discriminator_field_declared, discriminator_field_required) =
5023 self.discriminator_property_presence(variant_schema, &discriminator_field);
5024
5025 let base_name = if discriminator_value.starts_with("variant_") {
5027 format!("Variant{variant_index}")
5028 } else {
5029 let clean_name = self.discriminator_to_variant_name(&discriminator_value);
5031 self.to_rust_variant_name(&clean_name)
5032 };
5033 let rust_name = self.ensure_unique_variant_name(base_name, &mut used_variant_names);
5034
5035 let final_discriminator_value = discriminator_value;
5037
5038 let inline_type_name = self.add_inline_union_branch_schema(
5041 &inline_type_name,
5042 variant_schema,
5043 dependencies,
5044 parent_name,
5045 union_kind,
5046 original_index,
5047 Some(&final_discriminator_value),
5048 )?;
5049
5050 variants.push(UnionVariant {
5051 rust_name,
5052 type_name: inline_type_name.clone(),
5053 discriminator_value: final_discriminator_value,
5054 preferred_discriminator_values: discriminator_values.clone(),
5055 discriminator_values,
5056 discriminator_field_declared,
5057 discriminator_field_required,
5058 schema_ref: format!("inline_{variant_index}"),
5059 });
5060 }
5061 }
5062
5063 self.disambiguate_shared_discriminator_values(&mut variants, discriminator);
5064
5065 if variants.is_empty() {
5066 let mut union_variants = Vec::new();
5069
5070 for (variant_schema, original_index) in
5071 one_of_schemas.iter().zip(source_indices.iter().copied())
5072 {
5073 if let Some(ref_str) = variant_schema.reference() {
5075 if let Some(schema_name) = self.extract_schema_name(ref_str) {
5076 dependencies.insert(schema_name.to_string());
5077 union_variants.push(SchemaRef {
5078 target: schema_name.to_string(),
5079 nullable: false,
5080 });
5081 }
5082 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
5083 let schema_name = if recursive_ref == "#" {
5084 self.find_recursive_anchor_schema()
5086 .or_else(|| self.current_schema_name.clone())
5087 .unwrap_or_else(|| "CompoundFilter".to_string())
5088 } else {
5089 self.extract_schema_name(recursive_ref)
5090 .map(|s| s.to_string())
5091 .unwrap_or_else(|| "RecursiveType".to_string())
5092 };
5093 dependencies.insert(schema_name.clone());
5094 union_variants.push(SchemaRef {
5095 target: schema_name,
5096 nullable: false,
5097 });
5098 } else {
5099 let branch_discriminator = self.inline_union_branch_discriminator_value(
5100 variant_schema,
5101 discriminator,
5102 original_index,
5103 );
5104 let inline_name = self.generate_context_aware_name(
5106 parent_name,
5107 "InlineVariant",
5108 original_index,
5109 Some(variant_schema),
5110 );
5111 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
5112 let variant_type = analyzed.schema_type;
5113
5114 for dep in &analyzed.dependencies {
5116 dependencies.insert(dep.clone());
5117 }
5118
5119 match &variant_type {
5120 SchemaType::Primitive { rust_type, .. } => {
5122 union_variants.push(SchemaRef {
5123 target: rust_type.clone(),
5124 nullable: false,
5125 });
5126 }
5127 SchemaType::Array { item_type } => {
5129 match item_type.as_ref() {
5130 SchemaType::Primitive { rust_type, .. } => {
5131 let type_name = format!("Vec<{rust_type}>");
5132 union_variants.push(SchemaRef {
5133 target: type_name,
5134 nullable: false,
5135 });
5136 }
5137 SchemaType::Reference { target } => {
5138 let type_name = format!("Vec<{target}>");
5139 union_variants.push(SchemaRef {
5140 target: type_name,
5141 nullable: false,
5142 });
5143 }
5144 _ => {
5145 let inline_type_name = self.generate_context_aware_name(
5147 parent_name,
5148 "Variant",
5149 original_index,
5150 None,
5151 );
5152 let inline_type_name = self.add_inline_union_branch_schema(
5153 &inline_type_name,
5154 variant_schema,
5155 dependencies,
5156 parent_name,
5157 union_kind,
5158 original_index,
5159 branch_discriminator.as_deref(),
5160 )?;
5161 union_variants.push(SchemaRef {
5162 target: inline_type_name,
5163 nullable: false,
5164 });
5165 }
5166 }
5167 }
5168 SchemaType::Reference { target } => {
5170 union_variants.push(SchemaRef {
5171 target: target.clone(),
5172 nullable: false,
5173 });
5174 }
5175 _ => {
5177 let inline_type_name =
5178 format!("{}Variant{}", parent_name, original_index + 1);
5179 let inline_type_name = self.add_inline_union_branch_schema(
5180 &inline_type_name,
5181 variant_schema,
5182 dependencies,
5183 parent_name,
5184 union_kind,
5185 original_index,
5186 branch_discriminator.as_deref(),
5187 )?;
5188 union_variants.push(SchemaRef {
5189 target: inline_type_name,
5190 nullable: false,
5191 });
5192 }
5193 }
5194 }
5195 }
5196
5197 if !union_variants.is_empty() {
5198 return Ok(SchemaType::Union {
5199 variants: union_variants,
5200 exclusive: matches!(union_kind, InlineUnionKind::OneOf),
5201 });
5202 }
5203
5204 return Ok(self.untyped_value(
5206 self.untyped_context(""),
5207 UntypedReason::UnrepresentableUnion,
5208 ));
5209 }
5210
5211 Ok(SchemaType::DiscriminatedUnion {
5212 discriminator_field,
5213 variants,
5214 exclusive: matches!(union_kind, InlineUnionKind::OneOf),
5215 })
5216 }
5217
5218 fn analyze_untagged_oneof_union(
5219 &mut self,
5220 one_of_schemas: &[Schema],
5221 parent_name: &str,
5222 dependencies: &mut HashSet<String>,
5223 union_kind: InlineUnionKind,
5224 source_indices: &[usize],
5225 discriminator: Option<&Discriminator>,
5226 ) -> Result<SchemaType> {
5227 let filtered: Vec<(usize, &Schema)> = one_of_schemas
5232 .iter()
5233 .zip(source_indices.iter().copied())
5234 .filter_map(|(schema, original_index)| {
5235 (!schema.is_explicit_null_only()).then_some((original_index, schema))
5236 })
5237 .collect();
5238
5239 if filtered.len() == 1 {
5241 return self
5242 .analyze_schema_value(filtered[0].1, parent_name)
5243 .map(|a| a.schema_type);
5244 }
5245
5246 let exclusive_object_union = matches!(union_kind, InlineUnionKind::OneOf)
5252 && filtered
5253 .iter()
5254 .all(|(_, schema)| self.branch_resolves_to_object(schema));
5255
5256 let mut union_variants = Vec::new();
5257
5258 for (original_index, variant_schema) in filtered {
5259 if let Some(ref_str) = variant_schema.reference() {
5261 if let Some(schema_name) = self.extract_schema_name(ref_str) {
5262 dependencies.insert(schema_name.to_string());
5263 union_variants.push(SchemaRef {
5264 target: schema_name.to_string(),
5265 nullable: self.schema_or_reference_is_nullable(variant_schema),
5266 });
5267 }
5268 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
5269 let schema_name = if recursive_ref == "#" {
5270 self.find_recursive_anchor_schema()
5272 .or_else(|| self.current_schema_name.clone())
5273 .unwrap_or_else(|| "CompoundFilter".to_string())
5274 } else {
5275 self.extract_schema_name(recursive_ref)
5276 .map(|s| s.to_string())
5277 .unwrap_or_else(|| "RecursiveType".to_string())
5278 };
5279 dependencies.insert(schema_name.clone());
5280 union_variants.push(SchemaRef {
5281 target: schema_name,
5282 nullable: false,
5283 });
5284 } else {
5285 let branch_discriminator = self.inline_union_branch_discriminator_value(
5286 variant_schema,
5287 discriminator,
5288 original_index,
5289 );
5290 let inline_name = self.generate_context_aware_name(
5292 parent_name,
5293 "InlineVariant",
5294 original_index,
5295 Some(variant_schema),
5296 );
5297 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
5298 let variant_type = analyzed.schema_type;
5299
5300 for dep in &analyzed.dependencies {
5302 dependencies.insert(dep.clone());
5303 }
5304
5305 match &variant_type {
5306 SchemaType::Primitive { rust_type, .. } => {
5308 union_variants.push(SchemaRef {
5309 target: rust_type.clone(),
5310 nullable: false,
5311 });
5312 }
5313 SchemaType::Array { item_type } => {
5315 match item_type.as_ref() {
5316 SchemaType::Primitive { rust_type, .. } => {
5317 let type_name = format!("Vec<{rust_type}>");
5318 union_variants.push(SchemaRef {
5319 target: type_name,
5320 nullable: false,
5321 });
5322 }
5323 SchemaType::Reference { target } => {
5324 let type_name = format!("Vec<{target}>");
5325 union_variants.push(SchemaRef {
5326 target: type_name,
5327 nullable: false,
5328 });
5329 }
5330 SchemaType::Array {
5332 item_type: inner_item_type,
5333 } => {
5334 match inner_item_type.as_ref() {
5335 SchemaType::Primitive { rust_type, .. } => {
5336 let type_name = format!("Vec<Vec<{rust_type}>>");
5337 union_variants.push(SchemaRef {
5338 target: type_name,
5339 nullable: false,
5340 });
5341 }
5342 SchemaType::Reference { target } => {
5343 let type_name = format!("Vec<Vec<{target}>>");
5344 union_variants.push(SchemaRef {
5345 target: type_name,
5346 nullable: false,
5347 });
5348 }
5349 _ => {
5350 let inline_type_name = self.generate_context_aware_name(
5352 parent_name,
5353 "Variant",
5354 original_index,
5355 None,
5356 );
5357 let inline_type_name = self
5358 .add_inline_union_branch_schema(
5359 &inline_type_name,
5360 variant_schema,
5361 dependencies,
5362 parent_name,
5363 union_kind,
5364 original_index,
5365 branch_discriminator.as_deref(),
5366 )?;
5367 union_variants.push(SchemaRef {
5368 target: inline_type_name,
5369 nullable: false,
5370 });
5371 }
5372 }
5373 }
5374 _ => {
5375 let inline_type_name = self.generate_context_aware_name(
5377 parent_name,
5378 "Variant",
5379 original_index,
5380 None,
5381 );
5382 let inline_type_name = self.add_inline_union_branch_schema(
5383 &inline_type_name,
5384 variant_schema,
5385 dependencies,
5386 parent_name,
5387 union_kind,
5388 original_index,
5389 branch_discriminator.as_deref(),
5390 )?;
5391 union_variants.push(SchemaRef {
5392 target: inline_type_name,
5393 nullable: false,
5394 });
5395 }
5396 }
5397 }
5398 SchemaType::Reference { target } => {
5400 union_variants.push(SchemaRef {
5401 target: target.clone(),
5402 nullable: false,
5403 });
5404 }
5405 _ => {
5407 let inline_type_name = self.generate_context_aware_name(
5408 parent_name,
5409 "Variant",
5410 original_index,
5411 None,
5412 );
5413 let inline_type_name = self.add_inline_union_branch_schema(
5414 &inline_type_name,
5415 variant_schema,
5416 dependencies,
5417 parent_name,
5418 union_kind,
5419 original_index,
5420 branch_discriminator.as_deref(),
5421 )?;
5422 union_variants.push(SchemaRef {
5423 target: inline_type_name,
5424 nullable: false,
5425 });
5426 }
5427 }
5428 }
5429 }
5430
5431 if !union_variants.is_empty() {
5432 return Ok(SchemaType::Union {
5433 variants: union_variants,
5434 exclusive: exclusive_object_union,
5435 });
5436 }
5437
5438 Ok(self.untyped_value(
5440 self.untyped_context(""),
5441 UntypedReason::UnrepresentableUnion,
5442 ))
5443 }
5444
5445 fn add_inline_schema(
5446 &mut self,
5447 type_name: &str,
5448 schema: &Schema,
5449 dependencies: &mut HashSet<String>,
5450 ) -> Result<String> {
5451 let allocated_name = self.allocate_inline_schema_name(
5452 type_name,
5453 &format!("{type_name}Inline"),
5454 "inline-schema",
5455 schema,
5456 );
5457 self.add_allocated_inline_schema(allocated_name, schema, dependencies)
5458 }
5459
5460 #[allow(clippy::too_many_arguments)]
5461 fn add_inline_union_branch_schema(
5462 &mut self,
5463 type_name: &str,
5464 schema: &Schema,
5465 dependencies: &mut HashSet<String>,
5466 owner_context: &str,
5467 union_kind: InlineUnionKind,
5468 original_index: usize,
5469 discriminator: Option<&str>,
5470 ) -> Result<String> {
5471 let allocated_name = self.allocate_inline_union_branch_name(
5472 type_name,
5473 owner_context,
5474 union_kind,
5475 original_index,
5476 discriminator,
5477 schema,
5478 );
5479 let branch_name = self.add_allocated_inline_schema(allocated_name, schema, dependencies)?;
5480 self.narrow_opaque_object_branch(&branch_name);
5481 Ok(branch_name)
5482 }
5483
5484 fn narrow_opaque_object_branch(&mut self, branch_name: &str) {
5495 if let Some(cached) = self.resolved_cache.get_mut(branch_name)
5496 && let SchemaType::Untyped {
5497 shape: shape @ UntypedShape::Value,
5498 reason: UntypedReason::OpaqueObject,
5499 } = &mut cached.schema_type
5500 {
5501 *shape = UntypedShape::ValueMap;
5502 }
5503 }
5504
5505 fn add_allocated_inline_schema(
5506 &mut self,
5507 allocated_name: String,
5508 schema: &Schema,
5509 dependencies: &mut HashSet<String>,
5510 ) -> Result<String> {
5511 if let Some(schema_type) = schema.schema_type() {
5513 match schema_type {
5514 OpenApiSchemaType::String
5515 | OpenApiSchemaType::Integer
5516 | OpenApiSchemaType::Number
5517 | OpenApiSchemaType::Boolean => {
5518 let rust_type =
5519 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
5520
5521 self.resolved_cache.insert(
5523 allocated_name.clone(),
5524 AnalyzedSchema {
5525 name: allocated_name.clone(),
5526 original: serde_json::to_value(schema).unwrap_or(Value::Null),
5527 schema_type: SchemaType::Primitive {
5528 rust_type,
5529 serde_with: None,
5530 },
5531 dependencies: HashSet::new(),
5532 nullable: false,
5533 description: schema.details().description.clone(),
5534 default: None,
5535 },
5536 );
5537 return Ok(allocated_name);
5538 }
5539 _ => {}
5540 }
5541 }
5542
5543 let analyzed = self.with_schema_context(&allocated_name, |analyzer| {
5547 analyzer.analyze_schema_value(schema, &allocated_name)
5548 })?;
5549
5550 self.resolved_cache.insert(allocated_name.clone(), analyzed);
5552
5553 if let Some(cached) = self.resolved_cache.get(&allocated_name) {
5555 for dep in &cached.dependencies {
5556 dependencies.insert(dep.clone());
5557 }
5558 }
5559
5560 Ok(allocated_name)
5561 }
5562
5563 fn inline_union_branch_discriminator_value(
5564 &self,
5565 schema: &Schema,
5566 discriminator: Option<&Discriminator>,
5567 original_index: usize,
5568 ) -> Option<String> {
5569 let discriminator = discriminator?;
5570 discriminator
5571 .mapping
5572 .as_ref()
5573 .and_then(|mappings| {
5574 mappings
5575 .iter()
5576 .find(|(_, target_ref)| {
5577 target_ref.contains(&format!("variant_{original_index}"))
5578 })
5579 .map(|(key, _)| key.clone())
5580 })
5581 .or_else(|| {
5582 Some(self.extract_inline_discriminator_value(
5583 schema,
5584 &discriminator.property_name,
5585 original_index,
5586 ))
5587 })
5588 }
5589
5590 fn extract_inline_discriminator_value(
5591 &self,
5592 schema: &Schema,
5593 discriminator_field: &str,
5594 variant_index: usize,
5595 ) -> String {
5596 if let Some(properties) = &schema.details().properties {
5598 if let Some(discriminator_prop) = properties.get(discriminator_field) {
5599 if let Some(enum_values) = &discriminator_prop.details().enum_values {
5601 if enum_values.len() == 1 {
5602 if let Some(value) = enum_values[0].as_str() {
5603 return value.to_string();
5604 }
5605 }
5606 }
5607 if let Some(const_value) = discriminator_prop.details().extra.get("const") {
5609 if let Some(value) = const_value.as_str() {
5610 return value.to_string();
5611 }
5612 }
5613 if let Some(const_value) = &discriminator_prop.details().const_value {
5615 if let Some(value) = const_value.as_str() {
5616 return value.to_string();
5617 }
5618 }
5619 }
5620 }
5621
5622 if let Some(inferred_name) = self.infer_variant_name_from_structure(schema, variant_index) {
5624 return inferred_name;
5625 }
5626
5627 format!("variant_{variant_index}")
5629 }
5630
5631 fn infer_variant_name_from_structure(
5632 &self,
5633 schema: &Schema,
5634 _variant_index: usize,
5635 ) -> Option<String> {
5636 let details = schema.details();
5637
5638 if let Some(properties) = &details.properties {
5640 if properties.contains_key("text") && properties.len() <= 3 {
5642 return Some("text".to_string());
5643 }
5644 if properties.contains_key("image") || properties.contains_key("source") {
5645 return Some("image".to_string());
5646 }
5647 if properties.contains_key("document") {
5648 return Some("document".to_string());
5649 }
5650 if properties.contains_key("tool_use_id") || properties.contains_key("tool_result") {
5651 return Some("tool_result".to_string());
5652 }
5653 if properties.contains_key("content") && properties.contains_key("is_error") {
5654 return Some("tool_result".to_string());
5655 }
5656 if properties.contains_key("partial_json") {
5657 return Some("partial_json".to_string());
5658 }
5659
5660 let property_names: Vec<&String> = properties.keys().collect();
5662
5663 for prop_name in &property_names {
5665 if prop_name.contains("result") {
5666 return Some("result".to_string());
5667 }
5668 if prop_name.contains("error") {
5669 return Some("error".to_string());
5670 }
5671 if prop_name.contains("content") && property_names.len() <= 2 {
5672 return Some("content".to_string());
5673 }
5674 }
5675
5676 let significant_props = property_names
5678 .iter()
5679 .filter(|&name| !["type", "id", "cache_control"].contains(&name.as_str()))
5680 .collect::<Vec<_>>();
5681
5682 if significant_props.len() == 1 {
5683 return Some((*significant_props[0]).clone());
5684 }
5685 }
5686
5687 if let Some(description) = &details.description {
5689 let desc_lower = description.to_lowercase();
5690 if desc_lower.contains("text") && desc_lower.len() < 100 {
5691 return Some("text".to_string());
5692 }
5693 if desc_lower.contains("image") {
5694 return Some("image".to_string());
5695 }
5696 if desc_lower.contains("document") {
5697 return Some("document".to_string());
5698 }
5699 if desc_lower.contains("tool") && desc_lower.contains("result") {
5700 return Some("tool_result".to_string());
5701 }
5702 }
5703
5704 None
5705 }
5706
5707 fn discriminator_to_variant_name(&self, discriminator: &str) -> String {
5708 if discriminator.is_empty() {
5710 return "Variant".to_string();
5711 }
5712
5713 let mut result = String::new();
5714 let mut next_upper = true;
5715
5716 for c in discriminator.chars() {
5717 match c {
5718 'a'..='z' => {
5719 if next_upper {
5720 result.push(c.to_ascii_uppercase());
5721 next_upper = false;
5722 } else {
5723 result.push(c);
5724 }
5725 }
5726 'A'..='Z' => {
5727 result.push(c);
5728 next_upper = false;
5729 }
5730 '0'..='9' => {
5731 result.push(c);
5732 next_upper = false;
5733 }
5734 '_' | '-' | '.' | ' ' | '/' | '\\' => {
5735 next_upper = true;
5737 }
5738 _ => {
5739 next_upper = true;
5741 }
5742 }
5743 }
5744
5745 if result.is_empty() || result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
5747 result = format!("Variant{result}");
5748 }
5749
5750 result
5751 }
5752
5753 fn ensure_unique_variant_name(
5754 &self,
5755 base_name: String,
5756 used_names: &mut std::collections::HashSet<String>,
5757 ) -> String {
5758 let mut candidate = base_name.clone();
5759 let mut counter = 1;
5760
5761 while used_names.contains(&candidate) {
5762 counter += 1;
5763 candidate = format!("{base_name}{counter}");
5764 }
5765
5766 used_names.insert(candidate.clone());
5767 candidate
5768 }
5769
5770 fn generate_inline_type_name(&self, schema: &Schema, variant_index: usize) -> String {
5771 if let Some(meaningful_name) = self.infer_type_name_from_structure(schema) {
5773 return meaningful_name;
5774 }
5775
5776 let context = self.current_schema_name.as_deref().unwrap_or("Inline");
5778 self.generate_context_aware_name(context, "Variant", variant_index, Some(schema))
5779 }
5780
5781 fn infer_type_name_from_structure(&self, schema: &Schema) -> Option<String> {
5782 let details = schema.details();
5783
5784 if let Some(description) = &details.description {
5786 if let Some(name_from_desc) = self.extract_type_name_from_description(description) {
5787 return Some(name_from_desc);
5788 }
5789 }
5790
5791 if let Some(properties) = &details.properties {
5793 if let Some(name_from_props) = self.extract_type_name_from_properties(properties) {
5794 return Some(format!("{name_from_props}Block"));
5795 }
5796 }
5797
5798 None
5799 }
5800
5801 fn extract_type_name_from_description(&self, description: &str) -> Option<String> {
5802 if description.len() > 100 || description.contains('\n') {
5804 return None;
5805 }
5806
5807 let words: Vec<&str> = description
5809 .split_whitespace()
5810 .take(2) .filter(|word| {
5812 let w = word.to_lowercase();
5813 word.len() > 2
5814 && ![
5815 "the", "and", "for", "with", "that", "this", "are", "can", "will", "was",
5816 ]
5817 .contains(&w.as_str())
5818 })
5819 .collect();
5820
5821 if words.is_empty() {
5822 return None;
5823 }
5824
5825 let combined = words.join("_");
5827 let pascal_name = self.discriminator_to_variant_name(&combined);
5828
5829 if !pascal_name.ends_with("Content")
5831 && !pascal_name.ends_with("Block")
5832 && !pascal_name.ends_with("Type")
5833 {
5834 Some(format!("{pascal_name}Content"))
5835 } else {
5836 Some(pascal_name)
5837 }
5838 }
5839
5840 fn extract_type_name_from_properties(
5841 &self,
5842 properties: &std::collections::BTreeMap<String, crate::openapi::Schema>,
5843 ) -> Option<String> {
5844 let significant_props: Vec<&String> = properties
5846 .keys()
5847 .filter(|name| !["type", "id", "cache_control"].contains(&name.as_str()))
5848 .collect();
5849
5850 if significant_props.is_empty() {
5851 return None;
5852 }
5853
5854 if significant_props.len() == 1 {
5856 let prop_name = significant_props[0];
5857 return Some(self.discriminator_to_variant_name(prop_name));
5858 }
5859
5860 let mut sorted_props = significant_props.clone();
5863 sorted_props.sort();
5864 if let Some(first_prop) = sorted_props.first() {
5865 return Some(self.discriminator_to_variant_name(first_prop));
5866 }
5867
5868 None
5869 }
5870
5871 fn openapi_type_to_rust_type(
5872 &self,
5873 openapi_type: OpenApiSchemaType,
5874 details: &crate::openapi::SchemaDetails,
5875 ) -> String {
5876 if openapi_type == OpenApiSchemaType::Integer {
5881 self.integer_rust_type(details)
5882 } else {
5883 self.type_mapper.map(openapi_type, details).rust_type
5884 }
5885 }
5886
5887 #[allow(dead_code)]
5888 fn fallback_discriminator_value(&self, schema_name: &str) -> String {
5889 self.fallback_discriminator_value_for_field(schema_name, "type")
5890 }
5891
5892 fn fallback_discriminator_value_for_field(
5893 &self,
5894 schema_name: &str,
5895 field_name: &str,
5896 ) -> String {
5897 if let Some(ref_schema) = self.schemas.get(schema_name) {
5899 if let Some(extracted) =
5900 self.extract_discriminator_value_for_field(ref_schema, field_name)
5901 {
5902 return extracted;
5903 }
5904 }
5905
5906 self.generate_discriminator_value_from_name(schema_name)
5908 }
5909
5910 fn disambiguate_shared_discriminator_values(
5911 &self,
5912 variants: &mut [UnionVariant],
5913 discriminator: Option<&Discriminator>,
5914 ) {
5915 let mut owners_by_value: BTreeMap<String, Vec<usize>> = BTreeMap::new();
5916 for (index, variant) in variants.iter().enumerate() {
5917 for value in &variant.discriminator_values {
5918 owners_by_value
5919 .entry(value.clone())
5920 .or_default()
5921 .push(index);
5922 }
5923 }
5924
5925 let mut removals: Vec<(usize, String)> = Vec::new();
5926 for (value, candidate_owners) in owners_by_value {
5927 if candidate_owners.len() < 2 {
5928 continue;
5929 }
5930
5931 let explicitly_mapped_owners: Vec<usize> = discriminator
5932 .and_then(|disc| disc.mapping.as_ref())
5933 .and_then(|mappings| mappings.get(&value))
5934 .map(|target_ref| {
5935 candidate_owners
5936 .iter()
5937 .copied()
5938 .filter(|index| {
5939 let variant = &variants[*index];
5940 target_ref == &variant.schema_ref
5941 || self.extract_schema_name(target_ref)
5942 == Some(variant.type_name.as_str())
5943 || (variant.schema_ref.starts_with("inline_")
5944 && target_ref.contains(&variant.schema_ref))
5945 })
5946 .collect()
5947 })
5948 .unwrap_or_default();
5949
5950 let winner = if explicitly_mapped_owners.len() == 1 {
5951 explicitly_mapped_owners.first().copied()
5952 } else {
5953 let mut scored: Vec<(usize, usize)> = candidate_owners
5954 .iter()
5955 .map(|index| {
5956 (
5957 *index,
5958 Self::discriminator_name_affinity(&variants[*index].type_name, &value),
5959 )
5960 })
5961 .collect();
5962 scored.sort_by(|left, right| right.1.cmp(&left.1).then(left.0.cmp(&right.0)));
5963 match scored.as_slice() {
5964 [(index, score), rest @ ..]
5965 if *score > 0 && rest.first().is_none_or(|(_, next)| score > next) =>
5966 {
5967 Some(*index)
5968 }
5969 _ => None,
5970 }
5971 };
5972
5973 if let Some(winner) = winner {
5974 for index in candidate_owners {
5975 if index != winner && variants[index].preferred_discriminator_values.len() > 1 {
5976 removals.push((index, value.clone()));
5977 }
5978 }
5979 }
5980 }
5981
5982 for (index, value) in removals {
5983 variants[index]
5984 .preferred_discriminator_values
5985 .retain(|candidate| candidate != &value);
5986 }
5987 for variant in variants {
5988 if let Some(canonical) = variant.preferred_discriminator_values.first() {
5989 variant.discriminator_value.clone_from(canonical);
5990 }
5991 }
5992 }
5993
5994 fn discriminator_name_affinity(schema_name: &str, value: &str) -> usize {
5995 let schema_compact: String = schema_name
5996 .chars()
5997 .filter(|character| character.is_ascii_alphanumeric())
5998 .flat_map(char::to_lowercase)
5999 .collect();
6000 let value_compact: String = value
6001 .chars()
6002 .filter(|character| character.is_ascii_alphanumeric())
6003 .flat_map(char::to_lowercase)
6004 .collect();
6005 let exact_bonus = usize::from(
6006 !value_compact.is_empty() && schema_compact.contains(value_compact.as_str()),
6007 ) * 10;
6008 let token_matches = value
6009 .split(|character: char| !character.is_ascii_alphanumeric())
6010 .filter(|token| !token.is_empty())
6011 .filter(|token| schema_compact.contains(&token.to_ascii_lowercase()))
6012 .count();
6013 exact_bonus + token_matches
6014 }
6015
6016 fn generate_discriminator_value_from_name(&self, schema_name: &str) -> String {
6017 let mut result = String::new();
6019 let mut chars = schema_name.chars().peekable();
6020 let mut first = true;
6021
6022 while let Some(c) = chars.next() {
6023 if c.is_uppercase()
6024 && !first
6025 && chars
6026 .peek()
6027 .map(|&next| next.is_lowercase())
6028 .unwrap_or(false)
6029 {
6030 result.push('.');
6031 }
6032 result.push(c.to_ascii_lowercase());
6033 first = false;
6034 }
6035
6036 if result.ends_with("event") {
6038 result = result[..result.len() - 5].to_string();
6039 }
6040
6041 if schema_name.starts_with("Response") && !result.starts_with("response.") {
6043 result = format!("response.{}", result.trim_start_matches("response"));
6044 }
6045
6046 result
6047 }
6048
6049 fn to_rust_variant_name(&self, schema_name: &str) -> String {
6050 let mut name = schema_name;
6052
6053 if name.starts_with("Response") && name.len() > 8 {
6055 name = &name[8..]; }
6057
6058 if name.ends_with("Event") && name.len() > 5 {
6060 name = &name[..name.len() - 5]; }
6062
6063 name = name.trim_matches('_');
6065
6066 if name.is_empty() {
6068 schema_name.to_string()
6069 } else {
6070 self.discriminator_to_variant_name(name)
6072 }
6073 }
6074
6075 fn hoist_inline_string_enum(
6099 &mut self,
6100 schema: &Schema,
6101 enum_values: Vec<String>,
6102 primary_name: String,
6103 dependencies: &mut HashSet<String>,
6104 ) -> SchemaType {
6105 let suffix = enum_values
6106 .first()
6107 .map(|value| self.to_pascal_case(value))
6108 .unwrap_or_else(|| "Variant".to_string());
6109 let collision_name = format!("{primary_name}{suffix}");
6110 let enum_type_name =
6111 self.allocate_inline_schema_name(&primary_name, &collision_name, "string-enum", schema);
6112 let should_insert = !self.resolved_cache.contains_key(&enum_type_name);
6113
6114 if should_insert {
6117 self.resolved_cache.insert(
6118 enum_type_name.clone(),
6119 AnalyzedSchema {
6120 name: enum_type_name.clone(),
6121 original: serde_json::to_value(schema).unwrap_or(Value::Null),
6122 schema_type: SchemaType::StringEnum {
6123 values: enum_values,
6124 },
6125 dependencies: HashSet::new(),
6126 nullable: false,
6127 description: schema.details().description.clone(),
6128 default: schema.details().default.clone(),
6129 },
6130 );
6131 }
6132
6133 dependencies.insert(enum_type_name.clone());
6135 SchemaType::Reference {
6136 target: enum_type_name,
6137 }
6138 }
6139
6140 fn analyze_array_schema(
6141 &mut self,
6142 schema: &Schema,
6143 parent_schema_name: &str,
6144 dependencies: &mut HashSet<String>,
6145 ) -> Result<SchemaType> {
6146 let details = schema.details();
6147
6148 if let Some(positions) = details.positional_items() {
6152 return self.analyze_positional_items(
6153 positions,
6154 details,
6155 parent_schema_name,
6156 dependencies,
6157 );
6158 }
6159
6160 if let Some(items_schema) = details.item_schema() {
6162 let item_type = self.analyze_item_schema(
6163 items_schema,
6164 parent_schema_name,
6165 &format!("{parent_schema_name}Item"),
6166 dependencies,
6167 )?;
6168 let item_type = self.hoist_inline_property_type(
6169 parent_schema_name,
6170 "Item",
6171 item_type,
6172 dependencies,
6173 );
6174 Ok(SchemaType::Array {
6175 item_type: Box::new(item_type),
6176 })
6177 } else {
6178 Ok(
6180 self.untyped_value_array(
6181 self.untyped_context(""),
6182 UntypedReason::ArrayWithoutItems,
6183 ),
6184 )
6185 }
6186 }
6187
6188 fn shared_branch_type(&self, branches: &[Schema]) -> Option<SchemaType> {
6197 let mut mapped: Option<(String, Option<String>)> = None;
6198 let mut scalar_kind: Option<OpenApiSchemaType> = None;
6199 let mut formats_agree = true;
6200 for branch in branches {
6201 if branch.reference().is_some() {
6202 return None;
6203 }
6204 let details = branch.details();
6205 if details.enum_values.is_some()
6206 || details.const_value.is_some()
6207 || details.properties.is_some()
6208 {
6209 return None;
6210 }
6211 let scalar = match branch.schema_type()? {
6212 scalar @ (OpenApiSchemaType::String
6213 | OpenApiSchemaType::Integer
6214 | OpenApiSchemaType::Number
6215 | OpenApiSchemaType::Boolean) => scalar.clone(),
6216 _ => return None,
6217 };
6218 match &scalar_kind {
6219 Some(existing) if *existing != scalar => return None,
6220 Some(_) => {}
6221 None => scalar_kind = Some(scalar.clone()),
6222 }
6223
6224 let candidate = self.type_mapper.map(scalar, details);
6225 let candidate = (candidate.rust_type, candidate.serde_with);
6226 match &mapped {
6227 Some(existing) if *existing != candidate => formats_agree = false,
6228 Some(_) => {}
6229 None => mapped = Some(candidate),
6230 }
6231 }
6232
6233 if formats_agree {
6234 return mapped.map(|(rust_type, serde_with)| SchemaType::Primitive {
6235 rust_type,
6236 serde_with,
6237 });
6238 }
6239
6240 let scalar = scalar_kind?;
6246 let mapped = self
6247 .type_mapper
6248 .map(scalar, &crate::openapi::SchemaDetails::default());
6249 Some(SchemaType::Primitive {
6250 rust_type: mapped.rust_type,
6251 serde_with: mapped.serde_with,
6252 })
6253 }
6254
6255 fn expand_pointer_branches(&self, branches: &[Schema]) -> Option<Vec<Schema>> {
6264 let mut expanded = Vec::with_capacity(branches.len());
6265 let mut changed = false;
6266 for branch in branches {
6267 let resolved = branch
6268 .reference()
6269 .filter(|reference| self.extract_schema_name(reference).is_none())
6270 .and_then(|reference| reference.strip_prefix('#'))
6271 .filter(|pointer| pointer.starts_with('/'))
6272 .and_then(|pointer| self.openapi_spec.pointer(pointer))
6273 .and_then(|value| Schema::deserialize(value).ok())
6274 .filter(|schema| schema.reference().is_none());
6275 match resolved {
6276 Some(schema) => {
6277 expanded.push(schema);
6278 changed = true;
6279 }
6280 None => expanded.push(branch.clone()),
6281 }
6282 }
6283 changed.then_some(expanded)
6284 }
6285
6286 fn analyze_object_with_variants(
6299 &mut self,
6300 schema: &Schema,
6301 branches: &[Schema],
6302 schema_name: &str,
6303 dependencies: &mut HashSet<String>,
6304 ) -> Result<Option<SchemaType>> {
6305 let details = schema.details();
6306 if details.properties.as_ref().is_none_or(BTreeMap::is_empty) || branches.is_empty() {
6307 return Ok(None);
6308 }
6309 if schema.is_nullable_pattern() || Self::union_only_constrains_requiredness(branches) {
6312 return Ok(None);
6313 }
6314
6315 let base = self.analyze_object_schema(schema, dependencies)?;
6316 let SchemaType::Object {
6317 properties,
6318 required,
6319 additional_properties,
6320 ..
6321 } = base
6322 else {
6323 return Ok(None);
6324 };
6325
6326 let preferred_variant_name = format!("{schema_name}Variant");
6327 let variant_name = self.allocate_inline_schema_name(
6328 &preferred_variant_name,
6329 &format!("{preferred_variant_name}Inline"),
6330 "object-variant",
6331 schema,
6332 );
6333 let variant_type = self.analyze_anyof_union(
6334 branches,
6335 schema.discriminator(),
6336 dependencies,
6337 &variant_name,
6338 )?;
6339 if matches!(variant_type, SchemaType::Untyped { .. }) {
6342 return Ok(Some(SchemaType::Object {
6343 properties,
6344 required,
6345 additional_properties,
6346 variant: None,
6347 }));
6348 }
6349
6350 self.resolved_cache.insert(
6351 variant_name.clone(),
6352 AnalyzedSchema {
6353 name: variant_name.clone(),
6354 original: Value::Null,
6355 dependencies: schema_type_dependencies(&variant_type),
6356 schema_type: variant_type,
6357 nullable: false,
6358 description: None,
6359 default: None,
6360 },
6361 );
6362 dependencies.insert(variant_name.clone());
6363
6364 Ok(Some(SchemaType::Object {
6365 properties,
6366 required,
6367 additional_properties,
6368 variant: Some(SchemaRef {
6369 target: variant_name,
6370 nullable: false,
6371 }),
6372 }))
6373 }
6374
6375 #[allow(clippy::result_unit_err)]
6383 fn resolve_boolean_branches(branches: &[Schema]) -> std::result::Result<Vec<Schema>, ()> {
6384 if branches
6385 .iter()
6386 .any(|branch| matches!(branch, Schema::Bool(true)))
6387 {
6388 return Err(());
6389 }
6390 Ok(branches
6391 .iter()
6392 .filter(|branch| !matches!(branch, Schema::Bool(false)))
6393 .cloned()
6394 .collect())
6395 }
6396
6397 fn union_only_constrains_requiredness(branches: &[Schema]) -> bool {
6406 !branches.is_empty()
6407 && branches
6408 .iter()
6409 .all(Self::schema_only_constrains_requiredness)
6410 }
6411
6412 fn schema_only_constrains_requiredness(schema: &Schema) -> bool {
6420 let keys_are_requiredness_or_annotations = serde_json::to_value(schema)
6421 .ok()
6422 .and_then(|value| value.as_object().cloned())
6423 .is_some_and(|object| {
6424 object.iter().all(|(key, value)| {
6425 (key == "type" && value == "object")
6426 || matches!(
6427 key.as_str(),
6428 "required"
6429 | "not"
6430 | "anyOf"
6431 | "oneOf"
6432 | "title"
6433 | "description"
6434 | "deprecated"
6435 | "readOnly"
6436 | "writeOnly"
6437 | "examples"
6438 | "example"
6439 | "default"
6440 | "externalDocs"
6441 | "xml"
6442 | "$comment"
6443 )
6444 || key.starts_with("x-")
6445 })
6446 });
6447 if !keys_are_requiredness_or_annotations {
6448 return false;
6449 }
6450
6451 match schema {
6452 Schema::AnyOf { any_of, .. } => {
6453 !any_of.is_empty() && any_of.iter().all(Self::schema_only_constrains_requiredness)
6454 }
6455 Schema::OneOf { one_of, .. } => {
6456 !one_of.is_empty() && one_of.iter().all(Self::schema_only_constrains_requiredness)
6457 }
6458 other => {
6459 let details = other.details();
6460 details
6461 .required
6462 .as_ref()
6463 .is_some_and(|required| !required.is_empty())
6464 || details
6465 .not
6466 .as_deref()
6467 .is_some_and(Self::schema_only_constrains_requiredness)
6468 }
6469 }
6470 }
6471
6472 fn analyze_empty_union(
6479 &mut self,
6480 schema: &Schema,
6481 dependencies: &mut HashSet<String>,
6482 ) -> Result<SchemaType> {
6483 let Some(declared) = schema
6487 .declared_type()
6488 .cloned()
6489 .or_else(|| schema.inferred_type())
6490 .or_else(|| {
6491 schema
6492 .details()
6493 .properties
6494 .is_some()
6495 .then_some(OpenApiSchemaType::Object)
6496 })
6497 else {
6498 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema));
6499 };
6500 match declared {
6501 OpenApiSchemaType::Object => self.analyze_object_schema(schema, dependencies),
6502 OpenApiSchemaType::Array => {
6503 let context = self
6504 .current_schema_name
6505 .clone()
6506 .unwrap_or_else(|| "Inline".to_string());
6507 self.analyze_array_schema(schema, &context, dependencies)
6508 }
6509 scalar => {
6510 let mapped = self.type_mapper.map(scalar, schema.details());
6511 Ok(SchemaType::Primitive {
6512 rust_type: mapped.rust_type,
6513 serde_with: mapped.serde_with,
6514 })
6515 }
6516 }
6517 }
6518
6519 fn resolve_pointer_schema(
6532 &mut self,
6533 reference: &str,
6534 dependencies: &mut HashSet<String>,
6535 ) -> Result<Option<SchemaType>> {
6536 let Some(pointer) = reference.strip_prefix('#') else {
6537 return Ok(None);
6538 };
6539 if pointer.is_empty() || !pointer.starts_with('/') {
6540 return Ok(None);
6541 }
6542 let preferred_name = pointer_type_name(pointer);
6543 if preferred_name.is_empty() {
6544 return Ok(None);
6545 }
6546 let name = self.allocate_pointer_schema_name(pointer, &preferred_name);
6547 if self.resolved_cache.contains_key(&name) || !self.resolving_pointers.insert(name.clone())
6550 {
6551 dependencies.insert(name.clone());
6552 return Ok(Some(SchemaType::Reference { target: name }));
6553 }
6554
6555 let resolved = (|| {
6556 let value = self.openapi_spec.pointer(pointer)?.clone();
6557 Schema::deserialize(&value).ok()
6558 })();
6559 let Some(schema) = resolved else {
6560 self.resolving_pointers.remove(&name);
6561 return Ok(None);
6562 };
6563
6564 let saved_context = self.current_schema_name.clone();
6569 self.current_schema_name = Some(name.clone());
6570 let analyzed = self.analyze_schema_value(&schema, &name);
6571 self.current_schema_name = saved_context;
6572 self.resolving_pointers.remove(&name);
6573 let analyzed = analyzed?;
6574 dependencies.extend(analyzed.dependencies.iter().cloned());
6575 if analyzed.schema_type.renders_inline() {
6576 return Ok(Some(analyzed.schema_type));
6577 }
6578
6579 self.resolved_cache.insert(name.clone(), analyzed);
6580 dependencies.insert(name.clone());
6581 Ok(Some(SchemaType::Reference { target: name }))
6582 }
6583
6584 fn hoist_inline_property_type(
6593 &mut self,
6594 schema_name: &str,
6595 property_name: &str,
6596 schema_type: SchemaType,
6597 dependencies: &mut HashSet<String>,
6598 ) -> SchemaType {
6599 if schema_type.renders_inline() {
6600 return schema_type;
6601 }
6602
6603 use heck::ToPascalCase;
6604
6605 let preferred_name = format!("{schema_name}{}", property_name.to_pascal_case());
6606 let hoisted_name = self.allocate_synthetic_schema_name(
6607 &preferred_name,
6608 &format!("{preferred_name}Inline"),
6609 "hoisted-property",
6610 format!("{schema_type:?}"),
6611 );
6612 let hoisted_dependencies = schema_type_dependencies(&schema_type);
6613 self.resolved_cache.insert(
6614 hoisted_name.clone(),
6615 AnalyzedSchema {
6616 name: hoisted_name.clone(),
6617 original: Value::Null,
6618 schema_type,
6619 dependencies: hoisted_dependencies,
6620 nullable: false,
6621 description: None,
6622 default: None,
6623 },
6624 );
6625 dependencies.insert(hoisted_name.clone());
6626 SchemaType::Reference {
6627 target: hoisted_name,
6628 }
6629 }
6630
6631 fn analyze_positional_items(
6644 &mut self,
6645 positions: &[Schema],
6646 details: &crate::openapi::SchemaDetails,
6647 parent_schema_name: &str,
6648 dependencies: &mut HashSet<String>,
6649 ) -> Result<SchemaType> {
6650 if details.positional_items_are_exact() && !positions.is_empty() {
6651 let mut element_types = Vec::with_capacity(positions.len());
6652 for (index, position) in positions.iter().enumerate() {
6653 let element_type = self.analyze_item_schema(
6654 position,
6655 parent_schema_name,
6656 &format!("{parent_schema_name}Item{}", index + 1),
6657 dependencies,
6658 )?;
6659 element_types.push(self.hoist_inline_property_type(
6660 parent_schema_name,
6661 &format!("Item{}", index + 1),
6662 element_type,
6663 dependencies,
6664 ));
6665 }
6666 return Ok(SchemaType::Tuple { element_types });
6667 }
6668
6669 if details.positional_items_are_closed()
6673 && let Some(shared) = shared_positional_schema(positions)
6674 {
6675 let item_type = self.analyze_item_schema(
6676 shared,
6677 parent_schema_name,
6678 &format!("{parent_schema_name}Item"),
6679 dependencies,
6680 )?;
6681 return Ok(SchemaType::Array {
6682 item_type: Box::new(item_type),
6683 });
6684 }
6685
6686 Ok(self.untyped_value_array(self.untyped_context(""), UntypedReason::OpenPositionalItems))
6687 }
6688
6689 fn analyze_item_schema(
6696 &mut self,
6697 items_schema: &Schema,
6698 parent_schema_name: &str,
6699 inline_name: &str,
6700 dependencies: &mut HashSet<String>,
6701 ) -> Result<SchemaType> {
6702 let item_type = match items_schema {
6703 Schema::Bool(accepts_anything) => self.untyped_value(
6704 self.untyped_context(""),
6705 if *accepts_anything {
6706 UntypedReason::AnySchema
6707 } else {
6708 UntypedReason::NeverMatches
6709 },
6710 ),
6711 Schema::Reference { reference, .. } => {
6712 if let Some(target) = self.extract_schema_name(reference) {
6714 let target = target.to_string();
6715 dependencies.insert(target.clone());
6716 SchemaType::Reference { target }
6717 } else {
6718 self.resolve_pointer_schema(reference, dependencies)?
6719 .ok_or_else(|| GeneratorError::UnresolvedReference(reference.to_string()))?
6720 }
6721 }
6722 Schema::RecursiveRef { recursive_ref, .. } => {
6723 if recursive_ref == "#" {
6725 let target = self
6727 .find_recursive_anchor_schema()
6728 .unwrap_or_else(|| parent_schema_name.to_string());
6729 dependencies.insert(target.clone());
6730 SchemaType::Reference { target }
6731 } else {
6732 let target = self
6733 .extract_schema_name(recursive_ref)
6734 .unwrap_or("RecursiveType")
6735 .to_string();
6736 dependencies.insert(target.clone());
6737 SchemaType::Reference { target }
6738 }
6739 }
6740 Schema::Typed { schema_type, .. } => {
6741 match schema_type {
6743 OpenApiSchemaType::String => {
6744 match items_schema
6748 .details()
6749 .string_enum_values()
6750 .filter(|values| !values.is_empty())
6751 {
6752 Some(values) => self.hoist_inline_string_enum(
6753 items_schema,
6754 values,
6755 inline_name.to_string(),
6756 dependencies,
6757 ),
6758 None => SchemaType::Primitive {
6759 rust_type: "String".to_string(),
6760 serde_with: None,
6761 },
6762 }
6763 }
6764 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
6765 let details = items_schema.details();
6766 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
6767 SchemaType::Primitive {
6768 rust_type,
6769 serde_with: None,
6770 }
6771 }
6772 OpenApiSchemaType::Boolean => SchemaType::Primitive {
6773 rust_type: "bool".to_string(),
6774 serde_with: None,
6775 },
6776 OpenApiSchemaType::Object => {
6777 let preferred_object_type_name = inline_name.to_string();
6779 let object_type_name = self.allocate_inline_schema_name(
6780 &preferred_object_type_name,
6781 &format!("{preferred_object_type_name}Inline"),
6782 "array-item-object",
6783 items_schema,
6784 );
6785
6786 self.add_allocated_object_schema(
6787 object_type_name,
6788 items_schema,
6789 dependencies,
6790 )?
6791 }
6792 OpenApiSchemaType::Array => {
6793 self.analyze_array_schema(items_schema, parent_schema_name, dependencies)?
6795 }
6796 _ => self.untyped_value(
6797 self.untyped_context(""),
6798 UntypedReason::UnsupportedTypeKeyword,
6799 ),
6800 }
6801 }
6802 Schema::OneOf { .. } | Schema::AnyOf { .. } => {
6803 let analyzed = self.analyze_schema_value(items_schema, "ArrayItem")?;
6805
6806 match &analyzed.schema_type {
6808 SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => {
6809 let preferred_union_name = format!("{inline_name}Union");
6812 let union_name = self.allocate_inline_schema_name(
6813 &preferred_union_name,
6814 &format!("{preferred_union_name}Inline"),
6815 "array-item-union",
6816 items_schema,
6817 );
6818
6819 let mut union_schema = analyzed;
6821 union_schema.name = union_name.clone();
6822
6823 self.resolved_cache.insert(union_name.clone(), union_schema);
6825
6826 dependencies.insert(union_name.clone());
6828
6829 SchemaType::Reference { target: union_name }
6831 }
6832 _ => analyzed.schema_type,
6833 }
6834 }
6835 Schema::Untyped { .. } => {
6836 if let Some(inferred) = items_schema.inferred_type() {
6838 match inferred {
6839 OpenApiSchemaType::Object => {
6840 let preferred_object_type_name = inline_name.to_string();
6842 let object_type_name = self.allocate_inline_schema_name(
6843 &preferred_object_type_name,
6844 &format!("{preferred_object_type_name}Inline"),
6845 "array-item-object",
6846 items_schema,
6847 );
6848
6849 self.add_allocated_object_schema(
6850 object_type_name,
6851 items_schema,
6852 dependencies,
6853 )?
6854 }
6855 OpenApiSchemaType::String => {
6856 match items_schema
6859 .details()
6860 .string_enum_values()
6861 .filter(|values| !values.is_empty())
6862 {
6863 Some(values) => self.hoist_inline_string_enum(
6864 items_schema,
6865 values,
6866 inline_name.to_string(),
6867 dependencies,
6868 ),
6869 None => SchemaType::Primitive {
6870 rust_type: "String".to_string(),
6871 serde_with: None,
6872 },
6873 }
6874 }
6875 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
6876 let details = items_schema.details();
6877 let rust_type = self.get_number_rust_type(inferred, details);
6878 SchemaType::Primitive {
6879 rust_type,
6880 serde_with: None,
6881 }
6882 }
6883 OpenApiSchemaType::Boolean => SchemaType::Primitive {
6884 rust_type: "bool".to_string(),
6885 serde_with: None,
6886 },
6887 OpenApiSchemaType::Null => SchemaType::Primitive {
6890 rust_type: self.type_mapper.null_unit().rust_type,
6891 serde_with: None,
6892 },
6893 _ => self.untyped_value(
6894 self.untyped_context(""),
6895 UntypedReason::UnsupportedTypeKeyword,
6896 ),
6897 }
6898 } else {
6899 self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema)
6900 }
6901 }
6902 _ => self.analyze_property_schema_with_context(items_schema, None, dependencies)?,
6907 };
6908
6909 Ok(self.nullable_container_value(items_schema, item_type))
6910 }
6911
6912 fn get_number_rust_type(
6913 &self,
6914 schema_type: OpenApiSchemaType,
6915 details: &crate::openapi::SchemaDetails,
6916 ) -> String {
6917 let format = details.format.as_deref();
6921 match schema_type {
6922 OpenApiSchemaType::Integer => self.integer_rust_type(details),
6923 OpenApiSchemaType::Number => self.type_mapper.number_format(format).rust_type,
6924 _ => self.type_mapper.dynamic_json().rust_type,
6925 }
6926 }
6927
6928 fn integer_rust_type(&self, details: &crate::openapi::SchemaDetails) -> String {
6934 fn integer_value(number: &serde_json::Number) -> Option<i128> {
6935 number
6936 .as_i64()
6937 .map(i128::from)
6938 .or_else(|| number.as_u64().map(i128::from))
6939 .or_else(|| {
6940 number.as_f64().and_then(|value| {
6941 (value.fract() == 0.0
6942 && value >= i128::MIN as f64
6943 && value <= i128::MAX as f64)
6944 .then_some(value as i128)
6945 })
6946 })
6947 }
6948
6949 fn below(number: &serde_json::Number, boundary: i128) -> bool {
6950 integer_value(number).is_some_and(|value| value < boundary)
6951 }
6952
6953 fn above(number: &serde_json::Number, boundary: i128) -> bool {
6954 integer_value(number).is_some_and(|value| value > boundary)
6955 }
6956
6957 let explicitly_nonnegative = details
6958 .minimum
6959 .as_ref()
6960 .and_then(integer_value)
6961 .is_some_and(|value| value >= 0)
6962 || matches!(
6963 details.exclusive_minimum,
6964 Some(crate::openapi::ExclusiveBound::Number(value)) if value >= 0.0
6965 );
6966
6967 let annotated_numbers = details
6968 .const_value
6969 .iter()
6970 .chain(details.default.iter())
6971 .chain(details.example.iter())
6972 .chain(details.enum_values.iter().flatten())
6973 .chain(details.examples.iter().flatten())
6974 .filter_map(Value::as_number)
6975 .collect::<Vec<_>>();
6976 let below_i32 = details
6977 .minimum
6978 .as_ref()
6979 .is_some_and(|number| below(number, i128::from(i32::MIN)))
6980 || annotated_numbers
6981 .iter()
6982 .any(|number| below(number, i128::from(i32::MIN)))
6983 || matches!(
6984 details.exclusive_minimum,
6985 Some(crate::openapi::ExclusiveBound::Number(value)) if value < i32::MIN as f64
6986 );
6987 let above_i32 = details
6988 .maximum
6989 .as_ref()
6990 .is_some_and(|number| above(number, i128::from(i32::MAX)))
6991 || annotated_numbers
6992 .iter()
6993 .any(|number| above(number, i128::from(i32::MAX)))
6994 || matches!(
6995 details.exclusive_maximum,
6996 Some(crate::openapi::ExclusiveBound::Number(value)) if value > i32::MAX as f64
6997 );
6998 let below_i64 = details
6999 .minimum
7000 .as_ref()
7001 .is_some_and(|number| below(number, i128::from(i64::MIN)))
7002 || annotated_numbers
7003 .iter()
7004 .any(|number| below(number, i128::from(i64::MIN)))
7005 || matches!(
7006 details.exclusive_minimum,
7007 Some(crate::openapi::ExclusiveBound::Number(value)) if value < i64::MIN as f64
7008 );
7009 let above_i64 = details
7010 .maximum
7011 .as_ref()
7012 .is_some_and(|number| above(number, i128::from(i64::MAX)))
7013 || annotated_numbers
7014 .iter()
7015 .any(|number| above(number, i128::from(i64::MAX)))
7016 || matches!(
7017 details.exclusive_maximum,
7018 Some(crate::openapi::ExclusiveBound::Number(value)) if value >= 9_223_372_036_854_775_808.0
7019 );
7020 let below_zero = details
7021 .minimum
7022 .as_ref()
7023 .and_then(integer_value)
7024 .is_some_and(|value| value < 0)
7025 || annotated_numbers
7026 .iter()
7027 .filter_map(|number| integer_value(number))
7028 .any(|value| value < 0)
7029 || matches!(
7030 details.exclusive_minimum,
7031 Some(crate::openapi::ExclusiveBound::Number(value)) if value < 0.0
7032 );
7033 let above_u32 = details
7034 .maximum
7035 .as_ref()
7036 .is_some_and(|number| above(number, i128::from(u32::MAX)))
7037 || annotated_numbers
7038 .iter()
7039 .any(|number| above(number, i128::from(u32::MAX)))
7040 || matches!(
7041 details.exclusive_maximum,
7042 Some(crate::openapi::ExclusiveBound::Number(value)) if value > u32::MAX as f64
7043 );
7044
7045 let configured = self
7046 .type_mapper
7047 .integer_format(details.format.as_deref())
7048 .rust_type;
7049 match configured.as_str() {
7050 "i32" if below_i32 || above_i32 => {
7051 if below_i64 || above_i64 {
7052 "i128".to_string()
7053 } else {
7054 "i64".to_string()
7055 }
7056 }
7057 "i64" if below_i64 => "i128".to_string(),
7058 "i64" if above_i64 && explicitly_nonnegative => "u64".to_string(),
7059 "i64" if above_i64 => "i128".to_string(),
7060 "u32" if below_zero => {
7061 if below_i64 || above_i64 {
7062 "i128".to_string()
7063 } else {
7064 "i64".to_string()
7065 }
7066 }
7067 "u32" if above_u32 => "u64".to_string(),
7068 "u64" if below_zero => "i128".to_string(),
7069 configured => configured.to_string(),
7070 }
7071 }
7072
7073 fn analyze_anyof_union(
7074 &mut self,
7075 any_of_schemas: &[Schema],
7076 discriminator: Option<&Discriminator>,
7077 dependencies: &mut HashSet<String>,
7078 context_name: &str,
7079 ) -> Result<SchemaType> {
7080 let original_indices = (0..any_of_schemas.len()).collect::<Vec<_>>();
7081
7082 let expanded_branches;
7084 let any_of_schemas = match self.expand_pointer_branches(any_of_schemas) {
7085 Some(expanded) => {
7086 expanded_branches = expanded;
7087 expanded_branches.as_slice()
7088 }
7089 None => any_of_schemas,
7090 };
7091
7092 let boolean_resolved;
7094 let boolean_resolved_indices;
7095 let any_of_schemas = match Self::resolve_boolean_branches(any_of_schemas) {
7096 Ok(resolved) => {
7097 boolean_resolved_indices = any_of_schemas
7098 .iter()
7099 .zip(original_indices.iter().copied())
7100 .filter_map(|(branch, index)| {
7101 (!matches!(branch, Schema::Bool(false))).then_some(index)
7102 })
7103 .collect::<Vec<_>>();
7104 boolean_resolved = resolved;
7105 boolean_resolved.as_slice()
7106 }
7107 Err(()) => {
7108 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema));
7109 }
7110 };
7111 let source_indices = boolean_resolved_indices.as_slice();
7112 if any_of_schemas.is_empty() {
7113 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::NeverMatches));
7114 }
7115
7116 let filtered_owned: Vec<Schema>;
7122 let filtered_indices: Vec<usize>;
7123 let (any_of_schemas, source_indices): (&[Schema], &[usize]) = if any_of_schemas
7124 .iter()
7125 .any(Schema::is_explicit_null_only)
7126 {
7127 filtered_owned = any_of_schemas
7128 .iter()
7129 .filter(|s| !s.is_explicit_null_only())
7130 .cloned()
7131 .collect();
7132 filtered_indices = any_of_schemas
7133 .iter()
7134 .zip(source_indices.iter().copied())
7135 .filter_map(|(schema, index)| (!schema.is_explicit_null_only()).then_some(index))
7136 .collect();
7137 if filtered_owned.is_empty() {
7138 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema));
7139 }
7140 if filtered_owned.len() == 1 {
7141 return self
7142 .analyze_schema_value(&filtered_owned[0], context_name)
7143 .map(|a| a.schema_type);
7144 }
7145 (&filtered_owned, &filtered_indices)
7146 } else {
7147 (any_of_schemas, source_indices)
7148 };
7149
7150 if let [only] = any_of_schemas {
7153 return self
7154 .analyze_schema_value(only, context_name)
7155 .map(|analyzed| analyzed.schema_type);
7156 }
7157
7158 if let Some(shared) = self.shared_branch_type(any_of_schemas) {
7162 return Ok(shared);
7163 }
7164
7165 let has_refs = any_of_schemas.iter().any(|s| s.is_reference());
7167 let has_objects = any_of_schemas.iter().any(|s| {
7168 matches!(s.schema_type(), Some(OpenApiSchemaType::Object))
7169 || s.inferred_type() == Some(OpenApiSchemaType::Object)
7170 });
7171 let has_arrays = any_of_schemas
7172 .iter()
7173 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Array)));
7174
7175 let all_string_like = any_of_schemas.iter().all(|s| {
7178 matches!(s.schema_type(), Some(OpenApiSchemaType::String))
7179 || s.details().const_value.is_some()
7180 });
7181
7182 if (has_refs || has_objects || has_arrays || any_of_schemas.len() > 1) && !all_string_like {
7183 if let Some(disc) = discriminator {
7185 return self.analyze_oneof_union(
7187 any_of_schemas,
7188 Some(disc),
7189 context_name,
7190 dependencies,
7191 InlineUnionKind::AnyOf,
7192 Some(source_indices),
7193 );
7194 }
7195
7196 if let Some(disc_field) = self.detect_discriminator_field(any_of_schemas) {
7198 return self.analyze_oneof_union(
7199 any_of_schemas,
7200 Some(&Discriminator {
7201 property_name: disc_field,
7202 mapping: None,
7203 default_mapping: None,
7204 extensions: crate::extensions::Extensions::default(),
7205 }),
7206 context_name,
7207 dependencies,
7208 InlineUnionKind::AnyOf,
7209 Some(source_indices),
7210 );
7211 }
7212
7213 let mut variants = Vec::new();
7215
7216 for (schema, original_index) in
7217 any_of_schemas.iter().zip(source_indices.iter().copied())
7218 {
7219 if let Some(ref_str) = schema.reference() {
7220 if let Some(target) = self.extract_schema_name(ref_str) {
7221 dependencies.insert(target.to_string());
7222 variants.push(SchemaRef {
7223 target: target.to_string(),
7224 nullable: self.schema_or_reference_is_nullable(schema),
7225 });
7226 }
7227 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object))
7228 || schema.inferred_type() == Some(OpenApiSchemaType::Object)
7229 {
7230 let inline_type_name = self.generate_inline_type_name(schema, original_index);
7232
7233 let inline_type_name = self.add_inline_union_branch_schema(
7235 &inline_type_name,
7236 schema,
7237 dependencies,
7238 context_name,
7239 InlineUnionKind::AnyOf,
7240 original_index,
7241 None,
7242 )?;
7243
7244 variants.push(SchemaRef {
7245 target: inline_type_name,
7246 nullable: false,
7247 });
7248 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Array)) {
7249 let preferred_array_type_name =
7251 if let Some(items_schema) = schema.details().item_schema() {
7252 if let Some(ref_str) = items_schema.reference() {
7253 if let Some(item_type_name) = self.extract_schema_name(ref_str) {
7254 dependencies.insert(item_type_name.to_string());
7255 format!("{item_type_name}Array")
7256 } else {
7257 self.generate_context_aware_name(
7258 context_name,
7259 "Array",
7260 original_index,
7261 Some(schema),
7262 )
7263 }
7264 } else {
7265 self.generate_context_aware_name(
7266 context_name,
7267 "Array",
7268 original_index,
7269 Some(schema),
7270 )
7271 }
7272 } else {
7273 self.generate_context_aware_name(
7274 context_name,
7275 "Array",
7276 original_index,
7277 Some(schema),
7278 )
7279 };
7280 let array_type_name = self.allocate_inline_union_branch_name(
7281 &preferred_array_type_name,
7282 context_name,
7283 InlineUnionKind::AnyOf,
7284 original_index,
7285 None,
7286 schema,
7287 );
7288 let array_type =
7290 self.analyze_array_schema(schema, context_name, dependencies)?;
7291
7292 self.resolved_cache.insert(
7294 array_type_name.clone(),
7295 AnalyzedSchema {
7296 name: array_type_name.clone(),
7297 original: serde_json::to_value(schema).unwrap_or(Value::Null),
7298 schema_type: array_type,
7299 dependencies: HashSet::new(),
7300 nullable: false,
7301 description: Some("Array variant in union".to_string()),
7302 default: None,
7303 },
7304 );
7305
7306 dependencies.insert(array_type_name.clone());
7308
7309 variants.push(SchemaRef {
7310 target: array_type_name,
7311 nullable: false,
7312 });
7313 } else if let Some(schema_type) = schema.schema_type() {
7314 let primitive_unions = self
7324 .type_mapper
7325 .config_shape_primitive_unions()
7326 .unwrap_or(true);
7327
7328 if primitive_unions {
7329 variants.push(SchemaRef {
7330 target: self
7331 .openapi_type_to_rust_type(schema_type.clone(), schema.details()),
7332 nullable: false,
7333 });
7334 } else {
7335 let preferred_inline_type_name = match schema_type {
7336 OpenApiSchemaType::String => {
7337 if original_index == 0 {
7338 format!("{context_name}String")
7339 } else {
7340 format!("{context_name}StringVariant{original_index}")
7341 }
7342 }
7343 OpenApiSchemaType::Number => {
7344 if original_index == 0 {
7345 format!("{context_name}Number")
7346 } else {
7347 format!("{context_name}NumberVariant{original_index}")
7348 }
7349 }
7350 OpenApiSchemaType::Integer => {
7351 if original_index == 0 {
7352 format!("{context_name}Integer")
7353 } else {
7354 format!("{context_name}IntegerVariant{original_index}")
7355 }
7356 }
7357 OpenApiSchemaType::Boolean => {
7358 if original_index == 0 {
7359 format!("{context_name}Boolean")
7360 } else {
7361 format!("{context_name}BooleanVariant{original_index}")
7362 }
7363 }
7364 _ => format!("{context_name}Variant{original_index}"),
7365 };
7366 let inline_type_name = self.allocate_inline_union_branch_name(
7367 &preferred_inline_type_name,
7368 context_name,
7369 InlineUnionKind::AnyOf,
7370 original_index,
7371 None,
7372 schema,
7373 );
7374
7375 let rust_type =
7376 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
7377
7378 self.resolved_cache.insert(
7379 inline_type_name.clone(),
7380 AnalyzedSchema {
7381 name: inline_type_name.clone(),
7382 original: serde_json::to_value(schema).unwrap_or(Value::Null),
7383 schema_type: SchemaType::Primitive {
7384 rust_type,
7385 serde_with: None,
7386 },
7387 dependencies: HashSet::new(),
7388 nullable: false,
7389 description: schema.details().description.clone(),
7390 default: None,
7391 },
7392 );
7393
7394 dependencies.insert(inline_type_name.clone());
7395
7396 variants.push(SchemaRef {
7397 target: inline_type_name,
7398 nullable: false,
7399 });
7400 }
7401 } else {
7402 let inline_type_name = self.generate_context_aware_name(
7410 context_name,
7411 "InlineVariant",
7412 original_index,
7413 Some(schema),
7414 );
7415 let inline_type_name = self.add_inline_union_branch_schema(
7416 &inline_type_name,
7417 schema,
7418 dependencies,
7419 context_name,
7420 InlineUnionKind::AnyOf,
7421 original_index,
7422 None,
7423 )?;
7424 dependencies.insert(inline_type_name.clone());
7425 variants.push(SchemaRef {
7426 target: inline_type_name,
7427 nullable: false,
7428 });
7429 }
7430 }
7431
7432 if !variants.is_empty() {
7433 return Ok(SchemaType::Union {
7434 variants,
7435 exclusive: false,
7436 });
7437 }
7438 }
7439
7440 let all_strings = any_of_schemas.iter().all(|schema| {
7442 matches!(schema.schema_type(), Some(OpenApiSchemaType::String))
7443 || schema.details().const_value.is_some()
7444 });
7445
7446 if all_strings {
7447 let mut enum_values = Vec::new();
7449 let mut has_open_string = false;
7450
7451 for schema in any_of_schemas {
7452 match schema
7457 .details()
7458 .string_enum_values()
7459 .filter(|values| !values.is_empty())
7460 {
7461 Some(values) => {
7462 for value in values {
7463 if !enum_values.contains(&value) {
7464 enum_values.push(value);
7465 }
7466 }
7467 }
7468 None => {
7469 if matches!(schema.schema_type(), Some(OpenApiSchemaType::String)) {
7470 has_open_string = true;
7471 }
7472 }
7473 }
7474 }
7475
7476 if !enum_values.is_empty() {
7477 if has_open_string {
7478 return Ok(SchemaType::ExtensibleEnum {
7481 known_values: enum_values,
7482 });
7483 } else {
7484 return Ok(SchemaType::StringEnum {
7486 values: enum_values,
7487 });
7488 }
7489 }
7490 }
7491
7492 Ok(self.untyped_value(
7494 self.untyped_context(""),
7495 UntypedReason::UnrepresentableUnion,
7496 ))
7497 }
7498
7499 fn find_recursive_anchor_schema(&self) -> Option<String> {
7501 for (schema_name, schema) in &self.schemas {
7503 let details = schema.details();
7504 if details.recursive_anchor == Some(true) {
7505 return Some(schema_name.clone());
7506 }
7507 }
7508
7509 None
7513 }
7514
7515 fn should_use_dynamic_json(&self, schema: &Schema) -> bool {
7518 if let Schema::AnyOf { any_of, .. } = schema {
7520 if any_of.len() == 2 {
7521 let has_null = any_of
7522 .iter()
7523 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)));
7524 let has_empty_object = any_of.iter().any(|s| self.is_dynamic_object_pattern(s));
7525
7526 if has_null && has_empty_object {
7527 return true;
7528 }
7529 }
7530 }
7531
7532 self.is_dynamic_object_pattern(schema)
7534 }
7535
7536 fn is_dynamic_object_pattern(&self, schema: &Schema) -> bool {
7538 let is_object = match schema.schema_type() {
7540 Some(OpenApiSchemaType::Object) => true,
7541 None => schema.inferred_type() == Some(OpenApiSchemaType::Object),
7542 _ => false,
7543 };
7544
7545 if !is_object {
7546 return false;
7547 }
7548
7549 Self::object_shape_is_unconstrained(schema.details())
7550 }
7551
7552 fn object_shape_is_unconstrained(details: &crate::openapi::SchemaDetails) -> bool {
7560 if details.additional_properties.is_some() {
7565 return false;
7566 }
7567
7568 let no_properties = details
7570 .properties
7571 .as_ref()
7572 .map(|props| props.is_empty())
7573 .unwrap_or(true);
7574
7575 if no_properties {
7576 let has_structural_constraints = details
7579 .required
7580 .as_ref()
7581 .map(|req| !req.is_empty())
7582 .unwrap_or(false)
7583 || details.pattern_properties.is_some()
7584 || details.property_names.is_some()
7585 || details.min_properties.is_some()
7586 || details.max_properties.is_some()
7587 || details.dependent_required.is_some()
7588 || details.dependent_schemas.is_some()
7589 || details.if_schema.is_some()
7590 || details.then_schema.is_some()
7591 || details.else_schema.is_some();
7592
7593 return !has_structural_constraints;
7594 }
7595
7596 false
7597 }
7598
7599 fn analyze_operations(&mut self, analysis: &mut SchemaAnalysis) -> Result<()> {
7601 let spec: crate::openapi::OpenApiSpec = parse_spec_document(&self.openapi_spec)?;
7602 let mut canonical_operation_ids = HashSet::new();
7607
7608 if let Some(paths) = &spec.paths {
7609 for (path, path_item) in paths {
7610 let resolved = self.resolve_path_item(path_item, &spec)?;
7612 let pi: &crate::openapi::PathItem = resolved.as_ref().unwrap_or(path_item);
7613 self.ingest_path_item_operations(
7614 path,
7615 pi,
7616 analysis,
7617 &mut canonical_operation_ids,
7618 None,
7619 path_item.reference.as_deref(),
7620 )?;
7621 }
7622 }
7623 if let Some(webhooks) = &spec.webhooks {
7630 for (name, path_item) in webhooks {
7631 let synthetic_path = format!("/__webhook__/{name}");
7632 let resolved = self.resolve_path_item(path_item, &spec)?;
7633 let pi = resolved.as_ref().unwrap_or(path_item);
7634 self.ingest_path_item_operations(
7635 &synthetic_path,
7636 pi,
7637 analysis,
7638 &mut canonical_operation_ids,
7639 Some(name),
7640 path_item.reference.as_deref(),
7641 )?;
7642 }
7643 }
7644 Ok(())
7645 }
7646
7647 fn resolve_path_item(
7651 &self,
7652 path_item: &crate::openapi::PathItem,
7653 spec: &crate::openapi::OpenApiSpec,
7654 ) -> Result<Option<crate::openapi::PathItem>> {
7655 let Some(reference) = &path_item.reference else {
7656 return Ok(None);
7657 };
7658 let target_name = reference
7659 .strip_prefix("#/components/pathItems/")
7660 .ok_or_else(|| {
7661 GeneratorError::UnresolvedReference(format!(
7662 "Path Item $ref must point at #/components/pathItems/{{name}}, got {reference}"
7663 ))
7664 })?;
7665 let pi = spec
7666 .components
7667 .as_ref()
7668 .and_then(|c| c.path_items.as_ref())
7669 .and_then(|map| map.get(target_name))
7670 .ok_or_else(|| {
7671 GeneratorError::UnresolvedReference(format!(
7672 "Path Item ref {reference} not found in components/pathItems"
7673 ))
7674 })?;
7675 Ok(Some(pi.clone()))
7676 }
7677
7678 fn ingest_path_item_operations(
7679 &mut self,
7680 path: &str,
7681 path_item: &crate::openapi::PathItem,
7682 analysis: &mut SchemaAnalysis,
7683 canonical_operation_ids: &mut HashSet<String>,
7684 webhook_name: Option<&str>,
7685 path_item_reference: Option<&str>,
7686 ) -> Result<()> {
7687 for (method, operation) in path_item.operations() {
7688 let raw_operation_id = operation
7690 .operation_id
7691 .clone()
7692 .unwrap_or_else(|| Self::generate_operation_id(method, path));
7693
7694 let operation_id = if canonical_operation_ids
7705 .contains(&Self::canonical_operation_id(&raw_operation_id))
7706 {
7707 let method_lower = method.to_lowercase();
7708 let mut candidate = format!("{}_{}", raw_operation_id, method_lower);
7709 let mut suffix = 2;
7710 while canonical_operation_ids.contains(&Self::canonical_operation_id(&candidate)) {
7711 candidate = format!("{}_{}_{}", raw_operation_id, method_lower, suffix);
7712 suffix += 1;
7713 }
7714 eprintln!(
7715 "⚠️ duplicate operationId `{}` at `{} {}` — disambiguated to `{}`",
7716 raw_operation_id, method, path, candidate
7717 );
7718 candidate
7719 } else {
7720 raw_operation_id.clone()
7721 };
7722
7723 let (op_info, responses) = self.analyze_single_operation(
7724 &operation_id,
7725 method,
7726 path,
7727 operation,
7728 path_item.parameters.as_ref(),
7729 analysis,
7730 )?;
7731 analysis
7732 .operation_id_aliases
7733 .entry(raw_operation_id)
7734 .or_default()
7735 .push(operation_id.clone());
7736 canonical_operation_ids.insert(Self::canonical_operation_id(&operation_id));
7737 analysis
7738 .operation_responses
7739 .insert(operation_id.clone(), responses);
7740 let source_path = webhook_name.unwrap_or(path);
7741 let escaped = source_path.replace('~', "~0").replace('/', "~1");
7742 let collection = if webhook_name.is_some() {
7743 "webhooks"
7744 } else {
7745 "paths"
7746 };
7747 let base_pointer = path_item_reference
7748 .and_then(|reference| reference.strip_prefix('#'))
7749 .map(str::to_owned)
7750 .unwrap_or_else(|| format!("/{collection}/{escaped}"));
7751 let operation_pointer = if path_item.additional_operations.as_ref().is_some_and(|map| {
7752 map.get(method)
7753 .is_some_and(|candidate| std::ptr::eq(candidate, operation))
7754 }) {
7755 format!(
7756 "{base_pointer}/additionalOperations/{}",
7757 method.replace('~', "~0").replace('/', "~1")
7758 )
7759 } else {
7760 format!("{base_pointer}/{}", method.to_ascii_lowercase())
7761 };
7762 analysis.operation_sources.insert(
7763 operation_id.clone(),
7764 OperationSource {
7765 json_pointer: operation_pointer,
7766 method: method.to_ascii_uppercase(),
7767 path: source_path.to_string(),
7768 operation_id: operation.operation_id.clone(),
7769 webhook: webhook_name.is_some(),
7770 },
7771 );
7772 analysis.operations.insert(operation_id, op_info);
7773 }
7774 Ok(())
7775 }
7776
7777 fn canonical_operation_id(operation_id: &str) -> String {
7778 use heck::ToPascalCase;
7779 operation_id.replace('.', "_").to_pascal_case()
7780 }
7781
7782 fn generate_operation_id(method: &str, path: &str) -> String {
7785 let mut operation_id = method.to_lowercase();
7787
7788 let path_parts: Vec<&str> = path.trim_start_matches('/').split('/').collect();
7790
7791 for part in path_parts {
7792 if part.is_empty() {
7793 continue;
7794 }
7795
7796 let cleaned_part = if part.starts_with('{') && part.ends_with('}') {
7798 &part[1..part.len() - 1]
7799 } else {
7800 part
7801 };
7802
7803 let pascal_case_part = cleaned_part
7805 .split(&['-', '_'][..])
7806 .map(|s| {
7807 let mut chars = s.chars();
7808 match chars.next() {
7809 None => String::new(),
7810 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
7811 }
7812 })
7813 .collect::<String>();
7814
7815 operation_id.push_str(&pascal_case_part);
7816 }
7817
7818 operation_id
7819 }
7820
7821 fn analyze_single_operation(
7823 &mut self,
7824 operation_id: &str,
7825 method: &str,
7826 path: &str,
7827 operation: &crate::openapi::Operation,
7828 path_item_parameters: Option<&Vec<crate::openapi::Parameter>>,
7829 _analysis: &mut SchemaAnalysis,
7830 ) -> Result<(OperationInfo, BTreeMap<String, OperationResponse>)> {
7831 let raw_path_item = self
7832 .openapi_spec
7833 .get("paths")
7834 .and_then(|paths| paths.get(path))
7835 .cloned();
7836 let raw_operation = raw_path_item
7837 .as_ref()
7838 .and_then(|path_item| path_item.get(method.to_ascii_lowercase()))
7839 .cloned();
7840 let request_body = operation
7841 .request_body
7842 .as_ref()
7843 .map(|request_body| self.resolve_request_body(request_body))
7844 .transpose()?;
7845 let mut op_info = OperationInfo {
7846 operation_id: operation_id.to_string(),
7847 method: method.to_uppercase(),
7848 path: normalize_operation_path(path),
7849 summary: operation.summary.clone(),
7850 description: operation.description.clone(),
7851 request_body: None,
7852 request_body_required: request_body
7854 .as_ref()
7855 .and_then(|rb| rb.required)
7856 .unwrap_or(false),
7857 response_schemas: BTreeMap::new(),
7858 parameters: Vec::new(),
7859 supports_streaming: false, stream_parameter: None, tags: operation.tags.clone().unwrap_or_default(),
7862 deprecated: operation.deprecated.unwrap_or(false),
7863 };
7864 let mut operation_responses = BTreeMap::new();
7865
7866 if let Some(request_body) = &request_body {
7868 use crate::openapi::{
7869 is_binary_media_type, is_form_urlencoded_media_type, is_json_media_type,
7870 media_type_essence,
7871 };
7872 if let Some((content_type, maybe_schema)) = request_body.best_content() {
7873 op_info.request_body = if is_json_media_type(content_type) {
7874 match maybe_schema {
7875 Some(s) => {
7876 let validation_schema = self
7877 .raw_request_body_schema(raw_operation.as_ref(), content_type)
7878 .unwrap_or(
7879 serde_json::to_value(s).map_err(GeneratorError::ParseError)?,
7880 );
7881 Some(
7882 self.resolve_or_inline_schema(s, operation_id, "Request")
7883 .map(|name| RequestBodyContent::Json {
7884 schema_name: name,
7885 media_type: content_type.to_string(),
7886 validation_schema,
7887 })?,
7888 )
7889 }
7890 None => Some(RequestBodyContent::SchemaLess {
7891 media_type: content_type.to_string(),
7892 }),
7893 }
7894 } else if is_form_urlencoded_media_type(content_type) {
7895 match maybe_schema {
7896 Some(s) => {
7897 let validation_schema = self
7898 .raw_request_body_schema(raw_operation.as_ref(), content_type)
7899 .unwrap_or(
7900 serde_json::to_value(s).map_err(GeneratorError::ParseError)?,
7901 );
7902 Some(
7903 self.resolve_or_inline_schema(s, operation_id, "Request")
7904 .map(|name| RequestBodyContent::FormUrlEncoded {
7905 schema_name: name,
7906 media_type: content_type.to_string(),
7907 validation_schema,
7908 })?,
7909 )
7910 }
7911 None => Some(RequestBodyContent::SchemaLess {
7912 media_type: content_type.to_string(),
7913 }),
7914 }
7915 } else if media_type_essence(content_type)
7916 .eq_ignore_ascii_case("multipart/form-data")
7917 {
7918 match maybe_schema {
7919 Some(schema) => {
7920 let validation_schema = self
7921 .raw_request_body_schema(raw_operation.as_ref(), content_type)
7922 .unwrap_or(
7923 serde_json::to_value(schema)
7924 .map_err(GeneratorError::ParseError)?,
7925 );
7926 let encoding = request_body
7927 .content
7928 .as_ref()
7929 .and_then(|content| content.get(content_type))
7930 .and_then(|media_type| media_type.encoding.as_ref())
7931 .into_iter()
7932 .flatten()
7933 .filter_map(|(field, encoding)| {
7934 Some((field.clone(), encoding.content_type.clone()?))
7935 })
7936 .collect();
7937 Some(
7938 self.resolve_or_inline_schema(schema, operation_id, "Request")
7939 .map(|schema_name| RequestBodyContent::Multipart {
7940 schema_name,
7941 media_type: content_type.to_string(),
7942 validation_schema,
7943 encoding,
7944 })?,
7945 )
7946 }
7947 None => Some(RequestBodyContent::SchemaLess {
7948 media_type: content_type.to_string(),
7949 }),
7950 }
7951 } else if is_binary_media_type(content_type, maybe_schema) {
7952 if media_type_essence(content_type)
7953 .eq_ignore_ascii_case("application/octet-stream")
7954 {
7955 Some(RequestBodyContent::OctetStream {
7956 media_type: content_type.to_string(),
7957 })
7958 } else {
7959 Some(RequestBodyContent::Binary {
7960 media_type: content_type.to_string(),
7961 })
7962 }
7963 } else if crate::openapi::is_text_media_type(content_type) {
7964 Some(RequestBodyContent::TextPlain {
7969 media_type: content_type.to_string(),
7970 })
7971 } else {
7972 None
7973 };
7974 }
7975 if op_info.request_body.is_none() {
7976 let mut media_types = request_body
7977 .content
7978 .as_ref()
7979 .map(|content| content.keys().cloned().collect::<Vec<_>>())
7980 .unwrap_or_default();
7981 media_types.sort();
7982 if !media_types.is_empty() {
7983 op_info.request_body = Some(RequestBodyContent::Unsupported { media_types });
7984 }
7985 }
7986 }
7987
7988 if let Some(responses) = &operation.responses {
7990 for (status_code, response) in responses {
7991 let response = self.resolve_response(response)?;
7992 let supports_streaming = response.content.as_ref().is_some_and(|content| {
7998 content
7999 .keys()
8000 .any(|ct| crate::openapi::is_event_stream_media_type(ct))
8001 });
8002 if supports_streaming {
8003 op_info.supports_streaming = true;
8004 }
8005
8006 let mut response_info = OperationResponse {
8007 supports_streaming,
8008 has_content: response
8009 .content
8010 .as_ref()
8011 .is_some_and(|content| !content.is_empty()),
8012 ..Default::default()
8013 };
8014 if let Some((media_type, schema)) = response.json_content() {
8015 if let Some(schema_ref) = schema.reference() {
8016 if let Some(schema_name) = self.extract_schema_name(schema_ref) {
8018 op_info
8019 .response_schemas
8020 .insert(status_code.clone(), schema_name.to_string());
8021 response_info.schema_name = Some(schema_name.to_string());
8022 response_info.media_type = Some(media_type.to_string());
8023 response_info.body = Some(OperationResponseBody::Json {
8024 schema_name: schema_name.to_string(),
8025 media_type: media_type.to_string(),
8026 });
8027 }
8028 } else {
8029 let synthetic_name =
8031 self.generate_inline_response_type_name(operation_id, status_code);
8032
8033 let mut deps = HashSet::new();
8035 let synthetic_name =
8036 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
8037
8038 op_info
8039 .response_schemas
8040 .insert(status_code.clone(), synthetic_name.clone());
8041 response_info.body = Some(OperationResponseBody::Json {
8042 schema_name: synthetic_name.clone(),
8043 media_type: media_type.to_string(),
8044 });
8045 response_info.schema_name = Some(synthetic_name);
8046 response_info.media_type = Some(media_type.to_string());
8047 }
8048 }
8049 if response_info.body.is_none()
8050 && let Some(content) = response.content.as_ref()
8051 {
8052 let selected = content
8053 .iter()
8054 .find(|(media_type, media)| {
8055 matches!(
8056 crate::openapi::classify_response_media_type(
8057 media_type,
8058 media.schema.as_ref()
8059 ),
8060 crate::openapi::ResponseMediaKind::Text
8061 )
8062 })
8063 .or_else(|| {
8064 content.iter().find(|(media_type, media)| {
8065 matches!(
8066 crate::openapi::classify_response_media_type(
8067 media_type,
8068 media.schema.as_ref()
8069 ),
8070 crate::openapi::ResponseMediaKind::Binary
8071 ) && !crate::openapi::is_wildcard_media_type(media_type)
8072 })
8073 })
8074 .or_else(|| {
8075 content.iter().find(|(media_type, media)| {
8076 matches!(
8077 crate::openapi::classify_response_media_type(
8078 media_type,
8079 media.schema.as_ref()
8080 ),
8081 crate::openapi::ResponseMediaKind::Binary
8082 )
8083 })
8084 });
8085 if let Some((media_type, media)) = selected {
8086 response_info.body = match crate::openapi::classify_response_media_type(
8087 media_type,
8088 media.schema.as_ref(),
8089 ) {
8090 crate::openapi::ResponseMediaKind::Text => {
8091 Some(OperationResponseBody::Text {
8092 media_type: media_type.clone(),
8093 })
8094 }
8095 crate::openapi::ResponseMediaKind::Binary => {
8096 Some(OperationResponseBody::Binary {
8097 media_type: media_type.clone(),
8098 wildcard: crate::openapi::is_wildcard_media_type(media_type),
8099 })
8100 }
8101 _ => None,
8102 };
8103 }
8104 }
8105 if let Some(content) = &response.content {
8106 for (media_type, media) in content {
8107 let representation = match crate::openapi::classify_response_media_type(
8108 media_type,
8109 media.schema.as_ref(),
8110 ) {
8111 crate::openapi::ResponseMediaKind::Json => {
8112 let Some(schema) = media.schema.as_ref() else {
8113 continue;
8114 };
8115 let schema_name =
8116 if response_info.media_type.as_ref() == Some(media_type) {
8117 response_info.schema_name.clone()
8118 } else if let Some(reference) = schema.reference() {
8119 self.extract_schema_name(reference).map(str::to_owned)
8120 } else {
8121 let preferred = format!(
8122 "{}{}",
8123 self.generate_inline_response_type_name(
8124 operation_id,
8125 status_code
8126 ),
8127 base_param_ident(media_type)
8128 );
8129 Some(self.add_inline_schema(
8130 &preferred,
8131 schema,
8132 &mut HashSet::new(),
8133 )?)
8134 };
8135 let Some(schema_name) = schema_name else {
8136 continue;
8137 };
8138 OperationRepresentation::Json { schema_name }
8139 }
8140 crate::openapi::ResponseMediaKind::Text => {
8141 OperationRepresentation::Text
8142 }
8143 crate::openapi::ResponseMediaKind::Binary => {
8144 OperationRepresentation::Binary {
8145 wildcard: crate::openapi::is_wildcard_media_type(media_type),
8146 }
8147 }
8148 crate::openapi::ResponseMediaKind::EventStream => {
8149 OperationRepresentation::EventStream
8150 }
8151 crate::openapi::ResponseMediaKind::Unsupported => continue,
8152 };
8153 response_info
8154 .representations
8155 .insert(media_type.clone(), representation);
8156 }
8157 }
8158 response_info.unsupported_media_types = response
8159 .content
8160 .as_ref()
8161 .into_iter()
8162 .flat_map(|content| content.iter())
8163 .filter(|(media_type, content)| {
8164 match crate::openapi::classify_response_media_type(
8165 media_type,
8166 content.schema.as_ref(),
8167 ) {
8168 crate::openapi::ResponseMediaKind::Json => content.schema.is_none(),
8169 crate::openapi::ResponseMediaKind::Unsupported => true,
8170 crate::openapi::ResponseMediaKind::EventStream
8171 | crate::openapi::ResponseMediaKind::Text
8172 | crate::openapi::ResponseMediaKind::Binary => false,
8173 }
8174 })
8175 .map(|(media_type, _)| media_type.clone())
8176 .collect();
8177 operation_responses.insert(status_code.clone(), response_info);
8178 }
8179 }
8180
8181 if op_info.supports_streaming
8184 && let Some(parameters) = &operation.parameters
8185 {
8186 for param in parameters {
8187 if let Some(name) = param.name.as_deref() {
8188 if name.eq_ignore_ascii_case("stream") {
8189 op_info.stream_parameter = Some(name.to_string());
8190 break;
8191 }
8192 }
8193 }
8194 }
8195
8196 if let Some(parameters) = &operation.parameters {
8198 for (index, param) in parameters.iter().enumerate() {
8199 let resolved = self.resolve_parameter(param).into_owned();
8203 let validation_schema = raw_operation
8204 .as_ref()
8205 .and_then(|operation| operation.get("parameters"))
8206 .and_then(Value::as_array)
8207 .and_then(|parameters| parameters.get(index))
8208 .and_then(|parameter| self.raw_parameter_schema(parameter));
8209 if let Some(param_info) =
8210 self.analyze_parameter(&resolved, operation_id, validation_schema)?
8211 {
8212 op_info.parameters.push(param_info);
8213 }
8214 }
8215 }
8216
8217 if let Some(path_params) = path_item_parameters {
8219 let existing_keys: std::collections::HashSet<(String, String)> = op_info
8220 .parameters
8221 .iter()
8222 .map(|p| (p.name.clone(), p.location.clone()))
8223 .collect();
8224 for (index, param) in path_params.iter().enumerate() {
8225 let resolved = self.resolve_parameter(param).into_owned();
8226 let validation_schema = raw_path_item
8227 .as_ref()
8228 .and_then(|path_item| path_item.get("parameters"))
8229 .and_then(Value::as_array)
8230 .and_then(|parameters| parameters.get(index))
8231 .and_then(|parameter| self.raw_parameter_schema(parameter));
8232 if let Some(param_info) =
8233 self.analyze_parameter(&resolved, operation_id, validation_schema)?
8234 {
8235 if !existing_keys
8236 .contains(&(param_info.name.clone(), param_info.location.clone()))
8237 {
8238 op_info.parameters.push(param_info);
8239 }
8240 }
8241 }
8242 }
8243
8244 let mut declared_path_names: std::collections::HashSet<String> = op_info
8252 .parameters
8253 .iter()
8254 .filter(|p| p.location == "path")
8255 .map(|p| p.name.clone())
8256 .collect();
8257 let bytes = path.as_bytes().iter();
8258 let mut current = String::new();
8259 let mut in_brace = false;
8260 let mut synthesized: Vec<String> = Vec::new();
8261 for b in bytes {
8262 match *b {
8263 b'{' => {
8264 in_brace = true;
8265 current.clear();
8266 }
8267 b'}' if in_brace => {
8268 in_brace = false;
8269 if !current.is_empty() && !declared_path_names.contains(¤t) {
8270 synthesized.push(current.clone());
8271 declared_path_names.insert(current.clone());
8272 }
8273 }
8274 _ if in_brace => current.push(*b as char),
8275 _ => {}
8276 }
8277 }
8278 for name in synthesized {
8279 eprintln!(
8280 "⚠️ path `{}` references `{{{}}}` but the spec doesn't declare it as a parameter — synthesizing as required String",
8281 path, name
8282 );
8283 op_info.parameters.push(ParameterInfo {
8284 name,
8285 location: "path".to_string(),
8286 required: true,
8287 schema_ref: None,
8288 rust_type: "String".to_string(),
8289 description: None,
8290 enum_values: None,
8291 enum_varnames: None,
8292 rust_ident: None,
8293 query_serialization: None,
8294 allow_reserved: false,
8295 validation_schema: None,
8296 });
8297 }
8298
8299 let mut used: std::collections::HashSet<String> =
8307 ["request", "body", "req", "request_url", "client", "form"]
8308 .into_iter()
8309 .map(str::to_owned)
8310 .collect();
8311 for p in op_info.parameters.iter_mut() {
8312 let raw = base_param_ident(&p.name);
8313 let raw = if matches!(raw.as_str(), "self" | "super" | "crate") {
8314 format!("{raw}_param")
8315 } else {
8316 raw
8317 };
8318 let mut chosen = raw.clone();
8319 let mut suffix = 2;
8320 while !used.insert(chosen.clone()) {
8321 chosen = format!("{raw}_{suffix}");
8322 suffix += 1;
8323 }
8324 p.rust_ident = Some(chosen);
8325 }
8326
8327 Ok((op_info, operation_responses))
8328 }
8329
8330 fn resolve_request_body(
8332 &self,
8333 request_body: &crate::openapi::RequestBody,
8334 ) -> Result<crate::openapi::RequestBody> {
8335 let mut current = request_body.clone();
8336 let mut visited = HashSet::new();
8337 while let Some(reference) = current.reference.clone() {
8338 if !visited.insert(reference.clone()) {
8339 return Err(GeneratorError::CircularDependency(format!(
8340 "request body reference {reference}"
8341 )));
8342 }
8343
8344 let pointer = reference.strip_prefix('#').ok_or_else(|| {
8345 GeneratorError::UnresolvedReference(format!(
8346 "external request body reference `{reference}` is not supported"
8347 ))
8348 })?;
8349 if !pointer.is_empty() && !pointer.starts_with('/') {
8350 return Err(GeneratorError::UnresolvedReference(format!(
8351 "request body reference `{reference}` is not a local JSON Pointer"
8352 )));
8353 }
8354 let value = self.openapi_spec.pointer(pointer).ok_or_else(|| {
8355 GeneratorError::UnresolvedReference(format!(
8356 "request body reference `{reference}` does not exist"
8357 ))
8358 })?;
8359 let object = value.as_object().ok_or_else(|| {
8360 GeneratorError::InvalidSchema(format!(
8361 "request body reference `{reference}` must target an object"
8362 ))
8363 })?;
8364 if !["$ref", "description", "required", "content"]
8365 .iter()
8366 .any(|field| object.contains_key(*field))
8367 {
8368 return Err(GeneratorError::InvalidSchema(format!(
8369 "request body reference `{reference}` does not target a structurally compatible OpenAPI Request Body Object"
8370 )));
8371 }
8372 current = serde_json::from_value(value.clone()).map_err(|error| {
8373 GeneratorError::InvalidSchema(format!(
8374 "request body reference `{reference}` is not a valid OpenAPI Request Body Object: {error}"
8375 ))
8376 })?;
8377 }
8378 Ok(current)
8379 }
8380
8381 fn resolve_response(
8388 &self,
8389 response: &crate::openapi::Response,
8390 ) -> Result<crate::openapi::Response> {
8391 let mut current = response.clone();
8392 let mut visited = HashSet::new();
8393 while let Some(reference) = current.reference.clone() {
8394 if !visited.insert(reference.clone()) {
8395 return Err(GeneratorError::CircularDependency(format!(
8396 "response reference {reference}"
8397 )));
8398 }
8399
8400 let pointer = reference.strip_prefix('#').ok_or_else(|| {
8401 GeneratorError::UnresolvedReference(format!(
8402 "external response reference `{reference}` is not supported"
8403 ))
8404 })?;
8405 if !pointer.is_empty() && !pointer.starts_with('/') {
8406 return Err(GeneratorError::UnresolvedReference(format!(
8407 "response reference `{reference}` is not a local JSON Pointer"
8408 )));
8409 }
8410 let value = self.openapi_spec.pointer(pointer).ok_or_else(|| {
8411 GeneratorError::UnresolvedReference(format!(
8412 "response reference `{reference}` does not exist"
8413 ))
8414 })?;
8415 let object = value.as_object().ok_or_else(|| {
8416 GeneratorError::InvalidSchema(format!(
8417 "response reference `{reference}` must target an object"
8418 ))
8419 })?;
8420 if !["$ref", "description", "headers", "content", "links"]
8421 .iter()
8422 .any(|field| object.contains_key(*field))
8423 {
8424 return Err(GeneratorError::InvalidSchema(format!(
8425 "response reference `{reference}` does not target a structurally compatible OpenAPI Response Object"
8426 )));
8427 }
8428 current = serde_json::from_value(value.clone()).map_err(|error| {
8429 GeneratorError::InvalidSchema(format!(
8430 "response reference `{reference}` is not a valid OpenAPI Response Object: {error}"
8431 ))
8432 })?;
8433 }
8434 Ok(current)
8435 }
8436
8437 fn generate_inline_response_type_name(&self, operation_id: &str, status_code: &str) -> String {
8444 use heck::ToPascalCase;
8445 let base_name = operation_id.replace('.', "_").to_pascal_case();
8446 let suffix = Self::status_code_suffix(status_code);
8447 format!("{}Response{}", base_name, suffix)
8448 }
8449
8450 fn status_code_suffix(status_code: &str) -> String {
8457 match status_code {
8458 "" | "200" => String::new(),
8459 "default" | "Default" => "Default".to_string(),
8460 other if other.chars().all(|c| c.is_ascii_digit()) => other.to_string(),
8461 other => other.to_ascii_lowercase(),
8462 }
8463 }
8464
8465 fn generate_inline_request_type_name(&self, operation_id: &str) -> String {
8467 use heck::ToPascalCase;
8468 let base_name = operation_id.replace('.', "_").to_pascal_case();
8472 format!("{}Request", base_name)
8473 }
8474
8475 fn resolve_or_inline_schema(
8478 &mut self,
8479 schema: &crate::openapi::Schema,
8480 operation_id: &str,
8481 suffix: &str,
8482 ) -> Result<String> {
8483 if let Some(schema_ref) = schema.reference()
8484 && let Some(schema_name) = self.extract_schema_name(schema_ref)
8485 {
8486 return Ok(schema_name.to_string());
8487 }
8488 let synthetic_name = if suffix == "Request" {
8490 self.generate_inline_request_type_name(operation_id)
8491 } else {
8492 self.generate_inline_response_type_name(operation_id, "")
8493 };
8494 let mut deps = HashSet::new();
8495 self.add_inline_schema(&synthetic_name, schema, &mut deps)
8496 }
8497
8498 fn resolve_parameter<'a>(
8501 &'a self,
8502 param: &'a crate::openapi::Parameter,
8503 ) -> std::borrow::Cow<'a, crate::openapi::Parameter> {
8504 if let Some(ref_str) = param.reference.as_deref() {
8505 if let Some(param_name) = ref_str.strip_prefix("#/components/parameters/") {
8506 if let Some(resolved) = self.component_parameters.get(param_name) {
8507 return std::borrow::Cow::Borrowed(resolved);
8508 }
8509 }
8510 }
8511 std::borrow::Cow::Borrowed(param)
8512 }
8513
8514 fn referenced_schema_is_string_enum(&self, name: &str) -> bool {
8527 if self.resolve_cached_schema(name).is_some_and(|schema| {
8528 matches!(
8529 schema.schema_type,
8530 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. }
8531 )
8532 }) {
8533 return true;
8534 }
8535 let Some(schema_value) = self
8536 .openapi_spec
8537 .get("components")
8538 .and_then(|c| c.get("schemas"))
8539 .and_then(|s| s.get(name))
8540 else {
8541 return false;
8542 };
8543 let is_string_type = schema_value
8544 .get("type")
8545 .and_then(|v| v.as_str())
8546 .map(|s| s == "string")
8547 .unwrap_or(false);
8548 let has_enum_or_const =
8549 schema_value.get("enum").is_some() || schema_value.get("const").is_some();
8550 is_string_type && has_enum_or_const
8551 }
8552
8553 fn resolve_raw_local_reference(&self, value: &Value) -> Option<Value> {
8554 let Some(reference) = value.get("$ref").and_then(Value::as_str) else {
8555 return Some(value.clone());
8556 };
8557 let pointer = reference.strip_prefix('#')?;
8558 self.openapi_spec.pointer(pointer).cloned()
8559 }
8560
8561 fn raw_request_body_schema(
8562 &self,
8563 operation: Option<&Value>,
8564 content_type: &str,
8565 ) -> Option<Value> {
8566 let request_body = operation?.get("requestBody")?;
8567 self.resolve_raw_local_reference(request_body)?
8568 .get("content")?
8569 .get(content_type)?
8570 .get("schema")
8571 .cloned()
8572 }
8573
8574 fn raw_parameter_schema(&self, parameter: &Value) -> Option<Value> {
8575 self.resolve_raw_local_reference(parameter)?
8576 .get("schema")
8577 .cloned()
8578 }
8579
8580 fn analyze_parameter(
8581 &mut self,
8582 param: &crate::openapi::Parameter,
8583 operation_id: &str,
8584 raw_validation_schema: Option<Value>,
8585 ) -> Result<Option<ParameterInfo>> {
8586 use heck::ToPascalCase;
8587
8588 let name = param.name.as_deref().unwrap_or("");
8589 let location = param.location.as_deref().unwrap_or("");
8590 let required = param.required.unwrap_or(false);
8591 let validation_schema = match raw_validation_schema {
8592 Some(schema) => Some(schema),
8593 None => param
8594 .schema
8595 .as_ref()
8596 .map(serde_json::to_value)
8597 .transpose()
8598 .map_err(GeneratorError::ParseError)?,
8599 };
8600
8601 let mut rust_type = "String".to_string();
8602 let mut schema_ref = None;
8603 let mut enum_values: Option<Vec<String>> = None;
8604 let mut enum_varnames: Option<Vec<String>> = None;
8605 let mut query_serialization: Option<QuerySerialization> = None;
8606
8607 let is_query = location == "query";
8613 let is_simple_header = location == "header"
8614 && matches!(param.style.as_deref(), None | Some("simple"))
8615 && param.explode != Some(true);
8616 let form_style = matches!(param.style.as_deref(), None | Some("form"));
8617 let form_exploded = form_style && param.explode.unwrap_or(true);
8618 let deep_object =
8619 param.style.as_deref() == Some("deepObject") && param.explode != Some(false);
8620
8621 let object_serialization = if !is_query {
8622 None
8623 } else if deep_object {
8624 Some(QuerySerialization::DeepObject)
8625 } else if form_exploded {
8626 Some(QuerySerialization::FormExplodedObject)
8627 } else if form_style {
8628 Some(QuerySerialization::FormObject)
8629 } else {
8630 None
8631 };
8632
8633 if let Some(schema) = ¶m.schema {
8634 if let Some(ref_str) = schema.reference() {
8635 if let Some(name) = self.extract_schema_name(ref_str) {
8641 if self.referenced_schema_is_string_enum(name) {
8642 schema_ref = Some(name.to_string());
8643 } else if object_serialization.is_some()
8644 && self.referenced_schema_is_object(name)
8645 {
8646 schema_ref = Some(name.to_string());
8647 query_serialization = if form_exploded && self.uses_aws_query_conventions()
8648 {
8649 match self.referenced_array_struct_item_type(name, 1) {
8650 Some(ArrayItemType::NestedStructRef { properties, .. }) => {
8651 Some(QuerySerialization::FormExplodedNestedObject {
8652 properties,
8653 })
8654 }
8655 _ => object_serialization.clone(),
8656 }
8657 } else {
8658 object_serialization.clone()
8659 };
8660 } else if (is_query && form_style || is_simple_header)
8661 && let Some(item_type) = self.referenced_array_param_item_type(name)
8662 {
8663 schema_ref = Some(name.to_string());
8669 query_serialization = Some(if is_simple_header {
8670 QuerySerialization::SimpleHeaderArray { item_type }
8671 } else if form_exploded {
8672 QuerySerialization::FormExplodedArray { item_type }
8673 } else {
8674 QuerySerialization::FormArray { item_type }
8675 });
8676 }
8677 }
8678 } else if object_serialization.is_some() && Self::schema_is_inline_object(schema) {
8679 let op_pascal = operation_id.replace('.', "_").to_pascal_case();
8684 let param_pascal = name.to_pascal_case();
8685 let synthetic_name = format!("{op_pascal}{param_pascal}");
8686 let mut deps = HashSet::new();
8687 let synthetic_name = self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
8688 schema_ref = Some(synthetic_name.clone());
8689 query_serialization = if form_exploded && self.uses_aws_query_conventions() {
8690 match self.referenced_array_struct_item_type(&synthetic_name, 1) {
8691 Some(ArrayItemType::NestedStructRef { properties, .. }) => {
8692 Some(QuerySerialization::FormExplodedNestedObject { properties })
8693 }
8694 _ => object_serialization.clone(),
8695 }
8696 } else {
8697 object_serialization.clone()
8698 };
8699 } else if (is_query && form_style || is_simple_header)
8700 && matches!(
8701 schema.schema_type(),
8702 Some(crate::openapi::SchemaType::Array)
8703 )
8704 && let Some(item_type) = self.array_param_item_type(schema)
8705 {
8706 query_serialization = Some(if is_simple_header {
8714 QuerySerialization::SimpleHeaderArray { item_type }
8715 } else if form_exploded {
8716 QuerySerialization::FormExplodedArray { item_type }
8717 } else {
8718 QuerySerialization::FormArray { item_type }
8719 });
8720 } else if let Some(schema_type) = schema.schema_type() {
8721 let format = schema.details().format.clone();
8727 rust_type = match schema_type {
8728 crate::openapi::SchemaType::Boolean => "bool".to_string(),
8729 crate::openapi::SchemaType::Integer => self.integer_rust_type(schema.details()),
8730 crate::openapi::SchemaType::Number => {
8731 self.type_mapper.number_format(format.as_deref()).rust_type
8732 }
8733 crate::openapi::SchemaType::String => "String".to_string(),
8734 _ => "String".to_string(),
8735 };
8736
8737 if matches!(schema_type, crate::openapi::SchemaType::String) {
8738 let details = schema.details();
8739 if details.is_string_enum() {
8740 if let Some(values) = details.string_enum_values() {
8741 if !values.is_empty() {
8742 let op_pascal = operation_id.replace('.', "_").to_pascal_case();
8743 let param_pascal = name.to_pascal_case();
8744 rust_type = format!("{op_pascal}{param_pascal}");
8745 enum_varnames = details
8750 .extra
8751 .get("x-enum-varnames")
8752 .and_then(Value::as_array)
8753 .map(|raw| {
8754 raw.iter()
8755 .filter_map(Value::as_str)
8756 .map(str::to_owned)
8757 .collect::<Vec<_>>()
8758 })
8759 .filter(|names| names.len() == values.len());
8760 enum_values = Some(values);
8761 }
8762 }
8763 }
8764 }
8765 }
8766
8767 if is_query && query_serialization.is_none() {
8768 let referenced_name = schema
8769 .reference()
8770 .and_then(|reference| self.extract_schema_name(reference));
8771 let is_object = referenced_name
8772 .is_some_and(|name| self.referenced_schema_is_object(name))
8773 || Self::schema_is_inline_object(schema);
8774 let is_array = referenced_name
8775 .is_some_and(|name| self.referenced_schema_is_array(name))
8776 || matches!(
8777 schema.schema_type(),
8778 Some(crate::openapi::SchemaType::Array)
8779 );
8780 let is_composed = referenced_name
8781 .is_some_and(|name| self.referenced_schema_is_composed_query_shape(name));
8782 let reason = if param.style.as_deref() == Some("deepObject")
8783 && param.explode == Some(false)
8784 {
8785 Some("style=deepObject with explode=false is undefined by OpenAPI".to_string())
8786 } else if param.style.as_deref() == Some("deepObject") && !is_object {
8787 Some("style=deepObject is defined only for object query parameters".to_string())
8788 } else if is_object {
8789 Some(format!(
8790 "object query parameters do not support style={}",
8791 param.style.as_deref().unwrap_or("form")
8792 ))
8793 } else if is_array && form_style {
8794 Some(
8795 "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"
8796 .to_string(),
8797 )
8798 } else if is_array {
8799 Some(format!(
8800 "array query parameters do not yet support style={}",
8801 param.style.as_deref().unwrap_or("form")
8802 ))
8803 } else if is_composed {
8804 Some(
8805 "composed or union query schemas cannot be projected to an unambiguous flat wire shape"
8806 .to_string(),
8807 )
8808 } else {
8809 None
8810 };
8811 if let Some(reason) = reason {
8812 query_serialization = Some(QuerySerialization::Unsupported { reason });
8813 }
8814 }
8815 }
8816
8817 Ok(Some(ParameterInfo {
8818 name: name.to_string(),
8819 location: location.to_string(),
8820 required,
8821 schema_ref,
8822 rust_type,
8823 description: param.description.clone(),
8824 enum_values,
8825 enum_varnames,
8826 rust_ident: None,
8827 query_serialization,
8828 allow_reserved: param.allow_reserved.unwrap_or(false),
8829 validation_schema,
8830 }))
8831 }
8832
8833 fn array_param_item_type(&self, schema: &crate::openapi::Schema) -> Option<ArrayItemType> {
8842 let items = schema.details().item_schema()?;
8843 let unwrapped = unwrap_annotation_allof(items);
8847 if let Some(ref_str) = unwrapped.reference() {
8848 let name = self.extract_schema_name(ref_str)?;
8849 return self
8850 .referenced_array_scalar_item_type(name)
8851 .or_else(|| self.referenced_array_struct_item_type(name, 1));
8852 }
8853 let format = unwrapped.details().format.clone();
8854 let scalar = match unwrapped.schema_type()? {
8855 crate::openapi::SchemaType::String => "String".to_string(),
8856 crate::openapi::SchemaType::Integer => self.integer_rust_type(unwrapped.details()),
8857 crate::openapi::SchemaType::Number => {
8858 self.type_mapper.number_format(format.as_deref()).rust_type
8859 }
8860 crate::openapi::SchemaType::Boolean => "bool".to_string(),
8861 _ => return None,
8862 };
8863 Some(ArrayItemType::Scalar(scalar))
8864 }
8865
8866 fn referenced_array_param_item_type(&self, name: &str) -> Option<ArrayItemType> {
8869 let schema = self.resolve_cached_schema(name)?;
8870 let SchemaType::Array { item_type } = &schema.schema_type else {
8871 return None;
8872 };
8873 self.analyzed_array_item_type(item_type)
8874 }
8875
8876 fn analyzed_array_item_type(&self, item_type: &SchemaType) -> Option<ArrayItemType> {
8877 self.analyzed_array_item_type_at_depth(item_type, 1)
8878 }
8879
8880 fn referenced_array_struct_item_type(
8885 &self,
8886 name: &str,
8887 nested_array_depth: usize,
8888 ) -> Option<ArrayItemType> {
8889 let resolved = self.resolve_cached_schema(name)?;
8890 let SchemaType::Object {
8891 properties,
8892 required,
8893 additional_properties,
8894 ..
8895 } = &resolved.schema_type
8896 else {
8897 return None;
8898 };
8899 if properties.is_empty() || additional_properties.is_open() {
8900 return None;
8901 }
8902 let mut projected = Vec::with_capacity(properties.len());
8903 let mut has_array = false;
8904 for (wire_name, property) in properties {
8905 let value_type = if let Some(scalar) = self.query_scalar_type(&property.schema_type) {
8906 QueryStructPropertyType::Scalar(scalar)
8907 } else {
8908 if nested_array_depth == 0 {
8909 return None;
8910 }
8911 if let Some(array) = self.resolve_query_array_type(&property.schema_type) {
8912 let item_type =
8913 self.analyzed_array_item_type_at_depth(array, nested_array_depth - 1)?;
8914 if matches!(item_type, ArrayItemType::NestedStructRef { .. }) {
8915 return None;
8916 }
8917 has_array = true;
8918 QueryStructPropertyType::Array { item_type }
8919 } else {
8920 has_array = true;
8921 QueryStructPropertyType::Object {
8922 properties: self.query_flat_object_properties(&property.schema_type)?,
8923 }
8924 }
8925 };
8926 projected.push(QueryStructProperty {
8927 wire_name: wire_name.clone(),
8928 required: required.contains(wire_name),
8929 value_type,
8930 });
8931 }
8932 if has_array {
8933 Some(ArrayItemType::NestedStructRef {
8934 schema_name: name.to_string(),
8935 properties: projected,
8936 })
8937 } else {
8938 Some(ArrayItemType::FlatStructRef {
8939 schema_name: name.to_string(),
8940 properties: projected,
8941 })
8942 }
8943 }
8944
8945 fn analyzed_array_item_type_at_depth(
8946 &self,
8947 item_type: &SchemaType,
8948 nested_array_depth: usize,
8949 ) -> Option<ArrayItemType> {
8950 match item_type {
8951 SchemaType::Primitive { rust_type, .. } => {
8952 Some(ArrayItemType::Scalar(rust_type.clone()))
8953 }
8954 SchemaType::Reference { target } => self
8955 .referenced_array_scalar_item_type(target)
8956 .or_else(|| self.referenced_array_struct_item_type(target, nested_array_depth)),
8957 _ => None,
8958 }
8959 }
8960
8961 fn resolve_query_array_type<'a>(
8962 &'a self,
8963 schema_type: &'a SchemaType,
8964 ) -> Option<&'a SchemaType> {
8965 match schema_type {
8966 SchemaType::Array { item_type } => Some(item_type),
8967 SchemaType::Reference { target } => {
8968 let resolved = self.resolve_cached_schema(target)?;
8969 let SchemaType::Array { item_type } = &resolved.schema_type else {
8970 return None;
8971 };
8972 Some(item_type)
8973 }
8974 _ => None,
8975 }
8976 }
8977
8978 fn query_flat_object_properties(
8979 &self,
8980 schema_type: &SchemaType,
8981 ) -> Option<Vec<QueryStructProperty>> {
8982 let schema_type = match schema_type {
8983 SchemaType::Reference { target } => &self.resolve_cached_schema(target)?.schema_type,
8984 other => other,
8985 };
8986 let SchemaType::Object {
8987 properties,
8988 required,
8989 additional_properties,
8990 ..
8991 } = schema_type
8992 else {
8993 return None;
8994 };
8995 if properties.is_empty() || additional_properties.is_open() {
8996 return None;
8997 }
8998 properties
8999 .iter()
9000 .map(|(wire_name, property)| {
9001 Some(QueryStructProperty {
9002 wire_name: wire_name.clone(),
9003 required: required.contains(wire_name),
9004 value_type: QueryStructPropertyType::Scalar(
9005 self.query_scalar_type(&property.schema_type)?,
9006 ),
9007 })
9008 })
9009 .collect()
9010 }
9011
9012 fn query_scalar_type(&self, schema_type: &SchemaType) -> Option<QueryScalarType> {
9013 match schema_type {
9014 SchemaType::Primitive { rust_type, .. } => match rust_type.as_str() {
9015 "String" => Some(QueryScalarType::String),
9016 "bool" => Some(QueryScalarType::Boolean),
9017 value if value.starts_with('i') || value.starts_with('u') => {
9018 Some(QueryScalarType::Integer)
9019 }
9020 value if value.starts_with('f') => Some(QueryScalarType::Number),
9021 "serde_json::Value" => None,
9022 _ => Some(QueryScalarType::String),
9023 },
9024 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. } => {
9025 Some(QueryScalarType::String)
9026 }
9027 SchemaType::Reference { target } => {
9028 let resolved = self.resolve_cached_schema(target)?;
9029 self.query_scalar_type(&resolved.schema_type)
9030 }
9031 _ => None,
9032 }
9033 }
9034
9035 fn referenced_array_scalar_item_type(&self, name: &str) -> Option<ArrayItemType> {
9043 let resolved = self.resolve_cached_schema(name)?;
9044 let supported = match &resolved.schema_type {
9045 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. } => true,
9046 SchemaType::Primitive { .. } => resolved
9047 .original
9048 .get("type")
9049 .is_some_and(Self::query_scalar_type_value),
9050 _ => false,
9051 };
9052 supported.then(|| ArrayItemType::SchemaRef(name.to_string()))
9053 }
9054
9055 fn query_scalar_type_value(value: &Value) -> bool {
9056 const SCALARS: [&str; 4] = ["string", "integer", "number", "boolean"];
9057 if let Some(value) = value.as_str() {
9058 return SCALARS.contains(&value);
9059 }
9060 let Some(values) = value.as_array() else {
9061 return false;
9062 };
9063 if !values.iter().all(Value::is_string) {
9064 return false;
9065 }
9066 let mut non_null = values
9067 .iter()
9068 .filter_map(Value::as_str)
9069 .filter(|value| *value != "null");
9070 let Some(scalar) = non_null.next() else {
9071 return false;
9072 };
9073 non_null.next().is_none() && SCALARS.contains(&scalar)
9074 }
9075
9076 fn referenced_schema_is_object(&self, name: &str) -> bool {
9080 self.resolve_cached_schema(name)
9081 .is_some_and(|schema| matches!(schema.schema_type, SchemaType::Object { .. }))
9082 }
9083
9084 fn referenced_schema_is_array(&self, name: &str) -> bool {
9085 self.resolve_cached_schema(name)
9086 .is_some_and(|schema| matches!(schema.schema_type, SchemaType::Array { .. }))
9087 }
9088
9089 fn referenced_schema_is_composed_query_shape(&self, name: &str) -> bool {
9090 self.resolve_cached_schema(name).is_some_and(|schema| {
9091 matches!(
9092 schema.schema_type,
9093 SchemaType::Composition { .. }
9094 | SchemaType::Union { .. }
9095 | SchemaType::DiscriminatedUnion { .. }
9096 )
9097 })
9098 }
9099
9100 fn resolve_cached_schema(&self, name: &str) -> Option<&AnalyzedSchema> {
9101 let mut current = name;
9102 let mut visited = HashSet::new();
9103 loop {
9104 if !visited.insert(current) {
9105 return None;
9106 }
9107 let schema = self.resolved_cache.get(current)?;
9108 if let SchemaType::Reference { target } = &schema.schema_type {
9109 current = target;
9110 } else {
9111 return Some(schema);
9112 }
9113 }
9114 }
9115
9116 fn schema_is_inline_object(schema: &crate::openapi::Schema) -> bool {
9118 match schema.schema_type() {
9119 Some(crate::openapi::SchemaType::Object) => true,
9120 None => schema.details().properties.is_some(),
9121 _ => false,
9122 }
9123 }
9124}
9125
9126fn pointer_type_name(pointer: &str) -> String {
9137 use heck::ToPascalCase;
9138
9139 pointer
9140 .split('/')
9141 .skip(1)
9142 .filter(|segment| !matches!(*segment, "components" | "schemas" | "properties"))
9143 .map(|segment| {
9144 segment
9145 .replace("~1", "/")
9146 .replace("~0", "~")
9147 .to_pascal_case()
9148 })
9149 .collect::<String>()
9150}
9151
9152fn shared_positional_schema(positions: &[Schema]) -> Option<&Schema> {
9157 let first = positions.first()?;
9158 let key = positional_schema_key(first)?;
9159 positions
9160 .iter()
9161 .skip(1)
9162 .all(|position| positional_schema_key(position).as_deref() == Some(key.as_str()))
9163 .then_some(first)
9164}
9165
9166fn positional_schema_key(schema: &Schema) -> Option<String> {
9167 if let Some(reference) = schema.reference() {
9168 return Some(format!("$ref {reference}"));
9169 }
9170 let details = schema.details();
9171 if details.properties.is_some() || details.enum_values.is_some() || details.items.is_some() {
9172 return None;
9173 }
9174 match schema.schema_type()? {
9175 crate::openapi::SchemaType::Object | crate::openapi::SchemaType::Array => None,
9176 scalar => Some(format!(
9177 "{scalar:?} {}",
9178 details.format.as_deref().unwrap_or_default()
9179 )),
9180 }
9181}
9182
9183fn parse_spec_document(openapi_spec: &Value) -> Result<OpenApiSpec> {
9184 serde_path_to_error::deserialize(openapi_spec).map_err(|error| {
9185 let mut pointer = json_pointer(error.path());
9186 pointer.push_str(&refine_schema_failure(openapi_spec, &pointer));
9190 GeneratorError::ParseErrorAt {
9191 pointer,
9192 message: error.into_inner().to_string(),
9193 }
9194 })
9195}
9196
9197const SUBSCHEMA_KEYWORDS: [&str; 11] = [
9199 "items",
9200 "additionalProperties",
9201 "propertyNames",
9202 "unevaluatedProperties",
9203 "unevaluatedItems",
9204 "contains",
9205 "contentSchema",
9206 "if",
9207 "then",
9208 "else",
9209 "not",
9210];
9211
9212const SUBSCHEMA_LIST_KEYWORDS: [&str; 4] = ["oneOf", "anyOf", "allOf", "prefixItems"];
9214
9215const SUBSCHEMA_MAP_KEYWORDS: [&str; 5] = [
9217 "properties",
9218 "patternProperties",
9219 "dependentSchemas",
9220 "$defs",
9221 "definitions",
9222];
9223
9224const REFINE_PARSE_BUDGET: usize = 20_000;
9228
9229fn refine_schema_failure(openapi_spec: &Value, pointer: &str) -> String {
9237 let Some(path) = pointer.strip_prefix('#') else {
9238 return String::new();
9239 };
9240 let Some(node) = openapi_spec.pointer(path) else {
9241 return String::new();
9242 };
9243 let segments = path.split('/').skip(1).collect::<Vec<_>>();
9244 let last = segments.last().copied().unwrap_or_default();
9245 let parent = segments
9246 .len()
9247 .checked_sub(2)
9248 .map(|index| segments[index])
9249 .unwrap_or_default();
9250
9251 if last == "schema" || holds_schemas(parent) {
9252 return if parses_as_schema(node) {
9253 String::new()
9254 } else {
9255 deepest_schema_failure(node)
9256 };
9257 }
9258
9259 let mut budget = REFINE_PARSE_BUDGET;
9260 locate_failing_schema(node, holds_schemas(last), &mut budget).unwrap_or_default()
9261}
9262
9263fn holds_schemas(key: &str) -> bool {
9266 matches!(key, "schemas" | "$defs" | "definitions")
9267}
9268
9269fn locate_failing_schema(
9277 node: &Value,
9278 children_are_schemas: bool,
9279 budget: &mut usize,
9280) -> Option<String> {
9281 for (segment, key, child) in child_nodes(node) {
9282 if *budget == 0 {
9283 return None;
9284 }
9285 *budget -= 1;
9286 if children_are_schemas || key == "schema" {
9287 if !parses_as_schema(child) {
9288 return Some(format!("/{segment}{}", deepest_schema_failure(child)));
9289 }
9290 continue;
9291 }
9292 if let Some(rest) = locate_failing_schema(child, holds_schemas(key), budget) {
9293 return Some(format!("/{segment}{rest}"));
9294 }
9295 }
9296 None
9297}
9298
9299fn parses_as_schema(node: &Value) -> bool {
9300 Schema::deserialize(node).is_ok()
9301}
9302
9303fn deepest_schema_failure(node: &Value) -> String {
9307 let Some(object) = node.as_object() else {
9308 return String::new();
9309 };
9310
9311 let descend = |segment: String, child: &Value| -> Option<String> {
9312 if parses_as_schema(child) {
9313 return None;
9314 }
9315 Some(format!("/{segment}{}", deepest_schema_failure(child)))
9316 };
9317
9318 for keyword in SUBSCHEMA_KEYWORDS {
9319 if let Some(child) = object.get(keyword)
9320 && let Some(suffix) = descend(escape_pointer_segment(keyword), child)
9321 {
9322 return suffix;
9323 }
9324 }
9325 for keyword in SUBSCHEMA_LIST_KEYWORDS {
9326 if let Some(Value::Array(children)) = object.get(keyword) {
9327 for (index, child) in children.iter().enumerate() {
9328 if let Some(suffix) = descend(
9329 format!("{}/{index}", escape_pointer_segment(keyword)),
9330 child,
9331 ) {
9332 return suffix;
9333 }
9334 }
9335 }
9336 }
9337 for keyword in SUBSCHEMA_MAP_KEYWORDS {
9338 if let Some(Value::Object(children)) = object.get(keyword) {
9339 for (name, child) in children {
9340 if let Some(suffix) = descend(
9341 format!(
9342 "{}/{}",
9343 escape_pointer_segment(keyword),
9344 escape_pointer_segment(name)
9345 ),
9346 child,
9347 ) {
9348 return suffix;
9349 }
9350 }
9351 }
9352 }
9353 String::new()
9354}
9355
9356fn child_nodes(node: &Value) -> Vec<(String, &str, &Value)> {
9362 const DATA_KEYWORDS: [&str; 5] = ["example", "examples", "default", "enum", "const"];
9363
9364 match node {
9365 Value::Object(members) => members
9366 .iter()
9367 .filter(|(name, child)| {
9368 (child.is_object() || child.is_array())
9369 && !name.starts_with("x-")
9370 && !DATA_KEYWORDS.contains(&name.as_str())
9371 })
9372 .map(|(name, child)| (escape_pointer_segment(name), name.as_str(), child))
9373 .collect(),
9374 Value::Array(elements) => elements
9375 .iter()
9376 .enumerate()
9377 .filter(|(_, child)| child.is_object() || child.is_array())
9378 .map(|(index, child)| (index.to_string(), "", child))
9379 .collect(),
9380 _ => Vec::new(),
9381 }
9382}
9383
9384fn escape_pointer_segment(segment: &str) -> String {
9385 segment.replace('~', "~0").replace('/', "~1")
9386}
9387
9388fn json_pointer(path: &serde_path_to_error::Path) -> String {
9392 use serde_path_to_error::Segment;
9393
9394 let mut pointer = String::from("#");
9395 for segment in path.iter() {
9396 match segment {
9397 Segment::Seq { index } => {
9398 pointer.push('/');
9399 pointer.push_str(&index.to_string());
9400 }
9401 Segment::Map { key } | Segment::Enum { variant: key } => {
9402 pointer.push('/');
9403 pointer.push_str(&escape_pointer_segment(key));
9404 }
9405 Segment::Unknown => pointer.push_str("/?"),
9406 }
9407 }
9408 pointer
9409}
9410
9411pub(crate) fn component_schema_name_aliases(openapi_spec: &Value) -> BTreeMap<String, String> {
9412 let Some(schemas) = openapi_spec
9413 .pointer("/components/schemas")
9414 .and_then(Value::as_object)
9415 else {
9416 return BTreeMap::new();
9417 };
9418
9419 let mut names_by_rust_name = BTreeMap::<String, Vec<String>>::new();
9420 for name in schemas.keys() {
9421 names_by_rust_name
9422 .entry(crate::generator::rust_type_name(name))
9423 .or_default()
9424 .push(name.clone());
9425 }
9426
9427 let mut claimed_rust_names = names_by_rust_name.keys().cloned().collect::<HashSet<_>>();
9430 let mut aliases = BTreeMap::new();
9431
9432 for (rust_name, mut names) in names_by_rust_name {
9433 if names.len() < 2 {
9434 continue;
9435 }
9436
9437 names.sort_by_key(|name| (name != &rust_name, name.clone()));
9440 for source_name in names.into_iter().skip(1) {
9441 let mut suffix = 2;
9442 let replacement = loop {
9443 let candidate = format!("{rust_name}{suffix}");
9444 if claimed_rust_names.insert(candidate.clone()) {
9445 break candidate;
9446 }
9447 suffix += 1;
9448 };
9449
9450 aliases.insert(source_name, replacement);
9451 }
9452 }
9453
9454 aliases
9455}
9456
9457fn disambiguate_component_schema_names(openapi_spec: &mut Value) {
9458 let aliases = component_schema_name_aliases(openapi_spec);
9459 if aliases.is_empty() {
9460 return;
9461 }
9462
9463 for (source_name, replacement) in &aliases {
9464 let rust_name = crate::generator::rust_type_name(source_name);
9465 eprintln!(
9466 "⚠️ schema `{source_name}` maps to the existing Rust type `{rust_name}` — disambiguated to `{replacement}`"
9467 );
9468 }
9469
9470 let Some(schemas) = openapi_spec
9471 .pointer_mut("/components/schemas")
9472 .and_then(Value::as_object_mut)
9473 else {
9474 return;
9475 };
9476
9477 let original_schemas = std::mem::take(schemas);
9478 for (name, schema) in original_schemas {
9479 schemas.insert(aliases.get(&name).cloned().unwrap_or(name), schema);
9480 }
9481
9482 rewrite_component_schema_references(openapi_spec, &aliases);
9483}
9484
9485fn resolve_multipart_wire_schema<'a>(
9489 schema: &'a Value,
9490 component_schemas: &'a BTreeMap<String, Value>,
9491 visited: &mut HashSet<String>,
9492) -> Option<&'a Value> {
9493 let Some(reference) = schema.get("$ref").and_then(Value::as_str) else {
9494 for keyword in ["anyOf", "oneOf"] {
9495 if let Some(branches) = schema.get(keyword).and_then(Value::as_array) {
9496 let concrete: Vec<_> = branches
9497 .iter()
9498 .filter(|branch| branch.get("type").and_then(Value::as_str) != Some("null"))
9499 .collect();
9500 if concrete.len() == 1 && concrete.len() < branches.len() {
9501 return resolve_multipart_wire_schema(concrete[0], component_schemas, visited);
9502 }
9503 }
9504 }
9505 if let Some(branches) = schema.get("allOf").and_then(Value::as_array) {
9506 if branches.len() == 1 {
9507 return resolve_multipart_wire_schema(&branches[0], component_schemas, visited);
9508 }
9509 }
9510 return Some(schema);
9511 };
9512 let name = reference.strip_prefix("#/components/schemas/")?;
9513 if !visited.insert(name.to_string()) {
9514 return None;
9515 }
9516 resolve_multipart_wire_schema(component_schemas.get(name)?, component_schemas, visited)
9517}
9518
9519fn is_multipart_file_schema(schema: &Value, component_schemas: &BTreeMap<String, Value>) -> bool {
9524 let Some(schema) =
9525 resolve_multipart_wire_schema(schema, component_schemas, &mut HashSet::new())
9526 else {
9527 return false;
9528 };
9529 let is_string = |schema: &Value| schema.get("type").and_then(Value::as_str) == Some("string");
9530 let is_binary = |schema: &Value| {
9531 is_string(schema) && schema.get("format").and_then(Value::as_str) == Some("binary")
9532 };
9533 if is_binary(schema) {
9534 return true;
9535 }
9536 ["anyOf", "oneOf"].into_iter().any(|keyword| {
9537 let Some(branches) = schema.get(keyword).and_then(Value::as_array) else {
9538 return false;
9539 };
9540 let branches: Option<Vec<&Value>> = branches
9541 .iter()
9542 .filter(|branch| branch.get("type").and_then(Value::as_str) != Some("null"))
9543 .map(|branch| {
9544 resolve_multipart_wire_schema(branch, component_schemas, &mut HashSet::new())
9545 })
9546 .collect();
9547 branches.is_some_and(|branches| {
9548 branches.iter().all(|branch| is_string(branch)) && branches.iter().any(|b| is_binary(b))
9549 })
9550 })
9551}
9552
9553fn retype_multipart_file_fields(analysis: &mut SchemaAnalysis) {
9564 fn file_type(original: &SchemaType, type_name: &str) -> SchemaType {
9565 match original {
9566 SchemaType::Nullable { inner_type } => SchemaType::Nullable {
9567 inner_type: Box::new(file_type(inner_type, type_name)),
9568 },
9569 _ => SchemaType::Primitive {
9570 rust_type: type_name.to_string(),
9571 serde_with: None,
9572 },
9573 }
9574 }
9575 fn file_array_type(original: &SchemaType, type_name: &str) -> SchemaType {
9576 match original {
9577 SchemaType::Nullable { inner_type } => SchemaType::Nullable {
9578 inner_type: Box::new(file_array_type(inner_type, type_name)),
9579 },
9580 SchemaType::Array { item_type } => SchemaType::Array {
9581 item_type: Box::new(file_type(item_type, type_name)),
9582 },
9583 _ => SchemaType::Array {
9584 item_type: Box::new(file_type(original, type_name)),
9585 },
9586 }
9587 }
9588
9589 let claimed: HashSet<String> = analysis
9590 .schemas
9591 .keys()
9592 .map(|name| crate::generator::rust_type_name(name))
9593 .collect();
9594 let mut type_name = MULTIPART_FILE_TYPE.to_string();
9595 let mut suffix = 2;
9596 while claimed.contains(&type_name) {
9597 type_name = format!("{MULTIPART_FILE_TYPE}{suffix}");
9598 suffix += 1;
9599 }
9600
9601 let bodies: Vec<(String, Value)> = analysis
9602 .operations
9603 .values()
9604 .filter_map(|operation| match &operation.request_body {
9605 Some(RequestBodyContent::Multipart {
9606 schema_name,
9607 validation_schema,
9608 ..
9609 }) => Some((schema_name.clone(), validation_schema.clone())),
9610 _ => None,
9611 })
9612 .collect();
9613 let components = analysis.validation_context.component_schemas.clone();
9614 for (schema_name, validation_schema) in bodies {
9615 let Some(wire_properties) =
9616 resolve_multipart_wire_schema(&validation_schema, &components, &mut HashSet::new())
9617 .and_then(|schema| schema.get("properties"))
9618 .and_then(Value::as_object)
9619 else {
9620 continue;
9621 };
9622 let mut name = schema_name;
9623 let mut seen = HashSet::new();
9624 while let Some(SchemaType::Reference { target }) = analysis
9625 .schemas
9626 .get(&name)
9627 .map(|schema| &schema.schema_type)
9628 {
9629 if !seen.insert(target.clone()) {
9630 break;
9631 }
9632 name = target.clone();
9633 }
9634 let Some(SchemaType::Object { properties, .. }) = analysis
9635 .schemas
9636 .get_mut(&name)
9637 .map(|schema| &mut schema.schema_type)
9638 else {
9639 continue;
9640 };
9641 for (wire_name, property) in properties.iter_mut() {
9642 let Some(wire_property) = wire_properties.get(wire_name) else {
9643 continue;
9644 };
9645 if is_multipart_file_schema(wire_property, &components) {
9646 property.schema_type = file_type(&property.schema_type, &type_name);
9647 analysis.multipart_file_type = Some(type_name.clone());
9648 continue;
9649 }
9650 let items =
9651 resolve_multipart_wire_schema(wire_property, &components, &mut HashSet::new())
9652 .filter(|schema| schema.get("type").and_then(Value::as_str) == Some("array"))
9653 .and_then(|schema| schema.get("items"));
9654 if items.is_some_and(|items| is_multipart_file_schema(items, &components)) {
9655 property.schema_type = file_array_type(&property.schema_type, &type_name);
9656 analysis.multipart_file_type = Some(type_name.clone());
9657 }
9658 }
9659 }
9660}
9661
9662pub(crate) const MULTIPART_FILE_TYPE: &str = "MultipartFile";
9665
9666fn disambiguate_analyzed_schema_names(
9667 analysis: &mut SchemaAnalysis,
9668 component_schemas: &BTreeMap<String, Schema>,
9669) {
9670 let mut names_by_rust_name = BTreeMap::<String, Vec<String>>::new();
9671 for name in analysis.schemas.keys() {
9672 names_by_rust_name
9673 .entry(crate::generator::rust_type_name(name))
9674 .or_default()
9675 .push(name.clone());
9676 }
9677
9678 let mut claimed_rust_names = names_by_rust_name.keys().cloned().collect::<HashSet<_>>();
9679 let mut aliases = BTreeMap::<String, String>::new();
9680
9681 for (rust_name, mut names) in names_by_rust_name {
9682 if names.len() < 2 {
9683 continue;
9684 }
9685 names.sort_by_key(|name| {
9686 (
9687 !component_schemas.contains_key(name),
9688 name != &rust_name,
9689 name.clone(),
9690 )
9691 });
9692
9693 for source_name in names.into_iter().skip(1) {
9694 let mut suffix = 2;
9695 let replacement = loop {
9696 let candidate = format!("{rust_name}{suffix}");
9697 if claimed_rust_names.insert(candidate.clone()) {
9698 break candidate;
9699 }
9700 suffix += 1;
9701 };
9702 eprintln!(
9703 "⚠️ generated schema `{source_name}` maps to the existing Rust type `{rust_name}` — disambiguated to `{replacement}`"
9704 );
9705 aliases.insert(source_name, replacement);
9706 }
9707 }
9708
9709 if aliases.is_empty() {
9710 return;
9711 }
9712
9713 let original_schemas = std::mem::take(&mut analysis.schemas);
9714 for (name, mut schema) in original_schemas {
9715 schema.name = renamed_schema_name(&schema.name, &aliases);
9716 schema.dependencies = schema
9717 .dependencies
9718 .into_iter()
9719 .map(|name| renamed_schema_name(&name, &aliases))
9720 .collect();
9721 rewrite_schema_type_names(&mut schema.schema_type, &aliases);
9722 analysis
9723 .schemas
9724 .insert(renamed_schema_name(&name, &aliases), schema);
9725 }
9726
9727 let original_edges = std::mem::take(&mut analysis.dependencies.edges);
9728 for (name, dependencies) in original_edges {
9729 analysis.dependencies.edges.insert(
9730 renamed_schema_name(&name, &aliases),
9731 dependencies
9732 .into_iter()
9733 .map(|name| renamed_schema_name(&name, &aliases))
9734 .collect(),
9735 );
9736 }
9737 analysis.dependencies.recursive_schemas = analysis
9738 .dependencies
9739 .recursive_schemas
9740 .iter()
9741 .map(|name| renamed_schema_name(name, &aliases))
9742 .collect();
9743
9744 analysis.patterns.tagged_enum_schemas = analysis
9745 .patterns
9746 .tagged_enum_schemas
9747 .iter()
9748 .map(|name| renamed_schema_name(name, &aliases))
9749 .collect();
9750 analysis.patterns.untagged_enum_schemas = analysis
9751 .patterns
9752 .untagged_enum_schemas
9753 .iter()
9754 .map(|name| renamed_schema_name(name, &aliases))
9755 .collect();
9756 analysis.patterns.type_mappings = std::mem::take(&mut analysis.patterns.type_mappings)
9757 .into_iter()
9758 .map(|(name, mappings)| {
9759 (
9760 renamed_schema_name(&name, &aliases),
9761 mappings
9762 .into_iter()
9763 .map(|(value, schema_name)| {
9764 (value, renamed_schema_name(&schema_name, &aliases))
9765 })
9766 .collect(),
9767 )
9768 })
9769 .collect();
9770
9771 for operation in analysis.operations.values_mut() {
9772 if let Some(request_body) = &mut operation.request_body {
9773 rewrite_request_body_schema_name(request_body, &aliases);
9774 }
9775 for schema_name in operation.response_schemas.values_mut() {
9776 *schema_name = renamed_schema_name(schema_name, &aliases);
9777 }
9778 for parameter in &mut operation.parameters {
9779 if let Some(schema_name) = &mut parameter.schema_ref {
9780 *schema_name = renamed_schema_name(schema_name, &aliases);
9781 }
9782 if let Some(serialization) = &mut parameter.query_serialization {
9783 rewrite_query_serialization_schema_names(serialization, &aliases);
9784 }
9785 }
9786 }
9787
9788 for responses in analysis.operation_responses.values_mut() {
9789 for response in responses.values_mut() {
9790 if let Some(schema_name) = &mut response.schema_name {
9791 *schema_name = renamed_schema_name(schema_name, &aliases);
9792 }
9793 for representation in response.representations.values_mut() {
9794 if let OperationRepresentation::Json { schema_name } = representation {
9795 *schema_name = renamed_schema_name(schema_name, &aliases);
9796 }
9797 }
9798 if let Some(OperationResponseBody::Json { schema_name, .. }) = &mut response.body {
9799 *schema_name = renamed_schema_name(schema_name, &aliases);
9800 }
9801 }
9802 }
9803}
9804
9805fn renamed_schema_name(name: &str, aliases: &BTreeMap<String, String>) -> String {
9806 aliases
9807 .get(name)
9808 .cloned()
9809 .unwrap_or_else(|| name.to_string())
9810}
9811
9812fn rewrite_schema_type_names(schema_type: &mut SchemaType, aliases: &BTreeMap<String, String>) {
9813 match schema_type {
9814 SchemaType::Object {
9815 properties,
9816 additional_properties,
9817 ..
9818 } => {
9819 for property in properties.values_mut() {
9820 rewrite_schema_type_names(&mut property.schema_type, aliases);
9821 }
9822 if let ObjectAdditionalProperties::Typed { value_type } = additional_properties {
9823 rewrite_schema_type_names(value_type, aliases);
9824 }
9825 }
9826 SchemaType::DiscriminatedUnion { variants, .. } => {
9827 for variant in variants {
9828 variant.type_name = renamed_schema_name(&variant.type_name, aliases);
9829 variant.schema_ref = renamed_schema_name(&variant.schema_ref, aliases);
9830 }
9831 }
9832 SchemaType::Union { variants, .. } | SchemaType::Composition { schemas: variants } => {
9833 for variant in variants {
9834 variant.target = renamed_schema_name(&variant.target, aliases);
9835 }
9836 }
9837 SchemaType::Array { item_type } => rewrite_schema_type_names(item_type, aliases),
9838 SchemaType::Nullable { inner_type } => rewrite_schema_type_names(inner_type, aliases),
9839 SchemaType::Untyped { .. } => {}
9840 SchemaType::Tuple { element_types } => {
9841 for element_type in element_types {
9842 rewrite_schema_type_names(element_type, aliases);
9843 }
9844 }
9845 SchemaType::Reference { target } => {
9846 *target = renamed_schema_name(target, aliases);
9847 }
9848 SchemaType::Primitive { .. }
9849 | SchemaType::StringEnum { .. }
9850 | SchemaType::ExtensibleEnum { .. } => {}
9851 }
9852}
9853
9854fn rewrite_request_body_schema_name(
9855 request_body: &mut RequestBodyContent,
9856 aliases: &BTreeMap<String, String>,
9857) {
9858 match request_body {
9859 RequestBodyContent::Json { schema_name, .. }
9860 | RequestBodyContent::FormUrlEncoded { schema_name, .. }
9861 | RequestBodyContent::Multipart { schema_name, .. } => {
9862 *schema_name = renamed_schema_name(schema_name, aliases);
9863 }
9864 _ => {}
9865 }
9866}
9867
9868fn rewrite_query_serialization_schema_names(
9869 serialization: &mut QuerySerialization,
9870 aliases: &BTreeMap<String, String>,
9871) {
9872 match serialization {
9873 QuerySerialization::FormExplodedArray { item_type }
9874 | QuerySerialization::FormArray { item_type }
9875 | QuerySerialization::SimpleHeaderArray { item_type } => {
9876 rewrite_array_item_type_schema_names(item_type, aliases);
9877 }
9878 QuerySerialization::FormExplodedNestedObject { properties } => {
9879 for property in properties {
9880 rewrite_query_property_type_schema_names(&mut property.value_type, aliases);
9881 }
9882 }
9883 _ => {}
9884 }
9885}
9886
9887fn rewrite_array_item_type_schema_names(
9888 item_type: &mut ArrayItemType,
9889 aliases: &BTreeMap<String, String>,
9890) {
9891 match item_type {
9892 ArrayItemType::SchemaRef(name) => *name = renamed_schema_name(name, aliases),
9893 ArrayItemType::FlatStructRef {
9894 schema_name,
9895 properties,
9896 }
9897 | ArrayItemType::NestedStructRef {
9898 schema_name,
9899 properties,
9900 } => {
9901 *schema_name = renamed_schema_name(schema_name, aliases);
9902 for property in properties {
9903 rewrite_query_property_type_schema_names(&mut property.value_type, aliases);
9904 }
9905 }
9906 ArrayItemType::Scalar(_) => {}
9907 }
9908}
9909
9910fn rewrite_query_property_type_schema_names(
9911 property_type: &mut QueryStructPropertyType,
9912 aliases: &BTreeMap<String, String>,
9913) {
9914 match property_type {
9915 QueryStructPropertyType::Array { item_type } => {
9916 rewrite_array_item_type_schema_names(item_type, aliases)
9917 }
9918 QueryStructPropertyType::Object { properties } => {
9919 for property in properties {
9920 rewrite_query_property_type_schema_names(&mut property.value_type, aliases);
9921 }
9922 }
9923 QueryStructPropertyType::Scalar(_) => {}
9924 }
9925}
9926
9927fn rewrite_component_schema_references(value: &mut Value, aliases: &BTreeMap<String, String>) {
9928 match value {
9929 Value::Array(values) => {
9930 for value in values {
9931 rewrite_component_schema_references(value, aliases);
9932 }
9933 }
9934 Value::Object(object) => {
9935 if let Some(Value::String(reference)) = object.get_mut("$ref") {
9936 rewrite_component_schema_reference(reference, aliases);
9937 }
9938
9939 if let Some(Value::Object(mapping)) = object.get_mut("mapping") {
9940 for target_value in mapping.values_mut() {
9941 let Some(target) = target_value.as_str() else {
9942 continue;
9943 };
9944 let replacement = aliases.get(target).cloned().or_else(|| {
9945 let mut target = target.to_string();
9946 rewrite_component_schema_reference(&mut target, aliases).then_some(target)
9947 });
9948 if let Some(replacement) = replacement {
9949 *target_value = Value::String(replacement);
9950 }
9951 }
9952 }
9953
9954 for value in object.values_mut() {
9955 rewrite_component_schema_references(value, aliases);
9956 }
9957 }
9958 _ => {}
9959 }
9960}
9961
9962fn rewrite_component_schema_reference(
9963 reference: &mut String,
9964 aliases: &BTreeMap<String, String>,
9965) -> bool {
9966 const PREFIX: &str = "#/components/schemas/";
9967 let Some(encoded_name) = reference.strip_prefix(PREFIX) else {
9968 return false;
9969 };
9970 let encoded_name = encoded_name.split('/').next().unwrap_or(encoded_name);
9971
9972 for (source, replacement) in aliases {
9973 let encoded_source = source.replace('~', "~0").replace('/', "~1");
9974 if encoded_name == encoded_source {
9975 reference.replace_range(
9976 PREFIX.len()..PREFIX.len() + encoded_source.len(),
9977 replacement,
9978 );
9979 return true;
9980 }
9981 }
9982
9983 false
9984}