dptree
An implementation of the chain (tree) of responsibility pattern.
use dptree::prelude::*;
type WebHandler = Endpoint<'static, DependencyMap, String>;
#[rustfmt::skip]
#[tokio::main]
async fn main() {
let web_server = dptree::entry()
.branch(smiles_handler())
.branch(sqrt_handler())
.branch(not_found_handler());
assert_eq!(
web_server.dispatch(dptree::deps!["/smile"]).await,
ControlFlow::Break("π".to_owned())
);
assert_eq!(
web_server.dispatch(dptree::deps!["/sqrt 16"]).await,
ControlFlow::Break("4".to_owned())
);
assert_eq!(
web_server.dispatch(dptree::deps!["/lol"]).await,
ControlFlow::Break("404 Not Found".to_owned())
);
}
fn smiles_handler() -> WebHandler {
dptree::filter(|req: &'static str| req.starts_with("/smile"))
.endpoint(|| async { "π".to_owned() })
}
fn sqrt_handler() -> WebHandler {
dptree::filter_map(|req: &'static str| {
if req.starts_with("/sqrt") {
let (_, n) = req.split_once(' ')?;
n.parse::<f64>().ok()
} else {
None
}
})
.endpoint(|n: f64| async move { format!("{}", n.sqrt()) })
}
fn not_found_handler() -> WebHandler {
dptree::endpoint(|| async { "404 Not Found".to_owned() })
}
Features
- βοΈ Declarative handlers:
dptree::{endpoint, filter, filter_map, ...}
. - βοΈ A lightweight functional design using a form of continuation-passing style (CPS) internally.
- βοΈ Dependency injection (DI) out-of-the-box.
- βοΈ Supports both handler chaining and branching operations.
- βοΈ Handler introspection facilities.
- βοΈ Battle-tested: dptree is used in teloxide as a framework for Telegram update dispatching.
- βοΈ Runtime-agnostic: uses only the futures crate.
Explanation
The above code is a simple web server dispatching tree. In pseudocode, it would look like this:
dptree::entry()
: dispatch an update to the following branch handlers:.branch(smiles_handler())
: if the update satisfies the condition (dptree::filter
), return a smile (.endpoint
). Otherwise, pass the update forwards..branch(sqrt_handler())
: if the update is a number (dptree::filter_map
), return the square of it. Otherwise, pass the update forwards..branch(not_found_handler())
: return404 Not Found
immediately.
Control flow: as you can see, we have just described a dispatching scheme consisting of three branches. First, dptree enters the first handler smiles_handler
, then, if it fails to process an update, it passes the update to sqrt_handler
and so on. If nobody have succeeded in handling an update, the control flow enters not_found_handler
that returns the error. In other words, the result of the whole .dispatch
call would be the result of the first handler that succeeded to handle an incoming update.
Dependency injection: instead of passing straightforward values to .dispatch
, we use the dptree::deps!
macro. It accepts a sequence of values and constructs DependencyMap
out of them. The handlers request values of certain types in their signatures (such as |req: &'static str|
), and dptree automatically injects the values from dptree::deps!
into these functions. If it fails to obtain values of requested types, it will panic at run-time, so be careful and always test your code before pushing to production.
Using dptree, you can specify arbitrary complex dispatching schemes using the same recurring patterns you have seen above.
Pitfalls
DependencyMap
can panic at run-time if a non-existing dependency is requested. Always test your code and ensure that all dependencies are specified before they are being requested..branch
and.chain
are different operations. See "The difference between chaining and branching".
Design choices
Functional
We decided to use a continuation-passing style (CPS) internally and expose neat handler patterns to library users. This is contrary to what you might have seen in typical object-oriented models of the chain of responsibility pattern. In fact, we have first tried to make a typical OO design, but then resorted to FP because of its simplicity. With this design, each handler accepts a continuation representing the rest of the handlers in the chain; the handler can either call this continuation or not. Using this simple model, we can express pretty much any handler pattern like filter
and filter_map
, using only functions and nothing else. You do not need complex programming machinery such as abstract factories, builders, etc.
DI
In Rust, it is possible to have a type-safe DI container instead of DependencyMap
that panics at run-time. However, this would require complex type-level manipulations (like those in the frunk library). @p0lunin and I (@Hirrolot) decided not to trade comprehensible error messages for compile-time safety, since we had a plenty of experience that the uninitiated users simply cannot understand what is wrong with their code, owing to the utterly inadequate diagnostic messages from rustc.
Troubleshooting
the trait bound [closure@examples/state_machine.rs:150:20: 150:92]: Injectable<_, bool, _> is not satisfied
This error means that your handler does not implement the Injectable
trait. Ensure that your update type implements Clone
. If it is too expensive to clone every single update, you can wrap it into Arc
.