Skip to content

Commit 5f4f52c

Browse files
authored
fix: Implement rudimentary support for anyOf and array items (#6)
1 parent 03840b7 commit 5f4f52c

File tree

1 file changed

+175
-21
lines changed

1 file changed

+175
-21
lines changed

src/lib.rs

Lines changed: 175 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,34 @@ pub enum ChangeKind {
4747
/// The name of the added property.
4848
removed: String,
4949
},
50+
/// An array-type item has been changed from tuple validation to array validation.
51+
///
52+
/// See https://json-schema.org/understanding-json-schema/reference/array.html
53+
///
54+
/// Changes will still be emitted for inner items.
55+
TupleToArray {
56+
/// The length of the (old) tuple
57+
old_length: usize,
58+
},
59+
/// An array-type item has been changed from array validation to tuple validation.
60+
///
61+
/// See https://json-schema.org/understanding-json-schema/reference/array.html
62+
///
63+
/// Changes will still be emitted for inner items.
64+
ArrayToTuple {
65+
/// The length of the (new) tuple
66+
new_length: usize,
67+
},
68+
/// An array-type item with tuple validation has changed its length ("items" array got longer
69+
/// or shorter.
70+
///
71+
/// See https://json-schema.org/understanding-json-schema/reference/array.html
72+
///
73+
/// Changes will still be emitted for inner items.
74+
TupleChange {
75+
/// The new length of the tuple
76+
new_length: usize,
77+
},
5078
}
5179

5280
impl ChangeKind {
@@ -69,6 +97,9 @@ impl ChangeKind {
6997
lhs_additional_properties,
7098
..
7199
} => !*lhs_additional_properties,
100+
Self::TupleToArray { .. } => false,
101+
Self::ArrayToTuple { .. } => true,
102+
Self::TupleChange { .. } => true,
72103
}
73104
}
74105
}
@@ -156,12 +187,14 @@ impl JsonSchemaType {
156187
}
157188
}
158189

159-
#[derive(Default, Eq, PartialEq)]
190+
#[derive(Default, Eq, PartialEq, Debug)]
160191
struct JsonSchema {
161192
ty: Option<JsonSchemaType>,
162193
constant: Option<Value>,
163194
additional_properties: Option<Box<JsonSchema>>,
164195
properties: BTreeMap<String, JsonSchema>,
196+
any_of: Vec<JsonSchema>,
197+
items: Option<JsonSchemaItems>,
165198
}
166199

167200
impl JsonSchema {
@@ -194,8 +227,16 @@ impl<'de> Deserialize<'de> for JsonSchema {
194227
}
195228
}
196229

