UNPKG

mozjexl

Version:

Javascript Expression Language: Powerful context-based expression parser and evaluator

158 lines (149 loc) 6.15 kB
/* * 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;