jaq-interpret 1.5.0

Interpreter for the jaq language
Documentation
//! Tests for unnamed core filters.

pub mod common;

use common::{give, gives};
use serde_json::json;

#[test]
fn update_assign() {
    let ab = |v| json!({"a": v, "b": 2});
    gives(ab(1), ".a  = (.a, .b)", [ab(1), ab(2)]);
    gives(ab(1), ".a += (.a, .b)", [ab(2), ab(3)]);
    gives(ab(1), ".a |= (.+1, .)", [ab(2)]);
}

// here, jaq diverges from jq, which returns [3,6,4,8]!
// idem for other arithmetic operations
yields!(cartesian_arith, "[(1,2) * (3,4)]", [3, 4, 6, 8]);

#[test]
fn add() {
    give(json!(1), ". + 2", json!(3));
    give(json!(1.0), ". + 2.", json!(3.0));
    give(json!(1), "2.0 + .", json!(3.0));
    give(json!(null), "1.e1 + 2.1e2", json!(220.0));

    give(json!("Hello "), ". + \"world\"", json!("Hello world"));
    give(json!([1, 2]), ". + [3, 4]", json!([1, 2, 3, 4]));
    give(
        json!({"a": 1, "b": 2}),
        ". + {c: 3, a: 4}",
        json!({"a": 4, "b": 2, "c": 3}),
    );

    give(json!({}), ". + {}", json!({}));
    give(json!({"a": 1}), ". + {}", json!({"a": 1}));
}

#[test]
fn sub() {
    give(json!(1), ". - -2", json!(3));
    give(json!(1.0), ". - 0.1", json!(0.9));
    give(json!(1.0), ". - 1", json!(0.0));
}

yields!(sub_arr, "[1, 2, 3] - [2, 3, 4]", json!([1]));

#[test]
fn mul() {
    give(json!(1), ". * 2", json!(2));
    give(json!(1.0), ". * 2.", json!(2.0));
    give(json!(1), "2.0 * .", json!(2.0));

    give(json!("Hello"), "2 * .", json!("HelloHello"));
    give(json!(2), ". * \"Hello\"", json!("HelloHello"));

    give(json!("Hello"), "0 * .", json!(null));
    give(json!(-1), ". * \"Hello\"", json!(null));
    give(
        json!({"k": {"a": 1, "b": 2}}),
        ". * {k: {a: 0, c: 3}}",
        json!({"k": {"a": 0, "b": 2, "c": 3}}),
    );
}

