Skip to content

Commit 0276ef2

Browse files
6293untitaker
andauthored
compare anyOf based on handmade diff score (#32)
Co-authored-by: Markus Unterwaditzer <markus-tarpit+git@unterwaditzer.net>
1 parent 1b5c7b4 commit 0276ef2

11 files changed

+284
-33
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ schemars = { version = "0.8.12", default_features = false }
2525
serde = "1.0.158"
2626
serde_json = "1.0.94"
2727
thiserror = "1.0.40"
28+
pathfinding = "4.2.1"
2829

2930
[features]
3031
build-binary = ["clap", "anyhow"]

src/diff_walker.rs

Lines changed: 62 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,21 @@ use serde_json::Value;
88

99
use crate::{Change, ChangeKind, Error, JsonSchemaType, Range};
1010

11-
pub struct DiffWalker {
12-
pub changes: Vec<Change>,
11+
pub struct DiffWalker<F: FnMut(Change)> {
12+
pub cb: F,
1313
pub lhs_root: RootSchema,
1414
pub rhs_root: RootSchema,
1515
}
1616

17-
impl DiffWalker {
17+
impl<F: FnMut(Change)> DiffWalker<F> {
18+
pub fn new(cb: F, lhs_root: RootSchema, rhs_root: RootSchema) -> Self {
19+
Self {
20+
cb,
21+
lhs_root,
22+
rhs_root,
23+
}
24+
}
25+
1826
fn diff_any_of(
1927
&mut self,
2028
json_path: &str,
@@ -27,21 +35,47 @@ impl DiffWalker {
2735
if let (Some(lhs_any_of), Some(rhs_any_of)) =
2836
(&mut lhs.subschemas().any_of, &mut rhs.subschemas().any_of)
2937
{
30-
lhs_any_of.sort_by_cached_key(|x| format!("{x:?}"));
31-
rhs_any_of.sort_by_cached_key(|x| format!("{x:?}"));
32-
33-
for (i, (lhs_inner, rhs_inner)) in
34-
lhs_any_of.iter_mut().zip(rhs_any_of.iter_mut()).enumerate()
35-
{
38+
match (lhs_any_of.len(), rhs_any_of.len()) {
39+
(l, r) if l <= r => {
40+
lhs_any_of.append(&mut vec![Schema::Bool(false); r - l]);
41+
}
42+
(l, r) => {
43+
rhs_any_of.append(&mut vec![Schema::Bool(false); l - r]);
44+
}
45+
}
46+
let max_len = lhs_any_of.len().max(rhs_any_of.len());
47+
lhs_any_of.resize(max_len, Schema::Bool(false));
48+
rhs_any_of.resize(max_len, Schema::Bool(false));
49+
50+
let mut mat = pathfinding::matrix::Matrix::new(max_len, max_len, 0i32);
51+
for (i, l) in lhs_any_of.iter_mut().enumerate() {
52+
for (j, r) in rhs_any_of.iter_mut().enumerate() {
53+
let mut count = 0;
54+
let counter = |_change: Change| count += 1;
55+
DiffWalker::new(
56+
Box::new(counter) as Box<dyn FnMut(Change)>,
57+
self.lhs_root.clone(),
58+
self.rhs_root.clone(),
59+
)
60+
.diff(
61+
"",
62+
&mut l.clone().into_object(),
63+
&mut r.clone().into_object(),
64+
)?;
65+
mat[(i, j)] = count;
66+
}
67+
}
68+
let pairs = pathfinding::kuhn_munkres::kuhn_munkres_min(&mat).1;
69+
for i in 0..max_len {
3670
let new_path = match is_rhs_split {
3771
true => json_path.to_owned(),
38-
false => format!("{json_path}.<anyOf:{i}>"),
72+
false => format!("{json_path}.<anyOf:{}>", pairs[i]),
3973
};
4074
self.do_diff(
4175
&new_path,
4276
true,
43-
&mut lhs_inner.clone().into_object(),
44-
&mut rhs_inner.clone().into_object(),
77+
&mut lhs_any_of[i].clone().into_object(),
78+
&mut rhs_any_of[pairs[i]].clone().into_object(),
4579
)?;
4680
}
4781
}
@@ -59,7 +93,7 @@ impl DiffWalker {
5993
let rhs_ty = rhs.effective_type().into_set();
6094

6195
for removed in lhs_ty.difference(&rhs_ty) {
62-
self.changes.push(Change {
96+
(self.cb)(Change {
6397
path: json_path.to_owned(),
6498
change: ChangeKind::TypeRemove {
6599
removed: removed.clone(),
@@ -68,7 +102,7 @@ impl DiffWalker {
68102
}
69103

70104
for added in rhs_ty.difference(&lhs_ty) {
71-
self.changes.push(Change {
105+
(self.cb)(Change {
72106
path: json_path.to_owned(),
73107
change: ChangeKind::TypeAdd {
74108
added: added.clone(),
@@ -81,25 +115,25 @@ impl DiffWalker {
81115
Self::normalize_const(lhs);
82116
Self::normalize_const(rhs);
83117
match (&lhs.const_value, &rhs.const_value) {
84-
(Some(value), None) => self.changes.push(Change {
118+
(Some(value), None) => (self.cb)(Change {
85119
path: json_path.to_owned(),
86120
change: ChangeKind::ConstRemove {
87121
removed: value.clone(),
88122
},
89123
}),
90-
(None, Some(value)) => self.changes.push(Change {
124+
(None, Some(value)) => (self.cb)(Change {
91125
path: json_path.to_owned(),
92126
change: ChangeKind::ConstAdd {
93127
added: value.clone(),
94128
},
95129
}),
96130
(Some(l), Some(r)) if l != r => {
97131
if l.is_object() && r.is_object() {}
98-
self.changes.push(Change {
132+
(self.cb)(Change {
99133
path: json_path.to_owned(),
100134
change: ChangeKind::ConstRemove { removed: l.clone() },
101135
});
102-
self.changes.push(Change {
136+
(self.cb)(Change {
103137
path: json_path.to_owned(),
104138
change: ChangeKind::ConstAdd { added: r.clone() },
105139
});
@@ -124,7 +158,7 @@ impl DiffWalker {
124158
.map_or(true, |x| x.clone().into_object().is_true());
125159

126160
for removed in lhs_props.difference(&rhs_props) {
127-
self.changes.push(Change {
161+
(self.cb)(Change {
128162
path: json_path.to_owned(),
129163
change: ChangeKind::PropertyRemove {
130164
lhs_additional_properties,
@@ -134,7 +168,7 @@ impl DiffWalker {
134168
}
135169

136170
for added in rhs_props.difference(&lhs_props) {
137-
self.changes.push(Change {
171+
(self.cb)(Change {
138172
path: json_path.to_owned(),
139173
change: ChangeKind::PropertyAdd {
140174
lhs_additional_properties,
@@ -218,14 +252,14 @@ impl DiffWalker {
218252
rhs.number_validation().minimum,
219253
Range::Minimum,
220254
) {
221-
self.changes.push(diff)
255+
(self.cb)(diff)
222256
}
223257
if let Some(diff) = diff(
224258
lhs.number_validation().maximum,
225259
rhs.number_validation().maximum,
226260
Range::Maximum,
227261
) {
228-
self.changes.push(diff)
262+
(self.cb)(diff)
229263
}
230264
Ok(())
231265
}
@@ -239,7 +273,7 @@ impl DiffWalker {
239273
match (&lhs.array().items, &rhs.array().items) {
240274
(Some(SingleOrVec::Vec(lhs_items)), Some(SingleOrVec::Vec(rhs_items))) => {
241275
if lhs_items.len() != rhs_items.len() {
242-
self.changes.push(Change {
276+
(self.cb)(Change {
243277
path: json_path.to_owned(),
244278
change: ChangeKind::TupleChange {
245279
new_length: rhs_items.len(),
@@ -267,7 +301,7 @@ impl DiffWalker {
267301
)?;
268302
}
269303
(Some(SingleOrVec::Single(lhs_inner)), Some(SingleOrVec::Vec(rhs_items))) => {
270-
self.changes.push(Change {
304+
(self.cb)(Change {
271305
path: json_path.to_owned(),
272306
change: ChangeKind::ArrayToTuple {
273307
new_length: rhs_items.len(),
@@ -284,7 +318,7 @@ impl DiffWalker {
284318
}
285319
}
286320
(Some(SingleOrVec::Vec(lhs_items)), Some(SingleOrVec::Single(rhs_inner))) => {
287-
self.changes.push(Change {
321+
(self.cb)(Change {
288322
path: json_path.to_owned(),
289323
change: ChangeKind::TupleToArray {
290324
old_length: lhs_items.len(),
@@ -321,7 +355,7 @@ impl DiffWalker {
321355
let rhs_required = &rhs.object().required;
322356

323357
for removed in lhs_required.difference(rhs_required) {
324-
self.changes.push(Change {
358+
(self.cb)(Change {
325359
path: json_path.to_owned(),
326360
change: ChangeKind::RequiredRemove {
327361
property: removed.clone(),
@@ -330,7 +364,7 @@ impl DiffWalker {
330364
}
331365

332366
for added in rhs_required.difference(lhs_required) {
333-
self.changes.push(Change {
367+
(self.cb)(Change {
334368
path: json_path.to_owned(),
335369
change: ChangeKind::RequiredAdd {
336370
property: added.clone(),
@@ -532,6 +566,7 @@ impl JsonSchemaExt for SchemaObject {
532566
self.subschemas()
533567
.any_of
534568
.as_ref()
569+
.filter(|schemas| schemas.len() == 1)
535570
.and_then(|a| a.get(0))
536571
.map(|subschema| subschema.clone().into_object().number().clone())
537572
.unwrap_or_default()

src/lib.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,20 @@ pub use types::*;
1414
///
1515
/// `lhs` (left-hand side) is the old schema, `rhs` (right-hand side) is the new schema.
1616
pub fn diff(lhs: Value, rhs: Value) -> Result<Vec<Change>, Error> {
17-
let changes = Vec::new();
1817
let lhs_root: RootSchema = serde_json::from_value(lhs)?;
1918
let rhs_root: RootSchema = serde_json::from_value(rhs)?;
2019

21-
let mut walker = diff_walker::DiffWalker {
22-
changes,
23-
lhs_root,
24-
rhs_root,
20+
let mut changes = vec![];
21+
let cb = |change: Change| {
22+
changes.push(change);
2523
};
24+
let mut walker = diff_walker::DiffWalker::new(Box::new(cb), lhs_root, rhs_root);
2625
walker.diff(
2726
"",
2827
&mut walker.lhs_root.schema.clone(),
2928
&mut walker.rhs_root.schema.clone(),
3029
)?;
30+
drop(walker);
3131

32-
Ok(walker.changes)
32+
Ok(changes)
3333
}

tests/fixtures/any_of/number_1.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"lhs": {
3+
"anyOf": [
4+
{"type": "number", "maximum": 10},
5+
{"type": "number", "minimum": 1},
6+
{"type": "number", "minimum": 100, "maximum": 200}
7+
]
8+
},
9+
"rhs": {
10+
"anyOf": [
11+
{"type": "number", "minimum": 7, "maximum": 14},
12+
{"type": "number", "maximum": 3},
13+
{"type": "number", "minimum": 2}
14+
]
15+
}
16+
}

tests/fixtures/any_of/objects_1.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"lhs": {
3+
"anyOf": [
4+
{"properties": {"foo": {}}},
5+
{"properties": {"type": {"const": "bar"}}}
6+
]
7+
},
8+
"rhs": {
9+
"anyOf": [
10+
{
11+
"title": "replay_recording",
12+
"type": "object",
13+
"properties": {"foo": {}}
14+
},
15+
{"properties": {"type": {"const": "bar"}}}
16+
]
17+
}
18+
}

tests/fixtures/any_of/objects_2.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"lhs": {
3+
"anyOf": [
4+
{"properties": {"foo": {}}},
5+
{"properties": {"type": {"const": "bar"}}}
6+
]
7+
},
8+
"rhs": {
9+
"anyOf": [
10+
{ "type": "boolean" },
11+
{
12+
"title": "replay_recording",
13+
"type": "object",
14+
"properties": {"foo": {}}
15+
},
16+
{"properties": {"type": {"const": "bar"}}}
17+
]
18+
}
19+
}

tests/fixtures/any_of/objects_3.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"lhs": {
3+
"anyOf": [
4+
{ "type": "boolean" },
5+
{"properties": {"foo": {}}},
6+
{"properties": {"type": {"const": "bar"}}}
7+
]
8+
},
9+
"rhs": {
10+
"anyOf": [
11+
{
12+
"title": "replay_recording",
13+
"type": "object",
14+
"properties": {"foo": {}}
15+
},
16+
{"properties": {"type": {"const": "bar"}}}
17+
]
18+
}
19+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
source: tests/test.rs
3+
expression: diff
4+
info:
5+
lhs:
6+
anyOf:
7+
- maximum: 10
8+
type: number
9+
- minimum: 1
10+
type: number
11+
- maximum: 200
12+
minimum: 100
13+
type: number
14+
rhs:
15+
anyOf:
16+
- maximum: 14
17+
minimum: 7
18+
type: number
19+
- maximum: 3
20+
type: number
21+
- minimum: 2
22+
type: number
23+
input_file: tests/fixtures/any_of/number_1.json
24+
---
25+
[
26+
Change {
27+
path: ".<anyOf:1>",
28+
change: RangeChange {
29+
changed: Maximum,
30+
old_value: 10.0,
31+
new_value: 3.0,
32+
},
33+
},
34+
Change {
35+
path: ".<anyOf:2>",
36+
change: RangeChange {
37+
changed: Minimum,
38+
old_value: 1.0,
39+
new_value: 2.0,
40+
},
41+
},
42+
Change {
43+
path: ".<anyOf:0>",
44+
change: RangeChange {
45+
changed: Minimum,
46+
old_value: 100.0,
47+
new_value: 7.0,
48+
},
49+
},
50+
Change {
51+
path: ".<anyOf:0>",
52+
change: RangeChange {
53+
changed: Maximum,
54+
old_value: 200.0,
55+
new_value: 14.0,
56+
},
57+
},
58+
]

0 commit comments

Comments
 (0)