1.0.1-alpha.0 • Published 4 years ago

@alpaca-travel/fexp-js-lang v1.0.1-alpha.0

Weekly downloads
14
License
MIT
Repository
github
Last release
4 years ago

fexp-js (Functional Expressions for JS)

npmBuild StatusCoverage Statusnpm bundle size

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

Edit fexp-js-demo

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)

npm bundle size

  • 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);
  });
});

Edit fexp-js-demo

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)

npm bundle size

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
1.0.1-alpha.0

4 years ago

1.0.0-alpha.0

4 years ago

0.3.0

5 years ago

0.2.0

5 years ago

0.1.0

5 years ago

0.0.4

5 years ago

0.0.3

5 years ago

0.0.2

5 years ago

0.0.1

5 years ago