~dkellner/chronofold

025024f8a5fdcb6cbc52cdb1f7b6004e05c8557e — Dominik Kellner 1 year, 6 months ago 8242096
Revamp `Op`: remove `Change`, add `OpPayload`

This allows for having ops contain different types as the local chronofold, as
long as they are convertible via the new `FromLocalValue` and `IntoLocalValue`
traits.
M src/distributed.rs => src/distributed.rs +77 -12
@@ 2,7 2,7 @@

use std::fmt;

use crate::{Change, LogIndex};
use crate::{Chronofold, LogIndex};

/// A trait alias to reduce redundancy in type declarations.
pub trait Author:


@@ 44,27 44,92 @@ impl<A: fmt::Display> fmt::Display for Timestamp<A> {
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Op<A, T> {
    pub id: Timestamp<A>,
    pub reference: Option<Timestamp<A>>, // None = root
    pub change: Change<T>,
    pub payload: OpPayload<A, T>,
}

impl<A, T> Op<A, T> {
    pub fn new(id: Timestamp<A>, reference: Option<Timestamp<A>>, change: Change<T>) -> Self {
        Self {
            id,
            reference,
            change,
        }
    pub fn new(id: Timestamp<A>, payload: OpPayload<A, T>) -> Self {
        Self { id, payload }
    }

    pub fn root(id: Timestamp<A>) -> Self {
        Op::new(id, OpPayload::Root)
    }

    pub fn insert(id: Timestamp<A>, reference: Option<Timestamp<A>>, value: T) -> Self {
        Op::new(id, OpPayload::Insert(reference, value))
    }

    pub fn delete(id: Timestamp<A>, reference: Timestamp<A>) -> Self {
        Op::new(id, OpPayload::Delete(reference))
    }
}

impl<A, T: Clone> Op<A, &T> {
    /// Maps an Op<A, &T> to an Op<A, T> by cloning the contents of the change.
    /// Maps an Op<A, &T> to an Op<A, T> by cloning the payload.
    pub fn cloned(self) -> Op<A, T> {
        Op {
            id: self.id,
            reference: self.reference,
            change: self.change.cloned(),
            payload: self.payload.cloned(),
        }
    }
}

/// The payload of an operation.
///
/// Ops don't contain `Change<T>` directly, as these can contain information
/// that is only meaningful within the context of the local chronofold. E.g. a
/// change may refer to another change by log index, which has to be replaced
/// by a timestamp in the distributed operation.
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum OpPayload<A, T> {
    Root,
    Insert(Option<Timestamp<A>>, T),
    Delete(Timestamp<A>),
}

impl<A, T> OpPayload<A, T> {
    pub fn reference(&self) -> Option<&Timestamp<A>> {
        use OpPayload::*;
        match self {
            Root => None,
            Insert(reference, _) => reference.as_ref(),
            Delete(reference) => Some(&reference),
        }
    }
}

impl<A, T: Clone> OpPayload<A, &T> {
    pub fn cloned(self) -> OpPayload<A, T> {
        use OpPayload::*;
        match self {
            Root => Root,
            Insert(reference, t) => Insert(reference, t.clone()),
            Delete(reference) => Delete(reference),
        }
    }
}

pub trait IntoLocalValue<A, LocalValue> {
    fn into_local_value(self, chronofold: &Chronofold<A, LocalValue>) -> LocalValue;
}

pub trait FromLocalValue<'a, A, LocalValue> {
    fn from_local_value(source: &'a LocalValue, chronofold: &Chronofold<A, LocalValue>) -> Self;
}

impl<A, T, V> IntoLocalValue<A, T> for V
where
    V: Into<T>,
{
    fn into_local_value(self, _chronofold: &Chronofold<A, T>) -> T {
        self.into()
    }
}

impl<'a, A, T> FromLocalValue<'a, A, T> for &'a T {
    fn from_local_value(source: &'a T, _chronofold: &Chronofold<A, T>) -> Self {
        source
    }
}

M src/error.rs => src/error.rs +26 -22
@@ 1,6 1,6 @@
use std::fmt;

use crate::{Change, Op, Timestamp};
use crate::{Op, OpPayload};

/// Represents errors that can occur when applying an op.
///


@@ 12,50 12,54 @@ pub enum ChronofoldError<A, T> {
    ExistingTimestamp(Op<A, T>),
}

impl<A: fmt::Debug + fmt::Display + Copy, T> fmt::Debug for ChronofoldError<A, T> {
impl<A, T> fmt::Debug for ChronofoldError<A, T>
where
    A: fmt::Debug + fmt::Display + Copy,
{
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        use ChronofoldError::*;
        let (name, op) = match self {
            UnknownReference(op) => ("UnknownReference", op),
            ExistingTimestamp(op) => ("ExistingTimestamp", op),
        };
        f.debug_tuple(name).field(&DebugOp::from(op)).finish()
        f.debug_tuple(name).field(&op.omit_value()).finish()
    }
}

impl<A: fmt::Debug + fmt::Display + Copy, T> fmt::Display for ChronofoldError<A, T> {
impl<A, T> fmt::Display for ChronofoldError<A, T>
where
    A: fmt::Debug + fmt::Display + Copy,
{
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        use ChronofoldError::*;
        match self {
            UnknownReference(op) => write!(
                f,
                "unknown reference {}",
                op.reference.as_ref().expect("reference must not be `None`")
                op.payload
                    .reference()
                    .as_ref()
                    .expect("reference must not be `None`")
            ),
            ExistingTimestamp(op) => write!(f, "existing timestamp {}", op.id),
        }
    }
}

impl<A: fmt::Debug + fmt::Display + Copy, T> std::error::Error for ChronofoldError<A, T> {}
impl<A, T> std::error::Error for ChronofoldError<A, T> where A: fmt::Debug + fmt::Display + Copy {}

#[derive(Debug)]
struct DebugOp<A> {
    id: Timestamp<A>,
    reference: Option<Timestamp<A>>,
    change: Change<Omitted>,
}

impl<A: fmt::Debug + fmt::Display + Copy, T> From<&Op<A, T>> for DebugOp<A> {
    fn from(source: &Op<A, T>) -> Self {
        use Change::*;
        Self {
            id: source.id,
            reference: source.reference,
            change: match source.change {
impl<A, T> Op<A, T>
where
    A: Copy,
{
    fn omit_value(&self) -> Op<A, Omitted> {
        use OpPayload::*;
        Op {
            id: self.id,
            payload: match self.payload {
                Root => Root,
                Insert(_) => Insert(Omitted),
                Delete => Delete,
                Insert(t, _) => Insert(t, Omitted),
                Delete(t) => Delete(t),
            },
        }
    }

M src/iter.rs => src/iter.rs +26 -14
@@ 1,8 1,9 @@
use std::collections::HashSet;
use std::marker::PhantomData;
use std::matches;
use std::ops::{Bound, Range, RangeBounds};

use crate::{Author, Change, Chronofold, LogIndex, Op};
use crate::{Author, Change, Chronofold, FromLocalValue, LogIndex, Op, OpPayload};

impl<A: Author, T> Chronofold<A, T> {
    /// Returns an iterator over the log indices in causal order.


@@ 78,9 79,10 @@ impl<A: Author, T> Chronofold<A, T> {
    }

    /// Returns an iterator over ops in log order.
    pub fn iter_ops<'a, R>(&'a self, range: R) -> Ops<'a, A, T>
    pub fn iter_ops<'a, R, V>(&'a self, range: R) -> Ops<'a, A, T, V>
    where
        R: RangeBounds<LogIndex> + 'a,
        V: FromLocalValue<'a, A, T>,
    {
        let oob = LogIndex(self.log.len());
        let start = match range.start_bound() {


@@ 98,6 100,7 @@ impl<A: Author, T> Chronofold<A, T> {
        Ops {
            cfold: self,
            idx_iter: start..end,
            _op_value: PhantomData,
        }
    }
}


@@ 162,13 165,18 @@ impl<'a, A: Author, T> Iterator for Iter<'a, A, T> {
///
/// This struct is created by the `iter_ops` method on `Chronofold`. See its
/// documentation for more.
pub struct Ops<'a, A, T> {
pub struct Ops<'a, A, T, V> {
    cfold: &'a Chronofold<A, T>,
    idx_iter: Range<usize>,
    _op_value: PhantomData<V>,
}

impl<'a, A: Author, T> Iterator for Ops<'a, A, T> {
    type Item = Op<A, &'a T>;
impl<'a, A, T, V> Iterator for Ops<'a, A, T, V>
where
    A: Author,
    V: FromLocalValue<'a, A, T>,
{
    type Item = Op<A, V>;

    fn next(&mut self) -> Option<Self::Item> {
        let idx = LogIndex(self.idx_iter.next()?);


@@ 181,8 189,12 @@ impl<'a, A: Author, T> Iterator for Ops<'a, A, T> {
                .timestamp(&r)
                .expect("references of already applied ops have to exist")
        });
        let change = &self.cfold.log[idx.0];
        Some(Op::new(id, reference, change.as_ref()))
        let payload = match &self.cfold.log[idx.0] {
            Change::Root => OpPayload::Root,
            Change::Insert(v) => OpPayload::Insert(reference, V::from_local_value(v, self.cfold)),
            Change::Delete => OpPayload::Delete(reference.expect("deletes must have a reference")),
        };
        Some(Op::new(id, payload))
    }
}



@@ 229,21 241,21 @@ mod tests {
    fn iter_ops() {
        let mut cfold = Chronofold::<u8, char>::default();
        cfold.session(1).extend("Hi!".chars());
        let op0 = Op::new(Timestamp(LogIndex(0), 0), None, Change::Root);
        let op1 = Op::new(
        let op0 = Op::root(Timestamp(LogIndex(0), 0));
        let op1 = Op::insert(
            Timestamp(LogIndex(1), 1),
            Some(Timestamp(LogIndex(0), 0)),
            Change::Insert(&'H'),
            &'H',
        );
        let op2 = Op::new(
        let op2 = Op::insert(
            Timestamp(LogIndex(2), 1),
            Some(Timestamp(LogIndex(1), 1)),
            Change::Insert(&'i'),
            &'i',
        );
        let op3 = Op::new(
        let op3 = Op::insert(
            Timestamp(LogIndex(3), 1),
            Some(Timestamp(LogIndex(2), 1)),
            Change::Insert(&'!'),
            &'!',
        );
        assert_eq!(
            vec![op0.clone(), op1.clone(), op2.clone()],

M src/lib.rs => src/lib.rs +30 -9
@@ 197,7 197,10 @@ impl<A: Author, T> Chronofold<A, T> {
    }

    /// Applies an op to the chronofold.
    pub fn apply(&mut self, op: Op<A, T>) -> Result<(), ChronofoldError<A, T>> {
    pub fn apply<V>(&mut self, op: Op<A, V>) -> Result<(), ChronofoldError<A, V>>
    where
        V: IntoLocalValue<A, T>,
    {
        // Check if an op with the same id was applied already.
        // TODO: Consider adding an `apply_unchecked` variant to skip this
        // check.


@@ 205,20 208,38 @@ impl<A: Author, T> Chronofold<A, T> {
            return Err(ChronofoldError::ExistingTimestamp(op));
        }

        // Convert the reference timestamp, as all our internal functions work
        // with log indices.
        match op.reference {
            Some(t) => match self.log_index(&t) {
        use OpPayload::*;
        match op.payload {
            Root => {
                self.apply_change(op.id, None, Change::Root);
                Ok(())
            }
            Insert(Some(t), value) => match self.log_index(&t) {
                Some(reference) => {
                    self.apply_change(op.id, Some(reference), op.change);
                    self.apply_change(
                        op.id,
                        Some(reference),
                        Change::Insert(value.into_local_value(self)),
                    );
                    Ok(())
                }
                None => Err(ChronofoldError::UnknownReference(op)),
                None => Err(ChronofoldError::UnknownReference(Op::insert(
                    op.id,
                    Some(t),
                    value,
                ))),
            },
            None => {
                self.apply_change(op.id, None, op.change);
            Insert(None, value) => {
                self.apply_change(op.id, None, Change::Insert(value.into_local_value(self)));
                Ok(())
            }
            Delete(t) => match self.log_index(&t) {
                Some(reference) => {
                    self.apply_change(op.id, Some(reference), Change::Delete);
                    Ok(())
                }
                None => Err(ChronofoldError::UnknownReference(op)),
            },
        }
    }
}

M src/session.rs => src/session.rs +5 -2
@@ 1,6 1,6 @@
use std::ops::{Bound, RangeBounds};

use crate::{Author, Change, Chronofold, LogIndex, Op, Timestamp};
use crate::{Author, Change, Chronofold, FromLocalValue, LogIndex, Op, Timestamp};

/// An editing session tied to one author.
///


@@ 126,7 126,10 @@ impl<'a, A: Author, T> Session<'a, A, T> {

    /// Returns an iterator over ops in log order, that where created in this
    /// session.
    pub fn iter_ops(&'a self) -> impl Iterator<Item = Op<A, &'a T>> + 'a {
    pub fn iter_ops<V>(&'a self) -> impl Iterator<Item = Op<A, V>> + 'a
    where
        V: FromLocalValue<'a, A, T> + 'a,
    {
        self.chronofold
            .iter_ops(self.first_index..)
            .filter(move |op| op.id.1 == self.author)

M src/version.rs => src/version.rs +6 -3
@@ 1,7 1,7 @@
use std::cmp::Ordering;
use std::collections::BTreeMap;

use crate::{Author, Chronofold, LogIndex, Op, Timestamp};
use crate::{Author, Chronofold, FromLocalValue, LogIndex, Op, Timestamp};

/// A vector clock representing the chronofold's version.
#[derive(PartialEq, Eq, Clone, Debug)]


@@ 71,10 71,13 @@ impl<A: Author, T> Chronofold<A, T> {
    }

    /// Returns an iterator over ops newer than the given version in log order.
    pub fn iter_newer_ops<'a>(
    pub fn iter_newer_ops<'a, V>(
        &'a self,
        version: &'a Version<A>,
    ) -> impl Iterator<Item = Op<A, &'a T>> + 'a {
    ) -> impl Iterator<Item = Op<A, V>> + 'a
    where
        V: FromLocalValue<'a, A, T> + 'a,
    {
        // TODO: Don't iterate over all ops in cases where that is not
        // necessary.
        self.iter_ops(..)

M tests/corner_cases.rs => tests/corner_cases.rs +2 -2
@@ 129,12 129,12 @@ where
        expected,
        format!("{}", cfold_left),
        "Left ops:\n{:#?}",
        cfold_left.iter_ops(..).collect::<Vec<_>>(),
        cfold_left.iter_ops(..).collect::<Vec<Op<_, &char>>>(),
    );
    assert_eq!(
        expected,
        format!("{}", cfold_right),
        "Right ops:\n{:#?}",
        cfold_right.iter_ops(..).collect::<Vec<_>>()
        cfold_right.iter_ops(..).collect::<Vec<Op<_, &char>>>()
    );
}

M tests/errors.rs => tests/errors.rs +4 -8
@@ 1,14 1,10 @@
use chronofold::{Change, Chronofold, ChronofoldError, LogIndex, Op, Timestamp};
use chronofold::{Chronofold, ChronofoldError, LogIndex, Op, Timestamp};

#[test]
fn unknown_timestamp() {
    let mut cfold = Chronofold::<u8, char>::default();
    let unknown = Timestamp(LogIndex(1), 42);
    let op = Op::new(
        Timestamp(LogIndex(1), 1),
        Some(unknown),
        Change::Insert('!'),
    );
    let op = Op::insert(Timestamp(LogIndex(1), 1), Some(unknown), '!');
    let err = cfold.apply(op.clone()).unwrap_err();
    assert_eq!(ChronofoldError::UnknownReference(op), err);
    assert_eq!("unknown reference <1, 42>", format!("{}", err));


@@ 19,10 15,10 @@ fn existing_timestamp() {
    // Applying the same op twice results in a
    // `ChronofoldError::ExistingTimestamp`:
    let mut cfold = Chronofold::<u8, char>::default();
    let op = Op::new(
    let op = Op::insert(
        Timestamp(LogIndex(1), 1),
        Some(Timestamp(LogIndex(0), 0)),
        Change::Insert('.'),
        '.',
    );
    assert_eq!(Ok(()), cfold.apply(op.clone()));
    let err = cfold.apply(op.clone()).unwrap_err();

M tests/version.rs => tests/version.rs +9 -9
@@ 1,4 1,4 @@
use chronofold::{Change, Chronofold, LogIndex, Op, Timestamp, Version};
use chronofold::{Chronofold, LogIndex, Op, Timestamp, Version};

#[test]
fn partial_order() {


@@ 25,8 25,8 @@ fn iter_newer_ops() {

    assert_eq!(
        vec![
            Op::new(t(4, 1), Some(t(3, 1)), Change::Insert(&'!')),
            Op::new(t(5, 2), Some(t(4, 1)), Change::Insert(&'?'))
            Op::insert(t(4, 1), Some(t(3, 1)), &'!'),
            Op::insert(t(5, 2), Some(t(4, 1)), &'?')
        ],
        cfold.iter_newer_ops(&v1).collect::<Vec<_>>()
    );


@@ 35,12 35,12 @@ fn iter_newer_ops() {
    v2.inc(&Timestamp(LogIndex(1), 3));
    assert_eq!(
        vec![
            Op::new(t(0, 0), None, Change::Root),
            Op::new(t(1, 1), Some(t(0, 0)), Change::Insert(&'f')),
            Op::new(t(2, 1), Some(t(1, 1)), Change::Insert(&'o')),
            Op::new(t(3, 1), Some(t(2, 1)), Change::Insert(&'o')),
            Op::new(t(4, 1), Some(t(3, 1)), Change::Insert(&'!')),
            Op::new(t(5, 2), Some(t(4, 1)), Change::Insert(&'?'))
            Op::root(t(0, 0)),
            Op::insert(t(1, 1), Some(t(0, 0)), &'f'),
            Op::insert(t(2, 1), Some(t(1, 1)), &'o'),
            Op::insert(t(3, 1), Some(t(2, 1)), &'o'),
            Op::insert(t(4, 1), Some(t(3, 1)), &'!'),
            Op::insert(t(5, 2), Some(t(4, 1)), &'?')
        ],
        cfold.iter_newer_ops(&v2).collect::<Vec<_>>()
    );