1use core::fmt;
2
3use crate::rga::{Rga, RgaDelta, RgaError};
4use crate::{Crdt, DeltaCrdt, NodeId};
5
6#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum TextError {
9 IndexOutOfBounds {
11 index: usize,
13 len: usize,
15 },
16 RangeOutOfBounds {
18 start: usize,
20 end: usize,
22 len: usize,
24 },
25}
26
27impl fmt::Display for TextError {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 match self {
30 Self::IndexOutOfBounds { index, len } => {
31 write!(f, "index {index} out of bounds for text of length {len}")
32 }
33 Self::RangeOutOfBounds { start, end, len } => {
34 write!(f, "range {start}..{end} out of bounds for text of length {len}")
35 }
36 }
37 }
38}
39
40#[cfg(feature = "std")]
41impl std::error::Error for TextError {}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
66#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
67pub struct TextCrdt(Rga<char>);
68
69pub type TextDelta = RgaDelta<char>;
71
72impl TextCrdt {
73 pub fn new(actor: NodeId) -> Self {
75 Self(Rga::new(actor))
76 }
77
78 pub fn fork(&self, new_actor: NodeId) -> Self {
80 Self(self.0.fork(new_actor))
81 }
82
83 pub fn insert(&mut self, index: usize, ch: char) -> Result<(), TextError> {
85 self.0.insert_at(index, ch).map_err(|e| match e {
86 RgaError::IndexOutOfBounds { index, len } => {
87 TextError::IndexOutOfBounds { index, len }
88 }
89 })
90 }
91
92 pub fn insert_str(&mut self, index: usize, s: &str) -> Result<(), TextError> {
94 if index > self.0.len() {
95 return Err(TextError::IndexOutOfBounds {
96 index,
97 len: self.0.len(),
98 });
99 }
100 for (i, ch) in s.chars().enumerate() {
101 self.insert(index + i, ch)?;
102 }
103 Ok(())
104 }
105
106 pub fn remove(&mut self, index: usize) -> Result<(), TextError> {
108 self.0.remove(index).map(|_| ()).map_err(|e| match e {
109 RgaError::IndexOutOfBounds { index, len } => {
110 TextError::IndexOutOfBounds { index, len }
111 }
112 })
113 }
114
115 pub fn remove_range(&mut self, start: usize, count: usize) -> Result<(), TextError> {
117 let len = self.0.len();
118 if start + count > len {
119 return Err(TextError::RangeOutOfBounds {
120 start,
121 end: start + count,
122 len,
123 });
124 }
125 for _ in 0..count {
127 self.remove(start)?;
128 }
129 Ok(())
130 }
131
132 #[must_use]
134 pub fn len(&self) -> usize {
135 self.0.len()
136 }
137
138 #[must_use]
140 pub fn is_empty(&self) -> bool {
141 self.0.is_empty()
142 }
143
144 #[must_use]
146 pub fn actor(&self) -> NodeId {
147 self.0.actor()
148 }
149}
150
151impl Crdt for TextCrdt {
152 fn merge(&mut self, other: &Self) {
153 self.0.merge(&other.0);
154 }
155}
156
157impl DeltaCrdt for TextCrdt {
158 type Delta = TextDelta;
159
160 fn delta(&self, other: &Self) -> TextDelta {
161 self.0.delta(&other.0)
162 }
163
164 fn apply_delta(&mut self, delta: &TextDelta) {
165 self.0.apply_delta(delta);
166 }
167}
168
169impl fmt::Display for TextCrdt {
170 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171 for ch in self.0.iter() {
172 write!(f, "{}", ch)?;
173 }
174 Ok(())
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 #[test]
183 fn new_text_is_empty() {
184 let t = TextCrdt::new(1);
185 assert!(t.is_empty());
186 assert_eq!(t.len(), 0);
187 assert_eq!(t.to_string(), "");
188 }
189
190 #[test]
191 fn insert_single_char() {
192 let mut t = TextCrdt::new(1);
193 t.insert(0, 'x').unwrap();
194 assert_eq!(t.to_string(), "x");
195 assert_eq!(t.len(), 1);
196 }
197
198 #[test]
199 fn insert_at_beginning_middle_end() {
200 let mut t = TextCrdt::new(1);
201 t.insert(0, 'a').unwrap();
202 t.insert(1, 'c').unwrap();
203 t.insert(1, 'b').unwrap();
204 t.insert(0, 'z').unwrap();
205 assert_eq!(t.to_string(), "zabc");
206 }
207
208 #[test]
209 fn delete_char() {
210 let mut t = TextCrdt::new(1);
211 t.insert_str(0, "hello").unwrap();
212 t.remove(1).unwrap();
213 assert_eq!(t.to_string(), "hllo");
214 assert_eq!(t.len(), 4);
215 }
216
217 #[test]
218 fn insert_str_basic() {
219 let mut t = TextCrdt::new(1);
220 t.insert_str(0, "hello").unwrap();
221 assert_eq!(t.to_string(), "hello");
222 assert_eq!(t.len(), 5);
223 }
224
225 #[test]
226 fn remove_range_basic() {
227 let mut t = TextCrdt::new(1);
228 t.insert_str(0, "hello world").unwrap();
229 t.remove_range(5, 6).unwrap();
230 assert_eq!(t.to_string(), "hello");
231 }
232
233 #[test]
234 fn merge_disjoint_inserts() {
235 let mut t1 = TextCrdt::new(1);
236 t1.insert_str(0, "hello").unwrap();
237
238 let mut t2 = TextCrdt::new(2);
239 t2.insert_str(0, "world").unwrap();
240
241 t1.merge(&t2);
242 assert_eq!(t1.len(), 10);
243 }
244
245 #[test]
246 fn merge_propagates_tombstones() {
247 let mut t1 = TextCrdt::new(1);
248 t1.insert_str(0, "abc").unwrap();
249
250 let mut t2 = t1.fork(2);
251 t2.remove(1).unwrap();
252
253 t1.merge(&t2);
254 assert_eq!(t1.to_string(), "ac");
255 }
256
257 #[test]
258 fn merge_commutativity() {
259 let mut t1 = TextCrdt::new(1);
260 t1.insert_str(0, "hello").unwrap();
261
262 let mut t2 = TextCrdt::new(2);
263 t2.insert_str(0, "world").unwrap();
264
265 let mut left = t1.clone();
266 left.merge(&t2);
267
268 let mut right = t2.clone();
269 right.merge(&t1);
270
271 assert_eq!(left.to_string(), right.to_string());
272 }
273
274 #[test]
275 fn merge_idempotency() {
276 let mut t1 = TextCrdt::new(1);
277 t1.insert_str(0, "hello").unwrap();
278
279 let mut t2 = TextCrdt::new(2);
280 t2.insert_str(0, "world").unwrap();
281
282 t1.merge(&t2);
283 let after_first = t1.clone();
284 t1.merge(&t2);
285
286 assert_eq!(t1.to_string(), after_first.to_string());
287 }
288
289 #[test]
290 fn concurrent_inserts_at_same_position() {
291 let mut t1 = TextCrdt::new(1);
292 t1.insert(0, 'a').unwrap();
293
294 let mut t2 = TextCrdt::new(2);
295 t2.insert(0, 'b').unwrap();
296
297 let mut left = t1.clone();
298 left.merge(&t2);
299
300 let mut right = t2.clone();
301 right.merge(&t1);
302
303 assert_eq!(left.to_string(), right.to_string());
304 assert_eq!(left.len(), 2);
305 }
306
307 #[test]
308 fn delta_apply_equivalent_to_merge() {
309 let mut t1 = TextCrdt::new(1);
310 t1.insert_str(0, "hello").unwrap();
311
312 let mut t2 = TextCrdt::new(2);
313 t2.insert_str(0, "world").unwrap();
314
315 let mut via_merge = t2.clone();
316 via_merge.merge(&t1);
317
318 let mut via_delta = t2.clone();
319 let d = t1.delta(&t2);
320 via_delta.apply_delta(&d);
321
322 assert_eq!(via_merge.to_string(), via_delta.to_string());
323 }
324
325 #[test]
326 fn triple_merge_convergence() {
327 let mut t1 = TextCrdt::new(1);
328 t1.insert_str(0, "base").unwrap();
329
330 let mut t2 = t1.fork(2);
331 let mut t3 = t1.fork(3);
332
333 t1.insert(4, '!').unwrap();
334 t2.insert(0, '>').unwrap();
335 t3.remove(2).unwrap();
336
337 let mut r1 = t1.clone();
338 r1.merge(&t2);
339 r1.merge(&t3);
340
341 let mut r2 = t2.clone();
342 r2.merge(&t3);
343 r2.merge(&t1);
344
345 let mut r3 = t3.clone();
346 r3.merge(&t1);
347 r3.merge(&t2);
348
349 assert_eq!(r1.to_string(), r2.to_string());
350 assert_eq!(r2.to_string(), r3.to_string());
351 }
352
353 #[test]
354 fn insert_out_of_bounds_returns_error() {
355 let mut t = TextCrdt::new(1);
356 let err = t.insert(1, 'x');
357 assert_eq!(err, Err(TextError::IndexOutOfBounds { index: 1, len: 0 }));
358 }
359
360 #[test]
361 fn remove_out_of_bounds_returns_error() {
362 let mut t = TextCrdt::new(1);
363 let err = t.remove(0);
364 assert_eq!(err, Err(TextError::IndexOutOfBounds { index: 0, len: 0 }));
365 }
366
367 #[test]
368 fn display_trait() {
369 let mut t = TextCrdt::new(1);
370 t.insert_str(0, "hello").unwrap();
371 assert_eq!(format!("{t}"), "hello");
372 }
373}