ploson

3.1.5 • Public • Published

🥋 ploson

Programming Language On Serializable Object Notation (JSON)

Build your own DSL and have contracts as JSON documents stored in a database.

There are many "JSON Lisp" type of packages, but they're often too narrow in functionality and have a lengthy and arduous syntax meant to be generated rather than manually written. The point with these type of modules is often to enable domain specific languages (DSL) with a DB friendly syntax (JSON) - and so is Ploson. But it's:

  • Human developer friendly (made to easily write by hand).
  • Supporting asynchronous, also concurrent.
  • Customizable regarding available environment identifiers.
    • Build your own API/DSL.
  • Secure.
    • eval & other unsafe constructs are forbidden and impossible to access.
  • Functional, with a Lisp-like syntax suitable for JSON.
  • Easy to learn, yet powerful.
  • Implemented with a plugin based system.
  • Thoroughly tested.
    • 100% test coverage for the default plugin setup.

Getting started

Recommended basic setup:

import { createRunner, defaultEnv } from 'ploson';
import * as R from 'ramda';

const ploson = createRunner({
  staticEnv: { ...defaultEnv, R },
});

Now run the created ploson parser:

await ploson(['R.map', ['R.add', 3], ['of', 3, 4, 5]]);
// -> [6, 7, 8]

Utilize some built-in features to get more control and readability:

await ploson([
  ',',
  {
    data: ['of', 3, 4, 5],
    func: ['R.map', ['R.add', 3]],
  },
  ['$func', '$data'],
]);
// -> [6, 7, 8]

Ploson does not include data processing utilities since Ramda is perfect to add to Ploson in order to be able to build any pure function only through functional composition. Operators are also not included in Ploson, because they are sometimes flawed, not fit for FP, and suitably implemented in Ramda. More on this below.

Primitives

await ploson(2); // -> 2
await ploson(3.14); // -> 3.14
await ploson(true); // -> true
await ploson(null); // -> null
await ploson(undefined); // -> undefined

String Syntax

await ploson('`hello world`'); // -> "hello world"

Would be considered an identifier without the backticks.

Array Syntax: Calling Functions

With Ramda added, run like:

await ploson(['R.add', 1, 2]); // -> 3

Easily create arrays with built in of method:

await ploson(['of', 1, true, '`foo`']); // -> [1, true, "foo"]

Create a Date object with time now (requires defaultEnv):

await ploson(['newDate']); // -> Mon Jul 05 2021 19:41:35 GMT+0200 (Central European Summer Time)

Nest calls in a classic FP manner:

await ploson(['R.add', 7, ['Math.floor', ['R.divide', '$myAge', 2]]]);
// -> Minimum acceptable partner age?

Empty Array

An empty array is not evaluated as a function, but left as is. This means that there are 2 ways to define an empty array:

await ploson([]); //     -> []
await ploson(['of']); // -> []

Object Syntax

  • Returns the object, evaluated
  • Will put key/value pairs in the variable scope as an automatic side effect
  • Parallel async

Let's break these points down

It returns the object:

await ploson({ a: 1 }); // -> { a: 1 }

...evaluated:

await ploson({ a: '`hello world`' }); // -> { a: "hello world" }
await ploson({ a: ['of', 1, 2, 3] }); // -> { a: [1, 2, 3] }

Keys & values will be automatically put in a per-parser variable scope, and accessed with prefix $:

await ploson({ a: '`foo`', b: '`bar`' }); // VARS: { a: "foo", b: "bar" }
await ploson({ a: ['of', 1, 2, '$b'] }); // VARS: { a: [1, 2, "bar"], b: "bar" }

Objects are treated as if its members were run with Promise.all — Async & in parallel:

await ploson({ user: ['fetchProfile', '$uId'], conf: ['fetchConfiguration'] });
// VARS: { user: <result-from-fetchProfile>, conf: <result-from-fetchConfiguration> }

Note: Each object value is awaited which means that it doesn't matter if it's a promise or not, because await 5 is evaluated to 5 in JavaScript.

Adding the use of our amazing comma operator , (see Built-in functions below), we can continue sequentially after that parallel async work:

