joyce

1.2.4 • Public • Published

Joyce

An AST-based Expression Language

Version Travis License

Joyce is a powerful expression language for dynamic data processing. It's particularly useful for processing configuration data.

Installation

Local Installation

npm i joyce

Global Installation

Use the global install if you want to use the joyce command-line tool.

npm i -g joyce

Usage

Joyce accepts any type of value. Expressions are represented inside strings as {{<expression>}}. Expressions are parsed and evaluated within the context of the originally given value.

A Basic Example

const Joyce = require('joyce');
 
Joyce({
    hello: 'world',
    isWorld: '{{hello == "world"}}'
});
 
// {
//     hello: 'world',
//     isWorld: true
// }

More Examples

Refer to unit tests for many many more examples.

const Joyce = require('joyce');
 
Joyce({
    foo: [ 1, 2, 3, 4 ],
    bar: '{{filter >= foo 3}}',
    baz: '{{join "-" foo}}',
    qux: '{{foo}}, a "note" to follow {{"foo"}}'
});
 
// {
//     foo: [ 1, 2, 3, 4 ],
//     bar: [ 3, 4 ],
//     baz: '1-2-3-4',
//     qux: 'bar, a note to follow foo'
// }

Command-Line Usage

echo '{"foo":"bar","bar":"{{== foo \"bar\"}}"}' | joyce
 
# { 
#     "foo": "bar", 
#     "bar": true 
# } 

Expressions

All expressions are strings and take the following form.

{{expression}}

Expression can be used at any position within a string.

plain text {{expression}} plain text

Multiple expressions can be used in a single string.

plain text {{expression}} plain text {{expression}} plain text

Nesting expressions is supported, but not arbitrarily. If you which to nest expressions you must use the eval operation (see examples below). Expressions can only be nested one level deep.

{{expression eval{"{{expression}}"}}}

For expressions containing a single operator argument, the operator may be in any position. This allows you to choose your own style depending on whether you prefer polish notation or infix notation.

{{operator ...operands}}
{{operand operator operand}}

For expressions containing an operation which acts upon an operator, the operation name must precede the operator.

{{operation operator ...operands}}

Operation names are case insensitive.

{{OPERATION operator ...operands}}

API

==

Loose equality

Joyce({
    foo: 123,
    bar: '{{foo == "123"}}',
    baz: '{{== foo "123"}}'
});
 
// {
//   foo: 123,
//   bar: true,
//   baz: true
// }

===

Strict equality

Joyce({
    foo: 123,
    bar: '{{foo === "123"}}',
    baz: '{{=== foo "123"}}'
});
 
// {
//   foo: 123,
//   bar: false,
//   baz: false
// }

!=

Loose inequality

Joyce({
    foo: 123,
    bar: '{{foo != "123"}}',
    baz: '{{!= foo "123"}}'
});
 
// {
//   foo: 123,
//   bar: false,
//   baz: false
// }

!==

Strict inequality

Joyce({
    foo: 123,
    bar: '{{foo !== "123"}}',
    baz: '{{!== foo "123"}}'
});
 
// {
//   foo: 123,
//   bar: true,
//   baz: true
// }

>

Greater than

Joyce({
    foo: 2,
    bar: '{{foo > 2}}',
    baz: '{{> foo 2}}'
});
 
// {
//   foo: 2,
//   bar: false,
//   baz: false
// }

>=

Greater than or equal to

Joyce({
    foo: 2,
    bar: '{{foo >= 2}}',
    baz: '{{>= foo 2}}'
});
 
// {
//   foo: 2,
//   bar: true,
//   baz: true
// }

<

Less than

Joyce({
    foo: 2,
    bar: '{{foo < 2}}',
    baz: '{{< foo 2}}'
});
 
// {
//   foo: 2,
//   bar: false,
//   baz: false
// }

<=

Less than or equal to

Joyce({
    foo: 2,
    bar: '{{foo <= 2}}',
    baz: '{{<= foo 2}}'
});
 
// {
//   foo: 2,
//   bar: true,
//   baz: true
// }

%

Modulus

Joyce({
    foo: 101,
    bar: '{{% foo 5}}',
    baz: '{{foo % 5}}'
});
 
// {
//   foo: 100,
//   bar: 1,
//   baz: 1
// }

+

Add

