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

## [Unreleased]

## [0.12.1] - 2025-10-30

### Added
- Introduced the `Context` builder for enriching error conversions with
metadata, caller tracking, and redaction hints via `ResultExt::ctx`.
- Implemented the `ResultExt` trait to wrap fallible operations into
`masterror::Error` without extra allocations while merging context fields.

### Documentation
- Added rustdoc examples showcasing `Context` chaining and the new
`ResultExt` helper.

### Tests
- Added unit coverage for `ResultExt::ctx`, ensuring happy-path results pass
through and error branches preserve metadata and sources.

## [0.12.0] - 2025-10-29

### Added
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.12.0"
version = "0.12.1"
rust-version = "1.90"
edition = "2024"
license = "MIT OR Apache-2.0"
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ guides, comparisons with `thiserror`/`anyhow`, and troubleshooting recipes.

~~~toml
[dependencies]
masterror = { version = "0.12.0", default-features = false }
masterror = { version = "0.12.1", default-features = false }
# or with features:
# masterror = { version = "0.12.0", features = [
# masterror = { version = "0.12.1", features = [
# "axum", "actix", "openapi", "serde_json",
# "sqlx", "sqlx-migrate", "reqwest", "redis",
# "validator", "config", "tokio", "multipart",
Expand Down Expand Up @@ -75,10 +75,10 @@ masterror = { version = "0.12.0", default-features = false }
~~~toml
[dependencies]
# lean core
masterror = { version = "0.12.0", default-features = false }
masterror = { version = "0.12.1", default-features = false }

# with Axum/Actix + JSON + integrations
# masterror = { version = "0.12.0", features = [
# masterror = { version = "0.12.1", features = [
# "axum", "actix", "openapi", "serde_json",
# "sqlx", "sqlx-migrate", "reqwest", "redis",
# "validator", "config", "tokio", "multipart",
Expand Down Expand Up @@ -637,13 +637,13 @@ assert_eq!(resp.status, 401);
Minimal core:

~~~toml
masterror = { version = "0.12.0", default-features = false }
masterror = { version = "0.12.1", default-features = false }
~~~

API (Axum + JSON + deps):

