ferridriver_expect/
diff.rs1use similar::{ChangeTag, TextDiff};
10
11use crate::asymmetric::Asymmetric;
12
13pub fn pretty_json(v: &serde_json::Value) -> String {
17 let humanized = humanize_asymmetric(v);
18 serde_json::to_string_pretty(&humanized).unwrap_or_else(|_| humanized.to_string())
19}
20
21fn humanize_asymmetric(v: &serde_json::Value) -> serde_json::Value {
22 if let Some(asym) = Asymmetric::from_value(v) {
23 return serde_json::Value::String(asym.description());
24 }
25 match v {
26 serde_json::Value::Array(arr) => serde_json::Value::Array(arr.iter().map(humanize_asymmetric).collect()),
27 serde_json::Value::Object(map) => {
28 let mut out = serde_json::Map::with_capacity(map.len());
29 for (k, val) in map {
30 out.insert(k.clone(), humanize_asymmetric(val));
31 }
32 serde_json::Value::Object(out)
33 },
34 other => other.clone(),
35 }
36}
37
38pub fn unified_diff(expected: &str, received: &str) -> String {
41 let diff = TextDiff::from_lines(expected, received);
42 let mut out = String::new();
43 for change in diff.iter_all_changes() {
44 let sign = match change.tag() {
45 ChangeTag::Delete => '-',
46 ChangeTag::Insert => '+',
47 ChangeTag::Equal => ' ',
48 };
49 out.push(sign);
50 out.push_str(change.value().trim_end_matches('\n'));
51 out.push('\n');
52 }
53 out.pop(); out
55}
56
57pub fn json_diff(expected: &serde_json::Value, received: &serde_json::Value) -> String {
61 let expected_str = pretty_json(expected);
62 let received_str = pretty_json(received);
63 unified_diff(&expected_str, &received_str)
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69 use pretty_assertions::assert_eq;
70 use serde_json::json;
71
72 #[test]
73 fn pretty_json_renders_multiline() {
74 let v = json!({"a": 1, "b": [2, 3]});
75 let s = pretty_json(&v);
76 assert!(s.contains('\n'), "expected multi-line: {s}");
77 }
78
79 #[test]
80 fn pretty_json_humanizes_asymmetric() {
81 let v = json!({ "id": { crate::ASYM_TAG_KEY: "any", "name": "Number" } });
82 let s = pretty_json(&v);
83 assert!(s.contains("Any<Number>"), "humanized asym missing: {s}");
84 assert!(!s.contains("@@asym"), "raw asym tag leaked: {s}");
85 }
86
87 #[test]
88 fn unified_diff_marks_changed_lines() {
89 let a = "line1\nline2\nline3";
90 let b = "line1\nLINE2\nline3";
91 let d = unified_diff(a, b);
92 assert!(d.contains("-line2"), "missing '-' marker: {d}");
93 assert!(d.contains("+LINE2"), "missing '+' marker: {d}");
94 assert!(d.contains(" line1"), "missing context: {d}");
95 }
96
97 #[test]
98 fn json_diff_for_object_mismatch() {
99 let expected = json!({"a": 1, "b": 2});
100 let actual = json!({"a": 1, "b": 3});
101 let d = json_diff(&expected, &actual);
102 assert!(d.contains('-'), "diff has no removals: {d}");
103 assert!(d.contains('+'), "diff has no additions: {d}");
104 }
105
106 #[test]
107 fn json_diff_empty_for_equal_values() {
108 let v = json!({"a": 1});
112 let d = json_diff(&v, &v);
113 assert_eq!(
114 d.lines().filter(|l| l.starts_with('-') || l.starts_with('+')).count(),
115 0
116 );
117 }
118}