use quarb::{Defs, expand, parse_defs};
fn exp(q: &str) -> String {
expand(q, &Defs::default()).unwrap()
}
#[test]
fn expansion() {
assert_eq!(
exp("def &adults: /row[::Age >= 18]; &adults @| count"),
"/row[::Age >= 18] @| count"
);
assert_eq!(
exp("def &above($col, $min): /row[$col >= $min]; &above(::Fare, 500) | ::Name"),
"/row[::Fare >= 500] | ::Name"
);
assert_eq!(
exp("def &cols: | .n(::Name) | %.; /row | &cols"),
"/row | .n(::Name) | %."
);
assert_eq!(
exp("def &adults: /row[::Age >= 18]; \
def &rich: &adults | [::Fare > 200]; &rich @| count"),
"/row[::Age >= 18] | [::Fare > 200] @| count"
);
assert_eq!(
exp("def &stats: /row::Age @| mean; &stats"),
"/row::Age @| mean"
);
}
#[test]
fn expansion_errors() {
let err = |q: &str| expand(q, &Defs::default()).unwrap_err().to_string();
assert!(err("&nope | count").contains("unknown fragment '&nope'"));
assert!(err("def &a: /x; def &a: /y; /z").contains("already defined"));
assert!(err("def &a: /x; /row | &a").contains("query fragment"));
assert!(err("def &p: | upper; &p").contains("pipeline fragment"));
assert!(err("def &p: | upper; /row @| &p").contains("invoked with '@|'"));
assert!(err("def &f($x): /row[$x > 1]; &f(1, 2)").contains("takes 1 argument"));
assert!(err("def &r: &r; /x").contains("unknown fragment '&r'"));
assert!(err("def &f($x): /row[$x > 1]; /row[$x > 1]").contains("after '$'"));
assert!(err("def &a: /row; &a[::x > 1]").contains("pipeline filter"));
assert!(err("def &a: /row; &a::name").contains("through the pipe"));
}
#[test]
fn macros() {
assert_eq!(
exp(
r#"macro &sample($pct): ^ | "| [\$ord mod ${100 idiv $pct} = 1]";
/row | &sample(25) @| count"#
),
"/row | [$ord mod 4 = 1] @| count"
);
assert_eq!(
exp(r#"macro &above($col, $min): ^ | "/row[${$col} > ${$min}]";
&above(::fare, 500) | ::name"#),
"/row[::fare > 500] | ::name"
);
assert_eq!(
exp(r#"macro &describe(@cols):
/cols/* | .k(/projection::key)
| "| .${$.k}-mean(/row${::form} @| mean)";
^ | &describe(::fare, ::age) | %."#),
"^ | .fare-mean(/row::fare @| mean) | .age-mean(/row::age @| mean) | %."
);
assert_eq!(
exp(
r#"macro &cols($q): /q//projection::key | "| .${$_}(::${$_})";
/row | &cols(/row[::age > 30][::fare < 10]::name) | %."#
),
"/row | .age(::age) | .fare(::fare) | .name(::name) | %."
);
assert_eq!(
exp(r#"def &adults: /row[::age >= 18];
macro &counted: ^ | "&adults @| count";
&counted"#),
"/row[::age >= 18] @| count"
);
}
#[test]
fn macro_errors() {
let err = |q: &str| expand(q, &Defs::default()).unwrap_err().to_string();
assert!(err(r#"macro &m: ^ | "| upper"; &m"#).contains("expanded to a pipeline fragment"));
assert!(err(r#"macro &m: ^ | "/row"; /x | &m"#).contains("expanded to a query fragment"));
assert!(err(r#"macro &m($a, @r): ^ | "/x"; &m()"#).contains("takes 1+ argument(s)"));
assert!(err(r#"macro &m: /nope | "| x"; &m"#).contains("expanded to nothing"));
assert!(err(r#"macro &m: ^ | "/row[oops"; &m"#).contains("in expansion of '&m'"));
assert!(err(r#"macro &m: | upper; &m"#).contains("anchor a non-navigating body"));
assert!(err(r#"macro &r: ^ | "&r"; &r"#).contains("unknown fragment '&r'"));
assert!(err(r#"def &a: /x; macro &a: ^ | "/y"; /z"#).contains("already defined"));
}
#[test]
fn data_aware_errors() {
let err = |q: &str| expand(q, &Defs::default()).unwrap_err().to_string();
assert!(
err(r#"macro &p!($c): /data/row | $c @| unique | "| x"; ^ | &p(::k)"#)
.contains("invoke it as '&p!(...)'")
);
assert!(err(r#"def &a: /x; &a! @| count"#).contains("'&a' is pure"));
assert!(err(r#"macro &m: ^ | "/y"; &m! @| count"#).contains("'&m' is pure"));
assert!(
err(r#"macro &p!($c): /data/row | $c @| unique | "| x"; ^ | &p!(::k)"#)
.contains("needs an input")
);
assert!(err(r#"macro &p!($data): ^ | "/x"; ^ | &p!(::k)"#).contains("'/data'"));
}
#[test]
fn defs_files() {
let defs = parse_defs(
"def &adults: /row[::Age >= 18];\n\
def &fare-stats: | .name(::Name) | .fare(::Fare) | %.;\n",
)
.unwrap();
assert_eq!(
expand("&adults | &fare-stats", &defs).unwrap(),
"/row[::Age >= 18] | .name(::Name) | .fare(::Fare) | %."
);
assert!(parse_defs("def &a: /x; /row").is_err());
}
#[test]
fn unparse_fixpoint() {
let queries = [
"/row[::Age >= 18] @| count",
"//book[::pages > 200]::;id",
"//*<block>[:::index = 2][1..3]$",
"/items/*[/price:: * /qty:: > 15]/name::",
"/a || /b::x | upper @| join(', ')",
"/row @| group(::Pclass) | top(2, ::Fare) @| ungroup | rec($.Pclass, ::Name)",
"/row | .who | [$ord mod 2 = 1] | $.who",
"/row | .a(::x) | %. @| [2..-1]",
"/users/* | .total(/orders/*/amt:: @| sum) | $.total",
"//user <=> //order[::uid = $*1::id and ::amt > $*1::limit]::",
"//p[not (::a = 1 or ::b = 2)]",
"/x | s/foo/bar/g | trim",
"/x | (:::index + 1) * 3",
"/x[- :::index = -2]",
"::;n-rows",
"//~(^ch[0-9]+$)/*.rs",
"/row | [::name =~ /^(\\w+), (\\w+)/] | rec('surname', $1, 'title', $2)",
"/row | ::fare @| window(-2..0) | mean",
"/row | ::fare @| window(..0, ::class) | sum",
"/row | ::fare | .now @| shift(1, ::class) | $.now - $_",
"/row | ::fare @| window(3) | mean",
"/row | \"${::name} (${::age})\" @| join(', ')",
"/row | ::fare | .f | \"fare \\$${$.f}, doubled ${$_ * 2}\"",
"//h2>>p::text @| join(' ')",
"//aside<<?*::;tag || //a[1]>>!p",
"/row | .d(::dept) | .m(^/row[::dept = $$.d]::pay @| mean) | $.m - $$_",
"/users/* <=> /orders/*[/uid:: = $*1/id::] | rec('who', $*1/name::, 'amt', /amt::)",
"/tracks/* | rec(::title, 'artist', ::album_id~>::artist_id~>::name)",
"/invoices/* | ::qty * ::track_id~>::price @| group(::customer) | sum",
];
for q in queries {
let once = exp(q);
let twice = exp(&once);
assert_eq!(once, twice, "not a fixpoint for {q}");
}
}