nmbrs_workload/
template.rs1use std::collections::HashMap;
19
20pub fn expand_templates(source: &str, params: &HashMap<String, String>) -> String {
25 let mut resolved: HashMap<String, String> = params.clone();
26 let mut result = String::with_capacity(source.len());
27 let chars: Vec<char> = source.chars().collect();
28 let mut i = 0;
29
30 while i < chars.len() {
31 let template_match = i + 9 <= chars.len()
33 && chars[i] == 'T'
34 && chars[i + 1] == 'E'
35 && chars[i + 2] == 'M'
36 && chars[i + 3] == 'P'
37 && chars[i + 4] == 'L'
38 && chars[i + 5] == 'A'
39 && chars[i + 6] == 'T'
40 && chars[i + 7] == 'E'
41 && chars[i + 8] == '(';
42
43 if template_match {
44 i += 9; let mut depth = 1;
48 let arg_start = i;
49 let mut in_quote = false;
50 while i < chars.len() && depth > 0 {
51 match chars[i] {
52 '\'' if !in_quote => in_quote = true,
53 '\'' if in_quote => in_quote = false,
54 '(' if !in_quote => depth += 1,
55 ')' if !in_quote => depth -= 1,
56 _ => {}
57 }
58 if depth > 0 {
59 i += 1;
60 }
61 }
62
63 if depth != 0 {
64 for c in &chars[arg_start - 9..] {
66 result.push(*c);
67 }
68 break;
69 }
70
71 let args_str: String = chars[arg_start..i].iter().collect();
72 i += 1; let (name, default) = if let Some(comma_pos) = args_str.find(',') {
76 let name = args_str[..comma_pos].trim().to_string();
77 let default = args_str[comma_pos + 1..].trim().to_string();
78 (name, Some(default))
79 } else {
80 (args_str.trim().to_string(), None)
81 };
82
83 let value = if let Some(v) = resolved.get(&name) {
85 v.clone()
86 } else if let Some(ref d) = default {
87 if d.is_empty() {
88 String::new()
90 } else {
91 resolved.insert(name.clone(), d.clone());
93 d.clone()
94 }
95 } else {
96 format!("UNSET:{name}")
98 };
99
100 result.push_str(&value);
101 } else {
102 result.push(chars[i]);
103 i += 1;
104 }
105 }
106
107 result
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 fn expand(source: &str) -> String {
115 expand_templates(source, &HashMap::new())
116 }
117
118 fn expand_with(source: &str, params: &[(&str, &str)]) -> String {
119 let map: HashMap<String, String> = params
120 .iter()
121 .map(|(k, v)| (k.to_string(), v.to_string()))
122 .collect();
123 expand_templates(source, &map)
124 }
125
126 #[test]
127 fn template_with_default() {
128 assert_eq!(
129 expand("name: TEMPLATE(myname, thedefault)"),
130 "name: thedefault"
131 );
132 }
133
134 #[test]
135 fn template_no_default_unset() {
136 assert_eq!(expand("name: TEMPLATE(myname)"), "name: UNSET:myname");
137 }
138
139 #[test]
140 fn template_null_default() {
141 assert_eq!(expand("name: TEMPLATE(myname,)"), "name: ");
142 }
143
144 #[test]
145 fn template_provided_param() {
146 assert_eq!(
147 expand_with("count: TEMPLATE(n, 100)", &[("n", "500")]),
148 "count: 500"
149 );
150 }
151
152 #[test]
153 fn template_consistent_resolution() {
154 let result = expand("a: TEMPLATE(x, hello)\nb: TEMPLATE(x)");
156 assert_eq!(result, "a: hello\nb: hello");
157 }
158
159 #[test]
160 fn template_param_overrides_default() {
161 assert_eq!(
162 expand_with("v: TEMPLATE(x, default)", &[("x", "override")]),
163 "v: override"
164 );
165 }
166
167 #[test]
168 fn template_no_templates_passthrough() {
169 assert_eq!(expand("just plain text"), "just plain text");
170 }
171
172 #[test]
173 fn template_multiple_on_one_line() {
174 assert_eq!(expand("TEMPLATE(a, 1) and TEMPLATE(b, 2)"), "1 and 2");
175 }
176
177 #[test]
178 fn template_in_yaml_context() {
179 let yaml = r#"
180bindings:
181 key: Mod(TEMPLATE(keycount, 1000000))
182 val: Hash(); Mod(TEMPLATE(valcount, 500000))
183"#;
184 let result = expand(yaml);
185 assert!(result.contains("Mod(1000000)"));
186 assert!(result.contains("Mod(500000)"));
187 }
188
189 #[test]
190 fn template_nested_parens() {
191 assert_eq!(expand("TEMPLATE(expr, ToString())"), "ToString()");
193 }
194}