Skip to main content

aura_composition/
composite.rs

1//! Composite Handler for combining multiple effect handlers
2//!
3//! This module provides a composite handler that can delegate to multiple
4//! specialized handlers based on effect type, enabling flexible composition
5//! and modular handler architecture.
6
7use async_trait::async_trait;
8use std::collections::HashMap;
9
10use crate::registry::{
11    EffectRegistry, Handler, HandlerContext, HandlerError, RegisterAllOptions, RegistrableHandler,
12    RegistryError,
13};
14use aura_core::effects::registry as effect_registry;
15use aura_core::{DeviceId, EffectType, ExecutionMode};
16use aura_mpst::LocalSessionType;
17
18/// A composite handler that delegates to specialized handlers based on effect type
19pub struct CompositeHandler {
20    /// Effect registry for dispatching operations
21    registry: EffectRegistry,
22    /// Optional session handler for choreographic execution
23    session_handler: Option<Box<dyn Handler>>,
24    /// Device ID
25    device_id: DeviceId,
26}
27
28impl CompositeHandler {
29    // Adapter-style composite
30    /// Create a new composite handler
31    pub fn new(device_id: DeviceId, execution_mode: ExecutionMode) -> Self {
32        Self {
33            registry: EffectRegistry::new(execution_mode),
34            session_handler: None,
35            device_id,
36        }
37    }
38
39    /// Create a composite handler for testing
40    pub fn for_testing(device_id: DeviceId) -> Self {
41        Self::new(device_id, ExecutionMode::Testing)
42    }
43
44    /// Create a composite handler for production
45    pub fn for_production(device_id: DeviceId) -> Self {
46        Self::new(device_id, ExecutionMode::Production)
47    }
48
49    /// Create a composite handler for simulation
50    pub fn for_simulation(device_id: DeviceId, seed: u64) -> Self {
51        Self::new(device_id, ExecutionMode::Simulation { seed })
52    }
53
54    /// Register a handler for a specific effect type
55    pub fn register_handler(
56        &mut self,
57        effect_type: EffectType,
58        handler: Box<dyn Handler>,
59    ) -> Result<(), CompositeError> {
60        if !handler.supports_effect(effect_type) {
61            return Err(CompositeError::UnsupportedEffect { effect_type });
62        }
63        if effect_type == EffectType::Choreographic {
64            self.session_handler = Some(handler);
65            return Ok(());
66        }
67
68        let adapter = Box::new(HandlerRegistrableAdapter::new(handler));
69        self.registry
70            .register_handler(effect_type, adapter)
71            .map_err(|e| CompositeError::HandlerExecutionFailed {
72                effect_type,
73                source: HandlerError::ExecutionFailed {
74                    source: Box::new(e),
75                },
76            })?;
77
78        Ok(())
79    }
80
81    /// Register the default handler bundle.
82    ///
83    /// Requires explicit opt-in for impure handlers via `RegisterAllOptions`.
84    pub fn register_all(&mut self, options: RegisterAllOptions) -> Result<(), RegistryError> {
85        self.registry.register_all(options)
86    }
87
88    /// Unregister a handler for a specific effect type
89    pub fn unregister_handler(
90        &mut self,
91        effect_type: EffectType,
92    ) -> Option<Box<dyn RegistrableHandler>> {
93        if effect_type == EffectType::Choreographic {
94            self.session_handler.take();
95            return None;
96        }
97        self.registry.unregister_handler(effect_type)
98    }
99
100    /// Check if a handler is registered for an effect type
101    pub fn has_handler(&self, effect_type: EffectType) -> bool {
102        if effect_type == EffectType::Choreographic {
103            return self.session_handler.is_some();
104        }
105        self.registry.is_registered(effect_type)
106    }
107
108    /// Get all registered effect types
109    pub fn registered_effect_types(&self) -> Vec<EffectType> {
110        let mut effects = self.registry.registered_effect_types();
111        if self.session_handler.is_some() {
112            effects.push(EffectType::Choreographic);
113        }
114        effects
115    }
116
117    /// Get the device ID
118    pub fn device_id(&self) -> DeviceId {
119        self.device_id
120    }
121}
122
123/// Error type for composite handler operations
124#[derive(Debug, thiserror::Error)]
125pub enum CompositeError {
126    /// Effect type not supported by handler
127    #[error("Effect type {effect_type:?} not supported by handler")]
128    UnsupportedEffect { effect_type: EffectType },
129
130    /// No handler registered for effect type
131    #[error("No handler registered for effect type {effect_type:?}")]
132    NoHandlerRegistered { effect_type: EffectType },
133
134    /// Handler execution failed
135    #[error("Handler execution failed for effect type {effect_type:?}")]
136    HandlerExecutionFailed {
137        effect_type: EffectType,
138        #[source]
139        source: HandlerError,
140    },
141}
142
143#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
144#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
145impl Handler for CompositeHandler {
146    // Adapter-style composite
147    async fn execute_effect(
148        &self,
149        effect_type: EffectType,
150        operation: &str,
151        parameters: &[u8],
152        ctx: &HandlerContext,
153    ) -> Result<Vec<u8>, HandlerError> {
154        self.registry
155            .execute_effect(effect_type, operation, parameters, ctx)
156            .await
157    }
158
159    async fn execute_session(
160        &self,
161        session: LocalSessionType,
162        ctx: &HandlerContext,
163    ) -> Result<(), HandlerError> {
164        // Sessions are executed exclusively by a registered choreographic handler
165        if let Some(handler) = self.session_handler.as_ref() {
166            handler.execute_session(session, ctx).await
167        } else {
168            // Return a session execution error if no choreographic handler is available
169            Err(HandlerError::SessionExecution {
170                source: "No choreographic handler registered".into(),
171            })
172        }
173    }
174
175    fn supports_effect(&self, effect_type: EffectType) -> bool {
176        if effect_type == EffectType::Choreographic {
177            return self.session_handler.is_some();
178        }
179        self.registry.supports_effect(effect_type)
180    }
181
182    fn execution_mode(&self) -> ExecutionMode {
183        self.registry.execution_mode()
184    }
185}
186
187/// Builder for creating composite handlers
188pub struct CompositeHandlerBuilder {
189    device_id: DeviceId,
190    execution_mode: ExecutionMode,
191    handlers: HashMap<EffectType, Box<dyn Handler>>,
192}
193
194impl CompositeHandlerBuilder {
195    /// Create a new builder
196    pub fn new(device_id: DeviceId) -> Self {
197        Self {
198            device_id,
199            execution_mode: ExecutionMode::Testing,
200            handlers: HashMap::new(),
201        }
202    }
203
204    /// Set execution mode
205    pub fn execution_mode(mut self, mode: ExecutionMode) -> Self {
206        self.execution_mode = mode;
207        self
208    }
209
210    /// Add a handler for an effect type
211    pub fn with_handler(
212        mut self,
213        effect_type: EffectType,
214        handler: Box<dyn Handler>,
215    ) -> Result<Self, CompositeError> {
216        if !handler.supports_effect(effect_type) {
217            return Err(CompositeError::UnsupportedEffect { effect_type });
218        }
219        self.handlers.insert(effect_type, handler);
220        Ok(self)
221    }
222
223    /// Build the composite handler
224    pub fn build(self) -> CompositeHandler {
225        let mut composite = CompositeHandler::new(self.device_id, self.execution_mode);
226        for (effect_type, handler) in self.handlers {
227            // We know the handler supports the effect type from the with_handler check
228            let _ = composite.register_handler(effect_type, handler);
229        }
230        composite
231    }
232}
233
234/// Adapter to make CompositeHandler work as RegistrableHandler
235pub struct CompositeHandlerAdapter {
236    composite: CompositeHandler,
237}
238
239impl CompositeHandlerAdapter {
240    /// Create a new adapter
241    pub fn new(composite: CompositeHandler) -> Self {
242        Self { composite }
243    }
244
245    /// Create adapter for testing
246    pub fn for_testing(device_id: DeviceId) -> Self {
247        Self::new(CompositeHandler::for_testing(device_id))
248    }
249
250    /// Create adapter for production
251    pub fn for_production(device_id: DeviceId) -> Self {
252        Self::new(CompositeHandler::for_production(device_id))
253    }
254
255    /// Create adapter for simulation
256    pub fn for_simulation(device_id: DeviceId, seed: u64) -> Self {
257        Self::new(CompositeHandler::for_simulation(device_id, seed))
258    }
259
260    /// Register a handler
261    pub fn register_handler(
262        &mut self,
263        effect_type: EffectType,
264        handler: Box<dyn Handler>,
265    ) -> Result<(), CompositeError> {
266        self.composite.register_handler(effect_type, handler)
267    }
268
269    /// Get the underlying composite handler
270    pub fn into_composite(self) -> CompositeHandler {
271        self.composite
272    }
273
274    /// Get a reference to the composite handler
275    pub fn composite(&self) -> &CompositeHandler {
276        &self.composite
277    }
278
279    /// Get a mutable reference to the composite handler
280    pub fn composite_mut(&mut self) -> &mut CompositeHandler {
281        &mut self.composite
282    }
283}
284
285#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
286#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
287impl Handler for CompositeHandlerAdapter {
288    async fn execute_effect(
289        &self,
290        effect_type: EffectType,
291        operation: &str,
292        parameters: &[u8],
293        ctx: &HandlerContext,
294    ) -> Result<Vec<u8>, HandlerError> {
295        self.composite
296            .execute_effect(effect_type, operation, parameters, ctx)
297            .await
298    }
299
300    async fn execute_session(
301        &self,
302        session: LocalSessionType,
303        ctx: &HandlerContext,
304    ) -> Result<(), HandlerError> {
305        self.composite.execute_session(session, ctx).await
306    }
307
308    fn supports_effect(&self, effect_type: EffectType) -> bool {
309        self.composite.supports_effect(effect_type)
310    }
311
312    fn execution_mode(&self) -> ExecutionMode {
313        self.composite.execution_mode()
314    }
315}
316
317#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
318#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
319impl RegistrableHandler for CompositeHandlerAdapter {
320    async fn execute_operation_bytes(
321        &self,
322        effect_type: EffectType,
323        operation: &str,
324        parameters: &[u8],
325        ctx: &HandlerContext,
326    ) -> Result<Vec<u8>, HandlerError> {
327        self.execute_effect(effect_type, operation, parameters, ctx)
328            .await
329    }
330
331    fn supported_operations(&self, effect_type: EffectType) -> Vec<String> {
332        effect_registry::operations_for(effect_type)
333            .iter()
334            .map(|op| (*op).to_string())
335            .collect()
336    }
337
338    fn supports_effect(&self, effect_type: EffectType) -> bool {
339        self.composite.supports_effect(effect_type)
340    }
341
342    fn execution_mode(&self) -> ExecutionMode {
343        self.composite.execution_mode()
344    }
345}
346
347/// Adapter to expose a `Handler` as a `RegistrableHandler` for registry dispatch.
348struct HandlerRegistrableAdapter {
349    handler: Box<dyn Handler>,
350}
351
352impl HandlerRegistrableAdapter {
353    fn new(handler: Box<dyn Handler>) -> Self {
354        Self { handler }
355    }
356}
357
358#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
359#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
360impl RegistrableHandler for HandlerRegistrableAdapter {
361    async fn execute_operation_bytes(
362        &self,
363        effect_type: EffectType,
364        operation: &str,
365        parameters: &[u8],
366        ctx: &HandlerContext,
367    ) -> Result<Vec<u8>, HandlerError> {
368        self.handler
369            .execute_effect(effect_type, operation, parameters, ctx)
370            .await
371    }
372
373    fn supported_operations(&self, effect_type: EffectType) -> Vec<String> {
374        effect_registry::operations_for(effect_type)
375            .iter()
376            .map(|op| (*op).to_string())
377            .collect()
378    }
379
380    fn supports_effect(&self, effect_type: EffectType) -> bool {
381        self.handler.supports_effect(effect_type)
382    }
383
384    fn execution_mode(&self) -> ExecutionMode {
385        self.handler.execution_mode()
386    }
387}
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392    use crate::registry::Handler;
393
394    /// Minimal handler used for registration tests
395    struct TestConsoleHandler;
396
397    #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
398    #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
399    impl Handler for TestConsoleHandler {
400        async fn execute_effect(
401            &self,
402            _effect_type: EffectType,
403            operation: &str,
404            _parameters: &[u8],
405            _ctx: &HandlerContext,
406        ) -> Result<Vec<u8>, HandlerError> {
407            match operation {
408                "log_info" | "log_warn" | "log_error" => Ok(vec![]),
409                _ => Err(HandlerError::UnknownOperation {
410                    effect_type: EffectType::Console,
411                    operation: operation.to_string(),
412                }),
413            }
414        }
415
416        async fn execute_session(
417            &self,
418            _session: LocalSessionType,
419            _ctx: &HandlerContext,
420        ) -> Result<(), HandlerError> {
421            Err(HandlerError::SessionExecution {
422                source: "Console handler does not execute sessions".into(),
423            })
424        }
425
426        fn supports_effect(&self, effect_type: EffectType) -> bool {
427            effect_type == EffectType::Console
428        }
429
430        fn execution_mode(&self) -> ExecutionMode {
431            ExecutionMode::Testing
432        }
433    }
434
435    /// All three execution modes (Testing, Production, Simulation) produce
436    /// handlers with the correct mode and device identity.
437    #[test]
438    fn test_composite_handler_creation() {
439        let device_id = DeviceId::new_from_entropy([1u8; 32]);
440
441        let handler = CompositeHandler::for_testing(device_id);
442        assert_eq!(handler.execution_mode(), ExecutionMode::Testing);
443        assert_eq!(handler.device_id(), device_id);
444
445        let handler = CompositeHandler::for_production(device_id);
446        assert_eq!(handler.execution_mode(), ExecutionMode::Production);
447
448        let handler = CompositeHandler::for_simulation(device_id, 42);
449        assert_eq!(
450            handler.execution_mode(),
451            ExecutionMode::Simulation { seed: 42 }
452        );
453    }
454
455    /// Builder sets execution mode and preserves device identity through build.
456    #[test]
457    fn test_composite_handler_builder() {
458        let device_id = DeviceId::new_from_entropy([2u8; 32]);
459
460        let builder =
461            CompositeHandlerBuilder::new(device_id).execution_mode(ExecutionMode::Production);
462
463        // Note: We can't easily test handler registration here without mock handlers
464        // In a real test, we would create mock handlers and register them
465
466        let composite = builder.build();
467        assert_eq!(composite.execution_mode(), ExecutionMode::Production);
468        assert_eq!(composite.device_id(), device_id);
469    }
470
471    /// Adapter factories produce the correct execution mode for each variant.
472    #[test]
473    fn test_composite_handler_adapter() {
474        let device_id = DeviceId::new_from_entropy([3u8; 32]);
475
476        let adapter = CompositeHandlerAdapter::for_testing(device_id);
477        assert_eq!(Handler::execution_mode(&adapter), ExecutionMode::Testing);
478
479        let adapter = CompositeHandlerAdapter::for_production(device_id);
480        assert_eq!(Handler::execution_mode(&adapter), ExecutionMode::Production);
481
482        let adapter = CompositeHandlerAdapter::for_simulation(device_id, 42);
483        assert_eq!(
484            Handler::execution_mode(&adapter),
485            ExecutionMode::Simulation { seed: 42 }
486        );
487    }
488
489    /// Registering a handler updates `has_handler` and `registered_effect_types`.
490    #[test]
491    fn test_handler_registration() {
492        let device_id = DeviceId::new_from_entropy([4u8; 32]);
493        let mut composite = CompositeHandler::for_testing(device_id);
494
495        // Initially no handlers registered
496        assert!(!composite.has_handler(EffectType::Console));
497        assert!(composite.registered_effect_types().is_empty());
498
499        // Register a minimal console handler and validate registration bookkeeping
500        let handler = Box::new(TestConsoleHandler);
501        composite
502            .register_handler(EffectType::Console, handler)
503            .unwrap();
504
505        assert!(composite.has_handler(EffectType::Console));
506        assert_eq!(
507            composite.registered_effect_types(),
508            vec![EffectType::Console]
509        );
510    }
511
512    /// Operation mapping returns known operations for registered types and
513    /// empty for unsupported types.
514    #[test]
515    fn test_supported_operations() {
516        let device_id = DeviceId::new_from_entropy([5u8; 32]);
517        let adapter = CompositeHandlerAdapter::for_testing(device_id);
518
519        // Test that the operation mapping exists (even without registered handlers)
520        let console_ops = adapter.supported_operations(EffectType::Console);
521        assert!(console_ops.contains(&"log_info".to_string()));
522
523        let random_ops = adapter.supported_operations(EffectType::Random);
524        assert!(random_ops.contains(&"random_bytes".to_string()));
525
526        // Unsupported effect type should return empty list
527        let unknown_ops = adapter.supported_operations(EffectType::PropertyChecking);
528        assert!(unknown_ops.is_empty());
529    }
530}