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}