Skip to main content

dataflow_rs/expr/
document.rs

1//! Interpolated documents: data with `var` holes, `$` keys kept verbatim.
2
3use 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
15/// A document of `var` holes in otherwise literal data, `$` keys kept
16/// verbatim. It is for payloads such as a MongoDB filter, where `$oid` is data
17/// and `{"var": ...}` is the only live part.
18///
19/// Any other operator-shaped node, `val` included, stays data and is reported
20/// as `OPERATOR_IN_DOCUMENT`; a `secret` node is refused as
21/// `SECRET_IN_DOCUMENT`. A hole's default is data too, and is checked the same
22/// way.
23pub struct Interpolated {
24    raw: Value,
25    node: Node,
26    holes: Vec<Hole>,
27}
28
29/// A hole: the path it reads and what it yields when that is missing.
30pub(crate) struct Hole {
31    /// `None` when rooted at `payload`, which is never in the context: the
32    /// hole always yields its default.
33    pub path: Option<Path>,
34    pub default: OwnedDataValue,
35}
36
37enum Node {
38    /// A subtree with no hole, kept as authored.
39    Literal(Value),
40    Hole(usize),
41    Object(Vec<(String, Self)>),
42    Array(Vec<Self>),
43}
44
45impl Interpolated {
46    /// The document as authored, as a `serde_json::Value`, its objects'
47    /// keys in the order they were written: the crate turns on serde_json's
48    /// `preserve_order`.
49    pub fn raw(&self) -> &Value {
50        &self.raw
51    }
52
53    /// The paths the holes read, in document order. A hole rooted at
54    /// `payload` reads nothing and is not listed.
55    pub fn holes(&self) -> impl Iterator<Item = &Path> {
56        self.holes.iter().filter_map(|h| h.path.as_ref())
57    }
58
59    /// The document with each hole filled by `fill`, called once per hole in
60    /// document order. Nothing is copied: the skeleton is this document's,
61    /// and each hole holds what `fill` lent it.
62    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
81/// One hole's value, borrowed where it can be.
82pub(crate) enum Filled<'s> {
83    /// An owned value: lent from the owned tree, a log or an overlay, or
84    /// converted when only a spliced root knew it.
85    Owned(Cow<'s, OwnedDataValue>),
86    /// A value of the run's view, lent.
87    View(DataValue<'s>),
88    /// Nothing was there: the hole's default.
89    Default(&'s OwnedDataValue),
90}
91
92impl<'s> Filled<'s> {
93    /// An owned value, lent.
94    pub(crate) fn lent(value: &'s OwnedDataValue) -> Self {
95        Self::Owned(Cow::Borrowed(value))
96    }
97
98    /// The value as an owned one: lent unless it is a view, which is
99    /// converted.
100    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
127/// An [`Interpolated`] document with its holes resolved, borrowed from the
128/// compiled document and the context the call sees.
129///
130/// Made by [`Ctx::render`](crate::function::Ctx::render). Each hole was
131/// looked up once, when it was made; serializing writes the document's
132/// skeleton with each hole's value in place, straight into the serializer
133/// (BSON, SQL parameters, JWT claims, JSON), and builds no tree.
134/// [`Rendered::to_owned`] builds one for the caller that needs it.
135pub struct Rendered<'s> {
136    doc: &'s Interpolated,
137    /// Each hole's value, in the document's hole order.
138    filled: Vec<Filled<'s>>,
139}
140
141impl Rendered<'_> {
142    /// The document as a tree, each hole's value copied in.
143    ///
144    /// It serializes as the `Rendered` does, but for one value: an integer
145    /// literal above `i64::MAX`, which `OwnedDataValue` holds as a float. So
146    /// `18446744073709551615` is written with its digits by the `Rendered`
147    /// and as `1.8446744073709552e+19` by this tree.
148    pub fn to_owned(&self) -> OwnedDataValue {
149        self.doc.node.to_owned_value(&self.filled)
150    }
151
152    /// Each hole's value, in hole order.
153    #[cfg(test)]
154    pub(crate) fn filled(&self) -> &[Filled<'_>] {
155        &self.filled
156    }
157}
158
159impl Serialize for Rendered<'_> {
160    /// The skeleton with each hole's value in place: a literal subtree as
161    /// its `serde_json::Value` holds it, objects and arrays entry by entry.
162    /// An object's keys, a literal's or not, come out as authored, in every
163    /// build: a Mongo `{"$sort": {"created": -1, "_id": 1}}` keeps its sort
164    /// order (3.x's default build sorted the keys).
165    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    /// The document as authored and how many holes it filled; never their
176    /// values.
177    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
185/// One node of a rendered document, for its serialization.
186struct 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    /// The node as a tree, the holes' values from `filled`.
220    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
240/// Compiles a document, recording each issue at its node. `None` when any of
241/// them rejects.
242pub(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
253/// Walks one node, returning `Some(Node)` only when the subtree contains at
254/// least one `var` hole (`None` means the subtree is literal data, avoiding
255/// bottom-up clones of hole-free subtrees). `holes` is `None` inside a hole's
256/// default, which is data through and through: a `var` there opens no hole,
257/// and is an operator left as data like any other.
258fn 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
341/// A `var` node: a hole reading its path, its default walked as data.
342fn 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    // The default is never evaluated: a secret or an operator in it would be
365    // handed on as written, so it is reported as anywhere else in the
366    // document.
367    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    /// `null` is a document with no hole.
380    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    /// The document rendered for this call.
388    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    /// Compiles a document of `var` holes in otherwise literal data.
398    ///
399    /// Advisories and Defects (a payload hole, an operator left as data) are
400    /// dropped on `Ok`; a config field compiled through [`ConfigField`] is the
401    /// way to get them.
402    ///
403    /// # Errors
404    ///
405    /// Every issue, when any is Rejected.
406    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    /// `doc` rendered over `context`, an object holding the three roots,
430    /// each hole lent from it, a missing one its default; serialized, after
431    /// checking that the tree `to_owned` builds serializes the same.
432    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}