UNPKG

mozjexl

Version:

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

353 lines (285 loc) 12.8 kB
mozJexl is a fork of Jexl for use at Mozilla, specifically as a part of SHIELD and Normandy. --- <a href="http://promisesaplus.com/"> <img src="https://promises-aplus.github.io/promises-spec/assets/logo-small.png" align="right" valign="top" alt="Promises/A+ logo" /> </a> # Jexl [![Build Status](https://travis-ci.org/TechnologyAdvice/Jexl.svg?branch=master)](https://travis-ci.org/TechnologyAdvice/Jexl) [![Code Climate](https://codeclimate.com/github/TechnologyAdvice/Jexl/badges/gpa.svg)](https://codeclimate.com/github/TechnologyAdvice/Jexl) [![Test Coverage](https://codeclimate.com/github/TechnologyAdvice/Jexl/badges/coverage.svg)](https://codeclimate.com/github/TechnologyAdvice/Jexl) Javascript Expression Language: Powerful context-based expression parser and evaluator ## Quick start Use it with promises or callbacks: ```javascript var context = { name: {first: 'Sterling', last: 'Archer'}, assoc: [ {first: 'Lana', last: 'Kane'}, {first: 'Cyril', last: 'Figgis'}, {first: 'Pam', last: 'Poovey'} ], age: 36 }; // Filter an array jexl.eval('assoc[.first == "Lana"].last', context).then(function(res) { console.log(res); // Output: Kane }); // Do math jexl.eval('age * (3 - 1)', context, function(err, res) { console.log(res); // Output: 72 }); // Concatenate jexl.eval('name.first + " " + name["la" + "st"]', context).then(function(res) { console.log(res); // Output: Sterling Archer }); // Compound jexl.eval('assoc[.last == "Figgis"].first == "Cyril" && assoc[.last == "Poovey"].first == "Pam"', context) .then(function(res) { console.log(res); // Output: true }); // Use array indexes jexl.eval('assoc[1]', context, function(err, res) { console.log(res.first + ' ' + res.last); // Output: Cyril Figgis }); // Use conditional logic jexl.eval('age > 62 ? "retired" : "working"', context).then(function(res) { console.log(res); // Output: working }); // Transform jexl.addTransform('upper', function(val) { return val.toUpperCase(); }); jexl.eval('"duchess"|upper + " " + name.last|upper', context).then(function(res) { console.log(res); // Output: DUCHESS ARCHER }); // Transform asynchronously, with arguments jexl.addTransform('getStat', function(val, stat) { return dbSelectByLastName(val, stat); // Returns a promise }); jexl.eval('name.last|getStat("weight")', context, function(err, res) { if (err) console.log('Database Error', err.stack); else console.log(res); // Output: 184 }); // Add your own (a)synchronous operators // Here's a case-insensitive string equality jexl.addBinaryOp('_=', 20, function(left, right) { return left.toLowerCase() === right.toLowerCase(); }); jexl.eval('"Guest" _= "gUeSt"').then(function(val) { console.log(res); // Output: true }); ``` ## Installation Jexl requires an environment that supports the [Promise/A+](https://promisesaplus.com/) specification as standardized in ES6. Node.js version 0.12.0 and up is great right out of the box (no --harmony flag necessary), as well as the latest versions of many browsers. To support older browsers, just include a Promise library such as [Bluebird](https://github.com/petkaantonov/bluebird). For Node.js, type this in your project folder: npm install jexl --save For the frontend, drop `dist/jexl.min.js` into your project and include it on your page with: <script src="path/to/jexl.min.js"></script> Access Jexl the same way, backend or front: var jexl = require('Jexl'); ## All the details ### Unary Operators | Operation | Symbol | |-----------|:------:| | Negate | ! | ### Binary Operators | Operation | Symbol | |------------------|:----------------:| | Add, Concat | + | | Subtract | - | | Multiply | * | | Divide | / | | Divide and floor | // | | Modulus | % | | Power of | ^ | | Logical AND | && | | Logical OR | &#124;&#124; | ### Comparisons | Comparison | Symbol | |----------------------------|:------:| | Equal | == | | Not equal | != | | Greater than | > | | Greater than or equal | >= | | Less than | < | | Less than or equal | <= | | Element in array or string | in | #### A note about `in`: The `in` operator can be used to check for a substring: `"Cad" in "Ron Cadillac"`, and it can be used to check for an array element: `"coarse" in ['fine', 'medium', 'coarse']`. However, the `==` operator is used behind-the-scenes to search arrays, so it should not be used with arrays of objects. The following expression returns false: `{a: 'b'} in [{a: 'b'}]`. ### Ternary operator Conditional expressions check to see if the first segment evaluates to a truthy value. If so, the consequent segment is evaluated. Otherwise, the alternate is. If the consequent section is missing, the test result itself will be used instead. | Expression | Result | |-----------------------------------|--------| | "" ? "Full" : "Empty" | Empty | | "foo" in "foobar" ? "Yes" : "No" | Yes | | {agent: "Archer"}.agent ?: "Kane" | Archer | ### Native Types | Type | Examples | |----------|:------------------------------:| | Booleans | `true`, `false` | | Strings | "Hello \"user\"", 'Hey there!' | | Numerics | 6, -7.2, 5, -3.14159 | | Objects | {hello: "world!"} | | Arrays | ['hello', 'world!'] | ### Groups Parentheses work just how you'd expect them to: | Expression | Result | |-------------------------------------|:-------| | (83 + 1) / 2 | 42 | | 1 < 3 && (4 > 2 &#124;&#124; 2 > 4) | true | ### Identifiers Access variables in the context object by just typing their name. Objects can be traversed with dot notation, or by using brackets to traverse to a dynamic property name. Example context: ```javascript { name: { first: "Malory", last: "Archer" }, exes: [ "Nikolai Jakov", "Len Trexler", "Burt Reynolds" ], lastEx: 2 } ``` | Expression | Result | |-------------------|---------------| | name.first | Malory | | name['la' + 'st'] | Archer | | exes[2] | Burt Reynolds | | exes[lastEx - 1] | Len Trexler | ### Collections Collections, or arrays of objects, can be filtered by including a filter expression in brackets. Properties of each collection can be referenced by prefixing them with a leading dot. The result will be an array of the objects for which the filter expression resulted in a truthy value. Example context: ```javascript { employees: [ {first: 'Sterling', last: 'Archer', age: 36}, {first: 'Malory', last: 'Archer', age: 75}, {first: 'Lana', last: 'Kane', age: 33}, {first: 'Cyril', last: 'Figgis', age: 45}, {first: 'Cheryl', last: 'Tunt', age: 28} ], retireAge: 62 } ``` | Expression | Result | |-----------------------------------------------|---------------------------------------------------------------------------------------| | employees[.first == 'Sterling'] | [{first: 'Sterling', last: 'Archer', age: 36}] | | employees[.last == 'Tu' + 'nt'].first | Cheryl | | employees[.age >= 30 && .age < 40] | [{first: 'Sterling', last: 'Archer', age: 36},{first: 'Lana', last: 'Kane', age: 33}] | | employees[.age >= 30 && .age < 40][.age < 35] | [{first: 'Lana', last: 'Kane', age: 33}] | | employees[.age >= retireAge].first | Malory | ### Transforms The power of Jexl is in transforming data, synchronously or asynchronously. Transform functions take one or more arguments: The value to be transformed, followed by anything else passed to it in the expression. They must return either the transformed value, or a Promise that resolves with the transformed value. Add them with `jexl.addTransform(name, function)`. ```javascript jexl.addTransform('split', function(val, char) { return val.split(char); }); jexl.addTransform('lower', function(val) { return val.toLowerCase(); }); ``` | Expression | Result | |--------------------------------------------|-----------------------| | "Pam Poovey"&#124;lower&#124;split(' ')[1] | poovey | | "password==guest"&#124;split('=' + '=') | ['password', 'guest'] | #### Advanced Transforms Using Transforms, Jexl can support additional string formats like embedded JSON, YAML, XML, and more. The following, with the help of the [xml2json](https://github.com/buglabs/node-xml2json) module, allows XML to be traversed just as easily as plain javascript objects: ```javascript var xml2json = require('xml2json'); jexl.addTransform('xml', function(val) { return xml2json.toJson(val, {object: true}); }); var context = { xmlDoc: "<Employees>" + "<Employee>" + "<FirstName>Cheryl</FirstName>" + "<LastName>Tunt</LastName>" + "</Employee>" + "<Employee>" + "<FirstName>Cyril</FirstName>" + "<LastName>Figgis</LastName>" + "</Employee>" + "</Employees>" }; var expr = 'xmlDoc|xml.Employees.Employee[.LastName == "Figgis"].FirstName'; jexl.eval(expr, context).then(function(res) { console.log(res); // Output: Cyril }); ``` ### Context Variable contexts are straightforward Javascript objects that can be accessed in the expression, but they have a hidden feature: they can include a Promise object, and when that property is used, Jexl will wait for the Promise to resolve and use that value! ### API #### jexl.Jexl 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_) 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 to calculate a result. The precedence determines the operator's position in the order of operations (please refer to `lib/grammar.js` to see the precedence of 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. #### jexl.addUnaryOp(_{string} operator_, _{function} fn_) Adds a unary operator to the Jexl instance. A unary operator is one that considers only the value on its right, such as "!", in order to calculate a result. The provided function will be called with one argument: the value to the operator's right. It should return either the resulting value, or a Promise that resolves to the resulting value. #### jexl.addTransform(_{string} name_, _{function} transform_) Adds a transform function to this Jexl instance. See the **Transforms** section above for information on the structure of a transform function. #### jexl.addTransforms(_{{}} map_) Adds multiple transforms from a supplied map of transform name to transform function. #### jexl.getTransform(_{string} name_) **Returns `{function|undefined}`.** Gets a previously set transform function, or `undefined` if no function of that name exists. #### jexl.eval(_{string} expression_, _{{}} [context]_, _{function} [callback]_) **Returns `{Promise<*>}`.** Evaluates an expression. The context map and callback function are optional. If a callback is specified, it will be called with the standard signature of `{Error}` first argument, and the expression's result in the second argument. Note that if a callback function is supplied, the returned Promise will already have a `.catch()` attached to it. #### jexl.removeOp(_{string} operator_) Removes a binary or unary operator from the Jexl instance. For example, "^" can be passed to eliminate the "power of" operator. ## License Jexl is licensed under the MIT license. Please see `LICENSE.txt` for full details. ## Credits Jexl was designed and created at [TechnologyAdvice](http://technologyadvice.com).