From 9ab3b3a1c5ab6ebbfce82b027cf50d2f961d87ba Mon Sep 17 00:00:00 2001 From: Hirrolot Date: Wed, 20 Jul 2022 17:44:39 +0600 Subject: [PATCH] Improve the dispatching explanation (docs) --- src/dispatching.rs | 130 +++++++++++++++++++++------------------------ 1 file changed, 62 insertions(+), 68 deletions(-) diff --git a/src/dispatching.rs b/src/dispatching.rs index e35d6b9d..ce939b28 100644 --- a/src/dispatching.rs +++ b/src/dispatching.rs @@ -4,31 +4,23 @@ //! [chain of responsibility] pattern enriched with a number of combinator //! functions, which together form an instance of the [`dptree::Handler`] type. //! -//! Let us look at this simple example: -//! -//! [[`examples/purchase.rs`](https://github.com/teloxide/teloxide/blob/master/examples/purchase.rs)] -//! ```no_run -//! // Imports omitted... -//! # use teloxide::{ -//! # dispatching::{dialogue::InMemStorage, UpdateHandler}, -//! # prelude::*, -//! # types::{InlineKeyboardButton, InlineKeyboardMarkup}, -//! # utils::command::BotCommands, -//! # }; -//! -//! type MyDialogue = Dialogue>; -//! type HandlerResult = Result<(), Box>; +//! Take [`examples/purchase.rs`] as an example of dispatching logic. First, we +//! define a type named `State` to represent the current state of a dialogue: //! +//! ```ignore //! #[derive(Clone, Default)] //! pub enum State { //! #[default] //! Start, //! ReceiveFullName, -//! ReceiveProductChoice { -//! full_name: String, -//! }, +//! ReceiveProductChoice { full_name: String }, //! } +//! ``` //! +//! Then, we define a type `Command` to represent user commands such as +//! `/start` or `/help`: +//! +//! ```ignore //! #[derive(BotCommands, Clone)] //! #[command(rename = "lowercase", description = "These commands are supported:")] //! enum Command { @@ -39,23 +31,15 @@ //! #[command(description = "cancel the purchase procedure.")] //! Cancel, //! } +//! ``` //! -//! #[tokio::main] -//! async fn main() { -//! // Setup is omitted... -//! # pretty_env_logger::init(); -//! # log::info!("Starting purchase bot..."); -//! # -//! # let bot = Bot::from_env().auto_send(); -//! # -//! # Dispatcher::builder(bot, schema()) -//! # .dependencies(dptree::deps![InMemStorage::::new()]) -//! # .build() -//! # .setup_ctrlc_handler() -//! # .dispatch() -//! # .await; -//! } +//! Now the key question: how to elegantly dispatch on different combinations of +//! `State`, `Command`, and Telegram updates? -- i.e., we may want to execute +//! specific endpoints only in response to specific user commands and while we +//! are in a given dialogue state (and possibly under other circumstances!). The +//! solution is to use [`dptree`]: //! +//! ```ignore //! fn schema() -> UpdateHandler> { //! let command_handler = teloxide::filter_command::() //! .branch( @@ -79,7 +63,30 @@ //! .branch(message_handler) //! .branch(callback_query_handler) //! } +//! ``` //! +//! The overall logic should be clear. Throughout the above example, we use +//! several techniques: +//! +//! - **Branching:** `a.branch(b)` roughly means "try to handle an update with +//! `a`, then, if it +//! neglects the update, try `b`". +//! - **Pattern matching:** We also use the [`dptree::case!`] macro +//! extensively, which acts as a filter on an enumeration: if it is of a +//! certain variant, it passes the variant's payload down the handler chain; +//! otherwise, it neglects an update. +//! - **Endpoints:** To specify the final function to handle an update, we use +//! [`dptree::Handler::endpoint`]. +//! +//! Notice the clear and uniform code structure: regardless of the dispatch +//! criteria, we use the same program constructions. In future, you may want to +//! introduce your application-specific filters or data structures to match upon +//! -- no problem, reuse [`dptree::Handler::filter`], [`dptree::case!`], and +//! other combinators in the same way! +//! +//! Finally, we define our endpoints like this: +//! +//! ```ignore //! // Handler definitions omitted... //! //! async fn start(bot: AutoSend, msg: Message, dialogue: MyDialogue) -> HandlerResult { @@ -116,55 +123,42 @@ //! } //! ``` //! -//! The above code shows how to dispatch on different combinations of a state -//! and command _elegantly_. We give a top-bottom explanation of the function -//! `schema`, which constructs the main update handler: +//! Each parameter is supplied as a dependency by teloxide. In particular: +//! - `bot: AutoSend` comes from the dispatcher (see below); +//! - `msg: Message` comes from [`Update::filter_message`]; +//! - `q: CallbackQuery` comes from [`Update::filter_callback_query`]; +//! - `dialogue: MyDialogue` comes from [`dialogue::enter`]; +//! - `full_name: String` comes from `dptree::case![State::ReceiveProductChoice +//! { full_name }]`. //! -//! - We call the [`dialogue::enter`] function to initiate dialogue -//! interaction. Then we call [`dptree::Handler::branch`] two times to form a -//! tree of responsibility of `message_handler` and `callback_query_handler`. -//! - Inside `message_handler`, we use [`Update::filter_message`] as a filter -//! for incoming messages. Then we create a tree of responsibility again, -//! consisting of three branches with a similar structure. -//! - Inside `callback_query_handler`, we use -//! [`Update::filter_callback_query`] as a filter and create one branch for -//! handling product selection. -//! -//! `a.branch(b)` roughly means "try to handle an update with `a`, then, if it -//! fails, try `b`". We use branching multiple times here, which is a natural -//! pattern for describing dispatching logic. We also use the [`dptree::case!`] -//! macro extensively, which acts as a filter on an enumeration: if it is of a -//! certain variant, it passes the variant's payload down the handler chain; -//! otherwise, it neglects an update. Note how we utilise this macro both for -//! `State` and `Command` in the same way! -//! -//! Finally, we plug the schema into [`Dispatcher`] like this: +//! Inside `main`, we plug the schema into [`Dispatcher`] like this: //! //! ```ignore -//! # #[tokio::main] -//! # async fn main() { -//! let bot = Bot::from_env().auto_send(); +//! #[tokio::main] +//! async fn main() { +//! let bot = Bot::from_env().auto_send(); //! -//! Dispatcher::builder(bot, schema()) -//! .dependencies(dptree::deps![InMemStorage::::new()]) -//! .build() -//! .setup_ctrlc_handler() -//! .dispatch() -//! .await; -//! # } +//! Dispatcher::builder(bot, schema()) +//! .dependencies(dptree::deps![InMemStorage::::new()]) +//! .build() +//! .setup_ctrlc_handler() +//! .dispatch() +//! .await; +//! } //! ``` //! //! In a call to [`DispatcherBuilder::dependencies`], we specify a list of -//! dependencies that all handlers will receive as parameters. Here, we only -//! specify an in-memory storage of dialogues needed for [`dialogue::enter`]. -//! However, in production bots, you normally also pass a database connection, -//! configuration, and other stuff. +//! additional dependencies that all handlers will receive as parameters. Here, +//! we only specify an in-memory storage of dialogues needed for +//! [`dialogue::enter`]. However, in production bots, you normally also pass a +//! database connection, configuration, and other stuff. //! //! All in all, [`dptree`] can be seen as an extensible alternative to pattern //! matching, with support for [dependency injection (DI)] and a few other //! useful features. See [`examples/dispatching_features.rs`] as a more involved //! example. //! +//! [`examples/purchase.rs`]: https://github.com/teloxide/teloxide/blob/master/examples/purchase.rs //! [`Update::filter_message`]: crate::types::Update::filter_message //! [`Update::filter_callback_query`]: crate::types::Update::filter_callback_query //! [chain of responsibility]: https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern