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