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::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#[derive(Clone, Copy, Debug)]
26#[non_exhaustive]
27pub struct CallInfo<'a> {
28 pub program: ProgramId,
31 pub workflow: WorkflowIx,
33 pub task: TaskIx,
35 pub function: &'a str,
37 pub workflow_id: &'a str,
39 pub task_id: &'a str,
41 pub channel: Option<&'a str>,
44 pub routing_bucket: Option<u8>,
48 pub loop_counter: Option<i64>,
50 pub element_index: Option<usize>,
52}
53
54pub struct Ctx<'r> {
111 start: CallStart<'r>,
116 callee: &'r dyn BoundCall,
119 overlay: Overlay,
121 changes: Vec<Change>,
123 errors: Vec<ErrorRecord>,
125}
126
127#[derive(Clone, Copy)]
131pub(crate) struct CallStart<'r> {
132 pub(crate) reader: Reader<'r>,
134 pub(crate) binding: Option<&'r Binding<'r>>,
136 pub(crate) payload: &'r OwnedDataValue,
138 pub(crate) exprs: &'r ExprEngine,
140 pub(crate) info: CallInfo<'r>,
142 pub(crate) deadline: Option<Instant>,
144 pub(crate) cancel: Option<&'r CancelToken>,
146 pub(crate) capture: bool,
148}
149
150#[derive(Clone, Copy, Debug)]
152pub(crate) enum Primary<'r> {
153 Undeclared,
155 Unset(&'r str),
158 Set(&'r Output),
160}
161
162pub(crate) struct Binding<'r> {
167 pub(crate) item: &'r Path,
169 pub(crate) value: &'r OwnedDataValue,
171 pub(crate) index: &'r Path,
173 pub(crate) index_value: OwnedDataValue,
175}
176
177impl Binding<'_> {
178 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 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
216fn 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
224fn 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 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 pub fn info(&self) -> &CallInfo<'r> {
251 &self.start.info
252 }
253
254 pub(crate) fn callee(&self) -> &'r dyn BoundCall {
256 self.callee
257 }
258
259 pub fn input(&self) -> Result<OwnedDataValue> {
283 let mut input = self.callee.resolve(&mut Redacted(self))?;
284 self.callee.visit(&mut Redact(&mut input));
287 Ok(input)
288 }
289
290 pub fn resolve<C: super::FunctionConfig>(&self, cfg: &C) -> Result<OwnedDataValue> {
305 cfg.resolve(&mut &*self)
306 }
307
308 pub fn payload(&self) -> &OwnedDataValue {
310 self.start.payload
311 }
312
313 pub fn exprs(&self) -> &ExprEngine {
316 self.start.exprs
317 }
318
319 pub fn secret(&self, name: &str) -> Option<&OwnedDataValue> {
322 self.start.exprs.secrets().get(name)
323 }
324
325 pub fn deadline(&self) -> Option<Instant> {
330 self.start.deadline
331 }
332
333 pub fn cancel(&self) -> Option<&CancelToken> {
338 self.start.cancel
339 }
340
341 pub fn get(&self, path: &Path) -> Option<Cow<'_, OwnedDataValue>> {
347 self.find(path).map(Filled::into_cow)
348 }
349
350 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 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 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 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 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 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 pub fn set(&mut self, path: &Path, value: OwnedDataValue) -> Result<()> {
464 path.check_set(&value)?;
465 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 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 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 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 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 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 pub fn record_error(&mut self, e: ErrorRecord) {
575 self.errors.push(e);
576 }
577
578 pub(crate) fn settle(self) -> (Overlay, Vec<Change>, Vec<ErrorRecord>) {
580 (self.overlay, self.changes, self.errors)
581 }
582}
583
584impl 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
600struct 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
620struct 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
634fn 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 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#[cfg(test)]
661pub(crate) mod testing {
662 use super::*;
663 use crate::function::{BoxFuture, Outcome};
664 use crate::state::{RunContext, Tuning};
665
666 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 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 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 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 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 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 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 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 type Call<'a> = Vec<(&'a str, Option<Value>)>;
940
941 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 #[test]
987 fn calls_settled_in_order_end_as_one_call_making_their_writes() {
988 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 #[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 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 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 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", "converted", "lent", "view", "view", "lent", "default", "default" ]
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 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 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}