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

## [Unreleased]

## [0.24.0] - 2025-10-16

### Added
- Introduced `AppCode::new` and `AppCode::try_new` constructors with strict
SCREAMING_SNAKE_CASE validation, plus regression tests covering custom codes
flowing through `AppError` and `ErrorResponse` JSON serialization.
- Documented runtime-defined codes across the wiki pages to highlight
`AppCode::try_new` usage.

### Changed
- Replaced the closed `AppCode` enum with a string-backed newtype supporting
caller-defined codes while preserving built-in constants.
- Updated mapping helpers and generated tables to work with the new representation
by returning references instead of copying codes.
- Adjusted serde parsing to validate custom codes and report
`ParseAppCodeError` on invalid payloads.

## [0.23.3] - 2025-10-15

### Changed
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.23.3"
version = "0.24.0"
rust-version = "1.90"
edition = "2024"
license = "MIT OR Apache-2.0"
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,9 @@ The build script keeps the full feature snippet below in sync with

~~~toml
[dependencies]
masterror = { version = "0.23.3", default-features = false }
masterror = { version = "0.24.0", default-features = false }
# or with features:
# masterror = { version = "0.23.3", features = [
# masterror = { version = "0.24.0", features = [
# "std", "axum", "actix", "openapi",
# "serde_json", "tracing", "metrics", "backtrace",
# "sqlx", "sqlx-migrate", "reqwest", "redis",
Expand Down
4 changes: 4 additions & 0 deletions docs/wiki/error-crate-comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ fn load_configuration(path: &std::path::Path) -> masterror::AppResult<String> {
}
```

If the configuration source must encode per-environment values in the code, use
`AppCode::try_new` to build the identifier dynamically and bubble up
`ParseAppCodeError` when validation fails.

`AppError` stores the `anyhow::Error` internally without exposing it to clients.
`with_context` reuses any shared `Arc` handles provided by upstream crates, so
you preserve pointer identity without extra allocations. You still emit clean
Expand Down
4 changes: 4 additions & 0 deletions docs/wiki/masterror-application-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ Enable the `serde_json`
feature to use `.with_details(..)`; without it, fall back to
`AppError::with_details_text` for plain-text payloads.

Need to generate codes dynamically (e.g., include partner identifiers)? Call
[`AppCode::try_new`](https://docs.rs/masterror/latest/masterror/struct.AppCode.html#method.try_new)
with a runtime string and propagate [`ParseAppCodeError`] when validation fails.

## Deriving domain errors

Combine `masterror::Error` derive macros with `#[app_error]` to convert domain
Expand Down
4 changes: 4 additions & 0 deletions docs/wiki/patterns-and-troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ pub async fn fetch_user(client: &reqwest::Client) -> masterror::AppResult<String
}
```

For runtime-defined identifiers (e.g., partner- or tenant-specific codes), use
[`AppCode::try_new`](https://docs.rs/masterror/latest/masterror/struct.AppCode.html#method.try_new)
and handle [`ParseAppCodeError`] if validation fails.

Enable the `reqwest` feature to classify timeouts and HTTP status codes
automatically. Similar conversions exist for `sqlx`, `redis`, `validator`,
`config`, and more.
Expand Down
2 changes: 1 addition & 1 deletion src/app_error/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ impl Context {
}

let mut error = AppError::new_raw(self.category, None);
error.code = self.code;
error.code = self.code.clone();
if !self.fields.is_empty() {
self.apply_field_redactions();
error.metadata.extend(self.fields);
Expand Down
22 changes: 21 additions & 1 deletion src/app_error/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ static BACKTRACE_ENV_GUARD: Mutex<()> = Mutex::new(());
static TELEMETRY_GUARD: Mutex<()> = Mutex::new(());

use super::{AppError, FieldRedaction, FieldValue, MessageEditPolicy, field};
use crate::{AppCode, AppErrorKind};
use crate::{AppCode, AppErrorKind, ErrorResponse};

// --- Helpers -------------------------------------------------------------

Expand Down Expand Up @@ -223,6 +223,26 @@ fn metadata_and_code_are_preserved() {
assert_eq!(metadata.get("attempt"), Some(&FieldValue::I64(2)));
}

#[test]
fn custom_literal_codes_flow_into_responses() {
let custom = AppCode::new("INVALID_JSON");
let err = AppError::bad_request("invalid").with_code(custom.clone());
assert_eq!(err.code, custom);

let response: ErrorResponse = err.into();
assert_eq!(response.code, custom);
}

#[test]
fn dynamic_codes_flow_into_responses() {
let custom = AppCode::try_new(String::from("THIRD_PARTY_FAILURE")).expect("valid code");
let err = AppError::service("down").with_code(custom.clone());
assert_eq!(err.code, custom);

let response: ErrorResponse = err.into();
assert_eq!(response.code, custom);
}

#[cfg(feature = "serde_json")]
#[test]
fn with_details_json_attaches_payload() {
Expand Down
49 changes: 31 additions & 18 deletions src/code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
//! which remains stable even if your transport mapping changes.
//!
//! ## Stability and SemVer
//! - New variants **may be added in minor releases** (non-breaking).
//! - The enum is marked `#[non_exhaustive]` so downstream users must include a
//! wildcard arm (`_`) when matching, which keeps them forward-compatible.
//! - New built-in constants **may be added in minor releases** (non-breaking).
//! - The type is marked `#[non_exhaustive]` to allow future metadata additions
//! without breaking downstream code.
//! - Custom codes can be defined at compile time with [`AppCode::new`] or at
//! runtime with [`AppCode::try_new`].
//!
//! ## Typical usage
//! Construct an `ErrorResponse` with a code and return it to clients:
Expand Down Expand Up @@ -45,27 +47,38 @@
//! # }
//! ```
//!
//! Match codes safely (note the wildcard arm due to `#[non_exhaustive]`):
//! Match codes safely:
//!
//! ```rust
//! use masterror::AppCode;
//!
//! fn is_client_error(code: AppCode) -> bool {
//! match code {
//! AppCode::NotFound
//! | AppCode::Validation
//! | AppCode::Conflict
//! | AppCode::Unauthorized
//! | AppCode::Forbidden
//! | AppCode::NotImplemented
//! | AppCode::BadRequest
//! | AppCode::RateLimited
//! | AppCode::TelegramAuth
//! | AppCode::InvalidJwt => true,
//! _ => false // future-proof: treat unknown as not client error
//! }
//! fn is_client_error(code: &AppCode) -> bool {
//! matches!(
//! code.as_str(),
//! "NOT_FOUND"
//! | "VALIDATION"
//! | "CONFLICT"
//! | "UNAUTHORIZED"
//! | "FORBIDDEN"
//! | "NOT_IMPLEMENTED"
//! | "BAD_REQUEST"
//! | "RATE_LIMITED"
//! | "TELEGRAM_AUTH"
//! | "INVALID_JWT"
//! )
//! }
//! ```
//!
//! Define custom codes:
//!
//! ```rust
//! use masterror::AppCode;
//!
//! const INVALID_JSON: AppCode = AppCode::new("INVALID_JSON");
//! let third_party = AppCode::try_new(String::from("THIRD_PARTY_FAILURE")).expect("valid code");
//! assert_eq!(INVALID_JSON.as_str(), "INVALID_JSON");
//! assert_eq!(third_party.as_str(), "THIRD_PARTY_FAILURE");
//! ```

mod app_code;

Expand Down
Loading
Loading