Skip to content
Merged
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

## [0.21.0] - 2025-10-08

### Added
- Introduced typed `ensure!` and `fail!` macros as allocation-free alternatives
to `anyhow::ensure!`/`anyhow::bail!`, complete with documentation and tests.

### Changed
- Highlighted the new control-flow macros across the English and Russian
READMEs and module documentation so adopters discover them alongside the
derive tooling.
## [0.20.8] - 2025-10-08

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "masterror"
version = "0.20.8"
version = "0.21.0"
rust-version = "1.90"
edition = "2024"
license = "MIT OR Apache-2.0"
Expand Down
32 changes: 30 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ of redaction and metadata.
- **Turnkey defaults.** The `turnkey` module ships a ready-to-use error catalog,
helper builders and tracing instrumentation for teams that want a consistent
baseline out of the box.
- **Typed control-flow macros.** `ensure!` and `fail!` short-circuit functions
with your domain errors without allocating or formatting on the happy path.

### Workspace crates

Expand Down Expand Up @@ -71,9 +73,9 @@ The build script keeps the full feature snippet below in sync with

~~~toml
[dependencies]
masterror = { version = "0.20.8", default-features = false }
masterror = { version = "0.21.0", default-features = false }
# or with features:
# masterror = { version = "0.20.8", features = [
# masterror = { version = "0.21.0", features = [
# "axum", "actix", "openapi", "serde_json",
# "tracing", "metrics", "backtrace", "sqlx",
# "sqlx-migrate", "reqwest", "redis", "validator",
Expand Down Expand Up @@ -114,6 +116,32 @@ fn do_work(flag: bool) -> AppResult<()> {

</details>

<details>
<summary><b>Fail fast without sacrificing typing</b></summary>

`ensure!` and `fail!` provide typed alternatives to the formatting-heavy
`anyhow::ensure!`/`anyhow::bail!` helpers. They evaluate the error expression
only when the guard trips, so success paths stay allocation-free.

~~~rust
use masterror::{AppError, AppErrorKind, AppResult};

fn guard(flag: bool) -> AppResult<()> {
masterror::ensure!(flag, AppError::bad_request("flag must be set"));
Ok(())
}

fn bail() -> AppResult<()> {
masterror::fail!(AppError::unauthorized("token expired"));
}

assert!(guard(true).is_ok());
assert!(matches!(guard(false).unwrap_err().kind, AppErrorKind::BadRequest));
assert!(matches!(bail().unwrap_err().kind, AppErrorKind::Unauthorized));
~~~

</details>

<details>
<summary><b>Derive domain errors and map them to transports</b></summary>

Expand Down
29 changes: 27 additions & 2 deletions README.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ MSRV зафиксирован, а родные деривы позволяют
- **Готовые настройки.** Модуль `turnkey` поставляет готовый каталог ошибок,
билдеры и интеграцию с `tracing` для команд, которым нужна стартовая
конфигурация «из коробки».
- **Типобезопасные макросы управления потоком.** `ensure!` и `fail!` прерывают
выполнение с доменной ошибкой без аллокаций и форматирования на удачной ветке.

## Состав workspace

Expand All @@ -65,9 +67,9 @@ MSRV зафиксирован, а родные деривы позволяют

~~~toml
[dependencies]
masterror = { version = "0.20.5", default-features = false }
masterror = { version = "0.21.0", default-features = false }
# или с нужными фичами:
# masterror = { version = "0.20.5", features = [
# masterror = { version = "0.21.0", features = [
# "axum", "actix", "openapi", "serde_json",
# "tracing", "metrics", "backtrace", "sqlx",
# "sqlx-migrate", "reqwest", "redis", "validator",
Expand Down Expand Up @@ -105,6 +107,29 @@ fn do_work(flag: bool) -> AppResult<()> {
}
~~~

### Макросы для раннего возврата без потери типизации

`ensure!` и `fail!` — типизированные аналоги `anyhow::ensure!`/`anyhow::bail!`.
Они вычисляют выражение ошибки только при срабатывании гварда, поэтому
успешный путь остаётся без аллокаций.

~~~rust
use masterror::{AppError, AppErrorKind, AppResult};

fn guard(flag: bool) -> AppResult<()> {
masterror::ensure!(flag, AppError::bad_request("Флаг обязателен"));
Ok(())
}

fn bail() -> AppResult<()> {
masterror::fail!(AppError::unauthorized("Токен истёк"));
}

assert!(guard(true).is_ok());
assert!(matches!(guard(false).unwrap_err().kind, AppErrorKind::BadRequest));
assert!(matches!(bail().unwrap_err().kind, AppErrorKind::Unauthorized));
~~~

### Деривы для доменных ошибок и транспорта

`masterror` предоставляет родные деривы, чтобы типы оставались выразительными, а
Expand Down
28 changes: 28 additions & 0 deletions README.template.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ of redaction and metadata.
- **Turnkey defaults.** The `turnkey` module ships a ready-to-use error catalog,
helper builders and tracing instrumentation for teams that want a consistent
baseline out of the box.
- **Typed control-flow macros.** `ensure!` and `fail!` short-circuit functions
with your domain errors without allocating or formatting on the happy path.

### Workspace crates

Expand Down Expand Up @@ -110,6 +112,32 @@ fn do_work(flag: bool) -> AppResult<()> {

</details>

<details>
<summary><b>Fail fast without sacrificing typing</b></summary>

`ensure!` and `fail!` provide typed alternatives to the formatting-heavy
`anyhow::ensure!`/`anyhow::bail!` helpers. They evaluate the error expression
only when the guard trips, so success paths stay allocation-free.

~~~rust
use masterror::{AppError, AppErrorKind, AppResult};

fn guard(flag: bool) -> AppResult<()> {
masterror::ensure!(flag, AppError::bad_request("flag must be set"));
Ok(())
}

fn bail() -> AppResult<()> {
masterror::fail!(AppError::unauthorized("token expired"));
}

assert!(guard(true).is_ok());
assert!(matches!(guard(false).unwrap_err().kind, AppErrorKind::BadRequest));
assert!(matches!(bail().unwrap_err().kind, AppErrorKind::Unauthorized));
~~~

</details>

<details>
<summary><b>Derive domain errors and map them to transports</b></summary>

Expand Down
30 changes: 30 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,35 @@
//! assert!(matches!(resp.code, AppCode::NotFound));
//! ```
//!
//! # Typed control-flow macros
//!
//! Reach for [`ensure!`] and [`fail!`] when you need to exit early with a typed
//! error without paying for string formatting or heap allocations on the
//! success path.
//!
//! ```rust
//! use masterror::{AppError, AppErrorKind, AppResult};
//!
//! fn guard(flag: bool) -> AppResult<()> {
//! masterror::ensure!(flag, AppError::bad_request("flag must be set"));
//! Ok(())
//! }
//!
//! fn bail() -> AppResult<()> {
//! masterror::fail!(AppError::unauthorized("token expired"));
//! }
//!
//! assert!(guard(true).is_ok());
//! assert!(matches!(
//! guard(false).unwrap_err().kind,
//! AppErrorKind::BadRequest
//! ));
//! assert!(matches!(
//! bail().unwrap_err().kind,
//! AppErrorKind::Unauthorized
//! ));
//! ```
//!
//! # Axum integration
//!
//! With the `axum` feature enabled, you can return [`AppError`] directly from
Expand Down Expand Up @@ -302,6 +331,7 @@ mod code;
mod convert;
pub mod error;
mod kind;
mod macros;
#[cfg(error_generic_member_access)]
#[doc(hidden)]
pub mod provide;
Expand Down
100 changes: 100 additions & 0 deletions src/macros.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//! Control-flow macros for early returns with typed errors.
//!
//! These macros complement the typed [`AppError`](crate::AppError) APIs by
//! providing a lightweight, allocation-free way to short-circuit functions when
//! invariants are violated. Unlike the dynamic formatting helpers offered by
//! `anyhow` or `eyre`, the macros operate on pre-constructed error values so
//! the compiler keeps strong typing guarantees and no formatting work happens
//! on the success path.
//!
//! ```rust
//! use masterror::{AppError, AppErrorKind, AppResult};
//!
//! fn guard(flag: bool) -> AppResult<()> {
//! masterror::ensure!(flag, AppError::bad_request("flag must be true"));
//! Ok(())
//! }
//!
//! assert!(guard(true).is_ok());
//! assert!(matches!(
//! guard(false).unwrap_err().kind,
//! AppErrorKind::BadRequest
//! ));
//! ```

/// Abort the enclosing function with an error when a condition fails.
///
/// The macro takes either a bare condition and error expression, or the more
/// explicit `cond = ..., else = ...` form. The error expression is evaluated
/// lazily only when the condition is false.
///
/// # Examples
///
/// Short-circuit a typed error:
///
/// ```rust
/// use masterror::{AppError, AppErrorKind, AppResult};
///
/// fn require(flag: bool) -> AppResult<()> {
/// masterror::ensure!(flag, AppError::bad_request("flag required"));
/// Ok(())
/// }
///
/// assert!(matches!(
/// require(false).unwrap_err().kind,
/// AppErrorKind::BadRequest
/// ));
/// ```
///
/// Use the verbose syntax for clarity in complex conditions:
///
/// ```rust
/// use masterror::{AppError, AppResult};
///
/// fn bounded(value: i32, max: i32) -> AppResult<()> {
/// masterror::ensure!(
/// cond = value <= max,
/// else = AppError::service("value too large")
/// );
/// Ok(())
/// }
///
/// assert!(bounded(2, 3).is_ok());
/// assert!(bounded(5, 3).is_err());
/// ```
#[macro_export]
macro_rules! ensure {
(cond = $cond:expr, else = $err:expr $(,)?) => {
$crate::ensure!($cond, $err)
};
($cond:expr, $err:expr $(,)?) => {
if !$cond {
return Err($err);
}
};
}

/// Abort the enclosing function with the provided error.
///
/// This macro is a typed alternative to `anyhow::bail!`, delegating the
/// decision of how to construct the error to the caller. It never performs
/// formatting or allocations on the success path.
///
/// # Examples
///
/// ```rust
/// use masterror::{AppError, AppErrorKind, AppResult};
///
/// fn reject() -> AppResult<()> {
/// masterror::fail!(AppError::unauthorized("token expired"));
/// }
///
/// let err = reject().unwrap_err();
/// assert!(matches!(err.kind, AppErrorKind::Unauthorized));
/// ```
#[macro_export]
macro_rules! fail {
($err:expr $(,)?) => {
return Err($err);
};
}
Loading
Loading