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 children: Vec<Option<Node>> = o
291 .iter()
292 .map(|(k, v)| cx.at(k.as_str(), |cx| walk(v, cx, holes.as_deref_mut())))
293 .collect();
294 children.iter().any(Option::is_some).then(|| {
295 Node::Object(
296 o.iter()
297 .zip(children)
298 .map(|((k, v), node)| {
299 (k.clone(), node.unwrap_or_else(|| Node::Literal(v.clone())))
300 })
301 .collect(),
302 )
303 })
304 }
305 Value::Array(a) => {
306 let children: Vec<Option<Node>> = a
307 .iter()
308 .enumerate()
309 .map(|(i, v)| cx.at(i, |cx| walk(v, cx, holes.as_deref_mut())))
310 .collect();
311 children.iter().any(Option::is_some).then(|| {
312 Node::Array(
313 a.iter()
314 .zip(children)
315 .map(|(v, node)| node.unwrap_or_else(|| Node::Literal(v.clone())))
316 .collect(),
317 )
318 })
319 }
320 _ => None,
321 }
322}
323
324fn hole(arg: &Value, cx: &mut CompileCx<'_>, holes: &mut Vec<Hole>) -> Option<Node> {
326 let (path, default) = match arg {
327 Value::String(p) => (Some(p.as_str()), None),
328 Value::Array(a) if matches!(a.len(), 1 | 2) => (a[0].as_str(), a.get(1)),
329 _ => (None, None),
330 };
331 let parsed = match path {
332 None => Err("`var` takes a path string, or [path] or [path, default]".to_owned()),
333 Some(path) if path.split('.').next() == Some("payload") => {
334 cx.issue(
335 IssueCode::PayloadVar,
336 format!("`{path}` reads the payload, which is not in the context; the hole always yields its default"),
337 );
338 Ok(None)
339 }
340 Some(path) => Path::from_str(path)
341 .map(Some)
342 .map_err(|e| e.message().to_owned()),
343 };
344 if let Err(message) = &parsed {
345 cx.issue(IssueCode::InvalidPath, message.clone());
346 }
347 if let Some(default) = default {
351 cx.at("var", |cx| cx.at(1, |cx| walk(default, cx, None)));
352 }
353 let path = parsed.ok()?;
354 holes.push(Hole {
355 path,
356 default: default.map_or(OwnedDataValue::Null, OwnedDataValue::from),
357 });
358 Some(Node::Hole(holes.len() - 1))
359}
360
361impl ConfigField for Interpolated {
362 const NULL_IS_VALUE: bool = true;
364 fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self> {
365 compile(raw, cx)
366 }
367 fn visit(&self, v: &mut Visit<'_>) {
368 v.interpolated(self);
369 }
370 fn resolve(&self, r: &mut dyn Resolver) -> crate::Result<Option<OwnedDataValue>> {
372 r.render(self).map(Some)
373 }
374 fn kind() -> FieldKind {
375 FieldKind::Interpolated
376 }
377}
378
379impl ExprEngine {
380 pub fn interpolated(&self, raw: &Value) -> Result<Interpolated, Vec<Issue>> {
390 let mut issues = Vec::new();
391 let doc = {
392 let mut cx = CompileCx::new(self, &mut issues);
393 compile(raw, &mut cx)
394 };
395 doc.ok_or(issues)
396 }
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402 use serde_json::json;
403
404 fn doc(raw: &Value) -> Interpolated {
405 ExprEngine::default().interpolated(raw).unwrap()
406 }
407
408 fn ctx(v: Value) -> OwnedDataValue {
409 OwnedDataValue::from(&v)
410 }
411
412 fn render(doc: &Interpolated, context: &OwnedDataValue) -> String {
416 let rendered = doc.render_with(|hole| {
417 hole.path
418 .as_ref()
419 .and_then(|p| p.get(context))
420 .map_or(Filled::Default(&hole.default), Filled::lent)
421 });
422 let wire = serde_json::to_string(&rendered).unwrap();
423 assert_eq!(rendered.to_owned().to_json_string(), wire);
424 wire
425 }
426
427 #[test]
428 fn mongo_write_resolves_with_dollar_keys_intact() {
429 let raw = json!({
430 "filter": { "_id": { "$oid": { "var": "data.payload.object.id" } } },
431 "update": { "$set": {
432 "deleted": false,
433 "payload": { "var": "data.payload" },
434 "updated_at": { "$date": { "var": "metadata.timestamp" } }
435 } },
436 "upsert": true
437 });
438 let c = ctx(json!({
439 "data": {"payload": {"n": 1, "object": {"id": "abc"}}},
440 "metadata": {"timestamp": "2026-01-01"},
441 "temp_data": {}
442 }));
443 let out = render(&doc(&raw), &c);
444 assert_eq!(
445 out,
446 r#"{"filter":{"_id":{"$oid":"abc"}},"update":{"$set":{"deleted":false,"payload":{"n":1,"object":{"id":"abc"}},"updated_at":{"$date":"2026-01-01"}}},"upsert":true}"#
447 );
448 }
449
450 #[test]
451 fn a_missing_hole_takes_its_default_or_null() {
452 let c = ctx(json!({"data": {}, "metadata": {}, "temp_data": {}}));
453 let d = doc(&json!({"a": {"var": ["data.x", 7]}, "b": {"var": "data.y"}}));
454 assert_eq!(render(&d, &c), r#"{"a":7,"b":null}"#);
455 }
456
457 #[test]
458 fn a_payload_hole_always_yields_its_default() {
459 let c = ctx(json!({"data": {"x": 1}, "metadata": {}, "temp_data": {}}));
460 let d = doc(&json!({"a": {"var": ["payload.x", "d"]}}));
461 assert_eq!(render(&d, &c), r#"{"a":"d"}"#);
462 }
463
464 #[test]
465 fn a_hole_free_subtree_is_one_literal_node() {
466 let d = doc(&json!({"a": [1, {"b": 2}], "c": {"var": "data.x"}}));
467 assert_eq!(d.holes.len(), 1);
468 match &d.node {
469 Node::Object(children) => {
470 assert!(matches!(children[0].1, Node::Literal(_)));
471 assert!(matches!(children[1].1, Node::Hole(0)));
472 }
473 _ => panic!("expected an object node"),
474 }
475 }
476
477 #[test]
478 fn a_val_and_an_operator_shaped_default_resolve_as_written() {
479 let c = ctx(json!({"data": {"x": 5}, "metadata": {}, "temp_data": {}}));
480 let d = doc(&json!({
481 "h": {"var": ["data.missing", {"var": "data.x"}]},
482 "v": {"val": ["data", "x"]}
483 }));
484 assert_eq!(
485 render(&d, &c),
486 r#"{"h":{"var":"data.x"},"v":{"val":["data","x"]}}"#
487 );
488 }
489
490 #[test]
491 fn holes_inside_arrays_are_filled() {
492 let c = ctx(json!({"data": {"x": 5}, "metadata": {}, "temp_data": {}}));
493 let d = doc(&json!([1, {"var": "data.x"}]));
494 assert_eq!(render(&d, &c), "[1,5]");
495 }
496}