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

## [Unreleased]

## [0.22.0] - 2025-10-11

### Added
- Introduced an explicit `std` feature (enabled by default) and made the core
crate compile in `no_std + alloc` environments, including metadata builders
and error helpers.

### Changed
- Reworked `AppError` internals to rely on `core`/`alloc` primitives and
`core::error::Error`, providing `std::error::Error` only when the `std`
feature is active.
- Replaced `thiserror` derives on `AppErrorKind` with manual `Display`/error
implementations so the taxonomy remains available without the standard
library.

## [0.21.2] - 2025-10-10

### 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.

61 changes: 35 additions & 26 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "masterror"
version = "0.21.2"
version = "0.22.0"
rust-version = "1.90"
edition = "2024"
license = "MIT OR Apache-2.0"
Expand Down Expand Up @@ -49,30 +49,34 @@ readme = "README.md"


[features]
default = []
tracing = ["dep:tracing", "dep:log", "dep:log-mdc"]
metrics = ["dep:metrics"]
backtrace = []
axum = ["dep:axum", "dep:serde_json"]
actix = ["dep:actix-web", "dep:serde_json"]
default = ["std"]
std = [
"uuid/std",
"serde/std"
]
tracing = ["dep:tracing", "dep:log", "dep:log-mdc", "std"]
metrics = ["dep:metrics", "std"]
backtrace = ["std"]
axum = ["dep:axum", "dep:serde_json", "std"]
actix = ["dep:actix-web", "dep:serde_json", "std"]

# Разделили: лёгкая обработка ошибок (sqlx-core) и опциональные миграции (полный sqlx)
sqlx = ["dep:sqlx-core"] # maps sqlx_core::Error
sqlx-migrate = ["dep:sqlx"] # maps sqlx::migrate::MigrateError

redis = ["dep:redis"]
validator = ["dep:validator"]
serde_json = ["dep:serde_json"]
config = ["dep:config"]
multipart = ["axum"]
tokio = ["dep:tokio"]
reqwest = ["dep:reqwest"]
teloxide = ["dep:teloxide-core"]
telegram-webapp-sdk = ["dep:telegram-webapp-sdk"]
frontend = ["dep:wasm-bindgen", "dep:js-sys", "dep:serde-wasm-bindgen"]
turnkey = []
tonic = ["dep:tonic"]
openapi = ["dep:utoipa"]
redis = ["dep:redis", "std"]
validator = ["dep:validator", "std"]
serde_json = ["dep:serde_json", "std"]
config = ["dep:config", "std"]
multipart = ["axum", "std"]
tokio = ["dep:tokio", "std"]
reqwest = ["dep:reqwest", "std"]
teloxide = ["dep:teloxide-core", "std"]
telegram-webapp-sdk = ["dep:telegram-webapp-sdk", "std"]
frontend = ["dep:wasm-bindgen", "dep:js-sys", "dep:serde-wasm-bindgen", "std"]
turnkey = ["std"]
tonic = ["dep:tonic", "std"]
openapi = ["dep:utoipa", "std"]

[workspace.dependencies]
masterror-derive = { version = "0.9.0" }
Expand All @@ -81,13 +85,16 @@ masterror-template = { version = "0.3.6" }
[dependencies]
masterror-derive = { version = "0.9" }
masterror-template = { workspace = true }
tracing = { version = "0.1", optional = true }
tracing = { version = "0.1", optional = true, default-features = false, features = [
"attributes",
"std"
] }
log = { version = "0.4", optional = true }
log-mdc = { version = "0.1", optional = true }
metrics = { version = "0.24", optional = true }

serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", optional = true }
serde = { version = "1", default-features = false, features = ["derive", "alloc"] }
serde_json = { version = "1", optional = true, default-features = false, features = ["std"] }
http = "1"
sha2 = "0.10"

Expand Down Expand Up @@ -118,9 +125,7 @@ telegram-webapp-sdk = { version = "0.2", optional = true }
wasm-bindgen = { version = "0.2", optional = true }
js-sys = { version = "0.3", optional = true }
serde-wasm-bindgen = { version = "0.6", optional = true }
uuid = { version = "1", default-features = false, features = [
"std"
] }
uuid = { version = "1", default-features = false }
tonic = { version = "0.12", optional = true }

[dev-dependencies]
Expand All @@ -142,6 +147,7 @@ toml = "0.9"

