use core::{fmt, ops::Range};
use std::borrow::Cow;
mod parser;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ErrorTemplate<'a> {
source: &'a str,
segments: Vec<TemplateSegment<'a>>
}
impl<'a> ErrorTemplate<'a> {
pub fn parse(source: &'a str) -> Result<Self, TemplateError> {
let segments = parser::parse_template(source)?;
Ok(Self {
source,
segments
})
}
pub const fn source(&self) -> &'a str {
self.source
}
pub fn segments(&self) -> &[TemplateSegment<'a>] {
&self.segments
}
pub fn placeholders(&self) -> impl Iterator<Item = &TemplatePlaceholder<'a>> {
self.segments.iter().filter_map(|segment| match segment {
TemplateSegment::Placeholder(placeholder) => Some(placeholder),
TemplateSegment::Literal(_) => None
})
}
pub fn display_with<F>(&'a self, resolver: F) -> DisplayWith<'a, 'a, F>
where
F: Fn(&TemplatePlaceholder<'a>, &mut fmt::Formatter<'_>) -> fmt::Result
{
DisplayWith {
template: self,
resolver
}
}
}
#[derive(Debug)]
pub struct DisplayWith<'a, 't, F>
where
F: Fn(&TemplatePlaceholder<'a>, &mut fmt::Formatter<'_>) -> fmt::Result
{
template: &'t ErrorTemplate<'a>,
resolver: F
}
impl<'a, 't, F> fmt::Display for DisplayWith<'a, 't, F>
where
F: Fn(&TemplatePlaceholder<'a>, &mut fmt::Formatter<'_>) -> fmt::Result
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for segment in &self.template.segments {
match segment {
TemplateSegment::Literal(literal) => f.write_str(literal)?,
TemplateSegment::Placeholder(placeholder) => {
(self.resolver)(placeholder, f)?;
}
}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TemplateSegment<'a> {
Literal(&'a str),
Placeholder(TemplatePlaceholder<'a>)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TemplatePlaceholder<'a> {
span: Range<usize>,
identifier: TemplateIdentifier<'a>,
formatter: TemplateFormatter
}
impl<'a> TemplatePlaceholder<'a> {
pub fn span(&self) -> Range<usize> {
self.span.clone()
}
pub const fn identifier(&self) -> &TemplateIdentifier<'a> {
&self.identifier
}
pub fn formatter(&self) -> &TemplateFormatter {
&self.formatter
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TemplateIdentifier<'a> {
Implicit(usize),
Positional(usize),
Named(&'a str)
}
impl<'a> TemplateIdentifier<'a> {
pub const fn as_str(&self) -> Option<&'a str> {
match self {
Self::Named(value) => Some(value),
Self::Positional(_) | Self::Implicit(_) => None
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TemplateFormatterKind {
Display,
Debug,
LowerHex,
UpperHex,
Pointer,
Binary,
Octal,
LowerExp,
UpperExp
}
impl TemplateFormatterKind {
pub const fn from_specifier(specifier: char) -> Option<Self> {
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
}
}
pub const fn specifier(self) -> Option<char> {
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')
}
}
pub const fn supports_alternate(self) -> bool {
!matches!(self, Self::Display)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TemplateFormatter {
Display {
spec: Option<Box<str>>
},
Debug {
alternate: bool
},
LowerHex {
alternate: bool
},
UpperHex {
alternate: bool
},
Pointer {
alternate: bool
},
Binary {
alternate: bool
},
Octal {
alternate: bool
},
LowerExp {
alternate: bool
},
UpperExp {
alternate: bool
}
}
impl TemplateFormatter {
pub const fn from_kind(kind: TemplateFormatterKind, alternate: bool) -> Self {
match kind {
TemplateFormatterKind::Display => Self::Display {
spec: None
},
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
}
}
}
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
}
}
pub fn from_format_spec(spec: &str) -> Option<Self> {
Self::parse_specifier(spec)
}
pub(crate) fn parse_specifier(spec: &str) -> Option<Self> {
parser::parse_formatter_spec(spec)
}
pub fn display_spec(&self) -> Option<&str> {
match self {
Self::Display {
spec: Some(spec)
} => Some(spec),
_ => None
}
}
pub fn has_display_spec(&self) -> bool {
matches!(
self,
Self::Display {
spec: Some(_)
}
)
}
pub fn format_fragment(&self) -> Option<Cow<'_, str>> {
match self {
Self::Display {
spec
} => spec.as_deref().map(Cow::Borrowed),
Self::Debug {
alternate
} => {
if *alternate {
Some(Cow::Borrowed("#?"))
} else {
Some(Cow::Borrowed("?"))
}
}
Self::LowerHex {
alternate
} => {
if *alternate {
Some(Cow::Borrowed("#x"))
} else {
Some(Cow::Borrowed("x"))
}
}
Self::UpperHex {
alternate
} => {
if *alternate {
Some(Cow::Borrowed("#X"))
} else {
Some(Cow::Borrowed("X"))
}
}
Self::Pointer {
alternate
} => {
if *alternate {
Some(Cow::Borrowed("#p"))
} else {
Some(Cow::Borrowed("p"))
}
}
Self::Binary {
alternate
} => {
if *alternate {
Some(Cow::Borrowed("#b"))
} else {
Some(Cow::Borrowed("b"))
}
}
Self::Octal {
alternate
} => {
if *alternate {
Some(Cow::Borrowed("#o"))
} else {
Some(Cow::Borrowed("o"))
}
}
Self::LowerExp {
alternate
} => {
if *alternate {
Some(Cow::Borrowed("#e"))
} else {
Some(Cow::Borrowed("e"))
}
}
Self::UpperExp {
alternate
} => {
if *alternate {
Some(Cow::Borrowed("#E"))
} else {
Some(Cow::Borrowed("E"))
}
}
}
}
pub const fn is_alternate(&self) -> bool {
match self {
Self::Display {
..
} => false,
Self::Debug {
alternate
}
| Self::LowerHex {
alternate
}
| Self::UpperHex {
alternate
}
| Self::Pointer {
alternate
}
| Self::Binary {
alternate
}
| Self::Octal {
alternate
}
| Self::LowerExp {
alternate
}
| Self::UpperExp {
alternate
} => *alternate
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TemplateError {
UnmatchedClosingBrace {
index: usize
},
UnterminatedPlaceholder {
start: usize
},
NestedPlaceholder {
index: usize
},
EmptyPlaceholder {
start: usize
},
InvalidIdentifier {
span: Range<usize>
},
InvalidIndex {
span: Range<usize>
},
InvalidFormatter {
span: Range<usize>
}
}
impl fmt::Display for TemplateError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnmatchedClosingBrace {
index
} => {
write!(f, "unmatched closing brace at byte {}", index)
}
Self::UnterminatedPlaceholder {
start
} => {
write!(f, "placeholder starting at byte {} is not closed", start)
}
Self::NestedPlaceholder {
index
} => {
write!(
f,
"nested placeholder starting at byte {} is not supported",
index
)
}
Self::EmptyPlaceholder {
start
} => {
write!(f, "placeholder starting at byte {} is empty", start)
}
Self::InvalidIdentifier {
span
} => {
write!(
f,
"invalid placeholder identifier spanning bytes {}..{}",
span.start, span.end
)
}
Self::InvalidIndex {
span
} => {
write!(
f,
"positional placeholder spanning bytes {}..{} is not a valid unsigned integer",
span.start, span.end
)
}
Self::InvalidFormatter {
span
} => {
write!(
f,
"placeholder spanning bytes {}..{} uses an unsupported formatter",
span.start, span.end
)
}
}
}
}
impl std::error::Error for TemplateError {}
#[cfg(test)]
mod tests {
use super::*;
fn named(name: &str) -> TemplateIdentifier<'_> {
TemplateIdentifier::Named(name)
}
#[test]
fn parses_basic_template() {
let template = ErrorTemplate::parse("{code}: {message}").expect("parse");
let segments = template.segments();
assert_eq!(segments.len(), 3);
assert!(matches!(segments[0], TemplateSegment::Placeholder(_)));
assert!(matches!(segments[1], TemplateSegment::Literal(": ")));
assert!(matches!(segments[2], TemplateSegment::Placeholder(_)));
let placeholders: Vec<_> = template.placeholders().collect();
assert_eq!(placeholders.len(), 2);
assert_eq!(placeholders[0].identifier(), &named("code"));
assert_eq!(placeholders[1].identifier(), &named("message"));
}
#[test]
fn parses_implicit_identifiers() {
let template = ErrorTemplate::parse("{}, {:?}, {name}, {}").expect("parse");
let mut placeholders = template.placeholders();
let first = placeholders.next().expect("first placeholder");
assert_eq!(first.identifier(), &TemplateIdentifier::Implicit(0));
assert_eq!(
first.formatter(),
&TemplateFormatter::Display {
spec: None
}
);
let second = placeholders.next().expect("second placeholder");
assert_eq!(second.identifier(), &TemplateIdentifier::Implicit(1));
assert_eq!(
second.formatter(),
&TemplateFormatter::Debug {
alternate: false
}
);
let third = placeholders.next().expect("third placeholder");
assert_eq!(third.identifier(), &named("name"));
let fourth = placeholders.next().expect("fourth placeholder");
assert_eq!(fourth.identifier(), &TemplateIdentifier::Implicit(2));
assert!(placeholders.next().is_none());
}
#[test]
fn parses_debug_formatter() {
let template = ErrorTemplate::parse("{0:#?}").expect("parse");
let placeholders: Vec<_> = template.placeholders().collect();
assert_eq!(placeholders.len(), 1);
assert_eq!(
placeholders[0].identifier(),
&TemplateIdentifier::Positional(0)
);
assert_eq!(
placeholders[0].formatter(),
&TemplateFormatter::Debug {
alternate: true
}
);
assert!(placeholders[0].formatter().is_alternate());
}
#[test]
fn parses_extended_formatters() {
let cases = [
(
"{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 (template_str, expected) in &cases {
let template = ErrorTemplate::parse(template_str).expect("parse");
let placeholder = template.placeholders().next().expect("placeholder present");
assert_eq!(placeholder.formatter(), expected, "case: {template_str}");
}
}
#[test]
fn preserves_hash_fill_display_specs() {
let template = ErrorTemplate::parse("{value:#>4}").expect("parse");
let placeholder = template.placeholders().next().expect("placeholder present");
assert_eq!(placeholder.formatter().display_spec(), Some("#>4"));
assert_eq!(
placeholder.formatter().format_fragment().as_deref(),
Some("#>4")
);
let expected = TemplateFormatter::Display {
spec: Some("#>4".into())
};
assert_eq!(placeholder.formatter(), &expected);
}
#[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");
let mut iter = template.segments().iter();
assert!(matches!(iter.next(), Some(TemplateSegment::Literal("{"))));
assert!(matches!(iter.next(), Some(TemplateSegment::Literal("}"))));
assert!(matches!(
iter.next(),
Some(TemplateSegment::Literal(" -> "))
));
assert!(matches!(
iter.next(),
Some(TemplateSegment::Placeholder(TemplatePlaceholder { .. }))
));
assert!(iter.next().is_none());
}
#[test]
fn rejects_unmatched_closing_brace() {
let err = ErrorTemplate::parse("oops}").expect_err("should fail");
assert!(matches!(
err,
TemplateError::UnmatchedClosingBrace {
index: 4
}
));
}
#[test]
fn rejects_unterminated_placeholder() {
let err = ErrorTemplate::parse("{oops").expect_err("should fail");
assert!(matches!(
err,
TemplateError::UnterminatedPlaceholder {
start: 0
}
));
}
#[test]
fn rejects_invalid_identifier() {
let err = ErrorTemplate::parse("{invalid-name}").expect_err("should fail");
assert!(matches!(err, TemplateError::InvalidIdentifier { span } if span == (0..14)));
}
#[test]
fn rejects_unknown_formatter() {
let err = ErrorTemplate::parse("{value:%}").expect_err("should fail");
assert!(matches!(err, TemplateError::InvalidFormatter { span } if span == (0..9)));
}
#[test]
fn display_with_resolves_placeholders() {
let template = ErrorTemplate::parse("{code}: {message}").expect("parse");
let code = 418;
let message = "I'm a teapot";
let rendered = format!(
"{}",
template.display_with(|placeholder, f| match placeholder.identifier() {
TemplateIdentifier::Named("code") => write!(f, "{}", code),
TemplateIdentifier::Named("message") => f.write_str(message),
other => panic!("unexpected placeholder: {:?}", other)
})
);
assert_eq!(rendered, "418: I'm a teapot");
}
}