ploson
v3.1.6
Published
Programming Language On Serializable Object Notation
Downloads
74
Maintainers
Readme
🥋 ploson
Programming Language On Serializable Object Notation (JSON)
Build your own DSL on JSON syntax to store safe executable code 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
await
ed which means that it doesn't matter if it's a promise or not, becauseawait 5
is evaluated to5
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: '[email protected]' }, 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: '`[email protected]`' });
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
overonEnter
in most cases.
Thanks to / Inspired by
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.
- Now adds a
- Removed
lastArg
alias. - Added
Boolean
todefaultEnv
.
Licence
Hippocratic License Version 2.1