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; }