1use super::config::{CompileCx, ConfigField, FieldKind, Resolver, Visit};
4use super::engine::ExprEngine;
5use super::path::Path;
6use super::secrets::SECRET_OPERATOR;
7use crate::base::{Issue, IssueCode};
8use datavalue::{DataValue, OwnedDataValue};
9use serde::ser::{Serialize, SerializeMap, SerializeSeq, Serializer};
10use serde_json::Value;
11use std::borrow::Cow;
12use std::fmt;
13use std::str::FromStr;
14
15pub struct Interpolated {
24 raw: Value,
25 node: Node,
26 holes: Vec<Hole>,
27}
28
29pub(crate) struct Hole {
31 pub path: Option<Path>,
34 pub default: OwnedDataValue,
35}
36
37enum Node {
38 Literal(Value),
40 Hole(usize),
41 Object(Vec<(String, Self)>),
42 Array(Vec<Self>),
43}
44
45impl Interpolated {
46 pub fn raw(&self) -> &Value {
50 &self.raw
51 }
52
53 pub fn holes(&self) -> impl Iterator<Item = &Path> {
56 self.holes.iter().filter_map(|h| h.path.as_ref())
57 }
58
59 pub(crate) fn render_with<'s>(
63 &'s self,
64 fill: impl FnMut(&'s Hole) -> Filled<'s>,
65 ) -> Rendered<'s> {
66 Rendered {
67 doc: self,
68 filled: self.holes.iter().map(fill).collect(),
69 }
70 }
71}
72
73impl fmt::Debug for Interpolated {
74 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75 f.debug_struct("Interpolated")
76 .field("raw", &self.raw)
77 .finish()
78 }
79}
80
81pub(crate) enum Filled<'s> {
83 Owned(Cow<'s, OwnedDataValue>),
86 View(DataValue<'s>),
88 Default(&'s OwnedDataValue),
90}
91
92impl<'s> Filled<'s> {
93 pub(crate) fn lent(value: &'s OwnedDataValue) -> Self {
95 Self::Owned(Cow::Borrowed(value))
96 }
97
98 pub(crate) fn into_cow(self) -> Cow<'s, OwnedDataValue> {
101 match self {
102 Self::Owned(value) => value,
103 Self::View(view) => Cow::Owned(view.to_owned()),
104 Self::Default(value) => Cow::Borrowed(value),
105 }
106 }
107
108 fn to_owned_value(&self) -> OwnedDataValue {
109 match self {
110 Self::Owned(value) => value.as_ref().clone(),
111 Self::View(view) => view.to_owned(),
112 Self::Default(value) => (*value).clone(),
113 }
114 }
115}
116
117impl Serialize for Filled<'_> {
118 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
119 match self {
120 Self::Owned(value) => value.serialize(s),
121 Self::View(view) => view.serialize(s),
122 Self::Default(value) => value.serialize(s),
123 }
124 }
125}
126
127pub struct Rendered<'s> {
136 doc: &'s Interpolated,
137 filled: Vec<Filled<'s>>,
139}
140
141impl Rendered<'_> {
142 pub fn to_owned(&self) -> OwnedDataValue {
149 self.doc.node.to_owned_value(&self.filled)
150 }
151
152 #[cfg(test)]
154 pub(crate) fn filled(&self) -> &[Filled<'_>] {
155 &self.filled
156 }
157}
158
159impl Serialize for Rendered<'_> {
160 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
166 At {
167 node: &self.doc.node,
168 filled: &self.filled,
169 }
170 .serialize(s)
171 }
172}
173
174impl fmt::Debug for Rendered<'_> {
175 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178 f.debug_struct("Rendered")
179 .field("raw", &self.doc.raw)
180 .field("holes", &self.filled.len())
181 .finish_non_exhaustive()
182 }
183}
184
185struct At<'r, 's> {
187 node: &'r Node,
188 filled: &'r [Filled<'s>],
189}
190
191impl Serialize for At<'_, '_> {
192 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
193 let at = |node| Self {
194 node,
195 filled: self.filled,
196 };
197 match self.node {
198 Node::Literal(value) => value.serialize(s),
199 Node::Hole(i) => self.filled[*i].serialize(s),
200 Node::Object(children) => {
201 let mut map = s.serialize_map(Some(children.len()))?;
202 for (key, node) in children {
203 map.serialize_entry(key, &at(node))?;
204 }
205 map.end()
206 }
207 Node::Array(children) => {
208 let mut seq = s.serialize_seq(Some(children.len()))?;
209 for node in children {
210 seq.serialize_element(&at(node))?;
211 }
212 seq.end()
213 }
214 }
215 }
216}
217
218impl Node {
219 fn to_owned_value(&self, filled: &[Filled<'_>]) -> OwnedDataValue {
221 match self {
222 Self::Literal(value) => OwnedDataValue::from(value),
223 Self::Hole(i) => filled[*i].to_owned_value(),
224 Self::Object(children) => OwnedDataValue::Object(
225 children
226 .iter()
227 .map(|(key, node)| (key.clone(), node.to_owned_value(filled)))
228 .collect(),
229 ),
230 Self::Array(children) => OwnedDataValue::Array(
231 children
232 .iter()
233 .map(|node| node.to_owned_value(filled))
234 .collect(),
235 ),
236 }
237 }
238}
239
240pub(crate) fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Interpolated> {
243 let before = cx.rejected();
244 let mut holes = Vec::new();
245 let node = walk(raw, cx, Some(&mut holes));
246 (cx.rejected() == before).then(|| Interpolated {
247 raw: raw.clone(),
248 node: node.unwrap_or_else(|| Node::Literal(raw.clone())),
249 holes,
250 })
251}
252
253fn walk(raw: &Value, cx: &mut CompileCx<'_>, mut holes: Option<&mut Vec<Hole>>) -> Option<Node> {
259 match raw {
260 Value::Object(o) => {
261 if o.len() == 1 {
262 let (key, arg) = o.iter().next().expect("one entry");
263 if key == "var"
264 && let Some(holes) = holes.as_deref_mut()
265 {
266 return hole(arg, cx, holes);
267 }
268 match key.as_str() {
269 SECRET_OPERATOR => {
270 cx.issue(
271 IssueCode::SecretInDocument,
272 "a document cannot read a secret",
273 );
274 return None;
275 }
276 name if cx.exprs().operator(name).is_some() => {
277 cx.at(name, |cx| {
278 cx.issue(
279 IssueCode::OperatorInDocument,
280 format!(
281 "`{name}` is not evaluated in a document, it stays data; \
282 only `var` holes are live"
283 ),
284 );
285 });
286 }
287 _ => {}
288 }
289 }
290 let mut children: Option<Vec<Option<Node>>> = None;
291 for (i, (k, v)) in o.iter().enumerate() {
292 if let Some(node) = cx.at(k.as_str(), |cx| walk(v, cx, holes.as_deref_mut())) {
293 let list = children.get_or_insert_with(|| {
294 let mut list = Vec::with_capacity(o.len());
295 list.resize_with(i, || None);
296 list
297 });
298 list.push(Some(node));
299 } else if let Some(list) = &mut children {
300 list.push(None);
301 }
302 }
303 children.map(|children| {
304 Node::Object(
305 o.iter()
306 .zip(children)
307 .map(|((k, v), node)| {
308 (k.clone(), node.unwrap_or_else(|| Node::Literal(v.clone())))
309 })
310 .collect(),
311 )
312 })
313 }
314 Value::Array(a) => {
315 let mut children: Option<Vec<Option<Node>>> = None;
316 for (i, v) in a.iter().enumerate() {
317 if let Some(node) = cx.at(i, |cx| walk(v, cx, holes.as_deref_mut())) {
318 let list = children.get_or_insert_with(|| {
319 let mut list = Vec::with_capacity(a.len());
320 list.resize_with(i, || None);
321 list
322 });
323 list.push(Some(node));
324 } else if let Some(list) = &mut children {
325 list.push(None);
326 }
327 }
328 children.map(|children| {
329 Node::Array(
330 a.iter()
331 .zip(children)
332 .map(|(v, node)| node.unwrap_or_else(|| Node::Literal(v.clone())))
333 .collect(),
334 )
335 })
336 }
337 _ => None,
338 }
339}
340
341fn hole(arg: &Value, cx: &mut CompileCx<'_>, holes: &mut Vec<Hole>) -> Option<Node> {
343 let (path, default) = match arg {
344 Value::String(p) => (Some(p.as_str()), None),
345 Value::Array(a) if matches!(a.len(), 1 | 2) => (a[0].as_str(), a.get(1)),
346 _ => (None, None),
347 };
348 let parsed = match path {
349 None => Err("`var` takes a path string, or [path] or [path, default]".to_owned()),
350 Some(path) if path.split('.').next() == Some("payload") => {
351 cx.issue(
352 IssueCode::PayloadVar,
353 format!("`{path}` reads the payload, which is not in the context; the hole always yields its default"),
354 );
355 Ok(None)
356 }
357 Some(path) => Path::from_str(path)
358 .map(Some)
359 .map_err(|e| e.message().to_owned()),
360 };
361 if let Err(message) = &parsed {
362 cx.issue(IssueCode::InvalidPath, message.clone());
363 }
364 if let Some(default) = default {
368 cx.at("var", |cx| cx.at(1, |cx| walk(default, cx, None)));
369 }
370 let path = parsed.ok()?;
371 holes.push(Hole {
372 path,
373 default: default.map_or(OwnedDataValue::Null, OwnedDataValue::from),
374 });
375 Some(Node::Hole(holes.len() - 1))
376}
377
378impl ConfigField for Interpolated {
379 const NULL_IS_VALUE: bool = true;
381 fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self> {
382 compile(raw, cx)
383 }
384 fn visit(&self, v: &mut Visit<'_>) {
385 v.interpolated(self);
386 }
387 fn resolve(&self, r: &mut dyn Resolver) -> crate::Result<Option<OwnedDataValue>> {
389 r.render(self).map(Some)
390 }
391 fn kind() -> FieldKind {
392 FieldKind::Interpolated
393 }
394}
395
396impl ExprEngine {
397 pub fn interpolated(&self, raw: &Value) -> Result<Interpolated, Vec<Issue>> {
407 let mut issues = Vec::new();
408 let doc = {
409 let mut cx = CompileCx::new(self, &mut issues);
410 compile(raw, &mut cx)
411 };
412 doc.ok_or(issues)
413 }
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419 use serde_json::json;
420
421 fn doc(raw: &Value) -> Interpolated {
422 ExprEngine::default().interpolated(raw).unwrap()
423 }
424
425 fn ctx(v: Value) -> OwnedDataValue {
426 OwnedDataValue::from(&v)
427 }
428
429 fn render(doc: &Interpolated, context: &OwnedDataValue) -> String {
433 let rendered = doc.render_with(|hole| {
434 hole.path
435 .as_ref()
436 .and_then(|p| p.get(context))
437 .map_or(Filled::Default(&hole.default), Filled::lent)
438 });
439 let wire = serde_json::to_string(&rendered).unwrap();
440 assert_eq!(rendered.to_owned().to_json_string(), wire);
441 wire
442 }
443
444 #[test]
445 fn mongo_write_resolves_with_dollar_keys_intact() {
446 let raw = json!({
447 "filter": { "_id": { "$oid": { "var": "data.payload.object.id" } } },
448 "update": { "$set": {
449 "deleted": false,
450 "payload": { "var": "data.payload" },
451 "updated_at": { "$date": { "var": "metadata.timestamp" } }
452 } },
453 "upsert": true
454 });
455 let c = ctx(json!({
456 "data": {"payload": {"n": 1, "object": {"id": "abc"}}},
457 "metadata": {"timestamp": "2026-01-01"},
458 "temp_data": {}
459 }));
460 let out = render(&doc(&raw), &c);
461 assert_eq!(
462 out,
463 r#"{"filter":{"_id":{"$oid":"abc"}},"update":{"$set":{"deleted":false,"payload":{"n":1,"object":{"id":"abc"}},"updated_at":{"$date":"2026-01-01"}}},"upsert":true}"#
464 );
465 }
466
467 #[test]
468 fn a_missing_hole_takes_its_default_or_null() {
469 let c = ctx(json!({"data": {}, "metadata": {}, "temp_data": {}}));
470 let d = doc(&json!({"a": {"var": ["data.x", 7]}, "b": {"var": "data.y"}}));
471 assert_eq!(render(&d, &c), r#"{"a":7,"b":null}"#);
472 }
473
474 #[test]
475 fn a_payload_hole_always_yields_its_default() {
476 let c = ctx(json!({"data": {"x": 1}, "metadata": {}, "temp_data": {}}));
477 let d = doc(&json!({"a": {"var": ["payload.x", "d"]}}));
478 assert_eq!(render(&d, &c), r#"{"a":"d"}"#);
479 }
480
481 #[test]
482 fn a_hole_free_subtree_is_one_literal_node() {
483 let d = doc(&json!({"a": [1, {"b": 2}], "c": {"var": "data.x"}}));
484 assert_eq!(d.holes.len(), 1);
485 match &d.node {
486 Node::Object(children) => {
487 assert!(matches!(children[0].1, Node::Literal(_)));
488 assert!(matches!(children[1].1, Node::Hole(0)));
489 }
490 _ => panic!("expected an object node"),
491 }
492 }
493
494 #[test]
495 fn a_val_and_an_operator_shaped_default_resolve_as_written() {
496 let c = ctx(json!({"data": {"x": 5}, "metadata": {}, "temp_data": {}}));
497 let d = doc(&json!({
498 "h": {"var": ["data.missing", {"var": "data.x"}]},
499 "v": {"val": ["data", "x"]}
500 }));
501 assert_eq!(
502 render(&d, &c),
503 r#"{"h":{"var":"data.x"},"v":{"val":["data","x"]}}"#
504 );
505 }
506
507 #[test]
508 fn holes_inside_arrays_are_filled() {
509 let c = ctx(json!({"data": {"x": 5}, "metadata": {}, "temp_data": {}}));
510 let d = doc(&json!([1, {"var": "data.x"}]));
511 assert_eq!(render(&d, &c), "[1,5]");
512 }
513}