From 54a6bf440be599cfce745f366e1f853911a1e626 Mon Sep 17 00:00:00 2001 From: Temirkhan Myrzamadi Date: Sun, 26 Jul 2020 23:16:49 +0600 Subject: [PATCH] Add an auxiliary parameter to (sub)transitions --- README.md | 138 ++++++++++-------- examples/dialogue_bot/src/dialogue/mod.rs | 15 ++ .../dialogue_bot/src/dialogue/states/mod.rs | 9 ++ .../src/dialogue/states/receive_age.rs | 27 ++++ .../src/dialogue/states/receive_full_name.rs | 15 ++ .../src/dialogue/states/receive_location.rs | 22 +++ .../dialogue_bot/src/dialogue/states/start.rs | 15 ++ examples/dialogue_bot/src/main.rs | 23 ++- examples/dialogue_bot/src/states.rs | 28 ---- examples/dialogue_bot/src/transitions.rs | 66 --------- examples/redis_remember_bot/src/main.rs | 19 ++- examples/redis_remember_bot/src/states.rs | 1 - .../redis_remember_bot/src/transitions.rs | 36 ++--- src/dispatching/dialogue/transition.rs | 24 ++- 14 files changed, 246 insertions(+), 192 deletions(-) create mode 100644 examples/dialogue_bot/src/dialogue/mod.rs create mode 100644 examples/dialogue_bot/src/dialogue/states/mod.rs create mode 100644 examples/dialogue_bot/src/dialogue/states/receive_age.rs create mode 100644 examples/dialogue_bot/src/dialogue/states/receive_full_name.rs create mode 100644 examples/dialogue_bot/src/dialogue/states/receive_location.rs create mode 100644 examples/dialogue_bot/src/dialogue/states/start.rs delete mode 100644 examples/dialogue_bot/src/states.rs delete mode 100644 examples/dialogue_bot/src/transitions.rs diff --git a/README.md b/README.md index 73fe9aa8..f38c3030 100644 --- a/README.md +++ b/README.md @@ -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 { + 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; - #[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::) { - Some(Ok(ans)) => { + ans: String, +) -> TransitionOut { + match ans.parse::() { + 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 { + 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 { + 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, + dialogue: Dialogue, +) -> TransitionOut { + match cx.update.text_owned() { + None => { + cx.answer_str("Send me a text message.").await?; + next(dialogue) + } + Some(ans) => dialogue.react(cx, ans).await, + } +} ```
diff --git a/examples/dialogue_bot/src/dialogue/mod.rs b/examples/dialogue_bot/src/dialogue/mod.rs new file mode 100644 index 00000000..e0ffa03c --- /dev/null +++ b/examples/dialogue_bot/src/dialogue/mod.rs @@ -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), +} diff --git a/examples/dialogue_bot/src/dialogue/states/mod.rs b/examples/dialogue_bot/src/dialogue/states/mod.rs new file mode 100644 index 00000000..4872cc8e --- /dev/null +++ b/examples/dialogue_bot/src/dialogue/states/mod.rs @@ -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; diff --git a/examples/dialogue_bot/src/dialogue/states/receive_age.rs b/examples/dialogue_bot/src/dialogue/states/receive_age.rs new file mode 100644 index 00000000..b1f66b00 --- /dev/null +++ b/examples/dialogue_bot/src/dialogue/states/receive_age.rs @@ -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 { + match ans.parse::() { + 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) + } + } +} diff --git a/examples/dialogue_bot/src/dialogue/states/receive_full_name.rs b/examples/dialogue_bot/src/dialogue/states/receive_full_name.rs new file mode 100644 index 00000000..557f0d38 --- /dev/null +++ b/examples/dialogue_bot/src/dialogue/states/receive_full_name.rs @@ -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 { + cx.answer_str("How old are you?").await?; + next(ReceiveAgeState::up(state, ans)) +} diff --git a/examples/dialogue_bot/src/dialogue/states/receive_location.rs b/examples/dialogue_bot/src/dialogue/states/receive_location.rs new file mode 100644 index 00000000..87146dcf --- /dev/null +++ b/examples/dialogue_bot/src/dialogue/states/receive_location.rs @@ -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 { + cx.answer_str(format!( + "Full name: {}\nAge: {}\nLocation: {}", + state.full_name, state.age, ans + )) + .await?; + exit() +} diff --git a/examples/dialogue_bot/src/dialogue/states/start.rs b/examples/dialogue_bot/src/dialogue/states/start.rs new file mode 100644 index 00000000..08eeb6ea --- /dev/null +++ b/examples/dialogue_bot/src/dialogue/states/start.rs @@ -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 { + cx.answer_str("Let's start! What's your full name?").await?; + next(ReceiveFullNameState) +} diff --git a/examples/dialogue_bot/src/main.rs b/examples/dialogue_bot/src/main.rs index c559e6d8..3d117b9b 100644 --- a/examples/dialogue_bot/src/main.rs +++ b/examples/dialogue_bot/src/main.rs @@ -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, + dialogue: Dialogue, +) -> TransitionOut { + match cx.update.text_owned() { + None => { + cx.answer_str("Send me a text message.").await?; + next(dialogue) + } + Some(ans) => dialogue.react(cx, ans).await, + } +} diff --git a/examples/dialogue_bot/src/states.rs b/examples/dialogue_bot/src/states.rs deleted file mode 100644 index bb933f19..00000000 --- a/examples/dialogue_bot/src/states.rs +++ /dev/null @@ -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, -} diff --git a/examples/dialogue_bot/src/transitions.rs b/examples/dialogue_bot/src/transitions.rs deleted file mode 100644 index 8d08b526..00000000 --- a/examples/dialogue_bot/src/transitions.rs +++ /dev/null @@ -1,66 +0,0 @@ -use crate::states::{ - Dialogue, ReceiveAgeState, ReceiveFullNameState, ReceiveLocationState, - StartState, -}; - -use teloxide::prelude::*; - -pub type Out = TransitionOut; - -#[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::) { - 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) - } - } -} diff --git a/examples/redis_remember_bot/src/main.rs b/examples/redis_remember_bot/src/main.rs index 9498f8bd..283061cf 100644 --- a/examples/redis_remember_bot/src/main.rs +++ b/examples/redis_remember_bot/src/main.rs @@ -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, + dialogue: Dialogue, +) -> TransitionOut { + match cx.update.text_owned() { + None => { + cx.answer_str("Send me a text message.").await?; + next(dialogue) + } + Some(ans) => dialogue.react(cx, ans).await, + } +} diff --git a/examples/redis_remember_bot/src/states.rs b/examples/redis_remember_bot/src/states.rs index a848f693..9492a31e 100644 --- a/examples/redis_remember_bot/src/states.rs +++ b/examples/redis_remember_bot/src/states.rs @@ -1,4 +1,3 @@ -use teloxide::prelude::*; use teloxide_macros::Transition; use serde::{Deserialize, Serialize}; diff --git a/examples/redis_remember_bot/src/transitions.rs b/examples/redis_remember_bot/src/transitions.rs index 31932602..7bc8fab3 100644 --- a/examples/redis_remember_bot/src/transitions.rs +++ b/examples/redis_remember_bot/src/transitions.rs @@ -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; - #[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 { + 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 { 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 { diff --git a/src/dispatching/dialogue/transition.rs b/src/dispatching/dialogue/transition.rs index b4f461d5..d53e990e 100644 --- a/src/dispatching/dialogue/transition.rs +++ b/src/dispatching/dialogue/transition.rs @@ -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: Sized { /// Turns itself into another state, depending on the input message. - fn react(self, cx: TransitionIn) - -> BoxFuture<'static, TransitionOut>; + /// + /// `aux` will be passed to each subtransition function. + fn react( + self, + cx: TransitionIn, + aux: T, + ) -> BoxFuture<'static, TransitionOut>; } /// Like [`Transition`], but from `StateN` -> `Dialogue`. /// /// [`Transition`]: crate::dispatching::dialogue::Transition -pub trait SubTransition +pub trait SubTransition where - Dialogue: Transition, + Self::Dialogue: Transition, { + 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>; + aux: Self::Aux, + ) -> BoxFuture<'static, TransitionOut>; } /// A type returned from a FSM subtransition function.