saluki_config/dynamic/
diff.rs

1//! Functions for diffing configuration values.
2
3use super::event::ConfigChangeEvent;
4
5/// Diffs two configuration values and returns a list of changes.
6pub fn diff_config(old_config: &figment::value::Value, new_config: &figment::value::Value) -> Vec<ConfigChangeEvent> {
7    let mut changes = Vec::new();
8    diff_recursive(old_config, new_config, "", &mut changes);
9    changes
10}
11
12fn diff_recursive(
13    old_config: &figment::value::Value, new_config: &figment::value::Value, path: &str,
14    changes: &mut Vec<ConfigChangeEvent>,
15) {
16    if let (Some(old_dict), Some(new_dict)) = (old_config.as_dict(), new_config.as_dict()) {
17        for (key, new_value) in new_dict {
18            let current_path = if path.is_empty() {
19                key.clone()
20            } else {
21                format!("{}.{}", path, key)
22            };
23
24            match old_dict.get(key) {
25                Some(old_value) => {
26                    if old_value != new_value {
27                        if new_value.as_dict().is_some() && old_value.as_dict().is_some() {
28                            diff_recursive(old_value, new_value, &current_path, changes);
29                        } else {
30                            changes.push(ConfigChangeEvent {
31                                key: current_path,
32                                old_value: Some(serde_json::to_value(old_value).unwrap()),
33                                new_value: Some(serde_json::to_value(new_value).unwrap()),
34                            });
35                        }
36                    }
37                }
38                None => {
39                    changes.push(ConfigChangeEvent {
40                        key: current_path,
41                        old_value: None,
42                        new_value: Some(serde_json::to_value(new_value).unwrap()),
43                    });
44                }
45            }
46        }
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use figment::{providers::Serialized, value::Value, Figment};
53    use serde_json::json;
54
55    use super::*;
56
57    fn to_figment_value(json: serde_json::Value) -> Value {
58        let serialized = Serialized::defaults(json);
59        let value: Value = Figment::from(serialized).extract().unwrap();
60        value
61    }
62
63    #[test]
64    fn test_diff_config_basic() {
65        let old_json = json!({
66            "a": "original",
67            "nested": {
68                "b": 100
69            },
70            "unchanged": true
71        });
72
73        let new_json = json!({
74            "a": "updated", // modified
75            "nested": {
76                "b": 200, // nested modified
77                "c": "new"  // nested added
78            },
79            "unchanged": true,
80            "d": "added" // added
81        });
82
83        let old_config = to_figment_value(old_json);
84        let new_config = to_figment_value(new_json);
85
86        let changes = diff_config(&old_config, &new_config);
87
88        // We expect 4 changes in total.
89        assert_eq!(changes.len(), 4);
90
91        println!("changes: {:?}", changes);
92
93        assert!(changes.contains(&ConfigChangeEvent {
94            key: "a".to_string(),
95            old_value: Some("original".into()),
96            new_value: Some("updated".into())
97        }));
98        assert!(changes.contains(&ConfigChangeEvent {
99            key: "nested.b".to_string(),
100            old_value: Some(100.into()),
101            new_value: Some(200.into())
102        }));
103        assert!(changes.contains(&ConfigChangeEvent {
104            key: "nested.c".to_string(),
105            old_value: None,
106            new_value: Some("new".into())
107        }));
108        assert!(changes.contains(&ConfigChangeEvent {
109            key: "d".to_string(),
110            old_value: None,
111            new_value: Some("added".into())
112        }));
113    }
114
115    #[test]
116    fn test_diff_config_no_change() {
117        let old_json = json!({
118            "a": "original",
119            "nested": {
120                "b": 100
121            },
122        });
123
124        let new_json = old_json.clone();
125
126        let old_config = to_figment_value(old_json);
127        let new_config = to_figment_value(new_json);
128
129        let changes = diff_config(&old_config, &new_config);
130
131        assert!(changes.is_empty());
132    }
133}