1use std::collections::BTreeSet;
12
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
18pub struct SchemaDiff {
19 pub from: u32,
20 pub to: u32,
21 pub added_paths: Vec<String>,
22 pub removed_paths: Vec<String>,
23 pub changed_types: Vec<TypeChange>,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
28pub struct TypeChange {
29 pub path: String,
30 pub old_type: String,
31 pub new_type: String,
32}
33
34#[must_use]
41pub fn diff(from_version: u32, to_version: u32, from: &Value, to: &Value) -> SchemaDiff {
42 let mut added = Vec::new();
43 let mut removed = Vec::new();
44 let mut changed = Vec::new();
45 walk(from, to, "", &mut added, &mut removed, &mut changed);
46 added.sort();
47 removed.sort();
48 changed.sort_by(|a, b| a.path.cmp(&b.path));
49 SchemaDiff {
50 from: from_version,
51 to: to_version,
52 added_paths: added,
53 removed_paths: removed,
54 changed_types: changed,
55 }
56}
57
58fn walk(
59 from: &Value,
60 to: &Value,
61 path: &str,
62 added: &mut Vec<String>,
63 removed: &mut Vec<String>,
64 changed: &mut Vec<TypeChange>,
65) {
66 match (from, to) {
67 (Value::Object(a), Value::Object(b)) => {
68 let keys: BTreeSet<&String> = a.keys().chain(b.keys()).collect();
69 for key in keys {
70 let child_path = format!("{path}/{}", escape_pointer_segment(key));
71 match (a.get(key), b.get(key)) {
72 (None, Some(_)) => added.push(child_path),
73 (Some(_), None) => removed.push(child_path),
74 (Some(av), Some(bv)) => walk(av, bv, &child_path, added, removed, changed),
75 (None, None) => unreachable!("key from union"),
76 }
77 }
78 }
79 (Value::Array(a), Value::Array(b)) => {
80 let max = a.len().max(b.len());
81 for idx in 0..max {
82 let child_path = format!("{path}/{idx}");
83 match (a.get(idx), b.get(idx)) {
84 (None, Some(_)) => added.push(child_path),
85 (Some(_), None) => removed.push(child_path),
86 (Some(av), Some(bv)) => walk(av, bv, &child_path, added, removed, changed),
87 (None, None) => unreachable!("index in range"),
88 }
89 }
90 }
91 (a, b) if json_type_name(a) != json_type_name(b) => {
92 changed.push(TypeChange {
93 path: path.to_owned(),
94 old_type: json_type_name(a).to_owned(),
95 new_type: json_type_name(b).to_owned(),
96 });
97 }
98 _ => {}
99 }
100}
101
102fn escape_pointer_segment(key: &str) -> String {
103 key.replace('~', "~0").replace('/', "~1")
104}
105
106fn json_type_name(v: &Value) -> &'static str {
107 match v {
108 Value::Null => "null",
109 Value::Bool(_) => "bool",
110 Value::Number(n) if n.is_i64() || n.is_u64() => "integer",
111 Value::Number(_) => "number",
112 Value::String(_) => "string",
113 Value::Array(_) => "array",
114 Value::Object(_) => "object",
115 }
116}