1use 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#[derive(Clone, Copy, Debug)]
27#[non_exhaustive]
28pub struct CallInfo<'a> {
29 pub program: ProgramId,
32 pub workflow: WorkflowIx,
34 pub task: TaskIx,
36 pub function: &'a str,
38 pub workflow_id: &'a str,
40 pub task_id: &'a str,
42 pub channel: Option<&'a str>,
45 pub routing_bucket: Option<u8>,
49 pub loop_counter: Option<i64>,
51 pub element_index: Option<usize>,
53}
54
55pub struct Ctx<'r> {
112 start: CallStart<'r>,
117 callee: &'r dyn BoundCall,
120 overlay: Overlay,
122 changes: Vec<Change>,
124 errors: Vec<ErrorRecord>,
126}
127
128#[derive(Clone, Copy)]
132pub(crate) struct CallStart<'r> {
133 pub(crate) reader: Reader<'r>,
135 pub(crate) binding: Option<&'r Binding<'r>>,
137 pub(crate) payload: &'r OwnedDataValue,
139 pub(crate) exprs: &'r ExprEngine,
141 pub(crate) budget: &'r TaskBudget<'r>,
143 pub(crate) info: CallInfo<'r>,
145 pub(crate) deadline: Option<Instant>,
147 pub(crate) cancel: Option<&'r CancelToken>,
149 pub(crate) capture: bool,
151}
152
153#[derive(Clone, Copy, Debug)]
155pub(crate) enum Primary<'r> {
156 Undeclared,
158 Unset(&'r str),
161 Set(&'r Output),
163}
164
165pub(crate) struct Binding<'r> {
170 pub(crate) item: &'r Path,
172 pub(crate) value: &'r OwnedDataValue,
174 pub(crate) index: &'r Path,
176 pub(crate) index_value: OwnedDataValue,
178}
179
180impl Binding<'_> {
181 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 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
219fn 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
227fn 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 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 pub fn info(&self) -> &CallInfo<'r> {
254 &self.start.info
255 }
256
257 pub(crate) fn callee(&self) -> &'r dyn BoundCall {
259 self.callee
260 }
261
262 pub fn input(&self) -> Result<OwnedDataValue> {
286 let mut input = self.callee.resolve(&mut Redacted(self))?;
287 self.callee.visit(&mut Redact(&mut input));
290 Ok(input)
291 }
292
293 pub fn resolve<C: super::FunctionConfig>(&self, cfg: &C) -> Result<OwnedDataValue> {
308 cfg.resolve(&mut &*self)
309 }
310
311 pub fn payload(&self) -> &OwnedDataValue {
313 self.start.payload
314 }
315
316 pub fn exprs(&self) -> &ExprEngine {
319 self.start.exprs
320 }
321
322 pub fn secret(&self, name: &str) -> Option<&OwnedDataValue> {
325 self.start.exprs.secrets().get(name)
326 }
327
328 pub fn deadline(&self) -> Option<Instant> {
333 self.start.deadline
334 }
335
336 pub fn cancel(&self) -> Option<&CancelToken> {
341 self.start.cancel
342 }
343
344 pub fn accountant(&self) -> Option<&'r dyn RunAccountant> {
348 self.start.budget.accountant()
349 }
350
351 pub fn get(&self, path: &Path) -> Option<Cow<'_, OwnedDataValue>> {
357 self.find(path).map(Filled::into_cow)
358 }
359
360 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 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 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 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 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 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 pub fn set(&mut self, path: &Path, value: OwnedDataValue) -> Result<()> {
482 path.check_set(&value)?;
483 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 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 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 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 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 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 pub fn record_error(&mut self, e: ErrorRecord) {
593 self.errors.push(e);
594 }
595
596 pub(crate) fn settle(self) -> (Overlay, Vec<Change>, Vec<ErrorRecord>) {
598 (self.overlay, self.changes, self.errors)
599 }
600}
601
602impl 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
618struct 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
638struct 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
652fn 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 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#[cfg(test)]
679pub(crate) mod testing {
680 use super::*;
681 use crate::function::{BoxFuture, Outcome};
682 use crate::state::{RunContext, Tuning};
683
684 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 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 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 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 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 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 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 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 type Call<'a> = Vec<(&'a str, Option<Value>)>;
964
965 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 #[test]
1013 fn calls_settled_in_order_end_as_one_call_making_their_writes() {
1014 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 #[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 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 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 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", "converted", "lent", "view", "view", "lent", "default", "default" ]
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 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 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}