prisme-flow
Version:
prisme platform flow engine
1,275 lines (1,223 loc) • 129 kB
JavaScript
/**
* © Copyright IBM Corp. 2016, 2017 All Rights Reserved
* Project name: JSONata
* This project is licensed under the MIT License, see LICENSE
*/
/**
* @module JSONata
* @description JSON query and transformation language
*/
/**
* jsonata
* @function
* @param {Object} expr - JSONata expression
* @returns {{evaluate: evaluate, assign: assign}} Evaluated expression
*/
var jsonata = (function() {
'use strict';
var operators = {
'.': 75,
'[': 80,
']': 0,
'{': 70,
'}': 0,
'(': 80,
')': 0,
',': 0,
'@': 75,
'#': 70,
';': 80,
':': 80,
'?': 20,
'+': 50,
'-': 50,
'*': 60,
'/': 60,
'%': 60,
'|': 20,
'=': 40,
'<': 40,
'>': 40,
'`': 80,
'**': 60,
'..': 20,
':=': 10,
'!=': 40,
'<=': 40,
'>=': 40,
'~>': 40,
'and': 30,
'or': 25,
'in': 40,
'&': 50,
'!': 0, // not an operator, but needed as a stop character for name tokens
'~': 0 // not an operator, but needed as a stop character for name tokens
};
var escapes = { // JSON string escape sequences - see json.org
'"': '"',
'\\': '\\',
'/': '/',
'b': '\b',
'f': '\f',
'n': '\n',
'r': '\r',
't': '\t'
};
// Tokenizer (lexer) - invoked by the parser to return one token at a time
var tokenizer = function (path) {
var position = 0;
var length = path.length;
var create = function (type, value) {
var obj = {type: type, value: value, position: position};
return obj;
};
var scanRegex = function() {
// the prefix '/' will have been previously scanned. Find the end of the regex.
// search for closing '/' ignoring any that are escaped, or within brackets
var start = position;
var depth = 0;
var pattern;
var flags;
while(position < length) {
var currentChar = path.charAt(position);
if(currentChar === '/' && path.charAt(position - 1) !== '\\' && depth === 0) {
// end of regex found
pattern = path.substring(start, position);
if(pattern === '') {
throw {
code: "S0301",
stack: (new Error()).stack,
position: position
};
}
position++;
currentChar = path.charAt(position);
// flags
start = position;
while(currentChar === 'i' || currentChar === 'm') {
position++;
currentChar = path.charAt(position);
}
flags = path.substring(start, position) + 'g';
return new RegExp(pattern, flags);
}
if((currentChar === '(' || currentChar === '[' || currentChar === '{') && path.charAt(position - 1) !== '\\' ) {
depth++;
}
if((currentChar === ')' || currentChar === ']' || currentChar === '}') && path.charAt(position - 1) !== '\\' ) {
depth--;
}
position++;
}
throw {
code: "S0302",
stack: (new Error()).stack,
position: position
};
};
var next = function (prefix) {
if (position >= length) return null;
var currentChar = path.charAt(position);
// skip whitespace
while (position < length && ' \t\n\r\v'.indexOf(currentChar) > -1) {
position++;
currentChar = path.charAt(position);
}
// test for regex
if (prefix !== true && currentChar === '/') {
position++;
return create('regex', scanRegex());
}
// handle double-char operators
if (currentChar === '.' && path.charAt(position + 1) === '.') {
// double-dot .. range operator
position += 2;
return create('operator', '..');
}
if (currentChar === ':' && path.charAt(position + 1) === '=') {
// := assignment
position += 2;
return create('operator', ':=');
}
if (currentChar === '!' && path.charAt(position + 1) === '=') {
// !=
position += 2;
return create('operator', '!=');
}
if (currentChar === '>' && path.charAt(position + 1) === '=') {
// >=
position += 2;
return create('operator', '>=');
}
if (currentChar === '<' && path.charAt(position + 1) === '=') {
// <=
position += 2;
return create('operator', '<=');
}
if (currentChar === '*' && path.charAt(position + 1) === '*') {
// ** descendant wildcard
position += 2;
return create('operator', '**');
}
if (currentChar === '~' && path.charAt(position + 1) === '>') {
// ~> chain function
position += 2;
return create('operator', '~>');
}
// test for single char operators
if (operators.hasOwnProperty(currentChar)) {
position++;
return create('operator', currentChar);
}
// test for string literals
if (currentChar === '"' || currentChar === "'") {
var quoteType = currentChar;
// double quoted string literal - find end of string
position++;
var qstr = "";
while (position < length) {
currentChar = path.charAt(position);
if (currentChar === '\\') { // escape sequence
position++;
currentChar = path.charAt(position);
if (escapes.hasOwnProperty(currentChar)) {
qstr += escapes[currentChar];
} else if (currentChar === 'u') {
// \u should be followed by 4 hex digits
var octets = path.substr(position + 1, 4);
if (/^[0-9a-fA-F]+$/.test(octets)) {
var codepoint = parseInt(octets, 16);
qstr += String.fromCharCode(codepoint);
position += 4;
} else {
throw {
code: "S0104",
stack: (new Error()).stack,
position: position
};
}
} else {
// illegal escape sequence
throw {
code: "S0103",
stack: (new Error()).stack,
position: position,
token: currentChar
};
}
} else if (currentChar === quoteType) {
position++;
return create('string', qstr);
} else {
qstr += currentChar;
}
position++;
}
throw {
code: "S0101",
stack: (new Error()).stack,
position: position
};
}
// test for numbers
var numregex = /^-?(0|([1-9][0-9]*))(\.[0-9]+)?([Ee][-+]?[0-9]+)?/;
var match = numregex.exec(path.substring(position));
if (match !== null) {
var num = parseFloat(match[0]);
if (!isNaN(num) && isFinite(num)) {
position += match[0].length;
return create('number', num);
} else {
throw {
code: "S0102",
stack: (new Error()).stack,
position: position,
token: match[0]
};
}
}
// test for names
var i = position;
var ch;
var name;
for (;;) {
ch = path.charAt(i);
if (i === length || ' \t\n\r\v'.indexOf(ch) > -1 || operators.hasOwnProperty(ch)) {
if (path.charAt(position) === '$') {
// variable reference
name = path.substring(position + 1, i);
position = i;
return create('variable', name);
} else {
name = path.substring(position, i);
position = i;
switch (name) {
case 'or':
case 'in':
case 'and':
return create('operator', name);
case 'true':
return create('value', true);
case 'false':
return create('value', false);
case 'null':
return create('value', null);
default:
if (position === length && name === '') {
// whitespace at end of input
return null;
}
return create('name', name);
}
}
} else {
i++;
}
}
};
return next;
};
/**
* Parses a function signature definition and returns a validation function
* @param {string} signature - the signature between the <angle brackets>
* @returns {Function} validation function
*/
function parseSignature(signature) {
// create a Regex that represents this signature and return a function that when invoked,
// returns the validated (possibly fixed-up) arguments, or throws a validation error
// step through the signature, one symbol at a time
var position = 1;
var params = [];
var param = {};
var prevParam = param;
while (position < signature.length) {
var symbol = signature.charAt(position);
if(symbol === ':') {
// TODO figure out what to do with the return type
// ignore it for now
break;
}
var next = function() {
params.push(param);
prevParam = param;
param = {};
};
var findClosingBracket = function(str, start, openSymbol, closeSymbol) {
// returns the position of the closing symbol (e.g. bracket) in a string
// that balances the opening symbol at position start
var depth = 1;
var position = start;
while(position < str.length) {
position++;
symbol = str.charAt(position);
if(symbol === closeSymbol) {
depth--;
if(depth === 0) {
// we're done
break; // out of while loop
}
} else if(symbol === openSymbol) {
depth++;
}
}
return position;
};
switch (symbol) {
case 's': // string
case 'n': // number
case 'b': // boolean
case 'l': // not so sure about expecting null?
case 'o': // object
param.regex = '[' + symbol + 'm]';
param.type = symbol;
next();
break;
case 'a': // array
// normally treat any value as singleton array
param.regex = '[asnblfom]';
param.type = symbol;
param.array = true;
next();
break;
case 'f': // function
param.regex = 'f';
param.type = symbol;
next();
break;
case 'j': // any JSON type
param.regex = '[asnblom]';
param.type = symbol;
next();
break;
case 'x': // any type
param.regex = '[asnblfom]';
param.type = symbol;
next();
break;
case '-': // use context if param not supplied
prevParam.context = true;
prevParam.contextRegex = new RegExp(prevParam.regex); // pre-compiled to test the context type at runtime
prevParam.regex += '?';
break;
case '?': // optional param
case '+': // one or more
prevParam.regex += symbol;
break;
case '(': // choice of types
// search forward for matching ')'
var endParen = findClosingBracket(signature, position, '(', ')');
var choice = signature.substring(position + 1, endParen);
if(choice.indexOf('<') === -1) {
// no parameterized types, simple regex
param.regex = '[' + choice + 'm]';
} else {
// TODO harder
throw {
code: "S0402",
stack: (new Error()).stack,
value: choice,
offset: position
};
}
param.type = '(' + choice + ')';
position = endParen;
next();
break;
case '<': // type parameter - can only be applied to 'a' and 'f'
if(prevParam.type === 'a' || prevParam.type === 'f') {
// search forward for matching '>'
var endPos = findClosingBracket(signature, position, '<', '>');
prevParam.subtype = signature.substring(position + 1, endPos);
position = endPos;
} else {
throw {
code: "S0401",
stack: (new Error()).stack,
value: prevParam.type,
offset: position
};
}
break;
}
position++;
}
var regexStr = '^' +
params.map(function(param) {
return '(' + param.regex + ')';
}).join('') +
'$';
var regex = new RegExp(regexStr);
var getSymbol = function(value) {
var symbol;
if(isFunction(value)) {
symbol = 'f';
} else {
var type = typeof value;
switch (type) {
case 'string':
symbol = 's';
break;
case 'number':
symbol = 'n';
break;
case 'boolean':
symbol = 'b';
break;
case 'object':
if (value === null) {
symbol = 'l';
} else if (Array.isArray(value)) {
symbol = 'a';
} else {
symbol = 'o';
}
break;
case 'undefined':
// any value can be undefined, but should be allowed to match
symbol = 'm'; // m for missing
}
}
return symbol;
};
var throwValidationError = function(badArgs, badSig) {
// to figure out where this went wrong we need apply each component of the
// regex to each argument until we get to the one that fails to match
var partialPattern = '^';
var goodTo = 0;
for(var index = 0; index < params.length; index++) {
partialPattern += params[index].regex;
var match = badSig.match(partialPattern);
if(match === null) {
// failed here
throw {
code: "T0410",
stack: (new Error()).stack,
value: badArgs[goodTo],
index: goodTo + 1
};
}
goodTo = match[0].length;
}
// if it got this far, it's probably because of extraneous arguments (we
// haven't added the trailing '$' in the regex yet.
throw {
code: "T0410",
stack: (new Error()).stack,
value: badArgs[goodTo],
index: goodTo + 1
};
};
return {
definition: signature,
validate: function(args, context) {
var suppliedSig = '';
args.forEach(function(arg) {
suppliedSig += getSymbol(arg);
});
var isValid = regex.exec(suppliedSig);
if(isValid) {
var validatedArgs = [];
var argIndex = 0;
params.forEach(function(param, index) {
var arg = args[argIndex];
var match = isValid[index + 1];
if(match === '') {
if (param.context) {
// substitute context value for missing arg
// first check that the context value is the right type
var contextType = getSymbol(context);
// test contextType against the regex for this arg (without the trailing ?)
if(param.contextRegex.test(contextType)) {
validatedArgs.push(context);
} else {
// context value not compatible with this argument
throw {
code: "T0411",
stack: (new Error()).stack,
value: context,
index: argIndex + 1
};
}
} else {
validatedArgs.push(arg);
argIndex++;
}
} else {
// may have matched multiple args (if the regex ends with a '+'
// split into single tokens
match.split('').forEach(function(single) {
if (param.type === 'a') {
if (single === 'm') {
// missing (undefined)
arg = undefined;
} else {
arg = args[argIndex];
var arrayOK = true;
// is there type information on the contents of the array?
if (typeof param.subtype !== 'undefined') {
if (single !== 'a' && match !== param.subtype) {
arrayOK = false;
} else if (single === 'a') {
if (arg.length > 0) {
var itemType = getSymbol(arg[0]);
if (itemType !== param.subtype.charAt(0)) { // TODO recurse further
arrayOK = false;
} else {
// make sure every item in the array is this type
var differentItems = arg.filter(function (val) {
return (getSymbol(val) !== itemType);
});
arrayOK = (differentItems.length === 0);
}
}
}
}
if (!arrayOK) {
throw {
code: "T0412",
stack: (new Error()).stack,
value: arg,
index: argIndex + 1,
type: param.subtype // TODO translate symbol to type name
};
}
// the function expects an array. If it's not one, make it so
if (single !== 'a') {
arg = [arg];
}
}
validatedArgs.push(arg);
argIndex++;
} else {
validatedArgs.push(arg);
argIndex++;
}
});
}
});
return validatedArgs;
}
throwValidationError(args, suppliedSig);
}
};
}
// This parser implements the 'Top down operator precedence' algorithm developed by Vaughan R Pratt; http://dl.acm.org/citation.cfm?id=512931.
// and builds on the Javascript framework described by Douglas Crockford at http://javascript.crockford.com/tdop/tdop.html
// and in 'Beautiful Code', edited by Andy Oram and Greg Wilson, Copyright 2007 O'Reilly Media, Inc. 798-0-596-51004-6
var parser = function (source) {
var node;
var lexer;
var symbol_table = {};
var base_symbol = {
nud: function () {
return this;
}
};
var symbol = function (id, bp) {
var s = symbol_table[id];
bp = bp || 0;
if (s) {
if (bp >= s.lbp) {
s.lbp = bp;
}
} else {
s = Object.create(base_symbol);
s.id = s.value = id;
s.lbp = bp;
symbol_table[id] = s;
}
return s;
};
var advance = function (id, infix) {
if (id && node.id !== id) {
var code;
if(node.id === '(end)') {
// unexpected end of buffer
code = "S0203";
} else {
code = "S0202";
}
throw {
code: code,
stack: (new Error()).stack,
position: node.position,
token: node.id,
value: id
};
}
var next_token = lexer(infix);
if (next_token === null) {
node = symbol_table["(end)"];
node.position = source.length;
return node;
}
var value = next_token.value;
var type = next_token.type;
var symbol;
switch (type) {
case 'name':
case 'variable':
symbol = symbol_table["(name)"];
break;
case 'operator':
symbol = symbol_table[value];
if (!symbol) {
throw {
code: "S0204",
stack: (new Error()).stack,
position: next_token.position,
token: value
};
}
break;
case 'string':
case 'number':
case 'value':
type = "literal";
symbol = symbol_table["(literal)"];
break;
case 'regex':
type = "regex";
symbol = symbol_table["(regex)"];
break;
/* istanbul ignore next */
default:
throw {
code: "S0205",
stack: (new Error()).stack,
position: next_token.position,
token: value
};
}
node = Object.create(symbol);
node.value = value;
node.type = type;
node.position = next_token.position;
return node;
};
// Pratt's algorithm
var expression = function (rbp) {
var left;
var t = node;
advance(null, true);
left = t.nud();
while (rbp < node.lbp) {
t = node;
advance();
left = t.led(left);
}
return left;
};
// match infix operators
// <expression> <operator> <expression>
// left associative
var infix = function (id, bp, led) {
var bindingPower = bp || operators[id];
var s = symbol(id, bindingPower);
s.led = led || function (left) {
this.lhs = left;
this.rhs = expression(bindingPower);
this.type = "binary";
return this;
};
return s;
};
// match infix operators
// <expression> <operator> <expression>
// right associative
var infixr = function (id, bp, led) {
var bindingPower = bp || operators[id];
var s = symbol(id, bindingPower);
s.led = led || function (left) {
this.lhs = left;
this.rhs = expression(bindingPower - 1); // subtract 1 from bindingPower for right associative operators
this.type = "binary";
return this;
};
return s;
};
// match prefix operators
// <operator> <expression>
var prefix = function (id, nud) {
var s = symbol(id);
s.nud = nud || function () {
this.expression = expression(70);
this.type = "unary";
return this;
};
return s;
};
symbol("(end)");
symbol("(name)");
symbol("(literal)");
symbol("(regex)");
symbol(":");
symbol(";");
symbol(",");
symbol(")");
symbol("]");
symbol("}");
symbol(".."); // range operator
infix("."); // field reference
infix("+"); // numeric addition
infix("-"); // numeric subtraction
infix("*"); // numeric multiplication
infix("/"); // numeric division
infix("%"); // numeric modulus
infix("="); // equality
infix("<"); // less than
infix(">"); // greater than
infix("!="); // not equal to
infix("<="); // less than or equal
infix(">="); // greater than or equal
infix("&"); // string concatenation
infix("and"); // Boolean AND
infix("or"); // Boolean OR
infix("in"); // is member of array
infixr(":="); // bind variable
prefix("-"); // unary numeric negation
infix("~>"); // function application
// field wildcard (single level)
prefix('*', function () {
this.type = "wildcard";
return this;
});
// descendant wildcard (multi-level)
prefix('**', function () {
this.type = "descendant";
return this;
});
// function invocation
infix("(", operators['('], function (left) {
// left is is what we are trying to invoke
this.procedure = left;
this.type = 'function';
this.arguments = [];
if (node.id !== ')') {
for (;;) {
if (node.type === 'operator' && node.id === '?') {
// partial function application
this.type = 'partial';
this.arguments.push(node);
advance('?');
} else {
this.arguments.push(expression(0));
}
if (node.id !== ',') break;
advance(',');
}
}
advance(")", true);
// if the name of the function is 'function' or λ, then this is function definition (lambda function)
if (left.type === 'name' && (left.value === 'function' || left.value === '\u03BB')) {
// all of the args must be VARIABLE tokens
this.arguments.forEach(function (arg, index) {
if (arg.type !== 'variable') {
throw {
code: "S0208",
stack: (new Error()).stack,
position: arg.position,
token: arg.value,
value: index + 1
};
}
});
this.type = 'lambda';
// is the next token a '<' - if so, parse the function signature
if(node.id === '<') {
var sigPos = node.position;
var depth = 1;
var sig = '<';
while(depth > 0 && node.id !== '{' && node.id !== '(end)') {
var tok = advance();
if(tok.id === '>') {
depth--;
} else if(tok.id === '<') {
depth++;
}
sig += tok.value;
}
advance('>');
try {
this.signature = parseSignature(sig);
} catch(err) {
// insert the position into this error
err.position = sigPos + err.offset;
throw err;
}
}
// parse the function body
advance('{');
this.body = expression(0);
advance('}');
}
return this;
});
// parenthesis - block expression
prefix("(", function () {
var expressions = [];
while (node.id !== ")") {
expressions.push(expression(0));
if (node.id !== ";") {
break;
}
advance(";");
}
advance(")", true);
this.type = 'block';
this.expressions = expressions;
return this;
});
// array constructor
prefix("[", function () {
var a = [];
if (node.id !== "]") {
for (;;) {
var item = expression(0);
if (node.id === "..") {
// range operator
var range = {type: "binary", value: "..", position: node.position, lhs: item};
advance("..");
range.rhs = expression(0);
item = range;
}
a.push(item);
if (node.id !== ",") {
break;
}
advance(",");
}
}
advance("]", true);
this.lhs = a;
this.type = "unary";
return this;
});
// filter - predicate or array index
infix("[", operators['['], function (left) {
if(node.id === "]") {
// empty predicate means maintain singleton arrays in the output
var step = left;
while(step && step.type === 'binary' && step.value === '[') {
step = step.lhs;
}
step.keepArray = true;
advance("]");
return left;
} else {
this.lhs = left;
this.rhs = expression(operators[']']);
this.type = 'binary';
advance("]", true);
return this;
}
});
var objectParser = function (left) {
var a = [];
if (node.id !== "}") {
for (;;) {
var n = expression(0);
advance(":");
var v = expression(0);
a.push([n, v]); // holds an array of name/value expression pairs
if (node.id !== ",") {
break;
}
advance(",");
}
}
advance("}", true);
if(typeof left === 'undefined') {
// NUD - unary prefix form
this.lhs = a;
this.type = "unary";
} else {
// LED - binary infix form
this.lhs = left;
this.rhs = a;
this.type = 'binary';
}
return this;
};
// object constructor
prefix("{", objectParser);
// object grouping
infix("{", operators['{'], objectParser);
// if/then/else ternary operator ?:
infix("?", operators['?'], function (left) {
this.type = 'condition';
this.condition = left;
this.then = expression(0);
if (node.id === ':') {
// else condition
advance(":");
this.else = expression(0);
}
return this;
});
// tail call optimization
// this is invoked by the post parser to analyse lambda functions to see
// if they make a tail call. If so, it is replaced by a thunk which will
// be invoked by the trampoline loop during function application.
// This enables tail-recursive functions to be written without growing the stack
var tail_call_optimize = function(expr) {
var result;
if(expr.type === 'function') {
var thunk = {type: 'lambda', thunk: true, arguments: [], position: expr.position};
thunk.body = expr;
result = thunk;
} else if(expr.type === 'condition') {
// analyse both branches
expr.then = tail_call_optimize(expr.then);
expr.else = tail_call_optimize(expr.else);
result = expr;
} else if(expr.type === 'block') {
// only the last expression in the block
var length = expr.expressions.length;
if(length > 0) {
expr.expressions[length - 1] = tail_call_optimize(expr.expressions[length - 1]);
}
result = expr;
} else {
result = expr;
}
return result;
};
// post-parse stage
// the purpose of this is flatten the parts of the AST representing location paths,
// converting them to arrays of steps which in turn may contain arrays of predicates.
// following this, nodes containing '.' and '[' should be eliminated from the AST.
var post_parse = function (expr) {
var result = [];
switch (expr.type) {
case 'binary':
switch (expr.value) {
case '.':
var lstep = post_parse(expr.lhs);
if (lstep.type === 'path') {
Array.prototype.push.apply(result, lstep);
} else {
result.push(lstep);
}
var rest = post_parse(expr.rhs);
if(rest.type !== 'path') {
rest = [rest];
}
Array.prototype.push.apply(result, rest);
result.type = 'path';
break;
case '[':
// predicated step
// LHS is a step or a predicated step
// RHS is the predicate expr
result = post_parse(expr.lhs);
var step = result;
if(result.type === 'path') {
step = result[result.length - 1];
}
if (typeof step.group !== 'undefined') {
throw {
code: "S0209",
stack: (new Error()).stack,
position: expr.position
};
}
if (typeof step.predicate === 'undefined') {
step.predicate = [];
}
step.predicate.push(post_parse(expr.rhs));
break;
case '{':
// group-by
// LHS is a step or a predicated step
// RHS is the object constructor expr
result = post_parse(expr.lhs);
if (typeof result.group !== 'undefined') {
throw {
code: "S0210",
stack: (new Error()).stack,
position: expr.position
};
}
// object constructor - process each pair
result.group = {
lhs: expr.rhs.map(function (pair) {
return [post_parse(pair[0]), post_parse(pair[1])];
}),
position: expr.position
};
break;
default:
result = {type: expr.type, value: expr.value, position: expr.position};
result.lhs = post_parse(expr.lhs);
result.rhs = post_parse(expr.rhs);
}
break;
case 'unary':
result = {type: expr.type, value: expr.value, position: expr.position};
if (expr.value === '[') {
// array constructor - process each item
result.lhs = expr.lhs.map(function (item) {
return post_parse(item);
});
} else if (expr.value === '{') {
// object constructor - process each pair
result.lhs = expr.lhs.map(function (pair) {
return [post_parse(pair[0]), post_parse(pair[1])];
});
} else {
// all other unary expressions - just process the expression
result.expression = post_parse(expr.expression);
// if unary minus on a number, then pre-process
if (expr.value === '-' && result.expression.type === 'literal' && isNumeric(result.expression.value)) {
result = result.expression;
result.value = -result.value;
}
}
break;
case 'function':
case 'partial':
result = {type: expr.type, name: expr.name, value: expr.value, position: expr.position};
result.arguments = expr.arguments.map(function (arg) {
return post_parse(arg);
});
result.procedure = post_parse(expr.procedure);
break;
case 'lambda':
result = {type: expr.type, arguments: expr.arguments, signature: expr.signature, position: expr.position};
var body = post_parse(expr.body);
result.body = tail_call_optimize(body);
break;
case 'condition':
result = {type: expr.type, position: expr.position};
result.condition = post_parse(expr.condition);
result.then = post_parse(expr.then);
if (typeof expr.else !== 'undefined') {
result.else = post_parse(expr.else);
}
break;
case 'block':
result = {type: expr.type, position: expr.position};
// array of expressions - process each one
result.expressions = expr.expressions.map(function (item) {
return post_parse(item);
});
// TODO scan the array of expressions to see if any of them assign variables
// if so, need to mark the block as one that needs to create a new frame
break;
case 'name':
result = [expr];
result.type = 'path';
break;
case 'literal':
case 'wildcard':
case 'descendant':
case 'variable':
case 'regex':
result = expr;
break;
case 'operator':
// the tokens 'and' and 'or' might have been used as a name rather than an operator
if (expr.value === 'and' || expr.value === 'or' || expr.value === 'in') {
expr.type = 'name';
result = post_parse(expr);
} else if (expr.value === '?') {
// partial application
result = expr;
} else {
throw {
code: "S0201",
stack: (new Error()).stack,
position: expr.position,
token: expr.value
};
}
break;
default:
var code = "S0206";
/* istanbul ignore else */
if (expr.id === '(end)') {
code = "S0207";
}
throw {
code: code,
stack: (new Error()).stack,
position: expr.position,
token: expr.value
};
}
return result;
};
// now invoke the tokenizer and the parser and return the syntax tree
lexer = tokenizer(source);
advance();
// parse the tokens
var expr = expression(0);
if (node.id !== '(end)') {
throw {
code: "S0201",
stack: (new Error()).stack,
position: node.position,
token: node.value
};
}
expr = post_parse(expr);
return expr;
};
// Start of Evaluator code
var staticFrame = createFrame(null);
/**
* Check if value is a finite number
* @param {float} n - number to evaluate
* @returns {boolean} True if n is a finite number
*/
function isNumeric(n) {
var isNum = false;
if(typeof n === 'number') {
var num = parseFloat(n);
isNum = !isNaN(num);
if (isNum && !isFinite(num)) {
throw {
code: "D1001",
value: n,
stack: (new Error()).stack
};
}
}
return isNum;
}
/**
* Returns true if the arg is an array of numbers
* @param {*} arg - the item to test
* @returns {boolean} True if arg is an array of numbers
*/
function isArrayOfNumbers(arg) {
var result = false;
if(Array.isArray(arg)) {
result = (arg.filter(function(item){return !isNumeric(item);}).length === 0);
}
return result;
}
// Polyfill
/* istanbul ignore next */
Number.isInteger = Number.isInteger || function(value) {
return typeof value === "number" &&
isFinite(value) &&
Math.floor(value) === value;
};
/**
* Evaluate expression against input data
* @param {Object} expr - JSONata expression
* @param {Object} input - Input data to evaluate against
* @param {Object} environment - Environment
* @returns {*} Evaluated input data
*/
function evaluate(expr, input, environment) {
var result;
var entryCallback = environment.lookup('__evaluate_entry');
if(entryCallback) {
entryCallback(expr, input, environment);
}
switch (expr.type) {
case 'path':
result = evaluatePath(expr, input, environment);
break;
case 'binary':
result = evaluateBinary(expr, input, environment);
break;
case 'unary':
result = evaluateUnary(expr, input, environment);
break;
case 'name':
result = evaluateName(expr, input, environment);
break;
case 'literal':
result = evaluateLiteral(expr, input, environment);
break;
case 'wildcard':
result = evaluateWildcard(expr, input, environment);
break;
case 'descendant':
result = evaluateDescendants(expr, input, environment);
break;
case 'condition':
result = evaluateCondition(expr, input, environment);
break;
case 'block':
result = evaluateBlock(expr, input, environment);
break;
case 'regex':
result = evaluateRegex(expr, input, environment);
break;
case 'function':
result = evaluateFunction(expr, input, environment);
break;
case 'variable':
result = evaluateVariable(expr, input, environment);
break;
case 'lambda':
result = evaluateLambda(expr, input, environment);
break;
case 'partial':
result = evaluatePartialApplication(expr, input, environment);
b