Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow addition of custom functions via decorate() #61

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
node_modules
/yarn.lock
package-lock.json
.idea
6 changes: 2 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
language: node_js
node_js:
- "0.12"
- "0.11"
- "0.10"
- "iojs"
- "12"
- "10"
63 changes: 52 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ through a JMESPath expression.
Using jmespath.js is really easy. There's a single function
you use, `jmespath.search`:

```js
var jmespath = require('jmespath');
jmespath.search({foo: {bar: {baz: [0, 1, 2, 3, 4]}}}, "foo.bar.baz[2]")

```
> var jmespath = require('jmespath');
> jmespath.search({foo: {bar: {baz: [0, 1, 2, 3, 4]}}}, "foo.bar.baz[2]")
2
// output = 2
```

In the example we gave the ``search`` function input data of
Expand All @@ -25,20 +25,61 @@ the expression against the input data to produce the result ``2``.
The JMESPath language can do a lot more than select an element
from a list. Here are a few more examples:

```
> jmespath.search({foo: {bar: {baz: [0, 1, 2, 3, 4]}}}, "foo.bar")
{ baz: [ 0, 1, 2, 3, 4 ] }
```js
jmespath.search({foo: {bar: {baz: [0, 1, 2, 3, 4]}}}, "foo.bar")

> jmespath.search({"foo": [{"first": "a", "last": "b"},
// { baz: [ 0, 1, 2, 3, 4 ] }

jmespath.search({"foo": [{"first": "a", "last": "b"},
{"first": "c", "last": "d"}]},
"foo[*].first")
[ 'a', 'c' ]

> jmespath.search({"foo": [{"age": 20}, {"age": 25},
// [ 'a', 'c' ]

jmespath.search({"foo": [{"age": 20}, {"age": 25},
{"age": 30}, {"age": 35},
{"age": 40}]},
"foo[?age > `30`]")
[ { age: 35 }, { age: 40 } ]

// [ { age: 35 }, { age: 40 } ]
```

## Adding custom functions

Custom functions can be added to the JMESPath runtime by using the
`decorate()` function:

```js
var TYPE_NUMBER = 0;
function customFunc(resolvedArgs) {
return resolvedArgs[0] + 99;
}
var extraFunctions = {
custom: {_func: customFunc, _signature: [{types: [TYPE_NUMBER]}]},
};
jmespath.decorate(extraFunctions);
```

The value returned by the decorate function is a curried function
(takes arguments one at a time) that takes the search expression
first and then the data to search against as the second parameter:

```js
var value = jmespath.decorate(extraFunctions)('custom(`1`)')({})
// value = 100
```

Because the return value from `decorate()` is a curried function
the result of compiling the expression can be cached and run
multiple times against different data:

```js
var expr = jmespath.decorate({})('a');
// expr is now a cached compiled version of the search expression
var value = expr({ a: 1 });
assert.strictEqual(value, 1);
value = expr({ a: 2 });
assert.strictEqual(value, 2);
```

## More Resources
Expand Down
4 changes: 2 additions & 2 deletions artifacts/jmespath.min.js

Large diffs are not rendered by default.

39 changes: 35 additions & 4 deletions jmespath.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@
var TOK_RBRACE = "Rbrace";
var TOK_NUMBER = "Number";
var TOK_CURRENT = "Current";
var TOK_ROOT = "Root";
var TOK_EXPREF = "Expref";
var TOK_PIPE = "Pipe";
var TOK_OR = "Or";
Expand Down Expand Up @@ -199,7 +200,8 @@
"]": TOK_RBRACKET,
"(": TOK_LPAREN,
")": TOK_RPAREN,
"@": TOK_CURRENT
"@": TOK_CURRENT,
"$": TOK_ROOT,
};

