rulemorph 0.3.2

YAML-based declarative data transformation engine for CSV/JSON to JSON
Documentation
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,
}

/// Evaluation result - either a value or missing
#[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,
        }
    }
}

/// Item in a map/filter operation
#[derive(Clone)]
pub struct EvalItem<'a> {
    pub value: &'a JsonValue,
    pub index: usize,
}

/// v2 evaluation context - tracks pipe value, let bindings, and iteration scopes
#[derive(Clone)]
pub struct V2EvalContext<'a> {
    /// Current pipe value ($)
    pipe_value: Option<EvalValue>,
    /// Let-bound variables (local scope)
    let_bindings: HashMap<String, EvalValue>,
    /// Item scope for map/filter operations (@item)
    item: Option<EvalItem<'a>>,
    /// Accumulator scope for reduce/fold operations (@acc)
    acc: Option<&'a JsonValue>,
    /// Trace-mode precomputed operator args, keyed by the operator rule path.
    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> {
    /// Create a new empty context
    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,
        }
    }

    /// Create a new context with a pipe value
    pub fn with_pipe_value(mut self, value: EvalValue) -> Self {
        self.pipe_value = Some(value);
        self
    }

    /// Create a new context with a let binding added
    pub fn with_let_binding(mut self, name: String, value: EvalValue) -> Self {
        self.let_bindings.insert(name, value);
        self
    }

    /// Create a new context with multiple let bindings added
    pub fn with_let_bindings(mut self, bindings: Vec<(String, EvalValue)>) -> Self {
        for (name, value) in bindings {
            self.let_bindings.insert(name, value);
        }
        self
    }

    /// Create a new context with item scope (for map/filter operations)
    pub fn with_item(mut self, item: EvalItem<'a>) -> Self {
        self.item = Some(item);
        self
    }

    /// Create a new context with accumulator scope (for reduce/fold operations)
    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(),
        }
    }

    /// Get the current pipe value
    pub fn get_pipe_value(&self) -> Option<&EvalValue> {
        self.pipe_value.as_ref()
    }

    /// Resolve a local variable name
    pub fn resolve_local(&self, name: &str) -> Option<&EvalValue> {
        self.let_bindings.get(name)
    }

    /// Get the current item (if in map/filter scope)
    pub fn get_item(&self) -> Option<&EvalItem<'a>> {
        self.item.as_ref()
    }

    /// Get the current accumulator (if in reduce/fold scope)
    pub fn get_acc(&self) -> Option<&JsonValue> {
        self.acc
    }

    /// Check if item scope is available
    pub fn has_item_scope(&self) -> bool {
        self.item.is_some()
    }

    /// Check if accumulator scope is available
    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");
}