Development Documentation (main branch) - For stable release docs, see docs.rs/eidetica

eidetica/instance/
settings_merge.rs

1//! Settings merge logic for combining user preferences.
2//!
3//! This module implements the "most aggressive" merge strategy for combining
4//! sync settings from multiple users who are tracking the same database.
5
6use crate::user::types::SyncSettings;
7
8/// Merge multiple user sync settings into a single combined setting.
9///
10/// Uses the "most aggressive" strategy:
11/// - `sync_enabled`: OR (true if any user wants sync)
12/// - `sync_on_commit`: OR (true if any user wants it)
13/// - `interval_seconds`: MIN (most frequent sync wins)
14/// - `properties`: UNION (combine all properties, later values override)
15///
16/// # Arguments
17/// * `settings` - Vector of sync settings from different users
18///
19/// # Returns
20/// Combined sync settings, or a default if the input is empty
21///
22/// # Examples
23/// ```
24/// use eidetica::instance::settings_merge::merge_sync_settings;
25/// use eidetica::user::types::SyncSettings;
26///
27/// let settings1 = SyncSettings {
28///     sync_enabled: true,
29///     sync_on_commit: false,
30///     interval_seconds: Some(300),
31///     properties: Default::default(),
32/// };
33///
34/// let settings2 = SyncSettings {
35///     sync_enabled: false,
36///     sync_on_commit: true,
37///     interval_seconds: Some(60),
38///     properties: Default::default(),
39/// };
40///
41/// let combined = merge_sync_settings(vec![settings1, settings2]);
42///
43/// // Most aggressive wins
44/// assert_eq!(combined.sync_enabled, true);  // OR
45/// assert_eq!(combined.sync_on_commit, true);  // OR
46/// assert_eq!(combined.interval_seconds, Some(60));  // MIN
47/// ```
48pub fn merge_sync_settings(settings: Vec<SyncSettings>) -> SyncSettings {
49    if settings.is_empty() {
50        return SyncSettings::disabled();
51    }
52
53    // Start with the first setting as base
54    let mut combined = settings[0].clone();
55
56    // Merge with remaining settings
57    for setting in settings.iter().skip(1) {
58        // OR for boolean flags
59        combined.sync_enabled = combined.sync_enabled || setting.sync_enabled;
60        combined.sync_on_commit = combined.sync_on_commit || setting.sync_on_commit;
61
62        // MIN for interval (most frequent sync)
63        combined.interval_seconds = match (combined.interval_seconds, setting.interval_seconds) {
64            (Some(a), Some(b)) => Some(a.min(b)),
65            (Some(a), None) => Some(a),
66            (None, Some(b)) => Some(b),
67            (None, None) => None,
68        };
69
70        // UNION properties (later values override)
71        for (key, value) in &setting.properties {
72            combined.properties.insert(key.clone(), value.clone());
73        }
74    }
75
76    combined
77}
78
79#[cfg(test)]
80mod tests {
81    use std::collections::HashMap;
82
83    use super::*;
84
85    #[test]
86    fn test_merge_empty_settings() {
87        let result = merge_sync_settings(vec![]);
88        assert!(!result.sync_enabled);
89        assert!(!result.sync_on_commit);
90        assert_eq!(result.interval_seconds, None);
91        assert!(result.properties.is_empty());
92    }
93
94    #[test]
95    fn test_merge_single_setting() {
96        let settings = SyncSettings {
97            sync_enabled: true,
98            sync_on_commit: true,
99            interval_seconds: Some(60),
100            properties: {
101                let mut map = HashMap::new();
102                map.insert("key1".to_string(), "value1".to_string());
103                map
104            },
105        };
106
107        let result = merge_sync_settings(vec![settings.clone()]);
108        assert_eq!(result.sync_enabled, settings.sync_enabled);
109        assert_eq!(result.sync_on_commit, settings.sync_on_commit);
110        assert_eq!(result.interval_seconds, settings.interval_seconds);
111        assert_eq!(result.properties.len(), 1);
112    }
113
114    #[test]
115    fn test_merge_sync_enabled_or() {
116        let settings1 = SyncSettings {
117            sync_enabled: true,
118            ..Default::default()
119        };
120
121        let settings2 = SyncSettings {
122            sync_enabled: false,
123            ..Default::default()
124        };
125
126        let result = merge_sync_settings(vec![settings1, settings2.clone()]);
127        assert!(result.sync_enabled); // true OR false = true
128
129        let result2 = merge_sync_settings(vec![settings2.clone(), settings2]);
130        assert!(!result2.sync_enabled); // false OR false = false
131    }
132
133    #[test]
134    fn test_merge_sync_on_commit_or() {
135        let settings1 = SyncSettings {
136            sync_on_commit: true,
137            ..Default::default()
138        };
139
140        let settings2 = SyncSettings {
141            sync_on_commit: false,
142            ..Default::default()
143        };
144
145        let result = merge_sync_settings(vec![settings1, settings2]);
146        assert!(result.sync_on_commit); // true OR false = true
147    }
148
149    #[test]
150    fn test_merge_interval_min() {
151        let settings1 = SyncSettings {
152            interval_seconds: Some(300),
153            ..Default::default()
154        };
155
156        let settings2 = SyncSettings {
157            interval_seconds: Some(60),
158            ..Default::default()
159        };
160
161        let settings3 = SyncSettings {
162            interval_seconds: Some(120),
163            ..Default::default()
164        };
165
166        let result = merge_sync_settings(vec![settings1, settings2, settings3]);
167        assert_eq!(result.interval_seconds, Some(60)); // MIN(300, 60, 120) = 60
168    }
169
170    #[test]
171    fn test_merge_interval_with_none() {
172        let settings1 = SyncSettings {
173            interval_seconds: Some(300),
174            ..Default::default()
175        };
176
177        let settings2 = SyncSettings {
178            interval_seconds: None,
179            ..Default::default()
180        };
181
182        let result = merge_sync_settings(vec![settings1.clone(), settings2.clone()]);
183        assert_eq!(result.interval_seconds, Some(300)); // Some takes precedence
184
185        let result2 = merge_sync_settings(vec![settings2.clone(), settings1]);
186        assert_eq!(result2.interval_seconds, Some(300));
187
188        let result3 = merge_sync_settings(vec![settings2.clone(), settings2]);
189        assert_eq!(result3.interval_seconds, None); // None + None = None
190    }
191
192    #[test]
193    fn test_merge_properties_union() {
194        let settings1 = SyncSettings {
195            properties: {
196                let mut map = HashMap::new();
197                map.insert("key1".to_string(), "value1".to_string());
198                map.insert("key2".to_string(), "value2".to_string());
199                map
200            },
201            ..Default::default()
202        };
203
204        let settings2 = SyncSettings {
205            properties: {
206                let mut map = HashMap::new();
207                map.insert("key2".to_string(), "value2_override".to_string());
208                map.insert("key3".to_string(), "value3".to_string());
209                map
210            },
211            ..Default::default()
212        };
213
214        let result = merge_sync_settings(vec![settings1, settings2]);
215
216        assert_eq!(result.properties.len(), 3);
217        assert_eq!(result.properties.get("key1"), Some(&"value1".to_string()));
218        assert_eq!(
219            result.properties.get("key2"),
220            Some(&"value2_override".to_string())
221        ); // Override
222        assert_eq!(result.properties.get("key3"), Some(&"value3".to_string()));
223    }
224
225    #[test]
226    fn test_merge_all_features() {
227        let settings1 = SyncSettings {
228            sync_enabled: true,
229            sync_on_commit: false,
230            interval_seconds: Some(300),
231            properties: {
232                let mut map = HashMap::new();
233                map.insert("priority".to_string(), "low".to_string());
234                map
235            },
236        };
237
238        let settings2 = SyncSettings {
239            sync_enabled: false,
240            sync_on_commit: true,
241            interval_seconds: Some(60),
242            properties: {
243                let mut map = HashMap::new();
244                map.insert("priority".to_string(), "high".to_string());
245                map.insert("transport".to_string(), "http".to_string());
246                map
247            },
248        };
249
250        let settings3 = SyncSettings {
251            sync_enabled: false,
252            sync_on_commit: false,
253            interval_seconds: Some(120),
254            properties: {
255                let mut map = HashMap::new();
256                map.insert("region".to_string(), "us-west".to_string());
257                map
258            },
259        };
260
261        let result = merge_sync_settings(vec![settings1, settings2, settings3]);
262
263        // Most aggressive settings win
264        assert!(result.sync_enabled); // true OR false OR false = true
265        assert!(result.sync_on_commit); // false OR true OR false = true
266        assert_eq!(result.interval_seconds, Some(60)); // MIN(300, 60, 120) = 60
267
268        // Properties are unioned
269        assert_eq!(result.properties.len(), 3);
270        assert_eq!(result.properties.get("priority"), Some(&"high".to_string())); // Last override
271        assert_eq!(
272            result.properties.get("transport"),
273            Some(&"http".to_string())
274        );
275        assert_eq!(
276            result.properties.get("region"),
277            Some(&"us-west".to_string())
278        );
279    }
280}