230+
#[derive(Deserialize, Eq, PartialEq, Debug)]
231+
#[serde(untagged)]
232+
enum JsonSchemaItems {
233+
ExactItems(Vec<JsonSchema>),
234+
Items(Box<JsonSchema>),
235+
}
236+
197237
#[derive(Deserialize, Default)]
198238
#[serde(rename_all = "camelCase")]
239+
#[cfg_attr(test, serde(deny_unknown_fields))]
199240
#[serde(default)]
200241
struct JsonSchemaRaw {
201242
#[serde(rename = "type")]
@@ -204,6 +245,8 @@ struct JsonSchemaRaw {
204245
constant: Option<Value>,
205246
additional_properties: Option<Box<JsonSchema>>,
206247
properties: BTreeMap<String, JsonSchema>,
248+
any_of: Vec<JsonSchema>,
249+
items: Option<JsonSchemaItems>,
207250
}
208251

209252
impl From<JsonSchemaRaw> for JsonSchema {
@@ -213,12 +256,16 @@ impl From<JsonSchemaRaw> for JsonSchema {
213256
constant,
214257
additional_properties,
215258
properties,
259+
any_of,
260+
items,
216261
} = raw;
217262
JsonSchema {
218263
ty,
219264
constant,
220265
additional_properties,
221266
properties,
267+
any_of,
268+
items,
222269
}
223270
}
224271
}
@@ -275,6 +322,18 @@ fn diff_inner(
275322
lhs: &JsonSchema,
276323
rhs: &JsonSchema,
277324
) -> Result<(), Error> {
325+
#[cfg(test)]
326+
{
327+
dbg!(&json_path);
328+
dbg!(&rhs);
329+
dbg!(&lhs);
330+
}
331+
332+
for (i, (lhs_inner, rhs_inner)) in lhs.any_of.iter().zip(rhs.any_of.iter()).enumerate() {
333+
let new_path = format!("{json_path}.<anyOf:{i}>");
334+
diff_inner(rv, new_path, lhs_inner, rhs_inner)?;
335+
}
336+
278337
let lhs_ty = lhs.effective_type().into_set();
279338
let rhs_ty = rhs.effective_type().into_set();
280339

@@ -329,19 +388,15 @@ fn diff_inner(
329388
let lhs_child = lhs.properties.get(common.as_str()).unwrap();
330389
let rhs_child = rhs.properties.get(common.as_str()).unwrap();
331390

332-
let mut new_path = json_path.clone();
333-
new_path.push('.');
334-
new_path.push_str(common);
335-
391+
let new_path = format!("{json_path}.{common}");
336392
diff_inner(rv, new_path, lhs_child, rhs_child)?;
337393
}
338394

339395
if let (Some(ref lhs_additional_properties), Some(ref rhs_additional_properties)) =
340396
(&lhs.additional_properties, &rhs.additional_properties)
341397
{
342398
if rhs_additional_properties != lhs_additional_properties {
343-
let mut new_path = json_path;
344-
new_path.push_str(".<additional properties>");
399+
let new_path = format!("{json_path}.<additionalProperties>");
345400

346401
diff_inner(
347402
rv,
@@ -352,6 +407,62 @@ fn diff_inner(
352407
}
353408
}
354409

410+
match (&lhs.items, &rhs.items) {
411+
(
412+
Some(JsonSchemaItems::ExactItems(lhs_items)),
413+
Some(JsonSchemaItems::ExactItems(rhs_items)),
414+
) => {
415+
if lhs_items.len() != rhs_items.len() {
416+
rv.push(Change {
417+
path: json_path.clone(),
418+
change: ChangeKind::TupleChange {
419+
new_length: rhs_items.len(),
420+
},
421+
});
422+
}
423+
424+
for (i, (lhs_inner, rhs_inner)) in lhs_items.iter().zip(rhs_items.iter()).enumerate() {
425+
let new_path = format!("{json_path}.{i}");
426+
diff_inner(rv, new_path, lhs_inner, rhs_inner)?;
427+
}
428+
}
429+
(Some(JsonSchemaItems::Items(lhs_inner)), Some(JsonSchemaItems::Items(rhs_inner))) => {
430+
let new_path = format!("{json_path}.?");
431+
diff_inner(rv, new_path, lhs_inner, rhs_inner)?;
432+
}
433+
(Some(JsonSchemaItems::Items(lhs_inner)), Some(JsonSchemaItems::ExactItems(rhs_items))) => {
434+
rv.push(Change {
435+
path: json_path.clone(),
436+
change: ChangeKind::ArrayToTuple {
437+
new_length: rhs_items.len(),
438+
},
439+
});
440+
441+
for (i, rhs_inner) in rhs_items.iter().enumerate() {
442+
let new_path = format!("{json_path}.{i}");
443+
diff_inner(rv, new_path, lhs_inner, rhs_inner)?;
444+
}
445+
}
446+
(Some(JsonSchemaItems::ExactItems(lhs_items)), Some(JsonSchemaItems::Items(rhs_inner))) => {
447+
rv.push(Change {
448+
path: json_path.clone(),
449+
change: ChangeKind::TupleToArray {
450+
old_length: lhs_items.len(),
451+
},
452+
});
453+
454+
for (i, lhs_inner) in lhs_items.iter().enumerate() {
455+
let new_path = format!("{json_path}.{i}");
456+
diff_inner(rv, new_path, lhs_inner, rhs_inner)?;
457+
}
458+
}
459+
(None, None) => (),
460+
_ => {
461+
#[cfg(test)]
462+
todo!("{:?} {:?}", lhs.items, rhs.items)
463+
}
464+
}
465+
355466
Ok(())
356467
}
357468

@@ -478,43 +589,43 @@ mod tests {
478589
@r###"
479590
[
480591
Change {
481-
path: ".<additional properties>",
592+
path: ".<additionalProperties>",
482593
change: TypeAdd {
483594
added: String,
484595
},
485596
},
486597
Change {
487-
path: ".<additional properties>",
598+
path: ".<additionalProperties>",
488599
change: TypeAdd {
489600
added: Number,
490601
},
491602
},
492603
Change {
493-
path: ".<additional properties>",
604+
path: ".<additionalProperties>",
494605
change: TypeAdd {
495606
added: Integer,
496607
},
497608
},
498609
Change {
499-
path: ".<additional properties>",
610+
path: ".<additionalProperties>",
500611
change: TypeAdd {
501612
added: Object,
502613
},
503614
},
504615
Change {
505-
path: ".<additional properties>",
616+
path: ".<additionalProperties>",
506617
change: TypeAdd {
507618
added: Array,
508619
},
509620
},
510621
Change {
511-
path: ".<additional properties>",
622+
path: ".<additionalProperties>",
512623
change: TypeAdd {
513624
added: Boolean,
514625
},
515626
},
516627
Change {
517-
path: ".<additional properties>",
628+
path: ".<additionalProperties>",
518629
change: TypeAdd {
519630
added: Null,
520631
},
@@ -536,43 +647,43 @@ mod tests {
536647
@r###"
537648
[
538649
Change {
539-
path: ".<additional properties>",
650+
path: ".<additionalProperties>",
540651
change: TypeRemove {
541652
removed: String,
542653
},
543654
},
544655
Change {
545-
path: ".<additional properties>",
656+
path: ".<additionalProperties>",
546657
change: TypeRemove {
547658
removed: Number,
548659
},
549660
},
550661
Change {
551-
path: ".<additional properties>",
662+
path: ".<additionalProperties>",
552663
change: TypeRemove {
553664
removed: Integer,
554665
},
555666
},
556667
Change {
557-
path: ".<additional properties>",
668+
path: ".<additionalProperties>",
558669
change: TypeRemove {
559670
removed: Object,
560671
},
561672
},
562673
Change {
563-
path: ".<additional properties>",
674+
path: ".<additionalProperties>",
564675
change: TypeRemove {
565676
removed: Array,
566677
},
567678
},
568679
Change {
569-
path: ".<additional properties>",
680+
path: ".<additionalProperties>",
570681
change: TypeRemove {
571682
removed: Boolean,
572683
},
573684
},
574685
Change {
575-
path: ".<additional properties>",
686+
path: ".<additionalProperties>",
576687
change: TypeRemove {
577688
removed: Null,
578689
},
@@ -737,4 +848,47 @@ mod tests {
737848
"###
738849
);
739850
}
851+
852+
#[test]
853+
fn add_property_in_array_of_anyof() {
854+
// rough shape of a sentry eventstream message
855+
// https://github.com/getsentry/sentry-kafka-schemas/pull/79/files
856+
let lhs = json! {{
857+
"anyOf": [
858+
{
859+
"type": "array",
860+
"items": [
861+
{"const": "start_unmerge"},
862+
{"type": "object"}
863+
]
864+
}
865+
]
866+
}};
867+
868+
let rhs = json! {{
869+
"anyOf": [
870+
{
871+
"type": "array",
872+
"items": [
873+
{"const": "start_unmerge"},
874+
{"type": "object", "properties": {"transaction_id": {"type": "string"}}}
875+
]
876+
}
877+
]
878+
}};
879+
880+
let diff = diff(lhs, rhs).unwrap();
881+
882+
assert_debug_snapshot!(diff, @r###"
883+
[
884+
Change {
885+
path: ".<anyOf:0>.1",
886+
change: PropertyAdd {
887+
lhs_additional_properties: true,
888+
added: "transaction_id",
889+
},
890+
},
891+
]
892+
"###);
893+
}
740894
}

0 commit comments

Comments
 (0)