Skip to main content

shep_core/config/
template.rs

1//! The `{{instance}}` grammar for Flockfile values.
2//!
3//! Two tokens, `{{instance}}` and `{{name}}`, in env values, args, and the
4//! two log-path fields. Anything else between doubled braces is refused by
5//! name at config time, so a typo dies at `shep start` rather than reaching
6//! a child process as a literal string.
7//!
8//! # Why doubled braces
9//!
10//! Single braces are ordinary content in the values this runs over: JSON
11//! blobs, regex quantifiers such as `{2,3}`, and Go or Helm templates passed
12//! through as args. Under a single-brace grammar with an unknown token
13//! refused, `LOG_FORMAT = '{"ts":"%t"}'` would stop a working Flockfile from
14//! starting. Doubled braces almost never appear by accident.
15//!
16//! # Escaping
17//!
18//! `{{{{` is a literal `{{` and `}}}}` is a literal `}}`, which is
19//! `format!`'s own doubling rule one level up. A lone `}}` is ordinary text,
20//! deliberately: `{"a":{"b":1}}` ends in one and must survive.
21
22use core::fmt;
23
24/// The tokens this grammar knows, in the order an error lists them.
25const TOKENS: &[&str] = &["instance", "name"];
26
27/// A value that is not a valid template.
28///
29/// `pub(crate)`, like [`validate`] that produces it: `normalize` is the only
30/// caller, and it renders this into its own
31/// [`NormalizeError::BadTemplate`](super::normalize::NormalizeError::BadTemplate)
32/// rather than handing it on. Nothing outside shep-core has ever named it.
33#[non_exhaustive]
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub(crate) enum TemplateError {
36    /// A `{{...}}` naming something this grammar does not define
37    UnknownToken {
38        /// The token as the user wrote it, without the braces
39        token: String,
40    },
41    /// A `{{` with no closing `}}`
42    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
64/// One piece of `value` as [`walk`] sees it: ordinary text, or a token name
65/// with the braces stripped.
66enum Segment<'a> {
67    /// A run of ordinary text, copied through unchanged.
68    Literal(&'a str),
69    /// The name between a `{{` and its `}}`, braces stripped.
70    Token(&'a str),
71}
72
73/// Walks `value`, calling `on_segment` for each literal run and each token.
74///
75/// One walker, one closure, so [`validate`] and [`render`] can never
76/// disagree about what a token is.
77fn 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
112/// Checks that every `{{...}}` in `value` names a token this grammar defines.
113///
114/// `pub(crate)`: config time is the only moment this question is asked, and
115/// `normalize` is where config time happens. [`render`] stays public because
116/// shep-daemon's `assemble` substitutes on values `normalize` has already
117/// passed.
118///
119/// # Errors
120///
121/// - [`TemplateError::UnknownToken`]: a token this grammar does not define.
122/// - [`TemplateError::Unclosed`]: a `{{` with no closing `}}`.
123pub(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/// Substitutes the tokens in `value`.
134///
135/// Call `validate` first: an unknown token here renders as nothing, because
136/// `normalize` is the seam that refuses one and a value reaching this
137/// function has already passed it. An unclosed `{{` is the other case
138/// `validate` exists to catch before this function ever sees the value: on
139/// one, `walk` stops with an error partway through, so this renders
140/// truncated at that point rather than including the rest of `value`.
141#[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        // The collision case the doubled braces exist for: single braces are
171        // ordinary content and must survive untouched.
172        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}