use super::erased::BoundCall;
use super::overlay::Overlay;
use crate::base::{
AuthoredPath, CancelToken, Change, Error, ErrorKind, ErrorRecord, Instant, PathSeg, ProgramId,
Result, TaskIx, WorkflowIx,
};
use crate::expr::arena::with_arena;
use crate::expr::path;
use crate::expr::{
CRATE, Expr, ExprEngine, ExprVisitor, Filled, FromValue, Interpolated, Output, Path, Rendered,
ResolveExpr, Resolver, SealedResolver, Seg, reads_secret,
};
use crate::state::{Lookup, Reader, splice};
use bumpalo::Bump;
use datavalue::{DataValue, OwnedDataValue};
use serde_json::Value;
use std::borrow::Cow;
use std::fmt;
use std::sync::Arc;
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub struct CallInfo<'a> {
pub program: ProgramId,
pub workflow: WorkflowIx,
pub task: TaskIx,
pub function: &'a str,
pub workflow_id: &'a str,
pub task_id: &'a str,
pub channel: Option<&'a str>,
pub routing_bucket: Option<u8>,
pub loop_counter: Option<i64>,
pub element_index: Option<usize>,
}
pub struct Ctx<'r> {
start: CallStart<'r>,
callee: &'r dyn BoundCall,
overlay: Overlay,
changes: Vec<Change>,
errors: Vec<ErrorRecord>,
}
#[derive(Clone, Copy)]
pub(crate) struct CallStart<'r> {
pub(crate) reader: Reader<'r>,
pub(crate) binding: Option<&'r Binding<'r>>,
pub(crate) payload: &'r OwnedDataValue,
pub(crate) exprs: &'r ExprEngine,
pub(crate) info: CallInfo<'r>,
pub(crate) deadline: Option<Instant>,
pub(crate) cancel: Option<&'r CancelToken>,
pub(crate) capture: bool,
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum Primary<'r> {
Undeclared,
Unset(&'r str),
Set(&'r Output),
}
pub(crate) struct Binding<'r> {
pub(crate) item: &'r Path,
pub(crate) value: &'r OwnedDataValue,
pub(crate) index: &'r Path,
pub(crate) index_value: OwnedDataValue,
}
impl Binding<'_> {
fn splice_onto<'a>(&'a self, root: DataValue<'a>, arena: &'a Bump) -> DataValue<'a> {
let root =
splice::set(arena, root, self.item.segs(), self.value.view_in(arena)).unwrap_or(root);
splice::set(
arena,
root,
self.index.segs(),
self.index_value.view_in(arena),
)
.unwrap_or(root)
}
fn get<'s>(
&'s self,
path: &Path,
root: DataValue<'_>,
) -> Option<Option<Cow<'s, OwnedDataValue>>> {
for (at, value) in [(self.item, self.value), (self.index, &self.index_value)] {
if let Some(rest) = below(path, at) {
return Some(path::get(value, rest).map(Cow::Borrowed));
}
}
if [self.item, self.index]
.iter()
.any(|at| below(at, path).is_some())
{
return Some(with_arena(|arena| {
splice::get(self.splice_onto(root, arena), path.segs())
.map(|v| Cow::Owned(v.to_owned()))
}));
}
None
}
}
fn below<'p>(path: &'p Path, prefix: &Path) -> Option<&'p [Seg]> {
let (path, prefix) = (path.segs(), prefix.segs());
(path.len() >= prefix.len() && path.iter().zip(prefix).all(|(a, b)| a.key == b.key))
.then(|| &path[prefix.len()..])
}
fn with_base<R>(
reader: Reader<'_>,
binding: Option<&Binding<'_>>,
f: impl FnOnce(DataValue<'_>) -> R,
) -> R {
match binding {
None => f(*reader.root),
Some(binding) => with_arena(|arena| f(binding.splice_onto(*reader.root, arena))),
}
}
impl<'r> Ctx<'r> {
pub(crate) fn new(start: CallStart<'r>, callee: &'r dyn BoundCall) -> Self {
Self {
start,
callee,
overlay: Overlay::default(),
changes: Vec::new(),
errors: Vec::new(),
}
}
pub fn info(&self) -> &CallInfo<'r> {
&self.start.info
}
pub(crate) fn callee(&self) -> &'r dyn BoundCall {
self.callee
}
pub fn input(&self) -> Result<OwnedDataValue> {
let mut input = self.callee.resolve(&mut Redacted(self))?;
self.callee.visit(&mut Redact(&mut input));
Ok(input)
}
pub fn resolve<C: super::FunctionConfig>(&self, cfg: &C) -> Result<OwnedDataValue> {
cfg.resolve(&mut &*self)
}
pub fn payload(&self) -> &OwnedDataValue {
self.start.payload
}
pub fn exprs(&self) -> &ExprEngine {
self.start.exprs
}
pub fn secret(&self, name: &str) -> Option<&OwnedDataValue> {
self.start.exprs.secrets().get(name)
}
pub fn deadline(&self) -> Option<Instant> {
self.start.deadline
}
pub fn cancel(&self) -> Option<&CancelToken> {
self.start.cancel
}
pub fn get(&self, path: &Path) -> Option<Cow<'_, OwnedDataValue>> {
self.find(path).map(Filled::into_cow)
}
fn find(&self, path: &Path) -> Option<Filled<'_>> {
match self.overlay.lookup(path) {
Lookup::Clean => {}
Lookup::Owned(v) => return v.map(Filled::lent),
Lookup::Dirty => {
return with_arena(|arena| {
splice::get(*self.root_in(arena), path.segs())
.map(|v| Filled::Owned(Cow::Owned(v.to_owned())))
});
}
}
if let Some(found) = self
.start
.binding
.and_then(|b| b.get(path, *self.start.reader.root))
{
return found.map(Filled::Owned);
}
self.start.reader.find(path)
}
pub fn render<'s>(&'s self, doc: &'s Interpolated) -> Rendered<'s> {
doc.render_with(|hole| {
hole.path
.as_ref()
.and_then(|path| self.find(path))
.unwrap_or(Filled::Default(&hole.default))
})
}
fn holds(&self, path: &Path) -> bool {
match self.overlay.lookup(path) {
Lookup::Owned(v) => v.is_some(),
Lookup::Dirty => {
with_arena(|arena| splice::get(*self.root_in(arena), path.segs()).is_some())
}
Lookup::Clean => with_base(self.start.reader, self.start.binding, |base| {
splice::get(base, path.segs()).is_some()
}),
}
}
pub fn eval<'e, T: FromValue>(&self, expr: &'e Expr<T>) -> Result<T::Out<'e>> {
if let Some(c) = expr.constant() {
return Ok(T::lend(c));
}
with_arena(|arena| {
let root = self.root_in(arena);
self.start.exprs.eval_view(expr, root, arena)
})
}
fn eval_owned(&self, expr: &dyn ResolveExpr) -> Result<OwnedDataValue> {
if let Some(constant) = expr.constant_value(CRATE) {
return Ok(constant);
}
with_arena(|arena| {
let root = self.root_in(arena);
let view = self.start.exprs.eval_in(expr.logic(CRATE), root, arena)?;
expr.value_of(view, CRATE)
})
}
fn root_in<'a>(&'a self, arena: &'a Bump) -> &'a DataValue<'a> {
if self.start.binding.is_none() && self.overlay.is_empty() {
return self.start.reader.root;
}
let base = match self.start.binding {
None => *self.start.reader.root,
Some(binding) => binding.splice_onto(*self.start.reader.root, arena),
};
if self.overlay.is_empty() {
return arena.alloc(base);
}
arena.alloc(self.overlay.splice_onto(base, arena))
}
pub fn set(&mut self, path: &Path, value: OwnedDataValue) -> Result<()> {
path.check_set(&value)?;
let first = (self.overlay.is_empty() && self.start.binding.is_none())
.then(|| splice::resolve(*self.start.reader.root, path.segs()));
if self.start.capture {
let old_value = match first {
Some(None) => None,
Some(Some(r)) if r.created.is_some() => None,
Some(Some(r)) => self.start.reader.get_resolved(path, r),
None => self.get(path),
};
self.changes.push(Change::assign(
path,
old_value.map_or(OwnedDataValue::Null, Cow::into_owned),
value.clone(),
));
}
if let Some(resolved) = first {
self.overlay.push(path, value, resolved.is_some());
return Ok(());
}
let Self { start, overlay, .. } = self;
with_base(start.reader, start.binding, |base| {
overlay.set(base, path, value)
});
Ok(())
}
pub fn emit(&mut self, out: &Output, value: OwnedDataValue) -> Result<()> {
let path = match out.constant() {
Some(path) => Cow::Borrowed(path),
None => self.eval(out.path())?,
};
self.set(&path, value)
}
pub fn primary_output(&self) -> Option<&'r Output> {
let callee: &'r dyn BoundCall = self.callee;
match callee.primary() {
Primary::Set(out) => Some(out),
Primary::Unset(_) | Primary::Undeclared => None,
}
}
pub fn emit_default(&mut self, value: OwnedDataValue) -> Result<()> {
let function = self.start.info.function;
let callee = self.callee;
match callee.primary() {
Primary::Set(out) => self.emit(out, value),
Primary::Unset(field) => Err(Error::new(
ErrorKind::Config,
format!("`{function}`'s primary output `{field}` is not set"),
)),
Primary::Undeclared => Err(Error::new(
ErrorKind::Config,
format!("`{function}` has no primary output"),
)),
}
}
pub fn remove(&mut self, path: &Path) -> Result<()> {
path.check_remove()?;
if self.start.capture {
let Some(old_value) = self.get(path).map(Cow::into_owned) else {
return Ok(());
};
self.changes
.push(Change::removal(Arc::clone(path.dotted()), old_value));
} else if !self.holds(path) {
return Ok(());
}
self.overlay.remove(path);
Ok(())
}
pub fn record_error(&mut self, e: ErrorRecord) {
self.errors.push(e);
}
pub(crate) fn settle(self) -> (Overlay, Vec<Change>, Vec<ErrorRecord>) {
(self.overlay, self.changes, self.errors)
}
}
impl SealedResolver for &Ctx<'_> {}
impl Resolver for &Ctx<'_> {
fn eval(&mut self, expr: &dyn ResolveExpr) -> Result<OwnedDataValue> {
Ctx::eval_owned(self, expr)
}
fn render(&mut self, doc: &Interpolated) -> Result<OwnedDataValue> {
Ok(Ctx::render(self, doc).to_owned())
}
}
struct Redacted<'c, 'r>(&'c Ctx<'r>);
impl SealedResolver for Redacted<'_, '_> {}
impl Resolver for Redacted<'_, '_> {
fn eval(&mut self, expr: &dyn ResolveExpr) -> Result<OwnedDataValue> {
if reads_secret(expr.raw()) {
return Ok(OwnedDataValue::Null);
}
self.0.eval_owned(expr)
}
fn render(&mut self, doc: &Interpolated) -> Result<OwnedDataValue> {
Ok(self.0.render(doc).to_owned())
}
}
struct Redact<'v>(&'v mut OwnedDataValue);
impl ExprVisitor for Redact<'_> {
fn expr(&mut self, at: &AuthoredPath, raw: &Value, _records: bool) {
if reads_secret(raw)
&& let Some(slot) = slot_at(self.0, at)
{
*slot = OwnedDataValue::Null;
}
}
}
fn slot_at<'v>(value: &'v mut OwnedDataValue, at: &AuthoredPath) -> Option<&'v mut OwnedDataValue> {
at.segs()
.iter()
.try_fold(value, |value, seg| match (value, seg) {
(OwnedDataValue::Object(entries), PathSeg::Key(key)) => entries
.iter_mut()
.find(|(k, _)| **k == **key)
.map(|(_, v)| v),
(OwnedDataValue::Array(items), PathSeg::Index(i)) => items.get_mut(*i),
_ => None,
})
}
impl fmt::Debug for Ctx<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Ctx")
.field("info", &self.start.info)
.field("overlay_empty", &self.overlay.is_empty())
.finish_non_exhaustive()
}
}
#[cfg(test)]
pub(crate) mod testing {
use super::*;
use crate::function::{BoxFuture, Outcome};
use crate::state::{RunContext, Tuning};
pub(crate) struct NoCallee;
impl BoundCall for NoCallee {
fn call<'a>(&'a self, _ctx: &'a mut Ctx<'_>) -> BoxFuture<'a, Result<Outcome>> {
Box::pin(std::future::ready(Ok(Outcome::Ok)))
}
fn visit(&self, _v: &mut dyn ExprVisitor) {}
fn primary(&self) -> Primary<'_> {
Primary::Undeclared
}
fn resolve(&self, _r: &mut dyn Resolver) -> Result<OwnedDataValue> {
Ok(OwnedDataValue::Null)
}
}
pub(crate) fn with_ctx<R>(
context: &serde_json::Value,
info: CallInfo<'_>,
binding: Option<&Binding<'_>>,
f: impl FnOnce(Ctx<'_>) -> R,
) -> R {
let exprs = ExprEngine::default();
let payload = OwnedDataValue::Null;
let mut slot = OwnedDataValue::from(context);
let mut rc = RunContext::new(&mut slot, Tuning::default());
let reader = rc.reader();
f(Ctx::new(
CallStart {
reader,
binding,
payload: &payload,
exprs: &exprs,
info,
deadline: None,
cancel: None,
capture: true,
},
&NoCallee,
))
}
}
#[cfg(test)]
mod tests {
use super::testing::{NoCallee, with_ctx};
use super::*;
use crate::state::{RunContext, Tuning};
use serde_json::{Value, json};
fn send_sync<T: Send + Sync>() {}
#[test]
fn ctx_is_send_and_sync() {
send_sync::<Ctx<'static>>();
}
fn info() -> CallInfo<'static> {
CallInfo {
program: ProgramId::next(),
workflow: WorkflowIx::new(0),
task: TaskIx::new(0),
function: "f",
workflow_id: "w",
task_id: "t",
channel: None,
routing_bucket: None,
loop_counter: None,
element_index: None,
}
}
fn parse(dotted: &str) -> Path {
dotted.parse().unwrap()
}
fn get(ctx: &Ctx<'_>, dotted: &str) -> Option<Value> {
ctx.get(&parse(dotted)).map(|v| Value::from(&*v))
}
fn eval(ctx: &Ctx<'_>, raw: Value) -> Value {
let expr: Expr = ExprEngine::default().compile(&raw).unwrap();
Value::from(&*ctx.eval(&expr).unwrap())
}
#[test]
fn the_binding_sits_under_the_calls_writes() {
let item = parse("temp_data.item");
let index = parse("temp_data.item_index");
let value = OwnedDataValue::from(&json!({"sku": "A", "qty": 2}));
let binding = Binding {
item: &item,
value: &value,
index: &index,
index_value: OwnedDataValue::from(3),
};
let context = json!({"data": {}, "metadata": {}, "temp_data": {"kept": true}});
with_ctx(&context, info(), Some(&binding), |mut ctx| {
assert_eq!(get(&ctx, "temp_data.item.sku"), Some(json!("A")));
assert_eq!(get(&ctx, "temp_data.item_index"), Some(json!(3)));
assert_eq!(
get(&ctx, "temp_data"),
Some(json!({"kept": true, "item": {"sku": "A", "qty": 2}, "item_index": 3}))
);
assert_eq!(eval(&ctx, json!({"var": "temp_data.item.qty"})), json!(2));
ctx.set(&parse("temp_data.item.qty"), OwnedDataValue::from(5))
.unwrap();
assert_eq!(
get(&ctx, "temp_data.item"),
Some(json!({"sku": "A", "qty": 5}))
);
assert_eq!(eval(&ctx, json!({"var": "temp_data.item.qty"})), json!(5));
assert_eq!(get(&ctx, "temp_data.kept"), Some(json!(true)));
});
}
#[test]
fn a_write_that_cannot_land_records_its_change_and_writes_nothing() {
let context = json!({"data": {"s": "scalar"}, "metadata": {}, "temp_data": {}});
let (overlay, changes, errors) = with_ctx(&context, info(), None, |mut ctx| {
ctx.set(&parse("data.s.below"), OwnedDataValue::from(1))
.unwrap();
ctx.remove(&parse("data.absent")).unwrap();
assert_eq!(get(&ctx, "data.s"), Some(json!("scalar")));
ctx.settle()
});
assert!(errors.is_empty());
let mut slot = OwnedDataValue::from(&context);
let mut rc = RunContext::new(&mut slot, Tuning::default());
overlay.apply(&mut rc);
rc.finish();
assert_eq!(Value::from(&slot), context);
assert_eq!(
serde_json::to_value(&changes).unwrap(),
json!([{"path": "data.s.below", "old_value": null, "new_value": 1}])
);
}
#[test]
fn a_removal_without_capture_removes_what_the_call_sees_and_records_nothing() {
let context = json!({"data": {"a": 1, "list": [1, 2]}, "metadata": {}, "temp_data": {}});
let exprs = ExprEngine::default();
let payload = OwnedDataValue::Null;
let mut slot = OwnedDataValue::from(&context);
let mut rc = RunContext::new(&mut slot, Tuning::default());
let mut ctx = Ctx::new(
CallStart {
reader: rc.reader(),
binding: None,
payload: &payload,
exprs: &exprs,
info: info(),
deadline: None,
cancel: None,
capture: false,
},
&NoCallee,
);
for (dotted, write) in [
("data.a", None),
("data.b", Some(2)),
("data.b", None),
("data.absent", None),
("data.list.0", None),
("data.list.1", None),
] {
match write {
Some(n) => ctx.set(&parse(dotted), OwnedDataValue::from(n)),
None => ctx.remove(&parse(dotted)),
}
.unwrap();
}
assert_eq!(get(&ctx, "data"), Some(json!({"list": [2]})));
let (overlay, changes, errors) = ctx.settle();
assert!(changes.is_empty() && errors.is_empty());
overlay.apply(&mut rc);
rc.finish();
assert_eq!(Value::from(&slot)["data"], json!({"list": [2]}));
}
fn first_writes(writes: &[&str], after_another: bool) -> (Vec<Value>, String) {
let context = json!({"data": {"kept": {"k": 1}, "list": [1, null], "s": "scalar"},
"metadata": {}, "temp_data": {}});
let exprs = ExprEngine::default();
let payload = OwnedDataValue::Null;
let mut slot = OwnedDataValue::from(&context);
let mut rc = RunContext::new(&mut slot, Tuning::default());
rc.read(|_, _| ());
rc.set(&parse("data.run"), OwnedDataValue::from(7), None);
let mut olds = Vec::new();
let mut overlays = Vec::new();
for dotted in writes {
let mut ctx = Ctx::new(
CallStart {
reader: rc.reader(),
binding: None,
payload: &payload,
exprs: &exprs,
info: info(),
deadline: None,
cancel: None,
capture: true,
},
&NoCallee,
);
if after_another {
ctx.set(&parse("temp_data.other"), OwnedDataValue::from(0))
.unwrap();
}
ctx.set(&parse(dotted), OwnedDataValue::from("w")).unwrap();
let (overlay, changes, _) = ctx.settle();
olds.push(serde_json::to_value(&changes.last().unwrap().old_value).unwrap());
overlays.push(overlay);
}
for overlay in overlays {
overlay.apply(&mut rc);
}
rc.finish();
let data = parse("data").get(&slot).map(serde_json::to_string);
(olds, data.unwrap().unwrap())
}
#[test]
fn a_first_write_reads_what_was_there_as_a_later_write_does() {
let writes = [
"data.run",
"data.kept.k",
"data.absent.x",
"data.list.1",
"data.list.1.x",
"data.s.below",
];
let (first, data) = first_writes(&writes, false);
assert_eq!(
first,
vec![
json!(7),
json!(1),
json!(null),
json!(null),
json!(null),
json!(null)
]
);
assert_eq!(first_writes(&writes, true), (first, data.clone()));
assert_eq!(
data,
r#"{"kept":{"k":"w"},"list":[1,"w"],"s":"scalar","run":"w","absent":{"x":"w"}}"#
);
}
type Call<'a> = Vec<(&'a str, Option<Value>)>;
fn settled(context: &Value, calls: &[Call<'_>]) -> String {
let exprs = ExprEngine::default();
let payload = OwnedDataValue::Null;
let mut slot = OwnedDataValue::from(context);
let mut rc = RunContext::new(&mut slot, Tuning::default());
let mut overlays = Vec::new();
for writes in calls {
let mut ctx = Ctx::new(
CallStart {
reader: rc.reader(),
binding: None,
payload: &payload,
exprs: &exprs,
info: info(),
deadline: None,
cancel: None,
capture: true,
},
&NoCallee,
);
for (dotted, value) in writes {
match value {
Some(value) => ctx.set(&parse(dotted), OwnedDataValue::from(value)),
None => ctx.remove(&parse(dotted)),
}
.unwrap();
}
overlays.push(ctx.settle().0);
}
for overlay in overlays {
overlay.apply(&mut rc);
}
rc.finish();
serde_json::to_string(&slot).unwrap()
}
#[test]
fn calls_settled_in_order_end_as_one_call_making_their_writes() {
let cases: [(Value, Vec<Call<'_>>, &str); 4] = [
(
json!({}),
vec![
vec![("data.prices.a", Some(json!(1)))],
vec![("data.prices.b", Some(json!(2)))],
],
r#"{"prices":{"a":1,"b":2}}"#,
),
(
json!({"list": [1, 2]}),
vec![
vec![("data.list.4", Some(json!(5)))],
vec![("data.list.3", Some(json!(4)))],
],
r#"{"list":[1,2,null,4,5]}"#,
),
(
json!({"list": [1, 2, 3]}),
vec![vec![("data.list.0", None)], vec![("data.list.2", None)]],
r#"{"list":[2,3]}"#,
),
(
json!({"a": 1, "b": 2}),
vec![
vec![("data.b", Some(json!(5)))],
vec![("data.a", None), ("data.a", Some(json!(3)))],
],
r#"{"b":5,"a":3}"#,
),
];
for (data, calls, want) in cases {
let context = json!({"data": data, "metadata": {}, "temp_data": {}});
let want = format!(r#"{{"data":{want},"metadata":{{}},"temp_data":{{}}}}"#);
let one_call = vec![calls.concat()];
assert_eq!(settled(&context, &one_call), want, "one call: {calls:?}");
assert_eq!(
settled(&context, &calls),
want,
"settled in order: {calls:?}"
);
}
}
#[test]
fn calls_with_bindings_settled_in_order_replay_each_write_at_its_path() {
let context = json!({"data": {}, "metadata": {}, "temp_data": {}});
let item = parse("temp_data.p");
let index = parse("temp_data.p_index");
let elements: [(Value, Call<'_>, Value, [Value; 2]); 3] = [
(
json!({"id": "a"}),
vec![
("temp_data.p.x", Some(json!(1))),
("data.a", Some(json!(1))),
],
json!({"id": "a", "x": 1}),
[json!(null), json!(null)],
),
(
json!(5),
vec![
("temp_data.p.y", Some(json!(2))),
("data.b", Some(json!(2))),
],
json!(5),
[json!(null), json!(null)],
),
(
json!({"id": "c"}),
vec![
("temp_data.p.id", Some(json!("C"))),
("data.a", Some(json!(3))),
],
json!({"id": "C"}),
[json!("c"), json!(null)],
),
];
let exprs = ExprEngine::default();
let payload = OwnedDataValue::Null;
let mut slot = OwnedDataValue::from(&context);
let mut rc = RunContext::new(&mut slot, Tuning::default());
let mut overlays = Vec::new();
let reader = rc.reader();
for (i, (element, writes, seen, olds)) in elements.iter().enumerate() {
let value = OwnedDataValue::from(element);
let binding = Binding {
item: &item,
value: &value,
index: &index,
index_value: OwnedDataValue::from(i),
};
let mut ctx = Ctx::new(
CallStart {
reader,
binding: Some(&binding),
payload: &payload,
exprs: &exprs,
info: info(),
deadline: None,
cancel: None,
capture: true,
},
&NoCallee,
);
for (dotted, value) in writes {
let value = value.as_ref().map(OwnedDataValue::from).unwrap();
ctx.set(&parse(dotted), value).unwrap();
}
assert_eq!(get(&ctx, "temp_data.p").as_ref(), Some(seen), "element {i}");
assert_eq!(
eval(&ctx, json!({"var": "temp_data.p"})),
*seen,
"element {i}"
);
assert_eq!(get(&ctx, "temp_data.p_index"), Some(json!(i)));
let (overlay, changes, _) = ctx.settle();
let recorded: Vec<Value> = changes.iter().map(|c| Value::from(&c.old_value)).collect();
assert_eq!(recorded, olds, "element {i}");
overlays.push(overlay);
}
for overlay in overlays {
overlay.apply(&mut rc);
}
rc.finish();
assert_eq!(
serde_json::to_string(&slot).unwrap(),
r#"{"data":{"a":3,"b":2},"metadata":{},"temp_data":{"p":{"x":1,"y":2,"id":"C"}}}"#
);
}
#[test]
fn render_lends_each_hole_from_where_it_is_and_builds_no_tree() {
let context = json!({"data": {"order": {"customer": "C", "qty": 3}},
"metadata": {}, "temp_data": {}});
let exprs = ExprEngine::default();
let payload = OwnedDataValue::Null;
let mut slot = OwnedDataValue::from(&context);
let mut rc = RunContext::new(&mut slot, Tuning::default());
rc.read(|_, _| ());
rc.set(
&parse("data.order.since"),
OwnedDataValue::from("2026"),
None,
);
let progress = parse("metadata.progress");
rc.edit(|e| {
e.set(
&progress,
OwnedDataValue::from("p").to_arena(e.arena()),
None,
)
});
let mut ctx = Ctx::new(
CallStart {
reader: rc.reader(),
binding: None,
payload: &payload,
exprs: &exprs,
info: info(),
deadline: None,
cancel: None,
capture: true,
},
&NoCallee,
);
ctx.set(&parse("temp_data.mine"), OwnedDataValue::from(1))
.unwrap();
ctx.set(&parse("temp_data.box.k"), OwnedDataValue::from(2))
.unwrap();
let doc = exprs
.interpolated(&json!([
{"var": "temp_data.mine"},
{"var": "temp_data.box"},
{"var": "data.order.since"},
{"var": "metadata.progress"},
{"var": "data.order"},
{"var": "data.order.customer"},
{"var": ["data.absent", "d"]},
{"var": ["payload.x", "p"]}
]))
.unwrap();
let rendered = ctx.render(&doc);
let kinds: Vec<&str> = rendered
.filled()
.iter()
.map(|filled| match filled {
Filled::Owned(Cow::Borrowed(_)) => "lent",
Filled::Owned(Cow::Owned(_)) => "converted",
Filled::View(_) => "view",
Filled::Default(_) => "default",
})
.collect();
assert_eq!(
kinds,
[
"lent", "converted", "lent", "view", "view", "lent", "default", "default" ]
);
assert_eq!(
serde_json::to_value(&rendered).unwrap(),
json!([1, {"k": 2}, "2026", "p",
{"customer": "C", "qty": 3, "since": "2026"}, "C", "d", "p"])
);
}
struct Leaky;
impl BoundCall for Leaky {
fn call<'a>(
&'a self,
_ctx: &'a mut Ctx<'_>,
) -> crate::function::BoxFuture<'a, Result<crate::function::Outcome>> {
Box::pin(std::future::ready(Ok(crate::function::Outcome::Ok)))
}
fn visit(&self, v: &mut dyn ExprVisitor) {
let at = |text: &str| text.parse::<AuthoredPath>().unwrap();
v.expr(&at("a.b[1]"), &json!({"secret": "k"}), false);
v.expr(
&at("a.c"),
&json!({"cat": ["x", {"secret": {"var": "data.k"}}]}),
false,
);
v.expr(&at("kept"), &json!({"var": "data.k"}), false);
v.expr(&at("absent.x"), &json!({"secret": "k"}), false);
}
fn primary(&self) -> Primary<'_> {
Primary::Undeclared
}
fn resolve(&self, _r: &mut dyn Resolver) -> Result<OwnedDataValue> {
Ok(OwnedDataValue::from(&json!({
"a": {"b": [1, "leaked"], "c": "leaked too"},
"kept": "v",
})))
}
}
#[test]
fn input_nulls_every_value_the_walk_reports_reading_a_secret() {
let context = json!({"data": {"k": "v"}, "metadata": {}, "temp_data": {}});
let exprs = ExprEngine::default();
let payload = OwnedDataValue::Null;
let mut slot = OwnedDataValue::from(&context);
let mut rc = RunContext::new(&mut slot, Tuning::default());
let ctx = Ctx::new(
CallStart {
reader: rc.reader(),
binding: None,
payload: &payload,
exprs: &exprs,
info: info(),
deadline: None,
cancel: None,
capture: true,
},
&Leaky,
);
assert_eq!(
ctx.input().unwrap().to_json_string(),
r#"{"a":{"b":[1,null],"c":null},"kept":"v"}"#
);
}
#[test]
fn debug_shows_the_call_and_never_the_context() {
let context = json!({"data": {"secret_looking": "value"}});
with_ctx(&context, info(), None, |ctx| {
let text = format!("{ctx:?}");
assert!(text.contains("task_id: \"t\"") && text.contains("overlay_empty: true"));
assert!(!text.contains("secret_looking"), "{text}");
assert!(text.ends_with(".. }"), "{text}");
});
}
}