Skip to main content

fv_template/
lib.rs

1/*!
2Compile-time support for interpolated string templates using field-value expressions.
3
4# Field-value templates
5
6A field-value template is a string literal surrounded by field-value expressions:
7
8```text
9   a, b: 42, "Some text {c} and {d: true}", e, f: "text"
10   ───┬────  ───────────────┬─────────────  ──────┬─────
11before literal           literal             after literal
12```
13
14The template string literal consists of blocks of text with holes between braces, where
15the value in a hole is a field-value expression:
16
17```text
18"Some text {c} and {d: true}"
19           ─┬─     ────┬────
20            └────┬─────┘
21                hole
22
23"Some text {c} and {d: true}"
24 ─────┬────   ──┬──
25      └────┬────┘
26         text
27```
28
29The syntax is similar to Rust's `format_args!` macro, but leans entirely on standard field-value
30expressions for specifying values to interpolate.
31
32# Why not `format_args!`?
33
34Rust's `format_args!` macro already defines a syntax for string interpolation, but isn't suitable
35for all situations:
36
37- It's core purpose is to build strings. `format_args!` is based on machinery that throws away
38type-specific information eagerly. It also performs optimizations at compile time that inline
39certain values into the builder.
40- It doesn't have a programmatic API. You can only make assumptions about how a `format_args!`
41invocation will behave by observing the syntactic tokens passed to it at compile-time. You don't get any
42visibility into the format literal itself.
43- Flags are compact for formatting, but don't scale. The `:?#<>` tokens used for customizing formatting
44are compact, but opaque, and don't naturally allow for arbitrarily complex annotation like attributes do.
45
46When any of those trade-offs in `format_args!` becomes a problem, field-value templates may be a solution.
47*/
48
49#[cfg(test)]
50#[macro_use]
51extern crate quote;
52
53use std::fmt::Formatter;
54use std::{
55    borrow::Cow,
56    fmt,
57    iter::Peekable,
58    ops::Range,
59    str::{self, CharIndices},
60};
61
62use proc_macro2::{token_stream, Literal, Span, TokenStream, TokenTree};
63use quote::ToTokens;
64use syn::{spanned::Spanned, FieldValue};
65
66/**
67A field-value template.
68 */
69pub struct Template {
70    before_template: Vec<FieldValue>,
71    literal: Vec<LiteralPart>,
72    after_template: Vec<FieldValue>,
73}
74
75/**
76A text fragment in a template.
77*/
78pub struct Text<'a> {
79    text: &'a str,
80    needs_escaping: bool,
81}
82
83impl<'a> Text<'a> {
84    /**
85    Get the value of the text fragment.
86
87    Any `{{` or `}}` escape sequences will be replaced by raw `{` and `}`.
88    The [`Text::needs_escaping`] method will return `true` if any escape sequences were present.
89    */
90    pub fn get(&self) -> &str {
91        &self.text
92    }
93
94    /**
95    Whether the original text fragment contained `{{` or `}}` escape sequences.
96
97    If this method returns `true`, then any `{` and `}` in the value of [`Text::get`] will need to
98    be re-escaped to roundtrip the fragment.
99    */
100    pub fn needs_escaping(&self) -> bool {
101        self.needs_escaping
102    }
103}
104
105/**
106A hole fragment in a template.
107*/
108pub struct Hole<'a> {
109    expr: &'a FieldValue,
110}
111
112impl<'a> Hole<'a> {
113    pub fn get(&self) -> &FieldValue {
114        self.expr
115    }
116}
117
118/**
119A visitor for the parts of a template string.
120 */
121pub trait LiteralVisitor {
122    /**
123    Visit a text part in a template literal.
124     */
125    fn visit_text(&mut self, text: Text);
126
127    /**
128    Visit a hole part in a template literal.
129     */
130    fn visit_hole(&mut self, hole: Hole);
131}
132
133impl<'a, V: ?Sized> LiteralVisitor for &'a mut V
134where
135    V: LiteralVisitor,
136{
137    fn visit_text(&mut self, text: Text) {
138        (**self).visit_text(text)
139    }
140
141    fn visit_hole(&mut self, hole: Hole) {
142        (**self).visit_hole(hole)
143    }
144}
145
146impl Template {
147    /**
148    Parse a template from a `TokenStream`.
149
150    The `TokenStream` is typically all the tokens given to a macro.
151     */
152    pub fn parse2(input: TokenStream) -> Result<Self, Error> {
153        let mut scan = ScanTemplate::new(input);
154
155        // Take any arguments up to the string template
156        // These are control arguments for the log statement that aren't key-value pairs
157        let mut parsing_value = false;
158        let (before_template, template) = scan.take_until(|tt| {
159            // If we're parsing a value then skip over this token
160            // It won't be interpreted as the template because it belongs to an arg
161            if parsing_value {
162                parsing_value = false;
163                return false;
164            }
165
166            match tt {
167                // A literal is interpreted as the template
168                TokenTree::Literal(_) => true,
169                // A `:` token marks the start of a value in a field-value
170                // The following token is the value, which isn't considered the template
171                TokenTree::Punct(p) if p.as_char() == ':' => {
172                    parsing_value = true;
173                    false
174                }
175                // Any other token isn't the template
176                _ => false,
177            }
178        });
179
180        // If there's more tokens, they should be a comma followed by comma-separated field-values
181        let after_template = if scan.has_input() {
182            scan.expect_punct(',')?;
183            scan.rest.collect()
184        } else {
185            TokenStream::new()
186        };
187
188        // Parse the template literal into its text fragments and field-value holes
189        let literal = if let Some(template) = template {
190            LiteralPart::parse_lit2(ScanTemplate::take_literal(template)?)?
191        } else {
192            Vec::new()
193        };
194
195        let before_template = ScanTemplate::new(before_template).collect_field_values()?;
196        let after_template = ScanTemplate::new(after_template).collect_field_values()?;
197
198        Ok(Template {
199            before_template,
200            literal,
201            after_template,
202        })
203    }
204
205    /**
206    Field-values that appear before the template string literal.
207     */
208    pub fn before_literal_field_values<'a>(&'a self) -> impl Iterator<Item = &'a FieldValue> {
209        self.before_template.iter()
210    }
211
212    /**
213    Field-values that appear within the template string literal.
214
215    This is a simple alternative to [`Template::visit_literal`] that iterates over the field-value holes.
216     */
217    pub fn literal_field_values<'a>(&'a self) -> impl Iterator<Item = &'a FieldValue> {
218        self.literal.iter().filter_map(|part| {
219            if let LiteralPart::Hole { expr, .. } = part {
220                Some(expr)
221            } else {
222                None
223            }
224        })
225    }
226
227    /**
228    Whether the template contains a literal.
229    */
230    pub fn has_literal(&self) -> bool {
231        !self.literal.is_empty()
232    }
233
234    /**
235    Field-values that appear after the template string literal.
236     */
237    pub fn after_literal_field_values<'a>(&'a self) -> impl Iterator<Item = &'a FieldValue> {
238        self.after_template.iter()
239    }
240
241    /**
242    Visit the parts of the string literal part of the template.
243
244    Each fragment of text and field-value hole will be visited in sequence.
245
246    Given a template string like:
247
248    ```text
249    Some text and a {hole} and some {more}.
250    ```
251
252    the visitor will be called with the following inputs:
253
254    1. `visit_text("Some text and a ")`
255    2. `visit_hole("hole")`
256    3. `visit_text(" and some ")`
257    4. `visit_hole("more")`
258    5. `visit_text(".")`
259
260    If the template doesn't contain a literal then the visitor won't be called.
261     */
262    pub fn visit_literal(&self, mut visitor: impl LiteralVisitor) {
263        for part in &self.literal {
264            match part {
265                LiteralPart::Text {
266                    text,
267                    needs_escaping,
268                    ..
269                } => visitor.visit_text(Text {
270                    text,
271                    needs_escaping: *needs_escaping,
272                }),
273                LiteralPart::Hole { expr, .. } => visitor.visit_hole(Hole { expr }),
274            }
275        }
276    }
277}
278
279/**
280A part of a parsed template string literal.
281 */
282enum LiteralPart {
283    /**
284    A fragment of text.
285     */
286    Text {
287        /**
288        The literal text content.
289        */
290        text: String,
291        /**
292        Whether the text contains `{` or `}` characters that would need to be escaped to roundtrip.
293        */
294        needs_escaping: bool,
295        /**
296        The range within the template string that covers this part.
297        */
298        range: Range<usize>,
299    },
300    /**
301    A replacement expression.
302     */
303    Hole {
304        /**
305        The expression within the hole.
306        */
307        expr: FieldValue,
308        /**
309        The range within the template string that covers this part.
310        */
311        range: Range<usize>,
312    },
313}
314
315impl fmt::Debug for LiteralPart {
316    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
317        match self {
318            LiteralPart::Text {
319                text,
320                needs_escaping,
321                range,
322            } => f
323                .debug_struct("Text")
324                .field("text", text)
325                .field("needs_escaping", needs_escaping)
326                .field("range", range)
327                .finish(),
328            LiteralPart::Hole { expr, range } => f
329                .debug_struct("Hole")
330                .field("expr", &format_args!("`{}`", expr.to_token_stream()))
331                .field("range", range)
332                .finish(),
333        }
334    }
335}
336
337impl LiteralPart {
338    fn parse_lit2(lit: Literal) -> Result<Vec<Self>, Error> {
339        enum Expecting {
340            TextOrEOF,
341            Hole,
342        }
343
344        let input = lit.to_string();
345
346        let mut parts = Vec::new();
347        let mut expecting = Expecting::TextOrEOF;
348
349        let mut scan = ScanPart::new(lit, &input)?;
350
351        // If the template is empty then return a single text part
352        // This distinguishes an empty template from a missing template
353        if !scan.has_input() {
354            return Ok(vec![LiteralPart::Text {
355                text: String::new(),
356                needs_escaping: false,
357                range: 0..0,
358            }]);
359        }
360
361        while scan.has_input() {
362            match expecting {
363                Expecting::TextOrEOF => {
364                    if let Some((text, needs_escaping, range)) =
365                        scan.take_until_eof_or_hole_start()?
366                    {
367                        parts.push(LiteralPart::Text {
368                            text: text.into_owned(),
369                            needs_escaping,
370                            range,
371                        });
372                    }
373
374                    expecting = Expecting::Hole;
375                    continue;
376                }
377                Expecting::Hole => {
378                    let (expr, range) = scan.take_until_hole_end()?;
379
380                    let expr_span = scan.lit.subspan(range.start..range.end);
381
382                    let tokens = {
383                        let tokens: TokenStream = str::parse(&*expr).map_err(|e| {
384                            Error::lex_fv_expr(expr_span.unwrap_or(scan.lit.span()), &*expr, e)
385                        })?;
386
387                        // Attempt to shrink the span of the parsed expression to just the
388                        // fragment of the literal it was parsed from
389                        if let Some(span) = scan.lit.subspan(range.start..range.end) {
390                            tokens
391                                .into_iter()
392                                .map(|mut tt| {
393                                    tt.set_span(span);
394                                    tt
395                                })
396                                .collect()
397                        } else {
398                            tokens
399                        }
400                    };
401
402                    let expr = syn::parse2(tokens).map_err(|e| {
403                        Error::parse_fv_expr(expr_span.unwrap_or(scan.lit.span()), &*expr, e)
404                    })?;
405
406                    parts.push(LiteralPart::Hole { expr, range });
407
408                    expecting = Expecting::TextOrEOF;
409                    continue;
410                }
411            }
412        }
413
414        Ok(parts)
415    }
416}
417
418struct ScanTemplate {
419    span: Span,
420    rest: Peekable<token_stream::IntoIter>,
421}
422
423impl ScanTemplate {
424    fn new(input: TokenStream) -> Self {
425        ScanTemplate {
426            span: input.span(),
427            rest: input.into_iter().peekable(),
428        }
429    }
430
431    fn has_input(&mut self) -> bool {
432        self.rest.peek().is_some()
433    }
434
435    fn take_until(
436        &mut self,
437        mut until_true: impl FnMut(&TokenTree) -> bool,
438    ) -> (TokenStream, Option<TokenTree>) {
439        let mut taken = TokenStream::new();
440
441        while let Some(tt) = self.rest.next() {
442            if until_true(&tt) {
443                return (taken, Some(tt));
444            }
445
446            taken.extend(Some(tt));
447        }
448
449        (taken, None)
450    }
451
452    fn is_punct(input: &TokenTree, c: char) -> bool {
453        match input {
454            TokenTree::Punct(p) if p.as_char() == c => true,
455            _ => false,
456        }
457    }
458
459    fn expect_punct(&mut self, c: char) -> Result<TokenTree, Error> {
460        match self.rest.next() {
461            Some(tt) => {
462                if Self::is_punct(&tt, c) {
463                    Ok(tt)
464                } else {
465                    Err(Error::invalid_char(tt.span(), &[c]))
466                }
467            }
468            None => Err(Error::invalid_char_eof(self.span, &[c])),
469        }
470    }
471
472    fn take_literal(tt: TokenTree) -> Result<Literal, Error> {
473        match tt {
474            TokenTree::Literal(l) => Ok(l),
475            _ => Err(Error::invalid_literal(tt.span())),
476        }
477    }
478
479    fn collect_field_values(mut self) -> Result<Vec<FieldValue>, Error> {
480        let mut result = Vec::new();
481
482        while self.has_input() {
483            let (arg, _) = self.take_until(|tt| Self::is_punct(&tt, ','));
484
485            if !arg.is_empty() {
486                let expr_span = arg.span();
487
488                result.push(syn::parse2::<FieldValue>(arg).map_err(|e| {
489                    Error::parse_fv_expr(expr_span, expr_span.source_text().as_deref(), e)
490                })?);
491            }
492        }
493
494        Ok(result)
495    }
496}
497
498struct ScanPart<'input> {
499    lit: Literal,
500    input: &'input str,
501    start: usize,
502    end: usize,
503    rest: Peekable<CharIndices<'input>>,
504}
505
506struct TakeUntil<'a, 'input> {
507    current: char,
508    current_idx: usize,
509    rest: &'a mut Peekable<CharIndices<'input>>,
510    lit: &'a Literal,
511}
512
513impl<'input> ScanPart<'input> {
514    fn new(lit: Literal, input: &'input str) -> Result<Self, Error> {
515        let mut iter = input.char_indices();
516        let start = iter.next();
517        let end = iter.next_back();
518
519        // This just checks that we're looking at a string
520        // It doesn't bother with ensuring that last quote is unescaped
521        // because the input to this is expected to be a proc-macro literal
522        if start.map(|(_, c)| c) != Some('"') || end.map(|(_, c)| c) != Some('"') {
523            return Err(Error::invalid_literal(lit.span()));
524        }
525
526        Ok(ScanPart {
527            lit,
528            input: &input,
529            start: 1,
530            end: input.len() - 1,
531            rest: iter.peekable(),
532        })
533    }
534
535    fn has_input(&mut self) -> bool {
536        self.rest.peek().is_some()
537    }
538
539    fn take_until(
540        &mut self,
541        mut until_true: impl FnMut(TakeUntil<'_, 'input>) -> Result<bool, Error>,
542    ) -> Result<Option<(Cow<'input, str>, Range<usize>)>, Error> {
543        let mut scan = || {
544            while let Some((i, c)) = self.rest.next() {
545                if until_true(TakeUntil {
546                    current: c,
547                    current_idx: i,
548                    rest: &mut self.rest,
549                    lit: &self.lit,
550                })? {
551                    let start = self.start;
552                    let end = i;
553
554                    self.start = end + 1;
555
556                    let range = start..end;
557
558                    return Ok((Cow::Borrowed(&self.input[range.clone()]), range));
559                }
560            }
561
562            let range = self.start..self.end;
563
564            Ok((Cow::Borrowed(&self.input[range.clone()]), range))
565        };
566
567        match scan()? {
568            (s, r) if s.len() > 0 => Ok(Some((s, r))),
569            _ => Ok(None),
570        }
571    }
572
573    fn take_until_eof_or_hole_start(
574        &mut self,
575    ) -> Result<Option<(Cow<'input, str>, bool, Range<usize>)>, Error> {
576        let mut escaped = false;
577        let scanned = self.take_until(|state| match state.current {
578            // A `{` that's followed by another `{` is escaped
579            // If it's followed by a different character then it's
580            // the start of an interpolated expression
581            '{' => {
582                let start = state.current_idx;
583
584                match state.rest.peek().map(|(_, peeked)| *peeked) {
585                    Some('{') => {
586                        escaped = true;
587                        let _ = state.rest.next();
588                        Ok(false)
589                    }
590                    Some(_) => Ok(true),
591                    None => Err(Error::incomplete_hole(
592                        state
593                            .lit
594                            .subspan(start..start + 1)
595                            .unwrap_or(state.lit.span()),
596                    )),
597                }
598            }
599            // A `}` that's followed by another `}` is escaped
600            // We should never see these in this parser unless they're escaped
601            // If we do it means an interpolated expression is missing its start
602            // or it's been improperly escaped
603            '}' => match state.rest.peek().map(|(_, peeked)| *peeked) {
604                Some('}') => {
605                    escaped = true;
606                    let _ = state.rest.next();
607                    Ok(false)
608                }
609                Some(_) => Err(Error::unescaped_hole(
610                    state
611                        .lit
612                        .subspan(state.current_idx..state.current_idx + 1)
613                        .unwrap_or(state.lit.span()),
614                )),
615                None => Err(Error::unescaped_hole(
616                    state
617                        .lit
618                        .subspan(state.current_idx..state.current_idx + 1)
619                        .unwrap_or(state.lit.span()),
620                )),
621            },
622            _ => Ok(false),
623        })?;
624
625        match scanned {
626            Some((input, range)) if escaped => {
627                // If the input is escaped, then replace `{{` and `}}` chars
628                let input = (&*input).replace("{{", "{").replace("}}", "}");
629                Ok(Some((Cow::Owned(input), true, range)))
630            }
631            Some((input, range)) => Ok(Some((input, false, range))),
632            None => Ok(None),
633        }
634    }
635
636    fn take_until_hole_end(&mut self) -> Result<(Cow<'input, str>, Range<usize>), Error> {
637        let mut depth = 1;
638        let mut matched_hole_end = false;
639        let mut escaped = false;
640        let mut next_terminator_escaped = false;
641        let mut terminator = None;
642
643        // NOTE: The starting point is the first char _after_ the opening `{`
644        // so to get a correct span here we subtract 1 from it to cover that character
645        let start = self.start - 1;
646
647        let scanned = self.take_until(|state| {
648            match state.current {
649                // If the depth would return to its start then we've got a full expression
650                '}' if terminator.is_none() && depth == 1 => {
651                    matched_hole_end = true;
652                    Ok(true)
653                }
654                // A block end will reduce the depth
655                '}' if terminator.is_none() => {
656                    depth -= 1;
657                    Ok(false)
658                }
659                // A block start will increase the depth
660                '{' if terminator.is_none() => {
661                    depth += 1;
662                    Ok(false)
663                }
664                // A double quote may be the start or end of a string
665                // It may also be escaped
666                '"' if terminator.is_none() => {
667                    terminator = Some('"');
668                    Ok(false)
669                }
670                // A single quote may be the start or end of a character
671                // It may also be escaped
672                '\'' if terminator.is_none() => {
673                    terminator = Some('\'');
674                    Ok(false)
675                }
676                // A `\` means there's embedded escaped characters
677                // These may be escapes the user needs to represent a `"`
678                // or they may be intended to appear in the final string
679                '\\' if state
680                    .rest
681                    .peek()
682                    .map(|(_, peeked)| *peeked == '\\')
683                    .unwrap_or(false) =>
684                {
685                    next_terminator_escaped = !next_terminator_escaped;
686                    escaped = true;
687                    Ok(false)
688                }
689                '\\' => {
690                    escaped = true;
691                    Ok(false)
692                }
693                // The sequence `//` or `/*` means the expression contains a comment
694                // These aren't supported so bail with an error
695                '/' if state
696                    .rest
697                    .peek()
698                    .map(|(_, peeked)| *peeked == '/' || *peeked == '*')
699                    .unwrap_or(false) =>
700                {
701                    Err(Error::unsupported_comment(
702                        state
703                            .lit
704                            .subspan(state.current_idx..state.current_idx + 1)
705                            .unwrap_or(state.lit.span()),
706                    ))
707                }
708                // If the current character is a terminator and it's not escaped
709                // then break out of the current string or character
710                c if Some(c) == terminator && !next_terminator_escaped => {
711                    terminator = None;
712                    Ok(false)
713                }
714                // If the current character is anything else then discard escaping
715                // for the next character
716                _ => {
717                    next_terminator_escaped = false;
718                    Ok(false)
719                }
720            }
721        })?;
722
723        if !matched_hole_end {
724            Err(Error::incomplete_hole(
725                self.lit
726                    .subspan(start..self.start)
727                    .unwrap_or(self.lit.span()),
728            ))?;
729        }
730
731        match scanned {
732            Some((input, range)) if escaped => {
733                // If the input is escaped then replace `\"` with `"`
734                let input = (&*input).replace("\\\"", "\"");
735                Ok((Cow::Owned(input), range))
736            }
737            Some((input, range)) => Ok((input, range)),
738            None => Err(Error::missing_expr(
739                self.lit
740                    .subspan(start..self.start)
741                    .unwrap_or(self.lit.span()),
742            ))?,
743        }
744    }
745}
746
747/**
748An error encountered while parsing a template.
749 */
750#[derive(Debug)]
751pub struct Error {
752    reason: String,
753    source: Option<Box<dyn std::error::Error>>,
754    span: Span,
755}
756
757impl std::error::Error for Error {
758    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
759        self.source.as_deref()
760    }
761}
762
763impl fmt::Display for Error {
764    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
765        write!(f, "parsing failed: {}", self.reason)
766    }
767}
768
769impl Error {
770    pub fn span(&self) -> Span {
771        self.span
772    }
773
774    fn incomplete_hole(span: Span) -> Self {
775        Error {
776            reason: format!("unexpected end of input, expected `}}`"),
777            source: None,
778            span,
779        }
780    }
781
782    fn unescaped_hole(span: Span) -> Self {
783        Error {
784            reason: format!("`{{` and `}}` characters must be escaped as `{{{{` and `}}}}`"),
785            source: None,
786            span,
787        }
788    }
789
790    fn missing_expr(span: Span) -> Self {
791        Error {
792            reason: format!("empty replacements (`{{}}`) aren't supported, put the replacement inside like `{{some_value}}`"),
793            source: None,
794            span,
795        }
796    }
797
798    fn lex_fv_expr(span: Span, expr: &str, err: proc_macro2::LexError) -> Self {
799        Error {
800            reason: format!("failed to parse `{}` as a field-value expression", expr),
801            span,
802            source: Some(format!("{:?}", err).into()),
803        }
804    }
805
806    fn parse_fv_expr<'a>(span: Span, expr: impl Into<Option<&'a str>>, err: syn::Error) -> Self {
807        Error {
808            reason: if let Some(expr) = expr.into() {
809                format!("failed to parse `{}` as a field-value expression", expr)
810            } else {
811                format!("failed to parse field-value expression")
812            },
813            span,
814            source: Some(err.into()),
815        }
816    }
817
818    fn invalid_literal(span: Span) -> Self {
819        Error {
820            reason: format!("templates must be parsed from string literals"),
821            source: None,
822            span,
823        }
824    }
825
826    fn invalid_char(span: Span, expected: &[char]) -> Self {
827        Error {
828            reason: format!(
829                "invalid character, expected: {}",
830                Error::display_list(expected)
831            ),
832            source: None,
833            span,
834        }
835    }
836
837    fn invalid_char_eof(span: Span, expected: &[char]) -> Self {
838        Error {
839            reason: format!(
840                "unexpected end-of-input, expected: {}",
841                Error::display_list(expected)
842            ),
843            source: None,
844            span,
845        }
846    }
847
848    fn unsupported_comment(span: Span) -> Self {
849        Error {
850            reason: format!("comments within expressions are not supported"),
851            source: None,
852            span,
853        }
854    }
855
856    fn display_list<'a>(l: &'a [impl fmt::Display]) -> impl fmt::Display + 'a {
857        struct DisplayList<'a, T>(&'a [T]);
858
859        impl<'a, T: fmt::Display> fmt::Display for DisplayList<'a, T> {
860            fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
861                match self.0.len() {
862                    1 => write!(f, "`{}`", self.0[0]),
863                    _ => {
864                        let mut first = true;
865
866                        for item in self.0 {
867                            if !first {
868                                write!(f, ", ")?;
869                            }
870                            first = false;
871
872                            write!(f, "`{}`", item)?;
873                        }
874
875                        Ok(())
876                    }
877                }
878            }
879        }
880
881        DisplayList(l)
882    }
883}
884
885#[cfg(test)]
886mod tests {
887    use super::*;
888    use syn::Member;
889
890    #[test]
891    fn parse_ok() {
892        let cases = vec![
893            (quote!(), None::<&str>),
894            (quote!(""), Some("")),
895            (quote!("template"), Some("template")),
896            (quote!(a: 42, "template"), Some("template")),
897            (quote!("template", a: 42), Some("template")),
898            (quote!(a: 42, "template", b: 42), Some("template")),
899        ];
900
901        for (case, expected) in cases {
902            let tpl = Template::parse2(case).unwrap();
903
904            if let Some(expected) = expected {
905                let Some(LiteralPart::Text { text, .. }) = tpl.literal.get(0) else {
906                    panic!(
907                        "unexpected template {:?} (expected {:?})",
908                        tpl.literal, expected
909                    );
910                };
911
912                assert_eq!(expected, text);
913            } else {
914                assert_eq!(0, tpl.literal.len(), "expected an empty template");
915            }
916        }
917    }
918
919    #[test]
920    fn parse_err() {
921        let cases = vec![
922            (
923                quote!(42),
924                "parsing failed: templates must be parsed from string literals",
925            ),
926            (
927                quote!(a: 42, true),
928                "parsing failed: failed to parse field-value expression",
929            ),
930            (
931                quote!(fn x() {}, "template"),
932                "parsing failed: failed to parse field-value expression",
933            ),
934            (
935                quote!("template", fn x() {}),
936                "parsing failed: failed to parse field-value expression",
937            ),
938        ];
939
940        for (input, expected) in cases {
941            let actual = match Template::parse2(input.clone()) {
942                Err(e) => e,
943                Ok(_) => panic!("parsing {} should've failed but produced a value", input),
944            };
945
946            assert_eq!(expected, actual.to_string(),);
947        }
948    }
949
950    #[test]
951    fn template_parse_ok() {
952        let cases = vec![
953            ("", vec![text("", false, 0..0)]),
954            (
955                "Hello world 🎈📌",
956                vec![text("Hello world 🎈📌", false, 1..21)],
957            ),
958            (
959                "Hello {world} 🎈📌",
960                vec![
961                    text("Hello ", false, 1..7),
962                    hole("world", 8..13),
963                    text(" 🎈📌", false, 14..23),
964                ],
965            ),
966            ("{world}", vec![hole("world", 2..7)]),
967            (
968                "Hello {#[log::debug] world} 🎈📌",
969                vec![
970                    text("Hello ", false, 1..7),
971                    hole("#[log::debug] world", 8..27),
972                    text(" 🎈📌", false, 28..37),
973                ],
974            ),
975            (
976                "Hello {#[log::debug] world: 42} 🎈📌",
977                vec![
978                    text("Hello ", false, 1..7),
979                    hole("#[log::debug] world: 42", 8..31),
980                    text(" 🎈📌", false, 32..41),
981                ],
982            ),
983            (
984                "Hello {#[log::debug] world: \"is text\"} 🎈📌",
985                vec![
986                    text("Hello ", false, 1..7),
987                    hole("#[log::debug] world: \"is text\"", 8..40),
988                    text(" 🎈📌", false, 41..50),
989                ],
990            ),
991            (
992                "{Hello} {world}",
993                vec![
994                    hole("Hello", 2..7),
995                    text(" ", false, 8..9),
996                    hole("world", 10..15),
997                ],
998            ),
999            (
1000                "{a}{b}{c}",
1001                vec![hole("a", 2..3), hole("b", 5..6), hole("c", 8..9)],
1002            ),
1003            (
1004                "🎈📌{a}🎈📌{b}🎈📌{c}🎈📌",
1005                vec![
1006                    text("🎈📌", false, 1..9),
1007                    hole("a", 10..11),
1008                    text("🎈📌", false, 12..20),
1009                    hole("b", 21..22),
1010                    text("🎈📌", false, 23..31),
1011                    hole("c", 32..33),
1012                    text("🎈📌", false, 34..42),
1013                ],
1014            ),
1015            (
1016                "Hello 🎈📌 {{world}}",
1017                vec![text("Hello 🎈📌 {world}", true, 1..25)],
1018            ),
1019            (
1020                "🎈📌 Hello world {{}}",
1021                vec![text("🎈📌 Hello world {}", true, 1..26)],
1022            ),
1023            (
1024                "Hello {#[log::debug] world: \"{\"} 🎈📌",
1025                vec![
1026                    text("Hello ", false, 1..7),
1027                    hole("#[log::debug] world: \"{\"", 8..34),
1028                    text(" 🎈📌", false, 35..44),
1029                ],
1030            ),
1031            (
1032                "Hello {#[log::debug] world: '{'} 🎈📌",
1033                vec![
1034                    text("Hello ", false, 1..7),
1035                    hole("#[log::debug] world: '{'", 8..32),
1036                    text(" 🎈📌", false, 33..42),
1037                ],
1038            ),
1039            (
1040                "Hello {#[log::debug] world: \"is text with 'embedded' stuff\"} 🎈📌",
1041                vec![
1042                    text("Hello ", false, 1..7),
1043                    hole(
1044                        "#[log::debug] world: \"is text with 'embedded' stuff\"",
1045                        8..62,
1046                    ),
1047                    text(" 🎈📌", false, 63..72),
1048                ],
1049            ),
1050            ("{{", vec![text("{", true, 1..3)]),
1051            ("}}", vec![text("}", true, 1..3)]),
1052        ];
1053
1054        for (template, expected) in cases {
1055            let actual = match LiteralPart::parse_lit2(Literal::string(template)) {
1056                Ok(template) => template,
1057                Err(e) => panic!("failed to parse {:?}: {}", template, e),
1058            };
1059
1060            assert_eq!(
1061                format!("{:?}", expected),
1062                format!("{:?}", actual),
1063                "parsing template: {:?}",
1064                template
1065            );
1066        }
1067    }
1068
1069    #[test]
1070    fn template_parse_err() {
1071        let cases = vec![
1072            ("a {{}", "parsing failed: `{` and `}` characters must be escaped as `{{` and `}}`"),
1073            ("{", "parsing failed: unexpected end of input, expected `}`"),
1074            ("a {", "parsing failed: unexpected end of input, expected `}`"),
1075            ("a { a", "parsing failed: unexpected end of input, expected `}`"),
1076            ("{ a", "parsing failed: unexpected end of input, expected `}`"),
1077            ("}", "parsing failed: `{` and `}` characters must be escaped as `{{` and `}}`"),
1078            ("} a", "parsing failed: `{` and `}` characters must be escaped as `{{` and `}}`"),
1079            ("a } a", "parsing failed: `{` and `}` characters must be escaped as `{{` and `}}`"),
1080            ("a }", "parsing failed: `{` and `}` characters must be escaped as `{{` and `}}`"),
1081            ("{}", "parsing failed: empty replacements (`{}`) aren\'t supported, put the replacement inside like `{some_value}`"),
1082            ("{not real rust}", "parsing failed: failed to parse `not real rust` as a field-value expression"),
1083            ("{// a comment!}", "parsing failed: comments within expressions are not supported"),
1084            ("{/* a comment! */}", "parsing failed: comments within expressions are not supported"),
1085        ];
1086
1087        for (template, expected) in cases {
1088            let actual = match LiteralPart::parse_lit2(Literal::string(template)) {
1089                Err(e) => e,
1090                Ok(actual) => panic!(
1091                    "parsing {:?} should've failed but produced {:?}",
1092                    template, actual
1093                ),
1094            };
1095
1096            assert_eq!(
1097                expected,
1098                actual.to_string(),
1099                "parsing template: {:?}",
1100                template
1101            );
1102        }
1103    }
1104
1105    fn text(text: &str, needs_escaping: bool, range: Range<usize>) -> LiteralPart {
1106        LiteralPart::Text {
1107            text: text.to_owned(),
1108            needs_escaping,
1109            range,
1110        }
1111    }
1112
1113    fn hole(expr: &str, range: Range<usize>) -> LiteralPart {
1114        LiteralPart::Hole {
1115            expr: syn::parse_str(expr)
1116                .unwrap_or_else(|e| panic!("failed to parse {:?} ({})", expr, e)),
1117            range,
1118        }
1119    }
1120
1121    #[test]
1122    fn visit_literal() {
1123        fn to_rt_tokens(template: &Template, base: TokenStream) -> TokenStream {
1124            struct DefaultVisitor {
1125                base: TokenStream,
1126                parts: Vec<TokenStream>,
1127            }
1128
1129            impl LiteralVisitor for DefaultVisitor {
1130                fn visit_text(&mut self, text: Text) {
1131                    let base = &self.base;
1132                    let text = text.get();
1133
1134                    self.parts.push(quote!(#base::Part::Text(#text)));
1135                }
1136
1137                fn visit_hole(&mut self, hole: Hole) {
1138                    let hole = match hole.get().member {
1139                        Member::Named(ref member) => member.to_string(),
1140                        Member::Unnamed(ref member) => member.index.to_string(),
1141                    };
1142
1143                    let base = &self.base;
1144
1145                    self.parts.push(quote!(#base::Part::Hole(#hole)));
1146                }
1147            }
1148
1149            let mut visitor = DefaultVisitor {
1150                base,
1151                parts: Vec::new(),
1152            };
1153            template.visit_literal(&mut visitor);
1154
1155            let base = &visitor.base;
1156            let parts = &visitor.parts;
1157
1158            quote!(
1159                #base::Template(&[#(#parts),*])
1160            )
1161        }
1162
1163        let cases = vec![(
1164            quote!("text and {label} and {more: 42}"),
1165            quote!(crate::rt::Template(&[
1166                crate::rt::Part::Text("text and "),
1167                crate::rt::Part::Hole("label"),
1168                crate::rt::Part::Text(" and "),
1169                crate::rt::Part::Hole("more")
1170            ])),
1171        )];
1172
1173        for (template, expected) in cases {
1174            let template = Template::parse2(template).unwrap();
1175
1176            assert!(template.has_literal());
1177
1178            assert_eq!(
1179                expected.to_string(),
1180                to_rt_tokens(&template, quote!(crate::rt)).to_string()
1181            );
1182        }
1183    }
1184
1185    #[test]
1186    fn visit_literal_empty() {
1187        struct DefaultVisitor {
1188            called: bool,
1189        }
1190
1191        impl LiteralVisitor for DefaultVisitor {
1192            fn visit_text(&mut self, _: Text) {
1193                self.called = true;
1194            }
1195
1196            fn visit_hole(&mut self, _: Hole) {
1197                unreachable!()
1198            }
1199        }
1200
1201        let mut visitor = DefaultVisitor { called: false };
1202
1203        let template = Template::parse2(quote!("")).unwrap();
1204
1205        template.visit_literal(&mut visitor);
1206
1207        assert!(template.has_literal());
1208        assert!(visitor.called);
1209    }
1210
1211    #[test]
1212    fn visit_literal_none() {
1213        struct DefaultVisitor {
1214            called: bool,
1215        }
1216
1217        impl LiteralVisitor for DefaultVisitor {
1218            fn visit_text(&mut self, _: Text) {
1219                unreachable!()
1220            }
1221
1222            fn visit_hole(&mut self, _: Hole) {
1223                unreachable!()
1224            }
1225        }
1226
1227        let mut visitor = DefaultVisitor { called: false };
1228
1229        let template = Template::parse2(quote!()).unwrap();
1230
1231        template.visit_literal(&mut visitor);
1232
1233        assert!(!template.has_literal());
1234        assert!(!visitor.called);
1235    }
1236}