diff --git a/CHANGELOG.md b/CHANGELOG.md index e01ab2f..5faf69b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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)). + - The `Fallible` trait (with a blanket impl for `Result`), used by the fallible handlers to build the `Err` half of the output. + - Fallible handlers `try_filter`, `try_map`, `try_filter_map` (and their `_async` variants), which let a function return `Result` and short-circuit the whole handler chain with `ControlFlow::Break` on `Err` (symmetric to how `endpoint` breaks with a value). Requires the handler `Output` to implement the new `Fallible` trait. ## 0.5.1 - 2025-07-10 diff --git a/README.md b/README.md index 46146fa..5fe6736 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ fn not_found_handler() -> WebHandler { ## Features - ✔️ Declarative handlers: `dptree::{endpoint, filter, filter_map, ...}`. + - ✔️ Fallible handlers (`try_filter`, `try_map`, `try_filter_map`) that short-circuit a handler chain with an error when the handler output is a `Result`. - ✔️ A lightweight functional design without typical OOP hodgepodge. - ✔️ [Dependency injection (DI)] out-of-the-box. - ✔️ Startup-time [type checking] of run-time dependencies via `dptree::type_check`. diff --git a/src/handler.rs b/src/handler.rs index eb354e6..a1f2a10 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,16 +1,24 @@ mod core; pub mod description; mod endpoint; +mod fallible; mod filter; mod filter_map; mod inspect; mod map; mod methods; +mod try_filter; +mod try_filter_map; +mod try_map; pub use self::core::*; pub use description::HandlerDescription; pub use endpoint::*; +pub use fallible::Fallible; pub use filter::*; pub use filter_map::*; pub use inspect::*; pub use map::*; +pub use try_filter::*; +pub use try_filter_map::*; +pub use try_map::*; diff --git a/src/handler/description.rs b/src/handler/description.rs index 422e40b..7bb2aa6 100644 --- a/src/handler/description.rs +++ b/src/handler/description.rs @@ -170,4 +170,70 @@ pub trait HandlerDescription: Sized + MaybeSend + MaybeSync + 'static { fn endpoint() -> Self { Self::user_defined() } + + /// Description for [`try_filter`](crate::try_filter). + /// + /// ## Default implementation + /// + /// By default this returns the value from + /// [`user_defined`](HandlerDescription::user_defined). + #[track_caller] + fn try_filter() -> Self { + Self::user_defined() + } + + /// Description for [`try_filter_async`](crate::try_filter_async). + /// + /// ## Default implementation + /// + /// By default this returns the value from + /// [`user_defined`](HandlerDescription::user_defined). + #[track_caller] + fn try_filter_async() -> Self { + Self::user_defined() + } + + /// Description for [`try_filter_map`](crate::try_filter_map). + /// + /// ## Default implementation + /// + /// By default this returns the value from + /// [`user_defined`](HandlerDescription::user_defined). + #[track_caller] + fn try_filter_map() -> Self { + Self::user_defined() + } + + /// Description for [`try_filter_map_async`](crate::try_filter_map_async). + /// + /// ## Default implementation + /// + /// By default this returns the value from + /// [`user_defined`](HandlerDescription::user_defined). + #[track_caller] + fn try_filter_map_async() -> Self { + Self::user_defined() + } + + /// Description for [`try_map`](crate::try_map). + /// + /// ## Default implementation + /// + /// By default this returns the value from + /// [`user_defined`](HandlerDescription::user_defined). + #[track_caller] + fn try_map() -> Self { + Self::user_defined() + } + + /// Description for [`try_map_async`](crate::try_map_async). + /// + /// ## Default implementation + /// + /// By default this returns the value from + /// [`user_defined`](HandlerDescription::user_defined). + #[track_caller] + fn try_map_async() -> Self { + Self::user_defined() + } } diff --git a/src/handler/fallible.rs b/src/handler/fallible.rs new file mode 100644 index 0000000..c3f575b --- /dev/null +++ b/src/handler/fallible.rs @@ -0,0 +1,32 @@ +//! The [`Fallible`] trait, used by the fallible handlers + +/// A handler `Output` that can represent a failure. +/// +/// The fallible handlers ([`try_filter`], [`try_map`], [`try_filter_map`]) +/// take a function that returns [`Result`]. On `Ok`, execution continues as +/// usual; on `Err(e)`, the chain short-circuits and ends the dispatch with the +/// error as the output; the same way [`endpoint`](crate::endpoint) ends +/// it with a value. +/// +/// [`Result`] implements this out of the box. Implement it yourself if your +/// `Output` is another type that can still represent an error (e.g. an HTTP +/// response that encodes it in the status code). +/// +/// [`try_filter`]: crate::try_filter +/// [`try_map`]: crate::try_map +/// [`try_filter_map`]: crate::try_filter_map +pub trait Fallible { + /// The error type carried by this output. + type Error: 'static; + + /// Constructs an output value representing the given error. + fn from_error(error: Self::Error) -> Self; +} + +impl Fallible for Result { + type Error = E; + + fn from_error(error: E) -> Self { + Err(error) + } +} diff --git a/src/handler/filter_map.rs b/src/handler/filter_map.rs index 6d43a5c..0dee416 100644 --- a/src/handler/filter_map.rs +++ b/src/handler/filter_map.rs @@ -79,7 +79,7 @@ where async move { let proj = proj.inject(&container); let res = proj().await; - std::mem::drop(proj); + drop(proj); match res { Some(new_type) => { diff --git a/src/handler/map.rs b/src/handler/map.rs index db5dbb8..f9283f2 100644 --- a/src/handler/map.rs +++ b/src/handler/map.rs @@ -81,7 +81,7 @@ where async move { let proj = proj.inject(&container); let res = proj().await; - std::mem::drop(proj); + drop(proj); let mut intermediate = container.clone(); intermediate.insert(res); diff --git a/src/handler/methods.rs b/src/handler/methods.rs index e7b9f5b..1ff92bb 100644 --- a/src/handler/methods.rs +++ b/src/handler/methods.rs @@ -1,7 +1,7 @@ use crate::{ di::{Asyncify, Injectable}, send::{MaybeSend, MaybeSync}, - Handler, HandlerDescription, + Fallible, Handler, HandlerDescription, }; impl<'a, Output, Descr> Handler<'a, Output, Descr> @@ -103,6 +103,110 @@ where { self.chain(crate::endpoint(f)) } + + /// Chain this handler with the fallible filter predicate `pred`. + /// + /// `pred` returns [`Result`] (where `E` is the error type of this + /// handler's `Output`). On `Ok(true)` execution continues; on `Ok(false)` + /// the handler returns [`ControlFlow::Continue`](std::ops::ControlFlow::Continue) + /// (try the next branch); on `Err(e)` the handler short-circuits with + /// [`ControlFlow::Break`](std::ops::ControlFlow::Break) carrying + /// the error, without falling through to sibling branches. + #[must_use] + #[track_caller] + pub fn try_filter(self, pred: Pred) -> Handler<'a, Output, Descr> + where + Asyncify: + Injectable, FnArgs> + MaybeSend + MaybeSync + 'a, + Output: Fallible, + Output::Error: MaybeSend, + { + self.chain(crate::try_filter(pred)) + } + + /// Chain this handler with the async fallible filter predicate `pred`. + /// + /// See [`try_filter`](Handler::try_filter). + #[must_use] + #[track_caller] + pub fn try_filter_async(self, pred: Pred) -> Handler<'a, Output, Descr> + where + Pred: Injectable, FnArgs> + MaybeSend + MaybeSync + 'a, + Output: Fallible, + Output::Error: MaybeSend, + { + self.chain(crate::try_filter_async(pred)) + } + + /// Chain this handler with the fallible filter projection `proj`. + /// + /// `proj` returns [`Result, E>`]. On `Ok(Some(v))` `v` is + /// inserted into the container and execution continues; on `Ok(None)` the + /// handler returns [`ControlFlow::Continue`](std::ops::ControlFlow::Continue) + /// (try the next branch); on `Err(e)` the handler short-circuits with + /// [`ControlFlow::Break`](std::ops::ControlFlow::Break) carrying + /// the error. + #[must_use] + #[track_caller] + pub fn try_filter_map(self, proj: Proj) -> Handler<'a, Output, Descr> + where + Asyncify: + Injectable, Output::Error>, Args> + MaybeSend + MaybeSync + 'a, + Output: Fallible, + Output::Error: MaybeSend, + NewType: Send + Sync + 'static, + { + self.chain(crate::try_filter_map(proj)) + } + + /// Chain this handler with the async fallible filter projection `proj`. + /// + /// See [`try_filter_map`](Handler::try_filter_map). + #[must_use] + #[track_caller] + pub fn try_filter_map_async(self, proj: Proj) -> Handler<'a, Output, Descr> + where + Proj: Injectable, Output::Error>, Args> + MaybeSend + MaybeSync + 'a, + Output: Fallible, + Output::Error: MaybeSend, + NewType: Send + Sync + 'static, + { + self.chain(crate::try_filter_map_async(proj)) + } + + /// Chain this handler with the fallible map projection `proj`. + /// + /// `proj` returns [`Result`]. On `Ok(v)` `v` is inserted into + /// the container and execution continues; on `Err(e)` the handler + /// short-circuits with [`ControlFlow::Break`](std::ops::ControlFlow::Break) + /// carrying the error, without calling the continuation. + #[must_use] + #[track_caller] + pub fn try_map(self, proj: Proj) -> Handler<'a, Output, Descr> + where + Asyncify: + Injectable, Args> + MaybeSend + MaybeSync + 'a, + Output: Fallible, + Output::Error: MaybeSend, + NewType: Send + Sync + 'static, + { + self.chain(crate::try_map(proj)) + } + + /// Chain this handler with the async fallible map projection `proj`. + /// + /// See [`try_map`](Handler::try_map). + #[must_use] + #[track_caller] + pub fn try_map_async(self, proj: Proj) -> Handler<'a, Output, Descr> + where + Proj: Injectable, Args> + MaybeSend + MaybeSync + 'a, + Output: Fallible, + Output::Error: MaybeSend, + NewType: Send + Sync + 'static, + { + self.chain(crate::try_map_async(proj)) + } } #[cfg(test)] @@ -148,6 +252,37 @@ mod tests { let _: ControlFlow<(), _> = help_inference(crate::entry()).endpoint(|| async {}).dispatch(deps![value]).await; + + // Fallible handlers require a fallible (`Result` in here) output. + let _: ControlFlow, _> = help_inference(crate::entry()) + .try_filter(|| Ok::(true)) + .dispatch(deps![value]) + .await; + + let _: ControlFlow, _> = help_inference(crate::entry()) + .try_filter_async(|| async { Ok::(true) }) + .dispatch(deps![value]) + .await; + + let _: ControlFlow, _> = help_inference(crate::entry()) + .try_filter_map(|| Ok::, &str>(Some(()))) + .dispatch(deps![value]) + .await; + + let _: ControlFlow, _> = help_inference(crate::entry()) + .try_filter_map_async(|| async { Ok::, &str>(Some(())) }) + .dispatch(deps![value]) + .await; + + let _: ControlFlow, _> = help_inference(crate::entry()) + .try_map(|| Ok::<(), &str>(())) + .dispatch(deps![value]) + .await; + + let _: ControlFlow, _> = help_inference(crate::entry()) + .try_map_async(|| async { Ok::<(), &str>(()) }) + .dispatch(deps![value]) + .await; } } } diff --git a/src/handler/try_filter.rs b/src/handler/try_filter.rs new file mode 100644 index 0000000..e9f6d27 --- /dev/null +++ b/src/handler/try_filter.rs @@ -0,0 +1,158 @@ +use crate::{ + di::{Asyncify, Injectable}, + from_fn_with_description, + send::{MaybeSend, MaybeSync}, + Fallible, Handler, HandlerDescription, HandlerSignature, +}; + +use std::{collections::BTreeSet, ops::ControlFlow, sync::Arc}; + +/// Constructs a fallible handler that filters input with the predicate `pred`. +/// +/// Like [`filter`](crate::filter), `pred` has access to all values in the input +/// container, but it returns [`Result`] instead of `bool`: +/// +/// - `Ok(true)`: the continuation is called (execution continues). +/// - `Ok(false)`: the handler returns [`ControlFlow::Continue`] (it +/// tries the next branch), just like `filter`. +/// - `Err(e)`: the handler short-circuits and returns +/// [`ControlFlow::Break`] with an error value built from `e`. +/// +/// The `Err` case is symmetric to how [`endpoint`](crate::endpoint) breaks the +/// chain with a value, and unlike `Ok(false)` it does **not** fall through to +/// sibling branches. +/// +/// The handler `Output` must be fallible (i.e. implement [`Fallible`]); in +/// practice this means it is a `Result`, and `E` is the error type that +/// `pred` returns on failure. +#[must_use] +#[track_caller] +pub fn try_filter<'a, Pred, Output, FnArgs, Descr>(pred: Pred) -> Handler<'a, Output, Descr> +where + Asyncify: Injectable, FnArgs> + MaybeSend + MaybeSync + 'a, + Output: Fallible + 'a, + Output::Error: MaybeSend, + Descr: HandlerDescription, +{ + try_filter_with_description(Descr::try_filter(), pred) +} + +/// The asynchronous version of [`try_filter`]. +#[must_use] +#[track_caller] +pub fn try_filter_async<'a, Pred, Output, FnArgs, Descr>(pred: Pred) -> Handler<'a, Output, Descr> +where + Pred: Injectable, FnArgs> + MaybeSend + MaybeSync + 'a, + Output: Fallible + 'a, + Output::Error: MaybeSend, + Descr: HandlerDescription, +{ + try_filter_async_with_description(Descr::try_filter_async(), pred) +} + +/// [`try_filter`] with a custom description. +#[must_use] +#[track_caller] +pub fn try_filter_with_description<'a, Pred, Output, FnArgs, Descr>( + description: Descr, + pred: Pred, +) -> Handler<'a, Output, Descr> +where + Asyncify: Injectable, FnArgs> + MaybeSend + MaybeSync + 'a, + Output: Fallible + 'a, + Output::Error: MaybeSend, +{ + try_filter_async_with_description(description, Asyncify(pred)) +} + +/// [`try_filter_async`] with a custom description. +#[must_use] +#[track_caller] +pub fn try_filter_async_with_description<'a, Pred, Output, FnArgs, Descr>( + description: Descr, + pred: Pred, +) -> Handler<'a, Output, Descr> +where + Pred: Injectable, FnArgs> + MaybeSend + MaybeSync + 'a, + Output: Fallible + 'a, + Output::Error: MaybeSend, +{ + let pred = Arc::new(pred); + + from_fn_with_description( + description, + move |event, cont| { + let pred = Arc::clone(&pred); + + async move { + let pred = pred.inject(&event); + let res = pred().await; + drop(pred); + + match res { + Ok(true) => cont(event).await, + Ok(false) => ControlFlow::Continue(event), + Err(err) => ControlFlow::Break(Output::from_error(err)), + } + } + }, + HandlerSignature::Other { + obligations: Pred::obligations(), + guaranteed_outcomes: BTreeSet::default(), + conditional_outcomes: BTreeSet::default(), + continues: true, + }, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{deps, help_inference}; + + type Out = Result; + + crate::cross_test! { + async fn ok_true_continues_to_endpoint() { + let result = help_inference::(try_filter(move |event: i32| { + assert_eq!(event, 5); + Ok(true) + })) + .endpoint(|| async move { Ok(7) }) + .dispatch(deps![5]) + .await; + + assert_eq!(result, ControlFlow::Break(Ok(7))); + } + + async fn ok_false_continues_to_next_branch() { + let result = help_inference::(try_filter(|| Ok(false))) + .endpoint(|| async move { unreachable!() }) + .dispatch(deps![]) + .await; + + assert!(matches!(result, ControlFlow::Continue(_))); + } + + async fn err_short_circuits_past_endpoint() { + let result = help_inference::(try_filter(|| Err::("nope"))) + .endpoint(|| async move { unreachable!() }) + .dispatch(deps![]) + .await; + + assert_eq!(result, ControlFlow::Break(Err("nope"))); + } + + async fn async_variant() { + let result = help_inference::(try_filter_async(move |event: i32| async move { + assert_eq!(event, 5); + Ok(event > 0) + })) + .endpoint(|| async move { Ok(7) }) + .dispatch(deps![5]) + .await; + + assert_eq!(result, ControlFlow::Break(Ok(7))); + } + } +} diff --git a/src/handler/try_filter_map.rs b/src/handler/try_filter_map.rs new file mode 100644 index 0000000..61a9a9c --- /dev/null +++ b/src/handler/try_filter_map.rs @@ -0,0 +1,174 @@ +use crate::{ + di::{Asyncify, Injectable}, + from_fn_with_description, + send::{MaybeSend, MaybeSync}, + Fallible, Handler, HandlerDescription, HandlerSignature, Type, +}; + +use std::{collections::BTreeSet, iter::FromIterator, ops::ControlFlow, sync::Arc}; + +/// Constructs a fallible handler that optionally passes a value of a new type +/// further. +/// +/// Like [`filter_map`](crate::filter_map), `proj` may add a value to the +/// container, but it returns [`Result, E>`]: +/// +/// - `Ok(Some(v))`: `v` is inserted into the container and the +/// continuation is called. +/// - `Ok(None)`: the handler returns [`ControlFlow::Continue`] (it +/// tries the next branch), just like `filter_map` returning `None`. +/// - `Err(e)`: the handler short-circuits and returns +/// [`ControlFlow::Break`] with an error value built from `e`. +/// +/// The handler `Output` must be fallible (i.e. implement [`Fallible`]); in +/// practice this means it is a `Result`, and `E` is the error type that +/// `proj` returns on failure. +#[must_use] +#[track_caller] +pub fn try_filter_map<'a, Projection, Output, NewType, Args, Descr>( + proj: Projection, +) -> Handler<'a, Output, Descr> +where + Asyncify: + Injectable, Output::Error>, Args> + MaybeSend + MaybeSync + 'a, + Output: Fallible + 'a, + Output::Error: MaybeSend, + Descr: HandlerDescription, + NewType: Send + Sync + 'static, +{ + try_filter_map_with_description(Descr::try_filter_map(), proj) +} + +/// The asynchronous version of [`try_filter_map`]. +#[must_use] +#[track_caller] +pub fn try_filter_map_async<'a, Projection, Output, NewType, Args, Descr>( + proj: Projection, +) -> Handler<'a, Output, Descr> +where + Projection: + Injectable, Output::Error>, Args> + MaybeSend + MaybeSync + 'a, + Output: Fallible + 'a, + Output::Error: MaybeSend, + Descr: HandlerDescription, + NewType: Send + Sync + 'static, +{ + try_filter_map_async_with_description(Descr::try_filter_map_async(), proj) +} + +/// [`try_filter_map`] with a custom description. +#[must_use] +#[track_caller] +pub fn try_filter_map_with_description<'a, Projection, Output, NewType, Args, Descr>( + description: Descr, + proj: Projection, +) -> Handler<'a, Output, Descr> +where + Asyncify: + Injectable, Output::Error>, Args> + MaybeSend + MaybeSync + 'a, + Output: Fallible + 'a, + Output::Error: MaybeSend, + NewType: Send + Sync + 'static, +{ + try_filter_map_async_with_description(description, Asyncify(proj)) +} + +/// [`try_filter_map_async`] with a custom description. +#[must_use] +#[track_caller] +pub fn try_filter_map_async_with_description<'a, Projection, Output, NewType, Args, Descr>( + description: Descr, + proj: Projection, +) -> Handler<'a, Output, Descr> +where + Projection: + Injectable, Output::Error>, Args> + MaybeSend + MaybeSync + 'a, + Output: Fallible + 'a, + Output::Error: MaybeSend, + NewType: Send + Sync + 'static, +{ + let proj = Arc::new(proj); + + from_fn_with_description( + description, + move |container, cont| { + let proj = Arc::clone(&proj); + + async move { + let proj = proj.inject(&container); + let res = proj().await; + drop(proj); + + match res { + Ok(Some(new_type)) => { + let mut intermediate = container.clone(); + intermediate.insert(new_type); + match cont(intermediate).await { + ControlFlow::Continue(_) => ControlFlow::Continue(container), + ControlFlow::Break(result) => ControlFlow::Break(result), + } + } + Ok(None) => ControlFlow::Continue(container), + Err(err) => ControlFlow::Break(Output::from_error(err)), + } + } + }, + HandlerSignature::Other { + obligations: Projection::obligations(), + guaranteed_outcomes: BTreeSet::from_iter(vec![Type::of::()]), + conditional_outcomes: BTreeSet::default(), + continues: true, + }, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{deps, help_inference}; + + type Out = Result; + + crate::cross_test! { + async fn ok_some_inserts_and_continues() { + let result = help_inference::(try_filter_map(move || Ok(Some(123)))) + .endpoint(move |event: i32| async move { + assert_eq!(event, 123); + Ok(event) + }) + .dispatch(deps![]) + .await; + + assert_eq!(result, ControlFlow::Break(Ok(123))); + } + + async fn ok_none_continues_to_next_branch() { + let result = help_inference::(try_filter_map(|| Ok::, &str>(None))) + .endpoint(|| async move { unreachable!() }) + .dispatch(deps![]) + .await; + + assert!(matches!(result, ControlFlow::Continue(_))); + } + + async fn err_short_circuits_past_endpoint() { + let result = help_inference::(try_filter_map(|| Err::, _>("nope"))) + .endpoint(|| async move { unreachable!() }) + .dispatch(deps![]) + .await; + + assert_eq!(result, ControlFlow::Break(Err("nope"))); + } + + async fn async_variant() { + let result = help_inference::(try_filter_map_async(move || async move { + Ok::, &str>(Some(123)) + })) + .endpoint(move |event: i32| async move { Ok(event) }) + .dispatch(deps![]) + .await; + + assert_eq!(result, ControlFlow::Break(Ok(123))); + } + } +} diff --git a/src/handler/try_map.rs b/src/handler/try_map.rs new file mode 100644 index 0000000..84bf7d5 --- /dev/null +++ b/src/handler/try_map.rs @@ -0,0 +1,161 @@ +use crate::{ + di::{Asyncify, Injectable}, + from_fn_with_description, + send::{MaybeSend, MaybeSync}, + Fallible, Handler, HandlerDescription, HandlerSignature, Type, +}; + +use std::{collections::BTreeSet, iter::FromIterator, ops::ControlFlow, sync::Arc}; + +/// Constructs a fallible handler that passes a value of a new type further. +/// +/// Like [`map`](crate::map), the result of invoking `proj` is added to the +/// container and passed further down the chain, but `proj` returns +/// [`Result`]: +/// +/// - `Ok(v)`: `v` is inserted into the container and the continuation +/// is called. +/// - `Err(e)`: the handler short-circuits and returns +/// [`ControlFlow::Break`] with an error value built from `e`, without +/// calling the continuation. +/// +/// The handler `Output` must be fallible (i.e. implement [`Fallible`]); in +/// practice this means it is a `Result`, and `E` is the error type that +/// `proj` returns on failure. +/// +/// See also: [`try_filter_map`](crate::try_filter_map). +#[must_use] +#[track_caller] +pub fn try_map<'a, Projection, Output, NewType, Args, Descr>( + proj: Projection, +) -> Handler<'a, Output, Descr> +where + Asyncify: + Injectable, Args> + MaybeSend + MaybeSync + 'a, + Output: Fallible + 'a, + Output::Error: MaybeSend, + Descr: HandlerDescription, + NewType: Send + Sync + 'static, +{ + try_map_with_description(Descr::try_map(), proj) +} + +/// The asynchronous version of [`try_map`]. +#[must_use] +#[track_caller] +pub fn try_map_async<'a, Projection, Output, NewType, Args, Descr>( + proj: Projection, +) -> Handler<'a, Output, Descr> +where + Projection: Injectable, Args> + MaybeSend + MaybeSync + 'a, + Output: Fallible + 'a, + Output::Error: MaybeSend, + Descr: HandlerDescription, + NewType: Send + Sync + 'static, +{ + try_map_async_with_description(Descr::try_map_async(), proj) +} + +/// [`try_map`] with a custom description. +#[must_use] +#[track_caller] +pub fn try_map_with_description<'a, Projection, Output, NewType, Args, Descr>( + description: Descr, + proj: Projection, +) -> Handler<'a, Output, Descr> +where + Asyncify: + Injectable, Args> + MaybeSend + MaybeSync + 'a, + Output: Fallible + 'a, + Output::Error: MaybeSend, + NewType: Send + Sync + 'static, +{ + try_map_async_with_description(description, Asyncify(proj)) +} + +/// [`try_map_async`] with a custom description. +#[must_use] +#[track_caller] +pub fn try_map_async_with_description<'a, Projection, Output, NewType, Args, Descr>( + description: Descr, + proj: Projection, +) -> Handler<'a, Output, Descr> +where + Projection: Injectable, Args> + MaybeSend + MaybeSync + 'a, + Output: Fallible + 'a, + Output::Error: MaybeSend, + NewType: Send + Sync + 'static, +{ + let proj = Arc::new(proj); + + from_fn_with_description( + description, + move |container, cont| { + let proj = Arc::clone(&proj); + + async move { + let proj = proj.inject(&container); + let res = proj().await; + drop(proj); + + match res { + Ok(new_type) => { + let mut intermediate = container.clone(); + intermediate.insert(new_type); + match cont(intermediate).await { + ControlFlow::Continue(_) => ControlFlow::Continue(container), + ControlFlow::Break(result) => ControlFlow::Break(result), + } + } + Err(err) => ControlFlow::Break(Output::from_error(err)), + } + } + }, + HandlerSignature::Other { + obligations: Projection::obligations(), + guaranteed_outcomes: BTreeSet::from_iter(vec![Type::of::()]), + conditional_outcomes: BTreeSet::new(), + continues: true, + }, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{deps, help_inference}; + + type Out = Result; + + crate::cross_test! { + async fn ok_inserts_and_continues() { + let result = help_inference::(try_map(move || Ok(123))) + .endpoint(move |event: i32| async move { + assert_eq!(event, 123); + Ok(event) + }) + .dispatch(deps![]) + .await; + + assert_eq!(result, ControlFlow::Break(Ok(123))); + } + + async fn err_short_circuits_past_endpoint() { + let result = help_inference::(try_map(|| Err::("nope"))) + .endpoint(|| async move { unreachable!() }) + .dispatch(deps![]) + .await; + + assert_eq!(result, ControlFlow::Break(Err("nope"))); + } + + async fn async_variant() { + let result = help_inference::(try_map_async(move || async move { Ok(123) })) + .endpoint(move |event: i32| async move { Ok(event) }) + .dispatch(deps![]) + .await; + + assert_eq!(result, ControlFlow::Break(Ok(123))); + } + } +}