armature_cqrs/
command.rs

1//! Command handling for CQRS
2
3use async_trait::async_trait;
4use dashmap::DashMap;
5use std::any::{Any, TypeId};
6use std::sync::Arc;
7use thiserror::Error;
8
9/// Command trait
10///
11/// Commands represent write operations in CQRS.
12pub trait Command: Send + Sync + 'static {
13    /// Command result type
14    type Result: Send;
15}
16
17/// Command handler trait
18#[async_trait]
19pub trait CommandHandler<C: Command>: Send + Sync {
20    /// Handle the command
21    async fn handle(&self, command: C) -> Result<C::Result, CommandError>;
22}
23
24/// Command error
25#[derive(Debug, Error)]
26pub enum CommandError {
27    #[error("Command execution failed: {0}")]
28    ExecutionFailed(String),
29
30    #[error("Handler not found for command")]
31    HandlerNotFound,
32
33    #[error("Validation error: {0}")]
34    ValidationError(String),
35
36    #[error("Business rule violation: {0}")]
37    BusinessRuleViolation(String),
38}
39
40/// Type-erased command handler
41#[async_trait]
42trait DynCommandHandler: Send + Sync {
43    async fn handle_dyn(
44        &self,
45        command: Box<dyn Any + Send>,
46    ) -> Result<Box<dyn Any + Send>, CommandError>;
47}
48
49/// Wrapper for typed command handlers
50struct TypedCommandHandler<C: Command, H: CommandHandler<C>> {
51    handler: H,
52    _phantom: std::marker::PhantomData<C>,
53}
54
55impl<C: Command, H: CommandHandler<C>> TypedCommandHandler<C, H> {
56    fn new(handler: H) -> Self {
57        Self {
58            handler,
59            _phantom: std::marker::PhantomData,
60        }
61    }
62}
63
64#[async_trait]
65impl<C: Command, H: CommandHandler<C>> DynCommandHandler for TypedCommandHandler<C, H> {
66    async fn handle_dyn(
67        &self,
68        command: Box<dyn Any + Send>,
69    ) -> Result<Box<dyn Any + Send>, CommandError> {
70        match command.downcast::<C>() {
71            Ok(cmd) => {
72                let result = self.handler.handle(*cmd).await?;
73                Ok(Box::new(result))
74            }
75            Err(_) => Err(CommandError::ExecutionFailed("Type mismatch".to_string())),
76        }
77    }
78}
79
80/// Command bus
81pub struct CommandBus {
82    handlers: DashMap<TypeId, Arc<dyn DynCommandHandler>>,
83}
84
85impl CommandBus {
86    /// Create new command bus
87    pub fn new() -> Self {
88        Self {
89            handlers: DashMap::new(),
90        }
91    }
92
93    /// Register a command handler
94    pub fn register<C, H>(&self, handler: H)
95    where
96        C: Command,
97        H: CommandHandler<C> + 'static,
98    {
99        let type_id = TypeId::of::<C>();
100        let handler = Arc::new(TypedCommandHandler::new(handler));
101        self.handlers.insert(type_id, handler);
102    }
103
104    /// Execute a command
105    pub async fn execute<C>(&self, command: C) -> Result<C::Result, CommandError>
106    where
107        C: Command,
108    {
109        let type_id = TypeId::of::<C>();
110
111        let handler = self
112            .handlers
113            .get(&type_id)
114            .ok_or(CommandError::HandlerNotFound)?;
115
116        let boxed_command: Box<dyn Any + Send> = Box::new(command);
117        let result = handler.handle_dyn(boxed_command).await?;
118
119        match result.downcast::<C::Result>() {
120            Ok(result) => Ok(*result),
121            Err(_) => Err(CommandError::ExecutionFailed(
122                "Result type mismatch".to_string(),
123            )),
124        }
125    }
126}
127
128impl Default for CommandBus {
129    fn default() -> Self {
130        Self::new()
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    struct CreateUserCommand {
139        email: String,
140    }
141
142    impl Command for CreateUserCommand {
143        type Result = String; // Returns user ID
144    }
145
146    struct CreateUserHandler;
147
148    #[async_trait]
149    impl CommandHandler<CreateUserCommand> for CreateUserHandler {
150        async fn handle(&self, command: CreateUserCommand) -> Result<String, CommandError> {
151            Ok(format!("user-{}", command.email))
152        }
153    }
154
155    #[tokio::test]
156    async fn test_command_bus() {
157        let bus = CommandBus::new();
158        bus.register::<CreateUserCommand, _>(CreateUserHandler);
159
160        let command = CreateUserCommand {
161            email: "alice@example.com".to_string(),
162        };
163
164        let result = bus.execute(command).await.unwrap();
165        assert_eq!(result, "user-alice@example.com");
166    }
167}