From d5f15676edbd5305df3b58d53e33ff450d8bb7c3 Mon Sep 17 00:00:00 2001 From: LasterAlex Date: Fri, 8 Nov 2024 01:09:57 +0200 Subject: [PATCH 1/9] Improved case macro --- CHANGELOG.md | 1 + src/lib.rs | 273 +++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 254 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c947f02..ad8f115 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The `Handler::sig` function. - The `di::Injectable::input_types` associated function [**BC**]. - The `DependencyMap::try_get` method ([PR #29](https://github.com/teloxide/dptree/pull/29)). + - More functionality to `case!` macro. Now it can inject almost anything that rust pattern matching can interpret. ### Changed diff --git a/src/lib.rs b/src/lib.rs index ff4be91..53349dd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -126,32 +126,76 @@ pub use handler::*; /// [`examples/purchase.rs`]: https://github.com/teloxide/teloxide/blob/master/examples/purchase.rs #[macro_export] macro_rules! case { - ($($variant:ident)::+) => { - $crate::filter(|x| matches!(x, $($variant)::+)) - }; - ($($variant:ident)::+ ($param:ident)) => { + ($($entry:tt)*) => { $crate::filter_map(|x| match x { - $($variant)::+($param) => Some($param), + $($entry)* + => Some($crate::_extract_values!($($entry)*)), _ => None, }) }; - ($($variant:ident)::+ ($($param:ident),+ $(,)?)) => { - $crate::filter_map(|x| match x { - $($variant)::+($($param),+) => Some(($($param),+ ,)), - _ => None, - }) +} + +/// A helper macro for [`case!`] to extract values recursively from structs and enums +/// +/// [`case!`]: crate::case +// +// This looks confusing, so here is how this works: +// +// The first rule is just a nicer panic +// +// The following four rules are escapes from the recursion. All branches end in either +// nothing, a variable from a struct/enum, or in an enum without any values (for filtering reasons). +// +// The two rules after that basically take any enum or struct, and dump out the contents to the +// next recursion depth. +// +// The next two rules take the comma separated tokens, +// and wrap a recursion layer aroud every token. E.g.: +// `field1, field2` will become `_extract_values(field1), _extract_values(field2)` +// +// The last rule just takes the value out of `key: value` +#[macro_export] +macro_rules! _extract_values { + (None) => { + { const _: () = panic!("Cannot infer the type of None, please add None::"); } }; - ($($variant:ident)::+ {$param:ident}) => { - $crate::filter_map(|x| match x { - $($variant)::+{$param} => Some($param), - _ => None, - }) + // Empty value, for trailing comma support + () => { + // This is a workaround, rust expects an expression here, but just () can't be returned, so + // we trick the compiler to think that we are returning an expression, while not doing it + #[cfg(any())] { () } }; - ($($variant:ident)::+ {$($param:ident),+ $(,)?}) => { - $crate::filter_map(|x| match x { - $($variant)::+ { $($param),+ } => Some(($($param),+ ,)), - _ => None, - }) + // Singular value + ($value:ident) => { + $value + }; + // Mostly for None support, but generalized + ($value:ident::<$($ty:ty),*>) => { + $value::<$($ty)*> + }; + // Enum without any values + ($inner:ident::$variant:ident) => { + $inner::$variant + }; + // Multiple fields in stuct/enum in braces + ($inner:ident$(::$variant:ident)? { $($rest:tt)* }) => { + $crate::_extract_values!($($rest)*) + }; + // Multiple fields in stuct/enum in parentheses + ($value:ident$(::$variant:ident)? ($($rest:tt)*)) => { + $crate::_extract_values!($($rest)*) + }; + // This takes a comma separated token list and wraps a recursion around every item + ($param:ident $(: $value:ident$(::$variant:ident)? $({$($inner:tt)*})? )?, $($outer:tt)*) => { + ($crate::_extract_values!($param $(: $value$(::$variant)? $({$($inner)*})? )?), $crate::_extract_values!($($outer)*)) + }; + // Same thing, it just works with parentheses instead of braces + ($param:ident $(: $value:ident$(::$variant:ident)? ($($inner:tt)*) )?, $($outer:tt)*) => { + ($crate::_extract_values!($param $(: $value$(::$variant)? ($($inner)*) )?), $crate::_extract_values!($($outer)*)) + }; + // Extracts `test: ...` to `...` + ($param:ident : $($value:tt)*) => { + $crate::_extract_values!($($value)*) }; } @@ -169,6 +213,25 @@ mod tests { Other, } + #[derive(Debug, Copy, Clone, Eq, PartialEq)] + enum ComplexState { + A(State), + B(i32, State), + C { foo: TestStruct, bar: i32 }, + D(Option), + E { foo: TestTupleStruct }, + Other, + } + + #[derive(Debug, Copy, Clone, Eq, PartialEq)] + struct TestStruct { + a: u32, + b: State, + } + + #[derive(Debug, Copy, Clone, Eq, PartialEq)] + struct TestTupleStruct(i32, State); + #[tokio::test] async fn handler_empty_variant() { let input = State::A; @@ -254,4 +317,174 @@ mod tests { assert_eq!(h.dispatch(crate::deps![input]).await, ControlFlow::Break(123)); assert!(matches!(h.dispatch(crate::deps![State::Other]).await, ControlFlow::Continue(_))); } + + #[tokio::test] + async fn handler_struct() { + let input = TestStruct { a: 5, b: State::A }; + let h: crate::Handler<_> = + case![TestStruct { a, b: State::A }].endpoint(|(a, state): (u32, State)| async move { + assert_eq!(a, 5); + assert_eq!(state, State::A); + 123 + }); + + assert_eq!(h.dispatch(crate::deps![input]).await, ControlFlow::Break(123)); + assert!(matches!( + h.dispatch(crate::deps![TestStruct { a: 5, b: State::Other }]).await, + ControlFlow::Continue(_) + )); + } + + #[tokio::test] + async fn handler_tuple_struct() { + let input = TestTupleStruct(6, State::A); + let h: crate::Handler<_> = + case![TestTupleStruct(x, State::A)].endpoint(|(x, state): (i32, State)| async move { + assert_eq!(x, 6); + assert_eq!(state, State::A); + 123 + }); + + assert_eq!(h.dispatch(crate::deps![input]).await, ControlFlow::Break(123)); + assert!(matches!( + h.dispatch(crate::deps![TestTupleStruct(6, State::Other)]).await, + ControlFlow::Continue(_) + )); + } + + // ComplexState tests + + #[tokio::test] + async fn handler_nested_fn_variant() { + let input = ComplexState::A(State::A); + let h: crate::Handler<_> = + case![ComplexState::A(State::A)].endpoint(|state: State| async move { + assert_eq!(state, State::A); + 123 + }); + + assert_eq!(h.dispatch(crate::deps![input]).await, ControlFlow::Break(123)); + assert!(matches!( + h.dispatch(crate::deps![ComplexState::Other]).await, + ControlFlow::Continue(_) + )); + } + + #[tokio::test] + async fn handler_nested_multiple_fn_variant() { + let input = ComplexState::B(1, State::B(2)); + let h: crate::Handler<_> = + case![ComplexState::B(x, State::B(y))].endpoint(|(x, y): (i32, i32)| async move { + assert_eq!(x, 1); + assert_eq!(y, 2); + 123 + }); + + assert_eq!(h.dispatch(crate::deps![input]).await, ControlFlow::Break(123)); + assert!(matches!( + h.dispatch(crate::deps![ComplexState::Other]).await, + ControlFlow::Continue(_) + )); + } + + #[tokio::test] + async fn handler_nested_struct() { + let input = ComplexState::C { foo: TestStruct { a: 10, b: State::C(1, "abc") }, bar: 11 }; + let h: crate::Handler<_> = + case![ComplexState::C { foo: TestStruct { a, b: State::C(c, d) }, bar }].endpoint( + |((a, (c, d)), bar): ((u32, (i32, &'static str)), i32)| async move { + assert_eq!(a, 10); + assert_eq!(c, 1); + assert_eq!(d, "abc"); + assert_eq!(bar, 11); + 123 + }, + ); + + assert_eq!(h.dispatch(crate::deps![input]).await, ControlFlow::Break(123)); + assert!(matches!( + h.dispatch(crate::deps![ComplexState::Other]).await, + ControlFlow::Continue(_) + )); + } + + #[tokio::test] + async fn handler_nested_option_some() { + let input = ComplexState::D(Some(2)); + let h: crate::Handler<_> = case![ComplexState::D(Some(x))].endpoint(|x: i32| async move { + assert_eq!(x, 2); + 123 + }); + + assert_eq!(h.dispatch(crate::deps![input]).await, ControlFlow::Break(123)); + assert!(matches!( + h.dispatch(crate::deps![ComplexState::Other]).await, + ControlFlow::Continue(_) + )); + } + + #[tokio::test] + async fn handler_option_none() { + let input = None::; + let h: crate::Handler<_> = case![None::].endpoint(|x: Option| async move { + assert_eq!(x, None); + 123 + }); + + assert_eq!(h.dispatch(crate::deps![input]).await, ControlFlow::Break(123)); + assert!(matches!(h.dispatch(crate::deps![Some(13)]).await, ControlFlow::Continue(_))); + } + + #[tokio::test] + async fn handler_nested_option_none() { + let input = ComplexState::D(None); + let h: crate::Handler<_> = + case![ComplexState::D(None::)].endpoint(|x: Option| async move { + assert_eq!(x, None); + 123 + }); + + assert_eq!(h.dispatch(crate::deps![input]).await, ControlFlow::Break(123)); + assert!(matches!( + h.dispatch(crate::deps![ComplexState::Other]).await, + ControlFlow::Continue(_) + )); + } + + #[tokio::test] + async fn handler_nested_tuple_struct() { + let input = ComplexState::E { foo: TestTupleStruct(100, State::B(9)) }; + let h: crate::Handler<_> = case![ComplexState::E { foo: TestTupleStruct(x, y) }].endpoint( + |(x, state): (i32, State)| async move { + assert_eq!(x, 100); + assert_eq!(state, State::B(9)); + 123 + }, + ); + + assert_eq!(h.dispatch(crate::deps![input]).await, ControlFlow::Break(123)); + assert!(matches!( + h.dispatch(crate::deps![ComplexState::Other]).await, + ControlFlow::Continue(_) + )); + } + + #[tokio::test] + async fn handler_nested_tuple_struct_trailing_comma() { + let input = ComplexState::E { foo: TestTupleStruct(100, State::A) }; + // Here rustfmt again removes a crutial comma + #[rustfmt::skip] + let h: crate::Handler<_> = case![ComplexState::E { foo: TestTupleStruct(x, State::A), }] + .endpoint(|((x, state),): ((i32, State),)| async move { + assert_eq!(x, 100); + assert_eq!(state, State::A); + 123 + }); + + assert_eq!(h.dispatch(crate::deps![input]).await, ControlFlow::Break(123)); + assert!(matches!( + h.dispatch(crate::deps![ComplexState::Other]).await, + ControlFlow::Continue(_) + )); + } } From 31652aba98eeb224970e3009167b288102e86ed6 Mon Sep 17 00:00:00 2001 From: LasterAlex Date: Fri, 8 Nov 2024 01:26:25 +0200 Subject: [PATCH 2/9] Linked PR + spelling --- CHANGELOG.md | 2 +- src/lib.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad8f115..a981877 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The `Handler::sig` function. - The `di::Injectable::input_types` associated function [**BC**]. - The `DependencyMap::try_get` method ([PR #29](https://github.com/teloxide/dptree/pull/29)). - - More functionality to `case!` macro. Now it can inject almost anything that rust pattern matching can interpret. + - More functionality to `case!` macro. Now it can inject almost anything that rust pattern matching can interpret. ([PR #31](https://github.com/teloxide/dptree/pull/31)) ### Changed diff --git a/src/lib.rs b/src/lib.rs index 53349dd..706f9f3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -177,11 +177,11 @@ macro_rules! _extract_values { ($inner:ident::$variant:ident) => { $inner::$variant }; - // Multiple fields in stuct/enum in braces + // Multiple fields in struct/enum in braces ($inner:ident$(::$variant:ident)? { $($rest:tt)* }) => { $crate::_extract_values!($($rest)*) }; - // Multiple fields in stuct/enum in parentheses + // Multiple fields in struct/enum in parentheses ($value:ident$(::$variant:ident)? ($($rest:tt)*)) => { $crate::_extract_values!($($rest)*) }; From c346ea0b2dde2e21174a5448de03324607bc23d3 Mon Sep 17 00:00:00 2001 From: LasterAlex Date: Fri, 8 Nov 2024 20:12:29 +0200 Subject: [PATCH 3/9] Added .. support to case macro --- src/lib.rs | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 706f9f3..8519bcd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -143,8 +143,9 @@ macro_rules! case { // // The first rule is just a nicer panic // -// The following four rules are escapes from the recursion. All branches end in either -// nothing, a variable from a struct/enum, or in an enum without any values (for filtering reasons). +// The following five rules are escapes from the recursion. +// All branches end in either an expansion, nothing, a variable from a struct/enum, +// or in an enum without any values (for filtering reasons). // // The two rules after that basically take any enum or struct, and dump out the contents to the // next recursion depth. @@ -159,12 +160,16 @@ macro_rules! _extract_values { (None) => { { const _: () = panic!("Cannot infer the type of None, please add None::"); } }; - // Empty value, for trailing comma support - () => { + // Two dots mean expansion. This has to go at the end, otherwise it will mess up the commas. + (..) => { // This is a workaround, rust expects an expression here, but just () can't be returned, so // we trick the compiler to think that we are returning an expression, while not doing it #[cfg(any())] { () } }; + // Empty value, for trailing comma support + () => { + #[cfg(any())] { () } + }; // Singular value ($value:ident) => { $value @@ -335,6 +340,22 @@ mod tests { )); } + #[tokio::test] + async fn handler_dots_expand_struct() { + let input = TestStruct { a: 5, b: State::A }; + let h: crate::Handler<_> = + case![TestStruct { b: State::A, .. }].endpoint(|(state,): (State,)| async move { + assert_eq!(state, State::A); + 123 + }); + + assert_eq!(h.dispatch(crate::deps![input]).await, ControlFlow::Break(123)); + assert!(matches!( + h.dispatch(crate::deps![TestStruct { a: 5, b: State::Other }]).await, + ControlFlow::Continue(_) + )); + } + #[tokio::test] async fn handler_tuple_struct() { let input = TestTupleStruct(6, State::A); From cfef6e04be30efb2e1261dd39ae69f5c291203f8 Mon Sep 17 00:00:00 2001 From: LasterAlex Date: Mon, 18 Nov 2024 03:46:08 +0200 Subject: [PATCH 4/9] Better comments and some fixes --- src/lib.rs | 154 +++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 127 insertions(+), 27 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 8519bcd..20ac690 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -135,70 +135,123 @@ macro_rules! case { }; } -/// A helper macro for [`case!`] to extract values recursively from structs and enums +/// A helper macro for [`case!`] to extract values recursively from structs and enums and returns a +/// tuple with all of the matched values. /// /// [`case!`]: crate::case // // This looks confusing, so here is how this works: // -// The first rule is just a nicer panic +// It's just a recursive TT muncher that takes all of the "endpoint" values in rusts pattern +// matching syntax. // -// The following five rules are escapes from the recursion. -// All branches end in either an expansion, nothing, a variable from a struct/enum, -// or in an enum without any values (for filtering reasons). +// The first three rules are just for `..` support and trailing comma support. ".." and ", .." +// differ because ".." can be at the front, where a lack of a value is an error, but a +// comma indicates that the value before is filled in, and we can return nothing. // -// The two rules after that basically take any enum or struct, and dump out the contents to the -// next recursion depth. +// The fourth rule just removes the comma, all other cases don't care about it, so, to converge the +// two possibilities, we just remove it. // -// The next two rules take the comma separated tokens, -// and wrap a recursion layer aroud every token. E.g.: -// `field1, field2` will become `_extract_values(field1), _extract_values(field2)` +// The sixth, seventh and eighth rules are escapes from the recursion. +// All branches end in either a variable or an enum variant with no values (for filtering reasons). // -// The last rule just takes the value out of `key: value` +// Ninth and tenth rules just extract the inner comma separated values from structs and enums. +// +// Eleventh and twelfth rules take the comma separated values from ninth and tenth rules and TT +// munch on every single one of the values. +// +// Thirteenth and forteenth rules do the same, but if the first field doesn't have a key, like in a +// tuple struct. +// +// Fifteenth and sixteenth rules are for typed values, with and without a key. +// +// The last rule just extracts `key: value` to `value`, the `key` is not needed in the return tuple. #[macro_export] macro_rules! _extract_values { - (None) => { - { const _: () = panic!("Cannot infer the type of None, please add None::"); } - }; - // Two dots mean expansion. This has to go at the end, otherwise it will mess up the commas. + // 1. Two dots mean expansion. This has to go at the end, otherwise it will mess up the commas. (..) => { + () + }; + // 2. Same thing, but no need to return anything because of the comma + (, ..) => { // This is a workaround, rust expects an expression here, but just () can't be returned, so // we trick the compiler to think that we are returning an expression, while not doing it #[cfg(any())] { () } }; - // Empty value, for trailing comma support - () => { + // 3. Trailing comma support + (,) => { #[cfg(any())] { () } }; - // Singular value + // 4. Removing the comma, converging it to all the other cases + (, $($rest:tt)*) => { + $crate::_extract_values!($($rest)*) + }; + // 5. Just a nice panic + (None) => { + { const _: () = panic!("Cannot infer the type of None, please add None::"); } + }; + // 6. Singular value ($value:ident) => { $value }; - // Mostly for None support, but generalized + // 7. Singular value with a type. Mostly for None:: support, but generalized. ($value:ident::<$($ty:ty),*>) => { $value::<$($ty)*> }; - // Enum without any values + // 8. Enum without any values ($inner:ident::$variant:ident) => { $inner::$variant }; - // Multiple fields in struct/enum in braces + // 9. Multiple fields in struct/enum in braces ($inner:ident$(::$variant:ident)? { $($rest:tt)* }) => { $crate::_extract_values!($($rest)*) }; - // Multiple fields in struct/enum in parentheses + // 10. Multiple fields in struct/enum in parentheses ($value:ident$(::$variant:ident)? ($($rest:tt)*)) => { $crate::_extract_values!($($rest)*) }; - // This takes a comma separated token list and wraps a recursion around every item + // 11. This takes a comma separated token list and wraps a recursion around every item. + // + // This basically filters for all of: + // 1. Just an endpoint value: `foo` in `foo, ..` + // 2. A key to a value: `SomeVal` in `foo: SomeVal, ..` + // 3. A key to an enum variant: `State::A` in `foo: State::A, ..` + // 4. A key to a struct enum variant: `State::A { bar }` in `foo: State::A { bar }, ..` + // 5. A key to an struct: `Pos { x, y }` in `foo: Pos { x, y }, ..` + // + // And passes the rest to the TT muncher. + // + // A comma is added in the front of the second `_extract_values` for the 2nd and 3rd rules. + // + // It's impossible to just put everything before a comma under $($var:tt)* because of the + // ambiguity due to $outer. ($param:ident $(: $value:ident$(::$variant:ident)? $({$($inner:tt)*})? )?, $($outer:tt)*) => { - ($crate::_extract_values!($param $(: $value$(::$variant)? $({$($inner)*})? )?), $crate::_extract_values!($($outer)*)) + ($crate::_extract_values!($param $(: $value$(::$variant)? $({$($inner)*})? )?), $crate::_extract_values!(, $($outer)*)) }; - // Same thing, it just works with parentheses instead of braces + // 12. Same thing, it just works with parentheses instead of braces. + // + // We don't need an extra optional group around the braced part to filter the enum variants, + // the previous rule does that already. ($param:ident $(: $value:ident$(::$variant:ident)? ($($inner:tt)*) )?, $($outer:tt)*) => { - ($crate::_extract_values!($param $(: $value$(::$variant)? ($($inner)*) )?), $crate::_extract_values!($($outer)*)) + ($crate::_extract_values!($param $(: $value$(::$variant)? ($($inner)*) )?), $crate::_extract_values!(, $($outer)*)) }; - // Extracts `test: ...` to `...` + // 13. Same as 11, but times where the first value doesn't have a key, like in tuple structs + ($value:ident$(::$variant:ident)? $({$($inner:tt)*})?, $($outer:tt)*) => { + ($crate::_extract_values!($value$(::$variant)? $({$($inner)*})?), $crate::_extract_values!(, $($outer)*)) + }; + // 14. Same as 13, but with parantheses + ($value:ident$(::$variant:ident)? ($($inner:tt)*), $($outer:tt)*) => { + ($crate::_extract_values!($value$(::$variant)? ($($inner)*)), $crate::_extract_values!(, $($outer)*)) + }; + // 15. If the first key is a typed value + ($key:ident : $value:ident::<$($ty:ty),*>, $($outer:tt)*) => { + ($crate::_extract_values!($key: $value::<$($ty),*>), $crate::_extract_values!(, $($outer)*)) + }; + // 16. If the first value is a typed value + ($value:ident::<$($ty:ty),*>, $($outer:tt)*) => { + ($crate::_extract_values!($value::<$($ty),*>), $crate::_extract_values!(, $($outer)*)) + }; + // 17. Extracts `key: value` to `value` ($param:ident : $($value:tt)*) => { $crate::_extract_values!($($value)*) }; @@ -225,6 +278,7 @@ mod tests { C { foo: TestStruct, bar: i32 }, D(Option), E { foo: TestTupleStruct }, + F(Option, i32), Other, } @@ -237,6 +291,9 @@ mod tests { #[derive(Debug, Copy, Clone, Eq, PartialEq)] struct TestTupleStruct(i32, State); + #[derive(Debug, Copy, Clone, Eq, PartialEq)] + struct TestTupleStruct2(State, i32); + #[tokio::test] async fn handler_empty_variant() { let input = State::A; @@ -340,6 +397,15 @@ mod tests { )); } + #[tokio::test] + async fn handler_expansion() { + let input = State::E { foo: 42, bar: "abc" }; + let h: crate::Handler<_> = case![State::E { .. }].endpoint(|| async move { 123 }); + + assert_eq!(h.dispatch(crate::deps![input]).await, ControlFlow::Break(123)); + assert!(matches!(h.dispatch(crate::deps![State::Other]).await, ControlFlow::Continue(_))); + } + #[tokio::test] async fn handler_dots_expand_struct() { let input = TestStruct { a: 5, b: State::A }; @@ -373,6 +439,24 @@ mod tests { )); } + #[tokio::test] + async fn handler_tuple_struct2() { + // To test for the enum in the first field + let input = TestTupleStruct2(State::A, 6); + let h: crate::Handler<_> = + case![TestTupleStruct2(State::A, x)].endpoint(|(state, x): (State, i32)| async move { + assert_eq!(state, State::A); + assert_eq!(x, 6); + 123 + }); + + assert_eq!(h.dispatch(crate::deps![input]).await, ControlFlow::Break(123)); + assert!(matches!( + h.dispatch(crate::deps![TestTupleStruct2(State::Other, 6)]).await, + ControlFlow::Continue(_) + )); + } + // ComplexState tests #[tokio::test] @@ -472,6 +556,22 @@ mod tests { )); } + #[tokio::test] + async fn handler_tuple_struct_none() { + // To test for a typed variable in the first field + let input = ComplexState::F(None, 6); + let h: crate::Handler<_> = case![ComplexState::F(None::, x)].endpoint( + |(x, y): (Option, i32)| async move { + assert_eq!(x, None); + assert_eq!(y, 6); + 123 + }, + ); + + assert_eq!(h.dispatch(crate::deps![input]).await, ControlFlow::Break(123)); + assert!(matches!(h.dispatch(crate::deps![Some(13)]).await, ControlFlow::Continue(_))); + } + #[tokio::test] async fn handler_nested_tuple_struct() { let input = ComplexState::E { foo: TestTupleStruct(100, State::B(9)) }; From 98f3c4375c6c66a7ebd3bc71010bd476676936ab Mon Sep 17 00:00:00 2001 From: LasterAlex Date: Tue, 19 Nov 2024 02:26:47 +0200 Subject: [PATCH 5/9] Fix the many attributes bug --- Cargo.toml | 1 + src/lib.rs | 218 +++++++++++++++++++++++++++++++++++------------------ 2 files changed, 147 insertions(+), 72 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 385fab5..141676c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ license = "MIT" [dependencies] futures = { version = "0.3", default-features = false, features = ["alloc"] } +tuples = "1.16" [dev-dependencies] tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "sync"] } diff --git a/src/lib.rs b/src/lib.rs index 20ac690..63288af 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -129,7 +129,11 @@ macro_rules! case { ($($entry:tt)*) => { $crate::filter_map(|x| match x { $($entry)* - => Some($crate::_extract_values!($($entry)*)), + => { + #[allow(unused_imports)] + use ::tuples::combin::CombinConcat; + Some($crate::_extract_values!($($entry)*)) + }, _ => None, }) }; @@ -145,72 +149,46 @@ macro_rules! case { // It's just a recursive TT muncher that takes all of the "endpoint" values in rusts pattern // matching syntax. // -// The first three rules are just for `..` support and trailing comma support. ".." and ", .." +// The first rule is just for `..` support. // differ because ".." can be at the front, where a lack of a value is an error, but a // comma indicates that the value before is filled in, and we can return nothing. // -// The fourth rule just removes the comma, all other cases don't care about it, so, to converge the -// two possibilities, we just remove it. -// -// The sixth, seventh and eighth rules are escapes from the recursion. +// 3-5 rules are escapes from the recursion. // All branches end in either a variable or an enum variant with no values (for filtering reasons). // -// Ninth and tenth rules just extract the inner comma separated values from structs and enums. -// -// Eleventh and twelfth rules take the comma separated values from ninth and tenth rules and TT -// munch on every single one of the values. -// -// Thirteenth and forteenth rules do the same, but if the first field doesn't have a key, like in a -// tuple struct. +// 6-7 rules feed the inners of structs and enums back to the macro, so that rules 8-13 can munch +// on them. // -// Fifteenth and sixteenth rules are for typed values, with and without a key. +// Speaking of which, 8-13 rules just take comma separated arguments and make them into a tuple. // -// The last rule just extracts `key: value` to `value`, the `key` is not needed in the return tuple. +// 14-15 rules just expand `key: value` to `value` #[macro_export] macro_rules! _extract_values { // 1. Two dots mean expansion. This has to go at the end, otherwise it will mess up the commas. - (..) => { + ($(@intuple)? ..) => { () }; - // 2. Same thing, but no need to return anything because of the comma - (, ..) => { - // This is a workaround, rust expects an expression here, but just () can't be returned, so - // we trick the compiler to think that we are returning an expression, while not doing it - #[cfg(any())] { () } - }; - // 3. Trailing comma support - (,) => { - #[cfg(any())] { () } - }; - // 4. Removing the comma, converging it to all the other cases - (, $($rest:tt)*) => { - $crate::_extract_values!($($rest)*) - }; - // 5. Just a nice panic + // 2. Just a nice panic (None) => { { const _: () = panic!("Cannot infer the type of None, please add None::"); } }; - // 6. Singular value - ($value:ident) => { - $value - }; - // 7. Singular value with a type. Mostly for None:: support, but generalized. - ($value:ident::<$($ty:ty),*>) => { - $value::<$($ty)*> - }; - // 8. Enum without any values - ($inner:ident::$variant:ident) => { - $inner::$variant - }; - // 9. Multiple fields in struct/enum in braces - ($inner:ident$(::$variant:ident)? { $($rest:tt)* }) => { - $crate::_extract_values!($($rest)*) - }; - // 10. Multiple fields in struct/enum in parentheses - ($value:ident$(::$variant:ident)? ($($rest:tt)*)) => { - $crate::_extract_values!($($rest)*) - }; - // 11. This takes a comma separated token list and wraps a recursion around every item. + // 3. Singular value + ($value:ident) => { $value }; + // 4. Singular value with a type. Mostly for None:: support, but generalized. + ($value:ident::<$($ty:ty),*>) => { $value::<$($ty)*> }; + // 5. Enum without any values + ($inner:ident::$variant:ident) => { $inner::$variant }; + // 6. Multiple fields in struct/enum in braces + ($inner:ident$(::$variant:ident)? { $($rest:tt)* }) => { $crate::_extract_values!($($rest)*) }; + // 7. Multiple fields in struct/enum in parentheses + ($value:ident$(::$variant:ident)? ($($rest:tt)*)) => { $crate::_extract_values!($($rest)*) }; + // Same 3-7, but forced to return a tuple + (@intuple $value:ident $(,)?) => { ($value,) }; + (@intuple $value:ident::<$($ty:ty),*> $(,)?) => { ($value::<$($ty)*>,) }; + (@intuple $inner:ident::$variant:ident $(,)?) => { ($inner::$variant,) }; + (@intuple $inner:ident$(::$variant:ident)? { $($rest:tt)* } $(,)?) => { ($crate::_extract_values!($($rest)*),) }; + (@intuple $value:ident$(::$variant:ident)? ($($rest:tt)*) $(,)?) => { ($crate::_extract_values!($($rest)*),) }; + // 8. This takes a comma separated token list and wraps a recursion around every item. // // This basically filters for all of: // 1. Just an endpoint value: `foo` in `foo, ..` @@ -221,40 +199,62 @@ macro_rules! _extract_values { // // And passes the rest to the TT muncher. // - // A comma is added in the front of the second `_extract_values` for the 2nd and 3rd rules. + // Weird `.concat()` syntax is needed because otherwise tuples will stack upon each other: + // (x, (y, z)) instead of (x, y, z). + // + // @intuple tells the macro to always return a tuple, so that `.concat()` can add the result. // // It's impossible to just put everything before a comma under $($var:tt)* because of the // ambiguity due to $outer. - ($param:ident $(: $value:ident$(::$variant:ident)? $({$($inner:tt)*})? )?, $($outer:tt)*) => { - ($crate::_extract_values!($param $(: $value$(::$variant)? $({$($inner)*})? )?), $crate::_extract_values!(, $($outer)*)) + // + // `$($outer:tt)+` has to have a + instead of * because trailing commas exist. + ($param:ident $(: $value:ident$(::$variant:ident)? $({$($inner:tt)*})? )?, $($outer:tt)+) => { + ($crate::_extract_values!($param $(: $value$(::$variant)? $({$($inner)*})? )?),).concat($crate::_extract_values!(@intuple $($outer)+)) }; - // 12. Same thing, it just works with parentheses instead of braces. + // 9. Same thing, it just works with parentheses instead of braces. // // We don't need an extra optional group around the braced part to filter the enum variants, // the previous rule does that already. - ($param:ident $(: $value:ident$(::$variant:ident)? ($($inner:tt)*) )?, $($outer:tt)*) => { - ($crate::_extract_values!($param $(: $value$(::$variant)? ($($inner)*) )?), $crate::_extract_values!(, $($outer)*)) + ($param:ident $(: $value:ident$(::$variant:ident)? ($($inner:tt)*) )?, $($outer:tt)+) => { + ($crate::_extract_values!($param $(: $value$(::$variant)? ($($inner)*) )?),).concat($crate::_extract_values!(@intuple $($outer)+)) }; - // 13. Same as 11, but times where the first value doesn't have a key, like in tuple structs - ($value:ident$(::$variant:ident)? $({$($inner:tt)*})?, $($outer:tt)*) => { - ($crate::_extract_values!($value$(::$variant)? $({$($inner)*})?), $crate::_extract_values!(, $($outer)*)) + // 10. Same as 8, but times where the first value doesn't have a key, like in tuple structs + ($value:ident$(::$variant:ident)? $({$($inner:tt)*})?, $($outer:tt)+) => { + ($crate::_extract_values!($value$(::$variant)? $({$($inner)*})?),).concat($crate::_extract_values!(@intuple $($outer)+)) }; - // 14. Same as 13, but with parantheses - ($value:ident$(::$variant:ident)? ($($inner:tt)*), $($outer:tt)*) => { - ($crate::_extract_values!($value$(::$variant)? ($($inner)*)), $crate::_extract_values!(, $($outer)*)) + // 11. Same as 10, but with parantheses + ($value:ident$(::$variant:ident)? ($($inner:tt)*), $($outer:tt)+) => { + ($crate::_extract_values!($value$(::$variant)? ($($inner)*)),).concat($crate::_extract_values!(@intuple $($outer)+)) }; - // 15. If the first key is a typed value - ($key:ident : $value:ident::<$($ty:ty),*>, $($outer:tt)*) => { - ($crate::_extract_values!($key: $value::<$($ty),*>), $crate::_extract_values!(, $($outer)*)) + // 12. If the first key is a typed value + ($key:ident : $value:ident::<$($ty:ty),*>, $($outer:tt)+) => { + ($crate::_extract_values!($key: $value::<$($ty),*>),).concat($crate::_extract_values!(@intuple $($outer)+)) }; - // 16. If the first value is a typed value - ($value:ident::<$($ty:ty),*>, $($outer:tt)*) => { - ($crate::_extract_values!($value::<$($ty),*>), $crate::_extract_values!(, $($outer)*)) + // 13. If the first value is a typed value + ($value:ident::<$($ty:ty),*>, $($outer:tt)+) => { + ($crate::_extract_values!($value::<$($ty),*>),).concat($crate::_extract_values!(@intuple $($outer)+)) }; - // 17. Extracts `key: value` to `value` + // 14. Extracts `key: value` to `value` ($param:ident : $($value:tt)*) => { $crate::_extract_values!($($value)*) }; + // 15. Same thing, but preserves the fact that this is inside a tuple. + (@intuple $param:ident : $($value:tt)*) => { + $crate::_extract_values!(@intuple $($value)*) + }; + // 16. If it made it this far without getting into any of the previous rules, it means that + // $rest is a sequence of arguments, and we can safely remove @intuple to let it walk + // through rules 8-13, knowing that it will return a tuple to please `.concat()` + (@intuple $($rest:tt)*) => { + $crate::_extract_values!($($rest)*) + }; + // 17. Finally, if a value made it through all of that, but didn't get parsed, it's probably a + // value with a comma at the end. To not have to write another set of rules to account for + // that, it's easier to add @intuple, because it forces the macro to return a tuple no + // matter what. + ($($args:tt)*) => { + $crate::_extract_values!(@intuple $($args)*) + }; } #[cfg(test)] @@ -279,6 +279,8 @@ mod tests { D(Option), E { foo: TestTupleStruct }, F(Option, i32), + G(i32, i32, i32), + H(i32, i32, i32, &'static str, u8, TestStruct, Option), Other, } @@ -569,7 +571,10 @@ mod tests { ); assert_eq!(h.dispatch(crate::deps![input]).await, ControlFlow::Break(123)); - assert!(matches!(h.dispatch(crate::deps![Some(13)]).await, ControlFlow::Continue(_))); + assert!(matches!( + h.dispatch(crate::deps![ComplexState::F(Some(1), 2)]).await, + ControlFlow::Continue(_) + )); } #[tokio::test] @@ -608,4 +613,73 @@ mod tests { ControlFlow::Continue(_) )); } + + #[tokio::test] + async fn test_many_fields() { + let input = ComplexState::G(1, 2, 3); + let h: crate::Handler<_> = + case![ComplexState::G(x, y, z)].endpoint(|(x, y, z): (i32, i32, i32)| async move { + assert_eq!(x, 1); + assert_eq!(y, 2); + assert_eq!(z, 3); + 123 + }); + + assert_eq!(h.dispatch(crate::deps![input]).await, ControlFlow::Break(123)); + assert!(matches!( + h.dispatch(crate::deps![ComplexState::Other]).await, + ControlFlow::Continue(_) + )); + } + + #[tokio::test] + async fn test_overkill() { + let input = ComplexState::H( + 1, + 2, + 3, + "345", + 4, + TestStruct { a: 5, b: State::E { foo: 6, bar: "123" } }, + Some(State::A), + ); + let h: crate::Handler<_> = + case![ComplexState::H( + a, + b, + c, + d, + e, + // a: f renames the already taken "a" to "f" + TestStruct { a: f, b: State::E { .. } }, + Some(State::A), + )] + .endpoint( + |(a, b, c, d, e, (f, ()), g): ( + i32, + i32, + i32, + &'static str, + u8, + (u32, ()), + State, + )| async move { + assert_eq!(a, 1); + assert_eq!(b, 2); + assert_eq!(c, 3); + assert_eq!(d, "345"); + assert_eq!(e, 4); + assert_eq!(f, 5); + assert_eq!(f, 5); + assert_eq!(g, State::A); + 123 + }, + ); + + assert_eq!(h.dispatch(crate::deps![input]).await, ControlFlow::Break(123)); + assert!(matches!( + h.dispatch(crate::deps![ComplexState::Other]).await, + ControlFlow::Continue(_) + )); + } } From c419f83f223995d38e829b1c4726ddf92381b622 Mon Sep 17 00:00:00 2001 From: LasterAlex Date: Tue, 19 Nov 2024 02:55:30 +0200 Subject: [PATCH 6/9] Improved deps + added new documentation for case! --- Cargo.toml | 2 +- src/lib.rs | 22 +++++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 141676c..f0b122b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ license = "MIT" [dependencies] futures = { version = "0.3", default-features = false, features = ["alloc"] } -tuples = "1.16" +tuples = { version = "1.16", default-features = false, features = ["combin"] } [dev-dependencies] tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "sync"] } diff --git a/src/lib.rs b/src/lib.rs index 63288af..4985d4f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -88,6 +88,23 @@ pub use handler::*; /// - For `Enum::MyVariant(param1, ..., paramN)` and `Enum::MyVariant { param1, /// ..., paramN }`, the payload is `(param1, ..., paramN)` (where `N`>1). /// +/// You can also go with even more complexity and write a syntax similar to rusts pattern matching +/// system: +/// +/// - For `Enum::MyVariant(SecondEnum::MyVariant)` and +/// `Enum::MyVariant { param: SecondEnum::MyVariant }`, the payload is `SecondEnum::MyVariant` +/// - For `Enum::MyVariant { param1, param2: SomeStruct { param3 } }`, the payload is `(param1, +/// param3)` +/// - For `Enum::MyVariant { param1, .. }` and `Enum::MyVariant( param1, .. )`, the payload is +/// `(param1,)` +/// - For `Enum::MyVariant { param1: Some(param2) }` and `Enum::MyVariant( Some(param2) )`, the payload is +/// `param2` +/// - For `Enum::MyVariant { param1: None:: }` and `Enum::MyVariant( None:: )`, the payload is +/// `Option` that is equal to None:: +/// +/// It's recursive, so on every parameter you can insert a new struct, tuple struct, enum variant or +/// anything else. +/// /// ## Dependency requirements /// /// - Your enumeration `Enum`. @@ -126,6 +143,9 @@ pub use handler::*; /// [`examples/purchase.rs`]: https://github.com/teloxide/teloxide/blob/master/examples/purchase.rs #[macro_export] macro_rules! case { + ($($variant:ident)::+) => { + $crate::filter(|x| matches!(x, $($variant)::+)) + }; ($($entry:tt)*) => { $crate::filter_map(|x| match x { $($entry)* @@ -402,7 +422,7 @@ mod tests { #[tokio::test] async fn handler_expansion() { let input = State::E { foo: 42, bar: "abc" }; - let h: crate::Handler<_> = case![State::E { .. }].endpoint(|| async move { 123 }); + let h: crate::Handler<_> = case![State::E { .. }].endpoint(|(): ()| async move { 123 }); assert_eq!(h.dispatch(crate::deps![input]).await, ControlFlow::Break(123)); assert!(matches!(h.dispatch(crate::deps![State::Other]).await, ControlFlow::Continue(_))); From ecaddc0299cf2458877d9d2a7831d3114338e972 Mon Sep 17 00:00:00 2001 From: LasterAlex Date: Tue, 19 Nov 2024 03:05:01 +0200 Subject: [PATCH 7/9] Add test --- src/lib.rs | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 4985d4f..261755a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -702,4 +702,54 @@ mod tests { ControlFlow::Continue(_) )); } + + #[tokio::test] + async fn handler_overkill2() { + let input = ComplexState::H( + 1, + 2, + 3, + "deep", + 255, + TestStruct { a: 42, b: State::B(100) }, + Some(State::C(10, "inner")), + ); + let h: crate::Handler<_> = case![ComplexState::H( + x, + y, + z, + w, + u, + TestStruct { a, b: State::B(v) }, + Some(State::C(c, d)) + )] + .endpoint( + |(x, y, z, w, u, (a, v), (c, d)): ( + i32, + i32, + i32, + &'static str, + u8, + (u32, i32), + (i32, &'static str), + )| async move { + assert_eq!(x, 1); + assert_eq!(y, 2); + assert_eq!(z, 3); + assert_eq!(w, "deep"); + assert_eq!(u, 255); + assert_eq!(a, 42); + assert_eq!(v, 100); + assert_eq!(c, 10); + assert_eq!(d, "inner"); + 123 + }, + ); + + assert_eq!(h.dispatch(crate::deps![input]).await, ControlFlow::Break(123)); + assert!(matches!( + h.dispatch(crate::deps![ComplexState::Other]).await, + ControlFlow::Continue(_) + )); + } } From 817e41dc39a6281ce4b1e96fefd9b48aef9d8157 Mon Sep 17 00:00:00 2001 From: LasterAlex Date: Fri, 6 Dec 2024 13:50:41 +0200 Subject: [PATCH 8/9] Types comma fix and better compile error --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 261755a..b84bb11 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -190,12 +190,12 @@ macro_rules! _extract_values { }; // 2. Just a nice panic (None) => { - { const _: () = panic!("Cannot infer the type of None, please add None::"); } + { compile_error!("Cannot infer the type of None, please add None::"); } }; // 3. Singular value ($value:ident) => { $value }; // 4. Singular value with a type. Mostly for None:: support, but generalized. - ($value:ident::<$($ty:ty),*>) => { $value::<$($ty)*> }; + ($value:ident::<$($ty:ty),*>) => { $value::<$($ty),*> }; // 5. Enum without any values ($inner:ident::$variant:ident) => { $inner::$variant }; // 6. Multiple fields in struct/enum in braces From ae9b911b476fca4b1339a08bbb951b7c938776ee Mon Sep 17 00:00:00 2001 From: hirrolot Date: Sun, 9 Feb 2025 19:39:10 -0500 Subject: [PATCH 9/9] Small documentation improvements --- src/lib.rs | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b84bb11..b2d70a0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -88,22 +88,22 @@ pub use handler::*; /// - For `Enum::MyVariant(param1, ..., paramN)` and `Enum::MyVariant { param1, /// ..., paramN }`, the payload is `(param1, ..., paramN)` (where `N`>1). /// -/// You can also go with even more complexity and write a syntax similar to rusts pattern matching -/// system: +/// You can also go with even more complexity and write a syntax similar to +/// rusts pattern matching system: /// -/// - For `Enum::MyVariant(SecondEnum::MyVariant)` and -/// `Enum::MyVariant { param: SecondEnum::MyVariant }`, the payload is `SecondEnum::MyVariant` -/// - For `Enum::MyVariant { param1, param2: SomeStruct { param3 } }`, the payload is `(param1, -/// param3)` -/// - For `Enum::MyVariant { param1, .. }` and `Enum::MyVariant( param1, .. )`, the payload is -/// `(param1,)` -/// - For `Enum::MyVariant { param1: Some(param2) }` and `Enum::MyVariant( Some(param2) )`, the payload is -/// `param2` -/// - For `Enum::MyVariant { param1: None:: }` and `Enum::MyVariant( None:: )`, the payload is -/// `Option` that is equal to None:: +/// - For `Enum::MyVariant(SecondEnum::MyVariant)` and `Enum::MyVariant { +/// param: SecondEnum::MyVariant }`, the payload is `SecondEnum::MyVariant` +/// - For `Enum::MyVariant { param1, param2: SomeStruct { param3 } }`, the +/// payload is `(param1, param3)` +/// - For `Enum::MyVariant { param1, .. }` and `Enum::MyVariant( param1, .. )`, +/// the payload is `(param1,)` +/// - For `Enum::MyVariant { param1: Some(param2) }` and `Enum::MyVariant( +/// Some(param2) )`, the payload is `param2` +/// - For `Enum::MyVariant { param1: None:: }` and `Enum::MyVariant( +/// None:: )`, the payload is `Option` that is equal to `None::` /// -/// It's recursive, so on every parameter you can insert a new struct, tuple struct, enum variant or -/// anything else. +/// It's recursive, so on every parameter you can insert a new struct, tuple +/// struct, enum variant or anything else. /// /// ## Dependency requirements /// @@ -159,8 +159,8 @@ macro_rules! case { }; } -/// A helper macro for [`case!`] to extract values recursively from structs and enums and returns a -/// tuple with all of the matched values. +/// A helper macro for [`case!`] to extract values recursively from structs and +/// enums and returns a tuple with all of the matched values. /// /// [`case!`]: crate::case // @@ -183,6 +183,7 @@ macro_rules! case { // // 14-15 rules just expand `key: value` to `value` #[macro_export] +#[doc(hidden)] macro_rules! _extract_values { // 1. Two dots mean expansion. This has to go at the end, otherwise it will mess up the commas. ($(@intuple)? ..) => {