1use super::arena::with_arena;
4use super::expr::{Evaluated, Expr, FromValue};
5use super::operators::{ALL_OPERATORS, Operator, OperatorFamily};
6use super::secrets::{SECRET_OPERATOR, SecretOperator, Secrets};
7use crate::base::{Error, ErrorKind, ExprCause, Result};
8use bumpalo::Bump;
9use datalogic_rs::{CustomOperator, DataValue, Engine, Logic};
10use datavalue::OwnedDataValue;
11use serde_json::Value;
12use std::collections::{HashMap, HashSet};
13use std::sync::Arc;
14
15pub(super) const TEMPLATE_KEY_ESCAPE: char = '$';
19
20struct SharedOperator(Arc<dyn CustomOperator>);
23
24impl CustomOperator for SharedOperator {
25 #[inline]
26 fn evaluate<'a>(
27 &self,
28 args: &[&'a datalogic_rs::DataValue<'a>],
29 ctx: &mut datalogic_rs::operator::EvalContext<'_, 'a>,
30 arena: &'a datalogic_rs::bumpalo::Bump,
31 ) -> datalogic_rs::Result<&'a datalogic_rs::DataValue<'a>> {
32 self.0.evaluate(args, ctx, arena)
33 }
34
35 fn info(&self) -> datalogic_rs::CustomOperatorInfo {
36 self.0.info()
37 }
38
39 fn check(&self, args: &[OwnedDataValue]) -> std::result::Result<(), datalogic_rs::Diagnostic> {
40 self.0.check(args)
41 }
42}
43
44mod sealed {
45 use bumpalo::Bump;
46 use datalogic_rs::{DataValue, EvalInput};
47 use datavalue::OwnedDataValue;
48 use serde_json::Value;
49
50 pub trait Sealed<'r> {
53 fn view_in<'a>(self, arena: &'a Bump) -> datalogic_rs::Result<&'a DataValue<'a>>
56 where
57 'r: 'a;
58 }
59
60 impl<'r> Sealed<'r> for &'r OwnedDataValue {
61 #[inline]
62 fn view_in<'a>(self, arena: &'a Bump) -> datalogic_rs::Result<&'a DataValue<'a>>
63 where
64 'r: 'a,
65 {
66 <&'a OwnedDataValue as EvalInput<'a>>::into_arena_value(self, arena)
67 }
68 }
69
70 impl<'r> Sealed<'r> for &'r Value {
71 #[inline]
72 fn view_in<'a>(self, arena: &'a Bump) -> datalogic_rs::Result<&'a DataValue<'a>>
73 where
74 'r: 'a,
75 {
76 <&'a Value as EvalInput<'a>>::into_arena_value(self, arena)
77 }
78 }
79}
80
81pub trait EvalRoot<'r>: sealed::Sealed<'r> {}
105
106impl<'r> EvalRoot<'r> for &'r OwnedDataValue {}
107
108impl<'r> EvalRoot<'r> for &'r Value {}
109
110struct Inner {
111 logic: Engine,
112 operators: Vec<Operator>,
113 by_name: HashMap<Box<str>, usize>,
114 secrets: Arc<Secrets>,
115}
116
117#[derive(Clone)]
122pub struct ExprEngine {
123 inner: Arc<Inner>,
124}
125
126impl Default for ExprEngine {
127 fn default() -> Self {
128 Self::builder()
130 .build()
131 .expect("an engine with no custom operators cannot fail to build")
132 }
133}
134
135impl std::fmt::Debug for ExprEngine {
136 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137 f.debug_struct("ExprEngine")
138 .field("operators", &self.inner.operators.len())
139 .field("secrets", &self.inner.secrets)
140 .finish()
141 }
142}
143
144impl ExprEngine {
145 pub fn builder() -> ExprEngineBuilder {
147 ExprEngineBuilder::default()
148 }
149
150 pub fn operators(&self) -> &[Operator] {
154 &self.inner.operators
155 }
156
157 pub(crate) fn operator(&self, name: &str) -> Option<&Operator> {
159 self.inner
160 .by_name
161 .get(name)
162 .map(|&i| &self.inner.operators[i])
163 }
164
165 pub fn secrets(&self) -> &Secrets {
167 &self.inner.secrets
168 }
169
170 pub fn escape(&self) -> char {
172 TEMPLATE_KEY_ESCAPE
173 }
174
175 pub fn compile<T: FromValue>(&self, raw: &Value) -> Result<Expr<T>> {
184 let reads_secret = super::walk::reads_secret(raw);
185 let logic = self
186 .logic()
187 .compile(raw)
188 .map_err(|e| compile_error(e, reads_secret))?;
189 let constant = if logic.is_constant() {
192 let folded = with_arena(|arena| {
193 self.logic()
194 .evaluate(&logic, DataValue::Object(&[]), arena)
195 .map_err(|e| eval_error(e, Some(&logic), None, reads_secret))
196 .and_then(|v| {
197 T::from_view(v).map_err(|why| {
198 Error::new(
199 ErrorKind::Config,
200 format!(
201 "`{}` must be {}: {why}",
202 super::expr::snippet(raw),
203 T::type_name()
204 ),
205 )
206 .with_expr_cause(ExprCause::type_mismatch(None))
207 })
208 })
209 })?;
210 Some(folded)
211 } else {
212 None
213 };
214 Ok(Expr {
215 raw: raw.clone(),
216 logic,
217 constant,
218 reads_secret,
219 })
220 }
221
222 pub fn eval<'e, 'r, T: FromValue>(
233 &self,
234 expr: &'e Expr<T>,
235 root: impl EvalRoot<'r>,
236 ) -> Result<Evaluated<T::Out<'e>>> {
237 if let Some(c) = &expr.constant {
238 return Ok(Evaluated {
239 value: T::lend(c),
240 ops: if cfg!(feature = "budget") {
241 Some(0)
242 } else {
243 None
244 },
245 });
246 }
247 with_arena(|arena| {
248 let root = sealed::Sealed::view_in(root, arena)
249 .map_err(|e| eval_error(e, Some(&expr.logic), None, expr.reads_secret))?;
250 let (view, ops) = self.eval_in(expr.logic(), expr.reads_secret, root, arena)?;
251 Ok(Evaluated {
252 value: T::give(expr.convert(view, ops)?),
253 ops,
254 })
255 })
256 }
257
258 pub(crate) fn eval_view_metered<'e, T: FromValue>(
262 &self,
263 expr: &'e Expr<T>,
264 root: &DataValue<'_>,
265 arena: &Bump,
266 ops_limit: Option<u64>,
267 ) -> (Result<T::Out<'e>>, Option<u64>) {
268 if let Some(c) = expr.constant() {
269 let ops = if cfg!(feature = "budget") {
270 Some(0)
271 } else {
272 None
273 };
274 return (Ok(T::lend(c)), ops);
275 }
276 let (res, ops) =
277 self.eval_in_metered(expr.logic(), expr.reads_secret, root, arena, ops_limit);
278 let out = res.and_then(|view| expr.convert(view, ops).map(T::give));
279 (out, ops)
280 }
281
282 pub(crate) fn eval_in<'a>(
292 &self,
293 logic: &'a Logic,
294 reads_secret: bool,
295 root: &'a DataValue<'a>,
296 arena: &'a Bump,
297 ) -> Result<(&'a DataValue<'a>, Option<u64>)> {
298 let (res, ops) = self.eval_in_metered(logic, reads_secret, root, arena, None);
299 res.map(|v| (v, ops))
300 }
301
302 pub(crate) fn eval_in_metered<'a>(
305 &self,
306 logic: &'a Logic,
307 reads_secret: bool,
308 root: &'a DataValue<'a>,
309 arena: &'a Bump,
310 ops_limit: Option<u64>,
311 ) -> (Result<&'a DataValue<'a>>, Option<u64>) {
312 #[cfg(feature = "budget")]
313 {
314 let engine_budget = self.logic().resolve_ops_budget(None);
315 let budget = match ops_limit {
316 Some(b) => engine_budget.min(b),
317 None => engine_budget,
318 };
319 let report = self
320 .logic()
321 .evaluate_metered_report(logic, root, arena, budget);
322 let ops = Some(report.ops);
323 let res = report
324 .value
325 .map_err(|e| eval_error(e, Some(logic), ops, reads_secret));
326 (res, ops)
327 }
328 #[cfg(not(feature = "budget"))]
329 {
330 let _ = ops_limit;
331 let res = self
332 .logic()
333 .evaluate(logic, root, arena)
334 .map_err(|e| eval_error(e, Some(logic), None, reads_secret));
335 (res, None)
336 }
337 }
338
339 pub(crate) fn logic(&self) -> &Engine {
341 &self.inner.logic
342 }
343}
344
345fn compile_error(e: datalogic_rs::Error, reads_secret: bool) -> Error {
348 let e = if reads_secret {
349 sanitize_secret_error(e)
350 } else {
351 e
352 };
353 let cause = ExprCause::from_datalogic(&e, None, None);
354 let msg = e.to_string();
355 Error::new(ErrorKind::Compile, msg)
356 .with_source(e)
357 .with_expr_cause(cause)
358}
359
360fn eval_error(
362 e: datalogic_rs::Error,
363 logic: Option<&Logic>,
364 ops: Option<u64>,
365 reads_secret: bool,
366) -> Error {
367 let e = if reads_secret {
368 sanitize_secret_error(e)
369 } else {
370 e
371 };
372 let kind = if matches!(e.kind, datalogic_rs::ErrorKind::BudgetExceeded { .. }) {
373 ErrorKind::Budget
374 } else {
375 ErrorKind::Eval
376 };
377 let cause = ExprCause::from_datalogic(&e, logic, ops);
378 let msg = e.to_string();
379 Error::new(kind, msg).with_source(e).with_expr_cause(cause)
380}
381
382fn sanitize_secret_error(mut e: datalogic_rs::Error) -> datalogic_rs::Error {
386 match &mut e.kind {
387 datalogic_rs::ErrorKind::Thrown(v) => {
388 *v = OwnedDataValue::Null;
389 }
390 datalogic_rs::ErrorKind::ParseError(s)
391 | datalogic_rs::ErrorKind::FormatError(s)
392 | datalogic_rs::ErrorKind::TypeError(s) => {
393 *s = std::borrow::Cow::Borrowed("<redacted>");
394 }
395 datalogic_rs::ErrorKind::Custom(_) => {
396 let op = e.operator().map(str::to_owned);
397 let ids = e.node_ids().to_vec();
398 let mut redacted = datalogic_rs::Error::custom_message("<redacted>");
399 if let Some(op) = op {
400 redacted = redacted.with_operator(op);
401 }
402 if !ids.is_empty() {
403 redacted = redacted.with_node_ids(ids);
404 }
405 return redacted;
406 }
407 _ => {}
408 }
409 e
410}
411
412#[derive(Default)]
418pub struct ExprEngineBuilder {
419 families: Vec<OperatorFamily>,
420 operators: Vec<(Operator, Arc<dyn CustomOperator>)>,
421 secrets: Secrets,
422 #[cfg(feature = "budget")]
423 ops_budget: Option<u64>,
424}
425
426impl std::fmt::Debug for ExprEngineBuilder {
427 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
429 let operators: Vec<&str> = self.operators.iter().map(|(info, _)| &*info.name).collect();
430 f.debug_struct("ExprEngineBuilder")
431 .field("families", &self.families)
432 .field("operators", &operators)
433 .field("secrets", &self.secrets)
434 .finish_non_exhaustive()
435 }
436}
437
438impl ExprEngineBuilder {
439 #[must_use]
448 pub fn family(mut self, family: OperatorFamily) -> Self {
449 self.families.push(family);
450 self
451 }
452
453 #[must_use]
455 pub fn families(mut self, families: impl IntoIterator<Item = OperatorFamily>) -> Self {
456 self.families.extend(families);
457 self
458 }
459
460 #[must_use]
474 pub fn all_operators(self) -> Self {
475 self.families(ALL_OPERATORS.iter().copied())
476 }
477
478 #[must_use]
488 pub fn operator(self, name: impl Into<String>, op: impl CustomOperator + 'static) -> Self {
489 self.operator_with(Operator::custom(name.into()), op)
490 }
491
492 #[must_use]
497 pub fn operator_with(mut self, mut info: Operator, op: impl CustomOperator + 'static) -> Self {
498 info.family = OperatorFamily::Custom;
499 self.operators.push((info, Arc::new(op)));
500 self
501 }
502
503 #[must_use]
505 pub fn secrets(mut self, secrets: Secrets) -> Self {
506 self.secrets = secrets;
507 self
508 }
509
510 #[cfg(feature = "budget")]
512 #[must_use]
513 pub fn ops_budget(mut self, ops: u64) -> Self {
514 self.ops_budget = Some(ops);
515 self
516 }
517
518 pub fn build(self) -> Result<ExprEngine> {
525 let mut upstream_families = Vec::with_capacity(self.families.len());
526 for family in self.families {
527 match family {
528 OperatorFamily::Core | OperatorFamily::Secret => {}
529 OperatorFamily::Custom => {
530 return Err(Error::new(
531 ErrorKind::Config,
532 "custom operators are registered with `operator`, not `family`",
533 ));
534 }
535 other => {
536 let upstream = other
537 .to_upstream()
538 .expect("every built-in family maps to a datalogic family");
539 if !upstream.is_compiled() {
540 return Err(Error::new(
541 ErrorKind::Config,
542 format!(
543 "operator family `{other:?}` requires its cargo feature to be enabled"
544 ),
545 ));
546 }
547 upstream_families.push(upstream);
548 }
549 }
550 }
551 let secrets = Arc::new(self.secrets);
552 let mut builder = Engine::builder()
553 .with_families(upstream_families)
554 .with_templating(true)
555 .with_template_key_escape(TEMPLATE_KEY_ESCAPE);
556 #[cfg(feature = "budget")]
559 {
560 builder = builder.with_config(
561 datalogic_rs::EvaluationConfig::default().with_ops_budget(self.ops_budget),
562 );
563 }
564 let mut infos = Vec::with_capacity(self.operators.len());
565 for (info, op) in self.operators {
566 builder = builder.add_operator(info.name.to_string(), SharedOperator(op));
567 infos.push(info);
568 }
569 builder = builder.add_operator(SECRET_OPERATOR, SecretOperator(Arc::clone(&secrets)));
570 let logic = builder.build();
571
572 let builtin: HashSet<&'static str> = logic.builtin_operator_names().collect();
575 let mut seen = HashSet::new();
576 for info in &infos {
577 let name = &*info.name;
578 let refusal = if name == SECRET_OPERATOR {
579 Some("is reserved for the engine's own operator")
580 } else if builtin.contains(name) {
581 Some("is the name of a built-in operator")
582 } else if !seen.insert(name) {
583 Some("is registered twice")
584 } else {
585 None
586 };
587 if let Some(why) = refusal {
588 return Err(Error::new(
589 ErrorKind::Config,
590 format!("operator `{name}` {why}"),
591 ));
592 }
593 }
594
595 let mut operators: Vec<Operator> =
598 logic.operators().flat_map(Operator::from_logic).collect();
599 operators.extend(infos);
600 operators.push(Operator::secret());
601 let by_name: HashMap<Box<str>, usize> = operators
602 .iter()
603 .enumerate()
604 .map(|(i, op)| (Box::from(&*op.name), i))
605 .collect();
606
607 Ok(ExprEngine {
608 inner: Arc::new(Inner {
609 logic,
610 operators,
611 by_name,
612 secrets,
613 }),
614 })
615 }
616}
617
618#[cfg(test)]
619impl ExprEngine {
620 pub(crate) fn try_eval_json(
622 &self,
623 logic: &serde_json::Value,
624 ) -> std::result::Result<serde_json::Value, datalogic_rs::Error> {
625 let compiled = self.logic().compile_arc(logic).expect("should compile");
626 let ctx = datavalue::OwnedDataValue::from(&serde_json::json!({}));
627 let text = self.logic().session().eval_str(&compiled, &ctx)?;
628 Ok(serde_json::from_str(&text).expect("eval_str output should be valid JSON"))
629 }
630
631 pub(crate) fn eval_json(&self, logic: &serde_json::Value) -> serde_json::Value {
633 self.try_eval_json(logic).expect("should evaluate")
634 }
635}
636
637#[cfg(test)]
638mod tests {
639 use super::*;
640 use serde_json::json;
641
642 #[test]
643 fn empty_operand_results_this_crate_would_silently_break_on() {
644 let e = ExprEngine::default();
650 for (logic, expected) in [
651 (json!({"and": []}), json!(null)),
652 (json!({"or": []}), json!(null)),
653 (json!({"+": []}), json!(0)),
654 (json!({"*": []}), json!(1)),
655 (json!({"cat": []}), json!("")),
656 (json!({"merge": []}), json!([])),
657 (json!({"missing": []}), json!([])),
658 ] {
659 assert_eq!(e.eval_json(&logic), expected, "for {logic}");
660 }
661 }
662
663 #[test]
664 fn a_missing_var_path_resolves_to_null_not_an_error() {
665 let e = ExprEngine::default();
671 assert_eq!(
672 e.eval_json(&json!({"var": "data.does_not_exist"})),
673 json!(null)
674 );
675 }
676
677 #[test]
678 fn truthy_falsy_matches_the_documented_semantics() {
679 let e = ExprEngine::default();
685 for (v, truthy) in [
686 (json!(0), false),
687 (json!(""), false),
688 (json!(false), false),
689 (json!(null), false),
690 (json!([]), false),
691 (json!({}), false),
692 (json!("x"), true),
693 (json!(1), true),
694 ] {
695 assert_eq!(
696 e.eval_json(&json!({"!!": v})),
697 json!(truthy),
698 "truthiness of {v}"
699 );
700 }
701 }
702
703 #[test]
704 fn an_unrecognised_operator_is_not_an_error_under_templating() {
705 let e = ExprEngine::default();
713 let logic = json!({"totally_made_up_op_xyz": ["a", "b"]});
714 assert_eq!(
715 e.eval_json(&logic),
716 logic,
717 "an unrecognised operator must echo back verbatim, not error"
718 );
719 }
720
721 #[test]
727 fn a_gated_operator_echoes_back_while_its_family_is_off() {
728 let e = ExprEngine::default();
729 let logic = json!({"starts_with": ["hello", "he"]});
730 assert_eq!(
731 e.eval_json(&logic),
732 logic,
733 "an operator behind an unenabled family must echo back, not error"
734 );
735 }
736
737 #[test]
743 fn a_gated_operator_evaluates_once_its_family_is_on() {
744 let e = ExprEngine::builder()
745 .family(OperatorFamily::ExtString)
746 .build()
747 .unwrap();
748 assert_eq!(
749 e.eval_json(&json!({"starts_with": ["hello", "he"]})),
750 json!(true),
751 "with ExtString on, starts_with must evaluate, not echo"
752 );
753 }
754
755 #[test]
762 fn datetime_feature_changes_plain_string_comparison() {
763 let e = ExprEngine::default();
764 let logic = json!({"==": ["2024-01-15T00:00:00Z", "2024-01-15T01:00:00+01:00"]});
765 #[cfg(feature = "datetime")]
766 assert_eq!(e.eval_json(&logic), json!(true));
767 #[cfg(not(feature = "datetime"))]
768 assert_eq!(e.eval_json(&logic), json!(false));
769 }
770
771 #[test]
774 fn ext_string_family_reaches_datalogic() {
775 let e = ExprEngine::builder()
776 .family(OperatorFamily::ExtString)
777 .build()
778 .unwrap();
779 assert_eq!(e.eval_json(&json!({"upper": "ab"})), json!("AB"));
780 }
781
782 #[test]
783 fn ext_array_family_reaches_datalogic() {
784 let e = ExprEngine::builder()
785 .family(OperatorFamily::ExtArray)
786 .build()
787 .unwrap();
788 assert_eq!(e.eval_json(&json!({"sort": [[3, 1, 2]]})), json!([1, 2, 3]));
789 }
790
791 #[test]
792 fn ext_math_family_reaches_datalogic() {
793 let e = ExprEngine::builder()
794 .family(OperatorFamily::ExtMath)
795 .build()
796 .unwrap();
797 assert_eq!(e.eval_json(&json!({"abs": -5})), json!(5));
798 }
799
800 #[test]
801 fn ext_control_family_reaches_datalogic() {
802 let e = ExprEngine::builder()
803 .family(OperatorFamily::ExtControl)
804 .build()
805 .unwrap();
806 assert_eq!(
807 e.eval_json(&json!({"??": [null, "fallback"]})),
808 json!("fallback")
809 );
810 }
811
812 #[test]
813 fn ext_object_family_reaches_datalogic() {
814 let e = ExprEngine::builder()
815 .family(OperatorFamily::ExtObject)
816 .build()
817 .unwrap();
818 assert_eq!(
819 e.eval_json(&json!({"keys": [{"a": 1, "b": 2}]})),
820 json!(["a", "b"])
821 );
822 }
823
824 #[test]
825 fn error_handling_family_reaches_datalogic() {
826 let e = ExprEngine::builder()
829 .family(OperatorFamily::ErrorHandling)
830 .build()
831 .unwrap();
832 assert_eq!(
833 e.eval_json(&json!({"try": [{"throw": "boom"}, "recovered"]})),
834 json!("recovered")
835 );
836 }
837
838 #[cfg(feature = "datetime")]
844 #[test]
845 fn datetime_family_reaches_datalogic() {
846 let logic = json!({"now": []});
847 assert_eq!(
848 ExprEngine::default().eval_json(&logic),
849 logic,
850 "without OperatorFamily::Datetime selected, `now` echoes back"
851 );
852 let e = ExprEngine::builder()
853 .family(OperatorFamily::Datetime)
854 .build()
855 .unwrap();
856 let result = e.eval_json(&logic);
857 assert_ne!(result, logic, "with Datetime on, `now` must not echo back");
858 assert!(!result.is_null(), "`now` should produce a value, got null");
859 }
860 #[cfg(feature = "budget")]
861 #[test]
862 fn the_ops_budget_stops_a_map_over_three_items() {
863 let e = ExprEngine::builder().ops_budget(2).build().unwrap();
864 let logic = json!({"map": [[1, 2, 3], {"+": [{"var": ""}, 1]}]});
865 let err = e.try_eval_json(&logic).unwrap_err();
866 assert_eq!(err.tag(), "BudgetExceeded");
867 assert_eq!(ExprEngine::default().eval_json(&logic), json!([2, 3, 4]));
869 }
870
871 #[test]
872 fn escape_is_dollar_and_the_escaped_key_is_literal() {
873 let e = ExprEngine::default();
874 assert_eq!(e.escape(), '$');
875 assert_eq!(e.eval_json(&json!({"$cat": [1]})), json!({"cat": [1]}));
876 }
877}