Expand description
noon
Strongly-typed, compile-time mediator.
This crate allows you to use the mediator pattern in Rust.
Unlike mediator implementations in most other languages, noon
’s mediators are checked at compile time for correct registration.
The design is partially inspired by the MediatR library for .NET.
Concepts
Components interact with this library through the mediator::Mediate
trait.
Mediators can be used to implement different behaviours by exchanging typed messages between components through different kinds of receivers. There are two kinds of receiver:
Handlers are used to perform some behaviour when presented a message of type TMsg
and produce a response of type TResp
.
A mediator typically has a single handler for a given type of message.
Notification receivers are used to perform some behaviour when presented a message of type TMsg
without returning anything in response.
A mediator might have multiple notification receivers for a given type of message, all of which are called in sequence whenever a message of that type is presented to the mediator.
In noon, both handlers are notification receivers may be either synchronous or asynchronous.
The types of messages that can be presented to the mediator are part of the mediator’s type, including whether a type of message is able to be used to invoke a handler, send notifications, or both. Likewise, whether the receivers for a type of message are synchronous, asynchronous, or both is also tracked as part of the mediator’s type.
Conventions
In order to validate noon’s registration constraints, this crate’s public API has many generic parameters and constraint clauses on its signatures.
To help maintain legibility, a few conventions are used with the names of generic parameters to suggest how they should be interpreted and used.
TMsg
is the type of a message the mediator should accept.
TResp
is the type of a response produced by a handler for a message.
F
and Fut
are opaque, unique types for functions and futures respectively. These should generally be inferred by the compiler, not supplied by the user.
I
is a type-level index into the type-level list of registered message handlers or notification receivers. If your mediator only has at most a single receiver (or set of receivers in the case of notifications) for each registered message type and each sync/async kind, the compiler should automatically infer the correct type-level index for this parameter. If you find yourself in a position where you’ve only registered at most one kind of receiver for a message type and you have to manually provide the index, something is probably wrong.
Usage with generic constraints
If you’d like to use a generic mediator and place constraints on the kinds of receivers it must contain, you’re likely looking to require the hlist::ContainsAt<T, I>
trait on the mediator::Mediate
’s associated Handlers
or NotificationReceivers
associated types.
These type-level lists implement different traits with generics populated from the entry
module depending on the receivers that are registered with the mediator.
For example, a mediator with a synchronous handler accepting a NewUserRequest
and producing a NewUserResponse
would have an entry::RequestResponse<NewUserRequest,NewUserResponse>
in its associated Handlers
type-level list. Concretely, this means the associated Handlers
type implements hlist::ContainsAt<entry:RequestResponse<NewUserRequest,NewUserResponse>, I>
for some I
.
Example
You can create a mediator using a builder interface. The following creates a mediator without any receivers.
use noon::mediator::{Mediate, MediatorBuilder};
let mediator = MediatorBuilder::new()
.build();
You can register synchronous or asynchronous handlers
use noon::mediator::{Mediate, MediatorBuilder};
struct NewUserMessage { id: i32 }
struct SendUserEmail { id: i32, msg: String };
async fn foo() {
let mediator = MediatorBuilder::new()
.add_handler(|req: NewUserMessage| {
println!("Hi, user {}!", req.id);
req.id
})
.add_async_handler(|req: SendUserEmail| async move {
// call email service
true // return some response
})
.build();
// prints "Hi, user 5!"
let result = mediator.handle(NewUserMessage { id: 5 });
assert_eq!(result, 5);
let result2 = mediator.handle_async(SendUserEmail {
id: 5, msg: "Hi!".to_string()
}).await;
}
You can’t ask a mediator to handle a message it doesn’t have a receiver for.
use noon::mediator::{Mediate, MediatorBuilder}
struct NewUserMessage { id: i32 }
let mediator = MediatorBuilder::new()
.build();
// Compile-time error, this mediator has no handler for that message.
mediator.handle(NewUserMessage { id: 10 });
You can register multiple notification receivers for a single message type
use noon::mediator::{Mediate, MediatorBuilder};
#[derive(Debug)]
struct NewUserMessage { id: i32 }
let mediator = MediatorBuilder::new()
.listen_for::<NewUserMessage>()
.add_notification_receiver(|msg: &NewUserMessage| {
println!("New user {:?} created", msg);
})
.add_notification_receiver(|msg: &NewUserMessage| {
// get total users in the system
let total = 42;
println!("The system now contains {} users", total);
})
.build();
// prints both messages in sequence
mediator.notify(&NewUserMessage { id: 5 });