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}