Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T, E>`), 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

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
8 changes: 8 additions & 0 deletions src/handler.rs
Original file line number Diff line number Diff line change
@@ -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::*;
66 changes: 66 additions & 0 deletions src/handler/description.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
32 changes: 32 additions & 0 deletions src/handler/fallible.rs
Original file line number Diff line number Diff line change
@@ -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<T, E: 'static> Fallible for Result<T, E> {
type Error = E;

fn from_error(error: E) -> Self {
Err(error)
}
}
2 changes: 1 addition & 1 deletion src/handler/filter_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
2 changes: 1 addition & 1 deletion src/handler/map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
137 changes: 136 additions & 1 deletion src/handler/methods.rs
Original file line number Diff line number Diff line change
@@ -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>
Expand Down Expand Up @@ -103,6 +103,110 @@ where
{
self.chain(crate::endpoint(f))
}

/// Chain this handler with the fallible filter predicate `pred`.
///
/// `pred` returns [`Result<bool, E>`] (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<Pred, FnArgs>(self, pred: Pred) -> Handler<'a, Output, Descr>
where
Asyncify<Pred>:
Injectable<Result<bool, Output::Error>, 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<Pred, FnArgs>(self, pred: Pred) -> Handler<'a, Output, Descr>
where
Pred: Injectable<Result<bool, Output::Error>, 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<Option<NewType>, 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<Proj, NewType, Args>(self, proj: Proj) -> Handler<'a, Output, Descr>
where
Asyncify<Proj>:
Injectable<Result<Option<NewType>, 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<Proj, NewType, Args>(self, proj: Proj) -> Handler<'a, Output, Descr>
where
Proj: Injectable<Result<Option<NewType>, 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<NewType, E>`]. 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<Proj, NewType, Args>(self, proj: Proj) -> Handler<'a, Output, Descr>
where
Asyncify<Proj>:
Injectable<Result<NewType, Output::Error>, 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<Proj, NewType, Args>(self, proj: Proj) -> Handler<'a, Output, Descr>
where
Proj: Injectable<Result<NewType, Output::Error>, Args> + MaybeSend + MaybeSync + 'a,
Output: Fallible,
Output::Error: MaybeSend,
NewType: Send + Sync + 'static,
{
self.chain(crate::try_map_async(proj))
}
}

#[cfg(test)]
Expand Down Expand Up @@ -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<Result<(), &str>, _> = help_inference(crate::entry())
.try_filter(|| Ok::<bool, &str>(true))
.dispatch(deps![value])
.await;

let _: ControlFlow<Result<(), &str>, _> = help_inference(crate::entry())
.try_filter_async(|| async { Ok::<bool, &str>(true) })
.dispatch(deps![value])
.await;

let _: ControlFlow<Result<(), &str>, _> = help_inference(crate::entry())
.try_filter_map(|| Ok::<Option<()>, &str>(Some(())))
.dispatch(deps![value])
.await;

let _: ControlFlow<Result<(), &str>, _> = help_inference(crate::entry())
.try_filter_map_async(|| async { Ok::<Option<()>, &str>(Some(())) })
.dispatch(deps![value])
.await;

let _: ControlFlow<Result<(), &str>, _> = help_inference(crate::entry())
.try_map(|| Ok::<(), &str>(()))
.dispatch(deps![value])
.await;

let _: ControlFlow<Result<(), &str>, _> = help_inference(crate::entry())
.try_map_async(|| async { Ok::<(), &str>(()) })
.dispatch(deps![value])
.await;
}
}
}
Loading
Loading