Improve the dispatching explanation (docs)

Former-commit-id: 9ab3b3a1c5
This commit is contained in:
Hirrolot 2022-07-20 17:44:39 +06:00
parent fb5b64bff9
commit d919c99b69

View file

@ -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<State, InMemStorage<State>>;
//! type HandlerResult = Result<(), Box<dyn std::error::Error + Send + Sync>>;
//! 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::<State>::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<Box<dyn std::error::Error + Send + Sync + 'static>> {
//! let command_handler = teloxide::filter_command::<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<Bot>, 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<Bot>` 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::<State>::new()])
//! .build()
//! .setup_ctrlc_handler()
//! .dispatch()
//! .await;
//! # }
//! Dispatcher::builder(bot, schema())
//! .dependencies(dptree::deps![InMemStorage::<State>::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