json_structural_diff/
colorize.rs

1use regex::Regex;
2use serde_json::Value;
3
4fn subcolorize<F>(key: Option<&str>, diff: &Value, output: &mut F, color: &str, indent: &str)
5where
6    F: FnMut(&str, &str),
7{
8    let prefix = if let Some(key) = key {
9        format!("{key}: ")
10    } else {
11        String::new()
12    };
13    let subindent = &format!("{indent}  ");
14
15    match diff {
16        Value::Object(obj) => {
17            if obj.len() == 2 && obj.contains_key("__old") && obj.contains_key("__new") {
18                let old = obj.get("__old").unwrap();
19                let new = obj.get("__new").unwrap();
20                subcolorize(key, old, output, "-", indent);
21                subcolorize(key, new, output, "+", indent);
22            } else {
23                output(color, &format!("{indent}{prefix}{{"));
24                let re_delete = Regex::new(r"^(.*)__deleted$").unwrap();
25                let re_added = Regex::new(r"^(.*)__added$").unwrap();
26                for (subkey, subvalue) in obj {
27                    if let Some(caps) = re_delete.captures(subkey) {
28                        subcolorize(
29                            Some(caps.get(1).unwrap().as_str()),
30                            subvalue,
31                            output,
32                            "-",
33                            subindent,
34                        );
35                        continue;
36                    }
37                    if let Some(caps) = re_added.captures(subkey) {
38                        subcolorize(
39                            Some(caps.get(1).unwrap().as_str()),
40                            subvalue,
41                            output,
42                            "+",
43                            subindent,
44                        );
45                        continue;
46                    }
47                    subcolorize(Some(subkey), subvalue, output, color, subindent);
48                }
49                output(color, &format!("{indent}}}"));
50            }
51        }
52        Value::Array(array) => {
53            output(color, &format!("{indent}{prefix}["));
54
55            let mut looks_like_diff = true;
56            for item in array {
57                looks_like_diff = if let Value::Array(arr) = item {
58                    if !(arr.len() == 2
59                        || (arr.len() == 1
60                            && (arr[0].is_string() && arr[0].as_str().unwrap() == " ")))
61                    {
62                        false
63                    } else if let Value::String(str1) = &arr[0] {
64                        str1.len() == 1 && ([" ", "-", "+", "~"].contains(&str1.as_str()))
65                    } else {
66                        false
67                    }
68                } else {
69                    false
70                };
71            }
72
73            if looks_like_diff {
74                for item in array {
75                    if let Value::Array(subitem) = item {
76                        let op = subitem[0].as_str().unwrap();
77                        let subvalue = &subitem.get(1);
78                        if op == " " && subvalue.is_none() {
79                            output(" ", &format!("{subindent}..."));
80                        } else {
81                            assert!(([" ", "-", "+", "~"].contains(&op)), "Unexpected op '{op}'");
82                            let subvalue = subvalue.unwrap();
83                            let color = if op == "~" { " " } else { op };
84                            subcolorize(None, subvalue, output, color, subindent);
85                        }
86                    }
87                }
88            } else {
89                for subvalue in array {
90                    subcolorize(None, subvalue, output, color, subindent);
91                }
92            }
93
94            output(color, &format!("{indent}]"));
95        }
96        _ => output(color, &(indent.to_owned() + &prefix + &diff.to_string())),
97    }
98}
99
100/// Returns the JSON structural difference formatted as a `Vec<String>`.
101///
102/// If `None`, there is no JSON structural difference to be formatted.
103#[must_use]
104#[allow(clippy::module_name_repetitions)]
105pub fn colorize_to_array(diff: &Value) -> Vec<String> {
106    let mut output: Vec<String> = Vec::new();
107
108    let mut output_func = |color: &str, line: &str| {
109        output.push(format!("{color}{line}"));
110    };
111
112    subcolorize(None, diff, &mut output_func, " ", "");
113
114    output
115}
116
117/// Returns the JSON structural difference formatted as a `String`.
118///
119/// If `None`, there is no JSON structural difference to be formatted.
120#[cfg(feature = "colorize")]
121#[must_use]
122#[allow(clippy::module_name_repetitions)]
123pub fn colorize(diff: &Value, is_color: bool) -> String {
124    use console::Style;
125
126    let mut output: Vec<String> = Vec::new();
127
128    let mut output_func = |color: &str, line: &str| {
129        let color_line = format!("{color}{line}");
130        let str_output = if is_color {
131            match color {
132                "+" => format!("{}", Style::new().green().apply_to(color_line)),
133                "-" => format!("{}", Style::new().red().apply_to(color_line)),
134                _ => color_line,
135            }
136        } else {
137            color_line
138        };
139        output.push(str_output + "\n");
140    };
141
142    subcolorize(None, diff, &mut output_func, " ", "");
143
144    output.join("")
145}
146
147#[cfg(test)]
148mod tests {
149
150    use super::colorize_to_array;
151
152    #[test]
153    fn test_colorize_to_array() {
154        assert_eq!(colorize_to_array(&json!(42)), &[" 42"]);
155
156        assert_eq!(colorize_to_array(&json!(null)), &[" null"]);
157
158        assert_eq!(colorize_to_array(&json!(false)), &[" false"]);
159
160        assert_eq!(
161            colorize_to_array(&json!({"__old": 42, "__new": 10 })),
162            &["-42", "+10"]
163        );
164
165        assert_eq!(
166            colorize_to_array(&json!({"__old": false, "__new": null })),
167            &["-false", "+null"]
168        );
169
170        assert_eq!(
171            colorize_to_array(&json!({"foo__deleted": 42 })),
172            &[" {", "-  foo: 42", " }"]
173        );
174
175        assert_eq!(
176            colorize_to_array(&json!({"foo__added": 42 })),
177            &[" {", "+  foo: 42", " }"]
178        );
179
180        assert_eq!(
181            colorize_to_array(&json!({ "foo__added": null })),
182            &[" {", "+  foo: null", " }"]
183        );
184
185        assert_eq!(
186            colorize_to_array(&json!({ "foo__added": false })),
187            &[" {", "+  foo: false", " }"]
188        );
189
190        assert_eq!(
191            colorize_to_array(&json!({"foo__added": {"bar": 42 } })),
192            &[" {", "+  foo: {", "+    bar: 42", "+  }", " }"]
193        );
194
195        assert_eq!(
196            colorize_to_array(&json!({"foo": {"__old": 42, "__new": 10 } })),
197            &[" {", "-  foo: 42", "+  foo: 10", " }"]
198        );
199
200        assert_eq!(
201            colorize_to_array(&json!([[' ', 10], ['+', 20], [' ', 30]])),
202            &[" [", "   10", "+  20", "   30", " ]"]
203        );
204
205        assert_eq!(
206            colorize_to_array(&json!([[' ', 10], ['-', 20], [' ', 30]])),
207            &[" [", "   10", "-  20", "   30", " ]"]
208        );
209
210        assert_eq!(
211            colorize_to_array(&json!([ [" "], ["~", {"foo__added": 42}], [" "] ])),
212            &[
213                " [",
214                "   ...",
215                "   {",
216                "+    foo: 42",
217                "   }",
218                "   ...",
219                " ]"
220            ],
221        );
222    }
223
224    #[test]
225    #[cfg(feature = "colorize")]
226    fn test_colorize_no_colors() {
227        use super::colorize;
228        assert_eq!(
229            colorize(&json!({"foo": {"__old": 42, "__new": 10 } }), false),
230            " {\n-  foo: 42\n+  foo: 10\n }\n"
231        );
232    }
233}