From c96f1d4e86b9dbbb12368dc4b6fa354fcadeaca4 Mon Sep 17 00:00:00 2001 From: RA <70325462+RAprogramm@users.noreply.github.com> Date: Fri, 19 Sep 2025 14:12:59 +0700 Subject: [PATCH] Add TemplateFormatterKind for template formatters --- CHANGELOG.md | 16 ++ Cargo.lock | 4 +- Cargo.toml | 4 +- README.md | 23 ++- README.ru.md | 9 +- README.template.md | 9 +- masterror-template/Cargo.toml | 2 +- masterror-template/src/template.rs | 269 ++++++++++++++++++++++++++--- 8 files changed, 293 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec7bc67..8382672 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,22 @@ All notable changes to this project will be documented in this file. ### Added - _Nothing yet._ +## [0.5.9] - 2025-10-01 + +### Added +- `TemplateFormatterKind` enumerating the formatter traits supported by + `#[error("...")]`, plus `TemplateFormatter::from_kind`/`kind()` helpers for + constructing and inspecting placeholders programmatically. + +### Changed +- Formatter parsing now routes through `TemplateFormatterKind`, ensuring lookup + tables, `is_alternate` handling and downstream derives share the same + canonical representation. + +### Documentation +- Documented `TemplateFormatterKind` usage and the new inspection helpers + across README variants. + ## [0.5.8] - 2025-09-30 ### Changed diff --git a/Cargo.lock b/Cargo.lock index 63dd565..03e37d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1527,7 +1527,7 @@ dependencies = [ [[package]] name = "masterror" -version = "0.5.8" +version = "0.5.9" dependencies = [ "actix-web", "axum", @@ -1567,7 +1567,7 @@ dependencies = [ [[package]] name = "masterror-template" -version = "0.1.2" +version = "0.1.3" [[package]] name = "matchit" diff --git a/Cargo.toml b/Cargo.toml index 8c57a76..3482381 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "masterror" -version = "0.5.8" +version = "0.5.9" rust-version = "1.90" edition = "2024" license = "MIT OR Apache-2.0" @@ -50,7 +50,7 @@ openapi = ["dep:utoipa"] [workspace.dependencies] masterror-derive = { version = "0.1.4", path = "masterror-derive" } -masterror-template = { version = "0.1.2", path = "masterror-template" } +masterror-template = { version = "0.1.3", path = "masterror-template" } [dependencies] masterror-derive = { workspace = true } diff --git a/README.md b/README.md index efe6679..212d373 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,9 @@ Stable categories, conservative HTTP mapping, no `unsafe`. ~~~toml [dependencies] -masterror = { version = "0.5.8", default-features = false } +masterror = { version = "0.5.9", default-features = false } # or with features: -# masterror = { version = "0.5.8", features = [ +# masterror = { version = "0.5.9", features = [ # "axum", "actix", "openapi", "serde_json", # "sqlx", "sqlx-migrate", "reqwest", "redis", # "validator", "config", "tokio", "multipart", @@ -66,10 +66,10 @@ masterror = { version = "0.5.8", default-features = false } ~~~toml [dependencies] # lean core -masterror = { version = "0.5.8", default-features = false } +masterror = { version = "0.5.9", default-features = false } # with Axum/Actix + JSON + integrations -# masterror = { version = "0.5.8", features = [ +# masterror = { version = "0.5.9", features = [ # "axum", "actix", "openapi", "serde_json", # "sqlx", "sqlx-migrate", "reqwest", "redis", # "validator", "config", "tokio", "multipart", @@ -155,6 +155,9 @@ assert_eq!(wrapped.to_string(), "I/O failed: disk offline"); - `TemplateFormatter` mirrors `thiserror`'s formatter detection so existing derives that relied on hexadecimal, pointer or exponential renderers keep compiling. +- `TemplateFormatterKind` exposes the formatter trait requested by a + placeholder, making it easy to branch on the requested rendering behaviour + without manually matching every enum variant. #### Formatter traits @@ -211,7 +214,9 @@ assert!(rendered.contains("upper=1.5625E-1")); ~~~ ~~~rust -use masterror::error::template::{ErrorTemplate, TemplateFormatter}; +use masterror::error::template::{ + ErrorTemplate, TemplateFormatter, TemplateFormatterKind +}; let template = ErrorTemplate::parse("{code:#x} → {payload:?}").expect("parse"); let mut placeholders = template.placeholders(); @@ -221,6 +226,8 @@ assert!(matches!( code.formatter(), TemplateFormatter::LowerHex { alternate: true } )); +assert_eq!(code.formatter().kind(), TemplateFormatterKind::LowerHex); +assert!(code.formatter().is_alternate()); let payload = placeholders.next().expect("payload placeholder"); assert_eq!( @@ -342,13 +349,13 @@ assert_eq!(resp.status, 401); Minimal core: ~~~toml -masterror = { version = "0.5.8", default-features = false } +masterror = { version = "0.5.9", default-features = false } ~~~ API (Axum + JSON + deps): ~~~toml -masterror = { version = "0.5.8", features = [ +masterror = { version = "0.5.9", features = [ "axum", "serde_json", "openapi", "sqlx", "reqwest", "redis", "validator", "config", "tokio" ] } @@ -357,7 +364,7 @@ masterror = { version = "0.5.8", features = [ API (Actix + JSON + deps): ~~~toml -masterror = { version = "0.5.8", features = [ +masterror = { version = "0.5.9", features = [ "actix", "serde_json", "openapi", "sqlx", "reqwest", "redis", "validator", "config", "tokio" ] } diff --git a/README.ru.md b/README.ru.md index 7fb065a..5bb4348 100644 --- a/README.ru.md +++ b/README.ru.md @@ -136,10 +136,13 @@ assert!(rendered.contains("upper=1.5625E-1")); ~~~ `masterror::error::template::ErrorTemplate` позволяет разобрать шаблон и -программно проверить запрошенные форматтеры: +программно проверить запрошенные форматтеры; перечисление +`TemplateFormatterKind` возвращает название трейта для каждого плейсхолдера: ~~~rust -use masterror::error::template::{ErrorTemplate, TemplateFormatter}; +use masterror::error::template::{ + ErrorTemplate, TemplateFormatter, TemplateFormatterKind +}; let template = ErrorTemplate::parse("{code:#x} → {payload:?}").expect("parse"); let mut placeholders = template.placeholders(); @@ -149,6 +152,8 @@ assert!(matches!( code.formatter(), TemplateFormatter::LowerHex { alternate: true } )); +assert_eq!(code.formatter().kind(), TemplateFormatterKind::LowerHex); +assert!(code.formatter().is_alternate()); let payload = placeholders.next().expect("payload placeholder"); assert_eq!( diff --git a/README.template.md b/README.template.md index 9f72b55..a3d263d 100644 --- a/README.template.md +++ b/README.template.md @@ -149,6 +149,9 @@ assert_eq!(wrapped.to_string(), "I/O failed: disk offline"); - `TemplateFormatter` mirrors `thiserror`'s formatter detection so existing derives that relied on hexadecimal, pointer or exponential renderers keep compiling. +- `TemplateFormatterKind` exposes the formatter trait requested by a + placeholder, making it easy to branch on the requested rendering behaviour + without manually matching every enum variant. #### Formatter traits @@ -205,7 +208,9 @@ assert!(rendered.contains("upper=1.5625E-1")); ~~~ ~~~rust -use masterror::error::template::{ErrorTemplate, TemplateFormatter}; +use masterror::error::template::{ + ErrorTemplate, TemplateFormatter, TemplateFormatterKind +}; let template = ErrorTemplate::parse("{code:#x} → {payload:?}").expect("parse"); let mut placeholders = template.placeholders(); @@ -215,6 +220,8 @@ assert!(matches!( code.formatter(), TemplateFormatter::LowerHex { alternate: true } )); +assert_eq!(code.formatter().kind(), TemplateFormatterKind::LowerHex); +assert!(code.formatter().is_alternate()); let payload = placeholders.next().expect("payload placeholder"); assert_eq!( diff --git a/masterror-template/Cargo.toml b/masterror-template/Cargo.toml index f5af006..0d9da04 100644 --- a/masterror-template/Cargo.toml +++ b/masterror-template/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "masterror-template" -version = "0.1.2" +version = "0.1.3" rust-version = "1.90" edition = "2024" repository = "https://github.com/RAprogramm/masterror" diff --git a/masterror-template/src/template.rs b/masterror-template/src/template.rs index 7652966..dd6f552 100644 --- a/masterror-template/src/template.rs +++ b/masterror-template/src/template.rs @@ -155,6 +155,115 @@ impl<'a> TemplateIdentifier<'a> { } } +/// Formatter traits recognised within placeholders. +/// +/// # Examples +/// +/// ``` +/// use masterror_template::template::{TemplateFormatter, TemplateFormatterKind}; +/// +/// let formatter = TemplateFormatter::LowerHex { +/// alternate: true +/// }; +/// +/// assert_eq!(formatter.kind(), TemplateFormatterKind::LowerHex); +/// assert_eq!(TemplateFormatterKind::LowerHex.specifier(), Some('x')); +/// assert!(TemplateFormatterKind::LowerHex.supports_alternate()); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TemplateFormatterKind { + /// Default `Display` trait (`{value}`). + Display, + /// `Debug` trait (`{value:?}` / `{value:#?}`). + Debug, + /// `LowerHex` trait (`{value:x}` / `{value:#x}`). + LowerHex, + /// `UpperHex` trait (`{value:X}` / `{value:#X}`). + UpperHex, + /// `Pointer` trait (`{value:p}` / `{value:#p}`). + Pointer, + /// `Binary` trait (`{value:b}` / `{value:#b}`). + Binary, + /// `Octal` trait (`{value:o}` / `{value:#o}`). + Octal, + /// `LowerExp` trait (`{value:e}` / `{value:#e}`). + LowerExp, + /// `UpperExp` trait (`{value:E}` / `{value:#E}`). + UpperExp +} + +impl TemplateFormatterKind { + /// Maps a format specifier character to a formatter kind. + /// + /// Returns `None` when the specifier is unsupported. + /// + /// # Examples + /// + /// ``` + /// use masterror_template::template::TemplateFormatterKind; + /// + /// assert_eq!( + /// TemplateFormatterKind::from_specifier('?'), + /// Some(TemplateFormatterKind::Debug) + /// ); + /// assert_eq!(TemplateFormatterKind::from_specifier('Q'), None); + /// ``` + pub const fn from_specifier(specifier: char) -> Option { + match specifier { + '?' => Some(Self::Debug), + 'x' => Some(Self::LowerHex), + 'X' => Some(Self::UpperHex), + 'p' => Some(Self::Pointer), + 'b' => Some(Self::Binary), + 'o' => Some(Self::Octal), + 'e' => Some(Self::LowerExp), + 'E' => Some(Self::UpperExp), + _ => None + } + } + + /// Returns the canonical format specifier character, if any. + /// + /// The default `Display` kind has no dedicated specifier and therefore + /// returns `None`. + /// + /// # Examples + /// + /// ``` + /// use masterror_template::template::TemplateFormatterKind; + /// + /// assert_eq!(TemplateFormatterKind::LowerHex.specifier(), Some('x')); + /// assert_eq!(TemplateFormatterKind::Display.specifier(), None); + /// ``` + pub const fn specifier(self) -> Option { + match self { + Self::Display => None, + Self::Debug => Some('?'), + Self::LowerHex => Some('x'), + Self::UpperHex => Some('X'), + Self::Pointer => Some('p'), + Self::Binary => Some('b'), + Self::Octal => Some('o'), + Self::LowerExp => Some('e'), + Self::UpperExp => Some('E') + } + } + + /// Indicates whether the formatter kind supports the alternate (`#`) flag. + /// + /// # Examples + /// + /// ``` + /// use masterror_template::template::TemplateFormatterKind; + /// + /// assert!(TemplateFormatterKind::Binary.supports_alternate()); + /// assert!(!TemplateFormatterKind::Display.supports_alternate()); + /// ``` + pub const fn supports_alternate(self) -> bool { + !matches!(self, Self::Display) + } +} + /// Formatting mode requested by the placeholder. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TemplateFormatter { @@ -203,6 +312,98 @@ pub enum TemplateFormatter { } impl TemplateFormatter { + /// Constructs a formatter from a [`TemplateFormatterKind`] and `alternate` + /// flag. + /// + /// The `alternate` flag is ignored for [`TemplateFormatterKind::Display`]. + /// + /// # Examples + /// + /// ``` + /// use masterror_template::template::{TemplateFormatter, TemplateFormatterKind}; + /// + /// let formatter = TemplateFormatter::from_kind(TemplateFormatterKind::Binary, true); + /// + /// assert!(matches!( + /// formatter, + /// TemplateFormatter::Binary { + /// alternate: true + /// } + /// )); + /// ``` + pub const fn from_kind(kind: TemplateFormatterKind, alternate: bool) -> Self { + match kind { + TemplateFormatterKind::Display => Self::Display, + TemplateFormatterKind::Debug => Self::Debug { + alternate + }, + TemplateFormatterKind::LowerHex => Self::LowerHex { + alternate + }, + TemplateFormatterKind::UpperHex => Self::UpperHex { + alternate + }, + TemplateFormatterKind::Pointer => Self::Pointer { + alternate + }, + TemplateFormatterKind::Binary => Self::Binary { + alternate + }, + TemplateFormatterKind::Octal => Self::Octal { + alternate + }, + TemplateFormatterKind::LowerExp => Self::LowerExp { + alternate + }, + TemplateFormatterKind::UpperExp => Self::UpperExp { + alternate + } + } + } + + /// Returns the underlying formatter kind. + /// + /// # Examples + /// + /// ``` + /// use masterror_template::template::{TemplateFormatter, TemplateFormatterKind}; + /// + /// let formatter = TemplateFormatter::Pointer { + /// alternate: false + /// }; + /// + /// assert_eq!(formatter.kind(), TemplateFormatterKind::Pointer); + /// ``` + pub const fn kind(&self) -> TemplateFormatterKind { + match self { + Self::Display => TemplateFormatterKind::Display, + Self::Debug { + .. + } => TemplateFormatterKind::Debug, + Self::LowerHex { + .. + } => TemplateFormatterKind::LowerHex, + Self::UpperHex { + .. + } => TemplateFormatterKind::UpperHex, + Self::Pointer { + .. + } => TemplateFormatterKind::Pointer, + Self::Binary { + .. + } => TemplateFormatterKind::Binary, + Self::Octal { + .. + } => TemplateFormatterKind::Octal, + Self::LowerExp { + .. + } => TemplateFormatterKind::LowerExp, + Self::UpperExp { + .. + } => TemplateFormatterKind::UpperExp + } + } + /// Parses a formatting specifier (the portion after `:`) into a formatter. pub fn from_format_spec(spec: &str) -> Option { Self::parse_specifier(spec) @@ -222,33 +423,8 @@ impl TemplateFormatter { _ => return None }; - match ty { - '?' => Some(Self::Debug { - alternate - }), - 'x' => Some(Self::LowerHex { - alternate - }), - 'X' => Some(Self::UpperHex { - alternate - }), - 'p' => Some(Self::Pointer { - alternate - }), - 'b' => Some(Self::Binary { - alternate - }), - 'o' => Some(Self::Octal { - alternate - }), - 'e' => Some(Self::LowerExp { - alternate - }), - 'E' => Some(Self::UpperExp { - alternate - }), - _ => None - } + let kind = TemplateFormatterKind::from_specifier(ty)?; + Some(Self::from_kind(kind, alternate)) } /// Returns `true` when alternate formatting (`#`) was requested. @@ -522,6 +698,45 @@ mod tests { } } + #[test] + fn formatter_kind_helpers_cover_all_variants() { + let table = [ + (TemplateFormatterKind::Debug, '?'), + (TemplateFormatterKind::LowerHex, 'x'), + (TemplateFormatterKind::UpperHex, 'X'), + (TemplateFormatterKind::Pointer, 'p'), + (TemplateFormatterKind::Binary, 'b'), + (TemplateFormatterKind::Octal, 'o'), + (TemplateFormatterKind::LowerExp, 'e'), + (TemplateFormatterKind::UpperExp, 'E') + ]; + + for (kind, specifier) in table { + assert_eq!(TemplateFormatterKind::from_specifier(specifier), Some(kind)); + assert_eq!(kind.specifier(), Some(specifier)); + + let with_alternate = TemplateFormatter::from_kind(kind, true); + let without_alternate = TemplateFormatter::from_kind(kind, false); + + assert_eq!(with_alternate.kind(), kind); + assert_eq!(without_alternate.kind(), kind); + + if kind.supports_alternate() { + assert!(with_alternate.is_alternate()); + assert!(!without_alternate.is_alternate()); + } else { + assert!(!with_alternate.is_alternate()); + assert!(!without_alternate.is_alternate()); + } + } + + let display = TemplateFormatter::from_kind(TemplateFormatterKind::Display, true); + assert_eq!(display.kind(), TemplateFormatterKind::Display); + assert!(!display.is_alternate()); + assert_eq!(TemplateFormatterKind::Display.specifier(), None); + assert!(!TemplateFormatterKind::Display.supports_alternate()); + } + #[test] fn handles_brace_escaping() { let template = ErrorTemplate::parse("{{}} -> {value}").expect("parse");