• Stars
    star
    520
  • Rank 85,117 (Top 2 %)
  • Language
    JavaScript
  • License
    MIT License
  • Created almost 10 years ago
  • Updated about 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

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

Jexl Build Status

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

Quick start

Use it with promises or synchronously:

const context = {
  name: { first: 'Sterling', last: 'Archer' },
  assoc: [
    { first: 'Lana', last: 'Kane' },
    { first: 'Cyril', last: 'Figgis' },
    { first: 'Pam', last: 'Poovey' }
  ],
  age: 36
}

// Filter an array asynchronously...
await const res = jexl.eval('assoc[.first == "Lana"].last', context)
console.log(res) // Output: Kane

// Or synchronously!
console.log(jexl.evalSync('assoc[.first == "Lana"].last')) // Output: Kane

// Do math
await jexl.eval('age * (3 - 1)', context)
// 72

// Concatenate
await jexl.eval('name.first + " " + name["la" + "st"]', context)
// "Sterling Archer"

// Compound
await jexl.eval(
  'assoc[.last == "Figgis"].first == "Cyril" && assoc[.last == "Poovey"].first == "Pam"',
  context
)
// true

// Use array indexes
await jexl.eval('assoc[1]', context)
// { first: 'Cyril', last: 'Figgis' }

// Use conditional logic
await jexl.eval('age > 62 ? "retired" : "working"', context)
// "working"

// Transform
jexl.addTransform('upper', (val) => val.toUpperCase())
await jexl.eval('"duchess"|upper + " " + name.last|upper', context)
// "DUCHESS ARCHER"

// Transform asynchronously, with arguments
jexl.addTransform('getStat', async (val, stat) => dbSelectByLastName(val, stat))
try {
  const res = await jexl.eval('name.last|getStat("weight")', context)
  console.log(res) // Output: 184
} catch (e) {
  console.log('Database Error', e.stack)
}

// Functions too, sync or async, args or no args
jexl.addFunction('getOldestAgent', () => db.getOldestAgent())
await jexl.eval('age == getOldestAgent().age', context)
// false

// Add your own (a)synchronous operators
// Here's a case-insensitive string equality
jexl.addBinaryOp(
  '_=',
  20,
  (left, right) => left.toLowerCase() === right.toLowerCase()
)
await jexl.eval('"Guest" _= "gUeSt"')
// true

// Compile your expression once, evaluate many times!
const { expr } = jexl
const danger = expr`"Danger " + place` // Also: jexl.compile('"Danger " + place')
danger.evalSync({ place: 'zone' }) // Danger zone
danger.evalSync({ place: 'ZONE!!!' }) // Danger ZONE!!! (Doesn't recompile the expression!)

Play with it

Installation

Jexl works on the backend, and on the frontend if bundled using a bundler like Parcel or Webpack.

Install from npm:

npm install jexl --save

or yarn:

yarn add jexl

and use it:

const jexl = require('jexl')

Async vs Sync: Which to use

There is little performance difference between eval and evalSync. The functional difference is that, if eval is used, Jexl can be customized with asynchronous operators, transforms, and even wait for unresolved promises in the context object with zero additional overhead or handling on the programmer's part. evalSync eliminates those advantages, exposing the expression to raw Promise objects if any are returned as the result of a custom transform or operator. However, if your application doesn't require async methods, the evalSync API can be simpler to use.

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 ||

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 || 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:

{
  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:

{
    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).

jexl.addTransform('split', (val, char) => val.split(char))
jexl.addTransform('lower', (val) => val.toLowerCase())
Expression Result
"Pam Poovey"|lower|split(' ')[1] poovey
"password==guest"|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 module, allows XML to be traversed just as easily as plain javascript objects:

const xml2json = require('xml2json')

jexl.addTransform('xml', (val) => xml2json.toJson(val, { object: true }))

const 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(console.log) // Output: Cyril

Functions

While Transforms are the preferred way to change one value into another value, Jexl also allows top-level expression functions to be defined. Use these to provide access to functions that either don't require an input, or require multiple equally-important inputs. They can be added with jexl.addFunction(name, function). Like transforms, functions can return a value, or a Promise that resolves to the resulting value.

jexl.addFunction('min', Math.min)
jexl.addFunction('expensiveQuery', async () => db.runExpensiveQuery())
Expression Result
min(4, 2, 19) 2
counts.missions || expensiveQuery() Query only runs if needed

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.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, {boolean} [manualEval])

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.

If manualEval is true, the left and right arguments will be wrapped in objects with an eval function. Calling left.eval() or right.eval() will return a promise that resolves to that operand's actual value. This is useful to conditionally evaluate operands, and is how && and || work.

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.addFunction({string} name, _{function} func)

Adds an expression function to this Jexl instance. See the Functions section above for information on the structure of an expression function.

jexl.addFunctions({{}} map)

Adds multiple functions from a supplied map of function name to expression function.

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.compile({string} expression)

