cob: Add validation step when loading COBs

This adds a simple `validate` function to COBs that is run on load. It
allows COBs to check whether they are valid, once all operations are
loaded, and return an error if not.
This commit is contained in:
Alexis Sellier 2023-05-29 11:27:04 +02:00
parent c400961cec
commit eeaf380067
No known key found for this signature in database
7 changed files with 73 additions and 11 deletions

View File

@ -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))
}

View File

@ -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;
}

View File

@ -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>,

View File

@ -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>,

View File

@ -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>,

View File

@ -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))
}))
}

View File

@ -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>>,