Comparing version 2.1.1 to 2.2.1
{ | ||
"name": "json-e", | ||
"version": "2.1.1", | ||
"version": "2.2.1", | ||
"description": "json parameterization module inspired from json-parameterization", | ||
@@ -21,5 +21,3 @@ "main": "./src/index.js", | ||
"license": "MPL-2.0", | ||
"dependencies": { | ||
"es6-error": "^4.0.1" | ||
}, | ||
"dependencies": {}, | ||
"devDependencies": { | ||
@@ -26,0 +24,0 @@ "assume": "^1.4.1", |
312
README.md
@@ -40,8 +40,31 @@ # [JSON-e](https://taskcluster.github.io/json-e) | ||
var context = {"foo": function(x) { return x + 2; }}; | ||
console.log(jsone(template, context)); | ||
// -> 3 | ||
console.log(jsone(template, context)); // -> 3 | ||
``` | ||
# Language | ||
*NOTE*: Context functions are called synchronously. Any complex asynchronous | ||
operations should be handled before rendering the template. | ||
*NOTE*: If the template is untrusted, it can pass arbitrary data to functions | ||
in the context, which must guard against such behavior. | ||
The Python distribution exposes a `render` function: | ||
```python | ||
import jsone | ||
var template = {"a": {"$eval": "foo.bar"}} | ||
var context = {"foo": {"bar": "zoo"}} | ||
print(jsone(template, contxt)) # -> {"a": "zoo"} | ||
``` | ||
and also allows custom functions in the context: | ||
```python | ||
var template = {"$eval": "foo(1)"} | ||
var context = {"foo": lambda x: x + 2} | ||
print(jsone(template, contxt)) # -> 3 | ||
``` | ||
# Language Reference | ||
The examples here are given in YAML for ease of reading. Of course, the | ||
@@ -62,2 +85,4 @@ rendering operation takes place on the parsed data, so the input format is | ||
## String Interpolation | ||
The simplest form of substitution occurs within strings, using `${..}`: | ||
@@ -131,14 +156,2 @@ | ||
### Truthiness | ||
Many values can be evaluated in context where booleans are required, | ||
not just booleans themselves. JSON-e defines the following values as false. | ||
Anything else will be true. | ||
```yaml | ||
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" | ||
``` | ||
### `$if` - `then` - `else` | ||
@@ -197,5 +210,5 @@ | ||
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. | ||
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 or, | ||
if `from` is given, from that time. The offset is specified by a sequence of | ||
number/unit pairs in a string. For example: | ||
@@ -208,2 +221,8 @@ ```yaml | ||
```yaml | ||
context: {} | ||
template: {$fromNow: '1 hour', from: '2017-01-19T16:27:20.974Z'} | ||
result: '2017-01-19T17:27:20.974Z' | ||
``` | ||
The available units are `day`, `hour`, and `minute`, for all of which a plural | ||
@@ -294,5 +313,6 @@ is also accepted. | ||
## Escaping operators | ||
### Escaping operators | ||
You can use `$$` to escape json-e operators. For example: | ||
All property names starting with `$` are reserved for JSON-e. | ||
You can use `$$` to escape such properties: | ||
@@ -305,43 +325,188 @@ ```yaml | ||
## Expressions | ||
## Truthiness | ||
Expression are given in a simple Python- or JavaScript-like language. It | ||
supports the following: | ||
Many values can be evaluated in context where booleans are required, | ||
not just booleans themselves. JSON-e defines the following values as false. | ||
Anything else will be true. | ||
* Numeric literals (decimal only) | ||
* String literals (enclosed in `'` or `"`, with no escaping) | ||
* Arrays in JSON format (`[.., ..]`) | ||
* Objects in JS format: `{"foo": 10}` or `{foo: 10}` | ||
* Parentheses for grouping (`(a + b) * c`) | ||
* Arithmetic on integers (`+`, `-`, `*`, `/`, `**` for exponentiation), with unary `-` and `+` | ||
* String concatenation (`+`) | ||
* Comparison of strings to strings or numbers to numbers (`<`, `<=`, `>`, `>=`) | ||
* Equality of anything (`==`, `!=`) | ||
* Boolean operators (`||`, `&&`, `!`) | ||
* Identifiers referring to variables (matching `/[a-zA-Z_][a-zA-Z_0-9]*/`) | ||
* Object property access: `obj.prop` or `obj["prop"]` | ||
* `obj,prop` is an error if there is no such property; in the same case `obj["prop"]` evaluates to `null`. | ||
* Array and string indexing and slicing with Python semantics | ||
* `array[1]` -- second element of array (zero-indexed) | ||
* `array[1:4]` -- second through fourth elements of the array (the slice includes the left index and excludes the right index) | ||
* `array[1:]` -- second through last element of the array | ||
* `array[:3]` -- first through third element of the array | ||
* `array[-2:]` -- the last two elements of the array | ||
* `array[:-1]` -- all but the last element of the array | ||
* `string[3]` -- fourth character of the string | ||
* `string[-4:]` -- all but the last four characters of the string | ||
* Containment operator: | ||
* `"string" in object` -- true if the object has the given property | ||
* `"string" in array` -- true if the string is an array element | ||
* `number in array` -- true if the number is an array element | ||
* `"string" in "another string"` -- true if the first string is a substring of the second | ||
* Function invocation: `fn(arg1, arg2)` | ||
```yaml | ||
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" | ||
``` | ||
### Built-In Functions | ||
## Expression Syntax | ||
The expression language provides a laundry-list of built-in functions. Library | ||
users can easily add additional functions, or override the built-ins, as part | ||
Expression are given in a simple Python- or JavaScript-like expression | ||
language. Its data types are limited to JSON types plus function objects. | ||
### Literals | ||
Literals are similar to those for JSON. Numeric literals only accept integer | ||
and decimal notation. Strings do not support any kind of escaping. The use of | ||
`\n` and `\t` in the example below depends on the YAML parser to expand the | ||
escapes. | ||
```yaml | ||
context: {} | ||
template: | ||
- {$eval: "1.3"} | ||
- {$eval: "'abc'"} | ||
- {$eval: '"abc"'} | ||
- {$eval: "'\n\t'"} | ||
result: | ||
- 1.3 | ||
- "abc" | ||
- "abc" | ||
- "\n\t" | ||
``` | ||
Array and object literals also look much like JSON, with bare identifiers | ||
allowed as keys like in Javascript: | ||
```yaml | ||
context: {} | ||
template: | ||
- {$eval: '[1, 2, "three"]'} | ||
- {$eval: '{foo: 1, "bar": 2}'} | ||
result: | ||
- [1, 2, "three"] | ||
- {"foo": 1, "bar": 2} | ||
``` | ||
### Context References | ||
Bare identifiers refer to items from the context or to built-ins (described below). | ||
```yaml | ||
context: {x: 'quick', z: 'sort'} | ||
template: {$eval: '[x, z, x+z]'} | ||
reslut: ['quick', 'sort', 'quicksort'] | ||
``` | ||
### Arithmetic Operations | ||
The usual arithmetic operators are all defined, with typical associativity and | ||
precedence: | ||
```yaml | ||
context: {x: 10, z: 20, s: "face", t: "plant"} | ||
template: | ||
- {$eval: 'x + z'} | ||
- {$eval: 's + t'} | ||
- {$eval: 'z - x'} | ||
- {$eval: 'x * z'} | ||
- {$eval: 'z / x'} | ||
- {$eval: 'z ** 2'} | ||
- {$eval: '(z / x) ** 2'} | ||
result: | ||
- 30 | ||
- "faceplant" | ||
- 10 | ||
- 200 | ||
- 2 | ||
- 400 | ||
- 4 | ||
``` | ||
Note that strings can be concatenated with `+`, but none of the other operators | ||
apply. | ||
### Comparison Operations | ||
Comparisons work as expected. Equality is "deep" in the sense of doing | ||
comparisons of the contents of data structures. | ||
```yaml | ||
context: {x: -10, z: 10, deep: [1, [3, {a: 5}]]} | ||
template: | ||
- {$eval: 'x < z'} | ||
- {$eval: 'x <= z'} | ||
- {$eval: 'x > z'} | ||
- {$eval: 'x >= z'} | ||
- {$eval: 'deep == [1, [3, {a: 5}]]'} | ||
- {$eval: 'deep != [1, [3, {a: 5}]]'} | ||
result: [true, true, false, false, true, false] | ||
``` | ||
### Boolean Operations | ||
Boolean operations use C- and Javascript-style symbls `||`, `&&`, and `!`: | ||
```yaml | ||
context: {} | ||
template: {$eval: '!(false || false) && true'} | ||
result: true | ||
``` | ||
### Object Property Access | ||
Like Javascript, object properties can be accessed either with array-index | ||
syntax or with dot syntax. Unlike Javascript, `obj.prop` is an error if `obj` | ||
does not have `prop`, while `obj['prop']` will evaulate to `null`. | ||
```yaml | ||
context: {v: {a: 'apple', b: 'bananna', c: 'carrot'}} | ||
template: {$eval: 'v.a + v["b"]'} | ||
result: 'applebananna' | ||
```` | ||
### Indexing and Slicing | ||
Strings and arrays can be indexed and sliced using a Python-like indexing | ||
scheme. Negative indexes are counted from the end of the value. Slices are | ||
treated as "half-open", meaning that the result contains the first index and | ||
does not contain the second index. A "backward" slice with the start index | ||
greater than the end index is treated as empty. | ||
```yaml | ||
context: {array: ['a', 'b', 'c', 'd', 'e'], string: 'abcde'} | ||
template: | ||
- {$eval: '[array[1], string[1]]'} | ||
- {$eval: '[array[1:4], string[1:4]]'} | ||
- {$eval: '[array[2:], string[2:]]'} | ||
- {$eval: '[array[:2], string[:2]]'} | ||
- {$eval: '[array[4:2], string[4:2]]'} | ||
- {$eval: '[array[-2], string[-2]]'} | ||
- {$eval: '[array[-2:], string[-2:]]'} | ||
- {$eval: '[array[:-3], string[:-3]]'} | ||
result: | ||
- ['b', 'b'] | ||
- [['b', 'c', 'd'], 'bcd'] | ||
- [['c', 'd', 'e'], 'cde'] | ||
- [['a', 'b'], 'ab'] | ||
- [[], ''] | ||
- ['d', 'd'] | ||
- [['d', 'e'], 'de'] | ||
- [['a', 'b'], 'ab'] | ||
``` | ||
### Containment Operation | ||
The `in` keyword can be used to check for containment: a property in an object, | ||
an element in an array, or a substring in a string. | ||
```yaml | ||
context: {} | ||
template: | ||
- {$eval: '"foo" in {foo: 1, bar: 2}'} | ||
- {$eval: '"foo" in ["foo", "bar"]'} | ||
- {$eval: '"foo" in "foobar"'} | ||
result: [true, true, true] | ||
``` | ||
### Function Invocation | ||
Function calls are made with the usual `fn(arg1, arg2)` syntax. Functions are | ||
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-In Functions and Variables | ||
The expression language provides a laundry-list of built-in functions/variables. Library | ||
users can easily add additional functions/variables, or override the built-ins, as part | ||
of the context. | ||
* `fromNow(x)` -- JSON datestamp for a time relative to the current time | ||
* `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 | ||
@@ -354,30 +519,3 @@ * `max(a, b, ..)` -- the largest of the arguments | ||
### Custom Functions | ||
The context supplied to JSON-e can contain JS function objects. These will be | ||
available just like the built-in functions are. For example: | ||
```js | ||
var context = { | ||
imageData: function(img) { | ||
return ...; | ||
}, | ||
}; | ||
var template = { | ||
title: "Trip to Hawaii", | ||
thumbnail: {$eval: 'imageData("hawaii")'}, | ||
}; | ||
return jsone(template, context); | ||
``` | ||
NOTE: Context functions are called synchronously. Any complex asynchronous | ||
operations should be handled before rendering the template. | ||
NOTE: If the template is untrusted, it can pass arbitrary data to functions | ||
in the context, which must guard against such behavior. For example, if the | ||
`imageData` function above reads data from a file, it must sanitize the | ||
filename before opening it. | ||
# Development and testing | ||
@@ -411,1 +549,11 @@ | ||
``` | ||
## Development Notes | ||
### Making a Release | ||
* Update the version, commit, and tag -- `npm patch` (or minor or major, depending) | ||
* Push to release the JS version -- `git push && git push --tags` | ||
* Release to PyPi: | ||
* `python setup.py sdist` | ||
* `twine upload dist/json-e-<version>.tar.gz` |
@@ -22,100 +22,103 @@ var {BuiltinError} = require('./error'); | ||
let builtins = {}; | ||
module.exports = (context) => { | ||
let builtins = {}; | ||
let define = (name, context, { | ||
argumentTests = [], | ||
minArgs = false, | ||
variadic = null, | ||
invoke, | ||
}) => context[name] = (...args) => { | ||
if (!variadic && args.length < argumentTests.length) { | ||
throw builtinError(`builtin: ${name}`, `${args.toString()}, too few arguments`); | ||
} | ||
let define = (name, context, { | ||
argumentTests = [], | ||
minArgs = false, | ||
variadic = null, | ||
invoke, | ||
}) => context[name] = (...args) => { | ||
if (!variadic && args.length < argumentTests.length) { | ||
throw builtinError(`builtin: ${name}`, `${args.toString()}, too few arguments`); | ||
} | ||
if (minArgs && args.length < minArgs) { | ||
throw builtinError(`builtin: ${name}: expected at least ${minArgs} arguments`); | ||
} | ||
if (minArgs && args.length < minArgs) { | ||
throw builtinError(`builtin: ${name}: expected at least ${minArgs} arguments`); | ||
} | ||
if (variadic) { | ||
argumentTests = args.map(() => variadic); | ||
} | ||
if (variadic) { | ||
argumentTests = args.map(() => variadic); | ||
} | ||
args.forEach((arg, i) => { | ||
if (!argumentTests[i].split('|').some(test => types[test](arg))) { | ||
throw builtinError(`builtin: ${name}`, `argument ${i + 1} to be ${argumentTests[i]} found ${typeof arg}`); | ||
args.forEach((arg, i) => { | ||
if (!argumentTests[i].split('|').some(test => types[test](arg))) { | ||
throw builtinError(`builtin: ${name}`, `argument ${i + 1} to be ${argumentTests[i]} found ${typeof arg}`); | ||
} | ||
}); | ||
return invoke(...args); | ||
}; | ||
// Math functions | ||
['max', 'min'].forEach(name => { | ||
if (Math[name] == undefined) { | ||
throw new Error(`${name} in Math undefined`); | ||
} | ||
define(name, builtins, { | ||
minArgs: 1, | ||
variadic: 'number', | ||
invoke: (...args) => Math[name](...args), | ||
}); | ||
}); | ||
return invoke(...args); | ||
}; | ||
['sqrt', 'ceil', 'floor', 'abs'].forEach(name => { | ||
if (Math[name] == undefined) { | ||
throw new Error(`${name} in Math undefined`); | ||
} | ||
define(name, builtins, { | ||
argumentTests: ['number'], | ||
invoke: num => Math[name](num), | ||
}); | ||
}); | ||
// Math functions | ||
['max', 'min'].forEach(name => { | ||
if (Math[name] == undefined) { | ||
throw new Error(`${name} in Math undefined`); | ||
} | ||
define(name, builtins, { | ||
minArgs: 1, | ||
variadic: 'number', | ||
invoke: (...args) => Math[name](...args), | ||
// String manipulation | ||
define('lowercase', builtins, { | ||
argumentTests: ['string'], | ||
invoke: str => str.toLowerCase(), | ||
}); | ||
}); | ||
['sqrt', 'ceil', 'floor', 'abs'].forEach(name => { | ||
if (Math[name] == undefined) { | ||
throw new Error(`${name} in Math undefined`); | ||
} | ||
define(name, builtins, { | ||
argumentTests: ['number'], | ||
invoke: num => Math[name](num), | ||
define('uppercase', builtins, { | ||
argumentTests: ['string'], | ||
invoke: str => str.toUpperCase(), | ||
}); | ||
}); | ||
// String manipulation | ||
define('lowercase', builtins, { | ||
argumentTests: ['string'], | ||
invoke: str => str.toLowerCase(), | ||
}); | ||
define('str', builtins, { | ||
argumentTests: ['string|number|boolean|array|null'], | ||
invoke: obj => { | ||
if (obj === null) { | ||
return 'null'; | ||
} | ||
return obj.toString(); | ||
}, | ||
}); | ||
define('uppercase', builtins, { | ||
argumentTests: ['string'], | ||
invoke: str => str.toUpperCase(), | ||
}); | ||
define('len', builtins, { | ||
argumentTests: ['string|array'], | ||
invoke: obj => obj.length, | ||
}); | ||
define('str', builtins, { | ||
argumentTests: ['string|number|boolean|array|null'], | ||
invoke: obj => { | ||
if (obj === null) { | ||
return 'null'; | ||
} | ||
return obj.toString(); | ||
}, | ||
}); | ||
// Miscellaneous | ||
define('fromNow', builtins, { | ||
variadic: 'string', | ||
minArgs: 1, | ||
invoke: (str, reference) => fromNow(str, reference || context.now), | ||
}); | ||
define('len', builtins, { | ||
argumentTests: ['string|array'], | ||
invoke: obj => obj.length, | ||
}); | ||
// Miscellaneous | ||
define('fromNow', builtins, { | ||
argumentTests: ['string'], | ||
invoke: str => fromNow(str), | ||
}); | ||
define('typeof', builtins, { | ||
argumentTests: ['string|number|boolean|array|object|null|function'], | ||
invoke: x => { | ||
for (type of ['string', 'number', 'boolean', 'array', 'object', 'function']) { | ||
if (types[type](x)) { | ||
return type; | ||
define('typeof', builtins, { | ||
argumentTests: ['string|number|boolean|array|object|null|function'], | ||
invoke: x => { | ||
for (type of ['string', 'number', 'boolean', 'array', 'object', 'function']) { | ||
if (types[type](x)) { | ||
return type; | ||
} | ||
} | ||
} | ||
if (types['null'](x)) { | ||
return null; | ||
} | ||
throw builtinError('builtin: typeof', `argument ${x} to be a valid json-e type. found ${typeof arg}`); | ||
}, | ||
}); | ||
if (types['null'](x)) { | ||
return null; | ||
} | ||
throw builtinError('builtin: typeof', `argument ${x} to be a valid json-e type. found ${typeof arg}`); | ||
}, | ||
}); | ||
module.exports = builtins; | ||
return Object.assign({}, builtins, context); | ||
}; |
@@ -1,14 +0,29 @@ | ||
var ExtendableError = require('es6-error'); | ||
class JSONTemplateError extends Error { | ||
constructor(message) { | ||
super(message); | ||
this.location = []; | ||
} | ||
class SyntaxError extends ExtendableError { | ||
constructor(message, {start, end}) { | ||
add_location(loc) { | ||
this.location.unshift(loc); | ||
} | ||
toString() { | ||
if (this.location.length) { | ||
return `${this.name} at template${this.location.join('')}: ${this.message}`; | ||
} else { | ||
return `${this.name}: ${this.message}`; | ||
} | ||
} | ||
} | ||
class SyntaxError extends JSONTemplateError { | ||
constructor(message) { | ||
super(message); | ||
this.message = message; | ||
this.name = 'SyntaxError'; | ||
this.message = message; | ||
this.start = start; | ||
this.end = end; | ||
} | ||
} | ||
class BaseError extends ExtendableError { | ||
class BaseError extends JSONTemplateError { | ||
constructor(message) { | ||
@@ -42,2 +57,2 @@ super(message); | ||
module.exports = {SyntaxError, InterpreterError, TemplateError, BuiltinError}; | ||
module.exports = {JSONTemplateError, SyntaxError, InterpreterError, TemplateError, BuiltinError}; |
@@ -37,3 +37,3 @@ // Regular expression matching: | ||
// Render timespan fromNow as JSON timestamp | ||
module.exports = (timespan = '', reference = new Date()) => { | ||
module.exports = (timespan = '', reference) => { | ||
let offset = parseTime(timespan); | ||
@@ -45,2 +45,8 @@ | ||
if (reference) { | ||
reference = new Date(reference); | ||
} else { | ||
reference = new Date(); | ||
} | ||
var retval = new Date( | ||
@@ -47,0 +53,0 @@ reference.getTime() |
@@ -9,4 +9,4 @@ var interpreter = require('./interpreter'); | ||
} = require('./type-utils'); | ||
var builtins = require('./builtins'); | ||
var {TemplateError} = require('./error'); | ||
var addBuiltins = require('./builtins'); | ||
var {JSONTemplateError, TemplateError} = require('./error'); | ||
@@ -17,4 +17,2 @@ let flattenDeep = (a) => { | ||
let jsonTemplateError = (msg, template) => new TemplateError(msg + JSON.stringify(template, null, '\t')); | ||
let interpolate = (string, context) => { | ||
@@ -30,3 +28,4 @@ let result = ''; | ||
if (isArray(v.result) || isObject(v.result)) { | ||
throw new TemplateError('cannot interpolate array/object: ' + string); | ||
let input = remaining.slice(offset + 2, offset + v.offset); | ||
throw new TemplateError(`interpolation of '${input}' produced an array or object`); | ||
} | ||
@@ -58,5 +57,2 @@ | ||
let value = render(template['$eval'], context); | ||
if (!isString(value)) { | ||
throw jsonTemplateError('$eval can evaluate string expressions only\n', template); | ||
} | ||
return interpreter.parse(value, context); | ||
@@ -69,3 +65,3 @@ }; | ||
if (!isArray(value)) { | ||
throw jsonTemplateError('$flatten requires array as value\n', template); | ||
throw new TemplateError('$flatten value must evaluate to an array'); | ||
} | ||
@@ -80,3 +76,3 @@ | ||
if (!isArray(value)) { | ||
throw jsonTemplateError('$flattenDeep requires array as value\n', template); | ||
throw new TemplateError('$flattenDeep value must evaluate to an array'); | ||
} | ||
@@ -89,6 +85,10 @@ | ||
let value = render(template['$fromNow'], context); | ||
let reference = context.now; | ||
if (template['from']) { | ||
reference = render(template['from'], context); | ||
} | ||
if (!isString(value)) { | ||
throw jsonTemplateError('$fromNow can evaluate string expressions only\n', template); | ||
throw new TemplateError('$fromNow expects a string'); | ||
} | ||
return fromNow(value); | ||
return fromNow(value, reference); | ||
}; | ||
@@ -98,3 +98,3 @@ | ||
if (!isString(template['$if'])) { | ||
throw jsonTemplateError('$if can evaluate string expressions only\n', template); | ||
throw new TemplateError('$if can evaluate string expressions only'); | ||
} | ||
@@ -117,7 +117,7 @@ if (isTruthy(interpreter.parse(template['$if'], context))) { | ||
if (!isObject(variables)) { | ||
throw jsonTemplateError('$let operator requires an object as the context\n', template); | ||
throw new TemplateError('$let value must evaluate to an object'); | ||
} | ||
if (template.in == undefined) { | ||
throw jsonTemplateError('$let operator requires `in` clause\n', template); | ||
throw new TemplateError('$let operator requires an `in` clause'); | ||
} | ||
@@ -131,7 +131,7 @@ | ||
if (!isArray(value) && !isObject(value)) { | ||
throw jsonTemplateError('$map requires array or object as value\n', template); | ||
throw new TemplateError('$map value must evaluate to an array or object'); | ||
} | ||
if (Object.keys(template).length !== 2) { | ||
throw jsonTemplateError('$map requires cannot have more than two properties\n', template); | ||
throw new TemplateError('$map requires cannot have more than two properties'); | ||
} | ||
@@ -142,3 +142,3 @@ | ||
if (!match) { | ||
throw jsonTemplateError('$map requires each(identifier) syntax\n', template); | ||
throw new TemplateError('$map requires each(identifier) syntax'); | ||
} | ||
@@ -169,3 +169,3 @@ | ||
if (!isArray(value) || value.some(o => !isObject(o))) { | ||
throw jsonTemplateError('$merge requires array as value\n', template); | ||
throw new TemplateError('$merge value must evaluate to an array of objects'); | ||
} | ||
@@ -180,7 +180,7 @@ | ||
if (!isArray(value) && !isArray(template['$reverse'])) { | ||
throw jsonTemplateError('$reverse value must evaluate to an array\n', template); | ||
throw new TemplateError('$reverse value must evaluate to an array of objects'); | ||
} | ||
if (!isArray(value)) { | ||
throw jsonTemplateError('$reverse requires array as value\n', template); | ||
throw new TemplateError('$reverse requires array as value'); | ||
} | ||
@@ -193,3 +193,3 @@ return value.reverse(); | ||
if (!isArray(value)) { | ||
throw jsonTemplateError('$sort requires array as value\n', template); | ||
throw new TemplateError('$sort requires array as value'); | ||
} | ||
@@ -211,3 +211,3 @@ | ||
if (needBy) { | ||
throw jsonTemplateError('$sort requires by(identifier) for sorting arrays of objects/arrays\n', template); | ||
throw new TemplateError('$sort requires by(identifier) for sorting arrays of objects/arrays'); | ||
} | ||
@@ -225,3 +225,3 @@ by = value => value; | ||
tagged.some(e => eltType !== typeof e[0])) { | ||
throw jsonTemplateError('$sort requires all sorted values have the same type', template); | ||
throw new TemplateError('$sort requires all sorted values have the same type'); | ||
} | ||
@@ -250,3 +250,12 @@ } | ||
if (isArray(template)) { | ||
return template.map((v) => render(v, context)).filter((v) => v !== deleteMarker); | ||
return template.map((v, i) => { | ||
try { | ||
return render(v, context); | ||
} catch (err) { | ||
if (err instanceof JSONTemplateError) { | ||
err.add_location(`[${i}]`); | ||
} | ||
throw err; | ||
} | ||
}).filter((v) => v !== deleteMarker); | ||
} | ||
@@ -256,3 +265,3 @@ | ||
if (matches.length > 1) { | ||
throw jsonTemplateError('only one operator allowed\n', template); | ||
throw new TemplateError('only one operator allowed'); | ||
} | ||
@@ -266,3 +275,15 @@ if (matches.length === 1) { | ||
for (let key of Object.keys(template)) { | ||
let value = render(template[key], context); | ||
let value; | ||
try { | ||
value = render(template[key], context); | ||
} catch (err) { | ||
if (err instanceof JSONTemplateError) { | ||
if (/^[a-zA-Z][a-zA-Z0-9]*$/.test(key)) { | ||
err.add_location(`.${key}`); | ||
} else { | ||
err.add_location(`[${JSON.stringify(key)}]`); | ||
} | ||
} | ||
throw err; | ||
} | ||
if (value !== deleteMarker) { | ||
@@ -280,5 +301,7 @@ if (key.startsWith('$$') && operators.hasOwnProperty(key.substr(1))) { | ||
module.exports = (template, context = {}) => { | ||
let test = Object.keys(context).every(v => /^[a-zA-Z_][a-zA-Z0-9_]*$/.exec(v)[0]); | ||
context = Object.assign({}, builtins, context); | ||
assert(test, 'top level keys of context must follow /[a-zA-Z_][a-zA-Z0-9_]*/'); | ||
let test = Object.keys(context).every(v => /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(v)); | ||
if (!test) { | ||
throw new TemplateError('top level keys of context must follow /[a-zA-Z_][a-zA-Z0-9_]*/'); | ||
} | ||
context = addBuiltins(Object.assign({}, {now: new Date()}, context)); | ||
let result = render(template, context); | ||
@@ -285,0 +308,0 @@ if (result === deleteMarker) { |
@@ -8,3 +8,4 @@ /* | ||
var assert = require('assert'); | ||
var {SyntaxError} = require('./error'); | ||
var {isString} = require('./type-utils'); | ||
var {SyntaxError, TemplateError} = require('./error'); | ||
@@ -45,2 +46,5 @@ let syntaxRuleError = (token, expects) => new SyntaxError(`Found '${token.value}' expected '${expects}'`, token); | ||
parse(source, context = {}, offset = 0) { | ||
if (!isString(source)) { | ||
throw new TemplateError('expression to be evaluated must be a string'); | ||
} | ||
let ctx = new Context(this, source, context, offset); | ||
@@ -59,3 +63,8 @@ let result = ctx.parse(); | ||
let next = ctx.attempt(); | ||
if (next.kind !== terminator) { | ||
if (!next) { | ||
// string ended without the terminator | ||
let errorLocation = source.length; | ||
throw new SyntaxError(`Found end of string, expected ${terminator}`, | ||
{start: errorLocation, end: errorLocation}); | ||
} else if (next.kind !== terminator) { | ||
throw syntaxRuleError(next, terminator); | ||
@@ -62,0 +71,0 @@ } |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
64703
0
1018
551
1
- Removedes6-error@^4.0.1
- Removedes6-error@4.1.1(transitive)