npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

json-rule-processor

v1.2.3

Published

Load and run async "JSON-Lisp" configurations - with triggers, conditions, actions & more

Downloads

17

Readme

json-rule-processor

Load and run async "JSON-Lisp" configurations - with triggers, conditions, actions & more.

This package is meant to give a ton of possibilities to what a JSON/JS configuration can do. It will turn a serializable JS object notation into a functional asynchronous programming language, optionally packaged as a "rule configuration".

miniMAL syntax

The rule config part of this package is the last step in a small staircase of abstractions, where the first step is miniMAL which is an awesome invention by Joel Martin. Go check out miniMAL first, to get a hunch about the strange "json lisp" syntax that this library uses and will appear in all examples below.

Extended parser

But as a first abstraction on where miniMAL leaves off, minimalLispParser is a bit more real world useful:

import { minimalLispParser } from 'json-rule-processor/dist/minimal-lisp-parser';

const parserOptions = { envExtra: { add5: x => x + 5 } };
const parser = minimalLispParser(parserOptions);

const cmd = ['add5', 3];
parser.evaluate(cmd); // -> 8

minimalLispParser - options

  • env: Object of the entire additional set of functions and identifiers to add to the parser. If this is not used, the default set of functionality is added, see below.
  • envExtra = {}: This is where additional functions can be added to the parser, on top of the default set.
  • keepJsEval = false: By default, eval of JavaScript in strings is turned off for security reasons, but can be activated with this flag.
  • doLog: When using parser.evaluate, this tells the parser to log to the console the input and output of the evaluation.

The default set of JavaScript functions/identifiers in the extended parser:

  undefined,
  typeof: a => typeof a, // renaming of miniMAL's 'type'
  '>': (a, b) => a > b,
  '<=': (a, b) => a <= b,
  '>=': (a, b) => a >= b,
  '==': (a, b) => Object.is(a, b),
  '!=': (a, b) => !Object.is(a, b),
  '===': (a, b) => a === b,
  '!==': (a, b) => a !== b,
  '%': (a, b) => a % b,
  get,
  Array,
  Object,
  String,
  Number,
  Promise,
  Date,
  Math,
  setInterval,
  setTimeout,
  parseInt,
  parseFloat,
  Set,
  Map,
  RegExp,
  fetch,
  console,
  log: console.log,

More extended parser

As a second step, this package offers functional programming utilities (ramda & date-fns) to the very basic set of functions offered by miniMAL itself, and it can also add a controlled scope of variables that can be used as a bridge to surrounding JavaScript:

import { functionalParserWithVars } from 'json-rule-processor/dist/minimal-lisp-parser';

const vars = { value: 5 };
const parser = functionalParserWithVars(vars, parserOptions); // parserOptions like above

const cmd = ['var', ['`', 'result'], ['D.addSeconds', ['var', ['`', 'value']], ['new', 'Date']]];
parser.evaluate(cmd);

IN PSEUDO: vars.result = new Date() + seconds(vars.value)

vars.result will contain a Date object representing 5 seconds from now. (The return value of the parse will also be this value).

The functional programming utilities that are added in the functionalParserWithVars are Ramda & Date-fns/FP (select FP in the drop-down in the top right corner of the docs). They are accessed through R. & D..

The var command gets a variable from the variable scope if it is given one argument. If a value is given after the variable name (2 arguments), it is instead assigned. The first parameter can be written as a path, since it uses lodash.get.

miniMAL command blocks (sync/async)

The next abstraction utility that builds on the above is a possibility of running blocks of miniMAL code to be run both synchronously, and asynchronously in parallel:

A block is an Array of miniMAL commands, run in sequence/imperatively: [['log', true], ['+', 1, 2]].

A variant of this is an Array of Objects, where everything in the same object will be run in parallel, and in sequence if commands are in different objects. The keys becomes variables inside vars and may be referenced later in the block:

import { asyncBlockEvaluator } from 'json-rule-processor/dist/minimal-lisp-parser';

const parserOptions = {
  envExtra: {
    fetcher: url => /*...*/,
    rpc: (id, args) => /*...*/,
  },
};
const parser = functionalParserWithVars(...[, parserOptions]); // default vars = {}

const cmdBlock = [
  { position: ['fetcher', ['`', '?f=locationData']] },
  {
    weather: [
      'rpc',
      ['`', 'readWeather'],
      ['R.objOf', ['`', 'position'], ['var', ['`', 'position']]],
    ],
    indoorTemp: ['rpc', ['`', 'getTemperature']],
  },
  { tempDiff: ['-', ['var', ['`', 'weather.parameters.temp']], ['var', ['`', 'indoorTemp']]] },
];

await asyncBlockEvaluator(parser, cmdBlock);

Basically, a somewhat corresponding JavaScript version of above cmdBlock would be:

vars.position = await fetcher('?f=locationData');
[vars.weather, vars.indoorTemp] = await Promise.all([
  rpc('readWeather', { position: vars.position }),
  rpc('getTemperature'),
]);
vars.tempDiff = vars.weather.parameters.temp - vars.indoorTemp;

