rulemorph 0.3.1

YAML-based declarative data transformation engine for CSV/JSON to JSON
Documentation
use super::*;

#[allow(clippy::too_many_arguments)]
pub(in crate::transform) fn eval_v2_condition_traced<'a>(
    condition: &V2Condition,
    record: &'a JsonValue,
    context: Option<&'a JsonValue>,
    out: &'a JsonValue,
    path: &str,
    ctx: &V2EvalContext<'a>,
    collector: &mut TraceCollector,
) -> Result<bool, TransformError> {
    match condition {
        V2Condition::All(conditions) => {
            for (index, cond) in conditions.iter().enumerate() {
                let cond_path = format!("{}[{}]", path, index);
                if !eval_v2_condition_traced(
                    cond, record, context, out, &cond_path, ctx, collector,
                )? {
                    return Ok(false);
                }
            }
            Ok(true)
        }
        V2Condition::Any(conditions) => {
            for (index, cond) in conditions.iter().enumerate() {
                let cond_path = format!("{}[{}]", path, index);
                if eval_v2_condition_traced(cond, record, context, out, &cond_path, ctx, collector)?
                {
                    return Ok(true);
                }
            }
            Ok(false)
        }
        V2Condition::Comparison(comparison) => {
            eval_v2_comparison_traced(comparison, record, context, out, path, ctx, collector)
        }
        V2Condition::Expr(expr) => {
            let expr_path = format!("{}.expr", path);
            let value =
                eval_v2_expr_traced(expr, record, context, out, &expr_path, ctx, collector)?;
            match value {
                V2EvalValue::Value(JsonValue::Bool(flag)) => Ok(flag),
                V2EvalValue::Missing => Ok(false),
                V2EvalValue::Value(_) => Err(TransformError::new(
                    TransformErrorKind::ExprError,
                    "when/record_when must evaluate to boolean",
                )
                .with_path(&expr_path)),
            }
        }
    }
}

#[allow(clippy::too_many_arguments)]
fn eval_v2_comparison_traced<'a>(
    comparison: &crate::v2_model::V2Comparison,
    record: &'a JsonValue,
    context: Option<&'a JsonValue>,
    out: &'a JsonValue,
    path: &str,
    ctx: &V2EvalContext<'a>,
    collector: &mut TraceCollector,
) -> Result<bool, TransformError> {
    if comparison.args.len() != 2 {
        return Err(TransformError::new(
            TransformErrorKind::ExprError,
            format!(
                "comparison requires exactly 2 arguments, got {}",
                comparison.args.len()
            ),
        )
        .with_path(path));
    }

    let operator = v2_comparison_operator_name(comparison.op);
    collector
        .start_span(TraceEventKind::OpStart, TracePhase::Start)
        .rule_path(path)
        .operator(operator)
        .attr_count("arg_count", 2)
        .finish(collector);

    let result =
        (|| {
            let left_path = format!("{}.args[0]", path);
            let right_path = format!("{}.args[1]", path);
            let left = eval_v2_expr_traced(
                &comparison.args[0],
                record,
                context,
                out,
                &left_path,
                ctx,
                collector,
            )?;
            emit_v2_arg_eval(collector, &left_path, 0, operator, &left);
            let right = eval_v2_expr_traced(
                &comparison.args[1],
                record,
                context,
                out,
                &right_path,
                ctx,
                collector,
            )?;
            emit_v2_arg_eval(collector, &right_path, 1, operator, &right);

            match comparison.op {
                V2ComparisonOp::Eq => Ok(compare_v2_eval_eq(&left, &right)),
                V2ComparisonOp::Ne => Ok(!compare_v2_eval_eq(&left, &right)),
                V2ComparisonOp::Gt => compare_v2_eval_ord(&left, &right, path)
                    .map(|ordering| ordering == Ordering::Greater),
                V2ComparisonOp::Gte => compare_v2_eval_ord(&left, &right, path)
                    .map(|ordering| ordering != Ordering::Less),
                V2ComparisonOp::Lt => compare_v2_eval_ord(&left, &right, path)
                    .map(|ordering| ordering == Ordering::Less),
                V2ComparisonOp::Lte => compare_v2_eval_ord(&left, &right, path)
                    .map(|ordering| ordering != Ordering::Greater),
                V2ComparisonOp::Match => compare_v2_eval_match(&left, &right, path),
            }
        })();

    match result {
        Ok(flag) => {
            collector
                .end_span(TraceEventKind::OpEnd, TracePhase::End)
                .rule_path(path)
                .operator(operator)
                .finish_with_output(collector, &JsonValue::Bool(flag), None);
            Ok(flag)
        }
        Err(error) => {
            collector
                .error_span(TraceEventKind::OpError, "OP_ERROR", "operator failed")
                .rule_path(path)
                .operator(operator)
                .finish(collector);
            Err(error)
        }
    }
}

