rulemorph 0.3.3

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

mod args;
mod collection;
mod lazy;

use args::{
    emit_arg_eval, eval_array_arg_traced, eval_eager_op_traced, eval_expr_at_index_traced,
    v1_operator_has_scoped_expr_args,
};
use collection::{eval_array_fold_traced, eval_array_map_traced, eval_array_reduce_traced};
use lazy::{eval_bool_and_or_traced, eval_coalesce_traced};

pub(super) fn eval_expr_traced(
    expr: &Expr,
    record: &JsonValue,
    context: Option<&JsonValue>,
    out: &JsonValue,
    base_path: &str,
    locals: Option<&EvalLocals<'_>>,
    collector: &mut TraceCollector,
) -> Result<EvalValue, TransformError> {
    collector
        .start_span(TraceEventKind::ExprStart, TracePhase::Start)
        .rule_path(base_path)
        .finish(collector);

    let result = match expr {
        Expr::Literal(value) => {
            let value = EvalValue::Value(value.clone());
            collector
                .emit(TraceEventKind::LiteralEval, TracePhase::Instant)
                .rule_path(base_path)
                .finish_with_eval_output(collector, &value, None);
            Ok(value)
        }
        Expr::Ref(expr_ref) => {
            let value = eval_ref(expr_ref, record, context, out, base_path, locals);
            if let Ok(value) = &value {
                let path_hint = ref_read_path_hint(base_path, &expr_ref.ref_path);
                collector
                    .emit(TraceEventKind::RefRead, TracePhase::Instant)
                    .rule_path(base_path)
                    .input_path(canonical_ref_path(&expr_ref.ref_path))
                    .finish_with_eval_output(collector, value, path_hint);
            }
            value
        }
        Expr::Op(expr_op) => {
            collector
                .start_span(TraceEventKind::OpStart, TracePhase::Start)
                .rule_path(base_path)
                .operator(&expr_op.op)
                .finish(collector);
            let op_result = match expr_op.op.as_str() {
                "coalesce" => eval_coalesce_traced(
                    &expr_op.args,
                    None,
                    record,
                    context,
                    out,
                    base_path,
                    locals,
                    collector,
                ),
                "and" => eval_bool_and_or_traced(
                    &expr_op.args,
                    None,
                    record,
                    context,
                    out,
                    base_path,
                    true,
                    locals,
                    collector,
                ),
                "or" => eval_bool_and_or_traced(
                    &expr_op.args,
                    None,
                    record,
                    context,
                    out,
                    base_path,
                    false,
                    locals,
                    collector,
                ),
                "map" => eval_array_map_traced(
                    &expr_op.args,
                    None,
                    record,
                    context,
                    out,
                    base_path,
                    locals,
                    collector,
                ),
                "reduce" => eval_array_reduce_traced(
                    &expr_op.args,
                    None,
                    record,
                    context,
                    out,
                    base_path,
                    locals,
                    collector,
                ),
                "fold" => eval_array_fold_traced(
                    &expr_op.args,
                    None,
                    record,
                    context,
                    out,
                    base_path,
                    locals,
                    collector,
                ),
                op if v1_operator_has_scoped_expr_args(op) => {
                    eval_op(expr_op, record, context, out, base_path, None, locals)
                }
                _ => eval_eager_op_traced(
                    expr_op, record, context, out, base_path, locals, collector,
                ),
            };

            match op_result {
                Ok(value) => {
                    collector
                        .end_span(TraceEventKind::OpEnd, TracePhase::End)
                        .rule_path(base_path)
                        .operator(&expr_op.op)
                        .finish_with_eval_output(collector, &value, None);
                    Ok(value)
                }
                Err(error) => {
                    collector
                        .error_span(TraceEventKind::OpError, "OP_ERROR", "operator failed")
                        .rule_path(base_path)
                        .operator(&expr_op.op)
                        .finish(collector);
                    Err(error)
                }
            }
        }
        Expr::Chain(expr_chain) => eval_chain(expr_chain, record, context, out, base_path, locals),
    };

    match &result {
        Ok(value) => {
            collector
                .end_span(TraceEventKind::ExprEnd, TracePhase::End)
                .rule_path(base_path)
                .finish_with_eval_output(collector, value, None);
        }
        Err(_) => {
            collector
                .error_span(TraceEventKind::Error, "EXPR_ERROR", "expression failed")
                .rule_path(base_path)
                .finish(collector);
        }
    }

    result
}

fn ref_read_path_hint<'a>(base_path: &str, ref_path: &'a str) -> Option<&'a str> {
    if base_path.starts_with("defs.") && is_custom_body_local_ref(ref_path) {
        None
    } else {
        Some(ref_path)
    }
}

fn is_custom_body_local_ref(ref_path: &str) -> bool {
    matches!(
        ref_path,
        "input" | "out" | "local" | "item" | "acc" | "pipe"
    ) || ref_path.starts_with("input.")
        || ref_path.starts_with("input[")
        || ref_path.starts_with("out.")
        || ref_path.starts_with("out[")
        || ref_path.starts_with("local.")
        || ref_path.starts_with("local[")
        || ref_path.starts_with("item.")
        || ref_path.starts_with("item[")
        || ref_path.starts_with("acc.")
        || ref_path.starts_with("acc[")
        || ref_path.starts_with("pipe.")
        || ref_path.starts_with("pipe[")
}