Skip to main content

dataflow_rs/function/
ctx.rs

1//! `Ctx`: a function's view of one call, and the static facts about it.
2
3use super::erased::BoundCall;
4use super::overlay::Overlay;
5use crate::base::{
6    AuthoredPath, CancelToken, Change, Error, ErrorKind, ErrorRecord, Instant, PathSeg, ProgramId,
7    Result, TaskIx, WorkflowIx,
8};
9use crate::expr::arena::with_arena;
10use crate::expr::path;
11use crate::expr::{
12    CRATE, Expr, ExprEngine, ExprVisitor, Filled, FromValue, Interpolated, Output, Path, Rendered,
13    ResolveExpr, Resolver, SealedResolver, Seg, reads_secret,
14};
15use crate::state::{Lookup, Reader, splice};
16use bumpalo::Bump;
17use datavalue::{DataValue, OwnedDataValue};
18use serde_json::Value;
19use std::borrow::Cow;
20use std::fmt;
21use std::sync::Arc;
22
23/// Static facts about one call: where it sits in the program, and which
24/// iteration it is.
25#[derive(Clone, Copy, Debug)]
26#[non_exhaustive]
27pub struct CallInfo<'a> {
28    /// The program the call belongs to. It tells the indices of one build
29    /// from another's across a reload.
30    pub program: ProgramId,
31    /// The workflow's position in the program.
32    pub workflow: WorkflowIx,
33    /// The task's position in the program.
34    pub task: TaskIx,
35    /// The name the function was called by.
36    pub function: &'a str,
37    /// The workflow's id.
38    pub workflow_id: &'a str,
39    /// The task's id.
40    pub task_id: &'a str,
41    /// The channel the run was started on; `None` when the run takes every
42    /// active workflow.
43    pub channel: Option<&'a str>,
44    /// The run's routing bucket, `0..=99`; `None` when the run was given
45    /// none, which every rollout admits. A function that starts a child run
46    /// passes it on, so the child keeps the caller's rollout decision.
47    pub routing_bucket: Option<u8>,
48    /// The loop counter, inside a workflow's loop.
49    pub loop_counter: Option<i64>,
50    /// The element's index, inside a `for_each` fan-out.
51    pub element_index: Option<usize>,
52}
53
54/// A [`Function`](super::Function)'s view of one call.
55///
56/// It reads the run's single view of the context, so no view is built per
57/// call, and keeps the call's writes in an overlay of its own: a read
58/// sees them over the run's context, and they reach the message only when
59/// the call returns `Ok` (whatever the [`Outcome`](super::Outcome)). An `Err`
60/// drops them, and their `Change`s, and keeps the records the call added.
61///
62/// Once the call has written, each evaluation splices the overlay onto the
63/// view in an arena borrowed from a pool for that evaluation only. `Ctx`
64/// holds no arena, so it is `Send` and `Sync`. A host never builds one: the
65/// engine hands it to [`Function::call`](super::Function::call) and to each
66/// [`CallLayer`](super::CallLayer), and [`testing::call`](super::testing::call)
67/// to one call outside a run:
68///
69/// ```
70/// # tokio::runtime::Builder::new_current_thread().build().unwrap().block_on(async {
71/// use dataflow_rs::datavalue::OwnedDataValue;
72/// use dataflow_rs::function::testing::{self, Write};
73/// use dataflow_rs::run::Change;
74/// use dataflow_rs::{Ctx, Function, Message, Outcome, Path, Result};
75/// use serde_json::json;
76///
77/// /// Counts visits at `data.visits`, and drops the stale `data.cache`.
78/// struct Visit;
79///
80/// impl Function for Visit {
81///     type Config = ();
82///
83///     async fn call(&self, ctx: &mut Ctx<'_>, _cfg: &()) -> Result<Outcome> {
84///         let visits: Path = "data.visits".parse()?;
85///         let seen = ctx.get(&visits).and_then(|v| v.as_i64()).unwrap_or(0);
86///         ctx.set(&visits, OwnedDataValue::from(&json!(seen + 1)))?;
87///         // The call reads its own write at once; the message, once the
88///         // call returns `Ok`.
89///         assert_eq!(ctx.get(&visits).and_then(|v| v.as_i64()), Some(seen + 1));
90///         ctx.remove(&"data.cache".parse()?)?;
91///         Ok(Outcome::Ok)
92///     }
93/// }
94///
95/// let dv = |v: serde_json::Value| OwnedDataValue::from(&v);
96/// let data = dv(json!({"visits": 1, "cache": "stale"}));
97/// let called = testing::call(Visit, json!({}))
98///     .message(Message::from_parts(OwnedDataValue::Null, data, OwnedDataValue::Null))
99///     .await;
100/// let visits: Path = "data.visits".parse().unwrap();
101/// assert_eq!(called.writes[0], (visits, Write::Set(dv(json!(2)))));
102/// assert_eq!(called.writes[1].1, Write::Removed);
103/// assert_eq!(
104///     called.changes[0],
105///     Change::write("data.visits", dv(json!(1)), dv(json!(2)))
106/// );
107/// assert_eq!(called.message.context().data(), &dv(json!({"visits": 2})));
108/// # });
109/// ```
110pub struct Ctx<'r> {
111    /// What the runtime handed the call: the run's reader, a fan-out
112    /// element's bindings (under the call's own writes), the payload, the
113    /// engine, the call's facts, the run's limits, and whether `set` and
114    /// `remove` record `Change`s.
115    start: CallStart<'r>,
116    /// The function and its config: `emit_default` writes through its
117    /// primary output, and `input` resolves its config.
118    callee: &'r dyn BoundCall,
119    /// The call's writes, in order.
120    overlay: Overlay,
121    /// The call's `Change`s, in write order.
122    changes: Vec<Change>,
123    /// The records the call added.
124    errors: Vec<ErrorRecord>,
125}
126
127/// What the runtime hands a new [`Ctx`]. A fan-out builds one for the
128/// task and copies it for each element, with the element's binding and
129/// index.
130#[derive(Clone, Copy)]
131pub(crate) struct CallStart<'r> {
132    /// The run's reader, taken in its own statement: never a `&RunView`.
133    pub(crate) reader: Reader<'r>,
134    /// A fan-out element's bindings.
135    pub(crate) binding: Option<&'r Binding<'r>>,
136    /// The message's payload.
137    pub(crate) payload: &'r OwnedDataValue,
138    /// The engine the program was compiled with.
139    pub(crate) exprs: &'r ExprEngine,
140    /// The call's static facts.
141    pub(crate) info: CallInfo<'r>,
142    /// When the run must end.
143    pub(crate) deadline: Option<Instant>,
144    /// The run's cancellation token.
145    pub(crate) cancel: Option<&'r CancelToken>,
146    /// Whether the call records its `Change`s.
147    pub(crate) capture: bool,
148}
149
150/// A callee's primary output, as [`Ctx::emit_default`] writes through it.
151#[derive(Clone, Copy, Debug)]
152pub(crate) enum Primary<'r> {
153    /// The config declares none.
154    Undeclared,
155    /// The config's primary output, the field named, which the input left
156    /// unset: an `Option<Output>`.
157    Unset(&'r str),
158    /// The config's primary output, as the call site set it.
159    Set(&'r Output),
160}
161
162/// A fan-out element's bindings: `temp_data.<as>` holds the element and
163/// `temp_data.<as>_index` its index. They sit between the run's context and
164/// the call's own writes, and are never written to the run: the settle
165/// replays the call's writes at their paths, where the binding is not.
166pub(crate) struct Binding<'r> {
167    /// `temp_data.<as>`.
168    pub(crate) item: &'r Path,
169    /// The element.
170    pub(crate) value: &'r OwnedDataValue,
171    /// `temp_data.<as>_index`.
172    pub(crate) index: &'r Path,
173    /// The element's index.
174    pub(crate) index_value: OwnedDataValue,
175}
176
177impl Binding<'_> {
178    /// `root` with both bindings written, in `arena`.
179    fn splice_onto<'a>(&'a self, root: DataValue<'a>, arena: &'a Bump) -> DataValue<'a> {
180        let root =
181            splice::set(arena, root, self.item.segs(), self.value.view_in(arena)).unwrap_or(root);
182        splice::set(
183            arena,
184            root,
185            self.index.segs(),
186            self.index_value.view_in(arena),
187        )
188        .unwrap_or(root)
189    }
190
191    /// What the bindings say `path` holds, over the run's `root`: `Some` for
192    /// a path at, below or above one of them, `None` for any other.
193    fn get<'s>(
194        &'s self,
195        path: &Path,
196        root: DataValue<'_>,
197    ) -> Option<Option<Cow<'s, OwnedDataValue>>> {
198        for (at, value) in [(self.item, self.value), (self.index, &self.index_value)] {
199            if let Some(rest) = below(path, at) {
200                return Some(path::get(value, rest).map(Cow::Borrowed));
201            }
202        }
203        if [self.item, self.index]
204            .iter()
205            .any(|at| below(at, path).is_some())
206        {
207            return Some(with_arena(|arena| {
208                splice::get(self.splice_onto(root, arena), path.segs())
209                    .map(|v| Cow::Owned(v.to_owned()))
210            }));
211        }
212        None
213    }
214}
215
216/// The segments of `path` below `prefix`, when `path` is `prefix` or below
217/// it. A binding's path is a root and a key, so segments compare by key.
218fn below<'p>(path: &'p Path, prefix: &Path) -> Option<&'p [Seg]> {
219    let (path, prefix) = (path.segs(), prefix.segs());
220    (path.len() >= prefix.len() && path.iter().zip(prefix).all(|(a, b)| a.key == b.key))
221        .then(|| &path[prefix.len()..])
222}
223
224/// `f` with the root a call's writes sit on, taking an arena only when there
225/// is a binding to write.
226fn with_base<R>(
227    reader: Reader<'_>,
228    binding: Option<&Binding<'_>>,
229    f: impl FnOnce(DataValue<'_>) -> R,
230) -> R {
231    match binding {
232        None => f(*reader.root),
233        Some(binding) => with_arena(|arena| f(binding.splice_onto(*reader.root, arena))),
234    }
235}
236
237impl<'r> Ctx<'r> {
238    /// A context for one call of `callee`.
239    pub(crate) fn new(start: CallStart<'r>, callee: &'r dyn BoundCall) -> Self {
240        Self {
241            start,
242            callee,
243            overlay: Overlay::default(),
244            changes: Vec::new(),
245            errors: Vec::new(),
246        }
247    }
248
249    /// The static facts about this call.
250    pub fn info(&self) -> &CallInfo<'r> {
251        &self.start.info
252    }
253
254    /// The function this call reaches and its config, bound at compile.
255    pub(crate) fn callee(&self) -> &'r dyn BoundCall {
256        self.callee
257    }
258
259    /// The callee's config resolved for this call: every field with a value,
260    /// set by the input or by its default, keyed by its authored key, in
261    /// declaration order, an `Option` left unset absent, as
262    /// [`FunctionConfig::resolve`](super::FunctionConfig::resolve) gives it
263    /// over the context as this call sees it, a fan-out element's binding
264    /// included. A hand-written config gives what its own `resolve` does.
265    /// A field of type `f64` writes a float, `2.0` for an authored `2` too,
266    /// where a [`Templated`](super::Templated) number field writes the
267    /// number as it was authored.
268    ///
269    /// A field whose expression reads a secret is `null`, the one redaction
270    /// the engine writes, as [`TraceOptions::redact`](crate::observe::TraceOptions::redact)
271    /// writes a redacted path's value, and it is never evaluated, so not
272    /// even its error can carry the value: a logging layer or a dry run
273    /// reads the input without reading a secret. The function itself
274    /// reads its own fields, secrets included, through [`Self::eval`].
275    ///
276    /// # Errors
277    ///
278    /// The first field's that fails: [`ErrorKind::Budget`] when the
279    /// operation ceiling is exceeded, [`ErrorKind::Eval`] for any other
280    /// evaluation failure, [`ErrorKind::Config`] for a serde field that does
281    /// not serialize.
282    pub fn input(&self) -> Result<OwnedDataValue> {
283        let mut input = self.callee.resolve(&mut Redacted(self))?;
284        // The walk the secret checks trust: whatever a field's resolve did,
285        // the value where its secret-reading expression sits is `null`.
286        self.callee.visit(&mut Redact(&mut input));
287        Ok(input)
288    }
289
290    /// `cfg` resolved for this call, unredacted:
291    /// [`FunctionConfig::resolve`](super::FunctionConfig::resolve) over the
292    /// context as this call sees it, a fan-out element's binding included,
293    /// with every expression evaluated, a secret-reading one too. It is
294    /// [`Self::input`]'s counterpart: `input` is what a layer or a dry run
295    /// reads, and this is the function's own view of its config, which it
296    /// may hand on whole.
297    ///
298    /// # Errors
299    ///
300    /// The first field's that fails: [`ErrorKind::Budget`] when the
301    /// operation ceiling is exceeded, [`ErrorKind::Eval`] for any other
302    /// evaluation failure, [`ErrorKind::Config`] for a serde field that does
303    /// not serialize.
304    pub fn resolve<C: super::FunctionConfig>(&self, cfg: &C) -> Result<OwnedDataValue> {
305        cfg.resolve(&mut &*self)
306    }
307
308    /// The message's payload, which no expression reads.
309    pub fn payload(&self) -> &OwnedDataValue {
310        self.start.payload
311    }
312
313    /// The engine the program was compiled with, for an evaluation over a
314    /// root of the function's own.
315    pub fn exprs(&self) -> &ExprEngine {
316        self.start.exprs
317    }
318
319    /// The secret at the dotted path `name`, from the engine's store. What a
320    /// function does with it is the function's: the engine never records it.
321    pub fn secret(&self, name: &str) -> Option<&OwnedDataValue> {
322        self.start.exprs.secrets().get(name)
323    }
324
325    /// When the run must end, if it has a deadline: a run this function
326    /// starts passes it on, or an earlier one. An `Err` this call returns
327    /// once the deadline has passed, such as that run's own `TimedOut`, ends
328    /// this run `TimedOut` too.
329    pub fn deadline(&self) -> Option<Instant> {
330        self.start.deadline
331    }
332
333    /// The run's cancellation token, if it has one: a child of it cancels a
334    /// run this function starts along with this one, so that run ends
335    /// `Cancelled` and reports it rather than being dropped. An `Err` this
336    /// call returns once the token is cancelled ends this run `Cancelled`.
337    pub fn cancel(&self) -> Option<&CancelToken> {
338        self.start.cancel
339    }
340
341    /// The value at `path` as this call sees it: its own writes, over a
342    /// fan-out element's bindings, over the run's context. Borrowed unless
343    /// it must be put together, when it is converted: a value only the run's
344    /// view knows, or one with this call's writes or a binding below `path`,
345    /// such as `temp_data` while `temp_data.p` is bound.
346    pub fn get(&self, path: &Path) -> Option<Cow<'_, OwnedDataValue>> {
347        self.find(path).map(Filled::into_cow)
348    }
349
350    /// Where the value at `path` is, as this call sees it, lent from
351    /// wherever it is: the call's writes, the binding, then the run's log,
352    /// owned tree or view. Only a value the call wrote below `path` is
353    /// converted, off the root `eval` sees.
354    fn find(&self, path: &Path) -> Option<Filled<'_>> {
355        match self.overlay.lookup(path) {
356            Lookup::Clean => {}
357            Lookup::Owned(v) => return v.map(Filled::lent),
358            Lookup::Dirty => {
359                return with_arena(|arena| {
360                    splice::get(*self.root_in(arena), path.segs())
361                        .map(|v| Filled::Owned(Cow::Owned(v.to_owned())))
362                });
363            }
364        }
365        if let Some(found) = self
366            .start
367            .binding
368            .and_then(|b| b.get(path, *self.start.reader.root))
369        {
370            return found.map(Filled::Owned);
371        }
372        self.start.reader.find(path)
373    }
374
375    /// `doc` with each hole looked up once, as [`Self::get`] would, and
376    /// lent where it was found; a hole that finds nothing yields its
377    /// default. No tree is built: the [`Rendered`] serializes the
378    /// document's skeleton with each value in place, so a consumer that is
379    /// itself a serializer pays for no intermediate value.
380    pub fn render<'s>(&'s self, doc: &'s Interpolated) -> Rendered<'s> {
381        doc.render_with(|hole| {
382            hole.path
383                .as_ref()
384                .and_then(|path| self.find(path))
385                .unwrap_or(Filled::Default(&hole.default))
386        })
387    }
388
389    /// Whether this call sees a value at `path`, converting none.
390    fn holds(&self, path: &Path) -> bool {
391        match self.overlay.lookup(path) {
392            Lookup::Owned(v) => v.is_some(),
393            Lookup::Dirty => {
394                with_arena(|arena| splice::get(*self.root_in(arena), path.segs()).is_some())
395            }
396            Lookup::Clean => with_base(self.start.reader, self.start.binding, |base| {
397                splice::get(base, path.segs()).is_some()
398            }),
399        }
400    }
401
402    /// Evaluates `expr` against the context as this call sees it. A folded
403    /// constant is lent from `expr` and takes no arena; anything else is
404    /// evaluated in a pooled arena and converted to `T` before the arena goes
405    /// back.
406    ///
407    /// # Errors
408    ///
409    /// [`ErrorKind::Budget`](crate::base::ErrorKind::Budget) when the
410    /// operation ceiling is exceeded; [`ErrorKind::Eval`](crate::base::ErrorKind::Eval)
411    /// for any other failure, including a result that is not a `T`.
412    pub fn eval<'e, T: FromValue>(&self, expr: &'e Expr<T>) -> Result<T::Out<'e>> {
413        if let Some(c) = expr.constant() {
414            return Ok(T::lend(c));
415        }
416        with_arena(|arena| {
417            let root = self.root_in(arena);
418            self.start.exprs.eval_view(expr, root, arena)
419        })
420    }
421
422    /// `expr` evaluated as [`Self::eval`] evaluates it, as an owned value: a
423    /// folded constant converted, anything else evaluated in a pooled arena
424    /// and converted before the arena goes back.
425    fn eval_owned(&self, expr: &dyn ResolveExpr) -> Result<OwnedDataValue> {
426        if let Some(constant) = expr.constant_value(CRATE) {
427            return Ok(constant);
428        }
429        with_arena(|arena| {
430            let root = self.root_in(arena);
431            let view = self.start.exprs.eval_in(expr.logic(CRATE), root, arena)?;
432            expr.value_of(view, CRATE)
433        })
434    }
435
436    /// The root this call's evaluations see: the run's view, with the binding
437    /// and then the call's writes spliced on in `arena` when there are any.
438    fn root_in<'a>(&'a self, arena: &'a Bump) -> &'a DataValue<'a> {
439        if self.start.binding.is_none() && self.overlay.is_empty() {
440            return self.start.reader.root;
441        }
442        let base = match self.start.binding {
443            None => *self.start.reader.root,
444            Some(binding) => binding.splice_onto(*self.start.reader.root, arena),
445        };
446        if self.overlay.is_empty() {
447            return arena.alloc(base);
448        }
449        arena.alloc(self.overlay.splice_onto(base, arena))
450    }
451
452    /// Assigns `value` at `path`, by `Path::set`'s rules: a hop that cannot
453    /// continue writes nothing. The `Change` is recorded first, under
454    /// capture, with the value this call sees there now, so a second write to
455    /// one path leaves two `Change`s and one value. At a root the write
456    /// replaces the root, and its `Change` says so
457    /// ([`replaced`](Change::replaced)).
458    ///
459    /// # Errors
460    ///
461    /// [`ErrorKind::Config`](crate::base::ErrorKind::Config) when `path` is a
462    /// root and `value` is not an object.
463    pub fn set(&mut self, path: &Path, value: OwnedDataValue) -> Result<()> {
464        path.check_set(&value)?;
465        // A first write with no binding is resolved against the run's view
466        // once: the walk says whether the write lands, and whether anything
467        // is there to record as the old value.
468        let first = (self.overlay.is_empty() && self.start.binding.is_none())
469            .then(|| splice::resolve(*self.start.reader.root, path.segs()));
470        if self.start.capture {
471            let old_value = match first {
472                // Below a scalar, or creating the node at the path or above
473                // it: nothing is there, or a `null` element, read as `null`.
474                Some(None) => None,
475                Some(Some(r)) if r.created.is_some() => None,
476                Some(Some(r)) => self.start.reader.get_resolved(path, r),
477                None => self.get(path),
478            };
479            self.changes.push(Change::assign(
480                path,
481                old_value.map_or(OwnedDataValue::Null, Cow::into_owned),
482                value.clone(),
483            ));
484        }
485        if let Some(resolved) = first {
486            self.overlay.push(path, value, resolved.is_some());
487            return Ok(());
488        }
489        let Self { start, overlay, .. } = self;
490        with_base(start.reader, start.binding, |base| {
491            overlay.set(base, path, value)
492        });
493        Ok(())
494    }
495
496    /// Writes `value` through `out`, as [`Self::set`] does: at its path
497    /// when it is literal, else at the path it evaluates to now, against the
498    /// context as this call sees it.
499    ///
500    /// # Errors
501    ///
502    /// [`Self::eval`]'s, when `out` is computed and does not give a path;
503    /// [`Self::set`]'s.
504    pub fn emit(&mut self, out: &Output, value: OwnedDataValue) -> Result<()> {
505        let path = match out.constant() {
506            Some(path) => Cow::Borrowed(path),
507            None => self.eval(out.path())?,
508        };
509        self.set(&path, value)
510    }
511
512    /// The callee's primary output, the one [`Self::emit_default`] writes
513    /// through, as the call site set it: `None` when the config declares
514    /// none, or leaves an optional one unset. A layer reads it to know
515    /// whether, and where, an answer would be written. It borrows the
516    /// callee, not this context, so a layer can [`Self::emit`] through it.
517    pub fn primary_output(&self) -> Option<&'r Output> {
518        let callee: &'r dyn BoundCall = self.callee;
519        match callee.primary() {
520            Primary::Set(out) => Some(out),
521            Primary::Unset(_) | Primary::Undeclared => None,
522        }
523    }
524
525    /// [`Self::emit`] through the function's primary output, the one its
526    /// config's [`FunctionConfig::primary_output`](super::FunctionConfig::primary_output)
527    /// names.
528    ///
529    /// # Errors
530    ///
531    /// [`ErrorKind::Config`] when the config has no primary output, or
532    /// leaves an optional one unset, which the error names; [`Self::emit`]'s.
533    pub fn emit_default(&mut self, value: OwnedDataValue) -> Result<()> {
534        let function = self.start.info.function;
535        let callee = self.callee;
536        match callee.primary() {
537            Primary::Set(out) => self.emit(out, value),
538            Primary::Unset(field) => Err(Error::new(
539                ErrorKind::Config,
540                format!("`{function}`'s primary output `{field}` is not set"),
541            )),
542            Primary::Undeclared => Err(Error::new(
543                ErrorKind::Config,
544                format!("`{function}` has no primary output"),
545            )),
546        }
547    }
548
549    /// Removes the value at `path`, recording the removal under capture.
550    /// Nothing there is nothing to do, and records nothing.
551    ///
552    /// # Errors
553    ///
554    /// [`ErrorKind::Config`](crate::base::ErrorKind::Config) when `path` is a
555    /// root.
556    pub fn remove(&mut self, path: &Path) -> Result<()> {
557        path.check_remove()?;
558        if self.start.capture {
559            let Some(old_value) = self.get(path).map(Cow::into_owned) else {
560                return Ok(());
561            };
562            self.changes
563                .push(Change::removal(Arc::clone(path.dotted()), old_value));
564        } else if !self.holds(path) {
565            return Ok(());
566        }
567        self.overlay.remove(path);
568        Ok(())
569    }
570
571    /// Adds a record to the message's errors, whatever the call returns. What
572    /// the record leaves unset the engine fills: the workflow's and the
573    /// task's ids, the run's timestamp, and in a fan-out the element's index.
574    pub fn record_error(&mut self, e: ErrorRecord) {
575        self.errors.push(e);
576    }
577
578    /// The call's writes, its `Change`s and the records it added.
579    pub(crate) fn settle(self) -> (Overlay, Vec<Change>, Vec<ErrorRecord>) {
580        (self.overlay, self.changes, self.errors)
581    }
582}
583
584/// A call's context resolves a config's fields against the context as the
585/// call sees it, as [`Ctx::eval`] and [`Ctx::render`] do, secrets read: a
586/// host's [`ConfigField::resolve`](crate::expr::ConfigField::resolve) calls
587/// it. A function resolves its whole config with [`Ctx::resolve`].
588impl SealedResolver for &Ctx<'_> {}
589
590impl Resolver for &Ctx<'_> {
591    fn eval(&mut self, expr: &dyn ResolveExpr) -> Result<OwnedDataValue> {
592        Ctx::eval_owned(self, expr)
593    }
594
595    fn render(&mut self, doc: &Interpolated) -> Result<OwnedDataValue> {
596        Ok(Ctx::render(self, doc).to_owned())
597    }
598}
599
600/// The resolver behind [`Ctx::input`]: an expression that reads a secret
601/// is `null`, and is never evaluated; anything else as the call's context
602/// resolves it.
603struct Redacted<'c, 'r>(&'c Ctx<'r>);
604
605impl SealedResolver for Redacted<'_, '_> {}
606
607impl Resolver for Redacted<'_, '_> {
608    fn eval(&mut self, expr: &dyn ResolveExpr) -> Result<OwnedDataValue> {
609        if reads_secret(expr.raw()) {
610            return Ok(OwnedDataValue::Null);
611        }
612        self.0.eval_owned(expr)
613    }
614
615    fn render(&mut self, doc: &Interpolated) -> Result<OwnedDataValue> {
616        Ok(self.0.render(doc).to_owned())
617    }
618}
619
620/// Sets the value of every expression that reads a secret to `null`, at the
621/// path the config's walk reports it, in a resolved config.
622struct Redact<'v>(&'v mut OwnedDataValue);
623
624impl ExprVisitor for Redact<'_> {
625    fn expr(&mut self, at: &AuthoredPath, raw: &Value, _records: bool) {
626        if reads_secret(raw)
627            && let Some(slot) = slot_at(self.0, at)
628        {
629            *slot = OwnedDataValue::Null;
630        }
631    }
632}
633
634/// The value at `at` in `value`, a key naming an object's entry and an
635/// index an array's element; `None` when nothing is there.
636fn slot_at<'v>(value: &'v mut OwnedDataValue, at: &AuthoredPath) -> Option<&'v mut OwnedDataValue> {
637    at.segs()
638        .iter()
639        .try_fold(value, |value, seg| match (value, seg) {
640            (OwnedDataValue::Object(entries), PathSeg::Key(key)) => entries
641                .iter_mut()
642                .find(|(k, _)| **k == **key)
643                .map(|(_, v)| v),
644            (OwnedDataValue::Array(items), PathSeg::Index(i)) => items.get_mut(*i),
645            _ => None,
646        })
647}
648
649impl fmt::Debug for Ctx<'_> {
650    /// The call's facts and whether it has written; never the context.
651    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
652        f.debug_struct("Ctx")
653            .field("info", &self.start.info)
654            .field("overlay_empty", &self.overlay.is_empty())
655            .finish_non_exhaustive()
656    }
657}
658
659/// A `Ctx` over a context of the tests' choosing.
660#[cfg(test)]
661pub(crate) mod testing {
662    use super::*;
663    use crate::function::{BoxFuture, Outcome};
664    use crate::state::{RunContext, Tuning};
665
666    /// A callee with no config: no primary output, and nothing to resolve.
667    pub(crate) struct NoCallee;
668
669    impl BoundCall for NoCallee {
670        fn call<'a>(&'a self, _ctx: &'a mut Ctx<'_>) -> BoxFuture<'a, Result<Outcome>> {
671            Box::pin(std::future::ready(Ok(Outcome::Ok)))
672        }
673
674        fn visit(&self, _v: &mut dyn ExprVisitor) {}
675
676        fn primary(&self) -> Primary<'_> {
677            Primary::Undeclared
678        }
679
680        fn resolve(&self, _r: &mut dyn Resolver) -> Result<OwnedDataValue> {
681            Ok(OwnedDataValue::Null)
682        }
683    }
684
685    /// Runs `f` with a call context over `context`, the call described by
686    /// `info`, with `binding` if any.
687    pub(crate) fn with_ctx<R>(
688        context: &serde_json::Value,
689        info: CallInfo<'_>,
690        binding: Option<&Binding<'_>>,
691        f: impl FnOnce(Ctx<'_>) -> R,
692    ) -> R {
693        let exprs = ExprEngine::default();
694        let payload = OwnedDataValue::Null;
695        let mut slot = OwnedDataValue::from(context);
696        let mut rc = RunContext::new(&mut slot, Tuning::default());
697        let reader = rc.reader();
698        f(Ctx::new(
699            CallStart {
700                reader,
701                binding,
702                payload: &payload,
703                exprs: &exprs,
704                info,
705                deadline: None,
706                cancel: None,
707                capture: true,
708            },
709            &NoCallee,
710        ))
711    }
712}
713
714#[cfg(test)]
715mod tests {
716    use super::testing::{NoCallee, with_ctx};
717    use super::*;
718    use crate::state::{RunContext, Tuning};
719    use serde_json::{Value, json};
720
721    fn send_sync<T: Send + Sync>() {}
722
723    #[test]
724    fn ctx_is_send_and_sync() {
725        send_sync::<Ctx<'static>>();
726    }
727
728    fn info() -> CallInfo<'static> {
729        CallInfo {
730            program: ProgramId::next(),
731            workflow: WorkflowIx::new(0),
732            task: TaskIx::new(0),
733            function: "f",
734            workflow_id: "w",
735            task_id: "t",
736            channel: None,
737            routing_bucket: None,
738            loop_counter: None,
739            element_index: None,
740        }
741    }
742
743    fn parse(dotted: &str) -> Path {
744        dotted.parse().unwrap()
745    }
746
747    fn get(ctx: &Ctx<'_>, dotted: &str) -> Option<Value> {
748        ctx.get(&parse(dotted)).map(|v| Value::from(&*v))
749    }
750
751    fn eval(ctx: &Ctx<'_>, raw: Value) -> Value {
752        let expr: Expr = ExprEngine::default().compile(&raw).unwrap();
753        Value::from(&*ctx.eval(&expr).unwrap())
754    }
755
756    #[test]
757    fn the_binding_sits_under_the_calls_writes() {
758        let item = parse("temp_data.item");
759        let index = parse("temp_data.item_index");
760        let value = OwnedDataValue::from(&json!({"sku": "A", "qty": 2}));
761        let binding = Binding {
762            item: &item,
763            value: &value,
764            index: &index,
765            index_value: OwnedDataValue::from(3),
766        };
767        let context = json!({"data": {}, "metadata": {}, "temp_data": {"kept": true}});
768        with_ctx(&context, info(), Some(&binding), |mut ctx| {
769            assert_eq!(get(&ctx, "temp_data.item.sku"), Some(json!("A")));
770            assert_eq!(get(&ctx, "temp_data.item_index"), Some(json!(3)));
771            assert_eq!(
772                get(&ctx, "temp_data"),
773                Some(json!({"kept": true, "item": {"sku": "A", "qty": 2}, "item_index": 3}))
774            );
775            assert_eq!(eval(&ctx, json!({"var": "temp_data.item.qty"})), json!(2));
776            // A write below a binding is seen over it, by `get` and `eval`.
777            ctx.set(&parse("temp_data.item.qty"), OwnedDataValue::from(5))
778                .unwrap();
779            assert_eq!(
780                get(&ctx, "temp_data.item"),
781                Some(json!({"sku": "A", "qty": 5}))
782            );
783            assert_eq!(eval(&ctx, json!({"var": "temp_data.item.qty"})), json!(5));
784            assert_eq!(get(&ctx, "temp_data.kept"), Some(json!(true)));
785        });
786    }
787
788    #[test]
789    fn a_write_that_cannot_land_records_its_change_and_writes_nothing() {
790        // v3's `TaskContext::set` records before it writes. Removing nothing
791        // records nothing.
792        let context = json!({"data": {"s": "scalar"}, "metadata": {}, "temp_data": {}});
793        let (overlay, changes, errors) = with_ctx(&context, info(), None, |mut ctx| {
794            ctx.set(&parse("data.s.below"), OwnedDataValue::from(1))
795                .unwrap();
796            ctx.remove(&parse("data.absent")).unwrap();
797            assert_eq!(get(&ctx, "data.s"), Some(json!("scalar")));
798            ctx.settle()
799        });
800        assert!(errors.is_empty());
801        // The write is kept for the settle, which replays it where the
802        // context may take it, as v3 replays a `Change`; here it cannot.
803        let mut slot = OwnedDataValue::from(&context);
804        let mut rc = RunContext::new(&mut slot, Tuning::default());
805        overlay.apply(&mut rc);
806        rc.finish();
807        assert_eq!(Value::from(&slot), context);
808        assert_eq!(
809            serde_json::to_value(&changes).unwrap(),
810            json!([{"path": "data.s.below", "old_value": null, "new_value": 1}])
811        );
812    }
813
814    #[test]
815    fn a_removal_without_capture_removes_what_the_call_sees_and_records_nothing() {
816        let context = json!({"data": {"a": 1, "list": [1, 2]}, "metadata": {}, "temp_data": {}});
817        let exprs = ExprEngine::default();
818        let payload = OwnedDataValue::Null;
819        let mut slot = OwnedDataValue::from(&context);
820        let mut rc = RunContext::new(&mut slot, Tuning::default());
821        let mut ctx = Ctx::new(
822            CallStart {
823                reader: rc.reader(),
824                binding: None,
825                payload: &payload,
826                exprs: &exprs,
827                info: info(),
828                deadline: None,
829                cancel: None,
830                capture: false,
831            },
832            &NoCallee,
833        );
834        // A value of the run's, one the call wrote, nothing, and an element
835        // the call's own removal moved: after it, `list.1` holds nothing.
836        for (dotted, write) in [
837            ("data.a", None),
838            ("data.b", Some(2)),
839            ("data.b", None),
840            ("data.absent", None),
841            ("data.list.0", None),
842            ("data.list.1", None),
843        ] {
844            match write {
845                Some(n) => ctx.set(&parse(dotted), OwnedDataValue::from(n)),
846                None => ctx.remove(&parse(dotted)),
847            }
848            .unwrap();
849        }
850        assert_eq!(get(&ctx, "data"), Some(json!({"list": [2]})));
851        let (overlay, changes, errors) = ctx.settle();
852        assert!(changes.is_empty() && errors.is_empty());
853        overlay.apply(&mut rc);
854        rc.finish();
855        assert_eq!(Value::from(&slot)["data"], json!({"list": [2]}));
856    }
857
858    /// What each call recorded and wrote, when each of `writes` is made by a
859    /// call of its own, as its first write or, with `after_another`, after
860    /// a write elsewhere: the two ways `Ctx::set` reads the old value. The
861    /// run has written `data.run` into its view, so its log holds it.
862    fn first_writes(writes: &[&str], after_another: bool) -> (Vec<Value>, String) {
863        let context = json!({"data": {"kept": {"k": 1}, "list": [1, null], "s": "scalar"},
864            "metadata": {}, "temp_data": {}});
865        let exprs = ExprEngine::default();
866        let payload = OwnedDataValue::Null;
867        let mut slot = OwnedDataValue::from(&context);
868        let mut rc = RunContext::new(&mut slot, Tuning::default());
869        rc.read(|_, _| ());
870        rc.set(&parse("data.run"), OwnedDataValue::from(7), None);
871        let mut olds = Vec::new();
872        let mut overlays = Vec::new();
873        for dotted in writes {
874            let mut ctx = Ctx::new(
875                CallStart {
876                    reader: rc.reader(),
877                    binding: None,
878                    payload: &payload,
879                    exprs: &exprs,
880                    info: info(),
881                    deadline: None,
882                    cancel: None,
883                    capture: true,
884                },
885                &NoCallee,
886            );
887            if after_another {
888                ctx.set(&parse("temp_data.other"), OwnedDataValue::from(0))
889                    .unwrap();
890            }
891            ctx.set(&parse(dotted), OwnedDataValue::from("w")).unwrap();
892            let (overlay, changes, _) = ctx.settle();
893            olds.push(serde_json::to_value(&changes.last().unwrap().old_value).unwrap());
894            overlays.push(overlay);
895        }
896        for overlay in overlays {
897            overlay.apply(&mut rc);
898        }
899        rc.finish();
900        let data = parse("data").get(&slot).map(serde_json::to_string);
901        (olds, data.unwrap().unwrap())
902    }
903
904    #[test]
905    fn a_first_write_reads_what_was_there_as_a_later_write_does() {
906        // In the run's log, in the owned tree, missing below a missing key, a
907        // `null` element and below it, and below a scalar, where nothing lands.
908        // Settled in order, the write below `list.1` finds the string the
909        // write before it put there, and lands nowhere.
910        let writes = [
911            "data.run",
912            "data.kept.k",
913            "data.absent.x",
914            "data.list.1",
915            "data.list.1.x",
916            "data.s.below",
917        ];
918        let (first, data) = first_writes(&writes, false);
919        assert_eq!(
920            first,
921            vec![
922                json!(7),
923                json!(1),
924                json!(null),
925                json!(null),
926                json!(null),
927                json!(null)
928            ]
929        );
930        assert_eq!(first_writes(&writes, true), (first, data.clone()));
931        assert_eq!(
932            data,
933            r#"{"kept":{"k":"w"},"list":[1,"w"],"s":"scalar","run":"w","absent":{"x":"w"}}"#
934        );
935    }
936
937    /// One call's writes: `(path, Some(value))`, or `(path, None)` for a
938    /// removal.
939    type Call<'a> = Vec<(&'a str, Option<Value>)>;
940
941    /// The run context over `context` after each call's writes are made
942    /// through a `Ctx` of its own over the run's one view, and the overlays
943    /// applied in call order, as a fan-out's elements settle. Wire text.
944    fn settled(context: &Value, calls: &[Call<'_>]) -> String {
945        let exprs = ExprEngine::default();
946        let payload = OwnedDataValue::Null;
947        let mut slot = OwnedDataValue::from(context);
948        let mut rc = RunContext::new(&mut slot, Tuning::default());
949        let mut overlays = Vec::new();
950        for writes in calls {
951            let mut ctx = Ctx::new(
952                CallStart {
953                    reader: rc.reader(),
954                    binding: None,
955                    payload: &payload,
956                    exprs: &exprs,
957                    info: info(),
958                    deadline: None,
959                    cancel: None,
960                    capture: true,
961                },
962                &NoCallee,
963            );
964            for (dotted, value) in writes {
965                match value {
966                    Some(value) => ctx.set(&parse(dotted), OwnedDataValue::from(value)),
967                    None => ctx.remove(&parse(dotted)),
968                }
969                .unwrap();
970            }
971            overlays.push(ctx.settle().0);
972        }
973        for overlay in overlays {
974            overlay.apply(&mut rc);
975        }
976        rc.finish();
977        serde_json::to_string(&slot).unwrap()
978    }
979
980    /// Calls planned over one base, then settled in order, end as one call
981    /// making all their writes in that order would: each write lands at its
982    /// own path on the writes before it. Two keys under a parent neither
983    /// call's base has, then the three cases a write of a whole container
984    /// would lose: padding, an element's removal, a key written after its
985    /// removal.
986    #[test]
987    fn calls_settled_in_order_end_as_one_call_making_their_writes() {
988        // The data root before, each call's writes, and the data root after
989        // as wire text: key order is part of what is checked.
990        let cases: [(Value, Vec<Call<'_>>, &str); 4] = [
991            (
992                json!({}),
993                vec![
994                    vec![("data.prices.a", Some(json!(1)))],
995                    vec![("data.prices.b", Some(json!(2)))],
996                ],
997                r#"{"prices":{"a":1,"b":2}}"#,
998            ),
999            (
1000                json!({"list": [1, 2]}),
1001                vec![
1002                    vec![("data.list.4", Some(json!(5)))],
1003                    vec![("data.list.3", Some(json!(4)))],
1004                ],
1005                r#"{"list":[1,2,null,4,5]}"#,
1006            ),
1007            (
1008                json!({"list": [1, 2, 3]}),
1009                vec![vec![("data.list.0", None)], vec![("data.list.2", None)]],
1010                r#"{"list":[2,3]}"#,
1011            ),
1012            (
1013                json!({"a": 1, "b": 2}),
1014                vec![
1015                    vec![("data.b", Some(json!(5)))],
1016                    vec![("data.a", None), ("data.a", Some(json!(3)))],
1017                ],
1018                r#"{"b":5,"a":3}"#,
1019            ),
1020        ];
1021        for (data, calls, want) in cases {
1022            let context = json!({"data": data, "metadata": {}, "temp_data": {}});
1023            let want = format!(r#"{{"data":{want},"metadata":{{}},"temp_data":{{}}}}"#);
1024            let one_call = vec![calls.concat()];
1025            assert_eq!(settled(&context, &one_call), want, "one call: {calls:?}");
1026            assert_eq!(
1027                settled(&context, &calls),
1028                want,
1029                "settled in order: {calls:?}"
1030            );
1031        }
1032    }
1033
1034    /// A fan-out's calls, each over the run's one view with its own
1035    /// element bound, each writing under its binding and under `data`,
1036    /// then settled in element order onto a run where no binding is: each
1037    /// call reads its writes over its element, and each write lands at its
1038    /// own path on the writes settled before it, as v3 replays an element's
1039    /// `Change`s onto a message that holds no binding. A write below a
1040    /// scalar element is blocked in its call and lands at the settle.
1041    #[test]
1042    fn calls_with_bindings_settled_in_order_replay_each_write_at_its_path() {
1043        let context = json!({"data": {}, "metadata": {}, "temp_data": {}});
1044        let item = parse("temp_data.p");
1045        let index = parse("temp_data.p_index");
1046        // Each element, its writes, what the call then sees at
1047        // `temp_data.p`, and the old values its `Change`s record.
1048        let elements: [(Value, Call<'_>, Value, [Value; 2]); 3] = [
1049            (
1050                json!({"id": "a"}),
1051                vec![
1052                    ("temp_data.p.x", Some(json!(1))),
1053                    ("data.a", Some(json!(1))),
1054                ],
1055                json!({"id": "a", "x": 1}),
1056                [json!(null), json!(null)],
1057            ),
1058            (
1059                json!(5),
1060                vec![
1061                    ("temp_data.p.y", Some(json!(2))),
1062                    ("data.b", Some(json!(2))),
1063                ],
1064                json!(5),
1065                [json!(null), json!(null)],
1066            ),
1067            (
1068                json!({"id": "c"}),
1069                vec![
1070                    ("temp_data.p.id", Some(json!("C"))),
1071                    ("data.a", Some(json!(3))),
1072                ],
1073                json!({"id": "C"}),
1074                [json!("c"), json!(null)],
1075            ),
1076        ];
1077        let exprs = ExprEngine::default();
1078        let payload = OwnedDataValue::Null;
1079        let mut slot = OwnedDataValue::from(&context);
1080        let mut rc = RunContext::new(&mut slot, Tuning::default());
1081        let mut overlays = Vec::new();
1082        let reader = rc.reader();
1083        for (i, (element, writes, seen, olds)) in elements.iter().enumerate() {
1084            let value = OwnedDataValue::from(element);
1085            let binding = Binding {
1086                item: &item,
1087                value: &value,
1088                index: &index,
1089                index_value: OwnedDataValue::from(i),
1090            };
1091            let mut ctx = Ctx::new(
1092                CallStart {
1093                    reader,
1094                    binding: Some(&binding),
1095                    payload: &payload,
1096                    exprs: &exprs,
1097                    info: info(),
1098                    deadline: None,
1099                    cancel: None,
1100                    capture: true,
1101                },
1102                &NoCallee,
1103            );
1104            for (dotted, value) in writes {
1105                let value = value.as_ref().map(OwnedDataValue::from).unwrap();
1106                ctx.set(&parse(dotted), value).unwrap();
1107            }
1108            assert_eq!(get(&ctx, "temp_data.p").as_ref(), Some(seen), "element {i}");
1109            assert_eq!(
1110                eval(&ctx, json!({"var": "temp_data.p"})),
1111                *seen,
1112                "element {i}"
1113            );
1114            assert_eq!(get(&ctx, "temp_data.p_index"), Some(json!(i)));
1115            let (overlay, changes, _) = ctx.settle();
1116            let recorded: Vec<Value> = changes.iter().map(|c| Value::from(&c.old_value)).collect();
1117            assert_eq!(recorded, olds, "element {i}");
1118            overlays.push(overlay);
1119        }
1120        for overlay in overlays {
1121            overlay.apply(&mut rc);
1122        }
1123        rc.finish();
1124        assert_eq!(
1125            serde_json::to_string(&slot).unwrap(),
1126            r#"{"data":{"a":3,"b":2},"metadata":{},"temp_data":{"p":{"x":1,"y":2,"id":"C"}}}"#
1127        );
1128    }
1129
1130    #[test]
1131    fn render_lends_each_hole_from_where_it_is_and_builds_no_tree() {
1132        // Only a value the call wrote below a hole is converted.
1133        let context = json!({"data": {"order": {"customer": "C", "qty": 3}},
1134            "metadata": {}, "temp_data": {}});
1135        let exprs = ExprEngine::default();
1136        let payload = OwnedDataValue::Null;
1137        let mut slot = OwnedDataValue::from(&context);
1138        let mut rc = RunContext::new(&mut slot, Tuning::default());
1139        rc.read(|_, _| ());
1140        // The run writes one value with its owned copy, as `map` does under
1141        // capture, and one into the view only, as the progress write does.
1142        rc.set(
1143            &parse("data.order.since"),
1144            OwnedDataValue::from("2026"),
1145            None,
1146        );
1147        let progress = parse("metadata.progress");
1148        rc.edit(|e| {
1149            e.set(
1150                &progress,
1151                OwnedDataValue::from("p").to_arena(e.arena()),
1152                None,
1153            )
1154        });
1155        let mut ctx = Ctx::new(
1156            CallStart {
1157                reader: rc.reader(),
1158                binding: None,
1159                payload: &payload,
1160                exprs: &exprs,
1161                info: info(),
1162                deadline: None,
1163                cancel: None,
1164                capture: true,
1165            },
1166            &NoCallee,
1167        );
1168        ctx.set(&parse("temp_data.mine"), OwnedDataValue::from(1))
1169            .unwrap();
1170        ctx.set(&parse("temp_data.box.k"), OwnedDataValue::from(2))
1171            .unwrap();
1172        let doc = exprs
1173            .interpolated(&json!([
1174                {"var": "temp_data.mine"},
1175                {"var": "temp_data.box"},
1176                {"var": "data.order.since"},
1177                {"var": "metadata.progress"},
1178                {"var": "data.order"},
1179                {"var": "data.order.customer"},
1180                {"var": ["data.absent", "d"]},
1181                {"var": ["payload.x", "p"]}
1182            ]))
1183            .unwrap();
1184        let rendered = ctx.render(&doc);
1185        let kinds: Vec<&str> = rendered
1186            .filled()
1187            .iter()
1188            .map(|filled| match filled {
1189                Filled::Owned(Cow::Borrowed(_)) => "lent",
1190                Filled::Owned(Cow::Owned(_)) => "converted",
1191                Filled::View(_) => "view",
1192                Filled::Default(_) => "default",
1193            })
1194            .collect();
1195        assert_eq!(
1196            kinds,
1197            [
1198                "lent",      // the call's own write
1199                "converted", // written below by the call
1200                "lent",      // the run's log, its owned copy
1201                "view",      // the run's log, the view alone
1202                "view",      // above a write in the run's log
1203                "lent",      // the owned tree
1204                "default",   // nothing there
1205                "default"    // the payload, never in the context
1206            ]
1207        );
1208        assert_eq!(
1209            serde_json::to_value(&rendered).unwrap(),
1210            json!([1, {"k": 2}, "2026", "p",
1211                {"customer": "C", "qty": 3, "since": "2026"}, "C", "d", "p"])
1212        );
1213    }
1214
1215    /// A callee whose resolve puts a value where its walk reports a
1216    /// secret-reading expression, as a field resolving its secret some other
1217    /// way than through the resolver would.
1218    struct Leaky;
1219
1220    impl BoundCall for Leaky {
1221        fn call<'a>(
1222            &'a self,
1223            _ctx: &'a mut Ctx<'_>,
1224        ) -> crate::function::BoxFuture<'a, Result<crate::function::Outcome>> {
1225            Box::pin(std::future::ready(Ok(crate::function::Outcome::Ok)))
1226        }
1227
1228        fn visit(&self, v: &mut dyn ExprVisitor) {
1229            let at = |text: &str| text.parse::<AuthoredPath>().unwrap();
1230            v.expr(&at("a.b[1]"), &json!({"secret": "k"}), false);
1231            v.expr(
1232                &at("a.c"),
1233                &json!({"cat": ["x", {"secret": {"var": "data.k"}}]}),
1234                false,
1235            );
1236            v.expr(&at("kept"), &json!({"var": "data.k"}), false);
1237            // Nothing is there: nothing is written.
1238            v.expr(&at("absent.x"), &json!({"secret": "k"}), false);
1239        }
1240
1241        fn primary(&self) -> Primary<'_> {
1242            Primary::Undeclared
1243        }
1244
1245        fn resolve(&self, _r: &mut dyn Resolver) -> Result<OwnedDataValue> {
1246            Ok(OwnedDataValue::from(&json!({
1247                "a": {"b": [1, "leaked"], "c": "leaked too"},
1248                "kept": "v",
1249            })))
1250        }
1251    }
1252
1253    #[test]
1254    fn input_nulls_every_value_the_walk_reports_reading_a_secret() {
1255        let context = json!({"data": {"k": "v"}, "metadata": {}, "temp_data": {}});
1256        let exprs = ExprEngine::default();
1257        let payload = OwnedDataValue::Null;
1258        let mut slot = OwnedDataValue::from(&context);
1259        let mut rc = RunContext::new(&mut slot, Tuning::default());
1260        let ctx = Ctx::new(
1261            CallStart {
1262                reader: rc.reader(),
1263                binding: None,
1264                payload: &payload,
1265                exprs: &exprs,
1266                info: info(),
1267                deadline: None,
1268                cancel: None,
1269                capture: true,
1270            },
1271            &Leaky,
1272        );
1273        assert_eq!(
1274            ctx.input().unwrap().to_json_string(),
1275            r#"{"a":{"b":[1,null],"c":null},"kept":"v"}"#
1276        );
1277    }
1278
1279    #[test]
1280    fn debug_shows_the_call_and_never_the_context() {
1281        let context = json!({"data": {"secret_looking": "value"}});
1282        with_ctx(&context, info(), None, |ctx| {
1283            let text = format!("{ctx:?}");
1284            assert!(text.contains("task_id: \"t\"") && text.contains("overlay_empty: true"));
1285            assert!(!text.contains("secret_looking"), "{text}");
1286            assert!(text.ends_with(".. }"), "{text}");
1287        });
1288    }
1289}