Skip to content

Commit 2f12f3d

Browse files
committed
fix: reorder untagged enum variants to put Integer before Number
Cherry-picked from upstream PR oxidecomputer#991. When a schema defines an untagged enum with both integer and number types, serde tries variants in order. Having Number (f64) before Integer (i64) made the Integer variant unreachable since any integer is also a valid f64. Introduces ReorderedInstanceType to ensure Integer is always matched before Number during deserialization.
1 parent 471ffda commit 2f12f3d

File tree

3 files changed

+99
-17
lines changed

3 files changed

+99
-17
lines changed

typify-impl/src/convert.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::type_entry::{
77
EnumTagType, TypeEntry, TypeEntryDetails, TypeEntryEnum, TypeEntryNewtype, TypeEntryStruct,
88
Variant, VariantDetails,
99
};
10-
use crate::util::{all_mutually_exclusive, ref_key, StringValidator};
10+
use crate::util::{all_mutually_exclusive, ref_key, ReorderedInstanceType, StringValidator};
1111
use log::{debug, info};
1212
use schemars::schema::{
1313
ArrayValidation, InstanceType, Metadata, ObjectValidation, Schema, SchemaObject, SingleOrVec,
@@ -672,7 +672,13 @@ impl TypeSpace {
672672
} => {
673673
// Eliminate duplicates (they hold no significance); they
674674
// aren't supposed to be there, but we can still handle it.
675-
let unique_types = instance_types.iter().collect::<BTreeSet<_>>();
675+
// Convert the types into a form that puts integers before numbers to ensure that
676+
// integer get matched before numbers in untagged enum generation.
677+
let unique_types = instance_types
678+
.iter()
679+
.copied()
680+
.map(ReorderedInstanceType::from)
681+
.collect::<BTreeSet<_>>();
676682

677683
// Massage the data into labeled subschemas with the following
678684
// format:
@@ -695,8 +701,9 @@ impl TypeSpace {
695701
// but why do tomorrow what we could easily to today?
696702
let subschemas = unique_types
697703
.into_iter()
704+
.map(InstanceType::from)
698705
.map(|it| {
699-
let instance_type = Some(SingleOrVec::Single(Box::new(*it)));
706+
let instance_type = Some(SingleOrVec::Single(Box::new(it)));
700707
let (label, inner_schema) = match it {
701708
InstanceType::Null => (
702709
"null",

typify-impl/src/util.rs

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,63 @@ impl StringValidator {
933933
}
934934
}
935935

936+
/// A re-ordering of JSON schema instance types that puts integer values before
937+
/// number values.
938+
///
939+
/// This is used for untagged enum generation to ensure that integer values
940+
/// are matched before number values.
941+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
942+
pub(crate) enum ReorderedInstanceType {
943+
/// The JSON schema instance type `null`.
944+
Null,
945+
946+
/// The JSON schema instance type `boolean`.
947+
Boolean,
948+
949+
/// The JSON schema instance type `integer`.
950+
Integer,
951+
952+
/// The JSON schema instance type `number`.
953+
Number,
954+
955+
/// The JSON schema instance type `string`.
956+
String,
957+
958+
/// The JSON schema instance type `array`.
959+
Array,
960+
961+
/// The JSON schema instance type `object`.
962+
Object,
963+
}
964+
965+
impl From<InstanceType> for ReorderedInstanceType {
966+
fn from(instance_type: InstanceType) -> Self {
967+
match instance_type {
968+
InstanceType::Null => Self::Null,
969+
InstanceType::Boolean => Self::Boolean,
970+
InstanceType::Object => Self::Object,
971+
InstanceType::Array => Self::Array,
972+
InstanceType::Integer => Self::Integer,
973+
InstanceType::Number => Self::Number,
974+
InstanceType::String => Self::String,
975+
}
976+
}
977+
}
978+
979+
impl From<ReorderedInstanceType> for InstanceType {
980+
fn from(instance_type: ReorderedInstanceType) -> Self {
981+
match instance_type {
982+
ReorderedInstanceType::Null => Self::Null,
983+
ReorderedInstanceType::Boolean => Self::Boolean,
984+
ReorderedInstanceType::Object => Self::Object,
985+
ReorderedInstanceType::Array => Self::Array,
986+
ReorderedInstanceType::Integer => Self::Integer,
987+
ReorderedInstanceType::Number => Self::Number,
988+
ReorderedInstanceType::String => Self::String,
989+
}
990+
}
991+
}
992+
936993
#[cfg(test)]
937994
mod tests {
938995
use std::collections::BTreeMap;
@@ -944,7 +1001,7 @@ mod tests {
9441001
};
9451002

9461003
use crate::{
947-
util::{decode_segment, sanitize, schemas_mutually_exclusive, Case},
1004+
util::{decode_segment, sanitize, schemas_mutually_exclusive, Case, ReorderedInstanceType},
9481005
Name,
9491006
};
9501007

@@ -1121,4 +1178,22 @@ mod tests {
11211178
assert!(ach.is_valid("Meshach"));
11221179
assert!(!ach.is_valid("Abednego"));
11231180
}
1181+
1182+
#[test]
1183+
fn test_instance_type_ordering() {
1184+
let null = ReorderedInstanceType::Null;
1185+
let boolean = ReorderedInstanceType::Boolean;
1186+
let integer = ReorderedInstanceType::Integer;
1187+
let number = ReorderedInstanceType::Number;
1188+
let string = ReorderedInstanceType::String;
1189+
let array = ReorderedInstanceType::Array;
1190+
let object = ReorderedInstanceType::Object;
1191+
1192+
assert!(null < boolean);
1193+
assert!(boolean < integer);
1194+
assert!(integer < number);
1195+
assert!(number < string);
1196+
assert!(string < array);
1197+
assert!(array < object);
1198+
}
11241199
}

typify/tests/schemas/multiple-instance-types.rs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,14 @@ pub mod error {
4141
#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)]
4242
#[serde(untagged)]
4343
pub enum IntOrStr {
44-
String(::std::string::String),
4544
Integer(i64),
45+
String(::std::string::String),
4646
}
4747
impl ::std::fmt::Display for IntOrStr {
4848
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
4949
match self {
50-
Self::String(x) => x.fmt(f),
5150
Self::Integer(x) => x.fmt(f),
51+
Self::String(x) => x.fmt(f),
5252
}
5353
}
5454
}
@@ -79,31 +79,31 @@ impl ::std::convert::From<i64> for IntOrStr {
7979
pub enum OneOfSeveral {
8080
Null,
8181
Boolean(bool),
82-
Object(::serde_json::Map<::std::string::String, ::serde_json::Value>),
83-
Array(::std::vec::Vec<::serde_json::Value>),
84-
String(::std::string::String),
8582
Integer(i64),
83+
String(::std::string::String),
84+
Array(::std::vec::Vec<::serde_json::Value>),
85+
Object(::serde_json::Map<::std::string::String, ::serde_json::Value>),
8686
}
8787
impl ::std::convert::From<bool> for OneOfSeveral {
8888
fn from(value: bool) -> Self {
8989
Self::Boolean(value)
9090
}
9191
}
92-
impl ::std::convert::From<::serde_json::Map<::std::string::String, ::serde_json::Value>>
93-
for OneOfSeveral
94-
{
95-
fn from(value: ::serde_json::Map<::std::string::String, ::serde_json::Value>) -> Self {
96-
Self::Object(value)
92+
impl ::std::convert::From<i64> for OneOfSeveral {
93+
fn from(value: i64) -> Self {
94+
Self::Integer(value)
9795
}
9896
}
9997
impl ::std::convert::From<::std::vec::Vec<::serde_json::Value>> for OneOfSeveral {
10098
fn from(value: ::std::vec::Vec<::serde_json::Value>) -> Self {
10199
Self::Array(value)
102100
}
103101
}
104-
impl ::std::convert::From<i64> for OneOfSeveral {
105-
fn from(value: i64) -> Self {
106-
Self::Integer(value)
102+
impl ::std::convert::From<::serde_json::Map<::std::string::String, ::serde_json::Value>>
103+
for OneOfSeveral
104+
{
105+
fn from(value: ::serde_json::Map<::std::string::String, ::serde_json::Value>) -> Self {
106+
Self::Object(value)
107107
}
108108
}
109109
#[doc = "`ReallyJustNull`"]

0 commit comments

Comments
 (0)