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

## [Unreleased]

## [0.24.9] - 2025-10-25

### Fixed
- Treat compile-time and runtime custom `AppCode` values as equal by comparing
their canonical string representation, restoring successful JSON roundtrips
for `AppCode::new("…")` literals.

### Changed
- Equality for `AppCode` is now string-based; prefer `==` checks instead of
pattern matching on `AppCode::Variant` constants.

## [0.24.8] - 2025-10-24

### Changed
Expand Down
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.24.8"
version = "0.24.9"
rust-version = "1.90"
edition = "2024"
license = "MIT OR Apache-2.0"
Expand Down
39 changes: 24 additions & 15 deletions src/code/app_code.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
use alloc::{borrow::ToOwned, boxed::Box, string::String};
use alloc::{
borrow::{Cow, ToOwned},
string::String
};
use core::{
error::Error as CoreError,
fmt::{self, Display},
hash::{Hash, Hasher},
str::FromStr
};

Expand Down Expand Up @@ -44,15 +48,9 @@ impl CoreError for ParseAppCodeError {}
/// - Validate custom codes using [`AppCode::try_new`] before exposing them
/// publicly.
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[derive(Debug, Clone)]
pub struct AppCode {
repr: CodeRepr
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum CodeRepr {
Static(&'static str),
Owned(Box<str>)
repr: Cow<'static, str>
}

#[allow(non_upper_case_globals)]
Expand Down Expand Up @@ -108,13 +106,13 @@ impl AppCode {

const fn from_static(code: &'static str) -> Self {
Self {
repr: CodeRepr::Static(code)
repr: Cow::Borrowed(code)
}
}

fn from_owned(code: String) -> Self {
Self {
repr: CodeRepr::Owned(code.into_boxed_str())
repr: Cow::Owned(code)
}
}

Expand Down Expand Up @@ -169,10 +167,21 @@ impl AppCode {
/// This matches the JSON serialization.
#[must_use]
pub fn as_str(&self) -> &str {
match &self.repr {
CodeRepr::Static(value) => value,
CodeRepr::Owned(value) => value
}
self.repr.as_ref()
}
}

impl PartialEq for AppCode {
fn eq(&self, other: &Self) -> bool {
self.as_str() == other.as_str()
}
}

impl Eq for AppCode {}

impl Hash for AppCode {
fn hash<H: Hasher>(&self, state: &mut H) {
self.as_str().hash(state);
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@
//! let app_err = AppError::new(AppErrorKind::NotFound, "user_not_found");
//! let resp: ErrorResponse = (&app_err).into();
//! assert_eq!(resp.status, 404);
//! assert!(matches!(resp.code, AppCode::NotFound));
//! assert_eq!(resp.code, AppCode::NotFound);
//! ```
//!
//! # Typed control-flow macros
Expand Down Expand Up @@ -393,7 +393,7 @@ pub use kind::AppErrorKind;
/// name: "other"
/// }
/// .into();
/// assert!(matches!(code, AppCode::BadRequest));
/// assert_eq!(code, AppCode::BadRequest);
/// ```
pub use masterror_derive::{Error, Masterror};
pub use response::{
Expand Down
16 changes: 8 additions & 8 deletions src/response/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::{AppCode, AppError, AppErrorKind, ProblemJson};
fn new_sets_status_code_and_message() {
let e = ErrorResponse::new(404, AppCode::NotFound, "missing").expect("status");
assert_eq!(e.status, 404);
assert!(matches!(e.code, AppCode::NotFound));
assert_eq!(e.code, AppCode::NotFound);
assert_eq!(e.message, "missing");
assert!(e.retry.is_none());
assert!(e.www_authenticate.is_none());
Expand Down Expand Up @@ -246,7 +246,7 @@ fn from_app_error_preserves_status_and_sets_code() {
let app = AppError::new(AppErrorKind::NotFound, "user");
let e: ErrorResponse = (&app).into();
assert_eq!(e.status, 404);
assert!(matches!(e.code, AppCode::NotFound));
assert_eq!(e.code, AppCode::NotFound);
assert_eq!(e.message, "user");
assert!(e.retry.is_none());
}
Expand All @@ -256,7 +256,7 @@ fn from_app_error_uses_default_message_when_none() {
let app = AppError::bare(AppErrorKind::Internal);
let e: ErrorResponse = (&app).into();
assert_eq!(e.status, 500);
assert!(matches!(e.code, AppCode::Internal));
assert_eq!(e.code, AppCode::Internal);
assert_eq!(e.message, AppErrorKind::Internal.label());
}

Expand All @@ -269,7 +269,7 @@ fn from_owned_app_error_moves_message_and_metadata() {
let resp: ErrorResponse = err.into();

assert_eq!(resp.status, 401);
assert!(matches!(resp.code, AppCode::Unauthorized));
assert_eq!(resp.code, AppCode::Unauthorized);
assert_eq!(resp.message, "owned message");
assert_eq!(resp.retry.unwrap().after_seconds, 5);
assert_eq!(resp.www_authenticate.as_deref(), Some("Bearer"));
Expand All @@ -280,7 +280,7 @@ fn from_owned_app_error_defaults_message_when_absent() {
let resp: ErrorResponse = AppError::bare(AppErrorKind::Internal).into();

assert_eq!(resp.status, 500);
assert!(matches!(resp.code, AppCode::Internal));
assert_eq!(resp.code, AppCode::Internal);
assert_eq!(resp.message, AppErrorKind::Internal.label());
}

Expand All @@ -290,7 +290,7 @@ fn from_app_error_bare_uses_kind_display_as_message() {
let resp: ErrorResponse = app.into();

assert_eq!(resp.status, 504);
assert!(matches!(resp.code, AppCode::Timeout));
assert_eq!(resp.code, AppCode::Timeout);
assert_eq!(resp.message, AppErrorKind::Timeout.label());
}

Expand Down Expand Up @@ -367,7 +367,7 @@ fn display_is_concise_and_does_not_leak_details() {
fn new_legacy_defaults_to_internal_code() {
let e = ErrorResponse::new_legacy(404, "boom");
assert_eq!(e.status, 404);
assert!(matches!(e.code, AppCode::Internal));
assert_eq!(e.code, AppCode::Internal);
assert_eq!(e.message, "boom");
}

Expand All @@ -376,7 +376,7 @@ fn new_legacy_defaults_to_internal_code() {
fn new_legacy_invalid_status_falls_back_to_internal_error() {
let e = ErrorResponse::new_legacy(0, "boom");
assert_eq!(e.status, 500);
assert!(matches!(e.code, AppCode::Internal));
assert_eq!(e.code, AppCode::Internal);
assert_eq!(e.message, "boom");
}

Expand Down
2 changes: 1 addition & 1 deletion tests/ui/app_error/pass/enum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,5 @@ fn main() {
assert!(app_backend.message.is_none());

let code: AppCode = ApiError::Backend.into();
assert!(matches!(code, AppCode::Service));
assert_eq!(code, AppCode::Service);
}
2 changes: 1 addition & 1 deletion tests/ui/app_error/pass/struct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ fn main() {
assert_eq!(app.message.as_deref(), Some("missing flag: feature"));

let code: AppCode = MissingFlag { name: "other" }.into();
assert!(matches!(code, AppCode::BadRequest));
assert_eq!(code, AppCode::BadRequest);
}
Loading