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#[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#[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}