=encoding utf8
=head1 NAME
std/path/simple - Tiny JSONPath/XPath-like traversal helper.
=head1 SYNOPSIS
from std/path/simple import SimplePath;
let data := {
store: {
books: [
{ author: "Nigel Rees" },
{ author: "J. R. R. Tolkien" },
],
},
};
let p := new SimplePath( path: "store.books[*].author" );
for ( let name in p.query(data) ) {
say name;
}
=head1 IMPLEMENTATION SUPPORT
This module is supported by all implementations of ZuzuScript.
=head1 DESCRIPTION
C<SimplePath> implements only a tiny subset of path traversal:
=over
=item * C<.something>
Dict/PairList lookup by key.
=item * C<.*>
Dict/PairList wildcard that yields all values.
=item * C<[1]>
Array lookup by numeric index.
=item * C<[-1]>
Negative indexes count from the end of arrays.
=item * C<[*]>
Array/Bag/Set wildcard that yields all items.
=back
No other syntax is supported.
The public API intentionally mirrors key C<std/path/z> methods:
C<get>, C<select>, C<query>, C<first>, C<exists>,
C<expression>, C<assign_first>, C<assign_all>, C<assign_maybe>,
C<ref_first>, C<ref_all>, and C<ref_maybe>.
The path operators C<@>, C<@@>, and C<@?> can be set to use this module
in a lexical scope:
from std/path/simple import SimplePath;
SimplePath.use();
=head1 EXPORTS
=head2 Classes
=over
=item C<< SimplePath({ path: String }) >>
Constructs a simple path selector. Returns: C<SimplePath>.
=over
=item C<< SimplePath.use() >>
Parameters: none. Returns: C<null>. Makes this path class the lexical
implementation for C<@>, C<@@>, and C<@?>.
=item C<< path.expression() >>
Parameters: none. Returns: C<String>. Returns the original path
expression.
=item C<< path.get(value) >>, C<< path.select(value) >>, C<< path.query(value) >>
Parameters: C<value> is the query root. Returns: C<Array>. Evaluates the
path and returns selected values.
=item C<< path.first(value, fallback) >>
Parameters: C<value> is the query root and C<fallback> is returned when
there is no match. Returns: value. Returns the first selected value.
=item C<< path.exists(value) >>
Parameters: C<value> is the query root. Returns: C<Boolean>. Returns
true when the path selects at least one value.
=item C<< path.assign_first(target, value, op := ":=", weak := false) >>
Parameters: C<target> is the query root, C<value> is the assignment
value, C<op> is an assignment operator, and C<weak> is accepted for path
API compatibility. Returns: value. Updates the first selected location.
=item C<< path.assign_all(target, value, op := ":=", weak := false) >>
Parameters: same as C<assign_first>. Returns: value. Updates every
selected location.
=item C<< path.assign_maybe(target, value, op := ":=", weak := false) >>
Parameters: same as C<assign_first>. Returns: C<Boolean>. Updates the
first selected location when one exists.
=item C<< path.ref_first(target) >>
Parameters: C<target> is the query root. Returns: C<Function>. Returns a
reference-like getter/setter for the first selected location.
=item C<< path.ref_all(target) >>
Parameters: C<target> is the query root. Returns: C<Array>. Returns
reference-like getter/setters for all selected locations.
=item C<< path.ref_maybe(target) >>
Parameters: C<target> is the query root. Returns: C<Function> or
C<null>. Returns a reference-like getter/setter for the first selected
location when one exists.
=back
=back
=cut
from std/string import substr;
class SimplePath {
let String path;
let Array _steps := [];
static method use () {
from std/internals import setupperprop;
setupperprop( 1, "paths", self );
}
method __build__ () {
_steps := self._parse(path);
}
method expression () {
return path;
}
method _parse ( String raw ) {
let text := raw;
let steps := [];
let i := 0;
while ( i < length text ) {
let ch := substr( text, i, 1 );
if ( ch ≡ "." ) {
i++;
die "SimplePath parse error: expected name or * after '.'"
if i >= length text;
let after_dot := substr( text, i, 1 );
if ( after_dot ≡ "*" ) {
steps.push( { kind: "dict_wildcard" } );
i++;
next;
}
let start := i;
while ( i < length text ) {
let c := substr( text, i, 1 );
last if c ≡ "." or c ≡ "[" or c ≡ "]";
i++;
}
let key := substr( text, start, i - start );
die "SimplePath parse error: empty key"
if key ≡ "";
steps.push( { kind: "key", value: key } );
next;
}
if ( ch ≡ "[" ) {
let close := i + 1;
while ( close < length text and substr( text, close, 1 ) ≢ "]" ) {
close++;
}
die "SimplePath parse error: missing closing ']'"
if close >= length text;
let inner := substr( text, i + 1, close - i - 1 );
if ( inner ≡ "*" ) {
steps.push( { kind: "list_wildcard" } );
}
else if ( inner ~ /^-?[0-9]+$/ ) {
steps.push( { kind: "index", value: int(inner) } );
}
else {
die `SimplePath parse error: unsupported bracket token '[${inner}]'`;
}
i := close + 1;
next;
}
let start := i;
while ( i < length text ) {
let c := substr( text, i, 1 );
last if c ≡ "." or c ≡ "[" or c ≡ "]";
i++;
}
let key := substr( text, start, i - start );
die `SimplePath parse error: unexpected token '${ch}'`
if key ≡ "";
steps.push( { kind: "key", value: key } );
}
return steps;
}
method _node ( value, parent, key ) {
return {
value: value,
parent: parent,
key: key,
};
}
method _evaluate_nodes ( value ) {
let current := [ self._node( value, null, null ) ];
for ( let step in _steps ) {
let next_nodes := [];
for ( let node in current ) {
let v := node{value};
if ( step{kind} ≡ "key" ) {
if ( v instanceof Dict and v.exists( step{value} ) ) {
next_nodes.push( self._node( v.get( step{value} ), node, step{value} ) );
}
else if ( v instanceof PairList ) {
for ( let item in v.get_all( step{value} ) ) {
next_nodes.push( self._node( item, node, step{value} ) );
}
}
next;
}
if ( step{kind} ≡ "dict_wildcard" ) {
if ( v instanceof Dict ) {
for ( let k in v.keys() ) {
next_nodes.push( self._node( v.get(k), node, k ) );
}
}
else if ( v instanceof PairList ) {
for ( let pair in v.to_Array() ) {
next_nodes.push( self._node( pair.value, node, pair.key ) );
}
}
next;
}
if ( step{kind} ≡ "index" ) {
if ( v instanceof Array ) {
let idx := step{value};
if ( idx < 0 ) {
idx := v.length() + idx;
}
if ( idx >= 0 and idx < v.length() ) {
next_nodes.push( self._node( v[idx], node, idx ) );
}
}
next;
}
if ( step{kind} ≡ "list_wildcard" ) {
if ( v instanceof Array ) {
let i := 0;
while ( i < v.length() ) {
next_nodes.push( self._node( v[i], node, i ) );
i++;
}
}
else if ( v instanceof Bag or v instanceof Set ) {
for ( let item in v.to_Array() ) {
next_nodes.push( self._node( item, node, "*" ) );
}
}
next;
}
}
current := next_nodes;
}
return current;
}
method _evaluate ( value ) {
return self._evaluate_nodes(value).map( fn n -> n{value} );
}
method get ( value ) {
return self._evaluate(value);
}
method select ( value ) {
return self._evaluate(value);
}
method query ( value ) {
return self._evaluate(value);
}
method first ( value, fallback ) {
let out := self._evaluate(value);
return out.length() = 0 ? fallback : out[0];
}
method exists ( value ) {
return self._evaluate(value).length() > 0;
}
method _assign_node ( Dict node, value ) {
let parent := node{parent};
die "SimplePath assignment target has no parent node"
if parent ≡ null;
let container := parent{value};
let key := node{key};
if ( container instanceof Array ) {
die "SimplePath assignment expects numeric array index"
if not( key instanceof Number );
container[key] := value;
return value;
}
if ( container instanceof Dict ) {
die "SimplePath assignment expects string dict key"
if not( key instanceof String );
container{(key)} := value;
return value;
}
if ( container instanceof PairList ) {
die "SimplePath assignment expects string pairlist key"
if not( key instanceof String );
container.set( key, value );
return value;
}
die `SimplePath assignment target container '${typeof container}' is not assignable`;
}
method _ref_for_node ( Dict node ) {
let parent := node{parent};
die "SimplePath assignment target has no parent node"
if parent ≡ null;
let container := parent{value};
let key := node{key};
if ( container instanceof Array ) {
die "SimplePath assignment expects numeric array index"
if not( key instanceof Number );
return \ container[key];
}
if ( container instanceof Dict ) {
die "SimplePath assignment expects string dict key"
if not( key instanceof String );
return \ container{(key)};
}
if ( container instanceof PairList ) {
die "SimplePath assignment expects string pairlist key"
if not( key instanceof String );
return \ container{(key)};
}
die `SimplePath assignment target container '${typeof container}' is not assignable`;
}
method _apply_assignment_ref ( ref, value, op := ":=", weak := false ) {
die "SimplePath weak assignment is not supported"
if weak;
if ( op ≡ ":=" ) {
return ref(value);
}
let current := ref();
if ( op ≡ "+=" ) {
current += value;
}
else if ( op ≡ "-=" ) {
current -= value;
}
else if ( op ≡ "*=" or op ≡ "×=" ) {
current *= value;
}
else if ( op ≡ "/=" or op ≡ "÷=" ) {
current /= value;
}
else if ( op ≡ "**=" ) {
current **= value;
}
else if ( op ≡ "_=" ) {
current _= value;
}
else if ( op ≡ "?:=" ) {
current ?:= value;
}
else if ( op ≡ "~=" ) {
current ~= value[0] -> value[1](m);
}
else {
die `Unsupported path assignment operator '${op}'`;
}
ref(current);
return current;
}
method _assign_all_result ( value, op, last_result ) {
return op ≡ "~=" ? last_result : value;
}
method assign_first ( target, value, op := ":=", weak := false ) {
let nodes := self._evaluate_nodes(target);
die "SimplePath assignment found no matches"
if nodes.length() ≡ 0;
return self._apply_assignment_ref(
self._ref_for_node( nodes[0] ),
value,
op,
weak,
);
}
method assign_all ( target, value, op := ":=", weak := false ) {
let nodes := self._evaluate_nodes(target);
if ( nodes.length() ≡ 0 ) {
return self._assign_all_result( value, op, value );
}
let i := 0;
let last_result := value;
while ( i < nodes.length() ) {
last_result := self._apply_assignment_ref(
self._ref_for_node( nodes[i] ),
value,
op,
weak,
);
i++;
}
return self._assign_all_result( value, op, last_result );
}
method assign_maybe ( target, value, op := ":=", weak := false ) {
let nodes := self._evaluate_nodes(target);
if ( nodes.length() ≡ 0 ) {
return false;
}
self._apply_assignment_ref(
self._ref_for_node( nodes[0] ),
value,
op,
weak,
);
return true;
}
method ref_first ( target ) {
let nodes := self._evaluate_nodes(target);
die "SimplePath assignment found no matches"
if nodes.length() ≡ 0;
return self._ref_for_node( nodes[0] );
}
method ref_all ( target ) {
let nodes := self._evaluate_nodes(target);
let out := [];
for ( let node in nodes ) {
out.push( self._ref_for_node(node) );
}
return out;
}
method ref_maybe ( target ) {
let nodes := self._evaluate_nodes(target);
return nodes.length() ≡ 0 ? null : self._ref_for_node( nodes[0] );
}
}
=head1 COPYRIGHT AND LICENCE
B<< std/path/simple >> is copyright Toby Inkster.
It is free software; you may redistribute it and/or modify it under
the terms of either the Artistic License 1.0 or the GNU General Public
License version 2.
=cut
std/path/simple
Standard Library source code
Tiny JSONPath/XPath-like traversal helper.
Module
- Name
std/path/simple- Area
- Standard Library
- Source
modules/std/path/simple.zzm