Skip to main content

rskit_util/template/
typed.rs

1use std::fmt;
2
3/// A placeholder token that can be parsed from a template.
4pub trait Placeholder: Copy + Eq + fmt::Display {
5    /// Return the user-facing token name, without braces.
6    fn token(self) -> &'static str;
7}
8
9/// One parsed template part.
10#[derive(Debug, Clone, Eq, PartialEq)]
11pub enum TemplatePart<P> {
12    /// Literal text.
13    Literal(String),
14    /// Placeholder token.
15    Placeholder(P),
16}
17
18/// Parsed template string with typed placeholders.
19#[derive(Debug, Clone, Eq, PartialEq)]
20pub struct Template<P> {
21    pub(crate) parts: Vec<TemplatePart<P>>,
22}
23
24impl<P> Template<P>
25where
26    P: Placeholder,
27{
28    /// Parse a template string and reject unknown placeholders.
29    pub fn parse(value: &str, placeholders: &[P]) -> Result<Self, super::TemplateError> {
30        super::parser::parse_template(value, placeholders)
31    }
32
33    /// Return parsed template parts.
34    #[must_use]
35    pub fn parts(&self) -> &[TemplatePart<P>] {
36        &self.parts
37    }
38
39    /// Return true when the template contains the placeholder.
40    #[must_use]
41    pub fn contains(&self, placeholder: P) -> bool {
42        self.parts
43            .iter()
44            .any(|part| matches!(part, TemplatePart::Placeholder(found) if *found == placeholder))
45    }
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51    use crate::template::TemplateError;
52
53    #[derive(Debug, Clone, Copy, Eq, PartialEq)]
54    enum Token {
55        Name,
56        Args,
57    }
58
59    impl Placeholder for Token {
60        fn token(self) -> &'static str {
61            match self {
62                Self::Name => "name",
63                Self::Args => "args",
64            }
65        }
66    }
67
68    impl fmt::Display for Token {
69        fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
70            formatter.write_str(self.token())
71        }
72    }
73
74    const TOKENS: &[Token] = &[Token::Name, Token::Args];
75
76    #[test]
77    fn parses_known_placeholders() {
78        let template = Template::parse("cargo {name} {args}", TOKENS).expect("template parses");
79
80        assert!(template.contains(Token::Name));
81        assert!(template.contains(Token::Args));
82        assert_eq!(
83            template.parts(),
84            [
85                TemplatePart::Literal("cargo ".to_string()),
86                TemplatePart::Placeholder(Token::Name),
87                TemplatePart::Literal(" ".to_string()),
88                TemplatePart::Placeholder(Token::Args),
89            ]
90        );
91    }
92
93    #[test]
94    fn rejects_unknown_placeholders() {
95        let error = Template::parse("{project.root}", TOKENS).expect_err("unknown fails");
96        assert_eq!(
97            error,
98            TemplateError::UnknownPlaceholder("project.root".to_string())
99        );
100    }
101
102    #[test]
103    fn rejects_unclosed_placeholders() {
104        let error = Template::parse("cargo {name", TOKENS).expect_err("unclosed fails");
105        assert_eq!(
106            error,
107            TemplateError::UnclosedPlaceholder("cargo {name".to_string())
108        );
109    }
110
111    #[test]
112    fn rejects_empty_placeholders() {
113        let error = Template::parse("{}", TOKENS).expect_err("empty fails");
114        assert_eq!(error, TemplateError::EmptyPlaceholder);
115    }
116
117    #[test]
118    fn rejects_unmatched_closing_braces() {
119        let error =
120            Template::parse("cargo } {name}", TOKENS).expect_err("closing brace should fail");
121        assert_eq!(
122            error,
123            TemplateError::UnmatchedClosingBrace("cargo } {name}".to_string())
124        );
125    }
126
127    #[test]
128    fn renders_with_callback() {
129        let template = Template::parse("{name}:{args}", TOKENS).expect("template parses");
130
131        let rendered = template
132            .render_with(|placeholder| match placeholder {
133                Token::Name => Ok::<_, &'static str>("build".to_string()),
134                Token::Args => Ok::<_, &'static str>("--all".to_string()),
135            })
136            .expect("template renders");
137
138        assert_eq!(rendered, "build:--all");
139    }
140}