cob: Consolidate `from_history` impls

By implementing `FromHistory::apply`, COBs don't need to implement
`from_history`.

Signed-off-by: Alexis Sellier <self@cloudhead.io>
This commit is contained in:
Alexis Sellier 2023-01-01 22:48:25 +01:00
parent 2f42bc942c
commit 139f095d51
No known key found for this signature in database
4 changed files with 100 additions and 128 deletions

View File

@ -1,4 +1,4 @@
use std::ops::{ControlFlow, Deref};
use std::ops::Deref;
use std::str::FromStr;
use once_cell::sync::Lazy;
@ -10,6 +10,7 @@ use radicle_crdt::{LWWReg, LWWSet, Max, Semilattice};
use crate::cob;
use crate::cob::common::{Author, Reaction, Tag};
use crate::cob::store::FromHistory as _;
use crate::cob::store::Transaction;
use crate::cob::thread;
use crate::cob::thread::{CommentId, Thread};
@ -17,8 +18,6 @@ use crate::cob::{store, ActorId, ObjectId, OpId, TypeName};
use crate::crypto::{PublicKey, Signer};
use crate::storage::git as storage;
use super::op::Ops;
/// Issue operation.
pub type Op = cob::Op<Action>;
@ -111,27 +110,44 @@ impl Default for Issue {
impl store::FromHistory for Issue {
type Action = Action;
type Error = Error;
fn type_name() -> &'static TypeName {
&*TYPENAME
}
fn from_history(
history: &radicle_cob::History,
) -> Result<(Self, clock::Lamport), store::Error> {
let obj = history.traverse(Self::default(), |mut acc, entry| {
if let Ok(Ops(ops)) = Ops::try_from(entry) {
if let Err(err) = acc.apply(ops) {
log::warn!("Error applying op to issue state: {err}");
return ControlFlow::Break(acc);
fn apply(&mut self, ops: impl IntoIterator<Item = Op>) -> Result<(), Error> {
for op in ops {
match op.action {
Action::Assign { add, remove } => {
for assignee in add {
self.assignees.insert(assignee, op.clock);
}
for assignee in remove {
self.assignees.remove(assignee, op.clock);
}
}
Action::Edit { title } => {
self.title.set(title, op.clock);
}
Action::Lifecycle { state } => {
self.state.set(state, op.clock);
}
Action::Tag { add, remove } => {
for tag in add {
self.tags.insert(tag, op.clock);
}
for tag in remove {
self.tags.remove(tag, op.clock);
}
}
Action::Thread { action } => {
self.thread
.apply([cob::Op::new(action, op.author, op.timestamp, op.clock)])?;
}
} else {
return ControlFlow::Break(acc);
}
ControlFlow::Continue(acc)
});
Ok((obj, history.clock().into()))
}
Ok(())
}
}
@ -166,40 +182,6 @@ impl Issue {
pub fn comments(&self) -> impl Iterator<Item = (&CommentId, &thread::Comment)> {
self.thread.comments()
}
pub fn apply(&mut self, ops: impl IntoIterator<Item = Op>) -> Result<(), Error> {
for op in ops {
match op.action {
Action::Assign { add, remove } => {
for assignee in add {
self.assignees.insert(assignee, op.clock);
}
for assignee in remove {
self.assignees.remove(assignee, op.clock);
}
}
Action::Edit { title } => {
self.title.set(title, op.clock);
}
Action::Lifecycle { state } => {
self.state.set(state, op.clock);
}
Action::Tag { add, remove } => {
for tag in add {
self.tags.insert(tag, op.clock);
}
for tag in remove {
self.tags.remove(tag, op.clock);
}
}
Action::Thread { action } => {
self.thread
.apply([cob::Op::new(action, op.author, op.timestamp, op.clock)])?;
}
}
}
Ok(())
}
}
impl Deref for Issue {

View File

@ -1,6 +1,5 @@
#![allow(clippy::too_many_arguments)]
use std::fmt;
use std::ops::ControlFlow;
use std::ops::Deref;
use std::ops::Range;
use std::str::FromStr;
@ -14,7 +13,7 @@ use radicle_crdt::{GMap, LWWReg, LWWSet, Max, Redactable, Semilattice};
use crate::cob;
use crate::cob::common::{Author, Tag, Timestamp};
use crate::cob::op::Ops;
use crate::cob::store::FromHistory as _;
use crate::cob::store::Transaction;
use crate::cob::thread;
use crate::cob::thread::CommentId;
@ -227,9 +226,17 @@ impl Patch {
pub fn is_archived(&self) -> bool {
matches!(self.state.get().get(), &State::Archived)
}
}
/// Apply a list of operations to the state.
pub fn apply(&mut self, ops: impl IntoIterator<Item = Op>) -> Result<(), ApplyError> {
impl store::FromHistory for Patch {
type Action = Action;
type Error = ApplyError;
fn type_name() -> &'static TypeName {
&*TYPENAME
}
fn apply(&mut self, ops: impl IntoIterator<Item = Op>) -> Result<(), ApplyError> {
for op in ops {
let id = op.id();
let author = Author::new(op.author);
@ -313,32 +320,6 @@ impl Patch {
}
}
impl store::FromHistory for Patch {
type Action = Action;
fn type_name() -> &'static TypeName {
&*TYPENAME
}
fn from_history(
history: &radicle_cob::History,
) -> Result<(Self, clock::Lamport), store::Error> {
let obj = history.traverse(Self::default(), |mut acc, entry| {
if let Ok(Ops(ops)) = Ops::try_from(entry) {
if let Err(err) = acc.apply(ops) {
log::warn!("Error applying op to patch state: {err}");
return ControlFlow::Break(acc);
}
} else {
return ControlFlow::Break(acc);
}
ControlFlow::Continue(acc)
});
Ok((obj, history.clock().into()))
}
}
/// A patch revision.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Revision {

View File

@ -2,14 +2,15 @@
#![allow(clippy::large_enum_variant)]
#![allow(clippy::type_complexity)]
use std::marker::PhantomData;
use std::ops::ControlFlow;
use nonempty::NonEmpty;
use radicle_crdt::Lamport;
use serde::Serialize;
use serde::{Deserialize, Serialize};
use crate::cob;
use crate::cob::common::Author;
use crate::cob::op::OpId;
use crate::cob::op::{Op, OpId, Ops};
use crate::cob::CollaborativeObject;
use crate::cob::{ActorId, Create, History, ObjectId, TypeName, Update};
use crate::crypto::PublicKey;
@ -24,14 +25,35 @@ pub const HISTORY_TYPE: &str = "radicle";
/// A type that can be materialized from an event history.
/// All collaborative objects implement this trait.
pub trait FromHistory: Sized {
// TODO(finto): Action not being used smells fishy to me
type Action;
pub trait FromHistory: Sized + Default {
/// The underlying action composing each operation.
type Action: for<'de> Deserialize<'de>;
/// Error returned by `apply` function.
type Error: std::error::Error;
/// The object type name.
fn type_name() -> &'static TypeName;
/// Apply a list of operations to the state.
fn apply(&mut self, ops: impl IntoIterator<Item = Op<Self::Action>>)
-> Result<(), Self::Error>;
/// Create an object from a history.
fn from_history(history: &History) -> Result<(Self, Lamport), Error>;
fn from_history(history: &History) -> Result<(Self, Lamport), Error> {
let obj = history.traverse(Self::default(), |mut acc, entry| {
if let Ok(Ops(ops)) = Ops::try_from(entry) {
if let Err(err) = acc.apply(ops) {
log::warn!("Error applying op to `{}` state: {err}", Self::type_name());
return ControlFlow::Break(acc);
}
} else {
return ControlFlow::Break(acc);
}
ControlFlow::Continue(acc)
});
Ok((obj, history.clock().into()))
}
}
/// Store error.

View File

@ -1,6 +1,8 @@
use std::cmp::Ordering;
use std::ops::{Deref, DerefMut};
use std::str::FromStr;
use once_cell::sync::Lazy;
use radicle_crdt as crdt;
use serde::{Deserialize, Serialize};
use thiserror::Error;
@ -13,6 +15,11 @@ use crate::crypto::Signer;
use crdt::clock::Lamport;
use crdt::{GMap, LWWSet, Max, Redactable, Semilattice};
/// Type name of a thread, as well as the domain for all thread operations.
/// Note that threads are not usually used standalone. They are embeded into other COBs.
pub static TYPENAME: Lazy<cob::TypeName> =
Lazy::new(|| FromStr::from_str("xyz.radicle.thread").expect("type name is valid"));
/// Error applying an operation onto a state.
#[derive(Error, Debug)]
pub enum OpError {
@ -228,7 +235,26 @@ impl Thread {
.map(|(a, r)| (a, r))
}
pub fn apply(&mut self, ops: impl IntoIterator<Item = Op<Action>>) -> Result<(), OpError> {
pub fn comments(&self) -> impl Iterator<Item = (&CommentId, &Comment)> + '_ {
self.comments.iter().filter_map(|(id, comment)| {
if let Redactable::Present(c) = comment {
Some((id, c))
} else {
None
}
})
}
}
impl cob::store::FromHistory for Thread {
type Action = Action;
type Error = OpError;
fn type_name() -> &'static radicle_cob::TypeName {
&*TYPENAME
}
fn apply(&mut self, ops: impl IntoIterator<Item = Op<Action>>) -> Result<(), OpError> {
for op in ops.into_iter() {
let id = op.id();
let author = op.author;
@ -270,16 +296,6 @@ impl Thread {
}
Ok(())
}
pub fn comments(&self) -> impl Iterator<Item = (&CommentId, &Comment)> + '_ {
self.comments.iter().filter_map(|(id, comment)| {
if let Redactable::Present(c) = comment {
Some((id, c))
} else {
None
}
})
}
}
/// An object that can be used to create and sign changes.
@ -346,13 +362,9 @@ impl<G> DerefMut for Actor<G> {
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use std::ops::ControlFlow;
use std::str::FromStr;
use std::{array, iter};
use cob::op::Ops;
use nonempty::NonEmpty;
use once_cell::sync::Lazy;
use pretty_assertions::assert_eq;
use qcheck::{Arbitrary, TestResult};
@ -360,34 +372,9 @@ mod tests {
use super::*;
use crate as radicle;
use crate::cob::store::FromHistory;
use crate::crypto::test::signer::MockSigner;
/// Type name of a thread.
pub static TYPENAME: Lazy<cob::TypeName> =
Lazy::new(|| FromStr::from_str("xyz.radicle.thread").expect("type name is valid"));
impl cob::store::FromHistory for Thread {
type Action = Action;
fn type_name() -> &'static radicle_cob::TypeName {
&*TYPENAME
}
fn from_history(history: &cob::History) -> Result<(Self, Lamport), cob::store::Error> {
let obj = history.traverse(Thread::default(), |mut acc, entry| {
if let Ok(Ops(ops)) = Ops::try_from(entry) {
if acc.apply(ops).is_err() {
return ControlFlow::Break(acc);
}
} else {
return ControlFlow::Break(acc);
}
ControlFlow::Continue(acc)
});
Ok((obj, history.clock().into()))
}
}
#[derive(Clone)]
struct Changes<const N: usize> {
permutations: [Vec<Op<Action>>; N],