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] - ???
### 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}`.

View file

@ -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<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?")
.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!")
},

View file

@ -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!")
},

View file

@ -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,
);

View file

@ -2,19 +2,23 @@ use crate::states::{
Dialogue, Receive10x5AnswerState, ReceiveDaysOfWeekState,
ReceiveGandalfAlternativeNameState, StartState,
};
use teloxide::prelude::*;
use teloxide_macros::teloxide;
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?")
.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" => {

View file

@ -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!")
},

View file

@ -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),
}

View file

@ -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<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);
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;

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:
//!
//! 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<D>`], 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<D>;
//!
//! 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<D>`]: 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<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::{
dispatching::{
dialogue::{
exit, next, BotDialogue, DialogueDispatcher, DialogueStage,
DialogueWithCx, GetChatId, TransitionIn, TransitionOut,
exit, next, DialogueDispatcher, DialogueStage, DialogueWithCx,
GetChatId, Transition, TransitionIn, TransitionOut,
},
Dispatcher, DispatcherHandlerRx, DispatcherHandlerRxExt, UpdateWithCx,
},