Skip to main content

peat_protocol/policy/
policies.rs

1//! Resolution policies for conflict resolution
2//!
3//! Defines the trait and common implementations for conflict resolution strategies.
4
5use crate::error::Result;
6use crate::policy::conflictable::{AttributeValue, Conflictable};
7
8/// Trait for conflict resolution policies
9///
10/// Implement this trait to define custom conflict resolution strategies.
11///
12/// ## Example
13///
14/// ```rust,no_run
15/// use peat_protocol::policy::{ResolutionPolicy, Conflictable};
16/// use peat_protocol::error::Result;
17///
18/// struct MyCustomPolicy;
19///
20/// impl<T: Conflictable> ResolutionPolicy<T> for MyCustomPolicy {
21///     fn resolve(&self, mut items: Vec<T>) -> Result<T> {
22///         // Custom logic here
23///         Ok(items.into_iter().next().unwrap())
24///     }
25///
26///     fn name(&self) -> &str {
27///         "MY_CUSTOM_POLICY"
28///     }
29/// }
30/// ```
31pub trait ResolutionPolicy<T: Conflictable>: Send + Sync {
32    /// Resolve conflict between multiple items
33    ///
34    /// Takes a list of conflicting items and returns the "winning" item
35    /// according to the policy's logic.
36    ///
37    /// # Errors
38    ///
39    /// Returns an error if the policy cannot resolve the conflict
40    /// (e.g., all items are equal, required attributes missing).
41    fn resolve(&self, items: Vec<T>) -> Result<T>;
42
43    /// Policy name for logging and debugging
44    fn name(&self) -> &str;
45}
46
47/// Policy: Most recent item wins (based on timestamp)
48///
49/// Uses the `timestamp()` method from Conflictable.
50/// Items without timestamps are treated as oldest.
51pub struct LastWriteWinsPolicy;
52
53impl<T: Conflictable> ResolutionPolicy<T> for LastWriteWinsPolicy {
54    fn resolve(&self, mut items: Vec<T>) -> Result<T> {
55        if items.is_empty() {
56            return Err(crate::Error::InvalidInput(
57                "Cannot resolve empty item list".to_string(),
58            ));
59        }
60
61        if items.len() == 1 {
62            return Ok(items.into_iter().next().unwrap());
63        }
64
65        items.sort_by(|a, b| {
66            let a_time = a.timestamp().unwrap_or(0);
67            let b_time = b.timestamp().unwrap_or(0);
68            b_time.cmp(&a_time) // Most recent first
69        });
70
71        Ok(items.into_iter().next().unwrap())
72    }
73
74    fn name(&self) -> &str {
75        "LAST_WRITE_WINS"
76    }
77}
78
79/// Policy: Item with highest value for specified attribute wins
80///
81/// Supports Int, Uint, and Float attribute types.
82/// Items without the attribute are treated as having value 0.
83///
84/// ## Example
85///
86/// ```rust,no_run
87/// use peat_protocol::policy::HighestAttributeWinsPolicy;
88///
89/// // Resolve based on "priority" attribute
90/// let policy = HighestAttributeWinsPolicy::new("priority");
91///
92/// // Resolve based on "confidence" attribute
93/// let policy = HighestAttributeWinsPolicy::new("confidence");
94/// ```
95pub struct HighestAttributeWinsPolicy {
96    attribute_name: String,
97}
98
99impl HighestAttributeWinsPolicy {
100    /// Create a new policy that selects based on highest attribute value
101    pub fn new(attribute_name: impl Into<String>) -> Self {
102        Self {
103            attribute_name: attribute_name.into(),
104        }
105    }
106}
107
108impl<T: Conflictable> ResolutionPolicy<T> for HighestAttributeWinsPolicy {
109    fn resolve(&self, mut items: Vec<T>) -> Result<T> {
110        if items.is_empty() {
111            return Err(crate::Error::InvalidInput(
112                "Cannot resolve empty item list".to_string(),
113            ));
114        }
115
116        if items.len() == 1 {
117            return Ok(items.into_iter().next().unwrap());
118        }
119
120        items.sort_by(|a, b| {
121            let a_attrs = a.attributes();
122            let b_attrs = b.attributes();
123
124            let a_val = a_attrs.get(&self.attribute_name);
125            let b_val = b_attrs.get(&self.attribute_name);
126
127            match (a_val, b_val) {
128                (Some(AttributeValue::Int(a)), Some(AttributeValue::Int(b))) => b.cmp(a),
129                (Some(AttributeValue::Uint(a)), Some(AttributeValue::Uint(b))) => b.cmp(a),
130                (Some(AttributeValue::Float(a)), Some(AttributeValue::Float(b))) => {
131                    b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
132                }
133                (Some(a_val), Some(b_val)) => {
134                    // Try to coerce to comparable types
135                    let a_float = a_val.as_float();
136                    let b_float = b_val.as_float();
137                    b_float
138                        .partial_cmp(&a_float)
139                        .unwrap_or(std::cmp::Ordering::Equal)
140                }
141                (Some(_), None) => std::cmp::Ordering::Less, // a wins (has attribute)
142                (None, Some(_)) => std::cmp::Ordering::Greater, // b wins (has attribute)
143                (None, None) => std::cmp::Ordering::Equal,
144            }
145        });
146
147        Ok(items.into_iter().next().unwrap())
148    }
149
150    fn name(&self) -> &str {
151        &self.attribute_name
152    }
153}
154
155/// Policy: Reject all conflicts (return error)
156///
157/// Use this when conflicts are not allowed and should be treated as errors.
158pub struct RejectConflictPolicy;
159
160impl<T: Conflictable> ResolutionPolicy<T> for RejectConflictPolicy {
161    fn resolve(&self, items: Vec<T>) -> Result<T> {
162        if items.len() > 1 {
163            return Err(crate::Error::ConflictDetected(format!(
164                "Conflict detected between {} items (policy: REJECT)",
165                items.len()
166            )));
167        }
168
169        items
170            .into_iter()
171            .next()
172            .ok_or_else(|| crate::Error::InvalidInput("Empty item list".to_string()))
173    }
174
175    fn name(&self) -> &str {
176        "REJECT_CONFLICT"
177    }
178}
179
180/// Policy: Item with lowest value for specified attribute wins
181///
182/// Opposite of `HighestAttributeWinsPolicy`.
183/// Useful for selecting items with lowest cost, earliest deadline, etc.
184#[allow(dead_code)]
185pub struct LowestAttributeWinsPolicy {
186    attribute_name: String,
187}
188
189#[allow(dead_code)]
190impl LowestAttributeWinsPolicy {
191    /// Create a new policy that selects based on lowest attribute value
192    pub fn new(attribute_name: impl Into<String>) -> Self {
193        Self {
194            attribute_name: attribute_name.into(),
195        }
196    }
197}
198
199impl<T: Conflictable> ResolutionPolicy<T> for LowestAttributeWinsPolicy {
200    fn resolve(&self, mut items: Vec<T>) -> Result<T> {
201        if items.is_empty() {
202            return Err(crate::Error::InvalidInput(
203                "Cannot resolve empty item list".to_string(),
204            ));
205        }
206
207        if items.len() == 1 {
208            return Ok(items.into_iter().next().unwrap());
209        }
210
211        items.sort_by(|a, b| {
212            let a_attrs = a.attributes();
213            let b_attrs = b.attributes();
214
215            let a_val = a_attrs.get(&self.attribute_name);
216            let b_val = b_attrs.get(&self.attribute_name);
217
218            match (a_val, b_val) {
219                (Some(AttributeValue::Int(a)), Some(AttributeValue::Int(b))) => a.cmp(b),
220                (Some(AttributeValue::Uint(a)), Some(AttributeValue::Uint(b))) => a.cmp(b),
221                (Some(AttributeValue::Float(a)), Some(AttributeValue::Float(b))) => {
222                    a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
223                }
224                (Some(a_val), Some(b_val)) => {
225                    let a_float = a_val.as_float();
226                    let b_float = b_val.as_float();
227                    a_float
228                        .partial_cmp(&b_float)
229                        .unwrap_or(std::cmp::Ordering::Equal)
230                }
231                (Some(_), None) => std::cmp::Ordering::Less, // a wins (has attribute)
232                (None, Some(_)) => std::cmp::Ordering::Greater, // b wins (has attribute)
233                (None, None) => std::cmp::Ordering::Equal,
234            }
235        });
236
237        Ok(items.into_iter().next().unwrap())
238    }
239
240    fn name(&self) -> &str {
241        &self.attribute_name
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248    use std::collections::HashMap;
249
250    // Test type for policy tests
251    #[derive(Clone)]
252    struct TestItem {
253        id: String,
254        timestamp: u64,
255        priority: i64,
256        confidence: f64,
257    }
258
259    impl Conflictable for TestItem {
260        fn id(&self) -> String {
261            self.id.clone()
262        }
263
264        fn conflict_keys(&self) -> Vec<String> {
265            vec!["test-key".to_string()]
266        }
267
268        fn timestamp(&self) -> Option<u64> {
269            Some(self.timestamp)
270        }
271
272        fn attributes(&self) -> HashMap<String, AttributeValue> {
273            let mut attrs = HashMap::new();
274            attrs.insert("priority".to_string(), AttributeValue::Int(self.priority));
275            attrs.insert(
276                "confidence".to_string(),
277                AttributeValue::Float(self.confidence),
278            );
279            attrs
280        }
281    }
282
283    #[test]
284    fn test_last_write_wins() {
285        let policy = LastWriteWinsPolicy;
286
287        let items = vec![
288            TestItem {
289                id: "item-1".to_string(),
290                timestamp: 1000,
291                priority: 1,
292                confidence: 0.5,
293            },
294            TestItem {
295                id: "item-2".to_string(),
296                timestamp: 2000,
297                priority: 2,
298                confidence: 0.7,
299            },
300            TestItem {
301                id: "item-3".to_string(),
302                timestamp: 1500,
303                priority: 3,
304                confidence: 0.9,
305            },
306        ];
307
308        let winner = policy.resolve(items).unwrap();
309        assert_eq!(winner.id, "item-2"); // Most recent (2000)
310    }
311
312    #[test]
313    fn test_highest_attribute_wins_int() {
314        let policy = HighestAttributeWinsPolicy::new("priority");
315
316        let items = vec![
317            TestItem {
318                id: "item-1".to_string(),
319                timestamp: 1000,
320                priority: 1,
321                confidence: 0.5,
322            },
323            TestItem {
324                id: "item-2".to_string(),
325                timestamp: 2000,
326                priority: 5,
327                confidence: 0.7,
328            },
329            TestItem {
330                id: "item-3".to_string(),
331                timestamp: 1500,
332                priority: 3,
333                confidence: 0.9,
334            },
335        ];
336
337        let winner = policy.resolve(items).unwrap();
338        assert_eq!(winner.id, "item-2"); // Highest priority (5)
339    }
340
341    #[test]
342    fn test_highest_attribute_wins_float() {
343        let policy = HighestAttributeWinsPolicy::new("confidence");
344
345        let items = vec![
346            TestItem {
347                id: "item-1".to_string(),
348                timestamp: 1000,
349                priority: 1,
350                confidence: 0.5,
351            },
352            TestItem {
353                id: "item-2".to_string(),
354                timestamp: 2000,
355                priority: 5,
356                confidence: 0.7,
357            },
358            TestItem {
359                id: "item-3".to_string(),
360                timestamp: 1500,
361                priority: 3,
362                confidence: 0.95,
363            },
364        ];
365
366        let winner = policy.resolve(items).unwrap();
367        assert_eq!(winner.id, "item-3"); // Highest confidence (0.95)
368    }
369
370    #[test]
371    fn test_reject_conflict_policy() {
372        let policy = RejectConflictPolicy;
373
374        let items = vec![
375            TestItem {
376                id: "item-1".to_string(),
377                timestamp: 1000,
378                priority: 1,
379                confidence: 0.5,
380            },
381            TestItem {
382                id: "item-2".to_string(),
383                timestamp: 2000,
384                priority: 5,
385                confidence: 0.7,
386            },
387        ];
388
389        let result = policy.resolve(items);
390        assert!(result.is_err());
391        assert!(matches!(result, Err(crate::Error::ConflictDetected(_))));
392    }
393
394    #[test]
395    fn test_lowest_attribute_wins() {
396        let policy = LowestAttributeWinsPolicy::new("priority");
397
398        let items = vec![
399            TestItem {
400                id: "item-1".to_string(),
401                timestamp: 1000,
402                priority: 10,
403                confidence: 0.5,
404            },
405            TestItem {
406                id: "item-2".to_string(),
407                timestamp: 2000,
408                priority: 2,
409                confidence: 0.7,
410            },
411            TestItem {
412                id: "item-3".to_string(),
413                timestamp: 1500,
414                priority: 5,
415                confidence: 0.9,
416            },
417        ];
418
419        let winner = policy.resolve(items).unwrap();
420        assert_eq!(winner.id, "item-2"); // Lowest priority (2)
421    }
422}