diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index cb8e2b5..ace2802 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -11,12 +11,35 @@ env: jobs: build: - runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - name: Clippy + run: cargo clippy --all --all-features --all-targets --examples + - name: Tests + run: cargo test --all + + wasm: + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Clippy - run: cargo clippy --all --all-features --all-targets --examples - - name: Tests - run: cargo test --all + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + components: clippy + + - name: Clippy (wasm32) + run: cargo clippy --all --all-features --all-targets --examples --target wasm32-unknown-unknown + + - name: Install wasm-pack + uses: jetli/wasm-pack-action@v0.4.0 + + - name: Tests (wasm32, node) + # doctests aren't run here: rustdoc's doctest harness doesn't integrate + # cleanly with wasm-bindgen-test-runner (should_panic detection breaks). + # Doctests are still covered by the native `build` job above so it should be safe. + run: wasm-pack test --node --lib diff --git a/CHANGELOG.md b/CHANGELOG.md index 743a1cf..e01ab2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Support for the trailing comma in the `deps` macro ([PR #37](https://github.com/teloxide/dptree/pull/37)). + - Support for the `wasm32-unknown-unknown` target via `MaybeSend`/`MaybeSync` traits and a conditional `BoxFuture` alias ([PR #38](https://github.com/teloxide/dptree/issues/38)). ## 0.5.1 - 2025-07-10 diff --git a/Cargo.toml b/Cargo.toml index 1e27200..901374c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,9 +17,12 @@ futures = { version = "0.3", default-features = false, features = ["alloc"] } colored = "3" [dev-dependencies] -tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "sync"] } +tokio = { version = "1", features = ["rt", "macros", "sync"] } maplit = "1" +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +wasm-bindgen-test = "0.3" + [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs", "-Znormalize-docs"] diff --git a/examples/diagnostics.rs b/examples/diagnostics.rs index 2aff278..f4b6369 100644 --- a/examples/diagnostics.rs +++ b/examples/diagnostics.rs @@ -9,7 +9,7 @@ struct C; #[derive(Clone)] struct D; -#[tokio::main] +#[tokio::main(flavor = "current_thread")] async fn main() { let h: Handler = dptree::entry().map(|_: A| B).inspect(|_: C| ()).endpoint(|| async { D }); diff --git a/examples/simple_dispatcher.rs b/examples/simple_dispatcher.rs index 03ab5b1..1bb8c60 100644 --- a/examples/simple_dispatcher.rs +++ b/examples/simple_dispatcher.rs @@ -27,7 +27,7 @@ use std::{ use dptree::prelude::*; -#[tokio::main] +#[tokio::main(flavor = "current_thread")] async fn main() { let store = Arc::new(AtomicI32::new(0)); diff --git a/examples/state_machine.rs b/examples/state_machine.rs index 2dba927..13e7fd3 100644 --- a/examples/state_machine.rs +++ b/examples/state_machine.rs @@ -8,7 +8,7 @@ use std::{ use dptree::prelude::*; -#[tokio::main] +#[tokio::main(flavor = "current_thread")] async fn main() { let state = CommandState::Inactive; diff --git a/examples/storage.rs b/examples/storage.rs index f1f7ea3..6f136e0 100644 --- a/examples/storage.rs +++ b/examples/storage.rs @@ -1,10 +1,14 @@ -use dptree::prelude::*; -use std::net::Ipv4Addr; +#[cfg(target_arch = "wasm32")] +fn main() { + // This example uses tokio::spawn/multithreading, which isn't + // supported on wasm32-unknown-unknown. +} +#[cfg(not(target_arch = "wasm32"))] fn assert_num_string_handler( expected_num: u32, expected_string: &'static str, -) -> Endpoint<'static, ()> { +) -> dptree::Endpoint<'static, ()> { // The handler requires `u32` and `String` types from the input storage. dptree::endpoint(move |num: u32, string: String| async move { assert_eq!(num, expected_num); @@ -12,7 +16,8 @@ fn assert_num_string_handler( }) } -#[tokio::main] +#[cfg(not(target_arch = "wasm32"))] +#[tokio::main(flavor = "current_thread")] async fn main() { // Init the storage with `u32` and `String` values. let store = dptree::deps![10u32, "Hello".to_owned()]; @@ -23,9 +28,10 @@ async fn main() { // This will cause a panic because we do not store `Ipv4Addr` in out store. let handle = tokio::spawn(async move { - let ip_handler: Endpoint<_> = dptree::endpoint(|ip: Ipv4Addr| async move { - assert_eq!(ip, Ipv4Addr::new(0, 0, 0, 0)); - }); + let ip_handler: dptree::Endpoint<_> = + dptree::endpoint(|ip: std::net::Ipv4Addr| async move { + assert_eq!(ip, std::net::Ipv4Addr::new(0, 0, 0, 0)); + }); let _ = ip_handler.dispatch(store).await; }); let result = handle.await; diff --git a/examples/web_server.rs b/examples/web_server.rs index e905e49..628677c 100644 --- a/examples/web_server.rs +++ b/examples/web_server.rs @@ -3,7 +3,7 @@ use dptree::prelude::*; type WebHandler = Endpoint<'static, String>; #[rustfmt::skip] -#[tokio::main] +#[tokio::main(flavor = "current_thread")] async fn main() { let web_server = dptree::entry() .branch(smiles_handler()) diff --git a/src/di.rs b/src/di.rs index b983e49..7bdf57a 100644 --- a/src/di.rs +++ b/src/di.rs @@ -13,7 +13,9 @@ //! [dependency injection]: https://en.wikipedia.org/wiki/Dependency_injection //! [this discussion on StackOverflow]: https://stackoverflow.com/questions/130794/what-is-dependency-injection -use futures::future::{ready, BoxFuture}; +use futures::future::ready; + +use crate::send::{BoxFuture, MaybeSend, MaybeSync}; use std::{ any::{Any, TypeId}, @@ -93,7 +95,7 @@ impl PartialEq for DependencyMap { fn eq(&self, other: &Self) -> bool { let keys1 = self.map.keys(); let keys2 = other.map.keys(); - keys1.len() == keys2.len() && keys1.zip(keys2).map(|(k1, k2)| k1 == k2).all(|x| x) + keys1.len() == keys2.len() && keys1.zip(keys2).all(|(k1, k2)| k1 == k2) } } @@ -201,7 +203,10 @@ where } /// A function with all dependencies satisfied. +#[cfg(not(target_arch = "wasm32"))] pub type CompiledFn<'a, Output> = Arc BoxFuture<'a, Output> + Send + Sync + 'a>; +#[cfg(target_arch = "wasm32")] +pub type CompiledFn<'a, Output> = Arc BoxFuture<'a, Output> + 'a>; /// Turns a synchronous function into a type that implements [`Injectable`]. pub struct Asyncify(pub F); @@ -210,8 +215,8 @@ macro_rules! impl_into_di { ($($generic:ident),*) => { impl Injectable for Func where - Func: Fn($($generic),*) -> Fut + Send + Sync + 'static, - Fut: Future + Send + 'static, + Func: Fn($($generic),*) -> Fut + MaybeSend + MaybeSync + 'static, + Fut: Future + MaybeSend + 'static, Output: 'static, $($generic: Clone + Send + Sync + 'static),* { @@ -234,8 +239,8 @@ macro_rules! impl_into_di { impl Injectable for Asyncify where - Func: Fn($($generic),*) -> Output + Send + Sync + 'static, - Output: Send + 'static, + Func: Fn($($generic),*) -> Output + MaybeSend + MaybeSync + 'static, + Output: MaybeSend + 'static, $($generic: Clone + Send + Sync + 'static),* { #[allow(non_snake_case)] @@ -306,38 +311,37 @@ macro_rules! deps { mod tests { use super::*; - #[test] - fn get() { - let mut map = DependencyMap::new(); - map.insert(42i32); - map.insert("hello world"); - map.insert_container(deps![true]); + crate::cross_test! { + fn get() { + let mut map = DependencyMap::new(); + map.insert(42i32); + map.insert("hello world"); + map.insert_container(deps![true]); - assert_eq!(map.get(), Arc::new(42i32)); - assert_eq!(map.get(), Arc::new("hello world")); - assert_eq!(map.get(), Arc::new(true)); - } + assert_eq!(map.get(), Arc::new(42i32)); + assert_eq!(map.get(), Arc::new("hello world")); + assert_eq!(map.get(), Arc::new(true)); + } - #[test] - fn try_get() { - let mut map = DependencyMap::new(); - assert_eq!(map.try_get::(), None); - map.insert(42i32); - assert_eq!(map.try_get(), Some(Arc::new(42i32))); - assert_eq!(map.try_get::(), None); - } + fn try_get() { + let mut map = DependencyMap::new(); + assert_eq!(map.try_get::(), None); + map.insert(42i32); + assert_eq!(map.try_get(), Some(Arc::new(42i32))); + assert_eq!(map.try_get::(), None); + } - #[test] - fn same_keys() { - let mut map_bool1 = DependencyMap::new(); - let mut map_bool2 = DependencyMap::new(); - let map_empty = DependencyMap::new(); + fn same_keys() { + let mut map_bool1 = DependencyMap::new(); + let mut map_bool2 = DependencyMap::new(); + let map_empty = DependencyMap::new(); - map_bool1.insert(false); - map_bool2.insert(true); + map_bool1.insert(false); + map_bool2.insert(true); - assert_eq!(map_bool1, map_bool2); - assert_ne!(map_bool1, map_empty); - assert_ne!(map_bool2, map_empty); + assert_eq!(map_bool1, map_bool2); + assert_ne!(map_bool1, map_empty); + assert_ne!(map_bool2, map_empty); + } } } diff --git a/src/handler/core.rs b/src/handler/core.rs index ec74358..d4fd421 100644 --- a/src/handler/core.rs +++ b/src/handler/core.rs @@ -3,7 +3,12 @@ #[cfg(test)] const FIXED_LOCATION: &Location = Location::caller(); -use crate::{description, prelude::DependencyMap, HandlerDescription}; +use crate::{ + description, + prelude::DependencyMap, + send::{BoxFuture, MaybeSend, MaybeSync}, + HandlerDescription, +}; use std::{ any::TypeId, @@ -17,7 +22,6 @@ use std::{ }; use colored::Colorize; -use futures::future::BoxFuture; /// An instance that receives an input and decides whether to break a chain or /// pass the value further. @@ -120,12 +124,19 @@ impl Hash for Type { } } +#[cfg(not(target_arch = "wasm32"))] type DynFn<'a, Output> = dyn Fn(DependencyMap, Cont<'a, Output>) -> HandlerResult<'a, Output> + Send + Sync + 'a; +#[cfg(target_arch = "wasm32")] +type DynFn<'a, Output> = dyn Fn(DependencyMap, Cont<'a, Output>) -> HandlerResult<'a, Output> + 'a; /// A continuation representing the rest of a handler chain. -pub type Cont<'a, Output> = +pub type Cont<'a, Output> = ContInner<'a, Output>; +#[cfg(not(target_arch = "wasm32"))] +type ContInner<'a, Output> = Box HandlerResult<'a, Output> + Send + Sync + 'a>; +#[cfg(target_arch = "wasm32")] +type ContInner<'a, Output> = Box HandlerResult<'a, Output> + 'a>; /// An output type produced by a handler. pub type HandlerResult<'a, Output> = BoxFuture<'a, ControlFlow>; @@ -157,7 +168,7 @@ where /// # Examples /// /// ``` - /// # #[tokio::main] + /// # #[tokio::main(flavor = "current_thread")] /// # async fn main() { /// use dptree::prelude::*; /// @@ -293,7 +304,7 @@ where /// ``` /// use dptree::prelude::*; /// - /// # #[tokio::main] + /// # #[tokio::main(flavor = "current_thread")] /// # async fn main() { /// /// #[derive(Debug, PartialEq)] @@ -321,7 +332,7 @@ where #[track_caller] pub fn branch(self, next: Self) -> Self where - Output: Send, + Output: MaybeSend, { let required_update_kinds_set = self.description().merge_branch(next.description()); @@ -416,7 +427,7 @@ where /// # Examples /// /// ``` - /// # #[tokio::main] + /// # #[tokio::main(flavor = "current_thread")] /// # async fn main() { /// use dptree::prelude::*; /// @@ -434,8 +445,8 @@ where ) -> ControlFlow where Cont: FnOnce(DependencyMap) -> ContFut, - Cont: Send + Sync + 'a, - ContFut: Future> + Send + 'a, + Cont: MaybeSend + MaybeSync + 'a, + ContFut: Future> + MaybeSend + 'a, { (self.data.f)(input, Box::new(|event| Box::pin(cont(event)))).await } @@ -504,8 +515,8 @@ impl Type { pub fn from_fn<'a, F, Fut, Output, Descr>(f: F, sig: HandlerSignature) -> Handler<'a, Output, Descr> where F: Fn(DependencyMap, Cont<'a, Output>) -> Fut, - F: Send + Sync + 'a, - Fut: Future> + Send + 'a, + F: MaybeSend + MaybeSync + 'a, + Fut: Future> + MaybeSend + 'a, Descr: HandlerDescription, { from_fn_with_description(Descr::user_defined(), f, sig) @@ -520,8 +531,8 @@ pub fn from_fn_with_description<'a, F, Fut, Output, Descr>( ) -> Handler<'a, Output, Descr> where F: Fn(DependencyMap, Cont<'a, Output>) -> Fut, - F: Send + Sync + 'a, - Fut: Future> + Send + 'a, + F: MaybeSend + MaybeSync + 'a, + Fut: Future> + MaybeSend + 'a, { Handler { data: Arc::new(HandlerData { @@ -627,13 +638,7 @@ pub fn type_check(sig: &HandlerSignature, container: &DependencyMap, assumptions missing_types_msg.red().bold().to_string() }, print_types( - obligations.iter().filter_map(|(ty, _location)| { - if !container_types.contains(ty) { - Some(ty) - } else { - None - } - }), + obligations.keys().filter(|ty| !container_types.contains(ty)), |ty| { format!("`{}` from {}", ty.name, obligations[ty]) }, ), note_msg, @@ -650,6 +655,7 @@ pub(crate) fn help_inference(h: Handler) -> Handler { #[cfg(test)] mod tests { + use super::*; use crate::{ @@ -661,305 +667,298 @@ mod tests { use maplit::{btreemap, btreeset, hashset}; - #[tokio::test] - async fn test_from_fn_break() { - let input = 123; - let output = "ABC"; + crate::cross_test! { + async fn test_from_fn_break() { + let input = 123; + let output = "ABC"; - let input_types = [Type::of::()]; - let location = Location::caller(); + let input_types = [Type::of::()]; + let location = Location::caller(); - let result = help_inference(from_fn( - |event, _cont: Cont<&'static str>| async move { - assert_eq!(event, deps![input]); - ControlFlow::Break(output) - }, - HandlerSignature::Other { - obligations: BTreeMap::from_iter( - input_types.iter().cloned().map(|ty| (ty, location)), - ), - guaranteed_outcomes: btreeset! {}, - conditional_outcomes: btreeset! {}, - continues: false, - }, - )) - .dispatch(deps![input]) - .await; + let result = help_inference(from_fn( + |event, _cont: Cont<&'static str>| async move { + assert_eq!(event, deps![input]); + ControlFlow::Break(output) + }, + HandlerSignature::Other { + obligations: BTreeMap::from_iter( + input_types.iter().cloned().map(|ty| (ty, location)), + ), + guaranteed_outcomes: btreeset! {}, + conditional_outcomes: btreeset! {}, + continues: false, + }, + )) + .dispatch(deps![input]) + .await; - assert!(result == ControlFlow::Break(output)); - } + assert!(result == ControlFlow::Break(output)); + } - #[tokio::test] - async fn test_from_fn_continue() { - let input = 123; - type Output = &'static str; + async fn test_from_fn_continue() { + let input = 123; + type Output = &'static str; - let input_types = [Type::of::()]; - let location = Location::caller(); + let input_types = [Type::of::()]; + let location = Location::caller(); - let result = help_inference(from_fn( - |event, _cont: Cont<&'static str>| async move { - assert_eq!(event, deps![input]); - ControlFlow::::Continue(event) - }, - HandlerSignature::Other { - obligations: BTreeMap::from_iter( - input_types.iter().cloned().map(|ty| (ty, location)), - ), - guaranteed_outcomes: btreeset! {Type::of::()}, - conditional_outcomes: btreeset! {}, - continues: true, - }, - )) - .dispatch(deps![input]) - .await; + let result = help_inference(from_fn( + |event, _cont: Cont<&'static str>| async move { + assert_eq!(event, deps![input]); + ControlFlow::::Continue(event) + }, + HandlerSignature::Other { + obligations: BTreeMap::from_iter( + input_types.iter().cloned().map(|ty| (ty, location)), + ), + guaranteed_outcomes: btreeset! {Type::of::()}, + conditional_outcomes: btreeset! {}, + continues: true, + }, + )) + .dispatch(deps![input]) + .await; - assert!(result == ControlFlow::Continue(deps![input])); - } + assert!(result == ControlFlow::Continue(deps![input])); + } - #[tokio::test] - async fn test_entry() { - let input = 123; - type Output = &'static str; + async fn test_entry() { + let input = 123; + type Output = &'static str; - let result = help_inference(entry::()).dispatch(deps![input]).await; + let result = help_inference(entry::()).dispatch(deps![input]).await; - assert!(result == ControlFlow::Continue(deps![input])); - } + assert!(result == ControlFlow::Continue(deps![input])); + } - #[tokio::test] - async fn test_execute() { - let input = 123; - let output = "ABC"; + async fn test_execute() { + let input = 123; + let output = "ABC"; - let input_types = [Type::of::()]; - let location = Location::caller(); + let input_types = [Type::of::()]; + let location = Location::caller(); - let result = help_inference(from_fn( - |event, cont| { + let result = help_inference(from_fn( + |event, cont| { + assert!(event == deps![input]); + cont(event) + }, + HandlerSignature::Other { + obligations: BTreeMap::from_iter( + input_types.iter().cloned().map(|ty| (ty, location)), + ), + guaranteed_outcomes: btreeset! {Type::of::()}, + conditional_outcomes: btreeset! {}, + continues: true, + }, + )) + .execute(deps![input], |event| async move { assert!(event == deps![input]); - cont(event) - }, - HandlerSignature::Other { - obligations: BTreeMap::from_iter( - input_types.iter().cloned().map(|ty| (ty, location)), - ), - guaranteed_outcomes: btreeset! {Type::of::()}, - conditional_outcomes: btreeset! {}, - continues: true, - }, - )) - .execute(deps![input], |event| async move { - assert!(event == deps![input]); - ControlFlow::Break(output) - }) - .await; - - assert!(result == ControlFlow::Break(output)); - } + ControlFlow::Break(output) + }) + .await; - #[tokio::test] - async fn test_deeply_nested_tree() { - #[derive(Debug, PartialEq)] - enum Output { - LT, - MinusOne, - Zero, - One, - GT, + assert!(result == ControlFlow::Break(output)); } - let negative_handler = filter(|num: i32| num < 0) - .branch( - filter_async(|num: i32| async move { num == -1 }) - .endpoint(|| async move { Output::MinusOne }), - ) - .branch(endpoint(|| async move { Output::LT })); - - let zero_handler = filter_async(|num: i32| async move { num == 0 }) - .endpoint(|| async move { Output::Zero }); - - let positive_handler = filter_async(|num: i32| async move { num > 0 }) - .branch( - filter_async(|num: i32| async move { num == 1 }) - .endpoint(|| async move { Output::One }), - ) - .branch(endpoint(|| async move { Output::GT })); - - let dispatcher = help_inference(entry()) - .branch(negative_handler) - .branch(zero_handler) - .branch(positive_handler); - - assert_eq!(dispatcher.dispatch(deps![2]).await, ControlFlow::Break(Output::GT)); - assert_eq!(dispatcher.dispatch(deps![1]).await, ControlFlow::Break(Output::One)); - assert_eq!(dispatcher.dispatch(deps![0]).await, ControlFlow::Break(Output::Zero)); - assert_eq!(dispatcher.dispatch(deps![-1]).await, ControlFlow::Break(Output::MinusOne)); - assert_eq!(dispatcher.dispatch(deps![-2]).await, ControlFlow::Break(Output::LT)); - } + async fn test_deeply_nested_tree() { + #[derive(Debug, PartialEq)] + enum Output { + LT, + MinusOne, + Zero, + One, + GT, + } - #[tokio::test] - async fn allowed_updates() { - use crate::description::{EventKind, InterestSet}; - use UpdateKind::*; + let negative_handler = filter(|num: i32| num < 0) + .branch( + filter_async(|num: i32| async move { num == -1 }) + .endpoint(|| async move { Output::MinusOne }), + ) + .branch(endpoint(|| async move { Output::LT })); - #[derive(Debug, Clone, PartialEq, Eq, Hash)] - enum UpdateKind { - A, - B, - C, + let zero_handler = filter_async(|num: i32| async move { num == 0 }) + .endpoint(|| async move { Output::Zero }); + + let positive_handler = filter_async(|num: i32| async move { num > 0 }) + .branch( + filter_async(|num: i32| async move { num == 1 }) + .endpoint(|| async move { Output::One }), + ) + .branch(endpoint(|| async move { Output::GT })); + + let dispatcher = help_inference(entry()) + .branch(negative_handler) + .branch(zero_handler) + .branch(positive_handler); + + assert_eq!(dispatcher.dispatch(deps![2]).await, ControlFlow::Break(Output::GT)); + assert_eq!(dispatcher.dispatch(deps![1]).await, ControlFlow::Break(Output::One)); + assert_eq!(dispatcher.dispatch(deps![0]).await, ControlFlow::Break(Output::Zero)); + assert_eq!(dispatcher.dispatch(deps![-1]).await, ControlFlow::Break(Output::MinusOne)); + assert_eq!(dispatcher.dispatch(deps![-2]).await, ControlFlow::Break(Output::LT)); } - impl EventKind for UpdateKind { - fn full_set() -> HashSet { - hashset! { A, B, C } + async fn allowed_updates() { + use crate::description::{EventKind, InterestSet}; + use UpdateKind::*; + + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + enum UpdateKind { + A, + B, + C, + } + + impl EventKind for UpdateKind { + fn full_set() -> HashSet { + hashset! { A, B, C } + } + + fn empty_set() -> HashSet { + hashset! {} + } } - fn empty_set() -> HashSet { - hashset! {} + #[derive(Clone)] + #[allow(dead_code)] + enum Update { + A(i32), + B(u8), + C(u64), } - } - #[derive(Clone)] - #[allow(dead_code)] - enum Update { - A(i32), - B(u8), - C(u64), - } + fn filter_a() -> Handler<'static, Out, InterestSet> + where + Out: Send + Sync + 'static, + { + filter_map_with_description( + InterestSet::new_filter(hashset! { A }), + |update: Update| match update { + Update::A(x) => Some(x), + _ => None, + }, + ) + } - fn filter_a() -> Handler<'static, Out, InterestSet> - where - Out: Send + Sync + 'static, - { - filter_map_with_description( - InterestSet::new_filter(hashset! { A }), - |update: Update| match update { - Update::A(x) => Some(x), - _ => None, - }, - ) - } + fn filter_b() -> Handler<'static, Out, InterestSet> + where + Out: Send + Sync + 'static, + { + filter_map_with_description( + InterestSet::new_filter(hashset! { B }), + |update: Update| match update { + Update::B(x) => Some(x), + _ => None, + }, + ) + } - fn filter_b() -> Handler<'static, Out, InterestSet> - where - Out: Send + Sync + 'static, - { - filter_map_with_description( - InterestSet::new_filter(hashset! { B }), - |update: Update| match update { - Update::B(x) => Some(x), - _ => None, - }, - ) - } + fn filter_c() -> Handler<'static, Out, InterestSet> + where + Out: Send + Sync + 'static, + { + filter_map_with_description( + InterestSet::new_filter(hashset! { C }), + |update: Update| match update { + Update::B(x) => Some(x), + _ => None, + }, + ) + } - fn filter_c() -> Handler<'static, Out, InterestSet> - where - Out: Send + Sync + 'static, - { - filter_map_with_description( - InterestSet::new_filter(hashset! { C }), - |update: Update| match update { + // User-defined filter that doesn't provide allowed updates + fn user_defined_filter() -> Handler<'static, Out, InterestSet> + where + Out: Send + Sync + 'static, + { + filter_map(|update: Update| match update { Update::B(x) => Some(x), _ => None, - }, - ) - } + }) + } - // User-defined filter that doesn't provide allowed updates - fn user_defined_filter() -> Handler<'static, Out, InterestSet> - where - Out: Send + Sync + 'static, - { - filter_map(|update: Update| match update { - Update::B(x) => Some(x), - _ => None, - }) - } + #[track_caller] + fn assert( + handler: Handler<'static, (), description::InterestSet>, + allowed: HashSet, + ) { + assert_eq!(handler.description().observed, allowed); + } - #[track_caller] - fn assert( - handler: Handler<'static, (), description::InterestSet>, - allowed: HashSet, - ) { - assert_eq!(handler.description().observed, allowed); - } + // Filters do not observe anything on their own. + assert(filter_a(), hashset! {}); + assert(entry().chain(filter_b()), hashset! {}); + assert(filter_a().chain(filter_b()), hashset! {}); + assert(filter_a().branch(filter_b()), hashset! {}); + assert(filter_a().branch(filter_b()).branch(filter_c().chain(filter_c())), hashset! {}); + + // Anything user-defined observes everything that it can observe. + assert(filter_a().chain(filter(|| true)), hashset! { A }); + assert(user_defined_filter().chain(filter_a()), hashset! { A, B, C }); + assert(filter_a().chain(user_defined_filter()), hashset! { A }); + assert( + entry().branch(filter_a()).branch(filter_b()).chain(user_defined_filter()), + hashset! { A, B, C }, + ); + assert( + entry() + .branch(filter_a().endpoint(|| async {})) + .branch(filter_b().endpoint(|| async {})), + hashset! { A, B }, + ); + assert(user_defined_filter(), hashset! { A, B, C }); + assert(user_defined_filter().branch(filter_a()), hashset! { A, B, C }); - // Filters do not observe anything on their own. - assert(filter_a(), hashset! {}); - assert(entry().chain(filter_b()), hashset! {}); - assert(filter_a().chain(filter_b()), hashset! {}); - assert(filter_a().branch(filter_b()), hashset! {}); - assert(filter_a().branch(filter_b()).branch(filter_c().chain(filter_c())), hashset! {}); - - // Anything user-defined observes everything that it can observe. - assert(filter_a().chain(filter(|| true)), hashset! { A }); - assert(user_defined_filter().chain(filter_a()), hashset! { A, B, C }); - assert(filter_a().chain(user_defined_filter()), hashset! { A }); - assert( - entry().branch(filter_a()).branch(filter_b()).chain(user_defined_filter()), - hashset! { A, B, C }, - ); - assert( - entry() - .branch(filter_a().endpoint(|| async {})) - .branch(filter_b().endpoint(|| async {})), - hashset! { A, B }, - ); - assert(user_defined_filter(), hashset! { A, B, C }); - assert(user_defined_filter().branch(filter_a()), hashset! { A, B, C }); - - // An entry is "invisible". - assert(entry(), hashset! {}); - assert(entry().chain(filter_a().endpoint(|| async {})), hashset! { A }); - assert(entry().branch(filter_a()), hashset! {}); - - // Chained non-overlapping filters do not allow anything. - assert(filter_a().chain(filter_b()).endpoint(|| async {}), hashset! {}); - } + // An entry is "invisible". + assert(entry(), hashset! {}); + assert(entry().chain(filter_a().endpoint(|| async {})), hashset! { A }); + assert(entry().branch(filter_a()), hashset! {}); - #[tokio::test] - async fn type_check_success() { - #[derive(Clone)] - struct A; - #[derive(Clone)] - struct B; - #[derive(Clone)] - struct C; - - macro_rules! test { - ($($key:ty),*) => { - type_check( - &HandlerSignature::Other { - obligations: btreemap! { - $(Type::of::<$key>() => Location::caller(),)* - }, - guaranteed_outcomes: btreeset! {}, - conditional_outcomes: btreeset! {}, - continues: true, - }, - &deps![A, B, C], - &[], - ); - }; + // Chained non-overlapping filters do not allow anything. + assert(filter_a().chain(filter_b()).endpoint(|| async {}), hashset! {}); } - // Type-checking an entry must succeed. - type_check(&HandlerSignature::Entry, &deps![], &[]); - - // Type-checking subsets of provided types must succeed. - test!(A, B, C); - test!(A, B); - test!(A, C); - test!(B, C); - test!(A); - test!(B); - test!(C); - } + async fn type_check_success() { + #[derive(Clone)] + struct A; + #[derive(Clone)] + struct B; + #[derive(Clone)] + struct C; + + macro_rules! test { + ($($key:ty),*) => { + type_check( + &HandlerSignature::Other { + obligations: btreemap! { + $(Type::of::<$key>() => Location::caller(),)* + }, + guaranteed_outcomes: btreeset! {}, + conditional_outcomes: btreeset! {}, + continues: true, + }, + &deps![A, B, C], + &[], + ); + }; + } + + // Type-checking an entry must succeed. + type_check(&HandlerSignature::Entry, &deps![], &[]); + + // Type-checking subsets of provided types must succeed. + test!(A, B, C); + test!(A, B); + test!(A, C); + test!(B, C); + test!(A); + test!(B); + test!(C); + } - #[test] - #[should_panic(expected = "Your handler accepts the following types: + #[should_panic(expected = "Your handler accepts the following types: `dptree::handler::core::tests::type_check_panic::A` `dptree::handler::core::tests::type_check_panic::B` `dptree::handler::core::tests::type_check_panic::C` @@ -971,376 +970,359 @@ The missing values are: Make sure all the required values are provided to the handler. For more information, visit . ")] - fn type_check_panic() { - #[derive(Clone)] - struct A; - #[derive(Clone)] - struct B; - #[derive(Clone)] - struct C; - - type_check( - &HandlerSignature::Other { - obligations: btreemap! { - Type::of::() => Location::caller(), - Type::of::() => Location::caller(), - Type::of::() => FIXED_LOCATION, + fn type_check_panic() { + #[derive(Clone)] + struct A; + #[derive(Clone)] + struct B; + #[derive(Clone)] + struct C; + + type_check( + &HandlerSignature::Other { + obligations: btreemap! { + Type::of::() => Location::caller(), + Type::of::() => Location::caller(), + Type::of::() => FIXED_LOCATION, + }, + guaranteed_outcomes: btreeset! {}, + conditional_outcomes: btreeset! {}, + continues: true, }, - guaranteed_outcomes: btreeset! {}, - conditional_outcomes: btreeset! {}, - continues: true, - }, - // `C` is required but not provided. - &deps![A, B], - &[], - ); - } - - #[test] - fn type_eq_ord_consistent() { - #[derive(Clone)] - struct A; + // `C` is required but not provided. + &deps![A, B], + &[], + ); + } - let ta1 = Type { id: A.type_id(), name: "A1" }; - let ta2 = Type { id: A.type_id(), name: "A2" }; + fn type_eq_ord_consistent() { + #[derive(Clone)] + struct A; - assert!(!(ta1 == ta2)); - assert!(ta1 < ta2); - assert!(!(ta1 > ta2)); - } + let ta1 = Type { id: A.type_id(), name: "A1" }; + let ta2 = Type { id: A.type_id(), name: "A2" }; - #[test] - fn type_btreeset_not_contains_duplicate_name() { - #[derive(Clone)] - struct A; - #[derive(Clone)] - struct B; - - let ta = Type { id: A.type_id(), name: "DuplicateName" }; - let tb = Type { id: B.type_id(), name: "DuplicateName" }; - let set = btreeset! {ta}; + assert!(!(ta1 == ta2)); + assert!(ta1 < ta2); + assert!(!(ta1 > ta2)); + } - assert!(ta != tb); - assert!(!set.contains(&tb)); - } + fn type_btreeset_not_contains_duplicate_name() { + #[derive(Clone)] + struct A; + #[derive(Clone)] + struct B; - #[tokio::test] - async fn type_infer_check_chained_combinators() { - #[derive(Clone)] - struct A; - #[derive(Clone)] - struct B; - #[derive(Clone)] - struct C; - #[derive(Clone)] - struct D; - #[derive(Clone)] - struct E; - #[derive(Clone)] - struct F; - #[derive(Clone)] - struct G; - #[derive(Clone, Debug, Eq, PartialEq)] - struct H; - - let h: Handler = entry() - .map(|/* In the final input types. */ _: A| B) - .inspect( - |/* This type must be removed from the final input types because it is - * provided by the `.map` above. */ - _: B, - /* This type must "propagate" to the final input types. */ - _: E| (), - ) - .filter_map(|/* In the final input types. */ _: C| Some(D)) - .filter( - |/* This type is provided by the `.filter_map` above. */ _: D, - /* Must propagate. */ _: F| true, - ) - .endpoint( - /* `B` and `D` are provided at this point. */ - |_: B, _: D, /* Must propagate. */ _: G| async { H }, - ); + let ta = Type { id: A.type_id(), name: "DuplicateName" }; + let tb = Type { id: B.type_id(), name: "DuplicateName" }; + let set = btreeset! {ta}; - let input_types = - [Type::of::(), Type::of::(), Type::of::(), Type::of::(), Type::of::()]; - let outcomes = btreeset! {Type::of::(), Type::of::()}; - - if let HandlerSignature::Other { - obligations: actual_obligations, - guaranteed_outcomes: actual_guaranteed_outcomes, - conditional_outcomes: actual_conditional_outcomes, - continues: _continues, - } = h.sig() - { - assert_eq!( - actual_obligations.keys().collect::>(), - input_types.iter().collect::>() - ); - let all_outcomes = actual_guaranteed_outcomes - .union(actual_conditional_outcomes) - .cloned() - .collect::>(); - assert_eq!(all_outcomes, outcomes); - } else { - panic!("Expected `HandlerSignature::Other`"); + assert!(ta != tb); + assert!(!set.contains(&tb)); } - let deps = deps![A, C, E, F, G]; + async fn type_infer_check_chained_combinators() { + #[derive(Clone)] + struct A; + #[derive(Clone)] + struct B; + #[derive(Clone)] + struct C; + #[derive(Clone)] + struct D; + #[derive(Clone)] + struct E; + #[derive(Clone)] + struct F; + #[derive(Clone)] + struct G; + #[derive(Clone, Debug, Eq, PartialEq)] + struct H; + + let h: Handler = entry() + .map(|/* In the final input types. */ _: A| B) + .inspect( + |/* This type must be removed from the final input types because it is + * provided by the `.map` above. */ + _: B, + /* This type must "propagate" to the final input types. */ + _: E| (), + ) + .filter_map(|/* In the final input types. */ _: C| Some(D)) + .filter( + |/* This type is provided by the `.filter_map` above. */ _: D, + /* Must propagate. */ _: F| true, + ) + .endpoint( + /* `B` and `D` are provided at this point. */ + |_: B, _: D, /* Must propagate. */ _: G| async { H }, + ); - type_check(h.sig(), &deps, &[]); + let input_types = + [Type::of::(), Type::of::(), Type::of::(), Type::of::(), Type::of::()]; + let outcomes = btreeset! {Type::of::(), Type::of::()}; - // Must not panic during execution. - assert_eq!(h.dispatch(deps).await, ControlFlow::Break(H)); - } + if let HandlerSignature::Other { + obligations: actual_obligations, + guaranteed_outcomes: actual_guaranteed_outcomes, + conditional_outcomes: actual_conditional_outcomes, + continues: _continues, + } = h.sig() + { + assert_eq!( + actual_obligations.keys().collect::>(), + input_types.iter().collect::>() + ); + let all_outcomes = actual_guaranteed_outcomes + .union(actual_conditional_outcomes) + .cloned() + .collect::>(); + assert_eq!(all_outcomes, outcomes); + } else { + panic!("Expected `HandlerSignature::Other`"); + } - #[tokio::test] - async fn type_infer_check_branched_combinators() { - #[derive(Clone)] - struct A; - #[derive(Clone)] - struct B; - #[derive(Clone)] - struct C; - #[derive(Clone)] - struct D; - #[derive(Clone)] - struct E; - #[derive(Clone, Debug, Eq, PartialEq)] - struct F; - - let h: Handler = entry() - .branch( - crate::inspect(|_: A| ()).map(|| B).map(|| C).map(|| D).endpoint(|| async { F }), - ) - .branch(crate::inspect(|_: E| ()).map(|| B).map(|| D).endpoint(|| async { F })); - - // The union of the input types of both branches. - let input_types = btreeset! {Type::of::(), Type::of::()}; - // Both branches provide different guaranteed outcomes. - let output_types = btreeset! {Type::of::(), Type::of::(), Type::of::()}; - - if let HandlerSignature::Other { - obligations: actual_obligations, - guaranteed_outcomes: actual_guaranteed_outcomes, - conditional_outcomes: actual_conditional_outcomes, - continues: _continues, - } = h.sig() - { - assert_eq!( - actual_obligations.keys().collect::>(), - input_types.iter().collect::>() - ); - let all_outcomes = actual_guaranteed_outcomes - .union(actual_conditional_outcomes) - .cloned() - .collect::>(); - assert_eq!(all_outcomes, output_types); - } else { - panic!("Expected `HandlerSignature::Other`"); - } + let deps = deps![A, C, E, F, G]; - let deps = deps![A, E]; + type_check(h.sig(), &deps, &[]); - type_check(h.sig(), &deps, &[]); + // Must not panic during execution. + assert_eq!(h.dispatch(deps).await, ControlFlow::Break(H)); + } - // Must not panic during execution. - assert_eq!(h.dispatch(deps).await, ControlFlow::Break(F)); - } + async fn type_infer_check_branched_combinators() { + #[derive(Clone)] + struct A; + #[derive(Clone)] + struct B; + #[derive(Clone)] + struct C; + #[derive(Clone)] + struct D; + #[derive(Clone)] + struct E; + #[derive(Clone, Debug, Eq, PartialEq)] + struct F; + + let h: Handler = entry() + .branch( + crate::inspect(|_: A| ()).map(|| B).map(|| C).map(|| D).endpoint(|| async { F }), + ) + .branch(crate::inspect(|_: E| ()).map(|| B).map(|| D).endpoint(|| async { F })); - #[tokio::test] - async fn obligations_priority() { - #[derive(Clone)] - struct A; - #[derive(Clone)] - struct B; - #[derive(Clone, Debug, Eq, PartialEq)] - struct C; + // The union of the input types of both branches. + let input_types = btreeset! {Type::of::(), Type::of::()}; + // Both branches provide different guaranteed outcomes. + let output_types = btreeset! {Type::of::(), Type::of::(), Type::of::()}; - fn test(h: Handler, column: u32) { if let HandlerSignature::Other { - obligations, - guaranteed_outcomes: _, - conditional_outcomes: _, - continues: _, + obligations: actual_obligations, + guaranteed_outcomes: actual_guaranteed_outcomes, + conditional_outcomes: actual_conditional_outcomes, + continues: _continues, } = h.sig() { - let (_ty, &location) = obligations - .iter() - .find(|(ty, _location)| ty.id == TypeId::of::()) - .expect("Missing obligation"); - assert_eq!(location.column(), column); + assert_eq!( + actual_obligations.keys().collect::>(), + input_types.iter().collect::>() + ); + let all_outcomes = actual_guaranteed_outcomes + .union(actual_conditional_outcomes) + .cloned() + .collect::>(); + assert_eq!(all_outcomes, output_types); } else { panic!("Expected `HandlerSignature::Other`"); } + + let deps = deps![A, E]; + + type_check(h.sig(), &deps, &[]); + + // Must not panic during execution. + assert_eq!(h.dispatch(deps).await, ControlFlow::Break(F)); } - #[rustfmt::skip] - let h: Handler = entry().map(|_: A| ()).endpoint(|_: A, _: B| async { C }); - test::(h, 37); + async fn obligations_priority() { + #[derive(Clone)] + struct A; + #[derive(Clone)] + struct B; + #[derive(Clone, Debug, Eq, PartialEq)] + struct C; + + fn test(h: Handler, column: u32) { + if let HandlerSignature::Other { + obligations, + guaranteed_outcomes: _, + conditional_outcomes: _, + continues: _, + } = h.sig() + { + let (_ty, &location) = obligations + .iter() + .find(|(ty, _location)| ty.id == TypeId::of::()) + .expect("Missing obligation"); + assert_eq!(location.column(), column); + } else { + panic!("Expected `HandlerSignature::Other`"); + } + } - #[rustfmt::skip] - let h: Handler = - entry().branch(crate::map(|_: A| { C })).branch(endpoint(|_: A, _: B| async { C })); - test::(h, 28); - } + #[rustfmt::skip] + let h: Handler = entry().map(|_: A| ()).endpoint(|_: A, _: B| async { C }); + test::(h, 41); - #[test] - #[should_panic(expected = "Ill-typed handler chain: the second handler cannot be an entry")] - fn chain_entry() { - let _: Handler<()> = entry().chain(entry()); - } + #[rustfmt::skip] + let h: Handler = + entry().branch(crate::map(|_: A| { C })).branch(endpoint(|_: A, _: B| async { C })); + test::(h, 32); + } - #[test] - #[should_panic(expected = "Ill-typed handler branch: the second handler cannot be an entry")] - fn branch_entry() { - let _: Handler<()> = entry().branch(entry()); - } + #[should_panic(expected = "Ill-typed handler chain: the second handler cannot be an entry")] + fn chain_entry() { + let _: Handler<()> = entry().chain(entry()); + } - #[tokio::test] - async fn chain_branch_type_check() { - #[derive(Clone)] - struct A; + #[should_panic(expected = "Ill-typed handler branch: the second handler cannot be an entry")] + fn branch_entry() { + let _: Handler<()> = entry().branch(entry()); + } - let handler: Handler<()> = - entry().chain(crate::map(|| A)).branch(endpoint(|_: A| async {})); + async fn chain_branch_type_check() { + #[derive(Clone)] + struct A; - let _no_panic_here: ControlFlow<(), _> = handler.dispatch(deps![]).await; + let handler: Handler<()> = + entry().chain(crate::map(|| A)).branch(endpoint(|_: A| async {})); - type_check(handler.sig(), &deps![], &[]); - } + let _no_panic_here: ControlFlow<(), _> = handler.dispatch(deps![]).await; - #[tokio::test] - async fn guaranteed_outcomes_chain_success() { - #[derive(Clone)] - struct A; - #[derive(Clone)] - struct B; + type_check(handler.sig(), &deps![], &[]); + } - let handler: Handler<()> = entry().map(|| A).map(|_: A| B).endpoint(|_: B| async {}); + async fn guaranteed_outcomes_chain_success() { + #[derive(Clone)] + struct A; + #[derive(Clone)] + struct B; - let result = handler.dispatch(deps![]).await; - assert_eq!(result, ControlFlow::Break(())); + let handler: Handler<()> = entry().map(|| A).map(|_: A| B).endpoint(|_: B| async {}); - type_check(handler.sig(), &deps![], &[]); - } + let result = handler.dispatch(deps![]).await; + assert_eq!(result, ControlFlow::Break(())); - #[tokio::test] - async fn conditional_outcomes_chain_success() { - #[derive(Clone)] - struct A; + type_check(handler.sig(), &deps![], &[]); + } - let handler: Handler<()> = entry().filter_map(|| Some(A)).endpoint(|_: A| async {}); + async fn conditional_outcomes_chain_success() { + #[derive(Clone)] + struct A; - let result = handler.dispatch(deps![]).await; - assert_eq!(result, ControlFlow::Break(())); + let handler: Handler<()> = entry().filter_map(|| Some(A)).endpoint(|_: A| async {}); - type_check(handler.sig(), &deps![], &[]); - } + let result = handler.dispatch(deps![]).await; + assert_eq!(result, ControlFlow::Break(())); - #[tokio::test] - async fn guaranteed_outcomes_branch_success() { - #[derive(Clone)] - struct A; + type_check(handler.sig(), &deps![], &[]); + } - let producer = entry().map(|| A); + async fn guaranteed_outcomes_branch_success() { + #[derive(Clone)] + struct A; - // In branch context, guaranteed outcomes can satisfy obligations. - let handler: Handler<()> = producer.branch(endpoint(|_: A| async {})); + let producer = entry().map(|| A); - let result = handler.dispatch(deps![]).await; - assert_eq!(result, ControlFlow::Break(())); + // In branch context, guaranteed outcomes can satisfy obligations. + let handler: Handler<()> = producer.branch(endpoint(|_: A| async {})); - type_check(handler.sig(), &deps![], &[]); - } + let result = handler.dispatch(deps![]).await; + assert_eq!(result, ControlFlow::Break(())); - #[tokio::test] - async fn mixed_outcomes_chain() { - #[derive(Clone)] - struct A; - #[derive(Clone)] - struct B; - #[derive(Clone)] - struct C; - - let handler: Handler<()> = entry() - .map(|| A) // guaranteed - .filter_map(|_: A| Some(B)) // conditional, but `A` is guaranteed - .map(|_: B| C) // guaranteed, but `B` is conditional - .endpoint(|_: C| async {}); // consumes `C` - - let result = handler.dispatch(deps![]).await; - assert_eq!(result, ControlFlow::Break(())); - - type_check(handler.sig(), &deps![], &[]); - } + type_check(handler.sig(), &deps![], &[]); + } - #[tokio::test] - async fn branch_with_guaranteed_continuation() { - #[derive(Clone)] - struct A; + async fn mixed_outcomes_chain() { + #[derive(Clone)] + struct A; + #[derive(Clone)] + struct B; + #[derive(Clone)] + struct C; - // The first branch produces `A` and continues (`map` always continues). - let first_branch = entry().map(|| A).inspect(|_: A| ()); // `inspect` continues, keeping `A` available + let handler: Handler<()> = entry() + .map(|| A) // guaranteed + .filter_map(|_: A| Some(B)) // conditional, but `A` is guaranteed + .map(|_: B| C) // guaranteed, but `B` is conditional + .endpoint(|_: C| async {}); // consumes `C` - // The second branch can use `A`, because the first branch guarantees it and - // continues the execution. - let handler: Handler<()> = first_branch.branch(endpoint(|_: A| async {})); + let result = handler.dispatch(deps![]).await; + assert_eq!(result, ControlFlow::Break(())); - let result = handler.dispatch(deps![]).await; - assert_eq!(result, ControlFlow::Break(())); + type_check(handler.sig(), &deps![], &[]); + } - type_check(handler.sig(), &deps![], &[]); - } + async fn branch_with_guaranteed_continuation() { + #[derive(Clone)] + struct A; - #[tokio::test] - #[should_panic(expected = "Your handler accepts the following types:")] - async fn deeply_nested_conditional_failure() { - #[derive(Clone)] - struct A; - #[derive(Clone)] - struct B; - - // Deep nesting where conditional outcome propagation should fail. - let handler: Handler<()> = entry() - .branch( - entry() - .branch(crate::filter_map(|| Some(A)).endpoint(|| async {})) - .branch(crate::filter_map(|_: A| Some(B)).endpoint(|| async {})), - ) - .branch(endpoint(|_: B| async {})); // `B` is not guaranteed to be available + // The first branch produces `A` and continues (`map` always continues). + let first_branch = entry().map(|| A).inspect(|_: A| ()); // `inspect` continues, keeping `A` available - type_check(handler.sig(), &deps![], &[]); - } + // The second branch can use `A`, because the first branch guarantees it and + // continues the execution. + let handler: Handler<()> = first_branch.branch(endpoint(|_: A| async {})); - #[test] - #[should_panic(expected = "Dead code detected: since the first handler aborts execution, the \ - second handler will never be called.")] - fn chain_endpoint_with_handler() { - let _: Handler<()> = endpoint(|| async {}).endpoint(|| async {}); - } + let result = handler.dispatch(deps![]).await; + assert_eq!(result, ControlFlow::Break(())); - #[test] - #[should_panic(expected = "Dead code detected: since the first handler aborts execution, the \ - second handler will never be called.")] - fn chain_endpoint_with_filter() { - let _: Handler<()> = endpoint(|| async {}).filter(|_: i32| true); - } + type_check(handler.sig(), &deps![], &[]); + } - #[test] - #[should_panic(expected = "Dead code detected: since the first handler aborts execution, the \ - second handler will never be called.")] - fn chain_endpoint_with_map() { - let _: Handler<()> = endpoint(|| async {}).map(|| 42); - } + #[should_panic(expected = "Your handler accepts the following types:")] + async fn deeply_nested_conditional_failure() { + #[derive(Clone)] + struct A; + #[derive(Clone)] + struct B; + + // Deep nesting where conditional outcome propagation should fail. + let handler: Handler<()> = entry() + .branch( + entry() + .branch(crate::filter_map(|| Some(A)).endpoint(|| async {})) + .branch(crate::filter_map(|_: A| Some(B)).endpoint(|| async {})), + ) + .branch(endpoint(|_: B| async {})); // `B` is not guaranteed to be available - #[test] - #[should_panic(expected = "Dead code detected: since the first handler aborts execution, the \ - second handler will never be called.")] - fn chain_complex_endpoint_dead_code() { - let _: Handler<()> = entry() - .chain(filter(|x: i32| x > 0)) - .chain(endpoint(|| async {})) - .chain(crate::map(|| "Unreachable")); + type_check(handler.sig(), &deps![], &[]); + } + + #[should_panic(expected = "Dead code detected: since the first handler aborts execution, the \ + second handler will never be called.")] + fn chain_endpoint_with_handler() { + let _: Handler<()> = endpoint(|| async {}).endpoint(|| async {}); + } + + #[should_panic(expected = "Dead code detected: since the first handler aborts execution, the \ + second handler will never be called.")] + fn chain_endpoint_with_filter() { + let _: Handler<()> = endpoint(|| async {}).filter(|_: i32| true); + } + + #[should_panic(expected = "Dead code detected: since the first handler aborts execution, the \ + second handler will never be called.")] + fn chain_endpoint_with_map() { + let _: Handler<()> = endpoint(|| async {}).map(|| 42); + } + + #[should_panic(expected = "Dead code detected: since the first handler aborts execution, the \ + second handler will never be called.")] + fn chain_complex_endpoint_dead_code() { + let _: Handler<()> = entry() + .chain(filter(|x: i32| x > 0)) + .chain(endpoint(|| async {})) + .chain(crate::map(|| "Unreachable")); + } } } diff --git a/src/handler/description.rs b/src/handler/description.rs index 67f2fe2..422e40b 100644 --- a/src/handler/description.rs +++ b/src/handler/description.rs @@ -3,6 +3,8 @@ mod interest_set; mod unspecified; +use crate::send::{MaybeSend, MaybeSync}; + pub use interest_set::{EventKind, InterestSet}; pub use unspecified::Unspecified; @@ -56,7 +58,7 @@ pub use unspecified::Unspecified; /// .branch(dptree::inspect(|| ())), /// ); /// ``` -pub trait HandlerDescription: Sized + Send + Sync + 'static { +pub trait HandlerDescription: Sized + MaybeSend + MaybeSync + 'static { /// Description for [`entry`](crate::entry). fn entry() -> Self; diff --git a/src/handler/endpoint.rs b/src/handler/endpoint.rs index bec5201..26ffc3e 100644 --- a/src/handler/endpoint.rs +++ b/src/handler/endpoint.rs @@ -1,6 +1,9 @@ use crate::{ - description, di::Injectable, from_fn_with_description, Handler, HandlerDescription, - HandlerSignature, + description, + di::Injectable, + from_fn_with_description, + send::{MaybeSend, MaybeSync}, + Handler, HandlerDescription, HandlerSignature, }; use std::{collections::BTreeSet, ops::ControlFlow, sync::Arc}; @@ -16,7 +19,7 @@ use futures::FutureExt; #[track_caller] pub fn endpoint<'a, F, Output, FnArgs, Descr>(f: F) -> Endpoint<'a, Output, Descr> where - F: Injectable + Send + Sync + 'a, + F: Injectable + MaybeSend + MaybeSync + 'a, Output: 'static, Descr: HandlerDescription, { @@ -48,22 +51,23 @@ mod tests { use super::*; use crate::{deps, help_inference}; - #[tokio::test] - async fn test_endpoint() { - let input = 123; - let output = 7; + crate::cross_test! { + async fn test_endpoint() { + let input = 123; + let output = 7; - let result = help_inference(endpoint(move |num: i32| async move { - assert_eq!(num, input); - output - })) - .dispatch(deps![input]) - .await; + let result = help_inference(endpoint(move |num: i32| async move { + assert_eq!(num, input); + output + })) + .dispatch(deps![input]) + .await; - let result = match result { - ControlFlow::Break(b) => b, - _ => panic!("Unexpected: handler return ControlFlow::Break"), - }; - assert_eq!(result, output); + let result = match result { + ControlFlow::Break(b) => b, + _ => panic!("Unexpected: handler return ControlFlow::Break"), + }; + assert_eq!(result, output); + } } } diff --git a/src/handler/filter.rs b/src/handler/filter.rs index cfae8eb..ab6d16e 100644 --- a/src/handler/filter.rs +++ b/src/handler/filter.rs @@ -2,6 +2,7 @@ use crate::{ di::{Asyncify, Injectable}, from_fn_with_description, handler::core::Handler, + send::{MaybeSend, MaybeSync}, HandlerDescription, HandlerSignature, }; @@ -16,7 +17,7 @@ use std::{collections::BTreeSet, ops::ControlFlow, sync::Arc}; #[track_caller] pub fn filter<'a, Pred, Output, FnArgs, Descr>(pred: Pred) -> Handler<'a, Output, Descr> where - Asyncify: Injectable + Send + Sync + 'a, + Asyncify: Injectable + MaybeSend + MaybeSync + 'a, Output: 'a, Descr: HandlerDescription, { @@ -28,7 +29,7 @@ where #[track_caller] pub fn filter_async<'a, Pred, Output, FnArgs, Descr>(pred: Pred) -> Handler<'a, Output, Descr> where - Pred: Injectable + Send + Sync + 'a, + Pred: Injectable + MaybeSend + MaybeSync + 'a, Output: 'a, Descr: HandlerDescription, { @@ -43,7 +44,7 @@ pub fn filter_with_description<'a, Pred, Output, FnArgs, Descr>( pred: Pred, ) -> Handler<'a, Output, Descr> where - Asyncify: Injectable + Send + Sync + 'a, + Asyncify: Injectable + MaybeSend + MaybeSync + 'a, Output: 'a, { filter_async_with_description(description, Asyncify(pred)) @@ -57,7 +58,7 @@ pub fn filter_async_with_description<'a, Pred, Output, FnArgs, Descr>( pred: Pred, ) -> Handler<'a, Output, Descr> where - Pred: Injectable + Send + Sync + 'a, + Pred: Injectable + MaybeSend + MaybeSync + 'a, Output: 'a, { let pred = Arc::new(pred); @@ -93,48 +94,48 @@ mod tests { use super::*; use crate::{deps, help_inference}; - #[tokio::test] - async fn test_filter() { - let input_value = 123; - let input = deps![input_value]; - let output = 7; + crate::cross_test! { + async fn test_filter() { + let input_value = 123; + let input = deps![input_value]; + let output = 7; - let result = help_inference(filter_async(move |event: i32| async move { - assert_eq!(event, input_value); - true - })) - .endpoint(move |event: i32| async move { - assert_eq!(event, input_value); - output - }) - .dispatch(input) - .await; + let result = help_inference(filter_async(move |event: i32| async move { + assert_eq!(event, input_value); + true + })) + .endpoint(move |event: i32| async move { + assert_eq!(event, input_value); + output + }) + .dispatch(input) + .await; - assert!(result == ControlFlow::Break(output)); - } + assert!(result == ControlFlow::Break(output)); + } - #[tokio::test] - async fn test_and_then_filter() { - let input = 123; - let output = 7; + async fn test_and_then_filter() { + let input = 123; + let output = 7; - let result = help_inference(filter(move |event: i32| { - assert_eq!(event, input); - true - })) - .chain( - filter_async(move |event: i32| async move { + let result = help_inference(filter(move |event: i32| { assert_eq!(event, input); true - }) - .endpoint(move |event: i32| async move { - assert_eq!(event, input); - output - }), - ) - .dispatch(deps![input]) - .await; + })) + .chain( + filter_async(move |event: i32| async move { + assert_eq!(event, input); + true + }) + .endpoint(move |event: i32| async move { + assert_eq!(event, input); + output + }), + ) + .dispatch(deps![input]) + .await; - assert!(result == ControlFlow::Break(output)); + assert!(result == ControlFlow::Break(output)); + } } } diff --git a/src/handler/filter_map.rs b/src/handler/filter_map.rs index 3262cc3..6d43a5c 100644 --- a/src/handler/filter_map.rs +++ b/src/handler/filter_map.rs @@ -1,6 +1,8 @@ use crate::{ di::{Asyncify, Injectable}, - from_fn_with_description, Handler, HandlerDescription, HandlerSignature, Type, + from_fn_with_description, + send::{MaybeSend, MaybeSync}, + Handler, HandlerDescription, HandlerSignature, Type, }; use std::{collections::BTreeSet, iter::FromIterator, ops::ControlFlow, sync::Arc}; @@ -17,7 +19,7 @@ pub fn filter_map<'a, Projection, Output, NewType, Args, Descr>( proj: Projection, ) -> Handler<'a, Output, Descr> where - Asyncify: Injectable, Args> + Send + Sync + 'a, + Asyncify: Injectable, Args> + MaybeSend + MaybeSync + 'a, Output: 'a, Descr: HandlerDescription, NewType: Send + Sync + 'static, @@ -32,7 +34,7 @@ pub fn filter_map_async<'a, Projection, Output, NewType, Args, Descr>( proj: Projection, ) -> Handler<'a, Output, Descr> where - Projection: Injectable, Args> + Send + Sync + 'a, + Projection: Injectable, Args> + MaybeSend + MaybeSync + 'a, Output: 'a, Descr: HandlerDescription, NewType: Send + Sync + 'static, @@ -48,7 +50,7 @@ pub fn filter_map_with_description<'a, Projection, Output, NewType, Args, Descr> proj: Projection, ) -> Handler<'a, Output, Descr> where - Asyncify: Injectable, Args> + Send + Sync + 'a, + Asyncify: Injectable, Args> + MaybeSend + MaybeSync + 'a, Output: 'a, NewType: Send + Sync + 'static, { @@ -63,7 +65,7 @@ pub fn filter_map_async_with_description<'a, Projection, Output, NewType, Args, proj: Projection, ) -> Handler<'a, Output, Descr> where - Projection: Injectable, Args> + Send + Sync + 'a, + Projection: Injectable, Args> + MaybeSend + MaybeSync + 'a, Output: 'a, NewType: Send + Sync + 'static, { @@ -106,28 +108,28 @@ mod tests { use super::*; use crate::{deps, help_inference}; - #[tokio::test] - async fn test_some() { - let value = 123; + crate::cross_test! { + async fn test_some() { + let value = 123; - let result = help_inference(filter_map(move || Some(value))) - .endpoint(move |event: i32| async move { - assert_eq!(event, value); - value - }) - .dispatch(deps![]) - .await; + let result = help_inference(filter_map(move || Some(value))) + .endpoint(move |event: i32| async move { + assert_eq!(event, value); + value + }) + .dispatch(deps![]) + .await; - assert!(result == ControlFlow::Break(value)); - } + assert!(result == ControlFlow::Break(value)); + } - #[tokio::test] - async fn test_none() { - let result = help_inference(filter_map(|| None::)) - .endpoint(|| async move { unreachable!() }) - .dispatch(deps![]) - .await; + async fn test_none() { + let result = help_inference(filter_map(|| None::)) + .endpoint(|| async move { unreachable!() }) + .dispatch(deps![]) + .await; - assert!(result == ControlFlow::Continue(crate::deps![])); + assert!(result == ControlFlow::Continue(crate::deps![])); + } } } diff --git a/src/handler/inspect.rs b/src/handler/inspect.rs index 060d7da..1c0fcbc 100644 --- a/src/handler/inspect.rs +++ b/src/handler/inspect.rs @@ -1,6 +1,8 @@ use crate::{ di::{Asyncify, Injectable}, - from_fn_with_description, Handler, HandlerDescription, HandlerSignature, + from_fn_with_description, + send::{MaybeSend, MaybeSync}, + Handler, HandlerDescription, HandlerSignature, }; use std::{collections::BTreeSet, sync::Arc}; @@ -13,7 +15,7 @@ use std::{collections::BTreeSet, sync::Arc}; #[track_caller] pub fn inspect<'a, F, Output, Args, Descr>(f: F) -> Handler<'a, Output, Descr> where - Asyncify: Injectable<(), Args> + Send + Sync + 'a, + Asyncify: Injectable<(), Args> + MaybeSend + MaybeSync + 'a, Output: 'a, Descr: HandlerDescription, { @@ -25,7 +27,7 @@ where #[track_caller] pub fn inspect_async<'a, F, Output, Args, Descr>(f: F) -> Handler<'a, Output, Descr> where - F: Injectable<(), Args> + Send + Sync + 'a, + F: Injectable<(), Args> + MaybeSend + MaybeSync + 'a, Output: 'a, Descr: HandlerDescription, { @@ -40,7 +42,7 @@ pub fn inspect_with_description<'a, F, Output, Args, Descr>( f: F, ) -> Handler<'a, Output, Descr> where - Asyncify: Injectable<(), Args> + Send + Sync + 'a, + Asyncify: Injectable<(), Args> + MaybeSend + MaybeSync + 'a, Output: 'a, { inspect_async_with_description(description, Asyncify(f)) @@ -54,7 +56,7 @@ pub fn inspect_async_with_description<'a, F, Output, Args, Descr>( f: F, ) -> Handler<'a, Output, Descr> where - F: Injectable<(), Args> + Send + Sync + 'a, + F: Injectable<(), Args> + MaybeSend + MaybeSync + 'a, Output: 'a, { let f = Arc::new(f); @@ -91,20 +93,21 @@ mod tests { use super::*; use crate::{deps, help_inference}; - #[tokio::test] - async fn test_inspect() { - let value = 123; - let inspect_passed = Arc::new(AtomicBool::new(false)); - let inspect_passed_cloned = Arc::clone(&inspect_passed); + crate::cross_test! { + async fn test_inspect() { + let value = 123; + let inspect_passed = Arc::new(AtomicBool::new(false)); + let inspect_passed_cloned = Arc::clone(&inspect_passed); - let result: ControlFlow<(), _> = help_inference(inspect(move |x: i32| { - assert_eq!(x, value); - inspect_passed_cloned.swap(true, Ordering::Relaxed); - })) - .dispatch(deps![value]) - .await; + let result: ControlFlow<(), _> = help_inference(inspect(move |x: i32| { + assert_eq!(x, value); + inspect_passed_cloned.swap(true, Ordering::Relaxed); + })) + .dispatch(deps![value]) + .await; - assert!(matches!(result, ControlFlow::Continue(_))); - assert!(inspect_passed.load(Ordering::Relaxed)); + assert!(matches!(result, ControlFlow::Continue(_))); + assert!(inspect_passed.load(Ordering::Relaxed)); + } } } diff --git a/src/handler/map.rs b/src/handler/map.rs index b9ebcac..db5dbb8 100644 --- a/src/handler/map.rs +++ b/src/handler/map.rs @@ -1,6 +1,8 @@ use crate::{ di::{Asyncify, Injectable}, - from_fn_with_description, Handler, HandlerDescription, HandlerSignature, Type, + from_fn_with_description, + send::{MaybeSend, MaybeSync}, + Handler, HandlerDescription, HandlerSignature, Type, }; use std::{collections::BTreeSet, iter::FromIterator, ops::ControlFlow, sync::Arc}; @@ -17,7 +19,7 @@ pub fn map<'a, Projection, Output, NewType, Args, Descr>( proj: Projection, ) -> Handler<'a, Output, Descr> where - Asyncify: Injectable + Send + Sync + 'a, + Asyncify: Injectable + MaybeSend + MaybeSync + 'a, Output: 'a, Descr: HandlerDescription, NewType: Send + Sync + 'static, @@ -32,7 +34,7 @@ pub fn map_async<'a, Projection, Output, NewType, Args, Descr>( proj: Projection, ) -> Handler<'a, Output, Descr> where - Projection: Injectable + Send + Sync + 'a, + Projection: Injectable + MaybeSend + MaybeSync + 'a, Output: 'a, Descr: HandlerDescription, NewType: Send + Sync + 'static, @@ -48,7 +50,7 @@ pub fn map_with_description<'a, Projection, Output, NewType, Args, Descr>( proj: Projection, ) -> Handler<'a, Output, Descr> where - Asyncify: Injectable + Send + Sync + 'a, + Asyncify: Injectable + MaybeSend + MaybeSync + 'a, Output: 'a, Descr: HandlerDescription, NewType: Send + Sync + 'static, @@ -64,7 +66,7 @@ pub fn map_async_with_description<'a, Projection, Output, NewType, Args, Descr>( proj: Projection, ) -> Handler<'a, Output, Descr> where - Projection: Injectable + Send + Sync + 'a, + Projection: Injectable + MaybeSend + MaybeSync + 'a, Output: 'a, Descr: HandlerDescription, NewType: Send + Sync + 'static, @@ -103,18 +105,19 @@ mod tests { use super::*; use crate::{deps, help_inference}; - #[tokio::test] - async fn test_map() { - let value = 123; + crate::cross_test! { + async fn test_map() { + let value = 123; - let result = help_inference(map(move || value)) - .endpoint(move |event: i32| async move { - assert_eq!(event, value); - value - }) - .dispatch(deps![]) - .await; + let result = help_inference(map(move || value)) + .endpoint(move |event: i32| async move { + assert_eq!(event, value); + value + }) + .dispatch(deps![]) + .await; - assert!(result == ControlFlow::Break(value)); + assert!(result == ControlFlow::Break(value)); + } } } diff --git a/src/handler/methods.rs b/src/handler/methods.rs index bfe2fec..e7b9f5b 100644 --- a/src/handler/methods.rs +++ b/src/handler/methods.rs @@ -1,5 +1,6 @@ use crate::{ di::{Asyncify, Injectable}, + send::{MaybeSend, MaybeSync}, Handler, HandlerDescription, }; @@ -13,7 +14,7 @@ where #[track_caller] pub fn filter(self, pred: Pred) -> Handler<'a, Output, Descr> where - Asyncify: Injectable + Send + Sync + 'a, + Asyncify: Injectable + MaybeSend + MaybeSync + 'a, { self.chain(crate::filter(pred)) } @@ -23,7 +24,7 @@ where #[track_caller] pub fn filter_async(self, pred: Pred) -> Handler<'a, Output, Descr> where - Pred: Injectable + Send + Sync + 'a, + Pred: Injectable + MaybeSend + MaybeSync + 'a, { self.chain(crate::filter_async(pred)) } @@ -33,7 +34,7 @@ where #[track_caller] pub fn filter_map(self, proj: Proj) -> Handler<'a, Output, Descr> where - Asyncify: Injectable, Args> + Send + Sync + 'a, + Asyncify: Injectable, Args> + MaybeSend + MaybeSync + 'a, NewType: Send + Sync + 'static, { self.chain(crate::filter_map(proj)) @@ -44,7 +45,7 @@ where #[track_caller] pub fn filter_map_async(self, proj: Proj) -> Handler<'a, Output, Descr> where - Proj: Injectable, Args> + Send + Sync + 'a, + Proj: Injectable, Args> + MaybeSend + MaybeSync + 'a, NewType: Send + Sync + 'static, { self.chain(crate::filter_map_async(proj)) @@ -55,7 +56,7 @@ where #[track_caller] pub fn map(self, proj: Proj) -> Handler<'a, Output, Descr> where - Asyncify: Injectable + Send + Sync + 'a, + Asyncify: Injectable + MaybeSend + MaybeSync + 'a, NewType: Send + Sync + 'static, { self.chain(crate::map(proj)) @@ -66,7 +67,7 @@ where #[track_caller] pub fn map_async(self, proj: Proj) -> Handler<'a, Output, Descr> where - Proj: Injectable + Send + Sync + 'a, + Proj: Injectable + MaybeSend + MaybeSync + 'a, NewType: Send + Sync + 'static, { self.chain(crate::map_async(proj)) @@ -77,7 +78,7 @@ where #[track_caller] pub fn inspect(self, f: F) -> Handler<'a, Output, Descr> where - Asyncify: Injectable<(), Args> + Send + Sync + 'a, + Asyncify: Injectable<(), Args> + MaybeSend + MaybeSync + 'a, { self.chain(crate::inspect(f)) } @@ -87,7 +88,7 @@ where #[track_caller] pub fn inspect_async(self, f: F) -> Handler<'a, Output, Descr> where - F: Injectable<(), Args> + Send + Sync + 'a, + F: Injectable<(), Args> + MaybeSend + MaybeSync + 'a, { self.chain(crate::inspect_async(f)) } @@ -97,7 +98,7 @@ where #[track_caller] pub fn endpoint(self, f: F) -> Handler<'a, Output, Descr> where - F: Injectable + Send + Sync + 'a, + F: Injectable + MaybeSend + MaybeSync + 'a, Output: 'static, { self.chain(crate::endpoint(f)) @@ -110,42 +111,43 @@ mod tests { use crate::{deps, help_inference}; - // Test that these methods just do compile. - #[tokio::test] - async fn test_methods() { - let value = 42; + crate::cross_test! { + // Test that these methods just do compile. + async fn test_methods() { + let value = 42; - let _: ControlFlow<(), _> = - help_inference(crate::entry()).filter(|| true).dispatch(deps![value]).await; + let _: ControlFlow<(), _> = + help_inference(crate::entry()).filter(|| true).dispatch(deps![value]).await; - let _: ControlFlow<(), _> = help_inference(crate::entry()) - .filter_async(|| async { true }) - .dispatch(deps![value]) - .await; + let _: ControlFlow<(), _> = help_inference(crate::entry()) + .filter_async(|| async { true }) + .dispatch(deps![value]) + .await; - let _: ControlFlow<(), _> = - help_inference(crate::entry()).filter_map(|| Some("abc")).dispatch(deps![value]).await; + let _: ControlFlow<(), _> = + help_inference(crate::entry()).filter_map(|| Some("abc")).dispatch(deps![value]).await; - let _: ControlFlow<(), _> = help_inference(crate::entry()) - .filter_map_async(|| async { Some("abc") }) - .dispatch(deps![value]) - .await; + let _: ControlFlow<(), _> = help_inference(crate::entry()) + .filter_map_async(|| async { Some("abc") }) + .dispatch(deps![value]) + .await; - let _: ControlFlow<(), _> = - help_inference(crate::entry()).map(|| "abc").dispatch(deps![value]).await; + let _: ControlFlow<(), _> = + help_inference(crate::entry()).map(|| "abc").dispatch(deps![value]).await; - let _: ControlFlow<(), _> = help_inference(crate::entry()) - .map_async(|| async { "abc" }) - .dispatch(deps![value]) - .await; + let _: ControlFlow<(), _> = help_inference(crate::entry()) + .map_async(|| async { "abc" }) + .dispatch(deps![value]) + .await; - let _: ControlFlow<(), _> = - help_inference(crate::entry()).inspect(|| {}).dispatch(deps![value]).await; + let _: ControlFlow<(), _> = + help_inference(crate::entry()).inspect(|| {}).dispatch(deps![value]).await; - let _: ControlFlow<(), _> = - help_inference(crate::entry()).inspect_async(|| async {}).dispatch(deps![value]).await; + let _: ControlFlow<(), _> = + help_inference(crate::entry()).inspect_async(|| async {}).dispatch(deps![value]).await; - let _: ControlFlow<(), _> = - help_inference(crate::entry()).endpoint(|| async {}).dispatch(deps![value]).await; + let _: ControlFlow<(), _> = + help_inference(crate::entry()).endpoint(|| async {}).dispatch(deps![value]).await; + } } } diff --git a/src/lib.rs b/src/lib.rs index ff4be91..adfe3c6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,19 +1,19 @@ //! An implementation of the [chain (tree) of responsibility] pattern. //! //! [[`examples/web_server.rs`](https://github.com/teloxide/dptree/blob/master/examples/web_server.rs)] -//! ``` +//! ```rust //! use dptree::prelude::*; //! //! type WebHandler = Endpoint<'static, String>; //! //! #[rustfmt::skip] -//! #[tokio::main] +//! #[tokio::main(flavor = "current_thread")] //! async fn main() { //! let web_server = dptree::entry() //! .branch(smiles_handler()) //! .branch(sqrt_handler()) //! .branch(not_found_handler()); -//! +//! //! assert_eq!( //! web_server.dispatch(dptree::deps!["/smile"]).await, //! ControlFlow::Break("🙃".to_owned()) @@ -58,6 +58,7 @@ mod handler; pub mod di; pub mod prelude; +pub mod send; pub use handler::*; @@ -94,10 +95,10 @@ pub use handler::*; /// /// ## Examples /// -/// ``` +/// ```rust /// use dptree::prelude::*; /// -/// # #[tokio::main] +/// # #[tokio::main(flavor = "current_thread")] /// # async fn main() { /// #[derive(Clone)] /// enum Command { @@ -155,6 +156,32 @@ macro_rules! case { }; } +/// Conditionally applies `#[tokio::test]`/`#[test]` for native builds +/// and `#[wasm_bindgen_test]` for Wasm targets. +#[cfg(test)] +macro_rules! cross_test { + () => {}; + ($(#[$($attr:tt)*])* async fn $name:ident() $body:block $($rest:tt)*) => { + #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + $(#[$($attr)*])* + async fn $name() $body + + crate::cross_test! { $($rest)* } + }; + ($(#[$($attr:tt)*])* fn $name:ident() $body:block $($rest:tt)*) => { + #[cfg_attr(not(target_arch = "wasm32"), test)] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + $(#[$($attr)*])* + fn $name() $body + + crate::cross_test! { $($rest)* } + }; +} + +#[cfg(test)] +pub(crate) use cross_test; + #[cfg(test)] mod tests { use std::ops::ControlFlow; @@ -169,89 +196,84 @@ mod tests { Other, } - #[tokio::test] - async fn handler_empty_variant() { - let input = State::A; - let h: crate::Handler<_> = case![State::A].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_single_fn_variant() { - let input = State::B(42); - let h: crate::Handler<_> = case![State::B(x)].endpoint(|x: i32| async move { - assert_eq!(x, 42); - 123 - }); + crate::cross_test! { + async fn handler_empty_variant() { + let input = State::A; + let h: crate::Handler<_> = case![State::A].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(_))); - } + 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_single_fn_variant_trailing_comma() { - let input = State::B(42); - let h: crate::Handler<_> = case![State::B(x,)].endpoint(|(x,): (i32,)| async move { - assert_eq!(x, 42); - 123 - }); + async fn handler_single_fn_variant() { + let input = State::B(42); + let h: crate::Handler<_> = case![State::B(x)].endpoint(|x: i32| async move { + assert_eq!(x, 42); + 123 + }); - assert_eq!(h.dispatch(crate::deps![input]).await, ControlFlow::Break(123)); - assert!(matches!(h.dispatch(crate::deps![State::Other]).await, ControlFlow::Continue(_))); - } + 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_fn_variant() { - let input = State::C(42, "abc"); - let h: crate::Handler<_> = - case![State::C(x, y)].endpoint(|(x, str): (i32, &'static str)| async move { + async fn handler_single_fn_variant_trailing_comma() { + let input = State::B(42); + let h: crate::Handler<_> = case![State::B(x,)].endpoint(|(x,): (i32,)| async move { assert_eq!(x, 42); - assert_eq!(str, "abc"); 123 }); - assert_eq!(h.dispatch(crate::deps![input]).await, ControlFlow::Break(123)); - assert!(matches!(h.dispatch(crate::deps![State::Other]).await, ControlFlow::Continue(_))); - } + 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_single_struct_variant() { - let input = State::D { foo: 42 }; - let h: crate::Handler<_> = case![State::D { foo }].endpoint(|x: i32| async move { - assert_eq!(x, 42); - 123 - }); + async fn handler_fn_variant() { + let input = State::C(42, "abc"); + let h: crate::Handler<_> = + case![State::C(x, y)].endpoint(|(x, str): (i32, &'static str)| async move { + assert_eq!(x, 42); + assert_eq!(str, "abc"); + 123 + }); - assert_eq!(h.dispatch(crate::deps![input]).await, ControlFlow::Break(123)); - assert!(matches!(h.dispatch(crate::deps![State::Other]).await, ControlFlow::Continue(_))); - } + 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_single_struct_variant_trailing_comma() { - let input = State::D { foo: 42 }; - #[rustfmt::skip] // rustfmt removes the trailing comma from `State::D { foo, }`, but it plays a vital role in this test. - let h: crate::Handler<_> = case![State::D { foo, }].endpoint(|(x,): (i32,)| async move { - assert_eq!(x, 42); - 123 - }); + async fn handler_single_struct_variant() { + let input = State::D { foo: 42 }; + let h: crate::Handler<_> = case![State::D { foo }].endpoint(|x: i32| async move { + assert_eq!(x, 42); + 123 + }); - assert_eq!(h.dispatch(crate::deps![input]).await, ControlFlow::Break(123)); - assert!(matches!(h.dispatch(crate::deps![State::Other]).await, ControlFlow::Continue(_))); - } + 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_variant() { - let input = State::E { foo: 42, bar: "abc" }; - let h: crate::Handler<_> = - case![State::E { foo, bar }].endpoint(|(x, str): (i32, &'static str)| async move { + async fn handler_single_struct_variant_trailing_comma() { + let input = State::D { foo: 42 }; + #[rustfmt::skip] // rustfmt removes the trailing comma from `State::D { foo, }`, but it plays a vital role in this test. + let h: crate::Handler<_> = case![State::D { foo, }].endpoint(|(x,): (i32,)| async move { assert_eq!(x, 42); - assert_eq!(str, "abc"); 123 }); - assert_eq!(h.dispatch(crate::deps![input]).await, ControlFlow::Break(123)); - assert!(matches!(h.dispatch(crate::deps![State::Other]).await, ControlFlow::Continue(_))); + assert_eq!(h.dispatch(crate::deps![input]).await, ControlFlow::Break(123)); + assert!(matches!(h.dispatch(crate::deps![State::Other]).await, ControlFlow::Continue(_))); + } + + async fn handler_struct_variant() { + let input = State::E { foo: 42, bar: "abc" }; + let h: crate::Handler<_> = + case![State::E { foo, bar }].endpoint(|(x, str): (i32, &'static str)| async move { + assert_eq!(x, 42); + assert_eq!(str, "abc"); + 123 + }); + + assert_eq!(h.dispatch(crate::deps![input]).await, ControlFlow::Break(123)); + assert!(matches!(h.dispatch(crate::deps![State::Other]).await, ControlFlow::Continue(_))); + } } } diff --git a/src/send.rs b/src/send.rs new file mode 100644 index 0000000..29ab792 --- /dev/null +++ b/src/send.rs @@ -0,0 +1,35 @@ +//! Conditional `Send`/`Sync` helpers for wasm32 target. + +use std::future::Future; +use std::pin::Pin; + +/// `Send` on native targets; no bound on `wasm32`. +#[cfg(not(target_arch = "wasm32"))] +pub trait MaybeSend: Send {} +#[cfg(not(target_arch = "wasm32"))] +impl MaybeSend for T {} + +/// `Send` on native targets; no bound on `wasm32`. +#[cfg(target_arch = "wasm32")] +pub trait MaybeSend {} +#[cfg(target_arch = "wasm32")] +impl MaybeSend for T {} + +/// `Sync` on native targets; no bound on `wasm32`. +#[cfg(not(target_arch = "wasm32"))] +pub trait MaybeSync: Sync {} +#[cfg(not(target_arch = "wasm32"))] +impl MaybeSync for T {} + +/// `Sync` on native targets; no bound on `wasm32`. +#[cfg(target_arch = "wasm32")] +pub trait MaybeSync {} +#[cfg(target_arch = "wasm32")] +impl MaybeSync for T {} + +/// A boxed future that is `Send` on native targets and not required to be `Send` +/// on `wasm32`. +#[cfg(not(target_arch = "wasm32"))] +pub type BoxFuture<'a, T> = Pin + Send + 'a>>; +#[cfg(target_arch = "wasm32")] +pub type BoxFuture<'a, T> = Pin + 'a>>;