Repository: spebern/operational-transform-rs Branch: master Commit: 9faa17f0a2b2 Files: 14 Total size: 40.8 KB Directory structure: gitextract_p95hh1cn/ ├── .cargo/ │ └── config ├── .github/ │ └── workflows/ │ └── build.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── operational-transform/ │ ├── .gitignore │ ├── Cargo.toml │ ├── benches/ │ │ └── benchmark.rs │ └── src/ │ ├── lib.rs │ ├── serde.rs │ └── utilities.rs └── xtask/ ├── Cargo.toml └── src/ └── main.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .cargo/config ================================================ [alias] xtask = "run --package xtask --" ================================================ FILE: .github/workflows/build.yml ================================================ name: Build on: pull_request: push: branches: - master # you can enable a schedule to build # schedule: # - cron: '00 01 * * *' jobs: check: name: Check runs-on: ubuntu-latest steps: - name: Checkout sources uses: actions/checkout@v2 - name: Install stable toolchain uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - uses: Swatinem/rust-cache@v1 - name: Run cargo check uses: actions-rs/cargo@v1 with: command: check test: name: Test Suite strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] rust: [stable] runs-on: ${{ matrix.os }} steps: - name: Checkout sources uses: actions/checkout@v2 - name: Install stable toolchain uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: ${{ matrix.rust }} override: true - uses: Swatinem/rust-cache@v1 - name: Run cargo test uses: actions-rs/cargo@v1 with: command: test coverage: name: Coverage strategy: matrix: os: [ubuntu-latest] rust: [stable] runs-on: ${{ matrix.os }} steps: - name: Checkout sources uses: actions/checkout@v2 - name: Install stable toolchain uses: actions-rs/toolchain@v1 with: toolchain: ${{ matrix.rust }} override: true components: llvm-tools-preview - uses: Swatinem/rust-cache@v1 - name: Download grcov run: | mkdir -p "${HOME}/.local/bin" curl -sL https://github.com/mozilla/grcov/releases/download/v0.8.10/grcov-x86_64-unknown-linux-gnu.tar.bz2 | tar jxf - -C "${HOME}/.local/bin" echo "$HOME/.local/bin" >> $GITHUB_PATH - name: Run xtask coverage uses: actions-rs/cargo@v1 with: command: xtask args: coverage - name: Upload to codecov.io uses: codecov/codecov-action@v3 with: files: coverage/*.lcov lints: name: Lints runs-on: ubuntu-latest steps: - name: Checkout sources uses: actions/checkout@v2 with: submodules: true - name: Install stable toolchain uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true components: rustfmt, clippy - uses: Swatinem/rust-cache@v1 - name: Run cargo fmt uses: actions-rs/cargo@v1 with: command: fmt args: --all -- --check - name: Run cargo clippy uses: actions-rs/cargo@v1 with: command: clippy args: -- -D warnings ================================================ FILE: .gitignore ================================================ .DS_Store .idea *.log tmp/ target/ coverage/ ================================================ FILE: Cargo.toml ================================================ [workspace] members = [ "operational-transform", "xtask" ] ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 bold Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # operational-transform [![Crates.io][crates-badge]][crates-url] [![docs.rs docs][docs-badge]][docs-url] [![ci][ci-badge]][ci-url] [![codecov](https://codecov.io/gh/spebern/operational-transform-rs/branch/master/graph/badge.svg?token=JM8YS98GDV)](https://codecov.io/gh/spebern/operational-transform-rs) [crates-badge]: https://img.shields.io/crates/v/operational-transform.svg [crates-url]: https://crates.io/crates/operational-transform [docs-badge]: https://img.shields.io/badge/docs-latest-blue.svg [docs-url]: https://docs.rs/operational-transform [ci-badge]: https://github.com/spebern/operational-transform-rs/workflows/Rust/badge.svg [ci-url]: https://github.com/spebern/operational-transform-rs/actions A library for Operational Transformation Operational transformation (OT) is a technology for supporting a range of collaboration functionalities in advanced collaborative software systems. [[1]](https://en.wikipedia.org/wiki/Operational_transformation) When working on the same document over the internet concurrent operations from multiple users might be in conflict. **Operational Transform** can help to resolve conflicts in such a way that documents stay in sync. The basic operations that are supported are: - Retain(n): Move the cursor `n` positions forward - Delete(n): Delete `n` characters at the current position - Insert(s): Insert the string `s` at the current position This library can be used to... ... compose sequences of operations: ```rust use operational_transform::OperationSeq; let mut a = OperationSeq::default(); a.insert("abc"); let mut b = OperationSeq::default(); b.retain(3); b.insert("def"); let after_a = a.apply("").unwrap(); let after_b = b.apply(&after_a).unwrap(); let c = a.compose(&b).unwrap(); let after_ab = a.compose(&b).unwrap().apply("").unwrap(); assert_eq!(after_ab, after_b); ``` ... transform sequences of operations ```rust use operational_transform::OperationSeq; let s = "abc"; let mut a = OperationSeq::default(); a.retain(3); a.insert("def"); let mut b = OperationSeq::default(); b.retain(3); b.insert("ghi"); let (a_prime, b_prime) = a.transform(&b).unwrap(); let ab_prime = a.compose(&b_prime).unwrap(); let ba_prime = b.compose(&a_prime).unwrap(); let after_ab_prime = ab_prime.apply(s).unwrap(); let after_ba_prime = ba_prime.apply(s).unwrap(); assert_eq!(ab_prime, ba_prime); assert_eq!(after_ab_prime, after_ba_prime); ``` ... invert sequences of operations ```rust use operational_transform::OperationSeq; let s = "abc"; let mut o = OperationSeq::default(); o.retain(3); o.insert("def"); let p = o.invert(s); assert_eq!(p.apply(&o.apply(s).unwrap()).unwrap(), s); ``` ### Features Serialisation is supporeted by using the `serde` feature. - Delete(n) will be serialized to -n - Insert(s) will be serialized to "{s}" - Retain(n) will be serailized to n ```rust use operational_transform::OperationSeq; use serde_json; let o: OperationSeq = serde_json::from_str("[1,-1,\"abc\"]").unwrap(); let mut o_exp = OperationSeq::default(); o_exp.retain(1); o_exp.delete(1); o_exp.insert("abc"); assert_eq!(o, o_exp); ``` ### Acknowledgement In the current state the code is ported from [here](https://github.com/Operational-Transformation/ot.js/). It might change in the future as there is much room for optimisation and also usability. ================================================ FILE: operational-transform/.gitignore ================================================ /target Cargo.lock ================================================ FILE: operational-transform/Cargo.toml ================================================ [package] name = "operational-transform" version = "0.6.1" authors = ["bold "] edition = "2018" license = "MIT" description = "A library for Operational Transformation" readme = "README.md" keywords = ["operational", "transform", "collaborative", "editing"] repository = "https://github.com/spebern/operational-transform-rs" [features] runtime-dispatch-simd = ["bytecount/runtime-dispatch-simd"] generic-simd = ["bytecount/generic-simd"] [dependencies] serde = { version = "1", default-features = false, optional = true } bytecount = "0.6.0" [dev-dependencies] rand = "0.7.3" serde_json = "1.0.50" criterion = "0.3" [[bench]] name = "benchmark" harness = false ================================================ FILE: operational-transform/benches/benchmark.rs ================================================ use operational_transform::OperationSeq; #[path = "../src/utilities.rs"] mod utilities; use criterion::{black_box, criterion_group, criterion_main, Criterion}; use utilities::Rng; pub fn compose(c: &mut Criterion) { let mut rng = Rng::from_seed(Default::default()); let input = (0..1000) .map(|_| { let s = rng.gen_string(20); let a = rng.gen_operation_seq(&s); let after_a = a.apply(&s).unwrap(); let b = rng.gen_operation_seq(&after_a); (a, b) }) .collect::>(); c.bench_function("compose", |b| { b.iter(|| { for (a, b) in input.iter() { let _ = a.compose(black_box(b)); } }) }); } pub fn transform(c: &mut Criterion) { let mut rng = Rng::from_seed(Default::default()); let input = (0..1000) .map(|_| { let s = rng.gen_string(20); let a = rng.gen_operation_seq(&s); let b = rng.gen_operation_seq(&s); (a, b) }) .collect::>(); c.bench_function("transform", |b| { b.iter(|| { for (a, b) in input.iter() { let _ = a.transform(black_box(b)); } }) }); } pub fn invert(c: &mut Criterion) { let mut rng = Rng::from_seed(Default::default()); let input = (0..1000) .map(|_| { let s = rng.gen_string(50); let o = rng.gen_operation_seq(&s); (o, s) }) .collect::>(); c.bench_function("invert", |b| { b.iter(|| { for (o, s) in input.iter() { let _ = o.invert(black_box(&s)); } }) }); } pub fn apply(c: &mut Criterion) { let mut rng = Rng::from_seed(Default::default()); let input = (0..1000) .map(|_| { let s = rng.gen_string(50); let o = rng.gen_operation_seq(&s); (o, s) }) .collect::>(); c.bench_function("apply", |b| { b.iter(|| { for (o, s) in input.iter() { let _ = o.apply(black_box(&s)); } }) }); } criterion_group!(benches, compose, transform, invert, apply); criterion_main!(benches); ================================================ FILE: operational-transform/src/lib.rs ================================================ //! A library for Operational Transformation //! //! Operational transformation (OT) is a technology for supporting a range of //! collaboration functionalities in advanced collaborative software systems. //! [[1]](https://en.wikipedia.org/wiki/Operational_transformation) //! //! When working on the same document over the internet concurrent operations //! from multiple users might be in conflict. **Operational Transform** can help //! to resolve conflicts in such a way that documents stay in sync. //! //! The basic operations that are supported are: //! - Retain(n): Move the cursor `n` positions forward //! - Delete(n): Delete `n` characters at the current position //! - Insert(s): Insert the string `s` at the current position //! //! This library can be used to... //! //! ... compose sequences of operations: //! ```rust //! use operational_transform::OperationSeq; //! //! let mut a = OperationSeq::default(); //! a.insert("abc"); //! let mut b = OperationSeq::default(); //! b.retain(3); //! b.insert("def"); //! let after_a = a.apply("").unwrap(); //! let after_b = b.apply(&after_a).unwrap(); //! let c = a.compose(&b).unwrap(); //! let after_ab = a.compose(&b).unwrap().apply("").unwrap(); //! assert_eq!(after_ab, after_b); //! ``` //! //! ... transform sequences of operations //! ```rust //! use operational_transform::OperationSeq; //! //! let s = "abc"; //! let mut a = OperationSeq::default(); //! a.retain(3); //! a.insert("def"); //! let mut b = OperationSeq::default(); //! b.retain(3); //! b.insert("ghi"); //! let (a_prime, b_prime) = a.transform(&b).unwrap(); //! let ab_prime = a.compose(&b_prime).unwrap(); //! let ba_prime = b.compose(&a_prime).unwrap(); //! let after_ab_prime = ab_prime.apply(s).unwrap(); //! let after_ba_prime = ba_prime.apply(s).unwrap(); //! assert_eq!(ab_prime, ba_prime); //! assert_eq!(after_ab_prime, after_ba_prime); //! ``` //! //! ... invert sequences of operations //! ```rust //! use operational_transform::OperationSeq; //! //! let s = "abc"; //! let mut o = OperationSeq::default(); //! o.retain(3); //! o.insert("def"); //! let p = o.invert(s); //! assert_eq!(p.apply(&o.apply(s).unwrap()).unwrap(), s); //! ``` //! //! ## Features //! //! Serialization is supported by using the `serde` feature. //! //! - Delete(n) will be serialized to -n //! - Insert(s) will be serialized to "{s}" //! - Retain(n) will be serialized to n //! //! ```rust,ignore //! use operational_transform::OperationSeq; //! use serde_json; //! //! let o: OperationSeq = serde_json::from_str("[1,-1,\"abc\"]").unwrap(); //! let mut o_exp = OperationSeq::default(); //! o_exp.retain(1); //! o_exp.delete(1); //! o_exp.insert("abc"); //! assert_eq!(o, o_exp); //! ``` //! //! ## Acknowledgment //! In the current state the code is ported from //! [here](https://github.com/Operational-Transformation/ot.js/). It might //! change in the future as there is much room for optimization and also //! usability. #[cfg(feature = "serde")] pub mod serde; #[cfg(any(test, bench))] pub mod utilities; use bytecount::num_chars; use std::{cmp::Ordering, error::Error, fmt, iter::FromIterator}; /// A single operation to be executed at the cursor's current position. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Operation { // Deletes n characters at the current cursor position. Delete(u64), // Moves the cursor n positions forward. Retain(u64), // Inserts string at the current cursor position. Insert(String), } /// A sequence of `Operation`s on text. #[derive(Clone, Debug, PartialEq, Eq, Default)] pub struct OperationSeq { // The consecutive operations to be applied to the target. ops: Vec, // The required length of a string these operations can be applied to. base_len: usize, // The length of the resulting string after the operations have been // applied. target_len: usize, } impl FromIterator for OperationSeq { fn from_iter>(ops: T) -> Self { let mut operations = OperationSeq::default(); for op in ops { operations.add(op); } operations } } /// Error for failed operational transform operations. #[derive(Clone, Debug)] pub struct OTError; impl fmt::Display for OTError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "incompatible lengths") } } impl Error for OTError { fn source(&self) -> Option<&(dyn Error + 'static)> { None } } impl OperationSeq { /// Creates a store for operations which does not need to allocate until /// `capacity` operations have been stored inside. #[inline] pub fn with_capacity(capacity: usize) -> Self { Self { ops: Vec::with_capacity(capacity), base_len: 0, target_len: 0, } } /// Merges the operation with `other` into one operation while preserving /// the changes of both. Or, in other words, for each input string S and a /// pair of consecutive operations A and B. /// `apply(apply(S, A), B) = apply(S, compose(A, B))` /// must hold. /// /// # Error /// /// Returns an `OTError` if the operations are not composable due to length /// conflicts. pub fn compose(&self, other: &Self) -> Result { if self.target_len != other.base_len { return Err(OTError); } let mut new_op_seq = OperationSeq::default(); let mut ops1 = self.ops.iter().cloned(); let mut ops2 = other.ops.iter().cloned(); let mut maybe_op1 = ops1.next(); let mut maybe_op2 = ops2.next(); loop { match (&maybe_op1, &maybe_op2) { (None, None) => break, (Some(Operation::Delete(i)), _) => { new_op_seq.delete(*i); maybe_op1 = ops1.next(); } (_, Some(Operation::Insert(s))) => { new_op_seq.insert(s); maybe_op2 = ops2.next(); } (None, _) | (_, None) => { return Err(OTError); } (Some(Operation::Retain(i)), Some(Operation::Retain(j))) => match i.cmp(j) { Ordering::Less => { new_op_seq.retain(*i); maybe_op2 = Some(Operation::Retain(*j - *i)); maybe_op1 = ops1.next(); } std::cmp::Ordering::Equal => { new_op_seq.retain(*i); maybe_op1 = ops1.next(); maybe_op2 = ops2.next(); } std::cmp::Ordering::Greater => { new_op_seq.retain(*j); maybe_op1 = Some(Operation::Retain(*i - *j)); maybe_op2 = ops2.next(); } }, (Some(Operation::Insert(s)), Some(Operation::Delete(j))) => { match (num_chars(s.as_bytes()) as u64).cmp(j) { Ordering::Less => { maybe_op2 = Some(Operation::Delete(*j - num_chars(s.as_bytes()) as u64)); maybe_op1 = ops1.next(); } Ordering::Equal => { maybe_op1 = ops1.next(); maybe_op2 = ops2.next(); } Ordering::Greater => { maybe_op1 = Some(Operation::Insert(s.chars().skip(*j as usize).collect())); maybe_op2 = ops2.next(); } } } (Some(Operation::Insert(s)), Some(Operation::Retain(j))) => { match (num_chars(s.as_bytes()) as u64).cmp(j) { Ordering::Less => { new_op_seq.insert(s); maybe_op2 = Some(Operation::Retain(*j - num_chars(s.as_bytes()) as u64)); maybe_op1 = ops1.next(); } Ordering::Equal => { new_op_seq.insert(s); maybe_op1 = ops1.next(); maybe_op2 = ops2.next(); } Ordering::Greater => { let chars = &mut s.chars(); new_op_seq.insert(&chars.take(*j as usize).collect::()); maybe_op1 = Some(Operation::Insert(chars.collect())); maybe_op2 = ops2.next(); } } } (Some(Operation::Retain(i)), Some(Operation::Delete(j))) => match i.cmp(j) { Ordering::Less => { new_op_seq.delete(*i); maybe_op2 = Some(Operation::Delete(*j - *i)); maybe_op1 = ops1.next(); } Ordering::Equal => { new_op_seq.delete(*j); maybe_op2 = ops2.next(); maybe_op1 = ops1.next(); } Ordering::Greater => { new_op_seq.delete(*j); maybe_op1 = Some(Operation::Retain(*i - *j)); maybe_op2 = ops2.next(); } }, }; } Ok(new_op_seq) } fn add(&mut self, op: Operation) { match op { Operation::Delete(i) => self.delete(i), Operation::Insert(s) => self.insert(&s), Operation::Retain(i) => self.retain(i), } } /// Deletes `n` characters at the current cursor position. pub fn delete(&mut self, n: u64) { if n == 0 { return; } self.base_len += n as usize; if let Some(Operation::Delete(n_last)) = self.ops.last_mut() { *n_last += n; } else { self.ops.push(Operation::Delete(n)); } } /// Inserts a `s` at the current cursor position. pub fn insert(&mut self, s: &str) { if s.is_empty() { return; } self.target_len += num_chars(s.as_bytes()); let new_last = match self.ops.as_mut_slice() { [.., Operation::Insert(s_last)] => { *s_last += s; return; } [.., Operation::Insert(s_pre_last), Operation::Delete(_)] => { *s_pre_last += s; return; } [.., op_last @ Operation::Delete(_)] => { let new_last = op_last.clone(); *op_last = Operation::Insert(s.to_owned()); new_last } _ => Operation::Insert(s.to_owned()), }; self.ops.push(new_last); } /// Moves the cursor `n` characters forwards. pub fn retain(&mut self, n: u64) { if n == 0 { return; } self.base_len += n as usize; self.target_len += n as usize; if let Some(Operation::Retain(i_last)) = self.ops.last_mut() { *i_last += n; } else { self.ops.push(Operation::Retain(n)); } } /// Transforms two operations A and B that happened concurrently and produces /// two operations A' and B' (in an array) such that /// `apply(apply(S, A), B') = apply(apply(S, B), A')`. /// This function is the heart of OT. /// /// # Error /// /// Returns an `OTError` if the operations cannot be transformed due to /// length conflicts. pub fn transform(&self, other: &Self) -> Result<(Self, Self), OTError> { if self.base_len != other.base_len { return Err(OTError); } let mut a_prime = OperationSeq::default(); let mut b_prime = OperationSeq::default(); let mut ops1 = self.ops.iter().cloned(); let mut ops2 = other.ops.iter().cloned(); let mut maybe_op1 = ops1.next(); let mut maybe_op2 = ops2.next(); loop { match (&maybe_op1, &maybe_op2) { (None, None) => break, (Some(Operation::Insert(s)), Some(Operation::Insert(t))) => match s.cmp(t) { Ordering::Less => { a_prime.insert(s); b_prime.retain(num_chars(s.as_bytes()) as _); maybe_op1 = ops1.next(); } Ordering::Equal => { a_prime.insert(s); a_prime.retain(num_chars(s.as_bytes()) as _); b_prime.insert(s); b_prime.retain(num_chars(s.as_bytes()) as _); maybe_op1 = ops1.next(); maybe_op2 = ops2.next(); } Ordering::Greater => { a_prime.retain(num_chars(t.as_bytes()) as _); b_prime.insert(t); maybe_op2 = ops2.next(); } }, (Some(Operation::Insert(s)), _) => { a_prime.insert(s); b_prime.retain(num_chars(s.as_bytes()) as _); maybe_op1 = ops1.next(); } (_, Some(Operation::Insert(s))) => { a_prime.retain(num_chars(s.as_bytes()) as _); b_prime.insert(s); maybe_op2 = ops2.next(); } (None, _) | (_, None) => { return Err(OTError); } (Some(Operation::Retain(i)), Some(Operation::Retain(j))) => { match i.cmp(j) { Ordering::Less => { a_prime.retain(*i); b_prime.retain(*i); maybe_op2 = Some(Operation::Retain(*j - *i)); maybe_op1 = ops1.next(); } Ordering::Equal => { a_prime.retain(*i); b_prime.retain(*i); maybe_op1 = ops1.next(); maybe_op2 = ops2.next(); } Ordering::Greater => { a_prime.retain(*j); b_prime.retain(*j); maybe_op1 = Some(Operation::Retain(*i - *j)); maybe_op2 = ops2.next(); } }; } (Some(Operation::Delete(i)), Some(Operation::Delete(j))) => match i.cmp(j) { Ordering::Less => { maybe_op2 = Some(Operation::Delete(*j - *i)); maybe_op1 = ops1.next(); } Ordering::Equal => { maybe_op1 = ops1.next(); maybe_op2 = ops2.next(); } Ordering::Greater => { maybe_op1 = Some(Operation::Delete(*i - *j)); maybe_op2 = ops2.next(); } }, (Some(Operation::Delete(i)), Some(Operation::Retain(j))) => { match i.cmp(j) { Ordering::Less => { a_prime.delete(*i); maybe_op2 = Some(Operation::Retain(*j - *i)); maybe_op1 = ops1.next(); } Ordering::Equal => { a_prime.delete(*i); maybe_op1 = ops1.next(); maybe_op2 = ops2.next(); } Ordering::Greater => { a_prime.delete(*j); maybe_op1 = Some(Operation::Delete(*i - *j)); maybe_op2 = ops2.next(); } }; } (Some(Operation::Retain(i)), Some(Operation::Delete(j))) => { match i.cmp(j) { Ordering::Less => { b_prime.delete(*i); maybe_op2 = Some(Operation::Delete(*j - *i)); maybe_op1 = ops1.next(); } Ordering::Equal => { b_prime.delete(*i); maybe_op1 = ops1.next(); maybe_op2 = ops2.next(); } Ordering::Greater => { b_prime.delete(*j); maybe_op1 = Some(Operation::Retain(*i - *j)); maybe_op2 = ops2.next(); } }; } } } Ok((a_prime, b_prime)) } /// Applies an operation to a string, returning a new string. /// /// # Error /// /// Returns an error if the operation cannot be applied due to length /// conflicts. pub fn apply(&self, s: &str) -> Result { if num_chars(s.as_bytes()) != self.base_len { return Err(OTError); } let mut new_s = String::new(); let chars = &mut s.chars(); for op in &self.ops { match op { Operation::Retain(retain) => { for c in chars.take(*retain as usize) { new_s.push(c); } } Operation::Delete(delete) => { for _ in 0..*delete { chars.next(); } } Operation::Insert(insert) => { new_s += insert; } } } Ok(new_s) } /// Computes the inverse of an operation. The inverse of an operation is the /// operation that reverts the effects of the operation, e.g. when you have /// an operation 'insert("hello "); skip(6);' then the inverse is /// 'delete("hello "); skip(6);'. The inverse should be used for /// implementing undo. pub fn invert(&self, s: &str) -> Self { let mut inverse = OperationSeq::default(); let chars = &mut s.chars(); for op in &self.ops { match op { Operation::Retain(retain) => { inverse.retain(*retain); for _ in 0..*retain { chars.next(); } } Operation::Insert(insert) => { inverse.delete(num_chars(insert.as_bytes()) as u64); } Operation::Delete(delete) => { inverse.insert(&chars.take(*delete as usize).collect::()); } } } inverse } /// Checks if this operation has no effect. #[inline] pub fn is_noop(&self) -> bool { matches!(self.ops.as_slice(), [] | [Operation::Retain(_)]) } /// Returns the length of a string these operations can be applied to #[inline] pub fn base_len(&self) -> usize { self.base_len } /// Returns the length of the resulting string after the operations have /// been applied. #[inline] pub fn target_len(&self) -> usize { self.target_len } /// Returns the wrapped sequence of operations. #[inline] pub fn ops(&self) -> &[Operation] { &self.ops } } #[cfg(test)] mod tests { use super::*; use crate::utilities::Rng; #[test] fn lengths() { let mut o = OperationSeq::default(); assert_eq!(o.base_len, 0); assert_eq!(o.target_len, 0); o.retain(5); assert_eq!(o.base_len, 5); assert_eq!(o.target_len, 5); o.insert("abc"); assert_eq!(o.base_len, 5); assert_eq!(o.target_len, 8); o.retain(2); assert_eq!(o.base_len, 7); assert_eq!(o.target_len, 10); o.delete(2); assert_eq!(o.base_len, 9); assert_eq!(o.target_len, 10); } #[test] fn sequence() { let mut o = OperationSeq::default(); o.retain(5); o.retain(0); o.insert("lorem"); o.insert(""); o.delete(3); o.delete(0); assert_eq!(o.ops.len(), 3); } #[test] fn apply() { for _ in 0..1000 { let mut rng = Rng::default(); let s = rng.gen_string(50); let o = rng.gen_operation_seq(&s); assert_eq!(num_chars(s.as_bytes()), o.base_len); assert_eq!(o.apply(&s).unwrap().chars().count(), o.target_len); } } #[test] fn invert() { for _ in 0..1000 { let mut rng = Rng::default(); let s = rng.gen_string(50); let o = rng.gen_operation_seq(&s); let p = o.invert(&s); assert_eq!(o.base_len, p.target_len); assert_eq!(o.target_len, p.base_len); assert_eq!(p.apply(&o.apply(&s).unwrap()).unwrap(), s); } } #[test] fn empty_ops() { let mut o = OperationSeq::default(); o.retain(0); o.insert(""); o.delete(0); assert_eq!(o.ops.len(), 0); } #[test] fn eq() { let mut o1 = OperationSeq::default(); o1.delete(1); o1.insert("lo"); o1.retain(2); o1.retain(3); let mut o2 = OperationSeq::default(); o2.delete(1); o2.insert("l"); o2.insert("o"); o2.retain(5); assert_eq!(o1, o2); o1.delete(1); o2.retain(1); assert_ne!(o1, o2); } #[test] fn ops_merging() { let mut o = OperationSeq::default(); assert_eq!(o.ops.len(), 0); o.retain(2); assert_eq!(o.ops.len(), 1); assert_eq!(o.ops.last(), Some(&Operation::Retain(2))); o.retain(3); assert_eq!(o.ops.len(), 1); assert_eq!(o.ops.last(), Some(&Operation::Retain(5))); o.insert("abc"); assert_eq!(o.ops.len(), 2); assert_eq!(o.ops.last(), Some(&Operation::Insert("abc".to_owned()))); o.insert("xyz"); assert_eq!(o.ops.len(), 2); assert_eq!(o.ops.last(), Some(&Operation::Insert("abcxyz".to_owned()))); o.delete(1); assert_eq!(o.ops.len(), 3); assert_eq!(o.ops.last(), Some(&Operation::Delete(1))); o.delete(1); assert_eq!(o.ops.len(), 3); assert_eq!(o.ops.last(), Some(&Operation::Delete(2))); } #[test] fn is_noop() { let mut o = OperationSeq::default(); assert!(o.is_noop()); o.retain(5); assert!(o.is_noop()); o.retain(3); assert!(o.is_noop()); o.insert("lorem"); assert!(!o.is_noop()); } #[test] fn compose() { for _ in 0..1000 { let mut rng = Rng::default(); let s = rng.gen_string(20); let a = rng.gen_operation_seq(&s); let after_a = a.apply(&s).unwrap(); assert_eq!(a.target_len, num_chars(after_a.as_bytes())); let b = rng.gen_operation_seq(&after_a); let after_b = b.apply(&after_a).unwrap(); assert_eq!(b.target_len, num_chars(after_b.as_bytes())); let ab = a.compose(&b).unwrap(); assert_eq!(ab.target_len, b.target_len); let after_ab = ab.apply(&s).unwrap(); assert_eq!(after_b, after_ab); } } #[test] fn transform() { for _ in 0..1000 { let mut rng = Rng::default(); let s = rng.gen_string(20); let a = rng.gen_operation_seq(&s); let b = rng.gen_operation_seq(&s); let (a_prime, b_prime) = a.transform(&b).unwrap(); let (b_prime_2, a_prime_2) = b.transform(&a).unwrap(); assert_eq!(a_prime, a_prime_2); assert_eq!(b_prime, b_prime_2); let ab_prime = a.compose(&b_prime).unwrap(); let ba_prime = b.compose(&a_prime).unwrap(); let after_ab_prime = ab_prime.apply(&s).unwrap(); let after_ba_prime = ba_prime.apply(&s).unwrap(); assert_eq!(ab_prime, ba_prime); assert_eq!(after_ab_prime, after_ba_prime); } } #[test] #[cfg(feature = "serde")] fn serde() { use serde_json; let mut rng = Rng::default(); let o: OperationSeq = serde_json::from_str("[1,-1,\"abc\"]").unwrap(); let mut o_exp = OperationSeq::default(); o_exp.retain(1); o_exp.delete(1); o_exp.insert("abc"); assert_eq!(o, o_exp); for _ in 0..1000 { let s = rng.gen_string(20); let o = rng.gen_operation_seq(&s); assert_eq!( o, serde_json::from_str(&serde_json::to_string(&o).unwrap()).unwrap() ); } } } ================================================ FILE: operational-transform/src/serde.rs ================================================ use crate::{Operation, OperationSeq}; use serde::{ de::{self, Deserializer, SeqAccess, Visitor}, ser::{SerializeSeq, Serializer}, Deserialize, Serialize, }; use std::fmt; impl Serialize for Operation { fn serialize(&self, serializer: S) -> Result where S: Serializer, { match self { Operation::Retain(i) => serializer.serialize_u64(*i), Operation::Delete(i) => serializer.serialize_i64(-(*i as i64)), Operation::Insert(s) => serializer.serialize_str(s), } } } impl<'de> Deserialize<'de> for Operation { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { struct OperationVisitor; impl<'de> Visitor<'de> for OperationVisitor { type Value = Operation; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("an integer between -2^64 and 2^63 or a string") } fn visit_u64(self, value: u64) -> Result where E: de::Error, { Ok(Operation::Retain(value as u64)) } fn visit_i64(self, value: i64) -> Result where E: de::Error, { Ok(Operation::Delete((-value) as u64)) } fn visit_str(self, value: &str) -> Result where E: de::Error, { Ok(Operation::Insert(value.to_owned())) } } deserializer.deserialize_any(OperationVisitor) } } impl Serialize for OperationSeq { fn serialize(&self, serializer: S) -> Result where S: Serializer, { let mut seq = serializer.serialize_seq(Some(self.ops.len()))?; for op in self.ops.iter() { seq.serialize_element(op)?; } seq.end() } } impl<'de> Deserialize<'de> for OperationSeq { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { struct OperationSeqVisitor; impl<'de> Visitor<'de> for OperationSeqVisitor { type Value = OperationSeq; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a sequence") } fn visit_seq(self, mut seq: A) -> Result where A: SeqAccess<'de>, { let mut o = OperationSeq::default(); while let Some(op) = seq.next_element()? { o.add(op); } Ok(o) } } deserializer.deserialize_seq(OperationSeqVisitor) } } ================================================ FILE: operational-transform/src/utilities.rs ================================================ use crate::OperationSeq; use rand::prelude::*; use rand::Rng as WrappedRng; pub struct Rng(StdRng); impl Default for Rng { fn default() -> Self { Rng(StdRng::from_rng(thread_rng()).unwrap()) } } impl Rng { pub fn from_seed(seed: [u8; 32]) -> Self { Rng(StdRng::from_seed(seed)) } pub fn gen_string(&mut self, len: usize) -> String { (0..len).map(|_| self.0.gen::()).collect() } pub fn gen_operation_seq(&mut self, s: &str) -> OperationSeq { let mut op = OperationSeq::default(); loop { let left = s.chars().count() - op.base_len(); if left == 0 { break; } let i = if left == 1 { 1 } else { 1 + self.0.gen_range(0, std::cmp::min(left - 1, 20)) }; match self.0.gen_range(0.0, 1.0) { f if f < 0.2 => { op.insert(&self.gen_string(i)); } f if f < 0.4 => { op.delete(i as u64); } _ => { op.retain(i as u64); } } } if self.0.gen_range(0.0, 1.0) < 0.3 { op.insert(&("1".to_owned() + &self.gen_string(10))); } op } } ================================================ FILE: xtask/Cargo.toml ================================================ [package] name = "xtask" version = "0.1.0" edition = "2021" [dependencies] xtaskops = "^0.2.0" anyhow = "1" clap = "3" ================================================ FILE: xtask/src/main.rs ================================================ #![allow(dead_code)] use clap::{AppSettings, Arg, Command}; use xtaskops::ops; use xtaskops::tasks; fn main() -> Result<(), anyhow::Error> { let cli = Command::new("xtask") .setting(AppSettings::SubcommandRequiredElseHelp) .subcommand( Command::new("coverage").arg( Arg::new("dev") .short('d') .long("dev") .help("generate an html report") .takes_value(false), ), ) .subcommand(Command::new("vars")) .subcommand(Command::new("ci")) .subcommand(Command::new("powerset")) .subcommand(Command::new("bloat-deps")) .subcommand(Command::new("bloat-time")) .subcommand(Command::new("docs")); let matches = cli.get_matches(); let root = ops::root_dir(); let res = match matches.subcommand() { Some(("coverage", sm)) => tasks::coverage(sm.is_present("dev")), Some(("vars", _)) => { println!("root: {:?}", root); Ok(()) } Some(("ci", _)) => tasks::ci(), Some(("docs", _)) => tasks::docs(), Some(("powerset", _)) => tasks::powerset(), Some(("bloat-deps", _)) => tasks::bloat_deps(), Some(("bloat-time", _)) => tasks::bloat_time(), _ => unreachable!("unreachable branch"), }; res }