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:
([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
// Imports are omitted...
@ -203,60 +203,43 @@ pub enum Dialogue {
ReceiveAge(ReceiveAgeState),
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)]
pub struct StartState;
#[derive(Generic)]
pub struct ReceiveFullNameState;
#[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)
}
```
([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)]
pub struct ReceiveAgeState {
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)]
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,
async fn receive_age_state(
state: ReceiveAgeState,
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)) => {
ans: String,
) -> TransitionOut<Dialogue> {
match ans.parse::<u8>() {
Ok(ans) => {
cx.answer_str("What's your location?").await?;
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)]
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)
}
}
ans: String,
) -> TransitionOut<Dialogue> {
cx.answer_str(format!(
"Full name: {}\nAge: {}\nLocation: {}",
state.full_name, state.age, ans
))
.await?;
exit()
}
```
@ -309,12 +314,27 @@ async fn main() {
|DialogueWithCx { cx, dialogue }: In| async move {
// No panic because of std::convert::Infallible.
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()
.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">

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]
extern crate teloxide_macros;
mod states;
mod transitions;
use states::*;
mod dialogue;
use crate::dialogue::Dialogue;
use std::convert::Infallible;
use teloxide::prelude::*;
@ -53,9 +51,24 @@ async fn run() {
|DialogueWithCx { cx, dialogue }: In| async move {
// No panic because of std::convert::Infallible.
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()
.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 {
// No panic because of std::convert::Infallible.
let dialogue = dialogue.unwrap();
dialogue
.react(cx)
handle_message(cx, dialogue)
.await
.expect("Something is wrong with the bot!")
.expect("Something wrong with the bot!")
},
// You can also choose serializer::JSON or serializer::CBOR
// All serializers but JSON require enabling feature
@ -55,3 +53,16 @@ async fn run() {
.dispatch()
.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 serde::{Deserialize, Serialize};

View file

@ -3,26 +3,13 @@ use teloxide_macros::teloxide;
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)]
async fn start(state: StartState, cx: TransitionIn) -> Out {
let text = extract_text!(cx);
if let Ok(number) = text.parse() {
async fn start(
state: StartState,
cx: TransitionIn,
ans: String,
) -> TransitionOut<Dialogue> {
if let Ok(number) = ans.parse() {
cx.answer_str(format!(
"Remembered number {}. Now use /get or /reset",
number
@ -36,14 +23,17 @@ async fn start(state: StartState, cx: TransitionIn) -> Out {
}
#[teloxide(transition)]
async fn have_number(state: HaveNumberState, cx: TransitionIn) -> Out {
let text = extract_text!(cx);
async fn have_number(
state: HaveNumberState,
cx: TransitionIn,
ans: String,
) -> TransitionOut<Dialogue> {
let num = state.number;
if text.starts_with("/get") {
if ans.starts_with("/get") {
cx.answer_str(format!("Here is your number: {}", num)).await?;
next(state)
} else if text.starts_with("/reset") {
} else if ans.starts_with("/reset") {
cx.answer_str("Resetted number").await?;
next(StartState)
} else {

View file

@ -6,24 +6,36 @@ use crate::{
use futures::future::BoxFuture;
/// 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.
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`.
///
/// [`Transition`]: crate::dispatching::dialogue::Transition
pub trait SubTransition<Dialogue>
pub trait SubTransition
where
Dialogue: Transition,
Self::Dialogue: Transition<Self::Aux>,
{
type Aux;
type Dialogue;
/// 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(
self,
cx: TransitionIn,
) -> BoxFuture<'static, TransitionOut<Dialogue>>;
aux: Self::Aux,
) -> BoxFuture<'static, TransitionOut<Self::Dialogue>>;
}
/// A type returned from a FSM subtransition function.