Inquiry

Information
        desk at Indianapolis Central Library by Phil Jern

JSON Queries

Inquiry is a JSON path langauge that supports parameters, JavaScript expressions, and sub-queries.

Pre-Compiled

Inquiry is a functional library that compiles a reusable parameterized JavaScript function from a path expression.

< 1K

Inquiry is a tiny library, under 1k minified and gzipped, suitable for use on the server or in the browser.

Synopsis

Select Andrew Jackson from the set of the first sixteen US Presidents.

var $q = require("inquiry"), hickory, object =
  { presidents:
  [ { firstName: "George", lastName:"Washington" }
  , { firstName: "John", lastName:"Adams" }
  , { firstName: "Thomas", lastName:"Jefferson" }
  , { firstName: "James", lastName:"Madison" }
  , { firstName: "James", lastName:"Monroe" }
  , { firstName: "John", lastName:"Quincy Adams" }
  , { firstName: "Andrew", lastName:"Jackson" }
  , { firstName: "Martin", lastName:"Van Buren" }
  , { firstName: "William", lastName:"Henry Harrison" }
  , { firstName: "John", lastName:"Tyler" }
  , { firstName: "James", middleInitial:"K", lastName:"Polk" }
  , { firstName: "Zachary", lastName:"Taylor" }
  , { firstName: "Millard", lastName:"Fillmore" }
  , { firstName: "Franklin", lastName:"Pierce" }
  , { firstName: "James", lastName:"Buchanan" }
  , { firstName: "Abraham", lastName:"Lincoln" }
  ]};

hickory = $q('/p*{$.lastName == $1}')(object, "Jackson").pop();

console.log("Found: " + hickory.firstName + " " + hickory.lastName);

Contributing

One of the goals of Inquiry is to be tiny, so let's talk before submitting Pull Requests for new features. I enjoy recieving help from the community, but I also enjoy being able to say that Inquiry is less than 1k.

Installation

Inquiry is inquiry on NPM.

$ npm install inquiry

Inquiry and also available from GitHub as bigeasy/inquiry.

Getting Help

If you have a general question or need help, please ask me under the release discusssion, it's our make shift forum.

If you have a bug or other issue please open a new issue.

Paths

