test-jsonata
Version:
JSON query and transformation language
1,419 lines (1,294 loc) • 85.6 kB
JavaScript
/**
* © Copyright IBM Corp. 2016, 2017 All Rights Reserved
* Project name: JSONata
* This project is licensed under the MIT License, see LICENSE
*/
/**
* @module JSONata
* @description JSON query and transformation language
*/
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