serde_json_diff/
lib.rs

1#![doc = include_str!("../README.md")]
2use serde::{ser::SerializeMap, Serialize};
3
4#[derive(Debug, Serialize)]
5#[serde(tag = "entry_difference", rename_all = "snake_case")]
6pub enum EntryDifference {
7    /// An entry from `target` that `source` is missing
8    Missing { value: serde_json::Value },
9    /// An entry that `source` has, and `target` doesn't
10    Extra,
11    /// The entry exists in both JSONs, but the values are different
12    Value { value_diff: Difference },
13}
14
15#[derive(Debug)]
16pub struct DumbMap<K: Serialize, V: Serialize>(pub Vec<(K, V)>);
17
18impl<K: Serialize, V: Serialize> Serialize for DumbMap<K, V> {
19    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
20    where
21        S: serde::Serializer,
22    {
23        let mut map = serializer.serialize_map(Some(self.0.len()))?;
24        for (key, value) in &self.0 {
25            map.serialize_entry(key, value)?;
26        }
27        map.end()
28    }
29}
30
31#[derive(Debug, Serialize)]
32#[serde(tag = "array_difference", rename_all = "snake_case")]
33pub enum ArrayDifference {
34    /// `source` and `target` are the same length, but some values of the same indices are different
35    PairsOnly {
36        /// differing pairs that appear in the overlapping indices of `source` and `target`
37        different_pairs: DumbMap<usize, Difference>,
38    },
39    /// `source` is shorter than `target`
40    Shorter {
41        /// differing pairs that appear in the overlapping indices of `source` and `target`
42        different_pairs: Option<DumbMap<usize, Difference>>,
43        /// elements missing in `source` that appear in `target`
44        missing_elements: Vec<serde_json::Value>,
45    },
46    /// `source` is longer than `target`
47    Longer {
48        /// differing pairs that appear in the overlapping indices of `source` and `target`
49        different_pairs: Option<DumbMap<usize, Difference>>,
50        /// The amount of extra elements `source` has that `target` does not
51        extra_length: usize,
52    },
53}
54
55#[derive(Debug, Serialize)]
56#[serde(rename_all = "snake_case")]
57pub enum Type {
58    Null,
59    Array,
60    Bool,
61    Object,
62    String,
63    Number,
64}
65
66#[derive(Debug, Serialize)]
67#[serde(untagged)]
68pub enum ScalarDifference {
69    Bool {
70        source: bool,
71        target: bool,
72    },
73    String {
74        source: String,
75        target: String,
76    },
77    Number {
78        source: serde_json::Number,
79        target: serde_json::Number,
80    },
81}
82
83#[derive(Debug, Serialize)]
84#[serde(tag = "difference_of", rename_all = "snake_case")]
85pub enum Difference {
86    Scalar(ScalarDifference),
87    Type {
88        source_type: Type,
89        target_type: Type,
90        target_value: serde_json::Value,
91    },
92    Array(ArrayDifference),
93    Object {
94        different_entries: DumbMap<String, EntryDifference>,
95    },
96}
97
98#[must_use]
99// FIXME: This feels pretty overwrought compared to `objects` and `values`. Maybe there's a better way to diff arrays...
100pub fn arrays(
101    source: Vec<serde_json::Value>,
102    target: Vec<serde_json::Value>,
103) -> Option<ArrayDifference> {
104    let mut source_iter = source.into_iter().enumerate().peekable();
105    let mut target_iter = target.into_iter().peekable();
106
107    let mut different_pairs = vec![];
108    while let (Some(_), Some(_)) = (source_iter.peek(), target_iter.peek()) {
109        let (Some((i, source)), Some(target)) = (source_iter.next(), target_iter.next()) else {
110            unreachable!("checked by peek()");
111        };
112        different_pairs.push(values(source, target).map(|diff| (i, diff)));
113    }
114    let different_pairs = different_pairs.into_iter().flatten().collect::<Vec<_>>();
115    let different_pairs = if different_pairs.is_empty() {
116        None
117    } else {
118        Some(DumbMap(different_pairs))
119    };
120
121    let extra_elements = source_iter.map(|(_, source)| source).collect::<Vec<_>>();
122    let missing_elements = target_iter.collect::<Vec<_>>();
123
124    if !extra_elements.is_empty() {
125        return Some(ArrayDifference::Longer {
126            different_pairs,
127            extra_length: extra_elements.len(),
128        });
129    }
130
131    if !missing_elements.is_empty() {
132        return Some(ArrayDifference::Shorter {
133            different_pairs,
134            missing_elements,
135        });
136    }
137
138    different_pairs.map(|different_pairs| ArrayDifference::PairsOnly { different_pairs })
139}
140
141#[must_use]
142pub fn objects(
143    source: serde_json::Map<String, serde_json::Value>,
144    mut target: serde_json::Map<String, serde_json::Value>,
145) -> Option<DumbMap<String, EntryDifference>> {
146    let mut value_differences = source
147        .into_iter()
148        .filter_map(|(key, source)| {
149            let Some(target) = target.remove(&key) else {
150                return Some((key, EntryDifference::Extra));
151            };
152
153            values(source, target).map(|diff| (key, EntryDifference::Value { value_diff: diff }))
154        })
155        .collect::<Vec<_>>();
156
157    value_differences.extend(target.into_iter().map(|(missing_key, missing_value)| {
158        (
159            missing_key,
160            EntryDifference::Missing {
161                value: missing_value,
162            },
163        )
164    }));
165
166    if value_differences.is_empty() {
167        None
168    } else {
169        Some(DumbMap(value_differences))
170    }
171}
172
173impl From<serde_json::Value> for Type {
174    fn from(value: serde_json::Value) -> Self {
175        match value {
176            serde_json::Value::Null => Type::Null,
177            serde_json::Value::Bool(_) => Type::Bool,
178            serde_json::Value::Number(_) => Type::Number,
179            serde_json::Value::String(_) => Type::String,
180            serde_json::Value::Array(_) => Type::Array,
181            serde_json::Value::Object(_) => Type::Object,
182        }
183    }
184}
185
186#[must_use]
187pub fn values(source: serde_json::Value, target: serde_json::Value) -> Option<Difference> {
188    use serde_json::Value::{Array, Bool, Null, Number, Object, String};
189
190    match (source, target) {
191        (Null, Null) => None,
192        (Bool(source), Bool(target)) => {
193            if source == target {
194                None
195            } else {
196                Some(Difference::Scalar(ScalarDifference::Bool {
197                    source,
198                    target,
199                }))
200            }
201        }
202        (Number(source), Number(target)) => {
203            if source == target {
204                None
205            } else {
206                Some(Difference::Scalar(ScalarDifference::Number {
207                    source,
208                    target,
209                }))
210            }
211        }
212        (String(source), String(target)) => {
213            if source == target {
214                None
215            } else {
216                Some(Difference::Scalar(ScalarDifference::String {
217                    source,
218                    target,
219                }))
220            }
221        }
222        (Array(source), Array(target)) => arrays(source, target).map(Difference::Array),
223        (Object(source), Object(target)) => objects(source, target)
224            .map(|different_entries| Difference::Object { different_entries }),
225        (source, target) => Some(Difference::Type {
226            source_type: source.into(),
227            target_type: target.clone().into(),
228            target_value: target,
229        }),
230    }
231}