fn v2_comparison_operator_name(op: V2ComparisonOp) -> &'static str {
    match op {
        V2ComparisonOp::Eq => "eq",
        V2ComparisonOp::Ne => "ne",
        V2ComparisonOp::Gt => "gt",
        V2ComparisonOp::Gte => "gte",
        V2ComparisonOp::Lt => "lt",
        V2ComparisonOp::Lte => "lte",
        V2ComparisonOp::Match => "match",
    }
}

fn compare_v2_eval_eq(left: &V2EvalValue, right: &V2EvalValue) -> bool {
    match (left, right) {
        (V2EvalValue::Value(left), V2EvalValue::Value(right)) => left == right,
        (V2EvalValue::Missing, V2EvalValue::Missing) => true,
        (V2EvalValue::Missing, V2EvalValue::Value(right)) => right.is_null(),
        (V2EvalValue::Value(left), V2EvalValue::Missing) => left.is_null(),
    }
}

fn compare_v2_eval_ord(
    left: &V2EvalValue,
    right: &V2EvalValue,
    path: &str,
) -> Result<Ordering, TransformError> {
    match (left, right) {
        (V2EvalValue::Value(left), V2EvalValue::Value(right)) => {
            if let (Some(left), Some(right)) = (json_value_as_f64(left), json_value_as_f64(right)) {
                return Ok(left.partial_cmp(&right).unwrap_or(Ordering::Equal));
            }
            if let (Some(left), Some(right)) = (left.as_str(), right.as_str()) {
                return Ok(left.cmp(right));
            }
            Err(TransformError::new(
                TransformErrorKind::ExprError,
                "cannot compare values of different types",
            )
            .with_path(path))
        }
        _ => Err(TransformError::new(
            TransformErrorKind::ExprError,
            "cannot compare missing values",
        )
        .with_path(path)),
    }
}

fn compare_v2_eval_match(
    left: &V2EvalValue,
    right: &V2EvalValue,
    path: &str,
) -> Result<bool, TransformError> {
    let text = match left {
        V2EvalValue::Value(JsonValue::String(value)) => value,
        _ => {
            return Err(TransformError::new(
                TransformErrorKind::ExprError,
                "match operator requires string on left side",
            )
            .with_path(path));
        }
    };
    let pattern = match right {
        V2EvalValue::Value(JsonValue::String(value)) => value,
        _ => {
            return Err(TransformError::new(
                TransformErrorKind::ExprError,
                "match operator requires regex pattern string on right side",
            )
            .with_path(path));
        }
    };
    Regex::new(pattern)
        .map_err(|err| {
            TransformError::new(
                TransformErrorKind::ExprError,
                format!("invalid regex pattern: {}", err),
            )
            .with_path(path)
        })
        .map(|regex| regex.is_match(text))
}

fn json_value_as_f64(value: &JsonValue) -> Option<f64> {
    match value {
        JsonValue::Number(number) => number.as_f64(),
        JsonValue::String(value) => value.parse::<f64>().ok(),
        _ => None,
    }
}