Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cob: Add validation step when loading COBs
Alexis Sellier committed 2 years ago
commit eeaf380067f9ad14d5d0fbe26b0cc052fa052c6b
parent c400961cec340b8caa6eee4489c07596326ee5a5
7 files changed +73 -11
modified radicle-cli/src/commands/issue.rs
@@ -327,12 +327,13 @@ fn list(

    let mut all = Vec::new();
    for result in issues.all()? {
-
        let (id, issue, _) = result?;
-

+
        let Ok((id, issue, _)) = result else {
+
            // Skip issues that failed to load.
+
            continue;
+
        };
        if Some(true) == assignee.map(|a| !issue.assigned().any(|v| v == Did::from(a))) {
            continue;
        }
-

        all.push((id, issue))
    }

modified radicle-cli/src/commands/patch/list.rs
@@ -20,8 +20,10 @@ pub fn run(

    let mut all = Vec::new();
    for patch in patches.all()? {
-
        let (id, patch, _) = patch?;
-

+
        let Ok((id, patch, _)) = patch else {
+
            // Skip patches that failed to load.
+
            continue;
+
        };
        if !filter(patch.state()) {
            continue;
        }
modified radicle/src/cob/identity.rs
@@ -93,6 +93,9 @@ pub enum ApplyError {
    /// Error applying an op to the proposal thread.
    #[error("thread apply failed: {0}")]
    Thread(#[from] thread::OpError),
+
    /// Error validating the state.
+
    #[error("validation failed: {0}")]
+
    Validate(&'static str),
}

/// Error committing the proposal.
@@ -311,6 +314,13 @@ impl store::FromHistory for Proposal {
        &*TYPENAME
    }

+
    fn validate(&self) -> Result<(), Self::Error> {
+
        if self.revisions.is_empty() {
+
            return Err(ApplyError::Validate("no revisions found"));
+
        }
+
        Ok(())
+
    }
+

    fn apply<R: ReadRepository>(
        &mut self,
        ops: impl IntoIterator<Item = Op>,
modified radicle/src/cob/issue.rs
@@ -34,6 +34,8 @@ pub type IssueId = ObjectId;
pub enum Error {
    #[error("apply failed")]
    Apply,
+
    #[error("validation failed: {0}")]
+
    Validate(&'static str),
    #[error("description missing")]
    DescriptionMissing,
    #[error("thread apply failed: {0}")]
@@ -134,6 +136,16 @@ impl store::FromHistory for Issue {
        &*TYPENAME
    }

+
    fn validate(&self) -> Result<(), Self::Error> {
+
        if self.title.get().is_empty() {
+
            return Err(Error::Validate("title is empty"));
+
        }
+
        if self.thread.validate().is_err() {
+
            return Err(Error::Validate("invalid thread"));
+
        }
+
        Ok(())
+
    }
+

    fn apply<R: ReadRepository>(
        &mut self,
        ops: impl IntoIterator<Item = Op>,
modified radicle/src/cob/patch.rs
@@ -75,6 +75,9 @@ pub enum ApplyError {
    /// Git error.
    #[error("git: {0}")]
    Git(#[from] git::ext::Error),
+
    /// Validation error.
+
    #[error("validation failed: {0}")]
+
    Validate(&'static str),
}

/// Error updating or creating patches.
@@ -344,6 +347,16 @@ impl store::FromHistory for Patch {
        &*TYPENAME
    }

+
    fn validate(&self) -> Result<(), Self::Error> {
+
        if self.revisions.is_empty() {
+
            return Err(ApplyError::Validate("no revisions found"));
+
        }
+
        if self.title().is_empty() {
+
            return Err(ApplyError::Validate("empty title"));
+
        }
+
        Ok(())
+
    }
+

    fn apply<R: ReadRepository>(
        &mut self,
        ops: impl IntoIterator<Item = Op>,
modified radicle/src/cob/store.rs
@@ -3,6 +3,7 @@
#![allow(clippy::type_complexity)]
use std::marker::PhantomData;
use std::ops::ControlFlow;
+
use std::sync::Arc;

use nonempty::NonEmpty;
use radicle_crdt::Lamport;
@@ -28,11 +29,11 @@ pub trait HistoryAction {

/// A type that can be materialized from an event history.
/// All collaborative objects implement this trait.
-
pub trait FromHistory: Sized + Default {
+
pub trait FromHistory: Sized + Default + PartialEq {
    /// The underlying action composing each operation.
    type Action: HistoryAction + for<'de> Deserialize<'de> + Serialize;
    /// Error returned by `apply` function.
-
    type Error: std::error::Error;
+
    type Error: std::error::Error + Send + Sync + 'static;

    /// The object type name.
    fn type_name() -> &'static TypeName;
@@ -44,11 +45,14 @@ pub trait FromHistory: Sized + Default {
        repo: &R,
    ) -> Result<(), Self::Error>;

+
    /// Validate the object. Returns an error if the object is invalid.
+
    fn validate(&self) -> Result<(), Self::Error>;
+

    /// Create an object from a history.
    fn from_history<R: ReadRepository>(
        history: &History,
        repo: &R,
-
    ) -> Result<(Self, Lamport), Error> {
+
    ) -> Result<(Self, Lamport), Self::Error> {
        let obj = history.traverse(Self::default(), |mut acc, entry| {
            match Ops::try_from(entry) {
                Ok(Ops(ops)) => {
@@ -68,6 +72,8 @@ pub trait FromHistory: Sized + Default {
            ControlFlow::Continue(acc)
        });

+
        obj.validate()?;
+

        Ok((obj, history.clock().into()))
    }

@@ -103,6 +109,8 @@ pub enum Error {
    HistoryType(String),
    #[error("object `{1}` of type `{0}` was not found")]
    NotFound(TypeName, ObjectId),
+
    #[error("apply: {0}")]
+
    Apply(Arc<dyn std::error::Error + Sync + Send + 'static>),
    #[error("signed refs: {0}")]
    SignRefs(#[from] storage::Error),
    #[error("failed to find reference '{name}': {err}")]
@@ -113,6 +121,12 @@ pub enum Error {
    },
}

+
impl Error {
+
    fn apply(e: impl std::error::Error + Sync + Send + 'static) -> Self {
+
        Self::Apply(Arc::new(e))
+
    }
+
}
+

/// Storage for collaborative objects of a specific type `T` in a single repository.
pub struct Store<'a, T> {
    identity: git::Oid,
@@ -197,7 +211,7 @@ where
                contents,
            },
        )?;
-
        let (object, clock) = T::from_history(cob.history(), self.repo)?;
+
        let (object, clock) = T::from_history(cob.history(), self.repo).map_err(Error::apply)?;

        self.repo.sign_refs(signer).map_err(Error::SignRefs)?;

@@ -212,7 +226,7 @@ where
            if cob.manifest().history_type != HISTORY_TYPE {
                return Err(Error::HistoryType(cob.manifest().history_type.clone()));
            }
-
            let (obj, clock) = T::from_history(cob.history(), self.repo)?;
+
            let (obj, clock) = T::from_history(cob.history(), self.repo).map_err(Error::apply)?;

            Ok(Some((obj, clock)))
        } else {
@@ -227,7 +241,7 @@ where
        let raw = cob::list(self.repo, T::type_name())?;

        Ok(raw.into_iter().map(|o| {
-
            let (obj, clock) = T::from_history(o.history(), self.repo)?;
+
            let (obj, clock) = T::from_history(o.history(), self.repo).map_err(Error::apply)?;
            Ok((*o.id(), obj, clock))
        }))
    }
modified radicle/src/cob/thread.rs
@@ -31,6 +31,9 @@ pub enum OpError {
    /// that hasn't happened yet.
    #[error("causal dependency {0:?} missing")]
    Missing(EntryId),
+
    /// Validation error.
+
    #[error("validation failed: {0}")]
+
    Validate(&'static str),
}

/// Identifies a comment.
@@ -264,6 +267,13 @@ impl cob::store::FromHistory for Thread {
        &*TYPENAME
    }

+
    fn validate(&self) -> Result<(), Self::Error> {
+
        if self.comments.is_empty() {
+
            return Err(OpError::Validate("no comments found"));
+
        }
+
        Ok(())
+
    }
+

    fn apply<R: ReadRepository>(
        &mut self,
        ops: impl IntoIterator<Item = Op<Action>>,