Skip to main content

coding_tools/
template.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Jonathan Shook
3
4//! Token substitution for `--emit` verdict templates, shared by every tool.
5//!
6//! A template carries `{TOKEN}` placeholders; [`render`] expands the ones it
7//! recognises from a `(name, value)` table and leaves anything else — including
8//! unknown `{TOKEN}`s and stray braces — untouched. The substitution is a single
9//! left-to-right pass, so replacement text is **never rescanned**: a value that
10//! happens to contain `{RESULT}` (e.g. a command's captured stdout) is emitted
11//! verbatim rather than re-expanded.
12
13/// Expand recognised `{TOKEN}` placeholders in `template` from `tokens`.
14///
15/// Tokens are matched by exact name between a `{` and the next `}`. An unknown
16/// token, or a `{` with no closing `}`, is copied through unchanged.
17///
18/// # Examples
19///
20/// ```
21/// use coding_tools::template::render;
22///
23/// let tokens = [("RESULT", "SUCCESS"), ("CODE", "0")];
24/// assert_eq!(render("{RESULT} (exit {CODE})", &tokens), "SUCCESS (exit 0)");
25///
26/// // Unknown tokens pass through, and replacement text is never re-scanned.
27/// assert_eq!(render("{MISSING}", &[]), "{MISSING}");
28/// assert_eq!(render("{X}", &[("X", "{CODE}")]), "{CODE}");
29/// ```
30pub fn render(template: &str, tokens: &[(&str, &str)]) -> String {
31    let mut out = String::with_capacity(template.len());
32    let mut rest = template;
33    while let Some(open) = rest.find('{') {
34        out.push_str(&rest[..open]);
35        let after = &rest[open + 1..];
36        match after.find('}') {
37            Some(close_rel) => {
38                let name = &after[..close_rel];
39                match tokens.iter().find(|(n, _)| *n == name) {
40                    Some((_, value)) => out.push_str(value),
41                    None => {
42                        // Unknown token: keep the braces and name verbatim.
43                        out.push('{');
44                        out.push_str(name);
45                        out.push('}');
46                    }
47                }
48                rest = &after[close_rel + 1..];
49            }
50            None => {
51                // Unbalanced '{': nothing left to substitute.
52                out.push_str(&rest[open..]);
53                return out;
54            }
55        }
56    }
57    out.push_str(rest);
58    out
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    fn expands_known_tokens() {
67        let out = render(
68            "{QUESTION} -> {RESULT}",
69            &[("QUESTION", "ok?"), ("RESULT", "SUCCESS")],
70        );
71        assert_eq!(out, "ok? -> SUCCESS");
72    }
73
74    #[test]
75    fn leaves_unknown_tokens_verbatim() {
76        let out = render("{RESULT} {MISSING}", &[("RESULT", "ERROR")]);
77        assert_eq!(out, "ERROR {MISSING}");
78    }
79
80    #[test]
81    fn does_not_rescan_substituted_values() {
82        // A value containing a token name must not be re-expanded.
83        let out = render(
84            "{STDOUT}|{CODE}",
85            &[("STDOUT", "see {CODE}"), ("CODE", "0")],
86        );
87        assert_eq!(out, "see {CODE}|0");
88    }
89
90    #[test]
91    fn copies_unbalanced_brace_through() {
92        let out = render("a {RESULT} b {dangling", &[("RESULT", "SUCCESS")]);
93        assert_eq!(out, "a SUCCESS b {dangling");
94    }
95}