diffx_core/
lib.rs

1use serde::Serialize;
2use serde_json::Value;
3
4#[derive(Debug, PartialEq, Serialize)]
5pub enum DiffResult {
6    Added(String, Value),
7    Removed(String, Value),
8    Modified(String, Value, Value),
9    TypeChanged(String, Value, Value),
10}
11
12pub fn diff(
13    v1: &Value,
14    v2: &Value,
15) -> Vec<DiffResult> {
16    let mut results = Vec::new();
17
18    // Handle root level type or value change first
19    if v1 != v2 {
20        let type_match = match (v1, v2) {
21            (Value::Null, Value::Null) => true,
22            (Value::Bool(_), Value::Bool(_)) => true,
23            (Value::Number(_), Value::Number(_)) => true,
24            (Value::String(_), Value::String(_)) => true,
25            (Value::Array(_), Value::Array(_)) => true,
26            (Value::Object(_), Value::Object(_)) => true,
27            _ => false,
28        };
29
30        if !type_match {
31            results.push(DiffResult::TypeChanged("".to_string(), v1.clone(), v2.clone()));
32            return results; // If root type changed, no further diffing needed
33        } else if v1.is_object() && v2.is_object() {
34            diff_objects("", v1.as_object().unwrap(), v2.as_object().unwrap(), &mut results);
35        } else if v1.is_array() && v2.is_array() {
36            diff_arrays("", v1.as_array().unwrap(), v2.as_array().unwrap(), &mut results);
37        } else {
38            // Simple value modification at root
39            results.push(DiffResult::Modified("".to_string(), v1.clone(), v2.clone()));
40            return results;
41        }
42    }
43
44    results
45}
46
47fn diff_recursive(
48    path: &str,
49    v1: &Value,
50    v2: &Value,
51    results: &mut Vec<DiffResult>,
52) {
53    match (v1, v2) {
54        (Value::Object(map1), Value::Object(map2)) => {
55            diff_objects(path, map1, map2, results);
56        }
57        (Value::Array(arr1), Value::Array(arr2)) => {
58            diff_arrays(path, arr1, arr2, results);
59        }
60        _ => { /* Should not happen if called correctly from diff_objects/diff_arrays */ }
61    }
62}
63
64fn diff_objects(
65    path: &str,
66    map1: &serde_json::Map<String, Value>,
67    map2: &serde_json::Map<String, Value>,
68    results: &mut Vec<DiffResult>,
69) {
70    // Check for modified or removed keys
71    for (key, value1) in map1 {
72        let current_path = if path.is_empty() { key.clone() } else { format!("{}.{}", path, key) };
73        match map2.get(key) {
74            Some(value2) => {
75                // Recurse for nested objects/arrays
76                if value1.is_object() && value2.is_object() || value1.is_array() && value2.is_array() {
77                    diff_recursive(&current_path, value1, value2, results);
78                } else if value1 != value2 {
79                    let type_match = match (value1, value2) {
80                        (Value::Null, Value::Null) => true,
81                        (Value::Bool(_), Value::Bool(_)) => true,
82                        (Value::Number(_), Value::Number(_)) => true,
83                        (Value::String(_), Value::String(_)) => true,
84                        (Value::Array(_), Value::Array(_)) => true,
85                        (Value::Object(_), Value::Object(_)) => true,
86                        _ => false,
87                    };
88
89                    if !type_match {
90                        results.push(DiffResult::TypeChanged(current_path, value1.clone(), value2.clone()));
91                    } else {
92                        results.push(DiffResult::Modified(current_path, value1.clone(), value2.clone()));
93                    }
94                }
95            }
96            None => {
97                results.push(DiffResult::Removed(current_path, value1.clone()));
98            }
99        }
100    }
101
102    // Check for added keys
103    for (key, value2) in map2 {
104        if !map1.contains_key(key) {
105            let current_path = if path.is_empty() { key.clone() } else { format!("{}.{}", path, key) };
106            results.push(DiffResult::Added(current_path, value2.clone()));
107        }
108    }
109}
110
111fn diff_arrays(
112    path: &str,
113    arr1: &Vec<Value>,
114    arr2: &Vec<Value>,
115    results: &mut Vec<DiffResult>,
116) {
117    let max_len = arr1.len().max(arr2.len());
118    for i in 0..max_len {
119        let current_path = format!("{}[{}]", path, i);
120        match (arr1.get(i), arr2.get(i)) {
121            (Some(val1), Some(val2)) => {
122                // Recurse for nested objects/arrays within arrays
123                if val1.is_object() && val2.is_object() || val1.is_array() && val2.is_array() {
124                    diff_recursive(&current_path, val1, val2, results);
125                } else if val1 != val2 {
126                    let type_match = match (val1, val2) {
127                        (Value::Null, Value::Null) => true,
128                        (Value::Bool(_), Value::Bool(_)) => true,
129                        (Value::Number(_), Value::Number(_)) => true,
130                        (Value::String(_), Value::String(_)) => true,
131                        (Value::Array(_), Value::Array(_)) => true,
132                        (Value::Object(_), Value::Object(_)) => true,
133                        _ => false,
134                    };
135
136                    if !type_match {
137                        results.push(DiffResult::TypeChanged(current_path, val1.clone(), val2.clone()));
138                    } else {
139                        results.push(DiffResult::Modified(current_path, val1.clone(), val2.clone()));
140                    }
141                }
142            }
143            (Some(val1), None) => {
144                results.push(DiffResult::Removed(current_path, val1.clone()));
145            }
146            (None, Some(val2)) => {
147                results.push(DiffResult::Added(current_path, val2.clone()));
148            }
149            (None, None) => { /* Should not happen */ }
150        }
151    }
152}
153
154pub fn value_type_name(value: &Value) -> &str {
155    match value {
156        Value::Null => "Null",
157        Value::Bool(_) => "Boolean",
158        Value::Number(_) => "Number",
159        Value::String(_) => "String",
160        Value::Array(_) => "Array",
161        Value::Object(_) => "Object",
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use serde_json::json;
169
170    #[test]
171    fn test_diff_no_changes() {
172        let v1 = json!({ "a": 1, "b": 2 });
173        let v2 = json!({ "a": 1, "b": 2 });
174        let differences = diff(&v1, &v2);
175        assert!(differences.is_empty());
176    }
177
178    #[test]
179    fn test_diff_value_modified() {
180        let v1 = json!({ "a": 1, "b": 2 });
181        let v2 = json!({ "a": 1, "b": 3 });
182        let differences = diff(&v1, &v2);
183        assert_eq!(differences.len(), 1);
184        assert_eq!(differences[0], DiffResult::Modified("b".to_string(), json!(2), json!(3)));
185    }
186
187    #[test]
188    fn test_diff_key_added() {
189        let v1 = json!({ "a": 1 });
190        let v2 = json!({ "a": 1, "b": 2 });
191        let differences = diff(&v1, &v2);
192        assert_eq!(differences.len(), 1);
193        assert_eq!(differences[0], DiffResult::Added("b".to_string(), json!(2)));
194    }
195
196    #[test]
197    fn test_diff_key_removed() {
198        let v1 = json!({ "a": 1, "b": 2 });
199        let v2 = json!({ "a": 1 });
200        let differences = diff(&v1, &v2);
201        assert_eq!(differences.len(), 1);
202        assert_eq!(differences[0], DiffResult::Removed("b".to_string(), json!(2)));
203    }
204
205    #[test]
206    fn test_diff_type_changed() {
207        let v1 = json!({ "a": 1 });
208        let v2 = json!({ "a": "1" });
209        let differences = diff(&v1, &v2);
210        assert_eq!(differences.len(), 1);
211        assert_eq!(differences[0], DiffResult::TypeChanged("a".to_string(), json!(1), json!("1")));
212    }
213
214    #[test]
215    fn test_diff_nested_object_modified() {
216        let v1 = json!({ "a": { "b": 1 } });
217        let v2 = json!({ "a": { "b": 2 } });
218        let differences = diff(&v1, &v2);
219        assert_eq!(differences.len(), 1);
220        assert_eq!(differences[0], DiffResult::Modified("a.b".to_string(), json!(1), json!(2)));
221    }
222
223    #[test]
224    fn test_diff_array_element_added() {
225        let v1 = json!([1, 2]);
226        let v2 = json!([1, 2, 3]);
227        let differences = diff(&v1, &v2);
228        assert_eq!(differences.len(), 1);
229        assert_eq!(differences[0], DiffResult::Added("[2]".to_string(), json!(3)));
230    }
231
232    #[test]
233    fn test_diff_array_element_removed() {
234        let v1 = json!([1, 2, 3]);
235        let v2 = json!([1, 2]);
236        let differences = diff(&v1, &v2);
237        assert_eq!(differences.len(), 1);
238        assert_eq!(differences[0], DiffResult::Removed("[2]".to_string(), json!(3)));
239    }
240
241    #[test]
242    fn test_diff_array_element_modified() {
243        let v1 = json!([1, 2, 3]);
244        let v2 = json!([1, 2, 4]);
245        let differences = diff(&v1, &v2);
246        assert_eq!(differences.len(), 1);
247        assert_eq!(differences[0], DiffResult::Modified("[2]".to_string(), json!(3), json!(4)));
248    }
249
250    #[test]
251    fn test_diff_nested_array_element_modified() {
252        let v1 = json!({ "a": [1, 2, 3] });
253        let v2 = json!({ "a": [1, 2, 4] });
254        let differences = diff(&v1, &v2);
255        assert_eq!(differences.len(), 1);
256        assert_eq!(differences[0], DiffResult::Modified("a[2]".to_string(), json!(3), json!(4)));
257    }
258
259    #[test]
260    fn test_diff_root_type_changed() {
261        let v1 = json!(1);
262        let v2 = json!("1");
263        let differences = diff(&v1, &v2);
264        assert_eq!(differences.len(), 1);
265        assert_eq!(differences[0], DiffResult::TypeChanged("".to_string(), json!(1), json!("1")));
266    }
267
268    #[test]
269    fn test_diff_nested_object_and_array() {
270        let v1 = json!({
271            "config": {
272                "users": [
273                    {"id": 1, "name": "Alice"},
274                    {"id": 2, "name": "Bob"}
275                ],
276                "settings": {"theme": "dark"}
277            }
278        });
279        let v2 = json!({
280            "config": {
281                "users": [
282                    {"id": 1, "name": "Alice"},
283                    {"id": 2, "name": "Robert"},
284                    {"id": 3, "name": "Charlie"}
285                ],
286                "settings": {"theme": "light", "font_size": 12}
287            }
288        });
289        let differences = diff(&v1, &v2);
290        assert_eq!(differences.len(), 4);
291        assert!(differences.contains(&DiffResult::Modified("config.users[1].name".to_string(), json!("Bob"), json!("Robert"))));
292        assert!(differences.contains(&DiffResult::Added("config.users[2]".to_string(), json!({"id": 3, "name": "Charlie"}))));
293        assert!(differences.contains(&DiffResult::Modified("config.settings.theme".to_string(), json!("dark"), json!("light"))));
294        assert!(differences.contains(&DiffResult::Added("config.settings.font_size".to_string(), json!(12))));
295    }
296
297    #[test]
298    fn test_diff_empty_objects_and_arrays() {
299        let v1 = json!({
300            "empty_obj": {},
301            "empty_arr": [],
302            "data": "value"
303        });
304        let v2 = json!({
305            "empty_obj": {},
306            "empty_arr": [],
307            "data": "new_value"
308        });
309        let differences = diff(&v1, &v2);
310        assert_eq!(differences.len(), 1);
311        assert_eq!(differences[0], DiffResult::Modified("data".to_string(), json!("value"), json!("new_value")));
312    }
313
314    #[test]
315    fn test_diff_root_array_changes() {
316        let v1 = json!([
317            {"id": 1},
318            {"id": 2}
319        ]);
320        let v2 = json!([
321            {"id": 1},
322            {"id": 3},
323            {"id": 4}
324        ]);
325        let differences = diff(&v1, &v2);
326        assert_eq!(differences.len(), 2);
327        assert!(differences.contains(&DiffResult::Modified("[1].id".to_string(), json!(2), json!(3))));
328        assert!(differences.contains(&DiffResult::Added("[2]".to_string(), json!({"id": 4}))));
329    }
330}