use crate::aggregate::{Aggregate, AggregateRoot, EventOf, Handle};
use crate::event::DomainEvent;
use crate::events::Events;
use crate::saga::{React, Saga};
use crate::version::Version;
use alloc::vec::Vec;
use core::fmt::Debug;
pub struct AggregateFixture<A: Aggregate> {
id: A::Id,
}
impl<A: Aggregate> AggregateFixture<A>
where
A::Id: Default,
{
#[must_use]
pub fn new() -> Self {
Self {
id: A::Id::default(),
}
}
}
impl<A: Aggregate> Default for AggregateFixture<A>
where
A::Id: Default,
{
fn default() -> Self {
Self::new()
}
}
impl<A: Aggregate> AggregateFixture<A> {
#[must_use]
pub const fn with_id(id: A::Id) -> Self {
Self { id }
}
#[must_use]
pub fn given(self, history: impl IntoIterator<Item = EventOf<A>>) -> Given<A> {
let mut root = AggregateRoot::<A>::new(self.id);
let mut version = Version::INITIAL;
for (index, event) in history.into_iter().enumerate() {
let outcome = root.replay(version, &event);
assert!(
outcome.is_ok(),
"given: replaying history event #{index} at version {version:?} failed: {outcome:?}"
);
if let Some(next) = version.next() {
version = next;
}
}
Given { root }
}
}
pub struct Given<A: Aggregate> {
root: AggregateRoot<A>,
}
impl<A: Aggregate> Given<A> {
#[must_use]
pub fn when<C, const N: usize>(self, cmd: C) -> Acted<A, N>
where
A: Handle<C, N>,
{
let mut root = self.root;
let result = root.handle(cmd);
if let Ok(events) = &result {
root.apply_events(events);
}
Acted { root, result }
}
#[must_use]
pub fn then_expect_state(self, assertion: impl FnOnce(&A::State)) -> Self {
assertion(self.root.state());
self
}
}
pub struct Acted<A: Aggregate, const N: usize> {
root: AggregateRoot<A>,
result: Result<Events<EventOf<A>, N>, <A as Aggregate>::Error>,
}
impl<A: Aggregate, const N: usize> Acted<A, N> {
#[must_use]
pub fn then_expect_events(self, expected: impl IntoIterator<Item = EventOf<A>>) -> Self
where
EventOf<A>: PartialEq + Debug,
{
let expected_events: Vec<EventOf<A>> = expected.into_iter().collect();
assert!(
self.result.is_ok(),
"then_expect_events: command was rejected with error: {:?}",
self.result.as_ref().err()
);
let Ok(produced) = &self.result else {
return self; };
let produced_refs: Vec<&EventOf<A>> = produced.iter().collect();
let expected_refs: Vec<&EventOf<A>> = expected_events.iter().collect();
assert_eq!(
produced_refs, expected_refs,
"then_expect_events: decided events did not match expected"
);
self
}
#[must_use]
#[allow(
clippy::needless_pass_by_value,
reason = "by-value `expected` is the documented assertion ergonomics (mirrors assert_eq! call style); PartialEq comparison only borrows, so it is not consumed, but requiring &Error at call sites would be worse DX"
)]
pub fn then_expect_error(self, expected: <A as Aggregate>::Error) -> Self
where
<A as Aggregate>::Error: PartialEq,
{
assert!(
self.result.is_err(),
"then_expect_error: expected the command to be rejected, but it produced events"
);
let Err(actual) = &self.result else {
return self; };
assert_eq!(
*actual, expected,
"then_expect_error: error did not match expected"
);
self
}
#[must_use]
pub fn then_expect_error_matching(
self,
predicate: impl FnOnce(&<A as Aggregate>::Error) -> bool,
) -> Self {
assert!(
self.result.is_err(),
"then_expect_error_matching: expected the command to be rejected, but it produced events"
);
let Err(actual) = &self.result else {
return self; };
assert!(
predicate(actual),
"then_expect_error_matching: error did not satisfy predicate: {actual:?}"
);
self
}
#[must_use]
pub fn then_expect_state(self, assertion: impl FnOnce(&A::State)) -> Self {
assertion(self.root.state());
self
}
}
pub struct SagaFixture<S: Saga> {
id: S::Id,
}
impl<S: Saga> SagaFixture<S>
where
S::Id: Default,
{
#[must_use]
pub fn new() -> Self {
Self {
id: S::Id::default(),
}
}
}
impl<S: Saga> Default for SagaFixture<S>
where
S::Id: Default,
{
fn default() -> Self {
Self::new()
}
}
impl<S: Saga> SagaFixture<S> {
#[must_use]
pub const fn with_id(id: S::Id) -> Self {
Self { id }
}
#[must_use]
pub fn given(self, history: impl IntoIterator<Item = EventOf<S>>) -> SagaGiven<S> {
let mut root = AggregateRoot::<S>::new(self.id);
let mut version = Version::INITIAL;
for (index, event) in history.into_iter().enumerate() {
let outcome = root.replay(version, &event);
assert!(
outcome.is_ok(),
"given: replaying saga history event #{index} at version {version:?} failed: {outcome:?}"
);
if let Some(next) = version.next() {
version = next;
}
}
SagaGiven { root }
}
}
pub struct SagaGiven<S: Saga> {
root: AggregateRoot<S>,
}
impl<S: Saga> SagaGiven<S> {
#[must_use]
pub fn when<E, const N: usize>(self, event: &E) -> SagaReacted<S, N>
where
S: React<E, N>,
E: DomainEvent,
{
let mut root = self.root;
let result = root.react::<E, N>(event);
let commands = match &result {
Ok(Some(events)) => {
let cmds: Vec<S::Command> =
events.iter().filter_map(|e| S::intent_for(e)).collect();
root.apply_events(events);
cmds
}
_ => Vec::new(),
};
SagaReacted {
root,
result,
commands,
}
}
#[must_use]
pub fn then_expect_state(self, assertion: impl FnOnce(&S::State)) -> Self {
assertion(self.root.state());
self
}
}
pub struct SagaReacted<S: Saga, const N: usize> {
root: AggregateRoot<S>,
result: Result<Option<Events<EventOf<S>, N>>, <S as Aggregate>::Error>,
commands: Vec<S::Command>,
}
impl<S: Saga, const N: usize> SagaReacted<S, N> {
#[must_use]
pub fn then_expect_events(self, expected: impl IntoIterator<Item = EventOf<S>>) -> Self
where
EventOf<S>: PartialEq + Debug,
{
let expected_events: Vec<EventOf<S>> = expected.into_iter().collect();
assert!(
self.result.is_ok(),
"then_expect_events: react was rejected with error: {:?}",
self.result.as_ref().err()
);
let Ok(maybe) = &self.result else {
return self; };
assert!(
maybe.is_some(),
"then_expect_events: react ignored the event (Ok(None)) but events were expected"
);
let Some(produced) = maybe else {
return self; };
let produced_refs: Vec<&EventOf<S>> = produced.iter().collect();
let expected_refs: Vec<&EventOf<S>> = expected_events.iter().collect();
assert_eq!(
produced_refs, expected_refs,
"then_expect_events: produced own-events did not match expected"
);
self
}
#[must_use]
pub fn then_expect_commands(self, expected: impl IntoIterator<Item = S::Command>) -> Self
where
S::Command: PartialEq + Debug,
{
let expected_cmds: Vec<S::Command> = expected.into_iter().collect();
assert_eq!(
self.commands, expected_cmds,
"then_expect_commands: projected intents did not match expected"
);
self
}
#[must_use]
pub fn then_expect_ignored(self) -> Self {
assert!(
matches!(self.result, Ok(None)),
"then_expect_ignored: expected react to ignore the event (Ok(None)), got: {:?}",
self.result
);
self
}
#[must_use]
#[allow(
clippy::needless_pass_by_value,
reason = "by-value `expected` mirrors assert_eq! call style; PartialEq only borrows"
)]
pub fn then_expect_error(self, expected: <S as Aggregate>::Error) -> Self
where
<S as Aggregate>::Error: PartialEq,
{
assert!(
self.result.is_err(),
"then_expect_error: expected react to be rejected, but it produced: {:?}",
self.result.as_ref().ok()
);
let Err(actual) = &self.result else {
return self; };
assert_eq!(
*actual, expected,
"then_expect_error: error did not match expected"
);
self
}
#[must_use]
pub fn then_expect_error_matching(
self,
predicate: impl FnOnce(&<S as Aggregate>::Error) -> bool,
) -> Self {
assert!(
self.result.is_err(),
"then_expect_error_matching: expected react to be rejected, but it produced: {:?}",
self.result.as_ref().ok()
);
let Err(actual) = &self.result else {
return self; };
assert!(
predicate(actual),
"then_expect_error_matching: error did not satisfy predicate: {actual:?}"
);
self
}
#[must_use]
pub fn then_expect_state(self, assertion: impl FnOnce(&S::State)) -> Self {
assertion(self.root.state());
self
}
}