Path

TIBET 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)

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() or set() 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-plus.

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