rulemorph 0.3.4

YAML-based declarative data transformation engine for CSV/JSON to JSON
Documentation
#[test]
fn trace_v2_missing_short_circuit_does_not_evaluate_later_invalid_arg() {
    let cases = [
        (
            "concat",
            r#"
version: 2
input:
  format: json
mappings:
  - target: "out"
    expr:
      - "@input.missing"
      - concat: ["@item.invalid"]
"#,
        ),
        (
            "pick",
            r#"
version: 2
input:
  format: json
mappings:
  - target: "out"
    expr:
      - "@input.obj"
      - pick: ["@input.missing", "@item.invalid"]
"#,
        ),
        (
            "lookup_first",
            r#"
version: 2
input:
  format: json
mappings:
  - target: "out"
    expr:
      - "@input.missing"
      - lookup_first: ["@input.not_array", "@item.invalid", "@item.invalid"]
"#,
        ),
    ];
    let input = r#"[{"obj":{"a":1},"not_array":"not-array"}]"#;

    for (name, yaml) in cases {
        let rule = parse_rule_file(yaml).unwrap_or_else(|err| panic!("{name} parse: {err:?}"));
        let normal = transform(&rule, input, None)
            .unwrap_or_else(|err| panic!("{name} normal transform: {err:?}"));
        let traced = transform_input_with_trace(
            &rule,
            InputData::Text(input),
            None,
            &TransformTraceOptions::raw(),
        )
        .unwrap_or_else(|err| panic!("{name} traced transform: {err:?}"));

        assert_eq!(normal, json!([{}]), "{name} normal output");
        assert_eq!(traced.output, normal, "{name} traced output");
        assert!(
            iter_trace_events(&traced.trace).into_iter().all(|event| {
                event.kind != TraceEventKind::RefRead
                    || event.input_path.as_deref() != Some("@item.invalid")
            }),
            "{name} should not evaluate skipped invalid @item arg"
        );
        assert_trace_shape(&traced.trace);
    }
}

#[test]
fn trace_v2_and_short_circuits_false_pipe_without_evaluating_invalid_arg() {
    let yaml = r#"
version: 2
input:
  format: json
mappings:
  - target: "flag"
    expr:
      - "@input.enabled"
      - and: ["@item.enabled"]
"#;

    assert_traced_output_matches_normal(yaml, r#"[{"enabled":false}]"#, json!([{ "flag": false }]));
}

#[test]
fn trace_v2_or_short_circuits_true_pipe_without_evaluating_invalid_arg() {
    let yaml = r#"
version: 2
input:
  format: json
mappings:
  - target: "flag"
    expr:
      - "@input.enabled"
      - or: ["@item.enabled"]
"#;

    assert_traced_output_matches_normal(yaml, r#"[{"enabled":true}]"#, json!([{ "flag": true }]));
}

#[test]
fn trace_v2_coalesce_short_circuits_after_first_present_arg() {
    let yaml = r#"
version: 2
input:
  format: json
mappings:
  - target: "name"
    expr:
      - "@input.primary"
      - coalesce: ["@input.secondary", "@item.name"]
"#;

    let rule = parse_rule(yaml);
    let traced = transform_text_raw_trace(&rule, r#"[{"secondary":"fallback"}]"#);

    assert_eq!(traced.output, json!([{ "name": "fallback" }]));
    let arg_eval_indexes = iter_trace_events(&traced.trace)
        .into_iter()
        .filter(|event| {
            event.kind == TraceEventKind::ArgEval && event.operator.as_deref() == Some("coalesce")
        })
        .filter_map(|event| event.attributes.get("arg_index"))
        .cloned()
        .collect::<Vec<_>>();
    assert_eq!(
        arg_eval_indexes,
        vec![TraceAttributeValue::Number(0.into())],
        "coalesce should trace evaluated fallback args but not short-circuited args"
    );
    assert_trace_shape(&traced.trace);
}

#[test]
fn trace_v2_short_circuited_args_are_not_reported_as_arg_eval() {
    let yaml = r#"
version: 2
input:
  format: json
mappings:
  - target: "flag"
    expr:
      - "@input.enabled"
      - and: ["@item.enabled"]
"#;
    let rule = parse_rule(yaml);
    let traced = transform_text_raw_trace(&rule, r#"[{"enabled":false}]"#);

    assert!(
        iter_trace_events(&traced.trace)
            .into_iter()
            .all(|event| event.kind != TraceEventKind::ArgEval),
        "short-circuited v2 args must not be reported as evaluated"
    );
}

#[test]
fn trace_v2_non_short_circuited_invalid_arg_errors_like_normal() {
    let yaml = r#"
version: 2
input:
  format: json
mappings:
  - target: "flag"
    expr:
      - "@input.enabled"
      - and: ["@item.enabled"]
"#;
    let rule = parse_rule(yaml);
    let input = r#"[{"enabled":true}]"#;

    let normal = transform(&rule, input, None).expect_err("normal error");
    let traced = transform_input_with_trace(
        &rule,
        InputData::Text(input),
        None,
        &TransformTraceOptions::raw(),
    )
    .expect_err("traced error");

    assert_eq!(traced.error, normal);
    assert_trace_shape(&traced.trace);
}