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 handler;
161mod resource;
162/// Internal typed-tuple primitives used by generated mediator code.
163#[doc(hidden)]
164pub mod tlist;
165
166// flatten the module structure
167pub use adapters::lifecycle::Lifecycle;
168pub use adapters::queue::EventQueue;
169pub use adapters::shutdown::ShutdownSignal;
170pub use error::*;
171pub use handler::*;
172
173pub use medi_rs_macros::{__medi_rs_finalize_composition, MediCommand, medi_handler, medi_module, medi_task};
174
175/// Metadata about a failed asynchronous event-handler invocation.
176///
177/// Event handlers may use unrelated error types, so this deliberately contains
178/// route metadata rather than the handler's concrete error value. Reporters
179/// can use it for logging, metrics, and alerting without imposing a common
180/// application error type on every event route.
181#[derive(Clone, Copy, Debug, Eq, PartialEq)]
182pub struct EventHandlerFailure {
183 event_name: &'static str,
184 handler_name: &'static str,
185}
186
187impl EventHandlerFailure {
188 /// Create failure metadata for an event route and handler.
189 #[doc(hidden)]
190 pub const fn new(event_name: &'static str, handler_name: &'static str) -> Self {
191 Self {
192 event_name,
193 handler_name,
194 }
195 }
196
197 /// The registered event type as written in its module manifest.
198 pub const fn event_name(self) -> &'static str {
199 self.event_name
200 }
201
202 /// The registered handler path as written in its module manifest.
203 pub const fn handler_name(self) -> &'static str {
204 self.handler_name
205 }
206}
207
208/// Observes failures from generated asynchronous event-handler dispatch.
209///
210/// A reporter is configured with `event_failure_reporter:` in [`mediator!`].
211/// It is awaited once for each failed handler, but reporting never prevents
212/// dispatch to later handlers. The returned future must be `Send` so the same
213/// reporter works with Tokio workers; `async fn` implementations satisfy this
214/// when their future is `Send`.
215pub trait EventFailureReporter: Send + Sync + 'static {
216 /// Observe one failed event-handler invocation.
217 fn report(&self, failure: EventHandlerFailure) -> impl core::future::Future<Output = ()> + Send;
218}
219
220/// Continuation supplied to a function decorator.
221///
222/// Decorators call [`Self::call`] to forward a command to the next decorator
223/// or the handler. The blanket implementation means an ordinary `FnOnce`
224/// closure generated by [`medi_handler`] implements this trait automatically.
225pub trait DecoratorNext<C>: Send {
226 /// Response returned by the remaining decorator pipeline or handler.
227 type Response: Send;
228 /// Error returned by the remaining decorator pipeline or handler.
229 type Error: Send;
230
231 /// Forward `command` through the remaining pipeline.
232 fn call(
233 self,
234 command: C,
235 ) -> impl core::future::Future<Output = core::result::Result<Self::Response, Self::Error>> + Send;
236}
237
238impl<C, F, Fut, Response, Error> DecoratorNext<C> for F
239where
240 F: FnOnce(C) -> Fut + Send,
241 Fut: core::future::Future<Output = core::result::Result<Response, Error>> + Send,
242 Response: Send,
243 Error: Send,
244{
245 type Response = Response;
246 type Error = Error;
247
248 fn call(self, command: C) -> impl core::future::Future<Output = core::result::Result<Response, Error>> + Send {
249 self(command)
250 }
251}
252
253/// Command metadata used by generated mediators.
254///
255/// Most users should derive this with `#[derive(MediCommand)]` from the
256/// `medi-rs-macros` crate.
257pub trait Command
258where
259 Self: Send + Sync + 'static,
260{
261 /// Response type returned by the command handler.
262 type Response: Send + Sync + 'static;
263}
264
265/// Static-dispatch metadata for a command.
266///
267/// [`MediCommand`] derives this trait automatically. When its `error_type`
268/// attribute is omitted, [`core::convert::Infallible`] is used.
269pub trait StaticCommand: Command {
270 /// Concrete application error returned by this command's handler.
271 type Error: Send;
272}
273
274/// Static route generated for a command and a concrete mediator type.
275///
276/// This is implemented by `mediator!`; applications call the generated
277/// mediator's inherent `send` method instead of implementing it directly.
278#[doc(hidden)]
279pub trait StaticSendCommand<M>: Sized {
280 /// Value returned by the command handler.
281 type Response;
282
283 /// Concrete error returned by the command handler.
284 type Error;
285
286 /// Invoke this command's generated route.
287 fn send(
288 self,
289 mediator: &M,
290 ) -> impl core::future::Future<Output = core::result::Result<Self::Response, Self::Error>> + Send;
291}
292
293/// Static event route generated for an event and a concrete mediator type.
294#[doc(hidden)]
295pub trait StaticPublish<M>: Sized {
296 /// Enqueue this event for the generated worker.
297 fn publish(self, mediator: &M) -> impl core::future::Future<Output = Result<()>> + Send;
298}
299
300/// Static non-blocking event route generated for an event and a mediator type.
301#[doc(hidden)]
302pub trait StaticTryPublish<M>: Sized {
303 /// Attempt to enqueue this event without waiting for queue capacity.
304 fn try_publish(self, mediator: &M) -> core::result::Result<(), TryPublishError<Self>>;
305}
306
307//-- region: Implement static handler traits
308crate::impl_static_handler!();
309crate::impl_static_handler!(T1: I1);
310crate::impl_static_handler!(T1: I1, T2: I2);
311crate::impl_static_handler!(T1: I1, T2: I2, T3: I3);
312crate::impl_static_handler!(T1: I1, T2: I2, T3: I3, T4: I4);
313crate::impl_static_handler!(T1: I1, T2: I2, T3: I3, T4: I4, T5: I5);
314crate::impl_static_handler!(T1: I1, T2: I2, T3: I3, T4: I4, T5: I5, T6: I6);
315crate::impl_static_handler!(T1: I1, T2: I2, T3: I3, T4: I4, T5: I5, T6: I6, T7: I7);
316//-- endregion: Implement the handler traits