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}