From 48db55da6b13cb029425e6451562114fdf34a7f1 Mon Sep 17 00:00:00 2001 From: RA <70325462+RAprogramm@users.noreply.github.com> Date: Fri, 26 Sep 2025 20:18:05 +0700 Subject: [PATCH] Refactor AppCode newtype and update docs --- CHANGELOG.md | 17 + Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 4 +- docs/wiki/error-crate-comparison.md | 4 + docs/wiki/masterror-application-guide.md | 4 + docs/wiki/patterns-and-troubleshooting.md | 4 + src/app_error/context.rs | 2 +- src/app_error/tests.rs | 22 +- src/code.rs | 49 +- src/code/app_code.rs | 460 ++++++++++-------- src/convert/sqlx.rs | 2 +- src/convert/tonic.rs | 2 +- src/mapping.rs | 18 +- src/response/core.rs | 2 +- src/response/mapping.rs | 4 +- src/response/problem_json.rs | 43 +- src/response/tests.rs | 13 + .../fail/enum_missing_variant.stderr | 7 +- tests/ui/app_error/fail/missing_code.stderr | 4 +- tests/ui/app_error/fail/missing_kind.stderr | 2 +- tests/ui/formatter/fail/duplicate_fmt.stderr | 2 +- .../fail/implicit_after_named.stderr | 1 + .../ui/formatter/fail/unsupported_flag.stderr | 4 +- .../fail/unsupported_formatter.stderr | 4 +- .../ui/formatter/fail/uppercase_binary.stderr | 4 +- .../formatter/fail/uppercase_pointer.stderr | 4 +- tests/ui/masterror/fail/duplicate_attr.stderr | 2 +- .../masterror/fail/duplicate_telemetry.stderr | 2 +- tests/ui/masterror/fail/empty_redact.stderr | 2 +- .../fail/enum_missing_variant.stderr | 7 +- .../ui/masterror/fail/missing_category.stderr | 4 +- tests/ui/masterror/fail/missing_code.stderr | 4 +- tests/ui/masterror/fail/unknown_option.stderr | 2 +- 34 files changed, 433 insertions(+), 276 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce93848..22b5dad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Cargo.lock b/Cargo.lock index 9164e4d..5102aca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1665,7 +1665,7 @@ dependencies = [ [[package]] name = "masterror" -version = "0.23.3" +version = "0.24.0" dependencies = [ "actix-web", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 4143887..e28329d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/README.md b/README.md index 3b93967..855543d 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.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", diff --git a/docs/wiki/error-crate-comparison.md b/docs/wiki/error-crate-comparison.md index b5bf6ba..10394eb 100644 --- a/docs/wiki/error-crate-comparison.md +++ b/docs/wiki/error-crate-comparison.md @@ -122,6 +122,10 @@ fn load_configuration(path: &std::path::Path) -> masterror::AppResult { } ``` +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 diff --git a/docs/wiki/masterror-application-guide.md b/docs/wiki/masterror-application-guide.md index 7d854b5..95aec11 100644 --- a/docs/wiki/masterror-application-guide.md +++ b/docs/wiki/masterror-application-guide.md @@ -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 diff --git a/docs/wiki/patterns-and-troubleshooting.md b/docs/wiki/patterns-and-troubleshooting.md index c42978d..2f76ca2 100644 --- a/docs/wiki/patterns-and-troubleshooting.md +++ b/docs/wiki/patterns-and-troubleshooting.md @@ -24,6 +24,10 @@ pub async fn fetch_user(client: &reqwest::Client) -> masterror::AppResult = 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 ------------------------------------------------------------- @@ -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() { diff --git a/src/code.rs b/src/code.rs index b9dc44a..1d3b3c5 100644 --- a/src/code.rs +++ b/src/code.rs @@ -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: @@ -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; diff --git a/src/code/app_code.rs b/src/code/app_code.rs index 37ffc12..b6236c5 100644 --- a/src/code/app_code.rs +++ b/src/code/app_code.rs @@ -1,10 +1,11 @@ +use alloc::{boxed::Box, string::String}; use core::{ error::Error as CoreError, fmt::{self, Display}, str::FromStr }; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; #[cfg(feature = "openapi")] use utoipa::ToSchema; @@ -12,8 +13,9 @@ use crate::kind::AppErrorKind; /// Error returned when parsing [`AppCode`] from a string fails. /// -/// The parser only accepts the canonical SCREAMING_SNAKE_CASE representations -/// emitted by [`AppCode::as_str`]. Any other value results in this error. +/// The parser only accepts SCREAMING_SNAKE_CASE values accepted by +/// [`AppCode::new`] and [`AppCode::try_new`]. Any other value results in this +/// error. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ParseAppCodeError; @@ -28,174 +30,146 @@ impl CoreError for ParseAppCodeError {} /// Stable machine-readable error code exposed to clients. /// /// Values are serialized as **SCREAMING_SNAKE_CASE** strings (e.g., -/// `"NOT_FOUND"`). This type is part of the public wire contract. +/// `"NOT_FOUND"`). This type is part of the public wire contract and supports +/// both built-in constants and caller-defined codes created via +/// [`AppCode::new`] or [`AppCode::try_new`]. /// /// Design rules: /// - Keep the set small and meaningful. /// - Prefer adding new variants over overloading existing ones. /// - 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, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum AppCode { - // ───────────── 4xx family (client-visible categories) ───────────── - /// Resource does not exist or is not visible to the caller. - /// - /// Typically mapped to HTTP **404 Not Found**. - NotFound, - - /// Input failed validation (shape, constraints, business rules). - /// - /// Typically mapped to HTTP **422 Unprocessable Entity**. - Validation, - - /// State conflict with an existing resource or concurrent update. - /// - /// Typically mapped to HTTP **409 Conflict**. - Conflict, - - /// Attempted to create a user that already exists (unique constraint). - /// - /// Typically mapped to HTTP **409 Conflict**. - UserAlreadyExists, - - /// Authentication required or failed (missing/invalid credentials). - /// - /// Typically mapped to HTTP **401 Unauthorized**. - Unauthorized, - - /// Authenticated but not allowed to perform the operation. - /// - /// Typically mapped to HTTP **403 Forbidden**. - Forbidden, - - /// Operation is not implemented or not supported by this deployment. - /// - /// Typically mapped to HTTP **501 Not Implemented**. - NotImplemented, - - /// Malformed request or missing required parameters. - /// - /// Typically mapped to HTTP **400 Bad Request**. - BadRequest, - - /// Client exceeded rate limits or quota. - /// - /// Typically mapped to HTTP **429 Too Many Requests**. - RateLimited, - - /// Telegram authentication flow failed (signature, timestamp, or payload). - /// - /// Typically mapped to HTTP **401 Unauthorized**. - TelegramAuth, - - /// Provided JWT is invalid (expired, malformed, wrong signature/claims). - /// - /// Typically mapped to HTTP **401 Unauthorized**. - InvalidJwt, - - // ───────────── 5xx family (server/infra categories) ───────────── - /// Unexpected server-side failure not captured by more specific kinds. - /// - /// Typically mapped to HTTP **500 Internal Server Error**. - Internal, +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct AppCode { + repr: CodeRepr +} - /// Database-related failure (query, connection, migration, etc.). - /// - /// Typically mapped to HTTP **500 Internal Server Error**. - Database, +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +enum CodeRepr { + Static(&'static str), + Owned(Box) +} - /// Generic service-layer failure (business logic or orchestration). - /// - /// Typically mapped to HTTP **500 Internal Server Error**. - Service, +#[allow(non_upper_case_globals)] +impl AppCode { + /// Machine code emitted when a resource is not found. + pub const NotFound: Self = Self::from_static("NOT_FOUND"); + /// Machine code emitted when validation fails. + pub const Validation: Self = Self::from_static("VALIDATION"); + /// Machine code emitted when a conflict is detected. + pub const Conflict: Self = Self::from_static("CONFLICT"); + /// Machine code emitted when attempting to create an existing user. + pub const UserAlreadyExists: Self = Self::from_static("USER_ALREADY_EXISTS"); + /// Machine code emitted when authentication fails or is required. + pub const Unauthorized: Self = Self::from_static("UNAUTHORIZED"); + /// Machine code emitted when an operation is not permitted. + pub const Forbidden: Self = Self::from_static("FORBIDDEN"); + /// Machine code emitted when functionality is missing. + pub const NotImplemented: Self = Self::from_static("NOT_IMPLEMENTED"); + /// Machine code emitted when a request is malformed. + pub const BadRequest: Self = Self::from_static("BAD_REQUEST"); + /// Machine code emitted when a caller is throttled. + pub const RateLimited: Self = Self::from_static("RATE_LIMITED"); + /// Machine code emitted when Telegram authentication fails. + pub const TelegramAuth: Self = Self::from_static("TELEGRAM_AUTH"); + /// Machine code emitted when a JWT token is invalid. + pub const InvalidJwt: Self = Self::from_static("INVALID_JWT"); + /// Machine code emitted for internal server failures. + pub const Internal: Self = Self::from_static("INTERNAL"); + /// Machine code emitted for database-related issues. + pub const Database: Self = Self::from_static("DATABASE"); + /// Machine code emitted for service-layer failures. + pub const Service: Self = Self::from_static("SERVICE"); + /// Machine code emitted for configuration issues. + pub const Config: Self = Self::from_static("CONFIG"); + /// Machine code emitted for Turnkey integration failures. + pub const Turnkey: Self = Self::from_static("TURNKEY"); + /// Machine code emitted for timeout failures. + pub const Timeout: Self = Self::from_static("TIMEOUT"); + /// Machine code emitted for network issues. + pub const Network: Self = Self::from_static("NETWORK"); + /// Machine code emitted when dependencies are unavailable. + pub const DependencyUnavailable: Self = Self::from_static("DEPENDENCY_UNAVAILABLE"); + /// Machine code emitted for serialization failures. + pub const Serialization: Self = Self::from_static("SERIALIZATION"); + /// Machine code emitted for deserialization failures. + pub const Deserialization: Self = Self::from_static("DESERIALIZATION"); + /// Machine code emitted when an external API fails. + pub const ExternalApi: Self = Self::from_static("EXTERNAL_API"); + /// Machine code emitted for queue processing errors. + pub const Queue: Self = Self::from_static("QUEUE"); + /// Machine code emitted for cache subsystem failures. + pub const Cache: Self = Self::from_static("CACHE"); + + const fn from_static(code: &'static str) -> Self { + Self { + repr: CodeRepr::Static(code) + } + } - /// Configuration error (missing/invalid environment or runtime config). - /// - /// Typically mapped to HTTP **500 Internal Server Error**. - Config, + fn from_owned(code: String) -> Self { + Self { + repr: CodeRepr::Owned(code.into_boxed_str()) + } + } - /// Failure in the Turnkey subsystem/integration. + /// Construct an [`AppCode`] from a compile-time string literal. /// - /// Typically mapped to HTTP **500 Internal Server Error**. - Turnkey, - - /// Operation did not complete within the allotted time. + /// # Examples + /// ``` + /// use masterror::AppCode; /// - /// Typically mapped to HTTP **504 Gateway Timeout**. - Timeout, - - /// Network-level error (DNS, connect, TLS, request build). + /// let code = AppCode::new("INVALID_JSON"); + /// assert_eq!(code.as_str(), "INVALID_JSON"); + /// ``` /// - /// Typically mapped to HTTP **503 Service Unavailable**. - Network, - - /// External dependency is unavailable or degraded (cache, broker, - /// third-party). + /// # Panics /// - /// Typically mapped to HTTP **503 Service Unavailable**. - DependencyUnavailable, + /// Panics when the literal is not SCREAMING_SNAKE_CASE. Use + /// [`AppCode::try_new`] to validate dynamic strings at runtime. + #[must_use] + pub const fn new(code: &'static str) -> Self { + if !is_valid_literal(code) { + panic!("AppCode literals must be SCREAMING_SNAKE_CASE"); + } + Self::from_static(code) + } - /// Failed to serialize data (encode). + /// Construct an [`AppCode`] from a dynamically provided string. /// - /// Typically mapped to HTTP **500 Internal Server Error**. - Serialization, - - /// Failed to deserialize data (decode). + /// The input must be SCREAMING_SNAKE_CASE. This constructor allocates to + /// own the string, making it suitable for runtime-defined codes. /// - /// Typically mapped to HTTP **500 Internal Server Error**. - Deserialization, - - /// Upstream API returned an error or protocol-level failure. + /// # Errors /// - /// Typically mapped to HTTP **500 Internal Server Error**. - ExternalApi, - - /// Queue processing failure (publish/consume/ack). + /// Returns [`ParseAppCodeError`] when the string is empty or contains + /// characters outside of `A-Z`, `0-9`, and `_`. /// - /// Typically mapped to HTTP **500 Internal Server Error**. - Queue, - - /// Cache subsystem failure (read/write/encoding). + /// # Examples + /// ``` + /// use masterror::AppCode; /// - /// Typically mapped to HTTP **500 Internal Server Error**. - Cache -} + /// let code = AppCode::try_new(String::from("THIRD_PARTY_FAILURE"))?; + /// assert_eq!(code.as_str(), "THIRD_PARTY_FAILURE"); + /// # Ok::<(), masterror::ParseAppCodeError>(()) + /// ``` + pub fn try_new(code: impl Into) -> Result { + let code = code.into(); + validate_code(&code)?; + Ok(Self::from_owned(code)) + } -impl AppCode { /// Get the canonical string form of this code (SCREAMING_SNAKE_CASE). /// - /// This is equivalent to how the code is serialized to JSON. - pub const fn as_str(&self) -> &'static str { - match self { - // 4xx - AppCode::NotFound => "NOT_FOUND", - AppCode::Validation => "VALIDATION", - AppCode::Conflict => "CONFLICT", - AppCode::UserAlreadyExists => "USER_ALREADY_EXISTS", - AppCode::Unauthorized => "UNAUTHORIZED", - AppCode::Forbidden => "FORBIDDEN", - AppCode::NotImplemented => "NOT_IMPLEMENTED", - AppCode::BadRequest => "BAD_REQUEST", - AppCode::RateLimited => "RATE_LIMITED", - AppCode::TelegramAuth => "TELEGRAM_AUTH", - AppCode::InvalidJwt => "INVALID_JWT", - - // 5xx - AppCode::Internal => "INTERNAL", - AppCode::Database => "DATABASE", - AppCode::Service => "SERVICE", - AppCode::Config => "CONFIG", - AppCode::Turnkey => "TURNKEY", - AppCode::Timeout => "TIMEOUT", - AppCode::Network => "NETWORK", - AppCode::DependencyUnavailable => "DEPENDENCY_UNAVAILABLE", - AppCode::Serialization => "SERIALIZATION", - AppCode::Deserialization => "DESERIALIZATION", - AppCode::ExternalApi => "EXTERNAL_API", - AppCode::Queue => "QUEUE", - AppCode::Cache => "CACHE" + /// This matches the JSON serialization. + #[must_use] + pub fn as_str(&self) -> &str { + match &self.repr { + CodeRepr::Static(value) => value, + CodeRepr::Owned(value) => value } } } @@ -211,7 +185,7 @@ impl Display for AppCode { /// /// # Errors /// -/// Returns [`ParseAppCodeError`] when the input does not match any known code. +/// Returns [`ParseAppCodeError`] when the input is not SCREAMING_SNAKE_CASE. /// /// # Examples /// ``` @@ -227,36 +201,11 @@ impl FromStr for AppCode { type Err = ParseAppCodeError; fn from_str(s: &str) -> Result { - match s { - // 4xx - "NOT_FOUND" => Ok(Self::NotFound), - "VALIDATION" => Ok(Self::Validation), - "CONFLICT" => Ok(Self::Conflict), - "USER_ALREADY_EXISTS" => Ok(Self::UserAlreadyExists), - "UNAUTHORIZED" => Ok(Self::Unauthorized), - "FORBIDDEN" => Ok(Self::Forbidden), - "NOT_IMPLEMENTED" => Ok(Self::NotImplemented), - "BAD_REQUEST" => Ok(Self::BadRequest), - "RATE_LIMITED" => Ok(Self::RateLimited), - "TELEGRAM_AUTH" => Ok(Self::TelegramAuth), - "INVALID_JWT" => Ok(Self::InvalidJwt), - - // 5xx - "INTERNAL" => Ok(Self::Internal), - "DATABASE" => Ok(Self::Database), - "SERVICE" => Ok(Self::Service), - "CONFIG" => Ok(Self::Config), - "TURNKEY" => Ok(Self::Turnkey), - "TIMEOUT" => Ok(Self::Timeout), - "NETWORK" => Ok(Self::Network), - "DEPENDENCY_UNAVAILABLE" => Ok(Self::DependencyUnavailable), - "SERIALIZATION" => Ok(Self::Serialization), - "DESERIALIZATION" => Ok(Self::Deserialization), - "EXTERNAL_API" => Ok(Self::ExternalApi), - "QUEUE" => Ok(Self::Queue), - "CACHE" => Ok(Self::Cache), - _ => Err(ParseAppCodeError) + if let Some(code) = match_static(s) { + return Ok(code); } + + Self::try_new(s.to_owned()) } } @@ -297,6 +246,119 @@ impl From for AppCode { } } +impl Serialize for AppCode { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer + { + serializer.serialize_str(self.as_str()) + } +} + +impl<'de> Deserialize<'de> for AppCode { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de> + { + struct Visitor; + + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = AppCode; + + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("a SCREAMING_SNAKE_CASE code") + } + + fn visit_borrowed_str(self, value: &'de str) -> Result + where + E: serde::de::Error + { + AppCode::from_str(value).map_err(E::custom) + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error + { + AppCode::from_str(value).map_err(E::custom) + } + + fn visit_string(self, value: String) -> Result + where + E: serde::de::Error + { + AppCode::try_new(value).map_err(E::custom) + } + } + + deserializer.deserialize_str(Visitor) + } +} + +fn validate_code(value: &str) -> Result<(), ParseAppCodeError> { + if !is_valid_literal(value) { + return Err(ParseAppCodeError); + } + + Ok(()) +} + +fn match_static(value: &str) -> Option { + match value { + "NOT_FOUND" => Some(AppCode::NotFound), + "VALIDATION" => Some(AppCode::Validation), + "CONFLICT" => Some(AppCode::Conflict), + "USER_ALREADY_EXISTS" => Some(AppCode::UserAlreadyExists), + "UNAUTHORIZED" => Some(AppCode::Unauthorized), + "FORBIDDEN" => Some(AppCode::Forbidden), + "NOT_IMPLEMENTED" => Some(AppCode::NotImplemented), + "BAD_REQUEST" => Some(AppCode::BadRequest), + "RATE_LIMITED" => Some(AppCode::RateLimited), + "TELEGRAM_AUTH" => Some(AppCode::TelegramAuth), + "INVALID_JWT" => Some(AppCode::InvalidJwt), + "INTERNAL" => Some(AppCode::Internal), + "DATABASE" => Some(AppCode::Database), + "SERVICE" => Some(AppCode::Service), + "CONFIG" => Some(AppCode::Config), + "TURNKEY" => Some(AppCode::Turnkey), + "TIMEOUT" => Some(AppCode::Timeout), + "NETWORK" => Some(AppCode::Network), + "DEPENDENCY_UNAVAILABLE" => Some(AppCode::DependencyUnavailable), + "SERIALIZATION" => Some(AppCode::Serialization), + "DESERIALIZATION" => Some(AppCode::Deserialization), + "EXTERNAL_API" => Some(AppCode::ExternalApi), + "QUEUE" => Some(AppCode::Queue), + "CACHE" => Some(AppCode::Cache), + _ => None + } +} + +const fn is_valid_literal(value: &str) -> bool { + let bytes = value.as_bytes(); + let len = bytes.len(); + if len == 0 { + return false; + } + + if bytes[0] == b'_' || bytes[len - 1] == b'_' { + return false; + } + + let mut index = 0; + while index < len { + let byte = bytes[index]; + if !matches!(byte, b'A'..=b'Z' | b'0'..=b'9' | b'_') { + return false; + } + if byte == b'_' && index + 1 < len && bytes[index + 1] == b'_' { + return false; + } + index += 1; + } + + true +} + #[cfg(test)] mod tests { use std::str::FromStr; @@ -316,22 +378,10 @@ mod tests { #[test] fn mapping_from_kind_is_stable() { // Spot checks to guard against accidental remaps. - assert!(matches!( - AppCode::from(AppErrorKind::NotFound), - AppCode::NotFound - )); - assert!(matches!( - AppCode::from(AppErrorKind::Validation), - AppCode::Validation - )); - assert!(matches!( - AppCode::from(AppErrorKind::Internal), - AppCode::Internal - )); - assert!(matches!( - AppCode::from(AppErrorKind::Timeout), - AppCode::Timeout - )); + assert_eq!(AppCode::from(AppErrorKind::NotFound), AppCode::NotFound); + assert_eq!(AppCode::from(AppErrorKind::Validation), AppCode::Validation); + assert_eq!(AppCode::from(AppErrorKind::Internal), AppCode::Internal); + assert_eq!(AppCode::from(AppErrorKind::Timeout), AppCode::Timeout); } #[test] @@ -339,6 +389,20 @@ mod tests { assert_eq!(AppCode::BadRequest.to_string(), "BAD_REQUEST"); } + #[test] + fn new_and_try_new_validate_input() { + let code = AppCode::new("CUSTOM_CODE"); + assert_eq!(code.as_str(), "CUSTOM_CODE"); + assert!(AppCode::try_new(String::from("ANOTHER_CODE")).is_ok()); + assert!(AppCode::try_new(String::from("lower")).is_err()); + } + + #[test] + #[should_panic] + fn new_panics_on_invalid_literal() { + let _ = AppCode::new("not_snake"); + } + #[test] fn from_str_parses_known_codes() { for code in [ @@ -354,8 +418,14 @@ mod tests { } #[test] - fn from_str_rejects_unknown_code() { - let err = AppCode::from_str("NOT_A_REAL_CODE").unwrap_err(); + fn from_str_allows_dynamic_codes() { + let parsed = AppCode::from_str("THIRD_PARTY_FAILURE").expect("parse"); + assert_eq!(parsed.as_str(), "THIRD_PARTY_FAILURE"); + } + + #[test] + fn from_str_rejects_unknown_code_shape() { + let err = AppCode::from_str("NOT-A-REAL-CODE").unwrap_err(); assert_eq!(err, ParseAppCodeError); } } diff --git a/src/convert/sqlx.rs b/src/convert/sqlx.rs index d16f549..33c8695 100644 --- a/src/convert/sqlx.rs +++ b/src/convert/sqlx.rs @@ -231,7 +231,7 @@ fn classify_database_error(error: &(dyn DatabaseError + 'static)) -> (Context, O .iter() .find(|(state, _)| *state == sqlstate.as_str()) { - code_override = Some(*app_code); + code_override = Some(app_code.clone()); } } diff --git a/src/convert/tonic.rs b/src/convert/tonic.rs index f0058db..a7bb30e 100644 --- a/src/convert/tonic.rs +++ b/src/convert/tonic.rs @@ -62,7 +62,7 @@ impl From for Status { fn status_from_error(error: &Error) -> Status { error.emit_telemetry(); - let mapping = mapping_for_code(error.code); + let mapping = mapping_for_code(&error.code); let grpc_code = Code::from_i32(mapping.grpc().value); let detail = sanitize_detail(error.message.as_ref(), error.kind, error.edit_policy); let mut meta = MetadataMap::new(); diff --git a/src/mapping.rs b/src/mapping.rs index 8b56f3d..0223eb2 100644 --- a/src/mapping.rs +++ b/src/mapping.rs @@ -12,7 +12,7 @@ use crate::{AppCode, AppErrorKind}; /// Stores the stable public [`AppCode`] and semantic [`AppErrorKind`]. The /// HTTP status code can be derived from the kind via /// [`AppErrorKind::http_status`]. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct HttpMapping { code: AppCode, kind: AppErrorKind @@ -30,8 +30,8 @@ impl HttpMapping { /// Stable machine-readable error code. #[must_use] - pub const fn code(&self) -> AppCode { - self.code + pub fn code(&self) -> &AppCode { + &self.code } /// Semantic application error category. @@ -50,7 +50,7 @@ impl HttpMapping { /// gRPC mapping for a domain error. /// /// Stores the [`AppCode`], [`AppErrorKind`] and a gRPC status code (as `i32`). -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct GrpcMapping { code: AppCode, kind: AppErrorKind, @@ -70,8 +70,8 @@ impl GrpcMapping { /// Stable machine-readable error code. #[must_use] - pub const fn code(&self) -> AppCode { - self.code + pub fn code(&self) -> &AppCode { + &self.code } /// Semantic application error category. @@ -91,7 +91,7 @@ impl GrpcMapping { /// /// Associates an error with the [`AppCode`], [`AppErrorKind`] and a canonical /// problem `type` URI. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct ProblemMapping { code: AppCode, kind: AppErrorKind, @@ -111,8 +111,8 @@ impl ProblemMapping { /// Stable machine-readable error code. #[must_use] - pub const fn code(&self) -> AppCode { - self.code + pub fn code(&self) -> &AppCode { + &self.code } /// Semantic application error category. diff --git a/src/response/core.rs b/src/response/core.rs index 8240c37..76dabcf 100644 --- a/src/response/core.rs +++ b/src/response/core.rs @@ -27,7 +27,7 @@ pub struct RetryAdvice { pub struct ErrorResponse { /// HTTP status code (e.g. 404, 422, 500). pub status: u16, - /// Stable machine-readable error code (enum). + /// Stable machine-readable error code. pub code: AppCode, /// Human-oriented, non-sensitive message. pub message: String, diff --git a/src/response/mapping.rs b/src/response/mapping.rs index fb4848f..9addbbb 100644 --- a/src/response/mapping.rs +++ b/src/response/mapping.rs @@ -14,7 +14,7 @@ impl Display for ErrorResponse { impl From for ErrorResponse { fn from(mut err: AppError) -> Self { let kind = err.kind; - let code = err.code; + let code = err.code.clone(); let retry = err.retry.take(); let www_authenticate = err.www_authenticate.take(); let policy = err.edit_policy; @@ -71,7 +71,7 @@ impl From<&AppError> for ErrorResponse { Self { status, - code: err.code, + code: err.code.clone(), message, details, retry: err.retry, diff --git a/src/response/problem_json.rs b/src/response/problem_json.rs index d6eaa03..73ced8f 100644 --- a/src/response/problem_json.rs +++ b/src/response/problem_json.rs @@ -26,7 +26,7 @@ use crate::{ /// ```rust /// use masterror::{AppCode, mapping_for_code}; /// -/// let mapping = mapping_for_code(AppCode::NotFound); +/// let mapping = mapping_for_code(&AppCode::NotFound); /// assert_eq!(mapping.http_status(), 404); /// assert_eq!( /// mapping.problem_type(), @@ -78,7 +78,7 @@ impl CodeMapping { /// ```rust /// use masterror::{AppCode, mapping_for_code}; /// -/// let grpc = mapping_for_code(AppCode::Internal).grpc(); +/// let grpc = mapping_for_code(&AppCode::Internal).grpc(); /// assert_eq!(grpc.name, "INTERNAL"); /// assert_eq!(grpc.value, 13); /// ``` @@ -162,7 +162,7 @@ impl ProblemJson { pub fn from_app_error(mut error: AppError) -> Self { error.emit_telemetry(); - let code = error.code; + let code = error.code.clone(); let kind = error.kind; let message = error.message.take(); let metadata = core::mem::take(&mut error.metadata); @@ -171,7 +171,7 @@ impl ProblemJson { let retry = error.retry.take(); let www_authenticate = error.www_authenticate.take(); - let mapping = mapping_for_code(code); + let mapping = mapping_for_code(&code); let status = kind.http_status(); let title = Cow::Owned(kind.to_string()); let detail = sanitize_detail(message, kind, edit_policy); @@ -208,7 +208,7 @@ impl ProblemJson { /// ``` #[must_use] pub fn from_ref(error: &AppError) -> Self { - let mapping = mapping_for_code(error.code); + let mapping = mapping_for_code(&error.code); let status = error.kind.http_status(); let title = Cow::Owned(error.kind.to_string()); let detail = sanitize_detail_ref(error); @@ -221,7 +221,7 @@ impl ProblemJson { status, detail, details, - code: error.code, + code: error.code.clone(), grpc: Some(mapping.grpc()), metadata, retry_after: error.retry.map(|value| value.after_seconds), @@ -245,24 +245,33 @@ impl ProblemJson { /// ``` #[must_use] pub fn from_error_response(response: ErrorResponse) -> Self { - let mapping = mapping_for_code(response.code); - let detail = if response.message.is_empty() { + let ErrorResponse { + status, + code, + message, + details, + retry, + www_authenticate + } = response; + + let mapping = mapping_for_code(&code); + let detail = if message.is_empty() { None } else { - Some(Cow::Owned(response.message)) + Some(Cow::Owned(message)) }; Self { type_uri: Some(Cow::Borrowed(mapping.problem_type())), title: Cow::Owned(mapping.kind().to_string()), - status: response.status, + status, detail, - details: response.details, - code: response.code, + details, + code, grpc: Some(mapping.grpc()), metadata: None, - retry_after: response.retry.map(|value| value.after_seconds), - www_authenticate: response.www_authenticate + retry_after: retry.map(|value| value.after_seconds), + www_authenticate } } @@ -995,15 +1004,15 @@ const DEFAULT_MAPPING: CodeMapping = CodeMapping { /// ```rust /// use masterror::{AppCode, mapping_for_code}; /// -/// let mapping = mapping_for_code(AppCode::Timeout); +/// let mapping = mapping_for_code(&AppCode::Timeout); /// assert_eq!(mapping.grpc().name, "DEADLINE_EXCEEDED"); /// ``` #[must_use] -pub fn mapping_for_code(code: AppCode) -> CodeMapping { +pub fn mapping_for_code(code: &AppCode) -> CodeMapping { CODE_MAPPINGS .iter() .find_map(|(candidate, mapping)| { - if *candidate == code { + if candidate == code { Some(*mapping) } else { None diff --git a/src/response/tests.rs b/src/response/tests.rs index 5eb8b74..ddd59cc 100644 --- a/src/response/tests.rs +++ b/src/response/tests.rs @@ -72,6 +72,19 @@ fn details_json_are_attached() { assert_eq!(e.details.unwrap(), payload); } +#[cfg(feature = "serde_json")] +#[test] +fn custom_codes_roundtrip_via_json() { + let custom = AppCode::new("INVALID_JSON"); + let response = ErrorResponse::new(400, custom.clone(), "invalid body").expect("status"); + + let json = serde_json::to_string(&response).expect("serialize"); + let decoded: ErrorResponse = serde_json::from_str(&json).expect("decode"); + + assert_eq!(decoded.code, custom); + assert_eq!(decoded.code.as_str(), "INVALID_JSON"); +} + #[cfg(feature = "serde_json")] #[test] fn with_details_serializes_custom_struct() { diff --git a/tests/ui/app_error/fail/enum_missing_variant.stderr b/tests/ui/app_error/fail/enum_missing_variant.stderr index d000de1..bbc297c 100644 --- a/tests/ui/app_error/fail/enum_missing_variant.stderr +++ b/tests/ui/app_error/fail/enum_missing_variant.stderr @@ -1,8 +1,9 @@ error: all variants must use #[app_error(...)] to derive AppError conversion --> tests/ui/app_error/fail/enum_missing_variant.rs:8:5 | -8 | #[error("without")] - | ^ +8 | / #[error("without")] +9 | | Without, + | |___________^ warning: unused import: `AppErrorKind` --> tests/ui/app_error/fail/enum_missing_variant.rs:1:17 @@ -10,4 +11,4 @@ warning: unused import: `AppErrorKind` 1 | use masterror::{AppErrorKind, Error}; | ^^^^^^^^^^^^ | - = note: `#[warn(unused_imports)]` on by default + = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default diff --git a/tests/ui/app_error/fail/missing_code.stderr b/tests/ui/app_error/fail/missing_code.stderr index 70ccade..4f02301 100644 --- a/tests/ui/app_error/fail/missing_code.stderr +++ b/tests/ui/app_error/fail/missing_code.stderr @@ -2,7 +2,7 @@ error: AppCode conversion requires `code = ...` in #[app_error(...)] --> tests/ui/app_error/fail/missing_code.rs:9:5 | 9 | #[app_error(kind = AppErrorKind::Service)] - | ^ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ warning: unused imports: `AppCode` and `AppErrorKind` --> tests/ui/app_error/fail/missing_code.rs:1:17 @@ -10,4 +10,4 @@ warning: unused imports: `AppCode` and `AppErrorKind` 1 | use masterror::{AppCode, AppErrorKind, Error}; | ^^^^^^^ ^^^^^^^^^^^^ | - = note: `#[warn(unused_imports)]` on by default + = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default diff --git a/tests/ui/app_error/fail/missing_kind.stderr b/tests/ui/app_error/fail/missing_kind.stderr index c615e98..021c135 100644 --- a/tests/ui/app_error/fail/missing_kind.stderr +++ b/tests/ui/app_error/fail/missing_kind.stderr @@ -2,4 +2,4 @@ error: missing `kind = ...` in #[app_error(...)] --> tests/ui/app_error/fail/missing_kind.rs:5:1 | 5 | #[app_error(message)] - | ^ + | ^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/ui/formatter/fail/duplicate_fmt.stderr b/tests/ui/formatter/fail/duplicate_fmt.stderr index 5b08225..5b8f363 100644 --- a/tests/ui/formatter/fail/duplicate_fmt.stderr +++ b/tests/ui/formatter/fail/duplicate_fmt.stderr @@ -2,4 +2,4 @@ error: duplicate `fmt` handler specified --> tests/ui/formatter/fail/duplicate_fmt.rs:4:36 | 4 | #[error(fmt = crate::format_error, fmt = crate::format_error)] - | ^^^ + | ^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/ui/formatter/fail/implicit_after_named.stderr b/tests/ui/formatter/fail/implicit_after_named.stderr index d416399..be76742 100644 --- a/tests/ui/formatter/fail/implicit_after_named.stderr +++ b/tests/ui/formatter/fail/implicit_after_named.stderr @@ -8,4 +8,5 @@ error: multiple unused formatting arguments | argument never used | argument never used | + = note: consider adding 2 format specifiers = note: this error originates in the derive macro `Error` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/formatter/fail/unsupported_flag.stderr b/tests/ui/formatter/fail/unsupported_flag.stderr index d7acdb1..b8bf229 100644 --- a/tests/ui/formatter/fail/unsupported_flag.stderr +++ b/tests/ui/formatter/fail/unsupported_flag.stderr @@ -1,5 +1,5 @@ error: placeholder spanning bytes 0..11 uses an unsupported formatter - --> tests/ui/formatter/fail/unsupported_flag.rs:4:9 + --> tests/ui/formatter/fail/unsupported_flag.rs:4:10 | 4 | #[error("{value:##x}")] - | ^^^^^^^^^^^^^ + | ^^^^^^^^^^^ diff --git a/tests/ui/formatter/fail/unsupported_formatter.stderr b/tests/ui/formatter/fail/unsupported_formatter.stderr index 5869420..a6a40c2 100644 --- a/tests/ui/formatter/fail/unsupported_formatter.stderr +++ b/tests/ui/formatter/fail/unsupported_formatter.stderr @@ -1,5 +1,5 @@ error: placeholder spanning bytes 0..9 uses an unsupported formatter - --> tests/ui/formatter/fail/unsupported_formatter.rs:4:9 + --> tests/ui/formatter/fail/unsupported_formatter.rs:4:10 | 4 | #[error("{value:y}")] - | ^^^^^^^^^^^ + | ^^^^^^^^^ diff --git a/tests/ui/formatter/fail/uppercase_binary.stderr b/tests/ui/formatter/fail/uppercase_binary.stderr index bbe04b4..3d332c7 100644 --- a/tests/ui/formatter/fail/uppercase_binary.stderr +++ b/tests/ui/formatter/fail/uppercase_binary.stderr @@ -1,5 +1,5 @@ error: placeholder spanning bytes 0..9 uses an unsupported formatter - --> tests/ui/formatter/fail/uppercase_binary.rs:4:9 + --> tests/ui/formatter/fail/uppercase_binary.rs:4:10 | 4 | #[error("{value:B}")] - | ^^^^^^^^^^^ + | ^^^^^^^^^ diff --git a/tests/ui/formatter/fail/uppercase_pointer.stderr b/tests/ui/formatter/fail/uppercase_pointer.stderr index 2c30e71..0bd10fa 100644 --- a/tests/ui/formatter/fail/uppercase_pointer.stderr +++ b/tests/ui/formatter/fail/uppercase_pointer.stderr @@ -1,5 +1,5 @@ error: placeholder spanning bytes 0..9 uses an unsupported formatter - --> tests/ui/formatter/fail/uppercase_pointer.rs:4:9 + --> tests/ui/formatter/fail/uppercase_pointer.rs:4:10 | 4 | #[error("{value:P}")] - | ^^^^^^^^^^^ + | ^^^^^^^^^ diff --git a/tests/ui/masterror/fail/duplicate_attr.stderr b/tests/ui/masterror/fail/duplicate_attr.stderr index c3fb86b..113a10d 100644 --- a/tests/ui/masterror/fail/duplicate_attr.stderr +++ b/tests/ui/masterror/fail/duplicate_attr.stderr @@ -10,4 +10,4 @@ warning: unused imports: `AppCode` and `AppErrorKind` 1 | use masterror::{AppCode, AppErrorKind, Masterror}; | ^^^^^^^ ^^^^^^^^^^^^ | - = note: `#[warn(unused_imports)]` on by default + = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default diff --git a/tests/ui/masterror/fail/duplicate_telemetry.stderr b/tests/ui/masterror/fail/duplicate_telemetry.stderr index b331baa..9ada290 100644 --- a/tests/ui/masterror/fail/duplicate_telemetry.stderr +++ b/tests/ui/masterror/fail/duplicate_telemetry.stderr @@ -10,4 +10,4 @@ warning: unused imports: `AppCode` and `AppErrorKind` 1 | use masterror::{AppCode, AppErrorKind, Masterror}; | ^^^^^^^ ^^^^^^^^^^^^ | - = note: `#[warn(unused_imports)]` on by default + = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default diff --git a/tests/ui/masterror/fail/empty_redact.stderr b/tests/ui/masterror/fail/empty_redact.stderr index b2658a1..fd151cc 100644 --- a/tests/ui/masterror/fail/empty_redact.stderr +++ b/tests/ui/masterror/fail/empty_redact.stderr @@ -10,4 +10,4 @@ warning: unused imports: `AppCode` and `AppErrorKind` 1 | use masterror::{AppCode, AppErrorKind, Masterror}; | ^^^^^^^ ^^^^^^^^^^^^ | - = note: `#[warn(unused_imports)]` on by default + = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default diff --git a/tests/ui/masterror/fail/enum_missing_variant.stderr b/tests/ui/masterror/fail/enum_missing_variant.stderr index 83d517f..5a25e12 100644 --- a/tests/ui/masterror/fail/enum_missing_variant.stderr +++ b/tests/ui/masterror/fail/enum_missing_variant.stderr @@ -1,8 +1,9 @@ error: all variants must use #[masterror(...)] to derive masterror::Error conversion --> tests/ui/masterror/fail/enum_missing_variant.rs:8:5 | -8 | #[error("missing")] - | ^ +8 | / #[error("missing")] +9 | | Missing + | |___________^ warning: unused imports: `AppCode` and `AppErrorKind` --> tests/ui/masterror/fail/enum_missing_variant.rs:1:17 @@ -10,4 +11,4 @@ warning: unused imports: `AppCode` and `AppErrorKind` 1 | use masterror::{AppCode, AppErrorKind, Masterror}; | ^^^^^^^ ^^^^^^^^^^^^ | - = note: `#[warn(unused_imports)]` on by default + = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default diff --git a/tests/ui/masterror/fail/missing_category.stderr b/tests/ui/masterror/fail/missing_category.stderr index f929951..bdadf45 100644 --- a/tests/ui/masterror/fail/missing_category.stderr +++ b/tests/ui/masterror/fail/missing_category.stderr @@ -2,7 +2,7 @@ error: missing `category = ...` in #[masterror(...)] --> tests/ui/masterror/fail/missing_category.rs:5:1 | 5 | #[masterror(code = AppCode::Internal)] - | ^ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ warning: unused import: `AppCode` --> tests/ui/masterror/fail/missing_category.rs:1:17 @@ -10,4 +10,4 @@ warning: unused import: `AppCode` 1 | use masterror::{AppCode, Masterror}; | ^^^^^^^ | - = note: `#[warn(unused_imports)]` on by default + = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default diff --git a/tests/ui/masterror/fail/missing_code.stderr b/tests/ui/masterror/fail/missing_code.stderr index 34abc91..037fac8 100644 --- a/tests/ui/masterror/fail/missing_code.stderr +++ b/tests/ui/masterror/fail/missing_code.stderr @@ -2,7 +2,7 @@ error: missing `code = ...` in #[masterror(...)] --> tests/ui/masterror/fail/missing_code.rs:5:1 | 5 | #[masterror(category = AppErrorKind::Internal)] - | ^ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ warning: unused import: `AppErrorKind` --> tests/ui/masterror/fail/missing_code.rs:1:17 @@ -10,4 +10,4 @@ warning: unused import: `AppErrorKind` 1 | use masterror::{AppErrorKind, Masterror}; | ^^^^^^^^^^^^ | - = note: `#[warn(unused_imports)]` on by default + = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default diff --git a/tests/ui/masterror/fail/unknown_option.stderr b/tests/ui/masterror/fail/unknown_option.stderr index d579838..1822edf 100644 --- a/tests/ui/masterror/fail/unknown_option.stderr +++ b/tests/ui/masterror/fail/unknown_option.stderr @@ -10,4 +10,4 @@ warning: unused imports: `AppCode` and `AppErrorKind` 1 | use masterror::{AppCode, AppErrorKind, Masterror}; | ^^^^^^^ ^^^^^^^^^^^^ | - = note: `#[warn(unused_imports)]` on by default + = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default