joyce
v1.2.4
Published
An AST-based Expression Language
Downloads
16
Maintainers
Readme
Joyce
An AST-based Expression Language
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