diff --git a/CHANGELOG.md b/CHANGELOG.md index 19f8d77..c382531 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [0.24.4] - 2025-10-20 + +### Fixed +- Implemented a manual OpenAPI schema for `AppCode`, restoring `utoipa` + compatibility and documenting the SCREAMING_SNAKE_CASE contract in generated + specs. +- Emitted owned label values when incrementing `error_total` telemetry metrics + so the updated `metrics` crate no longer requires `'static` lifetimes. +- Relaxed gRPC metadata serialization to avoid `'static` lifetime requirements + introduced by recent compiler changes, preserving zero-copy formatting where + possible. + ## [0.24.3] - 2025-10-19 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 9e4de6f..96b7deb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1826,7 +1826,7 @@ dependencies = [ [[package]] name = "masterror" -version = "0.24.3" +version = "0.24.4" dependencies = [ "actix-web", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 7965c7c..534ef7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "masterror" -version = "0.24.3" +version = "0.24.4" rust-version = "1.90" edition = "2024" license = "MIT OR Apache-2.0" diff --git a/README.md b/README.md index c9279af..b73e739 100644 --- a/README.md +++ b/README.md @@ -74,9 +74,9 @@ The build script keeps the full feature snippet below in sync with ~~~toml [dependencies] -masterror = { version = "0.24.3", default-features = false } +masterror = { version = "0.24.4", default-features = false } # or with features: -# masterror = { version = "0.24.3", features = [ +# masterror = { version = "0.24.4", features = [ # "std", "axum", "actix", "openapi", # "serde_json", "tracing", "metrics", "backtrace", # "sqlx", "sqlx-migrate", "reqwest", "redis", diff --git a/src/app_error/core.rs b/src/app_error/core.rs index cc743c0..b329d72 100644 --- a/src/app_error/core.rs +++ b/src/app_error/core.rs @@ -311,10 +311,12 @@ impl Error { #[cfg(feature = "metrics")] { + let code_label = self.code.as_str().to_owned(); + let category_label = kind_label(self.kind).to_owned(); metrics::counter!( "error_total", - "code" => self.code.as_str(), - "category" => kind_label(self.kind) + "code" => code_label, + "category" => category_label ) .increment(1); } diff --git a/src/code/app_code.rs b/src/code/app_code.rs index b6236c5..280d545 100644 --- a/src/code/app_code.rs +++ b/src/code/app_code.rs @@ -7,7 +7,10 @@ use core::{ use serde::{Deserialize, Deserializer, Serialize, Serializer}; #[cfg(feature = "openapi")] -use utoipa::ToSchema; +use utoipa::{ + PartialSchema, ToSchema, + openapi::schema::{ObjectBuilder, Type} +}; use crate::kind::AppErrorKind; @@ -40,7 +43,6 @@ impl CoreError for ParseAppCodeError {} /// - Do not encode private/internal details in codes. /// - Validate custom codes using [`AppCode::try_new`] before exposing them /// publicly. -#[cfg_attr(feature = "openapi", derive(ToSchema))] #[non_exhaustive] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct AppCode { @@ -295,6 +297,23 @@ impl<'de> Deserialize<'de> for AppCode { } } +#[cfg(feature = "openapi")] +impl PartialSchema for AppCode { + fn schema() -> utoipa::openapi::RefOr { + ObjectBuilder::new() + .schema_type(Type::String) + .description(Some( + "Stable machine-readable error code in SCREAMING_SNAKE_CASE.".to_owned() + )) + .pattern(Some("^[A-Z0-9_]+$".to_owned())) + .build() + .into() + } +} + +#[cfg(feature = "openapi")] +impl ToSchema for AppCode {} + fn validate_code(value: &str) -> Result<(), ParseAppCodeError> { if !is_valid_literal(value) { return Err(ParseAppCodeError); diff --git a/src/convert/tonic.rs b/src/convert/tonic.rs index 7193e1c..c9cb656 100644 --- a/src/convert/tonic.rs +++ b/src/convert/tonic.rs @@ -147,7 +147,8 @@ enum MetadataAscii<'a> { impl AsRef for MetadataAscii<'_> { fn as_ref(&self) -> &str { match self { - Self::Static(text) | Self::Buffer(text) => text, + Self::Static(text) => text, + Self::Buffer(text) => text, Self::Owned(text) => text.as_str() } } @@ -175,7 +176,14 @@ fn metadata_value_to_ascii<'a>( match value { FieldValue::Str(value) => { let text = value.as_ref(); - is_ascii_metadata_value(text).then_some(MetadataAscii::Static(text)) + if !is_ascii_metadata_value(text) { + return None; + } + + match value { + Cow::Borrowed(borrowed) => Some(MetadataAscii::Static(borrowed)), + Cow::Owned(owned) => Some(MetadataAscii::Owned(owned.clone())) + } } FieldValue::I64(value) => Some(MetadataAscii::Buffer(formatter.integers.format(*value))), FieldValue::U64(value) => Some(MetadataAscii::Buffer(formatter.integers.format(*value))),