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:
parent
c400961cec
commit
eeaf380067
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>>,
|
||||
|
|
|
|||
Loading…
Reference in New Issue