use prost_types::Any;
use std::error::Error;
use tonic::Status;
use crate::proto::{CommandBook, Cover, EventBook, Notification, Projection};
use crate::router::StateRouter;
#[derive(Debug, Clone)]
pub struct CommandRejectedError {
pub reason: String,
}
impl CommandRejectedError {
pub fn new(reason: impl Into<String>) -> Self {
Self {
reason: reason.into(),
}
}
}
impl std::fmt::Display for CommandRejectedError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Command rejected: {}", self.reason)
}
}
impl std::error::Error for CommandRejectedError {}
impl From<CommandRejectedError> for Status {
fn from(err: CommandRejectedError) -> Self {
Status::failed_precondition(err.reason)
}
}
pub type CommandResult<T> = std::result::Result<T, CommandRejectedError>;
#[derive(Default)]
pub struct RejectionHandlerResponse {
pub events: Option<EventBook>,
pub notification: Option<Notification>,
}
#[derive(Default)]
pub struct SagaHandlerResponse {
pub commands: Vec<CommandBook>,
pub events: Vec<EventBook>,
}
#[derive(Default)]
pub struct ProcessManagerResponse {
pub commands: Vec<CommandBook>,
pub process_events: Option<EventBook>,
pub facts: Vec<EventBook>,
}
pub trait UnpackAny {
fn unpack<M: prost::Message + Default>(&self) -> Result<M, prost::DecodeError>;
}
impl UnpackAny for Any {
fn unpack<M: prost::Message + Default>(&self) -> Result<M, prost::DecodeError> {
M::decode(self.value.as_slice())
}
}
pub trait CommandHandlerDomainHandler: Send + Sync {
type State: Default + 'static;
fn command_types(&self) -> Vec<String>;
fn state_router(&self) -> &StateRouter<Self::State>;
fn rebuild(&self, events: &EventBook) -> Self::State {
self.state_router().with_event_book(events)
}
fn handle(
&self,
cmd: &CommandBook,
payload: &Any,
state: &Self::State,
seq: u32,
) -> CommandResult<EventBook>;
fn on_rejected(
&self,
_notification: &Notification,
_state: &Self::State,
_target_domain: &str,
_target_command: &str,
) -> CommandResult<RejectionHandlerResponse> {
Ok(RejectionHandlerResponse::default())
}
}
pub trait SagaDomainHandler: Send + Sync {
fn event_types(&self) -> Vec<String>;
fn handle(&self, source: &EventBook, event: &Any) -> CommandResult<SagaHandlerResponse>;
fn on_rejected(
&self,
_notification: &Notification,
_target_domain: &str,
_target_command: &str,
) -> CommandResult<RejectionHandlerResponse> {
Ok(RejectionHandlerResponse::default())
}
}
pub trait ProcessManagerDomainHandler<S>: Send + Sync {
fn event_types(&self) -> Vec<String>;
fn prepare(&self, trigger: &EventBook, state: &S, event: &Any) -> Vec<Cover>;
fn handle(
&self,
trigger: &EventBook,
state: &S,
event: &Any,
destinations: &[EventBook],
) -> CommandResult<ProcessManagerResponse>;
fn on_rejected(
&self,
_notification: &Notification,
_state: &S,
_target_domain: &str,
_target_command: &str,
) -> CommandResult<RejectionHandlerResponse> {
Ok(RejectionHandlerResponse::default())
}
}
pub trait ProjectorDomainHandler: Send + Sync {
fn event_types(&self) -> Vec<String>;
fn project(&self, events: &EventBook) -> Result<Projection, Box<dyn Error + Send + Sync>>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn command_rejected_error_display() {
let err = CommandRejectedError::new("insufficient funds");
assert_eq!(err.to_string(), "Command rejected: insufficient funds");
}
#[test]
fn command_rejected_error_to_status() {
let err = CommandRejectedError::new("invalid input");
let status: Status = err.into();
assert_eq!(status.code(), tonic::Code::FailedPrecondition);
}
#[test]
fn rejection_handler_response_default() {
let response = RejectionHandlerResponse::default();
assert!(response.events.is_none());
assert!(response.notification.is_none());
}
#[test]
fn process_manager_response_default() {
let response = ProcessManagerResponse::default();
assert!(response.commands.is_empty());
assert!(response.process_events.is_none());
assert!(response.facts.is_empty());
}
#[test]
fn saga_handler_response_default() {
let response = SagaHandlerResponse::default();
assert!(response.commands.is_empty());
assert!(response.events.is_empty());
}
}