Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 61 additions & 31 deletions typify-impl/src/merge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -632,9 +632,9 @@ fn try_merge_schema_object_not(
Ok(schema_object)
}

/// Merge instance types which could be None (meaning type is valid), a
/// Merge instance types which could be None (meaning any type is valid), a
/// singleton type, or an array of types. An error result indicates that the
/// types were non-overlappin and therefore incompatible.
/// types were non-overlapping and therefore incompatible.
fn merge_so_instance_type(
a: Option<&SingleOrVec<InstanceType>>,
b: Option<&SingleOrVec<InstanceType>>,
Expand All @@ -643,39 +643,31 @@ fn merge_so_instance_type(
(None, None) => Ok(None),
(None, other @ Some(_)) | (other @ Some(_), None) => Ok(other.cloned()),

// If each has a single type, it must match.
(Some(SingleOrVec::Single(aa)), Some(SingleOrVec::Single(bb))) => {
if aa == bb {
Ok(Some(SingleOrVec::Single(aa.clone())))
} else {
Err(())
}
}
(Some(aa), Some(bb)) => {
let expand = |types: &SingleOrVec<InstanceType>| {
let mut types = match types {
SingleOrVec::Single(it) => BTreeSet::from([**it]),
SingleOrVec::Vec(types) => types.iter().copied().collect(),
};
// JSON Schema numbers include integers.
if types.contains(&InstanceType::Number) {
types.insert(InstanceType::Integer);
}
types
};

// If one has a single type and the other is an array, the type must
// appear in the array (and that's the resulting type).
(Some(SingleOrVec::Vec(types)), Some(SingleOrVec::Single(it)))
| (Some(SingleOrVec::Single(it)), Some(SingleOrVec::Vec(types))) => {
if types.contains(it) {
Ok(Some(SingleOrVec::Single(it.clone())))
} else {
Err(())
}
}
let mut types = expand(aa)
.intersection(&expand(bb))
.copied()
.collect::<BTreeSet<_>>();

// If both are arrays, we take the intersection; if the intersection is
// empty, we return an error.
(Some(SingleOrVec::Vec(aa)), Some(SingleOrVec::Vec(bb))) => {
let types = aa
.iter()
.collect::<BTreeSet<_>>()
.intersection(&bb.iter().collect::<BTreeSet<_>>())
.cloned()
.cloned()
.collect::<Vec<_>>();
// Integer is redundant when Number is also in the union.
if types.contains(&InstanceType::Number) {
types.remove(&InstanceType::Integer);
}
let types = types.into_iter().collect::<Vec<_>>();

match types.len() {
// No intersection
0 => Err(()),
1 => Ok(Some(types.into_iter().next().unwrap().into())),
_ => Ok(Some(types.into())),
Expand Down Expand Up @@ -1501,6 +1493,8 @@ mod tests {

#[test]
fn test_merge_instance_types() {
use InstanceType::{Integer, Null, Number, String};

// Simple cases
assert_eq!(merge_so_instance_type(None, None), Ok(None));

Expand Down Expand Up @@ -1579,6 +1573,42 @@ mod tests {
),
Err(()),
);

let cases = [
(Number.into(), Integer.into(), Ok(Integer.into())),
(Integer.into(), vec![Number].into(), Ok(Integer.into())),
(
Number.into(),
vec![Integer, Null].into(),
Ok(Integer.into()),
),
(
vec![Number, Null].into(),
vec![Integer, Null].into(),
Ok(vec![Null, Integer].into()),
),
(
Number.into(),
vec![Number, Integer].into(),
Ok(Number.into()),
),
(
vec![Number, Integer].into(),
vec![Number, Integer].into(),
Ok(Number.into()),
),
(vec![Number].into(), vec![String, Null].into(), Err(())),
];

for (a, b, expected) in cases {
for (a, b) in [(&a, &b), (&b, &a)] {
assert_eq!(
merge_so_instance_type(Some(a), Some(b)),
expected.clone().map(Some),
"{a:?} intersect {b:?}",
);
}
}
}

#[test]
Expand Down
10 changes: 10 additions & 0 deletions typify/tests/schemas/numeric-intersection.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"title": "NumericIntersection",
"type": "object",
"required": ["n"],
"properties": {
"n": {
"allOf": [{"type": "number"}, {"type": "integer"}]
}
}
}
77 changes: 77 additions & 0 deletions typify/tests/schemas/numeric-intersection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#![deny(warnings)]
#[doc = "`NumericIntersection`"]
#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)]
pub struct NumericIntersection {
pub n: i64,
}
impl NumericIntersection {
pub fn builder() -> builder::NumericIntersection {
Default::default()
}
}
#[doc = " Types for composing complex structures."]
pub mod builder {
#[derive(Clone, Debug)]
pub struct NumericIntersection {
n: ::std::result::Result<i64, ::std::string::String>,
}
impl ::std::default::Default for NumericIntersection {
fn default() -> Self {
Self {
n: Err("no value supplied for n".to_string()),
}
}
}
impl NumericIntersection {
pub fn n<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<i64>,
T::Error: ::std::fmt::Display,
{
self.n = value
.try_into()
.map_err(|e| format!("error converting supplied value for n: {e}"));
self
}
}
impl ::std::convert::TryFrom<NumericIntersection> for super::NumericIntersection {
type Error = super::error::ConversionError;
fn try_from(
value: NumericIntersection,
) -> ::std::result::Result<Self, super::error::ConversionError> {
Ok(Self { n: value.n? })
}
}
impl ::std::convert::From<super::NumericIntersection> for NumericIntersection {
fn from(value: super::NumericIntersection) -> Self {
Self { n: Ok(value.n) }
}
}
}
#[doc = " Error types."]
pub mod error {
#[doc = r" Error from a `TryFrom` or `FromStr` implementation."]
pub struct ConversionError(::std::borrow::Cow<'static, str>);
impl ::std::error::Error for ConversionError {}
impl ::std::fmt::Display for ConversionError {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> {
::std::fmt::Display::fmt(&self.0, f)
}
}
impl ::std::fmt::Debug for ConversionError {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> {
::std::fmt::Debug::fmt(&self.0, f)
}
}
impl From<&'static str> for ConversionError {
fn from(value: &'static str) -> Self {
Self(value.into())
}
}
impl From<String> for ConversionError {
fn from(value: String) -> Self {
Self(value.into())
}
}
}
fn main() {}