Rule Processor

Finally we are at the last step of the abstractions staircase, where "rules" are possible. These rules are defined by configurations containing a set of keys defined by rule-dm.js.

Rule Data Model

| path | type | presence | description | default | conforms | | ------------------ | ------- | -------- | ------------------------------------------------------------------------------------ | ------- | -------- | | id | string | optional | Identifier for this particular rule. | | | | active | boolean | optional | If the rule is active or not. An inactive rule is not run at all. | false | | | ttl | date | optional | At this time (ISO timestamp) the rule will be set to inactive. | | | | cooldown | number | optional | A rule can't be triggered again unless this number of seconds has passed. | | >=0 | | onLoad | array | optional | MiniMAL command block to run when rule is loaded. | | | | onLoad[x] | object | optional | | | | | onLoad[x] | array | optional | | | >=1 | | onLoad[x][x] | string | required | | | | | onLoad[x][x] | any | optional | | | | | process | array | optional | MiniMAL command block to run when rule is triggeed, before condition. | | | | process[x] | object | optional | | | | | process[x] | array | optional | | | >=1 | | process[x][x] | string | required | | | | | process[x][x] | any | optional | | | | | condition | array | optional | MiniMAL command to check if rule should execute (state to flipped, run actions etc). | | >=1 | | condition[x] | string | required | | | | | condition[x] | any | optional | | | | | actions | array | optional | MiniMAL command block to execute when condition is true (& not in flipped state). | | | | actions[x] | object | optional | | | | | actions[x] | array | optional | | | >=1 | | actions[x][x] | string | required | | | | | actions[x][x] | any | optional | | | | | resetCondition | array | optional | MiniMAL command to check if rule should reset, if it is in flipped state. | | >=1 | | resetCondition[x] | string | required | | | | | resetCondition[x] | any | optional | | | | | resetActions | array | optional | MiniMAL command block to execute when resetCondition is true. | | | | resetActions[x] | object | optional | | | | | resetActions[x] | array | optional | | | >=1 | | resetActions[x][x] | string | required | | | | | resetActions[x][x] | any | optional | | | |

Full Rule Example

import { load } from 'json-rule-processor/dist';

const conf = {
  active: true,
  cooldown: 3,
  onLoad: [{ msg: ['subscribe', ['`', 'temperature']] }],
  process: [
    { position: ['fetcher', ['`', '?f=locationData']] },
    {
      weather: [
        'rpc',
        ['`', 'readWeather'],
        ['R.objOf', ['`', 'position'], ['var', ['`', 'position']]],
      ],
    },
    { tempDiff: ['-', ['var', ['`', 'weather.parameters.temp']], 20] },
    {
      tooCold: ['<', ['var', ['`', 'tempDiff']], -2],
      closeEnough: ['>', ['var', ['`', 'tempDiff']], -0.5],
    },
  ],
  condition: ['var', ['`', 'tooCold']],
  actions: [['rpc', ['`', 'startHeater']]],
  resetCondition: ['var', ['`', 'closeEnough']],
  resetActions: [['rpc', ['`', 'stopHeater']]],
};

const parserOptions = {
  envExtra: {
    fetcher: url => /*...*/,
    rpc: (id, args) => /*...*/,
  },
};

const runOptions = { parserOptions, reuseParser: true };

const client = { // Example pub/sub client
  sub: (channel, onMsg) => /*...*/,
};
let run;

const loadOptions = {
  // parserPatcher is ONLY needed for the special case of wanting a dynamic value into the object
  // key in the config ('msg' in this case). Normal case would be to put 'subscribe' in envExtra.
  parserPatcher: (parser, triggerKey) => {
    parser.subscribe = channel =>
      client.sub(channel, msg => {
        run({ ...runOptions, ...(triggerKey ? { vars: { [triggerKey]: msg } } : {}) });
      });
  },
  parserOptions,
};
run = await load(conf, loadOptions);

Notice that condition & resetCondition are plain miniMAL commands, and onLoad, process, actions & resetActions are miniMAL command blocks.

In the above example, we are utilizing the rare case of wanting to use a key from the onLoad config in a function. We achieve this through parserPatcher. The config we can then use is

onLoad: [{ msg: ['subscribe', ['`', 'temperature']] }];

where msg is used as the variable name for each message received after the subscription. The more straightforward way of doing this, that doesn't require parserPatcher, would be for subscribe to take a key name as additional argument.

load, at the bottom of the example, is the actual initiator of the whole rule processor.

Stateless Load

There is also a statelessLoad function if one wants to manage the state of each loaded rule explicitly. The statelessLoad returns a tuple with both state and run, like so:

import { statelessLoad } from 'json-rule-processor/dist';

/* ... */

let state;
let run;
[state, run] = statelessLoad(conf, loadOptions);
run(state, runOptions);

where the run function then also needs the state as first argument.