-
Notifications
You must be signed in to change notification settings - Fork 2
Documentation
Since JSONExps are all about matching JSON, the most basic JSONExp is simply a JSON-encoded struct. Here are some valid JSONExps:
{ "foo": "bar", ["Hello", "world!"] }
[true, false, 42, null, 3.141]
Any type of value that can be encoded as JSON can be a JSONExp, and will match any JSON object that is identical in value. It checks "deep equality" - recursively checking the objects for value equality at each level. So { "foo": { "bar": "baz" } }
would match itself, but not { "foo": { "just": "kidding" } }
.
For Arrays, order matters. [1, 2, 3]
will match itself, but not [3, 1, 2]
.
For Objects, order does not matter.
Other types of values - the primitives - cannot currently be used at the top-level. While strings, bools, null and numerics can appear as an element of an array, or as values of an object, a JSONExp cannot match simple primitives. This is, however, planned for a future release.
One of the useful things in RegExps is to be able to capture the result of a certain match. In JSONExp, you can replace any value with a binding. The syntax for a binding is anything alphanumeric surrounded by square brackets.
{ "first_name": [FIRST_NAME], "last_name": [lastName], "middle_name": [_name2], "gender": [s] }
This may seem cumbersome. Why not simply ask for object.last_name
, rather than var match = exp(object), match.last_name
? The trick is that you can also pass in the value of bindings:
var match1 = exp(obj, {s:"male"}) // `obj.gender == "male"`
var match2 = exp(obj, {lastName:"Tesla"}) // `obj.last_name == "Tesla"`
There may be many use cases for this. The two most obvious are parameterization and shared bindings.
By using bindings, we can create a single expression that can be used to match different predicates easily. This could be used, for instance, to declaratively convert a set of user inputs for well-known attributes to the underlying object format.
var exp = JSONExp('{ "name": [name], "location": { "zipCode": [zip], "city": [city], "state": [state] } }')
exp(person, { zip: 11044 })
var match = exp(person, { name: "Winston Churchill" })
This allows us to simply drop the user input fields into the query. This solves a similar purpose to parameterization of SQL from the purpose of separation of the query from the data representation. If, for example, the format of the user object changes, we need only change the JSONExp once to match the new format. It's also much cleaner and more convenient than splicing the user input into the query.
In LiterAPI, we want to keep bindings static for the lifetime of a story. In this case, the bindings are shared between different calls of different JSONExps. Continuing the previous story:
var barExp = JSONExp('{ "name": [bar_name], "zipCode": [zip] }')
var match = exp(bar, match)
This would match a bar where the zip matches the zip from the previously-matched user, a little bit like a SQL JOIN
. In a new match, bindings will either capture (if they are previously unset, and now found), match (if they were previously captured with the same value), or fail (if they were previously captured with a different value).
There are currently two types of globs, which in a future version of JSONExp will be unified.
The wildcard, *
, is simple: it can match any value. It's like a binding that doesn't capture anything.
{ "name": "Alex", age: 30, "gender": *, "location": * } // match any age-30 Alex, regardless of gender or location
In this example, the keys "gender" and "location" must be present, but may take any value (including null
).
The ellipsis, ...
is for matching an arbitrary number of key-value pairs:
{ "rating": 5, ... } // match any element with rating 5, regardless of other attributes
New in 0.0.3
The ellipsis can also be used to match arbitrary elements at the beginning or the end of an Array (but not both!).
[1, 2, 3, ...] // match any Array beginning with 1, 2, 3
[..., "the end"] // match any Array ending with "the end"
[...] // match any Array at all
Currently, there is no way to match an array that only contains a known element, or known elements — only arrays by prefix and suffix.
Further, it should be unnecessary to use multiple different globbing syntaxes. This is the result of how JSONExp is implemented at this early stage (namely, as a pipeline of regex replacements, JSON.parse
, and a recursive matcher). In the future, there will be only one glob that can be used.
Given the inspiration from regexes, a likely future syntax could be that .
matches any value, ?
matches 0 or 1 of the preceding expression, *
matches 0 or more, and +
matches 1 or more.
It might look something like this:
{ "title": ., "comments": [{ "title": ., .* }*], "authors": [{ "name": ., .* }+], ("private": [secret])? }