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