medi-rs
medi-rs is a static async mediator for Rust. Applications declare commands, events, resources, and handlers in feature-local modules; mediator! then combines those manifests into one concrete mediator. Command dispatch and resource injection are generated at compile time—there is no runtime handler registry or type-based resource lookup.
Documentation
Runtime features
Choose one runtime adapter for event processing:
| Feature | Runtime | Notes |
|---|---|---|
tokio |
Tokio | Hosted applications and the runnable Tokio examples. |
wasm |
wasm_bindgen_futures |
WebAssembly event workers use spawn_local. |
embassy |
Embassy | no_std embedded applications; queue capacity must be a const expression. |
The runtime features are mutually exclusive. Command-only mediators need no runtime feature. Events and runtime tasks require exactly one adapter feature.
[]
= { = "1", = ["tokio"] }
Event configuration
Event mediators use a bounded queue. event_queue_capacity and event_workers must both be greater than zero; publishing waits while the queue is full. Tokio and WebAssembly accept any usize expression for the capacity. Embassy uses the capacity as a const generic, so its value must be a const expression such as a literal or named const.
Quick start
A command derives MediCommand; its handler is marked with #[medi_handler]. A medi_module! manifest declares the route, and mediator! creates the application mediator.
use ;
async
medi_module!
mediator!
async
MediCommand defaults to a () response and core::convert::Infallible error. Specify return_type and error_type when the handler returns other types.
Static-dispatch pattern
The registration graph is fixed where mediator! is expanded:
- Define a message type and derive
MediCommandfor commands. - Mark each async handler with
#[medi_handler]; its final parameter is the command or event it receives. - In each feature-local module, use
medi_module!to list its resources and routes. - At the application boundary, select those manifests with
mediator!.
mediator! generates one concrete mediator type. For every command route it implements a route for that command type and mediator type; send therefore calls the selected handler directly. Resources live in a typed nested tuple and are cloned into handler parameters by their compile-time tuple position. There is no runtime TypeId lookup, boxed handler registry, or handler selection at runtime. A command or resource registered twice is rejected while expanding the composition, and requesting an undeclared handler resource fails type checking.
Macro reference
#[derive(MediCommand)] and #[medi_command(...)]
Derive MediCommand on a command or query type. It implements Command, which supplies the response type, and StaticCommand, which supplies the handler error type:
Both options are optional: return_type defaults to () and error_type defaults to core::convert::Infallible. return_type and error_type accept Rust type syntax, so application-specific result and error types work without conversion to a framework error.
#[medi_handler]
Apply this attribute to an async function. It retains the function and creates a typed, crate-visible internal invoker used by generated routes, so the handler function itself can remain private to its feature module. The last parameter is always the message. Value parameters before it are resources, which must be listed in the composed manifest and implement Clone. Optionally, the first parameter can be &AppMediator so a handler can send another command or publish an event.
async
The handler's return type must match the command metadata for a command route. For event routes it should return Result<()>; event-handler errors are ignored after the handler completes. To wrap an invocation with cross-cutting behavior, define middleware functions that receive the command and a next continuation, then list them in declaration order:
use DecoratorNext;
async
async
next is the remaining decorator pipeline and handler. Calling next.call(command).await forwards the command; a decorator can reject or modify the command before forwarding it, and can run behavior after it returns. The generated continuation is inferred automatically; only the DecoratorNext<Command> parameter type must be declared.
To apply a decorator to every command and event handler in one mediator, add it to the mediator composition:
mediator!
medi_module!
Declare a reusable, feature-local manifest. It contains zero or more resources, commands, and events sections in any order. With a runtime feature, manifests may additionally contain a tasks section. Commands have one handler; events have one or more handlers. The macro creates the named manifest for inclusion by mediator!.
medi_module!
Use semicolons between resource and command entries, and commas between event handlers. When a handler is private in a feature module, use its crate-qualified path (for example, crate::users::create_user) in the manifest; the generated invoker remains crate-visible while the handler stays private. The manifest contains declarations only: it does not construct a mediator or register anything dynamically.
mediator!
Compose one or more manifests into the concrete application mediator. Its explicit modules list is the routing boundary and its order determines the order of resource arguments accepted by new.
mediator!
The generated type has new, send, and, when an event route exists, publish and start. It uses the runtime selected by the enabled tokio, wasm, or embassy feature. The event configuration rules are described in Event configuration. start requires 'static mediator storage (start(spawner) for Embassy).
mediator_composition_marker! is also exported for internal macro expansion. It is not an application-facing API; use mediator! instead.
Resources
Resources are ordinary Clone values. List each resource in a module manifest, pass the values to the generated mediator constructor in declaration order, and request them as handler parameters before the command or event.
use ;
;
;
async
medi_module!
mediator!
async
A missing or duplicate resource is a compile-time error. Resource derive macros are not required.
Request-scoped data
Resources are fixed when the mediator is constructed; send accepts only the command, so it cannot inject a resource for one call. Model request-scoped dependencies such as authentication, tenant information, and correlation IDs as command data instead. A command is moved into send and then into its handler, so its fields are not cloned during normal command dispatch.
async
Use Option<T> in resources { ... } only when a dependency is optional for the lifetime of the mediator. For request-scoped data, keep the command field small (or use Arc inside it when sharing larger data is necessary).
Runtime tasks
With a runtime feature, #[medi_task] creates a task with the same typed resource injection as a handler. It may take &AppMediator as its first parameter when it needs mediator access; remaining value parameters are declared resources. Register it in a tasks section. mediator.start(spawner) starts tasks on Embassy; mediator.start() does so on Tokio and Wasm.
async
medi_module!
Events
Events are plain Clone + Send + 'static values. List each event route in a module manifest, create a 'static mediator, and call start before publishing. Each generated worker dispatches an event to every registered handler. publish waits when the configured bounded queue is full. Event handler errors are currently ignored after dispatch.
use ;
;
async
medi_module!
mediator!
async
For Embassy, initialize the mediator in a StaticCell and call mediator.start(spawner). The Embassy integration requires embassy-executor 0.10 (with the platform feature appropriate for the target, such as platform-cortex-m). See the micro:bit example below.
Examples
Development
The repository uses mise for its Rust toolchain and commands: