Add an auxiliary parameter to (sub)transitions

This commit is contained in:
Temirkhan Myrzamadi 2020-07-26 23:16:49 +06:00
parent 1da19f3d30
commit 54a6bf440b
14 changed files with 246 additions and 192 deletions

138
README.md
View file

@ -191,7 +191,7 @@ A dialogue is described by an enumeration, where each variant is one of possible
Below is a bot, which asks you three questions and then sends the answers back to you. Here's possible states for a dialogue: Below is a bot, which asks you three questions and then sends the answers back to you. Here's possible states for a dialogue:
([dialogue_bot/src/states.rs](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/states.rs)) ([dialogue_bot/src/states/mod.rs](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/states/mod.rs))
```rust ```rust
// Imports are omitted... // Imports are omitted...
@ -203,60 +203,43 @@ pub enum Dialogue {
ReceiveAge(ReceiveAgeState), ReceiveAge(ReceiveAgeState),
ReceiveLocation(ReceiveLocationState), ReceiveLocation(ReceiveLocationState),
} }
```
([dialogue_bot/src/states/start.rs](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/states/start.rs))
```rust
// Imports are omitted...
#[derive(Default)] #[derive(Default)]
pub struct StartState; pub struct StartState;
#[derive(Generic)] #[teloxide(transition)]
pub struct ReceiveFullNameState; async fn start(
_state: StartState,
cx: TransitionIn,
_ans: String,
) -> TransitionOut<Dialogue> {
cx.answer_str("Let's start! What's your full name?").await?;
next(ReceiveFullNameState)
}
```
([dialogue_bot/src/states/receive_age.rs](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/states/receive_age.rs))
```rust
// Imports are omitted...
#[derive(Generic)] #[derive(Generic)]
pub struct ReceiveAgeState { pub struct ReceiveAgeState {
pub full_name: String, pub full_name: String,
} }
#[derive(Generic)]
pub struct ReceiveLocationState {
pub full_name: String,
pub age: u8,
}
```
... and here are the transition functions, which turn one state into another:
([dialogue_bot/src/transitions.rs](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/transitions.rs))
```rust
// Imports are omitted...
pub type Out = TransitionOut<Dialogue>;
#[teloxide(transition)] #[teloxide(transition)]
async fn start(_state: StartState, cx: TransitionIn) -> Out { async fn receive_age_state(
cx.answer_str("Let's start! What's your full name?").await?; state: ReceiveAgeState,
next(ReceiveFullNameState)
}
#[teloxide(transition)]
async fn receive_full_name(
state: ReceiveFullNameState,
cx: TransitionIn, cx: TransitionIn,
) -> Out { ans: String,
match cx.update.text_owned() { ) -> TransitionOut<Dialogue> {
Some(ans) => { match ans.parse::<u8>() {
cx.answer_str("How old are you?").await?; Ok(ans) => {
next(ReceiveAgeState::up(state, ans))
}
_ => {
cx.answer_str("Send me a text message.").await?;
next(state)
}
}
}
#[teloxide(transition)]
async fn receive_age_state(state: ReceiveAgeState, cx: TransitionIn) -> Out {
match cx.update.text().map(str::parse::<u8>) {
Some(Ok(ans)) => {
cx.answer_str("What's your location?").await?; cx.answer_str("What's your location?").await?;
next(ReceiveLocationState::up(state, ans)) next(ReceiveLocationState::up(state, ans))
} }
@ -266,26 +249,48 @@ async fn receive_age_state(state: ReceiveAgeState, cx: TransitionIn) -> Out {
} }
} }
} }
```
([dialogue_bot/src/states/receive_full_name.rs](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/states/receive_full_name.rs))
```rust
// Imports are omitted...
#[derive(Generic)]
pub struct ReceiveFullNameState;
#[teloxide(transition)]
async fn receive_full_name(
state: ReceiveFullNameState,
cx: TransitionIn,
ans: String,
) -> TransitionOut<Dialogue> {
cx.answer_str("How old are you?").await?;
next(ReceiveAgeState::up(state, ans))
}
```
([dialogue_bot/src/states/receive_location.rs](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/states/receive_location.rs))
```rust
// Imports are omitted...
#[derive(Generic)]
pub struct ReceiveLocationState {
pub full_name: String,
pub age: u8,
}
#[teloxide(transition)] #[teloxide(transition)]
async fn receive_location( async fn receive_location(
state: ReceiveLocationState, state: ReceiveLocationState,
cx: TransitionIn, cx: TransitionIn,
) -> Out { ans: String,
match cx.update.text() { ) -> TransitionOut<Dialogue> {
Some(ans) => { cx.answer_str(format!(
cx.answer_str(format!( "Full name: {}\nAge: {}\nLocation: {}",
"Full name: {}\nAge: {}\nLocation: {}", state.full_name, state.age, ans
state.full_name, state.age, ans ))
)) .await?;
.await?; exit()
exit()
}
_ => {
cx.answer_str("Send me a text message.").await?;
next(state)
}
}
} }
``` ```
@ -309,12 +314,27 @@ async fn main() {
|DialogueWithCx { cx, dialogue }: In| async move { |DialogueWithCx { cx, dialogue }: In| async move {
// No panic because of std::convert::Infallible. // No panic because of std::convert::Infallible.
let dialogue = dialogue.unwrap(); let dialogue = dialogue.unwrap();
dialogue.react(cx).await.expect("Something wrong with the bot!") handle_message(cx, dialogue)
.await
.expect("Something wrong with the bot!")
}, },
)) ))
.dispatch() .dispatch()
.await; .await;
} }
async fn handle_message(
cx: UpdateWithCx<Message>,
dialogue: Dialogue,
) -> TransitionOut<Dialogue> {
match cx.update.text_owned() {
None => {
cx.answer_str("Send me a text message.").await?;
next(dialogue)
}
Some(ans) => dialogue.react(cx, ans).await,
}
}
``` ```
<div align="center"> <div align="center">

View file

@ -0,0 +1,15 @@
mod states;
use crate::dialogue::states::{
ReceiveAgeState, ReceiveFullNameState, ReceiveLocationState, StartState,
};
use teloxide_macros::Transition;
#[derive(Transition, SmartDefault, From)]
pub enum Dialogue {
#[default]
Start(StartState),
ReceiveFullName(ReceiveFullNameState),
ReceiveAge(ReceiveAgeState),
ReceiveLocation(ReceiveLocationState),
}

View file

@ -0,0 +1,9 @@
mod receive_age;
mod receive_full_name;
mod receive_location;
mod start;
pub use receive_age::ReceiveAgeState;
pub use receive_full_name::ReceiveFullNameState;
pub use receive_location::ReceiveLocationState;
pub use start::StartState;

View file

@ -0,0 +1,27 @@
use crate::dialogue::{
states::receive_location::ReceiveLocationState, Dialogue,
};
use teloxide::prelude::*;
#[derive(Generic)]
pub struct ReceiveAgeState {
pub full_name: String,
}
#[teloxide(transition)]
async fn receive_age_state(
state: ReceiveAgeState,
cx: TransitionIn,
ans: String,
) -> TransitionOut<Dialogue> {
match ans.parse::<u8>() {
Ok(ans) => {
cx.answer_str("What's your location?").await?;
next(ReceiveLocationState::up(state, ans))
}
_ => {
cx.answer_str("Send me a number.").await?;
next(state)
}
}
}

View file

@ -0,0 +1,15 @@
use crate::dialogue::{states::receive_age::ReceiveAgeState, Dialogue};
use teloxide::prelude::*;
#[derive(Generic)]
pub struct ReceiveFullNameState;
#[teloxide(transition)]
async fn receive_full_name(
state: ReceiveFullNameState,
cx: TransitionIn,
ans: String,
) -> TransitionOut<Dialogue> {
cx.answer_str("How old are you?").await?;
next(ReceiveAgeState::up(state, ans))
}

View file

@ -0,0 +1,22 @@
use crate::dialogue::Dialogue;
use teloxide::prelude::*;
#[derive(Generic)]
pub struct ReceiveLocationState {
pub full_name: String,
pub age: u8,
}
#[teloxide(transition)]
async fn receive_location(
state: ReceiveLocationState,
cx: TransitionIn,
ans: String,
) -> TransitionOut<Dialogue> {
cx.answer_str(format!(
"Full name: {}\nAge: {}\nLocation: {}",
state.full_name, state.age, ans
))
.await?;
exit()
}

View file

@ -0,0 +1,15 @@
use crate::dialogue::{states::ReceiveFullNameState, Dialogue};
use teloxide::prelude::*;
#[derive(Default)]
pub struct StartState;
#[teloxide(transition)]
async fn start(
_state: StartState,
cx: TransitionIn,
_ans: String,
) -> TransitionOut<Dialogue> {
cx.answer_str("Let's start! What's your full name?").await?;
next(ReceiveFullNameState)
}

View file

@ -27,11 +27,9 @@ extern crate frunk_core;
#[macro_use] #[macro_use]
extern crate teloxide_macros; extern crate teloxide_macros;
mod states; mod dialogue;
mod transitions;
use states::*;
use crate::dialogue::Dialogue;
use std::convert::Infallible; use std::convert::Infallible;
use teloxide::prelude::*; use teloxide::prelude::*;
@ -53,9 +51,24 @@ async fn run() {
|DialogueWithCx { cx, dialogue }: In| async move { |DialogueWithCx { cx, dialogue }: In| async move {
// No panic because of std::convert::Infallible. // No panic because of std::convert::Infallible.
let dialogue = dialogue.unwrap(); let dialogue = dialogue.unwrap();
dialogue.react(cx).await.expect("Something wrong with the bot!") handle_message(cx, dialogue)
.await
.expect("Something wrong with the bot!")
}, },
)) ))
.dispatch() .dispatch()
.await; .await;
} }
async fn handle_message(
cx: UpdateWithCx<Message>,
dialogue: Dialogue,
) -> TransitionOut<Dialogue> {
match cx.update.text_owned() {
None => {
cx.answer_str("Send me a text message.").await?;
next(dialogue)
}
Some(ans) => dialogue.react(cx, ans).await,
}
}

View file

@ -1,28 +0,0 @@
use teloxide::prelude::*;
use teloxide_macros::Transition;
#[derive(Transition, SmartDefault, From)]
pub enum Dialogue {
#[default]
Start(StartState),
ReceiveFullName(ReceiveFullNameState),
ReceiveAge(ReceiveAgeState),
ReceiveLocation(ReceiveLocationState),
}
#[derive(Default)]
pub struct StartState;
#[derive(Generic)]
pub struct ReceiveFullNameState;
#[derive(Generic)]
pub struct ReceiveAgeState {
pub full_name: String,
}
#[derive(Generic)]
pub struct ReceiveLocationState {
pub full_name: String,
pub age: u8,
}

View file

@ -1,66 +0,0 @@
use crate::states::{
Dialogue, ReceiveAgeState, ReceiveFullNameState, ReceiveLocationState,
StartState,
};
use teloxide::prelude::*;
pub type Out = TransitionOut<Dialogue>;
#[teloxide(transition)]
async fn start(_state: StartState, cx: TransitionIn) -> Out {
cx.answer_str("Let's start! What's your full name?").await?;
next(ReceiveFullNameState)
}
#[teloxide(transition)]
async fn receive_full_name(
state: ReceiveFullNameState,
cx: TransitionIn,
) -> Out {
match cx.update.text_owned() {
Some(ans) => {
cx.answer_str("How old are you?").await?;
next(ReceiveAgeState::up(state, ans))
}
_ => {
cx.answer_str("Send me a text message.").await?;
next(state)
}
}
}
#[teloxide(transition)]
async fn receive_age_state(state: ReceiveAgeState, cx: TransitionIn) -> Out {
match cx.update.text().map(str::parse::<u8>) {
Some(Ok(ans)) => {
cx.answer_str("What's your location?").await?;
next(ReceiveLocationState::up(state, ans))
}
_ => {
cx.answer_str("Send me a number.").await?;
next(state)
}
}
}
#[teloxide(transition)]
async fn receive_location(
state: ReceiveLocationState,
cx: TransitionIn,
) -> Out {
match cx.update.text() {
Some(ans) => {
cx.answer_str(format!(
"Full name: {}\nAge: {}\nLocation: {}",
state.full_name, state.age, ans
))
.await?;
exit()
}
_ => {
cx.answer_str("Send me a text message.").await?;
next(state)
}
}
}

View file

@ -38,11 +38,9 @@ async fn run() {
|DialogueWithCx { cx, dialogue }: In| async move { |DialogueWithCx { cx, dialogue }: In| async move {
// No panic because of std::convert::Infallible. // No panic because of std::convert::Infallible.
let dialogue = dialogue.unwrap(); let dialogue = dialogue.unwrap();
handle_message(cx, dialogue)
dialogue
.react(cx)
.await .await
.expect("Something is wrong with the bot!") .expect("Something wrong with the bot!")
}, },
// You can also choose serializer::JSON or serializer::CBOR // You can also choose serializer::JSON or serializer::CBOR
// All serializers but JSON require enabling feature // All serializers but JSON require enabling feature
@ -55,3 +53,16 @@ async fn run() {
.dispatch() .dispatch()
.await; .await;
} }
async fn handle_message(
cx: UpdateWithCx<Message>,
dialogue: Dialogue,
) -> TransitionOut<Dialogue> {
match cx.update.text_owned() {
None => {
cx.answer_str("Send me a text message.").await?;
next(dialogue)
}
Some(ans) => dialogue.react(cx, ans).await,
}
}

View file

@ -1,4 +1,3 @@
use teloxide::prelude::*;
use teloxide_macros::Transition; use teloxide_macros::Transition;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View file

@ -3,26 +3,13 @@ use teloxide_macros::teloxide;
use super::states::*; use super::states::*;
#[macro_export]
macro_rules! extract_text {
($cx:ident) => {
match $cx.update.text_owned() {
Some(text) => text,
None => {
$cx.answer_str("Please, send me a text message").await?;
return next(StartState);
}
}
};
}
pub type Out = TransitionOut<Dialogue>;
#[teloxide(transition)] #[teloxide(transition)]
async fn start(state: StartState, cx: TransitionIn) -> Out { async fn start(
let text = extract_text!(cx); state: StartState,
cx: TransitionIn,
if let Ok(number) = text.parse() { ans: String,
) -> TransitionOut<Dialogue> {
if let Ok(number) = ans.parse() {
cx.answer_str(format!( cx.answer_str(format!(
"Remembered number {}. Now use /get or /reset", "Remembered number {}. Now use /get or /reset",
number number
@ -36,14 +23,17 @@ async fn start(state: StartState, cx: TransitionIn) -> Out {
} }
#[teloxide(transition)] #[teloxide(transition)]
async fn have_number(state: HaveNumberState, cx: TransitionIn) -> Out { async fn have_number(
let text = extract_text!(cx); state: HaveNumberState,
cx: TransitionIn,
ans: String,
) -> TransitionOut<Dialogue> {
let num = state.number; let num = state.number;
if text.starts_with("/get") { if ans.starts_with("/get") {
cx.answer_str(format!("Here is your number: {}", num)).await?; cx.answer_str(format!("Here is your number: {}", num)).await?;
next(state) next(state)
} else if text.starts_with("/reset") { } else if ans.starts_with("/reset") {
cx.answer_str("Resetted number").await?; cx.answer_str("Resetted number").await?;
next(StartState) next(StartState)
} else { } else {

View file

@ -6,24 +6,36 @@ use crate::{
use futures::future::BoxFuture; use futures::future::BoxFuture;
/// Represents a transition function of a dialogue FSM. /// Represents a transition function of a dialogue FSM.
pub trait Transition: Sized { pub trait Transition<T>: Sized {
/// Turns itself into another state, depending on the input message. /// Turns itself into another state, depending on the input message.
fn react(self, cx: TransitionIn) ///
-> BoxFuture<'static, TransitionOut<Self>>; /// `aux` will be passed to each subtransition function.
fn react(
self,
cx: TransitionIn,
aux: T,
) -> BoxFuture<'static, TransitionOut<Self>>;
} }
/// Like [`Transition`], but from `StateN` -> `Dialogue`. /// Like [`Transition`], but from `StateN` -> `Dialogue`.
/// ///
/// [`Transition`]: crate::dispatching::dialogue::Transition /// [`Transition`]: crate::dispatching::dialogue::Transition
pub trait SubTransition<Dialogue> pub trait SubTransition
where where
Dialogue: Transition, Self::Dialogue: Transition<Self::Aux>,
{ {
type Aux;
type Dialogue;
/// Turns itself into another state, depending on the input message. /// Turns itself into another state, depending on the input message.
///
/// `aux` is something that is provided by the call side, for example, a
/// message's text.
fn react( fn react(
self, self,
cx: TransitionIn, cx: TransitionIn,
) -> BoxFuture<'static, TransitionOut<Dialogue>>; aux: Self::Aux,
) -> BoxFuture<'static, TransitionOut<Self::Dialogue>>;
} }
/// A type returned from a FSM subtransition function. /// A type returned from a FSM subtransition function.