1use serde_json::{Map, Value};
15use std::collections::BTreeMap;
16
17pub type Data = BTreeMap<String, Value>;
19
20pub fn render(template: &Value, data: &Data) -> Result<Value, String> {
22 match template {
23 Value::String(s) => render_str(s, data),
24 Value::Array(a) => a
25 .iter()
26 .map(|v| render(v, data))
27 .collect::<Result<Vec<_>, _>>()
28 .map(Value::Array),
29 Value::Object(o) => {
30 let mut out = Map::new();
31 for (k, v) in o {
32 out.insert(k.clone(), render(v, data)?);
33 }
34 Ok(Value::Object(out))
35 }
36 other => Ok(other.clone()),
37 }
38}
39
40pub fn render_str(s: &str, data: &Data) -> Result<Value, String> {
43 let t = s.trim_start();
44 if let Some(expr) = t.strip_prefix("CEL:") {
45 let vars: Vec<(&str, &Value)> = data.iter().map(|(k, v)| (k.as_str(), v)).collect();
46 return crate::cel::eval_value(expr.trim(), &vars).map_err(|e| format!("CEL: {e}"));
47 }
48 if !s.contains("{{") {
49 return Ok(Value::String(s.to_string()));
50 }
51 let trimmed = s.trim();
53 if trimmed.starts_with("{{") && trimmed.ends_with("}}") && trimmed.matches("{{").count() == 1 {
54 let inner = &trimmed[2..trimmed.len() - 2];
55 return resolve_placeholder(inner, data);
56 }
57 let mut out = String::with_capacity(s.len());
58 let mut rest = s;
59 while let Some(start) = rest.find("{{") {
60 out.push_str(&rest[..start]);
61 let after = &rest[start + 2..];
62 let Some(end) = after.find("}}") else {
63 return Err(format!("unterminated placeholder in {s:?}"));
64 };
65 let inner = &after[..end];
66 let v = resolve_placeholder(inner, data)?;
67 out.push_str(&match v {
68 Value::String(x) => x,
69 Value::Null => String::new(),
70 other => other.to_string(),
71 });
72 rest = &after[end + 2..];
73 }
74 out.push_str(rest);
75 Ok(Value::String(out))
76}
77
78fn resolve_placeholder(inner: &str, data: &Data) -> Result<Value, String> {
80 let t = inner.trim();
85 if t.starts_with("secret:") || t.starts_with("secret-file:") {
86 return Ok(Value::String(format!("{{{{{t}}}}}")));
87 }
88 let (path, default) = match inner.split_once('|') {
89 Some((p, d)) => (p.trim(), Some(d.trim())),
90 None => (inner.trim(), None),
91 };
92 match lookup(path, data) {
93 Some(v) => Ok(v),
94 None => match default {
95 Some(d) => Ok(serde_json::from_str::<Value>(d)
96 .unwrap_or_else(|_| Value::String(d.trim_matches(['"', '\'']).to_string()))),
97 None => Err(format!(
98 "template path {path:?} is not set (no default given)"
99 )),
100 },
101 }
102}
103
104pub fn lookup(path: &str, data: &Data) -> Option<Value> {
106 if path.is_empty() {
107 return None;
108 }
109 if let Some(p) = path.strip_prefix('/') {
110 let (head, rest) = match p.split_once('/') {
111 Some((h, r)) => (h, Some(r)),
112 None => (p, None),
113 };
114 let root = data.get(head)?;
115 return match rest {
116 None => Some(root.clone()),
117 Some(r) => root.pointer(&format!("/{r}")).cloned(),
118 };
119 }
120 let mut segs = path.split('.');
121 let head = segs.next()?;
122 let mut cur = data.get(head)?;
123 for seg in segs {
124 cur = match cur {
125 Value::Object(m) => m.get(seg)?,
126 Value::Array(a) => a.get(seg.parse::<usize>().ok()?)?,
127 _ => return None,
128 };
129 }
130 Some(cur.clone())
131}
132
133pub fn referenced_roots(template: &Value) -> Vec<String> {
136 let mut out = Vec::new();
137 fn walk(v: &Value, out: &mut Vec<String>) {
138 match v {
139 Value::String(s) => {
140 let mut rest = s.as_str();
141 while let Some(start) = rest.find("{{") {
142 let after = &rest[start + 2..];
143 let Some(end) = after.find("}}") else { break };
144 let inner = after[..end].split('|').next().unwrap_or("").trim();
145 let root = inner
146 .trim_start_matches('/')
147 .split(['.', '/'])
148 .next()
149 .unwrap_or("")
150 .to_string();
151 if !root.is_empty() && !out.contains(&root) {
152 out.push(root);
153 }
154 rest = &after[end + 2..];
155 }
156 }
157 Value::Array(a) => a.iter().for_each(|x| walk(x, out)),
158 Value::Object(o) => o.values().for_each(|x| walk(x, out)),
159 _ => {}
160 }
161 }
162 walk(template, &mut out);
163 out
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169 use serde_json::json;
170
171 fn data() -> Data {
172 let mut d = Data::new();
173 d.insert("inputs".into(), json!({"instruction": "do it", "n": 3}));
174 d.insert(
175 "steps".into(),
176 json!({"fetch": {"status": "done", "output": {"items": [1, 2, 3], "name": "x"}}}),
177 );
178 d.insert("vars".into(), json!({"count": 2}));
179 d.insert(
180 "env".into(),
181 json!({"instance": "i", "instruction": "brief"}),
182 );
183 d
184 }
185
186 #[test]
187 fn typed_lone_placeholders_interpolation_defaults_and_pointers() {
188 let d = data();
189 assert_eq!(
190 render(&json!("{{steps.fetch.output.items}}"), &d).unwrap(),
191 json!([1, 2, 3])
192 );
193 assert_eq!(render(&json!(" {{inputs.n}} "), &d).unwrap(), json!(3));
194 assert_eq!(render(&json!("count={{vars.count}}, first={{steps.fetch.output.items.0}}, name={{steps.fetch.output.name}}"), &d).unwrap(), json!("count=2, first=1, name=x"));
195 assert_eq!(
196 render(&json!("{{/steps/fetch/output/items/1}}"), &d).unwrap(),
197 json!(2)
198 );
199 assert_eq!(
200 render(&json!("{{vars.missing | 7}}"), &d).unwrap(),
201 json!(7)
202 );
203 assert_eq!(
204 render(&json!("{{vars.missing | \"dflt\"}}"), &d).unwrap(),
205 json!("dflt")
206 );
207 assert_eq!(
208 render(&json!("x{{vars.missing | y}}z"), &d).unwrap(),
209 json!("xyz")
210 );
211 assert!(
212 render(&json!("{{vars.missing}}"), &d)
213 .unwrap_err()
214 .contains("not set")
215 );
216 assert!(
217 render(&json!("{{oops"), &d)
218 .unwrap_err()
219 .contains("unterminated")
220 );
221 let v = render(
223 &json!({"a": ["{{inputs.n}}", {"b": "{{env.instruction}}"}], "c": 5, "d": null}),
224 &d,
225 )
226 .unwrap();
227 assert_eq!(v, json!({"a": [3, {"b": "brief"}], "c": 5, "d": null}));
228 assert_eq!(
229 referenced_roots(
230 &json!({"a": "{{steps.x.output}} {{vars.y | 1}}", "b": ["{{/inputs/z}}"]})
231 ),
232 vec!["steps", "vars", "inputs"]
233 );
234 assert_eq!(render(&json!("plain"), &d).unwrap(), json!("plain"));
235 }
236
237 #[cfg(feature = "cel")]
238 #[test]
239 fn cel_values_evaluate_over_the_data() {
240 let d = data();
241 assert_eq!(render(&json!("CEL: inputs.n * 2"), &d).unwrap(), json!(6));
242 assert_eq!(
243 render(&json!("CEL: steps.fetch.output.items.size()"), &d).unwrap(),
244 json!(3)
245 );
246 assert!(render(&json!("CEL: nope.x"), &d).is_err());
247 }
248}