rskit_util/template/
mod.rs1use std::fmt;
4
5mod dynamic;
6mod error;
7mod parser;
8mod renderer;
9
10pub use dynamic::DynamicTemplate;
11pub use error::TemplateError;
12
13pub trait Placeholder: Copy + Eq + fmt::Display {
15 fn token(self) -> &'static str;
17}
18
19#[derive(Debug, Clone, Eq, PartialEq)]
21pub enum TemplatePart<P> {
22 Literal(String),
24 Placeholder(P),
26}
27
28#[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 pub fn parse(value: &str, placeholders: &[P]) -> Result<Self, TemplateError> {
40 parser::parse_template(value, placeholders)
41 }
42
43 #[must_use]
45 pub fn parts(&self) -> &[TemplatePart<P>] {
46 &self.parts
47 }
48
49 #[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}