yields!(div_str, r#""abcabcdab" / "ab""#, ["", "c", "cd", ""]);
yields!(div_str_empty, r#""" / """#, json!([]));
yields!(div_str_empty_str, r#""" / "ab""#, json!([]));
yields!(div_str_empty_sep, r#""aöß" / """#, ["a", "ö", "ß"]);

#[test]
fn logic() {
    let tf = json!([true, false]);
    give(tf.clone(), "[.[] and .[]]", json!([true, false, false]));
    give(tf, "[.[] or .[]]", json!([true, true, false]));
}

#[test]
fn alt() {
    give(json!([]), ".[] // 0", json!(0));
    give(json!([null, false]), ".[] // 0", json!(0));
    give(json!([null, 1, false, 2]), "[.[] // 0]", json!([1, 2]));
    give(json!([1, 2]), "[.[] // 0]", json!([1, 2]));
    give(json!([1, 2]), r#"[.[] // -"a"]"#, json!([1, 2]));
}

#[test]
fn try_() {
    give(json!(0), ".?", json!(0));
    give(json!(0), r#"(-"a")?, 1"#, json!(1));
    give(json!(0), r#"[(1, -"a", 2)?]"#, json!([1, 2]));
}

#[test]
fn precedence() {
    // concatenation binds stronger than application
    give(json!(null), "[0, 1 | . + 1]", json!([1, 2]));
    give(json!(null), "[0, (1 | . + 1)]", json!([0, 2]));

    // assignment binds stronger than concatenation
    give(json!(1), "[. += 1, 2]", json!([2, 2]));
    give(json!(1), "[. += (1, 2)]", json!([2, 3]));

    // alternation binds stronger than assignment
    give(json!(false), "[(., .) | . = . // 0]", json!([0, 0]));
    give(json!(false), "((., .) | . = .) // 0", json!(0));

    // disjunction binds stronger than alternation
    give(json!(false), ". or . // 0", json!(0));
    give(json!(false), ". or (. // 0)", json!(true));

    // conjunction binds stronger than disjunction
    give(json!(true), "(0 != 0) and . or .", json!(true));
    give(json!(true), "(0 != 0) and (. or .)", json!(false));

    give(json!(null), "1 + 2 * 3", json!(7));
    give(json!(null), "2 * 3 + 1", json!(7));
}

yields!(interpolation, r#"1 | "yields \(.+1)!""#, "yields 2!");
// this diverges from jq, which yields ["2 2", "3 2", "2 4", "3 4"],
// probably due to different order of evaluation addition
yields!(
    interpolation_many,
    r#"2 | ["\(., .+1) \(., .*2)"]"#,
    ["2 2", "2 4", "3 2", "3 4"]
);
// this does not work in jq, because jq does not allow for defining formatters
yields!(
    interpolation_fmt,
    r#"def @say: "say " + .; @say "I \("disco"), you \("party")""#,
    "I say disco, you say party"
);
yields!(
    interpolation_nested,
    r#""Here \("be \("nestings")")""#,
    "Here be nestings"
);

yields!(interpolation_str, r#""\("\tHi'\"\n❤\\")""#, "\tHi'\"\n\\");
yields!(
    interpolation_arr_str,
    r#""\(["\tHi'\"\n❤\\"])""#,
    "[\"\\tHi'\\\"\\n❤\\\\\"]"
);
yields!(interpolation_obj_str, r#""\({"❤\n": 0})""#, "{\"\\n\":0}");

yields!(
    obj_trailing_comma,
    "{a:1, b:2, c:3,}",
    json!({"a": 1, "b": 2, "c": 3})
);
yields!(
    obj_complex_key,
    r#""c" | {a: 1, "b": 2, (.): 3}"#,
    json!({"a": 1, "b": 2, "c": 3})
);
yields!(obj_proj, "{a: 1, b: 2} | {a,}", json!({"a": 1}));
yields!(
    obj_proj_set,
    "{a: 1, b: 2} | {a, c: 3}",
    json!({"a": 1, "c": 3})
);
yields!(
    obj_multi_keys,
    r#"[{("a", "b"): 1}]"#,
    json!([{"a": 1}, {"b": 1}])
);
yields!(
    obj_multi_vals,
    "[{a: (1,2), b: (3,4)}]",
    json!([{"a": 1, "b": 3}, {"a": 1, "b": 4}, {"a": 2, "b": 3}, {"a": 2, "b": 4}])
);

#[test]
fn if_then_else() {
    gives(json!([]), "if . | .[] then 0 else 0 end", []);

    let f = r#".[] | if . < 0 then "n" else "p" end"#;
    gives(json!([-1, 1, -1]), f, [json!("n"), json!("p"), json!("n")]);

    let f = r#".[] | if .<0 then "n" elif .>0 then "p" else "z" end"#;
    gives(json!([-1, 0, 1]), f, [json!("n"), json!("z"), json!("p")]);

    let f = r#"if .>0, .<0 then 0 elif .>0, .<0 then 1 else 2 end"#;
    gives(json!(1), f, [json!(0), json!(1), json!(2)]);
}

// This behaviour diverges from jq. In jaq, a `try` will propagate all
// errors in the stream to the `catch` filter.
yields!(
    try_catch_does_not_short_circuit,
    "[try (\"1\", \"2\", {}[0], \"4\") catch .]",
    ["1", "2", "cannot index {} with 0", "4"]
);
yields!(
    try_catch_nested,
    "try try {}[0] catch {}[1] catch .",
    "cannot index {} with 1"
);
yields!(
    try_catch_multi_valued,
    "[(try (1,2,3[0]) catch (3,4)) | . - 1]",
    [0, 1, 2, 3]
);
yields!(try_without_catch, "[try (1,2,3[0],4)]", [1, 2, 4]);
yields!(
    try_catch_prefix_operation,
    r#"(try -[] catch .) | . > "" and . < []"#,
    true
);
yields!(
    try_catch_postfix_operation,
    "[try 0[0]? catch .]",
    json!([])
);

// try should not gulp expressions after an infix operator; if it did,
// the inner try in these tests would resolve to empty and omit the
// 1[1] error expression, and the whole expression would yield an
// empty stream
yields!(
    try_parsing_isnt_greedy_wrt_comma,
    "try (try 0[0], 1[1]) catch . == try 1[1] catch .",
    true
);
yields!(
    try_parsing_isnt_greedy_wrt_pipe,
    "try (try 0 | 1[1]) catch . == try 1[1] catch .",
    true
);
yields!(
    try_parsing_isnt_greedy_wrt_plus,
    "try (try 0 + 1[1]) catch . == try 1[1] catch .",
    true
);

#[test]
fn ord() {
    give(json!(null), ". < (0 != 0)", json!(true));
    give(json!(false), ". < (0 == 0)", json!(true));
    give(json!(1), ". > 0.0", json!(true));
    give(json!(1), ". < 1.5", json!(true));
    give(json!(1.1), ". < 1.5", json!(true));
    give(json!(1.5), ". > 1.1", json!(true));
    give(json!("ab"), ". < \"b\"", json!(true));
    give(json!("a"), ". < \"ab\"", json!(true));
    give(json!({"a": 2}), r#". < {"a": 1, "b": 0}"#, json!(true));
}

#[test]
fn eq() {
    give(json!(1), ". == 1", json!(true));
    give(json!(1), "0 == . - .", json!(true));
    give(json!(1), ". == -1 * -1", json!(true));
    give(json!(1), ". == 2 / 2", json!(true));

    give(json!(0), ". == -.", json!(true));
    give(json!(0), "-. == .", json!(true));
    give(json!(0.0), ". == -.", json!(true));
    give(json!(0.0), "-. == .", json!(true));

    gives(json!([0, 1]), ".[] == 0", [json!(true), json!(false)]);

    give(json!(1), ". == 1.0", json!(true));
    give(json!(1), ". == 2 / 2.0", json!(true));

    give(json!({"a": 1, "b": 2}), ". == {b: 2, a: 1}", json!(true));
}

yields!(def_var_filter, "def f($a; b): $a+b; f(1; 2)", 3);

#[test]
fn vars() {
    give(json!(1), " 2  as $x | . + $x", json!(3));
    give(json!(1), ".+1 as $x | . + $x", json!(3));
    give(json!(1), ". as $x | (2 as $y | 3) | $x", json!(1));

    give(json!(1), "def g(f): f; . as $x | g($x)", json!(1));

    let f = r#"def g(f): "z" as $z | f | .+$z; "x" as $x | g("y" as $y | $x+$y)"#;
    give(json!(null), f, json!("xyz"));

    let f = r#"def g(f): "a" as $a | "b" as $b | $a + $b + f; "c" as $c | g($c)"#;
    give(json!(null), f, json!("abc"));

    let f = r#". as $x | ("y" as $y | "z") | $x"#;
    give(json!("x"), f, json!("x"));

    let out = || json!([[1, 4], [1, 5], [2, 4], [2, 5]]);
    let f = "def f($a; b; $c; d): [$a+b, $c+d]; [f((1, 2); 0; (3, 4); 1)]";
    give(json!(null), f, out());
    let f = "def f($a; b; $c; d): [$a+b, $c+d]; 0 as $a | 1 as $b | [f((1, 2); $a; (3, 4); $b)]";
    give(json!(null), f, out());
}

yields!(shadow_funs, "def a: 1; def b: a; def a: 2; a + b", 3);
yields!(shadow_vars, "1 as $x | 2 as $x | $x", 2);
// arguments from the right are stronger than from the left
yields!(shadow_args, "def f(g; g): g; f(1; 2)", 2);

yields!(id_var, "def f($a): $a; f(0)", 0);
yields!(id_arg, "def f( a):  a; f(0)", 0);
yields!(args_mixed, "def f(a; $b): a + $b; 1 as $a | f($a; 2)", 3);

yields!(nested_comb_args, "def f(a): def g(b): a + b; g(1); f(2)", 3);

const ACKERMANN: &str = "def ack($m; $n):
  if $m == 0 then $n + 1
  elif $n == 0 then ack($m-1; 1)
  else ack($m-1; ack($m; $n-1))
  end;";

yields!(ackermann, &(ACKERMANN.to_owned() + "ack(3; 4)"), 125);

#[test]
fn reduce() {
    let ff = |s| format!(". as $x | reduce 2 as $y (4; {}) | . + $x", s);

    let f = ff("3 as $z | . + $x + $y + $z");
    give(json!(1), &f, json!(11));

    let f = "def g(x; y): 3 as $z | . + x + y + $z; ".to_owned() + &ff("g($x; $y)");
    give(json!(1), &f, json!(11));
}

yields!(
    foreach_cumulative_sum,
    "[1, 2, 3] | [foreach .[] as $x (0; .+$x)]",
    [1, 3, 6]
);
yields!(
    for_cumulative_sum,
    "[1, 2, 3] | [for .[] as $x (0; .+$x)]",
    [0, 1, 3, 6]
);

// jq will give only [4, 3, 7, 12] here because
// it keeps only the *last* output value as input value for the next iteration, whereas
// jaq keeps all output values as input values
yields!(
    foreach_many_outputs,
    "[foreach (3,4) as $x (1; .+$x, .*$x)]",
    [4, 8, 16, 3, 7, 12]
);
yields!(
    for_many_outputs,
    "[for (3,4) as $x (1; .+$x, .*$x)]",
    [1, 4, 8, 16, 3, 7, 12]
);