rulemorph 0.3.1

YAML-based declarative data transformation engine for CSV/JSON to JSON
Documentation
#[test]
fn trace_v2_pipe_operator_lifecycle_and_collection_items() {
    let yaml = r#"
version: 2
input:
  format: json
mappings:
  - target: "names"
    expr:
      - "@input.users"
      - map:
          - "@item.name"
          - uppercase
"#;
    let rule = parse_rule_file(yaml).expect("parse rule");
    let traced = transform_input_with_trace(
        &rule,
        InputData::Text(r#"[{"users":[{"name":"alice"},{"name":"bob"}]}]"#),
        None,
        &TransformTraceOptions::raw(),
    )
    .expect("traced transform");

    assert_eq!(traced.output, json!([{ "names": ["ALICE", "BOB"] }]));
    assert_operator_lifecycle(&traced.trace, "map");
    assert_operator_lifecycle(&traced.trace, "uppercase");
    assert_parent_ids_point_to_emitted_events(&traced.trace);
    assert_trace_paths_are_canonical(&traced.trace);

    let text = serde_json::to_string(&traced.trace).unwrap();
    assert!(text.contains("@item"));
    assert!(text.contains("ALICE"));
    assert!(text.contains("BOB"));
}

#[test]
fn trace_v2_operator_inputs_preserve_missing_values() {
    let yaml = r#"
version: 2
input:
  format: json
mappings:
  - target: "name"
    expr:
      - "@input.missing_name"
      - coalesce: ["anonymous"]
"#;
    let rule = parse_rule_file(yaml).expect("parse rule");
    let traced = transform_input_with_trace(
        &rule,
        InputData::Text(r#"[{}]"#),
        None,
        &TransformTraceOptions::raw(),
    )
    .expect("traced transform");

    assert_eq!(traced.output, json!([{ "name": "anonymous" }]));
    assert_operator_lifecycle(&traced.trace, "coalesce");
    let events = iter_trace_events(&traced.trace);
    let coalesce_start = events
        .iter()
        .find(|event| {
            event.kind == TraceEventKind::OpStart && event.operator.as_deref() == Some("coalesce")
        })
        .expect("coalesce op_start");
    assert_eq!(coalesce_start.inputs[0].state, TraceValueState::Missing);
    assert_eq!(coalesce_start.inputs[0].value_type, TraceJsonType::Missing);
    assert_parent_ids_point_to_emitted_events(&traced.trace);
    assert_trace_paths_are_canonical(&traced.trace);
}

#[test]
fn trace_v2_filter_preserves_item_scope() {
    let yaml = r#"
version: 2
input:
  format: json
mappings:
  - target: "active_names"
    expr:
      - "@input.users"
      - filter: ["@item.active"]
      - map:
          - "@item.name"
"#;
    let rule = parse_rule_file(yaml).expect("parse rule");
    let input = r#"[{"users":[{"name":"alice","active":true},{"name":"bob","active":false}]}]"#;

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

    assert_eq!(traced.output, normal);
    assert_eq!(traced.output, json!([{ "active_names": ["alice"] }]));
    assert_parent_ids_point_to_emitted_events(&traced.trace);
    assert_trace_paths_are_canonical(&traced.trace);
}

#[test]
fn trace_v2_sort_by_preserves_item_scope() {
    let yaml = r#"
version: 2
input:
  format: json
mappings:
  - target: "names"
    expr:
      - "@input.users"
      - sort_by: ["@item.rank", "asc"]
      - map:
          - "@item.name"
"#;
    let rule = parse_rule_file(yaml).expect("parse rule");
    let input = r#"[{"users":[{"name":"bob","rank":2},{"name":"alice","rank":1}]}]"#;

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

    assert_eq!(traced.output, normal);
    assert_eq!(traced.output, json!([{ "names": ["alice", "bob"] }]));
    assert_parent_ids_point_to_emitted_events(&traced.trace);
    assert_trace_paths_are_canonical(&traced.trace);
}

#[test]
fn trace_v2_reduce_preserves_acc_scope() {
    let yaml = r#"
version: 2
input:
  format: json
mappings:
  - target: "sum"
    expr:
      - "@input.numbers"
      - reduce:
          - ["@acc", { "+": "@item" }]
"#;
    let rule = parse_rule_file(yaml).expect("parse rule");
    let input = r#"[{"numbers":[1,2,3]}]"#;

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

    assert_eq!(traced.output, normal);
    assert_eq!(traced.output, json!([{ "sum": 6.0 }]));
    assert_parent_ids_point_to_emitted_events(&traced.trace);
    assert_trace_paths_are_canonical(&traced.trace);
}

#[test]
fn trace_v2_coalesce_preserves_short_circuit() {
    let yaml = r#"
version: 2
input:
  format: json
mappings:
  - target: "name"
    expr:
      - "@input.name"
      - coalesce: ["@item.name"]
"#;
    let rule = parse_rule_file(yaml).expect("parse rule");
    let input = r#"[{"name":"alice"}]"#;

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

    assert_eq!(traced.output, normal);
    assert_eq!(traced.output, json!([{ "name": "alice" }]));
    assert_parent_ids_point_to_emitted_events(&traced.trace);
    assert_trace_paths_are_canonical(&traced.trace);
}

#[test]
fn trace_v2_let_binding_is_visible_to_following_steps() {
    let yaml = r#"
version: 2
input:
  format: json
mappings:
  - target: "discounted"
    expr:
      - "@input.price"
      - let: { factor: 0.9 }
      - multiply: ["@factor"]
"#;
    let rule = parse_rule_file(yaml).expect("parse rule");
    let input = r#"[{"price":100}]"#;

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

    assert_eq!(traced.output, normal);
    assert_eq!(traced.output, json!([{ "discounted": 90.0 }]));
    assert_parent_ids_point_to_emitted_events(&traced.trace);
    assert_trace_paths_are_canonical(&traced.trace);
}