1use std::collections::HashMap;
4use crate::Value;
5
6#[derive(Debug, Clone)]
8pub struct DiffResult {
9 pub added: HashMap<String, Value>,
10 pub removed: HashMap<String, Value>,
11 pub changed: HashMap<String, DiffChange>,
12 pub unchanged: Vec<String>,
13}
14
15#[derive(Debug, Clone)]
17pub struct DiffChange {
18 pub from: Value,
19 pub to: Value,
20}
21
22pub fn diff(a: &HashMap<String, Value>, b: &HashMap<String, Value>) -> DiffResult {
29 let mut added = HashMap::new();
30 let mut removed = HashMap::new();
31 let mut changed = HashMap::new();
32 let mut unchanged = Vec::new();
33
34 for (key, a_val) in a {
35 match b.get(key) {
36 None => { removed.insert(key.clone(), a_val.clone()); }
37 Some(b_val) => {
38 if deep_equal(a_val, b_val) {
39 unchanged.push(key.clone());
40 } else {
41 changed.insert(key.clone(), DiffChange {
42 from: a_val.clone(),
43 to: b_val.clone(),
44 });
45 }
46 }
47 }
48 }
49
50 for (key, b_val) in b {
51 if !a.contains_key(key) {
52 added.insert(key.clone(), b_val.clone());
53 }
54 }
55
56 unchanged.sort();
57
58 DiffResult { added, removed, changed, unchanged }
59}
60
61pub fn diff_to_value(d: &DiffResult) -> Value {
65 let mut root = HashMap::new();
66
67 root.insert("added".into(), Value::Object(d.added.clone()));
68 root.insert("removed".into(), Value::Object(d.removed.clone()));
69
70 let mut changed_map = HashMap::new();
71 for (k, c) in &d.changed {
72 let mut entry = HashMap::new();
73 entry.insert("from".into(), c.from.clone());
74 entry.insert("to".into(), c.to.clone());
75 changed_map.insert(k.clone(), Value::Object(entry));
76 }
77 root.insert("changed".into(), Value::Object(changed_map));
78
79 let unchanged_arr: Vec<Value> = d.unchanged.iter().map(|s| Value::String(s.clone())).collect();
80 root.insert("unchanged".into(), Value::Array(unchanged_arr));
81
82 Value::Object(root)
83}
84
85fn deep_equal(a: &Value, b: &Value) -> bool {
86 match (a, b) {
87 (Value::Null, Value::Null) => true,
88 (Value::Bool(x), Value::Bool(y)) => x == y,
89 (Value::Int(x), Value::Int(y)) => x == y,
90 (Value::Float(x), Value::Float(y)) => x == y,
91 (Value::String(x), Value::String(y)) => x == y,
92 (Value::Secret(x), Value::Secret(y)) => x == y,
93 (Value::Array(x), Value::Array(y)) => {
94 x.len() == y.len() && x.iter().zip(y.iter()).all(|(a, b)| deep_equal(a, b))
95 }
96 (Value::Object(x), Value::Object(y)) => {
97 x.len() == y.len() && x.iter().all(|(k, v)| y.get(k).is_some_and(|yv| deep_equal(v, yv)))
98 }
99 _ => false,
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 fn obj(pairs: Vec<(&str, Value)>) -> HashMap<String, Value> {
108 pairs.into_iter().map(|(k, v)| (k.to_string(), v)).collect()
109 }
110
111 #[test]
112 fn identical_objects() {
113 let a = obj(vec![("name", Value::String("John".into())), ("age", Value::Int(25))]);
114 let b = a.clone();
115 let d = diff(&a, &b);
116 assert!(d.added.is_empty());
117 assert!(d.removed.is_empty());
118 assert!(d.changed.is_empty());
119 assert_eq!(d.unchanged.len(), 2);
120 }
121
122 #[test]
123 fn added_and_removed() {
124 let a = obj(vec![("x", Value::Int(1))]);
125 let b = obj(vec![("y", Value::Int(2))]);
126 let d = diff(&a, &b);
127 assert_eq!(d.added.len(), 1);
128 assert_eq!(d.removed.len(), 1);
129 assert!(d.changed.is_empty());
130 assert!(d.unchanged.is_empty());
131 }
132
133 #[test]
134 fn changed_value() {
135 let a = obj(vec![("name", Value::String("Alice".into()))]);
136 let b = obj(vec![("name", Value::String("Bob".into()))]);
137 let d = diff(&a, &b);
138 assert_eq!(d.changed.len(), 1);
139 assert!(d.changed.contains_key("name"));
140 }
141
142 #[test]
143 fn nested_diff() {
144 let inner_a = obj(vec![("host", Value::String("localhost".into()))]);
145 let inner_b = obj(vec![("host", Value::String("0.0.0.0".into()))]);
146 let a = obj(vec![("server", Value::Object(inner_a))]);
147 let b = obj(vec![("server", Value::Object(inner_b))]);
148 let d = diff(&a, &b);
149 assert_eq!(d.changed.len(), 1);
150 }
151
152 #[test]
153 fn to_value_roundtrip() {
154 let a = obj(vec![("x", Value::Int(1)), ("y", Value::Int(2))]);
155 let b = obj(vec![("x", Value::Int(1)), ("z", Value::Int(3))]);
156 let d = diff(&a, &b);
157 let val = diff_to_value(&d);
158 assert!(matches!(val, Value::Object(_)));
159 }
160}