use std::future::Future;
use std::marker::PhantomData;
use std::pin::Pin;
use std::sync::Arc;
use serde::Serialize;
use serde_json::Value;
use crate::aggregate::Aggregate;
use crate::bus::Message;
use crate::domain_event::DomainEvent;
use crate::graphql::command_contract::CommandOutcome;
use crate::graphql::{Atomic, Eventual, GraphqlOutputType, PreparedCommand, Succeeded};
use crate::microsvc::causal::{AggregatePublication, CausalWorkspace, CausalWorkspaceError};
use crate::microsvc::context::Context;
use crate::microsvc::error::HandlerError;
use crate::microsvc::session::Session;
use crate::outbox::{OutboxMessage, PreparedDomainEvent};
use crate::projection::lower::{DirectCandidate, ProjectionDescriptor};
use crate::read_model::{ReadModelWritePlanBuilder, RelationalReadModel};
pub(super) type GuardFn<D> = dyn Fn(&Context<D>) -> bool + Send + Sync;
pub(super) type HandlerFuture<'a> =
Pin<Box<dyn Future<Output = Result<Value, HandlerError>> + Send + 'a>>;
pub(super) type ProjectorBootstrapFuture<'a> =
Pin<Box<dyn Future<Output = Result<(), HandlerError>> + Send + 'a>>;
pub(super) type HandlerFn<D> =
dyn for<'a> Fn(&'a Context<'a, D>) -> HandlerFuture<'a> + Send + Sync;
pub trait Handler<'a, D: 'a>: Send + Sync {
type Future: Future<Output = Result<Value, HandlerError>> + Send + 'a;
fn call(&self, ctx: &'a Context<'a, D>) -> Self::Future;
}
pub struct CausalCommandContext<'a, A>
where
A: Aggregate + Send + Sync + 'static,
{
message: &'a Message,
session: &'a Session,
workspace: &'a CausalWorkspace<'a, A>,
}
pub struct CausalRepository<'context, 'route, A>
where
A: Aggregate + Send + Sync + 'static,
{
context: &'context CausalCommandContext<'route, A>,
}
impl<'context, 'route, A> CausalRepository<'context, 'route, A>
where
A: Aggregate + Send + Sync + 'static,
{
pub async fn get(
&self,
id: &str,
) -> Result<Option<crate::microsvc::AggregateCheckout<A>>, HandlerError> {
self.context.get(id).await
}
pub fn create(&self) -> crate::microsvc::AggregateCheckout<A> {
self.context.create()
}
pub fn commit(
&self,
checkout: crate::microsvc::AggregateCheckout<A>,
) -> Result<PreparedCausalCommit<'_, 'route, A, NoPublication, NoDirectProjection>, HandlerError>
{
self.context.commit(checkout)
}
pub fn publish_events(
&self,
) -> CausalCommitBuilder<'_, 'route, A, WithPublication, NoDirectProjection> {
self.context.publish_events()
}
pub fn publish<E: DomainEvent>(
&self,
event: E,
) -> CausalCommitBuilder<'_, 'route, A, WithPublication, NoDirectProjection> {
self.context.publish(event)
}
pub fn read_models(
&self,
writes: ReadModelWritePlanBuilder,
) -> CausalCommitBuilder<'_, 'route, A, NoPublication, NoDirectProjection> {
self.context.read_models(writes)
}
pub fn readmodel<M>(
&self,
row: M,
) -> CausalCommitBuilder<'_, 'route, A, NoPublication, StagedProjectedRow<M>>
where
M: RelationalReadModel + Serialize + Send + Sync + 'static,
{
self.context.readmodel(row)
}
pub fn outbox(
&self,
message: OutboxMessage,
) -> CausalCommitBuilder<'_, 'route, A, WithPublication, NoDirectProjection> {
self.context.outbox(message)
}
}
#[doc(hidden)]
pub struct NoPublication;
#[doc(hidden)]
pub struct WithPublication;
#[doc(hidden)]
pub struct NoDirectProjection;
pub struct DirectReadModelProjection<M>(PhantomData<fn() -> M>);
impl<M> DirectReadModelProjection<M> {
pub const fn new() -> Self {
Self(PhantomData)
}
}
impl<M> Default for DirectReadModelProjection<M> {
fn default() -> Self {
Self::new()
}
}
pub const fn direct_read_model<M>() -> DirectReadModelProjection<M> {
DirectReadModelProjection::new()
}
pub struct StagedProjectedRow<M>(M);
pub struct CausalCommitBuilder<
'context,
'route,
A,
Publication = NoPublication,
Projection = NoDirectProjection,
> where
A: Aggregate + Send + Sync + 'static,
{
context: &'context CausalCommandContext<'route, A>,
publish_captured_events: bool,
explicit_events: Vec<PreparedDomainEvent>,
outbox_messages: Vec<OutboxMessage>,
read_model_plans: Vec<ReadModelWritePlanBuilder>,
error: Option<HandlerError>,
projection: Projection,
_publication: PhantomData<fn() -> Publication>,
}
impl<'context, 'route, A>
CausalCommitBuilder<'context, 'route, A, NoPublication, NoDirectProjection>
where
A: Aggregate + Send + Sync + 'static,
{
fn empty(context: &'context CausalCommandContext<'route, A>) -> Self {
Self {
context,
publish_captured_events: false,
explicit_events: Vec::new(),
outbox_messages: Vec::new(),
read_model_plans: Vec::new(),
error: None,
projection: NoDirectProjection,
_publication: PhantomData,
}
}
}
impl<'context, 'route, A, Publication, Projection>
CausalCommitBuilder<'context, 'route, A, Publication, Projection>
where
A: Aggregate + Send + Sync + 'static,
{
pub fn publish_events(
self,
) -> CausalCommitBuilder<'context, 'route, A, WithPublication, Projection> {
CausalCommitBuilder {
context: self.context,
publish_captured_events: true,
explicit_events: self.explicit_events,
outbox_messages: self.outbox_messages,
read_model_plans: self.read_model_plans,
error: self.error,
projection: self.projection,
_publication: PhantomData,
}
}
pub fn publish<E: DomainEvent>(
mut self,
event: E,
) -> CausalCommitBuilder<'context, 'route, A, WithPublication, Projection> {
if self.error.is_none() {
match PreparedDomainEvent::new(event) {
Ok(event) => self.explicit_events.push(event),
Err(error) => {
self.error = Some(HandlerError::Other(Box::new(error)));
}
}
}
CausalCommitBuilder {
context: self.context,
publish_captured_events: self.publish_captured_events,
explicit_events: self.explicit_events,
outbox_messages: self.outbox_messages,
read_model_plans: self.read_model_plans,
error: self.error,
projection: self.projection,
_publication: PhantomData,
}
}
pub fn outbox(
mut self,
message: OutboxMessage,
) -> CausalCommitBuilder<'context, 'route, A, WithPublication, Projection> {
self.outbox_messages.push(message);
CausalCommitBuilder {
context: self.context,
publish_captured_events: self.publish_captured_events,
explicit_events: self.explicit_events,
outbox_messages: self.outbox_messages,
read_model_plans: self.read_model_plans,
error: self.error,
projection: self.projection,
_publication: PhantomData,
}
}
pub fn read_models(mut self, plan: ReadModelWritePlanBuilder) -> Self {
self.read_model_plans.push(plan);
self
}
pub fn readmodel<M>(
self,
row: M,
) -> CausalCommitBuilder<'context, 'route, A, Publication, StagedProjectedRow<M>>
where
M: RelationalReadModel + Serialize + Send + Sync + 'static,
{
CausalCommitBuilder {
context: self.context,
publish_captured_events: self.publish_captured_events,
explicit_events: self.explicit_events,
outbox_messages: self.outbox_messages,
read_model_plans: self.read_model_plans,
error: self.error,
projection: StagedProjectedRow(row),
_publication: PhantomData,
}
}
pub fn aggregate(
mut self,
checkout: crate::microsvc::AggregateCheckout<A>,
) -> Result<Self, HandlerError> {
self.stage_aggregate(checkout)?;
Ok(self)
}
pub fn commit(
mut self,
checkout: crate::microsvc::AggregateCheckout<A>,
) -> Result<PreparedCausalCommit<'context, 'route, A, Publication, Projection>, HandlerError>
{
self.stage_aggregate(checkout)?;
if let Some(error) = self.error.take() {
return Err(error);
}
for message in self.outbox_messages {
self.context
.workspace
.stage_outbox(message)
.map_err(workspace_handler_error)?;
}
for plan in self.read_model_plans {
self.context
.workspace
.stage_read_models(plan)
.map_err(workspace_handler_error)?;
}
Ok(PreparedCausalCommit {
context: self.context,
projection: self.projection,
_publication: PhantomData,
})
}
fn stage_aggregate(
&mut self,
checkout: crate::microsvc::AggregateCheckout<A>,
) -> Result<(), HandlerError> {
if let Some(error) = self.error.take() {
return Err(error);
}
self.context
.workspace
.stage_with_publication(
checkout,
AggregatePublication {
publish_captured_events: self.publish_captured_events,
explicit_events: std::mem::take(&mut self.explicit_events),
},
)
.map_err(workspace_handler_error)
}
}
pub struct PreparedCausalCommit<'context, 'route, A, Publication, Projection>
where
A: Aggregate + Send + Sync + 'static,
{
context: &'context CausalCommandContext<'route, A>,
projection: Projection,
_publication: PhantomData<fn() -> Publication>,
}
impl<A, Publication, Projection> PreparedCausalCommit<'_, '_, A, Publication, Projection>
where
A: Aggregate + Send + Sync + 'static,
{
pub fn succeeded<T>(self, payload: T) -> Result<PreparedCommand<Succeeded<T>>, HandlerError>
where
T: GraphqlOutputType + Serialize + Send + Sync + 'static,
{
let _ = self.projection;
PreparedCommand::prepare(payload).map_err(|error| HandlerError::Other(Box::new(error)))
}
}
impl<A, Projection> PreparedCausalCommit<'_, '_, A, WithPublication, Projection>
where
A: Aggregate + Send + Sync + 'static,
{
pub fn eventual<T>(self, payload: T) -> Result<PreparedCommand<Eventual<T>>, HandlerError>
where
T: GraphqlOutputType + Serialize + Send + Sync + 'static,
{
let _ = self.projection;
PreparedCommand::prepare(payload).map_err(|error| HandlerError::Other(Box::new(error)))
}
}
impl<A, Publication, M> PreparedCausalCommit<'_, '_, A, Publication, DirectReadModelProjection<M>>
where
A: Aggregate + Send + Sync + 'static,
{
pub fn atomic(self, payload: M) -> Result<PreparedCommand<Atomic<M>>, HandlerError>
where
M: RelationalReadModel + Serialize + Send + Sync + 'static,
{
let _ = self.projection;
self.context
.workspace
.prepare_atomic(payload)
.map_err(workspace_handler_error)
}
}
impl<A, Publication, M> PreparedCausalCommit<'_, '_, A, Publication, StagedProjectedRow<M>>
where
A: Aggregate + Send + Sync + 'static,
M: RelationalReadModel + Serialize + Send + Sync + 'static,
{
pub fn atomic(self) -> Result<PreparedCommand<Atomic<M>>, HandlerError> {
self.context
.workspace
.prepare_atomic(self.projection.0)
.map_err(workspace_handler_error)
}
}
impl<A, Publication>
PreparedCausalCommit<'_, '_, A, Publication, ProjectionDescriptor<DirectCandidate>>
where
A: Aggregate + Send + Sync + 'static,
{
pub fn atomic<M>(self) -> Result<PreparedCommand<Atomic<M>>, HandlerError>
where
M: RelationalReadModel + Serialize + Send + Sync + 'static,
{
self.context
.workspace
.prepare_modeled_atomic(self.projection)
.map_err(workspace_handler_error)
}
}
impl<A, Publication> PreparedCausalCommit<'_, '_, A, Publication, NoDirectProjection>
where
A: Aggregate + Send + Sync + 'static,
{
pub fn atomic<M>(self) -> Result<PreparedCommand<Atomic<M>>, HandlerError>
where
M: RelationalReadModel + Serialize + Send + Sync + 'static,
{
self.context
.workspace
.prepare_placement_selected_atomic()
.map_err(workspace_handler_error)
}
}
impl<'a, A> CausalCommandContext<'a, A>
where
A: Aggregate + Send + Sync + 'static,
{
#[cfg(feature = "graphql")]
pub(super) fn new(
message: &'a Message,
session: &'a Session,
workspace: &'a CausalWorkspace<'a, A>,
) -> Self {
Self {
message,
session,
workspace,
}
}
pub fn command_name(&self) -> &str {
self.message.name()
}
pub fn message_id(&self) -> Option<&str> {
self.message.id()
}
pub fn correlation_id(&self) -> Option<&str> {
self.message.correlation_id()
}
pub fn causation_id(&self) -> Option<&str> {
self.message.causation_id()
}
pub fn trace_context(&self) -> crate::TraceContext {
self.message.trace_context()
}
pub fn session(&self) -> &Session {
self.session
}
pub fn user_id(&self) -> Result<&str, HandlerError> {
self.session
.user_id()
.filter(|s| !s.is_empty())
.ok_or_else(|| HandlerError::Unauthorized("missing user ID in session".into()))
}
pub fn role(&self) -> Option<&str> {
self.session.role()
}
pub fn claim(&self, name: &str) -> Option<&str> {
self.session.get(name)
}
pub fn repo(&self) -> CausalRepository<'_, 'a, A> {
CausalRepository { context: self }
}
pub async fn get(
&self,
id: &str,
) -> Result<Option<crate::microsvc::AggregateCheckout<A>>, HandlerError> {
self.load(id).await
}
pub async fn load(
&self,
id: &str,
) -> Result<Option<crate::microsvc::AggregateCheckout<A>>, HandlerError> {
self.workspace
.load(id)
.await
.map_err(workspace_handler_error)
}
pub fn create(&self) -> crate::microsvc::AggregateCheckout<A> {
self.workspace.create()
}
pub fn commit(
&self,
checkout: crate::microsvc::AggregateCheckout<A>,
) -> Result<PreparedCausalCommit<'_, 'a, A, NoPublication, NoDirectProjection>, HandlerError>
{
CausalCommitBuilder::empty(self).commit(checkout)
}
pub fn publish_events(
&self,
) -> CausalCommitBuilder<'_, 'a, A, WithPublication, NoDirectProjection> {
CausalCommitBuilder::empty(self).publish_events()
}
pub fn publish<E: DomainEvent>(
&self,
event: E,
) -> CausalCommitBuilder<'_, 'a, A, WithPublication, NoDirectProjection> {
CausalCommitBuilder::empty(self).publish(event)
}
pub fn read_models(
&self,
writes: ReadModelWritePlanBuilder,
) -> CausalCommitBuilder<'_, 'a, A, NoPublication, NoDirectProjection> {
CausalCommitBuilder::empty(self).read_models(writes)
}
pub fn readmodel<M>(
&self,
row: M,
) -> CausalCommitBuilder<'_, 'a, A, NoPublication, StagedProjectedRow<M>>
where
M: RelationalReadModel + Serialize + Send + Sync + 'static,
{
CausalCommitBuilder::empty(self).readmodel(row)
}
pub fn outbox(
&self,
message: OutboxMessage,
) -> CausalCommitBuilder<'_, 'a, A, WithPublication, NoDirectProjection> {
CausalCommitBuilder::empty(self).outbox(message)
}
#[cfg(all(test, feature = "graphql", feature = "sqlite"))]
pub(crate) fn stage_outbox(&self, message: OutboxMessage) -> Result<(), HandlerError> {
self.workspace
.stage_outbox(message)
.map_err(workspace_handler_error)
}
}
pub(super) fn workspace_handler_error(error: CausalWorkspaceError) -> HandlerError {
HandlerError::Other(Box::new(error))
}
pub trait PreparedCommandHandler<'a, A, I, K>: Send + Sync
where
A: Aggregate + Send + Sync + 'static,
K: CommandOutcome,
{
type Future: Future<Output = Result<PreparedCommand<K>, HandlerError>> + Send + 'a;
fn call(&self, ctx: &'a CausalCommandContext<'a, A>, input: I) -> Self::Future;
}
impl<'a, D, F, Fut> Handler<'a, D> for F
where
D: 'a,
F: Fn(&'a Context<'a, D>) -> Fut + Send + Sync,
Fut: Future<Output = Result<Value, HandlerError>> + Send + 'a,
{
type Future = Fut;
fn call(&self, ctx: &'a Context<'a, D>) -> Fut {
self(ctx)
}
}
impl<'a, A, I, K, F, Fut> PreparedCommandHandler<'a, A, I, K> for F
where
A: Aggregate + Send + Sync + 'static,
I: 'a,
K: CommandOutcome,
F: Fn(&'a CausalCommandContext<'a, A>, I) -> Fut + Send + Sync,
Fut: Future<Output = Result<PreparedCommand<K>, HandlerError>> + Send + 'a,
{
type Future = Fut;
fn call(&self, ctx: &'a CausalCommandContext<'a, A>, input: I) -> Self::Future {
self(ctx, input)
}
}
pub(super) fn boxed_handler<D, F>(handler: F) -> Arc<HandlerFn<D>>
where
F: for<'a> Handler<'a, D> + 'static,
{
Arc::new(move |ctx| Box::pin(handler.call(ctx)) as HandlerFuture<'_>)
}
pub(super) type PreparedHandlerFuture<'a, K> =
Pin<Box<dyn Future<Output = Result<PreparedCommand<K>, HandlerError>> + Send + 'a>>;
pub(super) type PreparedHandlerFn<A, I, K> = dyn for<'a> Fn(&'a CausalCommandContext<'a, A>, I) -> PreparedHandlerFuture<'a, K>
+ Send
+ Sync;
pub(super) type CausalGuardFn<A> =
dyn for<'a> Fn(&CausalCommandContext<'a, A>) -> bool + Send + Sync;
pub(super) fn boxed_prepared_handler<A, I, K, F>(handler: F) -> Arc<PreparedHandlerFn<A, I, K>>
where
A: Aggregate + Send + Sync + 'static,
I: serde::de::DeserializeOwned + Send + 'static,
K: CommandOutcome,
F: for<'a> PreparedCommandHandler<'a, A, I, K> + 'static,
{
Arc::new(move |context, input| {
Box::pin(handler.call(context, input)) as PreparedHandlerFuture<'_, K>
})
}
pub(super) fn boxed_causal_guard<A, G>(guard: G) -> Arc<CausalGuardFn<A>>
where
A: Aggregate + Send + Sync + 'static,
G: for<'a> Fn(&CausalCommandContext<'a, A>) -> bool + Send + Sync + 'static,
{
Arc::new(guard)
}