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'{' {
205 2
206 } else {
207 1
208 };
209 let start = i + open;
210 let mut j = start;
211 while j < bytes.len()
212 && (bytes[j].is_ascii_alphanumeric() || bytes[j] == b'_' || bytes[j] == b'.')
213 {
214 j += 1;
215 }
216 let closes = j + open <= bytes.len() && bytes[j..j + open].iter().all(|b| *b == b'}');
217 if j > start && closes && is_ident(&t[start..j]) {
218 let name = &t[start..j];
219 let end = j + open; let value = lookup(vars, name)
221 .ok_or_else(|| MappingError(format!("unknown placeholder {{{name}}}")))?;
222 match mode {
223 Mode::Text => out.push_str(&match value {
224 Value::String(s) => s.clone(),
225 Value::Null => String::new(),
226 other => other.to_string(),
227 }),
228 Mode::Json => {
229 let quoted = i > 0
230 && bytes[i - 1] == b'"'
231 && end < bytes.len()
232 && bytes[end] == b'"';
233 match value {
234 Value::String(s) if quoted => {
235 let js = serde_json::to_string(s).unwrap_or_default();
238 out.push_str(&js[1..js.len() - 1]);
239 }
240 other => out.push_str(&other.to_string()),
241 }
242 }
243 }
244 i = end;
245 continue;
246 }
247 }
248 let ch_len = utf8_len(bytes[i]);
250 out.push_str(&t[i..(i + ch_len).min(bytes.len())]);
251 i += ch_len;
252 }
253 Ok(out)
254}
255
256fn utf8_len(lead: u8) -> usize {
257 if lead >= 0xF0 {
258 4
259 } else if lead >= 0xE0 {
260 3
261 } else if lead >= 0xC0 {
262 2
263 } else {
264 1
265 }
266}
267
268pub fn vars_from(map: &Map<String, Value>) -> Vars {
270 map.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276 use serde_json::json;
277
278 fn vars() -> Vars {
279 let mut v = Vars::new();
280 v.insert("key".into(), json!("agentd/x/run/1"));
281 v.insert("seq".into(), json!(7));
282 v.insert(
283 "envelope".into(),
284 json!({"v": 2, "state": {"a": "q\"uote"}}),
285 );
286 v.insert("prefix".into(), json!("agentd"));
287 v.insert(
288 "nested".into(),
289 json!({"deep": {"n": 3}, "arr": ["x", "y"]}),
290 );
291 v
292 }
293
294 #[test]
295 fn json_templates_substitute_typed_values() {
296 let v = render_json(
297 r#"{"key": "{key}", "seq": {seq}, "state": {envelope}}"#,
298 &vars(),
299 )
300 .unwrap();
301 assert_eq!(
302 v,
303 json!({"key": "agentd/x/run/1", "seq": 7, "state": {"v": 2, "state": {"a": "q\"uote"}}})
304 );
305 assert_eq!(
307 render_json("{envelope}", &vars()).unwrap(),
308 vars()["envelope"]
309 );
310 assert_eq!(
312 render_json(r#"{"n": {nested.deep.n}, "y": "{nested.arr.1}"}"#, &vars()).unwrap(),
313 json!({"n": 3, "y": "y"})
314 );
315 assert_eq!(
317 render_json(r#"{"k": {key}}"#, &vars()).unwrap(),
318 json!({"k": "agentd/x/run/1"})
319 );
320 assert_eq!(
322 render_json(r#"{"k": "{{key}}", "n": {{nested.deep.n}}}"#, &vars()).unwrap(),
323 json!({"k": "agentd/x/run/1", "n": 3})
324 );
325 assert_eq!(
326 render_json("{{envelope}}", &vars()).unwrap(),
327 vars()["envelope"]
328 );
329 assert_eq!(
330 render_text("x={{key}}", &vars()).unwrap(),
331 "x=agentd/x/run/1"
332 );
333 assert!(render_json(r#"{"k": {nope}}"#, &vars()).is_err());
335 assert!(render_json(r#"{"k": "{key}"#, &vars()).is_err());
336 let mut v2 = vars();
338 v2.insert("key".into(), json!("a\"b"));
339 assert_eq!(
340 render_json(r#"{"k": "{key}"}"#, &v2).unwrap(),
341 json!({"k": "a\"b"})
342 );
343 }
344
345 #[test]
346 fn text_templates_and_extraction() {
347 assert_eq!(
348 render_text("{prefix}/kv/{key}?seq={seq}", &vars()).unwrap(),
349 "agentd/kv/agentd/x/run/1?seq=7"
350 );
351 assert!(render_text("{missing}", &vars()).is_err());
352 let ctx = json!({"result": {"structuredContent": {"state": {"x": 1}, "keys": ["a"]}, "isError": false}, "status": 200});
353 assert_eq!(
354 extract("result.structuredContent.state", &ctx).unwrap(),
355 Some(json!({"x": 1}))
356 );
357 assert_eq!(
358 extract("/result/structuredContent/keys/0", &ctx).unwrap(),
359 Some(json!("a"))
360 );
361 assert_eq!(
362 extract("result.structuredContent.keys.0", &ctx).unwrap(),
363 Some(json!("a"))
364 );
365 assert_eq!(extract("result.nope.deeper", &ctx).unwrap(), None);
366 assert_eq!(extract("", &ctx).unwrap(), Some(ctx.clone()));
367 assert!(
368 truthy(&json!(true))
369 && truthy(&json!(1))
370 && truthy(&json!("x"))
371 && !truthy(&json!(null))
372 && !truthy(&json!(0))
373 );
374 }
375
376 #[cfg(feature = "cel")]
377 #[test]
378 fn cel_templates_render_and_extract() {
379 let v = render_json(r#"CEL: {"k": key, "next": seq + 1}"#, &vars()).unwrap();
380 assert_eq!(v, json!({"k": "agentd/x/run/1", "next": 8}));
381 assert_eq!(
382 render_text("CEL: prefix + '/' + key", &vars()).unwrap(),
383 "agentd/agentd/x/run/1"
384 );
385 let ctx = json!({"result": {"structuredContent": {"ok": true, "latest": 9}}});
386 assert_eq!(
387 extract("CEL: result.structuredContent.latest * 2", &ctx).unwrap(),
388 Some(json!(18))
389 );
390 }
391}