var operatorStartToken = {
Expand All @@ -212,6 +214,7 @@
var skipChars = {
" ": true,
"\t": true,
"\r": true,
"\n": true
};

Expand Down Expand Up @@ -481,6 +484,7 @@
bindingPower[TOK_RBRACE] = 0;
bindingPower[TOK_NUMBER] = 0;
bindingPower[TOK_CURRENT] = 0;
bindingPower[TOK_ROOT] = 0;
bindingPower[TOK_EXPREF] = 0;
bindingPower[TOK_PIPE] = 1;
bindingPower[TOK_OR] = 2;
Expand Down Expand Up @@ -602,6 +606,8 @@
return this._parseMultiselectList();
case TOK_CURRENT:
return {type: TOK_CURRENT};
case TOK_ROOT:
return {type: TOK_ROOT};
case TOK_EXPREF:
expression = this.expression(bindingPower.Expref);
return {type: "ExpressionReference", children: [expression]};
Expand Down Expand Up @@ -805,7 +811,7 @@
right = this._parseDotRHS(rbp);
} else {
var t = this._lookaheadToken(0);
var error = new Error("Sytanx error, unexpected token: " +
var error = new Error("Syntax error, unexpected token: " +
t.value + "(" + t.type + ")");
error.name = "ParserError";
throw error;
Expand Down Expand Up @@ -863,6 +869,7 @@

TreeInterpreter.prototype = {
search: function(node, value) {
this._rootValue = value;
return this.visit(node, value);
},

Expand Down Expand Up @@ -1060,6 +1067,8 @@
return this.visit(node.children[1], left);
case TOK_CURRENT:
return value;
case TOK_ROOT:
return this._rootValue;
case "Function":
var resolvedArgs = [];
for (i = 0; i < node.children.length; i++) {
Expand Down Expand Up @@ -1654,19 +1663,41 @@
}

function search(data, expression) {
return decorate({})(expression)(data);
}

function decorate(fns) {
var parser = new Parser();
// This needs to be improved. Both the interpreter and runtime depend on
// each other. The runtime needs the interpreter to support exprefs.
// There's likely a clean way to avoid the cyclic dependency.
var runtime = new Runtime();
Object.assign(runtime.functionTable, fns);
var interpreter = new TreeInterpreter(runtime);
runtime._interpreter = interpreter;
var node = parser.parse(expression);
return interpreter.search(node, data);
return function (expression) {
var node = parser.parse(expression);
return function (data) {
return interpreter.search(node, data);
};
};
}

exports.tokenize = tokenize;
exports.compile = compile;
exports.search = search;
exports.decorate = decorate;
exports.strictDeepEqual = strictDeepEqual;
exports.types = {
TYPE_NUMBER: 0,
TYPE_ANY: 1,
TYPE_STRING: 2,
TYPE_ARRAY: 3,
TYPE_OBJECT: 4,
TYPE_BOOLEAN: 5,
TYPE_EXPREF: 6,
TYPE_NULL: 7,
TYPE_ARRAY_NUMBER: 8,
TYPE_ARRAY_STRING: 9,
};
})(typeof exports === "undefined" ? this.jmespath = {} : exports);
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
"homepage": "https://github.com/jmespath/jmespath.js",
"contributors": [],
"devDependencies": {
"grunt": "^0.4.5",
"grunt-contrib-jshint": "^0.11.0",
"grunt": "^1.0.4",
"grunt-contrib-jshint": "^2.1.0",
"grunt-contrib-uglify": "^0.11.1",
"grunt-eslint": "^17.3.1",
"mocha": "^2.1.0"
"grunt-eslint": "^22.0.0",
"mocha": "^7.1.0"
},
"dependencies": {},
"main": "jmespath.js",
Expand Down
44 changes: 44 additions & 0 deletions test/jmespath.js
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,47 @@ describe('search', function() {
}
);
});

describe('decorate', function() {
it(
'should call a custom function when called via decorator',
function() {
var TYPE_NUMBER = 0;
function customFunc(resolvedArgs) {
return resolvedArgs[0] + 99;
}
var extraFunctions = {
custom: {_func: customFunc, _signature: [{types: [TYPE_NUMBER]}]},
};
var value = jmespath.decorate(extraFunctions)('custom(`1`)')({});
assert.strictEqual(value, 100);
}
);
it(
'should provide a compiled expression that can be cached and reused',
function() {
var expr = jmespath.decorate({})('a');
var value = expr({ a: 1 });
assert.strictEqual(value, 1);
value = expr({ a: 2 });
assert.strictEqual(value, 2);
}
);
});

describe('root', function() {
it(
'$ should give access to the root value',
function() {
var value = jmespath.search({ foo: { bar: 1 }}, 'foo.{ value: $.foo.bar }');
assert.equal(value.value, 1);
}
);
it(
'$ should give access to the root value after pipe',
function() {
var value = jmespath.search({ foo: { bar: 1 }}, 'foo | $.foo.bar');
assert.strictEqual(value, 1);
}
);
});