bcx-expression-evaluator
Safely evaluate a JavaScript-like expression in given context.
In Buttonwood, we heavily use meta-data (JSON format) to deliver business logic from backend to front-end. We don't want to design a meta-data format too complex to maintain, this tool allows us to define some light logic in pure string, way more flexible than rigid meta-data, much safer and more maintainable than passing js function as string (we did that) from backend to front-end.
This tool was mainly extracted, modified and extended from the expression parser of aurelia-binding.
Install
npm install bcx-expression-evaluator
Document
function evaluate(expression, context, helper, opts)
expression
: the expression string to be evaluatedcontext
: the input model objecthelper
: optional helper objectopts
: optional hashmap, currently only support rejectAssignment
and stringInterpolationMode
rejectAssignment
rejects assignment in expressionstringInterpolationMode
treats the whole expression like if it's in backticks `expression`
function evaluateStringInterpolation
is a short-cut to call evaluate with stringInterpolationMode option.
Usage (in es6 syntax)
import {evaluate, evaluateStringInterpolation} from 'bcx-expression-evaluator';
const context = {
a: 1,
b: 2,
c: {
one: 'one',
two: 'two'
},
avg: function() { return (this.a + this.b) / 2; }
};
evaluate('avg() > a ? c.one : c.two', context);
use some helper
const helper = {
limit: 5,
sum: (v1, v2) => v1 + v2
};
evaluate('sum(a, b) > limit', context, helper);
access context object itself with special $this
variable
evaluate('$this', context);
evaluate('$this.a', context);
explicitly access helper object with special $parent
variable
(carried over from aurelia-binding, might change $parent to $helper in future releases.)
evaluate('a', {a:1}, {a:2});
evaluate('$this.a', {a:1}, {a:2});
evaluate('$parent.a', {a:1}, {a:2});
support es6 string interpolation
evaluate('`${a+1}`', {a:1});
You can evaluate a string interpolation without backtick "`"
evaluate('${a+1}', {a:1}, null, {stringInterpolationMode: true});
evaluateStringInterpolation('${a+1}', {a:1});
You don't have to escape backtick in stringInterpolationMode
evaluate('`\\`${a+1}\\``', {a:1});
evaluate('`${a+1}`', {a:1}, null, {stringInterpolationMode: true});
evaluateStringInterpolation('`${a+1}`', {a:1});
safe. It is not an eval in JavaScript, doesn't have access to global JavaScript objects
evaluate('parseInt(a, 10)', {a:"7"})
evaluate('parseInt(a, 10)', {a:"7"}, {parseInt: parseInt})
silent most of the time
evaluate('a.b', {})
evaluate('a.b || c', {c: 'lorem'})
you can use assignment to mutate context object (or even helper object)
let obj = {a: 1, b: 2};
evaluate('a = 3', obj);
evaluate('b > 3 ? (a = true) : (a = false)', obj);
disable assignment if you don't need it
This doesn't eliminate side effect, it would not prevent any function you called in bcx-expression to mutate something.
evaluate('a=1', {a:0}, null, {rejectAssignment: true});
Difference from real JavaScript expression
bcx-expression looks like JavaScript expression, but there are some difference.
wrong reference results undefined instead of error
let obj = {a: 1};
obj.b.a
evaluate('b.a', obj);
default result for +/- operators
Behaviour carried over from aurelia-binding.
undefined + 1
1 + undefined
null + 1
1 + null
undefined + undefined
null + null
evaluate('undefined + 1');
evaluate('1 + undefined');
evaluate('null + 1');
evaluate('1 + null');
evaluate('undefined + undefined');
evaluate('null + null');
no function expression
(function(){return 1})()
(() => 1)()
arr.sort((a, b) => a > b)
arr.sort(aHelperFunc)
no regular expression support
Regex syntax is too complex to be supported for our AST (abstract syntax tree).
/\w/.test(string)
One way to bypass this is to supply regex literal in helper object.
evaluate('tester.test(str)', {str: '%'}, {tester: /\w/});
some JavaScript operators would not work
typeof
, instanceof
, delete
would not work, because bcx-expression is not real JavaScript.
BUTTONWOODCX™ PTY LTD.