automerge/rust/automerge/src/types/opids.rs
Alex Good dd3c6d1303
Move rust workspace into ./rust
After some discussion with PVH I realise that the repo structure in the
last reorg was very rust-centric. In an attempt to put each language on
a level footing move the rust code and project files into ./rust
2022-10-16 19:55:51 +01:00

197 lines
6.1 KiB
Rust

use itertools::Itertools;
use super::OpId;
/// A wrapper around `Vec<Opid>` which preserves the invariant that the ops are
/// in ascending order with respect to their counters and actor IDs. In order to
/// maintain this invariant you must provide a comparator function when adding
/// ops as the actor indices in an OpId are not sufficient to order the OpIds
#[derive(Debug, Clone, PartialEq, Default)]
pub(crate) struct OpIds(Vec<OpId>);
impl<'a> IntoIterator for &'a OpIds {
type Item = &'a OpId;
type IntoIter = std::slice::Iter<'a, OpId>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl OpIds {
pub(crate) fn empty() -> Self {
Self(Vec::new())
}
pub(crate) fn new<I: Iterator<Item = OpId>, F: Fn(&OpId, &OpId) -> std::cmp::Ordering>(
opids: I,
cmp: F,
) -> Self {
let mut inner = opids.collect::<Vec<_>>();
inner.sort_by(cmp);
Self(inner)
}
/// Create a new OpIds if `opids` are sorted with respect to `cmp` and contain no duplicates.
///
/// Returns `Some(OpIds)` if `opids` is sorted and has no duplicates, otherwise returns `None`
pub(crate) fn new_if_sorted<F: Fn(&OpId, &OpId) -> std::cmp::Ordering>(
opids: Vec<OpId>,
cmp: F,
) -> Option<Self> {
if are_sorted_and_unique(opids.iter(), cmp) {
Some(Self(opids))
} else {
None
}
}
/// Add an op to this set of OpIds. The `comparator` must provide a
/// consistent ordering between successive calls to `add`.
pub(crate) fn add<F: Fn(&OpId, &OpId) -> std::cmp::Ordering>(
&mut self,
opid: OpId,
comparator: F,
) {
use std::cmp::Ordering::*;
if self.is_empty() {
self.0.push(opid);
return;
}
let idx_and_elem = self
.0
.iter()
.find_position(|an_opid| matches!(comparator(an_opid, &opid), Greater | Equal));
if let Some((idx, an_opid)) = idx_and_elem {
if comparator(an_opid, &opid) == Equal {
// nothing to do
} else {
self.0.insert(idx, opid);
}
} else {
self.0.push(opid);
}
}
pub(crate) fn retain<F: Fn(&OpId) -> bool>(&mut self, f: F) {
self.0.retain(f)
}
pub(crate) fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub(crate) fn len(&self) -> usize {
self.0.len()
}
pub(crate) fn iter(&self) -> std::slice::Iter<'_, OpId> {
self.0.iter()
}
pub(crate) fn contains(&self, op: &OpId) -> bool {
self.0.contains(op)
}
pub(crate) fn get(&self, idx: usize) -> Option<&OpId> {
self.0.get(idx)
}
}
fn are_sorted_and_unique<
'a,
I: Iterator<Item = &'a OpId>,
F: FnMut(&OpId, &OpId) -> std::cmp::Ordering,
>(
mut opids: I,
mut f: F,
) -> bool {
use std::cmp::Ordering;
let mut last = match opids.next() {
Some(e) => e,
None => return true,
};
for next in opids {
if matches!(f(last, next), Ordering::Greater | Ordering::Equal) {
return false;
}
last = next;
}
true
}
#[cfg(test)]
mod tests {
use super::{OpId, OpIds};
use crate::ActorId;
use proptest::prelude::*;
fn gen_opid(actors: Vec<ActorId>) -> impl Strategy<Value = OpId> {
(0..actors.len()).prop_flat_map(|actor_idx| {
(Just(actor_idx), 0..u64::MAX).prop_map(|(actor_idx, counter)| OpId(counter, actor_idx))
})
}
fn scenario(size: std::ops::Range<usize>) -> impl Strategy<Value = (Vec<ActorId>, Vec<OpId>)> {
let actors = vec![
"aaaa".try_into().unwrap(),
"cccc".try_into().unwrap(),
"bbbb".try_into().unwrap(),
];
proptest::collection::vec(gen_opid(actors.clone()), size)
.prop_map(move |opids| (actors.clone(), opids))
}
fn duplicate_unsorted_scenario() -> impl Strategy<Value = (Vec<ActorId>, Vec<OpId>)> {
scenario(1..100).prop_map(|(actors, mut opids)| {
let mut sorted_opids = opids.clone();
sorted_opids.sort_by(|left, right| cmp(&actors, left, right));
sorted_opids.dedup();
// Unwrap is okay due to the size we pass to `scenario()`
let last = *sorted_opids.last().unwrap();
if sorted_opids == opids {
// Opids are sorted and deduplicated, just copy the last opid and insert it at the
// front
opids.insert(0, last);
}
(actors, opids)
})
}
proptest! {
#[test]
fn test_sorted_opids((actors, opids) in scenario(0..100)) {
let mut sorted_opids = OpIds::default();
for opid in &opids {
sorted_opids.add(*opid, |left, right| cmp(&actors, left, right));
}
let result = sorted_opids.into_iter().cloned().collect::<Vec<_>>();
let mut expected = opids;
expected.sort_by(|left, right| cmp(&actors, left, right));
expected.dedup();
assert_eq!(result, expected);
}
#[test]
fn test_new_if_sorted((actors, opids) in duplicate_unsorted_scenario()) {
let mut expected = opids.clone();
assert_eq!(OpIds::new_if_sorted(opids, |left, right| cmp(&actors, left, right)), None);
expected.sort_by(|left, right| cmp(&actors, left, right));
expected.dedup();
let result = OpIds::new_if_sorted(expected.clone(), |left, right| cmp(&actors, left, right)).unwrap().into_iter().cloned().collect::<Vec<_>>();
assert_eq!(result, expected)
}
}
fn cmp(actors: &[ActorId], left: &OpId, right: &OpId) -> std::cmp::Ordering {
use std::cmp::Ordering;
match (left, right) {
(OpId(0, _), OpId(0, _)) => Ordering::Equal,
(OpId(0, _), OpId(_, _)) => Ordering::Less,
(OpId(_, _), OpId(0, _)) => Ordering::Greater,
(OpId(a, x), OpId(b, y)) if a == b => actors[*x].cmp(&actors[*y]),
(OpId(a, _), OpId(b, _)) => a.cmp(b),
}
}
}