radicle-heartwood-lfs/radicle/src/cob/op.rs

103 lines
2.6 KiB
Rust

use nonempty::NonEmpty;
use thiserror::Error;
use radicle_cob::history::{EntryId, EntryWithClock};
use radicle_crdt::clock;
use radicle_crdt::clock::Lamport;
use radicle_crypto::PublicKey;
/// The author of an [`Op`].
pub type ActorId = PublicKey;
/// Error decoding an operation from an entry.
#[derive(Error, Debug)]
pub enum OpEncodingError {
#[error("encoding failed: {0}")]
Encoding(#[from] serde_json::Error),
#[error("git: {0}")]
Git(#[from] git2::Error),
}
/// The `Op` is the operation that is applied onto a state to form a CRDT.
///
/// Everything that can be done in the system is represented by an `Op`.
/// Operations are applied to an accumulator to yield a final state.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Op<A> {
/// Id of the entry under which this operation lives.
pub id: EntryId,
/// The action carried out by this operation.
pub action: A,
/// The author of the operation.
pub author: ActorId,
/// Lamport clock.
pub clock: Lamport,
/// Timestamp of this operation.
pub timestamp: clock::Physical,
}
impl<A: Eq> PartialOrd for Op<A> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.id.partial_cmp(&other.id)
}
}
impl<A: Eq> Ord for Op<A> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.id.cmp(&other.id)
}
}
impl<A> Op<A> {
pub fn new(
id: EntryId,
action: A,
author: ActorId,
timestamp: impl Into<clock::Physical>,
clock: Lamport,
) -> Self {
Self {
id,
action,
author,
clock,
timestamp: timestamp.into(),
}
}
pub fn id(&self) -> EntryId {
self.id
}
}
pub struct Ops<A>(pub NonEmpty<Op<A>>);
impl<'a, A> TryFrom<&'a EntryWithClock> for Ops<A>
where
for<'de> A: serde::Deserialize<'de>,
{
type Error = OpEncodingError;
fn try_from(entry: &'a EntryWithClock) -> Result<Self, Self::Error> {
let id = *entry.id();
let ops = entry
.changes()
.map(|blob| {
let action = serde_json::from_slice(blob)?;
let op = Op {
id,
action,
author: *entry.actor(),
clock: entry.clock().into(),
timestamp: entry.timestamp().into(),
};
Ok::<_, Self::Error>(op)
})
.collect::<Result<Vec<_>, _>>()?;
// SAFETY: Entry is guaranteed to have at least one operation.
#[allow(clippy::unwrap_used)]
Ok(Self(NonEmpty::from_vec(ops).unwrap()))
}
}