use super::config::{CompileCx, ConfigField, FieldKind, Resolver, Visit};
use super::engine::ExprEngine;
use super::path::Path;
use super::secrets::SECRET_OPERATOR;
use crate::base::{Issue, IssueCode};
use datavalue::{DataValue, OwnedDataValue};
use serde::ser::{Serialize, SerializeMap, SerializeSeq, Serializer};
use serde_json::Value;
use std::borrow::Cow;
use std::fmt;
use std::str::FromStr;
pub struct Interpolated {
raw: Value,
node: Node,
holes: Vec<Hole>,
}
pub(crate) struct Hole {
pub path: Option<Path>,
pub default: OwnedDataValue,
}
enum Node {
Literal(Value),
Hole(usize),
Object(Vec<(String, Self)>),
Array(Vec<Self>),
}
impl Interpolated {
pub fn raw(&self) -> &Value {
&self.raw
}
pub fn holes(&self) -> impl Iterator<Item = &Path> {
self.holes.iter().filter_map(|h| h.path.as_ref())
}
pub(crate) fn render_with<'s>(
&'s self,
fill: impl FnMut(&'s Hole) -> Filled<'s>,
) -> Rendered<'s> {
Rendered {
doc: self,
filled: self.holes.iter().map(fill).collect(),
}
}
}
impl fmt::Debug for Interpolated {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Interpolated")
.field("raw", &self.raw)
.finish()
}
}
pub(crate) enum Filled<'s> {
Owned(Cow<'s, OwnedDataValue>),
View(DataValue<'s>),
Default(&'s OwnedDataValue),
}
impl<'s> Filled<'s> {
pub(crate) fn lent(value: &'s OwnedDataValue) -> Self {
Self::Owned(Cow::Borrowed(value))
}
pub(crate) fn into_cow(self) -> Cow<'s, OwnedDataValue> {
match self {
Self::Owned(value) => value,
Self::View(view) => Cow::Owned(view.to_owned()),
Self::Default(value) => Cow::Borrowed(value),
}
}
fn to_owned_value(&self) -> OwnedDataValue {
match self {
Self::Owned(value) => value.as_ref().clone(),
Self::View(view) => view.to_owned(),
Self::Default(value) => (*value).clone(),
}
}
}
impl Serialize for Filled<'_> {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
match self {
Self::Owned(value) => value.serialize(s),
Self::View(view) => view.serialize(s),
Self::Default(value) => value.serialize(s),
}
}
}
pub struct Rendered<'s> {
doc: &'s Interpolated,
filled: Vec<Filled<'s>>,
}
impl Rendered<'_> {
pub fn to_owned(&self) -> OwnedDataValue {
self.doc.node.to_owned_value(&self.filled)
}
#[cfg(test)]
pub(crate) fn filled(&self) -> &[Filled<'_>] {
&self.filled
}
}
impl Serialize for Rendered<'_> {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
At {
node: &self.doc.node,
filled: &self.filled,
}
.serialize(s)
}
}
impl fmt::Debug for Rendered<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Rendered")
.field("raw", &self.doc.raw)
.field("holes", &self.filled.len())
.finish_non_exhaustive()
}
}
struct At<'r, 's> {
node: &'r Node,
filled: &'r [Filled<'s>],
}
impl Serialize for At<'_, '_> {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
let at = |node| Self {
node,
filled: self.filled,
};
match self.node {
Node::Literal(value) => value.serialize(s),
Node::Hole(i) => self.filled[*i].serialize(s),
Node::Object(children) => {
let mut map = s.serialize_map(Some(children.len()))?;
for (key, node) in children {
map.serialize_entry(key, &at(node))?;
}
map.end()
}
Node::Array(children) => {
let mut seq = s.serialize_seq(Some(children.len()))?;
for node in children {
seq.serialize_element(&at(node))?;
}
seq.end()
}
}
}
}
impl Node {
fn to_owned_value(&self, filled: &[Filled<'_>]) -> OwnedDataValue {
match self {
Self::Literal(value) => OwnedDataValue::from(value),
Self::Hole(i) => filled[*i].to_owned_value(),
Self::Object(children) => OwnedDataValue::Object(
children
.iter()
.map(|(key, node)| (key.clone(), node.to_owned_value(filled)))
.collect(),
),
Self::Array(children) => OwnedDataValue::Array(
children
.iter()
.map(|node| node.to_owned_value(filled))
.collect(),
),
}
}
}
pub(crate) fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Interpolated> {
let before = cx.rejected();
let mut holes = Vec::new();
let node = walk(raw, cx, Some(&mut holes));
(cx.rejected() == before).then(|| Interpolated {
raw: raw.clone(),
node: node.unwrap_or_else(|| Node::Literal(raw.clone())),
holes,
})
}
fn walk(raw: &Value, cx: &mut CompileCx<'_>, mut holes: Option<&mut Vec<Hole>>) -> Option<Node> {
match raw {
Value::Object(o) => {
if o.len() == 1 {
let (key, arg) = o.iter().next().expect("one entry");
if key == "var"
&& let Some(holes) = holes.as_deref_mut()
{
return hole(arg, cx, holes);
}
match key.as_str() {
SECRET_OPERATOR => {
cx.issue(
IssueCode::SecretInDocument,
"a document cannot read a secret",
);
return None;
}
name if cx.exprs().operator(name).is_some() => {
cx.at(name, |cx| {
cx.issue(
IssueCode::OperatorInDocument,
format!(
"`{name}` is not evaluated in a document, it stays data; \
only `var` holes are live"
),
);
});
}
_ => {}
}
}
let mut children: Option<Vec<Option<Node>>> = None;
for (i, (k, v)) in o.iter().enumerate() {
if let Some(node) = cx.at(k.as_str(), |cx| walk(v, cx, holes.as_deref_mut())) {
let list = children.get_or_insert_with(|| {
let mut list = Vec::with_capacity(o.len());
list.resize_with(i, || None);
list
});
list.push(Some(node));
} else if let Some(list) = &mut children {
list.push(None);
}
}
children.map(|children| {
Node::Object(
o.iter()
.zip(children)
.map(|((k, v), node)| {
(k.clone(), node.unwrap_or_else(|| Node::Literal(v.clone())))
})
.collect(),
)
})
}
Value::Array(a) => {
let mut children: Option<Vec<Option<Node>>> = None;
for (i, v) in a.iter().enumerate() {
if let Some(node) = cx.at(i, |cx| walk(v, cx, holes.as_deref_mut())) {
let list = children.get_or_insert_with(|| {
let mut list = Vec::with_capacity(a.len());
list.resize_with(i, || None);
list
});
list.push(Some(node));
} else if let Some(list) = &mut children {
list.push(None);
}
}
children.map(|children| {
Node::Array(
a.iter()
.zip(children)
.map(|(v, node)| node.unwrap_or_else(|| Node::Literal(v.clone())))
.collect(),
)
})
}
_ => None,
}
}
fn hole(arg: &Value, cx: &mut CompileCx<'_>, holes: &mut Vec<Hole>) -> Option<Node> {
let (path, default) = match arg {
Value::String(p) => (Some(p.as_str()), None),
Value::Array(a) if matches!(a.len(), 1 | 2) => (a[0].as_str(), a.get(1)),
_ => (None, None),
};
let parsed = match path {
None => Err("`var` takes a path string, or [path] or [path, default]".to_owned()),
Some(path) if path.split('.').next() == Some("payload") => {
cx.issue(
IssueCode::PayloadVar,
format!("`{path}` reads the payload, which is not in the context; the hole always yields its default"),
);
Ok(None)
}
Some(path) => Path::from_str(path)
.map(Some)
.map_err(|e| e.message().to_owned()),
};
if let Err(message) = &parsed {
cx.issue(IssueCode::InvalidPath, message.clone());
}
if let Some(default) = default {
cx.at("var", |cx| cx.at(1, |cx| walk(default, cx, None)));
}
let path = parsed.ok()?;
holes.push(Hole {
path,
default: default.map_or(OwnedDataValue::Null, OwnedDataValue::from),
});
Some(Node::Hole(holes.len() - 1))
}
impl ConfigField for Interpolated {
const NULL_IS_VALUE: bool = true;
fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self> {
compile(raw, cx)
}
fn visit(&self, v: &mut Visit<'_>) {
v.interpolated(self);
}
fn resolve(&self, r: &mut dyn Resolver) -> crate::Result<Option<OwnedDataValue>> {
r.render(self).map(Some)
}
fn kind() -> FieldKind {
FieldKind::Interpolated
}
}
impl ExprEngine {
pub fn interpolated(&self, raw: &Value) -> Result<Interpolated, Vec<Issue>> {
let mut issues = Vec::new();
let doc = {
let mut cx = CompileCx::new(self, &mut issues);
compile(raw, &mut cx)
};
doc.ok_or(issues)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn doc(raw: &Value) -> Interpolated {
ExprEngine::default().interpolated(raw).unwrap()
}
fn ctx(v: Value) -> OwnedDataValue {
OwnedDataValue::from(&v)
}
fn render(doc: &Interpolated, context: &OwnedDataValue) -> String {
let rendered = doc.render_with(|hole| {
hole.path
.as_ref()
.and_then(|p| p.get(context))
.map_or(Filled::Default(&hole.default), Filled::lent)
});
let wire = serde_json::to_string(&rendered).unwrap();
assert_eq!(rendered.to_owned().to_json_string(), wire);
wire
}
#[test]
fn mongo_write_resolves_with_dollar_keys_intact() {
let raw = json!({
"filter": { "_id": { "$oid": { "var": "data.payload.object.id" } } },
"update": { "$set": {
"deleted": false,
"payload": { "var": "data.payload" },
"updated_at": { "$date": { "var": "metadata.timestamp" } }
} },
"upsert": true
});
let c = ctx(json!({
"data": {"payload": {"n": 1, "object": {"id": "abc"}}},
"metadata": {"timestamp": "2026-01-01"},
"temp_data": {}
}));
let out = render(&doc(&raw), &c);
assert_eq!(
out,
r#"{"filter":{"_id":{"$oid":"abc"}},"update":{"$set":{"deleted":false,"payload":{"n":1,"object":{"id":"abc"}},"updated_at":{"$date":"2026-01-01"}}},"upsert":true}"#
);
}
#[test]
fn a_missing_hole_takes_its_default_or_null() {
let c = ctx(json!({"data": {}, "metadata": {}, "temp_data": {}}));
let d = doc(&json!({"a": {"var": ["data.x", 7]}, "b": {"var": "data.y"}}));
assert_eq!(render(&d, &c), r#"{"a":7,"b":null}"#);
}
#[test]
fn a_payload_hole_always_yields_its_default() {
let c = ctx(json!({"data": {"x": 1}, "metadata": {}, "temp_data": {}}));
let d = doc(&json!({"a": {"var": ["payload.x", "d"]}}));
assert_eq!(render(&d, &c), r#"{"a":"d"}"#);
}
#[test]
fn a_hole_free_subtree_is_one_literal_node() {
let d = doc(&json!({"a": [1, {"b": 2}], "c": {"var": "data.x"}}));
assert_eq!(d.holes.len(), 1);
match &d.node {
Node::Object(children) => {
assert!(matches!(children[0].1, Node::Literal(_)));
assert!(matches!(children[1].1, Node::Hole(0)));
}
_ => panic!("expected an object node"),
}
}
#[test]
fn a_val_and_an_operator_shaped_default_resolve_as_written() {
let c = ctx(json!({"data": {"x": 5}, "metadata": {}, "temp_data": {}}));
let d = doc(&json!({
"h": {"var": ["data.missing", {"var": "data.x"}]},
"v": {"val": ["data", "x"]}
}));
assert_eq!(
render(&d, &c),
r#"{"h":{"var":"data.x"},"v":{"val":["data","x"]}}"#
);
}
#[test]
fn holes_inside_arrays_are_filled() {
let c = ctx(json!({"data": {"x": 5}, "metadata": {}, "temp_data": {}}));
let d = doc(&json!([1, {"var": "data.x"}]));
assert_eq!(render(&d, &c), "[1,5]");
}
}