Joyce({
    foo: 1,
    bar: '{{+ foo 1}}',
    baz: '{{foo + 1}}'
});
 
// {
//   foo: 1,
//   bar: 2,
//   baz: 2
// }

Concat

Joyce({
    foo: 'a',
    bar: '{{foo + "b"}}',
    baz: '{{+ foo "b"}}'
});
 
// {
//   foo: 'a',
//   bar: 'ab',
//   baz: 'ab'
// }

-

Subtract

Joyce({
    foo: 1,
    bar: '{{foo - 1}}',
    baz: '{{- foo 1}}'
});
 
// {
//   foo: 1,
//   bar: 0,
//   baz: 0
// }

*

Multiply

Joyce({
    foo: 100,
    bar: '{{foo * 5}}',
    baz: '{{* foo 5}}'
});
 
// {
//   foo: 100,
//   bar: 500,
//   baz: 500
// }

/

Divide

Joyce({
    foo: 100,
    bar: '{{foo / 5}}',
    baz: '{{/ foo 5}}'
});
 
// {
//   foo: 100,
//   bar: 20,
//   baz: 20
// }

Operators

filter

Filter an array.

Joyce({
    foo: [ 1, 2, 3, 4, 5 ],
    bar: '{{filter % foo 2}}'
});
 
// {
//   foo: [ 1, 2, 3, 4, 5 ],
//   bar: [ 1, 3, 5 ]
// }
Joyce({
    foo: [ 1, 2, 3, 4 ],
    bar: '{{filter <= foo 2}}'
});
 
// {
//   foo: [ 1, 2, 3, 4 ],
//   bar: [ 1, 2 ]
// }

join

Join an array

Joyce({
    foo: [ "a", "b", "c" ],
    bar: '{{join "-" foo}}'
});
 
// {
//   foo: [ "a", "b", "c" ],
//   bar: "a-b-c"
// }

When only one arg is given, array elements will be joined by an empty string.

Joyce({
    foo: [ "a", "b", "c" ],
    bar: '{{join foo}}'
});
 
// {
//   foo: [ "a", "b", "c" ],
//   bar: "abc"
// }

every

Returns true if every element in the given array match the given predicate, otherwise returns false.

Call signature is {{every operator array comparison-value}}.

Joyce({
    foo: [ 1, 2, 3, 4, 5 ],
    bar: '{{every < foo 6}}'
});
 
// {
//   foo: [ 1, 2, 3, 4, 5 ],
//   bar: true
// }
Joyce({
    foo: [ 1, 2, 3, 4, 5 ],
    bar: '{{every > foo 4}}'
});
 
// {
//   foo: [ 1, 2, 3, 4, 5 ],
//   bar: false
// }

some

Returns true if some of the elements in the given array match the given predicate, otherwise returns false.

Call signature is {{some operator array comparison-value}}.

Joyce({
    foo: [ 1, 2, 3, 4, 5 ],
    bar: '{{some > foo 4}}'
});
 
// {
//   foo: [ 1, 2, 3, 4, 5 ],
//   bar: true
// }

find

Returns first element in a given array which matches a given predicate.

Call signature is {{find operator array comparison-value}}.

Joyce({
    foo: [ 1, 2, 3, 4, 5 ],
    bar: '{{find > foo 3}}'
});
 
// {
//   foo: [ 1, 2, 3, 4, 5 ],
//   bar: 4
// }

map

Performs a binary operation on each element in a given array and return a new array.

Call signature is {{map operator array subject-value}}.

Joyce({
    foo: [ "file1", "file2", "file3" ],
    bar: '{{map + foo ".png"}}'
});
 
// {
//   foo: [ "file1", "file2", "file3" ],
//   bar: [ "file1.png", "file2.png", "file3.png" ]
// }

sum

(aliased as concat)

Adds or concats the elements of a given array.

Joyce({
    foo: [ 1, 2, 3, 4, 5 ],
    bar: '{{sum foo}}'
});
 
// {
//   foo: [ 1, 2, 3, 4, 5 ],
//   bar: 15
// }
Joyce({
    foo: [ "a", "b", "c" ],
    bar: '{{sum foo}}'
});
 
// {
//   foo: [ "a", "b", "c" ],
//   bar: "abc"
// }

concat

(alias for sum)

product

Multiply the elements of a given array.

