use core::fmt;
use core::future::Future;
use mnesis::{Aggregate, AggregateRoot, DomainEvent, 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())
}
}
#[must_use = "the read-your-writes position and the decided events should be inspected"]
pub enum Execution<A: Aggregate, P, const N: usize> {
Ignored,
Executed {
position: P,
events: Events<EventOf<A>, N>,
},
}
impl<A: Aggregate, P: fmt::Debug, const N: usize> fmt::Debug for Execution<A, P, N>
where
EventOf<A>: DomainEvent,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Ignored => f.write_str("Ignored"),
Self::Executed { position, events } => f
.debug_struct("Executed")
.field("position", position)
.field("events", events)
.finish(),
}
}
}
pub trait CommandRepository<A: Aggregate>: Repository<A> {
#[allow(
clippy::type_complexity,
reason = "the Execution-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<Execution<A, Self::Position, N>, ExecuteError<A::Error, Self::Error>>,
> + Send
where
A: Handle<C, N>,
C: Send,
{
execute_inner(self, root, command)
}
}
#[allow(
clippy::type_complexity,
reason = "the Execution-or-typed-error return is the same intrinsic contract as the trait method; \
an alias would hide the `impl Future`/`Send` capture the API depends on"
)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(
name = "mnesis.aggregate.execute",
level = "debug",
skip_all,
fields(
aggregate = core::any::type_name::<A>(),
stream = %root.id()
)
)
)]
async fn execute_inner<A, R, C, const N: usize>(
repo: &R,
root: &mut AggregateRoot<A>,
command: C,
) -> Result<
Execution<A, <R as Repository<A>>::Position, N>,
ExecuteError<A::Error, <R as Repository<A>>::Error>,
>
where
A: Aggregate + Handle<C, N>,
R: Repository<A> + ?Sized,
C: Send,
{
match root.handle::<C, N>(command).map_err(ExecuteError::Decide)? {
None => Ok(Execution::Ignored),
Some(decided) => {
let position = repo
.save(root, &decided)
.await
.map_err(ExecuteError::Store)?;
Ok(Execution::Executed {
position,
events: 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());
}
}