Skip to main content

semdiff_differ_json/
lib.rs

1use mime::Mime;
2use semdiff_core::fs::FileLeaf;
3use semdiff_core::{Diff, DiffCalculator, MayUnsupported};
4use serde_json::Value;
5use similar::algorithms::DiffHook;
6use std::cmp::Reverse;
7use std::collections::BinaryHeap;
8use std::fmt::Display;
9use std::{convert, fmt};
10
11pub mod report_html;
12pub mod report_json;
13pub mod report_summary;
14
15#[cfg(test)]
16mod tests;
17
18#[derive(Debug, Clone, Copy, Default)]
19pub struct JsonDiffReporter;
20
21#[derive(Debug)]
22enum JsonDiffBody {
23    Equal(String),
24    Modified(Vec<JsonDiffLine>),
25}
26
27#[derive(Debug)]
28pub struct JsonDiff {
29    body: JsonDiffBody,
30}
31
32impl Diff for JsonDiff {
33    fn equal(&self) -> bool {
34        matches!(self.body, JsonDiffBody::Equal(_))
35    }
36}
37
38impl JsonDiff {
39    fn body(&self) -> &JsonDiffBody {
40        &self.body
41    }
42}
43
44#[derive(Debug, Clone, Copy)]
45pub struct JsonDiffCalculator {
46    ignore_object_key_order: bool,
47}
48
49impl Default for JsonDiffCalculator {
50    fn default() -> Self {
51        Self::new(false)
52    }
53}
54
55impl JsonDiffCalculator {
56    pub fn new(ignore_object_key_order: bool) -> Self {
57        Self {
58            ignore_object_key_order,
59        }
60    }
61
62    pub fn ignore_object_key_order(&self) -> bool {
63        self.ignore_object_key_order
64    }
65}
66
67impl DiffCalculator<FileLeaf> for JsonDiffCalculator {
68    type Error = convert::Infallible;
69    type Diff = JsonDiff;
70
71    fn diff(
72        &self,
73        _name: &str,
74        expected: FileLeaf,
75        actual: FileLeaf,
76    ) -> Result<MayUnsupported<Self::Diff>, Self::Error> {
77        if !is_json_mime(&expected.kind) || !is_json_mime(&actual.kind) {
78            return Ok(MayUnsupported::Unsupported);
79        }
80        let Ok(mut expected) = serde_json::from_slice::<Value>(&expected.content) else {
81            return Ok(MayUnsupported::Unsupported);
82        };
83        let Ok(mut actual) = serde_json::from_slice::<Value>(&actual.content) else {
84            return Ok(MayUnsupported::Unsupported);
85        };
86        if self.ignore_object_key_order {
87            expected.sort_all_objects();
88            actual.sort_all_objects();
89        }
90        let diff = json_diff(&expected, &actual);
91        let body = if diff
92            .iter()
93            .all(|d| matches!(d.state, JsonDiffLineState::Unchanged { .. }))
94        {
95            JsonDiffBody::Equal(serde_json::to_string_pretty(&expected).unwrap())
96        } else {
97            JsonDiffBody::Modified(diff)
98        };
99        let result = JsonDiff { body };
100        Ok(MayUnsupported::Ok(result))
101    }
102}
103
104fn is_json_mime(kind: &Mime) -> bool {
105    if kind == &mime::APPLICATION_JSON {
106        return true;
107    }
108    if kind.type_() == mime::APPLICATION
109        && let Some(suffix) = kind.subtype().as_str().strip_suffix("+json")
110    {
111        return !suffix.is_empty();
112    }
113    kind.essence_str() == "text/json"
114}
115
116fn try_into_json(content: &[u8]) -> Option<String> {
117    let value = serde_json::from_slice::<Value>(content).ok()?;
118    Some(serde_json::to_string_pretty(&value).unwrap())
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122enum ChangeTag {
123    Unchanged,
124    Added,
125    Deleted,
126}
127
128#[derive(Debug)]
129struct JsonDiffLine {
130    indent: usize,
131    state: JsonDiffLineState,
132}
133
134impl JsonDiffLine {
135    fn tag(&self) -> ChangeTag {
136        match self.state {
137            JsonDiffLineState::Unchanged { .. } => ChangeTag::Unchanged,
138            JsonDiffLineState::Added(_) => ChangeTag::Added,
139            JsonDiffLineState::Deleted(_) => ChangeTag::Deleted,
140        }
141    }
142
143    fn display_expected(&self) -> impl Display {
144        fmt::from_fn(|f| {
145            let expected = match &self.state {
146                JsonDiffLineState::Unchanged { expected, .. } => expected,
147                JsonDiffLineState::Added(_) => return Ok(()),
148                JsonDiffLineState::Deleted(expected) => expected,
149            };
150            for _ in 0..self.indent {
151                f.write_str("  ")?;
152            }
153            f.write_str(expected)?;
154            Ok(())
155        })
156    }
157
158    fn display_actual(&self) -> impl Display {
159        fmt::from_fn(|f| {
160            let actual = match &self.state {
161                JsonDiffLineState::Unchanged { actual, .. } => actual,
162                JsonDiffLineState::Added(actual) => actual,
163                JsonDiffLineState::Deleted(_) => return Ok(()),
164            };
165            for _ in 0..self.indent {
166                f.write_str("  ")?;
167            }
168            f.write_str(actual)?;
169            Ok(())
170        })
171    }
172
173    fn unchanged(indent: usize, expected: String, actual: String) -> JsonDiffLine {
174        JsonDiffLine {
175            indent,
176            state: JsonDiffLineState::Unchanged { expected, actual },
177        }
178    }
179
180    fn added(indent: usize, actual: String) -> JsonDiffLine {
181        JsonDiffLine {
182            indent,
183            state: JsonDiffLineState::Added(actual),
184        }
185    }
186
187    fn deleted(indent: usize, expected: String) -> JsonDiffLine {
188        JsonDiffLine {
189            indent,
190            state: JsonDiffLineState::Deleted(expected),
191        }
192    }
193}
194
195#[derive(Debug)]
196enum JsonDiffLineState {
197    Unchanged { expected: String, actual: String },
198    Added(String),
199    Deleted(String),
200}
201
202fn json_diff(expected: &Value, actual: &Value) -> Vec<JsonDiffLine> {
203    fn json_array_diff(expected: &[Value], actual: &[Value], indent: usize, result: &mut Vec<JsonDiffLine>) {
204        let mut hook = ArrayDiffHook {
205            indent,
206            expected,
207            actual,
208            result,
209        };
210        similar::algorithms::patience::diff(
211            &mut similar::algorithms::Replace::new(&mut hook),
212            expected,
213            0..expected.len(),
214            actual,
215            0..actual.len(),
216        )
217        .unwrap();
218
219        struct ArrayDiffHook<'a> {
220            indent: usize,
221            expected: &'a [Value],
222            actual: &'a [Value],
223            result: &'a mut Vec<JsonDiffLine>,
224        }
225
226        impl DiffHook for ArrayDiffHook<'_> {
227            type Error = convert::Infallible;
228
229            fn equal(&mut self, old_index: usize, new_index: usize, len: usize) -> Result<(), Self::Error> {
230                for (expected_index, actual_index) in (old_index..).zip(new_index..).take(len) {
231                    let need_extra_comma_expected = expected_index < self.expected.len() - 1;
232                    let need_extra_comma_actual = actual_index < self.actual.len() - 1;
233                    let v = &self.expected[expected_index];
234                    let v = serde_json::to_string_pretty(v).unwrap();
235                    let mut lines = v.lines();
236                    let last_line = lines.next_back().unwrap();
237                    for line in lines {
238                        self.result
239                            .push(JsonDiffLine::unchanged(self.indent, line.to_owned(), line.to_owned()));
240                    }
241                    self.result.push(JsonDiffLine::unchanged(
242                        self.indent,
243                        format!("{}{}", last_line, if need_extra_comma_expected { "," } else { "" }),
244                        format!("{}{}", last_line, if need_extra_comma_actual { "," } else { "" }),
245                    ));
246                }
247                Ok(())
248            }
249
250            fn delete(&mut self, old_index: usize, old_len: usize, _new_index: usize) -> Result<(), Self::Error> {
251                for i in (old_index..).take(old_len) {
252                    let need_extra_comma = i < self.expected.len() - 1;
253                    let v = &self.expected[i];
254                    let v = serde_json::to_string_pretty(v).unwrap();
255                    let mut lines = v.lines();
256                    let last_line = lines.next_back().unwrap();
257                    for line in lines {
258                        self.result.push(JsonDiffLine::deleted(self.indent, line.to_owned()));
259                    }
260                    self.result.push(JsonDiffLine::deleted(
261                        self.indent,
262                        format!("{}{}", last_line, if need_extra_comma { "," } else { "" }),
263                    ));
264                }
265                Ok(())
266            }
267
268            fn insert(&mut self, _old_index: usize, new_index: usize, new_len: usize) -> Result<(), Self::Error> {
269                for i in (new_index..).take(new_len) {
270                    let need_extra_comma = i < self.actual.len() - 1;
271                    let v = &self.actual[i];
272                    let v = serde_json::to_string_pretty(v).unwrap();
273                    let mut lines = v.lines();
274                    let last_line = lines.next_back().unwrap();
275                    for line in lines {
276                        self.result.push(JsonDiffLine::added(self.indent, line.to_owned()));
277                    }
278                    self.result.push(JsonDiffLine::added(
279                        self.indent,
280                        format!("{}{}", last_line, if need_extra_comma { "," } else { "" }),
281                    ));
282                }
283                Ok(())
284            }
285
286            fn replace(
287                &mut self,
288                old_index: usize,
289                old_len: usize,
290                new_index: usize,
291                new_len: usize,
292            ) -> Result<(), Self::Error> {
293                let mut q = BinaryHeap::new();
294                for (expected_index, expected) in self.expected[old_index..][..old_len].iter().enumerate() {
295                    for (actual_index, actual) in self.actual[new_index..][..new_len].iter().enumerate() {
296                        let index_diff = Reverse(expected_index.abs_diff(actual_index));
297                        match (expected, actual) {
298                            (Value::Array(expected), Value::Array(actual)) => q.push((
299                                Reverse(expected.len().abs_diff(actual.len())),
300                                index_diff,
301                                expected_index,
302                                actual_index,
303                            )),
304                            (Value::Object(expected), Value::Object(actual)) => q.push((
305                                Reverse(
306                                    expected.keys().filter(|k| !actual.contains_key(k.as_str())).count()
307                                        + actual.keys().filter(|k| !expected.contains_key(k.as_str())).count(),
308                                ),
309                                index_diff,
310                                expected_index,
311                                actual_index,
312                            )),
313                            _ => {}
314                        }
315                    }
316                }
317                let mut expected_to_actual = vec![None::<usize>; old_len];
318                let mut actual_to_expected = vec![None::<usize>; new_len];
319                while let Some((_, _, expected_index, actual_index)) = q.pop() {
320                    if expected_to_actual[expected_index].is_some() || actual_to_expected[actual_index].is_some() {
321                        continue;
322                    }
323                    let requirements = [
324                        expected_to_actual[..expected_index]
325                            .iter()
326                            .rev()
327                            .find_map(Option::as_ref)
328                            .is_none_or(|&left| left < actual_index),
329                        expected_to_actual[expected_index..]
330                            .iter()
331                            .find_map(Option::as_ref)
332                            .is_none_or(|&right| right < actual_index),
333                        actual_to_expected[..actual_index]
334                            .iter()
335                            .rev()
336                            .find_map(Option::as_ref)
337                            .is_none_or(|&left| left < expected_index),
338                        actual_to_expected[actual_index..]
339                            .iter()
340                            .find_map(Option::as_ref)
341                            .is_none_or(|&right| right < expected_index),
342                    ];
343                    if requirements.into_iter().all(convert::identity) {
344                        expected_to_actual[expected_index] = Some(actual_index);
345                        actual_to_expected[actual_index] = Some(expected_index);
346                    }
347                }
348                let mut expected_to_actual = expected_to_actual.into_iter().enumerate().peekable();
349                let mut actual_to_expected = actual_to_expected.into_iter().enumerate().peekable();
350                let expected_index_base = old_index;
351                let actual_index_base = new_index;
352                loop {
353                    while let Some((expected_index, _)) =
354                        expected_to_actual.next_if(|(_, actual_index)| actual_index.is_none())
355                    {
356                        self.delete(expected_index_base + expected_index, 1, 0)?;
357                    }
358                    while let Some((actual_index, _)) =
359                        actual_to_expected.next_if(|(_, expected_index)| expected_index.is_none())
360                    {
361                        self.insert(0, actual_index_base + actual_index, 1)?;
362                    }
363                    match (expected_to_actual.next(), actual_to_expected.next()) {
364                        (None, None) => break,
365                        (Some((expected_index, Some(actual_index_))), Some((actual_index, Some(expected_index_)))) => {
366                            assert_eq!(expected_index, expected_index_);
367                            assert_eq!(actual_index, actual_index_);
368                            let expected_index = expected_index_base + expected_index;
369                            let actual_index = actual_index_base + actual_index;
370                            let need_extra_comma_expected = expected_index < self.expected.len() - 1;
371                            let need_extra_comma_actual = actual_index < self.actual.len() - 1;
372                            match (&self.expected[expected_index], &self.actual[actual_index]) {
373                                (Value::Array(expected), Value::Array(actual)) => {
374                                    self.result.push(JsonDiffLine::unchanged(
375                                        self.indent,
376                                        "[".to_owned(),
377                                        "[".to_owned(),
378                                    ));
379                                    json_array_diff(expected, actual, self.indent + 1, self.result);
380                                    self.result.push(JsonDiffLine::unchanged(
381                                        self.indent,
382                                        format!("]{}", if need_extra_comma_expected { "," } else { "" }),
383                                        format!("]{}", if need_extra_comma_actual { "," } else { "" }),
384                                    ));
385                                }
386                                (Value::Object(expected), Value::Object(actual)) => {
387                                    self.result.push(JsonDiffLine::unchanged(
388                                        self.indent,
389                                        "{".to_owned(),
390                                        "{".to_owned(),
391                                    ));
392                                    json_object_diff(expected, actual, self.indent + 1, self.result);
393                                    self.result.push(JsonDiffLine::unchanged(
394                                        self.indent,
395                                        format!("}}{}", if need_extra_comma_expected { "," } else { "" }),
396                                        format!("}}{}", if need_extra_comma_actual { "," } else { "" }),
397                                    ));
398                                }
399                                _ => unreachable!(),
400                            }
401                        }
402                        _ => unreachable!(),
403                    }
404                }
405                Ok(())
406            }
407        }
408    }
409    fn json_object_diff(
410        expected: &serde_json::Map<String, Value>,
411        actual: &serde_json::Map<String, Value>,
412        indent: usize,
413        result: &mut Vec<JsonDiffLine>,
414    ) {
415        let expected_keys = expected.keys().collect::<Vec<_>>();
416        let actual_keys = actual.keys().collect::<Vec<_>>();
417        let mut hook = ObjectDiffHook {
418            indent,
419            expected,
420            actual,
421            expected_keys: &expected_keys,
422            actual_keys: &actual_keys,
423            result,
424        };
425        similar::algorithms::patience::diff(
426            &mut hook,
427            &expected_keys,
428            0..expected_keys.len(),
429            &actual_keys,
430            0..actual_keys.len(),
431        )
432        .unwrap();
433
434        struct ObjectDiffHook<'a> {
435            indent: usize,
436            expected: &'a serde_json::Map<String, Value>,
437            actual: &'a serde_json::Map<String, Value>,
438            expected_keys: &'a [&'a String],
439            actual_keys: &'a [&'a String],
440            result: &'a mut Vec<JsonDiffLine>,
441        }
442
443        impl DiffHook for ObjectDiffHook<'_> {
444            type Error = convert::Infallible;
445
446            fn equal(&mut self, old_index: usize, new_index: usize, len: usize) -> Result<(), Self::Error> {
447                assert_eq!(len, 1);
448                let need_extra_comma_expected = old_index < self.expected_keys.len() - 1;
449                let need_extra_comma_actual = new_index < self.actual_keys.len() - 1;
450                let k = self.expected_keys[old_index];
451                let expected_v = self.expected.get(k).unwrap();
452                let actual_v = self.actual.get(k).unwrap();
453                match (expected_v, actual_v) {
454                    (expected @ Value::Null, actual @ Value::Null)
455                    | (expected @ Value::Bool(_), actual @ Value::Bool(_))
456                    | (expected @ Value::Number(_), actual @ Value::Number(_))
457                    | (expected @ Value::String(_), actual @ Value::String(_))
458                        if expected == actual =>
459                    {
460                        let k = serde_json::to_string(k).unwrap();
461                        let v = serde_json::to_string(expected).unwrap();
462                        self.result.push(JsonDiffLine::unchanged(
463                            self.indent,
464                            format!("{k}: {v}{}", if need_extra_comma_expected { "," } else { "" }),
465                            format!("{k}: {v}{}", if need_extra_comma_actual { "," } else { "" }),
466                        ));
467                    }
468                    (Value::Array(expected), Value::Array(actual)) => {
469                        let k = serde_json::to_string(k).unwrap();
470                        self.result.push(JsonDiffLine::unchanged(
471                            self.indent,
472                            format!("{k}: ["),
473                            format!("{k}: ["),
474                        ));
475                        json_array_diff(expected, actual, self.indent + 1, self.result);
476                        self.result.push(JsonDiffLine::unchanged(
477                            self.indent,
478                            format!("]{}", if need_extra_comma_expected { "," } else { "" }),
479                            format!("]{}", if need_extra_comma_actual { "," } else { "" }),
480                        ));
481                    }
482                    (Value::Object(expected), Value::Object(actual)) => {
483                        let k = serde_json::to_string(k).unwrap();
484                        self.result.push(JsonDiffLine::unchanged(
485                            self.indent,
486                            format!("{k}: {{"),
487                            format!("{k}: {{"),
488                        ));
489                        json_object_diff(expected, actual, self.indent + 1, self.result);
490                        self.result.push(JsonDiffLine::unchanged(
491                            self.indent,
492                            format!("}}{}", if need_extra_comma_expected { "," } else { "" }),
493                            format!("}}{}", if need_extra_comma_actual { "," } else { "" }),
494                        ));
495                    }
496                    _ => {
497                        self.delete(old_index, 1, 0)?;
498                        self.insert(0, new_index, 1)?;
499                    }
500                }
501                Ok(())
502            }
503
504            fn delete(&mut self, old_index: usize, old_len: usize, _new_index: usize) -> Result<(), Self::Error> {
505                assert_eq!(old_len, 1);
506                let need_extra_comma = old_index < self.expected.len() - 1;
507                let k = self.expected_keys[old_index];
508                let v = self.expected.get(k).unwrap();
509                if let Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) = v {
510                    self.result.push(JsonDiffLine::deleted(
511                        self.indent,
512                        format!(
513                            "{}: {}{}",
514                            serde_json::to_string(k).unwrap(),
515                            serde_json::to_string(v).unwrap(),
516                            if need_extra_comma { "," } else { "" }
517                        ),
518                    ));
519                    return Ok(());
520                }
521                let v = serde_json::to_string_pretty(v).unwrap();
522                let mut lines = v.lines().peekable();
523                let first_line = lines.next().unwrap();
524                self.result.push(JsonDiffLine::deleted(
525                    self.indent,
526                    format!("{}: {}", serde_json::to_string(k).unwrap(), first_line),
527                ));
528                while let Some(line) = lines.next() {
529                    if lines.peek().is_none() && need_extra_comma {
530                        self.result
531                            .push(JsonDiffLine::deleted(self.indent, format!("{},", line)));
532                    } else {
533                        self.result.push(JsonDiffLine::deleted(self.indent, line.to_owned()));
534                    }
535                }
536                Ok(())
537            }
538
539            fn insert(&mut self, _old_index: usize, new_index: usize, new_len: usize) -> Result<(), Self::Error> {
540                assert_eq!(new_len, 1);
541                let need_extra_comma = new_index < self.actual.len() - 1;
542                let k = self.actual_keys[new_index];
543                let v = self.actual.get(k).unwrap();
544                if let Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) = v {
545                    self.result.push(JsonDiffLine::added(
546                        self.indent,
547                        format!(
548                            "{}: {}{}",
549                            serde_json::to_string(k).unwrap(),
550                            serde_json::to_string(v).unwrap(),
551                            if need_extra_comma { "," } else { "" }
552                        ),
553                    ));
554                    return Ok(());
555                }
556                let v = serde_json::to_string_pretty(v).unwrap();
557                let mut lines = v.lines().peekable();
558                let first_line = lines.next().unwrap();
559                self.result.push(JsonDiffLine::added(
560                    self.indent,
561                    format!("{}: {}", serde_json::to_string(k).unwrap(), first_line),
562                ));
563                while let Some(line) = lines.next() {
564                    if lines.peek().is_none() && need_extra_comma {
565                        self.result.push(JsonDiffLine::added(self.indent, format!("{},", line)));
566                    } else {
567                        self.result.push(JsonDiffLine::added(self.indent, line.to_owned()));
568                    }
569                }
570                Ok(())
571            }
572
573            fn replace(
574                &mut self,
575                _old_index: usize,
576                _old_len: usize,
577                _new_index: usize,
578                _new_len: usize,
579            ) -> Result<(), Self::Error> {
580                unreachable!()
581            }
582        }
583    }
584    let mut result = Vec::new();
585    match (expected, actual) {
586        (expected @ Value::Null, actual @ Value::Null)
587        | (expected @ Value::Bool(_), actual @ Value::Bool(_))
588        | (expected @ Value::Number(_), actual @ Value::Number(_))
589        | (expected @ Value::String(_), actual @ Value::String(_)) => {
590            if expected == actual {
591                result.push(JsonDiffLine::unchanged(0, expected.to_string(), actual.to_string()));
592            } else {
593                result.push(JsonDiffLine::deleted(0, expected.to_string()));
594                result.push(JsonDiffLine::added(0, actual.to_string()));
595            }
596        }
597        (Value::Array(expected), Value::Array(actual)) => {
598            result.push(JsonDiffLine::unchanged(0, "[".to_owned(), "[".to_owned()));
599            json_array_diff(expected, actual, 1, &mut result);
600            result.push(JsonDiffLine::unchanged(0, "]".to_owned(), "]".to_owned()));
601        }
602        (Value::Object(expected), Value::Object(actual)) => {
603            result.push(JsonDiffLine::unchanged(0, "{".to_owned(), "{".to_owned()));
604            json_object_diff(expected, actual, 1, &mut result);
605            result.push(JsonDiffLine::unchanged(0, "}".to_owned(), "}".to_owned()));
606        }
607        (expected, actual) => {
608            for line in serde_json::to_string_pretty(expected).unwrap().lines() {
609                result.push(JsonDiffLine::deleted(0, line.to_owned()));
610            }
611            for line in serde_json::to_string_pretty(actual).unwrap().lines() {
612                result.push(JsonDiffLine::added(0, line.to_owned()));
613            }
614        }
615    }
616    result
617}