use serde_json::Value as JsonValue;
use std::cell::Cell;
use std::collections::HashMap;
use std::rc::Rc;
use crate::model::RuleFile;
use crate::transform::EvalLimits;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum CustomOpCounterScope {
Evaluation,
Shared,
}
#[derive(Debug, Clone, PartialEq)]
pub enum EvalValue {
Missing,
Value(JsonValue),
}
impl EvalValue {
pub fn is_missing(&self) -> bool {
matches!(self, EvalValue::Missing)
}
pub fn into_value(self) -> Option<JsonValue> {
match self {
EvalValue::Value(v) => Some(v),
EvalValue::Missing => None,
}
}
pub fn as_value(&self) -> Option<&JsonValue> {
match self {
EvalValue::Value(v) => Some(v),
EvalValue::Missing => None,
}
}
}
#[derive(Clone)]
pub struct EvalItem<'a> {
pub value: &'a JsonValue,
pub index: usize,
}
#[derive(Clone)]
pub struct V2EvalContext<'a> {
pipe_value: Option<EvalValue>,
let_bindings: HashMap<String, EvalValue>,
item: Option<EvalItem<'a>>,
acc: Option<&'a JsonValue>,
precomputed_op_args: Option<(String, Vec<EvalValue>)>,
limits: EvalLimits,
rule: Option<&'a RuleFile>,
custom_op_depth: usize,
custom_op_calls: Rc<Cell<usize>>,
custom_op_eval_depth: Rc<Cell<usize>>,
custom_op_counter_scope: CustomOpCounterScope,
}
impl<'a> V2EvalContext<'a> {
pub fn new() -> Self {
Self {
pipe_value: None,
let_bindings: HashMap::new(),
item: None,
acc: None,
precomputed_op_args: None,
limits: EvalLimits::default(),
rule: None,
custom_op_depth: 0,
custom_op_calls: Rc::new(Cell::new(0)),
custom_op_eval_depth: Rc::new(Cell::new(0)),
custom_op_counter_scope: CustomOpCounterScope::Evaluation,
}
}
pub fn with_pipe_value(mut self, value: EvalValue) -> Self {
self.pipe_value = Some(value);
self
}
pub fn with_let_binding(mut self, name: String, value: EvalValue) -> Self {
self.let_bindings.insert(name, value);
self
}
pub fn with_let_bindings(mut self, bindings: Vec<(String, EvalValue)>) -> Self {
for (name, value) in bindings {
self.let_bindings.insert(name, value);
}
self
}
pub fn with_item(mut self, item: EvalItem<'a>) -> Self {
self.item = Some(item);
self
}
pub fn with_acc(mut self, acc: &'a JsonValue) -> Self {
self.acc = Some(acc);
self
}
pub(crate) fn with_precomputed_op_args(
mut self,
base_path: impl Into<String>,
values: Vec<EvalValue>,
) -> Self {
self.precomputed_op_args = Some((base_path.into(), values));
self
}
pub(crate) fn with_limits(mut self, limits: EvalLimits) -> Self {
self.limits = limits;
self
}
pub fn with_rule(mut self, rule: &'a RuleFile) -> Self {
self.rule = Some(rule);
self
}
pub fn has_custom_op(&self, name: &str) -> bool {
self.rule.is_some_and(|rule| rule.defs.contains_key(name))
}
pub(crate) fn with_custom_op_depth(mut self, depth: usize) -> Self {
self.custom_op_depth = depth;
self
}
pub(crate) fn with_custom_op_counter_from(mut self, other: &Self) -> Self {
self.custom_op_calls = other.custom_op_calls.clone();
self.custom_op_eval_depth = other.custom_op_eval_depth.clone();
self.custom_op_counter_scope = other.custom_op_counter_scope;
self
}
#[doc(hidden)]
pub fn with_shared_custom_op_counter(mut self) -> Self {
self.custom_op_counter_scope = CustomOpCounterScope::Shared;
self
}
pub(crate) fn limits(&self) -> EvalLimits {
self.limits
}
pub(crate) fn rule(&self) -> Option<&'a RuleFile> {
self.rule
}
pub(crate) fn custom_op_depth(&self) -> usize {
self.custom_op_depth
}
pub(crate) fn increment_custom_op_calls(&self) -> usize {
let next = self.custom_op_calls.get().saturating_add(1);
self.custom_op_calls.set(next);
next
}
pub(crate) fn enter_eval_scope(&self) -> EvalScopeGuard<'_> {
let current_depth = self.custom_op_eval_depth.get();
if self.custom_op_counter_scope == CustomOpCounterScope::Evaluation && current_depth == 0 {
self.custom_op_calls.set(0);
}
self.custom_op_eval_depth
.set(current_depth.saturating_add(1));
EvalScopeGuard {
depth: self.custom_op_eval_depth.as_ref(),
}
}
pub fn get_pipe_value(&self) -> Option<&EvalValue> {
self.pipe_value.as_ref()
}
pub fn resolve_local(&self, name: &str) -> Option<&EvalValue> {
self.let_bindings.get(name)
}
pub fn get_item(&self) -> Option<&EvalItem<'a>> {
self.item.as_ref()
}
pub fn get_acc(&self) -> Option<&JsonValue> {
self.acc
}
pub fn has_item_scope(&self) -> bool {
self.item.is_some()
}
pub fn has_acc_scope(&self) -> bool {
self.acc.is_some()
}
pub(super) fn precomputed_arg_for_path(&self, path: &str) -> Option<EvalValue> {
let (base_path, values) = self.precomputed_op_args.as_ref()?;
let suffix = path.strip_prefix(base_path)?;
let index_text = suffix.strip_prefix(".args[")?.strip_suffix(']')?;
if index_text.contains(|ch: char| !ch.is_ascii_digit()) {
return None;
}
let index = index_text.parse::<usize>().ok()?;
values.get(index).cloned()
}
pub(super) fn let_bindings(&self) -> impl Iterator<Item = (&String, &EvalValue)> {
self.let_bindings.iter()
}
}
impl<'a> Default for V2EvalContext<'a> {
fn default() -> Self {
Self::new()
}
}
pub(crate) struct EvalScopeGuard<'a> {
depth: &'a Cell<usize>,
}
impl Drop for EvalScopeGuard<'_> {
fn drop(&mut self) {
self.depth.set(self.depth.get().saturating_sub(1));
}
}
#[cfg(test)]
mod tests {
include!("context/tests.rs");
}