diff --git a/CHANGELOG.md b/CHANGELOG.md index 8382672..e4fe582 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ All notable changes to this project will be documented in this file. ### Added - _Nothing yet._ +## [0.5.10] - 2025-10-02 + +### Changed +- Template parser now recognises formatter traits even when alignment, sign or + width flags precede the type specifier, constructing the matching + `TemplateFormatter` variant and keeping alternate (`#`) detection aligned with + `thiserror`. + +### Tests +- Extended parser unit tests to cover complex formatter specifiers and + additional malformed cases to guard diagnostic accuracy. + ## [0.5.9] - 2025-10-01 ### Added diff --git a/Cargo.lock b/Cargo.lock index 03e37d3..f327106 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1527,7 +1527,7 @@ dependencies = [ [[package]] name = "masterror" -version = "0.5.9" +version = "0.5.10" dependencies = [ "actix-web", "axum", @@ -1567,7 +1567,7 @@ dependencies = [ [[package]] name = "masterror-template" -version = "0.1.3" +version = "0.1.4" [[package]] name = "matchit" diff --git a/Cargo.toml b/Cargo.toml index 3482381..80f7fe0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "masterror" -version = "0.5.9" +version = "0.5.10" 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.3", path = "masterror-template" } +masterror-template = { version = "0.1.4", path = "masterror-template" } [dependencies] masterror-derive = { workspace = true } diff --git a/README.md b/README.md index 212d373..dd02fb8 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,9 @@ Stable categories, conservative HTTP mapping, no `unsafe`. ~~~toml [dependencies] -masterror = { version = "0.5.9", default-features = false } +masterror = { version = "0.5.10", default-features = false } # or with features: -# masterror = { version = "0.5.9", features = [ +# masterror = { version = "0.5.10", features = [ # "axum", "actix", "openapi", "serde_json", # "sqlx", "sqlx-migrate", "reqwest", "redis", # "validator", "config", "tokio", "multipart", @@ -66,10 +66,10 @@ masterror = { version = "0.5.9", default-features = false } ~~~toml [dependencies] # lean core -masterror = { version = "0.5.9", default-features = false } +masterror = { version = "0.5.10", default-features = false } # with Axum/Actix + JSON + integrations -# masterror = { version = "0.5.9", features = [ +# masterror = { version = "0.5.10", features = [ # "axum", "actix", "openapi", "serde_json", # "sqlx", "sqlx-migrate", "reqwest", "redis", # "validator", "config", "tokio", "multipart", @@ -349,13 +349,13 @@ assert_eq!(resp.status, 401); Minimal core: ~~~toml -masterror = { version = "0.5.9", default-features = false } +masterror = { version = "0.5.10", default-features = false } ~~~ API (Axum + JSON + deps): ~~~toml -masterror = { version = "0.5.9", features = [ +masterror = { version = "0.5.10", features = [ "axum", "serde_json", "openapi", "sqlx", "reqwest", "redis", "validator", "config", "tokio" ] } @@ -364,7 +364,7 @@ masterror = { version = "0.5.9", features = [ API (Actix + JSON + deps): ~~~toml -masterror = { version = "0.5.9", features = [ +masterror = { version = "0.5.10", features = [ "actix", "serde_json", "openapi", "sqlx", "reqwest", "redis", "validator", "config", "tokio" ] } diff --git a/masterror-template/Cargo.toml b/masterror-template/Cargo.toml index 0d9da04..d400a6d 100644 --- a/masterror-template/Cargo.toml +++ b/masterror-template/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "masterror-template" -version = "0.1.3" +version = "0.1.4" 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 dd6f552..74e6fca 100644 --- a/masterror-template/src/template.rs +++ b/masterror-template/src/template.rs @@ -410,21 +410,7 @@ impl TemplateFormatter { } 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 - }; - - let kind = TemplateFormatterKind::from_specifier(ty)?; - Some(Self::from_kind(kind, alternate)) + parser::parse_formatter_spec(spec) } /// Returns `true` when alternate formatting (`#`) was requested. diff --git a/masterror-template/src/template/parser.rs b/masterror-template/src/template/parser.rs index 6ede325..c438720 100644 --- a/masterror-template/src/template/parser.rs +++ b/masterror-template/src/template/parser.rs @@ -1,7 +1,8 @@ use core::ops::Range; use super::{ - TemplateError, TemplateFormatter, TemplateIdentifier, TemplatePlaceholder, TemplateSegment + TemplateError, TemplateFormatter, TemplateFormatterKind, TemplateIdentifier, + TemplatePlaceholder, TemplateSegment }; pub fn parse_template<'a>(source: &'a str) -> Result>, TemplateError> { @@ -152,16 +153,73 @@ fn split_placeholder<'a>( span }); } - Some(spec) => { - TemplateFormatter::parse_specifier(spec).ok_or(TemplateError::InvalidFormatter { - span - })? - } + Some(spec) => parse_formatter(spec, span.clone())? }; Ok((identifier, formatter)) } +fn parse_formatter(spec: &str, span: Range) -> Result { + parse_formatter_spec(spec).ok_or(TemplateError::InvalidFormatter { + span + }) +} + +pub(super) fn parse_formatter_spec(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 kind = TemplateFormatterKind::from_specifier(ty)?; + let alternate = detect_alternate_flag(prefix)?; + + Some(TemplateFormatter::from_kind(kind, alternate)) +} + +fn detect_alternate_flag(prefix: &str) -> Option { + let mut rest = prefix; + + if rest.len() >= 2 { + let mut iter = rest.char_indices(); + if let (Some((_, _)), Some((second_index, second))) = (iter.next(), iter.next()) + && matches!(second, '<' | '>' | '^' | '=') + { + let skip = second_index + second.len_utf8(); + rest = &rest[skip..]; + } + } + + if let Some(first) = rest.chars().next() + && matches!(first, '<' | '>' | '^' | '=') + { + rest = &rest[first.len_utf8()..]; + } + + loop { + let mut chars = rest.chars(); + let Some(ch) = chars.next() else { + return Some(false); + }; + + match ch { + '+' | '-' | ' ' => { + rest = &rest[ch.len_utf8()..]; + } + '#' => { + rest = &rest[ch.len_utf8()..]; + if rest.chars().any(|value| value == '#') { + return None; + } + return Some(true); + } + _ => return Some(false) + } + } +} + fn parse_identifier<'a>( text: &'a str, span: Range @@ -212,24 +270,54 @@ mod tests { alternate: true } ), + ( + "{value:*>#?}", + TemplateFormatter::Debug { + alternate: true + } + ), + ( + "{value:#>8?}", + TemplateFormatter::Debug { + alternate: false + } + ), ( "{value:x}", TemplateFormatter::LowerHex { alternate: false } ), + ( + "{value:>08x}", + TemplateFormatter::LowerHex { + alternate: false + } + ), ( "{value:#x}", TemplateFormatter::LowerHex { alternate: true } ), + ( + "{value:*<#x}", + TemplateFormatter::LowerHex { + alternate: true + } + ), ( "{value:X}", TemplateFormatter::UpperHex { alternate: false } ), + ( + "{value:*>#X}", + TemplateFormatter::UpperHex { + alternate: true + } + ), ( "{value:#X}", TemplateFormatter::UpperHex { @@ -242,6 +330,12 @@ mod tests { alternate: false } ), + ( + "{value:>+#18p}", + TemplateFormatter::Pointer { + alternate: true + } + ), ( "{value:#p}", TemplateFormatter::Pointer { @@ -254,6 +348,12 @@ mod tests { alternate: false } ), + ( + "{value:#08b}", + TemplateFormatter::Binary { + alternate: true + } + ), ( "{value:#b}", TemplateFormatter::Binary { @@ -266,6 +366,12 @@ mod tests { alternate: false } ), + ( + "{value:+#o}", + TemplateFormatter::Octal { + alternate: true + } + ), ( "{value:#o}", TemplateFormatter::Octal { @@ -278,6 +384,12 @@ mod tests { alternate: false } ), + ( + "{value:#0e}", + TemplateFormatter::LowerExp { + alternate: true + } + ), ( "{value:#e}", TemplateFormatter::LowerExp { @@ -290,6 +402,12 @@ mod tests { alternate: false } ), + ( + "{value:#^10E}", + TemplateFormatter::UpperExp { + alternate: false + } + ), ( "{value:#E}", TemplateFormatter::UpperExp { @@ -315,7 +433,14 @@ mod tests { #[test] fn rejects_malformed_formatters() { - let cases = ["{value:}", "{value:#}", "{value:0x}"]; + let cases = [ + "{value:}", + "{value:#}", + "{value:>8q}", + "{value:#>}", + "{value:*>}", + "{value:##x}" + ]; for source in &cases { let err = parse_template(source).expect_err("expected formatter error");