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}