1use serde_json::{Map, Value};
24use std::collections::BTreeMap;
25
26#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct MappingError(pub String);
30
31impl std::fmt::Display for MappingError {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 f.write_str(&self.0)
34 }
35}
36
37impl std::error::Error for MappingError {}
38
39pub type Vars = BTreeMap<String, Value>;
41
42pub fn render_json(template: &str, vars: &Vars) -> Result<Value, MappingError> {
44 let t = template.trim();
45 if let Some(expr) = t.strip_prefix("CEL:") {
46 return crate::cel::eval_value(expr.trim(), &crate::cel::vars_of(vars))
47 .map_err(|e| MappingError(format!("CEL: {e}")));
48 }
49 if let Some(name) = lone_placeholder(t) {
51 return vars
52 .get(name)
53 .cloned()
54 .ok_or_else(|| MappingError(format!("unknown placeholder {{{name}}}")));
55 }
56 let text = substitute(t, vars, Mode::Json)?;
57 serde_json::from_str(&text).map_err(|e| {
58 MappingError(format!(
59 "template does not render to valid JSON: {e} (rendered: {})",
60 text.chars().take(200).collect::<String>()
61 ))
62 })
63}
64
65pub fn render_text(template: &str, vars: &Vars) -> Result<String, MappingError> {
68 let t = template.trim();
69 if let Some(expr) = t.strip_prefix("CEL:") {
70 let v = crate::cel::eval_value(expr.trim(), &crate::cel::vars_of(vars))
71 .map_err(|e| MappingError(format!("CEL: {e}")))?;
72 return Ok(match v {
73 Value::String(s) => s,
74 other => other.to_string(),
75 });
76 }
77 substitute(t, vars, Mode::Text)
78}
79
80pub fn extract(expr: &str, ctx: &Value) -> Result<Option<Value>, MappingError> {
83 let e = expr.trim();
84 if let Some(cel) = e.strip_prefix("CEL:") {
85 let vars: Vars = match ctx {
86 Value::Object(m) => m.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
87 other => {
88 let mut m = Vars::new();
89 m.insert("value".into(), other.clone());
90 m
91 }
92 };
93 return crate::cel::eval_value(cel.trim(), &crate::cel::vars_of(&vars))
94 .map(Some)
95 .map_err(|e| MappingError(format!("CEL: {e}")));
96 }
97 if e.is_empty() {
98 return Ok(Some(ctx.clone()));
99 }
100 if e.starts_with('/') {
101 return Ok(ctx.pointer(e).cloned());
102 }
103 let mut cur = ctx;
105 for seg in e.split('.') {
106 cur = match cur {
107 Value::Object(m) => match m.get(seg) {
108 Some(v) => v,
109 None => return Ok(None),
110 },
111 Value::Array(a) => match seg.parse::<usize>().ok().and_then(|i| a.get(i)) {
112 Some(v) => v,
113 None => return Ok(None),
114 },
115 _ => return Ok(None),
116 };
117 }
118 Ok(Some(cur.clone()))
119}
120
121pub fn truthy(v: &Value) -> bool {
124 match v {
125 Value::Null => false,
126 Value::Bool(b) => *b,
127 Value::Number(n) => n.as_f64().is_some_and(|f| f != 0.0),
128 Value::String(s) => !s.is_empty(),
129 Value::Array(a) => !a.is_empty(),
130 Value::Object(o) => !o.is_empty(),
131 }
132}
133
134pub fn store_vars(
136 key: &str,
137 seq: Option<u64>,
138 prefix: &str,
139 instance: &str,
140 envelope: Option<&Value>,
141 kind: &str,
142 id: &str,
143) -> Vars {
144 let mut v = Vars::new();
145 v.insert("key".into(), Value::String(key.to_string()));
146 v.insert("seq".into(), seq.map(Value::from).unwrap_or(Value::Null));
147 v.insert("prefix".into(), Value::String(prefix.to_string()));
148 v.insert("instance".into(), Value::String(instance.to_string()));
149 v.insert("envelope".into(), envelope.cloned().unwrap_or(Value::Null));
150 v.insert("kind".into(), Value::String(kind.to_string()));
151 v.insert("id".into(), Value::String(id.to_string()));
152 v
153}
154
155#[derive(Clone, Copy, PartialEq, Eq)]
156enum Mode {
157 Json,
158 Text,
159}
160
161fn lone_placeholder(t: &str) -> Option<&str> {
162 let inner = t.strip_prefix('{')?.strip_suffix('}')?;
163 let inner = match (inner.strip_prefix('{'), inner.strip_suffix('}')) {
164 (Some(a), Some(_)) => &a[..a.len() - 1],
165 _ => inner,
166 };
167 is_ident(inner).then_some(inner)
168}
169
170fn is_ident(s: &str) -> bool {
171 let mut chars = s.chars();
172 match chars.next() {
173 Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
174 _ => return false,
175 }
176 chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.')
177}
178
179fn lookup<'a>(vars: &'a Vars, name: &str) -> Option<&'a Value> {
181 if let Some(v) = vars.get(name) {
182 return Some(v);
183 }
184 let (head, rest) = name.split_once('.')?;
185 let mut cur = vars.get(head)?;
186 for seg in rest.split('.') {
187 cur = match cur {
188 Value::Object(m) => m.get(seg)?,
189 Value::Array(a) => a.get(seg.parse::<usize>().ok()?)?,
190 _ => return None,
191 };
192 }
193 Some(cur)
194}
195
196fn substitute(t: &str, vars: &Vars, mode: Mode) -> Result<String, MappingError> {
197 let bytes = t.as_bytes();
198 let mut out = String::with_capacity(t.len() + 32);
199 let mut i = 0;
200 while i < bytes.len() {
201 if bytes[i] == b'{' {
202 let open = if i + 1 < bytes.len() && bytes[i + 1] == b'{' {
206 2
207 } else {
208 1
209 };
210 let start = i + open;
211 let mut j = start;
212 while j < bytes.len()
213 && (bytes[j].is_ascii_alphanumeric() || bytes[j] == b'_' || bytes[j] == b'.')
214 {
215 j += 1;
216 }
217 let closes = j + open <= bytes.len() && bytes[j..j + open].iter().all(|b| *b == b'}');
218 if j > start && closes && is_ident(&t[start..j]) {
219 let name = &t[start..j];
220 let end = j + open; let value = lookup(vars, name)
222 .ok_or_else(|| MappingError(format!("unknown placeholder {{{name}}}")))?;
223 match mode {
224 Mode::Text => out.push_str(&match value {
225 Value::String(s) => s.clone(),
226 Value::Null => String::new(),
227 other => other.to_string(),
228 }),
229 Mode::Json => {
230 let quoted = i > 0
231 && bytes[i - 1] == b'"'
232 && end < bytes.len()
233 && bytes[end] == b'"';
234 match value {
235 Value::String(s) if quoted => {
236 let js = serde_json::to_string(s).unwrap_or_default();
239 out.push_str(&js[1..js.len() - 1]);
240 }
241 other => out.push_str(&other.to_string()),
242 }
243 }
244 }
245 i = end;
246 continue;
247 }
248 }
249 let ch_len = utf8_len(bytes[i]);
251 out.push_str(&t[i..(i + ch_len).min(bytes.len())]);
252 i += ch_len;
253 }
254 Ok(out)
255}
256
257fn utf8_len(lead: u8) -> usize {
258 if lead >= 0xF0 {
259 4
260 } else if lead >= 0xE0 {
261 3
262 } else if lead >= 0xC0 {
263 2
264 } else {
265 1
266 }
267}
268
269pub fn vars_from(map: &Map<String, Value>) -> Vars {
271 map.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277 use serde_json::json;
278
279 fn vars() -> Vars {
280 let mut v = Vars::new();
281 v.insert("key".into(), json!("agentd/x/run/1"));
282 v.insert("seq".into(), json!(7));
283 v.insert(
284 "envelope".into(),
285 json!({"v": 2, "state": {"a": "q\"uote"}}),
286 );
287 v.insert("prefix".into(), json!("agentd"));
288 v.insert(
289 "nested".into(),
290 json!({"deep": {"n": 3}, "arr": ["x", "y"]}),
291 );
292 v
293 }
294
295 #[test]
296 fn json_templates_substitute_typed_values() {
297 let v = render_json(
298 r#"{"key": "{key}", "seq": {seq}, "state": {envelope}}"#,
299 &vars(),
300 )
301 .unwrap();
302 assert_eq!(
303 v,
304 json!({"key": "agentd/x/run/1", "seq": 7, "state": {"v": 2, "state": {"a": "q\"uote"}}})
305 );
306 assert_eq!(
308 render_json("{envelope}", &vars()).unwrap(),
309 vars()["envelope"]
310 );
311 assert_eq!(
313 render_json(r#"{"n": {nested.deep.n}, "y": "{nested.arr.1}"}"#, &vars()).unwrap(),
314 json!({"n": 3, "y": "y"})
315 );
316 assert_eq!(
318 render_json(r#"{"k": {key}}"#, &vars()).unwrap(),
319 json!({"k": "agentd/x/run/1"})
320 );
321 assert_eq!(
323 render_json(r#"{"k": "{{key}}", "n": {{nested.deep.n}}}"#, &vars()).unwrap(),
324 json!({"k": "agentd/x/run/1", "n": 3})
325 );
326 assert_eq!(
327 render_json("{{envelope}}", &vars()).unwrap(),
328 vars()["envelope"]
329 );
330 assert_eq!(
331 render_text("x={{key}}", &vars()).unwrap(),
332 "x=agentd/x/run/1"
333 );
334 assert!(render_json(r#"{"k": {nope}}"#, &vars()).is_err());
336 assert!(render_json(r#"{"k": "{key}"#, &vars()).is_err());
337 let mut v2 = vars();
339 v2.insert("key".into(), json!("a\"b"));
340 assert_eq!(
341 render_json(r#"{"k": "{key}"}"#, &v2).unwrap(),
342 json!({"k": "a\"b"})
343 );
344 }
345
346 #[test]
347 fn text_templates_and_extraction() {
348 assert_eq!(
349 render_text("{prefix}/kv/{key}?seq={seq}", &vars()).unwrap(),
350 "agentd/kv/agentd/x/run/1?seq=7"
351 );
352 assert!(render_text("{missing}", &vars()).is_err());
353 let ctx = json!({"result": {"structuredContent": {"state": {"x": 1}, "keys": ["a"]}, "isError": false}, "status": 200});
354 assert_eq!(
355 extract("result.structuredContent.state", &ctx).unwrap(),
356 Some(json!({"x": 1}))
357 );
358 assert_eq!(
359 extract("/result/structuredContent/keys/0", &ctx).unwrap(),
360 Some(json!("a"))
361 );
362 assert_eq!(
363 extract("result.structuredContent.keys.0", &ctx).unwrap(),
364 Some(json!("a"))
365 );
366 assert_eq!(extract("result.nope.deeper", &ctx).unwrap(), None);
367 assert_eq!(extract("", &ctx).unwrap(), Some(ctx.clone()));
368 assert!(
369 truthy(&json!(true))
370 && truthy(&json!(1))
371 && truthy(&json!("x"))
372 && !truthy(&json!(null))
373 && !truthy(&json!(0))
374 );
375 }
376
377 #[cfg(feature = "cel")]
378 #[test]
379 fn cel_templates_render_and_extract() {
380 let v = render_json(r#"CEL: {"k": key, "next": seq + 1}"#, &vars()).unwrap();
381 assert_eq!(v, json!({"k": "agentd/x/run/1", "next": 8}));
382 assert_eq!(
383 render_text("CEL: prefix + '/' + key", &vars()).unwrap(),
384 "agentd/agentd/x/run/1"
385 );
386 let ctx = json!({"result": {"structuredContent": {"ok": true, "latest": 9}}});
387 assert_eq!(
388 extract("CEL: result.structuredContent.latest * 2", &ctx).unwrap(),
389 Some(json!(18))
390 );
391 }
392}