Paths are forward slash delimited. The path part begins right after the slash and ends at the first unescaped forward slash, open square bracket, open curly brace; /, [, {.

var abe = $q('/presidents/15/firstName')(object).pop();

The initial slash is optional. Paths always begin at the root object.

var abe = $q('presidents/15/firstName')(object).pop();

Paths can have a dot . for self reference and two dots .. to reference the parent just like file paths. Below is a silly example. Parent paths are more useful in predicates and sub-queries.

var abe = $q('presidents/14/../15/./firstName')(object).pop();

When we want to use special characters in our path name, we use a JSON string literal. A JSON string is a double-quoted JavaScript string literal, but only double quoted, not single-quoted.

var object = { 'forward/slash': { '..': 1, 'square[bracket': 2 } };
var a = $q('"forward/slash"/".."')(object).pop();

This allows you to put most things in your paths.

var object = { "don't you love punctuation?": { 'yes!': 1, 'no': 0 } };
var yes = $q("don't you love punctuation?/yes!")(object).pop();

You can use whitespace to make your paths more legible, including tabs and new lines. If you have an unwieldy pattern you can break it up into separate lines.

var abe = $q('                        \
    presidents / 15 / firstName       \
')(object).pop();

If the leading and trailing whitespace is part of the path name, use quotes.

var object = { ' a ': { 'b': 1 } };
equal($q(' " a " / b ')(object).pop(), 1);

If you have new lines or tabs in your path name, use quotes.

var object = { '\na\n': { 'b': 1 } };
equal($q(' "\\na\\n" / b ')(object).pop(), 1);

Use quotes around a path name with a slash /, an asterisk * or if it, the percent sign % which is used for JSON pointer, if it is . or .. entirely, if leading or trailing whitespace is significant, or if it already looks like a JSON string.

If you're dynamically generating paths, just use JSON.stringify and you'll be safe.

var object = { '"a"': { 'b': 1 } };
equal($q(' ' + JSON.stringify('"a"') + ' / b ')(object).pop(), 1);

Keep in mind though, that Inquiry is supposed to read through well-organized object trees. It's a shorthand path language, not a query language.

Invocation

Now that you have a notion of what a path looks like, let's talk about invocation.

Inquiry is a function compiler. Inquiry compiles a path expression into a JavaScript function.

var $q = require('inquiry');

var firstNameByLastName = $q('presidents{$.lastName = $1}/firstName');

ok(firstNameByLastName(object, 'Lincoln').pop() == 'Abraham');
ok(firstNameByLastName(object, 'Washington').pop() == 'George');

Inquiry is all functional like that. You can then call that JavaScript function however many times you'd like. You can pass the function to another function as a parameter.

However, the Inquiry compiler is quick enough that you can simply compile and invoke in a one liner.

var $q = require('inquiry');

var abe = $q('presidents{$.lastName == $1}/firstName')(object, 'Lincoln');
ok(abe == 'Abraham');

One Wildcard Per Property

In a path, you're allowed one and only one wildcard represented by a star *.

Wildcards help to make verbose queries terse.

var instances = $q('reservationSet/reservation/instanceSet/instance')(object);

A few wildcards and the path is under control, but still readable.

var instances = $q('r*Set/reservation/i*Set/instance')(object);

You're only allowed one wildcard per property name. Wildcards are used to tame verbosity, not for pattern matching. There is no good way to pattern match against property names in inquiry. You're expected to know the structure of the JSON you're querying.

JSON Pointer

You can also JSON pointer which is simply URL encoded path parts.

var object = { '@#$%^&': { '>': 0, '%3E': 1 } };
equal($q('%40%23%24%25%5E%26/%3E')(object).pop(), 0, 'encoded');
equal($q('%40%23%24%25%5E%26/`%3E')(object).pop(), 1, 'escaped encoding');

I imagine this might be helpful if you want to add paths to URLs, but I've not found a use case in the wild. If you do find, one drop me a line.

Arrays

Arrays are a special case. When we visit an array, if the path step is all digits, we simply use that path step as an index.

ok( $q('presidents/15')(object).pop().lastName == 'Lincoln' );

If it is not all digits, we assume that we want to gather the property for every element in the array. This gathers values into the result array.

ok( $q('presidents/lastName')(object)[15] == 'Lincoln' );

You can, of course, invoke Inquiry against an array directly. The path will be applied to each element in the array.

ok( $q('lastName')(object.presidents).shift() == 'Washington' );
ok( $q('15/lastName')(object.presidents).shift() == 'Lincoln' );

Note that we do not use brackets [] to indicate an array element. Brackets are used to define sub-query predicates.

JavaScript Predicates

Curly braces indicate JavaScript predicates. A JavaScript predicate is simply a JavaScript expression that is compiled to a function that returns a boolean. If the JavaScript expression that evaluates to true, the path is included in the result set.

Each step in the path can include a single JavaScript predicate.

A predicate expression references the current object using the varaible $.

var abe = $q('{$.lastName == 'Lincoln'}')(object.presidents[15]).pop();

When a predicate expression is used with an array, it is tested against all the members of the array.

var abe = $q('presidents{$.lastName == 'Lincoln'}')(object).pop();

A predicate expression references arguments using the special variables $1 through $256, each variable representing an argument by position.

var abe = $q('presidents{$.lastName == $1}')(object, 'Lincoln').pop();

You can negate a JavaScript predicate using an exclamation point !.

var abe = $q('presidents!{$.lastName != 'Lincoln'}')(object).pop();

A predicate expression can reference the index of an array using the special variable $i.

var abe = $q('presidents{$i == 15}')(object).pop();

When you invoke Inquiry directly against an array, you apply a JavaScript predicate by defining it immediately.

var abe = $q('{$i == 15}')(object.presidents).pop();

Sub-Query Predicates

Square brackets define sub-query predicates. A sub-query predicate is a query that is performed in the context of each object that matches the path, current object or against each object in an array if the object that matches the path is an array. If the sub-query returns any objects at all, the predicate is considered true and the object matches.

var datacenter = {
  instances: [{
    id: 1,
    running: true,
    tags: [{
      key: 'name', value: 'server'
    }, {
      key: 'arch', value: 'i386'
    }]
  }, {
    id: 2,
    tags: [{
      key: 'name', value: 'balancer'
    }, {
      key: 'arch', value: 'x86_64'
    }]
  }, {
    id: 3,
    running: true,
    tags: []
  }]
};
var tagged = $q('instances[tags/key]')(datacenter);
ok(tagged.length == 2);

In the above, for each instance the sub-query looks for tags/key. This matches any of the instance objects who's tags array has an object with a key property, regardless of value.

You can nest JavaScript predicates inside sub-query predicates.

var server = $q('instances[tags{$.key == 'name' && $.value == $1}]')(datacenter, 'server').pop();

In the above, for each instance object we look in the tags array for a name property with the value 'server.'

You can use parent operator .. to compare against a parent in a sub-query predicate, or multiple parent operators ../.. to compare against other ancestors. It's just like .. in file paths.

However, Inquiry will dive into an array so, so to get back out of an array, you need to use ../... Use .. to go to the other array elements and ../.. to go up to the object that contains the array.

If you go up beyond the root, bad things happen. Don't do it.

The following will get the tags of all instances that have a running property.

var tags = $q('instances/tags[../../running]')(instances);
ok(tags.length == 2);

Granted, the above is not terribly useful since it returns the tags, but not the instance itself, so the tags have no context. Really useful queries of parents and siblings require capturing the properties of the object at the current path with the properties of a parent or sibling.

A sub-query predicate can reference the context of query that invoked it using the variable $$. This variable references the context object of the outer query at when the sub-query predicate was invoked. The variable $$i contains the index of the context object of the outer query when the sub-query predicate was invoked.

Here we look for any president that shares a first name with any another president.

var dup = $q('presidents[..{$.firstName == $$.firstName && $i != $$i}]')(object);
ok(dup.length == 7);
ok(dup[dup.length - 1].firstName = 'James');

We compared the first name of the outer president with the first names of all the other presidents, excluding the outer president himself by his index.

Here we look for a president that does not share a first name with any other president.

var uniq = $q('presidents![..{$.firstName == $$.firstName && $i != $$i}]')(object);
ok(uniq.length == 9);
ok(uniq[uniq.length - 1].firstName == 'Abraham');

If you're wondering, yes, you can nest deeper than a single sub-query; a $$$ variable and a $$$i variable will be created.

When you invoke Inquiry directly against an array, you apply a JavaScript predicate by defining it immediately.

var uniq = $q('![..{$.firstName == $$.firstName && $i != $$i}]')(object.presidents);
ok(uniq.length == 9);
ok(uniq[uniq.length - 1].firstName == 'Abraham');

Errors

Inquiry is a minimal path language for maximum effect. Error reporting is minimal, too. If you give it a bad pattern, it will raise an exception, but it doesn't offer much in the way of suggestions to fix the pattern. Diagnostics of that sort would be expensive.

Most of your patterns will be simple and obvious, so you're not going to want to pay for the complexity of details diagnostics. If you're having trouble with a complicated pattern, try building it incrementally, adding the bits and pieces to the pattern so you'll see when it breaks.