Skip to content

Commit

Permalink
Merge pull request #84 from TomFrost/evalOnDemand
Browse files Browse the repository at this point in the history
Add eval on demand functionality to binary operators
  • Loading branch information
TomFrost authored May 5, 2020
2 parents fde5a11 + 9020c5d commit ab2233a
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 13 deletions.
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,16 @@ This project adheres to [Semantic Versioning](http://semver.org/).

## [development]

Nothing yet!
### Added

- Binary operators can now be set to evaluate their operands manually, allowing
them to decide if and when to resolve the value of the left or right sides.
See the new `manualEval` option in `jexl.addBinaryOp`.

## Fixed

- The binary operators `&&` and `||` now evaluate the right operand
conditionally, depending on the value of the left.

## [v2.2.2]

Expand Down
17 changes: 11 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const context = {
}

// Filter an array asynchronously...
jexl.eval('assoc[.first == "Lana"].last', context).then(function(res) {
jexl.eval('assoc[.first == "Lana"].last', context).then(function (res) {
console.log(res) // Output: Kane
})

Expand Down Expand Up @@ -49,7 +49,7 @@ await jexl.eval('age > 62 ? "retired" : "working"', context)
// "working"

// Transform
jexl.addTransform('upper', val => val.toUpperCase())
jexl.addTransform('upper', (val) => val.toUpperCase())
await jexl.eval('"duchess"|upper + " " + name.last|upper', context)
// "DUCHESS ARCHER"

Expand Down Expand Up @@ -249,7 +249,7 @@ value. Add them with `jexl.addTransform(name, function)`.

```javascript
jexl.addTransform('split', (val, char) => val.split(char))
jexl.addTransform('lower', val => val.toLowerCase())
jexl.addTransform('lower', (val) => val.toLowerCase())
```

| Expression | Result |
Expand All @@ -267,7 +267,7 @@ traversed just as easily as plain javascript objects:
```javascript
const xml2json = require('xml2json')

jexl.addTransform('xml', val => xml2json.toJson(val, { object: true }))
jexl.addTransform('xml', (val) => xml2json.toJson(val, { object: true }))

const context = {
xmlDoc: `
Expand Down Expand Up @@ -305,7 +305,7 @@ A reference to the Jexl constructor. To maintain separate instances of Jexl
with each maintaining its own set of transforms, simply re-instantiate with
`new jexl.Jexl()`.

#### jexl.addBinaryOp(_{string} operator_, _{number} precedence_, _{function} fn_)
#### jexl.addBinaryOp(_{string} operator_, _{number} precedence_, _{function} fn_, _{boolean} [manualEval]_)

Adds a binary operator to the Jexl instance. A binary operator is one that
considers the values on both its left and right, such as "+" or "==", in order
Expand All @@ -315,6 +315,11 @@ existing operators). The provided function will be called with two arguments:
a left value and a right value. It should return either the resulting value,
or a Promise that resolves to the resulting value.

If `manualEval` is true, the `left` and `right` arguments will be wrapped in
objects with an `eval` function. Calling `left.eval()` or `right.eval()` will
return a promise that resolves to that operand's actual value. This is useful to
conditionally evaluate operands, and is how `&&` and `||` work.

#### jexl.addUnaryOp(_{string} operator_, _{function} fn_)

Adds a unary operator to the Jexl instance. A unary operator is one that
Expand Down Expand Up @@ -377,7 +382,7 @@ pulled out of context:

```javascript
const { expr } = jexl
jexl.addTransform('double', val => val * 2)
jexl.addTransform('double', (val) => val * 2)
const expression = expr`2|double`
console.log(expression.evalSync()) // 4
```
Expand Down
23 changes: 23 additions & 0 deletions __tests__/lib/Jexl.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,29 @@ describe('Jexl', () => {
Promise.all([inst.eval('1 + 2 ** 3 + 4'), inst.eval('1 + 2 *** 3 + 4')])
).resolves.toEqual([20, 15])
})
it('allows binaryOps to be defined with manual operand evaluation', () => {
inst.addBinaryOp(
'$$',
50,
(left, right) => {
return left.eval().then((val) => {
if (val > 0) return val
return right.eval()
})
},
true
)
let count = 0
inst.addTransform('inc', (elem) => {
count++
return elem
})
expect(inst.evalSync('-2|inc $$ 5|inc')).toEqual(5)
expect(count).toEqual(2)
count = 0
expect(inst.evalSync('2|inc $$ -5|inc')).toEqual(2)
expect(count).toEqual(1)
})
})
describe('addUnaryOp', () => {
it('allows unaryOps to be defined', async () => {
Expand Down
9 changes: 7 additions & 2 deletions lib/Jexl.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,17 @@ class Jexl {
* will be called with two arguments: left and right, denoting the values
* on either side of the operator. It should return either the resulting
* value, or a Promise that resolves with the resulting value.
* @param {boolean} [manualEval] If true, the `left` and `right` arguments
* will be wrapped in objects with an `eval` function. Calling
* left.eval() or right.eval() will return a promise that resolves to
* that operand's actual value. This is useful to conditionally evaluate
* operands.
*/
addBinaryOp(operator, precedence, fn) {
addBinaryOp(operator, precedence, fn, manualEval) {
this._addGrammarElement(operator, {
type: 'binaryOp',
precedence: precedence,
eval: fn
[manualEval ? 'evalOnDemand' : 'eval']: fn
})
}

Expand Down
14 changes: 12 additions & 2 deletions lib/evaluator/handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,28 @@ exports.ArrayLiteral = function (ast) {

/**
* Evaluates a BinaryExpression node by running the Grammar's evaluator for
* the given operator.
* the given operator. Note that binary expressions support two types of
* evaluators: `eval` is called with the left and right operands pre-evaluated.
* `evalOnDemand`, if it exists, will be called with the left and right operands
* each individually wrapped in an object with an "eval" function that returns
* a promise with the resulting value. This allows the binary expression to
* evaluate the operands conditionally.
* @param {{type: 'BinaryExpression', operator: <string>, left: {},
* right: {}}} ast An expression tree with a BinaryExpression as the top
* node
* @returns {Promise<*>} resolves with the value of the BinaryExpression.
* @private
*/
exports.BinaryExpression = function (ast) {
const grammarOp = this._grammar[ast.operator]
if (grammarOp.evalOnDemand) {
const wrap = (subAst) => ({ eval: () => this.eval(subAst) })
return grammarOp.evalOnDemand(wrap(ast.left), wrap(ast.right))
}
return this.Promise.all([
this.eval(ast.left),
this.eval(ast.right)
]).then((arr) => this._grammar[ast.operator].eval(arr[0], arr[1]))
]).then((arr) => grammarOp.eval(arr[0], arr[1]))
}

/**
Expand Down
14 changes: 12 additions & 2 deletions lib/grammar.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,22 @@ exports.elements = {
'&&': {
type: 'binaryOp',
precedence: 10,
eval: (left, right) => left && right
evalOnDemand: (left, right) => {
return left.eval().then((leftVal) => {
if (!leftVal) return leftVal
return right.eval()
})
}
},
'||': {
type: 'binaryOp',
precedence: 10,
eval: (left, right) => left || right
evalOnDemand: (left, right) => {
return left.eval().then((leftVal) => {
if (leftVal) return leftVal
return right.eval()
})
}
},
in: {
type: 'binaryOp',
Expand Down

0 comments on commit ab2233a

Please sign in to comment.