mirror of
https://github.com/teloxide/teloxide.git
synced 2024-12-22 22:46:39 +01:00
Improve the dispatching explanation (docs)
Former-commit-id: 9ab3b3a1c5
This commit is contained in:
parent
fb5b64bff9
commit
d919c99b69
1 changed files with 62 additions and 68 deletions
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue