Skip to main content

ferro_events/
traits.rs

1//! Core traits for the event system.
2
3use crate::Error;
4use async_trait::async_trait;
5use std::any::Any;
6
7/// Marker trait for events that can be dispatched.
8///
9/// Events are simple data structures that represent something that happened
10/// in your application. They should be cheap to clone and contain all the
11/// data needed by listeners.
12///
13/// # Example
14///
15/// ```rust
16/// use ferro_events::Event;
17///
18/// #[derive(Clone)]
19/// struct OrderPlaced {
20///     order_id: i64,
21///     user_id: i64,
22///     total: f64,
23/// }
24///
25/// impl Event for OrderPlaced {
26///     fn name(&self) -> &'static str {
27///         "OrderPlaced"
28///     }
29/// }
30///
31/// // Dispatch the event (Laravel-style API):
32/// // OrderPlaced { order_id: 1, user_id: 2, total: 99.99 }.dispatch().await?;
33/// ```
34pub trait Event: Clone + Send + Sync + 'static {
35    /// Returns the name of the event for logging and debugging.
36    fn name(&self) -> &'static str;
37
38    /// Returns the event as Any for type erasure.
39    fn as_any(&self) -> &dyn Any
40    where
41        Self: Sized,
42    {
43        self
44    }
45
46    /// Dispatch this event using the global dispatcher.
47    ///
48    /// This is the ergonomic Laravel-style API for dispatching events.
49    ///
50    /// # Example
51    ///
52    /// ```rust,ignore
53    /// use ferro_events::Event;
54    ///
55    /// #[derive(Clone)]
56    /// struct UserRegistered { user_id: i64 }
57    /// impl Event for UserRegistered {
58    ///     fn name(&self) -> &'static str { "UserRegistered" }
59    /// }
60    ///
61    /// async fn register_user() -> Result<(), ferro_events::Error> {
62    ///     // ... registration logic ...
63    ///     UserRegistered { user_id: 123 }.dispatch().await?;
64    ///     Ok(())
65    /// }
66    /// ```
67    fn dispatch(
68        self,
69    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), Error>> + Send>>
70    where
71        Self: Sized,
72    {
73        Box::pin(crate::dispatch(self))
74    }
75
76    /// Dispatch this event without waiting (fire and forget).
77    ///
78    /// This spawns the event handling as a background task and returns immediately.
79    /// Useful when you don't need to wait for listeners to complete.
80    ///
81    /// # Example
82    ///
83    /// ```rust,ignore
84    /// use ferro_events::Event;
85    ///
86    /// #[derive(Clone)]
87    /// struct PageViewed { page: String }
88    /// impl Event for PageViewed {
89    ///     fn name(&self) -> &'static str { "PageViewed" }
90    /// }
91    ///
92    /// fn track_page_view(page: &str) {
93    ///     PageViewed { page: page.to_string() }.dispatch_sync();
94    /// }
95    /// ```
96    fn dispatch_sync(self)
97    where
98        Self: Sized,
99    {
100        crate::dispatch_sync(self)
101    }
102}
103
104/// A listener that handles events of type `E`.
105///
106/// Listeners contain the logic that should run when an event is dispatched.
107/// They can be synchronous or asynchronous.
108///
109/// # Example
110///
111/// ```rust
112/// use ferro_events::{Event, Listener, Error, async_trait};
113///
114/// #[derive(Clone)]
115/// struct UserRegistered { email: String }
116///
117/// impl Event for UserRegistered {
118///     fn name(&self) -> &'static str { "UserRegistered" }
119/// }
120///
121/// struct SendWelcomeEmail;
122///
123/// #[async_trait]
124/// impl Listener<UserRegistered> for SendWelcomeEmail {
125///     async fn handle(&self, event: &UserRegistered) -> Result<(), Error> {
126///         println!("Welcome, {}!", event.email);
127///         Ok(())
128///     }
129/// }
130/// ```
131#[async_trait]
132pub trait Listener<E: Event>: Send + Sync + 'static {
133    /// Handle the event.
134    ///
135    /// This method is called when the event is dispatched. It receives
136    /// an immutable reference to the event data.
137    async fn handle(&self, event: &E) -> Result<(), Error>;
138
139    /// Returns the name of the listener for logging and debugging.
140    fn name(&self) -> &'static str {
141        std::any::type_name::<Self>()
142    }
143
144    /// Whether this listener should stop propagation to other listeners.
145    ///
146    /// If this returns `true` after handling, no further listeners will
147    /// be called for this event.
148    fn should_stop_propagation(&self) -> bool {
149        false
150    }
151}
152
153/// Marker trait for listeners that should be queued for background processing.
154///
155/// Listeners implementing this trait will not be executed immediately.
156/// Instead, they will be pushed to a job queue and processed asynchronously
157/// by a worker.
158///
159/// # Example
160///
161/// ```rust
162/// use ferro_events::{Event, Listener, ShouldQueue, Error, async_trait};
163///
164/// #[derive(Clone)]
165/// struct LargeFileUploaded { path: String }
166///
167/// impl Event for LargeFileUploaded {
168///     fn name(&self) -> &'static str { "LargeFileUploaded" }
169/// }
170///
171/// struct ProcessUploadedFile;
172///
173/// impl ShouldQueue for ProcessUploadedFile {
174///     fn queue(&self) -> &'static str {
175///         "file-processing"
176///     }
177/// }
178///
179/// #[async_trait]
180/// impl Listener<LargeFileUploaded> for ProcessUploadedFile {
181///     async fn handle(&self, event: &LargeFileUploaded) -> Result<(), Error> {
182///         // This will run in a background worker
183///         println!("Processing file: {}", event.path);
184///         Ok(())
185///     }
186/// }
187/// ```
188pub trait ShouldQueue {
189    /// The queue name to dispatch this listener to.
190    fn queue(&self) -> &'static str {
191        "default"
192    }
193
194    /// The number of seconds to delay before processing.
195    fn delay(&self) -> Option<u64> {
196        None
197    }
198
199    /// The number of times to retry on failure.
200    fn max_retries(&self) -> u32 {
201        3
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[derive(Clone)]
210    struct TestEvent {
211        message: String,
212    }
213
214    impl Event for TestEvent {
215        fn name(&self) -> &'static str {
216            "TestEvent"
217        }
218    }
219
220    struct TestListener;
221
222    #[async_trait]
223    impl Listener<TestEvent> for TestListener {
224        async fn handle(&self, event: &TestEvent) -> Result<(), Error> {
225            assert_eq!(event.message, "hello");
226            Ok(())
227        }
228    }
229
230    #[tokio::test]
231    async fn test_listener_handle() {
232        let listener = TestListener;
233        let event = TestEvent {
234            message: "hello".into(),
235        };
236        let result = listener.handle(&event).await;
237        assert!(result.is_ok());
238    }
239
240    #[test]
241    fn test_event_name() {
242        let event = TestEvent {
243            message: "test".into(),
244        };
245        assert_eq!(event.name(), "TestEvent");
246    }
247
248    // Event type for dispatch method test (unique to avoid test interference)
249    #[derive(Clone)]
250    struct DispatchTestEvent {
251        value: u32,
252    }
253
254    impl Event for DispatchTestEvent {
255        fn name(&self) -> &'static str {
256            "DispatchTestEvent"
257        }
258    }
259
260    #[tokio::test]
261    async fn test_event_dispatch_method() {
262        use crate::global_dispatcher;
263        use std::sync::atomic::{AtomicU32, Ordering};
264        use std::sync::Arc;
265
266        let counter = Arc::new(AtomicU32::new(0));
267        let counter_clone = Arc::clone(&counter);
268
269        // Register a listener using the global dispatcher
270        global_dispatcher().on::<DispatchTestEvent, _, _>(move |event| {
271            let counter = Arc::clone(&counter_clone);
272            async move {
273                counter.fetch_add(event.value, Ordering::SeqCst);
274                Ok(())
275            }
276        });
277
278        // Use the new ergonomic dispatch method
279        let event = DispatchTestEvent { value: 42 };
280        let result = event.dispatch().await;
281        assert!(result.is_ok());
282        assert_eq!(counter.load(Ordering::SeqCst), 42);
283    }
284
285    // Event type for dispatch_sync test (unique to avoid test interference)
286    #[derive(Clone)]
287    struct SyncDispatchTestEvent {
288        value: u32,
289    }
290
291    impl Event for SyncDispatchTestEvent {
292        fn name(&self) -> &'static str {
293            "SyncDispatchTestEvent"
294        }
295    }
296
297    #[tokio::test]
298    async fn test_event_dispatch_sync_method() {
299        use crate::global_dispatcher;
300        use std::sync::atomic::{AtomicU32, Ordering};
301        use std::sync::Arc;
302        use tokio::time::{sleep, Duration};
303
304        let counter = Arc::new(AtomicU32::new(0));
305        let counter_clone = Arc::clone(&counter);
306
307        // Register a listener
308        global_dispatcher().on::<SyncDispatchTestEvent, _, _>(move |event| {
309            let counter = Arc::clone(&counter_clone);
310            async move {
311                counter.fetch_add(event.value, Ordering::SeqCst);
312                Ok(())
313            }
314        });
315
316        // Use dispatch_sync (fire and forget)
317        let event = SyncDispatchTestEvent { value: 99 };
318        event.dispatch_sync();
319
320        // Give the background task time to complete
321        sleep(Duration::from_millis(50)).await;
322        assert_eq!(counter.load(Ordering::SeqCst), 99);
323    }
324}