1use boarddown_core::{Error, Task, TaskOp, Operation};
2
3pub struct OtEngine;
4
5impl OtEngine {
6 pub fn new() -> Self {
7 Self
8 }
9
10 pub fn transform(&self, op1: &TaskOp, op2: &TaskOp) -> Result<(TaskOp, TaskOp), Error> {
11 if op1.timestamp <= op2.timestamp {
12 return Ok((op1.clone(), op2.clone()));
13 }
14
15 let transformed_op2 = match (&op1.operation, &op2.operation) {
16 (
17 Operation::SetTitle { old: _, new: new1 },
18 Operation::SetTitle { old: _, new: new2 }
19 ) => {
20 TaskOp {
21 task_id: op2.task_id.clone(),
22 operation: Operation::SetTitle {
23 old: new1.clone(),
24 new: new2.clone(),
25 },
26 timestamp: op2.timestamp,
27 client_id: op2.client_id,
28 }
29 }
30 (
31 Operation::SetStatus { old: old1, new: _ },
32 Operation::SetStatus { old: _, new: new2 }
33 ) => {
34 TaskOp {
35 task_id: op2.task_id.clone(),
36 operation: Operation::SetStatus {
37 old: old1.clone(),
38 new: new2.clone(),
39 },
40 timestamp: op2.timestamp,
41 client_id: op2.client_id,
42 }
43 }
44 _ => op2.clone(),
45 };
46
47 Ok((op1.clone(), transformed_op2))
48 }
49
50 pub fn compose(&self, op1: &TaskOp, op2: &TaskOp) -> Result<TaskOp, Error> {
51 if op1.task_id != op2.task_id {
52 return Err(Error::Conflict("Cannot compose operations on different tasks".to_string()));
53 }
54
55 let composed = match (&op1.operation, &op2.operation) {
56 (Operation::SetTitle { old, new: _ }, Operation::SetTitle { old: _, new }) => {
57 Operation::SetTitle {
58 old: old.clone(),
59 new: new.clone(),
60 }
61 }
62 (Operation::SetStatus { old, new: _ }, Operation::SetStatus { old: _, new }) => {
63 Operation::SetStatus {
64 old: old.clone(),
65 new: new.clone(),
66 }
67 }
68 (Operation::AddDependency { dependency }, Operation::RemoveDependency { dependency: rem_dep }) => {
69 if dependency == rem_dep {
70 return Err(Error::Conflict("Add and remove dependency cancel out".to_string()));
71 }
72 op2.operation.clone()
73 }
74 _ => op2.operation.clone(),
75 };
76
77 Ok(TaskOp {
78 task_id: op2.task_id.clone(),
79 operation: composed,
80 timestamp: op2.timestamp,
81 client_id: op2.client_id,
82 })
83 }
84
85 pub fn invert(&self, op: &TaskOp) -> Result<TaskOp, Error> {
86 let inverted = match &op.operation {
87 Operation::SetTitle { old, new } => {
88 Operation::SetTitle {
89 old: new.clone(),
90 new: old.clone(),
91 }
92 }
93 Operation::SetStatus { old, new } => {
94 Operation::SetStatus {
95 old: *new,
96 new: *old,
97 }
98 }
99 Operation::SetMetadata { key, old, new } => {
100 Operation::SetMetadata {
101 key: key.clone(),
102 old: new.clone(),
103 new: old.clone(),
104 }
105 }
106 Operation::AddDependency { dependency } => {
107 Operation::RemoveDependency {
108 dependency: dependency.clone(),
109 }
110 }
111 Operation::RemoveDependency { dependency } => {
112 Operation::AddDependency {
113 dependency: dependency.clone(),
114 }
115 }
116 Operation::Move { from_column, to_column } => {
117 Operation::Move {
118 from_column: to_column.clone(),
119 to_column: from_column.clone(),
120 }
121 }
122 Operation::Create { title, column } => {
123 Operation::Delete
124 }
125 Operation::Delete => {
126 return Err(Error::Conflict("Cannot invert delete operation".to_string()));
127 }
128 };
129
130 Ok(TaskOp {
131 task_id: op.task_id.clone(),
132 operation: inverted,
133 timestamp: op.timestamp,
134 client_id: op.client_id,
135 })
136 }
137
138 pub fn apply(&self, task: &mut Task, op: &TaskOp) -> Result<(), Error> {
139 match &op.operation {
140 Operation::Delete => {
141 return Err(Error::Conflict("Use board-level delete, not task-level".to_string()));
142 }
143 Operation::Create { .. } => {
144 return Err(Error::Conflict("Cannot apply create operation via apply()".to_string()));
145 }
146 _ => {
147 op.operation.apply_to_task(task);
148 }
149 }
150 Ok(())
151 }
152}
153
154impl Default for OtEngine {
155 fn default() -> Self {
156 Self::new()
157 }
158}