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)]
1099 pub validation_schema: Option<Value>,
1100}
1101
1102#[derive(Debug, Clone, PartialEq, serde::Serialize)]
1105pub enum QuerySerialization {
1106 FormExplodedObject,
1110 FormExplodedNestedObject {
1116 properties: Vec<QueryStructProperty>,
1117 },
1118 FormObject,
1121 DeepObject,
1124 FormExplodedArray { item_type: ArrayItemType },
1127 FormArray { item_type: ArrayItemType },
1130 SimpleHeaderArray { item_type: ArrayItemType },
1133 Unsupported { reason: String },
1138}
1139
1140#[derive(Debug, Clone, PartialEq, serde::Serialize)]
1147pub enum ArrayItemType {
1148 Scalar(String),
1150 SchemaRef(String),
1152 FlatStructRef {
1158 schema_name: String,
1159 properties: Vec<QueryStructProperty>,
1160 },
1161 NestedStructRef {
1165 schema_name: String,
1166 properties: Vec<QueryStructProperty>,
1167 },
1168}
1169
1170#[derive(Debug, Clone, PartialEq, serde::Serialize)]
1171pub struct QueryStructProperty {
1172 pub wire_name: String,
1173 pub required: bool,
1174 pub value_type: QueryStructPropertyType,
1175}
1176
1177#[derive(Debug, Clone, PartialEq, serde::Serialize)]
1178pub enum QueryStructPropertyType {
1179 Scalar(QueryScalarType),
1180 Array {
1181 item_type: ArrayItemType,
1182 },
1183 Object {
1184 properties: Vec<QueryStructProperty>,
1185 },
1186}
1187
1188#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
1189pub enum QueryScalarType {
1190 String,
1191 Integer,
1192 Number,
1193 Boolean,
1194}
1195
1196impl Default for DependencyGraph {
1197 fn default() -> Self {
1198 Self::new()
1199 }
1200}
1201
1202impl DependencyGraph {
1203 pub fn new() -> Self {
1204 Self {
1205 edges: BTreeMap::new(),
1206 recursive_schemas: HashSet::new(),
1207 }
1208 }
1209
1210 pub fn add_dependency(&mut self, from: String, to: String) {
1211 self.edges.entry(from).or_default().insert(to);
1212 }
1213
1214 pub fn topological_sort(&mut self) -> Result<Vec<String>> {
1216 self.detect_recursive_schemas();
1218
1219 let mut temp_edges = self.edges.clone();
1221 for (schema, deps) in &mut temp_edges {
1222 deps.remove(schema); }
1224
1225 let mut visited = HashSet::new();
1226 let mut temp_visited = HashSet::new();
1227 let mut result = Vec::new();
1228
1229 let mut all_nodes: Vec<_> = temp_edges.keys().collect();
1231 all_nodes.sort();
1232 for node in all_nodes {
1233 if !visited.contains(node) {
1234 self.visit_node_recursive(
1235 node,
1236 &temp_edges,
1237 &mut visited,
1238 &mut temp_visited,
1239 &mut result,
1240 )?;
1241 }
1242 }
1243
1244 result.reverse();
1245 Ok(result)
1246 }
1247
1248 fn detect_recursive_schemas(&mut self) {
1249 for (schema, deps) in &self.edges {
1250 if deps.contains(schema) {
1251 self.recursive_schemas.insert(schema.clone());
1253 } else {
1254 if self.has_cycle_from(schema, schema, &mut HashSet::new()) {
1256 self.recursive_schemas.insert(schema.clone());
1257 }
1258 }
1259 }
1260
1261 for (schema, deps) in &self.edges {
1263 for dep in deps {
1264 if let Some(dep_deps) = self.edges.get(dep) {
1265 if dep_deps.contains(schema) {
1266 self.recursive_schemas.insert(schema.clone());
1268 self.recursive_schemas.insert(dep.clone());
1269 }
1270 }
1271 }
1272 }
1273 }
1274
1275 fn has_cycle_from(&self, start: &str, current: &str, visited: &mut HashSet<String>) -> bool {
1276 if visited.contains(current) {
1277 return false; }
1279
1280 visited.insert(current.to_string());
1281
1282 if let Some(deps) = self.edges.get(current) {
1283 for dep in deps {
1284 if dep == start {
1285 return true; }
1287 if self.has_cycle_from(start, dep, visited) {
1288 return true;
1289 }
1290 }
1291 }
1292
1293 false
1294 }
1295
1296 #[allow(clippy::only_used_in_recursion)]
1297 fn visit_node_recursive(
1298 &self,
1299 node: &str,
1300 temp_edges: &BTreeMap<String, HashSet<String>>,
1301 visited: &mut HashSet<String>,
1302 temp_visited: &mut HashSet<String>,
1303 result: &mut Vec<String>,
1304 ) -> Result<()> {
1305 if temp_visited.contains(node) {
1306 return Ok(());
1308 }
1309
1310 if visited.contains(node) {
1311 return Ok(());
1312 }
1313
1314 temp_visited.insert(node.to_string());
1315
1316 if let Some(dependencies) = temp_edges.get(node) {
1317 let mut sorted_deps: Vec<_> = dependencies.iter().collect();
1319 sorted_deps.sort();
1320 for dep in sorted_deps {
1321 self.visit_node_recursive(dep, temp_edges, visited, temp_visited, result)?;
1322 }
1323 }
1324
1325 temp_visited.remove(node);
1326 visited.insert(node.to_string());
1327 result.push(node.to_string());
1328
1329 Ok(())
1330 }
1331}
1332
1333pub fn merge_schema_extensions(
1336 main_spec: Value,
1337 extension_paths: &[impl AsRef<Path>],
1338) -> Result<Value> {
1339 let mut result = main_spec;
1340
1341 for path in extension_paths {
1342 let extension = load_extension_file(path.as_ref())?;
1343 result = merge_json_objects_with_replacements(result, extension)?;
1344 }
1345
1346 Ok(result)
1347}
1348
1349fn normalize_operation_path(path: &str) -> String {
1356 match path.split_once('#') {
1357 Some((route, _fragment)) if route.starts_with('/') => route.to_string(),
1358 _ => path.to_string(),
1359 }
1360}
1361
1362fn unwrap_annotation_allof(schema: &crate::openapi::Schema) -> &crate::openapi::Schema {
1367 let crate::openapi::Schema::AllOf { all_of, .. } = schema else {
1368 return schema;
1369 };
1370 let mut references = all_of.iter().filter(|s| s.reference().is_some());
1371 let (Some(first), None) = (references.next(), references.next()) else {
1372 return schema;
1373 };
1374 let others_annotation_only = all_of
1375 .iter()
1376 .all(|member| member.reference().is_some() || schema_is_annotation_only(member));
1377 if others_annotation_only {
1378 first
1379 } else {
1380 schema
1381 }
1382}
1383
1384fn schema_is_annotation_only(schema: &crate::openapi::Schema) -> bool {
1389 serde_json::to_value(schema)
1390 .ok()
1391 .and_then(|value| value.as_object().cloned())
1392 .is_some_and(|object| {
1393 object.keys().all(|key| {
1394 matches!(
1395 key.as_str(),
1396 "title"
1397 | "description"
1398 | "deprecated"
1399 | "readOnly"
1400 | "writeOnly"
1401 | "examples"
1402 | "example"
1403 | "default"
1404 | "externalDocs"
1405 | "xml"
1406 | "$comment"
1407 | "nullable"
1408 ) || key.starts_with("x-")
1409 })
1410 })
1411}
1412
1413fn load_extension_file(path: &Path) -> Result<Value> {
1417 let content = std::fs::read_to_string(path).map_err(|e| GeneratorError::FileError {
1418 message: format!("Failed to read file {}: {}", path.display(), e),
1419 })?;
1420
1421 let is_yaml = path
1422 .extension()
1423 .and_then(|extension| extension.to_str())
1424 .is_some_and(|extension| {
1425 extension.eq_ignore_ascii_case("yaml") || extension.eq_ignore_ascii_case("yml")
1426 });
1427
1428 if is_yaml {
1429 crate::spec_source::yaml_to_json_value(&content).map_err(|error| {
1430 GeneratorError::FileError {
1431 message: format!(
1432 "Failed to parse schema extension {} as YAML: {}",
1433 path.display(),
1434 error
1435 ),
1436 }
1437 })
1438 } else {
1439 serde_json::from_str(&content).map_err(|error| GeneratorError::FileError {
1440 message: format!(
1441 "Failed to parse schema extension {} as JSON: {}",
1442 path.display(),
1443 error
1444 ),
1445 })
1446 }
1447}
1448
1449fn merge_json_objects_with_replacements(main: Value, extension: Value) -> Result<Value> {
1451 let replacements = extract_replacement_rules(&extension);
1453
1454 Ok(merge_json_objects_with_rules(
1456 main,
1457 extension,
1458 &replacements,
1459 ))
1460}
1461
1462fn extract_replacement_rules(
1464 extension: &Value,
1465) -> std::collections::HashMap<String, (String, String)> {
1466 let mut rules = std::collections::HashMap::new();
1467
1468 if let Some(x_replacements) = extension.get("x-replacements") {
1469 if let Some(x_replacements_obj) = x_replacements.as_object() {
1470 for (schema_name, replacement_rule) in x_replacements_obj {
1471 if let Some(rule_obj) = replacement_rule.as_object() {
1472 if let (Some(replace), Some(with)) = (
1473 rule_obj.get("replace").and_then(|v| v.as_str()),
1474 rule_obj.get("with").and_then(|v| v.as_str()),
1475 ) {
1476 rules.insert(schema_name.clone(), (replace.to_string(), with.to_string()));
1477 }
1479 }
1480 }
1481 }
1482 }
1483
1484 rules
1485}
1486
1487fn should_replace_variant(
1489 schema_name: &str,
1490 extension_refs: &[String],
1491 replacements: &std::collections::HashMap<String, (String, String)>,
1492) -> bool {
1493 for (replace_schema, with_schema) in replacements.values() {
1495 if schema_name == replace_schema {
1496 let replacement_exists = extension_refs.iter().any(|ext_ref| {
1498 let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
1499 ext_schema_name == with_schema
1500 });
1501
1502 if replacement_exists {
1503 return true;
1504 }
1505 }
1506 }
1507
1508 extension_refs.iter().any(|ext_ref| {
1510 let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
1511 schema_name == ext_schema_name
1512 })
1513}
1514
1515fn merge_json_objects_with_rules(
1520 main: Value,
1521 extension: Value,
1522 replacements: &std::collections::HashMap<String, (String, String)>,
1523) -> Value {
1524 match (main, extension) {
1525 (Value::Object(mut main_obj), Value::Object(ext_obj)) => {
1527 let main_union_keyword = if main_obj.contains_key("oneOf") {
1530 Some("oneOf")
1531 } else if main_obj.contains_key("anyOf") {
1532 Some("anyOf")
1533 } else {
1534 None
1535 };
1536 if let (Some(main_variants), Some(ext_variants)) = (
1537 extract_schema_variants(&Value::Object(main_obj.clone())),
1538 extract_schema_variants(&Value::Object(ext_obj.clone())),
1539 ) {
1540 let union_key = main_union_keyword.unwrap_or("oneOf");
1541 println!(
1542 "🔍 Merging union schemas ({union_key}): {} main variants, {} extension variants",
1543 main_variants.len(),
1544 ext_variants.len()
1545 );
1546 let mut merged_variants = Vec::new();
1549 let extension_refs: Vec<String> = ext_variants
1550 .iter()
1551 .filter_map(|v| v.get("$ref").and_then(|r| r.as_str()))
1552 .map(|s| s.to_string())
1553 .collect();
1554
1555 for main_variant in main_variants {
1557 if let Some(main_ref) = main_variant.get("$ref").and_then(|r| r.as_str()) {
1558 let schema_name = main_ref.split('/').next_back().unwrap_or("");
1560 let should_replace =
1561 should_replace_variant(schema_name, &extension_refs, replacements);
1562
1563 if should_replace {
1564 println!("🔄 REPLACING {} (explicit rule)", schema_name);
1565 }
1566
1567 if !should_replace {
1568 merged_variants.push(main_variant);
1569 }
1570 } else {
1571 merged_variants.push(main_variant);
1573 }
1574 }
1575
1576 for ext_variant in ext_variants {
1578 merged_variants.push(ext_variant);
1579 }
1580
1581 main_obj.remove("oneOf");
1583 main_obj.remove("anyOf");
1584 main_obj.insert(union_key.to_string(), Value::Array(merged_variants));
1585
1586 for (key, ext_value) in ext_obj {
1588 if key != "oneOf" && key != "anyOf" {
1589 match main_obj.get(&key) {
1590 Some(main_value) => {
1591 let merged_value = merge_json_objects_with_rules(
1592 main_value.clone(),
1593 ext_value,
1594 replacements,
1595 );
1596 main_obj.insert(key, merged_value);
1597 }
1598 None => {
1599 main_obj.insert(key, ext_value);
1600 }
1601 }
1602 }
1603 }
1604
1605 return Value::Object(main_obj);
1606 }
1607
1608 for (key, ext_value) in ext_obj {
1610 match main_obj.get(&key) {
1611 Some(main_value) => {
1612 let merged_value = merge_json_objects_with_rules(
1614 main_value.clone(),
1615 ext_value,
1616 replacements,
1617 );
1618 main_obj.insert(key, merged_value);
1619 }
1620 None => {
1621 main_obj.insert(key, ext_value);
1623 }
1624 }
1625 }
1626 Value::Object(main_obj)
1627 }
1628
1629 (Value::Array(mut main_arr), Value::Array(ext_arr)) => {
1631 main_arr.extend(ext_arr);
1632 Value::Array(main_arr)
1633 }
1634
1635 (_, extension) => extension,
1637 }
1638}
1639
1640fn extract_schema_variants(obj: &Value) -> Option<Vec<Value>> {
1642 if let Value::Object(map) = obj {
1643 if let Some(Value::Array(variants)) = map.get("oneOf") {
1644 return Some(variants.clone());
1645 }
1646 if let Some(Value::Array(variants)) = map.get("anyOf") {
1647 return Some(variants.clone());
1648 }
1649 }
1650 None
1651}
1652
1653#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1658enum InlineUnionKind {
1659 OneOf,
1660 AnyOf,
1661}
1662
1663#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
1664enum SchemaIdentity {
1665 Component(String),
1666 Pointer(String),
1667 InlineUnionBranch {
1668 owner_context: String,
1669 union_kind: InlineUnionKind,
1670 original_index: usize,
1671 discriminator: Option<String>,
1672 fingerprint: String,
1673 },
1674 Inline {
1675 context: String,
1676 kind: &'static str,
1677 preferred_name: String,
1678 fingerprint: String,
1679 },
1680}
1681
1682#[derive(Debug, Default)]
1683struct SchemaNameRegistry {
1684 names_by_identity: BTreeMap<SchemaIdentity, String>,
1685 identities_by_name: BTreeMap<String, SchemaIdentity>,
1686}
1687
1688impl SchemaNameRegistry {
1689 fn with_components(component_names: impl IntoIterator<Item = String>) -> Self {
1690 let mut registry = Self::default();
1691 for name in component_names {
1692 let identity = SchemaIdentity::Component(name.clone());
1693 registry
1694 .names_by_identity
1695 .insert(identity.clone(), name.clone());
1696 registry.identities_by_name.insert(name, identity);
1697 }
1698 registry
1699 }
1700
1701 fn component_name(&self, source_name: &str) -> Option<&str> {
1702 self.names_by_identity
1703 .get(&SchemaIdentity::Component(source_name.to_string()))
1704 .map(String::as_str)
1705 }
1706
1707 fn allocate(
1708 &mut self,
1709 identity: SchemaIdentity,
1710 preferred_name: &str,
1711 collision_name: &str,
1712 ) -> String {
1713 if let Some(existing) = self.names_by_identity.get(&identity) {
1714 return existing.clone();
1715 }
1716
1717 let allocated = if !self.identities_by_name.contains_key(preferred_name) {
1718 preferred_name.to_string()
1719 } else if !self.identities_by_name.contains_key(collision_name) {
1720 collision_name.to_string()
1721 } else {
1722 let hash = stable_schema_identity_hash(&identity);
1723 let hashed = format!("{collision_name}{hash:016X}");
1724 if !self.identities_by_name.contains_key(&hashed) {
1725 hashed
1726 } else {
1727 let mut suffix = 2;
1728 loop {
1729 let candidate = format!("{hashed}{suffix}");
1730 if !self.identities_by_name.contains_key(&candidate) {
1731 break candidate;
1732 }
1733 suffix += 1;
1734 }
1735 }
1736 };
1737
1738 self.names_by_identity
1739 .insert(identity.clone(), allocated.clone());
1740 self.identities_by_name.insert(allocated.clone(), identity);
1741 allocated
1742 }
1743}
1744
1745fn stable_schema_identity_hash(identity: &SchemaIdentity) -> u64 {
1749 let bytes = format!("{identity:?}");
1750 bytes
1751 .as_bytes()
1752 .iter()
1753 .fold(0xcbf29ce484222325, |hash, byte| {
1754 (hash ^ u64::from(*byte)).wrapping_mul(0x100000001b3)
1755 })
1756}
1757
1758#[cfg(test)]
1759mod schema_name_registry_tests {
1760 use super::{SchemaIdentity, SchemaNameRegistry};
1761
1762 #[test]
1763 fn component_reservations_are_independent_of_input_traversal_order() {
1764 let component_names = ["ModelApi".to_string(), "OutputFormatContainer".to_string()];
1765 let mut forward = SchemaNameRegistry::with_components(component_names.clone());
1766 let mut reverse = SchemaNameRegistry::with_components(component_names.into_iter().rev());
1767 let inline = SchemaIdentity::Inline {
1768 context: "Model".to_string(),
1769 kind: "property-object",
1770 preferred_name: "ModelApi".to_string(),
1771 fingerprint: r#"{"type":"object"}"#.to_string(),
1772 };
1773
1774 assert_eq!(
1775 forward.allocate(inline.clone(), "ModelApi", "ModelApiInline"),
1776 "ModelApiInline"
1777 );
1778 assert_eq!(
1779 reverse.allocate(inline, "ModelApi", "ModelApiInline"),
1780 "ModelApiInline"
1781 );
1782 assert_eq!(forward.component_name("ModelApi"), Some("ModelApi"));
1783 assert_eq!(reverse.component_name("ModelApi"), Some("ModelApi"));
1784 assert_eq!(
1785 forward.component_name("OutputFormatContainer"),
1786 Some("OutputFormatContainer")
1787 );
1788 assert_eq!(
1789 reverse.component_name("OutputFormatContainer"),
1790 Some("OutputFormatContainer")
1791 );
1792 }
1793
1794 #[test]
1795 fn an_identity_reuses_its_exact_allocated_name() {
1796 let mut registry = SchemaNameRegistry::with_components(["ModelApi".to_string()]);
1797 let inline = SchemaIdentity::Inline {
1798 context: "Model".to_string(),
1799 kind: "property-object",
1800 preferred_name: "ModelApi".to_string(),
1801 fingerprint: r#"{"type":"object"}"#.to_string(),
1802 };
1803
1804 let first = registry.allocate(inline.clone(), "ModelApi", "ModelApiInline");
1805 let repeated = registry.allocate(inline, "Ignored", "IgnoredInline");
1806
1807 assert_eq!(first, "ModelApiInline");
1808 assert_eq!(repeated, first);
1809 assert_eq!(registry.component_name("ModelApi"), Some("ModelApi"));
1810 }
1811}
1812
1813pub struct SchemaAnalyzer {
1814 schemas: BTreeMap<String, Schema>,
1815 resolved_cache: BTreeMap<String, AnalyzedSchema>,
1816 schema_names: SchemaNameRegistry,
1817 openapi_spec: Value,
1818 current_schema_name: Option<String>,
1819 component_parameters: BTreeMap<String, crate::openapi::Parameter>,
1820 type_mapper: TypeMapper,
1825 resolving_pointers: HashSet<String>,
1828}
1829
1830impl SchemaAnalyzer {
1831 fn untyped_value(&self, _context: impl Into<String>, reason: UntypedReason) -> SchemaType {
1835 SchemaType::Untyped {
1836 shape: UntypedShape::Value,
1837 reason,
1838 }
1839 }
1840
1841 fn untyped_value_array(
1843 &self,
1844 _context: impl Into<String>,
1845 reason: UntypedReason,
1846 ) -> SchemaType {
1847 SchemaType::Untyped {
1848 shape: UntypedShape::ValueArray,
1849 reason,
1850 }
1851 }
1852
1853 fn untyped_value_map(&self, _context: impl Into<String>, reason: UntypedReason) -> SchemaType {
1857 SchemaType::Untyped {
1858 shape: UntypedShape::ValueMap,
1859 reason,
1860 }
1861 }
1862
1863 fn untyped_context(&self, detail: &str) -> String {
1865 match (&self.current_schema_name, detail) {
1866 (Some(schema), "") => schema.clone(),
1867 (Some(schema), detail) => format!("{schema}.{detail}"),
1868 (None, "") => "<anonymous>".to_string(),
1869 (None, detail) => detail.to_string(),
1870 }
1871 }
1872
1873 fn allocate_inline_schema_name(
1874 &mut self,
1875 preferred_name: &str,
1876 collision_name: &str,
1877 kind: &'static str,
1878 schema: &Schema,
1879 ) -> String {
1880 let identity = SchemaIdentity::Inline {
1881 context: self
1882 .current_schema_name
1883 .clone()
1884 .unwrap_or_else(|| "<anonymous>".to_string()),
1885 kind,
1886 preferred_name: preferred_name.to_string(),
1887 fingerprint: serde_json::to_string(schema).unwrap_or_else(|_| format!("{schema:?}")),
1888 };
1889 self.schema_names
1890 .allocate(identity, preferred_name, collision_name)
1891 }
1892
1893 fn with_schema_context<T>(
1898 &mut self,
1899 schema_name: &str,
1900 analyze: impl FnOnce(&mut Self) -> Result<T>,
1901 ) -> Result<T> {
1902 let previous = self.current_schema_name.replace(schema_name.to_string());
1903 let result = analyze(self);
1904 self.current_schema_name = previous;
1905 result
1906 }
1907
1908 fn add_allocated_object_schema(
1909 &mut self,
1910 object_type_name: String,
1911 schema: &Schema,
1912 dependencies: &mut HashSet<String>,
1913 ) -> Result<SchemaType> {
1914 let object_type = self.with_schema_context(&object_type_name, |analyzer| {
1915 analyzer.analyze_object_schema(schema, dependencies)
1916 })?;
1917 self.resolved_cache.insert(
1918 object_type_name.clone(),
1919 AnalyzedSchema {
1920 name: object_type_name.clone(),
1921 original: serde_json::to_value(schema).unwrap_or(Value::Null),
1922 schema_type: object_type,
1923 dependencies: dependencies.clone(),
1924 nullable: false,
1925 description: schema.details().description.clone(),
1926 default: None,
1927 },
1928 );
1929 dependencies.insert(object_type_name.clone());
1930 Ok(SchemaType::Reference {
1931 target: object_type_name,
1932 })
1933 }
1934
1935 fn allocate_inline_union_branch_name(
1936 &mut self,
1937 preferred_name: &str,
1938 owner_context: &str,
1939 union_kind: InlineUnionKind,
1940 original_index: usize,
1941 discriminator: Option<&str>,
1942 schema: &Schema,
1943 ) -> String {
1944 let identity = SchemaIdentity::InlineUnionBranch {
1945 owner_context: owner_context.to_string(),
1946 union_kind,
1947 original_index,
1948 discriminator: discriminator.map(str::to_string),
1949 fingerprint: serde_json::to_string(schema).unwrap_or_else(|_| format!("{schema:?}")),
1950 };
1951 self.schema_names
1952 .allocate(identity, preferred_name, &format!("{preferred_name}Inline"))
1953 }
1954
1955 fn allocate_pointer_schema_name(&mut self, pointer: &str, preferred_name: &str) -> String {
1956 self.schema_names.allocate(
1957 SchemaIdentity::Pointer(pointer.to_string()),
1958 preferred_name,
1959 &format!("{preferred_name}Pointer"),
1960 )
1961 }
1962
1963 fn allocate_synthetic_schema_name(
1964 &mut self,
1965 preferred_name: &str,
1966 collision_name: &str,
1967 kind: &'static str,
1968 fingerprint: String,
1969 ) -> String {
1970 let identity = SchemaIdentity::Inline {
1971 context: self
1972 .current_schema_name
1973 .clone()
1974 .unwrap_or_else(|| "<anonymous>".to_string()),
1975 kind,
1976 preferred_name: preferred_name.to_string(),
1977 fingerprint,
1978 };
1979 self.schema_names
1980 .allocate(identity, preferred_name, collision_name)
1981 }
1982
1983 fn uses_aws_query_conventions(&self) -> bool {
1984 self.openapi_spec
1985 .pointer("/info/x-providerName")
1986 .and_then(Value::as_str)
1987 .is_some_and(|provider| provider.eq_ignore_ascii_case("amazonaws.com"))
1988 }
1989
1990 pub fn new(openapi_spec: Value) -> Result<Self> {
1994 Self::with_type_mapper(openapi_spec, TypeMapper::default())
1995 }
1996
1997 pub fn with_type_mapper(mut openapi_spec: Value, type_mapper: TypeMapper) -> Result<Self> {
2001 disambiguate_component_schema_names(&mut openapi_spec);
2002 let spec: OpenApiSpec = parse_spec_document(&openapi_spec)?;
2003 let schemas = Self::extract_schemas(&spec)?;
2004
2005 let component_parameters = spec
2006 .components
2007 .as_ref()
2008 .and_then(|c| c.parameters.as_ref())
2009 .cloned()
2010 .unwrap_or_default();
2011 let schema_names = SchemaNameRegistry::with_components(schemas.keys().cloned());
2012 Ok(Self {
2013 schemas,
2014 resolved_cache: BTreeMap::new(),
2015 schema_names,
2016 openapi_spec,
2017 current_schema_name: None,
2018 component_parameters,
2019 type_mapper,
2020 resolving_pointers: HashSet::new(),
2021 })
2022 }
2023
2024 pub fn new_with_extensions(
2027 openapi_spec: Value,
2028 extension_paths: &[std::path::PathBuf],
2029 ) -> Result<Self> {
2030 let merged_spec = merge_schema_extensions(openapi_spec, extension_paths)?;
2031 Self::new(merged_spec)
2032 }
2033
2034 pub fn new_with_extensions_and_type_mapper(
2037 openapi_spec: Value,
2038 extension_paths: &[std::path::PathBuf],
2039 type_mapper: TypeMapper,
2040 ) -> Result<Self> {
2041 let merged_spec = merge_schema_extensions(openapi_spec, extension_paths)?;
2042 Self::with_type_mapper(merged_spec, type_mapper)
2043 }
2044
2045 pub fn type_mapper(&self) -> &TypeMapper {
2049 &self.type_mapper
2050 }
2051
2052 fn generate_context_aware_name(
2055 &self,
2056 base_context: &str,
2057 type_hint: &str,
2058 index: usize,
2059 schema: Option<&Schema>,
2060 ) -> String {
2061 if let Some(schema) = schema {
2063 if type_hint == "Array"
2065 && matches!(schema.schema_type(), Some(OpenApiSchemaType::Array))
2066 {
2067 if let Some(items_schema) = schema.details().item_schema() {
2068 if let Some(item_type) = items_schema.schema_type() {
2070 match item_type {
2071 OpenApiSchemaType::Object => {
2072 return format!("{base_context}ItemArray");
2073 }
2074 OpenApiSchemaType::String => {
2075 return format!("{base_context}StringArray");
2076 }
2077 _ => {}
2078 }
2079 }
2080 }
2081 }
2082 }
2083
2084 match type_hint {
2086 "Array" => {
2087 format!("{base_context}Array")
2089 }
2090 "Variant" | "InlineVariant" => {
2091 if index == 0 {
2093 format!("{base_context}{type_hint}")
2094 } else {
2095 format!("{}{}{}", base_context, type_hint, index + 1)
2096 }
2097 }
2098 _ => {
2099 format!("{base_context}{type_hint}{index}")
2101 }
2102 }
2103 }
2104
2105 fn to_pascal_case(&self, s: &str) -> String {
2107 s.split(['_', '-'])
2108 .filter(|part| !part.is_empty())
2109 .map(|part| {
2110 let mut chars = part.chars();
2111 match chars.next() {
2112 None => String::new(),
2113 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
2114 }
2115 })
2116 .collect()
2117 }
2118
2119 fn extract_schemas(spec: &OpenApiSpec) -> Result<BTreeMap<String, Schema>> {
2120 let schemas = spec.components.as_ref().and_then(|c| c.schemas.as_ref());
2125 Ok(schemas
2126 .map(|m| {
2127 m.iter()
2128 .map(|(k, v)| (k.clone(), v.clone()))
2129 .collect::<BTreeMap<_, _>>()
2130 })
2131 .unwrap_or_default())
2132 }
2133
2134 pub fn analyze(&mut self) -> Result<SchemaAnalysis> {
2135 let validation_context = ValidationContext {
2136 openapi_version: self
2137 .openapi_spec
2138 .get("openapi")
2139 .and_then(Value::as_str)
2140 .unwrap_or_default()
2141 .to_string(),
2142 json_schema_dialect: self
2143 .openapi_spec
2144 .get("jsonSchemaDialect")
2145 .and_then(Value::as_str)
2146 .map(str::to_string),
2147 component_schemas: self
2148 .openapi_spec
2149 .pointer("/components/schemas")
2150 .and_then(Value::as_object)
2151 .map(|schemas| {
2152 schemas
2153 .iter()
2154 .map(|(name, schema)| (name.clone(), schema.clone()))
2155 .collect()
2156 })
2157 .unwrap_or_default(),
2158 };
2159 let mut analysis = SchemaAnalysis {
2160 schemas: BTreeMap::new(),
2161 dependencies: DependencyGraph::new(),
2162 patterns: DetectedPatterns {
2163 tagged_enum_schemas: HashSet::new(),
2164 untagged_enum_schemas: HashSet::new(),
2165 type_mappings: BTreeMap::new(),
2166 },
2167 operations: BTreeMap::new(),
2168 operation_responses: BTreeMap::new(),
2169 operation_sources: BTreeMap::new(),
2170 operation_id_aliases: BTreeMap::new(),
2171 used_type_features: crate::type_mapping::UsedFeatures::default(),
2172 enum_extensions: BTreeMap::new(),
2173 validation_context,
2174 untagged_union_branches: BTreeSet::new(),
2175 multipart_file_type: None,
2176 };
2177
2178 self.detect_patterns(&mut analysis.patterns)?;
2180
2181 let schema_names: Vec<String> = self.schemas.keys().cloned().collect();
2183 for schema_name in schema_names {
2184 let analyzed = self.analyze_schema(&schema_name)?;
2185
2186 for dep in &analyzed.dependencies {
2188 analysis
2189 .dependencies
2190 .add_dependency(schema_name.clone(), dep.clone());
2191 }
2192
2193 analysis.schemas.insert(schema_name, analyzed);
2194 }
2195
2196 for (inline_name, inline_schema) in &self.resolved_cache {
2199 if !analysis.schemas.contains_key(inline_name) {
2200 analysis
2202 .schemas
2203 .insert(inline_name.clone(), inline_schema.clone());
2204
2205 for dep in &inline_schema.dependencies {
2207 analysis
2208 .dependencies
2209 .add_dependency(inline_name.clone(), dep.clone());
2210 }
2211
2212 let mut schemas_to_update = Vec::new();
2217 for (schema_name, schema) in &analysis.schemas {
2218 if schema_name == inline_name {
2220 continue;
2221 }
2222
2223 if schema.dependencies.contains(inline_name) {
2224 schemas_to_update.push(schema_name.clone());
2226 }
2227 }
2228
2229 for schema_name in schemas_to_update {
2231 analysis
2232 .dependencies
2233 .add_dependency(schema_name, inline_name.clone());
2234 }
2235 }
2236 }
2237
2238 self.analyze_operations(&mut analysis)?;
2240
2241 for (inline_name, inline_schema) in &self.resolved_cache {
2244 if !analysis.schemas.contains_key(inline_name) {
2245 analysis
2246 .schemas
2247 .insert(inline_name.clone(), inline_schema.clone());
2248
2249 for dep in &inline_schema.dependencies {
2251 analysis
2252 .dependencies
2253 .add_dependency(inline_name.clone(), dep.clone());
2254 }
2255 }
2256 }
2257
2258 disambiguate_analyzed_schema_names(&mut analysis, &self.schemas);
2259 retype_multipart_file_fields(&mut analysis);
2260
2261 analysis.used_type_features = self.type_mapper.used_features();
2265
2266 for (name, analyzed) in &analysis.schemas {
2271 let enum_value_count = match &analyzed.schema_type {
2272 SchemaType::StringEnum { values } => values.len(),
2273 SchemaType::ExtensibleEnum { known_values } => known_values.len(),
2274 _ => continue,
2275 };
2276 if let Some(ext) = extract_enum_extensions(&analyzed.original, enum_value_count, name) {
2277 analysis.enum_extensions.insert(name.clone(), ext);
2278 }
2279 }
2280
2281 analysis.untagged_union_branches = collect_untagged_union_branches(&analysis);
2282
2283 for schema in analysis.schemas.values_mut() {
2284 normalize_untyped(&mut schema.schema_type, 0);
2285 }
2286
2287 Ok(analysis)
2288 }
2289
2290 fn detect_patterns(&self, patterns: &mut DetectedPatterns) -> Result<()> {
2291 for (schema_name, schema) in &self.schemas {
2292 if self.is_discriminated_union(schema) {
2294 patterns.tagged_enum_schemas.insert(schema_name.clone());
2295
2296 if let Some(mappings) = self.extract_type_mappings(schema)? {
2298 patterns.type_mappings.insert(schema_name.clone(), mappings);
2299 }
2300 }
2301 else if self.is_simple_union(schema) {
2303 patterns.untagged_enum_schemas.insert(schema_name.clone());
2304 }
2305 }
2306
2307 Ok(())
2308 }
2309
2310 fn is_discriminated_union(&self, schema: &Schema) -> bool {
2311 if schema.is_discriminated_union() {
2313 return true;
2314 }
2315
2316 if let Some(variants) = schema.union_variants() {
2318 return variants.len() > 2 && self.detect_discriminator_field(variants).is_some();
2319 }
2320
2321 false
2322 }
2323
2324 fn all_variants_have_unique_const_values(&self, variants: &[Schema], field_name: &str) -> bool {
2325 let mut values = HashSet::new();
2326
2327 variants.iter().all(|variant| {
2328 let schema = if let Some(ref_str) = variant.reference() {
2329 let Some(schema_name) = self.extract_schema_name(ref_str) else {
2330 return false;
2331 };
2332 let Some(schema) = self.schemas.get(schema_name) else {
2333 return false;
2334 };
2335 schema
2336 } else {
2337 variant
2338 };
2339
2340 self.extract_discriminator_value_for_field(schema, field_name)
2341 .is_some_and(|value| values.insert(value))
2342 })
2343 }
2344
2345 fn branch_resolves_to_object(&self, schema: &Schema) -> bool {
2354 self.branch_resolves_to_object_inner(schema, &mut HashSet::new())
2355 }
2356
2357 fn branch_resolves_to_object_inner(
2358 &self,
2359 schema: &Schema,
2360 visited_refs: &mut HashSet<String>,
2361 ) -> bool {
2362 if let Some(ref_str) = schema.reference() {
2364 if !visited_refs.insert(ref_str.to_string()) {
2365 return false;
2366 }
2367 let result = match self
2368 .extract_schema_name(ref_str)
2369 .and_then(|n| self.schemas.get(n))
2370 {
2371 Some(target) => self.branch_resolves_to_object_inner(target, visited_refs),
2372 None => false,
2373 };
2374 visited_refs.remove(ref_str);
2375 return result;
2376 }
2377 if let Schema::AllOf { all_of, .. } = schema {
2381 if Self::single_non_object_allof_carrier(all_of).is_some() {
2382 return false;
2383 }
2384 return all_of
2385 .iter()
2386 .filter(|member| !schema_is_annotation_only(member))
2387 .any(|member| self.branch_resolves_to_object_inner(member, visited_refs));
2388 }
2389 if let Some(variants) = schema.union_variants() {
2394 return !variants.is_empty()
2395 && variants
2396 .iter()
2397 .all(|variant| self.branch_resolves_to_object_inner(variant, visited_refs));
2398 }
2399 if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object)) {
2400 return true;
2401 }
2402 if schema.inferred_type() == Some(OpenApiSchemaType::Object) {
2403 return true;
2404 }
2405 false
2408 }
2409
2410 fn detect_discriminator_field(&self, variants: &[Schema]) -> Option<String> {
2414 if variants.is_empty() {
2415 return None;
2416 }
2417
2418 let first_variant = &variants[0];
2420 let first_schema = if let Some(ref_str) = first_variant.reference() {
2421 let schema_name = self.extract_schema_name(ref_str)?;
2422 self.schemas.get(schema_name)?
2423 } else {
2424 first_variant
2425 };
2426
2427 let properties = first_schema.details().properties.as_ref()?;
2428 let mut candidates: Vec<String> = Vec::new();
2429
2430 for (field_name, field_schema) in properties {
2431 let details = field_schema.details();
2432 let is_const = details.const_value.is_some()
2433 || details.enum_values.as_ref().is_some_and(|v| v.len() == 1)
2434 || details.extra.contains_key("const");
2435 if is_const {
2436 candidates.push(field_name.clone());
2437 }
2438 }
2439
2440 if candidates.is_empty() {
2441 return None;
2442 }
2443
2444 candidates.sort_by(|a, b| {
2446 if a == "type" {
2447 std::cmp::Ordering::Less
2448 } else if b == "type" {
2449 std::cmp::Ordering::Greater
2450 } else {
2451 a.cmp(b)
2452 }
2453 });
2454
2455 for candidate in &candidates {
2461 if self.all_variants_have_unique_const_values(variants, candidate) {
2462 return Some(candidate.clone());
2463 }
2464 }
2465
2466 None
2467 }
2468
2469 fn is_simple_union(&self, schema: &Schema) -> bool {
2470 if let Some(variants) = schema.union_variants() {
2471 if variants.len() > 1 && !schema.is_nullable_pattern() {
2473 let has_refs = variants.iter().any(|v| v.is_reference());
2474 return has_refs;
2475 }
2476 }
2477 false
2478 }
2479
2480 fn schema_or_reference_is_nullable(&self, schema: &Schema) -> bool {
2485 let mut current = schema.clone();
2486 let mut visited = HashSet::new();
2487 loop {
2488 if current.is_nullable_any() {
2489 return true;
2490 }
2491 if let Schema::AllOf { all_of, .. } = ¤t
2492 && let Some(carrier) = Self::single_non_object_allof_carrier(all_of)
2493 {
2494 current = carrier.clone();
2495 continue;
2496 }
2497 let Some(reference) = current.reference() else {
2498 return false;
2499 };
2500 if !visited.insert(reference.to_string()) {
2501 return false;
2502 }
2503 let Some(target) = self.reference_target_schema(reference) else {
2504 return false;
2505 };
2506 current = target;
2507 }
2508 }
2509
2510 fn nullable_container_value(&self, schema: &Schema, schema_type: SchemaType) -> SchemaType {
2516 if !self.schema_or_reference_is_nullable(schema)
2517 || matches!(schema_type, SchemaType::Nullable { .. })
2518 || matches!(schema.schema_type(), Some(OpenApiSchemaType::Null))
2519 {
2520 schema_type
2521 } else {
2522 SchemaType::Nullable {
2523 inner_type: Box::new(schema_type),
2524 }
2525 }
2526 }
2527
2528 fn extract_type_mappings(&self, schema: &Schema) -> Result<Option<BTreeMap<String, String>>> {
2529 let variants = schema.union_variants().ok_or_else(|| {
2530 GeneratorError::InvalidSchema("No variants found for discriminated union".to_string())
2531 })?;
2532
2533 let discriminator_field = if let Some(discriminator) = schema.discriminator() {
2535 discriminator.property_name.clone()
2536 } else if let Some(detected) = self.detect_discriminator_field(variants) {
2537 detected
2538 } else {
2539 "type".to_string() };
2541
2542 let mut mappings = BTreeMap::new();
2543
2544 for variant in variants {
2545 if let Some(ref_str) = variant.reference() {
2546 if let Some(type_name) = self.extract_schema_name(ref_str) {
2547 if let Some(variant_schema) = self.schemas.get(type_name) {
2548 if let Some(discriminator_value) = self
2549 .extract_discriminator_value_for_field(
2550 variant_schema,
2551 &discriminator_field,
2552 )
2553 {
2554 mappings.insert(type_name.to_string(), discriminator_value);
2555 }
2556 }
2557 }
2558 }
2559 }
2560
2561 if mappings.is_empty() {
2562 Ok(None)
2563 } else {
2564 Ok(Some(mappings))
2565 }
2566 }
2567
2568 #[allow(dead_code)]
2569 fn extract_discriminator_value(&self, schema: &Schema) -> Option<String> {
2570 self.extract_discriminator_value_for_field(schema, "type")
2571 }
2572
2573 fn extract_discriminator_values_for_field(
2574 &self,
2575 schema: &Schema,
2576 field_name: &str,
2577 ) -> Vec<String> {
2578 self.extract_discriminator_value_domain_for_field(schema, field_name)
2579 .unwrap_or_default()
2580 }
2581
2582 fn extract_discriminator_value_domain_for_field(
2583 &self,
2584 schema: &Schema,
2585 field_name: &str,
2586 ) -> Option<Vec<String>> {
2587 let mut visited_refs = HashSet::new();
2588 self.discriminator_value_domain(schema, field_name, &mut visited_refs)
2589 }
2590
2591 fn discriminator_value_domain(
2598 &self,
2599 schema: &Schema,
2600 field_name: &str,
2601 visited_refs: &mut HashSet<String>,
2602 ) -> Option<Vec<String>> {
2603 if let Some(reference) = schema.reference() {
2604 if !visited_refs.insert(reference.to_string()) {
2605 return None;
2606 }
2607 let result = self
2608 .extract_schema_name(reference)
2609 .and_then(|name| self.schemas.get(name))
2610 .and_then(|target| {
2611 self.discriminator_value_domain(target, field_name, visited_refs)
2612 });
2613 visited_refs.remove(reference);
2614 return result;
2615 }
2616
2617 let own_domain = schema
2618 .details()
2619 .properties
2620 .as_ref()
2621 .and_then(|properties| properties.get(field_name))
2622 .and_then(|property| self.string_constraint_domain(property, visited_refs));
2623
2624 let composition_domain = match schema {
2625 Schema::AllOf { all_of, .. } => {
2626 let mut domain = None;
2627 for member in all_of {
2628 domain = Self::intersect_optional_domains(
2629 domain,
2630 self.discriminator_value_domain(member, field_name, visited_refs),
2631 );
2632 }
2633 domain
2634 }
2635 Schema::AnyOf { any_of, .. } => {
2636 self.union_discriminator_domains(any_of, field_name, visited_refs)
2637 }
2638 Schema::OneOf { one_of, .. } => {
2639 self.union_discriminator_domains(one_of, field_name, visited_refs)
2640 }
2641 Schema::Bool(false) => Some(Vec::new()),
2642 _ => None,
2643 };
2644
2645 Self::intersect_optional_domains(own_domain, composition_domain)
2646 }
2647
2648 fn union_discriminator_domains(
2649 &self,
2650 schemas: &[Schema],
2651 field_name: &str,
2652 visited_refs: &mut HashSet<String>,
2653 ) -> Option<Vec<String>> {
2654 if schemas.is_empty() {
2655 return Some(Vec::new());
2656 }
2657 let mut domain = Vec::new();
2658 for schema in schemas {
2659 let values = self.discriminator_value_domain(schema, field_name, visited_refs)?;
2660 for value in values {
2661 Self::push_unique_string(&mut domain, &value);
2662 }
2663 }
2664 Some(domain)
2665 }
2666
2667 fn string_constraint_domain(
2668 &self,
2669 schema: &Schema,
2670 visited_refs: &mut HashSet<String>,
2671 ) -> Option<Vec<String>> {
2672 let allow_vendor_default =
2673 !self.has_standard_string_constraint(schema, &mut HashSet::new());
2674 self.string_constraint_domain_inner(schema, visited_refs, allow_vendor_default)
2675 }
2676
2677 fn string_constraint_domain_inner(
2678 &self,
2679 schema: &Schema,
2680 visited_refs: &mut HashSet<String>,
2681 allow_vendor_default: bool,
2682 ) -> Option<Vec<String>> {
2683 if let Some(reference) = schema.reference() {
2684 if !visited_refs.insert(reference.to_string()) {
2685 return None;
2686 }
2687 let result = self
2688 .extract_schema_name(reference)
2689 .and_then(|name| self.schemas.get(name))
2690 .and_then(|target| {
2691 self.string_constraint_domain_inner(target, visited_refs, allow_vendor_default)
2692 });
2693 visited_refs.remove(reference);
2694 return result;
2695 }
2696
2697 let details = schema.details();
2698 let mut own_domain = None;
2699 if let Some(value) = details.const_value.as_ref() {
2700 own_domain = Self::intersect_optional_domains(
2701 own_domain,
2702 Some(value.as_str().into_iter().map(str::to_string).collect()),
2703 );
2704 }
2705 if let Some(enum_values) = &details.enum_values {
2706 own_domain = Self::intersect_optional_domains(
2707 own_domain,
2708 Some(
2709 enum_values
2710 .iter()
2711 .filter_map(Value::as_str)
2712 .map(str::to_string)
2713 .collect(),
2714 ),
2715 );
2716 }
2717 if allow_vendor_default
2718 && details
2719 .extra
2720 .get("x-stainless-const")
2721 .and_then(Value::as_bool)
2722 == Some(true)
2723 && let Some(default) = details.default.as_ref().and_then(Value::as_str)
2724 {
2725 own_domain =
2726 Self::intersect_optional_domains(own_domain, Some(vec![default.to_string()]));
2727 }
2728
2729 let composition_domain = match schema {
2730 Schema::AllOf { all_of, .. } => {
2731 let mut domain = None;
2732 for member in all_of {
2733 domain = Self::intersect_optional_domains(
2734 domain,
2735 self.string_constraint_domain_inner(
2736 member,
2737 visited_refs,
2738 allow_vendor_default,
2739 ),
2740 );
2741 }
2742 domain
2743 }
2744 Schema::AnyOf { any_of, .. } => {
2745 self.union_string_constraint_domains(any_of, visited_refs, allow_vendor_default)
2746 }
2747 Schema::OneOf { one_of, .. } => {
2748 self.union_string_constraint_domains(one_of, visited_refs, allow_vendor_default)
2749 }
2750 Schema::Bool(false) => Some(Vec::new()),
2751 _ => None,
2752 };
2753
2754 Self::intersect_optional_domains(own_domain, composition_domain)
2755 }
2756
2757 fn union_string_constraint_domains(
2758 &self,
2759 schemas: &[Schema],
2760 visited_refs: &mut HashSet<String>,
2761 allow_vendor_default: bool,
2762 ) -> Option<Vec<String>> {
2763 if schemas.is_empty() {
2764 return Some(Vec::new());
2765 }
2766 let mut domain = Vec::new();
2767 for schema in schemas {
2768 let values =
2769 self.string_constraint_domain_inner(schema, visited_refs, allow_vendor_default)?;
2770 for value in values {
2771 Self::push_unique_string(&mut domain, &value);
2772 }
2773 }
2774 Some(domain)
2775 }
2776
2777 fn has_standard_string_constraint(
2778 &self,
2779 schema: &Schema,
2780 visited_refs: &mut HashSet<String>,
2781 ) -> bool {
2782 if let Some(reference) = schema.reference() {
2783 if !visited_refs.insert(reference.to_string()) {
2784 return false;
2785 }
2786 let result = self
2787 .extract_schema_name(reference)
2788 .and_then(|name| self.schemas.get(name))
2789 .is_some_and(|target| self.has_standard_string_constraint(target, visited_refs));
2790 visited_refs.remove(reference);
2791 return result;
2792 }
2793
2794 let details = schema.details();
2795 if details.const_value.is_some() || details.enum_values.is_some() {
2796 return true;
2797 }
2798
2799 match schema {
2800 Schema::AllOf { all_of, .. } => all_of
2801 .iter()
2802 .any(|member| self.has_standard_string_constraint(member, visited_refs)),
2803 Schema::AnyOf { any_of, .. } => any_of
2804 .iter()
2805 .any(|member| self.has_standard_string_constraint(member, visited_refs)),
2806 Schema::OneOf { one_of, .. } => one_of
2807 .iter()
2808 .any(|member| self.has_standard_string_constraint(member, visited_refs)),
2809 _ => false,
2810 }
2811 }
2812
2813 fn intersect_optional_domains(
2814 left: Option<Vec<String>>,
2815 right: Option<Vec<String>>,
2816 ) -> Option<Vec<String>> {
2817 match (left, right) {
2818 (None, other) | (other, None) => other,
2819 (Some(left), Some(right)) => Some(
2820 left.into_iter()
2821 .filter(|value| right.contains(value))
2822 .collect(),
2823 ),
2824 }
2825 }
2826
2827 fn push_unique_string(values: &mut Vec<String>, value: &str) {
2828 if !values.iter().any(|existing| existing == value) {
2829 values.push(value.to_string());
2830 }
2831 }
2832
2833 fn discriminator_property_presence(&self, schema: &Schema, field_name: &str) -> (bool, bool) {
2834 self.discriminator_property_presence_inner(schema, field_name, &mut HashSet::new(), 0)
2835 }
2836
2837 fn discriminator_property_presence_inner(
2838 &self,
2839 schema: &Schema,
2840 field_name: &str,
2841 visited_refs: &mut HashSet<String>,
2842 depth: usize,
2843 ) -> (bool, bool) {
2844 if depth > 64 {
2845 return (false, false);
2846 }
2847 if let Some(reference) = schema.reference() {
2848 if !visited_refs.insert(reference.to_string()) {
2849 return (false, false);
2850 }
2851 let result = self
2852 .extract_schema_name(reference)
2853 .and_then(|name| self.schemas.get(name))
2854 .map(|target| {
2855 self.discriminator_property_presence_inner(
2856 target,
2857 field_name,
2858 visited_refs,
2859 depth + 1,
2860 )
2861 })
2862 .unwrap_or((false, false));
2863 visited_refs.remove(reference);
2864 return result;
2865 }
2866
2867 let details = schema.details();
2868 let mut declared = details
2869 .properties
2870 .as_ref()
2871 .is_some_and(|properties| properties.contains_key(field_name));
2872 let mut required = details
2873 .required
2874 .as_ref()
2875 .is_some_and(|names| names.iter().any(|name| name == field_name));
2876
2877 let members = match schema {
2878 Schema::AllOf { all_of, .. } => Some(all_of.as_slice()),
2879 Schema::AnyOf { any_of, .. } => Some(any_of.as_slice()),
2880 Schema::OneOf { one_of, .. } => Some(one_of.as_slice()),
2881 _ => None,
2882 };
2883 if let Some(members) = members {
2884 for member in members {
2885 let (member_declared, member_required) = self
2886 .discriminator_property_presence_inner(
2887 member,
2888 field_name,
2889 visited_refs,
2890 depth + 1,
2891 );
2892 declared |= member_declared;
2893 required |= member_required;
2894 }
2895 }
2896 (declared, required)
2897 }
2898
2899 fn extract_discriminator_value_for_field(
2900 &self,
2901 schema: &Schema,
2902 field_name: &str,
2903 ) -> Option<String> {
2904 self.extract_discriminator_values_for_field(schema, field_name)
2905 .into_iter()
2906 .next()
2907 }
2908
2909 fn get_any_reference<'a>(&self, schema: &'a Schema) -> Option<&'a str> {
2910 schema.reference().or_else(|| schema.recursive_reference())
2911 }
2912
2913 fn extract_schema_name<'a>(&self, ref_str: &'a str) -> Option<&'a str> {
2914 if ref_str == "#" {
2915 return None; }
2917
2918 let parts: Vec<&str> = ref_str.split('/').collect();
2919
2920 if parts.len() == 4 && parts[0] == "#" && parts[2] == "schemas" {
2924 return Some(parts[3]);
2925 }
2926
2927 if parts.len() == 3 && parts[0] == "#" && parts[1] == "definitions" {
2930 return Some(parts[2]);
2931 }
2932
2933 if ref_str.starts_with("#/") {
2937 return None;
2938 }
2939
2940 let last = parts.last()?;
2946 if last.is_empty()
2947 || last.chars().all(|c| c.is_ascii_digit())
2948 || matches!(
2949 *last,
2950 "schema" | "properties" | "items" | "additionalProperties"
2951 )
2952 {
2953 return None;
2954 }
2955 let first = last.chars().next().unwrap_or(' ');
2956 if !first.is_ascii_alphabetic() || !first.is_ascii_uppercase() {
2957 return None;
2958 }
2959 Some(last)
2960 }
2961
2962 fn reference_target_schema(&self, reference: &str) -> Option<Schema> {
2965 if let Some(name) = self.extract_schema_name(reference) {
2966 return self.schemas.get(name).cloned();
2967 }
2968 let pointer = reference.strip_prefix('#')?;
2969 if !pointer.starts_with('/') {
2970 return None;
2971 }
2972 Schema::deserialize(self.openapi_spec.pointer(pointer)?).ok()
2973 }
2974
2975 fn analyze_schema(&mut self, schema_name: &str) -> Result<AnalyzedSchema> {
2976 let emitted_name = self
2980 .schema_names
2981 .component_name(schema_name)
2982 .ok_or_else(|| GeneratorError::UnresolvedReference(schema_name.to_string()))?
2983 .to_string();
2984 if let Some(cached) = self.resolved_cache.get(&emitted_name) {
2985 return Ok(cached.clone());
2986 }
2987
2988 self.current_schema_name = Some(emitted_name.clone());
2990
2991 let schema = self
2992 .schemas
2993 .get(schema_name)
2994 .ok_or_else(|| GeneratorError::UnresolvedReference(schema_name.to_string()))?
2995 .clone();
2996
2997 self.resolved_cache.insert(
2999 emitted_name.clone(),
3000 AnalyzedSchema {
3001 name: emitted_name.clone(),
3002 original: serde_json::to_value(&schema).unwrap_or(Value::Null),
3003 schema_type: SchemaType::Reference {
3004 target: "placeholder".to_string(),
3005 },
3006 dependencies: HashSet::new(),
3007 nullable: false,
3008 description: None,
3009 default: None,
3010 },
3011 );
3012
3013 let analyzed = self.analyze_schema_value(&schema, &emitted_name)?;
3014
3015 self.resolved_cache.insert(emitted_name, analyzed.clone());
3017
3018 Ok(analyzed)
3019 }
3020
3021 fn analyze_schema_value(
3022 &mut self,
3023 schema: &Schema,
3024 schema_name: &str,
3025 ) -> Result<AnalyzedSchema> {
3026 let details = schema.details();
3027 let description = details.description.clone();
3028 let nullable = schema.is_nullable_any();
3032 let mut dependencies = HashSet::new();
3033
3034 let schema_type = match schema {
3035 Schema::Bool(accepts_anything) => self.untyped_value(
3038 self.untyped_context(""),
3039 if *accepts_anything {
3040 UntypedReason::AnySchema
3041 } else {
3042 UntypedReason::NeverMatches
3043 },
3044 ),
3045 Schema::Reference { reference, .. } => {
3046 match self.extract_schema_name(reference) {
3051 Some(name) => {
3052 let target = name.to_string();
3053 dependencies.insert(target.clone());
3054 SchemaType::Reference { target }
3055 }
3056 None => {
3057 let reference = reference.clone();
3058 if let Some(resolved) =
3059 self.resolve_pointer_schema(&reference, &mut dependencies)?
3060 {
3061 resolved
3062 } else {
3063 eprintln!(
3064 "⚠️ unresolvable $ref `{}` — typing as serde_json::Value",
3065 reference
3066 );
3067 self.untyped_value(
3068 format!("$ref {reference}"),
3069 UntypedReason::UnresolvedReference,
3070 )
3071 }
3072 }
3073 }
3074 }
3075 Schema::RecursiveRef { recursive_ref, .. }
3076 | Schema::DynamicRef {
3077 dynamic_ref: recursive_ref,
3078 ..
3079 } => {
3080 if recursive_ref == "#" {
3086 dependencies.insert(schema_name.to_string());
3087 SchemaType::Reference {
3088 target: schema_name.to_string(),
3089 }
3090 } else {
3091 let target = self
3092 .extract_schema_name(recursive_ref)
3093 .unwrap_or(schema_name)
3094 .to_string();
3095 dependencies.insert(target.clone());
3096 SchemaType::Reference { target }
3097 }
3098 }
3099 Schema::Typed { .. } | Schema::TypedMulti { .. } => {
3100 if let Some(non_null_types) = schema.non_null_schema_types() {
3101 let mut variants = Vec::with_capacity(non_null_types.len());
3102 for t in non_null_types {
3103 variants.push(self.build_typed_multi_union_variant(
3104 t,
3105 schema,
3106 schema_name,
3107 &mut dependencies,
3108 )?);
3109 }
3110 SchemaType::Union {
3111 variants,
3112 exclusive: false,
3113 }
3114 } else {
3115 self.analyze_single_typed_schema(
3116 schema,
3117 schema_name,
3118 details,
3119 &mut dependencies,
3120 )?
3121 }
3122 }
3123 Schema::AnyOf {
3124 any_of,
3125 discriminator,
3126 ..
3127 } => {
3128 if Self::union_only_constrains_requiredness(any_of) {
3129 return Ok(AnalyzedSchema {
3130 name: schema_name.to_string(),
3131 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3132 schema_type: self.analyze_empty_union(schema, &mut dependencies)?,
3133 dependencies,
3134 nullable,
3135 description,
3136 default: details.default.clone(),
3137 });
3138 }
3139 if let Some(schema_type) = self.analyze_object_with_variants(
3140 schema,
3141 any_of,
3142 schema_name,
3143 &mut dependencies,
3144 )? {
3145 return Ok(AnalyzedSchema {
3146 name: schema_name.to_string(),
3147 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3148 schema_type,
3149 dependencies,
3150 nullable,
3151 description,
3152 default: details.default.clone(),
3153 });
3154 }
3155 self.analyze_anyof_union(
3157 any_of,
3158 discriminator.as_ref(),
3159 &mut dependencies,
3160 schema_name,
3161 )?
3162 }
3163 Schema::OneOf {
3164 one_of,
3165 discriminator,
3166 ..
3167 } => {
3168 if one_of.is_empty() {
3169 self.analyze_empty_union(schema, &mut dependencies)?
3170 } else if let Some(schema_type) = self.analyze_object_with_variants(
3171 schema,
3172 one_of,
3173 schema_name,
3174 &mut dependencies,
3175 )? {
3176 schema_type
3177 } else {
3178 self.analyze_oneof_union(
3180 one_of,
3181 discriminator.as_ref(),
3182 schema_name,
3183 &mut dependencies,
3184 InlineUnionKind::OneOf,
3185 None,
3186 )?
3187 }
3188 }
3189 Schema::AllOf { all_of, .. } => {
3190 self.analyze_allof_composition(schema, all_of, &mut dependencies)?
3192 }
3193 Schema::Untyped { .. } => {
3194 if let Some(inferred) = schema.inferred_type() {
3196 match inferred {
3197 OpenApiSchemaType::Object => {
3198 if self.should_use_dynamic_json(schema) {
3199 self.untyped_value(
3200 self.untyped_context(""),
3201 UntypedReason::OpaqueObject,
3202 )
3203 } else {
3204 self.analyze_object_schema(schema, &mut dependencies)?
3205 }
3206 }
3207 OpenApiSchemaType::String if details.is_string_enum() => {
3208 SchemaType::StringEnum {
3209 values: details.string_enum_values().unwrap_or_default(),
3210 }
3211 }
3212 OpenApiSchemaType::Null => SchemaType::Primitive {
3215 rust_type: self.type_mapper.null_unit().rust_type,
3216 serde_with: None,
3217 },
3218 _ => self.untyped_value(
3219 self.untyped_context(""),
3220 UntypedReason::UnsupportedTypeKeyword,
3221 ),
3222 }
3223 } else {
3224 self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema)
3225 }
3226 }
3227 };
3228
3229 Ok(AnalyzedSchema {
3230 name: schema_name.to_string(),
3231 original: serde_json::to_value(schema).unwrap_or(Value::Null), schema_type,
3233 dependencies,
3234 nullable,
3235 description,
3236 default: details.default.clone(),
3237 })
3238 }
3239
3240 fn analyze_single_typed_schema(
3246 &mut self,
3247 schema: &Schema,
3248 schema_name: &str,
3249 details: &crate::openapi::SchemaDetails,
3250 dependencies: &mut HashSet<String>,
3251 ) -> Result<SchemaType> {
3252 let primary = schema
3253 .schema_type()
3254 .cloned()
3255 .unwrap_or(OpenApiSchemaType::Object);
3256 let format = details.format.as_deref();
3257 Ok(match primary {
3258 OpenApiSchemaType::String => {
3259 if let Some(values) = details.string_enum_values() {
3260 SchemaType::StringEnum { values }
3261 } else {
3262 let mapped = self.type_mapper.string_format(format);
3263 SchemaType::Primitive {
3264 rust_type: mapped.rust_type,
3265 serde_with: mapped.serde_with,
3266 }
3267 }
3268 }
3269 OpenApiSchemaType::Integer => SchemaType::Primitive {
3270 rust_type: self.integer_rust_type(details),
3271 serde_with: None,
3272 },
3273 OpenApiSchemaType::Number => SchemaType::Primitive {
3274 rust_type: self.type_mapper.number_format(format).rust_type,
3275 serde_with: None,
3276 },
3277 OpenApiSchemaType::Boolean => SchemaType::Primitive {
3278 rust_type: self.type_mapper.boolean().rust_type,
3279 serde_with: None,
3280 },
3281 OpenApiSchemaType::Array => {
3282 self.analyze_array_schema(schema, schema_name, dependencies)?
3283 }
3284 OpenApiSchemaType::Object => {
3285 if self.should_use_dynamic_json(schema) {
3286 self.untyped_value(self.untyped_context(""), UntypedReason::OpaqueObject)
3287 } else {
3288 self.analyze_object_schema(schema, dependencies)?
3289 }
3290 }
3291 OpenApiSchemaType::Null => SchemaType::Primitive {
3294 rust_type: self.type_mapper.null_unit().rust_type,
3295 serde_with: None,
3296 },
3297 })
3298 }
3299
3300 fn analyze_object_schema(
3301 &mut self,
3302 schema: &Schema,
3303 dependencies: &mut HashSet<String>,
3304 ) -> Result<SchemaType> {
3305 let details = schema.details();
3306 let properties = &details.properties;
3307 let required = details
3308 .required
3309 .as_ref()
3310 .map(|req| req.iter().cloned().collect::<HashSet<String>>())
3311 .unwrap_or_default();
3312
3313 let mut property_info = BTreeMap::new();
3314 let owner_name = self
3317 .current_schema_name
3318 .clone()
3319 .unwrap_or_else(|| "Inline".to_string());
3320
3321 if let Some(props) = properties {
3322 for (prop_name, prop_schema) in props {
3323 let prop_type = if let Schema::AnyOf { any_of, .. } = prop_schema {
3325 if Self::union_only_constrains_requiredness(any_of) {
3330 self.analyze_empty_union(prop_schema, dependencies)?
3331 } else if let Some(with_variants) = {
3332 let variant_owner =
3333 format!("{owner_name}{}", self.to_pascal_case(prop_name));
3334 self.with_schema_context(&variant_owner, |analyzer| {
3335 analyzer.analyze_object_with_variants(
3336 prop_schema,
3337 any_of,
3338 &variant_owner,
3339 dependencies,
3340 )
3341 })?
3342 } {
3343 with_variants
3344 } else if self.should_use_dynamic_json(prop_schema) {
3345 self.untyped_value(
3347 self.untyped_context(prop_name),
3348 UntypedReason::OpaqueObject,
3349 )
3350 } else if prop_schema.is_nullable_pattern()
3351 && let Some(non_null) = prop_schema.non_null_variant()
3352 {
3353 self.analyze_property_schema_with_context(
3361 non_null,
3362 Some(prop_name),
3363 dependencies,
3364 )?
3365 } else {
3366 let context_name = self
3369 .current_schema_name
3370 .clone()
3371 .unwrap_or_else(|| "Unknown".to_string());
3372
3373 let prop_pascal = self.to_pascal_case(prop_name);
3375 let preferred_union_name = format!("{context_name}{prop_pascal}");
3376 let union_type_name = self.allocate_inline_schema_name(
3377 &preferred_union_name,
3378 &format!("{preferred_union_name}Union2"),
3379 "property-anyof",
3380 prop_schema,
3381 );
3382
3383 let union_schema_type = self.analyze_anyof_union(
3385 any_of,
3386 prop_schema.discriminator(),
3387 dependencies,
3388 &union_type_name,
3389 )?;
3390
3391 self.resolved_cache.insert(
3393 union_type_name.clone(),
3394 AnalyzedSchema {
3395 name: union_type_name.clone(),
3396 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
3397 dependencies: schema_type_dependencies(&union_schema_type),
3398 schema_type: union_schema_type,
3399 nullable: false,
3400 description: prop_schema.details().description.clone(),
3401 default: None,
3402 },
3403 );
3404
3405 dependencies.insert(union_type_name.clone());
3407 SchemaType::Reference {
3408 target: union_type_name,
3409 }
3410 }
3411 } else if let Schema::OneOf {
3412 one_of,
3413 discriminator,
3414 ..
3415 } = prop_schema
3416 {
3417 if prop_schema.is_nullable_pattern()
3424 && let Some(non_null) = prop_schema.non_null_variant()
3425 {
3426 let unwrapped = self.analyze_property_schema_with_context(
3427 non_null,
3428 Some(prop_name),
3429 dependencies,
3430 )?;
3431 let owner_name = self
3432 .current_schema_name
3433 .clone()
3434 .unwrap_or_else(|| "Inline".to_string());
3435 let unwrapped = self.hoist_inline_property_type(
3436 &owner_name,
3437 prop_name,
3438 unwrapped,
3439 dependencies,
3440 );
3441 let prop_details = prop_schema.details();
3442 let prop_nullable = true;
3443 let prop_description = prop_details.description.clone();
3444 let prop_default = prop_details.default.clone();
3445 property_info.insert(
3446 prop_name.clone(),
3447 PropertyInfo {
3448 schema_type: unwrapped,
3449 nullable: prop_nullable,
3450 description: prop_description,
3451 default: prop_default,
3452 serde_attrs: Vec::new(),
3453 synthesized_required: false,
3454 constraints: PropertyConstraints::from_schema_details(prop_details),
3455 },
3456 );
3457 continue;
3458 }
3459
3460 let context_name = self
3462 .current_schema_name
3463 .clone()
3464 .unwrap_or_else(|| "Unknown".to_string());
3465 let prop_pascal = self.to_pascal_case(prop_name);
3466 let preferred_union_name = format!("{context_name}{prop_pascal}");
3467 let union_type_name = self.allocate_inline_schema_name(
3468 &preferred_union_name,
3469 &format!("{preferred_union_name}Union2"),
3470 "property-oneof",
3471 prop_schema,
3472 );
3473
3474 let union_schema_type = self.analyze_oneof_union(
3476 one_of,
3477 discriminator.as_ref(),
3478 &union_type_name,
3479 dependencies,
3480 InlineUnionKind::OneOf,
3481 None,
3482 )?;
3483
3484 self.resolved_cache.insert(
3486 union_type_name.clone(),
3487 AnalyzedSchema {
3488 name: union_type_name.clone(),
3489 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
3490 schema_type: union_schema_type,
3491 dependencies: HashSet::new(),
3492 nullable: false,
3493 description: prop_schema.details().description.clone(),
3494 default: None,
3495 },
3496 );
3497
3498 dependencies.insert(union_type_name.clone());
3500 SchemaType::Reference {
3501 target: union_type_name,
3502 }
3503 } else {
3504 self.analyze_property_schema_with_context(
3506 prop_schema,
3507 Some(prop_name),
3508 dependencies,
3509 )?
3510 };
3511
3512 let prop_type = self.hoist_inline_property_type(
3513 &owner_name,
3514 prop_name,
3515 prop_type,
3516 dependencies,
3517 );
3518
3519 let prop_details = prop_schema.details();
3520 let prop_nullable = self.schema_or_reference_is_nullable(prop_schema);
3522 let prop_description = prop_details.description.clone();
3523 let prop_default = prop_details.default.clone();
3524
3525 property_info.insert(
3526 prop_name.clone(),
3527 PropertyInfo {
3528 schema_type: prop_type,
3529 nullable: prop_nullable,
3530 description: prop_description,
3531 default: prop_default,
3532 serde_attrs: Vec::new(),
3533 synthesized_required: false,
3534 constraints: PropertyConstraints::from_schema_details(prop_details),
3535 },
3536 );
3537 }
3538 }
3539
3540 let typed_enabled = self
3548 .type_mapper
3549 .config()
3550 .shape
3551 .as_ref()
3552 .and_then(|s| s.additional_properties_typed)
3553 .unwrap_or(true);
3554
3555 let untyped_required_property = || PropertyInfo {
3556 schema_type: SchemaType::Untyped {
3557 shape: UntypedShape::Value,
3558 reason: UntypedReason::AnySchema,
3559 },
3560 nullable: false,
3561 description: None,
3562 default: None,
3563 serde_attrs: Vec::new(),
3564 synthesized_required: true,
3565 constraints: PropertyConstraints::default(),
3566 };
3567 let (mut additional_properties, required_additional_property, explicitly_forbidden) =
3568 match &details.additional_properties {
3569 Some(crate::openapi::AdditionalProperties::Boolean(true)) => (
3570 ObjectAdditionalProperties::Untyped,
3571 Some(untyped_required_property()),
3572 false,
3573 ),
3574 Some(crate::openapi::AdditionalProperties::Boolean(false)) => {
3575 (ObjectAdditionalProperties::Denied, None, true)
3576 }
3577 Some(crate::openapi::AdditionalProperties::Schema(value_schema))
3578 if typed_enabled =>
3579 {
3580 let analyzed = self.analyze_property_schema_with_context(
3581 value_schema,
3582 Some("AdditionalProperty"),
3583 dependencies,
3584 )?;
3585 let nullable_value =
3586 self.nullable_container_value(value_schema, analyzed.clone());
3587 let value_details = value_schema.details();
3588 let required_property = PropertyInfo {
3589 schema_type: analyzed.clone(),
3590 nullable: self.schema_or_reference_is_nullable(value_schema),
3591 description: value_details.description.clone(),
3592 default: None,
3595 serde_attrs: Vec::new(),
3596 synthesized_required: true,
3597 constraints: PropertyConstraints::from_schema_details(value_details),
3598 };
3599 (
3600 ObjectAdditionalProperties::Typed {
3601 value_type: Box::new(nullable_value),
3602 },
3603 Some(required_property),
3604 false,
3605 )
3606 }
3607 Some(crate::openapi::AdditionalProperties::Schema(_)) => (
3608 ObjectAdditionalProperties::Untyped,
3611 Some(untyped_required_property()),
3612 false,
3613 ),
3614 None if Self::object_shape_needs_additional_property_carrier(details) => (
3619 ObjectAdditionalProperties::Untyped,
3620 Some(untyped_required_property()),
3621 false,
3622 ),
3623 None => (
3624 ObjectAdditionalProperties::Closed,
3625 Some(untyped_required_property()),
3626 false,
3627 ),
3628 };
3629
3630 self.finalize_required_object_members(
3631 &mut property_info,
3632 &required,
3633 &mut additional_properties,
3634 required_additional_property,
3635 explicitly_forbidden,
3636 )?;
3637
3638 Ok(SchemaType::Object {
3639 properties: property_info,
3640 variant: None,
3641 required,
3642 additional_properties,
3643 })
3644 }
3645
3646 fn object_shape_needs_additional_property_carrier(
3652 details: &crate::openapi::SchemaDetails,
3653 ) -> bool {
3654 if details.min_properties.is_some() || details.max_properties.is_some() {
3655 return true;
3656 }
3657
3658 let declared = details.properties.as_ref();
3659 let has_undeclared_key = |value: &Value| {
3660 value.as_object().is_some_and(|object| {
3661 object
3662 .keys()
3663 .any(|key| declared.is_none_or(|properties| !properties.contains_key(key)))
3664 })
3665 };
3666 details.example.as_ref().is_some_and(has_undeclared_key)
3667 || details
3668 .examples
3669 .as_ref()
3670 .is_some_and(|examples| examples.iter().any(has_undeclared_key))
3671 }
3672
3673 fn finalize_required_object_members(
3678 &self,
3679 properties: &mut BTreeMap<String, PropertyInfo>,
3680 required: &HashSet<String>,
3681 additional_properties: &mut ObjectAdditionalProperties,
3682 required_additional_property: Option<PropertyInfo>,
3683 explicitly_forbidden: bool,
3684 ) -> Result<()> {
3685 let mut missing = required
3686 .iter()
3687 .filter(|name| !properties.contains_key(*name))
3688 .cloned()
3689 .collect::<Vec<_>>();
3690 missing.sort();
3691 if missing.is_empty() {
3692 return Ok(());
3693 }
3694
3695 if explicitly_forbidden {
3696 let owner = self
3697 .current_schema_name
3698 .as_deref()
3699 .unwrap_or("<inline object>");
3700 return Err(GeneratorError::InvalidSchema(format!(
3701 "object schema `{owner}` is unsatisfiable: required member(s) {} are not declared in properties while additionalProperties: false",
3702 missing
3703 .iter()
3704 .map(|name| format!("`{name}`"))
3705 .collect::<Vec<_>>()
3706 .join(", ")
3707 )));
3708 }
3709
3710 let required_additional_property = required_additional_property.ok_or_else(|| {
3711 GeneratorError::InvalidSchema(
3712 "undeclared required members have no additional-properties carrier".to_string(),
3713 )
3714 })?;
3715 for name in missing {
3716 properties.insert(name, required_additional_property.clone());
3717 }
3718
3719 if !additional_properties.is_open() {
3720 *additional_properties = ObjectAdditionalProperties::Untyped;
3721 }
3722 Ok(())
3723 }
3724
3725 fn build_typed_multi_union_variant(
3735 &mut self,
3736 member_type: OpenApiSchemaType,
3737 schema: &Schema,
3738 union_type_name: &str,
3739 dependencies: &mut HashSet<String>,
3740 ) -> Result<SchemaRef> {
3741 match member_type {
3742 OpenApiSchemaType::Array => {
3743 let preferred_array_type_name = format!("{union_type_name}Array");
3744 let array_type_name = self.allocate_inline_schema_name(
3745 &preferred_array_type_name,
3746 &format!("{preferred_array_type_name}Inline"),
3747 "typed-multi-array",
3748 schema,
3749 );
3750 let array_type =
3751 self.analyze_array_schema(schema, &array_type_name, dependencies)?;
3752 self.resolved_cache.insert(
3753 array_type_name.clone(),
3754 AnalyzedSchema {
3755 name: array_type_name.clone(),
3756 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3757 schema_type: array_type,
3758 dependencies: HashSet::new(),
3759 nullable: false,
3760 description: Some("Array variant in union".to_string()),
3761 default: None,
3762 },
3763 );
3764 dependencies.insert(array_type_name.clone());
3765 Ok(SchemaRef {
3766 target: array_type_name,
3767 nullable: false,
3768 })
3769 }
3770 OpenApiSchemaType::Object => {
3771 let preferred_object_type_name = format!("{union_type_name}Object");
3772 let object_type_name = self.allocate_inline_schema_name(
3773 &preferred_object_type_name,
3774 &format!("{preferred_object_type_name}Inline"),
3775 "typed-multi-object",
3776 schema,
3777 );
3778 if Self::object_shape_is_unconstrained(schema.details()) {
3786 let schema_type = self.untyped_value_map(
3787 self.untyped_context(&object_type_name),
3788 UntypedReason::OpaqueObject,
3789 );
3790 self.resolved_cache.insert(
3791 object_type_name.clone(),
3792 AnalyzedSchema {
3793 name: object_type_name.clone(),
3794 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3795 schema_type,
3796 dependencies: HashSet::new(),
3797 nullable: false,
3798 description: Some("Object variant in union".to_string()),
3799 default: None,
3800 },
3801 );
3802 dependencies.insert(object_type_name.clone());
3803 return Ok(SchemaRef {
3804 target: object_type_name,
3805 nullable: false,
3806 });
3807 }
3808 let object_type = self.add_allocated_object_schema(
3809 object_type_name.clone(),
3810 schema,
3811 dependencies,
3812 )?;
3813 let SchemaType::Reference { target } = object_type else {
3814 unreachable!("allocated object schemas always return a reference");
3815 };
3816 Ok(SchemaRef {
3817 target,
3818 nullable: false,
3819 })
3820 }
3821 _ => Ok(SchemaRef {
3822 target: self.openapi_type_to_rust_type(member_type, schema.details()),
3823 nullable: false,
3824 }),
3825 }
3826 }
3827
3828 fn analyze_property_schema_with_context(
3829 &mut self,
3830 schema: &Schema,
3831 property_name: Option<&str>,
3832 dependencies: &mut HashSet<String>,
3833 ) -> Result<SchemaType> {
3834 if let Schema::Bool(accepts_anything) = schema {
3836 let reason = if *accepts_anything {
3837 UntypedReason::AnySchema
3838 } else {
3839 UntypedReason::NeverMatches
3840 };
3841 return Ok(self.untyped_value(
3842 self.untyped_context(property_name.unwrap_or_default()),
3843 reason,
3844 ));
3845 }
3846
3847 if let Some(ref_str) = self.get_any_reference(schema) {
3848 let target_opt = if ref_str == "#" {
3849 Some(
3850 self.find_recursive_anchor_schema()
3851 .unwrap_or_else(|| "UnknownRecursive".to_string()),
3852 )
3853 } else {
3854 self.extract_schema_name(ref_str).map(|s| s.to_string())
3855 };
3856 match target_opt {
3857 Some(target) => {
3858 dependencies.insert(target.clone());
3859 return Ok(SchemaType::Reference { target });
3860 }
3861 None => {
3862 if let Some(resolved) = self.resolve_pointer_schema(ref_str, dependencies)? {
3867 return Ok(resolved);
3868 }
3869 eprintln!(
3870 "⚠️ unresolvable $ref `{}` — typing as serde_json::Value",
3871 ref_str
3872 );
3873 return Ok(self.untyped_value(
3874 format!("$ref {ref_str}"),
3875 UntypedReason::UnresolvedReference,
3876 ));
3877 }
3878 }
3879 }
3880
3881 if let Some(non_null_types) = schema.non_null_schema_types() {
3885 let context_name = self
3886 .current_schema_name
3887 .clone()
3888 .unwrap_or_else(|| "Unknown".to_string());
3889 let prop_pascal = property_name
3890 .map(|name| self.to_pascal_case(name))
3891 .unwrap_or_default();
3892 let preferred_union_name = format!("{context_name}{prop_pascal}");
3893 let union_type_name = self.allocate_inline_schema_name(
3894 &preferred_union_name,
3895 &format!("{preferred_union_name}Union2"),
3896 "property-typed-multi",
3897 schema,
3898 );
3899
3900 let details = schema.details();
3901 let mut variants = Vec::with_capacity(non_null_types.len());
3902 for t in non_null_types {
3903 variants.push(self.build_typed_multi_union_variant(
3904 t,
3905 schema,
3906 &union_type_name,
3907 dependencies,
3908 )?);
3909 }
3910
3911 self.resolved_cache.insert(
3912 union_type_name.clone(),
3913 AnalyzedSchema {
3914 name: union_type_name.clone(),
3915 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3916 schema_type: SchemaType::Union {
3917 variants,
3918 exclusive: false,
3919 },
3920 dependencies: HashSet::new(),
3921 nullable: false,
3922 description: details.description.clone(),
3923 default: None,
3924 },
3925 );
3926
3927 dependencies.insert(union_type_name.clone());
3928 return Ok(SchemaType::Reference {
3929 target: union_type_name,
3930 });
3931 }
3932
3933 if let Some(schema_type) = schema.schema_type() {
3934 match schema_type {
3935 OpenApiSchemaType::String => {
3936 if let Some(enum_values) = schema.details().string_enum_values() {
3938 let context_name = self
3941 .current_schema_name
3942 .clone()
3943 .unwrap_or_else(|| "Unknown".to_string());
3944
3945 let primary_name = if let Some(prop_name) = property_name {
3947 let prop_pascal = self.to_pascal_case(prop_name);
3949 format!("{context_name}{prop_pascal}")
3950 } else {
3951 let suffix = if !enum_values.is_empty() {
3954 let first_value = self.to_pascal_case(&enum_values[0]);
3955 format!("{first_value}Enum")
3956 } else {
3957 "StringEnum".to_string()
3958 };
3959 format!("{context_name}{suffix}")
3960 };
3961
3962 return Ok(self.hoist_inline_string_enum(
3963 schema,
3964 enum_values,
3965 primary_name,
3966 dependencies,
3967 ));
3968 } else {
3969 let mapped = self
3975 .type_mapper
3976 .string_format(schema.details().format.as_deref());
3977 return Ok(SchemaType::Primitive {
3978 rust_type: mapped.rust_type,
3979 serde_with: mapped.serde_with,
3980 });
3981 }
3982 }
3983 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
3984 let details = schema.details();
3985 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
3986 return Ok(SchemaType::Primitive {
3987 rust_type,
3988 serde_with: None,
3989 });
3990 }
3991 OpenApiSchemaType::Boolean => {
3992 return Ok(SchemaType::Primitive {
3993 rust_type: "bool".to_string(),
3994 serde_with: None,
3995 });
3996 }
3997 OpenApiSchemaType::Array => {
3998 let context_name = if let Some(prop_name) = property_name {
4000 let prop_pascal = self.to_pascal_case(prop_name);
4002 format!(
4003 "{}{}",
4004 self.current_schema_name.as_deref().unwrap_or("Unknown"),
4005 prop_pascal
4006 )
4007 } else {
4008 "ArrayItem".to_string()
4010 };
4011 return self.analyze_array_schema(schema, &context_name, dependencies);
4012 }
4013 OpenApiSchemaType::Object => {
4014 if self.should_use_dynamic_json(schema) {
4016 return Ok(self
4017 .untyped_value(self.untyped_context(""), UntypedReason::OpaqueObject));
4018 }
4019 let preferred_object_type_name = if let Some(prop_name) = property_name {
4021 let prop_pascal = self.to_pascal_case(prop_name);
4023 format!(
4024 "{}{}",
4025 self.current_schema_name.as_deref().unwrap_or("Unknown"),
4026 prop_pascal
4027 )
4028 } else {
4029 format!(
4031 "{}Object",
4032 self.current_schema_name.as_deref().unwrap_or("Unknown")
4033 )
4034 };
4035 let object_type_name = self.allocate_inline_schema_name(
4036 &preferred_object_type_name,
4037 &format!("{preferred_object_type_name}Inline"),
4038 "property-object",
4039 schema,
4040 );
4041
4042 return self.add_allocated_object_schema(
4043 object_type_name,
4044 schema,
4045 dependencies,
4046 );
4047 }
4048 OpenApiSchemaType::Null => {
4051 return Ok(SchemaType::Primitive {
4052 rust_type: self.type_mapper.null_unit().rust_type,
4053 serde_with: None,
4054 });
4055 }
4056 }
4057 }
4058
4059 if schema.is_nullable_pattern() {
4061 if let Some(non_null) = schema.non_null_variant() {
4062 return self.analyze_property_schema_with_context(
4063 non_null,
4064 property_name,
4065 dependencies,
4066 );
4067 }
4068 }
4069
4070 if self.should_use_dynamic_json(schema) {
4072 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::OpaqueObject));
4073 }
4074
4075 if let Schema::AllOf { all_of, .. } = schema {
4077 if let Some(property_name) = property_name {
4078 let owner = self
4079 .current_schema_name
4080 .clone()
4081 .unwrap_or_else(|| "Inline".to_string());
4082 let composition_name = format!("{owner}{}", self.to_pascal_case(property_name));
4083 return self.with_schema_context(&composition_name, |analyzer| {
4084 analyzer.analyze_allof_composition(schema, all_of, dependencies)
4085 });
4086 }
4087 return self.analyze_allof_composition(schema, all_of, dependencies);
4088 }
4089
4090 if let Some(variants) = schema.union_variants() {
4092 match variants.len().cmp(&1) {
4093 std::cmp::Ordering::Equal => {
4094 return self.analyze_property_schema_with_context(
4096 &variants[0],
4097 property_name,
4098 dependencies,
4099 );
4100 }
4101 std::cmp::Ordering::Greater => {
4102 let union_name = if let Some(prop_name) = property_name {
4105 let prop_pascal = self.to_pascal_case(prop_name);
4107 format!(
4108 "{}{}",
4109 self.current_schema_name.as_deref().unwrap_or(""),
4110 prop_pascal
4111 )
4112 } else {
4113 "UnionType".to_string()
4114 };
4115
4116 if let Schema::OneOf {
4118 one_of,
4119 discriminator,
4120 ..
4121 } = schema
4122 {
4123 let union_name = self.allocate_inline_schema_name(
4124 &union_name,
4125 &format!("{union_name}Union2"),
4126 "fallback-property-oneof",
4127 schema,
4128 );
4129 let oneof_result = self.analyze_oneof_union(
4131 one_of,
4132 discriminator.as_ref(),
4133 &union_name,
4134 dependencies,
4135 InlineUnionKind::OneOf,
4136 None,
4137 )?;
4138
4139 if let SchemaType::Union { .. } = &oneof_result {
4141 self.resolved_cache.insert(
4143 union_name.clone(),
4144 AnalyzedSchema {
4145 name: union_name.clone(),
4146 original: serde_json::to_value(schema).unwrap_or(Value::Null),
4147 schema_type: oneof_result.clone(),
4148 dependencies: dependencies.clone(),
4149 nullable: false,
4150 description: schema.details().description.clone(),
4151 default: None,
4152 },
4153 );
4154
4155 dependencies.insert(union_name.clone());
4157 return Ok(SchemaType::Reference { target: union_name });
4158 }
4159
4160 return Ok(oneof_result);
4161 } else if let Schema::AnyOf {
4162 any_of,
4163 discriminator,
4164 ..
4165 } = schema
4166 {
4167 let union_analysis = self.analyze_anyof_union(
4169 any_of,
4170 discriminator.as_ref(),
4171 dependencies,
4172 &union_name,
4173 )?;
4174 return Ok(union_analysis);
4175 } else {
4176 let mut union_variants = Vec::new();
4179 for variant in variants {
4180 if let Some(ref_str) = variant.reference() {
4181 if let Some(target) = self.extract_schema_name(ref_str) {
4182 dependencies.insert(target.to_string());
4183 union_variants.push(SchemaRef {
4184 target: target.to_string(),
4185 nullable: false,
4186 });
4187 }
4188 }
4189 }
4190 return Ok(SchemaType::Union {
4191 variants: union_variants,
4192 exclusive: false,
4193 });
4194 }
4195 }
4196 std::cmp::Ordering::Less => {}
4197 }
4198 }
4199
4200 if let Some(inferred_type) = schema.inferred_type() {
4202 match inferred_type {
4203 OpenApiSchemaType::Object => {
4204 if self.should_use_dynamic_json(schema) {
4206 return Ok(self
4207 .untyped_value(self.untyped_context(""), UntypedReason::OpaqueObject));
4208 }
4209 let owner = self
4210 .current_schema_name
4211 .clone()
4212 .unwrap_or_else(|| "Unknown".to_string());
4213 let preferred_name = property_name.map_or_else(
4214 || format!("{owner}Object"),
4215 |property_name| format!("{owner}{}", self.to_pascal_case(property_name)),
4216 );
4217 let object_type_name = self.allocate_inline_schema_name(
4218 &preferred_name,
4219 &format!("{preferred_name}Inline"),
4220 "property-object",
4221 schema,
4222 );
4223 return self.add_allocated_object_schema(
4224 object_type_name,
4225 schema,
4226 dependencies,
4227 );
4228 }
4229 OpenApiSchemaType::Array => {
4230 let context_name = if let Some(prop_name) = property_name {
4231 let prop_pascal = self.to_pascal_case(prop_name);
4233 format!(
4234 "{}{}",
4235 self.current_schema_name.as_deref().unwrap_or("Unknown"),
4236 prop_pascal
4237 )
4238 } else {
4239 "ArrayItem".to_string()
4241 };
4242 return self.analyze_array_schema(schema, &context_name, dependencies);
4243 }
4244 OpenApiSchemaType::String => {
4245 if let Some(enum_values) = schema.details().string_enum_values() {
4246 return Ok(SchemaType::StringEnum {
4247 values: enum_values,
4248 });
4249 } else {
4250 return Ok(SchemaType::Primitive {
4251 rust_type: "String".to_string(),
4252 serde_with: None,
4253 });
4254 }
4255 }
4256 _ => {
4257 let rust_type = self.openapi_type_to_rust_type(inferred_type, schema.details());
4259 return Ok(SchemaType::Primitive {
4260 rust_type,
4261 serde_with: None,
4262 });
4263 }
4264 }
4265 }
4266
4267 Ok(self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema))
4268 }
4269
4270 fn analyze_allof_composition(
4271 &mut self,
4272 owner_schema: &Schema,
4273 all_of_schemas: &[Schema],
4274 dependencies: &mut HashSet<String>,
4275 ) -> Result<SchemaType> {
4276 if let Some(carrier) = Self::single_non_object_allof_carrier(all_of_schemas) {
4281 return self.analyze_property_schema_with_context(carrier, None, dependencies);
4282 }
4283
4284 let referenced_targets = all_of_schemas
4289 .iter()
4290 .filter_map(|schema| schema.reference())
4291 .filter_map(|reference| self.extract_schema_name(reference))
4292 .collect::<Vec<_>>();
4293 let only_reference_and_annotations = all_of_schemas
4294 .iter()
4295 .all(|schema| schema.reference().is_some() || schema_is_annotation_only(schema));
4296 if referenced_targets.len() == 1 && only_reference_and_annotations {
4297 let target = referenced_targets[0];
4298 dependencies.insert(target.to_string());
4299 return Ok(SchemaType::Reference {
4300 target: target.to_string(),
4301 });
4302 }
4303
4304 if let [only] = all_of_schemas
4308 && !matches!(
4309 only.schema_type(),
4310 Some(OpenApiSchemaType::Object) | Some(OpenApiSchemaType::Null)
4311 )
4312 && only.details().properties.is_none()
4313 {
4314 return self.analyze_property_schema_with_context(only, None, dependencies);
4315 }
4316
4317 let mut merged_properties = BTreeMap::new();
4319 let mut merged_required = HashSet::new();
4320 let mut merged_variant = None;
4321 let mut descriptions = Vec::new();
4322
4323 let current_context = self.current_schema_name.clone();
4325 let owner_name = current_context.as_deref().unwrap_or("InlineComposition");
4326
4327 self.merge_schema_into_properties(
4331 owner_schema,
4332 &mut merged_properties,
4333 &mut merged_required,
4334 dependencies,
4335 )?;
4336
4337 for (member_index, schema) in all_of_schemas.iter().enumerate() {
4338 match schema {
4339 Schema::Reference { reference, .. } => {
4340 let (analyzed_type, analyzed_name, raw_target) =
4341 if let Some(target) = self.extract_schema_name(reference) {
4342 dependencies.insert(target.to_string());
4343 let analyzed_ref = self.analyze_schema(target)?;
4344 (
4345 Some(analyzed_ref.schema_type),
4346 Some(target.to_string()),
4347 self.schemas.get(target).cloned(),
4348 )
4349 } else {
4350 (
4351 self.resolve_pointer_schema(reference, dependencies)?,
4352 None,
4353 self.reference_target_schema(reference),
4354 )
4355 };
4356
4357 let merged = if let Some(analyzed_type) = analyzed_type {
4358 self.merge_analyzed_object_properties(
4359 &analyzed_type,
4360 analyzed_name.as_deref(),
4361 &mut merged_properties,
4362 &mut merged_required,
4363 &mut merged_variant,
4364 owner_name,
4365 )?
4366 } else {
4367 false
4368 };
4369 if !merged && let Some(raw_target) = raw_target {
4370 self.merge_schema_into_properties(
4371 &raw_target,
4372 &mut merged_properties,
4373 &mut merged_required,
4374 dependencies,
4375 )?;
4376 }
4377 }
4378 Schema::AnyOf { any_of, .. }
4379 if Self::union_only_constrains_requiredness(any_of) => {}
4380 Schema::OneOf { one_of, .. }
4381 if Self::union_only_constrains_requiredness(one_of) => {}
4382 Schema::AnyOf { .. } | Schema::OneOf { .. } => {
4383 let preferred_name = format!("{owner_name}AllOfVariant{}", member_index + 1);
4384 let variant_name =
4385 self.add_inline_schema(&preferred_name, schema, dependencies)?;
4386 dependencies.insert(variant_name.clone());
4387 let analyzed_type = self
4388 .resolved_cache
4389 .get(&variant_name)
4390 .map(|analyzed| analyzed.schema_type.clone())
4391 .ok_or_else(|| {
4392 GeneratorError::InvalidSchema(format!(
4393 "allOf union member `{variant_name}` was not analyzed"
4394 ))
4395 })?;
4396 if !self.merge_analyzed_object_properties(
4397 &analyzed_type,
4398 Some(&variant_name),
4399 &mut merged_properties,
4400 &mut merged_required,
4401 &mut merged_variant,
4402 owner_name,
4403 )? {
4404 return Ok(self.untyped_value(
4405 self.untyped_context(""),
4406 UntypedReason::UnrepresentableComposition,
4407 ));
4408 }
4409 }
4410 Schema::Typed {
4411 schema_type: OpenApiSchemaType::Object,
4412 ..
4413 }
4414 | Schema::Untyped { .. } => {
4415 let saved_context = self.current_schema_name.clone();
4417 self.current_schema_name = current_context.clone();
4418
4419 self.merge_schema_into_properties(
4421 schema,
4422 &mut merged_properties,
4423 &mut merged_required,
4424 dependencies,
4425 )?;
4426
4427 self.current_schema_name = saved_context;
4429 }
4430 _ => {
4431 self.merge_schema_into_properties(
4434 schema,
4435 &mut merged_properties,
4436 &mut merged_required,
4437 dependencies,
4438 )?;
4439 }
4440 }
4441
4442 if let Some(desc) = &schema.details().description {
4444 descriptions.push(desc.clone());
4445 }
4446 }
4447
4448 if !merged_properties.is_empty() || !merged_required.is_empty() || merged_variant.is_some()
4453 {
4454 let mut additional_properties = if merged_properties
4455 .values()
4456 .any(|property| property.synthesized_required)
4457 {
4458 ObjectAdditionalProperties::Untyped
4459 } else {
4460 ObjectAdditionalProperties::Closed
4461 };
4462 self.finalize_required_object_members(
4463 &mut merged_properties,
4464 &merged_required,
4465 &mut additional_properties,
4466 Some(PropertyInfo {
4467 schema_type: SchemaType::Untyped {
4468 shape: UntypedShape::Value,
4469 reason: UntypedReason::AnySchema,
4470 },
4471 nullable: false,
4472 description: None,
4473 default: None,
4474 serde_attrs: Vec::new(),
4475 synthesized_required: true,
4476 constraints: PropertyConstraints::default(),
4477 }),
4478 false,
4479 )?;
4480 Ok(SchemaType::Object {
4481 properties: merged_properties,
4482 required: merged_required,
4483 additional_properties,
4484 variant: merged_variant,
4485 })
4486 } else {
4487 let schemas = all_of_schemas
4488 .iter()
4489 .filter_map(|schema| {
4490 let reference = schema.reference()?;
4491 let target = self.extract_schema_name(reference)?;
4492 dependencies.insert(target.to_string());
4493 Some(SchemaRef {
4494 target: target.to_string(),
4495 nullable: false,
4496 })
4497 })
4498 .collect::<Vec<_>>();
4499 let references_are_objects = all_of_schemas
4505 .iter()
4506 .filter(|schema| schema.reference().is_some())
4507 .all(|schema| self.branch_resolves_to_object(schema));
4508 if schemas.is_empty() || !references_are_objects {
4509 Ok(self.untyped_value(
4510 self.untyped_context(""),
4511 UntypedReason::UnrepresentableComposition,
4512 ))
4513 } else {
4514 Ok(SchemaType::Composition { schemas })
4515 }
4516 }
4517 }
4518
4519 fn single_non_object_allof_carrier(all_of_schemas: &[Schema]) -> Option<&Schema> {
4524 let mut meaningful = all_of_schemas
4525 .iter()
4526 .filter(|schema| !schema_is_annotation_only(schema));
4527 let carrier = meaningful.next()?;
4528 if meaningful.next().is_some() {
4529 return None;
4530 }
4531
4532 match carrier {
4533 Schema::Typed {
4534 schema_type:
4535 OpenApiSchemaType::String
4536 | OpenApiSchemaType::Integer
4537 | OpenApiSchemaType::Number
4538 | OpenApiSchemaType::Boolean
4539 | OpenApiSchemaType::Array,
4540 ..
4541 } => Some(carrier),
4542 Schema::TypedMulti { schema_types, .. } => {
4543 let mut non_null = schema_types
4544 .iter()
4545 .filter(|schema_type| **schema_type != OpenApiSchemaType::Null);
4546 let only = non_null.next()?;
4547 (non_null.next().is_none()
4548 && matches!(
4549 only,
4550 OpenApiSchemaType::String
4551 | OpenApiSchemaType::Integer
4552 | OpenApiSchemaType::Number
4553 | OpenApiSchemaType::Boolean
4554 | OpenApiSchemaType::Array
4555 ))
4556 .then_some(carrier)
4557 }
4558 _ => None,
4559 }
4560 }
4561
4562 fn merge_analyzed_object_properties(
4567 &mut self,
4568 schema_type: &SchemaType,
4569 named_target: Option<&str>,
4570 merged_properties: &mut BTreeMap<String, PropertyInfo>,
4571 merged_required: &mut HashSet<String>,
4572 merged_variant: &mut Option<SchemaRef>,
4573 owner_name: &str,
4574 ) -> Result<bool> {
4575 let mut current = schema_type.clone();
4576 let mut visited = HashSet::new();
4577 let mut variant_target = named_target.map(str::to_string);
4578 loop {
4579 match current {
4580 SchemaType::Object {
4581 properties,
4582 required,
4583 variant,
4584 ..
4585 } => {
4586 for (name, property) in properties {
4587 let keep_declared_sibling =
4588 merged_properties.get(&name).is_some_and(|existing| {
4589 !existing.synthesized_required && property.synthesized_required
4590 });
4591 if !keep_declared_sibling {
4592 merged_properties.insert(name, property);
4593 }
4594 }
4595 merged_required.extend(required);
4596 if let Some(variant) = variant {
4597 Self::merge_allof_variant(merged_variant, variant, owner_name)?;
4598 }
4599 return Ok(true);
4600 }
4601 SchemaType::Reference { target } => {
4602 if !visited.insert(target.clone()) {
4603 return Ok(false);
4604 }
4605 if variant_target.is_none() {
4606 variant_target = Some(target.clone());
4607 }
4608 current = if let Some(analyzed) = self.resolved_cache.get(&target) {
4609 analyzed.schema_type.clone()
4610 } else if self.schemas.contains_key(&target) {
4611 self.analyze_schema(&target)?.schema_type
4612 } else {
4613 return Ok(false);
4614 };
4615 }
4616 SchemaType::Union { .. } | SchemaType::DiscriminatedUnion { .. } => {
4617 let Some(target) = variant_target else {
4618 return Ok(false);
4619 };
4620 Self::merge_allof_variant(
4621 merged_variant,
4622 SchemaRef {
4623 target,
4624 nullable: false,
4625 },
4626 owner_name,
4627 )?;
4628 return Ok(true);
4629 }
4630 _ => return Ok(false),
4631 }
4632 }
4633 }
4634
4635 fn merge_allof_variant(
4636 merged_variant: &mut Option<SchemaRef>,
4637 candidate: SchemaRef,
4638 owner_name: &str,
4639 ) -> Result<()> {
4640 match merged_variant {
4641 Some(existing) if existing.target == candidate.target => Ok(()),
4642 Some(existing) => Err(GeneratorError::InvalidSchema(format!(
4643 "allOf object `{owner_name}` intersects multiple union members (`{}` and `{}`), which cannot be represented by one flattened variant",
4644 existing.target, candidate.target
4645 ))),
4646 None => {
4647 *merged_variant = Some(candidate);
4648 Ok(())
4649 }
4650 }
4651 }
4652
4653 fn merge_schema_into_properties(
4654 &mut self,
4655 schema: &Schema,
4656 merged_properties: &mut BTreeMap<String, PropertyInfo>,
4657 merged_required: &mut HashSet<String>,
4658 dependencies: &mut HashSet<String>,
4659 ) -> Result<()> {
4660 let details = schema.details();
4661
4662 if let Some(properties) = &details.properties {
4664 for (prop_name, prop_schema) in properties {
4665 let prop_type = self.analyze_property_schema_with_context(
4666 prop_schema,
4667 Some(prop_name),
4668 dependencies,
4669 )?;
4670 let owner_name = self
4671 .current_schema_name
4672 .clone()
4673 .unwrap_or_else(|| "Inline".to_string());
4674 let prop_type = self.hoist_inline_property_type(
4675 &owner_name,
4676 prop_name,
4677 prop_type,
4678 dependencies,
4679 );
4680 let prop_details = prop_schema.details();
4681
4682 let nullable = self.schema_or_reference_is_nullable(prop_schema);
4689 merged_properties.insert(
4690 prop_name.clone(),
4691 PropertyInfo {
4692 schema_type: prop_type,
4693 nullable,
4694 description: prop_details.description.clone(),
4695 default: prop_details.default.clone(),
4696 serde_attrs: Vec::new(),
4697 synthesized_required: false,
4698 constraints: PropertyConstraints::from_schema_details(prop_details),
4699 },
4700 );
4701 }
4702 }
4703
4704 if let Some(required) = &details.required {
4706 for field in required {
4707 merged_required.insert(field.clone());
4708 }
4709 }
4710
4711 Ok(())
4712 }
4713
4714 fn analyze_oneof_union(
4715 &mut self,
4716 one_of_schemas: &[Schema],
4717 discriminator: Option<&crate::openapi::Discriminator>,
4718 parent_name: &str,
4719 dependencies: &mut HashSet<String>,
4720 union_kind: InlineUnionKind,
4721 source_indices: Option<&[usize]>,
4722 ) -> Result<SchemaType> {
4723 let default_indices = (0..one_of_schemas.len()).collect::<Vec<_>>();
4724 let source_indices = source_indices
4725 .filter(|indices| indices.len() == one_of_schemas.len())
4726 .unwrap_or(&default_indices);
4727
4728 let expanded_branches;
4730 let one_of_schemas = match self.expand_pointer_branches(one_of_schemas) {
4731 Some(expanded) => {
4732 expanded_branches = expanded;
4733 expanded_branches.as_slice()
4734 }
4735 None => one_of_schemas,
4736 };
4737
4738 let boolean_resolved;
4740 let boolean_resolved_indices;
4741 let one_of_schemas = match Self::resolve_boolean_branches(one_of_schemas) {
4742 Ok(resolved) => {
4743 boolean_resolved_indices = one_of_schemas
4744 .iter()
4745 .zip(source_indices.iter().copied())
4746 .filter_map(|(branch, index)| {
4747 (!matches!(branch, Schema::Bool(false))).then_some(index)
4748 })
4749 .collect::<Vec<_>>();
4750 boolean_resolved = resolved;
4751 boolean_resolved.as_slice()
4752 }
4753 Err(()) => {
4754 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema));
4755 }
4756 };
4757 let source_indices = boolean_resolved_indices.as_slice();
4758 if one_of_schemas.is_empty() {
4759 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::NeverMatches));
4760 }
4761
4762 if let [only] = one_of_schemas {
4767 return self
4768 .analyze_schema_value(only, parent_name)
4769 .map(|analyzed| analyzed.schema_type);
4770 }
4771 if let Some(shared) = self.shared_branch_type(one_of_schemas) {
4772 return Ok(shared);
4773 }
4774
4775 if one_of_schemas.len() == 2 {
4778 let null_count = one_of_schemas
4779 .iter()
4780 .filter(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
4781 .count();
4782 if null_count == 1 {
4783 if let Some(non_null) = one_of_schemas
4784 .iter()
4785 .find(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
4786 {
4787 return self
4788 .analyze_schema_value(non_null, parent_name)
4789 .map(|a| a.schema_type);
4790 }
4791 }
4792 }
4793
4794 if discriminator.is_none()
4798 || one_of_schemas
4799 .iter()
4800 .any(|schema| self.schema_or_reference_is_nullable(schema))
4801 {
4802 return self.analyze_untagged_oneof_union(
4804 one_of_schemas,
4805 parent_name,
4806 dependencies,
4807 union_kind,
4808 source_indices,
4809 None,
4810 );
4811 }
4812
4813 if one_of_schemas
4819 .iter()
4820 .any(|s| !self.branch_resolves_to_object(s))
4821 {
4822 return self.analyze_untagged_oneof_union(
4823 one_of_schemas,
4824 parent_name,
4825 dependencies,
4826 union_kind,
4827 source_indices,
4828 discriminator,
4829 );
4830 }
4831
4832 let discriminator_field = discriminator
4834 .ok_or_else(|| {
4835 GeneratorError::InvalidDiscriminator(
4836 "expected discriminator after guard check".to_string(),
4837 )
4838 })?
4839 .property_name
4840 .clone();
4841
4842 if one_of_schemas.iter().any(|branch| {
4846 self.extract_discriminator_value_domain_for_field(branch, &discriminator_field)
4847 .is_some_and(|values| values.is_empty())
4848 }) {
4849 eprintln!(
4850 "⚠️ discriminated union `{parent_name}` has a branch with contradictory `{discriminator_field}` constraints; using structural union fallback"
4851 );
4852 return self.analyze_untagged_oneof_union(
4853 one_of_schemas,
4854 parent_name,
4855 dependencies,
4856 union_kind,
4857 source_indices,
4858 discriminator,
4859 );
4860 }
4861
4862 let mut variants = Vec::new();
4863 let mut used_variant_names = std::collections::HashSet::new();
4864
4865 for (variant_schema, original_index) in
4866 one_of_schemas.iter().zip(source_indices.iter().copied())
4867 {
4868 let ref_info = if let Some(ref_str) = variant_schema.reference() {
4870 Some((ref_str, false))
4871 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
4872 Some((recursive_ref, true))
4873 } else if let Schema::AllOf { all_of, .. } = variant_schema {
4874 if all_of.len() == 1 {
4876 if let Some(ref_str) = all_of[0].reference() {
4877 Some((ref_str, false))
4878 } else {
4879 all_of[0]
4880 .recursive_reference()
4881 .map(|recursive_ref| (recursive_ref, true))
4882 }
4883 } else {
4884 None
4885 }
4886 } else {
4887 None
4888 };
4889
4890 if let Some((ref_str, is_recursive)) = ref_info {
4891 let schema_name = if is_recursive && ref_str == "#" {
4892 self.find_recursive_anchor_schema()
4894 .or_else(|| self.current_schema_name.clone())
4895 .unwrap_or_else(|| "CompoundFilter".to_string())
4896 } else {
4897 self.extract_schema_name(ref_str)
4898 .map(|s| s.to_string())
4899 .unwrap_or_else(|| "UnknownRef".to_string())
4900 };
4901
4902 if !schema_name.is_empty() {
4903 dependencies.insert(schema_name.clone());
4904
4905 let mut discriminator_values = Vec::new();
4909 let allowed_domain = self.schemas.get(&schema_name).and_then(|ref_schema| {
4910 self.extract_discriminator_value_domain_for_field(
4911 ref_schema,
4912 &discriminator_field,
4913 )
4914 });
4915 if let Some(mappings) = discriminator.and_then(|disc| disc.mapping.as_ref()) {
4916 for (key, target_ref) in mappings {
4917 if target_ref == ref_str
4918 || self.extract_schema_name(target_ref)
4919 == Some(schema_name.as_str())
4920 {
4921 if allowed_domain
4922 .as_ref()
4923 .is_some_and(|allowed| !allowed.contains(key))
4924 {
4925 let allowed = allowed_domain
4926 .as_ref()
4927 .map(|values| values.join("`, `"))
4928 .unwrap_or_default();
4929 eprintln!(
4930 "⚠️ discriminator mapping conflict in union `{parent_name}`: key `{key}` targets `{schema_name}` but branch allows `{allowed}`; ignoring mapping key"
4931 );
4932 } else {
4933 Self::push_unique_string(&mut discriminator_values, key);
4934 }
4935 }
4936 }
4937 }
4938 if let Some(allowed_domain) = allowed_domain {
4939 for value in allowed_domain {
4940 Self::push_unique_string(&mut discriminator_values, &value);
4941 }
4942 }
4943 if discriminator_values.is_empty() {
4944 discriminator_values
4945 .push(self.generate_discriminator_value_from_name(&schema_name));
4946 }
4947 let discriminator_value = discriminator_values[0].clone();
4948 let (discriminator_field_declared, discriminator_field_required) = self
4949 .schemas
4950 .get(&schema_name)
4951 .map(|schema| {
4952 self.discriminator_property_presence(schema, &discriminator_field)
4953 })
4954 .unwrap_or((false, false));
4955
4956 let base_name = self.to_rust_variant_name(&schema_name);
4958 let rust_name =
4959 self.ensure_unique_variant_name(base_name, &mut used_variant_names);
4960
4961 let final_discriminator_value = discriminator_value;
4963
4964 variants.push(UnionVariant {
4965 rust_name,
4966 type_name: schema_name,
4967 discriminator_value: final_discriminator_value,
4968 preferred_discriminator_values: discriminator_values.clone(),
4969 discriminator_values,
4970 discriminator_field_declared,
4971 discriminator_field_required,
4972 schema_ref: ref_str.to_string(),
4973 });
4974 }
4975 } else {
4976 let variant_index = original_index;
4978 let inline_type_name =
4979 self.generate_inline_type_name(variant_schema, variant_index);
4980
4981 let mut discriminator_values = Vec::new();
4984 let allowed_domain = self.extract_discriminator_value_domain_for_field(
4985 variant_schema,
4986 &discriminator_field,
4987 );
4988 if let Some(mappings) = discriminator.and_then(|disc| disc.mapping.as_ref()) {
4989 for (key, target_ref) in mappings {
4990 if target_ref.contains(&format!("variant_{variant_index}")) {
4991 if allowed_domain
4992 .as_ref()
4993 .is_some_and(|allowed| !allowed.contains(key))
4994 {
4995 let allowed = allowed_domain
4996 .as_ref()
4997 .map(|values| values.join("`, `"))
4998 .unwrap_or_default();
4999 eprintln!(
5000 "⚠️ discriminator mapping conflict in union `{parent_name}`: key `{key}` targets `{inline_type_name}` but branch allows `{allowed}`; ignoring mapping key"
5001 );
5002 } else {
5003 Self::push_unique_string(&mut discriminator_values, key);
5004 }
5005 }
5006 }
5007 }
5008 if let Some(allowed_domain) = allowed_domain {
5009 for value in allowed_domain {
5010 Self::push_unique_string(&mut discriminator_values, &value);
5011 }
5012 }
5013 if discriminator_values.is_empty() {
5014 discriminator_values.push(format!("variant_{variant_index}"));
5015 }
5016 let discriminator_value = discriminator_values[0].clone();
5017 let (discriminator_field_declared, discriminator_field_required) =
5018 self.discriminator_property_presence(variant_schema, &discriminator_field);
5019
5020 let base_name = if discriminator_value.starts_with("variant_") {
5022 format!("Variant{variant_index}")
5023 } else {
5024 let clean_name = self.discriminator_to_variant_name(&discriminator_value);
5026 self.to_rust_variant_name(&clean_name)
5027 };
5028 let rust_name = self.ensure_unique_variant_name(base_name, &mut used_variant_names);
5029
5030 let final_discriminator_value = discriminator_value;
5032
5033 let inline_type_name = self.add_inline_union_branch_schema(
5036 &inline_type_name,
5037 variant_schema,
5038 dependencies,
5039 parent_name,
5040 union_kind,
5041 original_index,
5042 Some(&final_discriminator_value),
5043 )?;
5044
5045 variants.push(UnionVariant {
5046 rust_name,
5047 type_name: inline_type_name.clone(),
5048 discriminator_value: final_discriminator_value,
5049 preferred_discriminator_values: discriminator_values.clone(),
5050 discriminator_values,
5051 discriminator_field_declared,
5052 discriminator_field_required,
5053 schema_ref: format!("inline_{variant_index}"),
5054 });
5055 }
5056 }
5057
5058 self.disambiguate_shared_discriminator_values(&mut variants, discriminator);
5059
5060 if variants.is_empty() {
5061 let mut union_variants = Vec::new();
5064
5065 for (variant_schema, original_index) in
5066 one_of_schemas.iter().zip(source_indices.iter().copied())
5067 {
5068 if let Some(ref_str) = variant_schema.reference() {
5070 if let Some(schema_name) = self.extract_schema_name(ref_str) {
5071 dependencies.insert(schema_name.to_string());
5072 union_variants.push(SchemaRef {
5073 target: schema_name.to_string(),
5074 nullable: false,
5075 });
5076 }
5077 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
5078 let schema_name = if recursive_ref == "#" {
5079 self.find_recursive_anchor_schema()
5081 .or_else(|| self.current_schema_name.clone())
5082 .unwrap_or_else(|| "CompoundFilter".to_string())
5083 } else {
5084 self.extract_schema_name(recursive_ref)
5085 .map(|s| s.to_string())
5086 .unwrap_or_else(|| "RecursiveType".to_string())
5087 };
5088 dependencies.insert(schema_name.clone());
5089 union_variants.push(SchemaRef {
5090 target: schema_name,
5091 nullable: false,
5092 });
5093 } else {
5094 let branch_discriminator = self.inline_union_branch_discriminator_value(
5095 variant_schema,
5096 discriminator,
5097 original_index,
5098 );
5099 let inline_name = self.generate_context_aware_name(
5101 parent_name,
5102 "InlineVariant",
5103 original_index,
5104 Some(variant_schema),
5105 );
5106 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
5107 let variant_type = analyzed.schema_type;
5108
5109 for dep in &analyzed.dependencies {
5111 dependencies.insert(dep.clone());
5112 }
5113
5114 match &variant_type {
5115 SchemaType::Primitive { rust_type, .. } => {
5117 union_variants.push(SchemaRef {
5118 target: rust_type.clone(),
5119 nullable: false,
5120 });
5121 }
5122 SchemaType::Array { item_type } => {
5124 match item_type.as_ref() {
5125 SchemaType::Primitive { rust_type, .. } => {
5126 let type_name = format!("Vec<{rust_type}>");
5127 union_variants.push(SchemaRef {
5128 target: type_name,
5129 nullable: false,
5130 });
5131 }
5132 SchemaType::Reference { target } => {
5133 let type_name = format!("Vec<{target}>");
5134 union_variants.push(SchemaRef {
5135 target: type_name,
5136 nullable: false,
5137 });
5138 }
5139 _ => {
5140 let inline_type_name = self.generate_context_aware_name(
5142 parent_name,
5143 "Variant",
5144 original_index,
5145 None,
5146 );
5147 let inline_type_name = self.add_inline_union_branch_schema(
5148 &inline_type_name,
5149 variant_schema,
5150 dependencies,
5151 parent_name,
5152 union_kind,
5153 original_index,
5154 branch_discriminator.as_deref(),
5155 )?;
5156 union_variants.push(SchemaRef {
5157 target: inline_type_name,
5158 nullable: false,
5159 });
5160 }
5161 }
5162 }
5163 SchemaType::Reference { target } => {
5165 union_variants.push(SchemaRef {
5166 target: target.clone(),
5167 nullable: false,
5168 });
5169 }
5170 _ => {
5172 let inline_type_name =
5173 format!("{}Variant{}", parent_name, original_index + 1);
5174 let inline_type_name = self.add_inline_union_branch_schema(
5175 &inline_type_name,
5176 variant_schema,
5177 dependencies,
5178 parent_name,
5179 union_kind,
5180 original_index,
5181 branch_discriminator.as_deref(),
5182 )?;
5183 union_variants.push(SchemaRef {
5184 target: inline_type_name,
5185 nullable: false,
5186 });
5187 }
5188 }
5189 }
5190 }
5191
5192 if !union_variants.is_empty() {
5193 return Ok(SchemaType::Union {
5194 variants: union_variants,
5195 exclusive: matches!(union_kind, InlineUnionKind::OneOf),
5196 });
5197 }
5198
5199 return Ok(self.untyped_value(
5201 self.untyped_context(""),
5202 UntypedReason::UnrepresentableUnion,
5203 ));
5204 }
5205
5206 Ok(SchemaType::DiscriminatedUnion {
5207 discriminator_field,
5208 variants,
5209 exclusive: matches!(union_kind, InlineUnionKind::OneOf),
5210 })
5211 }
5212
5213 fn analyze_untagged_oneof_union(
5214 &mut self,
5215 one_of_schemas: &[Schema],
5216 parent_name: &str,
5217 dependencies: &mut HashSet<String>,
5218 union_kind: InlineUnionKind,
5219 source_indices: &[usize],
5220 discriminator: Option<&Discriminator>,
5221 ) -> Result<SchemaType> {
5222 let filtered: Vec<(usize, &Schema)> = one_of_schemas
5227 .iter()
5228 .zip(source_indices.iter().copied())
5229 .filter_map(|(schema, original_index)| {
5230 (!schema.is_explicit_null_only()).then_some((original_index, schema))
5231 })
5232 .collect();
5233
5234 if filtered.len() == 1 {
5236 return self
5237 .analyze_schema_value(filtered[0].1, parent_name)
5238 .map(|a| a.schema_type);
5239 }
5240
5241 let exclusive_object_union = matches!(union_kind, InlineUnionKind::OneOf)
5247 && filtered
5248 .iter()
5249 .all(|(_, schema)| self.branch_resolves_to_object(schema));
5250
5251 let mut union_variants = Vec::new();
5252
5253 for (original_index, variant_schema) in filtered {
5254 if let Some(ref_str) = variant_schema.reference() {
5256 if let Some(schema_name) = self.extract_schema_name(ref_str) {
5257 dependencies.insert(schema_name.to_string());
5258 union_variants.push(SchemaRef {
5259 target: schema_name.to_string(),
5260 nullable: self.schema_or_reference_is_nullable(variant_schema),
5261 });
5262 }
5263 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
5264 let schema_name = if recursive_ref == "#" {
5265 self.find_recursive_anchor_schema()
5267 .or_else(|| self.current_schema_name.clone())
5268 .unwrap_or_else(|| "CompoundFilter".to_string())
5269 } else {
5270 self.extract_schema_name(recursive_ref)
5271 .map(|s| s.to_string())
5272 .unwrap_or_else(|| "RecursiveType".to_string())
5273 };
5274 dependencies.insert(schema_name.clone());
5275 union_variants.push(SchemaRef {
5276 target: schema_name,
5277 nullable: false,
5278 });
5279 } else {
5280 let branch_discriminator = self.inline_union_branch_discriminator_value(
5281 variant_schema,
5282 discriminator,
5283 original_index,
5284 );
5285 let inline_name = self.generate_context_aware_name(
5287 parent_name,
5288 "InlineVariant",
5289 original_index,
5290 Some(variant_schema),
5291 );
5292 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
5293 let variant_type = analyzed.schema_type;
5294
5295 for dep in &analyzed.dependencies {
5297 dependencies.insert(dep.clone());
5298 }
5299
5300 match &variant_type {
5301 SchemaType::Primitive { rust_type, .. } => {
5303 union_variants.push(SchemaRef {
5304 target: rust_type.clone(),
5305 nullable: false,
5306 });
5307 }
5308 SchemaType::Array { item_type } => {
5310 match item_type.as_ref() {
5311 SchemaType::Primitive { rust_type, .. } => {
5312 let type_name = format!("Vec<{rust_type}>");
5313 union_variants.push(SchemaRef {
5314 target: type_name,
5315 nullable: false,
5316 });
5317 }
5318 SchemaType::Reference { target } => {
5319 let type_name = format!("Vec<{target}>");
5320 union_variants.push(SchemaRef {
5321 target: type_name,
5322 nullable: false,
5323 });
5324 }
5325 SchemaType::Array {
5327 item_type: inner_item_type,
5328 } => {
5329 match inner_item_type.as_ref() {
5330 SchemaType::Primitive { rust_type, .. } => {
5331 let type_name = format!("Vec<Vec<{rust_type}>>");
5332 union_variants.push(SchemaRef {
5333 target: type_name,
5334 nullable: false,
5335 });
5336 }
5337 SchemaType::Reference { target } => {
5338 let type_name = format!("Vec<Vec<{target}>>");
5339 union_variants.push(SchemaRef {
5340 target: type_name,
5341 nullable: false,
5342 });
5343 }
5344 _ => {
5345 let inline_type_name = self.generate_context_aware_name(
5347 parent_name,
5348 "Variant",
5349 original_index,
5350 None,
5351 );
5352 let inline_type_name = self
5353 .add_inline_union_branch_schema(
5354 &inline_type_name,
5355 variant_schema,
5356 dependencies,
5357 parent_name,
5358 union_kind,
5359 original_index,
5360 branch_discriminator.as_deref(),
5361 )?;
5362 union_variants.push(SchemaRef {
5363 target: inline_type_name,
5364 nullable: false,
5365 });
5366 }
5367 }
5368 }
5369 _ => {
5370 let inline_type_name = self.generate_context_aware_name(
5372 parent_name,
5373 "Variant",
5374 original_index,
5375 None,
5376 );
5377 let inline_type_name = self.add_inline_union_branch_schema(
5378 &inline_type_name,
5379 variant_schema,
5380 dependencies,
5381 parent_name,
5382 union_kind,
5383 original_index,
5384 branch_discriminator.as_deref(),
5385 )?;
5386 union_variants.push(SchemaRef {
5387 target: inline_type_name,
5388 nullable: false,
5389 });
5390 }
5391 }
5392 }
5393 SchemaType::Reference { target } => {
5395 union_variants.push(SchemaRef {
5396 target: target.clone(),
5397 nullable: false,
5398 });
5399 }
5400 _ => {
5402 let inline_type_name = self.generate_context_aware_name(
5403 parent_name,
5404 "Variant",
5405 original_index,
5406 None,
5407 );
5408 let inline_type_name = self.add_inline_union_branch_schema(
5409 &inline_type_name,
5410 variant_schema,
5411 dependencies,
5412 parent_name,
5413 union_kind,
5414 original_index,
5415 branch_discriminator.as_deref(),
5416 )?;
5417 union_variants.push(SchemaRef {
5418 target: inline_type_name,
5419 nullable: false,
5420 });
5421 }
5422 }
5423 }
5424 }
5425
5426 if !union_variants.is_empty() {
5427 return Ok(SchemaType::Union {
5428 variants: union_variants,
5429 exclusive: exclusive_object_union,
5430 });
5431 }
5432
5433 Ok(self.untyped_value(
5435 self.untyped_context(""),
5436 UntypedReason::UnrepresentableUnion,
5437 ))
5438 }
5439
5440 fn add_inline_schema(
5441 &mut self,
5442 type_name: &str,
5443 schema: &Schema,
5444 dependencies: &mut HashSet<String>,
5445 ) -> Result<String> {
5446 let allocated_name = self.allocate_inline_schema_name(
5447 type_name,
5448 &format!("{type_name}Inline"),
5449 "inline-schema",
5450 schema,
5451 );
5452 self.add_allocated_inline_schema(allocated_name, schema, dependencies)
5453 }
5454
5455 #[allow(clippy::too_many_arguments)]
5456 fn add_inline_union_branch_schema(
5457 &mut self,
5458 type_name: &str,
5459 schema: &Schema,
5460 dependencies: &mut HashSet<String>,
5461 owner_context: &str,
5462 union_kind: InlineUnionKind,
5463 original_index: usize,
5464 discriminator: Option<&str>,
5465 ) -> Result<String> {
5466 let allocated_name = self.allocate_inline_union_branch_name(
5467 type_name,
5468 owner_context,
5469 union_kind,
5470 original_index,
5471 discriminator,
5472 schema,
5473 );
5474 let branch_name = self.add_allocated_inline_schema(allocated_name, schema, dependencies)?;
5475 self.narrow_opaque_object_branch(&branch_name);
5476 Ok(branch_name)
5477 }
5478
5479 fn narrow_opaque_object_branch(&mut self, branch_name: &str) {
5490 if let Some(cached) = self.resolved_cache.get_mut(branch_name)
5491 && let SchemaType::Untyped {
5492 shape: shape @ UntypedShape::Value,
5493 reason: UntypedReason::OpaqueObject,
5494 } = &mut cached.schema_type
5495 {
5496 *shape = UntypedShape::ValueMap;
5497 }
5498 }
5499
5500 fn add_allocated_inline_schema(
5501 &mut self,
5502 allocated_name: String,
5503 schema: &Schema,
5504 dependencies: &mut HashSet<String>,
5505 ) -> Result<String> {
5506 if let Some(schema_type) = schema.schema_type() {
5508 match schema_type {
5509 OpenApiSchemaType::String
5510 | OpenApiSchemaType::Integer
5511 | OpenApiSchemaType::Number
5512 | OpenApiSchemaType::Boolean => {
5513 let rust_type =
5514 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
5515
5516 self.resolved_cache.insert(
5518 allocated_name.clone(),
5519 AnalyzedSchema {
5520 name: allocated_name.clone(),
5521 original: serde_json::to_value(schema).unwrap_or(Value::Null),
5522 schema_type: SchemaType::Primitive {
5523 rust_type,
5524 serde_with: None,
5525 },
5526 dependencies: HashSet::new(),
5527 nullable: false,
5528 description: schema.details().description.clone(),
5529 default: None,
5530 },
5531 );
5532 return Ok(allocated_name);
5533 }
5534 _ => {}
5535 }
5536 }
5537
5538 let analyzed = self.with_schema_context(&allocated_name, |analyzer| {
5542 analyzer.analyze_schema_value(schema, &allocated_name)
5543 })?;
5544
5545 self.resolved_cache.insert(allocated_name.clone(), analyzed);
5547
5548 if let Some(cached) = self.resolved_cache.get(&allocated_name) {
5550 for dep in &cached.dependencies {
5551 dependencies.insert(dep.clone());
5552 }
5553 }
5554
5555 Ok(allocated_name)
5556 }
5557
5558 fn inline_union_branch_discriminator_value(
5559 &self,
5560 schema: &Schema,
5561 discriminator: Option<&Discriminator>,
5562 original_index: usize,
5563 ) -> Option<String> {
5564 let discriminator = discriminator?;
5565 discriminator
5566 .mapping
5567 .as_ref()
5568 .and_then(|mappings| {
5569 mappings
5570 .iter()
5571 .find(|(_, target_ref)| {
5572 target_ref.contains(&format!("variant_{original_index}"))
5573 })
5574 .map(|(key, _)| key.clone())
5575 })
5576 .or_else(|| {
5577 Some(self.extract_inline_discriminator_value(
5578 schema,
5579 &discriminator.property_name,
5580 original_index,
5581 ))
5582 })
5583 }
5584
5585 fn extract_inline_discriminator_value(
5586 &self,
5587 schema: &Schema,
5588 discriminator_field: &str,
5589 variant_index: usize,
5590 ) -> String {
5591 if let Some(properties) = &schema.details().properties {
5593 if let Some(discriminator_prop) = properties.get(discriminator_field) {
5594 if let Some(enum_values) = &discriminator_prop.details().enum_values {
5596 if enum_values.len() == 1 {
5597 if let Some(value) = enum_values[0].as_str() {
5598 return value.to_string();
5599 }
5600 }
5601 }
5602 if let Some(const_value) = discriminator_prop.details().extra.get("const") {
5604 if let Some(value) = const_value.as_str() {
5605 return value.to_string();
5606 }
5607 }
5608 if let Some(const_value) = &discriminator_prop.details().const_value {
5610 if let Some(value) = const_value.as_str() {
5611 return value.to_string();
5612 }
5613 }
5614 }
5615 }
5616
5617 if let Some(inferred_name) = self.infer_variant_name_from_structure(schema, variant_index) {
5619 return inferred_name;
5620 }
5621
5622 format!("variant_{variant_index}")
5624 }
5625
5626 fn infer_variant_name_from_structure(
5627 &self,
5628 schema: &Schema,
5629 _variant_index: usize,
5630 ) -> Option<String> {
5631 let details = schema.details();
5632
5633 if let Some(properties) = &details.properties {
5635 if properties.contains_key("text") && properties.len() <= 3 {
5637 return Some("text".to_string());
5638 }
5639 if properties.contains_key("image") || properties.contains_key("source") {
5640 return Some("image".to_string());
5641 }
5642 if properties.contains_key("document") {
5643 return Some("document".to_string());
5644 }
5645 if properties.contains_key("tool_use_id") || properties.contains_key("tool_result") {
5646 return Some("tool_result".to_string());
5647 }
5648 if properties.contains_key("content") && properties.contains_key("is_error") {
5649 return Some("tool_result".to_string());
5650 }
5651 if properties.contains_key("partial_json") {
5652 return Some("partial_json".to_string());
5653 }
5654
5655 let property_names: Vec<&String> = properties.keys().collect();
5657
5658 for prop_name in &property_names {
5660 if prop_name.contains("result") {
5661 return Some("result".to_string());
5662 }
5663 if prop_name.contains("error") {
5664 return Some("error".to_string());
5665 }
5666 if prop_name.contains("content") && property_names.len() <= 2 {
5667 return Some("content".to_string());
5668 }
5669 }
5670
5671 let significant_props = property_names
5673 .iter()
5674 .filter(|&name| !["type", "id", "cache_control"].contains(&name.as_str()))
5675 .collect::<Vec<_>>();
5676
5677 if significant_props.len() == 1 {
5678 return Some((*significant_props[0]).clone());
5679 }
5680 }
5681
5682 if let Some(description) = &details.description {
5684 let desc_lower = description.to_lowercase();
5685 if desc_lower.contains("text") && desc_lower.len() < 100 {
5686 return Some("text".to_string());
5687 }
5688 if desc_lower.contains("image") {
5689 return Some("image".to_string());
5690 }
5691 if desc_lower.contains("document") {
5692 return Some("document".to_string());
5693 }
5694 if desc_lower.contains("tool") && desc_lower.contains("result") {
5695 return Some("tool_result".to_string());
5696 }
5697 }
5698
5699 None
5700 }
5701
5702 fn discriminator_to_variant_name(&self, discriminator: &str) -> String {
5703 if discriminator.is_empty() {
5705 return "Variant".to_string();
5706 }
5707
5708 let mut result = String::new();
5709 let mut next_upper = true;
5710
5711 for c in discriminator.chars() {
5712 match c {
5713 'a'..='z' => {
5714 if next_upper {
5715 result.push(c.to_ascii_uppercase());
5716 next_upper = false;
5717 } else {
5718 result.push(c);
5719 }
5720 }
5721 'A'..='Z' => {
5722 result.push(c);
5723 next_upper = false;
5724 }
5725 '0'..='9' => {
5726 result.push(c);
5727 next_upper = false;
5728 }
5729 '_' | '-' | '.' | ' ' | '/' | '\\' => {
5730 next_upper = true;
5732 }
5733 _ => {
5734 next_upper = true;
5736 }
5737 }
5738 }
5739
5740 if result.is_empty() || result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
5742 result = format!("Variant{result}");
5743 }
5744
5745 result
5746 }
5747
5748 fn ensure_unique_variant_name(
5749 &self,
5750 base_name: String,
5751 used_names: &mut std::collections::HashSet<String>,
5752 ) -> String {
5753 let mut candidate = base_name.clone();
5754 let mut counter = 1;
5755
5756 while used_names.contains(&candidate) {
5757 counter += 1;
5758 candidate = format!("{base_name}{counter}");
5759 }
5760
5761 used_names.insert(candidate.clone());
5762 candidate
5763 }
5764
5765 fn generate_inline_type_name(&self, schema: &Schema, variant_index: usize) -> String {
5766 if let Some(meaningful_name) = self.infer_type_name_from_structure(schema) {
5768 return meaningful_name;
5769 }
5770
5771 let context = self.current_schema_name.as_deref().unwrap_or("Inline");
5773 self.generate_context_aware_name(context, "Variant", variant_index, Some(schema))
5774 }
5775
5776 fn infer_type_name_from_structure(&self, schema: &Schema) -> Option<String> {
5777 let details = schema.details();
5778
5779 if let Some(description) = &details.description {
5781 if let Some(name_from_desc) = self.extract_type_name_from_description(description) {
5782 return Some(name_from_desc);
5783 }
5784 }
5785
5786 if let Some(properties) = &details.properties {
5788 if let Some(name_from_props) = self.extract_type_name_from_properties(properties) {
5789 return Some(format!("{name_from_props}Block"));
5790 }
5791 }
5792
5793 None
5794 }
5795
5796 fn extract_type_name_from_description(&self, description: &str) -> Option<String> {
5797 if description.len() > 100 || description.contains('\n') {
5799 return None;
5800 }
5801
5802 let words: Vec<&str> = description
5804 .split_whitespace()
5805 .take(2) .filter(|word| {
5807 let w = word.to_lowercase();
5808 word.len() > 2
5809 && ![
5810 "the", "and", "for", "with", "that", "this", "are", "can", "will", "was",
5811 ]
5812 .contains(&w.as_str())
5813 })
5814 .collect();
5815
5816 if words.is_empty() {
5817 return None;
5818 }
5819
5820 let combined = words.join("_");
5822 let pascal_name = self.discriminator_to_variant_name(&combined);
5823
5824 if !pascal_name.ends_with("Content")
5826 && !pascal_name.ends_with("Block")
5827 && !pascal_name.ends_with("Type")
5828 {
5829 Some(format!("{pascal_name}Content"))
5830 } else {
5831 Some(pascal_name)
5832 }
5833 }
5834
5835 fn extract_type_name_from_properties(
5836 &self,
5837 properties: &std::collections::BTreeMap<String, crate::openapi::Schema>,
5838 ) -> Option<String> {
5839 let significant_props: Vec<&String> = properties
5841 .keys()
5842 .filter(|name| !["type", "id", "cache_control"].contains(&name.as_str()))
5843 .collect();
5844
5845 if significant_props.is_empty() {
5846 return None;
5847 }
5848
5849 if significant_props.len() == 1 {
5851 let prop_name = significant_props[0];
5852 return Some(self.discriminator_to_variant_name(prop_name));
5853 }
5854
5855 let mut sorted_props = significant_props.clone();
5858 sorted_props.sort();
5859 if let Some(first_prop) = sorted_props.first() {
5860 return Some(self.discriminator_to_variant_name(first_prop));
5861 }
5862
5863 None
5864 }
5865
5866 fn openapi_type_to_rust_type(
5867 &self,
5868 openapi_type: OpenApiSchemaType,
5869 details: &crate::openapi::SchemaDetails,
5870 ) -> String {
5871 if openapi_type == OpenApiSchemaType::Integer {
5876 self.integer_rust_type(details)
5877 } else {
5878 self.type_mapper.map(openapi_type, details).rust_type
5879 }
5880 }
5881
5882 #[allow(dead_code)]
5883 fn fallback_discriminator_value(&self, schema_name: &str) -> String {
5884 self.fallback_discriminator_value_for_field(schema_name, "type")
5885 }
5886
5887 fn fallback_discriminator_value_for_field(
5888 &self,
5889 schema_name: &str,
5890 field_name: &str,
5891 ) -> String {
5892 if let Some(ref_schema) = self.schemas.get(schema_name) {
5894 if let Some(extracted) =
5895 self.extract_discriminator_value_for_field(ref_schema, field_name)
5896 {
5897 return extracted;
5898 }
5899 }
5900
5901 self.generate_discriminator_value_from_name(schema_name)
5903 }
5904
5905 fn disambiguate_shared_discriminator_values(
5906 &self,
5907 variants: &mut [UnionVariant],
5908 discriminator: Option<&Discriminator>,
5909 ) {
5910 let mut owners_by_value: BTreeMap<String, Vec<usize>> = BTreeMap::new();
5911 for (index, variant) in variants.iter().enumerate() {
5912 for value in &variant.discriminator_values {
5913 owners_by_value
5914 .entry(value.clone())
5915 .or_default()
5916 .push(index);
5917 }
5918 }
5919
5920 let mut removals: Vec<(usize, String)> = Vec::new();
5921 for (value, candidate_owners) in owners_by_value {
5922 if candidate_owners.len() < 2 {
5923 continue;
5924 }
5925
5926 let explicitly_mapped_owners: Vec<usize> = discriminator
5927 .and_then(|disc| disc.mapping.as_ref())
5928 .and_then(|mappings| mappings.get(&value))
5929 .map(|target_ref| {
5930 candidate_owners
5931 .iter()
5932 .copied()
5933 .filter(|index| {
5934 let variant = &variants[*index];
5935 target_ref == &variant.schema_ref
5936 || self.extract_schema_name(target_ref)
5937 == Some(variant.type_name.as_str())
5938 || (variant.schema_ref.starts_with("inline_")
5939 && target_ref.contains(&variant.schema_ref))
5940 })
5941 .collect()
5942 })
5943 .unwrap_or_default();
5944
5945 let winner = if explicitly_mapped_owners.len() == 1 {
5946 explicitly_mapped_owners.first().copied()
5947 } else {
5948 let mut scored: Vec<(usize, usize)> = candidate_owners
5949 .iter()
5950 .map(|index| {
5951 (
5952 *index,
5953 Self::discriminator_name_affinity(&variants[*index].type_name, &value),
5954 )
5955 })
5956 .collect();
5957 scored.sort_by(|left, right| right.1.cmp(&left.1).then(left.0.cmp(&right.0)));
5958 match scored.as_slice() {
5959 [(index, score), rest @ ..]
5960 if *score > 0 && rest.first().is_none_or(|(_, next)| score > next) =>
5961 {
5962 Some(*index)
5963 }
5964 _ => None,
5965 }
5966 };
5967
5968 if let Some(winner) = winner {
5969 for index in candidate_owners {
5970 if index != winner && variants[index].preferred_discriminator_values.len() > 1 {
5971 removals.push((index, value.clone()));
5972 }
5973 }
5974 }
5975 }
5976
5977 for (index, value) in removals {
5978 variants[index]
5979 .preferred_discriminator_values
5980 .retain(|candidate| candidate != &value);
5981 }
5982 for variant in variants {
5983 if let Some(canonical) = variant.preferred_discriminator_values.first() {
5984 variant.discriminator_value.clone_from(canonical);
5985 }
5986 }
5987 }
5988
5989 fn discriminator_name_affinity(schema_name: &str, value: &str) -> usize {
5990 let schema_compact: String = schema_name
5991 .chars()
5992 .filter(|character| character.is_ascii_alphanumeric())
5993 .flat_map(char::to_lowercase)
5994 .collect();
5995 let value_compact: String = value
5996 .chars()
5997 .filter(|character| character.is_ascii_alphanumeric())
5998 .flat_map(char::to_lowercase)
5999 .collect();
6000 let exact_bonus = usize::from(
6001 !value_compact.is_empty() && schema_compact.contains(value_compact.as_str()),
6002 ) * 10;
6003 let token_matches = value
6004 .split(|character: char| !character.is_ascii_alphanumeric())
6005 .filter(|token| !token.is_empty())
6006 .filter(|token| schema_compact.contains(&token.to_ascii_lowercase()))
6007 .count();
6008 exact_bonus + token_matches
6009 }
6010
6011 fn generate_discriminator_value_from_name(&self, schema_name: &str) -> String {
6012 let mut result = String::new();
6014 let mut chars = schema_name.chars().peekable();
6015 let mut first = true;
6016
6017 while let Some(c) = chars.next() {
6018 if c.is_uppercase()
6019 && !first
6020 && chars
6021 .peek()
6022 .map(|&next| next.is_lowercase())
6023 .unwrap_or(false)
6024 {
6025 result.push('.');
6026 }
6027 result.push(c.to_ascii_lowercase());
6028 first = false;
6029 }
6030
6031 if result.ends_with("event") {
6033 result = result[..result.len() - 5].to_string();
6034 }
6035
6036 if schema_name.starts_with("Response") && !result.starts_with("response.") {
6038 result = format!("response.{}", result.trim_start_matches("response"));
6039 }
6040
6041 result
6042 }
6043
6044 fn to_rust_variant_name(&self, schema_name: &str) -> String {
6045 let mut name = schema_name;
6047
6048 if name.starts_with("Response") && name.len() > 8 {
6050 name = &name[8..]; }
6052
6053 if name.ends_with("Event") && name.len() > 5 {
6055 name = &name[..name.len() - 5]; }
6057
6058 name = name.trim_matches('_');
6060
6061 if name.is_empty() {
6063 schema_name.to_string()
6064 } else {
6065 self.discriminator_to_variant_name(name)
6067 }
6068 }
6069
6070 fn hoist_inline_string_enum(
6094 &mut self,
6095 schema: &Schema,
6096 enum_values: Vec<String>,
6097 primary_name: String,
6098 dependencies: &mut HashSet<String>,
6099 ) -> SchemaType {
6100 let suffix = enum_values
6101 .first()
6102 .map(|value| self.to_pascal_case(value))
6103 .unwrap_or_else(|| "Variant".to_string());
6104 let collision_name = format!("{primary_name}{suffix}");
6105 let enum_type_name =
6106 self.allocate_inline_schema_name(&primary_name, &collision_name, "string-enum", schema);
6107 let should_insert = !self.resolved_cache.contains_key(&enum_type_name);
6108
6109 if should_insert {
6112 self.resolved_cache.insert(
6113 enum_type_name.clone(),
6114 AnalyzedSchema {
6115 name: enum_type_name.clone(),
6116 original: serde_json::to_value(schema).unwrap_or(Value::Null),
6117 schema_type: SchemaType::StringEnum {
6118 values: enum_values,
6119 },
6120 dependencies: HashSet::new(),
6121 nullable: false,
6122 description: schema.details().description.clone(),
6123 default: schema.details().default.clone(),
6124 },
6125 );
6126 }
6127
6128 dependencies.insert(enum_type_name.clone());
6130 SchemaType::Reference {
6131 target: enum_type_name,
6132 }
6133 }
6134
6135 fn analyze_array_schema(
6136 &mut self,
6137 schema: &Schema,
6138 parent_schema_name: &str,
6139 dependencies: &mut HashSet<String>,
6140 ) -> Result<SchemaType> {
6141 let details = schema.details();
6142
6143 if let Some(positions) = details.positional_items() {
6147 return self.analyze_positional_items(
6148 positions,
6149 details,
6150 parent_schema_name,
6151 dependencies,
6152 );
6153 }
6154
6155 if let Some(items_schema) = details.item_schema() {
6157 let item_type = self.analyze_item_schema(
6158 items_schema,
6159 parent_schema_name,
6160 &format!("{parent_schema_name}Item"),
6161 dependencies,
6162 )?;
6163 let item_type = self.hoist_inline_property_type(
6164 parent_schema_name,
6165 "Item",
6166 item_type,
6167 dependencies,
6168 );
6169 Ok(SchemaType::Array {
6170 item_type: Box::new(item_type),
6171 })
6172 } else {
6173 Ok(
6175 self.untyped_value_array(
6176 self.untyped_context(""),
6177 UntypedReason::ArrayWithoutItems,
6178 ),
6179 )
6180 }
6181 }
6182
6183 fn shared_branch_type(&self, branches: &[Schema]) -> Option<SchemaType> {
6192 let mut mapped: Option<(String, Option<String>)> = None;
6193 let mut scalar_kind: Option<OpenApiSchemaType> = None;
6194 let mut formats_agree = true;
6195 for branch in branches {
6196 if branch.reference().is_some() {
6197 return None;
6198 }
6199 let details = branch.details();
6200 if details.enum_values.is_some()
6201 || details.const_value.is_some()
6202 || details.properties.is_some()
6203 {
6204 return None;
6205 }
6206 let scalar = match branch.schema_type()? {
6207 scalar @ (OpenApiSchemaType::String
6208 | OpenApiSchemaType::Integer
6209 | OpenApiSchemaType::Number
6210 | OpenApiSchemaType::Boolean) => scalar.clone(),
6211 _ => return None,
6212 };
6213 match &scalar_kind {
6214 Some(existing) if *existing != scalar => return None,
6215 Some(_) => {}
6216 None => scalar_kind = Some(scalar.clone()),
6217 }
6218
6219 let candidate = self.type_mapper.map(scalar, details);
6220 let candidate = (candidate.rust_type, candidate.serde_with);
6221 match &mapped {
6222 Some(existing) if *existing != candidate => formats_agree = false,
6223 Some(_) => {}
6224 None => mapped = Some(candidate),
6225 }
6226 }
6227
6228 if formats_agree {
6229 return mapped.map(|(rust_type, serde_with)| SchemaType::Primitive {
6230 rust_type,
6231 serde_with,
6232 });
6233 }
6234
6235 let scalar = scalar_kind?;
6241 let mapped = self
6242 .type_mapper
6243 .map(scalar, &crate::openapi::SchemaDetails::default());
6244 Some(SchemaType::Primitive {
6245 rust_type: mapped.rust_type,
6246 serde_with: mapped.serde_with,
6247 })
6248 }
6249
6250 fn expand_pointer_branches(&self, branches: &[Schema]) -> Option<Vec<Schema>> {
6259 let mut expanded = Vec::with_capacity(branches.len());
6260 let mut changed = false;
6261 for branch in branches {
6262 let resolved = branch
6263 .reference()
6264 .filter(|reference| self.extract_schema_name(reference).is_none())
6265 .and_then(|reference| reference.strip_prefix('#'))
6266 .filter(|pointer| pointer.starts_with('/'))
6267 .and_then(|pointer| self.openapi_spec.pointer(pointer))
6268 .and_then(|value| Schema::deserialize(value).ok())
6269 .filter(|schema| schema.reference().is_none());
6270 match resolved {
6271 Some(schema) => {
6272 expanded.push(schema);
6273 changed = true;
6274 }
6275 None => expanded.push(branch.clone()),
6276 }
6277 }
6278 changed.then_some(expanded)
6279 }
6280
6281 fn analyze_object_with_variants(
6294 &mut self,
6295 schema: &Schema,
6296 branches: &[Schema],
6297 schema_name: &str,
6298 dependencies: &mut HashSet<String>,
6299 ) -> Result<Option<SchemaType>> {
6300 let details = schema.details();
6301 if details.properties.as_ref().is_none_or(BTreeMap::is_empty) || branches.is_empty() {
6302 return Ok(None);
6303 }
6304 if schema.is_nullable_pattern() || Self::union_only_constrains_requiredness(branches) {
6307 return Ok(None);
6308 }
6309
6310 let base = self.analyze_object_schema(schema, dependencies)?;
6311 let SchemaType::Object {
6312 properties,
6313 required,
6314 additional_properties,
6315 ..
6316 } = base
6317 else {
6318 return Ok(None);
6319 };
6320
6321 let preferred_variant_name = format!("{schema_name}Variant");
6322 let variant_name = self.allocate_inline_schema_name(
6323 &preferred_variant_name,
6324 &format!("{preferred_variant_name}Inline"),
6325 "object-variant",
6326 schema,
6327 );
6328 let variant_type = self.analyze_anyof_union(
6329 branches,
6330 schema.discriminator(),
6331 dependencies,
6332 &variant_name,
6333 )?;
6334 if matches!(variant_type, SchemaType::Untyped { .. }) {
6337 return Ok(Some(SchemaType::Object {
6338 properties,
6339 required,
6340 additional_properties,
6341 variant: None,
6342 }));
6343 }
6344
6345 self.resolved_cache.insert(
6346 variant_name.clone(),
6347 AnalyzedSchema {
6348 name: variant_name.clone(),
6349 original: Value::Null,
6350 dependencies: schema_type_dependencies(&variant_type),
6351 schema_type: variant_type,
6352 nullable: false,
6353 description: None,
6354 default: None,
6355 },
6356 );
6357 dependencies.insert(variant_name.clone());
6358
6359 Ok(Some(SchemaType::Object {
6360 properties,
6361 required,
6362 additional_properties,
6363 variant: Some(SchemaRef {
6364 target: variant_name,
6365 nullable: false,
6366 }),
6367 }))
6368 }
6369
6370 #[allow(clippy::result_unit_err)]
6378 fn resolve_boolean_branches(branches: &[Schema]) -> std::result::Result<Vec<Schema>, ()> {
6379 if branches
6380 .iter()
6381 .any(|branch| matches!(branch, Schema::Bool(true)))
6382 {
6383 return Err(());
6384 }
6385 Ok(branches
6386 .iter()
6387 .filter(|branch| !matches!(branch, Schema::Bool(false)))
6388 .cloned()
6389 .collect())
6390 }
6391
6392 fn union_only_constrains_requiredness(branches: &[Schema]) -> bool {
6401 !branches.is_empty()
6402 && branches
6403 .iter()
6404 .all(Self::schema_only_constrains_requiredness)
6405 }
6406
6407 fn schema_only_constrains_requiredness(schema: &Schema) -> bool {
6415 let keys_are_requiredness_or_annotations = serde_json::to_value(schema)
6416 .ok()
6417 .and_then(|value| value.as_object().cloned())
6418 .is_some_and(|object| {
6419 object.iter().all(|(key, value)| {
6420 (key == "type" && value == "object")
6421 || matches!(
6422 key.as_str(),
6423 "required"
6424 | "not"
6425 | "anyOf"
6426 | "oneOf"
6427 | "title"
6428 | "description"
6429 | "deprecated"
6430 | "readOnly"
6431 | "writeOnly"
6432 | "examples"
6433 | "example"
6434 | "default"
6435 | "externalDocs"
6436 | "xml"
6437 | "$comment"
6438 )
6439 || key.starts_with("x-")
6440 })
6441 });
6442 if !keys_are_requiredness_or_annotations {
6443 return false;
6444 }
6445
6446 match schema {
6447 Schema::AnyOf { any_of, .. } => {
6448 !any_of.is_empty() && any_of.iter().all(Self::schema_only_constrains_requiredness)
6449 }
6450 Schema::OneOf { one_of, .. } => {
6451 !one_of.is_empty() && one_of.iter().all(Self::schema_only_constrains_requiredness)
6452 }
6453 other => {
6454 let details = other.details();
6455 details
6456 .required
6457 .as_ref()
6458 .is_some_and(|required| !required.is_empty())
6459 || details
6460 .not
6461 .as_deref()
6462 .is_some_and(Self::schema_only_constrains_requiredness)
6463 }
6464 }
6465 }
6466
6467 fn analyze_empty_union(
6474 &mut self,
6475 schema: &Schema,
6476 dependencies: &mut HashSet<String>,
6477 ) -> Result<SchemaType> {
6478 let Some(declared) = schema
6482 .declared_type()
6483 .cloned()
6484 .or_else(|| schema.inferred_type())
6485 .or_else(|| {
6486 schema
6487 .details()
6488 .properties
6489 .is_some()
6490 .then_some(OpenApiSchemaType::Object)
6491 })
6492 else {
6493 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema));
6494 };
6495 match declared {
6496 OpenApiSchemaType::Object => self.analyze_object_schema(schema, dependencies),
6497 OpenApiSchemaType::Array => {
6498 let context = self
6499 .current_schema_name
6500 .clone()
6501 .unwrap_or_else(|| "Inline".to_string());
6502 self.analyze_array_schema(schema, &context, dependencies)
6503 }
6504 scalar => {
6505 let mapped = self.type_mapper.map(scalar, schema.details());
6506 Ok(SchemaType::Primitive {
6507 rust_type: mapped.rust_type,
6508 serde_with: mapped.serde_with,
6509 })
6510 }
6511 }
6512 }
6513
6514 fn resolve_pointer_schema(
6527 &mut self,
6528 reference: &str,
6529 dependencies: &mut HashSet<String>,
6530 ) -> Result<Option<SchemaType>> {
6531 let Some(pointer) = reference.strip_prefix('#') else {
6532 return Ok(None);
6533 };
6534 if pointer.is_empty() || !pointer.starts_with('/') {
6535 return Ok(None);
6536 }
6537 let preferred_name = pointer_type_name(pointer);
6538 if preferred_name.is_empty() {
6539 return Ok(None);
6540 }
6541 let name = self.allocate_pointer_schema_name(pointer, &preferred_name);
6542 if self.resolved_cache.contains_key(&name) || !self.resolving_pointers.insert(name.clone())
6545 {
6546 dependencies.insert(name.clone());
6547 return Ok(Some(SchemaType::Reference { target: name }));
6548 }
6549
6550 let resolved = (|| {
6551 let value = self.openapi_spec.pointer(pointer)?.clone();
6552 Schema::deserialize(&value).ok()
6553 })();
6554 let Some(schema) = resolved else {
6555 self.resolving_pointers.remove(&name);
6556 return Ok(None);
6557 };
6558
6559 let saved_context = self.current_schema_name.clone();
6564 self.current_schema_name = Some(name.clone());
6565 let analyzed = self.analyze_schema_value(&schema, &name);
6566 self.current_schema_name = saved_context;
6567 self.resolving_pointers.remove(&name);
6568 let analyzed = analyzed?;
6569 dependencies.extend(analyzed.dependencies.iter().cloned());
6570 if analyzed.schema_type.renders_inline() {
6571 return Ok(Some(analyzed.schema_type));
6572 }
6573
6574 self.resolved_cache.insert(name.clone(), analyzed);
6575 dependencies.insert(name.clone());
6576 Ok(Some(SchemaType::Reference { target: name }))
6577 }
6578
6579 fn hoist_inline_property_type(
6588 &mut self,
6589 schema_name: &str,
6590 property_name: &str,
6591 schema_type: SchemaType,
6592 dependencies: &mut HashSet<String>,
6593 ) -> SchemaType {
6594 if schema_type.renders_inline() {
6595 return schema_type;
6596 }
6597
6598 use heck::ToPascalCase;
6599
6600 let preferred_name = format!("{schema_name}{}", property_name.to_pascal_case());
6601 let hoisted_name = self.allocate_synthetic_schema_name(
6602 &preferred_name,
6603 &format!("{preferred_name}Inline"),
6604 "hoisted-property",
6605 format!("{schema_type:?}"),
6606 );
6607 let hoisted_dependencies = schema_type_dependencies(&schema_type);
6608 self.resolved_cache.insert(
6609 hoisted_name.clone(),
6610 AnalyzedSchema {
6611 name: hoisted_name.clone(),
6612 original: Value::Null,
6613 schema_type,
6614 dependencies: hoisted_dependencies,
6615 nullable: false,
6616 description: None,
6617 default: None,
6618 },
6619 );
6620 dependencies.insert(hoisted_name.clone());
6621 SchemaType::Reference {
6622 target: hoisted_name,
6623 }
6624 }
6625
6626 fn analyze_positional_items(
6639 &mut self,
6640 positions: &[Schema],
6641 details: &crate::openapi::SchemaDetails,
6642 parent_schema_name: &str,
6643 dependencies: &mut HashSet<String>,
6644 ) -> Result<SchemaType> {
6645 if details.positional_items_are_exact() && !positions.is_empty() {
6646 let mut element_types = Vec::with_capacity(positions.len());
6647 for (index, position) in positions.iter().enumerate() {
6648 let element_type = self.analyze_item_schema(
6649 position,
6650 parent_schema_name,
6651 &format!("{parent_schema_name}Item{}", index + 1),
6652 dependencies,
6653 )?;
6654 element_types.push(self.hoist_inline_property_type(
6655 parent_schema_name,
6656 &format!("Item{}", index + 1),
6657 element_type,
6658 dependencies,
6659 ));
6660 }
6661 return Ok(SchemaType::Tuple { element_types });
6662 }
6663
6664 if details.positional_items_are_closed()
6668 && let Some(shared) = shared_positional_schema(positions)
6669 {
6670 let item_type = self.analyze_item_schema(
6671 shared,
6672 parent_schema_name,
6673 &format!("{parent_schema_name}Item"),
6674 dependencies,
6675 )?;
6676 return Ok(SchemaType::Array {
6677 item_type: Box::new(item_type),
6678 });
6679 }
6680
6681 Ok(self.untyped_value_array(self.untyped_context(""), UntypedReason::OpenPositionalItems))
6682 }
6683
6684 fn analyze_item_schema(
6691 &mut self,
6692 items_schema: &Schema,
6693 parent_schema_name: &str,
6694 inline_name: &str,
6695 dependencies: &mut HashSet<String>,
6696 ) -> Result<SchemaType> {
6697 let item_type = match items_schema {
6698 Schema::Bool(accepts_anything) => self.untyped_value(
6699 self.untyped_context(""),
6700 if *accepts_anything {
6701 UntypedReason::AnySchema
6702 } else {
6703 UntypedReason::NeverMatches
6704 },
6705 ),
6706 Schema::Reference { reference, .. } => {
6707 if let Some(target) = self.extract_schema_name(reference) {
6709 let target = target.to_string();
6710 dependencies.insert(target.clone());
6711 SchemaType::Reference { target }
6712 } else {
6713 self.resolve_pointer_schema(reference, dependencies)?
6714 .ok_or_else(|| GeneratorError::UnresolvedReference(reference.to_string()))?
6715 }
6716 }
6717 Schema::RecursiveRef { recursive_ref, .. } => {
6718 if recursive_ref == "#" {
6720 let target = self
6722 .find_recursive_anchor_schema()
6723 .unwrap_or_else(|| parent_schema_name.to_string());
6724 dependencies.insert(target.clone());
6725 SchemaType::Reference { target }
6726 } else {
6727 let target = self
6728 .extract_schema_name(recursive_ref)
6729 .unwrap_or("RecursiveType")
6730 .to_string();
6731 dependencies.insert(target.clone());
6732 SchemaType::Reference { target }
6733 }
6734 }
6735 Schema::Typed { schema_type, .. } => {
6736 match schema_type {
6738 OpenApiSchemaType::String => {
6739 match items_schema
6743 .details()
6744 .string_enum_values()
6745 .filter(|values| !values.is_empty())
6746 {
6747 Some(values) => self.hoist_inline_string_enum(
6748 items_schema,
6749 values,
6750 inline_name.to_string(),
6751 dependencies,
6752 ),
6753 None => SchemaType::Primitive {
6754 rust_type: "String".to_string(),
6755 serde_with: None,
6756 },
6757 }
6758 }
6759 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
6760 let details = items_schema.details();
6761 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
6762 SchemaType::Primitive {
6763 rust_type,
6764 serde_with: None,
6765 }
6766 }
6767 OpenApiSchemaType::Boolean => SchemaType::Primitive {
6768 rust_type: "bool".to_string(),
6769 serde_with: None,
6770 },
6771 OpenApiSchemaType::Object => {
6772 let preferred_object_type_name = inline_name.to_string();
6774 let object_type_name = self.allocate_inline_schema_name(
6775 &preferred_object_type_name,
6776 &format!("{preferred_object_type_name}Inline"),
6777 "array-item-object",
6778 items_schema,
6779 );
6780
6781 self.add_allocated_object_schema(
6782 object_type_name,
6783 items_schema,
6784 dependencies,
6785 )?
6786 }
6787 OpenApiSchemaType::Array => {
6788 self.analyze_array_schema(items_schema, parent_schema_name, dependencies)?
6790 }
6791 _ => self.untyped_value(
6792 self.untyped_context(""),
6793 UntypedReason::UnsupportedTypeKeyword,
6794 ),
6795 }
6796 }
6797 Schema::OneOf { .. } | Schema::AnyOf { .. } => {
6798 let analyzed = self.analyze_schema_value(items_schema, "ArrayItem")?;
6800
6801 match &analyzed.schema_type {
6803 SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => {
6804 let preferred_union_name = format!("{inline_name}Union");
6807 let union_name = self.allocate_inline_schema_name(
6808 &preferred_union_name,
6809 &format!("{preferred_union_name}Inline"),
6810 "array-item-union",
6811 items_schema,
6812 );
6813
6814 let mut union_schema = analyzed;
6816 union_schema.name = union_name.clone();
6817
6818 self.resolved_cache.insert(union_name.clone(), union_schema);
6820
6821 dependencies.insert(union_name.clone());
6823
6824 SchemaType::Reference { target: union_name }
6826 }
6827 _ => analyzed.schema_type,
6828 }
6829 }
6830 Schema::Untyped { .. } => {
6831 if let Some(inferred) = items_schema.inferred_type() {
6833 match inferred {
6834 OpenApiSchemaType::Object => {
6835 let preferred_object_type_name = inline_name.to_string();
6837 let object_type_name = self.allocate_inline_schema_name(
6838 &preferred_object_type_name,
6839 &format!("{preferred_object_type_name}Inline"),
6840 "array-item-object",
6841 items_schema,
6842 );
6843
6844 self.add_allocated_object_schema(
6845 object_type_name,
6846 items_schema,
6847 dependencies,
6848 )?
6849 }
6850 OpenApiSchemaType::String => {
6851 match items_schema
6854 .details()
6855 .string_enum_values()
6856 .filter(|values| !values.is_empty())
6857 {
6858 Some(values) => self.hoist_inline_string_enum(
6859 items_schema,
6860 values,
6861 inline_name.to_string(),
6862 dependencies,
6863 ),
6864 None => SchemaType::Primitive {
6865 rust_type: "String".to_string(),
6866 serde_with: None,
6867 },
6868 }
6869 }
6870 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
6871 let details = items_schema.details();
6872 let rust_type = self.get_number_rust_type(inferred, details);
6873 SchemaType::Primitive {
6874 rust_type,
6875 serde_with: None,
6876 }
6877 }
6878 OpenApiSchemaType::Boolean => SchemaType::Primitive {
6879 rust_type: "bool".to_string(),
6880 serde_with: None,
6881 },
6882 OpenApiSchemaType::Null => SchemaType::Primitive {
6885 rust_type: self.type_mapper.null_unit().rust_type,
6886 serde_with: None,
6887 },
6888 _ => self.untyped_value(
6889 self.untyped_context(""),
6890 UntypedReason::UnsupportedTypeKeyword,
6891 ),
6892 }
6893 } else {
6894 self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema)
6895 }
6896 }
6897 _ => self.analyze_property_schema_with_context(items_schema, None, dependencies)?,
6902 };
6903
6904 Ok(self.nullable_container_value(items_schema, item_type))
6905 }
6906
6907 fn get_number_rust_type(
6908 &self,
6909 schema_type: OpenApiSchemaType,
6910 details: &crate::openapi::SchemaDetails,
6911 ) -> String {
6912 let format = details.format.as_deref();
6916 match schema_type {
6917 OpenApiSchemaType::Integer => self.integer_rust_type(details),
6918 OpenApiSchemaType::Number => self.type_mapper.number_format(format).rust_type,
6919 _ => self.type_mapper.dynamic_json().rust_type,
6920 }
6921 }
6922
6923 fn integer_rust_type(&self, details: &crate::openapi::SchemaDetails) -> String {
6929 fn integer_value(number: &serde_json::Number) -> Option<i128> {
6930 number
6931 .as_i64()
6932 .map(i128::from)
6933 .or_else(|| number.as_u64().map(i128::from))
6934 .or_else(|| {
6935 number.as_f64().and_then(|value| {
6936 (value.fract() == 0.0
6937 && value >= i128::MIN as f64
6938 && value <= i128::MAX as f64)
6939 .then_some(value as i128)
6940 })
6941 })
6942 }
6943
6944 fn below(number: &serde_json::Number, boundary: i128) -> bool {
6945 integer_value(number).is_some_and(|value| value < boundary)
6946 }
6947
6948 fn above(number: &serde_json::Number, boundary: i128) -> bool {
6949 integer_value(number).is_some_and(|value| value > boundary)
6950 }
6951
6952 let explicitly_nonnegative = details
6953 .minimum
6954 .as_ref()
6955 .and_then(integer_value)
6956 .is_some_and(|value| value >= 0)
6957 || matches!(
6958 details.exclusive_minimum,
6959 Some(crate::openapi::ExclusiveBound::Number(value)) if value >= 0.0
6960 );
6961
6962 let annotated_numbers = details
6963 .const_value
6964 .iter()
6965 .chain(details.default.iter())
6966 .chain(details.example.iter())
6967 .chain(details.enum_values.iter().flatten())
6968 .chain(details.examples.iter().flatten())
6969 .filter_map(Value::as_number)
6970 .collect::<Vec<_>>();
6971 let below_i32 = details
6972 .minimum
6973 .as_ref()
6974 .is_some_and(|number| below(number, i128::from(i32::MIN)))
6975 || annotated_numbers
6976 .iter()
6977 .any(|number| below(number, i128::from(i32::MIN)))
6978 || matches!(
6979 details.exclusive_minimum,
6980 Some(crate::openapi::ExclusiveBound::Number(value)) if value < i32::MIN as f64
6981 );
6982 let above_i32 = details
6983 .maximum
6984 .as_ref()
6985 .is_some_and(|number| above(number, i128::from(i32::MAX)))
6986 || annotated_numbers
6987 .iter()
6988 .any(|number| above(number, i128::from(i32::MAX)))
6989 || matches!(
6990 details.exclusive_maximum,
6991 Some(crate::openapi::ExclusiveBound::Number(value)) if value > i32::MAX as f64
6992 );
6993 let below_i64 = details
6994 .minimum
6995 .as_ref()
6996 .is_some_and(|number| below(number, i128::from(i64::MIN)))
6997 || annotated_numbers
6998 .iter()
6999 .any(|number| below(number, i128::from(i64::MIN)))
7000 || matches!(
7001 details.exclusive_minimum,
7002 Some(crate::openapi::ExclusiveBound::Number(value)) if value < i64::MIN as f64
7003 );
7004 let above_i64 = details
7005 .maximum
7006 .as_ref()
7007 .is_some_and(|number| above(number, i128::from(i64::MAX)))
7008 || annotated_numbers
7009 .iter()
7010 .any(|number| above(number, i128::from(i64::MAX)))
7011 || matches!(
7012 details.exclusive_maximum,
7013 Some(crate::openapi::ExclusiveBound::Number(value)) if value >= 9_223_372_036_854_775_808.0
7014 );
7015 let below_zero = details
7016 .minimum
7017 .as_ref()
7018 .and_then(integer_value)
7019 .is_some_and(|value| value < 0)
7020 || annotated_numbers
7021 .iter()
7022 .filter_map(|number| integer_value(number))
7023 .any(|value| value < 0)
7024 || matches!(
7025 details.exclusive_minimum,
7026 Some(crate::openapi::ExclusiveBound::Number(value)) if value < 0.0
7027 );
7028 let above_u32 = details
7029 .maximum
7030 .as_ref()
7031 .is_some_and(|number| above(number, i128::from(u32::MAX)))
7032 || annotated_numbers
7033 .iter()
7034 .any(|number| above(number, i128::from(u32::MAX)))
7035 || matches!(
7036 details.exclusive_maximum,
7037 Some(crate::openapi::ExclusiveBound::Number(value)) if value > u32::MAX as f64
7038 );
7039
7040 let configured = self
7041 .type_mapper
7042 .integer_format(details.format.as_deref())
7043 .rust_type;
7044 match configured.as_str() {
7045 "i32" if below_i32 || above_i32 => {
7046 if below_i64 || above_i64 {
7047 "i128".to_string()
7048 } else {
7049 "i64".to_string()
7050 }
7051 }
7052 "i64" if below_i64 => "i128".to_string(),
7053 "i64" if above_i64 && explicitly_nonnegative => "u64".to_string(),
7054 "i64" if above_i64 => "i128".to_string(),
7055 "u32" if below_zero => {
7056 if below_i64 || above_i64 {
7057 "i128".to_string()
7058 } else {
7059 "i64".to_string()
7060 }
7061 }
7062 "u32" if above_u32 => "u64".to_string(),
7063 "u64" if below_zero => "i128".to_string(),
7064 configured => configured.to_string(),
7065 }
7066 }
7067
7068 fn analyze_anyof_union(
7069 &mut self,
7070 any_of_schemas: &[Schema],
7071 discriminator: Option<&Discriminator>,
7072 dependencies: &mut HashSet<String>,
7073 context_name: &str,
7074 ) -> Result<SchemaType> {
7075 let original_indices = (0..any_of_schemas.len()).collect::<Vec<_>>();
7076
7077 let expanded_branches;
7079 let any_of_schemas = match self.expand_pointer_branches(any_of_schemas) {
7080 Some(expanded) => {
7081 expanded_branches = expanded;
7082 expanded_branches.as_slice()
7083 }
7084 None => any_of_schemas,
7085 };
7086
7087 let boolean_resolved;
7089 let boolean_resolved_indices;
7090 let any_of_schemas = match Self::resolve_boolean_branches(any_of_schemas) {
7091 Ok(resolved) => {
7092 boolean_resolved_indices = any_of_schemas
7093 .iter()
7094 .zip(original_indices.iter().copied())
7095 .filter_map(|(branch, index)| {
7096 (!matches!(branch, Schema::Bool(false))).then_some(index)
7097 })
7098 .collect::<Vec<_>>();
7099 boolean_resolved = resolved;
7100 boolean_resolved.as_slice()
7101 }
7102 Err(()) => {
7103 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema));
7104 }
7105 };
7106 let source_indices = boolean_resolved_indices.as_slice();
7107 if any_of_schemas.is_empty() {
7108 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::NeverMatches));
7109 }
7110
7111 let filtered_owned: Vec<Schema>;
7117 let filtered_indices: Vec<usize>;
7118 let (any_of_schemas, source_indices): (&[Schema], &[usize]) = if any_of_schemas
7119 .iter()
7120 .any(Schema::is_explicit_null_only)
7121 {
7122 filtered_owned = any_of_schemas
7123 .iter()
7124 .filter(|s| !s.is_explicit_null_only())
7125 .cloned()
7126 .collect();
7127 filtered_indices = any_of_schemas
7128 .iter()
7129 .zip(source_indices.iter().copied())
7130 .filter_map(|(schema, index)| (!schema.is_explicit_null_only()).then_some(index))
7131 .collect();
7132 if filtered_owned.is_empty() {
7133 return Ok(self.untyped_value(self.untyped_context(""), UntypedReason::AnySchema));
7134 }
7135 if filtered_owned.len() == 1 {
7136 return self
7137 .analyze_schema_value(&filtered_owned[0], context_name)
7138 .map(|a| a.schema_type);
7139 }
7140 (&filtered_owned, &filtered_indices)
7141 } else {
7142 (any_of_schemas, source_indices)
7143 };
7144
7145 if let [only] = any_of_schemas {
7148 return self
7149 .analyze_schema_value(only, context_name)
7150 .map(|analyzed| analyzed.schema_type);
7151 }
7152
7153 if let Some(shared) = self.shared_branch_type(any_of_schemas) {
7157 return Ok(shared);
7158 }
7159
7160 let has_refs = any_of_schemas.iter().any(|s| s.is_reference());
7162 let has_objects = any_of_schemas.iter().any(|s| {
7163 matches!(s.schema_type(), Some(OpenApiSchemaType::Object))
7164 || s.inferred_type() == Some(OpenApiSchemaType::Object)
7165 });
7166 let has_arrays = any_of_schemas
7167 .iter()
7168 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Array)));
7169
7170 let all_string_like = any_of_schemas.iter().all(|s| {
7173 matches!(s.schema_type(), Some(OpenApiSchemaType::String))
7174 || s.details().const_value.is_some()
7175 });
7176
7177 if (has_refs || has_objects || has_arrays || any_of_schemas.len() > 1) && !all_string_like {
7178 if let Some(disc) = discriminator {
7180 return self.analyze_oneof_union(
7182 any_of_schemas,
7183 Some(disc),
7184 context_name,
7185 dependencies,
7186 InlineUnionKind::AnyOf,
7187 Some(source_indices),
7188 );
7189 }
7190
7191 if let Some(disc_field) = self.detect_discriminator_field(any_of_schemas) {
7193 return self.analyze_oneof_union(
7194 any_of_schemas,
7195 Some(&Discriminator {
7196 property_name: disc_field,
7197 mapping: None,
7198 default_mapping: None,
7199 extensions: crate::extensions::Extensions::default(),
7200 }),
7201 context_name,
7202 dependencies,
7203 InlineUnionKind::AnyOf,
7204 Some(source_indices),
7205 );
7206 }
7207
7208 let mut variants = Vec::new();
7210
7211 for (schema, original_index) in
7212 any_of_schemas.iter().zip(source_indices.iter().copied())
7213 {
7214 if let Some(ref_str) = schema.reference() {
7215 if let Some(target) = self.extract_schema_name(ref_str) {
7216 dependencies.insert(target.to_string());
7217 variants.push(SchemaRef {
7218 target: target.to_string(),
7219 nullable: self.schema_or_reference_is_nullable(schema),
7220 });
7221 }
7222 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object))
7223 || schema.inferred_type() == Some(OpenApiSchemaType::Object)
7224 {
7225 let inline_type_name = self.generate_inline_type_name(schema, original_index);
7227
7228 let inline_type_name = self.add_inline_union_branch_schema(
7230 &inline_type_name,
7231 schema,
7232 dependencies,
7233 context_name,
7234 InlineUnionKind::AnyOf,
7235 original_index,
7236 None,
7237 )?;
7238
7239 variants.push(SchemaRef {
7240 target: inline_type_name,
7241 nullable: false,
7242 });
7243 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Array)) {
7244 let preferred_array_type_name =
7246 if let Some(items_schema) = schema.details().item_schema() {
7247 if let Some(ref_str) = items_schema.reference() {
7248 if let Some(item_type_name) = self.extract_schema_name(ref_str) {
7249 dependencies.insert(item_type_name.to_string());
7250 format!("{item_type_name}Array")
7251 } else {
7252 self.generate_context_aware_name(
7253 context_name,
7254 "Array",
7255 original_index,
7256 Some(schema),
7257 )
7258 }
7259 } else {
7260 self.generate_context_aware_name(
7261 context_name,
7262 "Array",
7263 original_index,
7264 Some(schema),
7265 )
7266 }
7267 } else {
7268 self.generate_context_aware_name(
7269 context_name,
7270 "Array",
7271 original_index,
7272 Some(schema),
7273 )
7274 };
7275 let array_type_name = self.allocate_inline_union_branch_name(
7276 &preferred_array_type_name,
7277 context_name,
7278 InlineUnionKind::AnyOf,
7279 original_index,
7280 None,
7281 schema,
7282 );
7283 let array_type =
7285 self.analyze_array_schema(schema, context_name, dependencies)?;
7286
7287 self.resolved_cache.insert(
7289 array_type_name.clone(),
7290 AnalyzedSchema {
7291 name: array_type_name.clone(),
7292 original: serde_json::to_value(schema).unwrap_or(Value::Null),
7293 schema_type: array_type,
7294 dependencies: HashSet::new(),
7295 nullable: false,
7296 description: Some("Array variant in union".to_string()),
7297 default: None,
7298 },
7299 );
7300
7301 dependencies.insert(array_type_name.clone());
7303
7304 variants.push(SchemaRef {
7305 target: array_type_name,
7306 nullable: false,
7307 });
7308 } else if let Some(schema_type) = schema.schema_type() {
7309 let primitive_unions = self
7319 .type_mapper
7320 .config_shape_primitive_unions()
7321 .unwrap_or(true);
7322
7323 if primitive_unions {
7324 variants.push(SchemaRef {
7325 target: self
7326 .openapi_type_to_rust_type(schema_type.clone(), schema.details()),
7327 nullable: false,
7328 });
7329 } else {
7330 let preferred_inline_type_name = match schema_type {
7331 OpenApiSchemaType::String => {
7332 if original_index == 0 {
7333 format!("{context_name}String")
7334 } else {
7335 format!("{context_name}StringVariant{original_index}")
7336 }
7337 }
7338 OpenApiSchemaType::Number => {
7339 if original_index == 0 {
7340 format!("{context_name}Number")
7341 } else {
7342 format!("{context_name}NumberVariant{original_index}")
7343 }
7344 }
7345 OpenApiSchemaType::Integer => {
7346 if original_index == 0 {
7347 format!("{context_name}Integer")
7348 } else {
7349 format!("{context_name}IntegerVariant{original_index}")
7350 }
7351 }
7352 OpenApiSchemaType::Boolean => {
7353 if original_index == 0 {
7354 format!("{context_name}Boolean")
7355 } else {
7356 format!("{context_name}BooleanVariant{original_index}")
7357 }
7358 }
7359 _ => format!("{context_name}Variant{original_index}"),
7360 };
7361 let inline_type_name = self.allocate_inline_union_branch_name(
7362 &preferred_inline_type_name,
7363 context_name,
7364 InlineUnionKind::AnyOf,
7365 original_index,
7366 None,
7367 schema,
7368 );
7369
7370 let rust_type =
7371 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
7372
7373 self.resolved_cache.insert(
7374 inline_type_name.clone(),
7375 AnalyzedSchema {
7376 name: inline_type_name.clone(),
7377 original: serde_json::to_value(schema).unwrap_or(Value::Null),
7378 schema_type: SchemaType::Primitive {
7379 rust_type,
7380 serde_with: None,
7381 },
7382 dependencies: HashSet::new(),
7383 nullable: false,
7384 description: schema.details().description.clone(),
7385 default: None,
7386 },
7387 );
7388
7389 dependencies.insert(inline_type_name.clone());
7390
7391 variants.push(SchemaRef {
7392 target: inline_type_name,
7393 nullable: false,
7394 });
7395 }
7396 } else {
7397 let inline_type_name = self.generate_context_aware_name(
7405 context_name,
7406 "InlineVariant",
7407 original_index,
7408 Some(schema),
7409 );
7410 let inline_type_name = self.add_inline_union_branch_schema(
7411 &inline_type_name,
7412 schema,
7413 dependencies,
7414 context_name,
7415 InlineUnionKind::AnyOf,
7416 original_index,
7417 None,
7418 )?;
7419 dependencies.insert(inline_type_name.clone());
7420 variants.push(SchemaRef {
7421 target: inline_type_name,
7422 nullable: false,
7423 });
7424 }
7425 }
7426
7427 if !variants.is_empty() {
7428 return Ok(SchemaType::Union {
7429 variants,
7430 exclusive: false,
7431 });
7432 }
7433 }
7434
7435 let all_strings = any_of_schemas.iter().all(|schema| {
7437 matches!(schema.schema_type(), Some(OpenApiSchemaType::String))
7438 || schema.details().const_value.is_some()
7439 });
7440
7441 if all_strings {
7442 let mut enum_values = Vec::new();
7444 let mut has_open_string = false;
7445
7446 for schema in any_of_schemas {
7447 match schema
7452 .details()
7453 .string_enum_values()
7454 .filter(|values| !values.is_empty())
7455 {
7456 Some(values) => {
7457 for value in values {
7458 if !enum_values.contains(&value) {
7459 enum_values.push(value);
7460 }
7461 }
7462 }
7463 None => {
7464 if matches!(schema.schema_type(), Some(OpenApiSchemaType::String)) {
7465 has_open_string = true;
7466 }
7467 }
7468 }
7469 }
7470
7471 if !enum_values.is_empty() {
7472 if has_open_string {
7473 return Ok(SchemaType::ExtensibleEnum {
7476 known_values: enum_values,
7477 });
7478 } else {
7479 return Ok(SchemaType::StringEnum {
7481 values: enum_values,
7482 });
7483 }
7484 }
7485 }
7486
7487 Ok(self.untyped_value(
7489 self.untyped_context(""),
7490 UntypedReason::UnrepresentableUnion,
7491 ))
7492 }
7493
7494 fn find_recursive_anchor_schema(&self) -> Option<String> {
7496 for (schema_name, schema) in &self.schemas {
7498 let details = schema.details();
7499 if details.recursive_anchor == Some(true) {
7500 return Some(schema_name.clone());
7501 }
7502 }
7503
7504 None
7508 }
7509
7510 fn should_use_dynamic_json(&self, schema: &Schema) -> bool {
7513 if let Schema::AnyOf { any_of, .. } = schema {
7515 if any_of.len() == 2 {
7516 let has_null = any_of
7517 .iter()
7518 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)));
7519 let has_empty_object = any_of.iter().any(|s| self.is_dynamic_object_pattern(s));
7520
7521 if has_null && has_empty_object {
7522 return true;
7523 }
7524 }
7525 }
7526
7527 self.is_dynamic_object_pattern(schema)
7529 }
7530
7531 fn is_dynamic_object_pattern(&self, schema: &Schema) -> bool {
7533 let is_object = match schema.schema_type() {
7535 Some(OpenApiSchemaType::Object) => true,
7536 None => schema.inferred_type() == Some(OpenApiSchemaType::Object),
7537 _ => false,
7538 };
7539
7540 if !is_object {
7541 return false;
7542 }
7543
7544 Self::object_shape_is_unconstrained(schema.details())
7545 }
7546
7547 fn object_shape_is_unconstrained(details: &crate::openapi::SchemaDetails) -> bool {
7555 if details.additional_properties.is_some() {
7560 return false;
7561 }
7562
7563 let no_properties = details
7565 .properties
7566 .as_ref()
7567 .map(|props| props.is_empty())
7568 .unwrap_or(true);
7569
7570 if no_properties {
7571 let has_structural_constraints = details
7574 .required
7575 .as_ref()
7576 .map(|req| !req.is_empty())
7577 .unwrap_or(false)
7578 || details.pattern_properties.is_some()
7579 || details.property_names.is_some()
7580 || details.min_properties.is_some()
7581 || details.max_properties.is_some()
7582 || details.dependent_required.is_some()
7583 || details.dependent_schemas.is_some()
7584 || details.if_schema.is_some()
7585 || details.then_schema.is_some()
7586 || details.else_schema.is_some();
7587
7588 return !has_structural_constraints;
7589 }
7590
7591 false
7592 }
7593
7594 fn analyze_operations(&mut self, analysis: &mut SchemaAnalysis) -> Result<()> {
7596 let spec: crate::openapi::OpenApiSpec = parse_spec_document(&self.openapi_spec)?;
7597 let mut canonical_operation_ids = HashSet::new();
7602
7603 if let Some(paths) = &spec.paths {
7604 for (path, path_item) in paths {
7605 let resolved = self.resolve_path_item(path_item, &spec)?;
7607 let pi: &crate::openapi::PathItem = resolved.as_ref().unwrap_or(path_item);
7608 self.ingest_path_item_operations(
7609 path,
7610 pi,
7611 analysis,
7612 &mut canonical_operation_ids,
7613 None,
7614 path_item.reference.as_deref(),
7615 )?;
7616 }
7617 }
7618 if let Some(webhooks) = &spec.webhooks {
7625 for (name, path_item) in webhooks {
7626 let synthetic_path = format!("/__webhook__/{name}");
7627 let resolved = self.resolve_path_item(path_item, &spec)?;
7628 let pi = resolved.as_ref().unwrap_or(path_item);
7629 self.ingest_path_item_operations(
7630 &synthetic_path,
7631 pi,
7632 analysis,
7633 &mut canonical_operation_ids,
7634 Some(name),
7635 path_item.reference.as_deref(),
7636 )?;
7637 }
7638 }
7639 Ok(())
7640 }
7641
7642 fn resolve_path_item(
7646 &self,
7647 path_item: &crate::openapi::PathItem,
7648 spec: &crate::openapi::OpenApiSpec,
7649 ) -> Result<Option<crate::openapi::PathItem>> {
7650 let Some(reference) = &path_item.reference else {
7651 return Ok(None);
7652 };
7653 let target_name = reference
7654 .strip_prefix("#/components/pathItems/")
7655 .ok_or_else(|| {
7656 GeneratorError::UnresolvedReference(format!(
7657 "Path Item $ref must point at #/components/pathItems/{{name}}, got {reference}"
7658 ))
7659 })?;
7660 let pi = spec
7661 .components
7662 .as_ref()
7663 .and_then(|c| c.path_items.as_ref())
7664 .and_then(|map| map.get(target_name))
7665 .ok_or_else(|| {
7666 GeneratorError::UnresolvedReference(format!(
7667 "Path Item ref {reference} not found in components/pathItems"
7668 ))
7669 })?;
7670 Ok(Some(pi.clone()))
7671 }
7672
7673 fn ingest_path_item_operations(
7674 &mut self,
7675 path: &str,
7676 path_item: &crate::openapi::PathItem,
7677 analysis: &mut SchemaAnalysis,
7678 canonical_operation_ids: &mut HashSet<String>,
7679 webhook_name: Option<&str>,
7680 path_item_reference: Option<&str>,
7681 ) -> Result<()> {
7682 for (method, operation) in path_item.operations() {
7683 let raw_operation_id = operation
7685 .operation_id
7686 .clone()
7687 .unwrap_or_else(|| Self::generate_operation_id(method, path));
7688
7689 let operation_id = if canonical_operation_ids
7700 .contains(&Self::canonical_operation_id(&raw_operation_id))
7701 {
7702 let method_lower = method.to_lowercase();
7703 let mut candidate = format!("{}_{}", raw_operation_id, method_lower);
7704 let mut suffix = 2;
7705 while canonical_operation_ids.contains(&Self::canonical_operation_id(&candidate)) {
7706 candidate = format!("{}_{}_{}", raw_operation_id, method_lower, suffix);
7707 suffix += 1;
7708 }
7709 eprintln!(
7710 "⚠️ duplicate operationId `{}` at `{} {}` — disambiguated to `{}`",
7711 raw_operation_id, method, path, candidate
7712 );
7713 candidate
7714 } else {
7715 raw_operation_id.clone()
7716 };
7717
7718 let (op_info, responses) = self.analyze_single_operation(
7719 &operation_id,
7720 method,
7721 path,
7722 operation,
7723 path_item.parameters.as_ref(),
7724 analysis,
7725 )?;
7726 analysis
7727 .operation_id_aliases
7728 .entry(raw_operation_id)
7729 .or_default()
7730 .push(operation_id.clone());
7731 canonical_operation_ids.insert(Self::canonical_operation_id(&operation_id));
7732 analysis
7733 .operation_responses
7734 .insert(operation_id.clone(), responses);
7735 let source_path = webhook_name.unwrap_or(path);
7736 let escaped = source_path.replace('~', "~0").replace('/', "~1");
7737 let collection = if webhook_name.is_some() {
7738 "webhooks"
7739 } else {
7740 "paths"
7741 };
7742 let base_pointer = path_item_reference
7743 .and_then(|reference| reference.strip_prefix('#'))
7744 .map(str::to_owned)
7745 .unwrap_or_else(|| format!("/{collection}/{escaped}"));
7746 let operation_pointer = if path_item.additional_operations.as_ref().is_some_and(|map| {
7747 map.get(method)
7748 .is_some_and(|candidate| std::ptr::eq(candidate, operation))
7749 }) {
7750 format!(
7751 "{base_pointer}/additionalOperations/{}",
7752 method.replace('~', "~0").replace('/', "~1")
7753 )
7754 } else {
7755 format!("{base_pointer}/{}", method.to_ascii_lowercase())
7756 };
7757 analysis.operation_sources.insert(
7758 operation_id.clone(),
7759 OperationSource {
7760 json_pointer: operation_pointer,
7761 method: method.to_ascii_uppercase(),
7762 path: source_path.to_string(),
7763 operation_id: operation.operation_id.clone(),
7764 webhook: webhook_name.is_some(),
7765 },
7766 );
7767 analysis.operations.insert(operation_id, op_info);
7768 }
7769 Ok(())
7770 }
7771
7772 fn canonical_operation_id(operation_id: &str) -> String {
7773 use heck::ToPascalCase;
7774 operation_id.replace('.', "_").to_pascal_case()
7775 }
7776
7777 fn generate_operation_id(method: &str, path: &str) -> String {
7780 let mut operation_id = method.to_lowercase();
7782
7783 let path_parts: Vec<&str> = path.trim_start_matches('/').split('/').collect();
7785
7786 for part in path_parts {
7787 if part.is_empty() {
7788 continue;
7789 }
7790
7791 let cleaned_part = if part.starts_with('{') && part.ends_with('}') {
7793 &part[1..part.len() - 1]
7794 } else {
7795 part
7796 };
7797
7798 let pascal_case_part = cleaned_part
7800 .split(&['-', '_'][..])
7801 .map(|s| {
7802 let mut chars = s.chars();
7803 match chars.next() {
7804 None => String::new(),
7805 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
7806 }
7807 })
7808 .collect::<String>();
7809
7810 operation_id.push_str(&pascal_case_part);
7811 }
7812
7813 operation_id
7814 }
7815
7816 fn analyze_single_operation(
7818 &mut self,
7819 operation_id: &str,
7820 method: &str,
7821 path: &str,
7822 operation: &crate::openapi::Operation,
7823 path_item_parameters: Option<&Vec<crate::openapi::Parameter>>,
7824 _analysis: &mut SchemaAnalysis,
7825 ) -> Result<(OperationInfo, BTreeMap<String, OperationResponse>)> {
7826 let raw_path_item = self
7827 .openapi_spec
7828 .get("paths")
7829 .and_then(|paths| paths.get(path))
7830 .cloned();
7831 let raw_operation = raw_path_item
7832 .as_ref()
7833 .and_then(|path_item| path_item.get(method.to_ascii_lowercase()))
7834 .cloned();
7835 let request_body = operation
7836 .request_body
7837 .as_ref()
7838 .map(|request_body| self.resolve_request_body(request_body))
7839 .transpose()?;
7840 let mut op_info = OperationInfo {
7841 operation_id: operation_id.to_string(),
7842 method: method.to_uppercase(),
7843 path: normalize_operation_path(path),
7844 summary: operation.summary.clone(),
7845 description: operation.description.clone(),
7846 request_body: None,
7847 request_body_required: request_body
7849 .as_ref()
7850 .and_then(|rb| rb.required)
7851 .unwrap_or(false),
7852 response_schemas: BTreeMap::new(),
7853 parameters: Vec::new(),
7854 supports_streaming: false, stream_parameter: None, tags: operation.tags.clone().unwrap_or_default(),
7857 deprecated: operation.deprecated.unwrap_or(false),
7858 };
7859 let mut operation_responses = BTreeMap::new();
7860
7861 if let Some(request_body) = &request_body {
7863 use crate::openapi::{
7864 is_binary_media_type, is_form_urlencoded_media_type, is_json_media_type,
7865 media_type_essence,
7866 };
7867 if let Some((content_type, maybe_schema)) = request_body.best_content() {
7868 op_info.request_body = if is_json_media_type(content_type) {
7869 match maybe_schema {
7870 Some(s) => {
7871 let validation_schema = self
7872 .raw_request_body_schema(raw_operation.as_ref(), content_type)
7873 .unwrap_or(
7874 serde_json::to_value(s).map_err(GeneratorError::ParseError)?,
7875 );
7876 Some(
7877 self.resolve_or_inline_schema(s, operation_id, "Request")
7878 .map(|name| RequestBodyContent::Json {
7879 schema_name: name,
7880 media_type: content_type.to_string(),
7881 validation_schema,
7882 })?,
7883 )
7884 }
7885 None => Some(RequestBodyContent::SchemaLess {
7886 media_type: content_type.to_string(),
7887 }),
7888 }
7889 } else if is_form_urlencoded_media_type(content_type) {
7890 match maybe_schema {
7891 Some(s) => {
7892 let validation_schema = self
7893 .raw_request_body_schema(raw_operation.as_ref(), content_type)
7894 .unwrap_or(
7895 serde_json::to_value(s).map_err(GeneratorError::ParseError)?,
7896 );
7897 Some(
7898 self.resolve_or_inline_schema(s, operation_id, "Request")
7899 .map(|name| RequestBodyContent::FormUrlEncoded {
7900 schema_name: name,
7901 media_type: content_type.to_string(),
7902 validation_schema,
7903 })?,
7904 )
7905 }
7906 None => Some(RequestBodyContent::SchemaLess {
7907 media_type: content_type.to_string(),
7908 }),
7909 }
7910 } else if media_type_essence(content_type)
7911 .eq_ignore_ascii_case("multipart/form-data")
7912 {
7913 match maybe_schema {
7914 Some(schema) => {
7915 let validation_schema = self
7916 .raw_request_body_schema(raw_operation.as_ref(), content_type)
7917 .unwrap_or(
7918 serde_json::to_value(schema)
7919 .map_err(GeneratorError::ParseError)?,
7920 );
7921 let encoding = request_body
7922 .content
7923 .as_ref()
7924 .and_then(|content| content.get(content_type))
7925 .and_then(|media_type| media_type.encoding.as_ref())
7926 .into_iter()
7927 .flatten()
7928 .filter_map(|(field, encoding)| {
7929 Some((field.clone(), encoding.content_type.clone()?))
7930 })
7931 .collect();
7932 Some(
7933 self.resolve_or_inline_schema(schema, operation_id, "Request")
7934 .map(|schema_name| RequestBodyContent::Multipart {
7935 schema_name,
7936 media_type: content_type.to_string(),
7937 validation_schema,
7938 encoding,
7939 })?,
7940 )
7941 }
7942 None => Some(RequestBodyContent::SchemaLess {
7943 media_type: content_type.to_string(),
7944 }),
7945 }
7946 } else if is_binary_media_type(content_type, maybe_schema) {
7947 if media_type_essence(content_type)
7948 .eq_ignore_ascii_case("application/octet-stream")
7949 {
7950 Some(RequestBodyContent::OctetStream {
7951 media_type: content_type.to_string(),
7952 })
7953 } else {
7954 Some(RequestBodyContent::Binary {
7955 media_type: content_type.to_string(),
7956 })
7957 }
7958 } else if crate::openapi::is_text_media_type(content_type) {
7959 Some(RequestBodyContent::TextPlain {
7964 media_type: content_type.to_string(),
7965 })
7966 } else {
7967 None
7968 };
7969 }
7970 if op_info.request_body.is_none() {
7971 let mut media_types = request_body
7972 .content
7973 .as_ref()
7974 .map(|content| content.keys().cloned().collect::<Vec<_>>())
7975 .unwrap_or_default();
7976 media_types.sort();
7977 if !media_types.is_empty() {
7978 op_info.request_body = Some(RequestBodyContent::Unsupported { media_types });
7979 }
7980 }
7981 }
7982
7983 if let Some(responses) = &operation.responses {
7985 for (status_code, response) in responses {
7986 let response = self.resolve_response(response)?;
7987 let supports_streaming = response.content.as_ref().is_some_and(|content| {
7993 content
7994 .keys()
7995 .any(|ct| crate::openapi::is_event_stream_media_type(ct))
7996 });
7997 if supports_streaming {
7998 op_info.supports_streaming = true;
7999 }
8000
8001 let mut response_info = OperationResponse {
8002 supports_streaming,
8003 has_content: response
8004 .content
8005 .as_ref()
8006 .is_some_and(|content| !content.is_empty()),
8007 ..Default::default()
8008 };
8009 if let Some((media_type, schema)) = response.json_content() {
8010 if let Some(schema_ref) = schema.reference() {
8011 if let Some(schema_name) = self.extract_schema_name(schema_ref) {
8013 op_info
8014 .response_schemas
8015 .insert(status_code.clone(), schema_name.to_string());
8016 response_info.schema_name = Some(schema_name.to_string());
8017 response_info.media_type = Some(media_type.to_string());
8018 response_info.body = Some(OperationResponseBody::Json {
8019 schema_name: schema_name.to_string(),
8020 media_type: media_type.to_string(),
8021 });
8022 }
8023 } else {
8024 let synthetic_name =
8026 self.generate_inline_response_type_name(operation_id, status_code);
8027
8028 let mut deps = HashSet::new();
8030 let synthetic_name =
8031 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
8032
8033 op_info
8034 .response_schemas
8035 .insert(status_code.clone(), synthetic_name.clone());
8036 response_info.body = Some(OperationResponseBody::Json {
8037 schema_name: synthetic_name.clone(),
8038 media_type: media_type.to_string(),
8039 });
8040 response_info.schema_name = Some(synthetic_name);
8041 response_info.media_type = Some(media_type.to_string());
8042 }
8043 }
8044 if response_info.body.is_none()
8045 && let Some(content) = response.content.as_ref()
8046 {
8047 let selected = content
8048 .iter()
8049 .find(|(media_type, media)| {
8050 matches!(
8051 crate::openapi::classify_response_media_type(
8052 media_type,
8053 media.schema.as_ref()
8054 ),
8055 crate::openapi::ResponseMediaKind::Text
8056 )
8057 })
8058 .or_else(|| {
8059 content.iter().find(|(media_type, media)| {
8060 matches!(
8061 crate::openapi::classify_response_media_type(
8062 media_type,
8063 media.schema.as_ref()
8064 ),
8065 crate::openapi::ResponseMediaKind::Binary
8066 ) && !crate::openapi::is_wildcard_media_type(media_type)
8067 })
8068 })
8069 .or_else(|| {
8070 content.iter().find(|(media_type, media)| {
8071 matches!(
8072 crate::openapi::classify_response_media_type(
8073 media_type,
8074 media.schema.as_ref()
8075 ),
8076 crate::openapi::ResponseMediaKind::Binary
8077 )
8078 })
8079 });
8080 if let Some((media_type, media)) = selected {
8081 response_info.body = match crate::openapi::classify_response_media_type(
8082 media_type,
8083 media.schema.as_ref(),
8084 ) {
8085 crate::openapi::ResponseMediaKind::Text => {
8086 Some(OperationResponseBody::Text {
8087 media_type: media_type.clone(),
8088 })
8089 }
8090 crate::openapi::ResponseMediaKind::Binary => {
8091 Some(OperationResponseBody::Binary {
8092 media_type: media_type.clone(),
8093 wildcard: crate::openapi::is_wildcard_media_type(media_type),
8094 })
8095 }
8096 _ => None,
8097 };
8098 }
8099 }
8100 if let Some(content) = &response.content {
8101 for (media_type, media) in content {
8102 let representation = match crate::openapi::classify_response_media_type(
8103 media_type,
8104 media.schema.as_ref(),
8105 ) {
8106 crate::openapi::ResponseMediaKind::Json => {
8107 let Some(schema) = media.schema.as_ref() else {
8108 continue;
8109 };
8110 let schema_name =
8111 if response_info.media_type.as_ref() == Some(media_type) {
8112 response_info.schema_name.clone()
8113 } else if let Some(reference) = schema.reference() {
8114 self.extract_schema_name(reference).map(str::to_owned)
8115 } else {
8116 let preferred = format!(
8117 "{}{}",
8118 self.generate_inline_response_type_name(
8119 operation_id,
8120 status_code
8121 ),
8122 base_param_ident(media_type)
8123 );
8124 Some(self.add_inline_schema(
8125 &preferred,
8126 schema,
8127 &mut HashSet::new(),
8128 )?)
8129 };
8130 let Some(schema_name) = schema_name else {
8131 continue;
8132 };
8133 OperationRepresentation::Json { schema_name }
8134 }
8135 crate::openapi::ResponseMediaKind::Text => {
8136 OperationRepresentation::Text
8137 }
8138 crate::openapi::ResponseMediaKind::Binary => {
8139 OperationRepresentation::Binary {
8140 wildcard: crate::openapi::is_wildcard_media_type(media_type),
8141 }
8142 }
8143 crate::openapi::ResponseMediaKind::EventStream => {
8144 OperationRepresentation::EventStream
8145 }
8146 crate::openapi::ResponseMediaKind::Unsupported => continue,
8147 };
8148 response_info
8149 .representations
8150 .insert(media_type.clone(), representation);
8151 }
8152 }
8153 response_info.unsupported_media_types = response
8154 .content
8155 .as_ref()
8156 .into_iter()
8157 .flat_map(|content| content.iter())
8158 .filter(|(media_type, content)| {
8159 match crate::openapi::classify_response_media_type(
8160 media_type,
8161 content.schema.as_ref(),
8162 ) {
8163 crate::openapi::ResponseMediaKind::Json => content.schema.is_none(),
8164 crate::openapi::ResponseMediaKind::Unsupported => true,
8165 crate::openapi::ResponseMediaKind::EventStream
8166 | crate::openapi::ResponseMediaKind::Text
8167 | crate::openapi::ResponseMediaKind::Binary => false,
8168 }
8169 })
8170 .map(|(media_type, _)| media_type.clone())
8171 .collect();
8172 operation_responses.insert(status_code.clone(), response_info);
8173 }
8174 }
8175
8176 if op_info.supports_streaming
8179 && let Some(parameters) = &operation.parameters
8180 {
8181 for param in parameters {
8182 if let Some(name) = param.name.as_deref() {
8183 if name.eq_ignore_ascii_case("stream") {
8184 op_info.stream_parameter = Some(name.to_string());
8185 break;
8186 }
8187 }
8188 }
8189 }
8190
8191 if let Some(parameters) = &operation.parameters {
8193 for (index, param) in parameters.iter().enumerate() {
8194 let resolved = self.resolve_parameter(param).into_owned();
8198 let validation_schema = raw_operation
8199 .as_ref()
8200 .and_then(|operation| operation.get("parameters"))
8201 .and_then(Value::as_array)
8202 .and_then(|parameters| parameters.get(index))
8203 .and_then(|parameter| self.raw_parameter_schema(parameter));
8204 if let Some(param_info) =
8205 self.analyze_parameter(&resolved, operation_id, validation_schema)?
8206 {
8207 op_info.parameters.push(param_info);
8208 }
8209 }
8210 }
8211
8212 if let Some(path_params) = path_item_parameters {
8214 let existing_keys: std::collections::HashSet<(String, String)> = op_info
8215 .parameters
8216 .iter()
8217 .map(|p| (p.name.clone(), p.location.clone()))
8218 .collect();
8219 for (index, param) in path_params.iter().enumerate() {
8220 let resolved = self.resolve_parameter(param).into_owned();
8221 let validation_schema = raw_path_item
8222 .as_ref()
8223 .and_then(|path_item| path_item.get("parameters"))
8224 .and_then(Value::as_array)
8225 .and_then(|parameters| parameters.get(index))
8226 .and_then(|parameter| self.raw_parameter_schema(parameter));
8227 if let Some(param_info) =
8228 self.analyze_parameter(&resolved, operation_id, validation_schema)?
8229 {
8230 if !existing_keys
8231 .contains(&(param_info.name.clone(), param_info.location.clone()))
8232 {
8233 op_info.parameters.push(param_info);
8234 }
8235 }
8236 }
8237 }
8238
8239 let mut declared_path_names: std::collections::HashSet<String> = op_info
8247 .parameters
8248 .iter()
8249 .filter(|p| p.location == "path")
8250 .map(|p| p.name.clone())
8251 .collect();
8252 let bytes = path.as_bytes().iter();
8253 let mut current = String::new();
8254 let mut in_brace = false;
8255 let mut synthesized: Vec<String> = Vec::new();
8256 for b in bytes {
8257 match *b {
8258 b'{' => {
8259 in_brace = true;
8260 current.clear();
8261 }
8262 b'}' if in_brace => {
8263 in_brace = false;
8264 if !current.is_empty() && !declared_path_names.contains(¤t) {
8265 synthesized.push(current.clone());
8266 declared_path_names.insert(current.clone());
8267 }
8268 }
8269 _ if in_brace => current.push(*b as char),
8270 _ => {}
8271 }
8272 }
8273 for name in synthesized {
8274 eprintln!(
8275 "⚠️ path `{}` references `{{{}}}` but the spec doesn't declare it as a parameter — synthesizing as required String",
8276 path, name
8277 );
8278 op_info.parameters.push(ParameterInfo {
8279 name,
8280 location: "path".to_string(),
8281 required: true,
8282 schema_ref: None,
8283 rust_type: "String".to_string(),
8284 description: None,
8285 enum_values: None,
8286 enum_varnames: None,
8287 rust_ident: None,
8288 query_serialization: None,
8289 validation_schema: None,
8290 });
8291 }
8292
8293 let mut used: std::collections::HashSet<String> =
8301 ["request", "body", "req", "request_url", "client", "form"]
8302 .into_iter()
8303 .map(str::to_owned)
8304 .collect();
8305 for p in op_info.parameters.iter_mut() {
8306 let raw = base_param_ident(&p.name);
8307 let raw = if matches!(raw.as_str(), "self" | "super" | "crate") {
8308 format!("{raw}_param")
8309 } else {
8310 raw
8311 };
8312 let mut chosen = raw.clone();
8313 let mut suffix = 2;
8314 while !used.insert(chosen.clone()) {
8315 chosen = format!("{raw}_{suffix}");
8316 suffix += 1;
8317 }
8318 p.rust_ident = Some(chosen);
8319 }
8320
8321 Ok((op_info, operation_responses))
8322 }
8323
8324 fn resolve_request_body(
8326 &self,
8327 request_body: &crate::openapi::RequestBody,
8328 ) -> Result<crate::openapi::RequestBody> {
8329 let mut current = request_body.clone();
8330 let mut visited = HashSet::new();
8331 while let Some(reference) = current.reference.clone() {
8332 if !visited.insert(reference.clone()) {
8333 return Err(GeneratorError::CircularDependency(format!(
8334 "request body reference {reference}"
8335 )));
8336 }
8337
8338 let pointer = reference.strip_prefix('#').ok_or_else(|| {
8339 GeneratorError::UnresolvedReference(format!(
8340 "external request body reference `{reference}` is not supported"
8341 ))
8342 })?;
8343 if !pointer.is_empty() && !pointer.starts_with('/') {
8344 return Err(GeneratorError::UnresolvedReference(format!(
8345 "request body reference `{reference}` is not a local JSON Pointer"
8346 )));
8347 }
8348 let value = self.openapi_spec.pointer(pointer).ok_or_else(|| {
8349 GeneratorError::UnresolvedReference(format!(
8350 "request body reference `{reference}` does not exist"
8351 ))
8352 })?;
8353 let object = value.as_object().ok_or_else(|| {
8354 GeneratorError::InvalidSchema(format!(
8355 "request body reference `{reference}` must target an object"
8356 ))
8357 })?;
8358 if !["$ref", "description", "required", "content"]
8359 .iter()
8360 .any(|field| object.contains_key(*field))
8361 {
8362 return Err(GeneratorError::InvalidSchema(format!(
8363 "request body reference `{reference}` does not target a structurally compatible OpenAPI Request Body Object"
8364 )));
8365 }
8366 current = serde_json::from_value(value.clone()).map_err(|error| {
8367 GeneratorError::InvalidSchema(format!(
8368 "request body reference `{reference}` is not a valid OpenAPI Request Body Object: {error}"
8369 ))
8370 })?;
8371 }
8372 Ok(current)
8373 }
8374
8375 fn resolve_response(
8382 &self,
8383 response: &crate::openapi::Response,
8384 ) -> Result<crate::openapi::Response> {
8385 let mut current = response.clone();
8386 let mut visited = HashSet::new();
8387 while let Some(reference) = current.reference.clone() {
8388 if !visited.insert(reference.clone()) {
8389 return Err(GeneratorError::CircularDependency(format!(
8390 "response reference {reference}"
8391 )));
8392 }
8393
8394 let pointer = reference.strip_prefix('#').ok_or_else(|| {
8395 GeneratorError::UnresolvedReference(format!(
8396 "external response reference `{reference}` is not supported"
8397 ))
8398 })?;
8399 if !pointer.is_empty() && !pointer.starts_with('/') {
8400 return Err(GeneratorError::UnresolvedReference(format!(
8401 "response reference `{reference}` is not a local JSON Pointer"
8402 )));
8403 }
8404 let value = self.openapi_spec.pointer(pointer).ok_or_else(|| {
8405 GeneratorError::UnresolvedReference(format!(
8406 "response reference `{reference}` does not exist"
8407 ))
8408 })?;
8409 let object = value.as_object().ok_or_else(|| {
8410 GeneratorError::InvalidSchema(format!(
8411 "response reference `{reference}` must target an object"
8412 ))
8413 })?;
8414 if !["$ref", "description", "headers", "content", "links"]
8415 .iter()
8416 .any(|field| object.contains_key(*field))
8417 {
8418 return Err(GeneratorError::InvalidSchema(format!(
8419 "response reference `{reference}` does not target a structurally compatible OpenAPI Response Object"
8420 )));
8421 }
8422 current = serde_json::from_value(value.clone()).map_err(|error| {
8423 GeneratorError::InvalidSchema(format!(
8424 "response reference `{reference}` is not a valid OpenAPI Response Object: {error}"
8425 ))
8426 })?;
8427 }
8428 Ok(current)
8429 }
8430
8431 fn generate_inline_response_type_name(&self, operation_id: &str, status_code: &str) -> String {
8438 use heck::ToPascalCase;
8439 let base_name = operation_id.replace('.', "_").to_pascal_case();
8440 let suffix = Self::status_code_suffix(status_code);
8441 format!("{}Response{}", base_name, suffix)
8442 }
8443
8444 fn status_code_suffix(status_code: &str) -> String {
8451 match status_code {
8452 "" | "200" => String::new(),
8453 "default" | "Default" => "Default".to_string(),
8454 other if other.chars().all(|c| c.is_ascii_digit()) => other.to_string(),
8455 other => other.to_ascii_lowercase(),
8456 }
8457 }
8458
8459 fn generate_inline_request_type_name(&self, operation_id: &str) -> String {
8461 use heck::ToPascalCase;
8462 let base_name = operation_id.replace('.', "_").to_pascal_case();
8466 format!("{}Request", base_name)
8467 }
8468
8469 fn resolve_or_inline_schema(
8472 &mut self,
8473 schema: &crate::openapi::Schema,
8474 operation_id: &str,
8475 suffix: &str,
8476 ) -> Result<String> {
8477 if let Some(schema_ref) = schema.reference()
8478 && let Some(schema_name) = self.extract_schema_name(schema_ref)
8479 {
8480 return Ok(schema_name.to_string());
8481 }
8482 let synthetic_name = if suffix == "Request" {
8484 self.generate_inline_request_type_name(operation_id)
8485 } else {
8486 self.generate_inline_response_type_name(operation_id, "")
8487 };
8488 let mut deps = HashSet::new();
8489 self.add_inline_schema(&synthetic_name, schema, &mut deps)
8490 }
8491
8492 fn resolve_parameter<'a>(
8495 &'a self,
8496 param: &'a crate::openapi::Parameter,
8497 ) -> std::borrow::Cow<'a, crate::openapi::Parameter> {
8498 if let Some(ref_str) = param.reference.as_deref() {
8499 if let Some(param_name) = ref_str.strip_prefix("#/components/parameters/") {
8500 if let Some(resolved) = self.component_parameters.get(param_name) {
8501 return std::borrow::Cow::Borrowed(resolved);
8502 }
8503 }
8504 }
8505 std::borrow::Cow::Borrowed(param)
8506 }
8507
8508 fn referenced_schema_is_string_enum(&self, name: &str) -> bool {
8521 if self.resolve_cached_schema(name).is_some_and(|schema| {
8522 matches!(
8523 schema.schema_type,
8524 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. }
8525 )
8526 }) {
8527 return true;
8528 }
8529 let Some(schema_value) = self
8530 .openapi_spec
8531 .get("components")
8532 .and_then(|c| c.get("schemas"))
8533 .and_then(|s| s.get(name))
8534 else {
8535 return false;
8536 };
8537 let is_string_type = schema_value
8538 .get("type")
8539 .and_then(|v| v.as_str())
8540 .map(|s| s == "string")
8541 .unwrap_or(false);
8542 let has_enum_or_const =
8543 schema_value.get("enum").is_some() || schema_value.get("const").is_some();
8544 is_string_type && has_enum_or_const
8545 }
8546
8547 fn resolve_raw_local_reference(&self, value: &Value) -> Option<Value> {
8548 let Some(reference) = value.get("$ref").and_then(Value::as_str) else {
8549 return Some(value.clone());
8550 };
8551 let pointer = reference.strip_prefix('#')?;
8552 self.openapi_spec.pointer(pointer).cloned()
8553 }
8554
8555 fn raw_request_body_schema(
8556 &self,
8557 operation: Option<&Value>,
8558 content_type: &str,
8559 ) -> Option<Value> {
8560 let request_body = operation?.get("requestBody")?;
8561 self.resolve_raw_local_reference(request_body)?
8562 .get("content")?
8563 .get(content_type)?
8564 .get("schema")
8565 .cloned()
8566 }
8567
8568 fn raw_parameter_schema(&self, parameter: &Value) -> Option<Value> {
8569 self.resolve_raw_local_reference(parameter)?
8570 .get("schema")
8571 .cloned()
8572 }
8573
8574 fn analyze_parameter(
8575 &mut self,
8576 param: &crate::openapi::Parameter,
8577 operation_id: &str,
8578 raw_validation_schema: Option<Value>,
8579 ) -> Result<Option<ParameterInfo>> {
8580 use heck::ToPascalCase;
8581
8582 let name = param.name.as_deref().unwrap_or("");
8583 let location = param.location.as_deref().unwrap_or("");
8584 let required = param.required.unwrap_or(false);
8585 let validation_schema = match raw_validation_schema {
8586 Some(schema) => Some(schema),
8587 None => param
8588 .schema
8589 .as_ref()
8590 .map(serde_json::to_value)
8591 .transpose()
8592 .map_err(GeneratorError::ParseError)?,
8593 };
8594
8595 let mut rust_type = "String".to_string();
8596 let mut schema_ref = None;
8597 let mut enum_values: Option<Vec<String>> = None;
8598 let mut enum_varnames: Option<Vec<String>> = None;
8599 let mut query_serialization: Option<QuerySerialization> = None;
8600
8601 let is_query = location == "query";
8607 let is_simple_header = location == "header"
8608 && matches!(param.style.as_deref(), None | Some("simple"))
8609 && param.explode != Some(true);
8610 let form_style = matches!(param.style.as_deref(), None | Some("form"));
8611 let form_exploded = form_style && param.explode.unwrap_or(true);
8612 let deep_object =
8613 param.style.as_deref() == Some("deepObject") && param.explode != Some(false);
8614
8615 let object_serialization = if !is_query {
8616 None
8617 } else if deep_object {
8618 Some(QuerySerialization::DeepObject)
8619 } else if form_exploded {
8620 Some(QuerySerialization::FormExplodedObject)
8621 } else if form_style {
8622 Some(QuerySerialization::FormObject)
8623 } else {
8624 None
8625 };
8626
8627 if let Some(schema) = ¶m.schema {
8628 if let Some(ref_str) = schema.reference() {
8629 if let Some(name) = self.extract_schema_name(ref_str) {
8635 if self.referenced_schema_is_string_enum(name) {
8636 schema_ref = Some(name.to_string());
8637 } else if object_serialization.is_some()
8638 && self.referenced_schema_is_object(name)
8639 {
8640 schema_ref = Some(name.to_string());
8641 query_serialization = if form_exploded && self.uses_aws_query_conventions()
8642 {
8643 match self.referenced_array_struct_item_type(name, 1) {
8644 Some(ArrayItemType::NestedStructRef { properties, .. }) => {
8645 Some(QuerySerialization::FormExplodedNestedObject {
8646 properties,
8647 })
8648 }
8649 _ => object_serialization.clone(),
8650 }
8651 } else {
8652 object_serialization.clone()
8653 };
8654 } else if (is_query && form_style || is_simple_header)
8655 && let Some(item_type) = self.referenced_array_param_item_type(name)
8656 {
8657 schema_ref = Some(name.to_string());
8663 query_serialization = Some(if is_simple_header {
8664 QuerySerialization::SimpleHeaderArray { item_type }
8665 } else if form_exploded {
8666 QuerySerialization::FormExplodedArray { item_type }
8667 } else {
8668 QuerySerialization::FormArray { item_type }
8669 });
8670 }
8671 }
8672 } else if object_serialization.is_some() && Self::schema_is_inline_object(schema) {
8673 let op_pascal = operation_id.replace('.', "_").to_pascal_case();
8678 let param_pascal = name.to_pascal_case();
8679 let synthetic_name = format!("{op_pascal}{param_pascal}");
8680 let mut deps = HashSet::new();
8681 let synthetic_name = self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
8682 schema_ref = Some(synthetic_name.clone());
8683 query_serialization = if form_exploded && self.uses_aws_query_conventions() {
8684 match self.referenced_array_struct_item_type(&synthetic_name, 1) {
8685 Some(ArrayItemType::NestedStructRef { properties, .. }) => {
8686 Some(QuerySerialization::FormExplodedNestedObject { properties })
8687 }
8688 _ => object_serialization.clone(),
8689 }
8690 } else {
8691 object_serialization.clone()
8692 };
8693 } else if (is_query && form_style || is_simple_header)
8694 && matches!(
8695 schema.schema_type(),
8696 Some(crate::openapi::SchemaType::Array)
8697 )
8698 && let Some(item_type) = self.array_param_item_type(schema)
8699 {
8700 query_serialization = Some(if is_simple_header {
8708 QuerySerialization::SimpleHeaderArray { item_type }
8709 } else if form_exploded {
8710 QuerySerialization::FormExplodedArray { item_type }
8711 } else {
8712 QuerySerialization::FormArray { item_type }
8713 });
8714 } else if let Some(schema_type) = schema.schema_type() {
8715 let format = schema.details().format.clone();
8721 rust_type = match schema_type {
8722 crate::openapi::SchemaType::Boolean => "bool".to_string(),
8723 crate::openapi::SchemaType::Integer => self.integer_rust_type(schema.details()),
8724 crate::openapi::SchemaType::Number => {
8725 self.type_mapper.number_format(format.as_deref()).rust_type
8726 }
8727 crate::openapi::SchemaType::String => "String".to_string(),
8728 _ => "String".to_string(),
8729 };
8730
8731 if matches!(schema_type, crate::openapi::SchemaType::String) {
8732 let details = schema.details();
8733 if details.is_string_enum() {
8734 if let Some(values) = details.string_enum_values() {
8735 if !values.is_empty() {
8736 let op_pascal = operation_id.replace('.', "_").to_pascal_case();
8737 let param_pascal = name.to_pascal_case();
8738 rust_type = format!("{op_pascal}{param_pascal}");
8739 enum_varnames = details
8744 .extra
8745 .get("x-enum-varnames")
8746 .and_then(Value::as_array)
8747 .map(|raw| {
8748 raw.iter()
8749 .filter_map(Value::as_str)
8750 .map(str::to_owned)
8751 .collect::<Vec<_>>()
8752 })
8753 .filter(|names| names.len() == values.len());
8754 enum_values = Some(values);
8755 }
8756 }
8757 }
8758 }
8759 }
8760
8761 if is_query && query_serialization.is_none() {
8762 let referenced_name = schema
8763 .reference()
8764 .and_then(|reference| self.extract_schema_name(reference));
8765 let is_object = referenced_name
8766 .is_some_and(|name| self.referenced_schema_is_object(name))
8767 || Self::schema_is_inline_object(schema);
8768 let is_array = referenced_name
8769 .is_some_and(|name| self.referenced_schema_is_array(name))
8770 || matches!(
8771 schema.schema_type(),
8772 Some(crate::openapi::SchemaType::Array)
8773 );
8774 let is_composed = referenced_name
8775 .is_some_and(|name| self.referenced_schema_is_composed_query_shape(name));
8776 let reason = if param.style.as_deref() == Some("deepObject")
8777 && param.explode == Some(false)
8778 {
8779 Some("style=deepObject with explode=false is undefined by OpenAPI".to_string())
8780 } else if param.style.as_deref() == Some("deepObject") && !is_object {
8781 Some("style=deepObject is defined only for object query parameters".to_string())
8782 } else if is_object {
8783 Some(format!(
8784 "object query parameters do not support style={}",
8785 param.style.as_deref().unwrap_or("form")
8786 ))
8787 } else if is_array && form_style {
8788 Some(
8789 "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"
8790 .to_string(),
8791 )
8792 } else if is_array {
8793 Some(format!(
8794 "array query parameters do not yet support style={}",
8795 param.style.as_deref().unwrap_or("form")
8796 ))
8797 } else if is_composed {
8798 Some(
8799 "composed or union query schemas cannot be projected to an unambiguous flat wire shape"
8800 .to_string(),
8801 )
8802 } else {
8803 None
8804 };
8805 if let Some(reason) = reason {
8806 query_serialization = Some(QuerySerialization::Unsupported { reason });
8807 }
8808 }
8809 }
8810
8811 Ok(Some(ParameterInfo {
8812 name: name.to_string(),
8813 location: location.to_string(),
8814 required,
8815 schema_ref,
8816 rust_type,
8817 description: param.description.clone(),
8818 enum_values,
8819 enum_varnames,
8820 rust_ident: None,
8821 query_serialization,
8822 validation_schema,
8823 }))
8824 }
8825
8826 fn array_param_item_type(&self, schema: &crate::openapi::Schema) -> Option<ArrayItemType> {
8835 let items = schema.details().item_schema()?;
8836 let unwrapped = unwrap_annotation_allof(items);
8840 if let Some(ref_str) = unwrapped.reference() {
8841 let name = self.extract_schema_name(ref_str)?;
8842 return self
8843 .referenced_array_scalar_item_type(name)
8844 .or_else(|| self.referenced_array_struct_item_type(name, 1));
8845 }
8846 let format = unwrapped.details().format.clone();
8847 let scalar = match unwrapped.schema_type()? {
8848 crate::openapi::SchemaType::String => "String".to_string(),
8849 crate::openapi::SchemaType::Integer => self.integer_rust_type(unwrapped.details()),
8850 crate::openapi::SchemaType::Number => {
8851 self.type_mapper.number_format(format.as_deref()).rust_type
8852 }
8853 crate::openapi::SchemaType::Boolean => "bool".to_string(),
8854 _ => return None,
8855 };
8856 Some(ArrayItemType::Scalar(scalar))
8857 }
8858
8859 fn referenced_array_param_item_type(&self, name: &str) -> Option<ArrayItemType> {
8862 let schema = self.resolve_cached_schema(name)?;
8863 let SchemaType::Array { item_type } = &schema.schema_type else {
8864 return None;
8865 };
8866 self.analyzed_array_item_type(item_type)
8867 }
8868
8869 fn analyzed_array_item_type(&self, item_type: &SchemaType) -> Option<ArrayItemType> {
8870 self.analyzed_array_item_type_at_depth(item_type, 1)
8871 }
8872
8873 fn referenced_array_struct_item_type(
8878 &self,
8879 name: &str,
8880 nested_array_depth: usize,
8881 ) -> Option<ArrayItemType> {
8882 let resolved = self.resolve_cached_schema(name)?;
8883 let SchemaType::Object {
8884 properties,
8885 required,
8886 additional_properties,
8887 ..
8888 } = &resolved.schema_type
8889 else {
8890 return None;
8891 };
8892 if properties.is_empty() || additional_properties.is_open() {
8893 return None;
8894 }
8895 let mut projected = Vec::with_capacity(properties.len());
8896 let mut has_array = false;
8897 for (wire_name, property) in properties {
8898 let value_type = if let Some(scalar) = self.query_scalar_type(&property.schema_type) {
8899 QueryStructPropertyType::Scalar(scalar)
8900 } else {
8901 if nested_array_depth == 0 {
8902 return None;
8903 }
8904 if let Some(array) = self.resolve_query_array_type(&property.schema_type) {
8905 let item_type =
8906 self.analyzed_array_item_type_at_depth(array, nested_array_depth - 1)?;
8907 if matches!(item_type, ArrayItemType::NestedStructRef { .. }) {
8908 return None;
8909 }
8910 has_array = true;
8911 QueryStructPropertyType::Array { item_type }
8912 } else {
8913 has_array = true;
8914 QueryStructPropertyType::Object {
8915 properties: self.query_flat_object_properties(&property.schema_type)?,
8916 }
8917 }
8918 };
8919 projected.push(QueryStructProperty {
8920 wire_name: wire_name.clone(),
8921 required: required.contains(wire_name),
8922 value_type,
8923 });
8924 }
8925 if has_array {
8926 Some(ArrayItemType::NestedStructRef {
8927 schema_name: name.to_string(),
8928 properties: projected,
8929 })
8930 } else {
8931 Some(ArrayItemType::FlatStructRef {
8932 schema_name: name.to_string(),
8933 properties: projected,
8934 })
8935 }
8936 }
8937
8938 fn analyzed_array_item_type_at_depth(
8939 &self,
8940 item_type: &SchemaType,
8941 nested_array_depth: usize,
8942 ) -> Option<ArrayItemType> {
8943 match item_type {
8944 SchemaType::Primitive { rust_type, .. } => {
8945 Some(ArrayItemType::Scalar(rust_type.clone()))
8946 }
8947 SchemaType::Reference { target } => self
8948 .referenced_array_scalar_item_type(target)
8949 .or_else(|| self.referenced_array_struct_item_type(target, nested_array_depth)),
8950 _ => None,
8951 }
8952 }
8953
8954 fn resolve_query_array_type<'a>(
8955 &'a self,
8956 schema_type: &'a SchemaType,
8957 ) -> Option<&'a SchemaType> {
8958 match schema_type {
8959 SchemaType::Array { item_type } => Some(item_type),
8960 SchemaType::Reference { target } => {
8961 let resolved = self.resolve_cached_schema(target)?;
8962 let SchemaType::Array { item_type } = &resolved.schema_type else {
8963 return None;
8964 };
8965 Some(item_type)
8966 }
8967 _ => None,
8968 }
8969 }
8970
8971 fn query_flat_object_properties(
8972 &self,
8973 schema_type: &SchemaType,
8974 ) -> Option<Vec<QueryStructProperty>> {
8975 let schema_type = match schema_type {
8976 SchemaType::Reference { target } => &self.resolve_cached_schema(target)?.schema_type,
8977 other => other,
8978 };
8979 let SchemaType::Object {
8980 properties,
8981 required,
8982 additional_properties,
8983 ..
8984 } = schema_type
8985 else {
8986 return None;
8987 };
8988 if properties.is_empty() || additional_properties.is_open() {
8989 return None;
8990 }
8991 properties
8992 .iter()
8993 .map(|(wire_name, property)| {
8994 Some(QueryStructProperty {
8995 wire_name: wire_name.clone(),
8996 required: required.contains(wire_name),
8997 value_type: QueryStructPropertyType::Scalar(
8998 self.query_scalar_type(&property.schema_type)?,
8999 ),
9000 })
9001 })
9002 .collect()
9003 }
9004
9005 fn query_scalar_type(&self, schema_type: &SchemaType) -> Option<QueryScalarType> {
9006 match schema_type {
9007 SchemaType::Primitive { rust_type, .. } => match rust_type.as_str() {
9008 "String" => Some(QueryScalarType::String),
9009 "bool" => Some(QueryScalarType::Boolean),
9010 value if value.starts_with('i') || value.starts_with('u') => {
9011 Some(QueryScalarType::Integer)
9012 }
9013 value if value.starts_with('f') => Some(QueryScalarType::Number),
9014 "serde_json::Value" => None,
9015 _ => Some(QueryScalarType::String),
9016 },
9017 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. } => {
9018 Some(QueryScalarType::String)
9019 }
9020 SchemaType::Reference { target } => {
9021 let resolved = self.resolve_cached_schema(target)?;
9022 self.query_scalar_type(&resolved.schema_type)
9023 }
9024 _ => None,
9025 }
9026 }
9027
9028 fn referenced_array_scalar_item_type(&self, name: &str) -> Option<ArrayItemType> {
9036 let resolved = self.resolve_cached_schema(name)?;
9037 let supported = match &resolved.schema_type {
9038 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. } => true,
9039 SchemaType::Primitive { .. } => resolved
9040 .original
9041 .get("type")
9042 .is_some_and(Self::query_scalar_type_value),
9043 _ => false,
9044 };
9045 supported.then(|| ArrayItemType::SchemaRef(name.to_string()))
9046 }
9047
9048 fn query_scalar_type_value(value: &Value) -> bool {
9049 const SCALARS: [&str; 4] = ["string", "integer", "number", "boolean"];
9050 if let Some(value) = value.as_str() {
9051 return SCALARS.contains(&value);
9052 }
9053 let Some(values) = value.as_array() else {
9054 return false;
9055 };
9056 if !values.iter().all(Value::is_string) {
9057 return false;
9058 }
9059 let mut non_null = values
9060 .iter()
9061 .filter_map(Value::as_str)
9062 .filter(|value| *value != "null");
9063 let Some(scalar) = non_null.next() else {
9064 return false;
9065 };
9066 non_null.next().is_none() && SCALARS.contains(&scalar)
9067 }
9068
9069 fn referenced_schema_is_object(&self, name: &str) -> bool {
9073 self.resolve_cached_schema(name)
9074 .is_some_and(|schema| matches!(schema.schema_type, SchemaType::Object { .. }))
9075 }
9076
9077 fn referenced_schema_is_array(&self, name: &str) -> bool {
9078 self.resolve_cached_schema(name)
9079 .is_some_and(|schema| matches!(schema.schema_type, SchemaType::Array { .. }))
9080 }
9081
9082 fn referenced_schema_is_composed_query_shape(&self, name: &str) -> bool {
9083 self.resolve_cached_schema(name).is_some_and(|schema| {
9084 matches!(
9085 schema.schema_type,
9086 SchemaType::Composition { .. }
9087 | SchemaType::Union { .. }
9088 | SchemaType::DiscriminatedUnion { .. }
9089 )
9090 })
9091 }
9092
9093 fn resolve_cached_schema(&self, name: &str) -> Option<&AnalyzedSchema> {
9094 let mut current = name;
9095 let mut visited = HashSet::new();
9096 loop {
9097 if !visited.insert(current) {
9098 return None;
9099 }
9100 let schema = self.resolved_cache.get(current)?;
9101 if let SchemaType::Reference { target } = &schema.schema_type {
9102 current = target;
9103 } else {
9104 return Some(schema);
9105 }
9106 }
9107 }
9108
9109 fn schema_is_inline_object(schema: &crate::openapi::Schema) -> bool {
9111 match schema.schema_type() {
9112 Some(crate::openapi::SchemaType::Object) => true,
9113 None => schema.details().properties.is_some(),
9114 _ => false,
9115 }
9116 }
9117}
9118
9119fn pointer_type_name(pointer: &str) -> String {
9130 use heck::ToPascalCase;
9131
9132 pointer
9133 .split('/')
9134 .skip(1)
9135 .filter(|segment| !matches!(*segment, "components" | "schemas" | "properties"))
9136 .map(|segment| {
9137 segment
9138 .replace("~1", "/")
9139 .replace("~0", "~")
9140 .to_pascal_case()
9141 })
9142 .collect::<String>()
9143}
9144
9145fn shared_positional_schema(positions: &[Schema]) -> Option<&Schema> {
9150 let first = positions.first()?;
9151 let key = positional_schema_key(first)?;
9152 positions
9153 .iter()
9154 .skip(1)
9155 .all(|position| positional_schema_key(position).as_deref() == Some(key.as_str()))
9156 .then_some(first)
9157}
9158
9159fn positional_schema_key(schema: &Schema) -> Option<String> {
9160 if let Some(reference) = schema.reference() {
9161 return Some(format!("$ref {reference}"));
9162 }
9163 let details = schema.details();
9164 if details.properties.is_some() || details.enum_values.is_some() || details.items.is_some() {
9165 return None;
9166 }
9167 match schema.schema_type()? {
9168 crate::openapi::SchemaType::Object | crate::openapi::SchemaType::Array => None,
9169 scalar => Some(format!(
9170 "{scalar:?} {}",
9171 details.format.as_deref().unwrap_or_default()
9172 )),
9173 }
9174}
9175
9176fn parse_spec_document(openapi_spec: &Value) -> Result<OpenApiSpec> {
9177 serde_path_to_error::deserialize(openapi_spec).map_err(|error| {
9178 let mut pointer = json_pointer(error.path());
9179 pointer.push_str(&refine_schema_failure(openapi_spec, &pointer));
9183 GeneratorError::ParseErrorAt {
9184 pointer,
9185 message: error.into_inner().to_string(),
9186 }
9187 })
9188}
9189
9190const SUBSCHEMA_KEYWORDS: [&str; 11] = [
9192 "items",
9193 "additionalProperties",
9194 "propertyNames",
9195 "unevaluatedProperties",
9196 "unevaluatedItems",
9197 "contains",
9198 "contentSchema",
9199 "if",
9200 "then",
9201 "else",
9202 "not",
9203];
9204
9205const SUBSCHEMA_LIST_KEYWORDS: [&str; 4] = ["oneOf", "anyOf", "allOf", "prefixItems"];
9207
9208const SUBSCHEMA_MAP_KEYWORDS: [&str; 5] = [
9210 "properties",
9211 "patternProperties",
9212 "dependentSchemas",
9213 "$defs",
9214 "definitions",
9215];
9216
9217const REFINE_PARSE_BUDGET: usize = 20_000;
9221
9222fn refine_schema_failure(openapi_spec: &Value, pointer: &str) -> String {
9230 let Some(path) = pointer.strip_prefix('#') else {
9231 return String::new();
9232 };
9233 let Some(node) = openapi_spec.pointer(path) else {
9234 return String::new();
9235 };
9236 let segments = path.split('/').skip(1).collect::<Vec<_>>();
9237 let last = segments.last().copied().unwrap_or_default();
9238 let parent = segments
9239 .len()
9240 .checked_sub(2)
9241 .map(|index| segments[index])
9242 .unwrap_or_default();
9243
9244 if last == "schema" || holds_schemas(parent) {
9245 return if parses_as_schema(node) {
9246 String::new()
9247 } else {
9248 deepest_schema_failure(node)
9249 };
9250 }
9251
9252 let mut budget = REFINE_PARSE_BUDGET;
9253 locate_failing_schema(node, holds_schemas(last), &mut budget).unwrap_or_default()
9254}
9255
9256fn holds_schemas(key: &str) -> bool {
9259 matches!(key, "schemas" | "$defs" | "definitions")
9260}
9261
9262fn locate_failing_schema(
9270 node: &Value,
9271 children_are_schemas: bool,
9272 budget: &mut usize,
9273) -> Option<String> {
9274 for (segment, key, child) in child_nodes(node) {
9275 if *budget == 0 {
9276 return None;
9277 }
9278 *budget -= 1;
9279 if children_are_schemas || key == "schema" {
9280 if !parses_as_schema(child) {
9281 return Some(format!("/{segment}{}", deepest_schema_failure(child)));
9282 }
9283 continue;
9284 }
9285 if let Some(rest) = locate_failing_schema(child, holds_schemas(key), budget) {
9286 return Some(format!("/{segment}{rest}"));
9287 }
9288 }
9289 None
9290}
9291
9292fn parses_as_schema(node: &Value) -> bool {
9293 Schema::deserialize(node).is_ok()
9294}
9295
9296fn deepest_schema_failure(node: &Value) -> String {
9300 let Some(object) = node.as_object() else {
9301 return String::new();
9302 };
9303
9304 let descend = |segment: String, child: &Value| -> Option<String> {
9305 if parses_as_schema(child) {
9306 return None;
9307 }
9308 Some(format!("/{segment}{}", deepest_schema_failure(child)))
9309 };
9310
9311 for keyword in SUBSCHEMA_KEYWORDS {
9312 if let Some(child) = object.get(keyword)
9313 && let Some(suffix) = descend(escape_pointer_segment(keyword), child)
9314 {
9315 return suffix;
9316 }
9317 }
9318 for keyword in SUBSCHEMA_LIST_KEYWORDS {
9319 if let Some(Value::Array(children)) = object.get(keyword) {
9320 for (index, child) in children.iter().enumerate() {
9321 if let Some(suffix) = descend(
9322 format!("{}/{index}", escape_pointer_segment(keyword)),
9323 child,
9324 ) {
9325 return suffix;
9326 }
9327 }
9328 }
9329 }
9330 for keyword in SUBSCHEMA_MAP_KEYWORDS {
9331 if let Some(Value::Object(children)) = object.get(keyword) {
9332 for (name, child) in children {
9333 if let Some(suffix) = descend(
9334 format!(
9335 "{}/{}",
9336 escape_pointer_segment(keyword),
9337 escape_pointer_segment(name)
9338 ),
9339 child,
9340 ) {
9341 return suffix;
9342 }
9343 }
9344 }
9345 }
9346 String::new()
9347}
9348
9349fn child_nodes(node: &Value) -> Vec<(String, &str, &Value)> {
9355 const DATA_KEYWORDS: [&str; 5] = ["example", "examples", "default", "enum", "const"];
9356
9357 match node {
9358 Value::Object(members) => members
9359 .iter()
9360 .filter(|(name, child)| {
9361 (child.is_object() || child.is_array())
9362 && !name.starts_with("x-")
9363 && !DATA_KEYWORDS.contains(&name.as_str())
9364 })
9365 .map(|(name, child)| (escape_pointer_segment(name), name.as_str(), child))
9366 .collect(),
9367 Value::Array(elements) => elements
9368 .iter()
9369 .enumerate()
9370 .filter(|(_, child)| child.is_object() || child.is_array())
9371 .map(|(index, child)| (index.to_string(), "", child))
9372 .collect(),
9373 _ => Vec::new(),
9374 }
9375}
9376
9377fn escape_pointer_segment(segment: &str) -> String {
9378 segment.replace('~', "~0").replace('/', "~1")
9379}
9380
9381fn json_pointer(path: &serde_path_to_error::Path) -> String {
9385 use serde_path_to_error::Segment;
9386
9387 let mut pointer = String::from("#");
9388 for segment in path.iter() {
9389 match segment {
9390 Segment::Seq { index } => {
9391 pointer.push('/');
9392 pointer.push_str(&index.to_string());
9393 }
9394 Segment::Map { key } | Segment::Enum { variant: key } => {
9395 pointer.push('/');
9396 pointer.push_str(&escape_pointer_segment(key));
9397 }
9398 Segment::Unknown => pointer.push_str("/?"),
9399 }
9400 }
9401 pointer
9402}
9403
9404pub(crate) fn component_schema_name_aliases(openapi_spec: &Value) -> BTreeMap<String, String> {
9405 let Some(schemas) = openapi_spec
9406 .pointer("/components/schemas")
9407 .and_then(Value::as_object)
9408 else {
9409 return BTreeMap::new();
9410 };
9411
9412 let mut names_by_rust_name = BTreeMap::<String, Vec<String>>::new();
9413 for name in schemas.keys() {
9414 names_by_rust_name
9415 .entry(crate::generator::rust_type_name(name))
9416 .or_default()
9417 .push(name.clone());
9418 }
9419
9420 let mut claimed_rust_names = names_by_rust_name.keys().cloned().collect::<HashSet<_>>();
9423 let mut aliases = BTreeMap::new();
9424
9425 for (rust_name, mut names) in names_by_rust_name {
9426 if names.len() < 2 {
9427 continue;
9428 }
9429
9430 names.sort_by_key(|name| (name != &rust_name, name.clone()));
9433 for source_name in names.into_iter().skip(1) {
9434 let mut suffix = 2;
9435 let replacement = loop {
9436 let candidate = format!("{rust_name}{suffix}");
9437 if claimed_rust_names.insert(candidate.clone()) {
9438 break candidate;
9439 }
9440 suffix += 1;
9441 };
9442
9443 aliases.insert(source_name, replacement);
9444 }
9445 }
9446
9447 aliases
9448}
9449
9450fn disambiguate_component_schema_names(openapi_spec: &mut Value) {
9451 let aliases = component_schema_name_aliases(openapi_spec);
9452 if aliases.is_empty() {
9453 return;
9454 }
9455
9456 for (source_name, replacement) in &aliases {
9457 let rust_name = crate::generator::rust_type_name(source_name);
9458 eprintln!(
9459 "⚠️ schema `{source_name}` maps to the existing Rust type `{rust_name}` — disambiguated to `{replacement}`"
9460 );
9461 }
9462
9463 let Some(schemas) = openapi_spec
9464 .pointer_mut("/components/schemas")
9465 .and_then(Value::as_object_mut)
9466 else {
9467 return;
9468 };
9469
9470 let original_schemas = std::mem::take(schemas);
9471 for (name, schema) in original_schemas {
9472 schemas.insert(aliases.get(&name).cloned().unwrap_or(name), schema);
9473 }
9474
9475 rewrite_component_schema_references(openapi_spec, &aliases);
9476}
9477
9478fn resolve_multipart_wire_schema<'a>(
9482 schema: &'a Value,
9483 component_schemas: &'a BTreeMap<String, Value>,
9484 visited: &mut HashSet<String>,
9485) -> Option<&'a Value> {
9486 let Some(reference) = schema.get("$ref").and_then(Value::as_str) else {
9487 for keyword in ["anyOf", "oneOf"] {
9488 if let Some(branches) = schema.get(keyword).and_then(Value::as_array) {
9489 let concrete: Vec<_> = branches
9490 .iter()
9491 .filter(|branch| branch.get("type").and_then(Value::as_str) != Some("null"))
9492 .collect();
9493 if concrete.len() == 1 && concrete.len() < branches.len() {
9494 return resolve_multipart_wire_schema(concrete[0], component_schemas, visited);
9495 }
9496 }
9497 }
9498 if let Some(branches) = schema.get("allOf").and_then(Value::as_array) {
9499 if branches.len() == 1 {
9500 return resolve_multipart_wire_schema(&branches[0], component_schemas, visited);
9501 }
9502 }
9503 return Some(schema);
9504 };
9505 let name = reference.strip_prefix("#/components/schemas/")?;
9506 if !visited.insert(name.to_string()) {
9507 return None;
9508 }
9509 resolve_multipart_wire_schema(component_schemas.get(name)?, component_schemas, visited)
9510}
9511
9512fn is_multipart_file_schema(schema: &Value, component_schemas: &BTreeMap<String, Value>) -> bool {
9517 let Some(schema) =
9518 resolve_multipart_wire_schema(schema, component_schemas, &mut HashSet::new())
9519 else {
9520 return false;
9521 };
9522 let is_string = |schema: &Value| schema.get("type").and_then(Value::as_str) == Some("string");
9523 let is_binary = |schema: &Value| {
9524 is_string(schema) && schema.get("format").and_then(Value::as_str) == Some("binary")
9525 };
9526 if is_binary(schema) {
9527 return true;
9528 }
9529 ["anyOf", "oneOf"].into_iter().any(|keyword| {
9530 let Some(branches) = schema.get(keyword).and_then(Value::as_array) else {
9531 return false;
9532 };
9533 let branches: Option<Vec<&Value>> = branches
9534 .iter()
9535 .filter(|branch| branch.get("type").and_then(Value::as_str) != Some("null"))
9536 .map(|branch| {
9537 resolve_multipart_wire_schema(branch, component_schemas, &mut HashSet::new())
9538 })
9539 .collect();
9540 branches.is_some_and(|branches| {
9541 branches.iter().all(|branch| is_string(branch)) && branches.iter().any(|b| is_binary(b))
9542 })
9543 })
9544}
9545
9546fn retype_multipart_file_fields(analysis: &mut SchemaAnalysis) {
9557 fn file_type(original: &SchemaType, type_name: &str) -> SchemaType {
9558 match original {
9559 SchemaType::Nullable { inner_type } => SchemaType::Nullable {
9560 inner_type: Box::new(file_type(inner_type, type_name)),
9561 },
9562 _ => SchemaType::Primitive {
9563 rust_type: type_name.to_string(),
9564 serde_with: None,
9565 },
9566 }
9567 }
9568 fn file_array_type(original: &SchemaType, type_name: &str) -> SchemaType {
9569 match original {
9570 SchemaType::Nullable { inner_type } => SchemaType::Nullable {
9571 inner_type: Box::new(file_array_type(inner_type, type_name)),
9572 },
9573 SchemaType::Array { item_type } => SchemaType::Array {
9574 item_type: Box::new(file_type(item_type, type_name)),
9575 },
9576 _ => SchemaType::Array {
9577 item_type: Box::new(file_type(original, type_name)),
9578 },
9579 }
9580 }
9581
9582 let claimed: HashSet<String> = analysis
9583 .schemas
9584 .keys()
9585 .map(|name| crate::generator::rust_type_name(name))
9586 .collect();
9587 let mut type_name = MULTIPART_FILE_TYPE.to_string();
9588 let mut suffix = 2;
9589 while claimed.contains(&type_name) {
9590 type_name = format!("{MULTIPART_FILE_TYPE}{suffix}");
9591 suffix += 1;
9592 }
9593
9594 let bodies: Vec<(String, Value)> = analysis
9595 .operations
9596 .values()
9597 .filter_map(|operation| match &operation.request_body {
9598 Some(RequestBodyContent::Multipart {
9599 schema_name,
9600 validation_schema,
9601 ..
9602 }) => Some((schema_name.clone(), validation_schema.clone())),
9603 _ => None,
9604 })
9605 .collect();
9606 let components = analysis.validation_context.component_schemas.clone();
9607 for (schema_name, validation_schema) in bodies {
9608 let Some(wire_properties) =
9609 resolve_multipart_wire_schema(&validation_schema, &components, &mut HashSet::new())
9610 .and_then(|schema| schema.get("properties"))
9611 .and_then(Value::as_object)
9612 else {
9613 continue;
9614 };
9615 let mut name = schema_name;
9616 let mut seen = HashSet::new();
9617 while let Some(SchemaType::Reference { target }) = analysis
9618 .schemas
9619 .get(&name)
9620 .map(|schema| &schema.schema_type)
9621 {
9622 if !seen.insert(target.clone()) {
9623 break;
9624 }
9625 name = target.clone();
9626 }
9627 let Some(SchemaType::Object { properties, .. }) = analysis
9628 .schemas
9629 .get_mut(&name)
9630 .map(|schema| &mut schema.schema_type)
9631 else {
9632 continue;
9633 };
9634 for (wire_name, property) in properties.iter_mut() {
9635 let Some(wire_property) = wire_properties.get(wire_name) else {
9636 continue;
9637 };
9638 if is_multipart_file_schema(wire_property, &components) {
9639 property.schema_type = file_type(&property.schema_type, &type_name);
9640 analysis.multipart_file_type = Some(type_name.clone());
9641 continue;
9642 }
9643 let items =
9644 resolve_multipart_wire_schema(wire_property, &components, &mut HashSet::new())
9645 .filter(|schema| schema.get("type").and_then(Value::as_str) == Some("array"))
9646 .and_then(|schema| schema.get("items"));
9647 if items.is_some_and(|items| is_multipart_file_schema(items, &components)) {
9648 property.schema_type = file_array_type(&property.schema_type, &type_name);
9649 analysis.multipart_file_type = Some(type_name.clone());
9650 }
9651 }
9652 }
9653}
9654
9655pub(crate) const MULTIPART_FILE_TYPE: &str = "MultipartFile";
9658
9659fn disambiguate_analyzed_schema_names(
9660 analysis: &mut SchemaAnalysis,
9661 component_schemas: &BTreeMap<String, Schema>,
9662) {
9663 let mut names_by_rust_name = BTreeMap::<String, Vec<String>>::new();
9664 for name in analysis.schemas.keys() {
9665 names_by_rust_name
9666 .entry(crate::generator::rust_type_name(name))
9667 .or_default()
9668 .push(name.clone());
9669 }
9670
9671 let mut claimed_rust_names = names_by_rust_name.keys().cloned().collect::<HashSet<_>>();
9672 let mut aliases = BTreeMap::<String, String>::new();
9673
9674 for (rust_name, mut names) in names_by_rust_name {
9675 if names.len() < 2 {
9676 continue;
9677 }
9678 names.sort_by_key(|name| {
9679 (
9680 !component_schemas.contains_key(name),
9681 name != &rust_name,
9682 name.clone(),
9683 )
9684 });
9685
9686 for source_name in names.into_iter().skip(1) {
9687 let mut suffix = 2;
9688 let replacement = loop {
9689 let candidate = format!("{rust_name}{suffix}");
9690 if claimed_rust_names.insert(candidate.clone()) {
9691 break candidate;
9692 }
9693 suffix += 1;
9694 };
9695 eprintln!(
9696 "⚠️ generated schema `{source_name}` maps to the existing Rust type `{rust_name}` — disambiguated to `{replacement}`"
9697 );
9698 aliases.insert(source_name, replacement);
9699 }
9700 }
9701
9702 if aliases.is_empty() {
9703 return;
9704 }
9705
9706 let original_schemas = std::mem::take(&mut analysis.schemas);
9707 for (name, mut schema) in original_schemas {
9708 schema.name = renamed_schema_name(&schema.name, &aliases);
9709 schema.dependencies = schema
9710 .dependencies
9711 .into_iter()
9712 .map(|name| renamed_schema_name(&name, &aliases))
9713 .collect();
9714 rewrite_schema_type_names(&mut schema.schema_type, &aliases);
9715 analysis
9716 .schemas
9717 .insert(renamed_schema_name(&name, &aliases), schema);
9718 }
9719
9720 let original_edges = std::mem::take(&mut analysis.dependencies.edges);
9721 for (name, dependencies) in original_edges {
9722 analysis.dependencies.edges.insert(
9723 renamed_schema_name(&name, &aliases),
9724 dependencies
9725 .into_iter()
9726 .map(|name| renamed_schema_name(&name, &aliases))
9727 .collect(),
9728 );
9729 }
9730 analysis.dependencies.recursive_schemas = analysis
9731 .dependencies
9732 .recursive_schemas
9733 .iter()
9734 .map(|name| renamed_schema_name(name, &aliases))
9735 .collect();
9736
9737 analysis.patterns.tagged_enum_schemas = analysis
9738 .patterns
9739 .tagged_enum_schemas
9740 .iter()
9741 .map(|name| renamed_schema_name(name, &aliases))
9742 .collect();
9743 analysis.patterns.untagged_enum_schemas = analysis
9744 .patterns
9745 .untagged_enum_schemas
9746 .iter()
9747 .map(|name| renamed_schema_name(name, &aliases))
9748 .collect();
9749 analysis.patterns.type_mappings = std::mem::take(&mut analysis.patterns.type_mappings)
9750 .into_iter()
9751 .map(|(name, mappings)| {
9752 (
9753 renamed_schema_name(&name, &aliases),
9754 mappings
9755 .into_iter()
9756 .map(|(value, schema_name)| {
9757 (value, renamed_schema_name(&schema_name, &aliases))
9758 })
9759 .collect(),
9760 )
9761 })
9762 .collect();
9763
9764 for operation in analysis.operations.values_mut() {
9765 if let Some(request_body) = &mut operation.request_body {
9766 rewrite_request_body_schema_name(request_body, &aliases);
9767 }
9768 for schema_name in operation.response_schemas.values_mut() {
9769 *schema_name = renamed_schema_name(schema_name, &aliases);
9770 }
9771 for parameter in &mut operation.parameters {
9772 if let Some(schema_name) = &mut parameter.schema_ref {
9773 *schema_name = renamed_schema_name(schema_name, &aliases);
9774 }
9775 if let Some(serialization) = &mut parameter.query_serialization {
9776 rewrite_query_serialization_schema_names(serialization, &aliases);
9777 }
9778 }
9779 }
9780
9781 for responses in analysis.operation_responses.values_mut() {
9782 for response in responses.values_mut() {
9783 if let Some(schema_name) = &mut response.schema_name {
9784 *schema_name = renamed_schema_name(schema_name, &aliases);
9785 }
9786 for representation in response.representations.values_mut() {
9787 if let OperationRepresentation::Json { schema_name } = representation {
9788 *schema_name = renamed_schema_name(schema_name, &aliases);
9789 }
9790 }
9791 if let Some(OperationResponseBody::Json { schema_name, .. }) = &mut response.body {
9792 *schema_name = renamed_schema_name(schema_name, &aliases);
9793 }
9794 }
9795 }
9796}
9797
9798fn renamed_schema_name(name: &str, aliases: &BTreeMap<String, String>) -> String {
9799 aliases
9800 .get(name)
9801 .cloned()
9802 .unwrap_or_else(|| name.to_string())
9803}
9804
9805fn rewrite_schema_type_names(schema_type: &mut SchemaType, aliases: &BTreeMap<String, String>) {
9806 match schema_type {
9807 SchemaType::Object {
9808 properties,
9809 additional_properties,
9810 ..
9811 } => {
9812 for property in properties.values_mut() {
9813 rewrite_schema_type_names(&mut property.schema_type, aliases);
9814 }
9815 if let ObjectAdditionalProperties::Typed { value_type } = additional_properties {
9816 rewrite_schema_type_names(value_type, aliases);
9817 }
9818 }
9819 SchemaType::DiscriminatedUnion { variants, .. } => {
9820 for variant in variants {
9821 variant.type_name = renamed_schema_name(&variant.type_name, aliases);
9822 variant.schema_ref = renamed_schema_name(&variant.schema_ref, aliases);
9823 }
9824 }
9825 SchemaType::Union { variants, .. } | SchemaType::Composition { schemas: variants } => {
9826 for variant in variants {
9827 variant.target = renamed_schema_name(&variant.target, aliases);
9828 }
9829 }
9830 SchemaType::Array { item_type } => rewrite_schema_type_names(item_type, aliases),
9831 SchemaType::Nullable { inner_type } => rewrite_schema_type_names(inner_type, aliases),
9832 SchemaType::Untyped { .. } => {}
9833 SchemaType::Tuple { element_types } => {
9834 for element_type in element_types {
9835 rewrite_schema_type_names(element_type, aliases);
9836 }
9837 }
9838 SchemaType::Reference { target } => {
9839 *target = renamed_schema_name(target, aliases);
9840 }
9841 SchemaType::Primitive { .. }
9842 | SchemaType::StringEnum { .. }
9843 | SchemaType::ExtensibleEnum { .. } => {}
9844 }
9845}
9846
9847fn rewrite_request_body_schema_name(
9848 request_body: &mut RequestBodyContent,
9849 aliases: &BTreeMap<String, String>,
9850) {
9851 match request_body {
9852 RequestBodyContent::Json { schema_name, .. }
9853 | RequestBodyContent::FormUrlEncoded { schema_name, .. }
9854 | RequestBodyContent::Multipart { schema_name, .. } => {
9855 *schema_name = renamed_schema_name(schema_name, aliases);
9856 }
9857 _ => {}
9858 }
9859}
9860
9861fn rewrite_query_serialization_schema_names(
9862 serialization: &mut QuerySerialization,
9863 aliases: &BTreeMap<String, String>,
9864) {
9865 match serialization {
9866 QuerySerialization::FormExplodedArray { item_type }
9867 | QuerySerialization::FormArray { item_type }
9868 | QuerySerialization::SimpleHeaderArray { item_type } => {
9869 rewrite_array_item_type_schema_names(item_type, aliases);
9870 }
9871 QuerySerialization::FormExplodedNestedObject { properties } => {
9872 for property in properties {
9873 rewrite_query_property_type_schema_names(&mut property.value_type, aliases);
9874 }
9875 }
9876 _ => {}
9877 }
9878}
9879
9880fn rewrite_array_item_type_schema_names(
9881 item_type: &mut ArrayItemType,
9882 aliases: &BTreeMap<String, String>,
9883) {
9884 match item_type {
9885 ArrayItemType::SchemaRef(name) => *name = renamed_schema_name(name, aliases),
9886 ArrayItemType::FlatStructRef {
9887 schema_name,
9888 properties,
9889 }
9890 | ArrayItemType::NestedStructRef {
9891 schema_name,
9892 properties,
9893 } => {
9894 *schema_name = renamed_schema_name(schema_name, aliases);
9895 for property in properties {
9896 rewrite_query_property_type_schema_names(&mut property.value_type, aliases);
9897 }
9898 }
9899 ArrayItemType::Scalar(_) => {}
9900 }
9901}
9902
9903fn rewrite_query_property_type_schema_names(
9904 property_type: &mut QueryStructPropertyType,
9905 aliases: &BTreeMap<String, String>,
9906) {
9907 match property_type {
9908 QueryStructPropertyType::Array { item_type } => {
9909 rewrite_array_item_type_schema_names(item_type, aliases)
9910 }
9911 QueryStructPropertyType::Object { properties } => {
9912 for property in properties {
9913 rewrite_query_property_type_schema_names(&mut property.value_type, aliases);
9914 }
9915 }
9916 QueryStructPropertyType::Scalar(_) => {}
9917 }
9918}
9919
9920fn rewrite_component_schema_references(value: &mut Value, aliases: &BTreeMap<String, String>) {
9921 match value {
9922 Value::Array(values) => {
9923 for value in values {
9924 rewrite_component_schema_references(value, aliases);
9925 }
9926 }
9927 Value::Object(object) => {
9928 if let Some(Value::String(reference)) = object.get_mut("$ref") {
9929 rewrite_component_schema_reference(reference, aliases);
9930 }
9931
9932 if let Some(Value::Object(mapping)) = object.get_mut("mapping") {
9933 for target_value in mapping.values_mut() {
9934 let Some(target) = target_value.as_str() else {
9935 continue;
9936 };
9937 let replacement = aliases.get(target).cloned().or_else(|| {
9938 let mut target = target.to_string();
9939 rewrite_component_schema_reference(&mut target, aliases).then_some(target)
9940 });
9941 if let Some(replacement) = replacement {
9942 *target_value = Value::String(replacement);
9943 }
9944 }
9945 }
9946
9947 for value in object.values_mut() {
9948 rewrite_component_schema_references(value, aliases);
9949 }
9950 }
9951 _ => {}
9952 }
9953}
9954
9955fn rewrite_component_schema_reference(
9956 reference: &mut String,
9957 aliases: &BTreeMap<String, String>,
9958) -> bool {
9959 const PREFIX: &str = "#/components/schemas/";
9960 let Some(encoded_name) = reference.strip_prefix(PREFIX) else {
9961 return false;
9962 };
9963 let encoded_name = encoded_name.split('/').next().unwrap_or(encoded_name);
9964
9965 for (source, replacement) in aliases {
9966 let encoded_source = source.replace('~', "~0").replace('/', "~1");
9967 if encoded_name == encoded_source {
9968 reference.replace_range(
9969 PREFIX.len()..PREFIX.len() + encoded_source.len(),
9970 replacement,
9971 );
9972 return true;
9973 }
9974 }
9975
9976 false
9977}