From bf114de249b918d9dfff840130f305bc2c0cc677 Mon Sep 17 00:00:00 2001 From: Temirkhan Myrzamadi Date: Sat, 25 Jul 2020 22:37:58 +0600 Subject: [PATCH] A nicer approach to manage dialogues via #[derive(Transition)] + #[teloxide(transition)] --- CHANGELOG.md | 4 +- README.md | 35 +++++------- examples/dialogue_bot/src/main.rs | 23 ++++---- examples/dialogue_bot/src/states.rs | 21 +------- examples/dialogue_bot/src/transitions.rs | 20 ++++--- examples/redis_remember_bot/src/main.rs | 2 +- examples/redis_remember_bot/src/states.rs | 9 +--- .../redis_remember_bot/src/transitions.rs | 7 ++- src/dispatching/dialogue/bot_dialogue.rs | 11 ---- src/dispatching/dialogue/mod.rs | 54 +++++++++---------- src/dispatching/dialogue/transition.rs | 44 +++++++++++++++ src/prelude.rs | 4 +- 12 files changed, 120 insertions(+), 114 deletions(-) delete mode 100644 src/dispatching/dialogue/bot_dialogue.rs create mode 100644 src/dispatching/dialogue/transition.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0087853c..7530cbca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.3.0] - ??? ### Added - `BotBuilder`, which allows setting a default `ParseMode`. - - The `BotDialogue` trait. - - Automatic `dispatch` function generation via `#[derive(BotDialogue)]` + `#[transition(transition_fn)]`. + - The `Transition`, `SubTransition`, `SubTransitionOutputType` traits. + - A nicer approach to manage dialogues via `#[derive(Transition)]` + `#[teloxide(transition)]`. ### Deprecated - `Bot::{from_env_with_client, new, with_client}`. diff --git a/README.md b/README.md index 2b6456f0..e8a6d82f 100644 --- a/README.md +++ b/README.md @@ -195,19 +195,12 @@ States and transition functions are placed into separated modules. For example, ```rust // Imports are omitted... -#[derive(BotDialogue, SmartDefault, From)] +#[derive(Transition, SmartDefault, From)] pub enum Dialogue { #[default] - #[transition(start)] Start(StartState), - - #[transition(receive_days_of_week)] ReceiveDaysOfWeek(ReceiveDaysOfWeekState), - - #[transition(receive_10x5_answer)] Receive10x5Answer(Receive10x5AnswerState), - - #[transition(receive_gandalf_alternative_name)] ReceiveGandalfAlternativeName(ReceiveGandalfAlternativeNameState), } @@ -228,16 +221,10 @@ pub struct ReceiveGandalfAlternativeNameState { _10x5_answer: u8, } -pub struct ExitState { - rest: ReceiveGandalfAlternativeNameState, - gandalf_alternative_name: String, -} - up!( StartState -> ReceiveDaysOfWeekState, ReceiveDaysOfWeekState + [days_of_week: u8] -> Receive10x5AnswerState, Receive10x5AnswerState + [_10x5_answer: u8] -> ReceiveGandalfAlternativeNameState, - ReceiveGandalfAlternativeNameState + [gandalf_alternative_name: String] -> ExitState, ); ``` @@ -249,15 +236,17 @@ The handy `up!` macro automatically generates functions that complete one state pub type Out = TransitionOut; -pub async fn start(cx: TransitionIn, state: StartState) -> Out { +#[teloxide(transition)] +async fn start(state: StartState, cx: TransitionIn) -> Out { cx.answer_str("Let's start our test! How many days per week are there?") .await?; next(state.up()) } -pub async fn receive_days_of_week( - cx: TransitionIn, +#[teloxide(transition)] +async fn receive_days_of_week( state: ReceiveDaysOfWeekState, + cx: TransitionIn, ) -> Out { match cx.update.text().map(str::parse) { Some(Ok(ans)) if ans == 7 => { @@ -271,9 +260,10 @@ pub async fn receive_days_of_week( } } -pub async fn receive_10x5_answer( - cx: TransitionIn, +#[teloxide(transition)] +async fn receive_10x5_answer( state: Receive10x5AnswerState, + cx: TransitionIn, ) -> Out { match cx.update.text().map(str::parse) { Some(Ok(ans)) if ans == 50 => { @@ -287,9 +277,10 @@ pub async fn receive_10x5_answer( } } -pub async fn receive_gandalf_alternative_name( - cx: TransitionIn, +#[teloxide(transition)] +async fn receive_gandalf_alternative_name( state: ReceiveGandalfAlternativeNameState, + cx: TransitionIn, ) -> Out { match cx.update.text() { Some(ans) if ans == "Mithrandir" => { @@ -327,7 +318,7 @@ async fn main() { input .dialogue .unwrap() - .dispatch(input.cx) + .react(input.cx) .await .expect("Something wrong with the bot!") }, diff --git a/examples/dialogue_bot/src/main.rs b/examples/dialogue_bot/src/main.rs index 80e9ee24..accc2343 100644 --- a/examples/dialogue_bot/src/main.rs +++ b/examples/dialogue_bot/src/main.rs @@ -1,17 +1,14 @@ -// This is a bot that asks your full name, your age, your favourite kind of -// music and sends all the gathered information back. +// This is a bot that asks you three questions, e.g. a simple test. // // # Example // ``` -// - Let's start! First, what's your full name? -// - Luke Skywalker -// - What a wonderful name! Your age? -// - 26 -// - Good. Now choose your favourite music -// *A keyboard of music kinds is displayed* -// *You select Metal* -// - Metal -// - Fine. Your full name: Luke Skywalker, your age: 26, your favourite music: Metal +// - Let's start our test! How many days per week are there? +// - 7 +// - 10*5 = ? +// - 50 +// - What's an alternative name of Gandalf? +// - Mithrandir +// - Congratulations! You've successfully passed the test! // ``` #![allow(clippy::trivial_regex)] @@ -21,8 +18,6 @@ extern crate smart_default; #[macro_use] extern crate derive_more; -#[macro_use] -extern crate teloxide_macros; mod states; mod transitions; @@ -50,7 +45,7 @@ async fn run() { input .dialogue .unwrap() - .dispatch(input.cx) + .react(input.cx) .await .expect("Something wrong with the bot!") }, diff --git a/examples/dialogue_bot/src/states.rs b/examples/dialogue_bot/src/states.rs index a27ee3cc..c0b44ce7 100644 --- a/examples/dialogue_bot/src/states.rs +++ b/examples/dialogue_bot/src/states.rs @@ -1,23 +1,12 @@ use teloxide::prelude::*; +use teloxide_macros::Transition; -use super::transitions::{ - receive_10x5_answer, receive_days_of_week, - receive_gandalf_alternative_name, start, -}; - -#[derive(BotDialogue, SmartDefault, From)] +#[derive(Transition, SmartDefault, From)] pub enum Dialogue { #[default] - #[transition(start)] Start(StartState), - - #[transition(receive_days_of_week)] ReceiveDaysOfWeek(ReceiveDaysOfWeekState), - - #[transition(receive_10x5_answer)] Receive10x5Answer(Receive10x5AnswerState), - - #[transition(receive_gandalf_alternative_name)] ReceiveGandalfAlternativeName(ReceiveGandalfAlternativeNameState), } @@ -38,14 +27,8 @@ pub struct ReceiveGandalfAlternativeNameState { _10x5_answer: u8, } -pub struct ExitState { - rest: ReceiveGandalfAlternativeNameState, - gandalf_alternative_name: String, -} - up!( StartState -> ReceiveDaysOfWeekState, ReceiveDaysOfWeekState + [days_of_week: u8] -> Receive10x5AnswerState, Receive10x5AnswerState + [_10x5_answer: u8] -> ReceiveGandalfAlternativeNameState, - ReceiveGandalfAlternativeNameState + [gandalf_alternative_name: String] -> ExitState, ); diff --git a/examples/dialogue_bot/src/transitions.rs b/examples/dialogue_bot/src/transitions.rs index a3265652..cb6aa709 100644 --- a/examples/dialogue_bot/src/transitions.rs +++ b/examples/dialogue_bot/src/transitions.rs @@ -2,19 +2,23 @@ use crate::states::{ Dialogue, Receive10x5AnswerState, ReceiveDaysOfWeekState, ReceiveGandalfAlternativeNameState, StartState, }; + use teloxide::prelude::*; +use teloxide_macros::teloxide; pub type Out = TransitionOut; -pub async fn start(cx: TransitionIn, state: StartState) -> Out { +#[teloxide(transition)] +async fn start(state: StartState, cx: TransitionIn) -> Out { cx.answer_str("Let's start our test! How many days per week are there?") .await?; next(state.up()) } -pub async fn receive_days_of_week( - cx: TransitionIn, +#[teloxide(transition)] +async fn receive_days_of_week( state: ReceiveDaysOfWeekState, + cx: TransitionIn, ) -> Out { match cx.update.text().map(str::parse) { Some(Ok(ans)) if ans == 7 => { @@ -28,9 +32,10 @@ pub async fn receive_days_of_week( } } -pub async fn receive_10x5_answer( - cx: TransitionIn, +#[teloxide(transition)] +async fn receive_10x5_answer( state: Receive10x5AnswerState, + cx: TransitionIn, ) -> Out { match cx.update.text().map(str::parse) { Some(Ok(ans)) if ans == 50 => { @@ -44,9 +49,10 @@ pub async fn receive_10x5_answer( } } -pub async fn receive_gandalf_alternative_name( - cx: TransitionIn, +#[teloxide(transition)] +async fn receive_gandalf_alternative_name( state: ReceiveGandalfAlternativeNameState, + cx: TransitionIn, ) -> Out { match cx.update.text() { Some(ans) if ans == "Mithrandir" => { diff --git a/examples/redis_remember_bot/src/main.rs b/examples/redis_remember_bot/src/main.rs index 3d716972..ecfe8d2d 100644 --- a/examples/redis_remember_bot/src/main.rs +++ b/examples/redis_remember_bot/src/main.rs @@ -39,7 +39,7 @@ async fn run() { let (cx, dialogue) = input.unpack(); dialogue - .dispatch(cx) + .react(cx) .await .expect("Something is wrong with the bot!") }, diff --git a/examples/redis_remember_bot/src/states.rs b/examples/redis_remember_bot/src/states.rs index 927e7203..4ef180e0 100644 --- a/examples/redis_remember_bot/src/states.rs +++ b/examples/redis_remember_bot/src/states.rs @@ -1,7 +1,5 @@ use teloxide::prelude::*; -use teloxide_macros::BotDialogue; - -use super::transitions::{have_number, start}; +use teloxide_macros::Transition; use serde::{Deserialize, Serialize}; @@ -18,12 +16,9 @@ up!( StartState + [number: i32] -> HaveNumberState, ); -#[derive(BotDialogue, SmartDefault, From, Serialize, Deserialize)] +#[derive(Transition, SmartDefault, From, Serialize, Deserialize)] pub enum Dialogue { #[default] - #[transition(start)] Start(StartState), - - #[transition(have_number)] HaveNumber(HaveNumberState), } diff --git a/examples/redis_remember_bot/src/transitions.rs b/examples/redis_remember_bot/src/transitions.rs index f4bbb816..bdc272ac 100644 --- a/examples/redis_remember_bot/src/transitions.rs +++ b/examples/redis_remember_bot/src/transitions.rs @@ -1,4 +1,5 @@ use teloxide::prelude::*; +use teloxide_macros::teloxide; use super::states::*; @@ -17,7 +18,8 @@ macro_rules! extract_text { pub type Out = TransitionOut; -pub async fn start(cx: TransitionIn, state: StartState) -> Out { +#[teloxide(transition)] +async fn start(state: StartState, cx: TransitionIn) -> Out { let text = extract_text!(cx); if let Ok(number) = text.parse() { @@ -33,7 +35,8 @@ pub async fn start(cx: TransitionIn, state: StartState) -> Out { } } -pub async fn have_number(cx: TransitionIn, state: HaveNumberState) -> Out { +#[teloxide(transition)] +async fn have_number(state: HaveNumberState, cx: TransitionIn) -> Out { let text = extract_text!(cx); let num = state.number; diff --git a/src/dispatching/dialogue/bot_dialogue.rs b/src/dispatching/dialogue/bot_dialogue.rs deleted file mode 100644 index 8f9d4bcf..00000000 --- a/src/dispatching/dialogue/bot_dialogue.rs +++ /dev/null @@ -1,11 +0,0 @@ -use crate::dispatching::dialogue::{TransitionIn, TransitionOut}; -use futures::future::BoxFuture; - -/// Represents a dialogue FSM. -pub trait BotDialogue: Default { - /// Turns itself into another state, depending on the input message. - fn dispatch( - self, - cx: TransitionIn, - ) -> BoxFuture<'static, TransitionOut>; -} diff --git a/src/dispatching/dialogue/mod.rs b/src/dispatching/dialogue/mod.rs index 3358f8e3..ec89c24f 100644 --- a/src/dispatching/dialogue/mod.rs +++ b/src/dispatching/dialogue/mod.rs @@ -2,9 +2,11 @@ //! //! There are three main components: //! -//! 1. Your type `D` (typically an enumeration), implementing [`BotDialogue`]. +//! 1. Your type `D` (typically an enumeration), implementing [`Transition`]. //! It is essentially a [FSM]: its variants are possible dialogue states and -//! [`BotDialogue::dispatch`] is a transition function. +//! [`Transition::react`] is a transition function. +//! +//! 2. State types, forming `D`. They implement [`SubTransition`]. //! //! 2. [`Storage`], which encapsulates all the dialogues. //! @@ -30,7 +32,7 @@ //! use std::convert::Infallible; //! //! use teloxide::prelude::*; -//! use teloxide_macros::BotDialogue; +//! use teloxide_macros::{teloxide, Transition}; //! //! struct _1State; //! struct _2State; @@ -38,23 +40,25 @@ //! //! type Out = TransitionOut; //! -//! async fn _1_transition(_cx: TransitionIn, _state: _1State) -> Out { -//! todo!() -//! } -//! async fn _2_transition(_cx: TransitionIn, _state: _2State) -> Out { -//! todo!() -//! } -//! async fn _3_transition(_cx: TransitionIn, _state: _3State) -> Out { +//! #[teloxide(transition)] +//! async fn _1_transition(_state: _1State, _cx: TransitionIn) -> Out { //! todo!() //! } //! -//! #[derive(BotDialogue)] +//! #[teloxide(transition)] +//! async fn _2_transition(_state: _2State, _cx: TransitionIn) -> Out { +//! todo!() +//! } +//! +//! #[teloxide(transition)] +//! async fn _3_transition(_state: _3State, _cx: TransitionIn) -> Out { +//! todo!() +//! } +//! +//! #[derive(Transition)] //! enum D { -//! #[transition(_1_transition)] //! _1(_1State), -//! #[transition(_2_transition)] //! _2(_2State), -//! #[transition(_3_transition)] //! _3(_3State), //! } //! @@ -94,9 +98,10 @@ //! //! See [examples/dialogue_bot] as a real example. //! -//! [`BotDialogue`]: crate::dispatching::dialogue::BotDialogue -//! [`BotDialogue::dispatch`]: -//! crate::dispatching::dialogue::BotDialogue::dispatch +//! [`Transition`]: crate::dispatching::dialogue::Transition +//! [`SubTransition`]: crate::dispatching::dialogue::SubTransition +//! [`Transition::react`]: +//! crate::dispatching::dialogue::Transition::react //! [FSM]: https://en.wikipedia.org/wiki/Finite-state_machine //! //! [`Storage`]: crate::dispatching::dialogue::Storage @@ -122,26 +127,27 @@ #![allow(clippy::type_complexity)] -mod bot_dialogue; mod dialogue_dispatcher; mod dialogue_dispatcher_handler; mod dialogue_stage; mod dialogue_with_cx; mod get_chat_id; mod storage; +mod transition; -use crate::{requests::ResponseResult, types::Message}; -pub use bot_dialogue::BotDialogue; pub use dialogue_dispatcher::DialogueDispatcher; pub use dialogue_dispatcher_handler::DialogueDispatcherHandler; pub use dialogue_stage::{exit, next, DialogueStage}; pub use dialogue_with_cx::DialogueWithCx; pub use get_chat_id::GetChatId; +pub use transition::{ + SubTransition, SubTransitionOutputType, Transition, TransitionIn, + TransitionOut, +}; #[cfg(feature = "redis-storage")] pub use storage::{RedisStorage, RedisStorageError}; -use crate::dispatching::UpdateWithCx; pub use storage::{serializer, InMemStorage, Serializer, Storage}; /// Generates `.up(field)` methods for dialogue states. @@ -192,9 +198,3 @@ macro_rules! up { )+ }; } - -/// An input passed into a FSM transition function. -pub type TransitionIn = UpdateWithCx; - -/// A type returned from a FSM transition function. -pub type TransitionOut = ResponseResult>; diff --git a/src/dispatching/dialogue/transition.rs b/src/dispatching/dialogue/transition.rs new file mode 100644 index 00000000..b4f461d5 --- /dev/null +++ b/src/dispatching/dialogue/transition.rs @@ -0,0 +1,44 @@ +use crate::{ + dispatching::{dialogue::DialogueStage, UpdateWithCx}, + requests::ResponseResult, + types::Message, +}; +use futures::future::BoxFuture; + +/// Represents a transition function of a dialogue FSM. +pub trait Transition: Sized { + /// Turns itself into another state, depending on the input message. + fn react(self, cx: TransitionIn) + -> BoxFuture<'static, TransitionOut>; +} + +/// Like [`Transition`], but from `StateN` -> `Dialogue`. +/// +/// [`Transition`]: crate::dispatching::dialogue::Transition +pub trait SubTransition +where + Dialogue: Transition, +{ + /// Turns itself into another state, depending on the input message. + fn react( + self, + cx: TransitionIn, + ) -> BoxFuture<'static, TransitionOut>; +} + +/// A type returned from a FSM subtransition function. +/// +/// Now it is used only inside `#[teloxide(transition)]` for type inference. +pub trait SubTransitionOutputType { + type Output; +} + +impl SubTransitionOutputType for TransitionOut { + type Output = D; +} + +/// An input passed into a FSM (sub)transition function. +pub type TransitionIn = UpdateWithCx; + +/// A type returned from a FSM (sub)transition function. +pub type TransitionOut = ResponseResult>; diff --git a/src/prelude.rs b/src/prelude.rs index 7ebec1f1..8c68fe48 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -3,8 +3,8 @@ pub use crate::{ dispatching::{ dialogue::{ - exit, next, BotDialogue, DialogueDispatcher, DialogueStage, - DialogueWithCx, GetChatId, TransitionIn, TransitionOut, + exit, next, DialogueDispatcher, DialogueStage, DialogueWithCx, + GetChatId, Transition, TransitionIn, TransitionOut, }, Dispatcher, DispatcherHandlerRx, DispatcherHandlerRxExt, UpdateWithCx, },