spin_expressions/
template.rs

1use std::fmt::Display;
2
3use crate::{Error, Result};
4
5/// Template represents a simple string template that allows expressions in
6/// double curly braces, similar to Mustache or Liquid.
7#[derive(Clone, Debug, PartialEq)]
8pub struct Template {
9    parts: Vec<Part>,
10}
11
12impl Template {
13    pub fn new(template: impl Into<Box<str>>) -> Result<Self> {
14        let mut parts = vec![];
15        let mut remainder: Box<str> = template.into();
16        while !remainder.is_empty() {
17            let (part, rest) = if let Some(expr_rest) = remainder.strip_prefix("{{") {
18                // Expression should be next
19                if let Some((expr, rest)) = expr_rest.split_once("}}") {
20                    // Take up through the next '}}'...
21                    (Part::expr(expr.trim()), rest)
22                } else {
23                    // ...or we have unmatched braces
24                    return Err(Error::InvalidTemplate(
25                        "unmatched '{{' in template".to_string(),
26                    ));
27                }
28            } else {
29                // Literal is next
30                if let Some(idx) = remainder.find("{{") {
31                    // Take up to the next '{{'...
32                    let (lit, rest) = remainder.split_at(idx);
33                    (Part::lit(lit), rest)
34                } else {
35                    // ...or end of string
36                    (Part::lit(remainder), "")
37                }
38            };
39            parts.push(part);
40            remainder = rest.into();
41        }
42        Ok(Template { parts })
43    }
44
45    pub fn is_literal(&self) -> bool {
46        self.parts.iter().all(|p| matches!(p, Part::Lit(_)))
47    }
48
49    pub(crate) fn parts(&self) -> std::slice::Iter<Part> {
50        self.parts.iter()
51    }
52}
53
54impl Display for Template {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        self.parts().try_for_each(|part| match part {
57            Part::Lit(lit) => f.write_str(lit),
58            Part::Expr(expr) => write!(f, "{{ {} }}", expr),
59        })
60    }
61}
62
63#[derive(Clone, Debug, PartialEq)]
64pub(crate) enum Part {
65    Lit(Box<str>),
66    Expr(Box<str>),
67}
68
69impl Part {
70    pub fn lit(lit: impl Into<Box<str>>) -> Self {
71        Self::Lit(lit.into())
72    }
73
74    pub fn expr(expr: impl Into<Box<str>>) -> Self {
75        Self::Expr(expr.into())
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn template_parts() {
85        for (tmpl, expected) in [
86            ("", vec![]),
87            ("a", vec![Part::lit("a")]),
88            (
89                "a-{{ expr }}-b",
90                vec![Part::lit("a-"), Part::expr("expr"), Part::lit("-b")],
91            ),
92            (
93                "{{ expr1 }}{{ expr2 }}",
94                vec![Part::expr("expr1"), Part::expr("expr2")],
95            ),
96        ] {
97            let template = Template::new(tmpl).unwrap();
98            assert!(
99                template.parts().eq(&expected),
100                "{:?} -> {:?} != {:?}",
101                tmpl,
102                template,
103                expected,
104            );
105        }
106    }
107
108    #[test]
109    fn template_parts_bad() {
110        Template::new("{{ matched }} {{ unmatched").unwrap_err();
111    }
112}