Skip to content

Commit 1c6b1cb

Browse files
authored
split multiple types into anyOf subschema (#20)
1 parent 4a1a838 commit 1c6b1cb

File tree

3 files changed

+409
-32
lines changed

3 files changed

+409
-32
lines changed

src/diff_walker.rs

Lines changed: 130 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use std::collections::BTreeSet;
22

3-
use schemars::schema::{InstanceType, RootSchema, Schema, SchemaObject, SingleOrVec};
3+
use schemars::schema::{
4+
InstanceType, NumberValidation, RootSchema, Schema, SchemaObject, SingleOrVec,
5+
SubschemaValidation,
6+
};
47
use serde_json::Value;
58

69
use crate::{Change, ChangeKind, Error, JsonSchemaType, Range};
@@ -15,6 +18,7 @@ impl DiffWalker {
1518
fn diff_any_of(
1619
&mut self,
1720
json_path: &str,
21+
is_rhs_split: bool,
1822
lhs: &mut SchemaObject,
1923
rhs: &mut SchemaObject,
2024
) -> Result<(), Error> {
@@ -29,9 +33,13 @@ impl DiffWalker {
2933
for (i, (lhs_inner, rhs_inner)) in
3034
lhs_any_of.iter_mut().zip(rhs_any_of.iter_mut()).enumerate()
3135
{
32-
let new_path = format!("{json_path}.<anyOf:{i}>");
33-
self.diff(
36+
let new_path = match is_rhs_split {
37+
true => json_path.to_owned(),
38+
false => format!("{json_path}.<anyOf:{i}>"),
39+
};
40+
self.do_diff(
3441
&new_path,
42+
true,
3543
&mut lhs_inner.clone().into_object(),
3644
&mut rhs_inner.clone().into_object(),
3745
)?;
@@ -149,33 +157,45 @@ impl DiffWalker {
149157
lhs: &mut SchemaObject,
150158
rhs: &mut SchemaObject,
151159
) -> Result<(), Error> {
152-
let mut diff = |lhs, rhs, range| match (lhs, rhs) {
153-
(None, Some(value)) => self.changes.push(Change {
160+
let diff = |lhs, rhs, range| match (lhs, rhs) {
161+
(None, Some(value)) => Some(Change {
154162
path: json_path.to_owned(),
155163
change: ChangeKind::RangeAdd {
156164
added: range,
157165
value,
158166
},
159167
}),
160-
(Some(value), None) => self.changes.push(Change {
168+
(Some(value), None) => Some(Change {
161169
path: json_path.to_owned(),
162170
change: ChangeKind::RangeRemove {
163171
removed: range,
164172
value,
165173
},
166174
}),
167-
(Some(lhs), Some(rhs)) if lhs != rhs => self.changes.push(Change {
175+
(Some(lhs), Some(rhs)) if lhs != rhs => Some(Change {
168176
path: json_path.to_owned(),
169177
change: ChangeKind::RangeChange {
170178
changed: range,
171179
old_value: lhs,
172180
new_value: rhs,
173181
},
174182
}),
175-
_ => (),
183+
_ => None,
176184
};
177-
diff(lhs.number().minimum, rhs.number().minimum, Range::Minimum);
178-
diff(lhs.number().maximum, rhs.number().maximum, Range::Maximum);
185+
if let Some(diff) = diff(
186+
lhs.number_validation().minimum,
187+
rhs.number_validation().minimum,
188+
Range::Minimum,
189+
) {
190+
self.changes.push(diff)
191+
}
192+
if let Some(diff) = diff(
193+
lhs.number_validation().maximum,
194+
rhs.number_validation().maximum,
195+
Range::Maximum,
196+
) {
197+
self.changes.push(diff)
198+
}
179199
Ok(())
180200
}
181201

@@ -319,27 +339,94 @@ impl DiffWalker {
319339
Ok(())
320340
}
321341

322-
pub fn diff(
342+
fn restrictions_for_single_type(schema_object: &SchemaObject, ty: InstanceType) -> Schema {
343+
let mut ret = SchemaObject {
344+
instance_type: Some(SingleOrVec::Single(Box::new(ty))),
345+
..Default::default()
346+
};
347+
match ty {
348+
InstanceType::String => ret.string = schema_object.string.clone(),
349+
InstanceType::Number | InstanceType::Integer => {
350+
ret.number = schema_object.number.clone()
351+
}
352+
InstanceType::Object => ret.object = schema_object.object.clone(),
353+
InstanceType::Array => ret.array = schema_object.array.clone(),
354+
_ => (),
355+
}
356+
Schema::Object(ret)
357+
}
358+
359+
/// Split a schema into multiple schemas, one for each type in the multiple type.
360+
/// Returns the new schema and whether the schema was changed.
361+
fn split_types(schema_object: &mut SchemaObject) -> (&mut SchemaObject, bool) {
362+
let is_split = match schema_object.effective_type() {
363+
InternalJsonSchemaType::Multiple(types)
364+
if schema_object.subschemas().any_of.is_none() =>
365+
{
366+
*schema_object = SchemaObject {
367+
subschemas: Some(Box::new(SubschemaValidation {
368+
any_of: Some(
369+
types
370+
.into_iter()
371+
.map(|ty| {
372+
Self::restrictions_for_single_type(schema_object, ty.into())
373+
})
374+
.collect(),
375+
),
376+
..Default::default()
377+
})),
378+
..Default::default()
379+
};
380+
true
381+
}
382+
_ => false,
383+
};
384+
(schema_object, is_split)
385+
}
386+
387+
fn do_diff(
323388
&mut self,
324389
json_path: &str,
390+
// Whether we are comparing elements in any_of subschemas
391+
comparing_any_of: bool,
325392
lhs: &mut SchemaObject,
326393
rhs: &mut SchemaObject,
327394
) -> Result<(), Error> {
328395
self.resolve_references(lhs, rhs)?;
329-
self.diff_any_of(json_path, lhs, rhs)?;
330-
self.diff_instance_types(json_path, lhs, rhs);
331-
self.diff_properties(json_path, lhs, rhs)?;
332-
self.diff_range(json_path, lhs, rhs)?;
333-
self.diff_additional_properties(json_path, lhs, rhs)?;
334-
self.diff_array_items(json_path, lhs, rhs)?;
335-
self.diff_required(json_path, lhs, rhs)?;
396+
let (lhs, is_lhs_split) = Self::split_types(lhs);
397+
let (rhs, is_rhs_split) = Self::split_types(rhs);
398+
self.diff_any_of(json_path, is_rhs_split, lhs, rhs)?;
399+
if !comparing_any_of {
400+
self.diff_instance_types(json_path, lhs, rhs);
401+
}
402+
// If we split the types, we don't want to compare type-specific properties
403+
// because they are already compared in the `Self::diff_any_of`
404+
if !is_lhs_split && !is_rhs_split {
405+
self.diff_properties(json_path, lhs, rhs)?;
406+
self.diff_range(json_path, lhs, rhs)?;
407+
self.diff_additional_properties(json_path, lhs, rhs)?;
408+
self.diff_array_items(json_path, lhs, rhs)?;
409+
self.diff_required(json_path, lhs, rhs)?;
410+
}
336411
Ok(())
337412
}
413+
414+
pub fn diff(
415+
&mut self,
416+
json_path: &str,
417+
lhs: &mut SchemaObject,
418+
rhs: &mut SchemaObject,
419+
) -> Result<(), Error> {
420+
self.do_diff(json_path, false, lhs, rhs)
421+
}
338422
}
339423

340424
trait JsonSchemaExt {
341425
fn is_true(&self) -> bool;
342426
fn effective_type(&mut self) -> InternalJsonSchemaType;
427+
/// Look for NumberValidation from "number" property in the schema.
428+
/// Check if `anyOf` subschema has NumberValidation, if the subschema is a single type.
429+
fn number_validation(&mut self) -> NumberValidation;
343430
}
344431

345432
impl JsonSchemaExt for SchemaObject {
@@ -350,15 +437,24 @@ impl JsonSchemaExt for SchemaObject {
350437
fn effective_type(&mut self) -> InternalJsonSchemaType {
351438
if let Some(ref ty) = self.instance_type {
352439
match ty {
353-
SingleOrVec::Single(ty) => schemars_to_own(**ty).into(),
440+
SingleOrVec::Single(ty) => JsonSchemaType::from(**ty).into(),
354441
SingleOrVec::Vec(tys) => InternalJsonSchemaType::Multiple(
355-
tys.iter().copied().map(schemars_to_own).collect(),
442+
tys.iter().copied().map(JsonSchemaType::from).collect(),
356443
),
357444
}
358445
} else if let Some(ref constant) = self.const_value {
359446
serde_value_to_own(constant).into()
360447
} else if !self.object().properties.is_empty() {
361448
JsonSchemaType::Object.into()
449+
} else if let Some(ref any_of) = self.subschemas().any_of {
450+
InternalJsonSchemaType::Multiple(
451+
any_of
452+
.iter()
453+
.flat_map(|a| Self::effective_type(&mut a.clone().into_object()).explode())
454+
.collect::<BTreeSet<_>>()
455+
.into_iter()
456+
.collect(),
457+
)
362458
} else if self
363459
.subschemas()
364460
.not
@@ -370,6 +466,20 @@ impl JsonSchemaExt for SchemaObject {
370466
InternalJsonSchemaType::Any
371467
}
372468
}
469+
470+
fn number_validation(&mut self) -> NumberValidation {
471+
let number_validation = self.number().clone();
472+
if number_validation == NumberValidation::default() {
473+
self.subschemas()
474+
.any_of
475+
.as_ref()
476+
.and_then(|a| a.get(0))
477+
.map(|subschema| subschema.clone().into_object().number().clone())
478+
.unwrap_or_default()
479+
} else {
480+
number_validation
481+
}
482+
}
373483
}
374484

375485
#[derive(Clone, Ord, Eq, PartialEq, PartialOrd, Debug)]
@@ -426,15 +536,3 @@ fn serde_value_to_own(val: &Value) -> JsonSchemaType {
426536
Value::Object(_) => JsonSchemaType::Object,
427537
}
428538
}
429-
430-
fn schemars_to_own(other: InstanceType) -> JsonSchemaType {
431-
match other {
432-
InstanceType::Null => JsonSchemaType::Null,
433-
InstanceType::Boolean => JsonSchemaType::Boolean,
434-
InstanceType::Object => JsonSchemaType::Object,
435-
InstanceType::Array => JsonSchemaType::Array,
436-
InstanceType::Number => JsonSchemaType::Number,
437-
InstanceType::String => JsonSchemaType::String,
438-
InstanceType::Integer => JsonSchemaType::Integer,
439-
}
440-
}

0 commit comments

Comments
 (0)