From 561fa157bb3935fbb1411a109ee9cf1eb835972c Mon Sep 17 00:00:00 2001 From: RA <70325462+RAprogramm@users.noreply.github.com> Date: Fri, 19 Sep 2025 11:59:30 +0700 Subject: [PATCH] Improve template formatter parsing --- CHANGELOG.md | 12 ++ Cargo.lock | 6 +- Cargo.toml | 6 +- README.md | 14 +-- masterror-derive/Cargo.toml | 4 +- masterror-template/Cargo.toml | 2 +- masterror-template/src/template.rs | 75 ++++++------ masterror-template/src/template/parser.rs | 135 +++++++++++++++++++++- 8 files changed, 196 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fdc49f..89b6fd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,18 @@ All notable changes to this project will be documented in this file. - `masterror::Error` now uses the in-tree derive, removing the dependency on `thiserror` while keeping the same runtime behaviour and diagnostics. +## [0.5.4] - 2025-09-26 + +### Fixed +- Template parser mirrors `thiserror`'s formatter trait detection, ensuring + `:?`, `:x`, `:X`, `:p`, `:b`, `:o`, `:e` and `:E` specifiers resolve to the + appropriate `TemplateFormatter` variant while still flagging unsupported + flags precisely. + +### Tests +- Added parser-level unit tests that cover every supported formatter specifier + and assert graceful failures for malformed format strings. + ## [0.5.2] - 2025-09-25 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index d8c9b7d..52f0db7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1409,7 +1409,7 @@ dependencies = [ [[package]] name = "masterror" -version = "0.5.3" +version = "0.5.4" dependencies = [ "actix-web", "axum", @@ -1438,7 +1438,7 @@ dependencies = [ [[package]] name = "masterror-derive" -version = "0.1.1" +version = "0.1.2" dependencies = [ "masterror-template", "proc-macro2", @@ -1448,7 +1448,7 @@ dependencies = [ [[package]] name = "masterror-template" -version = "0.1.1" +version = "0.1.2" [[package]] name = "matchit" diff --git a/Cargo.toml b/Cargo.toml index 7d2c9d1..618a547 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "masterror" -version = "0.5.3" +version = "0.5.4" rust-version = "1.90" edition = "2024" description = "Application error types and response mapping" @@ -35,8 +35,8 @@ turnkey = [] openapi = ["dep:utoipa"] [workspace.dependencies] -masterror-derive = { version = "0.1.1", path = "masterror-derive" } -masterror-template = { version = "0.1.1", path = "masterror-template" } +masterror-derive = { version = "0.1.2", path = "masterror-derive" } +masterror-template = { version = "0.1.2", path = "masterror-template" } [dependencies] # masterror-derive = { path = "masterror-derive" } diff --git a/README.md b/README.md index 321e786..0c4d9c7 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,9 @@ Stable categories, conservative HTTP mapping, no `unsafe`. ~~~toml [dependencies] -masterror = { version = "0.5.3", default-features = false } +masterror = { version = "0.5.4", default-features = false } # or with features: -# masterror = { version = "0.5.3", features = [ +# masterror = { version = "0.5.4", features = [ # "axum", "actix", "openapi", "serde_json", # "sqlx", "reqwest", "redis", "validator", # "config", "tokio", "multipart", "teloxide", @@ -66,10 +66,10 @@ masterror = { version = "0.5.3", default-features = false } ~~~toml [dependencies] # lean core -masterror = { version = "0.5.3", default-features = false } +masterror = { version = "0.5.4", default-features = false } # with Axum/Actix + JSON + integrations -# masterror = { version = "0.5.3", features = [ +# masterror = { version = "0.5.4", features = [ # "axum", "actix", "openapi", "serde_json", # "sqlx", "reqwest", "redis", "validator", # "config", "tokio", "multipart", "teloxide", @@ -261,13 +261,13 @@ assert_eq!(resp.status, 401); Minimal core: ~~~toml -masterror = { version = "0.5.3", default-features = false } +masterror = { version = "0.5.4", default-features = false } ~~~ API (Axum + JSON + deps): ~~~toml -masterror = { version = "0.5.3", features = [ +masterror = { version = "0.5.4", features = [ "axum", "serde_json", "openapi", "sqlx", "reqwest", "redis", "validator", "config", "tokio" ] } @@ -276,7 +276,7 @@ masterror = { version = "0.5.3", features = [ API (Actix + JSON + deps): ~~~toml -masterror = { version = "0.5.3", features = [ +masterror = { version = "0.5.4", features = [ "actix", "serde_json", "openapi", "sqlx", "reqwest", "redis", "validator", "config", "tokio" ] } diff --git a/masterror-derive/Cargo.toml b/masterror-derive/Cargo.toml index 3f8e190..9fd8324 100644 --- a/masterror-derive/Cargo.toml +++ b/masterror-derive/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "masterror-derive" rust-version = "1.90" -version = "0.1.1" +version = "0.1.2" edition = "2024" license = "MIT OR Apache-2.0" @@ -12,4 +12,4 @@ proc-macro = true proc-macro2 = "1" quote = "1" syn = { version = "2", features = ["full", "extra-traits"] } -masterror-template = { path = "../masterror-template", version = "0.1.1" } +masterror-template = { path = "../masterror-template", version = "0.1.2" } diff --git a/masterror-template/Cargo.toml b/masterror-template/Cargo.toml index 3aa3600..928fa1e 100644 --- a/masterror-template/Cargo.toml +++ b/masterror-template/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "masterror-template" -version = "0.1.1" +version = "0.1.2" rust-version = "1.90" edition = "2024" license = "MIT OR Apache-2.0" diff --git a/masterror-template/src/template.rs b/masterror-template/src/template.rs index e3dad78..7652966 100644 --- a/masterror-template/src/template.rs +++ b/masterror-template/src/template.rs @@ -205,54 +205,47 @@ pub enum TemplateFormatter { impl TemplateFormatter { /// Parses a formatting specifier (the portion after `:`) into a formatter. pub fn from_format_spec(spec: &str) -> Option { - match spec { - "?" => Some(Self::Debug { - alternate: false - }), - "#?" => Some(Self::Debug { - alternate: true - }), - "x" => Some(Self::LowerHex { - alternate: false - }), - "#x" => Some(Self::LowerHex { - alternate: true - }), - "X" => Some(Self::UpperHex { - alternate: false - }), - "#X" => Some(Self::UpperHex { - alternate: true - }), - "p" => Some(Self::Pointer { - alternate: false - }), - "#p" => Some(Self::Pointer { - alternate: true - }), - "b" => Some(Self::Binary { - alternate: false + Self::parse_specifier(spec) + } + + pub(crate) fn parse_specifier(spec: &str) -> Option { + let trimmed = spec.trim(); + if trimmed.is_empty() { + return None; + } + + let (last_index, ty) = trimmed.char_indices().next_back()?; + let prefix = &trimmed[..last_index]; + let alternate = match prefix { + "" => false, + "#" => true, + _ => return None + }; + + match ty { + '?' => Some(Self::Debug { + alternate }), - "#b" => Some(Self::Binary { - alternate: true + 'x' => Some(Self::LowerHex { + alternate }), - "o" => Some(Self::Octal { - alternate: false + 'X' => Some(Self::UpperHex { + alternate }), - "#o" => Some(Self::Octal { - alternate: true + 'p' => Some(Self::Pointer { + alternate }), - "e" => Some(Self::LowerExp { - alternate: false + 'b' => Some(Self::Binary { + alternate }), - "#e" => Some(Self::LowerExp { - alternate: true + 'o' => Some(Self::Octal { + alternate }), - "E" => Some(Self::UpperExp { - alternate: false + 'e' => Some(Self::LowerExp { + alternate }), - "#E" => Some(Self::UpperExp { - alternate: true + 'E' => Some(Self::UpperExp { + alternate }), _ => None } diff --git a/masterror-template/src/template/parser.rs b/masterror-template/src/template/parser.rs index 2a82691..6ede325 100644 --- a/masterror-template/src/template/parser.rs +++ b/masterror-template/src/template/parser.rs @@ -153,7 +153,7 @@ fn split_placeholder<'a>( }); } Some(spec) => { - TemplateFormatter::from_format_spec(spec).ok_or(TemplateError::InvalidFormatter { + TemplateFormatter::parse_specifier(spec).ok_or(TemplateError::InvalidFormatter { span })? } @@ -192,3 +192,136 @@ fn parse_identifier<'a>( span }) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_supported_formatter_specs() { + let cases = [ + ( + "{value:?}", + TemplateFormatter::Debug { + alternate: false + } + ), + ( + "{value:#?}", + TemplateFormatter::Debug { + alternate: true + } + ), + ( + "{value:x}", + TemplateFormatter::LowerHex { + alternate: false + } + ), + ( + "{value:#x}", + TemplateFormatter::LowerHex { + alternate: true + } + ), + ( + "{value:X}", + TemplateFormatter::UpperHex { + alternate: false + } + ), + ( + "{value:#X}", + TemplateFormatter::UpperHex { + alternate: true + } + ), + ( + "{value:p}", + TemplateFormatter::Pointer { + alternate: false + } + ), + ( + "{value:#p}", + TemplateFormatter::Pointer { + alternate: true + } + ), + ( + "{value:b}", + TemplateFormatter::Binary { + alternate: false + } + ), + ( + "{value:#b}", + TemplateFormatter::Binary { + alternate: true + } + ), + ( + "{value:o}", + TemplateFormatter::Octal { + alternate: false + } + ), + ( + "{value:#o}", + TemplateFormatter::Octal { + alternate: true + } + ), + ( + "{value:e}", + TemplateFormatter::LowerExp { + alternate: false + } + ), + ( + "{value:#e}", + TemplateFormatter::LowerExp { + alternate: true + } + ), + ( + "{value:E}", + TemplateFormatter::UpperExp { + alternate: false + } + ), + ( + "{value:#E}", + TemplateFormatter::UpperExp { + alternate: true + } + ) + ]; + + for (source, expected_formatter) in &cases { + let segments = parse_template(source).expect("template parsed"); + let placeholder = match segments.first() { + Some(TemplateSegment::Placeholder(placeholder)) => placeholder, + other => panic!("unexpected segments for {source:?}: {other:?}") + }; + + assert_eq!( + placeholder.formatter(), + *expected_formatter, + "case: {source}" + ); + } + } + + #[test] + fn rejects_malformed_formatters() { + let cases = ["{value:}", "{value:#}", "{value:0x}"]; + + for source in &cases { + let err = parse_template(source).expect_err("expected formatter error"); + assert!( + matches!(err, TemplateError::InvalidFormatter { span } if span == (0..source.len())) + ); + } + } +}