mozjexl
Version:
Javascript Expression Language: Powerful context-based expression parser and evaluator
158 lines (149 loc) • 6.15 kB
JavaScript
/*
* Jexl
* Copyright (c) 2015 TechnologyAdvice
*/
var handlers = require('./handlers');
/**
* The Evaluator takes a Jexl expression tree as generated by the
* {@link Parser} and calculates its value within a given context. The
* collection of transforms, context, and a relative context to be used as the
* root for relative identifiers, are all specific to an Evaluator instance.
* When any of these things change, a new instance is required. However, a
* single instance can be used to simultaneously evaluate many different
* expressions, and does not have to be reinstantiated for each.
* @param {{}} grammar A grammar map against which to evaluate the expression
* tree
* @param {{}} [transforms] A map of transform names to transform functions. A
* transform function takes two arguments:
* - {*} val: A value to be transformed
* - {{}} args: A map of argument keys to their evaluated values, as
* specified in the expression string
* The transform function should return either the transformed value, or
* a Promises/A+ Promise object that resolves with the value and rejects
* or throws only when an unrecoverable error occurs. Transforms should
* generally return undefined when they don't make sense to be used on the
* given value type, rather than throw/reject. An error is only
* appropriate when the transform would normally return a value, but
* cannot due to some other failure.
* @param {{}} [context] A map of variable keys to their values. This will be
* accessed to resolve the value of each non-relative identifier. Any
* Promise values will be passed to the expression as their resolved
* value.
* @param {{}|Array<{}|Array>} [relativeContext] A map or array to be accessed
* to resolve the value of a relative identifier.
* @constructor
*/
var Evaluator = function(grammar, transforms, context, relativeContext) {
this._grammar = grammar;
this._transforms = transforms || {};
this._context = context || {};
this._relContext = relativeContext || this._context;
};
/**
* Evaluates an expression tree within the configured context.
* @param {{}} ast An expression tree object
* @returns {Promise<*>} resolves with the resulting value of the expression.
*/
Evaluator.prototype.eval = function(ast) {
var self = this;
return Promise.resolve().then(function() {
return handlers[ast.type].call(self, ast);
});
};
/**
* Simultaneously evaluates each expression within an array, and delivers the
* response as an array with the resulting values at the same indexes as their
* originating expressions.
* @param {Array<string>} arr An array of expression strings to be evaluated
* @returns {Promise<Array<{}>>} resolves with the result array
*/
Evaluator.prototype.evalArray = function(arr) {
return Promise.all(arr.map(function(elem) {
return this.eval(elem);
}, this));
};
/**
* Simultaneously evaluates each expression within a map, and delivers the
* response as a map with the same keys, but with the evaluated result for each
* as their value.
* @param {{}} map A map of expression names to expression trees to be
* evaluated
* @returns {Promise<{}>} resolves with the result map.
*/
Evaluator.prototype.evalMap = function(map) {
var keys = Object.keys(map),
result = {};
var asts = keys.map(function(key) {
return this.eval(map[key]);
}, this);
return Promise.all(asts).then(function(vals) {
vals.forEach(function(val, idx) {
result[keys[idx]] = val;
});
return result;
});
};
/**
* Applies a filter expression with relative identifier elements to a subject.
* The intent is for the subject to be an array of subjects that will be
* individually used as the relative context against the provided expression
* tree. Only the elements whose expressions result in a truthy value will be
* included in the resulting array.
*
* If the subject is not an array of values, it will be converted to a single-
* element array before running the filter.
* @param {*} subject The value to be filtered; usually an array. If this value is
* not an array, it will be converted to an array with this value as the
* only element.
* @param {{}} expr The expression tree to run against each subject. If the
* tree evaluates to a truthy result, then the value will be included in
* the returned array; otherwise, it will be eliminated.
* @returns {Promise<Array>} resolves with an array of values that passed the
* expression filter.
* @private
*/
Evaluator.prototype._filterRelative = function(subject, expr) {
if (subject === undefined)
return undefined;
var promises = [];
if (!Array.isArray(subject))
subject = [subject];
subject.forEach(function(elem) {
var evalInst = new Evaluator(this._grammar, this._transforms,
this._context, elem);
promises.push(evalInst.eval(expr));
}, this);
return Promise.all(promises).then(function(values) {
var results = [];
values.forEach(function(value, idx) {
if (value)
results.push(subject[idx]);
});
return results;
});
};
/**
* Applies a static filter expression to a subject value. If the filter
* expression evaluates to boolean true, the subject is returned; if false,
* undefined.
*
* For any other resulting value of the expression, this function will attempt
* to respond with the property at that name or index of the subject.
* @param {*} subject The value to be filtered. Usually an Array (for which
* the expression would generally resolve to a numeric index) or an
* Object (for which the expression would generally resolve to a string
* indicating a property name)
* @param {{}} expr The expression tree to run against the subject
* @returns {Promise<*>} resolves with the value of the drill-down.
* @private
*/
Evaluator.prototype._filterStatic = function(subject, expr) {
return this.eval(expr).then(function(res) {
if (typeof res === 'boolean')
return res ? subject : undefined;
if (subject === undefined)
return undefined;
return subject[res];
});
};
module.exports = Evaluator;