Queries & Paths
Wins
- De-facto standard query format for JSON content access (JSON Path)
- Industry-standard query format for XML content access (XPath 1.0)
- Powerful/extensible query format for JavaScript objects (TIBET paths)
Contents
Concepts
Cookbook
Path Construction
Path Options
Common Paths
Code
Concepts
Access paths are essentially queries, a way of defining a traversal that should be undertaken on a piece of data to arrive at the desired result set.
In most cases the only thing you need to be aware of to leverage a path/query in TIBET is the syntax of the particular query language you want to use.
In TIBET there are three primary data formats that can be queried using the
current set of TP.path.AccessPath
subtypes: JSON, XML, and JavaScript objects.
TIBET automatically creates and invokes instances of TP.path.AccessPath
subtype to process most queries. In particular it will create paths
automatically when:
- a URL includes an XPointer such as
#xpath(...)
,#jpath(...)
, or#tibet(...)
. - a
get()
orset()
call includes a "path character" such as/
,.
,[
, etc.
Here are two examples of using a JSON path to access data. One is formulated as an XPointer query to content coming in from a JSON file on a server.
// URL
target = TP.uc('~app_dat/sourcedoc.json#jpath($.foo.bar[1])');
// get/set
target = myObject.get('$.foo.bar[1]');
In both cases above TIBET will create an access path instance from the provided path string. The specific subtype of path depends on a number of factors and TIBET will try its best to "guess" the proper type based on the target data and the syntax of the path itself.
Constructing paths
Constructing paths is done either explicitly or implicitly, if the path is a
simple String supplied to a get()
or set()
method.
Different types of paths are used to query different data sources. To explictly construct a path, TIBET provides convenience methods.
TIBET paths
TIBET paths are used to query regular JavaScript objects in a TIBET application. Let's start by assuming this object structure within JavaScript:
// Create a JavaScript object from JSON (a slightly customized
// version of 'JSON.parse'). This will create a complete JavaScript
// object structure.
model = TP.json2js('{"foo":["1st","2nd",{"hi":"there"}]}');
We can query this object by using an explicitly constructed TIBET path:
// TIBET Path Construct
query = TP.tpc('foo.2.hi');
Now, we use that to get()
that result:
model.get(query); -> 'there'
And we use that to set()
that result:
model.set(query, 'buddy');
model.asJSONSource(); // -> {"foo":["1st","2nd",{"hi":"buddy"}]}
JSON paths
JSON paths are used to query content objects that contain JSON content that is being contained as a 'bag' of data and whose members aren't being referenced individually.
Let's start by assuming this JSON structure within JavaScript:
model = TP.core.JSONContent.construct(
'{"value":[' +
'{"fname":"january", "lname":"smith", "aliases":["jan", "j", "janny"]},' +
'{"fname":"august", "lname":"jones"},' +
'{"fname":"november", "lname":"white"},' +
'{"fname":"june", "lname":"cleaver"}' +
']}');
We can query this object by using an explicitly constructed JSON path:
// JSON Path Construct
query = TP.jpc('$.value[0].fname');
Now, we use that to get()
that result:
model.get(query); -> 'january'
And we use that to set()
that result:
model.set(query, 'march');
model.asJSONSource(); // -> {"value":[{"fname":"march","lname":"smith","aliases":["jan","j","janny"]},{"fname":"august","lname":"jones"},{"fname":"november","lname":"white"},{"fname":"june","lname":"cleaver"}]}
XPath paths
XPath paths are used to query content objects that contain XML content that is being contained as a 'bag' of data and whose members aren't being referenced individually.
Let's start by assuming this XML structure within JavaScript:
// The TP.tpdoc() call creates a TP.dom.DocumentNode wrapped #document node.
model = TP.tpdoc('<emp><lname>Jones</lname><age>47</age></emp>');
We can query this object by using an explicitly constructed XPath path:
// XPath Path Construct
query = TP.xpc('/emp/lname');
Now, we use that to get()
that result:
// The "get('value')" gets the value (i.e. innerText) of the returned element
model.get(query); -> 'Jones'
And we use that to set()
that result:
model.set(query, 'Smith');
model.asString(); // -> <?xml version=\"1.0\"?>\n<emp xmlns:tibet=\"http://www.technicalpursuit.com/1999/tibet\" tibet:globaldocid=\"document_1f4it3bha6urka2jsevg#document\"><lname>Jones</lname><age>47</age></emp>
Abstract path construction.
TIBET can try to automatically determine the kind of path you intend by trying to detect the path's characters and construct the correct path from that.
You can use this feature of TIBET by using the abstract path constructor,
TP.apc()
(TIBET Abstract path Construct).
JSON Path queries almost always begin with $.
, XPath queries with /
, and
typical "TIBET paths" with an initial JavaScript attribute name.
Here are some examples of using TP.apc()
that resolve to TIBET paths (the same
as using the specific TP.tpc()
constructor):
TP.apc('foo');
TP.apc('1');
TP.apc('.');
TP.apc('foo.hi');
TP.apc('foo.hi.boo');
TP.apc('2.1');
TP.apc('2.1.2');
TP.apc('foo.hi[boo,moo]');
TP.apc('foo.hi[boo,moo].gar');
TP.apc('2[1,2]');
TP.apc('[0:2]');
TP.apc('[:2]');
TP.apc('[2:]');
TP.apc('[:-2]');
TP.apc('[2:-1]');
TP.apc('[1:6:2]');
TP.apc('[6:1:-2]');
TP.apc('foo.1');
TP.apc('[0,2].fname');
TP.apc('0.aliases[1:2]');
TP.apc('0.aliases[:-1]');
TP.apc('3.1[1:4]');
Here are some examples of using TP.apc()
that resolve to JSONPath paths (the
same as using the specific TP.jpc()
constructor):
TP.apc('$.store.book[*].author');
TP.apc('$..author');
TP.apc('$.store.*');
TP.apc('$.store..price');
TP.apc('$.store..price.^');
TP.apc('$..book[2]');
TP.apc('$..book[(@.length-1)]');
TP.apc('$..book[:-1]');
TP.apc('$..book[:2]');
TP.apc('$..book[1:2]');
TP.apc('$..book[-2:]');
TP.apc('$..book[2:]');
TP.apc('$..book[?(@.isbn)]');
TP.apc('$..book[?(@.price < 10)]');
TP.apc('$..book[?(@.isbn && @.price < 10)]');
TP.apc('$..book[?(@.isbn || @.price < 10)]');
TP.apc('$..*');
TP.apc('$.');
TP.apc('$.store');
TP.apc('$.children[0].^');
TP.apc('$.store.book[*].reviews[?(@.nyt == @.cst)].^.title');
Here are some examples of using TP.apc()
that resolve to XPath paths (the same
as using the specific TP.xpc()
constructor):
TP.apc('/author');
TP.apc('./author');
TP.apc('/author/lname');
TP.apc('/author/lname|/author/fname');
TP.apc('/author/lname/@foo');
TP.apc('/author/lname/@foo|/author/fname/@baz');
TP.apc('//*');
TP.apc('//author');
TP.apc('.//author');
TP.apc('book[/bookstore/@specialty=@style]');
TP.apc('author/*');
TP.apc('author/first-name');
TP.apc('bookstore//title');
TP.apc('bookstore/*/title');
TP.apc('*/*');
TP.apc('/bookstore//book/excerpt//author');
TP.apc('./*[@foo]');
TP.apc('./@foo');
TP.apc('bookstore/@foo');
TP.apc('bookstore/@foo/bar');
TP.apc('./bookstore[name][2]');
TP.apc('@*');
TP.apc('@foo:*');
TP.apc('*/bar[@foo]');
TP.apc('/goo/bar[@foo]');
TP.apc('/goo/bar[@foo="baz"]');
TP.apc('//foo[text()=../../following-sibling::*//foo/text()]');
TP.apc('./foo:*');
Composite paths
You can use TIBET Paths to access data that cross boundaries of different data types. For instance, let's say you have a JavaScript object that has an attribute containing a JSON content object and a piece of XML content:
TP.lang.Object.defineSubtype('core.MyDataHolder');
TP.core.MyDataHolder.Inst.defineAttribute('jsondata');
TP.core.MyDataHolder.Inst.defineAttribute('xmldata');
model = TP.core.MyDataHolder.construct();
model.set('jsondata', TP.core.JSONContent.construct(
'{"value":[' +
'{"fname":"january", "lname":"smith", "aliases":["jan", "j", "janny"]},' +
'{"fname":"august", "lname":"jones"},' +
'{"fname":"november", "lname":"white"},' +
'{"fname":"june", "lname":"cleaver"}' +
']}'));
model.set('xmldata', TP.tpdoc('<emp><lname>Jones</lname><age>47</age></emp>'));
It's now easy to access parts of the JSON data using a composite path. Composite
paths segment parts of different kinds of paths using parentheses ((
and )
).
To access the JSON data, we start with a (simple) TIBET path - jsondata
- and
then add a JSON path wrapped in parentheses:
// This path is configured to collapse any single results - see below
// for more info.
path = TP.apc('jsondata.($.value[0].fname)', TP.hc('shouldCollapse', true));
model.get(path); // -> 'january'
Note here how we use abstract path construction (i.e. the TP.apc()
call) to
build composite paths. This because we're mixing two different kinds of paths
here so specific path constructors will throw errors if we try to use to
construct a composite path.
Using Paths With Get And Set
All objects in TIBET come with standard get()
and set()
methods for getting
and setting values of objects. See Encapsulation
for more information.
It is possible to construct and pass path objects to get()
and set()
for
more sophisticated retrieval of data within an object (as we've seen so far). In
addition to this capability, we can also use String paths with get()
and
set()
and those methods will automatically detect that you are giving them a
path and will convert it, using the same conversion rules that abstractly
constructed paths follow:
model.get('jsondata.($.value[0].fname)');
Unlike path objects, there is no way to configure String paths supplied to
get()
and set()
with configuration values. So it's useful to know what the
defaults are:
If you want to override one of these defaults, you will have to explictly create
a path object and supply that to the get()
and set()
call.
Path Configuration
Paths have several interesting features that go beyond simple query ability.
They can be configured to return their final result using a particular get()
attribute name, to 'collapse' single item Arrays into that item, to package the
final result into a particular result type or to 'build out' object structure as
they traverse.
Collapsing Single Item Arrays
A common need when writing your applications in TIBET is the need to 'collapse' single-item results when they are returned to you.
mySingleItemArray = TP.ac('foo');
mySingleItemArray.collapse(); // -> 'foo'
Path 'get operations will 'auto collapse' by default which means that when a
result set would be an Array with a single item, that single item itself will be
returned. This also means that, if the path didn't find any results, null
is
returned.
You can force this behavior by providing the 'shouldCollapse' flag to the path construction call:
// The results of this path will always be an Array, even an empty
// one if nothing could be found.
path = TP.apc('jsondata.($.value[0].address)', TP.hc('shouldCollapse', false));
An interesting side effect of this behavior is that when the path cannot find
any results and shouldCollapse
is false, an empty Array (not null) is
returned.
Using Paths To Build Data Structure
Paths can be instructed to force creation of the data structure at any intermediate steps. When used with data binding, this feature allows you to bind an empty XML or JSON structure to a form or other construct and have it build out the entire XML or JSON tree as it is queried, simplifying authoring of forms and other features.
Let's start with a JSON data structure and a path that queries for 'cleaver' as a last name and use that to set her first name to 'suzy':
model = TP.core.JSONContent.construct(
'{"value":[' +
'{"fname":"january", "lname":"smith", "aliases":["jan", "j", "janny"]},' +
'{"fname":"august", "lname":"jones"},' +
'{"fname":"november", "lname":"white"},' +
'{"fname":"june", "lname":"cleaver"}' +
']}');
path = TP.jpc('$.value[?(@.lname == "cleaver")].fname');
model.get(path); // -> 'june'
model.set(path, 'suzy');
model.get(path); // -> 'suzy'
Now, let's alter the path to set a new field that doesn't exist in the record. Because the structure doesn't exist, the path won't set anything:
path = TP.jpc('$.value[?(@.lname == "cleaver")].address.street');
model.get(path); // -> null
model.set(path, '111 Main Street');
model.get(path); // -> null
Now, let's configure the path with buildout
set to true and use that to set
the street name:
path = TP.jpc('$.value[?(@.lname == "cleaver")].address.street', TP.hc('buildout', true));
model.get(path); // -> null
model.set(path, '111 Main Street');
model.get(path); // -> '111 Main Street'
If we ask the model for the source, we can see that it has 'built out' new structure matching our path expression so that it can set the street address:
model.asJSONSource(); // -> {"value":[{"fname":"january","lname":"smith","aliases":["jan","j","janny"]},{"fname":"august","lname":"jones"},{"fname":"november","lname":"white"},{"fname":"suzy","lname":"cleaver","address":{"street":"111 Main Street"}}]}
This 'buildout' capability also works with TIBET paths and XMLPath paths, including generating the new objects or elements & attributes required, depending on the type of path.
Extracting A Final Result
Sometimes, retrieving a result involves running a particular get()
against the
path's result to produce the final result. For instance, invoking get('value')
for some objects in TIBET will return a 'more primitive' or 'deconstructed'
object. You can configure a path using the extractWith
property to do this:
path = TP.apc('jsondata.($.value[0].fname)', TP.hc('extractWith', 'value'));
This value can also be a Function that accepts a value and returns the transformed value for maximum flexibility and power:
path = TP.apc('jsondata.($.value[0].fname)', TP.hc('extractWith',
function(aVal) {return aVal + ' fluffy';}));
Packaging The Result
Sometimes you will want to package the result into an object of a particular
type before returning it. You can configure a path using the packageWith
property to do this:
// Using a type object
path = TP.apc('jsondata.($.value[0].address)', TP.hc('packageWith',
APP.MyApp.AddressData);
// OR
// Using a type name
path = TP.apc('jsondata.($.value[0].address)', TP.hc('packageWith',
'APP.MyApp.AddressData');
This value can also be a Function that returns the type or type name for maximum flexibility and power:
// Using a type object
path = TP.apc('jsondata.($.value[0].address)', TP.hc('packageWith',
function(aVal) {return APP.MyApp.AddressData;}));
// OR
// Using a type name
path = TP.apc('jsondata.($.value[0].address)', TP.hc('packageWith',
function(aVal) {return 'APP.MyApp.AddressData';}));
Fallback Values
You can also configure paths to supply a 'default value' when the path doesn't
produce any results. You can configure a path using the fallbackWith
property
to do this. It will be a Function that returns the fallback value:
// 'fluffy' is the canonical fallback value :-)
path = TP.apc('jsondata.($.value[0].fname)', TP.hc('fallbackWith',
function(aVal) {return 'fluffy';}));
Query Syntax
TIBET Paths
TIBET path syntax is effectively just a simple dot-separated path syntax
(foo.bar.baz
) augmented with the ability to use Python-style
slicing syntax.
Retrieving a single item using keys
model = TP.json2js('{"foo":{"hi":{"boo":"goo","moo":"too"}}}');
path = TP.tpc('foo.hi.boo');
model.get(path); // -> 'goo'
Retrieving a single item using numeric index
model = TP.json2js('["one", "two", ["a", ["6", "7", "8"], "c"]]');
path = TP.tpc('2.1.2');
model.get(path); // -> '8'
Retrieving multiple items using keys
model = TP.json2js('{"foo":{"hi":{"boo":{"gar":"bar"},"moo":{"gar":"tar"}}}}');
path = TP.tpc('foo.hi[boo,moo].gar');
model.get(path); // -> ['bar', 'tar']
Retrieving multiple items using numeric indices
model = TP.json2js('["one", "two", ["a", ["6", "7", "8"], "c"]]');
path = TP.tpc('2[1,2].2');
model.get(path); // -> [6, 6]
Retrieving multiple items using slicing
model = TP.json2js('["one", "two", ["a", ["6", "7", "8"], "c"], 37, "hi"]');
path = TP.tpc('[0:2]');
model.get(path); // -> ['one', 'two']
path = TP.tpc('[:2]');
model.get(path); // -> ['one', 'two']
path = TP.tpc('[2:]');
model.get(path); // -> [["a", ["6", "7", "8"], "c"], 37, "hi"]';
path = TP.tpc('[-2:]');
model.get(path); // -> [37, 'hi']
path = TP.tpc('[:-2]');
model.get(path); // -> ["one", "two", ["a", ["6", "7", "8"], "c"]
path = TP.tpc('[2:-1]');
model.get(path); // -> [["a", ["6", "7", "8"], "c"], 37]';
path = TP.tpc('[1:6:2]');
model.get(path); // -> ['two', 37]
path = TP.tpc('[6:1:-2]');
model.get(path); // -> [undefined, 37]
Complex retrieval using a combination names, indices and slicing
model = TP.json2js(
'[' +
'{"fname":"january", "lname":"smith", "aliases":["jan", "j", "janny"]},' +
'{"fname":"august", "lname":"jones"},' +
'{"fname":"november", "lname":"white"},' +
'{"fname":"june", "lname":"cleaver"}' +
']');
path = TP.tpc('0.fname');
model.get(path); // -> 'january'
path = TP.tpc('[0,2].fname');
model.get(path); // -> ['january', 'november']
path = TP.tpc('0.aliases[1:2]');
model.get(path); // -> 'j'
path = TP.tpc('0.aliases[:-1]');
model.get(path); // -> ['jan', 'j']
JSON Paths
For JSON queries see the documentation on the jsonpath npm
module.
Retrieving a single item using keys
model = TP.core.JSONContent.construct(
'{"foo":{"hi":{"boo":"goo","moo":"too"}}}');
path = TP.jpc('$.foo.hi.boo');
model.get(path); // -> 'goo'
Retrieving a single item using numeric index
model = TP.core.JSONContent.construct(
'{"value":["one", "two", ["a", ["6", "7", "8"], "c"]]}');
path = TP.jpc('$.value[2][1][2]');
model.get(path); // -> 8
Retrieving multiple items using keys
model = TP.core.JSONContent.construct(
'{"foo":{"hi":{"boo":{"gar":"bar"},"moo":{"gar":"tar"}}}}');
path = TP.jpc('$.foo.hi[\'boo\',\'moo\'].gar');
model.get(path); // -> ["bar", "tar"]
Retrieving multiple items using numeric indices
model = TP.core.JSONContent.construct(
'{"value": ["one", "two", ["a", ["6", "7", "8"], "c"]]}');
path = TP.jpc('$.value[2][1,2]');
model.get(path); // -> [["6", "7", "8"], "c"]
Retrieving multiple items using slicing
model = TP.core.JSONContent.construct(
'{"value": ["one", "two", ["a", ["6", "7", "8"], "c"], 37, "hi"]}');
path = TP.jpc('$.value[0:2]');
model.get(path); -> ['one', 'two']
path = TP.jpc('$.value[:2]');
model.get(path); -> ['one', 'two']
path = TP.jpc('$.value[2:]');
model.get(path); -> [["a", ["6", "7", "8"], "c"], 37, "hi"]';
path = TP.jpc('$.value[-2:]');
model.get(path); -> [37, 'hi']
path = TP.jpc('$.value[:-2]');
model.get(path); -> ["one", "two", ["a", ["6", "7", "8"], "c"]
path = TP.jpc('$.value[2:-1]');
model.get(path); -> [["a", ["6", "7", "8"], "c"], 37]';
Complex retrieval using a combination names, indices and slicing
model = TP.core.JSONContent.construct(
'{"value":[' +
'{"fname":"january", "lname":"smith", "aliases":["jan", "j", "janny"]},' +
'{"fname":"august", "lname":"jones"},' +
'{"fname":"november", "lname":"white"},' +
'{"fname":"june", "lname":"cleaver"}' +
']}');
path = TP.jpc('$.value[0].fname');
model.get(path); // -> 'january'
path = TP.jpc('$.value[0,2].fname');
model.get(path); // -> ['january', 'november']
path = TP.jpc('$.value[0].aliases[1:2]');
model.get(path); // -> 'j'
path = TP.jpc('$.value[0].aliases[:-1]');
model.get(path); // -> ['jan', 'j']
XPath Paths
TIBET supports standard XPath syntax for use with XML. The XPath language is quite powerful and complex and so the best place to find examples of XML path syntax is see the documentation here:
XPath 1.0. Google XPath 1.0 Examples
Let's a take a look at a few simple examples.
Retrieving elements
model = TP.tpdoc('<emp><lname>Jones</lname><age>47</age></emp>');
path = TP.xpc('/emp/lname|/emp/age');
model.set(path, 'fluffy');
model.get(path); // -> [<lname>fluffy</lname>, <age>fluffy</age>]
Set elements
model = TP.tpdoc('<emp><lname>Jones</lname><age>47</age></emp>');
path = TP.xpc('/emp/lname|/emp/age');
model.get(path); // -> [<lname>Jones</lname>, <age>47</age>]
Retrieving attributes
model = TP.tpdoc('<emp><lname foo="bar">Jones</lname><age baz="goo">47</age></emp>');
path = TP.apc('/emp/lname/@foo|/emp/age/@baz');
model.get(path); // -> [foo="Jones", baz="47"] (these are Attribute nodes)
Set attributes
model = TP.tpdoc('<emp><lname foo="bar">Jones</lname><age baz="goo">47</age></emp>');
path = TP.apc('/emp/lname/@foo|/emp/age/@baz');
model.set(path, 'fluffy');
model.get(path); // -> [foo="fluffy", baz="fluffy"] (these are Attribute nodes)
Cookbook
See ~lib/test/src/tibet/databinding/TP.core.DataPath_Tests.js
for examples of
a wide variety of paths and queries.
Path Construction
Create A Path
// create a path, letting TIBET determine the specific subtype:
path = TP.apc(somepath);
Create A Specific Type Of Path
// create a specific subtype by invoking the proper construct call:
path = TP.path.ComplexTIBETPath.construct(somepath);
Use A Constructed Path
// pass the path and get the result of the traversal:
data = sourceObject.get(path);
Path Options
Force Collapse Of Result Sets
path = TP.apc(somepath);
path.set('shouldCollapse', true);
Force Creation Along A Path
path = TP.apc(somepath);
path.set('buildout', true);
Common Paths
Referencing the object itself in a path
It is possible to reference the receiving object in a path by using the period
.
character:
path = TP.apc('.');
myObj = TP.lang.Object.construct();
myObj.get(path) === myObj; // -> true
This can be useful in more complex paths that have sophisticated querying needs.
Code
Numerous path tests (and syntax examples) can be found in TIBET's test
files, in particular
~lib/test/src/tibet/databinding/TP.core.DataPath_Tests.js
.
JSONPath support in TIBET is encapsulated within TP.path.JSONPath
type in
~lib/src/tibet/kernel/TIBETContentTypes.js
. The underlying implementation
is provided by the common npm
module jsonpath
.
TIBET's "custom path" support is implemented in the
TP.path.SimpleTIBETPath
and TP.path.ComplexTIBETPath
types in
~lib/src/tibet/kernel/TIBETContentTypes.js
Support for XPath 1.0 is built in to all major browsers. TIBET leverages this
infrastructure via the TP.path.XMLPath
type in
~lib/src/tibet/kernel/TIBETContentTypes.js