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

ploson

v3.1.6

Published

Programming Language On Serializable Object Notation

Downloads

74

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 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: '[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 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