rskit_util/template/
typed.rs1use std::fmt;
2
3pub trait Placeholder: Copy + Eq + fmt::Display {
5 fn token(self) -> &'static str;
7}
8
9#[derive(Debug, Clone, Eq, PartialEq)]
11pub enum TemplatePart<P> {
12 Literal(String),
14 Placeholder(P),
16}
17
18#[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 pub fn parse(value: &str, placeholders: &[P]) -> Result<Self, super::TemplateError> {
30 super::parser::parse_template(value, placeholders)
31 }
32
33 #[must_use]
35 pub fn parts(&self) -> &[TemplatePart<P>] {
36 &self.parts
37 }
38
39 #[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}