diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b9d410..0daaa22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [0.10.7] - 2025-09-22 + +### Changed +- Added an owning `From` conversion for `ErrorResponse` and updated the + Axum adapter to use it, eliminating redundant clones when building HTTP error + bodies. + ## [0.10.6] - 2025-09-21 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 20c236e..3ad3692 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1606,7 +1606,7 @@ dependencies = [ [[package]] name = "masterror" -version = "0.10.6" +version = "0.10.7" dependencies = [ "actix-web", "axum", diff --git a/Cargo.toml b/Cargo.toml index 509485f..21c8f5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "masterror" -version = "0.10.6" +version = "0.10.7" rust-version = "1.90" edition = "2024" license = "MIT OR Apache-2.0" diff --git a/README.md b/README.md index eb6568f..b376d9a 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,9 @@ Stable categories, conservative HTTP mapping, no `unsafe`. ~~~toml [dependencies] -masterror = { version = "0.10.6", default-features = false } +masterror = { version = "0.10.7", default-features = false } # or with features: -# masterror = { version = "0.10.6", features = [ +# masterror = { version = "0.10.7", features = [ # "axum", "actix", "openapi", "serde_json", # "sqlx", "sqlx-migrate", "reqwest", "redis", # "validator", "config", "tokio", "multipart", @@ -66,10 +66,10 @@ masterror = { version = "0.10.6", default-features = false } ~~~toml [dependencies] # lean core -masterror = { version = "0.10.6", default-features = false } +masterror = { version = "0.10.7", default-features = false } # with Axum/Actix + JSON + integrations -# masterror = { version = "0.10.6", features = [ +# masterror = { version = "0.10.7", features = [ # "axum", "actix", "openapi", "serde_json", # "sqlx", "sqlx-migrate", "reqwest", "redis", # "validator", "config", "tokio", "multipart", @@ -623,13 +623,13 @@ assert_eq!(resp.status, 401); Minimal core: ~~~toml -masterror = { version = "0.10.6", default-features = false } +masterror = { version = "0.10.7", default-features = false } ~~~ API (Axum + JSON + deps): ~~~toml -masterror = { version = "0.10.6", features = [ +masterror = { version = "0.10.7", features = [ "axum", "serde_json", "openapi", "sqlx", "reqwest", "redis", "validator", "config", "tokio" ] } @@ -638,7 +638,7 @@ masterror = { version = "0.10.6", features = [ API (Actix + JSON + deps): ~~~toml -masterror = { version = "0.10.6", features = [ +masterror = { version = "0.10.7", features = [ "actix", "serde_json", "openapi", "sqlx", "reqwest", "redis", "validator", "config", "tokio" ] } diff --git a/src/convert/axum.rs b/src/convert/axum.rs index e80815e..5c65a07 100644 --- a/src/convert/axum.rs +++ b/src/convert/axum.rs @@ -77,7 +77,7 @@ impl IntoResponse for AppError { #[cfg(feature = "serde_json")] { // Build the stable wire contract (includes `code`). - let body: ErrorResponse = (&self).into(); + let body: ErrorResponse = self.into(); return body.into_response(); } diff --git a/src/response/mapping.rs b/src/response/mapping.rs index cb9918d..866b8c5 100644 --- a/src/response/mapping.rs +++ b/src/response/mapping.rs @@ -13,6 +13,33 @@ impl Display for ErrorResponse { } } +impl From for ErrorResponse { + fn from(err: AppError) -> Self { + let AppError { + kind, + message, + retry, + www_authenticate + } = err; + + let status = kind.http_status(); + let code = AppCode::from(kind); + let message = match message { + Some(msg) => msg.into_owned(), + None => String::from("An error occurred") + }; + + Self { + status, + code, + message, + details: None, + retry, + www_authenticate + } + } +} + impl From<&AppError> for ErrorResponse { fn from(err: &AppError) -> Self { let status = err.kind.http_status(); diff --git a/src/response/tests.rs b/src/response/tests.rs index fad84be..38e7be8 100644 --- a/src/response/tests.rs +++ b/src/response/tests.rs @@ -147,6 +147,30 @@ fn from_app_error_uses_default_message_when_none() { assert_eq!(e.message, "An error occurred"); } +#[test] +fn from_owned_app_error_moves_message_and_metadata() { + let err = AppError::unauthorized(String::from("owned message")) + .with_retry_after_secs(5) + .with_www_authenticate("Bearer"); + + let resp: ErrorResponse = err.into(); + + assert_eq!(resp.status, 401); + assert!(matches!(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")); +} + +#[test] +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.message, "An error occurred"); +} + // --- Display formatting -------------------------------------------------- #[test]