rulemorph 0.3.2

YAML-based declarative data transformation engine for CSV/JSON to JSON
Documentation
fn eval_math_op(
    op: &str,
    pipe: JsonValue,
    args: Vec<JsonValue>,
) -> Result<EvalValue, crate::error::TransformError> {
    eval_math_op_with_range_limit(op, pipe, args, Some(10_000))
}

fn eval_range_op(
    args: Vec<JsonValue>,
    max_range_items: Option<usize>,
) -> Result<EvalValue, crate::error::TransformError> {
    let op = V2OpStep {
        op: "range".to_string(),
        args: args.into_iter().map(lit).collect(),
    };
    let ctx = V2EvalContext::new().with_limits(crate::transform::EvalLimits {
        max_range_items,
        ..Default::default()
    });
    eval_v2_op_step(
        &op,
        EvalValue::Missing,
        &json!({}),
        None,
        &json!({}),
        "test",
        &ctx,
    )
}

fn pipe_value_expr() -> V2Expr {
    V2Expr::Pipe(V2Pipe {
        start: V2Start::PipeValue,
        steps: vec![],
    })
}

fn input_ref_expr(path: &str) -> V2Expr {
    V2Expr::Pipe(V2Pipe {
        start: V2Start::Ref(V2Ref::Input(path.to_string())),
        steps: vec![],
    })
}

fn item_ref_expr(path: &str) -> V2Expr {
    V2Expr::Pipe(V2Pipe {
        start: V2Start::Ref(V2Ref::Item(path.to_string())),
        steps: vec![],
    })
}

fn eval_math_op_with_range_limit(
    op: &str,
    pipe: JsonValue,
    args: Vec<JsonValue>,
    max_range_items: Option<usize>,
) -> Result<EvalValue, crate::error::TransformError> {
    let op = V2OpStep {
        op: op.to_string(),
        args: args.into_iter().map(lit).collect(),
    };
    let ctx = V2EvalContext::new().with_limits(crate::transform::EvalLimits {
        max_range_items,
        ..Default::default()
    });
    eval_v2_op_step(
        &op,
        EvalValue::Value(pipe),
        &json!({}),
        None,
        &json!({}),
        "test",
        &ctx,
    )
}

#[test]
fn test_eval_math_ops_success() {
    assert!(matches!(eval_math_op("abs", json!(-3.5), vec![]), Ok(EvalValue::Value(v)) if v == json!(3.5)));
    assert!(matches!(eval_math_op("floor", json!(3.9), vec![]), Ok(EvalValue::Value(v)) if v == json!(3)));
    assert!(matches!(eval_math_op("ceil", json!(3.1), vec![]), Ok(EvalValue::Value(v)) if v == json!(4)));
    assert!(matches!(eval_math_op("trunc", json!(-3.9), vec![]), Ok(EvalValue::Value(v)) if v == json!(-3)));
    assert!(matches!(eval_math_op("sqrt", json!(81), vec![]), Ok(EvalValue::Value(v)) if v == json!(9)));
    assert!(matches!(eval_math_op("pow", json!(2), vec![json!(8)]), Ok(EvalValue::Value(v)) if v == json!(256)));
    assert!(matches!(eval_math_op("mod", json!(-5), vec![json!(3)]), Ok(EvalValue::Value(v)) if v == json!(1)));
    assert!(matches!(eval_math_op("mod", json!(5), vec![json!(-3)]), Ok(EvalValue::Value(v)) if v == json!(2)));
    assert!(matches!(eval_math_op("clamp", json!(120), vec![json!(0), json!(100)]), Ok(EvalValue::Value(v)) if v == json!(100)));
    assert!(matches!(eval_math_op("sign", json!(-0.25), vec![]), Ok(EvalValue::Value(v)) if v == json!(-1)));
    assert!(matches!(eval_range_op(vec![json!(2), json!(8)], Some(10_000)), Ok(EvalValue::Value(v)) if v == json!([2, 3, 4, 5, 6, 7])));
    assert!(matches!(eval_range_op(vec![json!(8), json!(2)], Some(10_000)), Ok(EvalValue::Value(v)) if v == json!([8, 7, 6, 5, 4, 3])));
    assert!(matches!(eval_range_op(vec![json!(8), json!(2), json!(-2)], Some(10_000)), Ok(EvalValue::Value(v)) if v == json!([8, 6, 4])));
    assert!(matches!(eval_range_op(vec![json!(0), json!(10), json!(-1)], Some(10_000)), Ok(EvalValue::Value(v)) if v == json!([])));
    assert!(matches!(eval_range_op(vec![json!(10), json!(0), json!(1)], Some(10_000)), Ok(EvalValue::Value(v)) if v == json!([])));
    assert!(matches!(
        eval_range_op(vec![json!(i64::MAX - 2), json!(i64::MAX), json!(1)], Some(10_000)),
        Ok(EvalValue::Value(v)) if v == json!([i64::MAX - 2, i64::MAX - 1])
    ));
}

