Skip to main content

aura_core/effects/
intent.rs

1//! Intent Effect Traits
2//!
3//! This module defines the effect trait for intent dispatch - the mechanism
4//! by which user actions are processed through the system.
5//!
6//! # Effect Classification
7//!
8//! - **Category**: Application Effect
9//! - **Implementation**: `aura-app` (Layer 6)
10//! - **Usage**: All UI layers needing to dispatch user actions
11//!
12//! # Design
13//!
14//! The `IntentEffects` trait is generic over:
15//! - `I`: The intent type (e.g., `aura_app::Intent`)
16//! - `E`: The error type (e.g., `aura_app::IntentError`)
17//!
18//! This allows the trait to be defined at the core layer while the specific
19//! intent variants are defined at the application layer where domain logic lives.
20//!
21//! # Flow
22//!
23//! ```text
24//! Intent → Authorize (Biscuit) → Journal → Reduce → View → Sync
25//!          └─────────────────────────────────────────────────┘
26//!                        IntentEffects::dispatch()
27//! ```
28//!
29//! When an intent is dispatched, the handler:
30//! 1. Validates the intent
31//! 2. Checks authorization (Biscuit tokens)
32//! 3. Checks flow budget
33//! 4. Creates a journal fact
34//! 5. Runs the reducer to update state
35//! 6. Notifies subscribers via reactive signals
36
37use async_trait::async_trait;
38use serde::{Deserialize, Serialize};
39use std::fmt::Debug;
40use std::sync::Arc;
41
42// ─────────────────────────────────────────────────────────────────────────────
43// Error Types
44// ─────────────────────────────────────────────────────────────────────────────
45
46/// Base error type for intent dispatch.
47///
48/// This provides common error variants that any intent system should support.
49/// Concrete implementations can wrap this or define their own more specific errors.
50#[derive(Debug, Clone, thiserror::Error, Serialize, Deserialize)]
51pub enum IntentDispatchError {
52    /// The intent was not authorized
53    #[error("Unauthorized: {reason}")]
54    Unauthorized { reason: String },
55
56    /// The intent failed validation
57    #[error("Validation failed: {reason}")]
58    ValidationFailed { reason: String },
59
60    /// Flow budget exceeded
61    #[error("Flow budget exceeded: {reason}")]
62    FlowBudgetExceeded { reason: String },
63
64    /// Journal error during fact recording
65    #[error("Journal error: {reason}")]
66    JournalError { reason: String },
67
68    /// Reactive system error during state update
69    #[error("Reactive error: {reason}")]
70    ReactiveError { reason: String },
71
72    /// Internal error during dispatch
73    #[error("Internal error: {reason}")]
74    InternalError { reason: String },
75}
76
77impl IntentDispatchError {
78    /// Create an unauthorized error
79    pub fn unauthorized(reason: impl Into<String>) -> Self {
80        Self::Unauthorized {
81            reason: reason.into(),
82        }
83    }
84
85    /// Create a validation error
86    pub fn validation_failed(reason: impl Into<String>) -> Self {
87        Self::ValidationFailed {
88            reason: reason.into(),
89        }
90    }
91
92    /// Create a flow budget error
93    pub fn flow_budget_exceeded(reason: impl Into<String>) -> Self {
94        Self::FlowBudgetExceeded {
95            reason: reason.into(),
96        }
97    }
98
99    /// Create a journal error
100    pub fn journal_error(reason: impl Into<String>) -> Self {
101        Self::JournalError {
102            reason: reason.into(),
103        }
104    }
105
106    /// Create a reactive error
107    pub fn reactive_error(reason: impl Into<String>) -> Self {
108        Self::ReactiveError {
109            reason: reason.into(),
110        }
111    }
112
113    /// Create an internal error
114    pub fn internal_error(reason: impl Into<String>) -> Self {
115        Self::InternalError {
116            reason: reason.into(),
117        }
118    }
119}
120
121// ─────────────────────────────────────────────────────────────────────────────
122// Intent Metadata
123// ─────────────────────────────────────────────────────────────────────────────
124
125/// Metadata about an intent for authorization and auditing.
126///
127/// This trait allows the effect system to introspect intents without
128/// knowing their concrete type.
129pub trait IntentMetadata {
130    /// Get a human-readable description of the intent
131    fn description(&self) -> &str;
132
133    /// Check if this intent should be recorded in the journal
134    ///
135    /// Pure queries (like navigation) typically shouldn't be journaled.
136    fn should_journal(&self) -> bool;
137
138    /// Get the authorization level required for this intent
139    fn authorization_level(&self) -> AuthorizationLevel {
140        AuthorizationLevel::Basic
141    }
142}
143
144/// Authorization levels for intent dispatch.
145#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
146pub enum AuthorizationLevel {
147    /// No authorization required (e.g., navigation)
148    Public,
149    /// Basic user authorization (e.g., sending messages)
150    Basic,
151    /// Elevated authorization for sensitive operations (e.g., recovery)
152    Sensitive,
153    /// Administrator-level authorization (e.g., banning users)
154    Admin,
155}
156
157impl AuthorizationLevel {
158    /// Get a human-readable description
159    pub fn description(&self) -> &'static str {
160        match self {
161            Self::Public => "public access",
162            Self::Basic => "basic user access",
163            Self::Sensitive => "sensitive operations",
164            Self::Admin => "administrator privileges",
165        }
166    }
167}
168
169// ─────────────────────────────────────────────────────────────────────────────
170// Intent Effects Trait
171// ─────────────────────────────────────────────────────────────────────────────
172
173/// Effect trait for dispatching intents.
174///
175/// This trait defines the interface for processing user actions through
176/// the system. Implementations compose authorization, journaling, state
177/// updates, and reactive notifications.
178///
179/// # Type Parameters
180///
181/// - `I`: The intent type (must implement `IntentMetadata`)
182/// - `E`: The error type (must be convertible from `IntentDispatchError`)
183///
184/// # Example
185///
186/// ```ignore
187/// // Dispatch an intent
188/// let result = effects.dispatch(Intent::SendMessage {
189///     channel_id: channel,
190///     content: "Hello!".to_string(),
191///     reply_to: None,
192/// }).await;
193///
194/// // Dispatch with explicit error handling
195/// match effects.dispatch(intent).await {
196///     Ok(()) => println!("Intent processed"),
197///     Err(e) => eprintln!("Failed: {}", e),
198/// }
199/// ```
200#[async_trait]
201pub trait IntentEffects<I, E>: Send + Sync
202where
203    I: IntentMetadata + Send + Sync + 'static,
204    E: From<IntentDispatchError> + Send + 'static,
205{
206    /// Dispatch an intent for processing.
207    ///
208    /// This method composes multiple effects:
209    /// 1. Authorization check
210    /// 2. Flow budget check
211    /// 3. Journal fact creation (if `should_journal()`)
212    /// 4. State reduction
213    /// 5. Reactive signal emission
214    ///
215    /// # Errors
216    ///
217    /// Returns an error if any step in the dispatch pipeline fails.
218    async fn dispatch(&self, intent: I) -> Result<(), E>;
219
220    /// Dispatch an intent and wait for sync confirmation.
221    ///
222    /// Like `dispatch()`, but also waits for the change to be synced
223    /// to other devices/participants (if applicable).
224    ///
225    /// # Errors
226    ///
227    /// Returns an error if dispatch or sync fails.
228    async fn dispatch_and_sync(&self, intent: I) -> Result<(), E> {
229        // Default implementation just dispatches without sync
230        self.dispatch(intent).await
231    }
232
233    /// Check if an intent would be authorized without dispatching.
234    ///
235    /// This is useful for UI hints (e.g., graying out unauthorized actions).
236    async fn can_dispatch(&self, intent: &I) -> bool;
237}
238
239/// Simplified intent effects trait with a fixed error type.
240///
241/// This is a convenience trait for handlers that use `IntentDispatchError` directly.
242#[async_trait]
243pub trait SimpleIntentEffects<I>: IntentEffects<I, IntentDispatchError>
244where
245    I: IntentMetadata + Send + Sync + 'static,
246{
247}
248
249// Blanket implementation
250impl<T, I> SimpleIntentEffects<I> for T
251where
252    T: IntentEffects<I, IntentDispatchError>,
253    I: IntentMetadata + Send + Sync + 'static,
254{
255}
256
257// ─────────────────────────────────────────────────────────────────────────────
258// Blanket Implementations
259// ─────────────────────────────────────────────────────────────────────────────
260
261/// Blanket implementation for Arc<T> where T: IntentEffects
262#[async_trait]
263impl<T, I, E> IntentEffects<I, E> for Arc<T>
264where
265    T: IntentEffects<I, E> + ?Sized,
266    I: IntentMetadata + Send + Sync + 'static,
267    E: From<IntentDispatchError> + Send + 'static,
268{
269    async fn dispatch(&self, intent: I) -> Result<(), E> {
270        (**self).dispatch(intent).await
271    }
272
273    async fn dispatch_and_sync(&self, intent: I) -> Result<(), E> {
274        (**self).dispatch_and_sync(intent).await
275    }
276
277    async fn can_dispatch(&self, intent: &I) -> bool {
278        (**self).can_dispatch(intent).await
279    }
280}
281
282// ─────────────────────────────────────────────────────────────────────────────
283// Tests
284// ─────────────────────────────────────────────────────────────────────────────
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289
290    #[test]
291    fn test_authorization_level_ordering() {
292        assert!(AuthorizationLevel::Public < AuthorizationLevel::Basic);
293        assert!(AuthorizationLevel::Basic < AuthorizationLevel::Sensitive);
294        assert!(AuthorizationLevel::Sensitive < AuthorizationLevel::Admin);
295    }
296
297    #[test]
298    fn test_intent_dispatch_error_display() {
299        let err = IntentDispatchError::unauthorized("missing token");
300        assert!(err.to_string().contains("missing token"));
301
302        let err = IntentDispatchError::flow_budget_exceeded("rate limited");
303        assert!(err.to_string().contains("rate limited"));
304    }
305
306    #[test]
307    fn test_authorization_level_description() {
308        assert_eq!(AuthorizationLevel::Public.description(), "public access");
309        assert_eq!(
310            AuthorizationLevel::Admin.description(),
311            "administrator privileges"
312        );
313    }
314}