# dptree [![Rust](https://github.com/p0lunin/dptree/actions/workflows/rust.yml/badge.svg)](https://github.com/p0lunin/dptree/actions/workflows/rust.yml) [![Crates.io](https://img.shields.io/crates/v/dptree.svg)](https://crates.io/crates/dptree) [![Docs.rs](https://docs.rs/dptree/badge.svg)](https://docs.rs/dptree) An implementation of the [chain (tree) of responsibility] pattern. [[`examples/web_server.rs`](examples/web_server.rs)] ```rust 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::().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. [continuation-passing style (CPS)]: https://en.wikipedia.org/wiki/Continuation-passing_style [Dependency injection (DI)]: https://en.wikipedia.org/wiki/Dependency_injection [teloxide]: https://github.com/teloxide/teloxide [futures]: https://github.com/rust-lang/futures-rs ## 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())`: return `404 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](#di), 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. [chain (tree) of responsibility]: https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern ## 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"](https://docs.rs/dptree/latest/dptree/struct.Handler.html#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. [frunk]: https://github.com/lloydmeta/frunk [@p0lunin]: https://github.com/p0lunin [@Hirrolot]: https://github.com/Hirrolot ## 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`.