Skip to main content

rskit_util/template/
mod.rs

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