Constructs an Expression object around the given Jexl expression string. Expression objects allow a Jexl expression to be compiled only once but evaluated many times. See the Expression API below. Note that the only difference between this function and jexl.createExpression is that this function will immediately compile the expression, and throw any errors associated with invalid expression syntax.

jexl.createExpression({string} expression)

Constructs an Expression object around the given Jexl expression string. Expression objects allow a Jexl expression to be compiled only once but evaluated many times. See the Expression API below.

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])

Returns {Promise<*>}. Evaluates an expression. The context map is optional.

jexl.evalSync({string} expression, {{}} [context])

Returns {*}. Evaluates an expression and returns the result. The context map is optional.

jexl.expr: tagged template literal

A convenient bit of syntactic sugar for jexl.createExpression

const someNumber = 10
const expression = jexl.expr`5 + ${someNumber}`
console.log(expression.evalSync()) // 15

Note that expr will stay bound to its associated Jexl instance even if it's pulled out of context:

const { expr } = jexl
jexl.addTransform('double', (val) => val * 2)
const expression = expr`2|double`
console.log(expression.evalSync()) // 4

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.

Expression

Expression objects are created via jexl.createExpression, jexl.compile, or jexl.expr, and are a convenient way to ensure jexl expressions compile only once, even if they're evaluated multiple times.

expression.compile()

Returns self {Expression}. Forces the expression to compile, even if it was compiled before. Note that each compile will happen with the latest grammar and transforms from the associated Jexl instance.

expression.eval({{}} [context])

Returns {Promise<*>}. Evaluates the expression. The context map is optional.

expression.evalSync({{}} [context])

Returns {*}. Evaluates the expression and returns the result. The context map is optional.

Other implementations

PyJEXL - A Python-based JEXL parser and evaluator.

License

Jexl is licensed under the MIT license. Please see LICENSE.txt for full details.

Credits

Created by Tom Shawver in 2015 and contributed to by these great people.

Jexl was originally created at TechnologyAdvice in Nashville, TN.

More Repositories

1

Cryptex

Secure secret storage and cryptographic key retrieval for Node.js
JavaScript
141
star
2

Vers

Effortless data model versioning for Javascript and Node.js
JavaScript
119
star
3

Bristol

Insanely configurable logging for Node.js
JavaScript
112
star
4

Squiss

High-volume Amazon SQS Poller for Node.js
JavaScript
97
star
5

node-phonetic

Generates unique, pronounceable names, random and seedable
JavaScript
81
star
6

Hydrogen

Hydrogen is a PHP 5.3+ performance and portability library
PHP
40
star
7

node-attempt

Automatically retry functions that fail, in crazily customizable ways.
JavaScript
37
star
8

node-vitalsigns

Powerful and customizable application health monitoring
JavaScript
27
star
9

fs_eventbridge

A TCP server to stream file-change events to a remote destination, written in Rust.
Rust
23
star
10

DevBox

🐳 A minimal bootstrapper to turn a brand new Mac into a docker-based dev environment
Shell
21
star
11

FlushWritable

A Node.js Writable stream that flushes before emitting finish
JavaScript
15
star
12

Embassy

🎏 Simple JSON Web Tokens (JWT) with embedded scopes for services
TypeScript
12
star
13

Semlocks

Mind-blowingly simple local resource management for Node.js
JavaScript
11
star
14

Hydrogen-MVC-Starter

A project template for using Hydrogen as an MVC framework
PHP
11
star
15

Toady

Wickedly extensible IRC bot written in Node.js. Load and reload mods without reconnecting.
JavaScript
8
star
16

MindEye

A proof-of-concept drawing recognition app.
JavaScript
4
star
17

DeepSea-Storm

A color scheme for IDEA-based *Storm IDEs.
4
star
18

fake-mocha

Run arbitrary commands instead of mocha to integrate with popular IDEs
JavaScript
4
star
19

fsbridge

A command-line client to stream file changes to an FS-EventBridge server
JavaScript
3
star
20

frost-zsh-prompt

Distinct, informative, and fast ZSH prompt
Shell
2
star
21

dotfiles

Mostly original configs, totally original installer. Compatible with macOS and various Linuxes.
Shell
2
star
22

node-argsnip

Snips arguments off of callback functions.
JavaScript
1
star
23

alpine-node-dind-compat

An alpine-node docker image, including automatic compatibility with linked docker-in-docker containers.
1
star
24

toady-github

Toady module: Provides details on Github repositories
JavaScript
1
star
25

RPi-LED-Matrix

A Raspberry Pi animator for 3x3 LED grids, with button input
Python
1
star
26

Imp

IMP: IRC Modularized Preprocessor. A Java-based single-channel IRC helper bot.
Java
1
star
27

fs-eventbridge-js

Node.js client for the FS-EventBridge server
JavaScript
1
star
28

toady-acrophobia

Toady module: A game to turn acronyms into funny phrases
JavaScript
1
star