async-boolean-expression-evaluator
v2.0.0
Published
Evaluates a boolean expression such as `{or: ['a', 'b']}` by passing each operand into an asynchronous iterator to compute the result.
Downloads
5
Maintainers
Readme
async-boolean-expression-evaluator
The AsyncBooleanExpressionEvaluator is a class designed to evaluate a special format of a boolean expression, where each operand in the expression can be tested asynchronously.
Examples
const AsyncBooleanExpressionEvaluator = require('async-boolean-expression-evaluator');
const evaluator = new AsyncBooleanExpressionEvaluator(function test (operand) {
// A simple asynchronous operation that returns a Promise. It resolves with true if the
// operand is an even number or false if it is an odd number. It rejects the promise if
// the operand is not a number.
return new Promise((resolve, reject) => {
setImmediate(() => {
if (typeof value !== 'number' && !isNaN(value)) {
resolve(value % 2 === 0);
} else {
reject(new TypeError('Input must be castable to Number'));
}
));
});
});
// Simple expression
evaluator.execute(2).then((result) => result === true);
evaluator.execute(1).then((result) => result === false);
evaluator.execute('a').catch((err) => err instanceof TypeError);
// And expression
evaluator.execute({and: [2, 4]}).then((result) => result === true);
evaluator.execute({and: [2, 3]}).then((result) => result === false);
// Or expression
evaluator.execute({or: [2, 3]}).then((result) => result === true);
evaluator.execute({or: [1, 3]}).then((result) => result === false);
// Not expression
evaluator.execute({not: 1}).then((result) => result === true);
evaluator.execute({not: 2}).then((result) => result === false);
// Nested expressions
evaluator.execute({and: [2, {or: [1, {not: {and: [3, 4]}}]}]})
.then((result) => result === true);
// Changing the iterator on the fly
evaluator.iterator = function (operand) {};
// Setting the maximum number of parallel tests (defaults to 1, i.e. execute in series)
evaluator.parallelLimit = 5;
// Traditional callback-style iterator
evaluator.iterator = function test (id, done) {
db.getItemById(id, (err, item) => {
if (err) return done(err);
done(null, item.property === 42);
});
};
evaluator.execute({or: [1, 2, 3]}).then((result) => log(result));
// Traditional callback-style expression execution
evaluator.execute({or: [3, 4]}, (err, result) => {
log(err);
log(result);
});
Features
- Supports boolean operators
and
andor
- Supports recursive expressions
- Supports negation of results with a
not
operator - Written in ES6 and uses Promises
- Detects when to use traditional callback-style operations
- Caches the result of the iterator for the given operand to save processing time
- Configurable limit to the number of parallel iterators run to conserve resources
Understanding Input
There are two "types" of parameters given to the evaluator: the expression and the operands. The expression is the boolean statement to be evaluated, while the operands are the items passed into the iterator to test their value.
Expressions
The expression is usually in the format of an object and can contain one of the three keyword properties: and
, or
,
or not
. An expression is not valid if it contains more than one of these keywords.
and
: The value for this property must be an array of nested expressions. If all expressions in the array evaluate to truthy values, then the result of the expression is true.or
: The value for this property must be an array of nested expressions. If any expression in the array evaluates to truthy values, then the result of the expression is true.not
: The value for this property must be a nested expression. If the result of the nested expression is true, then the result of this expression will be false.
The simplest expression is actually not an object at all, but just one operand. Each of the following are valid expressions:
1
: True if the iterator is truthy for1
{and: [1, 2, 3]}
: True if the iterator is truthy for1
,2
, and3
{or: [1, 2, 3]}
: True if the iterator is truthy for1
,2
, or3
{not: 1}
: True if the iterator is not truthy for1
{not: {and: [1, 2, 3]}
: True if the iterator is not truthy for1
,2
, and3
{and: [1, {or: [2, {not: 3}]}]}
: True if the iterator is truthy for1
and also either truthy for2
or falsy for3
An expression, however, may not contain more than one of the three keywords, so these expressions are invalid:
{and: [1, 2], or: [3, 4]}
{and: [1, 2], not: 3}
Operands
Operands are the subjects of the expression. Each operand is tested against the asynchronous iterator function to determine its truthiness. Operands can be any type.
The result of invoking the iterator against an operand is cached, so that if the same operand is used more than once in the expression or subsequent evaluations, the previous result is used, conserving server resources.
Object Operands
An operand may be of any type. Because of this, there is some ambiguity between whether an argument is an operand or an
expression. Operands may be objects, but if they are objects, they may not have the properties and
, or
, or not
on them to prevent the parser from considering the operand to be a nested expression. If the operand is an object
containing arbitrary fields that are not known, consider wrapping the operand inside another object to safeguard the
evaluator against it mistakenly determining than an operand is another expression. For example:
const obj1 = {name: 'an arbitrary object', and: 'a confusing property'};
const obj2 = {name: 'an arbitrary object 2', and: 'another confusing property'};
// Wrong! Evaluator sees `and` key and expects the object to be an expression
let evaluator = new AsyncBooleanExpressionEvaluator(function test (value) {
return new Promise((resolve) => {
setImmediate(() => resolve(value.name.indexOf('arbitrary') > -1));
});
});
evaluator.execute({and: [obj1, obj2]});
// Right. Now `and` is not seen by the evaluator
evaluator = new AsyncBooleanExpressionEvaluator(function test (value) {
return new Promise((resolve) => {
setImmediate(() => resolve(value.object.name.indexOf('arbitrary') > -1));
});
});
evaluator.execute({and: [{object: obj1}, {object: obj2}]});
Using Callback-Style Operations
The evaluator prefers promise-style asynchronous execution but will detect callback-style execution based on input.
- If
#execute
is passed an argument after the expression, the evaluator assumes that this is a callback and invokes it when the expression is evaluated, passing the error or null as the first argument and the result or null as the second. It will still return the promise. - If the iterator has a
length
property of 2, the evaluator assumes that it should pass a callback into the iterator and will not expect the iterator to return a promise, but instead that it will invoke the callback. For example, the evaluator expects thatfunction test (value) {}
will return a promise, whereasfunction test (value, callback) {}
will execute the callback. It is important, therefore, to be conscious of the number of arguments of the iterator function's signature.
If the iterator does not use the callback-style, make sure that it always returns promises and that it has defined the correct number of arguments in its signature (i.e. an iterator must expect one argument).
If the iterator does use the callback-style, make sure that the iterator expects exactly two arguments, the latter of
which is the callback. In addition, the callback must be invoked with either an error or null as the first parameter and
the result (truthy/falsy value) or null as the second parameter to the callback. Keep this in mind when working with
certain no-error-style asynchronous functions, such as the built-in fs module's exists
function.
Configuring Parallel Limit
Set the parallelLimit
property on the evaluator to change the number of concurrent iterators. For example, if an
expression contains many operands, such as an or
operation with 100 user IDs to check against, the evaluator can be
configured to only run up to 10 queries at a time to prevent the application from abusing the database connection.
Note that this parallel limit is local to the execution operation, i.e. if #execute
is called multiple times
concurrently, then this limit is actually parallelLimit
times the number of executions being run at once.
By default, this is set to 1
, i.e. only one operand is tested by the iterator at a time. This is more likely to
conserve resources but will take more time. Increase this value at your discretion.
API
API documentation is available in API.md.
Scripts
npm install
Install via npm install async-boolean-expression-evaluator
.
npm test
The unit tests are written in Mocha and Should.js.
They are located in the test/
directory and can be run via npm test
.
npm run docs
Generates the API documentation via jsdoc-to-markdown.