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}