masterror_template/
template.rs

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