@alpaca-travel/fexp-js-lang v1.0.1-alpha.0
fexp-js (Functional Expressions for JS)
Functional Expressions ("fexp") provides a simple functional scripting syntax. fexp-js is a supported JavaScript implementation for developers to offer in their applications.
- Simple syntax :relieved:
- General purpose expressions (filtering, map/reduce etc) :hammer: :wrench:
- Portable via serialization (JSON) :envelope:
- Compiles expressions into functions :speedboat: :rocket:
- Tiny, with a full-featured syntax :school_satchel:
- Optional libs, for GIS :earth_africa: :earth_americas: :earth_asia:
- Or able to support your own set of functions :scissors: :bulb:
Developers can implement fexp into your application environments to offer scripting syntax within their product for other developers. These could be used to describe filter evaluation criteria, or perform various tranformations or map/reduce expressions.
Syntax Overview
[<name>, [param1[, param2[, ..., paramN]]]]
fexp processes the syntax and will invoke the required language functions as defined in the supplied lang features.
Evaluation Order
"fexp" expressions form a tree structure. Params are evaluated using a depth-first approach, evaluating parameters before executing parent functions.
// Given the expression:
const expr = ["all", ["is-boolean", true], ["==", "foobar", "foobar"]];
// Order of evaluation, DFS
// 1. Evaluate ["is-boolean", ...]
// 2. Evaluate ["==", ...]
// 3. Evaluate ["all", ...]
Basic Example
The fexp-js library is generic enough in scripting purpose to have a wide range of use cases. It could be used for filtering, other map/reduce functions.
Expressions can be used to filter a collection.
import { compile } from "@alpaca-travel/fexp-js";
import lang from "@alpaca-travel/fexp-js-lang";
// Our collection (see hotels.json for example)
import hotels from "./hotels.json";
// Serializable/stringify expressions (not required)
const expr = [
"all",
[">=", ["get", "stars-rating"], 3.5],
["in", ["get", "tags"], "boutique"]
];
// Compile our expression
const { compiled: fn } = compile(expr, lang);
// Match against our collection
const firstMatch = hotels.find(item => fn(lang, item));
console.log(firstMatch);
Language Reference
Language Functions
The following language functions are part of the core expression library. They are used to support some of the runtime functions, such as dealing with literal arrays, negation and functions. You don't need to supply these in your language set for them to be used.
Literal
To stop processing through params (such as an string array which appears like an expression), use the "literal" function.
// Use literal to take the params without evaluating the array contents
const expr = ["in", ["literal", ["foo", "bar"]], "bar"];
Negate (!)
You can negate an expression with either the function prefix of !fn
or using "!" by itself.
const expr = ["!", ["==", "foo", "bar"]]; // Negates the == result
const expr2 = ["!my-function"]; // Negates the result of the function call to "my-function"
Function ("fn")
You can build functions that can be passed as function arguments to other functions (such as map reduce etc)
const expr = ["fn", ...]; // Returns a function that can be executed with arguments
Standard Language Library (@alpaca-travel/fexp-js-lang)
- Equality: ==, !=, <, >, <=, >=, eq, lt, lte, gt, gte
- Deep Equality: equal/equals, !equal/!equals
- Accessors: get (also using paths like foo.bar), at, length, fn-arg
- Existence: has/have/exist/exists/empty, !has/!have/!exist/!exists/!empty
- Membership: in/!in
- Types: typeof, to-boolean, to-string, to-number, to-regex, to-date, is-array, is-number, is-boolean, is-object, is-regex
- Regular Expressions: "regex-test"
- Combining: all/any/none
- String manipulation: concat, uppercase, lowercase
- Math: +, -, *, /, floor, ceil, sin/cos/tan/asin/acos/atan, pow, sqrt, min, max, random, e, pi, ln, ln2, ln10, log2e, log10e
- control: match, case
- map reduce: map/reduce/filter/find
- more..
Types
fexp-js-lang supports a number of functions to work with types in expressions.
// Obtain the "typeof" of parameter
["typeof", "example"] === "string"
["typeof", true] === "boolean"
["typeof", { foo: "bar" }] === "object"
["typeof", ["literal", ["value1", "value2"]]] === "array"
// Cast the param as boolean
["to-boolean", "yes"] === true
// Check if the param is a boolean
["is-boolean", false] === true
// Cast the param as string
["to-string", true] === "true"
// Check if the param is a string
["is-string", "foo"] === true
// Cast the param as number
["to-number", "10"] === 10
// Check if the param is a number
["is-number", "10"] === false
["is-number", 10] === true
// Cast the param as RegExp
["to-regex", "regex?", "i"] === new RegExp("regex?", "i")
// Check if the param is a RegExp
["is-regex", new RegExp("regex", "i")] === true
// Cast the param as Date
["to-date", "2020-01-01"] === new Date("2020-01-01")
// Check if the param is a Date
["is-date", new Date("2020-01-01")] === true
import { evaluate } from "@alpaca-travel/fexp-js";
import lang from "@alpaca-travel/fexp-js-lang";
describe("Using Types with fexp-js-lang", () => {
it("will return typeof for the supplied parameters", () => {
// ['typeof', 0] === 'number'
expect(evaluate(["typeof", 0], lang)).toBe("number");
expect(evaluate(["typeof", "test"], lang)).toBe("string");
expect(evaluate(["typeof", { foo: "bar" }], lang)).toBe("object");
expect(evaluate(["typeof", ["literal", ["value1", "value2"]]], lang)).toBe(
"object"
);
});
it("will cast using to-boolean", () => {
expect(evaluate(["to-boolean", "true"], lang)).toBe(true);
expect(evaluate(["to-boolean", "yes"], lang)).toBe(true);
expect(evaluate(["to-boolean", "false"], lang)).toBe(false);
expect(evaluate(["to-boolean", "0"], lang)).toBe(false);
});
});
Control
// Supporting basic if/then/else
["case", true, 1, 2] === 1
["case", false, 1, 2] === 2
["case", false, 1, true, 2, 3] === 2
["case", false, 1, false, 2, 3] === 3
// Match
["match", "target", ["a", "set", "of", "target"], 1, 2] === 1
["match", "foo", ["a", "set", "of", "target"], 1, 2] === 2
["match", "foo", ["a", "set", "of", "target"], 1, ["foo"], 2, 3] === 2
["match", "foo", ["a", "set", "of", "target"], 1, ["bar"], 2, 3] === 3
Map Reduce
Using the "fn" and "fn-arg" operators, you can combine with "map"/"reduce"/"filter".
// Map
[
"map",
[1, 2, 3], // Collection
[
"fn", // Build a map function
[
"*",
["fn-arg", 0], // item
["fn-arg", 1], // index
]
]
] === [0, 2, 3]
// Reduce
[
"reduce",
[1, 2, 3], // Collection
[
"fn", // Build a reduce function
[
"*",
["fn-arg", 0], // carry
["fn-arg", 1], // item
]
],
2 // initial value
] === 12
// Filter
[
"filter",
[1, 2, 3],
[
"fn",
[
">=",
2,
["fn-arg", 0]
]
]
] === [2, 3]
// Find
[
"find",
[1, 2, 3],
[
"fn",
[
"<"
2,
["fn-arg", 0]
]
]
] === 3
GIS Language Enhancements (@alpaca-travel/fexp-js-lang-gis)
The optional GIS language enhancements provides language enhancements for working with GIS based scripting requirements.
- Boolean comparisons; geo-within, geo-contains, geo-disjoint, geo-crosses, geo-overlap
Developing and Extending
Installation
yarn add @alpaca-travel/fexp-js @alpaca-travel/fexp-js-lang
Optionally installs
yarn add @alpaca-travel/fexp-js-lang-gis
API Surface
compile(expr, lang)
Compiles the expression into a function for speed in repeat use.
import { compile } from "@alpaca-travel/fexp-js";
import lang from "@alpaca-travel/fexp-js-lang";
// Simple expression
const expr = ["==", ["get", "foo"], "bar"];
// Compile into function to evaluate
const {
source, // <-- Source JS
compiled // <-- Function
} = compile(expr, lang);
// Execute the compiled function against a context
const result = compiled(lang, { foo: "bar" });
console.log(result); // <-- true
evaluate(expr, lang, context)
Evaluates an expression without use of compilation (so is therefore slower than compiling).
import { evaluate } from "@alpaca-travel/fexp-js";
import lang from "@alpaca-travel/fexp-js-lang";
// Simple expression
const expr = ["==", ["get", "foo"], "bar"];
// Execute the compiled function against a context
const result = evaluate(expr, lang, { foo: "bar" });
console.log(result); // <-- true
langs(lang1, [, lang2, lang3, ..., langN])
Composites langs together to mix in different function support
import { langs } from "@alpaca-travel/fexp-js";
// Lang modules offered
import std from "@alpaca-travel/fexp-js-lang";
import gis from "@alpaca-travel/fexp-js-lang-gis";
// Custom library with your own modues
import myLib from "./my-lib";
// Composite the languages, mixing standard, gis and custom libs
const lang = langs(std, gis, myLib);
// Evaluate now with support for multiple
evaluate(["all", ["my-function", "arg1"], ["==", "foo", "foo"]], lang);
console.log(result); // <-- true
Adding Custom Functions
import { langs } from "@alpaca-travel/fexp-js";
import std from "@alpaca-travel/fexp-js-lang";
// Implement a sum function to add resolved values
const sum = args => args.reduce((c, t) => c + t);
// Build an expression
const expr = ["sum", 1, 2, 3, 4];
// Add the "sum" function to the standard lang
const lang = langs(std, { sum });
// Compile for execution
const { compiled: exprFn } = compile(expr, lang);
// Process the compiled function
console.log(exprFn(lang)); // <-- 10
Accessing Context
You functions are provided with the signature fn(args, context)
. Context allows you to access the runtime context.
const context = {
vars: {},
prior: ...
}
When your function is invoked, the special vars of "arguments" is assigned the function arguments. In the case of using expressions (using API compiled or evaluate), they are assigned to vars.
const lang = {
// Capture the context and args
['my-function']: (args, context) => return { args, context };
}
// Execute to capture
const result = evaluate(["my-function", "farg1", "farg2"], lang, "arg1", "arg2");
console.log(result);
/*
{
args: ["farg1", "farg2"],
context: {
vars: {
arguments: ["arg1", "arg2"]
},
...
}
}
*/
By executing a function in your expression (e.g. by calling "fn" to create a sub-function), when invoked will contain a new context, and the context vars "arguments" contain the arguments passed to your function. By using "fn-args" (in the standard library), you can access the function arguments by index.
Embedding in MongoDB
MongoDB offers support for providing fexp-js JavaScript expression in the \$where clause via to the V8 Runtime scripting engine.
Template for String expression
Below is a verbose example of creating a string JavaScript expression with a compiled expression and fexp-js-lang runtime.
If you are creating your own language extensions, you will need to compile your lang additions using your preferred development environment into a target platform (e.g. rollup build configuration, see packages/fexp-js-lang/rollup.config.js for example) to provide MongoDB your language implementation.
const fs = require("fs");
const path = require("path");
const { compile } = require("@alpaca-travel/fexp-js");
const lang = require("@alpaca-travel/fexp-js-lang");
// Compile your expression with your lang
const { source } = compile(["==", ["get", "foo"], "foobar"], lang);
// Obtian your compiled lang source (example shows the IIFE named export of fexp-js-lang)
const langSource = fs.readFileSync(
path.resolve(__dirname, "./node_modules/fexp-js-lang/dist/index-inc.js")
);
// Build the MongoDB string JavaScript expression
// Substitute in the 2 compiled components; source and your language source
const expression = `function() {
// Lang source is named 'lang' (e.g. var lang = ... )
${langSource}
// Our Compiled fn
const sub = function() {
${source}
}
return sub(lang, this);
}`;
// Output the expression
console.log(expression);
// Use in MongoDB $where operator
// https://docs.mongodb.com/manual/reference/operator/query/where/
// db.players.find({ $where: expression })
Contributing
- This package uses lerna
- Builds are done using rollup
Testing
$ cd packages/fexp-js
$ yarn && yarn test
Benchmarking
$ cd packages/fexp-js
$ yarn && yarn build && yarn benchmark
Generating Documentation
$ docsify init ./docs
$ docsify serve ./docs
4 years ago
4 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago