use crate::core::base_payload::BasePayload;
use crate::core::command::Command;
use crate::domain::domain_event::DomainEvent;
use serde::{Serialize, de::DeserializeOwned};
pub trait Decider<TState, TCommand, TEventPayload>
where
TState: Serialize + DeserializeOwned + Send + Sync + Clone,
TCommand: Serialize + DeserializeOwned + Send + Sync + Clone,
TEventPayload: BasePayload,
{
fn initial_state() -> TState;
fn evolve(current_state: TState, event: DomainEvent<TEventPayload>) -> TState;
fn decide(current_state: TState, command: Command<TCommand>) -> DomainEvent<TEventPayload>;
}
#[cfg(test)]
mod decider_tests {
use super::*;
use rstest::rstest;
use serde::{Deserialize, Serialize};
use strum_macros::AsRefStr;
use uuid::Uuid;
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
pub struct User {
pub id: String,
pub name: String,
pub likes: u8,
}
impl User {
pub fn new(id: String, name: String) -> Self {
Self { id, name, likes: 0 }
}
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, AsRefStr)]
pub enum UserCommand {
CreateUser { id: String, name: String },
LikeUser,
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, AsRefStr)]
pub enum UserEventPayload {
UserCreated { id: String, name: String },
UserLiked,
}
struct UserDecider;
impl Decider<User, UserCommand, UserEventPayload> for UserDecider {
fn initial_state() -> User {
User::new(String::from(""), String::from(""))
}
fn evolve(mut current_state: User, event: DomainEvent<UserEventPayload>) -> User {
match event.payload {
UserEventPayload::UserCreated { id, name } => {
current_state.id = id;
current_state.name = name;
current_state
}
UserEventPayload::UserLiked => {
current_state.likes += 1;
current_state
}
}
}
fn decide(
current_state: User,
command: Command<UserCommand>,
) -> DomainEvent<UserEventPayload> {
match command.payload {
UserCommand::CreateUser { id, name } => {
DomainEvent::create(id.clone(), UserEventPayload::UserCreated { id, name })
}
UserCommand::LikeUser => {
DomainEvent::create(current_state.id, UserEventPayload::UserLiked)
}
}
}
}
#[rstest]
fn it_should_return_the_initial_state() {
let state = UserDecider::initial_state();
assert_eq!(state.id, String::from(""));
assert_eq!(state.name, String::from(""));
assert_eq!(state.likes, 0);
}
#[rstest]
fn it_should_evolve_to_the_current_created_state() {
let aggregate_id = Uuid::now_v7();
let past_events = vec![DomainEvent::create(
aggregate_id.to_string(),
UserEventPayload::UserCreated {
id: aggregate_id.to_string(),
name: "John Doe".to_string(),
},
)];
let state = past_events
.into_iter()
.fold(UserDecider::initial_state(), |state, event| {
UserDecider::evolve(state, event)
});
assert_eq!(state.id, aggregate_id.to_string());
assert_eq!(state.name, String::from("John Doe"));
assert_eq!(state.likes, 0);
}
#[rstest]
fn it_should_evolve_to_the_current_state_with_likes() {
let aggregate_id = Uuid::now_v7();
let mut past_events = vec![DomainEvent::create(
aggregate_id.to_string(),
UserEventPayload::UserCreated {
id: aggregate_id.to_string(),
name: "John Doe".to_string(),
},
)];
past_events.extend(
(0..10).map(|_| {
DomainEvent::create(aggregate_id.to_string(), UserEventPayload::UserLiked)
}),
);
let state = past_events
.into_iter()
.fold(UserDecider::initial_state(), |state, event| {
UserDecider::evolve(state, event)
});
assert_eq!(state.id, aggregate_id.to_string());
assert_eq!(state.name, String::from("John Doe"));
assert_eq!(state.likes, 10);
}
#[rstest]
fn it_should_decide_to_emit_an_user_created_event_based_on_create_user_command() {
let aggregate_id = Uuid::now_v7();
let current_state = UserDecider::initial_state();
let command = Command::create(
aggregate_id.to_string(),
UserCommand::CreateUser {
id: aggregate_id.to_string(),
name: "John Doe".to_string(),
},
);
let event = UserDecider::decide(current_state.clone(), command);
let next_state = vec![event.clone()]
.into_iter()
.fold(current_state, |state, event| {
UserDecider::evolve(state, event)
});
assert_eq!(event.aggregate_id.to_string(), aggregate_id.to_string());
assert_eq!(
event.payload,
UserEventPayload::UserCreated {
id: aggregate_id.to_string(),
name: "John Doe".to_string()
}
);
assert_eq!(next_state.id, aggregate_id.to_string());
assert_eq!(next_state.name, String::from("John Doe"));
assert_eq!(next_state.likes, 0);
}
#[rstest]
fn it_should_decide_to_emit_an_user_liked_event_based_on_like_user_command() {
let aggregate_id = Uuid::now_v7();
let past_events = vec![DomainEvent::create(
aggregate_id.to_string(),
UserEventPayload::UserCreated {
id: aggregate_id.to_string(),
name: "John Doe".to_string(),
},
)];
let current_state = past_events
.into_iter()
.fold(UserDecider::initial_state(), |state, event| {
UserDecider::evolve(state, event)
});
let command = Command::create(aggregate_id.to_string(), UserCommand::LikeUser);
let event = UserDecider::decide(current_state.clone(), command);
let next_state = vec![event.clone()]
.into_iter()
.fold(current_state, |state, event| {
UserDecider::evolve(state, event)
});
assert_eq!(event.aggregate_id.to_string(), aggregate_id.to_string());
assert_eq!(event.payload, UserEventPayload::UserLiked);
assert_eq!(next_state.id, aggregate_id.to_string());
assert_eq!(next_state.name, String::from("John Doe"));
assert_eq!(next_state.likes, 1);
}
}