await ploson([
  ',',
  { user: ['fetchProfile', '$uId'], conf: ['fetchConfiguration'] }, // parallell async
  { name: ['R.propOr', '$conf.defaultName', '`name`', '$user'] },
]);
// -> { name: 'John Doe' }

If the same parser would be used for all of the examples above in this section, the variable scope for that parser would now contain (of course depending on what the fetcher functions return):

{
  a: [1, 2, "bar"],
  b: "bar",
  user: { name: "John Doe", /*...*/ },
  conf: { defaultName: "Noname", /*...*/ },
  name: "John Doe",
}

The Static Environment (available functions)

Plugins Built-in Functions

Plugins add functions to the static environment scope (that you will get even without providing anything to staticEnv for the parser creation).

You would have to override these if you want to make them unavailable.

envPlugin

Function name Implementation Comment
of Array.of We should always be able to create arrays.
, R.pipe(Array.of, R.last) Evaluate the arguments and simply return the last one. (see JS's comma operator)
void () => undefined Could be used like ,, if no return value is desired.

varsPlugin

Function name Arguments
getVar string (path)
setVar string (path), any (value)

The above functions are alternatives to $ prefix and object setting respectively.

The Available defaultEnv

As seen in the example under "Getting Started" above, we can add defaultEnv to staticEnv to populate the environment scope with a bunch of basic JavaScript constructs. defaultEnv includes:

Function name Comment
undefined
console Access native console API.
Array Access native Array API.
Object Access native Object API.
String Access native String API.
Number Access native Number API.
Boolean Access native Number API.
Promise Access native Promise API.
newPromise Create Promises.
Date Access native Date API.
newDate Create Dates.
Math Access native Math API.
parseInt The native parseInt function.
parseFloat The native parseFloat function.
Set Access native Set API.
Map Access native Map API.
newSet Create Sets.
newMap Create Maps.
RegExp Create RegExps & access RegExp API.
fetch The fetch function. Works in both Node & Browser

Operators, Type Checks etc.

Ploson is not providing any equivalents to JavaScript's operators. Firstly because they have to be functions and so operators makes no full sense in a lisp-like language. Secondly because many JavaScript operators are problematic in implementation and sometimes not suitable, or ambiguous, for functional style and composition (if simply lifted into functions). Lastly because Ramda provides alternatives for all of the necessary operators, better implemented and suitable for functional language syntax. You could make aliases for these if you absolutely want, and that would start out something like this:

{
  '>': R.gt,
  '<': R.lt,
  '<=': R.lte,
  '>=': R.gte,
  '==': R.equals,
  '!=': R.complement(R.equals),
  '!': R.not,
}

For type checking I personally think that lodash has the best functions (all is*). There is also the library is.

For any date processing, the fp submodule of date-fns is recommended.

Ramda Introduction

Ramda is a utility library for JavaScript that builds on JS's possibilities with closures, currying (partially applying functions), first rate function values, etc. It provides a system of small generic composable functions that can, through functional composition, be used to build any other pure data processing function. This truly supercharges JavaScript into declarative expressiveness and immutability that other languages simply can not measure up to.

Ramda is the perfect tool belt for Ploson.

The ability to build any function by only composing Ramda functions means never having to specify another function head (argument parenthesis) ever again (this is a stretch, but possible). Here is an example:

// ES6+ JavaScript:
const reducer = (acc, item = {}) =>
  item && item.x ? { ...acc, [item.x]: (acc[item.x] || 0) + 1 } : acc;

// "The same function" with Ramda:
const reducer = R.useWith(R.mergeWith(R.add), [
  R.identity,
  R.pipe(
    R.when(R.complement(R.is(Object)), R.always({})),
    R.when(R.has('x'), R.pipe(R.prop('x'), R.objOf(R.__, 1))),
  ),
]);

This can feel complex and limiting, so Ploson provides a "lambda" or "arrow function" syntax.

Lambda/Arrow Function Syntax =>

A lambdaPlugin provides an arrow function syntax.

await ploson(['=>', ['x'], ['R.add', 3, '$x']]);

would be the same as x => x + 3 in JS.

The above example is somewhat unrealistic since you would simplify it to ['R.add', 3]

Any single argument function is easier written with only Ramda and does not require this lambda syntax.

A more realistic example is when you need a reduce iterator function (2 arguments), perhaps also with some default value for one of the arguments:

await ploson([
  '=>',
  ['acc', ['x', 1]],
  ['R.append', ['R.when', ['R.gt', 'R.__', 4], ['R.divide', 'R.__', 2], '$x'], '$acc'],
]);

Inside a lambda function:

  • Arguments share the same variable scope as all other variables (outside the function).
    • No local variables possible.
  • Object syntax is synchronous, it will not have any async behaviour as it has outside a lambda.

The lambdaPlugin requires envPlugin & varsPlugin (and doesn't make sense without evaluatePlugin).

Lambda Function Shorthands

Examples that highlight special cases of arrow syntax:

await ploson(['=>']); // -> () => undefined, Same as 'void'
await ploson(['=>', 'x', '$x']); // -> (x) => x, The identity function
await ploson(['=>', 'Math.PI']); // -> () => Math.PI

The last example means that it is possible to leave out arguments if the function should not have any.

Security

Blocked identifiers:

  • eval
  • Function
  • constructor
  • setTimeout, setInterval

The Function constructor is similar to the eval function in that it accepts a string that is evaluated as code. The timer functions as well.

These identifiers are forbidden even if they are added to the environment scope.

Customizing Default Plugins

varsPlugin

The constructor of the varsPlugin accepts:

Parameter Type Default Comment
prefix string $
vars Object {} The parser variable scope. Will be mutated.

To customize the varsPlugin with above parameters, one has to explicitly define the list of plugins (the order matters):

import {
  createRunner,
  defaultEnv,
  lambdaPlugin,
  envPlugin,
  evaluatePlugin,
  varsPlugin,
} from 'ploson';
import * as R from 'ramda';

const ploson = createRunner({
  staticEnv: { ...defaultEnv, R },
  plugins: [
    lambdaPlugin(),
    envPlugin(),
    evaluatePlugin(),
    varsPlugin({ vars: { uId: 'john@doe.ex' }, prefix: '@' }),
  ],
});

If you only want to initialize the variable scope however, instead of having to import all plugins and define the plugins property, you could simply do this directly after creation of the parser:

await ploson({ uId: '`john@doe.ex`' });

Yet another way to get this uId value into a parser is of course to add it to staticEnv (and reference it without prefix $).

Writing Custom Plugins

This is the default plugin sequence:

[lambdaPlugin(), envPlugin(), evaluatePlugin(), varsPlugin()];

If you want to add or modify on the plugin level, just modify the above plugins line (the order matters).

A stub for writing a custom plugin:

export const myPlugin = () => ({
  staticEnv: {
    /* ... */
  },
  onEnter: ({
    state,
    envHas,
    getFromEnv,
    originalNode,
    processNode,
    processNodeAsync,
    current,
  }) => {
    /* ... */
  },
  onLeave: ({
    state,
    envHas,
    getFromEnv,
    originalNode,
    processNode,
    processNodeAsync,
    node,
    current,
  }) => {
    /* ... */
  },
});

Both onEnter and onLeave functions should return undefined or one of 3 events:

  • { type: 'ERROR', error: Error('MSG') }
  • { type: 'REPLACE', current: X }
  • { type: 'PROTECT', current: X }

Recommended to use onLeave over onEnter in most cases.

Thanks to / Inspired by

kanaka's miniMAL

I have used miniMAL (extended a bit) as a DSL for a couple of years, and all my experience around that went into making Ploson.

Change Log

  • 3.1
    • Shorthand lambda function support
    • Building multiple bundle formats
  • 3.0
    • Lambda plugin providing syntax to create functions.
    • Remade error handling.
      • Now adds a plosonStack property instead of adding recursively to the message.
    • Removed lastArg alias.
    • Added Boolean to defaultEnv.

Licence

Hippocratic License Version 2.1

Package Sidebar

Install

npm i ploson

Weekly Downloads

1

Version

3.1.5

License

Hippocratic-2.1

Unpacked Size

189 kB

Total Files

21

Last publish

Collaborators

  • ez