diff --git a/Gruntfile.js b/Gruntfile.js
index ffb1df3c3..4fc9b6fed 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -160,7 +160,8 @@ module.exports = function(grunt) {
build: {
files: {
'public/red/red.min.js': 'public/red/red.js',
- 'public/red/main.min.js': 'public/red/main.js'
+ 'public/red/main.min.js': 'public/red/main.js',
+ 'public/vendor/jsonata/jsonata.min.js': 'editor/vendor/jsonata/jsonata.js'
}
}
},
diff --git a/editor/images/typedInput/expr.png b/editor/images/typedInput/expr.png
new file mode 100644
index 000000000..74d9516ae
Binary files /dev/null and b/editor/images/typedInput/expr.png differ
diff --git a/editor/js/ui/common/typedInput.js b/editor/js/ui/common/typedInput.js
index a455efc66..b30af8961 100644
--- a/editor/js/ui/common/typedInput.js
+++ b/editor/js/ui/common/typedInput.js
@@ -94,7 +94,8 @@
bool: {value:"bool",label:"boolean",icon:"red/images/typedInput/bool.png",options:["true","false"]},
json: {value:"json",label:"JSON",icon:"red/images/typedInput/json.png", validate: function(v) { try{JSON.parse(v);return true;}catch(e){return false;}}},
re: {value:"re",label:"regular expression",icon:"red/images/typedInput/re.png"},
- date: {value:"date",label:"timestamp",hasValue:false}
+ date: {value:"date",label:"timestamp",hasValue:false},
+ jsonata: {value:"jsonata",label:"expression",icon:"red/images/typedInput/expr.png", validate: function(v) { try{jsonata(v);return true;}catch(e){return false;}}},
};
var nlsd = false;
diff --git a/editor/templates/index.mst b/editor/templates/index.mst
index 146cc866b..0cd340863 100644
--- a/editor/templates/index.mst
+++ b/editor/templates/index.mst
@@ -160,6 +160,7 @@
+
diff --git a/editor/vendor/jsonata/jsonata.js b/editor/vendor/jsonata/jsonata.js
new file mode 100644
index 000000000..2883b5fc4
--- /dev/null
+++ b/editor/vendor/jsonata/jsonata.js
@@ -0,0 +1,2805 @@
+/**
+ * © Copyright IBM Corp. 2016 All Rights Reserved
+ * Project name: JSONata
+ * This project is licensed under the MIT License, see LICENSE
+ */
+
+'use strict';
+/**
+ * @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() {
+ 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,
+ 'and': 30,
+ 'or': 25,
+ 'in': 40,
+ '&': 50,
+ '!': 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 next = function () {
+ 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);
+ }
+ // 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', '**');
+ }
+ // test for 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 {
+ message: "The escape sequence \\u must be followed by 4 hex digits at column " + position,
+ stack: (new Error()).stack,
+ position: position
+ };
+ }
+ } else {
+ // illegal escape sequence
+ throw {
+ message: 'unsupported escape sequence: \\' + currentChar + ' at column ' + position,
+ stack: (new Error()).stack,
+ position: position,
+ token: currentChar
+ };
+
+ }
+ } else if (currentChar === quoteType) {
+ position++;
+ return create('string', qstr);
+ } else {
+ qstr += currentChar;
+ }
+ position++;
+ }
+ throw {
+ message: 'no terminating quote found in string literal at column ' + position,
+ 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 {
+ message: 'Number out of range: ' + match[0] + ' at column ' + position,
+ 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 'and':
+ case 'or':
+ case 'in':
+ 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;
+ };
+
+
+ // 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) {
+ if (id && node.id !== id) {
+ var msg;
+ if(node.id === '(end)') {
+ // unexpected end of buffer
+ msg = "Syntax error: expected '" + id + "' before end of expression";
+ } else {
+ msg = "Syntax error: expected '" + id + "', got '" + node.id + "' at column " + node.position;
+ }
+ throw {
+ message: msg ,
+ stack: (new Error()).stack,
+ position: node.position,
+ token: node.id,
+ value: id
+ };
+ }
+ var next_token = lexer();
+ if (next_token === null) {
+ node = symbol_table["(end)"];
+ 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 {
+ message: "Unknown operator: " + value + " at column " + next_token.position,
+ stack: (new Error()).stack,
+ position: next_token.position,
+ token: value
+ };
+ }
+ break;
+ case 'string':
+ case 'number':
+ case 'value':
+ type = "literal";
+ symbol = symbol_table["(literal)"];
+ break;
+ /* istanbul ignore next */
+ default:
+ throw {
+ message: "Unexpected token:" + value + " at column " + next_token.position,
+ 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();
+ left = t.nud();
+ while (rbp < node.lbp) {
+ t = node;
+ advance();
+ left = t.led(left);
+ }
+ return left;
+ };
+
+ // match infix operators
+ //
+ // 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
+ //
+ // 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
+ //
+ 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(":");
+ 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
+
+ // 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(")");
+ // 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 {
+ message: 'Parameter ' + (index + 1) + ' of function definition must be a variable name (start with $)',
+ stack: (new Error()).stack,
+ position: arg.position,
+ token: arg.value
+ };
+ }
+ });
+ this.type = 'lambda';
+ // 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(")");
+ this.type = 'block';
+ this.expressions = expressions;
+ return this;
+ });
+
+ // object constructor
+ prefix("{", function () {
+ 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("}");
+ this.lhs = a;
+ this.type = "unary";
+ 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("]");
+ this.lhs = a;
+ this.type = "unary";
+ return this;
+ });
+
+ // filter - predicate or array index
+ infix("[", operators['['], function (left) {
+ this.lhs = left;
+ this.rhs = expression(operators[']']);
+ this.type = 'binary';
+ advance("]");
+ return this;
+ });
+
+ // aggregator
+ infix("{", operators['{'], function (left) {
+ this.lhs = left;
+ this.rhs = expression(operators['}']);
+ this.type = 'binary';
+ advance("}");
+ return this;
+ });
+
+ // 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 step = post_parse(expr.lhs);
+ if (Array.isArray(step)) {
+ Array.prototype.push.apply(result, step);
+ } else {
+ result.push(step);
+ }
+ var rest = [post_parse(expr.rhs)];
+ 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);
+ if (typeof result.aggregate !== 'undefined') {
+ throw {
+ message: 'A predicate cannot follow an aggregate in a step. Error at column: ' + expr.position,
+ stack: (new Error()).stack,
+ position: expr.position
+ };
+ }
+ if (typeof result.predicate === 'undefined') {
+ result.predicate = [];
+ }
+ result.predicate.push(post_parse(expr.rhs));
+ break;
+ case '{':
+ // aggregate
+ // LHS is a step or a predicated step
+ // RHS is the predicate expr
+ result = post_parse(expr.lhs);
+ if (typeof result.aggregate !== 'undefined') {
+ throw {
+ message: 'Each step can only have one aggregator. Error at column: ' + expr.position,
+ stack: (new Error()).stack,
+ position: expr.position
+ };
+ }
+ result.aggregate = post_parse(expr.rhs);
+ 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, 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':
+ case 'literal':
+ case 'wildcard':
+ case 'descendant':
+ case 'variable':
+ 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 {
+ message: "Syntax error: " + expr.value + " at column " + expr.position,
+ stack: (new Error()).stack,
+ position: expr.position,
+ token: expr.value
+ };
+ }
+ break;
+ default:
+ var reason = "Unknown expression type: " + expr.value + " at column " + expr.position;
+ /* istanbul ignore else */
+ if (expr.id === '(end)') {
+ reason = "Syntax error: unexpected end of expression";
+ }
+ throw {
+ message: reason,
+ 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 {
+ message: "Syntax error: " + node.value + " at column " + node.position,
+ stack: (new Error()).stack,
+ position: node.position,
+ token: node.value
+ };
+ }
+ expr = post_parse(expr);
+
+ // a single name token is a single step location path
+ if (expr.type === 'name') {
+ expr = [expr];
+ expr.type = 'path';
+ }
+
+ return expr;
+ };
+
+ /**
+ * 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 {
+ message: "Number out of range",
+ 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 '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);
+ break;
+ }
+ if (expr.hasOwnProperty('predicate')) {
+ result = applyPredicates(expr.predicate, result, environment);
+ }
+ if (expr.hasOwnProperty('aggregate')) {
+ result = applyAggregate(expr.aggregate, result, environment);
+ }
+
+ var exitCallback = environment.lookup('__evaluate_exit');
+ if(exitCallback) {
+ exitCallback(expr, input, environment, result);
+ }
+
+ return result;
+ }
+
+ /**
+ * Evaluate path 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 evaluatePath(expr, input, environment) {
+ var result;
+ var inputSequence;
+ // expr is an array of steps
+ // if the first step is a variable reference ($...), including root reference ($$),
+ // then the path is absolute rather than relative
+ if (expr[0].type === 'variable') {
+ expr[0].absolute = true;
+ } else if(expr[0].type === 'unary' && expr[0].value === '[') {
+ // array constructor - not relative to the input
+ input = [null];// dummy singleton sequence for first step
+ }
+
+ // evaluate each step in turn
+ expr.forEach(function (step) {
+ var resultSequence = [];
+ result = undefined;
+ // if input is not an array, make it so
+ if (step.absolute === true) {
+ inputSequence = [input]; // dummy singleton sequence for first (absolute) step
+ } else if (Array.isArray(input)) {
+ inputSequence = input;
+ } else {
+ inputSequence = [input];
+ }
+ // if there is more than one step in the path, handle quoted field names as names not literals
+ if (expr.length > 1 && step.type === 'literal') {
+ step.type = 'name';
+ }
+ if (step.value === '{') {
+ if(typeof input !== 'undefined') {
+ result = evaluateGroupExpression(step, inputSequence, environment);
+ }
+ } else {
+ inputSequence.forEach(function (item) {
+ var res = evaluate(step, item, environment);
+ if (typeof res !== 'undefined') {
+ if (Array.isArray(res)) {
+ // is res an array - if so, flatten it into the parent array
+ res.forEach(function (innerRes) {
+ if (typeof innerRes !== 'undefined') {
+ resultSequence.push(innerRes);
+ }
+ });
+ } else {
+ resultSequence.push(res);
+ }
+ }
+ });
+ if (resultSequence.length == 1) {
+ result = resultSequence[0];
+ } else if (resultSequence.length > 1) {
+ result = resultSequence;
+ }
+ }
+
+ input = result;
+ });
+ return result;
+ }
+
+ /**
+ * Apply predicates to input data
+ * @param {Object} predicates - Predicates
+ * @param {Object} input - Input data to apply predicates against
+ * @param {Object} environment - Environment
+ * @returns {*} Result after applying predicates
+ */
+ function applyPredicates(predicates, input, environment) {
+ var result;
+ var inputSequence = input;
+ // lhs potentially holds an array
+ // we want to iterate over the array, and only keep the items that are
+ // truthy when applied to the predicate.
+ // if the predicate evaluates to an integer, then select that index
+
+ var results = [];
+ predicates.forEach(function (predicate) {
+ // if it's not an array, turn it into one
+ // since in XPath >= 2.0 an item is equivalent to a singleton sequence of that item
+ // if input is not an array, make it so
+ if (!Array.isArray(inputSequence)) {
+ inputSequence = [inputSequence];
+ }
+ results = [];
+ result = undefined;
+ if (predicate.type === 'literal' && isNumeric(predicate.value)) {
+ var index = predicate.value;
+ if (!Number.isInteger(index)) {
+ // round it down
+ index = Math.floor(index);
+ }
+ if (index < 0) {
+ // count in from end of array
+ index = inputSequence.length + index;
+ }
+ result = inputSequence[index];
+ } else {
+ inputSequence.forEach(function (item, index) {
+ var res = evaluate(predicate, item, environment);
+ if (isNumeric(res)) {
+ res = [res];
+ }
+ if(isArrayOfNumbers(res)) {
+ res.forEach(function(ires) {
+ if (!Number.isInteger(ires)) {
+ // round it down
+ ires = Math.floor(ires);
+ }
+ if (ires < 0) {
+ // count in from end of array
+ ires = inputSequence.length + ires;
+ }
+ if (ires == index) {
+ results.push(item);
+ }
+ });
+ } else if (functionBoolean(res)) { // truthy
+ results.push(item);
+ }
+ });
+ }
+ if (results.length == 1) {
+ result = results[0];
+ } else if (results.length > 1) {
+ result = results;
+ }
+ inputSequence = result;
+ });
+ return result;
+ }
+
+ /**
+ * Apply aggregate to input data
+ * @param {Object} expr - JSONata expression
+ * @param {Object} input - Input data to evaluate against
+ * @param {Object} environment - Environment
+ * @returns {{}} Result after applying aggregate
+ */
+ function applyAggregate(expr, input, environment) {
+ var result = {};
+ // this is effectively a 'reduce' HOF (fold left)
+ // if the input is a singleton, then just return this as the result
+ // otherwise iterate over the input array and aggregate the result
+ if (Array.isArray(input)) {
+ // create a new frame to limit the scope
+ var aggEnv = createFrame(environment);
+ // the variable $@ will hold the aggregated value, initialize this to the first array item
+ aggEnv.bind('_', input[0]);
+ // loop over the remainder of the array
+ for (var index = 1; index < input.length; index++) {
+ var reduce = evaluate(expr, input[index], aggEnv);
+ aggEnv.bind('_', reduce);
+ }
+ result = aggEnv.lookup('_');
+ } else {
+ result = input;
+ }
+ return result;
+ }
+
+ /**
+ * Evaluate binary 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 evaluateBinary(expr, input, environment) {
+ var result;
+
+ switch (expr.value) {
+ case '+':
+ case '-':
+ case '*':
+ case '/':
+ case '%':
+ result = evaluateNumericExpression(expr, input, environment);
+ break;
+ case '=':
+ case '!=':
+ case '<':
+ case '<=':
+ case '>':
+ case '>=':
+ result = evaluateComparisonExpression(expr, input, environment);
+ break;
+ case '&':
+ result = evaluateStringConcat(expr, input, environment);
+ break;
+ case 'and':
+ case 'or':
+ result = evaluateBooleanExpression(expr, input, environment);
+ break;
+ case '..':
+ result = evaluateRangeExpression(expr, input, environment);
+ break;
+ case ':=':
+ result = evaluateBindExpression(expr, input, environment);
+ break;
+ case 'in':
+ result = evaluateIncludesExpression(expr, input, environment);
+ break;
+ }
+ return result;
+ }
+
+ /**
+ * Evaluate unary 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 evaluateUnary(expr, input, environment) {
+ var result;
+
+ switch (expr.value) {
+ case '-':
+ result = evaluate(expr.expression, input, environment);
+ if (isNumeric(result)) {
+ result = -result;
+ } else {
+ throw {
+ message: "Cannot negate a non-numeric value: " + result + " at column " + expr.position,
+ stack: (new Error()).stack,
+ position: expr.position,
+ token: expr.value,
+ value: result
+ };
+ }
+ break;
+ case '[':
+ // array constructor - evaluate each item
+ result = [];
+ expr.lhs.forEach(function (item) {
+ var value = evaluate(item, input, environment);
+ if (typeof value !== 'undefined') {
+ if (item.value === '..') {
+ // array generated by the range operator - merge into results
+ result = functionAppend(result, value);
+ } else {
+ result.push(value);
+ }
+ }
+ });
+ break;
+ case '{':
+ // object constructor - apply grouping
+ result = evaluateGroupExpression(expr, input, environment);
+ break;
+
+ }
+ return result;
+ }
+
+ /**
+ * Evaluate name object 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 evaluateName(expr, input, environment) {
+ // lookup the 'name' item in the input
+ var result;
+ if (Array.isArray(input)) {
+ var results = [];
+ input.forEach(function (item) {
+ var res = evaluateName(expr, item, environment);
+ if (typeof res !== 'undefined') {
+ results.push(res);
+ }
+ });
+ if (results.length == 1) {
+ result = results[0];
+ } else if (results.length > 1) {
+ result = results;
+ }
+ } else if (input !== null && typeof input === 'object') {
+ result = input[expr.value];
+ }
+ return result;
+ }
+
+ /**
+ * Evaluate literal against input data
+ * @param {Object} expr - JSONata expression
+ * @returns {*} Evaluated input data
+ */
+ function evaluateLiteral(expr) {
+ return expr.value;
+ }
+
+ /**
+ * Evaluate wildcard against input data
+ * @param {Object} expr - JSONata expression
+ * @param {Object} input - Input data to evaluate against
+ * @returns {*} Evaluated input data
+ */
+ function evaluateWildcard(expr, input) {
+ var result;
+ var results = [];
+ if (input !== null && typeof input === 'object') {
+ Object.keys(input).forEach(function (key) {
+ var value = input[key];
+ if(Array.isArray(value)) {
+ value = flatten(value);
+ results = functionAppend(results, value);
+ } else {
+ results.push(value);
+ }
+ });
+ }
+
+ if (results.length == 1) {
+ result = results[0];
+ } else if (results.length > 1) {
+ result = results;
+ }
+ return result;
+ }
+
+ /**
+ * Returns a flattened array
+ * @param {Array} arg - the array to be flatten
+ * @param {Array} flattened - carries the flattened array - if not defined, will initialize to []
+ * @returns {Array} - the flattened array
+ */
+ function flatten(arg, flattened) {
+ if(typeof flattened === 'undefined') {
+ flattened = [];
+ }
+ if(Array.isArray(arg)) {
+ arg.forEach(function (item) {
+ flatten(item, flattened);
+ });
+ } else {
+ flattened.push(arg);
+ }
+ return flattened;
+ }
+
+ /**
+ * Evaluate descendants against input data
+ * @param {Object} expr - JSONata expression
+ * @param {Object} input - Input data to evaluate against
+ * @returns {*} Evaluated input data
+ */
+ function evaluateDescendants(expr, input) {
+ var result;
+ var resultSequence = [];
+ if (typeof input !== 'undefined') {
+ // traverse all descendants of this object/array
+ recurseDescendants(input, resultSequence);
+ if (resultSequence.length == 1) {
+ result = resultSequence[0];
+ } else {
+ result = resultSequence;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Recurse through descendants
+ * @param {Object} input - Input data
+ * @param {Object} results - Results
+ */
+ function recurseDescendants(input, results) {
+ // this is the equivalent of //* in XPath
+ if (!Array.isArray(input)) {
+ results.push(input);
+ }
+ if (Array.isArray(input)) {
+ input.forEach(function (member) {
+ recurseDescendants(member, results);
+ });
+ } else if (input !== null && typeof input === 'object') {
+ Object.keys(input).forEach(function (key) {
+ recurseDescendants(input[key], results);
+ });
+ }
+ }
+
+ /**
+ * Evaluate numeric 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 evaluateNumericExpression(expr, input, environment) {
+ var result;
+
+ var lhs = evaluate(expr.lhs, input, environment);
+ var rhs = evaluate(expr.rhs, input, environment);
+
+ if (typeof lhs === 'undefined' || typeof rhs === 'undefined') {
+ // if either side is undefined, the result is undefined
+ return result;
+ }
+
+ if (!isNumeric(lhs)) {
+ throw {
+ message: 'LHS of ' + expr.value + ' operator must evaluate to a number at column ' + expr.position,
+ stack: (new Error()).stack,
+ position: expr.position,
+ token: expr.value,
+ value: lhs
+ };
+ }
+ if (!isNumeric(rhs)) {
+ throw {
+ message: 'RHS of ' + expr.value + ' operator must evaluate to a number at column ' + expr.position,
+ stack: (new Error()).stack,
+ position: expr.position,
+ token: expr.value,
+ value: rhs
+ };
+ }
+
+ switch (expr.value) {
+ case '+':
+ result = lhs + rhs;
+ break;
+ case '-':
+ result = lhs - rhs;
+ break;
+ case '*':
+ result = lhs * rhs;
+ break;
+ case '/':
+ result = lhs / rhs;
+ break;
+ case '%':
+ result = lhs % rhs;
+ break;
+ }
+ return result;
+ }
+
+ /**
+ * Evaluate comparison 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 evaluateComparisonExpression(expr, input, environment) {
+ var result;
+
+ var lhs = evaluate(expr.lhs, input, environment);
+ var rhs = evaluate(expr.rhs, input, environment);
+
+ if (typeof lhs === 'undefined' || typeof rhs === 'undefined') {
+ // if either side is undefined, the result is false
+ return false;
+ }
+
+ switch (expr.value) {
+ case '=':
+ result = lhs == rhs;
+ break;
+ case '!=':
+ result = (lhs != rhs);
+ break;
+ case '<':
+ result = lhs < rhs;
+ break;
+ case '<=':
+ result = lhs <= rhs;
+ break;
+ case '>':
+ result = lhs > rhs;
+ break;
+ case '>=':
+ result = lhs >= rhs;
+ break;
+ }
+ return result;
+ }
+
+ /**
+ * Inclusion operator - in
+ *
+ * @param {Object} expr - AST
+ * @param {*} input - input context
+ * @param {Object} environment - frame
+ * @returns {boolean} - true if lhs is a member of rhs
+ */
+ function evaluateIncludesExpression(expr, input, environment) {
+ var result = false;
+
+ var lhs = evaluate(expr.lhs, input, environment);
+ var rhs = evaluate(expr.rhs, input, environment);
+
+ if (typeof lhs === 'undefined' || typeof rhs === 'undefined') {
+ // if either side is undefined, the result is false
+ return false;
+ }
+
+ if(!Array.isArray(rhs)) {
+ rhs = [rhs];
+ }
+
+ for(var i = 0; i < rhs.length; i++) {
+ if(rhs[i] === lhs) {
+ result = true;
+ break;
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Evaluate boolean 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 evaluateBooleanExpression(expr, input, environment) {
+ var result;
+
+ switch (expr.value) {
+ case 'and':
+ result = functionBoolean(evaluate(expr.lhs, input, environment)) &&
+ functionBoolean(evaluate(expr.rhs, input, environment));
+ break;
+ case 'or':
+ result = functionBoolean(evaluate(expr.lhs, input, environment)) ||
+ functionBoolean(evaluate(expr.rhs, input, environment));
+ break;
+ }
+ return result;
+ }
+
+ /**
+ * Evaluate string concatenation against input data
+ * @param {Object} expr - JSONata expression
+ * @param {Object} input - Input data to evaluate against
+ * @param {Object} environment - Environment
+ * @returns {string|*} Evaluated input data
+ */
+ function evaluateStringConcat(expr, input, environment) {
+ var result;
+ var lhs = evaluate(expr.lhs, input, environment);
+ var rhs = evaluate(expr.rhs, input, environment);
+
+ var lstr = '';
+ var rstr = '';
+ if (typeof lhs !== 'undefined') {
+ lstr = functionString(lhs);
+ }
+ if (typeof rhs !== 'undefined') {
+ rstr = functionString(rhs);
+ }
+
+ result = lstr.concat(rstr);
+ return result;
+ }
+
+ /**
+ * Evaluate group 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 evaluateGroupExpression(expr, input, environment) {
+ var result = {};
+ var groups = {};
+ // group the input sequence by 'key' expression
+ if (!Array.isArray(input)) {
+ input = [input];
+ }
+ input.forEach(function (item) {
+ expr.lhs.forEach(function (pair) {
+ var key = evaluate(pair[0], item, environment);
+ // key has to be a string
+ if (typeof key !== 'string') {
+ throw {
+ message: 'Key in object structure must evaluate to a string. Got: ' + key + ' at column ' + expr.position,
+ stack: (new Error()).stack,
+ position: expr.position,
+ value: key
+ };
+ }
+ var entry = {data: item, expr: pair[1]};
+ if (groups.hasOwnProperty(key)) {
+ // a value already exists in this slot
+ // append it as an array
+ groups[key].data = functionAppend(groups[key].data, item);
+ } else {
+ groups[key] = entry;
+ }
+ });
+ });
+ // iterate over the groups to evaluate the 'value' expression
+ for (var key in groups) {
+ var entry = groups[key];
+ var value = evaluate(entry.expr, entry.data, environment);
+ result[key] = value;
+ }
+ return result;
+ }
+
+ /**
+ * Evaluate range 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 evaluateRangeExpression(expr, input, environment) {
+ var result;
+
+ var lhs = evaluate(expr.lhs, input, environment);
+ var rhs = evaluate(expr.rhs, input, environment);
+
+ if (typeof lhs === 'undefined' || typeof rhs === 'undefined') {
+ // if either side is undefined, the result is undefined
+ return result;
+ }
+
+ if (lhs > rhs) {
+ // if the lhs is greater than the rhs, return undefined
+ return result;
+ }
+
+ if (!Number.isInteger(lhs)) {
+ throw {
+ message: 'LHS of range operator (..) must evaluate to an integer at column ' + expr.position,
+ stack: (new Error()).stack,
+ position: expr.position,
+ token: expr.value,
+ value: lhs
+ };
+ }
+ if (!Number.isInteger(rhs)) {
+ throw {
+ message: 'RHS of range operator (..) must evaluate to an integer at column ' + expr.position,
+ stack: (new Error()).stack,
+ position: expr.position,
+ token: expr.value,
+ value: rhs
+ };
+ }
+
+ result = new Array(rhs - lhs + 1);
+ for (var item = lhs, index = 0; item <= rhs; item++, index++) {
+ result[index] = item;
+ }
+ return result;
+ }
+
+ /**
+ * Evaluate bind 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 evaluateBindExpression(expr, input, environment) {
+ // The RHS is the expression to evaluate
+ // The LHS is the name of the variable to bind to - should be a VARIABLE token
+ var value = evaluate(expr.rhs, input, environment);
+ if (expr.lhs.type !== 'variable') {
+ throw {
+ message: "Left hand side of := must be a variable name (start with $) at column " + expr.position,
+ stack: (new Error()).stack,
+ position: expr.position,
+ token: expr.value,
+ value: expr.lhs.value
+ };
+ }
+ environment.bind(expr.lhs.value, value);
+ return value;
+ }
+
+ /**
+ * Evaluate condition 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 evaluateCondition(expr, input, environment) {
+ var result;
+ var condition = evaluate(expr.condition, input, environment);
+ if (functionBoolean(condition)) {
+ result = evaluate(expr.then, input, environment);
+ } else if (typeof expr.else !== 'undefined') {
+ result = evaluate(expr.else, input, environment);
+ }
+ return result;
+ }
+
+ /**
+ * Evaluate block 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 evaluateBlock(expr, input, environment) {
+ var result;
+ // create a new frame to limit the scope of variable assignments
+ // TODO, only do this if the post-parse stage has flagged this as required
+ var frame = createFrame(environment);
+ // invoke each expression in turn
+ // only return the result of the last one
+ expr.expressions.forEach(function (expression) {
+ result = evaluate(expression, input, frame);
+ });
+
+ return result;
+ }
+
+ /**
+ * Evaluate variable 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 evaluateVariable(expr, input, environment) {
+ // lookup the variable value in the environment
+ var result;
+ // if the variable name is empty string, then it refers to context value
+ if (expr.value === '') {
+ result = input;
+ } else {
+ result = environment.lookup(expr.value);
+ }
+ return result;
+ }
+
+ /**
+ * Evaluate function 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 evaluateFunction(expr, input, environment) {
+ var result;
+ // evaluate the arguments
+ var evaluatedArgs = [];
+ expr.arguments.forEach(function (arg) {
+ evaluatedArgs.push(evaluate(arg, input, environment));
+ });
+ // lambda function on lhs
+ // create the procedure
+ // can't assume that expr.procedure is a lambda type directly
+ // could be an expression that evaluates to a function (e.g. variable reference, parens expr etc.
+ // evaluate it generically first, then check that it is a function. Throw error if not.
+ var proc = evaluate(expr.procedure, input, environment);
+
+ if (typeof proc === 'undefined' && expr.procedure.type === 'name' && environment.lookup(expr.procedure.value)) {
+ // help the user out here if they simply forgot the leading $
+ throw {
+ message: 'Attempted to invoke a non-function at column ' + expr.position + '. Did you mean \'$' + expr.procedure.value + '\'?',
+ stack: (new Error()).stack,
+ position: expr.position,
+ token: expr.procedure.value
+ };
+ }
+ // apply the procedure
+ try {
+ result = apply(proc, evaluatedArgs, environment, input);
+ while(typeof result === 'object' && result.lambda == true && result.thunk == true) {
+ // trampoline loop - this gets invoked as a result of tail-call optimization
+ // the function returned a tail-call thunk
+ // unpack it, evaluate its arguments, and apply the tail call
+ var next = evaluate(result.body.procedure, result.input, result.environment);
+ evaluatedArgs = [];
+ result.body.arguments.forEach(function (arg) {
+ evaluatedArgs.push(evaluate(arg, result.input, result.environment));
+ });
+
+ result = apply(next, evaluatedArgs);
+ }
+ } catch (err) {
+ // add the position field to the error
+ err.position = expr.position;
+ // and the function identifier
+ err.token = expr.procedure.value;
+ throw err;
+ }
+ return result;
+ }
+
+ /**
+ * Apply procedure or function
+ * @param {Object} proc - Procedure
+ * @param {Array} args - Arguments
+ * @param {Object} environment - Environment
+ * @param {Object} self - Self
+ * @returns {*} Result of procedure
+ */
+ function apply(proc, args, environment, self) {
+ var result;
+ if (proc && proc.lambda) {
+ result = applyProcedure(proc, args);
+ } else if (typeof proc === 'function') {
+ result = proc.apply(self, args);
+ } else {
+ throw {
+ message: 'Attempted to invoke a non-function',
+ stack: (new Error()).stack
+ };
+ }
+ return result;
+ }
+
+ /**
+ * Evaluate lambda against input data
+ * @param {Object} expr - JSONata expression
+ * @param {Object} input - Input data to evaluate against
+ * @param {Object} environment - Environment
+ * @returns {{lambda: boolean, input: *, environment: *, arguments: *, body: *}} Evaluated input data
+ */
+ function evaluateLambda(expr, input, environment) {
+ // make a function (closure)
+ var procedure = {
+ lambda: true,
+ input: input,
+ environment: environment,
+ arguments: expr.arguments,
+ body: expr.body
+ };
+ if(expr.thunk == true) {
+ procedure.thunk = true;
+ }
+ return procedure;
+ }
+
+ /**
+ * Evaluate partial application
+ * @param {Object} expr - JSONata expression
+ * @param {Object} input - Input data to evaluate against
+ * @param {Object} environment - Environment
+ * @returns {*} Evaluated input data
+ */
+ function evaluatePartialApplication(expr, input, environment) {
+ // partially apply a function
+ var result;
+ // evaluate the arguments
+ var evaluatedArgs = [];
+ expr.arguments.forEach(function (arg) {
+ if (arg.type === 'operator' && arg.value === '?') {
+ evaluatedArgs.push(arg);
+ } else {
+ evaluatedArgs.push(evaluate(arg, input, environment));
+ }
+ });
+ // lookup the procedure
+ var proc = evaluate(expr.procedure, input, environment);
+ if (typeof proc === 'undefined' && expr.procedure.type === 'name' && environment.lookup(expr.procedure.value)) {
+ // help the user out here if they simply forgot the leading $
+ throw {
+ message: 'Attempted to partially apply a non-function at column ' + expr.position + '. Did you mean \'$' + expr.procedure.value + '\'?',
+ stack: (new Error()).stack,
+ position: expr.position,
+ token: expr.procedure.value
+ };
+ }
+ if (proc && proc.lambda) {
+ result = partialApplyProcedure(proc, evaluatedArgs);
+ } else if (typeof proc === 'function') {
+ result = partialApplyNativeFunction(proc, evaluatedArgs);
+ } else {
+ throw {
+ message: 'Attempted to partially apply a non-function at column ' + expr.position,
+ stack: (new Error()).stack,
+ position: expr.position,
+ token: expr.procedure.value
+ };
+ }
+ return result;
+ }
+
+ /**
+ * Apply procedure
+ * @param {Object} proc - Procedure
+ * @param {Array} args - Arguments
+ * @returns {*} Result of procedure
+ */
+ function applyProcedure(proc, args) {
+ var result;
+ var env = createFrame(proc.environment);
+ proc.arguments.forEach(function (param, index) {
+ env.bind(param.value, args[index]);
+ });
+ if (typeof proc.body === 'function') {
+ // this is a lambda that wraps a native function - generated by partially evaluating a native
+ result = applyNativeFunction(proc.body, env);
+ } else {
+ result = evaluate(proc.body, proc.input, env);
+ }
+ return result;
+ }
+
+ /**
+ * Partially apply procedure
+ * @param {Object} proc - Procedure
+ * @param {Array} args - Arguments
+ * @returns {{lambda: boolean, input: *, environment: {bind, lookup}, arguments: Array, body: *}} Result of partially applied procedure
+ */
+ function partialApplyProcedure(proc, args) {
+ // create a closure, bind the supplied parameters and return a function that takes the remaining (?) parameters
+ var env = createFrame(proc.environment);
+ var unboundArgs = [];
+ proc.arguments.forEach(function (param, index) {
+ var arg = args[index];
+ if (arg && arg.type === 'operator' && arg.value === '?') {
+ unboundArgs.push(param);
+ } else {
+ env.bind(param.value, arg);
+ }
+ });
+ var procedure = {
+ lambda: true,
+ input: proc.input,
+ environment: env,
+ arguments: unboundArgs,
+ body: proc.body
+ };
+ return procedure;
+ }
+
+ /**
+ * Partially apply native function
+ * @param {Function} native - Native function
+ * @param {Array} args - Arguments
+ * @returns {{lambda: boolean, input: *, environment: {bind, lookup}, arguments: Array, body: *}} Result of partially applying native function
+ */
+ function partialApplyNativeFunction(native, args) {
+ // create a lambda function that wraps and invokes the native function
+ // get the list of declared arguments from the native function
+ // this has to be picked out from the toString() value
+ var sigArgs = getNativeFunctionArguments(native);
+ sigArgs = sigArgs.map(function (sigArg) {
+ return '$' + sigArg.trim();
+ });
+ var body = 'function(' + sigArgs.join(', ') + '){ _ }';
+
+ var bodyAST = parser(body);
+ bodyAST.body = native;
+
+ var partial = partialApplyProcedure(bodyAST, args);
+ return partial;
+ }
+
+ /**
+ * Apply native function
+ * @param {Object} proc - Procedure
+ * @param {Object} env - Environment
+ * @returns {*} Result of applying native function
+ */
+ function applyNativeFunction(proc, env) {
+ var sigArgs = getNativeFunctionArguments(proc);
+ // generate the array of arguments for invoking the function - look them up in the environment
+ var args = sigArgs.map(function (sigArg) {
+ return env.lookup(sigArg.trim());
+ });
+
+ var result = proc.apply(null, args);
+ return result;
+ }
+
+ /**
+ * Get native function arguments
+ * @param {Function} func - Function
+ * @returns {*|Array} Native function arguments
+ */
+ function getNativeFunctionArguments(func) {
+ var signature = func.toString();
+ var sigParens = /\(([^\)]*)\)/.exec(signature)[1]; // the contents of the parens
+ var sigArgs = sigParens.split(',');
+ return sigArgs;
+ }
+
+ /**
+ * Tests whether arg is a lambda function
+ * @param {*} arg - the value to test
+ * @returns {boolean} - true if it is a lambda function
+ */
+ function isLambda(arg) {
+ var result = false;
+ if(arg && typeof arg === 'object' &&
+ arg.lambda === true &&
+ arg.hasOwnProperty('input') &&
+ arg.hasOwnProperty('arguments') &&
+ arg.hasOwnProperty('environment') &&
+ arg.hasOwnProperty('body')) {
+ result = true;
+ }
+
+ return result;
+ }
+
+ /**
+ * Sum function
+ * @param {Object} args - Arguments
+ * @returns {number} Total value of arguments
+ */
+ function functionSum(args) {
+ var total = 0;
+
+ if (arguments.length != 1) {
+ throw {
+ message: 'The sum function expects one argument',
+ stack: (new Error()).stack
+ };
+ }
+
+ // undefined inputs always return undefined
+ if(typeof args === 'undefined') {
+ return undefined;
+ }
+
+ if(!Array.isArray(args)) {
+ args = [args];
+ }
+
+ // it must be an array of numbers
+ var nonNumerics = args.filter(function(val) {return (typeof val !== 'number');});
+ if(nonNumerics.length > 0) {
+ throw {
+ message: 'Type error: argument of sum function must be an array of numbers',
+ stack: (new Error()).stack,
+ value: nonNumerics
+ };
+ }
+ args.forEach(function(num){total += num;});
+ return total;
+ }
+
+ /**
+ * Count function
+ * @param {Object} args - Arguments
+ * @returns {number} Number of elements in the array
+ */
+ function functionCount(args) {
+ if (arguments.length != 1) {
+ throw {
+ message: 'The count function expects one argument',
+ stack: (new Error()).stack
+ };
+ }
+
+ // undefined inputs always return undefined
+ if(typeof args === 'undefined') {
+ return 0;
+ }
+
+ if(!Array.isArray(args)) {
+ args = [args];
+ }
+
+ return args.length;
+ }
+
+ /**
+ * Max function
+ * @param {Object} args - Arguments
+ * @returns {number} Max element in the array
+ */
+ function functionMax(args) {
+ var max;
+
+ if (arguments.length != 1) {
+ throw {
+ message: 'The max function expects one argument',
+ stack: (new Error()).stack
+ };
+ }
+
+ // undefined inputs always return undefined
+ if(typeof args === 'undefined') {
+ return undefined;
+ }
+
+ if(!Array.isArray(args)) {
+ args = [args];
+ }
+
+ // it must be an array of numbers
+ var nonNumerics = args.filter(function(val) {return (typeof val !== 'number');});
+ if(nonNumerics.length > 0) {
+ throw {
+ message: 'Type error: argument of max function must be an array of numbers',
+ stack: (new Error()).stack,
+ value: nonNumerics
+ };
+ }
+ max = Math.max.apply(Math, args);
+ return max;
+ }
+
+ /**
+ * Min function
+ * @param {Object} args - Arguments
+ * @returns {number} Min element in the array
+ */
+ function functionMin(args) {
+ var min;
+
+ if (arguments.length != 1) {
+ throw {
+ message: 'The min function expects one argument',
+ stack: (new Error()).stack
+ };
+ }
+
+ // undefined inputs always return undefined
+ if(typeof args === 'undefined') {
+ return undefined;
+ }
+
+ if(!Array.isArray(args)) {
+ args = [args];
+ }
+
+ // it must be an array of numbers
+ var nonNumerics = args.filter(function(val) {return (typeof val !== 'number');});
+ if(nonNumerics.length > 0) {
+ throw {
+ message: 'Type error: argument of min function must be an array of numbers',
+ stack: (new Error()).stack,
+ value: nonNumerics
+ };
+ }
+ min = Math.min.apply(Math, args);
+ return min;
+ }
+
+ /**
+ * Average function
+ * @param {Object} args - Arguments
+ * @returns {number} Average element in the array
+ */
+ function functionAverage(args) {
+ var total = 0;
+
+ if (arguments.length != 1) {
+ throw {
+ message: 'The average function expects one argument',
+ stack: (new Error()).stack
+ };
+ }
+
+ // undefined inputs always return undefined
+ if(typeof args === 'undefined') {
+ return undefined;
+ }
+
+ if(!Array.isArray(args)) {
+ args = [args];
+ }
+
+ // it must be an array of numbers
+ var nonNumerics = args.filter(function(val) {return (typeof val !== 'number');});
+ if(nonNumerics.length > 0) {
+ throw {
+ message: 'Type error: argument of average function must be an array of numbers',
+ stack: (new Error()).stack,
+ value: nonNumerics
+ };
+ }
+ args.forEach(function(num){total += num;});
+ return total/args.length;
+ }
+
+ /**
+ * Stingify arguments
+ * @param {Object} arg - Arguments
+ * @returns {String} String from arguments
+ */
+ function functionString(arg) {
+ var str;
+
+ if(arguments.length != 1) {
+ throw {
+ message: 'The string function expects one argument',
+ stack: (new Error()).stack
+ };
+ }
+
+ // undefined inputs always return undefined
+ if(typeof arg === 'undefined') {
+ return undefined;
+ }
+
+ if (typeof arg === 'string') {
+ // already a string
+ str = arg;
+ } else if(typeof arg === 'function' || isLambda(arg)) {
+ // functions (built-in and lambda convert to empty string
+ str = '';
+ } else if (typeof arg === 'number' && !isFinite(arg)) {
+ throw {
+ message: "Attempting to invoke string function on Infinity or NaN",
+ value: arg,
+ stack: (new Error()).stack
+ };
+ } else
+ str = JSON.stringify(arg, function (key, val) {
+ return (typeof val !== 'undefined' && val !== null && val.toPrecision && isNumeric(val)) ? Number(val.toPrecision(13)) :
+ (val && isLambda(val)) ? '' :
+ (typeof val === 'function') ? '' : val;
+ });
+ return str;
+ }
+
+ /**
+ * Create substring based on character number and length
+ * @param {String} str - String to evaluate
+ * @param {Integer} start - Character number to start substring
+ * @param {Integer} [length] - Number of characters in substring
+ * @returns {string|*} Substring
+ */
+ function functionSubstring(str, start, length) {
+ if(arguments.length != 2 && arguments.length != 3) {
+ throw {
+ message: 'The substring function expects two or three arguments',
+ stack: (new Error()).stack
+ };
+ }
+
+ // undefined inputs always return undefined
+ if(typeof str === 'undefined') {
+ return undefined;
+ }
+
+ // otherwise it must be a string
+ if(typeof str !== 'string') {
+ throw {
+ message: 'Type error: first argument of substring function must evaluate to a string',
+ stack: (new Error()).stack,
+ value: str
+ };
+ }
+
+ if(typeof start !== 'number') {
+ throw {
+ message: 'Type error: second argument of substring function must evaluate to a number',
+ stack: (new Error()).stack,
+ value: start
+ };
+ }
+
+ if(typeof length !== 'undefined' && typeof length !== 'number') {
+ throw {
+ message: 'Type error: third argument of substring function must evaluate to a number',
+ stack: (new Error()).stack,
+ value: length
+ };
+ }
+
+ return str.substr(start, length);
+ }
+
+ /**
+ * Create substring up until a character
+ * @param {String} str - String to evaluate
+ * @param {String} chars - Character to define substring boundary
+ * @returns {*} Substring
+ */
+ function functionSubstringBefore(str, chars) {
+ if(arguments.length != 2) {
+ throw {
+ message: 'The substringBefore function expects two arguments',
+ stack: (new Error()).stack
+ };
+ }
+
+ // undefined inputs always return undefined
+ if(typeof str === 'undefined') {
+ return undefined;
+ }
+
+ // otherwise it must be a string
+ if(typeof str !== 'string') {
+ throw {
+ message: 'Type error: first argument of substringBefore function must evaluate to a string',
+ stack: (new Error()).stack,
+ value: str
+ };
+ }
+ if(typeof chars !== 'string') {
+ throw {
+ message: 'Type error: second argument of substringBefore function must evaluate to a string',
+ stack: (new Error()).stack,
+ value: chars
+ };
+ }
+
+ var pos = str.indexOf(chars);
+ if (pos > -1) {
+ return str.substr(0, pos);
+ } else {
+ return str;
+ }
+ }
+
+ /**
+ * Create substring after a character
+ * @param {String} str - String to evaluate
+ * @param {String} chars - Character to define substring boundary
+ * @returns {*} Substring
+ */
+ function functionSubstringAfter(str, chars) {
+ if(arguments.length != 2) {
+ throw {
+ message: 'The substringAfter function expects two arguments',
+ stack: (new Error()).stack
+ };
+ }
+
+ // undefined inputs always return undefined
+ if(typeof str === 'undefined') {
+ return undefined;
+ }
+
+ // otherwise it must be a string
+ if(typeof str !== 'string') {
+ throw {
+ message: 'Type error: first argument of substringAfter function must evaluate to a string',
+ stack: (new Error()).stack,
+ value: str
+ };
+ }
+ if(typeof chars !== 'string') {
+ throw {
+ message: 'Type error: second argument of substringAfter function must evaluate to a string',
+ stack: (new Error()).stack,
+ value: chars
+ };
+ }
+
+ var pos = str.indexOf(chars);
+ if (pos > -1) {
+ return str.substr(pos + chars.length);
+ } else {
+ return str;
+ }
+ }
+
+ /**
+ * Lowercase a string
+ * @param {String} str - String to evaluate
+ * @returns {string} Lowercase string
+ */
+ function functionLowercase(str) {
+ if(arguments.length != 1) {
+ throw {
+ message: 'The lowercase function expects one argument',
+ stack: (new Error()).stack
+ };
+ }
+
+ // undefined inputs always return undefined
+ if(typeof str === 'undefined') {
+ return undefined;
+ }
+
+ // otherwise it must be a string
+ if(typeof str !== 'string') {
+ throw {
+ message: 'Type error: argument of lowercase function must evaluate to a string',
+ stack: (new Error()).stack,
+ value: str
+ };
+ }
+
+ return str.toLowerCase();
+ }
+
+ /**
+ * Uppercase a string
+ * @param {String} str - String to evaluate
+ * @returns {string} Uppercase string
+ */
+ function functionUppercase(str) {
+ if(arguments.length != 1) {
+ throw {
+ message: 'The uppercase function expects one argument',
+ stack: (new Error()).stack
+ };
+ }
+
+ // undefined inputs always return undefined
+ if(typeof str === 'undefined') {
+ return undefined;
+ }
+
+ // otherwise it must be a string
+ if(typeof str !== 'string') {
+ throw {
+ message: 'Type error: argument of uppercase function must evaluate to a string',
+ stack: (new Error()).stack,
+ value: str
+ };
+ }
+
+ return str.toUpperCase();
+ }
+
+ /**
+ * length of a string
+ * @param {String} str - string
+ * @returns {Number} The number of characters in the string
+ */
+ function functionLength(str) {
+ if(arguments.length != 1) {
+ throw {
+ message: 'The length function expects one argument',
+ stack: (new Error()).stack
+ };
+ }
+
+ // undefined inputs always return undefined
+ if(typeof str === 'undefined') {
+ return undefined;
+ }
+
+ // otherwise it must be a string
+ if(typeof str !== 'string') {
+ throw {
+ message: 'Type error: argument of length function must evaluate to a string',
+ stack: (new Error()).stack,
+ value: str
+ };
+ }
+
+ return str.length;
+ }
+
+ /**
+ * Split a string into an array of substrings
+ * @param {String} str - string
+ * @param {String} separator - the token that splits the string
+ * @param {Integer} [limit] - max number of substrings
+ * @returns {Array} The array of string
+ */
+ function functionSplit(str, separator, limit) {
+ if(arguments.length != 2 && arguments.length != 3) {
+ throw {
+ message: 'The split function expects two or three arguments',
+ stack: (new Error()).stack
+ };
+ }
+
+ // undefined inputs always return undefined
+ if(typeof str === 'undefined') {
+ return undefined;
+ }
+
+ // otherwise it must be a string
+ if(typeof str !== 'string') {
+ throw {
+ message: 'Type error: first argument of split function must evaluate to a string',
+ stack: (new Error()).stack,
+ value: str
+ };
+ }
+
+ // separator must be a string
+ if(typeof separator !== 'string') {
+ throw {
+ message: 'Type error: second argument of split function must evaluate to a string',
+ stack: (new Error()).stack,
+ value: separator
+ };
+ }
+
+ // limit, if specified, must be a number
+ if(typeof limit !== 'undefined' && (typeof limit !== 'number' || limit < 0)) {
+ throw {
+ message: 'Type error: third argument of split function must evaluate to a positive number',
+ stack: (new Error()).stack,
+ value: limit
+ };
+ }
+
+ return str.split(separator, limit);
+ }
+
+ /**
+ * Join an array of strings
+ * @param {Array} strs - array of string
+ * @param {String} [separator] - the token that splits the string
+ * @returns {String} The concatenated string
+ */
+ function functionJoin(strs, separator) {
+ if(arguments.length != 1 && arguments.length != 2) {
+ throw {
+ message: 'The join function expects one or two arguments',
+ stack: (new Error()).stack
+ };
+ }
+
+ // undefined inputs always return undefined
+ if(typeof strs === 'undefined') {
+ return undefined;
+ }
+
+ if(!Array.isArray(strs)) {
+ strs = [strs];
+ }
+
+ // it must be an array of strings
+ var nonStrings = strs.filter(function(val) {return (typeof val !== 'string');});
+ if(nonStrings.length > 0) {
+ throw {
+ message: 'Type error: first argument of join function must be an array of strings',
+ stack: (new Error()).stack,
+ value: nonStrings
+ };
+ }
+
+
+ // if separator is not specified, default to empty string
+ if(typeof separator === 'undefined') {
+ separator = "";
+ }
+
+ // separator, if specified, must be a string
+ if(typeof separator !== 'string') {
+ throw {
+ message: 'Type error: second argument of split function must evaluate to a string',
+ stack: (new Error()).stack,
+ value: separator
+ };
+ }
+
+ return strs.join(separator);
+ }
+
+ /**
+ * Cast argument to number
+ * @param {Object} arg - Argument
+ * @returns {Number} numeric value of argument
+ */
+ function functionNumber(arg) {
+ var result;
+
+ if(arguments.length != 1) {
+ throw {
+ message: 'The number function expects one argument',
+ stack: (new Error()).stack
+ };
+ }
+
+ // undefined inputs always return undefined
+ if(typeof arg === 'undefined') {
+ return undefined;
+ }
+
+ if (typeof arg === 'number') {
+ // already a number
+ result = arg;
+ } else if(typeof arg === 'string' && /^-?(0|([1-9][0-9]*))(\.[0-9]+)?([Ee][-+]?[0-9]+)?$/.test(arg) && !isNaN(parseFloat(arg)) && isFinite(arg)) {
+ result = parseFloat(arg);
+ } else {
+ throw {
+ message: "Unable to cast value to a number",
+ value: arg,
+ stack: (new Error()).stack
+ };
+ }
+ return result;
+ }
+
+
+ /**
+ * Evaluate an input and return a boolean
+ * @param {*} arg - Arguments
+ * @returns {boolean} Boolean
+ */
+ function functionBoolean(arg) {
+ // cast arg to its effective boolean value
+ // boolean: unchanged
+ // string: zero-length -> false; otherwise -> true
+ // number: 0 -> false; otherwise -> true
+ // null -> false
+ // array: empty -> false; length > 1 -> true
+ // object: empty -> false; non-empty -> true
+ // function -> false
+
+ if(arguments.length != 1) {
+ throw {
+ message: 'The boolean function expects one argument',
+ stack: (new Error()).stack
+ };
+ }
+
+ // undefined inputs always return undefined
+ if(typeof arg === 'undefined') {
+ return undefined;
+ }
+
+ var result = false;
+ if (Array.isArray(arg)) {
+ if (arg.length == 1) {
+ result = functionBoolean(arg[0]);
+ } else if (arg.length > 1) {
+ var trues = arg.filter(function(val) {return functionBoolean(val);});
+ result = trues.length > 0;
+ }
+ } else if (typeof arg === 'string') {
+ if (arg.length > 0) {
+ result = true;
+ }
+ } else if (isNumeric(arg)) {
+ if (arg != 0) {
+ result = true;
+ }
+ } else if (arg != null && typeof arg === 'object') {
+ if (Object.keys(arg).length > 0) {
+ // make sure it's not a lambda function
+ if (!(isLambda(arg))) {
+ result = true;
+ }
+ }
+ } else if (typeof arg === 'boolean' && arg == true) {
+ result = true;
+ }
+ return result;
+ }
+
+ /**
+ * returns the Boolean NOT of the arg
+ * @param {*} arg - argument
+ * @returns {boolean} - NOT arg
+ */
+ function functionNot(arg) {
+ return !functionBoolean(arg);
+ }
+
+ /**
+ * Create a map from an array of arguments
+ * @param {Function} func - function to apply
+ * @returns {Array} Map array
+ */
+ function functionMap(func) {
+ // this can take a variable number of arguments - each one should be mapped to the equivalent arg of func
+ // assert that func is a function
+ var varargs = arguments;
+ var result = [];
+
+ // each subsequent arg must be an array - coerce if not
+ var args = [];
+ for (var ii = 1; ii < varargs.length; ii++) {
+ if (Array.isArray(varargs[ii])) {
+ args.push(varargs[ii]);
+ } else {
+ args.push([varargs[ii]]);
+ }
+
+ }
+ // do the map - iterate over the arrays, and invoke func
+ if (args.length > 0) {
+ for (var i = 0; i < args[0].length; i++) {
+ var func_args = [];
+ for (var j = 0; j < func.arguments.length; j++) {
+ func_args.push(args[j][i]);
+ }
+ // invoke func
+ result.push(apply(func, func_args, null, null));
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Fold left function
+ * @param {Function} func - Function
+ * @param {Array} sequence - Sequence
+ * @param {Object} init - Initial value
+ * @returns {*} Result
+ */
+ function functionFoldLeft(func, sequence, init) {
+ var result;
+
+ if (!(func.length == 2 || func.arguments.length == 2)) {
+ throw {
+ message: 'The first argument of the reduce function must be a function of arity 2',
+ stack: (new Error()).stack
+ };
+ }
+
+ if (!Array.isArray(sequence)) {
+ sequence = [sequence];
+ }
+
+ var index;
+ if (typeof init === 'undefined' && sequence.length > 0) {
+ result = sequence[0];
+ index = 1;
+ } else {
+ result = init;
+ index = 0;
+ }
+
+ while (index < sequence.length) {
+ result = apply(func, [result, sequence[index]], null, null);
+ index++;
+ }
+
+ return result;
+ }
+
+ /**
+ * Return keys for an object
+ * @param {Object} arg - Object
+ * @returns {Array} Array of keys
+ */
+ function functionKeys(arg) {
+ var result = [];
+
+ if(Array.isArray(arg)) {
+ // merge the keys of all of the items in the array
+ var merge = {};
+ arg.forEach(function(item) {
+ var keys = functionKeys(item);
+ if(Array.isArray(keys)) {
+ keys.forEach(function(key) {
+ merge[key] = true;
+ });
+ }
+ });
+ result = functionKeys(merge);
+ } else if(arg != null && typeof arg === 'object' && !(isLambda(arg))) {
+ result = Object.keys(arg);
+ if(result.length == 0) {
+ result = undefined;
+ }
+ } else {
+ result = undefined;
+ }
+ return result;
+ }
+
+ /**
+ * Return value from an object for a given key
+ * @param {Object} object - Object
+ * @param {String} key - Key in object
+ * @returns {*} Value of key in object
+ */
+ function functionLookup(object, key) {
+ var result = evaluateName({value: key}, object);
+ return result;
+ }
+
+ /**
+ * Append second argument to first
+ * @param {Array|Object} arg1 - First argument
+ * @param {Array|Object} arg2 - Second argument
+ * @returns {*} Appended arguments
+ */
+ function functionAppend(arg1, arg2) {
+ // disregard undefined args
+ if (typeof arg1 === 'undefined') {
+ return arg2;
+ }
+ if (typeof arg2 === 'undefined') {
+ return arg1;
+ }
+ // if either argument is not an array, make it so
+ if (!Array.isArray(arg1)) {
+ arg1 = [arg1];
+ }
+ if (!Array.isArray(arg2)) {
+ arg2 = [arg2];
+ }
+ Array.prototype.push.apply(arg1, arg2);
+ return arg1;
+ }
+
+ /**
+ * Determines if the argument is undefined
+ * @param {*} arg - argument
+ * @returns {boolean} False if argument undefined, otherwise true
+ */
+ function functionExists(arg){
+ if (arguments.length != 1) {
+ throw {
+ message: 'The exists function expects one argument',
+ stack: (new Error()).stack
+ };
+ }
+
+ if (typeof arg === 'undefined') {
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * Splits an object into an array of object with one property each
+ * @param {*} arg - the object to split
+ * @returns {*} - the array
+ */
+ function functionSpread(arg) {
+ var result = [];
+
+ if(Array.isArray(arg)) {
+ // spread all of the items in the array
+ arg.forEach(function(item) {
+ result = functionAppend(result, functionSpread(item));
+ });
+ } else if(arg != null && typeof arg === 'object' && !isLambda(arg)) {
+ for(var key in arg) {
+ var obj = {};
+ obj[key] = arg[key];
+ result.push(obj);
+ }
+ } else {
+ result = arg;
+ }
+ return result;
+ }
+
+ /**
+ * Create frame
+ * @param {Object} enclosingEnvironment - Enclosing environment
+ * @returns {{bind: bind, lookup: lookup}} Created frame
+ */
+ function createFrame(enclosingEnvironment) {
+ var bindings = {};
+ return {
+ bind: function (name, value) {
+ bindings[name] = value;
+ },
+ lookup: function (name) {
+ var value = bindings[name];
+ if (typeof value === 'undefined' && enclosingEnvironment) {
+ value = enclosingEnvironment.lookup(name);
+ }
+ return value;
+ }
+ };
+ }
+
+ var staticFrame = createFrame(null);
+
+ staticFrame.bind('sum', functionSum);
+ staticFrame.bind('count', functionCount);
+ staticFrame.bind('max', functionMax);
+ staticFrame.bind('min', functionMin);
+ staticFrame.bind('average', functionAverage);
+ staticFrame.bind('string', functionString);
+ staticFrame.bind('substring', functionSubstring);
+ staticFrame.bind('substringBefore', functionSubstringBefore);
+ staticFrame.bind('substringAfter', functionSubstringAfter);
+ staticFrame.bind('lowercase', functionLowercase);
+ staticFrame.bind('uppercase', functionUppercase);
+ staticFrame.bind('length', functionLength);
+ staticFrame.bind('split', functionSplit);
+ staticFrame.bind('join', functionJoin);
+ staticFrame.bind('number', functionNumber);
+ staticFrame.bind('boolean', functionBoolean);
+ staticFrame.bind('not', functionNot);
+ staticFrame.bind('map', functionMap);
+ staticFrame.bind('reduce', functionFoldLeft);
+ staticFrame.bind('keys', functionKeys);
+ staticFrame.bind('lookup', functionLookup);
+ staticFrame.bind('append', functionAppend);
+ staticFrame.bind('exists', functionExists);
+ staticFrame.bind('spread', functionSpread);
+
+ /**
+ * JSONata
+ * @param {Object} expr - JSONata expression
+ * @returns {{evaluate: evaluate, assign: assign}} Evaluated expression
+ */
+ function jsonata(expr) {
+ var ast = parser(expr);
+ var environment = createFrame(staticFrame);
+ return {
+ evaluate: function (input, bindings) {
+ if (typeof bindings !== 'undefined') {
+ var exec_env;
+ // the variable bindings have been passed in - create a frame to hold these
+ exec_env = createFrame(environment);
+ for (var v in bindings) {
+ exec_env.bind(v, bindings[v]);
+ }
+ } else {
+ exec_env = environment;
+ }
+ // put the input document into the environment as the root object
+ exec_env.bind('$', input);
+ return evaluate(ast, input, exec_env);
+ },
+ assign: function (name, value) {
+ environment.bind(name, value);
+ }
+ };
+ }
+
+ jsonata.parser = parser;
+
+ return jsonata;
+
+})();
+
+// node.js only - export the jsonata and parser functions
+// istanbul ignore else
+if(typeof module !== 'undefined') {
+ module.exports = jsonata;
+}
diff --git a/nodes/core/locales/en-US/messages.json b/nodes/core/locales/en-US/messages.json
index ab733c45d..ee8a20264 100644
--- a/nodes/core/locales/en-US/messages.json
+++ b/nodes/core/locales/en-US/messages.json
@@ -502,6 +502,9 @@
"null":"is null",
"nnull":"is not null",
"else":"otherwise"
+ },
+ "errors": {
+ "invalid-expr": "Invalid expression: __error__"
}
},
"change": {
diff --git a/nodes/core/logic/10-switch.html b/nodes/core/logic/10-switch.html
index 1330df14d..3ce53c89f 100644
--- a/nodes/core/logic/10-switch.html
+++ b/nodes/core/logic/10-switch.html
@@ -64,7 +64,7 @@
var node = this;
var previousValueType = {value:"prev",label:this._("inject.previous"),hasValue:false};
- $("#node-input-property").typedInput({default:this.propertyType||'msg',types:['msg','flow','global']});
+ $("#node-input-property").typedInput({default:this.propertyType||'msg',types:['msg','flow','global','jsonata']});
var operators = [
{v:"eq",t:"=="},
{v:"neq",t:"!="},
@@ -129,10 +129,10 @@
for (var d in operators) {
selectField.append($("").val(operators[d].v).text(operators[d].t));
}
- var valueField = $('',{class:"node-input-rule-value",type:"text",style:"margin-left: 5px;"}).appendTo(row).typedInput({default:'str',types:['msg','flow','global','str','num',previousValueType]});
- var btwnValueField = $('',{class:"node-input-rule-btwn-value",type:"text",style:"margin-left: 5px;"}).appendTo(row).typedInput({default:'num',types:['msg','flow','global','str','num',previousValueType]});
+ var valueField = $('',{class:"node-input-rule-value",type:"text",style:"margin-left: 5px;"}).appendTo(row).typedInput({default:'str',types:['msg','flow','global','str','num','jsonata',previousValueType]});
+ var btwnValueField = $('',{class:"node-input-rule-btwn-value",type:"text",style:"margin-left: 5px;"}).appendTo(row).typedInput({default:'num',types:['msg','flow','global','str','num','jsonata',previousValueType]});
var btwnAndLabel = $('',{class:"node-input-rule-btwn-label"}).text(" "+andLabel+" ").appendTo(row3);
- var btwnValue2Field = $('',{class:"node-input-rule-btwn-value2",type:"text",style:"margin-left:2px;"}).appendTo(row3).typedInput({default:'num',types:['msg','flow','global','str','num',previousValueType]});
+ var btwnValue2Field = $('',{class:"node-input-rule-btwn-value2",type:"text",style:"margin-left:2px;"}).appendTo(row3).typedInput({default:'num',types:['msg','flow','global','str','num','jsonata',previousValueType]});
var finalspan = $('',{style:"float: right;margin-top: 6px;"}).appendTo(row);
finalspan.append(' → '+(i+1)+' ');
var caseSensitive = $('',{id:"node-input-rule-case-"+i,class:"node-input-rule-case",type:"checkbox",style:"width:auto;vertical-align:top"}).appendTo(row2);
diff --git a/nodes/core/logic/10-switch.js b/nodes/core/logic/10-switch.js
index 6585eace1..3a335a675 100644
--- a/nodes/core/logic/10-switch.js
+++ b/nodes/core/logic/10-switch.js
@@ -16,6 +16,9 @@
module.exports = function(RED) {
"use strict";
+
+ var jsonata = require('jsonata');
+
var operators = {
'eq': function(a, b) { return a == b; },
'neq': function(a, b) { return a != b; },
@@ -38,9 +41,20 @@ module.exports = function(RED) {
this.rules = n.rules || [];
this.property = n.property;
this.propertyType = n.propertyType || "msg";
+
+ if (this.propertyType === 'jsonata') {
+ try {
+ this.property = jsonata(this.property);
+ } catch(err) {
+ this.error(RED._("switch.errors.invalid-expr",{error:err.message}));
+ return;
+ }
+ }
+
this.checkall = n.checkall || "true";
this.previousValue = null;
var node = this;
+ var valid = true;
for (var i=0; i',{class:"node-input-rule-property-value",type:"text"})
.appendTo(row2)
- .typedInput({default:'str',types:['msg','flow','global','str','num','bool','json','date']});
+ .typedInput({default:'str',types:['msg','flow','global','str','num','bool','json','date','jsonata']});
var row3_1 = $('').appendTo(row3);
$('',{style:"display:inline-block;text-align:right; width:120px; padding-right:10px; box-sizing:border-box;"})
diff --git a/nodes/core/logic/15-change.js b/nodes/core/logic/15-change.js
index 67f30f12f..805b708ed 100644
--- a/nodes/core/logic/15-change.js
+++ b/nodes/core/logic/15-change.js
@@ -16,6 +16,7 @@
module.exports = function(RED) {
"use strict";
+ var jsonata = require("jsonata");
function ChangeNode(n) {
RED.nodes.createNode(this, n);
@@ -85,6 +86,13 @@ module.exports = function(RED) {
}
} else if (rule.tot === 'bool') {
rule.to = /^true$/i.test(rule.to);
+ } else if (rule.tot === 'jsonata') {
+ try {
+ rule.to = jsonata(rule.to);
+ } catch(e) {
+ valid = false;
+ this.error(RED._("change.errors.invalid-from",{error:e.message}));
+ }
}
}
@@ -107,6 +115,8 @@ module.exports = function(RED) {
value = node.context().global.get(rule.to);
} else if (rule.tot === 'date') {
value = Date.now();
+ } else if (rule.tot === 'jsonata') {
+ value = rule.to.evaluate({msg:msg});
}
if (rule.t === 'change') {
if (rule.fromt === 'msg' || rule.fromt === 'flow' || rule.fromt === 'global') {
diff --git a/package.json b/package.json
index ac20ba7e0..33de70e92 100644
--- a/package.json
+++ b/package.json
@@ -40,6 +40,7 @@
"fs.notify":"0.0.4",
"i18next":"1.10.6",
"is-utf8":"0.2.1",
+ "jsonata":"1.0.7",
"media-typer": "0.3.0",
"mqtt": "1.14.1",
"mustache": "2.2.1",
diff --git a/red/runtime/util.js b/red/runtime/util.js
index f132564c8..97213a389 100644
--- a/red/runtime/util.js
+++ b/red/runtime/util.js
@@ -15,6 +15,7 @@
**/
var clone = require("clone");
+var jsonata = require("jsonata");
function generateId() {
return (1+Math.random()*4294967295).toString(16);
@@ -310,6 +311,8 @@ function evaluateNodeProperty(value, type, node, msg) {
return node.context().global.get(value);
} else if (type === 'bool') {
return /^true$/i.test(value);
+ } else if (type === 'jsonata') {
+ return jsonata(value).evaluate({msg:msg});
}
return value;
}