use core::future::Future;
use mnesis::{Aggregate, AggregateRoot, EventOf, Events, Handle};
use crate::conflict::ConflictPredicate;
use crate::repository::Repository;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ExecuteError<DecideErr, StoreErr> {
#[error("command rejected: {0}")]
Decide(#[source] DecideErr),
#[error(transparent)]
Store(StoreErr),
}
impl<DecideErr, StoreErr: ConflictPredicate> ExecuteError<DecideErr, StoreErr> {
#[must_use]
pub fn is_conflict(&self) -> bool {
matches!(self, Self::Store(e) if e.is_conflict())
}
}
pub trait CommandRepository<A: Aggregate>: Repository<A> {
#[allow(
clippy::type_complexity,
reason = "the decided-events-or-typed-error return is intrinsic to the contract; an \
alias would hide the `impl Future`/`Send` capture the API depends on"
)]
fn execute<C, const N: usize>(
&self,
root: &mut AggregateRoot<A>,
command: C,
) -> impl Future<Output = Result<Events<EventOf<A>, N>, ExecuteError<A::Error, Self::Error>>> + Send
where
A: Handle<C, N>,
C: Send,
{
async move {
let decided = root.handle::<C, N>(command).map_err(ExecuteError::Decide)?;
self.save(root, &decided)
.await
.map_err(ExecuteError::Store)?;
Ok(decided)
}
}
}
impl<A: Aggregate, R: Repository<A>> CommandRepository<A> for R {}
#[cfg(test)]
mod error_tests {
use super::ExecuteError;
use crate::error::StoreError;
use mnesis::{ErrorId, Version};
type TestStoreError =
StoreError<std::io::Error, std::convert::Infallible, std::convert::Infallible>;
type TestExecuteError = ExecuteError<&'static str, TestStoreError>;
#[test]
fn conflict_store_error_is_conflict() {
let e: TestExecuteError = ExecuteError::Store(StoreError::Conflict {
stream_id: ErrorId::from_display(&"s"),
expected: Some(Version::INITIAL),
actual: None,
});
assert!(e.is_conflict());
}
#[test]
fn decide_error_is_not_conflict() {
let e: TestExecuteError = ExecuteError::Decide("rejected");
assert!(!e.is_conflict());
}
}