A nicer approach to manage dialogues via #[derive(Transition)] + #[teloxide(transition)]

This commit is contained in:
Temirkhan Myrzamadi 2020-07-25 22:37:58 +06:00
parent 840fe93b61
commit bf114de249
12 changed files with 120 additions and 114 deletions

View file

@ -7,8 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [0.3.0] - ??? ## [0.3.0] - ???
### Added ### Added
- `BotBuilder`, which allows setting a default `ParseMode`. - `BotBuilder`, which allows setting a default `ParseMode`.
- The `BotDialogue` trait. - The `Transition`, `SubTransition`, `SubTransitionOutputType` traits.
- Automatic `dispatch` function generation via `#[derive(BotDialogue)]` + `#[transition(transition_fn)]`. - A nicer approach to manage dialogues via `#[derive(Transition)]` + `#[teloxide(transition)]`.
### Deprecated ### Deprecated
- `Bot::{from_env_with_client, new, with_client}`. - `Bot::{from_env_with_client, new, with_client}`.

View file

@ -195,19 +195,12 @@ States and transition functions are placed into separated modules. For example,
```rust ```rust
// Imports are omitted... // Imports are omitted...
#[derive(BotDialogue, SmartDefault, From)] #[derive(Transition, SmartDefault, From)]
pub enum Dialogue { pub enum Dialogue {
#[default] #[default]
#[transition(start)]
Start(StartState), Start(StartState),
#[transition(receive_days_of_week)]
ReceiveDaysOfWeek(ReceiveDaysOfWeekState), ReceiveDaysOfWeek(ReceiveDaysOfWeekState),
#[transition(receive_10x5_answer)]
Receive10x5Answer(Receive10x5AnswerState), Receive10x5Answer(Receive10x5AnswerState),
#[transition(receive_gandalf_alternative_name)]
ReceiveGandalfAlternativeName(ReceiveGandalfAlternativeNameState), ReceiveGandalfAlternativeName(ReceiveGandalfAlternativeNameState),
} }
@ -228,16 +221,10 @@ pub struct ReceiveGandalfAlternativeNameState {
_10x5_answer: u8, _10x5_answer: u8,
} }
pub struct ExitState {
rest: ReceiveGandalfAlternativeNameState,
gandalf_alternative_name: String,
}
up!( up!(
StartState -> ReceiveDaysOfWeekState, StartState -> ReceiveDaysOfWeekState,
ReceiveDaysOfWeekState + [days_of_week: u8] -> Receive10x5AnswerState, ReceiveDaysOfWeekState + [days_of_week: u8] -> Receive10x5AnswerState,
Receive10x5AnswerState + [_10x5_answer: u8] -> ReceiveGandalfAlternativeNameState, 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<Dialogue>; pub type Out = TransitionOut<Dialogue>;
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?") cx.answer_str("Let's start our test! How many days per week are there?")
.await?; .await?;
next(state.up()) next(state.up())
} }
pub async fn receive_days_of_week( #[teloxide(transition)]
cx: TransitionIn, async fn receive_days_of_week(
state: ReceiveDaysOfWeekState, state: ReceiveDaysOfWeekState,
cx: TransitionIn,
) -> Out { ) -> Out {
match cx.update.text().map(str::parse) { match cx.update.text().map(str::parse) {
Some(Ok(ans)) if ans == 7 => { Some(Ok(ans)) if ans == 7 => {
@ -271,9 +260,10 @@ pub async fn receive_days_of_week(
} }
} }
pub async fn receive_10x5_answer( #[teloxide(transition)]
cx: TransitionIn, async fn receive_10x5_answer(
state: Receive10x5AnswerState, state: Receive10x5AnswerState,
cx: TransitionIn,
) -> Out { ) -> Out {
match cx.update.text().map(str::parse) { match cx.update.text().map(str::parse) {
Some(Ok(ans)) if ans == 50 => { Some(Ok(ans)) if ans == 50 => {
@ -287,9 +277,10 @@ pub async fn receive_10x5_answer(
} }
} }
pub async fn receive_gandalf_alternative_name( #[teloxide(transition)]
cx: TransitionIn, async fn receive_gandalf_alternative_name(
state: ReceiveGandalfAlternativeNameState, state: ReceiveGandalfAlternativeNameState,
cx: TransitionIn,
) -> Out { ) -> Out {
match cx.update.text() { match cx.update.text() {
Some(ans) if ans == "Mithrandir" => { Some(ans) if ans == "Mithrandir" => {
@ -327,7 +318,7 @@ async fn main() {
input input
.dialogue .dialogue
.unwrap() .unwrap()
.dispatch(input.cx) .react(input.cx)
.await .await
.expect("Something wrong with the bot!") .expect("Something wrong with the bot!")
}, },

View file

@ -1,17 +1,14 @@
// This is a bot that asks your full name, your age, your favourite kind of // This is a bot that asks you three questions, e.g. a simple test.
// music and sends all the gathered information back.
// //
// # Example // # Example
// ``` // ```
// - Let's start! First, what's your full name? // - Let's start our test! How many days per week are there?
// - Luke Skywalker // - 7
// - What a wonderful name! Your age? // - 10*5 = ?
// - 26 // - 50
// - Good. Now choose your favourite music // - What's an alternative name of Gandalf?
// *A keyboard of music kinds is displayed* // - Mithrandir
// *You select Metal* // - Congratulations! You've successfully passed the test!
// - Metal
// - Fine. Your full name: Luke Skywalker, your age: 26, your favourite music: Metal
// ``` // ```
#![allow(clippy::trivial_regex)] #![allow(clippy::trivial_regex)]
@ -21,8 +18,6 @@
extern crate smart_default; extern crate smart_default;
#[macro_use] #[macro_use]
extern crate derive_more; extern crate derive_more;
#[macro_use]
extern crate teloxide_macros;
mod states; mod states;
mod transitions; mod transitions;
@ -50,7 +45,7 @@ async fn run() {
input input
.dialogue .dialogue
.unwrap() .unwrap()
.dispatch(input.cx) .react(input.cx)
.await .await
.expect("Something wrong with the bot!") .expect("Something wrong with the bot!")
}, },

View file

@ -1,23 +1,12 @@
use teloxide::prelude::*; use teloxide::prelude::*;
use teloxide_macros::Transition;
use super::transitions::{ #[derive(Transition, SmartDefault, From)]
receive_10x5_answer, receive_days_of_week,
receive_gandalf_alternative_name, start,
};
#[derive(BotDialogue, SmartDefault, From)]
pub enum Dialogue { pub enum Dialogue {
#[default] #[default]
#[transition(start)]
Start(StartState), Start(StartState),
#[transition(receive_days_of_week)]
ReceiveDaysOfWeek(ReceiveDaysOfWeekState), ReceiveDaysOfWeek(ReceiveDaysOfWeekState),
#[transition(receive_10x5_answer)]
Receive10x5Answer(Receive10x5AnswerState), Receive10x5Answer(Receive10x5AnswerState),
#[transition(receive_gandalf_alternative_name)]
ReceiveGandalfAlternativeName(ReceiveGandalfAlternativeNameState), ReceiveGandalfAlternativeName(ReceiveGandalfAlternativeNameState),
} }
@ -38,14 +27,8 @@ pub struct ReceiveGandalfAlternativeNameState {
_10x5_answer: u8, _10x5_answer: u8,
} }
pub struct ExitState {
rest: ReceiveGandalfAlternativeNameState,
gandalf_alternative_name: String,
}
up!( up!(
StartState -> ReceiveDaysOfWeekState, StartState -> ReceiveDaysOfWeekState,
ReceiveDaysOfWeekState + [days_of_week: u8] -> Receive10x5AnswerState, ReceiveDaysOfWeekState + [days_of_week: u8] -> Receive10x5AnswerState,
Receive10x5AnswerState + [_10x5_answer: u8] -> ReceiveGandalfAlternativeNameState, Receive10x5AnswerState + [_10x5_answer: u8] -> ReceiveGandalfAlternativeNameState,
ReceiveGandalfAlternativeNameState + [gandalf_alternative_name: String] -> ExitState,
); );

View file

@ -2,19 +2,23 @@ use crate::states::{
Dialogue, Receive10x5AnswerState, ReceiveDaysOfWeekState, Dialogue, Receive10x5AnswerState, ReceiveDaysOfWeekState,
ReceiveGandalfAlternativeNameState, StartState, ReceiveGandalfAlternativeNameState, StartState,
}; };
use teloxide::prelude::*; use teloxide::prelude::*;
use teloxide_macros::teloxide;
pub type Out = TransitionOut<Dialogue>; pub type Out = TransitionOut<Dialogue>;
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?") cx.answer_str("Let's start our test! How many days per week are there?")
.await?; .await?;
next(state.up()) next(state.up())
} }
pub async fn receive_days_of_week( #[teloxide(transition)]
cx: TransitionIn, async fn receive_days_of_week(
state: ReceiveDaysOfWeekState, state: ReceiveDaysOfWeekState,
cx: TransitionIn,
) -> Out { ) -> Out {
match cx.update.text().map(str::parse) { match cx.update.text().map(str::parse) {
Some(Ok(ans)) if ans == 7 => { Some(Ok(ans)) if ans == 7 => {
@ -28,9 +32,10 @@ pub async fn receive_days_of_week(
} }
} }
pub async fn receive_10x5_answer( #[teloxide(transition)]
cx: TransitionIn, async fn receive_10x5_answer(
state: Receive10x5AnswerState, state: Receive10x5AnswerState,
cx: TransitionIn,
) -> Out { ) -> Out {
match cx.update.text().map(str::parse) { match cx.update.text().map(str::parse) {
Some(Ok(ans)) if ans == 50 => { Some(Ok(ans)) if ans == 50 => {
@ -44,9 +49,10 @@ pub async fn receive_10x5_answer(
} }
} }
pub async fn receive_gandalf_alternative_name( #[teloxide(transition)]
cx: TransitionIn, async fn receive_gandalf_alternative_name(
state: ReceiveGandalfAlternativeNameState, state: ReceiveGandalfAlternativeNameState,
cx: TransitionIn,
) -> Out { ) -> Out {
match cx.update.text() { match cx.update.text() {
Some(ans) if ans == "Mithrandir" => { Some(ans) if ans == "Mithrandir" => {

View file

@ -39,7 +39,7 @@ async fn run() {
let (cx, dialogue) = input.unpack(); let (cx, dialogue) = input.unpack();
dialogue dialogue
.dispatch(cx) .react(cx)
.await .await
.expect("Something is wrong with the bot!") .expect("Something is wrong with the bot!")
}, },

View file

@ -1,7 +1,5 @@
use teloxide::prelude::*; use teloxide::prelude::*;
use teloxide_macros::BotDialogue; use teloxide_macros::Transition;
use super::transitions::{have_number, start};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -18,12 +16,9 @@ up!(
StartState + [number: i32] -> HaveNumberState, StartState + [number: i32] -> HaveNumberState,
); );
#[derive(BotDialogue, SmartDefault, From, Serialize, Deserialize)] #[derive(Transition, SmartDefault, From, Serialize, Deserialize)]
pub enum Dialogue { pub enum Dialogue {
#[default] #[default]
#[transition(start)]
Start(StartState), Start(StartState),
#[transition(have_number)]
HaveNumber(HaveNumberState), HaveNumber(HaveNumberState),
} }

View file

@ -1,4 +1,5 @@
use teloxide::prelude::*; use teloxide::prelude::*;
use teloxide_macros::teloxide;
use super::states::*; use super::states::*;
@ -17,7 +18,8 @@ macro_rules! extract_text {
pub type Out = TransitionOut<Dialogue>; pub type Out = TransitionOut<Dialogue>;
pub async fn start(cx: TransitionIn, state: StartState) -> Out { #[teloxide(transition)]
async fn start(state: StartState, cx: TransitionIn) -> Out {
let text = extract_text!(cx); let text = extract_text!(cx);
if let Ok(number) = text.parse() { 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 text = extract_text!(cx);
let num = state.number; let num = state.number;

View file

@ -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<Self>>;
}

View file

@ -2,9 +2,11 @@
//! //!
//! There are three main components: //! 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 //! 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<D>`], which encapsulates all the dialogues. //! 2. [`Storage<D>`], which encapsulates all the dialogues.
//! //!
@ -30,7 +32,7 @@
//! use std::convert::Infallible; //! use std::convert::Infallible;
//! //!
//! use teloxide::prelude::*; //! use teloxide::prelude::*;
//! use teloxide_macros::BotDialogue; //! use teloxide_macros::{teloxide, Transition};
//! //!
//! struct _1State; //! struct _1State;
//! struct _2State; //! struct _2State;
@ -38,23 +40,25 @@
//! //!
//! type Out = TransitionOut<D>; //! type Out = TransitionOut<D>;
//! //!
//! async fn _1_transition(_cx: TransitionIn, _state: _1State) -> Out { //! #[teloxide(transition)]
//! todo!() //! async fn _1_transition(_state: _1State, _cx: TransitionIn) -> Out {
//! }
//! async fn _2_transition(_cx: TransitionIn, _state: _2State) -> Out {
//! todo!()
//! }
//! async fn _3_transition(_cx: TransitionIn, _state: _3State) -> Out {
//! todo!() //! 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 { //! enum D {
//! #[transition(_1_transition)]
//! _1(_1State), //! _1(_1State),
//! #[transition(_2_transition)]
//! _2(_2State), //! _2(_2State),
//! #[transition(_3_transition)]
//! _3(_3State), //! _3(_3State),
//! } //! }
//! //!
@ -94,9 +98,10 @@
//! //!
//! See [examples/dialogue_bot] as a real example. //! See [examples/dialogue_bot] as a real example.
//! //!
//! [`BotDialogue`]: crate::dispatching::dialogue::BotDialogue //! [`Transition`]: crate::dispatching::dialogue::Transition
//! [`BotDialogue::dispatch`]: //! [`SubTransition`]: crate::dispatching::dialogue::SubTransition
//! crate::dispatching::dialogue::BotDialogue::dispatch //! [`Transition::react`]:
//! crate::dispatching::dialogue::Transition::react
//! [FSM]: https://en.wikipedia.org/wiki/Finite-state_machine //! [FSM]: https://en.wikipedia.org/wiki/Finite-state_machine
//! //!
//! [`Storage<D>`]: crate::dispatching::dialogue::Storage //! [`Storage<D>`]: crate::dispatching::dialogue::Storage
@ -122,26 +127,27 @@
#![allow(clippy::type_complexity)] #![allow(clippy::type_complexity)]
mod bot_dialogue;
mod dialogue_dispatcher; mod dialogue_dispatcher;
mod dialogue_dispatcher_handler; mod dialogue_dispatcher_handler;
mod dialogue_stage; mod dialogue_stage;
mod dialogue_with_cx; mod dialogue_with_cx;
mod get_chat_id; mod get_chat_id;
mod storage; mod storage;
mod transition;
use crate::{requests::ResponseResult, types::Message};
pub use bot_dialogue::BotDialogue;
pub use dialogue_dispatcher::DialogueDispatcher; pub use dialogue_dispatcher::DialogueDispatcher;
pub use dialogue_dispatcher_handler::DialogueDispatcherHandler; pub use dialogue_dispatcher_handler::DialogueDispatcherHandler;
pub use dialogue_stage::{exit, next, DialogueStage}; pub use dialogue_stage::{exit, next, DialogueStage};
pub use dialogue_with_cx::DialogueWithCx; pub use dialogue_with_cx::DialogueWithCx;
pub use get_chat_id::GetChatId; pub use get_chat_id::GetChatId;
pub use transition::{
SubTransition, SubTransitionOutputType, Transition, TransitionIn,
TransitionOut,
};
#[cfg(feature = "redis-storage")] #[cfg(feature = "redis-storage")]
pub use storage::{RedisStorage, RedisStorageError}; pub use storage::{RedisStorage, RedisStorageError};
use crate::dispatching::UpdateWithCx;
pub use storage::{serializer, InMemStorage, Serializer, Storage}; pub use storage::{serializer, InMemStorage, Serializer, Storage};
/// Generates `.up(field)` methods for dialogue states. /// 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<Message>;
/// A type returned from a FSM transition function.
pub type TransitionOut<D> = ResponseResult<DialogueStage<D>>;

View file

@ -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<Self>>;
}
/// Like [`Transition`], but from `StateN` -> `Dialogue`.
///
/// [`Transition`]: crate::dispatching::dialogue::Transition
pub trait SubTransition<Dialogue>
where
Dialogue: Transition,
{
/// Turns itself into another state, depending on the input message.
fn react(
self,
cx: TransitionIn,
) -> BoxFuture<'static, TransitionOut<Dialogue>>;
}
/// 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<D> SubTransitionOutputType for TransitionOut<D> {
type Output = D;
}
/// An input passed into a FSM (sub)transition function.
pub type TransitionIn = UpdateWithCx<Message>;
/// A type returned from a FSM (sub)transition function.
pub type TransitionOut<D> = ResponseResult<DialogueStage<D>>;

View file

@ -3,8 +3,8 @@
pub use crate::{ pub use crate::{
dispatching::{ dispatching::{
dialogue::{ dialogue::{
exit, next, BotDialogue, DialogueDispatcher, DialogueStage, exit, next, DialogueDispatcher, DialogueStage, DialogueWithCx,
DialogueWithCx, GetChatId, TransitionIn, TransitionOut, GetChatId, Transition, TransitionIn, TransitionOut,
}, },
Dispatcher, DispatcherHandlerRx, DispatcherHandlerRxExt, UpdateWithCx, Dispatcher, DispatcherHandlerRx, DispatcherHandlerRxExt, UpdateWithCx,
}, },