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