diff --git a/CHANGELOG.md b/CHANGELOG.md index cf24aca..bcfb589 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [0.20.5] - 2025-10-05 + +### Fixed +- Promoted the gRPC converter to an infallible `From` implementation + while retaining the `TryFrom` API via the new documented + `StatusConversionError`, satisfying Clippy's infallible conversion lint. +- Collapsed nested metadata guards in the Tonic adapter and reused borrowed + booleans to silence Clippy without regressing runtime behaviour. +- Simplified the `AppResult` alias test to avoid large `Err` variant warnings + from Clippy's `result_large_err` lint. + ## [0.20.4] - 2025-10-04 ### Added diff --git a/Cargo.lock b/Cargo.lock index 92e21e8..d6d73a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1727,7 +1727,7 @@ dependencies = [ [[package]] name = "masterror" -version = "0.20.4" +version = "0.20.5" dependencies = [ "actix-web", "axum 0.8.4", diff --git a/Cargo.toml b/Cargo.toml index 641a46b..9fd4521 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "masterror" -version = "0.20.4" +version = "0.20.5" rust-version = "1.90" edition = "2024" license = "MIT OR Apache-2.0" diff --git a/README.md b/README.md index 9cbec41..684ce25 100644 --- a/README.md +++ b/README.md @@ -38,9 +38,9 @@ guides, comparisons with `thiserror`/`anyhow`, and troubleshooting recipes. ~~~toml [dependencies] -masterror = { version = "0.20.4", default-features = false } +masterror = { version = "0.20.5", default-features = false } # or with features: -# masterror = { version = "0.20.4", features = [ +# masterror = { version = "0.20.5", features = [ # "axum", "actix", "openapi", "serde_json", # "tracing", "metrics", "backtrace", "sqlx", # "sqlx-migrate", "reqwest", "redis", "validator", @@ -78,10 +78,10 @@ masterror = { version = "0.20.4", default-features = false } ~~~toml [dependencies] # lean core -masterror = { version = "0.20.4", default-features = false } +masterror = { version = "0.20.5", default-features = false } # with Axum/Actix + JSON + integrations -# masterror = { version = "0.20.4", features = [ +# masterror = { version = "0.20.5", features = [ # "axum", "actix", "openapi", "serde_json", # "tracing", "metrics", "backtrace", "sqlx", # "sqlx-migrate", "reqwest", "redis", "validator", @@ -720,13 +720,13 @@ assert_eq!(problem.grpc.expect("grpc").name, "UNAUTHENTICATED"); Minimal core: ~~~toml -masterror = { version = "0.20.4", default-features = false } +masterror = { version = "0.20.5", default-features = false } ~~~ API (Axum + JSON + deps): ~~~toml -masterror = { version = "0.20.4", features = [ +masterror = { version = "0.20.5", features = [ "axum", "serde_json", "openapi", "sqlx", "reqwest", "redis", "validator", "config", "tokio" ] } @@ -735,7 +735,7 @@ masterror = { version = "0.20.4", features = [ API (Actix + JSON + deps): ~~~toml -masterror = { version = "0.20.4", features = [ +masterror = { version = "0.20.5", features = [ "actix", "serde_json", "openapi", "sqlx", "reqwest", "redis", "validator", "config", "tokio" ] } diff --git a/src/app_error/tests.rs b/src/app_error/tests.rs index 69be3b7..04fdf9d 100644 --- a/src/app_error/tests.rs +++ b/src/app_error/tests.rs @@ -537,14 +537,9 @@ fn metrics_counter_is_incremented_once() { #[test] fn result_alias_is_generic() { - fn app() -> super::AppResult { - Ok(1) - } - - fn other() -> super::AppResult { - Ok(2) - } + let default_result: super::AppResult = Ok(1); + let custom_result: super::AppResult = Ok(2); - assert_eq!(app().unwrap(), 1); - assert_eq!(other().unwrap(), 2); + assert_eq!(default_result.unwrap(), 1); + assert_eq!(custom_result.unwrap(), 2); } diff --git a/src/convert/tonic.rs b/src/convert/tonic.rs index 9328132..07e4b87 100644 --- a/src/convert/tonic.rs +++ b/src/convert/tonic.rs @@ -14,11 +14,11 @@ //! ```rust,ignore //! use masterror::{AppError, AppErrorKind}; //! -//! let status = tonic::Status::try_from(AppError::not_found("missing"))?; +//! let status = tonic::Status::from(AppError::not_found("missing")); //! assert_eq!(status.code(), tonic::Code::NotFound); //! ``` -use std::{borrow::Cow, convert::Infallible}; +use std::{borrow::Cow, fmt}; use tonic::{ Code, Status, @@ -32,11 +32,49 @@ use crate::{ mapping_for_code }; +/// Error returned when converting [`Error`] into [`Status`] fails. +/// +/// This type is never constructed in practice because the conversion is +/// guaranteed to succeed. It exists solely to preserve the `TryFrom` API in +/// addition to the infallible [`From`] conversion. +/// +/// # Examples +/// ```rust,ignore +/// use masterror::{AppError, StatusConversionError}; +/// use tonic::{Code, Status}; +/// +/// fn convert() -> Result { +/// Status::try_from(AppError::not_found("missing")) +/// } +/// +/// # fn main() -> Result<(), StatusConversionError> { +/// let status = convert()?; +/// assert_eq!(status.code(), Code::NotFound); +/// # Ok(()) +/// # } +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct StatusConversionError; + +impl fmt::Display for StatusConversionError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("conversion to tonic::Status cannot fail") + } +} + +impl std::error::Error for StatusConversionError {} + +impl From for Status { + fn from(error: Error) -> Self { + status_from_error(&error) + } +} + impl TryFrom for Status { - type Error = Infallible; + type Error = StatusConversionError; fn try_from(error: Error) -> Result { - Ok(status_from_error(&error)) + Ok(Status::from(error)) } } @@ -59,10 +97,10 @@ fn status_from_error(error: &Error) -> Status { if let Some(advice) = error.retry { insert_retry(&mut meta, advice); } - if let Some(challenge) = error.www_authenticate.as_deref() { - if is_ascii_metadata_value(challenge) { - insert_ascii(&mut meta, "www-authenticate", challenge); - } + if let Some(challenge) = error.www_authenticate.as_deref() + && is_ascii_metadata_value(challenge) + { + insert_ascii(&mut meta, "www-authenticate", challenge); } if !matches!(error.edit_policy, MessageEditPolicy::Redact) { @@ -123,9 +161,7 @@ fn metadata_value_to_ascii(value: &FieldValue) -> Option> { } FieldValue::I64(value) => Some(Cow::Owned(value.to_string())), FieldValue::U64(value) => Some(Cow::Owned(value.to_string())), - FieldValue::Bool(value) => Some(Cow::Owned( - if *value { "true" } else { "false" }.to_string() - )), + FieldValue::Bool(value) => Some(Cow::Borrowed(if *value { "true" } else { "false" })), FieldValue::Uuid(value) => Some(Cow::Owned(value.to_string())) } } diff --git a/src/lib.rs b/src/lib.rs index 1bcac14..956876b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -363,3 +363,7 @@ pub use response::{ } }; pub use result_ext::ResultExt; + +#[cfg(feature = "tonic")] +#[cfg_attr(docsrs, doc(cfg(feature = "tonic")))] +pub use crate::convert::tonic::StatusConversionError;