Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
heartwood crates radicle src cob op.rs
use nonempty::NonEmpty;
use radicle_cob::Manifest;
use serde::Serialize;
use thiserror::Error;

use radicle_cob::history::{Entry, EntryId};
use radicle_crypto::PublicKey;

use crate::cob;
use crate::cob::Timestamp;
use crate::git;
use crate::identity;
use crate::identity::DocAt;
use crate::storage::ReadRepository;

/// 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] git::raw::Error),
}

#[derive(Error, Debug)]
#[error("failed to load manifest of '{object}': {err}")]
pub struct ManifestError {
    object: git::Oid,
    #[source]
    err: Box<dyn std::error::Error + Send + Sync + 'static>,
}

/// Error loading an `Op` from storage.
#[derive(Error, Debug)]
pub enum LoadError {
    #[error("failed to load Op at '{object}': {source}")]
    Load {
        object: git::Oid,
        source: Box<dyn std::error::Error + Send + Sync + 'static>,
    },
    #[error("failed to decode Op at '{object}': {source}")]
    Encoding {
        object: git::Oid,
        source: OpEncodingError,
    },
}

/// 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, Serialize)]
pub struct Op<A> {
    /// Id of the entry under which this operation lives.
    pub id: EntryId,
    /// The action carried out by this operation.
    pub actions: NonEmpty<A>,
    /// The author of the operation.
    pub author: ActorId,
    /// Timestamp of this operation.
    pub timestamp: Timestamp,
    /// Parent operations.
    pub parents: Vec<EntryId>,
    /// Related objects.
    pub related: Vec<git::Oid>,
    /// Head of identity document committed to by this operation.
    pub identity: Option<git::Oid>,
    /// Object manifest.
    pub manifest: Manifest,
}

impl<A: Eq> PartialOrd for Op<A> {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(self.cmp(other))
    }
}

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,
        actions: impl Into<NonEmpty<A>>,
        author: ActorId,
        timestamp: impl Into<Timestamp>,
        identity: Option<git::Oid>,
        manifest: Manifest,
    ) -> Self {
        Self {
            id,
            actions: actions.into(),
            author,
            timestamp: timestamp.into(),
            parents: vec![],
            related: vec![],
            identity,
            manifest,
        }
    }

    pub fn id(&self) -> EntryId {
        self.id
    }

    pub fn identity_doc<R: ReadRepository>(
        &self,
        repo: &R,
    ) -> Result<Option<DocAt>, identity::DocError> {
        match self.identity {
            None => Ok(None),
            Some(head) => repo.identity_doc_at(head).map(Some),
        }
    }

    pub fn manifest_of<S>(store: &S, id: &git::Oid) -> Result<Manifest, ManifestError>
    where
        S: cob::change::Storage<
                ObjectId = git::Oid,
                Parent = git::Oid,
                Signatures = crypto::ssh::ExtendedSignature,
            >,
    {
        store.manifest_of(id).map_err(|err| ManifestError {
            object: *id,
            err: Box::new(err),
        })
    }

    /// Get the `Op` identified by the `id` in the provided `store`.
    pub fn load<S>(store: &S, id: git::Oid) -> Result<Self, LoadError>
    where
        S: cob::change::Storage<
                ObjectId = git::Oid,
                Parent = git::Oid,
                Signatures = crypto::ssh::ExtendedSignature,
            >,
        for<'de> A: serde::Deserialize<'de>,
    {
        let entry = store.load(id).map_err(|err| LoadError::Load {
            object: id,
            source: Box::new(err),
        })?;
        Op::try_from(&entry).map_err(|err| LoadError::Encoding {
            object: id,
            source: err,
        })
    }
}

impl From<Entry> for Op<Vec<u8>> {
    fn from(entry: Entry) -> Self {
        Self {
            id: *entry.id(),
            actions: entry.contents().clone(),
            author: *entry.author(),
            parents: entry.parents,
            related: entry.related,
            timestamp: Timestamp::from_secs(entry.timestamp),
            identity: entry.resource,
            manifest: entry.manifest.clone(),
        }
    }
}

impl<'a, A> TryFrom<&'a Entry> for Op<A>
where
    for<'de> A: serde::Deserialize<'de>,
{
    type Error = OpEncodingError;

    fn try_from(entry: &'a Entry) -> Result<Self, Self::Error> {
        let id = *entry.id();
        let identity = entry.resource().copied();
        let actions: Vec<_> = entry
            .contents()
            .iter()
            .map(|blob| serde_json::from_slice(blob.as_slice()))
            .collect::<Result<_, _>>()?;
        let manifest = entry.manifest.clone();

        // SAFETY: Entry is guaranteed to have at least one operation.
        #[allow(clippy::unwrap_used)]
        let actions = NonEmpty::from_vec(actions).unwrap();
        let op = Op {
            id,
            actions,
            author: *entry.author(),
            timestamp: Timestamp::from_secs(entry.timestamp),
            parents: entry.parents.to_owned(),
            related: entry.related.to_owned(),
            identity,
            manifest,
        };

        Ok(op)
    }
}

impl<A: 'static> IntoIterator for Op<A> {
    type Item = A;
    type IntoIter = <NonEmpty<A> as IntoIterator>::IntoIter;

    fn into_iter(self) -> Self::IntoIter {
        self.actions.into_iter()
    }
}