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