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

269 lines
6.6 KiB
Rust

use std::collections::BTreeMap;
use std::fmt;
use std::str;
use std::str::FromStr;
use nonempty::NonEmpty;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use radicle_cob::history::EntryWithClock;
use radicle_crdt::clock;
use radicle_crdt::clock::Lamport;
use radicle_crypto::{PublicKey, Signer};
/// Identifies an [`Op`] internally and within the change graph.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(into = "String", try_from = "String")]
pub struct OpId(Lamport, ActorId);
impl OpId {
/// Create a new operation id.
pub fn new(clock: Lamport, actor: ActorId) -> Self {
Self(clock, actor)
}
/// Get the initial operation id for the given actor.
pub fn initial(actor: ActorId) -> Self {
Self(Lamport::initial(), actor)
}
pub fn root(actor: ActorId) -> Self {
Self(Lamport::initial().tick(), actor)
}
/// Get operation id clock.
pub fn clock(&self) -> Lamport {
self.0
}
}
impl fmt::Display for OpId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}", self.1, self.0)
}
}
// Used by `serde::Serialize`.
impl From<OpId> for String {
fn from(value: OpId) -> Self {
value.to_string()
}
}
// Used by `serde::Deserialize`.
impl TryFrom<String> for OpId {
type Error = OpIdError;
fn try_from(value: String) -> Result<Self, Self::Error> {
value.as_str().try_into()
}
}
/// Error decoding an operation from an entry.
#[derive(Error, Debug)]
pub enum OpIdError {
#[error("cannot parse op id from empty string")]
Empty,
#[error("badly formatted op id")]
BadFormat,
}
impl FromStr for OpId {
type Err = OpIdError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::try_from(s)
}
}
impl TryFrom<&str> for OpId {
type Error = OpIdError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
if s.is_empty() {
return Err(OpIdError::Empty);
}
let Some((actor_id, clock)) = s.split_once('/') else {
return Err(OpIdError::BadFormat);
};
Ok(Self(
Lamport::from_str(clock).map_err(|_| OpIdError::BadFormat)?,
ActorId::from_str(actor_id).map_err(|_| OpIdError::BadFormat)?,
))
}
}
/// 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> {
/// 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: Serialize> Op<A> {
pub fn new(
action: A,
author: ActorId,
timestamp: impl Into<clock::Physical>,
clock: Lamport,
) -> Self {
Self {
action,
author,
clock,
timestamp: timestamp.into(),
}
}
}
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 mut clock = entry.clock().into();
entry
.contents()
.clone()
.try_map(|op| {
let action = serde_json::from_slice(&op)?;
let op = Op {
action,
author: *entry.actor(),
clock,
timestamp: entry.timestamp().into(),
};
clock.tick();
Ok(op)
})
.map(Self)
}
}
impl<A> Op<A> {
/// Get the op id.
/// This uniquely identifies each operation in the CRDT.
pub fn id(&self) -> OpId {
OpId(self.clock, self.author)
}
}
/// An object that can be used to create and sign operations.
#[derive(Default)]
pub struct Actor<G, A> {
pub signer: G,
pub clock: Lamport,
pub ops: BTreeMap<(Lamport, PublicKey), Op<A>>,
}
impl<G: Signer, A: Clone> Actor<G, A> {
pub fn new(signer: G) -> Self {
Self {
signer,
clock: Lamport::default(),
ops: BTreeMap::default(),
}
}
pub fn receive(&mut self, ops: impl IntoIterator<Item = Op<A>>) -> Lamport {
for op in ops {
let clock = op.clock;
self.ops.insert((clock, op.author), op);
self.clock.merge(clock);
}
self.clock
}
/// Reset actor state to initial state.
pub fn reset(&mut self) {
self.ops.clear();
self.clock = Lamport::default();
}
/// Returned an ordered list of events.
pub fn timeline(&self) -> impl Iterator<Item = &Op<A>> {
self.ops.values()
}
/// Create a new operation.
pub fn op(&mut self, action: A) -> Op<A> {
let author = *self.signer.public_key();
let clock = self.clock.tick();
let timestamp = clock::Physical::now();
let op = Op {
action,
author,
clock,
timestamp,
};
self.ops.insert((self.clock, author), op.clone());
op
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_opid_try_from_str() {
let s = "z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT/12";
let id = OpId::try_from(s).expect("Op ID parses string");
assert_eq!(s, id.to_string(), "string conversion is consistent");
let s = "";
assert!(OpId::try_from(s).is_err(), "empty strings are invalid");
let s = "jlkjfksgi";
assert!(OpId::try_from(s).is_err(), "badly formatted string");
assert_eq!(
serde_json::from_str::<OpId>(serde_json::to_string(&id).unwrap().as_str()).unwrap(),
id
);
}
}