~~~toml
masterror = { version = "0.12.0", features = [
masterror = { version = "0.12.1", features = [
"axum", "serde_json", "openapi",
"sqlx", "reqwest", "redis", "validator", "config", "tokio"
] }
Expand All @@ -652,7 +652,7 @@ masterror = { version = "0.12.0", features = [
API (Actix + JSON + deps):

~~~toml
masterror = { version = "0.12.0", features = [
masterror = { version = "0.12.1", features = [
"actix", "serde_json", "openapi",
"sqlx", "reqwest", "redis", "validator", "config", "tokio"
] }
Expand Down
4 changes: 2 additions & 2 deletions README.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@
~~~toml
[dependencies]
# минимальное ядро
masterror = { version = "0.12.0", default-features = false }
masterror = { version = "0.12.1", default-features = false }
# или с нужными интеграциями
# masterror = { version = "0.12.0", features = [
# masterror = { version = "0.12.1", features = [
# "axum", "actix", "openapi", "serde_json",
# "sqlx", "sqlx-migrate", "reqwest", "redis",
# "validator", "config", "tokio", "multipart",
Expand Down
2 changes: 2 additions & 0 deletions src/app_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,13 @@
//! transport boundary (e.g. in `IntoResponse`) to avoid duplicate logs.

mod constructors;
mod context;
mod core;
mod metadata;

pub use core::{AppError, AppResult, Error, MessageEditPolicy};

pub use context::Context;
pub use metadata::{Field, FieldValue, Metadata, field};

#[cfg(test)]
Expand Down
141 changes: 141 additions & 0 deletions src/app_error/context.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
use std::{error::Error as StdError, panic::Location};

use super::{
core::{AppError, Error, MessageEditPolicy},
metadata::{Field, FieldValue}
};
use crate::{AppCode, AppErrorKind};

/// Builder describing how to convert an external error into [`AppError`].
///
/// The context captures the target [`AppCode`], [`AppErrorKind`], optional
/// metadata fields and redaction policy. It is primarily consumed by
/// [`ResultExt`](crate::ResultExt) when promoting `Result<T, E>` values into
/// [`AppError`].
///
/// # Examples
///
/// ```rust
/// use std::io::{Error as IoError, ErrorKind};
///
/// use masterror::{AppErrorKind, Context, ResultExt, field};
///
/// fn failing_io() -> Result<(), IoError> {
/// Err(IoError::from(ErrorKind::Other))
/// }
///
/// let err = failing_io()
/// .ctx(|| {
/// Context::new(AppErrorKind::Service)
/// .with(field::str("operation", "sync"))
/// .redact(true)
/// .track_caller()
/// })
/// .unwrap_err();
///
/// assert_eq!(err.kind, AppErrorKind::Service);
/// assert!(err.metadata().get("operation").is_some());
/// ```
#[derive(Debug, Clone)]
pub struct Context {
code: AppCode,
category: AppErrorKind,
fields: Vec<Field>,
edit_policy: MessageEditPolicy,
caller_location: Option<&'static Location<'static>>,
code_overridden: bool
}

impl Context {
/// Create a new [`Context`] targeting the provided [`AppErrorKind`].
///
/// The initial [`AppCode`] defaults to the canonical mapping for the
/// supplied kind. Use [`Context::code`] to override it.
#[must_use]
pub fn new(category: AppErrorKind) -> Self {
Self {
code: AppCode::from(category),
category,
fields: Vec::new(),
edit_policy: MessageEditPolicy::Preserve,
caller_location: None,
code_overridden: false
}
}

/// Override the public [`AppCode`].
#[must_use]
pub fn code(mut self, code: AppCode) -> Self {
self.code = code;
self.code_overridden = true;
self
}

/// Update the [`AppErrorKind`].
///
/// When the code has not been overridden explicitly, it is kept in sync
/// with the new kind.
#[must_use]
pub fn category(mut self, category: AppErrorKind) -> Self {
self.category = category;
if !self.code_overridden {
self.code = AppCode::from(category);
}
self
}

/// Attach a metadata [`Field`].
#[must_use]
pub fn with(mut self, field: Field) -> Self {
self.fields.push(field);
self
}

/// Toggle message redaction policy.
#[must_use]
pub fn redact(mut self, redact: bool) -> Self {
self.edit_policy = if redact {
MessageEditPolicy::Redact
} else {
MessageEditPolicy::Preserve
};
self
}

/// Capture caller location and store it as metadata.
#[must_use]
#[track_caller]
pub fn track_caller(mut self) -> Self {
self.caller_location = Some(Location::caller());
self
}

pub(crate) fn into_error<E>(mut self, source: E) -> Error
where
E: StdError + Send + Sync + 'static
{
if let Some(location) = self.caller_location {
self.fields.push(Field::new(
"caller.file",
FieldValue::Str(location.file().into())
));
self.fields.push(Field::new(
"caller.line",
FieldValue::U64(u64::from(location.line()))
));
self.fields.push(Field::new(
"caller.column",
FieldValue::U64(u64::from(location.column()))
));
}

let mut error = AppError::bare(self.category).with_code(self.code);
if !self.fields.is_empty() {
error = error.with_fields(self.fields);
}
if matches!(self.edit_policy, MessageEditPolicy::Redact) {
error = error.redactable();
}
error.with_source(source)
}
}
4 changes: 3 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ mod kind;
#[doc(hidden)]
pub mod provide;
mod response;
mod result_ext;

#[cfg(feature = "frontend")]
#[cfg_attr(docsrs, doc(cfg(feature = "frontend")))]
Expand All @@ -246,7 +247,7 @@ pub mod turnkey;
pub mod prelude;

pub use app_error::{
AppError, AppResult, Error, Field, FieldValue, MessageEditPolicy, Metadata, field
AppError, AppResult, Context, Error, Field, FieldValue, MessageEditPolicy, Metadata, field
};
pub use code::AppCode;
pub use kind::AppErrorKind;
Expand Down Expand Up @@ -278,3 +279,4 @@ pub use kind::AppErrorKind;
/// ```
pub use masterror_derive::*;
pub use response::{ErrorResponse, RetryAdvice};
pub use result_ext::ResultExt;
Loading
Loading