boarddown_sync/
conflict.rs1use boarddown_core::{Board, Error, Task, ConflictResolution, TaskOp};
2
3use super::Conflict;
4
5#[derive(Debug, Clone, PartialEq)]
6pub enum Resolution {
7 Local,
8 Remote,
9 Manual { local: Task, remote: Task },
10}
11
12pub struct ConflictResolver {
13 strategy: ConflictResolution,
14}
15
16impl ConflictResolver {
17 pub fn new(strategy: ConflictResolution) -> Self {
18 Self { strategy }
19 }
20
21 pub fn resolve(&self, conflict: &Conflict, board: &mut Board) -> Result<Resolution, Error> {
22 match self.strategy {
23 ConflictResolution::LastWriteWins => {
24 Ok(Resolution::Remote)
25 }
26 ConflictResolution::Local => {
27 Ok(Resolution::Local)
28 }
29 ConflictResolution::Remote => {
30 Ok(Resolution::Remote)
31 }
32 ConflictResolution::Manual => {
33 let local_task = board.tasks.get(&conflict.task_id).cloned();
34 let remote_task = self.apply_operation_to_mock_board(&conflict.remote);
35 match (local_task, remote_task) {
36 (Some(local), Some(remote)) => Ok(Resolution::Manual { local, remote }),
37 (Some(local), None) => Ok(Resolution::Local),
38 (None, Some(remote)) => Ok(Resolution::Remote),
39 (None, None) => Ok(Resolution::Remote),
40 }
41 }
42 }
43 }
44
45 fn apply_operation_to_mock_board(&self, op: &TaskOp) -> Option<Task> {
46 let mut task = Task::builder()
47 .id(op.task_id.clone())
48 .title("temp")
49 .build()
50 .ok()?;
51
52 match &op.operation {
53 boarddown_core::Operation::SetTitle { old: _, new } => {
54 task.title = new.clone();
55 }
56 boarddown_core::Operation::SetStatus { old: _, new } => {
57 task.status = *new;
58 }
59 _ => return None,
60 }
61
62 Some(task)
63 }
64
65 pub fn resolve_task(&self, conflict: &Conflict) -> Result<Task, Error> {
66 match self.strategy {
67 ConflictResolution::LastWriteWins | ConflictResolution::Remote => {
68 self.apply_operation_to_mock_board(&conflict.remote)
69 .ok_or_else(|| Error::Conflict("Cannot resolve task".to_string()))
70 }
71 ConflictResolution::Local => {
72 self.apply_operation_to_mock_board(&conflict.local)
73 .ok_or_else(|| Error::Conflict("Cannot resolve task".to_string()))
74 }
75 ConflictResolution::Manual => {
76 self.apply_operation_to_mock_board(&conflict.remote)
77 .ok_or_else(|| Error::Conflict("Cannot resolve task".to_string()))
78 }
79 }
80 }
81
82 pub fn detect_conflicts(&self, local_ops: &[TaskOp], remote_ops: &[TaskOp]) -> Result<Vec<Conflict>, Error> {
83 let mut conflicts = Vec::new();
84
85 for local_op in local_ops {
86 for remote_op in remote_ops {
87 if local_op.task_id == remote_op.task_id && local_op.timestamp != remote_op.timestamp {
88 if self.operations_conflict(&local_op.operation, &remote_op.operation) {
89 conflicts.push(Conflict {
90 task_id: local_op.task_id.clone(),
91 local: local_op.clone(),
92 remote: remote_op.clone(),
93 });
94 }
95 }
96 }
97 }
98
99 Ok(conflicts)
100 }
101
102 fn operations_conflict(&self, op1: &boarddown_core::Operation, op2: &boarddown_core::Operation) -> bool {
103 use boarddown_core::Operation;
104
105 match (op1, op2) {
106 (Operation::SetTitle { .. }, Operation::SetTitle { .. }) => true,
107 (Operation::SetStatus { .. }, Operation::SetStatus { .. }) => true,
108 (Operation::SetMetadata { .. }, Operation::SetMetadata { .. }) => true,
109 (Operation::Delete, _) | (_, Operation::Delete) => true,
110 _ => false,
111 }
112 }
113}
114
115impl Default for ConflictResolver {
116 fn default() -> Self {
117 Self::new(ConflictResolution::LastWriteWins)
118 }
119}