#[test]
fn test_eval_range_can_use_pipe_value_when_explicitly_referenced() {
    let op = V2OpStep {
        op: "range".to_string(),
        args: vec![lit(json!(2)), pipe_value_expr()],
    };
    let ctx = V2EvalContext::new().with_limits(crate::transform::EvalLimits {
        max_range_items: Some(10_000),
        ..Default::default()
    });
    let result = eval_v2_op_step(
        &op,
        EvalValue::Value(json!(5)),
        &json!({}),
        None,
        &json!({}),
        "test",
        &ctx,
    );
    assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!([2, 3, 4])));
}

#[test]
fn test_eval_v1_fallback_math_ops_skip_args_when_pipe_is_missing() {
    let op = V2OpStep {
        op: "pow".to_string(),
        args: vec![item_ref_expr("invalid")],
    };
    let result = eval_v2_op_step(
        &op,
        EvalValue::Missing,
        &json!({}),
        None,
        &json!({}),
        "test",
        &V2EvalContext::new(),
    );
    assert!(matches!(result, Ok(EvalValue::Missing)));
}

#[test]
fn test_eval_v1_fallback_math_ops_validate_arg_count_before_missing_pipe() {
    let op = V2OpStep {
        op: "pow".to_string(),
        args: vec![],
    };
    let result = eval_v2_op_step(
        &op,
        EvalValue::Missing,
        &json!({}),
        None,
        &json!({}),
        "test",
        &V2EvalContext::new(),
    );
    assert!(result.is_err());
}

#[test]
fn test_eval_range_stops_after_missing_arg_without_evaluating_later_args() {
    let op = V2OpStep {
        op: "range".to_string(),
        args: vec![input_ref_expr("missing"), item_ref_expr("invalid")],
    };
    let result = eval_v2_op_step(
        &op,
        EvalValue::Missing,
        &json!({}),
        None,
        &json!({}),
        "test",
        &V2EvalContext::new(),
    );
    assert!(matches!(result, Ok(EvalValue::Missing)));
}

#[test]
fn test_eval_math_ops_reject_invalid_inputs() {
    assert!(eval_math_op("mod", json!(5), vec![json!(0)]).is_err());
    assert!(eval_math_op("sqrt", json!(-1), vec![]).is_err());
    assert!(eval_math_op("sqrt", json!("NaN"), vec![]).is_err());
    assert!(eval_math_op("sqrt", json!("inf"), vec![]).is_err());
    assert!(eval_math_op("pow", json!(0), vec![json!(-1)]).is_err());
    assert!(eval_math_op("pow", json!(-8), vec![json!(0.5)]).is_err());
    assert!(eval_math_op("pow", json!(1e308), vec![json!(2)]).is_err());
    assert!(eval_math_op("clamp", json!(5), vec![json!(10), json!(1)]).is_err());
    assert!(eval_math_op("clamp", json!(5), vec![json!("NaN"), json!(10)]).is_err());
    assert!(eval_range_op(vec![json!(0.5), json!(10)], Some(10_000)).is_err());
    assert!(eval_range_op(vec![json!(0), json!(10), json!(0)], Some(10_000)).is_err());
    assert!(eval_range_op(vec![json!(0), json!(10_001)], Some(10_000)).is_err());
    assert!(eval_range_op(vec![json!(0), json!(100_000), json!(1)], Some(10_000)).is_err());
    let pipe_first_err = eval_math_op("range", json!(2), vec![json!(8)]).unwrap_err();
    assert!(
        pipe_first_err
            .to_string()
            .contains("[{ range: [start, end, step?] }]")
    );
}

#[test]
fn test_eval_range_limit_can_be_disabled_by_trusted_options() {
    let result = eval_range_op(vec![json!(0), json!(10_001)], None);
    assert!(
        matches!(result, Ok(EvalValue::Value(v)) if v.as_array().is_some_and(|items| items.len() == 10_001))
    );
}