UNPKG

test-jsonata

Version:

JSON query and transformation language

1,419 lines (1,294 loc) 85.6 kB
/** * © Copyright IBM Corp. 2016, 2017 All Rights Reserved * Project name: JSONata * This project is licensed under the MIT License, see LICENSE */ /** * @module JSONata * @description JSON query and transformation language */ var datetime = require('./datetime'); var fn = require('./functions'); var utils = require('./utils'); var parser = require('./parser'); var parseSignature = require('./signature'); /** * jsonata * @function * @param {Object} expr - JSONata expression * @returns {{evaluate: evaluate, assign: assign}} Evaluated expression */ var jsonata = (function() { 'use strict'; var isNumeric = utils.isNumeric; var isArrayOfStrings = utils.isArrayOfStrings; var isArrayOfNumbers = utils.isArrayOfNumbers; var createSequence = utils.createSequence; var isSequence = utils.isSequence; var isFunction = utils.isFunction; var isLambda = utils.isLambda; var isIterable = utils.isIterable; var getFunctionArity = utils.getFunctionArity; var isDeepEqual = utils.isDeepEqual; // Start of Evaluator code var staticFrame = createFrame(null); /** * 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 = yield * evaluatePath(expr, input, environment); break; case 'binary': result = yield * evaluateBinary(expr, input, environment); break; case 'unary': result = yield * evaluateUnary(expr, input, environment); break; case 'name': result = evaluateName(expr, input, environment); break; case 'string': case 'number': case 'value': result = evaluateLiteral(expr, input, environment); break; case 'wildcard': result = evaluateWildcard(expr, input, environment); break; case 'descendant': result = evaluateDescendants(expr, input, environment); break; case 'parent': result = environment.lookup(expr.slot.label); break; case 'condition': result = yield * evaluateCondition(expr, input, environment); break; case 'block': result = yield * evaluateBlock(expr, input, environment); break; case 'bind': result = yield * evaluateBindExpression(expr, input, environment); break; case 'regex': result = evaluateRegex(expr, input, environment); break; case 'function': result = yield * evaluateFunction(expr, input, environment); break; case 'variable': result = evaluateVariable(expr, input, environment); break; case 'lambda': result = evaluateLambda(expr, input, environment); break; case 'partial': result = yield * evaluatePartialApplication(expr, input, environment); break; case 'apply': result = yield * evaluateApplyExpression(expr, input, environment); break; case 'transform': result = evaluateTransformExpression(expr, input, environment); break; } if(environment.async && (typeof result === 'undefined' || result === null || typeof result.then !== 'function')) { result = Promise.resolve(result); } if(environment.async && typeof result.then === 'function' && expr.nextFunction && typeof result[expr.nextFunction] === 'function') { // although this is a 'thenable', it is chaining a different function // so don't yield since yielding will trigger the .then() } else { result = yield result; } if (Object.prototype.hasOwnProperty.call(expr, 'predicate')) { for(var ii = 0; ii < expr.predicate.length; ii++) { result = yield * evaluateFilter(expr.predicate[ii].expr, result, environment); } } if (expr.type !== 'path' && Object.prototype.hasOwnProperty.call(expr, 'group')) { result = yield * evaluateGroupExpression(expr.group, result, environment); } var exitCallback = environment.lookup('__evaluate_exit'); if(exitCallback) { exitCallback(expr, input, environment, result); } if(result && isSequence(result) && !result.tupleStream) { if(expr.keepArray) { result.keepSingleton = true; } if(result.length === 0) { result = undefined; } else if(result.length === 1) { result = result.keepSingleton ? result : result[0]; } } 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 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 (Array.isArray(input) && expr.steps[0].type !== 'variable') { inputSequence = input; } else { // if input is not an array, make it so inputSequence = createSequence(input); } var resultSequence; var isTupleStream = false; var tupleBindings = undefined; // evaluate each step in turn for(var ii = 0; ii < expr.steps.length; ii++) { var step = expr.steps[ii]; if(step.tuple) { isTupleStream = true; } // if the first step is an explicit array constructor, then just evaluate that (i.e. don't iterate over a context array) if(ii === 0 && step.consarray) { resultSequence = yield * evaluate(step, inputSequence, environment); } else { if(isTupleStream) { tupleBindings = yield * evaluateTupleStep(step, inputSequence, tupleBindings, environment); } else { resultSequence = yield * evaluateStep(step, inputSequence, environment, ii === expr.steps.length - 1); } } if (!isTupleStream && (typeof resultSequence === 'undefined' || resultSequence.length === 0)) { break; } if(typeof step.focus === 'undefined') { inputSequence = resultSequence; } } if(isTupleStream) { if(expr.tuple) { // tuple stream is carrying ancestry information - keep this resultSequence = tupleBindings; } else { resultSequence = createSequence(); for (ii = 0; ii < tupleBindings.length; ii++) { resultSequence.push(tupleBindings[ii]['@']); } } } if(expr.keepSingletonArray) { if(!isSequence(resultSequence)) { resultSequence = createSequence(resultSequence); } resultSequence.keepSingleton = true; } if (expr.hasOwnProperty('group')) { resultSequence = yield* evaluateGroupExpression(expr.group, isTupleStream ? tupleBindings : resultSequence, environment) } return resultSequence; } function createFrameFromTuple(environment, tuple) { var frame = createFrame(environment); for(const prop in tuple) { frame.bind(prop, tuple[prop]); } return frame; } /** * Evaluate a step within a path * @param {Object} expr - JSONata expression * @param {Object} input - Input data to evaluate against * @param {Object} environment - Environment * @param {boolean} lastStep - flag the last step in a path * @returns {*} Evaluated input data */ function* evaluateStep(expr, input, environment, lastStep) { var result; if(expr.type === 'sort') { result = yield* evaluateSortExpression(expr, input, environment); if(expr.stages) { result = yield* evaluateStages(expr.stages, result, environment); } return result; } result = createSequence(); for(var ii = 0; ii < input.length; ii++) { var res = yield * evaluate(expr, input[ii], environment); if(expr.stages) { for(var ss = 0; ss < expr.stages.length; ss++) { res = yield* evaluateFilter(expr.stages[ss].expr, res, environment); } } if(typeof res !== 'undefined') { result.push(res); } } var resultSequence = createSequence(); if(lastStep && result.length === 1 && Array.isArray(result[0]) && !isSequence(result[0])) { resultSequence = result[0]; } else { // flatten the sequence result.forEach(function(res) { if (!Array.isArray(res) || res.cons) { // it's not an array - just push into the result sequence resultSequence.push(res); } else { // res is a sequence - flatten it into the parent sequence res.forEach(val => resultSequence.push(val)); } }); } return resultSequence; } function* evaluateStages(stages, input, environment) { var result = input; for(var ss = 0; ss < stages.length; ss++) { var stage = stages[ss]; switch(stage.type) { case 'filter': result = yield * evaluateFilter(stage.expr, result, environment); break; case 'index': for(var ee = 0; ee < result.length; ee++) { var tuple = result[ee]; tuple[stage.value] = ee; } break; } } return result; } /** * Evaluate a step within a path * @param {Object} expr - JSONata expression * @param {Object} input - Input data to evaluate against * @param {Object} tupleBindings - The tuple stream * @param {Object} environment - Environment * @returns {*} Evaluated input data */ function* evaluateTupleStep(expr, input, tupleBindings, environment) { var result; if(expr.type === 'sort') { if(tupleBindings) { result = yield* evaluateSortExpression(expr, tupleBindings, environment); } else { var sorted = yield* evaluateSortExpression(expr, input, environment); result = createSequence(); result.tupleStream = true; for(var ss = 0; ss < sorted.length; ss++) { var tuple = {'@': sorted[ss]}; tuple[expr.index] = ss; result.push(tuple); } } if(expr.stages) { result = yield* evaluateStages(expr.stages, result, environment); } return result; } result = createSequence(); result.tupleStream = true; var stepEnv = environment; if(tupleBindings === undefined) { tupleBindings = input.map(item => { return {'@': item} }); } for(var ee = 0; ee < tupleBindings.length; ee++) { stepEnv = createFrameFromTuple(environment, tupleBindings[ee]); var res = yield* evaluate(expr, tupleBindings[ee]['@'], stepEnv); // res is the binding sequence for the output tuple stream if(typeof res !== 'undefined') { if (!Array.isArray(res)) { res = [res]; } for (var bb = 0; bb < res.length; bb++) { tuple = {}; Object.assign(tuple, tupleBindings[ee]); if(res.tupleStream) { Object.assign(tuple, res[bb]); } else { if (expr.focus) { tuple[expr.focus] = res[bb]; tuple['@'] = tupleBindings[ee]['@']; } else { tuple['@'] = res[bb]; } if (expr.index) { tuple[expr.index] = bb; } if (expr.ancestor) { tuple[expr.ancestor.label] = tupleBindings[ee]['@']; } } result.push(tuple); } } } if(expr.stages) { result = yield * evaluateStages(expr.stages, result, environment); } return result; } /** * Apply filter predicate to input data * @param {Object} predicate - filter expression * @param {Object} input - Input data to apply predicates against * @param {Object} environment - Environment * @returns {*} Result after applying predicates */ function* evaluateFilter(predicate, input, environment) { var results = createSequence(); if( input && input.tupleStream) { results.tupleStream = true; } if (!Array.isArray(input)) { input = createSequence(input); } if (predicate.type === 'number') { var index = Math.floor(predicate.value); // round it down if (index < 0) { // count in from end of array index = input.length + index; } var item = input[index]; if(typeof item !== 'undefined') { if(Array.isArray(item)) { results = item; } else { results.push(item); } } } else { for (index = 0; index < input.length; index++) { var item = input[index]; var context = item; var env = environment; if(input.tupleStream) { context = item['@']; env = createFrameFromTuple(environment, item); } var res = yield* evaluate(predicate, context, env); if (isNumeric(res)) { res = [res]; } if (isArrayOfNumbers(res)) { res.forEach(function (ires) { // round it down var ii = Math.floor(ires); if (ii < 0) { // count in from end of array ii = input.length + ii; } if (ii === index) { results.push(item); } }); } else if (fn.boolean(res)) { // truthy results.push(item); } } } return results; } /** * 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; var lhs = yield * evaluate(expr.lhs, input, environment); var rhs = yield * evaluate(expr.rhs, input, environment); var op = expr.value; try { switch (op) { case '+': case '-': case '*': case '/': case '%': result = evaluateNumericExpression(lhs, rhs, op); break; case '=': case '!=': result = evaluateEqualityExpression(lhs, rhs, op); break; case '<': case '<=': case '>': case '>=': result = evaluateComparisonExpression(lhs, rhs, op); break; case '&': result = evaluateStringConcat(lhs, rhs); break; case 'and': case 'or': result = evaluateBooleanExpression(lhs, rhs, op); break; case '..': result = evaluateRangeExpression(lhs, rhs); break; case 'in': result = evaluateIncludesExpression(lhs, rhs); break; } } catch(err) { err.position = expr.position; err.token = op; throw err; } 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 = yield * evaluate(expr.expression, input, environment); if(typeof result === 'undefined') { result = undefined; } else if (isNumeric(result)) { result = -result; } else { throw { code: "D1002", stack: (new Error()).stack, position: expr.position, token: expr.value, value: result }; } break; case '[': // array constructor - evaluate each item result = []; for(var ii = 0; ii < expr.expressions.length; ii++) { var item = expr.expressions[ii]; var value = yield * evaluate(item, input, environment); if (typeof value !== 'undefined') { if(item.value === '[') { result.push(value); } else { result = fn.append(result, value); } } } if(expr.consarray) { Object.defineProperty(result, 'cons', { enumerable: false, configurable: false, value: true }); } break; case '{': // object constructor - apply grouping result = yield * 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 return fn.lookup(input, expr.value); } /** * 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 results = createSequence(); if (input !== null && typeof input === 'object') { Object.keys(input).forEach(function (key) { var value = input[key]; if(Array.isArray(value)) { value = flatten(value); results = fn.append(results, value); } else { results.push(value); } }); } // result = normalizeSequence(results); return results; } /** * 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 = createSequence(); 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} lhs - LHS value * @param {Object} rhs - RHS value * @param {Object} op - opcode * @returns {*} Result */ function evaluateNumericExpression(lhs, rhs, op) { var result; if (typeof lhs !== 'undefined' && !isNumeric(lhs)) { throw { code: "T2001", stack: (new Error()).stack, value: lhs }; } if (typeof rhs !== 'undefined' && !isNumeric(rhs)) { throw { code: "T2002", stack: (new Error()).stack, value: rhs }; } if (typeof lhs === 'undefined' || typeof rhs === 'undefined') { // if either side is undefined, the result is undefined return result; } switch (op) { 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 equality expression against input data * @param {Object} lhs - LHS value * @param {Object} rhs - RHS value * @param {Object} op - opcode * @returns {*} Result */ function evaluateEqualityExpression(lhs, rhs, op) { var result; // type checks var ltype = typeof lhs; var rtype = typeof rhs; if (ltype === 'undefined' || rtype === 'undefined') { // if either side is undefined, the result is false return false; } switch (op) { case '=': result = isDeepEqual(lhs, rhs); break; case '!=': result = !isDeepEqual(lhs, rhs); break; } return result; } /** * Evaluate comparison expression against input data * @param {Object} lhs - LHS value * @param {Object} rhs - RHS value * @param {Object} op - opcode * @returns {*} Result */ function evaluateComparisonExpression(lhs, rhs, op) { var result; // type checks var ltype = typeof lhs; var rtype = typeof rhs; var lcomparable = (ltype === 'undefined' || ltype === 'string' || ltype === 'number'); var rcomparable = (rtype === 'undefined' || rtype === 'string' || rtype === 'number'); // if either aa or bb are not comparable (string or numeric) values, then throw an error if (!lcomparable || !rcomparable) { throw { code: "T2010", stack: (new Error()).stack, value: !(ltype === 'string' || ltype === 'number') ? lhs : rhs }; } // if either side is undefined, the result is undefined if (ltype === 'undefined' || rtype === 'undefined') { return undefined; } //if aa and bb are not of the same type if (ltype !== rtype) { throw { code: "T2009", stack: (new Error()).stack, value: lhs, value2: rhs }; } switch (op) { 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} lhs - LHS value * @param {Object} rhs - RHS value * @returns {boolean} - true if lhs is a member of rhs */ function evaluateIncludesExpression(lhs, rhs) { var result = false; 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} lhs - LHS value * @param {Object} rhs - RHS value * @param {Object} op - opcode * @returns {*} Result */ function evaluateBooleanExpression(lhs, rhs, op) { var result; var lBool = fn.boolean(lhs); var rBool = fn.boolean(rhs); if (typeof lBool === 'undefined') { lBool = false; } if (typeof rBool === 'undefined') { rBool = false; } switch (op) { case 'and': result = lBool && rBool; break; case 'or': result = lBool || rBool; break; } return result; } /** * Evaluate string concatenation against input data * @param {Object} lhs - LHS value * @param {Object} rhs - RHS value * @returns {string|*} Concatenated string */ function evaluateStringConcat(lhs, rhs) { var result; var lstr = ''; var rstr = ''; if (typeof lhs !== 'undefined') { lstr = fn.string(lhs); } if (typeof rhs !== 'undefined') { rstr = fn.string(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 = {}; var reduce = input && input.tupleStream ? true : false; // group the input sequence by 'key' expression if (!Array.isArray(input)) { input = createSequence(input); } for(var itemIndex = 0; itemIndex < input.length; itemIndex++) { var item = input[itemIndex]; var env = reduce ? createFrameFromTuple(environment, item) : environment; for(var pairIndex = 0; pairIndex < expr.lhs.length; pairIndex++) { var pair = expr.lhs[pairIndex]; var key = yield * evaluate(pair[0], reduce ? item['@'] : item, env); // key has to be a string if (typeof key !== 'string') { throw { code: "T1003", stack: (new Error()).stack, position: expr.position, value: key }; } var entry = {data: item, exprIndex: pairIndex}; if (groups.hasOwnProperty(key)) { // a value already exists in this slot if(groups[key].exprIndex !== pairIndex) { // this key has been generated by another expression in this group // when multiple key expressions evaluate to the same key, then error D1009 must be thrown throw { code: "D1009", stack: (new Error()).stack, position: expr.position, value: key }; } // append it as an array groups[key].data = fn.append(groups[key].data, item); } else { groups[key] = entry; } } } // iterate over the groups to evaluate the 'value' expression for (key in groups) { entry = groups[key]; var context = entry.data; var env = environment; if (reduce) { var tuple = reduceTupleStream(entry.data); context = tuple['@']; delete tuple['@']; env = createFrameFromTuple(environment, tuple); } var value = yield * evaluate(expr.lhs[entry.exprIndex][1], context, env); if(typeof value !== 'undefined') { result[key] = value; } } return result; } function reduceTupleStream(tupleStream) { if(!Array.isArray(tupleStream)) { return tupleStream; } var result = {}; Object.assign(result, tupleStream[0]); for(var ii = 1; ii < tupleStream.length; ii++) { for(const prop in tupleStream[ii]) { result[prop] = fn.append(result[prop], tupleStream[ii][prop]); } } return result; } /** * Evaluate range expression against input data * @param {Object} lhs - LHS value * @param {Object} rhs - RHS value * @returns {Array} Resultant array */ function evaluateRangeExpression(lhs, rhs) { var result; if (typeof lhs !== 'undefined' && !Number.isInteger(lhs)) { throw { code: "T2003", stack: (new Error()).stack, value: lhs }; } if (typeof rhs !== 'undefined' && !Number.isInteger(rhs)) { throw { code: "T2004", stack: (new Error()).stack, value: rhs }; } 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; } // limit the size of the array to ten million entries (1e7) // this is an implementation defined limit to protect against // memory and performance issues. This value may increase in the future. var size = rhs - lhs + 1; if(size > 1e7) { throw { code: "D2014", stack: (new Error()).stack, value: size }; } result = new Array(size); for (var item = lhs, index = 0; item <= rhs; item++, index++) { result[index] = item; } result.sequence = true; 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 (enforced by parser) var value = yield * evaluate(expr.rhs, input, environment); 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 = yield * evaluate(expr.condition, input, environment); if (fn.boolean(condition)) { result = yield * evaluate(expr.then, input, environment); } else if (typeof expr.else !== 'undefined') { result = yield * 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 for(var ii = 0; ii < expr.expressions.length; ii++) { result = yield * evaluate(expr.expressions[ii], input, frame); } return result; } /** * Prepare a regex * @param {Object} expr - expression containing regex * @returns {Function} Higher order function representing prepared regex */ function evaluateRegex(expr) { var re = new jsonata.RegexEngine(expr.value); var closure = function(str, fromIndex) { var result; re.lastIndex = fromIndex || 0; var match = re.exec(str); if(match !== null) { result = { match: match[0], start: match.index, end: match.index + match[0].length, groups: [] }; if(match.length > 1) { for(var i = 1; i < match.length; i++) { result.groups.push(match[i]); } } result.next = function() { if(re.lastIndex >= str.length) { return undefined; } else { var next = closure(str, re.lastIndex); if(next && next.match === '') { // matches zero length string; this will never progress throw { code: "D1004", stack: (new Error()).stack, position: expr.position, value: expr.value.source }; } return next; } }; } return result; }; return closure; } /** * 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 && input.outerWrapper ? input[0] : input; } else { result = environment.lookup(expr.value); } return result; } /** * sort / order-by operator * @param {Object} expr - AST for operator * @param {Object} input - Input data to evaluate against * @param {Object} environment - Environment * @returns {*} Ordered sequence */ function* evaluateSortExpression(expr, input, environment) { var result; // evaluate the lhs, then sort the results in order according to rhs expression //var lhs = yield * evaluate(expr.lhs, input, environment); var lhs = input; var isTupleSort = input.tupleStream ? true : false; // sort the lhs array // use comparator function var comparator = function*(a, b) { // eslint-disable-line require-yield // expr.terms is an array of order-by in priority order var comp = 0; for(var index = 0; comp === 0 && index < expr.terms.length; index++) { var term = expr.terms[index]; //evaluate the sort term in the context of a var context = a; var env = environment; if(isTupleSort) { context = a['@']; env = createFrameFromTuple(environment, a); } var aa = yield * evaluate(term.expression, context, env); //evaluate the sort term in the context of b context = b; env = environment; if(isTupleSort) { context = b['@']; env = createFrameFromTuple(environment, b); } var bb = yield * evaluate(term.expression, context, env); // type checks var atype = typeof aa; var btype = typeof bb; // undefined should be last in sort order if(atype === 'undefined') { // swap them, unless btype is also undefined comp = (btype === 'undefined') ? 0 : 1; continue; } if(btype === 'undefined') { comp = -1; continue; } // if aa or bb are not string or numeric values, then throw an error if(!(atype === 'string' || atype === 'number') || !(btype === 'string' || btype === 'number')) { throw { code: "T2008", stack: (new Error()).stack, position: expr.position, value: !(atype === 'string' || atype === 'number') ? aa : bb }; } //if aa and bb are not of the same type if(atype !== btype) { throw { code: "T2007", stack: (new Error()).stack, position: expr.position, value: aa, value2: bb }; } if(aa === bb) { // both the same - move on to next term continue; } else if (aa < bb) { comp = -1; } else { comp = 1; } if(term.descending === true) { comp = -comp; } } // only swap a & b if comp equals 1 return comp === 1; }; var focus = { environment: environment, input: input }; // the `focus` is passed in as the `this` for the invoked function result = yield * fn.sort.apply(focus, [lhs, comparator]); return result; } /** * create a transformer function * @param {Object} expr - AST for operator * @param {Object} input - Input data to evaluate against * @param {Object} environment - Environment * @returns {*} tranformer function */ function evaluateTransformExpression(expr, input, environment) { // create a function to implement the transform definition var transformer = function*(obj) { // signature <(oa):o> // undefined inputs always return undefined if(typeof obj === 'undefined') { return undefined; } // this function returns a copy of obj with changes specified by the pattern/operation var cloneFunction = environment.lookup('clone'); if(!isFunction(cloneFunction)) { // throw type error throw { code: "T2013", stack: (new Error()).stack, position: expr.position }; } var result = yield * apply(cloneFunction, [obj], null, environment); var matches = yield * evaluate(expr.pattern, result, environment); if(typeof matches !== 'undefined') { if(!Array.isArray(matches)) { matches = [matches]; } for(var ii = 0; ii < matches.length; ii++) { var match = matches[ii]; // evaluate the update value for each match var update = yield * evaluate(expr.update, match, environment); // update must be an object var updateType = typeof update; if(updateType !== 'undefined') { if(updateType !== 'object' || update === null || Array.isArray(update)) { // throw type error throw { code: "T2011", stack: (new Error()).stack, position: expr.update.position, value: update }; } // merge the update for(var prop in update) { match[prop] = update[prop]; } } // delete, if specified, must be an array of strings (or single string) if(typeof expr.delete !== 'undefined') { var deletions = yield * evaluate(expr.delete, match, environment); if(typeof deletions !== 'undefined') { var val = deletions; if (!Array.isArray(deletions)) { deletions = [deletions]; } if (!isArrayOfStrings(deletions)) { // throw type error throw { code: "T2012", stack: (new Error()).stack, position: expr.delete.position, value: val }; } for (var jj = 0; jj < deletions.length; jj++) { if(typeof match === 'object' && match !== null) { delete match[deletions[jj]]; } } } } } } return result; }; return defineFunction(transformer, '<(oa):o>'); } var chainAST = parser('function($f, $g) { function($x){ $g($f($x)) } }'); /** * Apply the function on the RHS using the sequence on the LHS as the first argument * @param {Object} expr - JSONata expression * @param {Object} input - Input data to evaluate against * @param {Object} environment - Environment * @returns {*} Evaluated input data */ function* evaluateApplyExpression(expr, input, environment) { var result; var lhs = yield * evaluate(expr.lhs, input, environment); if(expr.rhs.type === 'function') { // this is a function _invocation_; invoke it with lhs expression as the first argument result = yield * evaluateFunction(expr.rhs, input, environment, { context: lhs }); } else { var func = yield * evaluate(expr.rhs, input, environment); if(!isFunction(func)) { throw { code: "T2006", stack: (new Error()).stack, position: expr.position, value: func }; } if(isFunction(lhs)) { // this is function chaining (func1 ~> func2) // λ($f, $g) { λ($x){ $g($f($x)) } } var chain = yield * evaluate(chainAST, null, environment); result = yield * apply(chain, [lhs, func], null, environment); } else { result = yield * apply(func, [lhs], null, environment); } } 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, applyto) { var result; // 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 = yield * evaluate(expr.procedure, input, environment); if (typeof proc === 'undefined' && expr.procedure.type === 'path' && environment.lookup(expr.procedure.steps[0].value)) { // help the user out here if they simply forgot the leading $ throw { code: "T1005", stack: (new Error()).stack, position: expr.position, token: expr.procedure.steps[0].value }; } var evaluatedArgs = []; if(typeof applyto !== 'undefined') { evaluatedArgs.push(applyto.context); } // eager evaluation - evaluate the arguments for (var jj = 0; jj < expr.arguments.length; jj++) { const arg = yield* evaluate(expr.arguments[jj], input, environment); if(isFunction(arg)) { // wrap this in a closure const closure = function* (...params) { // invoke func return yield * apply(arg, params, null, environment); }; closure.arity = getFunctionArity(arg); evaluatedArgs.push(clos