An AST-based Expression Language
Joyce is a powerful expression language for dynamic data processing. It's particularly useful for processing configuration data.
npm i joyce
Use the global install if you want to use the
joyce
command-line tool.
npm i -g joyce
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.
const Joyce = require('joyce');
Joyce({
hello: 'world',
isWorld: '{{hello == "world"}}'
});
// {
// hello: 'world',
// isWorld: true
// }
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'
// }
echo '{"foo":"bar","bar":"{{== foo \"bar\"}}"}' | joyce
# {
# "foo": "bar",
# "bar": true
# }
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}}
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
// }
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 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"
// }
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
// }
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
// }
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
// }
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" ]
// }
(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"
// }
(alias for sum
)
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
// }
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" ]
// }
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" ]
// }
Express simple conditions as a ternary.
Joyce({
foo: true,
bar: '{{foo ? "foo is true" : "foo is false"}}'
});
// {
// foo: true,
// bar: "foo is true"
// }
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'
// }
For operations which accept operator arugments, the follower operators are defined.
===
==
>
>
<
<
>=
<=
%
+
+
-
*
/
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
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
// }
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.
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.
MIT