1use crate::json::JsonValue;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct DiffEntry {
13 pub path: String,
15 pub kind: DiffKind,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum DiffKind {
22 Changed { actual: String, expected: String },
24 AddedInActual { actual: String },
26 MissingFromActual { expected: String },
28 TypeChanged { actual: String, expected: String },
30}
31
32pub fn diff(actual: &JsonValue, expected: &JsonValue) -> Vec<DiffEntry> {
35 let mut out = Vec::new();
36 walk("", actual, expected, &mut out);
37 out
38}
39
40fn kind_name(v: &JsonValue) -> &'static str {
41 match v {
42 JsonValue::Null => "null",
43 JsonValue::Bool(_) => "bool",
44 JsonValue::Number(_) => "number",
45 JsonValue::String(_) => "string",
46 JsonValue::Array(_) => "array",
47 JsonValue::Object(_) => "object",
48 }
49}
50
51fn render(v: &JsonValue) -> String {
52 crate::json::to_string(v)
53}
54
55fn walk(path: &str, a: &JsonValue, e: &JsonValue, out: &mut Vec<DiffEntry>) {
56 match (a, e) {
57 (JsonValue::Object(am), JsonValue::Object(em)) => {
58 for (k, av) in am {
60 let child = format!("{}/{}", path, k);
61 match em.iter().find(|(ek, _)| ek == k) {
62 Some((_, ev)) => walk(&child, av, ev, out),
63 None => out.push(DiffEntry {
64 path: child,
65 kind: DiffKind::AddedInActual { actual: render(av) },
66 }),
67 }
68 }
69 for (k, ev) in em {
71 if !am.iter().any(|(ak, _)| ak == k) {
72 out.push(DiffEntry {
73 path: format!("{}/{}", path, k),
74 kind: DiffKind::MissingFromActual { expected: render(ev) },
75 });
76 }
77 }
78 }
79 (JsonValue::Array(aa), JsonValue::Array(ea)) => {
80 let n = aa.len().max(ea.len());
81 for i in 0..n {
82 let child = format!("{}/{}", path, i);
83 match (aa.get(i), ea.get(i)) {
84 (Some(av), Some(ev)) => walk(&child, av, ev, out),
85 (Some(av), None) => out.push(DiffEntry {
86 path: child,
87 kind: DiffKind::AddedInActual { actual: render(av) },
88 }),
89 (None, Some(ev)) => out.push(DiffEntry {
90 path: child,
91 kind: DiffKind::MissingFromActual { expected: render(ev) },
92 }),
93 (None, None) => {}
94 }
95 }
96 }
97 _ => {
98 if kind_name(a) != kind_name(e) {
99 out.push(DiffEntry {
100 path: path_or_root(path),
101 kind: DiffKind::TypeChanged { actual: render(a), expected: render(e) },
102 });
103 } else if a != e {
104 out.push(DiffEntry {
105 path: path_or_root(path),
106 kind: DiffKind::Changed { actual: render(a), expected: render(e) },
107 });
108 }
109 }
110 }
111}
112
113fn path_or_root(path: &str) -> String {
114 if path.is_empty() {
115 "/".to_string()
116 } else {
117 path.to_string()
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124 use crate::json::parse;
125
126 #[test]
127 fn identical_packets_have_no_diff() {
128 let a = parse("{\"record\":\"R\",\"fields\":{\"A\":\"1\"}}").unwrap();
129 assert!(diff(&a, &a).is_empty());
130 }
131
132 #[test]
133 fn changed_value() {
134 let a = parse("{\"fields\":{\"AMT\":\"12.50\"}}").unwrap();
135 let e = parse("{\"fields\":{\"AMT\":\"99.99\"}}").unwrap();
136 let d = diff(&a, &e);
137 assert_eq!(d.len(), 1);
138 assert_eq!(d[0].path, "/fields/AMT");
139 assert_eq!(
140 d[0].kind,
141 DiffKind::Changed { actual: "\"12.50\"".into(), expected: "\"99.99\"".into() }
142 );
143 }
144
145 #[test]
146 fn added_and_missing_keys() {
147 let a = parse("{\"fields\":{\"A\":\"1\",\"B\":\"2\"}}").unwrap();
148 let e = parse("{\"fields\":{\"A\":\"1\",\"C\":\"3\"}}").unwrap();
149 let d = diff(&a, &e);
150 assert!(d.iter().any(|x| x.path == "/fields/B"
152 && matches!(x.kind, DiffKind::AddedInActual { .. })));
153 assert!(d.iter().any(|x| x.path == "/fields/C"
154 && matches!(x.kind, DiffKind::MissingFromActual { .. })));
155 }
156
157 #[test]
158 fn type_changed() {
159 let a = parse("{\"x\":\"1\"}").unwrap();
160 let e = parse("{\"x\":1}").unwrap();
161 let d = diff(&a, &e);
162 assert_eq!(d.len(), 1);
163 assert!(matches!(d[0].kind, DiffKind::TypeChanged { .. }));
164 }
165}