1use crate::error::Result;
6use crate::policy::conflictable::{AttributeValue, Conflictable};
7
8pub trait ResolutionPolicy<T: Conflictable>: Send + Sync {
32 fn resolve(&self, items: Vec<T>) -> Result<T>;
42
43 fn name(&self) -> &str;
45}
46
47pub 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) });
70
71 Ok(items.into_iter().next().unwrap())
72 }
73
74 fn name(&self) -> &str {
75 "LAST_WRITE_WINS"
76 }
77}
78
79pub struct HighestAttributeWinsPolicy {
96 attribute_name: String,
97}
98
99impl HighestAttributeWinsPolicy {
100 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 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, (None, Some(_)) => std::cmp::Ordering::Greater, (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
155pub 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#[allow(dead_code)]
185pub struct LowestAttributeWinsPolicy {
186 attribute_name: String,
187}
188
189#[allow(dead_code)]
190impl LowestAttributeWinsPolicy {
191 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, (None, Some(_)) => std::cmp::Ordering::Greater, (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 #[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"); }
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"); }
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"); }
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"); }
422}