masterror_template/
template.rs

1// SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
2//
3// SPDX-License-Identifier: MIT
4
5use core::{fmt, ops::Range};
6use std::borrow::Cow;
7
8mod parser;
9
10/// Parsed representation of an `#[error("...")]` template.
11///
12/// Templates are represented as a sequence of literal segments and
13/// placeholders.  The structure mirrors the internal representation used by
14/// formatting machinery, but keeps the slices borrowed from the original input
15/// to avoid unnecessary allocations.
16///
17/// # Examples
18///
19/// ```
20/// use masterror_template::template::{ErrorTemplate, TemplateIdentifier};
21///
22/// let template = ErrorTemplate::parse("{code}: {message}").expect("parse");
23/// let rendered = format!(
24///     "{}",
25///     template.display_with(|placeholder, f| match placeholder.identifier() {
26///         TemplateIdentifier::Named("code") => write!(f, "{}", 404),
27///         TemplateIdentifier::Named("message") => f.write_str("Not Found"),
28///         _ => Ok(())
29///     })
30/// );
31///
32/// assert_eq!(rendered, "404: Not Found");
33/// ```
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct ErrorTemplate<'a> {
36    source:   &'a str,
37    segments: Vec<TemplateSegment<'a>>
38}
39
40impl<'a> ErrorTemplate<'a> {
41    /// Parses an error display template.
42    pub fn parse(source: &'a str) -> Result<Self, TemplateError> {
43        let segments = parser::parse_template(source)?;
44        Ok(Self {
45            source,
46            segments
47        })
48    }
49
50    /// Returns the original template string.
51    pub const fn source(&self) -> &'a str {
52        self.source
53    }
54
55    /// Returns the parsed segments.
56    pub fn segments(&self) -> &[TemplateSegment<'a>] {
57        &self.segments
58    }
59
60    /// Iterates over placeholder segments in order of appearance.
61    pub fn placeholders(&self) -> impl Iterator<Item = &TemplatePlaceholder<'a>> {
62        self.segments.iter().filter_map(|segment| match segment {
63            TemplateSegment::Placeholder(placeholder) => Some(placeholder),
64            TemplateSegment::Literal(_) => None
65        })
66    }
67
68    /// Produces a display implementation that delegates placeholder rendering
69    /// to the provided resolver.
70    pub fn display_with<F>(&'a self, resolver: F) -> DisplayWith<'a, 'a, F>
71    where
72        F: Fn(&TemplatePlaceholder<'a>, &mut fmt::Formatter<'_>) -> fmt::Result
73    {
74        DisplayWith {
75            template: self,
76            resolver
77        }
78    }
79}
80
81/// A lazily formatted view over a template.
82#[derive(Debug)]
83pub struct DisplayWith<'a, 't, F>
84where
85    F: Fn(&TemplatePlaceholder<'a>, &mut fmt::Formatter<'_>) -> fmt::Result
86{
87    template: &'t ErrorTemplate<'a>,
88    resolver: F
89}
90
91impl<'a, 't, F> fmt::Display for DisplayWith<'a, 't, F>
92where
93    F: Fn(&TemplatePlaceholder<'a>, &mut fmt::Formatter<'_>) -> fmt::Result
94{
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        for segment in &self.template.segments {
97            match segment {
98                TemplateSegment::Literal(literal) => f.write_str(literal)?,
99                TemplateSegment::Placeholder(placeholder) => {
100                    (self.resolver)(placeholder, f)?;
101                }
102            }
103        }
104
105        Ok(())
106    }
107}
108
109/// A single segment of the parsed template.
110#[derive(Debug, Clone, PartialEq, Eq)]
111pub enum TemplateSegment<'a> {
112    /// Literal text copied verbatim.
113    Literal(&'a str),
114    /// Placeholder (`{name}` or `{0}`) that needs formatting.
115    Placeholder(TemplatePlaceholder<'a>)
116}
117
118/// Placeholder metadata extracted from a template.
119#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct TemplatePlaceholder<'a> {
121    span:       Range<usize>,
122    identifier: TemplateIdentifier<'a>,
123    formatter:  TemplateFormatter
124}
125
126impl<'a> TemplatePlaceholder<'a> {
127    /// Byte range (inclusive start, exclusive end) of the placeholder within
128    /// the original template.
129    pub fn span(&self) -> Range<usize> {
130        self.span.clone()
131    }
132
133    /// Returns the parsed identifier.
134    pub const fn identifier(&self) -> &TemplateIdentifier<'a> {
135        &self.identifier
136    }
137
138    /// Returns the requested formatter.
139    pub fn formatter(&self) -> &TemplateFormatter {
140        &self.formatter
141    }
142}
143
144/// Placeholder identifier parsed from the template.
145#[derive(Debug, Clone, PartialEq, Eq)]
146pub enum TemplateIdentifier<'a> {
147    /// Implicit positional index inferred from the placeholder order (`{}` /
148    /// `{:?}` / etc.).
149    Implicit(usize),
150    /// Positional index (`{0}` / `{1:?}` / etc.).
151    Positional(usize),
152    /// Named field (`{name}` / `{kind:?}` / etc.).
153    Named(&'a str)
154}
155
156impl<'a> TemplateIdentifier<'a> {
157    /// Returns the identifier as a string when it is named.
158    pub const fn as_str(&self) -> Option<&'a str> {
159        match self {
160            Self::Named(value) => Some(value),
161            Self::Positional(_) | Self::Implicit(_) => None
162        }
163    }
164}
165
166/// Formatter traits recognised within placeholders.
167///
168/// # Examples
169///
170/// ```
171/// use masterror_template::template::{TemplateFormatter, TemplateFormatterKind};
172///
173/// let formatter = TemplateFormatter::LowerHex {
174///     alternate: true
175/// };
176///
177/// assert_eq!(formatter.kind(), TemplateFormatterKind::LowerHex);
178/// assert_eq!(TemplateFormatterKind::LowerHex.specifier(), Some('x'));
179/// assert!(TemplateFormatterKind::LowerHex.supports_alternate());
180/// ```
181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
182pub enum TemplateFormatterKind {
183    /// Default `Display` trait (`{value}`).
184    Display,
185    /// `Debug` trait (`{value:?}` / `{value:#?}`).
186    Debug,
187    /// `LowerHex` trait (`{value:x}` / `{value:#x}`).
188    LowerHex,
189    /// `UpperHex` trait (`{value:X}` / `{value:#X}`).
190    UpperHex,
191    /// `Pointer` trait (`{value:p}` / `{value:#p}`).
192    Pointer,
193    /// `Binary` trait (`{value:b}` / `{value:#b}`).
194    Binary,
195    /// `Octal` trait (`{value:o}` / `{value:#o}`).
196    Octal,
197    /// `LowerExp` trait (`{value:e}` / `{value:#e}`).
198    LowerExp,
199    /// `UpperExp` trait (`{value:E}` / `{value:#E}`).
200    UpperExp
201}
202
203impl TemplateFormatterKind {
204    /// Maps a format specifier character to a formatter kind.
205    ///
206    /// Returns `None` when the specifier is unsupported.
207    ///
208    /// # Examples
209    ///
210    /// ```
211    /// use masterror_template::template::TemplateFormatterKind;
212    ///
213    /// assert_eq!(
214    ///     TemplateFormatterKind::from_specifier('?'),
215    ///     Some(TemplateFormatterKind::Debug)
216    /// );
217    /// assert_eq!(TemplateFormatterKind::from_specifier('Q'), None);
218    /// ```
219    pub const fn from_specifier(specifier: char) -> Option<Self> {
220        match specifier {
221            '?' => Some(Self::Debug),
222            'x' => Some(Self::LowerHex),
223            'X' => Some(Self::UpperHex),
224            'p' => Some(Self::Pointer),
225            'b' => Some(Self::Binary),
226            'o' => Some(Self::Octal),
227            'e' => Some(Self::LowerExp),
228            'E' => Some(Self::UpperExp),
229            _ => None
230        }
231    }
232
233    /// Returns the canonical format specifier character, if any.
234    ///
235    /// The default `Display` kind has no dedicated specifier and therefore
236    /// returns `None`.
237    ///
238    /// # Examples
239    ///
240    /// ```
241    /// use masterror_template::template::TemplateFormatterKind;
242    ///
243    /// assert_eq!(TemplateFormatterKind::LowerHex.specifier(), Some('x'));
244    /// assert_eq!(TemplateFormatterKind::Display.specifier(), None);
245    /// ```
246    pub const fn specifier(self) -> Option<char> {
247        match self {
248            Self::Display => None,
249            Self::Debug => Some('?'),
250            Self::LowerHex => Some('x'),
251            Self::UpperHex => Some('X'),
252            Self::Pointer => Some('p'),
253            Self::Binary => Some('b'),
254            Self::Octal => Some('o'),
255            Self::LowerExp => Some('e'),
256            Self::UpperExp => Some('E')
257        }
258    }
259
260    /// Indicates whether the formatter kind supports the alternate (`#`) flag.
261    ///
262    /// # Examples
263    ///
264    /// ```
265    /// use masterror_template::template::TemplateFormatterKind;
266    ///
267    /// assert!(TemplateFormatterKind::Binary.supports_alternate());
268    /// assert!(!TemplateFormatterKind::Display.supports_alternate());
269    /// ```
270    pub const fn supports_alternate(self) -> bool {
271        !matches!(self, Self::Display)
272    }
273}
274
275/// Formatting mode requested by the placeholder.
276#[derive(Debug, Clone, PartialEq, Eq)]
277pub enum TemplateFormatter {
278    /// Default `Display` formatting (`{value}`) with an optional format spec.
279    Display {
280        /// Raw display format specifier (for example `">8"` or ".3").
281        spec: Option<Box<str>>
282    },
283    /// `Debug` formatting (`{value:?}` or `{value:#?}`).
284    Debug {
285        /// Whether `{value:#?}` (alternate debug) was requested.
286        alternate: bool
287    },
288    /// Lower-hexadecimal formatting (`{value:x}` / `{value:#x}`).
289    LowerHex {
290        /// Whether alternate formatting (`{value:#x}`) was requested.
291        alternate: bool
292    },
293    /// Upper-hexadecimal formatting (`{value:X}` / `{value:#X}`).
294    UpperHex {
295        /// Whether alternate formatting (`{value:#X}`) was requested.
296        alternate: bool
297    },
298    /// Pointer formatting (`{value:p}` / `{value:#p}`).
299    Pointer {
300        /// Whether alternate formatting (`{value:#p}`) was requested.
301        alternate: bool
302    },
303    /// Binary formatting (`{value:b}` / `{value:#b}`).
304    Binary {
305        /// Whether alternate formatting (`{value:#b}`) was requested.
306        alternate: bool
307    },
308    /// Octal formatting (`{value:o}` / `{value:#o}`).
309    Octal {
310        /// Whether alternate formatting (`{value:#o}`) was requested.
311        alternate: bool
312    },
313    /// Lower exponential formatting (`{value:e}` / `{value:#e}`).
314    LowerExp {
315        /// Whether alternate formatting (`{value:#e}`) was requested.
316        alternate: bool
317    },
318    /// Upper exponential formatting (`{value:E}` / `{value:#E}`).
319    UpperExp {
320        /// Whether alternate formatting (`{value:#E}`) was requested.
321        alternate: bool
322    }
323}
324
325impl TemplateFormatter {
326    /// Constructs a formatter from a [`TemplateFormatterKind`] and `alternate`
327    /// flag.
328    ///
329    /// The `alternate` flag is ignored for [`TemplateFormatterKind::Display`].
330    ///
331    /// # Examples
332    ///
333    /// ```
334    /// use masterror_template::template::{TemplateFormatter, TemplateFormatterKind};
335    ///
336    /// let formatter = TemplateFormatter::from_kind(TemplateFormatterKind::Binary, true);
337    ///
338    /// assert!(matches!(
339    ///     formatter,
340    ///     TemplateFormatter::Binary {
341    ///         alternate: true
342    ///     }
343    /// ));
344    /// ```
345    pub const fn from_kind(kind: TemplateFormatterKind, alternate: bool) -> Self {
346        match kind {
347            TemplateFormatterKind::Display => Self::Display {
348                spec: None
349            },
350            TemplateFormatterKind::Debug => Self::Debug {
351                alternate
352            },
353            TemplateFormatterKind::LowerHex => Self::LowerHex {
354                alternate
355            },
356            TemplateFormatterKind::UpperHex => Self::UpperHex {
357                alternate
358            },
359            TemplateFormatterKind::Pointer => Self::Pointer {
360                alternate
361            },
362            TemplateFormatterKind::Binary => Self::Binary {
363                alternate
364            },
365            TemplateFormatterKind::Octal => Self::Octal {
366                alternate
367            },
368            TemplateFormatterKind::LowerExp => Self::LowerExp {
369                alternate
370            },
371            TemplateFormatterKind::UpperExp => Self::UpperExp {
372                alternate
373            }
374        }
375    }
376
377    /// Returns the underlying formatter kind.
378    ///
379    /// # Examples
380    ///
381    /// ```
382    /// use masterror_template::template::{TemplateFormatter, TemplateFormatterKind};
383    ///
384    /// let formatter = TemplateFormatter::Pointer {
385    ///     alternate: false
386    /// };
387    ///
388    /// assert_eq!(formatter.kind(), TemplateFormatterKind::Pointer);
389    /// ```
390    pub const fn kind(&self) -> TemplateFormatterKind {
391        match self {
392            Self::Display {
393                ..
394            } => TemplateFormatterKind::Display,
395            Self::Debug {
396                ..
397            } => TemplateFormatterKind::Debug,
398            Self::LowerHex {
399                ..
400            } => TemplateFormatterKind::LowerHex,
401            Self::UpperHex {
402                ..
403            } => TemplateFormatterKind::UpperHex,
404            Self::Pointer {
405                ..
406            } => TemplateFormatterKind::Pointer,
407            Self::Binary {
408                ..
409            } => TemplateFormatterKind::Binary,
410            Self::Octal {
411                ..
412            } => TemplateFormatterKind::Octal,
413            Self::LowerExp {
414                ..
415            } => TemplateFormatterKind::LowerExp,
416            Self::UpperExp {
417                ..
418            } => TemplateFormatterKind::UpperExp
419        }
420    }
421
422    /// Parses a formatting specifier (the portion after `:`) into a formatter.
423    pub fn from_format_spec(spec: &str) -> Option<Self> {
424        Self::parse_specifier(spec)
425    }
426
427    pub(crate) fn parse_specifier(spec: &str) -> Option<Self> {
428        parser::parse_formatter_spec(spec)
429    }
430
431    /// Returns the stored display format specifier, if any.
432    pub fn display_spec(&self) -> Option<&str> {
433        match self {
434            Self::Display {
435                spec: Some(spec)
436            } => Some(spec),
437            _ => None
438        }
439    }
440
441    /// Indicates whether a display formatter carries additional formatting
442    /// parameters.
443    pub fn has_display_spec(&self) -> bool {
444        matches!(
445            self,
446            Self::Display {
447                spec: Some(_)
448            }
449        )
450    }
451
452    /// Returns the formatter fragment that should follow the `:` in a format
453    /// string.
454    pub fn format_fragment(&self) -> Option<Cow<'_, str>> {
455        match self {
456            Self::Display {
457                spec
458            } => spec.as_deref().map(Cow::Borrowed),
459            Self::Debug {
460                alternate
461            } => {
462                if *alternate {
463                    Some(Cow::Borrowed("#?"))
464                } else {
465                    Some(Cow::Borrowed("?"))
466                }
467            }
468            Self::LowerHex {
469                alternate
470            } => {
471                if *alternate {
472                    Some(Cow::Borrowed("#x"))
473                } else {
474                    Some(Cow::Borrowed("x"))
475                }
476            }
477            Self::UpperHex {
478                alternate
479            } => {
480                if *alternate {
481                    Some(Cow::Borrowed("#X"))
482                } else {
483                    Some(Cow::Borrowed("X"))
484                }
485            }
486            Self::Pointer {
487                alternate
488            } => {
489                if *alternate {
490                    Some(Cow::Borrowed("#p"))
491                } else {
492                    Some(Cow::Borrowed("p"))
493                }
494            }
495            Self::Binary {
496                alternate
497            } => {
498                if *alternate {
499                    Some(Cow::Borrowed("#b"))
500                } else {
501                    Some(Cow::Borrowed("b"))
502                }
503            }
504            Self::Octal {
505                alternate
506            } => {
507                if *alternate {
508                    Some(Cow::Borrowed("#o"))
509                } else {
510                    Some(Cow::Borrowed("o"))
511                }
512            }
513            Self::LowerExp {
514                alternate
515            } => {
516                if *alternate {
517                    Some(Cow::Borrowed("#e"))
518                } else {
519                    Some(Cow::Borrowed("e"))
520                }
521            }
522            Self::UpperExp {
523                alternate
524            } => {
525                if *alternate {
526                    Some(Cow::Borrowed("#E"))
527                } else {
528                    Some(Cow::Borrowed("E"))
529                }
530            }
531        }
532    }
533
534    /// Returns `true` when alternate formatting (`#`) was requested.
535    pub const fn is_alternate(&self) -> bool {
536        match self {
537            Self::Display {
538                ..
539            } => false,
540            Self::Debug {
541                alternate
542            }
543            | Self::LowerHex {
544                alternate
545            }
546            | Self::UpperHex {
547                alternate
548            }
549            | Self::Pointer {
550                alternate
551            }
552            | Self::Binary {
553                alternate
554            }
555            | Self::Octal {
556                alternate
557            }
558            | Self::LowerExp {
559                alternate
560            }
561            | Self::UpperExp {
562                alternate
563            } => *alternate
564        }
565    }
566}
567
568/// Parsing errors produced when validating a template.
569#[derive(Debug, Clone, PartialEq, Eq)]
570pub enum TemplateError {
571    /// Encountered a stray closing brace.
572    UnmatchedClosingBrace {
573        /// Byte index of the stray `}` in the original template.
574        index: usize
575    },
576    /// Placeholder without a matching closing brace.
577    UnterminatedPlaceholder {
578        /// Byte index where the unterminated placeholder starts.
579        start: usize
580    },
581    /// Encountered `{{` or `}}` imbalance inside a placeholder.
582    NestedPlaceholder {
583        /// Byte index of the unexpected brace.
584        index: usize
585    },
586    /// Placeholder without an identifier.
587    EmptyPlaceholder {
588        /// Byte index where the empty placeholder starts.
589        start: usize
590    },
591    /// Identifier is malformed (contains illegal characters).
592    InvalidIdentifier {
593        /// Span (byte indices) covering the invalid identifier.
594        span: Range<usize>
595    },
596    /// Positional identifier is not a valid unsigned integer.
597    InvalidIndex {
598        /// Span (byte indices) covering the invalid positional identifier.
599        span: Range<usize>
600    },
601    /// Unsupported formatting specifier.
602    InvalidFormatter {
603        /// Span (byte indices) covering the unsupported formatter.
604        span: Range<usize>
605    }
606}
607
608impl fmt::Display for TemplateError {
609    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
610        match self {
611            Self::UnmatchedClosingBrace {
612                index
613            } => {
614                write!(f, "unmatched closing brace at byte {}", index)
615            }
616            Self::UnterminatedPlaceholder {
617                start
618            } => {
619                write!(f, "placeholder starting at byte {} is not closed", start)
620            }
621            Self::NestedPlaceholder {
622                index
623            } => {
624                write!(
625                    f,
626                    "nested placeholder starting at byte {} is not supported",
627                    index
628                )
629            }
630            Self::EmptyPlaceholder {
631                start
632            } => {
633                write!(f, "placeholder starting at byte {} is empty", start)
634            }
635            Self::InvalidIdentifier {
636                span
637            } => {
638                write!(
639                    f,
640                    "invalid placeholder identifier spanning bytes {}..{}",
641                    span.start, span.end
642                )
643            }
644            Self::InvalidIndex {
645                span
646            } => {
647                write!(
648                    f,
649                    "positional placeholder spanning bytes {}..{} is not a valid unsigned integer",
650                    span.start, span.end
651                )
652            }
653            Self::InvalidFormatter {
654                span
655            } => {
656                write!(
657                    f,
658                    "placeholder spanning bytes {}..{} uses an unsupported formatter",
659                    span.start, span.end
660                )
661            }
662        }
663    }
664}
665
666impl std::error::Error for TemplateError {}
667
668#[cfg(test)]
669mod tests {
670    use super::*;
671
672    fn named(name: &str) -> TemplateIdentifier<'_> {
673        TemplateIdentifier::Named(name)
674    }
675
676    #[test]
677    fn parses_basic_template() {
678        let template = ErrorTemplate::parse("{code}: {message}").expect("parse");
679        let segments = template.segments();
680
681        assert_eq!(segments.len(), 3);
682        assert!(matches!(segments[0], TemplateSegment::Placeholder(_)));
683        assert!(matches!(segments[1], TemplateSegment::Literal(": ")));
684        assert!(matches!(segments[2], TemplateSegment::Placeholder(_)));
685
686        let placeholders: Vec<_> = template.placeholders().collect();
687        assert_eq!(placeholders.len(), 2);
688        assert_eq!(placeholders[0].identifier(), &named("code"));
689        assert_eq!(placeholders[1].identifier(), &named("message"));
690    }
691
692    #[test]
693    fn parses_implicit_identifiers() {
694        let template = ErrorTemplate::parse("{}, {:?}, {name}, {}").expect("parse");
695        let mut placeholders = template.placeholders();
696
697        let first = placeholders.next().expect("first placeholder");
698        assert_eq!(first.identifier(), &TemplateIdentifier::Implicit(0));
699        assert_eq!(
700            first.formatter(),
701            &TemplateFormatter::Display {
702                spec: None
703            }
704        );
705
706        let second = placeholders.next().expect("second placeholder");
707        assert_eq!(second.identifier(), &TemplateIdentifier::Implicit(1));
708        assert_eq!(
709            second.formatter(),
710            &TemplateFormatter::Debug {
711                alternate: false
712            }
713        );
714
715        let third = placeholders.next().expect("third placeholder");
716        assert_eq!(third.identifier(), &named("name"));
717
718        let fourth = placeholders.next().expect("fourth placeholder");
719        assert_eq!(fourth.identifier(), &TemplateIdentifier::Implicit(2));
720        assert!(placeholders.next().is_none());
721    }
722
723    #[test]
724    fn parses_debug_formatter() {
725        let template = ErrorTemplate::parse("{0:#?}").expect("parse");
726        let placeholders: Vec<_> = template.placeholders().collect();
727
728        assert_eq!(placeholders.len(), 1);
729        assert_eq!(
730            placeholders[0].identifier(),
731            &TemplateIdentifier::Positional(0)
732        );
733        assert_eq!(
734            placeholders[0].formatter(),
735            &TemplateFormatter::Debug {
736                alternate: true
737            }
738        );
739        assert!(placeholders[0].formatter().is_alternate());
740    }
741
742    #[test]
743    fn parses_extended_formatters() {
744        let cases = [
745            (
746                "{value:x}",
747                TemplateFormatter::LowerHex {
748                    alternate: false
749                }
750            ),
751            (
752                "{value:#x}",
753                TemplateFormatter::LowerHex {
754                    alternate: true
755                }
756            ),
757            (
758                "{value:X}",
759                TemplateFormatter::UpperHex {
760                    alternate: false
761                }
762            ),
763            (
764                "{value:#X}",
765                TemplateFormatter::UpperHex {
766                    alternate: true
767                }
768            ),
769            (
770                "{value:p}",
771                TemplateFormatter::Pointer {
772                    alternate: false
773                }
774            ),
775            (
776                "{value:#p}",
777                TemplateFormatter::Pointer {
778                    alternate: true
779                }
780            ),
781            (
782                "{value:b}",
783                TemplateFormatter::Binary {
784                    alternate: false
785                }
786            ),
787            (
788                "{value:#b}",
789                TemplateFormatter::Binary {
790                    alternate: true
791                }
792            ),
793            (
794                "{value:o}",
795                TemplateFormatter::Octal {
796                    alternate: false
797                }
798            ),
799            (
800                "{value:#o}",
801                TemplateFormatter::Octal {
802                    alternate: true
803                }
804            ),
805            (
806                "{value:e}",
807                TemplateFormatter::LowerExp {
808                    alternate: false
809                }
810            ),
811            (
812                "{value:#e}",
813                TemplateFormatter::LowerExp {
814                    alternate: true
815                }
816            ),
817            (
818                "{value:E}",
819                TemplateFormatter::UpperExp {
820                    alternate: false
821                }
822            ),
823            (
824                "{value:#E}",
825                TemplateFormatter::UpperExp {
826                    alternate: true
827                }
828            )
829        ];
830
831        for (template_str, expected) in &cases {
832            let template = ErrorTemplate::parse(template_str).expect("parse");
833            let placeholder = template.placeholders().next().expect("placeholder present");
834            assert_eq!(placeholder.formatter(), expected, "case: {template_str}");
835        }
836    }
837
838    #[test]
839    fn preserves_hash_fill_display_specs() {
840        let template = ErrorTemplate::parse("{value:#>4}").expect("parse");
841        let placeholder = template.placeholders().next().expect("placeholder present");
842
843        assert_eq!(placeholder.formatter().display_spec(), Some("#>4"));
844        assert_eq!(
845            placeholder.formatter().format_fragment().as_deref(),
846            Some("#>4")
847        );
848
849        let expected = TemplateFormatter::Display {
850            spec: Some("#>4".into())
851        };
852
853        assert_eq!(placeholder.formatter(), &expected);
854    }
855
856    #[test]
857    fn formatter_kind_helpers_cover_all_variants() {
858        let table = [
859            (TemplateFormatterKind::Debug, '?'),
860            (TemplateFormatterKind::LowerHex, 'x'),
861            (TemplateFormatterKind::UpperHex, 'X'),
862            (TemplateFormatterKind::Pointer, 'p'),
863            (TemplateFormatterKind::Binary, 'b'),
864            (TemplateFormatterKind::Octal, 'o'),
865            (TemplateFormatterKind::LowerExp, 'e'),
866            (TemplateFormatterKind::UpperExp, 'E')
867        ];
868
869        for (kind, specifier) in table {
870            assert_eq!(TemplateFormatterKind::from_specifier(specifier), Some(kind));
871            assert_eq!(kind.specifier(), Some(specifier));
872
873            let with_alternate = TemplateFormatter::from_kind(kind, true);
874            let without_alternate = TemplateFormatter::from_kind(kind, false);
875
876            assert_eq!(with_alternate.kind(), kind);
877            assert_eq!(without_alternate.kind(), kind);
878
879            if kind.supports_alternate() {
880                assert!(with_alternate.is_alternate());
881                assert!(!without_alternate.is_alternate());
882            } else {
883                assert!(!with_alternate.is_alternate());
884                assert!(!without_alternate.is_alternate());
885            }
886        }
887
888        let display = TemplateFormatter::from_kind(TemplateFormatterKind::Display, true);
889        assert_eq!(display.kind(), TemplateFormatterKind::Display);
890        assert!(!display.is_alternate());
891        assert_eq!(TemplateFormatterKind::Display.specifier(), None);
892        assert!(!TemplateFormatterKind::Display.supports_alternate());
893    }
894
895    #[test]
896    fn handles_brace_escaping() {
897        let template = ErrorTemplate::parse("{{}} -> {value}").expect("parse");
898        let mut iter = template.segments().iter();
899
900        assert!(matches!(iter.next(), Some(TemplateSegment::Literal("{"))));
901        assert!(matches!(iter.next(), Some(TemplateSegment::Literal("}"))));
902        assert!(matches!(
903            iter.next(),
904            Some(TemplateSegment::Literal(" -> "))
905        ));
906        assert!(matches!(
907            iter.next(),
908            Some(TemplateSegment::Placeholder(TemplatePlaceholder { .. }))
909        ));
910        assert!(iter.next().is_none());
911    }
912
913    #[test]
914    fn rejects_unmatched_closing_brace() {
915        let err = ErrorTemplate::parse("oops}").expect_err("should fail");
916        assert!(matches!(
917            err,
918            TemplateError::UnmatchedClosingBrace {
919                index: 4
920            }
921        ));
922    }
923
924    #[test]
925    fn rejects_unterminated_placeholder() {
926        let err = ErrorTemplate::parse("{oops").expect_err("should fail");
927        assert!(matches!(
928            err,
929            TemplateError::UnterminatedPlaceholder {
930                start: 0
931            }
932        ));
933    }
934
935    #[test]
936    fn rejects_invalid_identifier() {
937        let err = ErrorTemplate::parse("{invalid-name}").expect_err("should fail");
938        assert!(matches!(err, TemplateError::InvalidIdentifier { span } if span == (0..14)));
939    }
940
941    #[test]
942    fn rejects_unknown_formatter() {
943        let err = ErrorTemplate::parse("{value:%}").expect_err("should fail");
944        assert!(matches!(err, TemplateError::InvalidFormatter { span } if span == (0..9)));
945    }
946
947    #[test]
948    fn display_with_resolves_placeholders() {
949        let template = ErrorTemplate::parse("{code}: {message}").expect("parse");
950        let code = 418;
951        let message = "I'm a teapot";
952
953        let rendered = format!(
954            "{}",
955            template.display_with(|placeholder, f| match placeholder.identifier() {
956                TemplateIdentifier::Named("code") => write!(f, "{}", code),
957                TemplateIdentifier::Named("message") => f.write_str(message),
958                other => panic!("unexpected placeholder: {:?}", other)
959            })
960        );
961
962        assert_eq!(rendered, "418: I'm a teapot");
963    }
964}