Joyce({
    foo: [ 1, 2, 3, 4, 5 ],
    bar: '{{product foo}}'
});
 
// {
//   foo: [ 1, 2, 3, 4, 5 ],
//   bar: 96
// }

keys

Returns an array containing a given object's keys.

Joyce({
    foo: {
        baz: "bim",
        bam: "boom"
    },
    bar: '{{keys foo}}'
});
 
// {
//   foo: {
//     baz: "bim",
//     bam: "boom"
//   },
//   bar: [ "baz", "bam" ]
// }

values

Returns an array containing a given object's values.

Joyce({
    foo: {
        baz: "bim",
        bam: "boom"
    },
    bar: '{{values foo}}'
});
 
// {
//   foo: {
//     baz: "bim",
//     bam: "boom"
//   },
//   bar: [ "bim", "boom" ]
// }

ternary

Express simple conditions as a ternary.

Joyce({
    foo: true,
    bar: '{{foo ? "foo is true" : "foo is false"}}'
});
 
// {
//   foo: true,
//   bar: "foo is true"
// }

eval

Nest joyce expressions with eval.

Note: If the expression passed to eval contains spaces you must quote the expression.

Joyce({
    foo: true,
    bar: '{{foo ? eval{"foo is {{boom}}"} : eval{"foo is {{splat}}"}}}',
    boom: 'bam',
    splat: 'squash'
});
 
// {
//   foo: true,
//   bar: "foo is bam",
//   boom: 'bam',
//   splat: 'squash'
// }

Operators

For operations which accept operator arugments, the follower operators are defined.

===
==
>
>
<
<
>=
<=
%
+
+
-
*
/

Explict Type Casting

There are times when you will want to explicitly cast arguments as a certain type.

For instance, consider the following.

Joyce({
    "==": "foo",
    bar: '{{== == "foo"}}'
});
 
// {
//   foo: "==",
//   bar: false
// }

Bar should be true, but Joyce has cast the == as an operation instead of a reference.

This is easily fixed.

Joyce({
    "==": "foo",
    bar: '{{ref{"=="} == "foo"}}'
});
 
// {
//   foo: "==",
//   bar: true
// }

Note that we not only wrap the == argument in ref{} but we also quote it. Anything in which you would have to use array-notation in JavaScript when referencing a property must be quoted.

All argument types can be explictly cast if you find that the need arises.

The casting notation is always: type{value}

The following types are supported:

  • ref
  • str
  • num
  • obj
  • arr
  • bool

Deep Property Resolution

You can evaluate deeply nested values in Joyce by referencing a keypath.

Joyce({
    sis: {
        boom: {
            bah: [ 'foo', 'bar', 'baz' ]
        }
    },
    bar: '{{sis.boom.bah[1] == "bar"}}'
});

// {
//   sis: {
//     boom: {
//       bah: [ 'foo', 'bar', 'baz' ]
//     }
//   },
//   bar: true
// }

Why an AST-based EL?

Security matters.

Most ELs are open to security vulnerabilities because they depend on dynamic code evaluation. Like any EL, Joyce also depends on dynamic code evaluation, but what differentiates Joyce from most other ELs is that it does very little direct evaluation of user input. Almost all user input in parsed into tightly restricted AST forms. Joyce uses astring to emit code from the resulting AST and it is only these curated code strings which are evaluated.

This mediation via AST provides a level of control and sanitization that makes Joyce one of the safest ELs out there.

In the interest of full transparency, there is one code path in Joyce which will result in direct evaluation of unsanitized user input: deep property evaluation (i.e. {{foo.bar.baz}}). Measures have been taken to ensure that this code path is not exploitable. See unit tests for coverage of this possible attack vector.

Using Joyce with Config Files

You have options...

'use strict';
const globble = require('globble');
const Joyce = require('Joyce');
 
(async () => {
    const data = await globble('./config/**', { cwd: __dirname, clobber: true });
    console.log(Joyce(data));
})();

Or...

cat foo.json | joyce > result.json

Or, use rc or answers or any other option out there for gathering config data and then just pass it to Joyce.

License

MIT

Package Sidebar

Install

npm i joyce

Weekly Downloads

3

Version

1.2.4

License

MIT

Unpacked Size

35.3 kB

Total Files

11

Last publish

Collaborators

  • machellerogden