saluki_config/dynamic/
diff.rs1use super::event::ConfigChangeEvent;
4
5pub fn diff_config(old_config: &figment::value::Value, new_config: &figment::value::Value) -> Vec<ConfigChangeEvent> {
10 let mut changes = Vec::new();
11 diff_recursive(old_config, new_config, "", &mut changes);
12 changes
13}
14
15fn diff_recursive(
16 old_config: &figment::value::Value, new_config: &figment::value::Value, path: &str,
17 changes: &mut Vec<ConfigChangeEvent>,
18) {
19 if let (Some(old_dict), Some(new_dict)) = (old_config.as_dict(), new_config.as_dict()) {
20 for (key, new_value) in new_dict {
21 let current_path = if path.is_empty() {
22 key.clone()
23 } else {
24 format!("{}.{}", path, key)
25 };
26
27 match old_dict.get(key) {
28 Some(old_value) => {
29 if old_value != new_value {
30 if new_value.as_dict().is_some() && old_value.as_dict().is_some() {
31 diff_recursive(old_value, new_value, ¤t_path, changes);
32 } else {
33 changes.push(ConfigChangeEvent {
34 key: current_path,
35 old_value: Some(serde_json::to_value(old_value).unwrap()),
36 new_value: Some(serde_json::to_value(new_value).unwrap()),
37 });
38 }
39 }
40 }
41 None => {
42 changes.push(ConfigChangeEvent {
43 key: current_path,
44 old_value: None,
45 new_value: Some(serde_json::to_value(new_value).unwrap()),
46 });
47 }
48 }
49 }
50 }
51}
52
53#[cfg(test)]
54mod tests {
55 use figment::{providers::Serialized, value::Value, Figment};
56 use serde_json::json;
57
58 use super::*;
59
60 fn to_figment_value(json: serde_json::Value) -> Value {
61 let serialized = Serialized::defaults(json);
62 let value: Value = Figment::from(serialized).extract().unwrap();
63 value
64 }
65
66 #[test]
67 fn diff_config_detects_modified_added_and_nested_changes() {
68 let old_json = json!({
69 "a": "original",
70 "nested": {
71 "b": 100
72 },
73 "unchanged": true
74 });
75
76 let new_json = json!({
77 "a": "updated", "nested": {
79 "b": 200, "c": "new" },
82 "unchanged": true,
83 "d": "added" });
85
86 let old_config = to_figment_value(old_json);
87 let new_config = to_figment_value(new_json);
88
89 let changes = diff_config(&old_config, &new_config);
90
91 assert_eq!(changes.len(), 4);
93
94 assert!(changes.contains(&ConfigChangeEvent {
95 key: "a".to_string(),
96 old_value: Some("original".into()),
97 new_value: Some("updated".into())
98 }));
99 assert!(changes.contains(&ConfigChangeEvent {
100 key: "nested.b".to_string(),
101 old_value: Some(100.into()),
102 new_value: Some(200.into())
103 }));
104 assert!(changes.contains(&ConfigChangeEvent {
105 key: "nested.c".to_string(),
106 old_value: None,
107 new_value: Some("new".into())
108 }));
109 assert!(changes.contains(&ConfigChangeEvent {
110 key: "d".to_string(),
111 old_value: None,
112 new_value: Some("added".into())
113 }));
114 }
115
116 #[test]
117 fn diff_config_reports_no_changes_for_identical_configs() {
118 let old_json = json!({
119 "a": "original",
120 "nested": {
121 "b": 100
122 },
123 });
124
125 let new_json = old_json.clone();
126
127 let old_config = to_figment_value(old_json);
128 let new_config = to_figment_value(new_json);
129
130 let changes = diff_config(&old_config, &new_config);
131
132 assert!(changes.is_empty());
133 }
134
135 #[test]
136 fn diff_config_ignores_keys_removed_from_new_config() {
137 let old_json = json!({
141 "kept": "same",
142 "removed": "gone",
143 "nested": {
144 "kept": 1,
145 "removed": 2
146 }
147 });
148
149 let new_json = json!({
150 "kept": "same",
151 "nested": {
152 "kept": 1
153 }
154 });
155
156 let old_config = to_figment_value(old_json);
157 let new_config = to_figment_value(new_json);
158
159 let changes = diff_config(&old_config, &new_config);
160
161 assert!(
162 changes.is_empty(),
163 "removed keys should produce no change events, got: {:?}",
164 changes
165 );
166 }
167}