shep_core/config/
template.rs1use core::fmt;
23
24const TOKENS: &[&str] = &["instance", "name"];
26
27#[non_exhaustive]
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub(crate) enum TemplateError {
36 UnknownToken {
38 token: String,
40 },
41 Unclosed,
43}
44
45impl fmt::Display for TemplateError {
46 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47 match self {
48 Self::UnknownToken { token } => write!(
49 f,
50 "`{{{{{token}}}}}` is not a template token: valid tokens are {}",
51 TOKENS
52 .iter()
53 .map(|t| format!("`{{{{{t}}}}}`"))
54 .collect::<Vec<_>>()
55 .join(" and ")
56 ),
57 Self::Unclosed => f.write_str("a `{{` in this value is never closed by a `}}`"),
58 }
59 }
60}
61
62impl core::error::Error for TemplateError {}
63
64enum Segment<'a> {
67 Literal(&'a str),
69 Token(&'a str),
71}
72
73fn walk(
78 value: &str,
79 mut on_segment: impl FnMut(Segment<'_>) -> Result<(), TemplateError>,
80) -> Result<(), TemplateError> {
81 let bytes = value.as_bytes();
82 let mut at = 0;
83 let mut literal_from = 0;
84 while at < bytes.len() {
85 if bytes[at..].starts_with(b"{{{{") {
86 on_segment(Segment::Literal(&value[literal_from..at]))?;
87 on_segment(Segment::Literal("{{"))?;
88 at += 4;
89 literal_from = at;
90 } else if bytes[at..].starts_with(b"}}}}") {
91 on_segment(Segment::Literal(&value[literal_from..at]))?;
92 on_segment(Segment::Literal("}}"))?;
93 at += 4;
94 literal_from = at;
95 } else if bytes[at..].starts_with(b"{{") {
96 on_segment(Segment::Literal(&value[literal_from..at]))?;
97 let rest = &value[at + 2..];
98 let Some(end) = rest.find("}}") else {
99 return Err(TemplateError::Unclosed);
100 };
101 on_segment(Segment::Token(&rest[..end]))?;
102 at += 2 + end + 2;
103 literal_from = at;
104 } else {
105 at += 1;
106 }
107 }
108 on_segment(Segment::Literal(&value[literal_from..]))?;
109 Ok(())
110}
111
112pub(crate) fn validate(value: &str) -> Result<(), TemplateError> {
124 walk(value, |segment| match segment {
125 Segment::Literal(_) => Ok(()),
126 Segment::Token(token) if TOKENS.contains(&token) => Ok(()),
127 Segment::Token(token) => Err(TemplateError::UnknownToken {
128 token: token.to_string(),
129 }),
130 })
131}
132
133#[must_use]
142pub fn render(value: &str, name: &str, instance: u32) -> String {
143 let mut out = String::with_capacity(value.len());
144 let slot = instance.to_string();
145 let _ = walk(value, |segment| {
146 match segment {
147 Segment::Literal(literal) => out.push_str(literal),
148 Segment::Token("instance") => out.push_str(&slot),
149 Segment::Token("name") => out.push_str(name),
150 Segment::Token(_) => {}
151 }
152 Ok(())
153 });
154 out
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn the_two_tokens_render() {
163 assert_eq!(render("z-{{instance}}", "worker", 3), "z-3");
164 assert_eq!(render("{{name}}-{{instance}}d", "worker", 3), "worker-3d");
165 assert_eq!(render("91{{instance}}", "worker", 7), "917");
166 }
167
168 #[test]
169 fn a_value_with_no_token_is_returned_unchanged() {
170 for value in [
173 r#"{"ts":"%t","level":"%l"}"#,
174 r#"{"a":{"b":1}}"#,
175 "^[a-z]{2,3}$",
176 "plain",
177 ] {
178 assert_eq!(render(value, "worker", 1), value, "unchanged: {value}");
179 assert!(validate(value).is_ok(), "and accepted: {value}");
180 }
181 }
182
183 #[test]
184 fn an_unknown_token_is_refused_by_name() {
185 let err = validate("z-{{instnace}}").unwrap_err();
186 assert!(matches!(&err, TemplateError::UnknownToken { token } if token == "instnace"));
187 let rendered = err.to_string();
188 assert!(rendered.contains("instnace"), "names the typo: {rendered}");
189 assert!(
190 rendered.contains("instance"),
191 "and what is valid: {rendered}"
192 );
193 assert!(
194 !rendered.contains('\u{2014}') && !rendered.contains('\u{2013}'),
195 "no em or en dash in copy a user reads: {rendered}"
196 );
197 }
198
199 #[test]
200 fn doubling_escapes_a_literal_token() {
201 assert_eq!(render("{{{{instance}}}}", "worker", 3), "{{instance}}");
202 assert!(validate("{{{{ .Values.port }}}}").is_ok());
203 assert_eq!(
204 render("{{{{ .Values.port }}}}", "worker", 3),
205 "{{ .Values.port }}",
206 "a Helm template passes through for the tool that consumes it"
207 );
208 }
209
210 #[test]
211 fn an_unclosed_token_is_refused() {
212 assert!(validate("z-{{instance").is_err());
213 }
214}