[package.metadata.masterror.readme]
feature_order = [
"std",
"axum",
"actix",
"openapi",
Expand Down Expand Up @@ -184,6 +190,9 @@ description = "IntoResponse integration with structured JSON bodies"
[package.metadata.masterror.readme.features.actix]
description = "Actix Web ResponseError and Responder implementations"

[package.metadata.masterror.readme.features.std]
description = "Enable std support (default); required for runtime integrations"

[package.metadata.masterror.readme.features.openapi]
description = "Generate utoipa OpenAPI schema for ErrorResponse"

Expand Down
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,15 @@ The build script keeps the full feature snippet below in sync with

~~~toml
[dependencies]
masterror = { version = "0.21.2", default-features = false }
masterror = { version = "0.22.0", default-features = false }
# or with features:
# masterror = { version = "0.21.2", features = [
# "axum", "actix", "openapi", "serde_json",
# "tracing", "metrics", "backtrace", "sqlx",
# "sqlx-migrate", "reqwest", "redis", "validator",
# "config", "tokio", "multipart", "teloxide",
# "telegram-webapp-sdk", "tonic", "frontend", "turnkey"
# masterror = { version = "0.22.0", features = [
# "std", "axum", "actix", "openapi",
# "serde_json", "tracing", "metrics", "backtrace",
# "sqlx", "sqlx-migrate", "reqwest", "redis",
# "validator", "config", "tokio", "multipart",
# "teloxide", "telegram-webapp-sdk", "tonic", "frontend",
# "turnkey"
# ] }
~~~

Expand Down Expand Up @@ -418,4 +419,3 @@ assert_eq!(problem.grpc.expect("grpc").name, "UNAUTHENTICATED");
---

MSRV: **1.90** · License: **MIT OR Apache-2.0** · No `unsafe`

2 changes: 1 addition & 1 deletion src/app_error/constructors.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::borrow::Cow;
use alloc::borrow::Cow;

use super::core::AppError;
use crate::AppErrorKind;
Expand Down
5 changes: 3 additions & 2 deletions src/app_error/context.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::{error::Error as StdError, panic::Location};
use alloc::vec::Vec;
use core::{error::Error as CoreError, panic::Location};

use super::{
core::{AppError, Error, MessageEditPolicy},
Expand Down Expand Up @@ -133,7 +134,7 @@ impl Context {

pub(crate) fn into_error<E>(mut self, source: E) -> Error
where
E: StdError + Send + Sync + 'static
E: CoreError + Send + Sync + 'static
{
if let Some(location) = self.caller_location {
self.fields.push(Field::new(
Expand Down
56 changes: 33 additions & 23 deletions src/app_error/core.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
use alloc::{
borrow::Cow,
boxed::Box,
string::{String, ToString},
sync::Arc
};
use core::{
error::Error as CoreError,
fmt::{Display, Formatter, Result as FmtResult},
ops::{Deref, DerefMut},
sync::atomic::{AtomicBool, Ordering}
};
#[cfg(feature = "backtrace")]
use std::{
backtrace::Backtrace,
Expand All @@ -7,23 +19,21 @@ use std::{
atomic::{AtomicU8, Ordering as AtomicOrdering}
}
};
use std::{
borrow::Cow,
error::Error as StdError,
fmt::{Display, Formatter, Result as FmtResult},
ops::{Deref, DerefMut},
sync::{
Arc,
atomic::{AtomicBool, Ordering}
}
};

#[cfg(feature = "tracing")]
use tracing::{Level, event};

use super::metadata::{Field, FieldRedaction, Metadata};
use crate::{AppCode, AppErrorKind, RetryAdvice};

#[cfg(feature = "std")]
pub type CapturedBacktrace = std::backtrace::Backtrace;

#[cfg(not(feature = "std"))]
#[allow(dead_code)]
#[derive(Debug)]
pub enum CapturedBacktrace {}

/// Controls whether the public message may be redacted before exposure.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum MessageEditPolicy {
Expand Down Expand Up @@ -51,7 +61,7 @@ pub struct ErrorInner {
pub retry: Option<RetryAdvice>,
/// Optional authentication challenge for `WWW-Authenticate`.
pub www_authenticate: Option<String>,
pub source: Option<Arc<dyn StdError + Send + Sync + 'static>>,
pub source: Option<Arc<dyn CoreError + Send + Sync + 'static>>,
#[cfg(feature = "backtrace")]
pub backtrace: Option<Backtrace>,
#[cfg(feature = "backtrace")]
Expand Down Expand Up @@ -184,11 +194,11 @@ impl Display for Error {
}
}

impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
impl CoreError for Error {
fn source(&self) -> Option<&(dyn CoreError + 'static)> {
self.source
.as_deref()
.map(|source| source as &(dyn StdError + 'static))
.map(|source| source as &(dyn CoreError + 'static))
}
}

Expand Down Expand Up @@ -247,7 +257,7 @@ impl Error {
}

#[cfg(feature = "backtrace")]
fn capture_backtrace(&self) -> Option<&std::backtrace::Backtrace> {
fn capture_backtrace(&self) -> Option<&CapturedBacktrace> {
if let Some(backtrace) = self.backtrace.as_ref() {
return Some(backtrace);
}
Expand All @@ -258,18 +268,18 @@ impl Error {
}

#[cfg(not(feature = "backtrace"))]
fn capture_backtrace(&self) -> Option<&std::backtrace::Backtrace> {
fn capture_backtrace(&self) -> Option<&CapturedBacktrace> {
None
}

#[cfg(feature = "backtrace")]
fn set_backtrace_slot(&mut self, backtrace: std::backtrace::Backtrace) {
fn set_backtrace_slot(&mut self, backtrace: CapturedBacktrace) {
self.backtrace = Some(backtrace);
self.captured_backtrace = OnceLock::new();
}

#[cfg(not(feature = "backtrace"))]
fn set_backtrace_slot(&mut self, _backtrace: std::backtrace::Backtrace) {}
fn set_backtrace_slot(&mut self, _backtrace: CapturedBacktrace) {}

pub(crate) fn emit_telemetry(&self) {
if self.take_dirty() {
Expand Down Expand Up @@ -416,7 +426,7 @@ impl Error {

/// Attach a source error for diagnostics.
#[must_use]
pub fn with_source(mut self, source: impl StdError + Send + Sync + 'static) -> Self {
pub fn with_source(mut self, source: impl CoreError + Send + Sync + 'static) -> Self {
self.source = Some(Arc::new(source));
self.mark_dirty();
self
Expand All @@ -437,15 +447,15 @@ impl Error {
/// assert_eq!(Arc::strong_count(&source), 2);
/// ```
#[must_use]
pub fn with_source_arc(mut self, source: Arc<dyn StdError + Send + Sync + 'static>) -> Self {
pub fn with_source_arc(mut self, source: Arc<dyn CoreError + Send + Sync + 'static>) -> Self {
self.source = Some(source);
self.mark_dirty();
self
}

/// Attach a captured backtrace.
#[must_use]
pub fn with_backtrace(mut self, backtrace: std::backtrace::Backtrace) -> Self {
pub fn with_backtrace(mut self, backtrace: CapturedBacktrace) -> Self {
self.set_backtrace_slot(backtrace);
self.mark_dirty();
self
Expand All @@ -460,13 +470,13 @@ impl Error {
/// Borrow the backtrace, capturing it lazily when the `backtrace` feature
/// is enabled.
#[must_use]
pub fn backtrace(&self) -> Option<&std::backtrace::Backtrace> {
pub fn backtrace(&self) -> Option<&CapturedBacktrace> {
self.capture_backtrace()
}

/// Borrow the source if present.
#[must_use]
pub fn source_ref(&self) -> Option<&(dyn StdError + Send + Sync + 'static)> {
pub fn source_ref(&self) -> Option<&(dyn CoreError + Send + Sync + 'static)> {
self.source.as_deref()
}

Expand Down
16 changes: 8 additions & 8 deletions src/app_error/metadata.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use std::{
borrow::Cow,
collections::BTreeMap,
use alloc::{borrow::Cow, collections::BTreeMap, string::String};
use core::{
fmt::{Display, Formatter, Result as FmtResult, Write},
net::IpAddr,
time::Duration
Expand Down Expand Up @@ -354,8 +353,8 @@ impl Metadata {

impl IntoIterator for Metadata {
type Item = Field;
type IntoIter = std::iter::Map<
std::collections::btree_map::IntoIter<&'static str, Field>,
type IntoIter = core::iter::Map<
alloc::collections::btree_map::IntoIter<&'static str, Field>,
fn((&'static str, Field)) -> Field
>;

Expand All @@ -371,7 +370,8 @@ impl IntoIterator for Metadata {

/// Factories for [`Field`] values.
pub mod field {
use std::{borrow::Cow, net::IpAddr, time::Duration};
use alloc::borrow::Cow;
use core::{net::IpAddr, time::Duration};

#[cfg(feature = "serde_json")]
use serde_json::Value as JsonValue;
Expand Down Expand Up @@ -425,7 +425,7 @@ pub mod field {
/// Build a duration metadata field.
///
/// ```
/// use std::time::Duration;
/// use core::time::Duration;
/// use masterror::{field, FieldValue};
///
/// let (_, value, _) = field::duration("elapsed", Duration::from_millis(1500)).into_parts();
Expand All @@ -439,7 +439,7 @@ pub mod field {
/// Build an IP address metadata field.
///
/// ```
/// use std::net::{IpAddr, Ipv4Addr};
/// use core::net::{IpAddr, Ipv4Addr};
/// use masterror::{field, FieldValue};
///
/// let (_, value, _) = field::ip("peer", IpAddr::from(Ipv4Addr::LOCALHOST)).into_parts();
Expand Down
Loading
Loading