telltale_runtime/effects/extension.rs
1//! Type-safe extension system for choreographic effects
2//!
3//! Extensions enable domain-specific effects (capabilities, budgets, logging, error handling)
4//! while maintaining full type safety and algebraic effects composition.
5
6use std::any::{Any, TypeId};
7use std::fmt::Debug;
8
9use crate::effects::RoleId;
10
11/// Trait for type-safe extension effects
12///
13/// Extension effects are domain-specific effects that extend the core
14/// choreography effect system. Each extension defines its own data type
15/// and semantics while participating in the full effect lifecycle.
16///
17/// # Type Safety
18///
19/// Extensions are identified by `TypeId`, ensuring compile-time type safety
20/// and preventing string-based errors. The type `Self` is the extension's
21/// payload and must be clonable for effect algebra operations.
22///
23/// # Projection Semantics
24///
25/// Extensions must specify which roles participate via `participating_roles()`.
26/// Only those roles will see the extension in their projected local types.
27/// Extensions that return an empty vec are included in all role projections.
28///
29/// # Example
30///
31/// ```text
32/// #[derive(Clone, Debug)]
33/// pub struct ValidateCapability<R: RoleId> {
34/// pub capability: String,
35/// pub role: R,
36/// }
37///
38/// impl<R: RoleId> ExtensionEffect<R> for ValidateCapability<R> {
39/// fn type_id(&self) -> TypeId {
40/// TypeId::of::<Self>()
41/// }
42///
43/// fn type_name(&self) -> &'static str {
44/// "ValidateCapability"
45/// }
46///
47/// fn participating_roles(&self) -> Vec<R> {
48/// vec![self.role]
49/// }
50///
51/// fn as_any(&self) -> &dyn Any {
52/// self
53/// }
54///
55/// fn as_any_mut(&mut self) -> &mut dyn Any {
56/// self
57/// }
58///
59/// fn clone_box(&self) -> Box<dyn ExtensionEffect<R>> {
60/// Box::new(self.clone())
61/// }
62/// }
63/// ```
64pub trait ExtensionEffect<R: RoleId>: Send + Sync + Debug {
65 /// Get the TypeId of this extension type
66 ///
67 /// This is used for type-safe downcasting and extension discrimination.
68 /// Typically implemented as `TypeId::of::<Self>()`.
69 fn type_id(&self) -> TypeId;
70
71 /// Get a human-readable name for this extension type
72 ///
73 /// Used in error messages and debugging. Should be the type name.
74 fn type_name(&self) -> &'static str;
75
76 /// Get the roles that participate in this extension effect
77 ///
78 /// # Projection Semantics
79 ///
80 /// Returns a vector of roles participating in the effect.
81 ///
82 /// - Empty vec: Extension appears in all role projections (global effect)
83 /// - Non-empty: Extension appears only in specified role projections
84 fn participating_roles(&self) -> Vec<R> {
85 vec![] // Default: global extensions
86 }
87
88 /// Downcast to `&dyn Any` for type-safe casting
89 fn as_any(&self) -> &dyn Any;
90
91 /// Downcast to `&mut dyn Any` for type-safe mutable casting
92 fn as_any_mut(&mut self) -> &mut dyn Any;
93
94 /// Clone this extension into a boxed trait object
95 ///
96 /// Required for cloning `Effect` enum which contains `Box<dyn ExtensionEffect>`.
97 fn clone_box(&self) -> Box<dyn ExtensionEffect<R>>;
98}
99
100/// Extension-specific errors
101#[derive(Debug, Clone, thiserror::Error)]
102pub enum ExtensionError {
103 #[error("Unknown extension type: {type_name} (TypeId: {type_id:?})")]
104 UnknownExtension {
105 type_name: &'static str,
106 type_id: TypeId,
107 },
108
109 #[error("Extension handler not registered for {type_name}")]
110 HandlerNotRegistered { type_name: &'static str },
111
112 #[error("Extension {type_name} failed: {error}")]
113 ExecutionFailed {
114 type_name: &'static str,
115 error: String,
116 },
117
118 #[error("Type mismatch: expected {expected}, got {actual}")]
119 TypeMismatch {
120 expected: &'static str,
121 actual: &'static str,
122 },
123
124 #[error("Handler already registered for extension type: {type_name}")]
125 DuplicateHandler { type_name: &'static str },
126
127 #[error("Cannot merge registries: duplicate handler for {type_name}")]
128 MergeConflict { type_name: &'static str },
129}