json-e
Advanced tools
Comparing version 2.3.1 to 2.3.2
{ | ||
"name": "json-e", | ||
"version": "2.3.1", | ||
"version": "2.3.2", | ||
"description": "json parameterization module inspired from json-parameterization", | ||
@@ -5,0 +5,0 @@ "main": "./src/index.js", |
218
README.md
@@ -1,6 +0,8 @@ | ||
# [JSON-e](https://taskcluster.github.io/json-e) | ||
* [Full documentation](https://taskcluster.github.io/json-e) | ||
JSON-e is a data-structure parameterization system written for embedding | ||
context in JSON objects. | ||
# JSON-e | ||
JSON-e is a data-structure parameterization system for embedding context in | ||
JSON objects. | ||
The central idea is to treat a data structure as a "template" and transform it, | ||
@@ -23,2 +25,4 @@ using another data structure as context, to produce an output data structure. | ||
## JavaScript | ||
The JS module exposes following interface: | ||
@@ -50,2 +54,4 @@ | ||
## Python | ||
The Python distribution exposes a `render` function: | ||
@@ -56,5 +62,5 @@ | ||
var template = {"a": {"$eval": "foo.bar"}} | ||
var context = {"foo": {"bar": "zoo"}} | ||
print(jsone(template, contxt)) # -> {"a": "zoo"} | ||
template = {"a": {"$eval": "foo.bar"}} | ||
context = {"foo": {"bar": "zoo"}} | ||
print(jsone.render(template, context)) # -> {"a": "zoo"} | ||
``` | ||
@@ -65,5 +71,5 @@ | ||
```python | ||
var template = {"$eval": "foo(1)"} | ||
var context = {"foo": lambda x: x + 2} | ||
print(jsone(template, contxt)) # -> 3 | ||
template = {"$eval": "foo(1)"} | ||
context = {"foo": lambda x: x + 2} | ||
print(jsone.render(template, context)) # -> 3 | ||
``` | ||
@@ -83,4 +89,4 @@ | ||
```yaml | ||
template: {key: [1,2,{key2: 'val', key3: 1}, true], f: false} | ||
context: {} | ||
template: {key: [1,2,{key2: 'val', key3: 1}, true], f: false} | ||
result: {key: [1,2,{key2: 'val', key3: 1}, true], f: false} | ||
@@ -94,4 +100,4 @@ ``` | ||
```yaml | ||
template: {message: 'hello ${key}', 'k=${num}': true} | ||
context: {key: 'world', num: 1} | ||
template: {message: 'hello ${key}', 'k=${num}': true} | ||
result: {message: 'hello world', 'k=1': true} | ||
@@ -101,4 +107,5 @@ ``` | ||
The bit inside the `${..}` is an expression, and must evaluate to something | ||
that interpolates obviously into a string (so, a string, number, boolean, or | ||
null). The expression syntax is described in more detail below. | ||
that interpolates obviously into a string (so, a string, number, boolean,). | ||
If it is null, then the expression interpolates into an empty string. | ||
The expression syntax is described in more detail below. | ||
@@ -108,5 +115,5 @@ Values interpolate as their JSON literal values: | ||
```yaml | ||
template: ["number: ${num}", "booleans: ${t} ${f}", "null: ${nil}"] | ||
context: {num: 3, t: true, f: false, nil: null} | ||
template: ["number: ${num}", "booleans: ${t} ${f}", "null: ${nil}"] | ||
result: ["number: 3", "booleans: true false", "null: null"] | ||
result: ["number: 3", "booleans: true false", "null: "] | ||
``` | ||
@@ -117,4 +124,4 @@ | ||
```yaml | ||
template: {"tc_${name}": "${value}"} | ||
context: {name: 'foo', value: 'bar'} | ||
template: {"tc_${name}": "${value}"} | ||
result: {"tc_foo": "bar"} | ||
@@ -139,2 +146,3 @@ ``` | ||
```yaml | ||
template: {config: {$eval: 'settings.staging'}} | ||
context: | ||
@@ -146,3 +154,2 @@ settings: | ||
transactionBackend: customerdb | ||
template: {config: {$eval: 'settings.staging'}} | ||
result: {config: {transactionBackend: 'mock'}} | ||
@@ -160,4 +167,4 @@ ``` | ||
```yaml | ||
template: {$json: [a, b, {$eval: 'a+b'}, 4]} | ||
context: {a: 1, b: 2} | ||
template: {$json: [a, b, {$eval: 'a+b'}, 4]} | ||
result: '["a", "b", 3, 4]' | ||
@@ -173,4 +180,4 @@ ``` | ||
```yaml | ||
template: {key: {$if: 'cond', then: 1}, k2: 3} | ||
context: {cond: true} | ||
template: {key: {$if: 'cond', then: 1}, k2: 3} | ||
result: {key: 1, k2: 3} | ||
@@ -180,4 +187,4 @@ ``` | ||
```yaml | ||
template: {$if: 'x > 5', then: 1, else: -1} | ||
context: {x: 10} | ||
template: {$if: 'x > 5', then: 1, else: -1} | ||
result: 1 | ||
@@ -187,4 +194,4 @@ ``` | ||
```yaml | ||
template: [1, {$if: 'cond', else: 2}, 3] | ||
context: {cond: false} | ||
template: [1, {$if: 'cond', else: 2}, 3] | ||
result: [1,2,3] | ||
@@ -194,4 +201,4 @@ ``` | ||
```yaml | ||
template: {key: {$if: 'cond', then: 2}, other: 3} | ||
context: {cond: false} | ||
template: {key: {$if: 'cond', then: 2}, other: 3} | ||
result: {other: 3} | ||
@@ -205,4 +212,4 @@ ``` | ||
```yaml | ||
template: {$flatten: [[1, 2], [3, 4], [5]]} | ||
context: {} | ||
template: {$flatten: [[1, 2], [3, 4], [5]]} | ||
result: [1, 2, 3, 4, 5] | ||
@@ -216,4 +223,4 @@ ``` | ||
```yaml | ||
template: {$flattenDeep: [[1, [2, [3]]]]} | ||
context: {} | ||
template: {$flattenDeep: [[1, [2, [3]]]]} | ||
result: [1, 2, 3] | ||
@@ -225,9 +232,10 @@ ``` | ||
The `$fromNow` operator is a shorthand for the built-in function `fromNow`. It | ||
creates a JSON (ISO 8601) datestamp for a time relative to the current time or, | ||
if `from` is given, from that time. The offset is specified by a sequence of | ||
number/unit pairs in a string. For example: | ||
creates a JSON (ISO 8601) datestamp for a time relative to the current time | ||
(see the `now` builtin, below) or, if `from` is given, relative to that time. | ||
The offset is specified by a sequence of number/unit pairs in a string. For | ||
example: | ||
```yaml | ||
template: {$fromNow: '2 days 1 hour'} | ||
context: {} | ||
template: {$fromNow: '2 days 1 hour'} | ||
result: '2017-01-19T16:27:20.974Z' | ||
@@ -237,4 +245,4 @@ ``` | ||
```yaml | ||
template: {$fromNow: '1 hour', from: '2017-01-19T16:27:20.974Z'} | ||
context: {} | ||
template: {$fromNow: '1 hour', from: '2017-01-19T16:27:20.974Z'} | ||
result: '2017-01-19T17:27:20.974Z' | ||
@@ -252,5 +260,5 @@ ``` | ||
```yaml | ||
context: {} | ||
template: {$let: {ts: 100, foo: 200}, | ||
in: [{$eval: "ts+foo"}, {$eval: "ts-foo"}, {$eval: "ts*foo"}]} | ||
context: {} | ||
result: [300, -100, 20000] | ||
@@ -273,6 +281,6 @@ ``` | ||
```yaml | ||
context: {a: 1} | ||
template: | ||
$map: [2, 4, 6] | ||
each(x): {$eval: 'x + a'} | ||
context: {a: 1} | ||
result: [3, 5, 7] | ||
@@ -282,6 +290,6 @@ ``` | ||
```yaml | ||
context: {} | ||
template: | ||
$map: {a: 1, b: 2, c: 3} | ||
each(y): {'${y.key}x': {$eval: 'y.val + 1'}} | ||
context: {} | ||
result: {ax: 2, bx: 3, cx: 4} | ||
@@ -302,4 +310,4 @@ ``` | ||
```yaml | ||
template: {$merge: [{a: 1, b: 1}, {b: 2, c: 3}, {d: 4}]} | ||
context: {} | ||
template: {$merge: [{a: 1, b: 1}, {b: 2, c: 3}, {d: 4}]} | ||
result: {a: 1, b: 2, c: 3, d: 4} | ||
@@ -314,3 +322,2 @@ ``` | ||
```yaml | ||
context: {} | ||
template: | ||
@@ -327,2 +334,3 @@ $mergeDeep: | ||
command: [c] | ||
context: {} | ||
result: | ||
@@ -343,6 +351,6 @@ task: | ||
```yaml | ||
context: {} | ||
template: | ||
$sort: [{a: 2}, {a: 1, b: []}, {a: 3}] | ||
by(x): 'x.a' | ||
context: {} | ||
result: [{a: 1, b: []}, {a: 2}, {a: 3}] | ||
@@ -356,4 +364,4 @@ ``` | ||
```yaml | ||
template: {$reverse: [3, 4, 1, 2]} | ||
context: {} | ||
template: {$reverse: [3, 4, 1, 2]} | ||
result: [2, 1, 4, 3] | ||
@@ -368,4 +376,4 @@ ``` | ||
```yaml | ||
template: {$$reverse: [3, 2, {$$eval: '2 - 1'}, 0]} | ||
context: {} | ||
template: {$$reverse: [3, 2, {$$eval: '2 - 1'}, 0]} | ||
result: {$reverse: [3, 2, {$eval: '2 - 1'}, 0]} | ||
@@ -381,4 +389,4 @@ ``` | ||
```yaml | ||
template: {$if: 'a || b || c || d || e || f', then: "uh oh", else: "falsy" } | ||
context: {a: null, b: [], c: {}, d: "", e: 0, f: false} | ||
template: {$if: 'a || b || c || d || e || f', then: "uh oh", else: "falsy" } | ||
result: "falsy" | ||
@@ -400,3 +408,2 @@ ``` | ||
```yaml | ||
context: {} | ||
template: | ||
@@ -407,2 +414,3 @@ - {$eval: "1.3"} | ||
- {$eval: "'\n\t'"} | ||
context: {} | ||
result: | ||
@@ -419,6 +427,6 @@ - 1.3 | ||
```yaml | ||
context: {} | ||
template: | ||
- {$eval: '[1, 2, "three"]'} | ||
- {$eval: '{foo: 1, "bar": 2}'} | ||
context: {} | ||
result: | ||
@@ -434,4 +442,4 @@ - [1, 2, "three"] | ||
```yaml | ||
template: {$eval: '[x, z, x+z]'} | ||
context: {x: 'quick', z: 'sort'} | ||
template: {$eval: '[x, z, x+z]'} | ||
reslut: ['quick', 'sort', 'quicksort'] | ||
@@ -446,3 +454,2 @@ ``` | ||
```yaml | ||
context: {x: 10, z: 20, s: "face", t: "plant"} | ||
template: | ||
@@ -456,2 +463,3 @@ - {$eval: 'x + z'} | ||
- {$eval: '(z / x) ** 2'} | ||
context: {x: 10, z: 20, s: "face", t: "plant"} | ||
result: | ||
@@ -476,3 +484,2 @@ - 30 | ||
```yaml | ||
context: {x: -10, z: 10, deep: [1, [3, {a: 5}]]} | ||
template: | ||
@@ -485,2 +492,3 @@ - {$eval: 'x < z'} | ||
- {$eval: 'deep != [1, [3, {a: 5}]]'} | ||
context: {x: -10, z: 10, deep: [1, [3, {a: 5}]]} | ||
result: [true, true, false, false, true, false] | ||
@@ -494,4 +502,4 @@ ``` | ||
```yaml | ||
template: {$eval: '!(false || false) && true'} | ||
context: {} | ||
template: {$eval: '!(false || false) && true'} | ||
result: true | ||
@@ -507,4 +515,4 @@ ``` | ||
```yaml | ||
template: {$eval: 'v.a + v["b"]'} | ||
context: {v: {a: 'apple', b: 'bananna', c: 'carrot'}} | ||
template: {$eval: 'v.a + v["b"]'} | ||
result: 'applebananna' | ||
@@ -522,3 +530,2 @@ ```` | ||
```yaml | ||
context: {array: ['a', 'b', 'c', 'd', 'e'], string: 'abcde'} | ||
template: | ||
@@ -533,2 +540,3 @@ - {$eval: '[array[1], string[1]]'} | ||
- {$eval: '[array[:-3], string[:-3]]'} | ||
context: {array: ['a', 'b', 'c', 'd', 'e'], string: 'abcde'} | ||
result: | ||
@@ -551,3 +559,2 @@ - ['b', 'b'] | ||
```yaml | ||
context: {} | ||
template: | ||
@@ -557,2 +564,3 @@ - {$eval: '"foo" in {foo: 1, bar: 2}'} | ||
- {$eval: '"foo" in "foobar"'} | ||
context: {} | ||
result: [true, true, true] | ||
@@ -565,5 +573,5 @@ ``` | ||
not JSON data, so they cannot be created in JSON-e, but they can be provided as | ||
built-ins or in the context and called from JSON-e. | ||
built-ins or supplied in the context and called from JSON-e. | ||
#### Built-In Functions and Variables | ||
### Built-In Functions and Variables | ||
@@ -574,46 +582,96 @@ The expression language provides a laundry-list of built-in functions/variables. Library | ||
* `fromNow(offset)` or `fromNow(offset, reference)` -- JSON datestamp for a time relative to the current time or, if given, the reference time | ||
* `now` -- the datestamp at the start of evaluation of the template. This is used implicitly as `from` in all fromNow calls. Override to set a different time. | ||
* `min(a, b, ..)` -- the smallest of the arguments | ||
* `max(a, b, ..)` -- the largest of the arguments | ||
* `sqrt(x)`, `ceil(x)`, `floor(x)`, `abs(x)` -- mathematical functions | ||
* `lowercase(s)`, `uppercase(s)` -- convert string case | ||
* `str(x)` -- convert string, number, boolean, or array to string | ||
* `lstrip(s)`, `rstrip(s)`, `strip(s)` -- strip whitespace from left, right, or both ends of a string | ||
* `len(x)` -- length of a string or array | ||
#### Time | ||
The built-in context value `now` is set to the current time at the start of | ||
evaluation of the template, and used as the default "from" value for `$fromNow` | ||
and the built-in `fromNow()`. | ||
# Development and testing | ||
```yaml | ||
template: | ||
- {$eval: 'now'} | ||
- {$eval: 'fromNow("1 minute")'} | ||
- {$eval: 'fromNow("1 minute", "2017-01-19T16:27:20.974Z")'} | ||
context: {} | ||
result: | ||
- '2017-01-19T16:27:20.974Z', | ||
- '2017-01-19T16:28:20.974Z', | ||
- '2017-01-19T16:28:20.974Z', | ||
``` | ||
You should run `npm install` to install the required packages for json-e's | ||
execution and development. | ||
#### Math | ||
You can run `./test.sh` to run json-e's unit tests and the `bundle.js` check. | ||
This is a breakdown of the commands inside the `test.sh` file. | ||
```yaml | ||
template: | ||
# the smallest of the arguments | ||
- {$eval: 'min(1, 3, 5)'} | ||
# the largest of the arguments | ||
- {$eval: 'max(2, 4, 6)'} | ||
# mathematical functions | ||
- {$eval: 'sqrt(16)'} | ||
- {$eval: 'ceil(0.3)'} | ||
- {$eval: 'floor(0.3)'} | ||
- {$eval: 'abs(-0.3)'} | ||
context: {} | ||
result: | ||
- 1 | ||
- 6 | ||
- 4 | ||
- 1 | ||
- 0 | ||
- 0.3 | ||
``` | ||
```bash | ||
# Run JavaScript unit tests | ||
npm test | ||
#### Strings | ||
# Run Python unit tests | ||
python setup.py test | ||
# bundle.js check. This section makes sure that | ||
# the demo website's bundle.js file is updated. | ||
mv docs/bundle.js docs/bundle.diff.js | ||
npm run-script build-demo | ||
diff docs/bundle.js docs/bundle.diff.js | ||
```yaml | ||
template: | ||
# convert string case | ||
- {$eval: 'lowercase("Fools!")'} | ||
- {$eval: 'uppercase("Fools!")'} | ||
# convert string, number, boolean, or array to string | ||
- {$eval: 'str(130)'} | ||
# strip whitespace from left, right, or both ends of a string | ||
- {$eval: 'lstrip(" room ")'} | ||
- {$eval: 'rstrip(" room ")'} | ||
- {$eval: 'strip(" room ")'} | ||
context: {} | ||
result: | ||
- "fools!" | ||
- "FOOLS!" | ||
- "130" | ||
- "room " | ||
- " room" | ||
- room | ||
``` | ||
You can also run the following command to | ||
update the demo website bundle.js file. | ||
#### Length | ||
```bash | ||
npm run-script build-demo | ||
The `len()` built-in returns the length of a string or array. | ||
```yaml | ||
template: {$eval: 'len([1, 2, 3])'} | ||
context: {} | ||
result: 3 | ||
``` | ||
## Development Notes | ||
# Development and testing | ||
### Making a Release | ||
## JSON-e development | ||
You should run `npm install` to install the required packages for json-e's | ||
execution and development. For Python, activate a virtualenv and run `pip | ||
install -e .`. | ||
You can run `./test.sh` to run json-e's tests and lint checks. | ||
## Demo development | ||
The demo website is a [Neutrino](https://neutrino.js.org/) app hosted in | ||
`demo/`. Follow the usual Neutrino development process (`yarn install && yarn | ||
start`) there. | ||
The resulting application embeds and enriches this README. | ||
## Making a Release | ||
* Update the version, commit, and tag -- `npm version patch` (or minor or major, depending) | ||
@@ -620,0 +678,0 @@ * Push to release the JS version -- `git push && git push --tags` |
@@ -20,3 +20,3 @@ var {BuiltinError} = require('./error'); | ||
let builtinError = (builtin, expectation) => new BuiltinError(`${builtin} expects ${expectation}`); | ||
let builtinError = (builtin) => new BuiltinError(`invalid arguments to ${builtin}`); | ||
@@ -87,3 +87,3 @@ module.exports = (context) => { | ||
define('str', builtins, { | ||
argumentTests: ['string|number|boolean|array|null'], | ||
argumentTests: ['string|number|boolean|null'], | ||
invoke: obj => { | ||
@@ -90,0 +90,0 @@ if (obj === null) { |
@@ -12,2 +12,15 @@ var interpreter = require('./interpreter'); | ||
function checkUndefinedProperties(template, allowed) { | ||
var unknownKeys = ''; | ||
var combined = new RegExp(allowed.join('|') + '$'); | ||
for (var key of Object.keys(template).sort()) { | ||
if (!combined.test(key)) { | ||
unknownKeys += ' ' + key; | ||
} | ||
} | ||
if (unknownKeys) { | ||
throw new TemplateError(allowed[0].replace('\\', '') + ' has undefined properties:' + unknownKeys); | ||
} | ||
}; | ||
let flattenDeep = (a) => { | ||
@@ -31,5 +44,5 @@ return Array.isArray(a) ? [].concat(...a.map(flattenDeep)) : a; | ||
// toString renders null as an empty string, which is not what we want | ||
// if it is null, result should just be appended with empty string | ||
if (v.result === null) { | ||
result += 'null'; | ||
result += ''; | ||
} else { | ||
@@ -60,2 +73,4 @@ result += v.result.toString(); | ||
operators.$flatten = (template, context) => { | ||
checkUndefinedProperties(template, ['\\$flatten']); | ||
let value = render(template['$flatten'], context); | ||
@@ -71,2 +86,4 @@ | ||
operators.$flattenDeep = (template, context) => { | ||
checkUndefinedProperties(template, ['\\$flattenDeep']); | ||
let value = render(template['$flattenDeep'], context); | ||
@@ -82,2 +99,4 @@ | ||
operators.$fromNow = (template, context) => { | ||
checkUndefinedProperties(template, ['\\$fromNow', 'from']); | ||
let value = render(template['$fromNow'], context); | ||
@@ -95,2 +114,4 @@ let reference = context.now; | ||
operators.$if = (template, context) => { | ||
checkUndefinedProperties(template, ['\\$if', 'then', 'else']); | ||
if (!isString(template['$if'])) { | ||
@@ -106,2 +127,4 @@ throw new TemplateError('$if can evaluate string expressions only'); | ||
operators.$json = (template, context) => { | ||
checkUndefinedProperties(template, ['\\$json']); | ||
return JSON.stringify(render(template['$json'], context)); | ||
@@ -111,2 +134,4 @@ }; | ||
operators.$let = (template, context) => { | ||
checkUndefinedProperties(template, ['\\$let', 'in']); | ||
let variables = render(template['$let'], context); | ||
@@ -118,2 +143,7 @@ | ||
throw new TemplateError('$let value must evaluate to an object'); | ||
} else { | ||
let match = Object.keys(variables).every((variableName) => /^[a-zA-Z_][a-zA-Z0-9_]*$/.exec(variableName)); | ||
if (!match) { | ||
throw new TemplateError('top level keys of $let must follow /[a-zA-Z_][a-zA-Z0-9_]*/'); | ||
} | ||
} | ||
@@ -129,2 +159,4 @@ | ||
operators.$map = (template, context) => { | ||
EACH_RE = 'each\\(([a-zA-Z_][a-zA-Z0-9_]*)\\)'; | ||
checkUndefinedProperties(template, ['\\$map', EACH_RE]); | ||
let value = render(template['$map'], context); | ||
@@ -165,2 +197,4 @@ if (!isArray(value) && !isObject(value)) { | ||
operators.$merge = (template, context) => { | ||
checkUndefinedProperties(template, ['\\$merge']); | ||
let value = render(template['$merge'], context); | ||
@@ -176,2 +210,4 @@ | ||
operators.$mergeDeep = (template, context) => { | ||
checkUndefinedProperties(template, ['\\$mergeDeep']); | ||
let value = render(template['$mergeDeep'], context); | ||
@@ -189,3 +225,2 @@ | ||
let merge = (l, r) => { | ||
console.log(`merge(${JSON.stringify(l)}, ${JSON.stringify(r)})`); | ||
if (isArray(l) && isArray(r)) { | ||
@@ -199,3 +234,2 @@ return l.concat(r); | ||
res[p] = merge(l[p], r[p]); | ||
console.log(`-> ${JSON.stringify(res[p])}`); | ||
} else { | ||
@@ -209,19 +243,14 @@ res[p] = r[p]; | ||
}; | ||
console.log(`merging ${JSON.stringify(value)}`); | ||
// start with the first element of the list | ||
return value.reduce(merge, value.shift()); | ||
return Object.assign({}, ...value); | ||
}; | ||
operators.$reverse = (template, context) => { | ||
checkUndefinedProperties(template, ['\\$reverse']); | ||
let value = render(template['$reverse'], context); | ||
if (!isArray(value) && !isArray(template['$reverse'])) { | ||
if (!isArray(value)) { | ||
throw new TemplateError('$reverse value must evaluate to an array of objects'); | ||
} | ||
if (!isArray(value)) { | ||
throw new TemplateError('$reverse requires array as value'); | ||
} | ||
return value.reverse(); | ||
@@ -231,2 +260,4 @@ }; | ||
operators.$sort = (template, context) => { | ||
BY_RE = 'by\\(([a-zA-Z_][a-zA-Z0-9_]*)\\)'; | ||
checkUndefinedProperties(template, ['\\$sort', BY_RE]); | ||
let value = render(template['$sort'], context); | ||
@@ -325,7 +356,11 @@ if (!isArray(value)) { | ||
if (value !== deleteMarker) { | ||
if (key.startsWith('$$') && operators.hasOwnProperty(key.substr(1))) { | ||
if (key.startsWith('$$')) { | ||
key = key.substr(1); | ||
} else if (/^\$[a-zA-Z][a-zA-Z0-9]*$/.test(key)) { | ||
throw new TemplateError('$<identifier> is reserved; ues $$<identifier>'); | ||
} else { | ||
key = interpolate(key, context); | ||
} | ||
result[interpolate(key, context)] = value; | ||
result[key] = value; | ||
} | ||
@@ -341,3 +376,3 @@ } | ||
} | ||
context = addBuiltins(Object.assign({}, {now: new Date()}, context)); | ||
context = addBuiltins(Object.assign({}, {now: fromNow('0 seconds')}, context)); | ||
let result = render(template, context); | ||
@@ -344,0 +379,0 @@ if (result === deleteMarker) { |
68429
1091
636