Skip to main content

medi_rs/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2
3//! Static async mediator with generated command dispatch, event workers, and
4//! typed resource injection.
5//!
6//! Applications define commands with [`MediCommand`], annotate async handlers
7//! with [`medi_handler`], group routes in [`medi_module`], and compose the
8//! selected modules with [`mediator!`]. The generated mediator dispatches
9//! commands directly to their handler and resolves handler resources from a
10//! typed tuple; it does not use dynamic handler or resource registries.
11//!
12//! # Commands
13//!
14//! ```no_run
15//! use medi_rs::{MediCommand, Result, medi_handler, medi_module, mediator};
16//!
17//! #[derive(MediCommand)]
18//! #[medi_command(return_type = String, error_type = medi_rs::Error)]
19//! struct Greet;
20//!
21//! #[medi_handler]
22//! async fn greet(_: Greet) -> Result<String> {
23//!     Ok("hello".into())
24//! }
25//!
26//! medi_module! {
27//!     manifest greeting;
28//!     commands { Greet => greet; }
29//! }
30//!
31//! mediator! {
32//!     struct AppMediator {
33//!         event_queue_capacity: 16;
34//!         event_workers: 1;
35//!         modules: [greeting];
36//!     }
37//! }
38//!
39//! # async fn run() -> Result<()> {
40//! assert_eq!(AppMediator::new().send(Greet).await?, "hello");
41//! # Ok(())
42//! # }
43//! ```
44//!
45//! Commands default to a `()` response and [`core::convert::Infallible`] error.
46//! Set `return_type` and `error_type` in `#[medi_command(...)]` for other
47//! response and application-error types.
48//!
49//! # Resources and events
50//!
51//! Resources are ordinary `Clone` values listed in a module's `resources`
52//! section. They are passed to the generated `new` constructor and injected as
53//! handler parameters before the message. Events are ordinary `Clone + Send +
54//! 'static` values listed in an `events` section. To process events, retain the
55//! mediator in `'static` storage and call its generated `start` method before
56//! calling `publish`.
57//!
58//! Enable one runtime feature for event processing: `tokio`, `wasm`, or
59//! `embassy`. The features are mutually exclusive. Command-only mediators do
60//! not need a runtime feature.
61
62#[cfg(test)]
63extern crate alloc;
64
65/// Embassy executor re-export used by the generated Embassy integration.
66#[cfg(feature = "embassy")]
67#[doc(hidden)]
68pub use embassy_executor;
69
70/// Compose module-owned mediator manifests into one application mediator.
71///
72/// Each manifest is declared by [`medi_rs_macros::medi_module!`]. The explicit
73/// list is the application's routing boundary: only listed modules participate
74/// in the generated mediator. Optionally, `decorators: [logging]` applies each
75/// listed decorator function to every command and event handler route.
76/// `event_failure_reporter: reporter;` observes failed event handlers without
77/// changing delivery to the remaining handlers.
78#[macro_export]
79macro_rules! mediator {
80    (
81        $vis:vis struct $name:ident {
82            event_queue_capacity: $capacity:expr;
83            event_workers: $workers:expr;
84            modules: [$first:ident $(, $rest:ident)* $(,)?];
85            $(decorators: [$($decorators:path),* $(,)?];)?
86            $(event_failure_reporter: $reporter:ty;)?
87        }
88    ) => {
89        $first!($crate::__medi_rs_collect_modules, {
90            $vis struct $name;
91            event_queue_capacity: $capacity;
92            event_workers: $workers;
93            modules: [];
94            decorators: [$($($decorators),*)?];
95            event_failure_reporter: [$($reporter)?];
96            count: [];
97            remaining: [$($rest),*];
98        });
99    };
100}
101
102/// Internal continuation used by [`mediator!`].
103#[doc(hidden)]
104#[macro_export]
105macro_rules! __medi_rs_collect_modules {
106    (
107        $vis:vis struct $name:ident;
108        event_queue_capacity: $capacity:expr;
109        event_workers: $workers:expr;
110        modules: [$($modules:tt)*];
111        decorators: [$($decorators:path),*];
112        event_failure_reporter: [$($reporter:ty)?];
113        count: [$($count:tt)*];
114        remaining: [];
115    ) => {
116        $crate::__medi_rs_finalize_composition! {
117            $vis struct $name;
118            event_queue_capacity: $capacity;
119            event_workers: $workers;
120            modules: [$($modules)*];
121            decorators: [$($decorators),*];
122            event_failure_reporter: [$($reporter)?];
123            count: [$($count)*];
124        }
125    };
126    (
127        $vis:vis struct $name:ident;
128        event_queue_capacity: $capacity:expr;
129        event_workers: $workers:expr;
130        modules: [$($modules:tt)*];
131        decorators: [$($decorators:path),*];
132        event_failure_reporter: [$($reporter:ty)?];
133        count: [$($count:tt)*];
134        remaining: [$next:ident $(, $rest:ident)*];
135    ) => {
136        $next!($crate::__medi_rs_collect_modules, {
137            $vis struct $name;
138            event_queue_capacity: $capacity;
139            event_workers: $workers;
140            modules: [$($modules)*];
141            decorators: [$($decorators),*];
142            event_failure_reporter: [$($reporter)?];
143            count: [$($count)*];
144            remaining: [$($rest),*];
145        });
146    };
147}
148
149#[cfg(any(
150    all(feature = "tokio", feature = "wasm"),
151    all(feature = "tokio", feature = "embassy"),
152    all(feature = "wasm", feature = "embassy")
153))]
154compile_error!("features `tokio`, `wasm`, and `embassy` are mutually exclusive; enable at most one runtime adapter");
155
156pub mod adapters;
157mod bus;
158mod error;
159mod event;
160mod resource;
161/// Internal typed-tuple primitives used by generated mediator code.
162#[doc(hidden)]
163pub mod tlist;
164
165// flatten the module structure
166pub use adapters::lifecycle::Lifecycle;
167pub use adapters::queue::EventQueue;
168pub use adapters::shutdown::ShutdownSignal;
169pub use error::*;
170
171pub use medi_rs_macros::{__medi_rs_finalize_composition, MediCommand, medi_handler, medi_module, medi_task};
172
173/// Metadata about a failed asynchronous event-handler invocation.
174///
175/// Event handlers may use unrelated error types, so this deliberately contains
176/// route metadata rather than the handler's concrete error value. Reporters
177/// can use it for logging, metrics, and alerting without imposing a common
178/// application error type on every event route.
179#[derive(Clone, Copy, Debug, Eq, PartialEq)]
180pub struct EventHandlerFailure {
181    event_name: &'static str,
182    handler_name: &'static str,
183}
184
185impl EventHandlerFailure {
186    /// Create failure metadata for an event route and handler.
187    #[doc(hidden)]
188    pub const fn new(event_name: &'static str, handler_name: &'static str) -> Self {
189        Self {
190            event_name,
191            handler_name,
192        }
193    }
194
195    /// The registered event type as written in its module manifest.
196    pub const fn event_name(self) -> &'static str {
197        self.event_name
198    }
199
200    /// The registered handler path as written in its module manifest.
201    pub const fn handler_name(self) -> &'static str {
202        self.handler_name
203    }
204}
205
206/// Observes failures from generated asynchronous event-handler dispatch.
207///
208/// A reporter is configured with `event_failure_reporter:` in [`mediator!`].
209/// It is awaited once for each failed handler, but reporting never prevents
210/// dispatch to later handlers. The returned future must be `Send` so the same
211/// reporter works with Tokio workers; `async fn` implementations satisfy this
212/// when their future is `Send`.
213pub trait EventFailureReporter: Send + Sync + 'static {
214    /// Observe one failed event-handler invocation.
215    fn report(&self, failure: EventHandlerFailure) -> impl core::future::Future<Output = ()> + Send;
216}
217
218/// Continuation supplied to a function decorator.
219///
220/// Decorators call [`Self::call`] to forward a command to the next decorator
221/// or the handler. The blanket implementation means an ordinary `FnOnce`
222/// closure generated by [`medi_handler`] implements this trait automatically.
223pub trait DecoratorNext<C>: Send {
224    /// Response returned by the remaining decorator pipeline or handler.
225    type Response: Send;
226    /// Error returned by the remaining decorator pipeline or handler.
227    type Error: Send;
228
229    /// Forward `command` through the remaining pipeline.
230    fn call(
231        self,
232        command: C,
233    ) -> impl core::future::Future<Output = core::result::Result<Self::Response, Self::Error>> + Send;
234}
235
236impl<C, F, Fut, Response, Error> DecoratorNext<C> for F
237where
238    F: FnOnce(C) -> Fut + Send,
239    Fut: core::future::Future<Output = core::result::Result<Response, Error>> + Send,
240    Response: Send,
241    Error: Send,
242{
243    type Response = Response;
244    type Error = Error;
245
246    fn call(self, command: C) -> impl core::future::Future<Output = core::result::Result<Response, Error>> + Send {
247        self(command)
248    }
249}
250
251/// Command metadata used by generated mediators.
252///
253/// Most users should derive this with `#[derive(MediCommand)]` from the
254/// `medi-rs-macros` crate.
255pub trait Command
256where
257    Self: Send + Sync + 'static,
258{
259    /// Response type returned by the command handler.
260    type Response: Send + Sync + 'static;
261
262    /// Concrete application error returned by the command handler.
263    ///
264    /// [`MediCommand`] uses [`core::convert::Infallible`] when its `error_type`
265    /// attribute is omitted.
266    type Error: Send;
267}
268
269/// Static route generated for a command and a concrete mediator type.
270///
271/// This is implemented by `mediator!`; applications call the generated
272/// mediator's inherent `send` method instead of implementing it directly.
273#[doc(hidden)]
274pub trait StaticSendCommand<M>: Command + Sized {
275    /// Invoke this command's generated route.
276    fn send(
277        self,
278        mediator: &M,
279    ) -> impl core::future::Future<Output = core::result::Result<Self::Response, Self::Error>> + Send;
280}
281
282/// Static event route generated for an event and a concrete mediator type.
283#[doc(hidden)]
284pub trait StaticPublish<M>: Sized {
285    /// Enqueue this event for the generated worker.
286    fn publish(self, mediator: &M) -> impl core::future::Future<Output = Result<()>> + Send;
287}
288
289/// Static non-blocking event route generated for an event and a mediator type.
290#[doc(hidden)]
291pub trait StaticTryPublish<M>: Sized {
292    /// Attempt to enqueue this event without waiting for queue capacity.
293    fn try_publish(self, mediator: &M) -> core::result::Result<(), TryPublishError<Self>>;
294}