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