use quarb::Defs;
use quarb::expand;
fn canon(q: &str) -> String {
expand(q, &Defs::default()).unwrap()
}
fn refuse(q: &str) -> String {
expand(q, &Defs::default()).unwrap_err().to_string()
}
#[test]
fn tolerated_reprints_strict() {
assert_eq!(canon("//body/(p|div)"), "//body(/p|/div)");
assert_eq!(canon("//body(/p|/div)"), "//body(/p|/div)");
}
#[test]
fn sugar_reprints_as_dot_group() {
assert_eq!(canon("/{2}"), "(/.){2}");
assert_eq!(canon("/div{2}"), "(/div){2}");
assert_eq!(canon("(/.){2}"), "(/.){2}");
assert_eq!(canon("/a->{2}"), "/a(->.){2}");
}
#[test]
fn quantifier_zoo_round_trips() {
for q in [
"/a(/b)+",
"/a(/b)*",
"/a(/b){2}",
"/a(/b){1,3}",
"/a(/b){2,}",
"/a(/b)+?",
"/a(/b)+!",
"/a(/b){1,3}?",
"/a(->mgr)+!",
] {
assert_eq!(canon(q), q, "not a fixpoint: {q}");
}
assert_eq!(canon("/a(/b){2,2}"), "/a(/b){2}");
}
#[test]
fn nesting_round_trips() {
let q = "//div(/ul/li|/ol/li|/dl(/dt|/dd))+";
assert_eq!(canon(q), q);
assert_eq!(canon("//div/(ul/li|ol/li|dl/(dt|dd))+"), q);
}
#[test]
fn groups_in_operand_position() {
assert_eq!(canon("/*[(->ref)+]"), "/*[(->ref)+]");
assert_eq!(canon("/*[(::a = 1 or ::b)]"), "/*[(::a = 1 || ::b)]");
assert_eq!(canon("/*[(::a = 1 || ::b)]"), "/*[(::a = 1 || ::b)]");
assert_eq!(canon("/*[(not ::a and ::b)]"), "/*[(!::a && ::b)]");
for q in [
"//order::amt <=>? //user[::id = $$::uid] | ...?",
"//u <=> //v <=>? //order[::uid = $*1::id]",
"//commit[::::short = ^/tags/*::::short]",
"/movie .m <-ACTED_IN[::born > ((m))::released] | %(::name; ((m))::title)",
"/c/* | (::kind ?= 'a' ? 1 : (/^b/) ? 2 : 0)",
"/tags/* | .t(:::name) | `cloc --git ${$.t}` | %($.t; lines = $_)",
"/x::v | base64 | decode('base64')",
"/x::v @| sort('ru-RU')",
"/a/*[^//error] | %(n = :::name; of = (^//*.rs @| count))",
] {
assert_eq!(canon(q), q, "not a fixpoint: {q}");
}
assert_eq!(canon("/a | (::x + 1) * 2"), "/a | (::x + 1) * 2");
}
#[test]
fn pattern_errors() {
assert!(refuse("(/a){3,2}").contains("max below min"));
assert!(refuse("(|/a)").contains("at least one hop"));
assert!(refuse("(.(::q))+").contains("at least one hop"));
assert!(refuse("/(p|/div)").contains("strict form"));
assert!(refuse("//{2}").contains("single-hop"));
assert_eq!(canon("/a/g++"), "/a/g++");
}
#[test]
fn edge_accessor_round_trips() {
for q in [
"/a->e[$-::qty > 1]",
"/a->*[$- = 'e']",
"/a<-mgr[$-::since = 2020]::name",
] {
assert_eq!(canon(q), q, "not a fixpoint: {q}");
}
assert!(refuse("/a->e[$-:::depth = 1]").contains("plain properties"));
}
#[test]
fn group_predicates_round_trip() {
for q in [
"/a(->e)+[::q > 1]?",
"/a(/b|/c/d)[::x]",
"(/employees .($-))+[::name = 'Meg']?",
] {
assert_eq!(canon(q), q, "not a fixpoint: {q}");
}
assert!(refuse("/a(/b)+[1]").contains("expression predicates only"));
assert!(refuse("/a(/b)+[1..2]").contains("expression predicates only"));
}
#[test]
fn pattern_pushes_round_trip() {
for q in [
"/a(->e .($-::qty))+ | @. | product",
"/a(->e .w($-))+",
"(/employees .($-))+?",
] {
assert_eq!(canon(q), q, "not a fixpoint: {q}");
}
assert!(expand("/a(->e.q($-))+", &Defs::default()).is_err());
}
#[test]
fn arrived_edges_round_trip() {
for q in [
"/a->e | .(@-::qty)",
"/a->e | .(@-)",
"/a | %(::x; l = @-)",
] {
assert_eq!(canon(q), q, "not a fixpoint: {q}");
}
assert!(refuse("/a | .(@-:::depth)").contains("plain properties"));
}
#[test]
fn context_and_piped_round_trip() {
for q in [
"/a | .p(((::x div (@*::x @| sum)) * 100 | round))",
"/a | %(::n; all = (@*:::name @| sort @| join(', ')))",
"/a[@* = null]",
] {
assert_eq!(canon(q), q, "not a fixpoint: {q}");
}
assert!(refuse("/a | .x((@* | .bad))").contains("real capsae"));
}
#[test]
fn conditional_round_trip() {
for q in [
"/a | %(::n; era = (::y < 2000 ? 'old' : 'new'))",
"/a[(::x ? 1 : 2) = 1]",
] {
assert_eq!(canon(q), q, "not a fixpoint: {q}");
}
assert_eq!(
canon("/a | (::x < 2 ? 'lo' : ::x < 9 ? 'mid' : 'hi')"),
"/a | (::x < 2 ? 'lo' : (::x < 9 ? 'mid' : 'hi'))"
);
assert!(refuse("/a | (::x ? 'lo')").contains("':'"));
}
#[test]
fn trait_algebra_round_trip() {
for q in [
"/a<code>",
"/a<block || inline>",
"/a<Person && !Employee>",
"/a<!*>",
"/a<(a || b) && !c>",
] {
assert_eq!(canon(q), q, "not a fixpoint: {q}");
}
assert_eq!(canon("/a<x><y>"), "/a<x && y>");
assert_eq!(canon("/a<(x && y) || z>"), "/a<(x || z) && (y || z)>");
assert_eq!(canon("/a<!!x>"), "/a<x>");
}
#[test]
fn spread_round_trip() {
for q in [
"/a->e | @-::roles | ... | ...",
"/a | (@*::x | ...) @| count",
] {
assert_eq!(canon(q), q, "not a fixpoint: {q}");
}
assert!(refuse("/a | @-::x | each").contains("each"));
}
#[test]
fn map_pipe_round_trip() {
for q in [
"/a | @-::roles $| upper",
"/a | @. $| [$_ > 1] $| [1..2]",
"/a | .p((@. $| $_ * 2))",
] {
assert_eq!(canon(q), q, "not a fixpoint: {q}");
}
assert!(refuse("/a | @. $| .bad").contains("real capsae"));
}
#[test]
fn defs_substitute_into_groups() {
let src = "def &chain($n): /employees(/reports[::id = $n])+; &chain(7)";
assert_eq!(canon(src), "/employees(/reports[::id = 7])+");
}
#[test]
fn headless_arrow_round_trips() {
assert_eq!(canon("/posts/5--ip"), "/posts/5--ip");
assert_eq!(
canon("/cookies/c1(--cookie--ip--ip--cookie)+::id"),
"/cookies/c1(--cookie--ip--ip--cookie)+::id"
);
assert_eq!(canon("/a(--*--*)+::name"), "/a(--*--*)+::name");
assert_eq!(canon("/ips/*[--ip]::id"), "/ips/*[--ip]::id");
assert_eq!(canon("/a-- -b"), "/a-- -b");
}
#[test]
fn resolution_respell_canonicalizes() {
assert_eq!(canon("/tracks/1::album_id~>::title"), "/tracks/1::album_id-->::title");
assert_eq!(canon("/tracks/1::album_id-->::title"), "/tracks/1::album_id-->::title");
assert_eq!(canon("/loan::book~>shelf"), "/loan::book-->shelf");
assert_eq!(canon("/page::id<~"), "/page::id<--");
assert_eq!(canon("/page::id<--cite"), "/page::id<--cite");
assert_eq!(canon("/x[::a <-3]"), "/x[::a < -3]");
}
#[test]
fn rounded_predicate_alias_canonicalizes() {
assert_eq!(
canon("//user(?::age >= 18)(?1)->profile/name"),
canon("//user[::age >= 18][1]->profile/name")
);
assert_eq!(canon("/a(?::x = 'y')"), "/a[::x = 'y']");
assert_eq!(canon("/a(?2..3)"), "/a[2..3]");
assert_eq!(canon("/a(?-1)"), "/a[-1]");
assert_eq!(canon("/a $| (?::n > 2)"), canon("/a $| [::n > 2]"));
assert_eq!(canon("/a $| (?..3)"), canon("/a $| [..3]"));
assert_eq!(
canon("/a(?(::x = 1 || ::y = 2) && ::z = 3)"),
canon("/a[(::x = 1 || ::y = 2) && ::z = 3]")
);
assert_eq!(canon("/a[::x = 1](?::y = 2)"), "/a[::x = 1][::y = 2]");
assert_eq!(canon("/~((?i)bob)(?::x = 1)"), "/(/(?i)bob/)[::x = 1]");
assert!(refuse("/a(?::x = 1]").contains("closes with ')'"));
assert_eq!(canon("/a(/b|/c)"), "/a(/b|/c)");
}
#[test]
fn rounded_family_canonicalizes() {
assert_eq!(canon("/page::id--:cite"), "/page::id<--cite");
assert_eq!(canon("/a-:b"), "/a<-b");
assert_eq!(canon("/a(-:b)+::name"), canon("/a(<-b)+::name"));
assert_eq!(canon("/a:-b(?$-::w > 1)"), canon("/a->b[$-::w > 1]"));
assert_eq!(canon("/a;-b"), canon("/a>b"));
assert_eq!(canon("/a-;b"), canon("/a<b"));
assert_eq!(canon("/a;;-?b"), canon("/a>>?b"));
assert_eq!(canon("/a-;;!b"), canon("/a<<!b"));
assert!(!refuse("/x[::a ;- 3]").is_empty());
assert_eq!(canon("/a/b../c"), canon("/a/b\\c"));
assert_eq!(canon("/a/b..//c"), canon("/a/b\\\\c"));
assert_eq!(canon("/a/b..//?c"), canon("/a/b\\\\?c"));
assert_eq!(canon("/a/b..//!c"), canon("/a/b\\\\!c"));
assert_eq!(
canon("//user(:admin && !banned)::name"),
canon("//user<admin && !banned>::name")
);
assert_eq!(canon("//user(:admin)(?::age > 18)"), canon("//user<admin>[::age > 18]"));
assert_eq!(canon("//x(:.custom)"), canon("//x<.custom>"));
assert_eq!(canon("/a(?(::x = 1 || ::y = 2))"), "/a[(::x = 1 || ::y = 2)]");
assert_eq!(canon("/e(:-manager_id)+::name"), "/e(->manager_id)+::name");
assert_eq!(canon("/a(/b)(;2,3)"), "/a(/b){2,3}");
assert_eq!(canon("/a(/b)(;2)"), "/a(/b){2}");
assert_eq!(canon("/a(/b)(;2,)"), "/a(/b){2,}");
assert_eq!(
canon("//user(:admin)(?::age >= 18)(?1):-profile/name"),
canon("//user<admin>[::age >= 18][1]->profile/name")
);
}
#[test]
fn root_anchor_and_comparison_words_canonicalize() {
assert_eq!(canon("(())/a/b::x"), canon("^/a/b::x"));
assert_eq!(canon("(())//x"), canon("^//x"));
assert_eq!(canon("/a[::x = (())/set/*::x]"), canon("/a[::x = ^/set/*::x]"));
assert_eq!(canon("(()) | count"), canon("^ | count"));
assert_eq!(canon("/x[::t < now()]"), canon("/x[::t < now()]"));
assert_eq!(canon("/u[::age .minor. 18]"), "/u[::age < 18]");
assert_eq!(canon("/u[::age .nonmaior. 18]"), "/u[::age <= 18]");
assert_eq!(canon("/u[::age .maior. 18]"), "/u[::age > 18]");
assert_eq!(canon("/u[::age .nonminor. 18]"), "/u[::age >= 18]");
assert_eq!(canon("/u[::age .inf. 18]"), "/u[::age < 18]");
assert_eq!(canon("/u[::age .nonsup. 18]"), "/u[::age <= 18]");
assert_eq!(canon("/u[::age .sup. 18]"), "/u[::age > 18]");
assert_eq!(canon("/u[::age .noninf. 18]"), "/u[::age >= 18]");
assert_eq!(canon("/u[::age.minor.18]"), "/u[::age.minor.18]");
assert_eq!(canon("/u(?::age .менее. 18)"), "/u[::age < 18]");
assert_eq!(canon("/u(?::age .неболее. 18)"), "/u[::age <= 18]");
assert_eq!(canon("/u(?::age .более. 18)"), "/u[::age > 18]");
assert_eq!(canon("/u(?::age .неменее. 18)"), "/u[::age >= 18]");
assert_eq!(canon("/u(?::age .μικρότερο. 18)"), "/u[::age < 18]");
assert_eq!(canon("/u(?::age .τοπολύ. 18)"), "/u[::age <= 18]");
assert_eq!(canon("/u(?::age .μεγαλύτερο. 18)"), "/u[::age > 18]");
assert_eq!(canon("/u(?::age .τουλάχιστον. 18)"), "/u[::age >= 18]");
assert_eq!(canon("/u(?::age .τουλαχιστον. 18)"), "/u[::age >= 18]");
assert_eq!(
canon("(())//user(:admin)(?::age .nonminor. 18)(?1):-profile/name"),
canon("^//user<admin>[::age >= 18][1]->profile/name")
);
}
#[test]
fn boolean_words_canonicalize() {
let pointy = canon("/u[::a = 1 and ::b = 2 or not ::c]");
for q in [
"/u[::a = 1 et ::b = 2 vel non ::c]",
"/u[::a = 1 et ::b = 2 ou non ::c]",
"/u(?::a = 1 и ::b = 2 или не ::c)",
"/u(?::a = 1 και ::b = 2 ή όχι ::c)",
"/u(?::a = 1 και ::b = 2 η οχι ::c)",
] {
assert_eq!(canon(q), pointy, "{q}");
}
assert_eq!(canon("/u(?не ::age .nonminor. 18)"), canon("/u[not ::age >= 18]"));
assert!(canon("/u[::'и' = 1]").contains("и"));
}
#[test]
fn rounded_pipes_registers_anchors_canonicalize() {
assert_eq!(canon("/a __ count"), canon("/a | count"));
assert_eq!(canon("/a::x *__ max"), canon("/a::x @| max"));
assert_eq!(canon("/a::x ,__ (?(_) .maior. 2)"), canon("/a::x $| [$_ > 2]"));
assert_eq!(canon("/a __ ..."), canon("/a | ..."));
assert_eq!(canon("/a __ ...?"), canon("/a | ...?"));
assert_eq!(
canon("//user :=: //order(?::uid = ((_))::id)"),
canon("//user <=> //order[::uid = $$::id]")
);
assert_eq!(
canon("//order::amt :=:? //user(?::id = ((_))::uid) __ ...?"),
canon("//order::amt <=>? //user[::id = $$::uid] | ...?")
);
assert_eq!(canon("/a ,__ (?(_) .maior. 2)"), canon("/a $| [$_ > 2]"));
assert_eq!(canon("/a .s /b __ (.s)"), canon("/a .s /b | $.s"));
assert_eq!(canon("/a . /b . __ (.2)"), canon("/a . /b . | $.2"));
assert_eq!(canon("/a :-b(?(-)::w .maior. 1)"), canon("/a->b[$-::w > 1]"));
assert_eq!(
canon("/users/* :=: /orders/*(?/uid:: = ((_))/id::) __ rec(::name, 'amt', (*1)/amt::)"),
canon("/users/* <=> /orders/*[/uid:: = $$/id::] | rec(::name, 'amt', $*1/amt::)")
);
assert_eq!(canon("/a . /b .m | ((1))/c | ((.))/d | ((@))/e | ((@m))/f"), "/a . /b .m | ((1))/c | ((.))/d | ((@))/e | ((@m))/f");
assert_eq!(canon("/a . /b .m | (@)/e | (@m)/f"), "/a . /b .m | ((@))/e | ((@m))/f");
assert_eq!(
canon("/row __ (?::name =~ (/^(\\w+), (\\w+)/)) __ rec('k', (1), 'j', (2))"),
canon("/row | [::name =~ /^(\\w+), (\\w+)/] | rec('k', $1, 'j', $2)")
);
assert_eq!(canon("/a .s /b | ((*s))::x"), canon("/a .s /b | (@s)::x"));
assert_eq!(canon("((m))/address"), canon("(m)/address"));
assert_eq!(canon("(m)/address"), "((m))/address");
}
#[test]
fn regex_literal_canonicalizes() {
assert_eq!(canon("//~(^ch)"), "//(/^ch/)");
assert_eq!(canon("//(/^ch/)"), "//(/^ch/)");
assert_eq!(canon("/u[::name =~ /^bob/i]"), "/u[::name =~ (/^bob/i)]");
assert_eq!(canon("/u[::name =~ (/^bob/i)]"), "/u[::name =~ (/^bob/i)]");
assert_eq!(canon("/u(?::name = (/^bob/i))"), "/u[::name =~ (/^bob/i)]");
assert_eq!(canon("/u[::name != (/x/)]"), "/u[::name !~ (/x/)]");
assert_eq!(canon("/u[::name !~ /x/]"), "/u[::name !~ (/x/)]");
assert_eq!(canon("/u[::name =~ 'x']"), "/u[::name =~ (/x/)]");
assert_eq!(canon("/u[::name = ~(x)]"), "/u[::name =~ (/x/)]");
assert_eq!(canon("//(/a\\/b/)"), "//(/a\\/b/)");
assert_eq!(canon("/u[::name =~ ::pat]"), "/u[::name =~ ::pat]");
assert_eq!(
canon("/c/* | (::kind ?= 'a' ? 1 : ~(^b) ? 2 : 0)"),
"/c/* | (::kind ?= 'a' ? 1 : (/^b/) ? 2 : 0)"
);
assert_eq!(canon("/a(/src/x)+"), "/a(/src/x)+");
}
#[test]
fn boolean_words_in_traits_canonicalize() {
let pointy = canon("//user<admin && !banned || staff>::name");
for q in [
"//user<admin and not banned or staff>::name",
"//user(:admin et non banned vel staff)::name",
"//user(:admin et non banned ou staff)::name",
"//user(:admin и не banned или staff)::name",
"//user(:admin και όχι banned ή staff)::name",
] {
assert_eq!(canon(q), pointy, "{q}");
}
assert_eq!(canon("/div(:(a ou b) et non c)"), canon("/div<(a || b) && !c>"));
assert_eq!(canon("/x<and>"), "/x<and>");
assert_eq!(canon("/x<и>"), "/x<и>");
}
#[test]
fn record_field_colon_canonicalizes() {
assert_eq!(canon("/a | .r | $.r:x"), "/a | .r | $.r:x");
assert_eq!(canon("/a | .r | (.r):x"), "/a | .r | $.r:x");
assert_eq!(canon("/a | $.r:a:b"), "/a | $.r:a:b");
assert_eq!(canon("/a | :n"), "/a | :n");
assert_eq!(canon("/a | $_:n"), "/a | :n");
assert_eq!(canon("/a | [:n .maior. 1]"), canon("/a | [$_:n > 1]"));
assert_eq!(canon("/a | %+"), "/a | %+");
assert_eq!(canon("/a | %+:year"), "/a | %+:year");
assert_eq!(canon("/a | [%+:year .maior. 2000]"), "/a | [%+:year > 2000]");
assert!(refuse("/user/*:name").contains("::name"));
assert_eq!(canon("/x | (::a ?= 1 ? 2 : 0)"), "/x | (::a ?= 1 ? 2 : 0)");
assert!(refuse("/x | (::a ?= 1 ? 2 :zero)").contains("':'"));
}
#[test]
fn keyed_aggregates_refuse_the_plain_pipe() {
assert!(refuse("/a | top(3, ::x)").contains("@| top"));
assert!(refuse("/a | sort_by(::x)").contains("@| sort_by"));
assert!(canon("/a @| top(3, ::x)").contains("@| top"));
assert!(canon("/a @| group(::k) | top(3, ::x)").contains("| top"));
assert!(canon("/a *__ top(3, ::x)").contains("@| top"));
for q in [
"/a | %(::n, 't', ::t) @| sort_by(:t)",
"/a | %(::n, 't', ::t) @| unique_by(:t)",
"/a | %(::n, 't', ::t) @| min_by(:t)",
"/a | %(::n, 't', ::t) @| max_by(:t)",
"/a | %(::n, 't', ::t) @| top(2, :t)",
"/a | %(::n, 't', ::t) @| group(:t) | top(1, :n)",
] {
assert!(canon(q).contains("(:"), "{q}: {}", canon(q));
}
}
#[test]
fn record_push_canonicalizes() {
assert_eq!(canon("/a | .%(::x, 'n', 1)"), "/a | .%(::x; n = 1)");
assert_eq!(canon("/a | .r%(::x)"), "/a | .r%(::x)");
assert_eq!(canon("/a | .r%%(::x)"), "/a | .r%%(::x)");
assert_eq!(canon("/a | .r%(::x) | $.r:x"), "/a | .r%(::x) | $.r:x");
assert!(refuse("/a | .r%").contains("record push"));
assert_eq!(canon("/a | .r%(::x) | /b"), "/a | .r%(::x) | /b");
}
#[test]
fn operand_paths_ascend_and_step_sideways() {
assert_eq!(canon("/a/b[\\*::x = 1]"), "/a/b[\\*::x = 1]");
assert_eq!(canon("/a/b[\\\\?c::x = 1]"), "/a/b[\\\\?c::x = 1]");
assert_eq!(canon("/p | rec(dir, \\*:::name, me, ::city)"), "/p | %(dir = \\*:::name; me = ::city)");
assert_eq!(canon("/a[;-*::x = 1]"), "/a[>*::x = 1]");
assert_eq!(canon("/a[>>?*::x = 1]"), "/a[>>?*::x = 1]");
assert_eq!(canon("/a | rec(up, ../*::n)"), "/a | %(up = \\*::n)");
}
#[test]
fn record_convention_canonicalizes() {
assert_eq!(canon("/a | %(n = ::age; city = /profile::city)"), "/a | %(n = ::age; city = /profile::city)");
assert_eq!(canon("/a | rec(\"n\", ::age, \"city\", /profile::city)"), "/a | %(n = ::age; city = /profile::city)");
assert_eq!(canon("/a | %(n = ::age, city = /profile::city)"), "/a | %(n = ::age; city = /profile::city)");
assert_eq!(canon("/a | record(n, ::age)"), "/a | %(n = ::age)");
assert_eq!(canon("/a | %(::name; kb = ::size)"), "/a | %(::name; kb = ::size)");
assert_eq!(canon("/a | %('my-key' = 1; adult = ::age >= 18)"), "/a | %('my-key' = 1; adult = (::age >= 18))");
assert_eq!(canon("/a | %(and = 1)"), "/a | %('and' = 1)");
assert_eq!(canon("/a | %%(k = 2)"), "/a | %%(k = 2)");
assert_eq!(canon("/a | .r%(k = ::x; j = 1)"), "/a | .r%(k = ::x; j = 1)");
assert_eq!(canon("/a @| group(k = ::x, ::y)"), "/a @| group(k = ::x, ::y)");
assert_eq!(canon("/a @| group(\"k\", ::x)"), "/a @| group(k = ::x)");
}
#[test]
fn list_literal_canonicalizes() {
assert_eq!(canon("/a | @(1; 2; 3)"), "/a | @(1; 2; 3)");
assert_eq!(canon("/a | @(1, 2, 3)"), "/a | @(1; 2; 3)");
assert_eq!(canon("/a | @()"), "/a | @()");
assert_eq!(canon("/a | %(tags = @(::x; 'y'); n = 1)"), "/a | %(tags = @(::x; 'y'); n = 1)");
assert_eq!(canon("/a | @(/tags/*::)"), "/a | @(/tags/*::)");
assert_eq!(canon("= @(1; 2) | count"), "^ | @(1; 2) | count");
assert_eq!(canon("/a | *(1; 2)"), "/a | @(1; 2)");
assert_eq!(canon("/a[::x = *(1; 2)]"), "/a[::x = @(1; 2)]");
assert_eq!(canon("/*(?::x = 1)"), "/*[::x = 1]");
}
#[test]
fn tail_colon_aliases_canonicalize() {
assert_eq!(canon("/employees/6(:-manager_id)+::name"), "/employees/6(->manager_id)+::name");
assert_eq!(canon("/tracks/1::album_id:--::title"), "/tracks/1::album_id-->::title");
assert_eq!(canon("/a:-b"), "/a->b");
assert_eq!(
canon("/x | (::a ?= 1 ? 2 :-3)"),
canon("/x | (::a ?= 1 ? 2 : -3)")
);
}