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, 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
36mod sealed {
37 use bumpalo::Bump;
38 use datalogic_rs::{DataValue, EvalInput};
39 use datavalue::OwnedDataValue;
40 use serde_json::Value;
41
42 pub trait Sealed<'r> {
45 fn view_in<'a>(self, arena: &'a Bump) -> datalogic_rs::Result<&'a DataValue<'a>>
48 where
49 'r: 'a;
50 }
51
52 impl<'r> Sealed<'r> for &'r OwnedDataValue {
53 #[inline]
54 fn view_in<'a>(self, arena: &'a Bump) -> datalogic_rs::Result<&'a DataValue<'a>>
55 where
56 'r: 'a,
57 {
58 <&'a OwnedDataValue as EvalInput<'a>>::into_arena_value(self, arena)
59 }
60 }
61
62 impl<'r> Sealed<'r> for &'r Value {
63 #[inline]
64 fn view_in<'a>(self, arena: &'a Bump) -> datalogic_rs::Result<&'a DataValue<'a>>
65 where
66 'r: 'a,
67 {
68 <&'a Value as EvalInput<'a>>::into_arena_value(self, arena)
69 }
70 }
71}
72
73pub trait EvalRoot<'r>: sealed::Sealed<'r> {}
97
98impl<'r> EvalRoot<'r> for &'r OwnedDataValue {}
99
100impl<'r> EvalRoot<'r> for &'r Value {}
101
102struct Inner {
103 logic: Engine,
104 operators: Vec<Operator>,
105 by_name: HashMap<Box<str>, usize>,
106 secrets: Arc<Secrets>,
107}
108
109#[derive(Clone)]
114pub struct ExprEngine {
115 inner: Arc<Inner>,
116}
117
118impl Default for ExprEngine {
119 fn default() -> Self {
120 Self::builder()
122 .build()
123 .expect("an engine with no custom operators cannot fail to build")
124 }
125}
126
127impl std::fmt::Debug for ExprEngine {
128 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 f.debug_struct("ExprEngine")
130 .field("operators", &self.inner.operators.len())
131 .field("secrets", &self.inner.secrets)
132 .finish()
133 }
134}
135
136impl ExprEngine {
137 pub fn builder() -> ExprEngineBuilder {
139 ExprEngineBuilder::default()
140 }
141
142 pub fn operators(&self) -> &[Operator] {
146 &self.inner.operators
147 }
148
149 pub(crate) fn operator(&self, name: &str) -> Option<&Operator> {
151 self.inner
152 .by_name
153 .get(name)
154 .map(|&i| &self.inner.operators[i])
155 }
156
157 pub fn secrets(&self) -> &Secrets {
159 &self.inner.secrets
160 }
161
162 pub fn escape(&self) -> char {
164 TEMPLATE_KEY_ESCAPE
165 }
166
167 pub fn compile<T: FromValue>(&self, raw: &Value) -> Result<Expr<T>> {
176 let logic = self
177 .logic()
178 .compile(raw)
179 .map_err(|e| Error::new(ErrorKind::Compile, e.to_string()))?;
180 let constant = if logic.is_constant() {
183 let folded = with_arena(|arena| {
184 self.logic()
185 .evaluate(&logic, DataValue::Object(&[]), arena)
186 .map_err(|e| Error::new(ErrorKind::Eval, e.to_string()))
187 .and_then(|v| {
188 T::from_view(v).map_err(|why| {
189 Error::new(
190 ErrorKind::Config,
191 format!(
192 "`{}` must be {}: {why}",
193 super::expr::snippet(raw),
194 T::type_name()
195 ),
196 )
197 })
198 })
199 })?;
200 Some(folded)
201 } else {
202 None
203 };
204 Ok(Expr {
205 raw: raw.clone(),
206 logic,
207 constant,
208 })
209 }
210
211 pub fn eval<'e, 'r, T: FromValue>(
222 &self,
223 expr: &'e Expr<T>,
224 root: impl EvalRoot<'r>,
225 ) -> Result<Evaluated<T::Out<'e>>> {
226 if let Some(c) = &expr.constant {
227 return Ok(Evaluated {
228 value: T::lend(c),
229 ops: if cfg!(feature = "budget") {
230 Some(0)
231 } else {
232 None
233 },
234 });
235 }
236 with_arena(|arena| {
237 let root = sealed::Sealed::view_in(root, arena).map_err(eval_error)?;
238 #[cfg(feature = "budget")]
239 let (view, ops) = {
240 let budget = self.logic().resolve_ops_budget(None);
241 let m = self
242 .logic()
243 .evaluate_metered(&expr.logic, root, arena, budget)
244 .map_err(eval_error)?;
245 (m.value, Some(m.ops))
246 };
247 #[cfg(not(feature = "budget"))]
248 let (view, ops) = (
249 self.logic()
250 .evaluate(&expr.logic, root, arena)
251 .map_err(eval_error)?,
252 None,
253 );
254 Ok(Evaluated {
255 value: T::give(expr.convert(view)?),
256 ops,
257 })
258 })
259 }
260
261 pub(crate) fn eval_view<'e, T: FromValue>(
265 &self,
266 expr: &'e Expr<T>,
267 root: &DataValue<'_>,
268 arena: &Bump,
269 ) -> Result<T::Out<'e>> {
270 if let Some(c) = expr.constant() {
271 return Ok(T::lend(c));
272 }
273 let view = self.eval_in(expr.logic(), root, arena)?;
274 expr.convert(view).map(T::give)
275 }
276
277 pub(crate) fn eval_in<'a>(
287 &self,
288 logic: &'a Logic,
289 root: &'a DataValue<'a>,
290 arena: &'a Bump,
291 ) -> Result<&'a DataValue<'a>> {
292 self.logic()
293 .evaluate(logic, root, arena)
294 .map_err(eval_error)
295 }
296
297 pub(crate) fn logic(&self) -> &Engine {
299 &self.inner.logic
300 }
301}
302
303fn eval_error(e: datalogic_rs::Error) -> Error {
305 let kind = if matches!(e.kind, datalogic_rs::ErrorKind::BudgetExceeded { .. }) {
306 ErrorKind::Budget
307 } else {
308 ErrorKind::Eval
309 };
310 Error::new(kind, e.to_string())
311}
312
313#[derive(Default)]
319pub struct ExprEngineBuilder {
320 families: Vec<OperatorFamily>,
321 operators: Vec<(Operator, Arc<dyn CustomOperator>)>,
322 secrets: Secrets,
323 #[cfg(feature = "budget")]
324 ops_budget: Option<u64>,
325}
326
327impl std::fmt::Debug for ExprEngineBuilder {
328 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
330 let operators: Vec<&str> = self.operators.iter().map(|(info, _)| &*info.name).collect();
331 f.debug_struct("ExprEngineBuilder")
332 .field("families", &self.families)
333 .field("operators", &operators)
334 .field("secrets", &self.secrets)
335 .finish_non_exhaustive()
336 }
337}
338
339impl ExprEngineBuilder {
340 #[must_use]
349 pub fn family(mut self, family: OperatorFamily) -> Self {
350 self.families.push(family);
351 self
352 }
353
354 #[must_use]
356 pub fn families(mut self, families: impl IntoIterator<Item = OperatorFamily>) -> Self {
357 self.families.extend(families);
358 self
359 }
360
361 #[must_use]
375 pub fn all_operators(self) -> Self {
376 self.families(ALL_OPERATORS.iter().copied())
377 }
378
379 #[must_use]
389 pub fn operator(self, name: impl Into<String>, op: impl CustomOperator + 'static) -> Self {
390 self.operator_with(Operator::custom(name.into()), op)
391 }
392
393 #[must_use]
398 pub fn operator_with(mut self, mut info: Operator, op: impl CustomOperator + 'static) -> Self {
399 info.family = OperatorFamily::Custom;
400 self.operators.push((info, Arc::new(op)));
401 self
402 }
403
404 #[must_use]
406 pub fn secrets(mut self, secrets: Secrets) -> Self {
407 self.secrets = secrets;
408 self
409 }
410
411 #[cfg(feature = "budget")]
413 #[must_use]
414 pub fn ops_budget(mut self, ops: u64) -> Self {
415 self.ops_budget = Some(ops);
416 self
417 }
418
419 pub fn build(self) -> Result<ExprEngine> {
426 let mut upstream_families = Vec::with_capacity(self.families.len());
427 for family in self.families {
428 match family {
429 OperatorFamily::Core | OperatorFamily::Secret => {}
430 OperatorFamily::Custom => {
431 return Err(Error::new(
432 ErrorKind::Config,
433 "custom operators are registered with `operator`, not `family`",
434 ));
435 }
436 other => {
437 let upstream = other
438 .to_upstream()
439 .expect("every built-in family maps to a datalogic family");
440 if !upstream.is_compiled() {
441 return Err(Error::new(
442 ErrorKind::Config,
443 format!(
444 "operator family `{other:?}` requires its cargo feature to be enabled"
445 ),
446 ));
447 }
448 upstream_families.push(upstream);
449 }
450 }
451 }
452 let secrets = Arc::new(self.secrets);
453 let mut builder = Engine::builder()
454 .with_families(upstream_families)
455 .with_templating(true)
456 .with_template_key_escape(TEMPLATE_KEY_ESCAPE);
457 #[cfg(feature = "budget")]
460 {
461 builder = builder.with_config(
462 datalogic_rs::EvaluationConfig::default().with_ops_budget(self.ops_budget),
463 );
464 }
465 let mut infos = Vec::with_capacity(self.operators.len());
466 for (info, op) in self.operators {
467 builder = builder.add_operator(info.name.to_string(), SharedOperator(op));
468 infos.push(info);
469 }
470 builder = builder.add_operator(SECRET_OPERATOR, SecretOperator(Arc::clone(&secrets)));
471 let logic = builder.build();
472
473 let builtin: HashSet<&'static str> = logic.builtin_operator_names().collect();
476 let mut seen = HashSet::new();
477 for info in &infos {
478 let name = &*info.name;
479 let refusal = if name == SECRET_OPERATOR {
480 Some("is reserved for the engine's own operator")
481 } else if builtin.contains(name) {
482 Some("is the name of a built-in operator")
483 } else if !seen.insert(name) {
484 Some("is registered twice")
485 } else {
486 None
487 };
488 if let Some(why) = refusal {
489 return Err(Error::new(
490 ErrorKind::Config,
491 format!("operator `{name}` {why}"),
492 ));
493 }
494 }
495
496 let mut operators: Vec<Operator> =
499 logic.operators().flat_map(Operator::from_logic).collect();
500 operators.extend(infos);
501 operators.push(Operator::secret());
502 let by_name: HashMap<Box<str>, usize> = operators
503 .iter()
504 .enumerate()
505 .map(|(i, op)| (Box::from(&*op.name), i))
506 .collect();
507
508 Ok(ExprEngine {
509 inner: Arc::new(Inner {
510 logic,
511 operators,
512 by_name,
513 secrets,
514 }),
515 })
516 }
517}
518
519#[cfg(test)]
520impl ExprEngine {
521 pub(crate) fn try_eval_json(
523 &self,
524 logic: &serde_json::Value,
525 ) -> std::result::Result<serde_json::Value, datalogic_rs::Error> {
526 let compiled = self.logic().compile_arc(logic).expect("should compile");
527 let ctx = datavalue::OwnedDataValue::from(&serde_json::json!({}));
528 let text = self.logic().session().eval_str(&compiled, &ctx)?;
529 Ok(serde_json::from_str(&text).expect("eval_str output should be valid JSON"))
530 }
531
532 pub(crate) fn eval_json(&self, logic: &serde_json::Value) -> serde_json::Value {
534 self.try_eval_json(logic).expect("should evaluate")
535 }
536}
537
538#[cfg(test)]
539mod tests {
540 use super::*;
541 use serde_json::json;
542
543 #[test]
544 fn empty_operand_results_this_crate_would_silently_break_on() {
545 let e = ExprEngine::default();
551 for (logic, expected) in [
552 (json!({"and": []}), json!(null)),
553 (json!({"or": []}), json!(null)),
554 (json!({"+": []}), json!(0)),
555 (json!({"*": []}), json!(1)),
556 (json!({"cat": []}), json!("")),
557 (json!({"merge": []}), json!([])),
558 (json!({"missing": []}), json!([])),
559 ] {
560 assert_eq!(e.eval_json(&logic), expected, "for {logic}");
561 }
562 }
563
564 #[test]
565 fn a_missing_var_path_resolves_to_null_not_an_error() {
566 let e = ExprEngine::default();
572 assert_eq!(
573 e.eval_json(&json!({"var": "data.does_not_exist"})),
574 json!(null)
575 );
576 }
577
578 #[test]
579 fn truthy_falsy_matches_the_documented_semantics() {
580 let e = ExprEngine::default();
586 for (v, truthy) in [
587 (json!(0), false),
588 (json!(""), false),
589 (json!(false), false),
590 (json!(null), false),
591 (json!([]), false),
592 (json!({}), false),
593 (json!("x"), true),
594 (json!(1), true),
595 ] {
596 assert_eq!(
597 e.eval_json(&json!({"!!": v})),
598 json!(truthy),
599 "truthiness of {v}"
600 );
601 }
602 }
603
604 #[test]
605 fn an_unrecognised_operator_is_not_an_error_under_templating() {
606 let e = ExprEngine::default();
614 let logic = json!({"totally_made_up_op_xyz": ["a", "b"]});
615 assert_eq!(
616 e.eval_json(&logic),
617 logic,
618 "an unrecognised operator must echo back verbatim, not error"
619 );
620 }
621
622 #[test]
628 fn a_gated_operator_echoes_back_while_its_family_is_off() {
629 let e = ExprEngine::default();
630 let logic = json!({"starts_with": ["hello", "he"]});
631 assert_eq!(
632 e.eval_json(&logic),
633 logic,
634 "an operator behind an unenabled family must echo back, not error"
635 );
636 }
637
638 #[test]
644 fn a_gated_operator_evaluates_once_its_family_is_on() {
645 let e = ExprEngine::builder()
646 .family(OperatorFamily::ExtString)
647 .build()
648 .unwrap();
649 assert_eq!(
650 e.eval_json(&json!({"starts_with": ["hello", "he"]})),
651 json!(true),
652 "with ExtString on, starts_with must evaluate, not echo"
653 );
654 }
655
656 #[test]
663 fn datetime_feature_changes_plain_string_comparison() {
664 let e = ExprEngine::default();
665 let logic = json!({"==": ["2024-01-15T00:00:00Z", "2024-01-15T01:00:00+01:00"]});
666 #[cfg(feature = "datetime")]
667 assert_eq!(e.eval_json(&logic), json!(true));
668 #[cfg(not(feature = "datetime"))]
669 assert_eq!(e.eval_json(&logic), json!(false));
670 }
671
672 #[test]
675 fn ext_string_family_reaches_datalogic() {
676 let e = ExprEngine::builder()
677 .family(OperatorFamily::ExtString)
678 .build()
679 .unwrap();
680 assert_eq!(e.eval_json(&json!({"upper": "ab"})), json!("AB"));
681 }
682
683 #[test]
684 fn ext_array_family_reaches_datalogic() {
685 let e = ExprEngine::builder()
686 .family(OperatorFamily::ExtArray)
687 .build()
688 .unwrap();
689 assert_eq!(e.eval_json(&json!({"sort": [[3, 1, 2]]})), json!([1, 2, 3]));
690 }
691
692 #[test]
693 fn ext_math_family_reaches_datalogic() {
694 let e = ExprEngine::builder()
695 .family(OperatorFamily::ExtMath)
696 .build()
697 .unwrap();
698 assert_eq!(e.eval_json(&json!({"abs": -5})), json!(5));
699 }
700
701 #[test]
702 fn ext_control_family_reaches_datalogic() {
703 let e = ExprEngine::builder()
704 .family(OperatorFamily::ExtControl)
705 .build()
706 .unwrap();
707 assert_eq!(
708 e.eval_json(&json!({"??": [null, "fallback"]})),
709 json!("fallback")
710 );
711 }
712
713 #[test]
714 fn ext_object_family_reaches_datalogic() {
715 let e = ExprEngine::builder()
716 .family(OperatorFamily::ExtObject)
717 .build()
718 .unwrap();
719 assert_eq!(
720 e.eval_json(&json!({"keys": [{"a": 1, "b": 2}]})),
721 json!(["a", "b"])
722 );
723 }
724
725 #[test]
726 fn error_handling_family_reaches_datalogic() {
727 let e = ExprEngine::builder()
730 .family(OperatorFamily::ErrorHandling)
731 .build()
732 .unwrap();
733 assert_eq!(
734 e.eval_json(&json!({"try": [{"throw": "boom"}, "recovered"]})),
735 json!("recovered")
736 );
737 }
738
739 #[cfg(feature = "datetime")]
745 #[test]
746 fn datetime_family_reaches_datalogic() {
747 let logic = json!({"now": []});
748 assert_eq!(
749 ExprEngine::default().eval_json(&logic),
750 logic,
751 "without OperatorFamily::Datetime selected, `now` echoes back"
752 );
753 let e = ExprEngine::builder()
754 .family(OperatorFamily::Datetime)
755 .build()
756 .unwrap();
757 let result = e.eval_json(&logic);
758 assert_ne!(result, logic, "with Datetime on, `now` must not echo back");
759 assert!(!result.is_null(), "`now` should produce a value, got null");
760 }
761 #[cfg(feature = "budget")]
762 #[test]
763 fn the_ops_budget_stops_a_map_over_three_items() {
764 let e = ExprEngine::builder().ops_budget(2).build().unwrap();
765 let logic = json!({"map": [[1, 2, 3], {"+": [{"var": ""}, 1]}]});
766 let err = e.try_eval_json(&logic).unwrap_err();
767 assert_eq!(err.tag(), "BudgetExceeded");
768 assert_eq!(ExprEngine::default().eval_json(&logic), json!([2, 3, 4]));
770 }
771
772 #[test]
773 fn escape_is_dollar_and_the_escaped_key_is_literal() {
774 let e = ExprEngine::default();
775 assert_eq!(e.escape(), '$');
776 assert_eq!(e.eval_json(&json!({"$cat": [1]})), json!({"cat": [1]}));
777 }
778}