from test/more import *;
requires_capability( "fs" );
from std/path/z import ZPath;
from std/data/cbor import TaggedValue;
from std/data/kdl import KDL;
from std/data/xml import XML;
function compile ( p ) { return new ZPath(path: p) }
function query ( d, p ) { return compile(p).query(d) }
function first ( d, p, f ) { return compile(p).first(d, f) }
function exists ( d, p ) { return compile(p).exists(d) }
function assign_first ( d, p, v ) { return compile(p).assign_first( d, v ) }
function assign_all ( d, p, v ) { return compile(p).assign_all( d, v ) }
let data := {
users: [
{
name: "Ada",
age: 32,
email: "ada@example.test",
},
{
name: "Bob",
age: 27,
email: null,
},
{
name: "Cara",
age: 40,
email: "cara@example.test",
},
],
meta: {
title: "Example",
tags: [ "x", "y" ],
},
address: {
street: "naist street",
city: "Nara",
postcode: "630-0192",
},
};
let names := query( data, "/users/*/name" );
is( names.length(), 3, "absolute path traverses arrays and dicts" );
is( names[0], "Ada", "first name" );
is( names[2], "Cara", "third name" );
is(
new ZPath( path: "/users/*/name" ).evaluate( data, { level: 9 } ).length(),
3,
"evaluate accepts optional meta argument",
);
is( first( data, "/users/#1/name", "n/a" ), "Bob", "#index segment" );
is( first( data, "/users/#9/name", "n/a" ), "n/a", "fallback for missing path" );
ok( exists( data, "/users/#1/name" ), "exists helper sees present path" );
ok( not exists( data, "/users/#9/name" ), "exists helper sees missing path" );
let assign_blob := {
users: [
{ name: "Ada", age: 32 },
{ name: "Bob", age: 27 },
{ name: "Cara", age: 40 },
],
meta: {
title: "Before",
},
};
is(
assign_first( assign_blob, "/users/*/name", "ALPHA" ),
"ALPHA",
"assign_first helper returns assigned value",
);
is( assign_blob{users}[0]{name}, "ALPHA", "assign_first mutates first match only" );
is( assign_blob{users}[1]{name}, "Bob", "assign_first leaves later matches untouched" );
is(
assign_all( assign_blob, "/users/*/name", "OMEGA" ),
"OMEGA",
"assign_all helper returns assigned value",
);
is( assign_blob{users}[0]{name}, "OMEGA", "assign_all updates first matching node" );
is( assign_blob{users}[1]{name}, "OMEGA", "assign_all updates second matching node" );
is( assign_blob{users}[2]{name}, "OMEGA", "assign_all updates third matching node" );
is(
new ZPath( path: "/meta/title" ).assign_first( assign_blob, "After" ),
"After",
"assign_first works directly on compiled object",
);
is( assign_blob{meta}{title}, "After", "compiled assign_first mutates dictionary value" );
is(
new ZPath( path: "/users/#9/name" ).assign_all( assign_blob, "UNCHANGED" ),
"UNCHANGED",
"assign_all returns value even when no nodes match",
);
is( assign_blob{users}[0]{name}, "OMEGA", "assign_all with no matches keeps original data" );
function _assign_first_throws ( blob, path, value ) {
try {
new ZPath( path: path ).assign_first( blob, value );
return false;
}
catch {
return true;
}
}
ok(
_assign_first_throws( assign_blob, "/users/#9/name", "X" ),
"assign_first throws when no nodes match",
);
let api_blob := {
users: [
{ name: "Ada", age: 32, title: "Reader" },
{ name: "Bob", age: 27, title: "Writer" },
],
meta: {
title: "Hello 2026",
},
};
let age_path := new ZPath( path: "/users/#0/age" );
is(
age_path.assign_first( api_blob, 8, "+=" ),
40,
"assign_first accepts compound operator argument",
);
is( api_blob{users}[0]{age}, 40, "compound assign_first mutates selected node" );
let title_path := new ZPath( path: "/users/*/title" );
is(
title_path.assign_all( api_blob, "!", "_=" ),
"!",
"assign_all keeps ordinary RHS return contract with operator argument",
);
is( api_blob{users}[0]{title}, "Reader!", "compound assign_all mutates first match" );
is( api_blob{users}[1]{title}, "Writer!", "compound assign_all mutates later match" );
ok(
new ZPath( path: "/users/#1/age" ).assign_maybe( api_blob, 3, "+=" ),
"assign_maybe returns true on match",
);
is( api_blob{users}[1]{age}, 30, "assign_maybe updates maybe-selected node" );
ok(
not new ZPath( path: "/users/#9/age" ).assign_maybe( api_blob, 1, "+=" ),
"assign_maybe returns false on no match",
);
let meta_title_path := new ZPath( path: "/meta/title" );
is(
meta_title_path.assign_first(
api_blob,
[ /[0-9]+/, fn m -> "world" ],
"~=",
),
"Hello world",
"assign_first accepts structured ~= payload",
);
is( api_blob{meta}{title}, "Hello world", "structured ~= mutates through ZPath" );
let meta_title_ref := meta_title_path.ref_first(api_blob);
is( meta_title_ref(), "Hello world", "ref_first returns getter/setter closure" );
is( meta_title_ref("Updated"), "Updated", "ref_first setter returns assigned value" );
is( api_blob{meta}{title}, "Updated", "ref_first setter mutates target" );
let title_refs := title_path.ref_all(api_blob);
is( title_refs.length(), 2, "ref_all returns one ref per selected target" );
is( title_refs[0](), "Reader!", "ref_all getter reads first target" );
title_refs[0]( "Lead!" );
is( api_blob{users}[0]{title}, "Lead!", "ref_all refs remain assignable" );
is(
new ZPath( path: "/users/#9/name" ).ref_maybe(api_blob),
null,
"ref_maybe returns null on no match",
);
is(
new ZPath( path: "/users/#1/name" ).ref_maybe(api_blob)(),
"Bob",
"ref_maybe returns ref on match",
);
let zp := new ZPath( path: "/users/*[is-first()]/name" );
let first_only := zp.get(data);
is( first_only.length(), 1, "is-first filter" );
is( first_only[0], "Ada", "is-first selected first entry" );
let compiled := compile( "/users/*/name" );
is( compiled.query(data).length(), 3, "compile still returns reusable zpath object" );
ok( compiled.exists(data), "compiled object has exists method" );
ok( not compile("/users/#9/name").exists(data), "compiled exists false on missing path" );
let last_only := query( data, "/users/*[is-last()]/name" );
is( last_only.length(), 1, "is-last filter" );
is( last_only[0], "Cara", "is-last selected last entry" );
let numbers := [ 10, 20, 30 ];
let keyed_last := query( { numbers: numbers }, "/numbers/*[is-last()]" );
is( keyed_last.length(), 1, "is-last works when sibling keys differ" );
is( keyed_last[0], 30, "is-last picks final array entry" );
let second := query( data, "/users/*[index() == 1]/name" );
is( second.length(), 1, "index() filter" );
is( second[0], "Bob", "index() == 1 selected second entry" );
let with_email := query( data, "/users/*[email]/name" );
is( with_email.length(), 3, "bare identifier filter checks field existence" );
is( with_email[0], "Ada", "first user with email" );
is( with_email[1], "Bob", "second user with email key" );
is( with_email[2], "Cara", "third user with email key" );
let no_email := query( data, "/users/*[!email]/name" );
is( no_email.length(), 1, "negated bare identifier still uses value truthiness" );
is( no_email[0], "Bob", "only Bob has a falsey email value" );
let age_compare := query( data, "/users/*[age >= 32]/name" );
is( age_compare.length(), 2, "comparison operators in filters" );
is( age_compare[0], "Ada", "age >= 32 includes ada" );
is( age_compare[1], "Cara", "age >= 32 includes cara" );
let logic_and := query( data, "/users/*[age >= 27 && age < 40]/name" );
is( logic_and.length(), 2, "logical and in filters" );
is( logic_and[0], "Ada", "and filter includes ada" );
is( logic_and[1], "Bob", "and filter includes bob" );
let logic_or := query( data, "/users/*[age < 30 || name == 'Cara']/name" );
is( logic_or.length(), 2, "logical or and == comparison in filters" );
is( logic_or[0], "Bob", "or filter includes bob" );
is( logic_or[1], "Cara", "or filter includes cara" );
let arithmetic := query( data, "/users/*[(age / 2) + 1 > 16]/name" );
is( arithmetic.length(), 2, "arithmetic operators in filters" );
is( arithmetic[0], "Ada", "arithmetic filter includes ada" );
is( arithmetic[1], "Cara", "arithmetic filter includes cara" );
let ternary := query( data, "/users/*[(age >= 32 ? true() : false())]/name" );
is( ternary.length(), 2, "ternary operator in filters" );
is( ternary[0], "Ada", "ternary filter includes ada" );
is( ternary[1], "Cara", "ternary filter includes cara" );
let strings := {
text: "Main street",
media: "application/pdf;version=1",
list: [ "one", "two", 3 ],
street: "naist street",
};
let person_names := {
first: "John",
last: "Smith",
};
is( first( strings, "index-of(\"street\", text)", "x" ), 5, "index-of(needle, expr) works" );
is( first( strings, "/text[index-of(\"street\") == 5]", "x" ), "Main street", "index-of(needle) uses current node" );
is( first( strings, "last-index-of(\"a\", \"bananas\")", "x" ), 5, "last-index-of(needle, expr) works" );
is( first( strings, "/text[string-length() == 11]", "x" ), "Main street", "string-length() uses current node" );
is( first( strings, "string-length(/text)", "x" ), 11, "string-length(path) string-coerces first path item" );
is( first( strings, "upper-case(text)", "x" ), "MAIN STREET", "upper-case(expr) works" );
is( first( strings, "lower-case(\"PDF\")", "x" ), "pdf", "lower-case(expr) works" );
is( first( strings, "substring(text, 5, 6)", "x" ), "street", "substring(expr, start, len) works" );
is( first( strings, "/text[substring(5, 6) == \"street\"]", "x" ), "Main street", "substring(start, len) uses current node" );
ok( first( strings, "match(\"street\", text)", false ), "match(pattern, expr) works" );
is( first( strings, "replace(\" street\", \" road\", text)", "x" ), "Main road", "replace(pattern, replacement, expr) works" );
is(
first( strings, "replace(\"(.*) street\", \"$1 road\", street)", "x" ),
"naist road",
"replace supports xpath-style $1 capture replacement",
);
is(
first( strings, "replace(\"road$\", \"\", replace(\"(.*) street\", \"$1 road\", street))", "x" ),
"naist ",
"replace supports anchored suffix pattern in nested calls",
);
is(
first( strings, "replace(\"^[^/]*/\", \"\", replace(\";.*\", \"\", media))", "x" ),
"pdf",
"replace supports leading-prefix cleanup for media types",
);
is( first( strings, "join(\"|\", list/*)", "x" ), "one|two|3", "join(joiner, expr) works" );
is(
first( data, "join(\"|\", users/*[age >= 27]/name)", "x" ),
"Ada|Bob|Cara",
"top-level function expression accepts filtered path arguments",
);
is(
first( data, "join(\"|\", users/*[name == \"Ada\"]/name)", "x" ),
"Ada",
"top-level join path args allow string-filter tokens",
);
is(
first( strings, "escape(\"A&B <test> \\\"q\\\"\")", "x" ),
"A&B <test> "q"",
"escape(expr) performs xml escaping",
);
is(
first( strings, "unescape(\"A&B <test>\")", "x" ),
"A&B <test>",
"unescape(expr) decodes xml entities",
);
is( first( strings, "format(\"%s!\", text)", "x" ), "Main street!", "format(fmt, expr) works" );
let ceil_math := query( data, "/users/*[ceil(age / 3) == 11]/name" );
is( ceil_math.length(), 1, "ceil() in filters" );
is( ceil_math[0], "Ada", "ceil() result matched ada" );
let ceil_math_current := query( data, "/users/*/age[ceil() == 32]" );
is( ceil_math_current.length(), 1, "ceil() without args uses current node" );
is( ceil_math_current[0], 32, "ceil() without args matched numeric node" );
let floor_math := query( data, "/users/*[floor(age / 3) == 13]/name" );
is( floor_math.length(), 1, "floor() in filters" );
is( floor_math[0], "Cara", "floor() result matched cara" );
let floor_math_current := query( data, "/users/*/age[floor() == 27]" );
is( floor_math_current.length(), 1, "floor() without args uses current node" );
is( floor_math_current[0], 27, "floor() without args matched numeric node" );
let round_math := query( data, "/users/*[round(age / 3) == 9]/name" );
is( round_math.length(), 1, "round() in filters" );
is( round_math[0], "Bob", "round() result matched bob" );
let round_math_current := query( data, "/users/*/age[round() == 40]" );
is( round_math_current.length(), 1, "round() without args uses current node" );
is( round_math_current[0], 40, "round() without args matched numeric node" );
let math_blob := {
values: [ 1, "2", 3.5 ],
mixed: [ 1, "oops", 2.5, null, "7" ],
solo: 1.2,
};
is( first( math_blob, "sum(values/*)", "x" ), 6.5, "sum(expr) works" );
is( first( math_blob, "sum(values/*, 1.5)", "x" ), 8, "sum(expr, literal) works" );
is( first( math_blob, "sum(1, 2, 3)", "x" ), 6, "sum(literal, literal, literal) works" );
is( first( math_blob, "sum(values/*, mixed/*)", "x" ), 17, "sum(expr, expr) supports multiple expressions and ignores non-numeric nodes" );
is( first( math_blob, "min(values/*)", "x" ), 1, "min(expr) handles numeric arrays" );
is( first( math_blob, "min(values/*, 0.5)", "x" ), 0.5, "min(expr, literal) flattens all argument values" );
is( first( math_blob, "min(values/*, mixed/*)", "x" ), 1, "min(expr, expr) supports multiple expressions and ignores non-numeric nodes" );
is( first( math_blob, "max(values/*)", "x" ), 3.5, "max(expr) handles numeric arrays" );
is( first( math_blob, "max(values/*, 9)", "x" ), 9, "max(expr, literal) flattens all argument values" );
is( first( math_blob, "max(values/*, mixed/*)", "x" ), 7, "max(expr, expr) supports multiple expressions and ignores non-numeric nodes" );
is( first( math_blob, "ceil(solo)", "x" ), 2, "ceil(expr) works" );
is( first( math_blob, "floor(solo)", "x" ), 1, "floor(expr) works" );
is( first( math_blob, "round(solo)", "x" ), 1, "round(expr) works" );
is( first( math_blob, "floor(solo, 9.9)", "x" ), 1, "floor(expr, expr) supports multiple expressions by using first numeric value" );
is( first( math_blob, "round(solo, 9.9)", "x" ), 1, "round(expr, expr) supports multiple expressions by using first numeric value" );
let non_null_email := query( data, "/users/*[type(email) != 'null']/name" );
is( non_null_email.length(), 2, "type() function in filters" );
is( non_null_email[0], "Ada", "type() keeps non-null email for ada" );
is( non_null_email[1], "Cara", "type() keeps non-null email for cara" );
let value_compare := query( data, "/meta/tags/*[value() == 'x']" );
is( value_compare.length(), 1, "value() function in filters" );
is( value_compare[0], "x", "value() compares primitive tag value" );
let under_40_by_count := query( data, "/users/*[count(split(name, 'a')) < 3]/name" );
let count_contract_blob := {
nodes: [
{ arr: [ 1, 2 ] },
{ arr: [ 3 ] },
{ arr: null },
],
};
is( under_40_by_count.length(), 3, "count(expression) works on array expression values" );
is( first( strings, "count(text, list/*)", "x" ), 4, "count(scalar, expr) accumulates sequence counts" );
is( first( person_names, "count(first, last)", "x" ), 2, "count(path, path) counts path argument values" );
is( first( data, "count(/users/*)", "x" ), 3, "count(path to sequence node) counts sequence members" );
is( first( data, "count(1, 2, 3)", "x" ), 3, "count(literal, literal, literal) preserves sequence count policy" );
is( first( count_contract_blob, "count(nodes/*/arr)", "x" ), 3, "count(path node-set) counts selected nodes without flattening nested array members" );
is( first( count_contract_blob, "count(nodes/*[index() == 0]/arr/*)", "x" ), 2, "count(single sequence node path) counts sequence members" );
is( first( count_contract_blob, "count(nodes/*/arr/*, 1)", "x" ), 4, "count(node-set, scalar) combines node and scalar sequence counts" );
is( first( person_names, "count(union(first, first, last))", "x" ), 2, "union(path, path, path) preserves node-set uniqueness" );
is( first( person_names, "count(intersection(first, first, last))", "x" ), 0, "intersection(path, path, path) preserves set contract" );
is(
first( strings, "replace(list/*, \"x\", \"one\")", "x" ),
"x",
"replace(pattern, replacement, scalar) string-coerces sequence arguments",
);
is(
first( strings, "substring(list/*, 0, 3)", "x" ),
"one",
"substring(sequence, start, len) string-coerces first item in sequence",
);
is(
first( strings, "index-of(\"ne\", list/*)", "x" ),
1,
"index-of(sequence, needle) string-coerces first item in sequence",
);
is( first( strings, "replace(\"street\", \"road\", \"Main street\")", "x" ), "Main road", "replace(pattern, replacement, literal) string-coerces literal input" );
let phase2_contract_blob := {
numbers: [ 1, 2, 3 ],
words: [ "alpha", "beta" ],
};
is( first( phase2_contract_blob, "sum(1, 2, 3)", "x" ), 6, "sum literal arguments flatten as numeric values" );
is( first( phase2_contract_blob, "sum(numbers/*)", "x" ), 6, "sum path argument flattens sequence values" );
is( first( phase2_contract_blob, "min(9, 4, 7)", "x" ), 4, "min literal arguments flatten as numeric values" );
is( first( phase2_contract_blob, "min(numbers/*)", "x" ), 1, "min path argument flattens sequence values" );
is( first( phase2_contract_blob, "max(9, 4, 7)", "x" ), 9, "max literal arguments flatten as numeric values" );
is( first( phase2_contract_blob, "max(numbers/*)", "x" ), 3, "max path argument flattens sequence values" );
is( first( phase2_contract_blob, "join(\"|\", words/*)", "x" ), "alpha|beta", "join(path) string-coerces sequence members" );
is( first( phase2_contract_blob, "replace(\"alpha\", \"omega\", words/*)", "x" ), "omega", "replace(path) string-coerces first sequence member" );
let segment_call_blob := {
numbers: [
{ values: [ 1, 2, 3 ] },
{ values: [ 8 ] },
],
};
let segment_count := query( segment_call_blob, "/numbers/*/count(values/*)" );
is( segment_count.length(), 2, "function call segments keep per-node sequence context" );
is( segment_count[0], 3, "count(path) works in function call segment for first node" );
is( segment_count[1], 1, "count(path) works in function call segment for second node" );
let segment_peer_count := query( segment_call_blob, "/numbers/*[index() == count() - 1]/values/#0" );
is( segment_peer_count.length(), 1, "count() in filter uses peer-sequence size with no args" );
is( segment_peer_count[0], 8, "count()-based last-item filter selects final peer" );
let cart := {
items: [
{ price: 2.5, quantity: 2 },
{ price: 4, quantity: 1 },
],
};
is(
first( cart, "sum(/items/*/value(number(price) * number(quantity)))", "x" ),
9,
"nested expression in path argument preserves sequence for sum",
);
is(
first( cart, "/items/#0/value(number(price))", "x" ),
2.5,
"number(path) string/sequence-coerces first selected path item",
);
let faceless_like_cart := {
body: {
items: [
{
item: [
{ price: 4.5, quantity: 3 },
{ price: 3, quantity: 3 },
],
},
],
},
};
let faceless_like_line_totals := query(
faceless_like_cart,
"**/items/*/item/*/value(number(price) * number(quantity))",
);
is(
faceless_like_line_totals.length(),
2,
"nested value(number(path) * number(path)) path traverses named segments through arrays",
);
is( faceless_like_line_totals[0], 13.5, "first faceless-like line total evaluates correctly" );
is( faceless_like_line_totals[1], 9, "second faceless-like line total evaluates correctly" );
is(
first(
faceless_like_cart,
"sum(**/items/*/item/*/value(number(price) * number(quantity)))",
"x",
),
22.5,
"sum(path nested expression) keeps sequence context through array-name traversal",
);
is(
first(
faceless_like_cart,
"format(\"$%02.2f\", sum(**/items/*/item/*/value(number(price) * number(quantity))))",
"x",
),
"$22.50",
"format(sum(path nested expression)) handles faceless-like cart totals",
);
let faceless_like_table := {
body: {
table: {
tr: [
{ td: [ "TD1.1", "TD1.2" ] },
{ td: [ "TD2.1", "TD2.2" ] },
{ td: [ "TD3.1", "TD3.2" ] },
],
},
},
};
is(
first( faceless_like_table, "count(**/tr/*)", "x" ),
3,
"count(descendant path) preserves node-set array member count",
);
let with_named_meta := query( data, "/users/*[/meta/title]/name" );
is( with_named_meta.length(), 3, "absolute subpath exists filter" );
// key() should be null as this is an array
let key_ne := query( data, "/meta/tags/*[key() != 1]" );
is( key_ne.length(), 2, "key() inequality filter" );
is( key_ne[0], "x", "key() != 1 selected first tag" );
let map_key_ne := query( data, "/address/*[key() != 'city']" );
is( map_key_ne.length(), 2, "key() inequality filter over map children" );
ok(
( "naist street" in map_key_ne ) and ( "630-0192" in map_key_ne ),
"key() != 'city' keeps street and postcode map children",
);
let ancestors := query( data, "/users/#0/name/..*/key" );
is( ancestors.length(), 0, "..* over primitive has no key child by default" );
let all_scalars := query( data, "/**" );
ok( all_scalars.length() > 8, "** returns many descendants including root" );
let parent_roundtrip := query( data, "/users/#2/name/../name" );
is( parent_roundtrip.length(), 1, "parent segment preserves metadata" );
is( parent_roundtrip[0], "Cara", "parent traversal returns to owning node" );
let parent_indexing := query( data, "/users/#1/../#0/name" );
is( parent_indexing.length(), 1, "parent + #index segment keeps index metadata" );
is( parent_indexing[0], "Ada", "parent + #index resolves first sibling" );
let descendant_parent_roundtrip := query( data, "/**/name/../name" );
ok( descendant_parent_roundtrip.length() >= 3, "** metadata supports parent traversal" );
let ancestor_reanchor := query( data, "/users/#2/name/..*/users/#0/name" );
is( ancestor_reanchor.length(), 1, "..* keeps ancestor parent chain" );
is( ancestor_reanchor[0], "Ada", "..* can re-anchor from ancestors" );
is( first( data, "/meta/title/../title", "x" ), "Example", "parent traversal with whitespace around segment" );
let xml_doc := XML.parse( "<root xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'><item id='x'/><rdf:seq rdf:about='aboutvalue'/></root>" );
let xml_attr := query( xml_doc, "rdf:seq/@rdf:about" );
let xml_attr_nodes := compile( "rdf:seq/@rdf:about" ).evaluate(xml_doc);
is( xml_attr.length(), 1, "xml attribute lookup by name" );
is(
first( xml_doc, "string(rdf:seq/@rdf:about)", "x" ),
"aboutvalue",
"xml attribute lookup string-coerces attribute node value",
);
is(
first( xml_attr_nodes[0], "type()", "x" ),
"attr",
"xml attribute lookup returns attribute-node type",
);
let xml_attr_parent := query( xml_doc, "rdf:seq/@rdf:about/../@rdf:about" );
is( xml_attr_parent.length(), 1, "attribute nodes keep parent metadata" );
is(
first( xml_doc, "string(rdf:seq/@rdf:about/../@rdf:about)", "x" ),
"aboutvalue",
"attribute parent traversal round-trips with string coercion",
);
is(
query( xml_doc, "/@*" ).length(),
1,
"xml attribute wildcard returns attribute node entries",
);
is(
query(
xml_doc,
"rdf:seq/@*[url() == \"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"]",
).length(),
1,
"xml attribute wildcard predicates can filter by namespace url()",
);
let xml_text_doc := XML.parse( "<root>alpha<b>mid</b>omega</root>" );
let text_nodes := query( xml_text_doc, "/*[type() == 'text']" );
is( text_nodes.length(), 2, "type() classifies XML text child nodes" );
let xml_faceless_doc := XML.parse(
"<html><body><table><tr id='tr1'><td id='td1.1'>TD1.1</td><td id='td1.2'>TD1.2</td></tr><tr id='tr2'><td id='td2.1'>TD2.1</td><td id='td2.2'>TD2.2</td></tr><tr id='tr3'><td id='td3.1'>TD3.1</td><td id='td3.2'>TD3.2</td></tr></table><person><items><item><price>4.50</price><quantity>3</quantity></item><item><price>2.25</price><quantity>4</quantity></item></items><data>mixed <span>content</span> test</data></person><rdf:seq xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'><rdf:li>data</rdf:li></rdf:seq></body></html>",
);
let xml_last_tr := query( xml_faceless_doc, "**/table/tr[index() == count() - 1]" );
let xml_last_tr_nodes := compile( "**/table/tr[index() == count() - 1]" ).evaluate(xml_faceless_doc);
is( xml_last_tr.length(), 1, "index() == count() - 1 works for XML siblings with ignorable whitespace trimmed" );
is(
first( xml_last_tr_nodes[0], "string(@id)", "x" ),
"tr3",
"count()-relative XML row filter selects last row",
);
let xml_count_three := query( xml_faceless_doc, "**[count(*) == 3]" );
is( xml_count_three.length(), 3, "count(*) over XML descendants ignores whitespace-only text nodes" );
is(
query( xml_faceless_doc, "**/person[data]" ).length(),
1,
"XML exists-filter accepts present data child nodes",
);
is(
query( xml_faceless_doc, "**/data/*" ).length(),
3,
"XML wildcard child traversal includes mixed-content text and element children",
);
is(
query( xml_faceless_doc, "**/data/*[type() == \"text\"]" ).length(),
2,
"XML type() filter selects text children under mixed-content data nodes",
);
is(
query( xml_faceless_doc, "**/data/*[type() == \"text\"][index() == 1]" ).length(),
1,
"XML text type() filter composes with index() filters on mixed-content children",
);
is(
first(
xml_faceless_doc,
"sum(**/items/item/value(number(price) * number(quantity)))",
"x",
),
22.5,
"sum over XML number(path) expressions coerces text-number nodes",
);
is(
first(
xml_faceless_doc,
"format(\"$%02.2f\", sum(**/items/item/value(number(price) * number(quantity))))",
"x",
),
"$22.50",
"format(sum(...)) over XML number(path) expressions returns expected currency text",
);
let cborish := {
tv: new TaggedValue(
tag: 32,
value: "http://www.ietf.org/rfc/rfc2396.txt",
),
nested: new TaggedValue(
tag: 24,
value: new TaggedValue( tag: 32, value: 42 ),
),
items: [
new TaggedValue( tag: 2, value: 1 ),
new TaggedValue( tag: 3, value: 2 ),
],
};
is(
new ZPath( path: "tv" ).first( cborish, "NOT FOUND" ),
"http://www.ietf.org/rfc/rfc2396.txt",
"named child unwraps TaggedValue payload",
);
is(
new ZPath( path: "type(tv)" ).first( cborish, "NOT FOUND" ),
"string",
"type(tv) uses wrapped value type",
);
is(
new ZPath( path: "tag(tv)" ).first( cborish, "NOT FOUND" ),
32,
"tag(tv) returns cbor tag number",
);
is(
new ZPath( path: "type(tag(tv))" ).first( cborish, "NOT FOUND" ),
"number",
"type(tag(tv)) is number",
);
is(
first( cborish, "/nested", "NOT FOUND" ),
42,
"nested TaggedValue wrappers unwrap recursively",
);
let cbor_item_types := query( cborish, "/items/*[type() == 'number']" );
is( cbor_item_types.length(), 2, "type() in filters sees unwrapped cbor values" );
let cbor_tag_filter := query( cborish, "/items/*[tag() == 3]" );
is( cbor_tag_filter.length(), 1, "tag() in filters can inspect current node tag" );
is( cbor_tag_filter[0], 2, "tag() filter still returns unwrapped value" );
let kdl_doc := ( new KDL() ).decode( """(pkg)package "zuzu" (email)"dev@example.test" version="0.1.0" {
foo "first"
bar "middle"
foo "second"
foo "third"
}
""" );
is(
first( kdl_doc, "/package/#0", "NOT FOUND" ),
"zuzu",
"KDLNode exposes args as indexed children",
);
is(
first( kdl_doc, "/package/foo#2/#0", "NOT FOUND" ),
"third",
"KDLNode supports named indexed child lookup",
);
is(
first( kdl_doc, "/package/@version", "NOT FOUND" ),
"0.1.0",
"KDLNode exposes props as attributes",
);
is(
first( kdl_doc, "tag(/package)", "NOT FOUND" ),
"pkg",
"KDLNode tag() returns type annotation",
);
is(
first( kdl_doc, "tag(/package/#1)", "NOT FOUND" ),
"email",
"KDLValue tag() returns type annotation",
);
is(
first( kdl_doc, "local-name(/package)", "NOT FOUND" ),
"package",
"local-name() returns KDLNode name",
);
is(
first( kdl_doc, "/package/foo#2/local-name()", "NOT FOUND" ),
"foo",
"local-name() works as a segment on KDLNode",
);
let phase3_compare_blob := {
age: 26,
vals: [ 1, 2 ],
ones: [ 1 ],
numbers: [
{ type: "iPhone", number: "0123-4567-8888" },
{ type: "home", number: "0123-4567-8910" },
{ type: "work", number: "0123-9999-8910" },
],
vals_holder: {
items: [ 1, 2 ],
},
miss_holder: {
present: null,
},
};
let phase3_filter_eq := query( phase3_compare_blob, "/vals/*[. == 1]" );
is( phase3_filter_eq.length(), 1, "filter [. == 1] uses sequence existential equality" );
is( phase3_filter_eq[0], 1, "filter [. == 1] keeps matching scalar" );
is(
first( phase3_compare_blob, "/miss_holder/type(missing)", "x" ),
"undefined",
"type() reports undefined for absent values",
);
is(
first( phase3_compare_blob, "/miss_holder/type(present)", "x" ),
"null",
"type() reports null for explicit null values",
);
is(
query( phase3_compare_blob, "/miss_holder[missing]" ).length(),
0,
"missing values are false in filter truthiness",
);
is(
query( phase3_compare_blob, "/miss_holder[present]" ).length(),
1,
"bare-identifier filters check existence even when value is null",
);
is(
query( phase3_compare_blob, "[age == 26]" ).length(),
1,
"relative bare-filter equality can match current/root node",
);
is(
query( phase3_compare_blob, "/[age == 26]" ).length(),
1,
"absolute bare-filter equality can match root node",
);
is(
query( phase3_compare_blob, "**[age == 26]" ).length(),
1,
"descendants-or-self filter equality includes matching self node",
);
is(
query( phase3_compare_blob, "/**[age == 26]" ).length(),
1,
"absolute descendants-or-self filter equality includes matching root",
);
is(
query( phase3_compare_blob, "/numbers/[type == 'iPhone']" ).length(),
1,
"container filter segment targets array children for equality checks",
);
is(
query( phase3_compare_blob, "numbers/*[type == 'home']" ).length(),
1,
"array child filter matches only home entry",
);
is(
first( phase3_compare_blob, "numbers/*[type == 'home']/number", "x" ),
"0123-4567-8910",
"array child filter can project a matched scalar field",
);
is(
query( phase3_compare_blob, "/**[type == 'home']" ).length(),
1,
"descendant filter keeps matching container child",
);
is(
first( phase3_compare_blob, "/**[type == 'home']/number", "x" ),
"0123-4567-8910",
"descendant filter can project matched child fields",
);
is(
query(
{
typetest: {
nullvalue: null,
},
},
"**/typetest[nullvalue]",
).length(),
1,
"exists-style filters treat present null fields as existing",
);
let pairlist_query_blob := {{ tag: "perl", page: 1, tag: "zuzu" }};
is(
first( pairlist_query_blob, "/#1/@key", "missing" ),
"page",
"pairlist global index exposes pair key",
);
is(
first( pairlist_query_blob, "/#1/@value", "missing" ),
1,
"pairlist global index exposes pair value",
);
let pairlist_tag_values := query( pairlist_query_blob, "/tag/@value" );
is( pairlist_tag_values.length(), 2, "pairlist name query preserves duplicates" );
is( pairlist_tag_values[0], "perl", "pairlist name query returns first duplicate" );
is( pairlist_tag_values[1], "zuzu", "pairlist name query returns second duplicate" );
is(
first( pairlist_query_blob, "/tag#1/@value", "missing" ),
"zuzu",
"pairlist name#index uses per-key occurrence index",
);
let pairlist_assign_blob := {{ tag: "perl", page: 1, tag: "zuzu" }};
is(
assign_first( pairlist_assign_blob, "/tag#1", "ruby" ),
"ruby",
"pairlist pair assignment returns assigned value",
);
let pairlist_assigned_values := query( pairlist_assign_blob, "/tag/@value" );
is(
pairlist_assigned_values[0],
"perl",
"pairlist pair assignment leaves first duplicate unchanged",
);
is(
pairlist_assigned_values[1],
"ruby",
"pairlist pair assignment updates selected duplicate",
);
is(
first( pairlist_assign_blob, "/#1/@key", "missing" ),
"page",
"pairlist pair assignment preserves pair order",
);
is(
assign_first( pairlist_assign_blob, "/tag#1/@value", "zuzu" ),
"zuzu",
"pairlist pair value assignment returns assigned value",
);
is(
first( pairlist_assign_blob, "/tag#1/@value", "missing" ),
"zuzu",
"pairlist pair value assignment updates selected duplicate",
);
is(
first( pairlist_assign_blob, "/#1/@value", "missing" ),
1,
"pairlist pair value assignment preserves unrelated pair",
);
done_testing();