(function(exports) { "use strict"; function isArray(obj) { if (obj !== null) { return Object.prototype.toString.call(obj) === "[object Array]"; } else { return false; } } function isObject(obj) { if (obj !== null) { return Object.prototype.toString.call(obj) === "[object Object]"; } else { return false; } } function strictDeepEqual(first, second) { // Check the scalar case first. if (first === second) { return true; } // Check if they are the same type. var firstType = Object.prototype.toString.call(first); if (firstType !== Object.prototype.toString.call(second)) { return false; } // We know that first and second have the same type so we can just check the // first type from now on. if (isArray(first) === true) { // Short circuit if they're not the same length; if (first.length !== second.length) { return false; } for (var i = 0; i < first.length; i++) { if (strictDeepEqual(first[i], second[i]) === false) { return false; } } return true; } if (isObject(first) === true) { // An object is equal if it has the same key/value pairs. var keysSeen = {}; for (var key in first) { if (hasOwnProperty.call(first, key)) { if (strictDeepEqual(first[key], second[key]) === false) { return false; } keysSeen[key] = true; } } // Now check that there aren't any keys in second that weren't // in first. for (var key2 in second) { if (hasOwnProperty.call(second, key2)) { if (keysSeen[key2] !== true) { return false; } } } return true; } return false; } function isFalse(obj) { // From the spec: // A false value corresponds to the following values: // Empty list // Empty object // Empty string // False boolean // null value // First check the scalar values. if (obj === "" || obj === false || obj === null) { return true; } else if (isArray(obj) && obj.length === 0) { // Check for an empty array. return true; } else if (isObject(obj)) { // Check for an empty object. for (var key in obj) { // If there are any keys, then // the object is not empty so the object // is not false. if (obj.hasOwnProperty(key)) { return false; } } return true; } else { return false; } } function objValues(obj) { var keys = Object.keys(obj); var values = []; for (var i = 0; i < keys.length; i++) { values.push(obj[keys[i]]); } return values; } function merge(a, b) { var merged = {}; for (var key in a) { merged[key] = a[key]; } for (var key2 in b) { merged[key2] = b[key2]; } return merged; } var trimLeft; if (typeof String.prototype.trimLeft === "function") { trimLeft = function(str) { return str.trimLeft(); }; } else { trimLeft = function(str) { return str.match(/^\s*(.*)/)[1]; }; } // Type constants used to define functions. var TYPE_NUMBER = 0; var TYPE_ANY = 1; var TYPE_STRING = 2; var TYPE_ARRAY = 3; var TYPE_OBJECT = 4; var TYPE_BOOLEAN = 5; var TYPE_EXPREF = 6; var TYPE_NULL = 7; var TYPE_ARRAY_NUMBER = 8; var TYPE_ARRAY_STRING = 9; var TOK_EOF = "EOF"; var TOK_UNQUOTEDIDENTIFIER = "UnquotedIdentifier"; var TOK_QUOTEDIDENTIFIER = "QuotedIdentifier"; var TOK_RBRACKET = "Rbracket"; var TOK_RPAREN = "Rparen"; var TOK_COMMA = "Comma"; var TOK_COLON = "Colon"; var TOK_RBRACE = "Rbrace"; var TOK_NUMBER = "Number"; var TOK_CURRENT = "Current"; var TOK_EXPREF = "Expref"; var TOK_PIPE = "Pipe"; var TOK_OR = "Or"; var TOK_AND = "And"; var TOK_EQ = "EQ"; var TOK_GT = "GT"; var TOK_LT = "LT"; var TOK_GTE = "GTE"; var TOK_LTE = "LTE"; var TOK_NE = "NE"; var TOK_FLATTEN = "Flatten"; var TOK_STAR = "Star"; var TOK_FILTER = "Filter"; var TOK_DOT = "Dot"; var TOK_NOT = "Not"; var TOK_LBRACE = "Lbrace"; var TOK_LBRACKET = "Lbracket"; var TOK_LPAREN= "Lparen"; var TOK_LITERAL= "Literal"; // The "&", "[", "<", ">" tokens // are not in basicToken because // there are two token variants // ("&&", "[?", "<=", ">="). This is specially handled // below. var basicTokens = { ".": TOK_DOT, "*": TOK_STAR, ",": TOK_COMMA, ":": TOK_COLON, "{": TOK_LBRACE, "}": TOK_RBRACE, "]": TOK_RBRACKET, "(": TOK_LPAREN, ")": TOK_RPAREN, "@": TOK_CURRENT }; var operatorStartToken = { "<": true, ">": true, "=": true, "!": true }; var skipChars = { " ": true, "\t": true, "\n": true }; function isAlpha(ch) { return (ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z") || ch === "_"; } function isNum(ch) { return (ch >= "0" && ch <= "9") || ch === "-"; } function isAlphaNum(ch) { return (ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z") || (ch >= "0" && ch <= "9") || ch === "_"; } function Lexer() { } Lexer.prototype = { tokenize: function(stream) { var tokens = []; this._current = 0; var start; var identifier; var token; while (this._current < stream.length) { if (isAlpha(stream[this._current])) { start = this._current; identifier = this._consumeUnquotedIdentifier(stream); tokens.push({type: TOK_UNQUOTEDIDENTIFIER, value: identifier, start: start}); } else if (basicTokens[stream[this._current]] !== undefined) { tokens.push({type: basicTokens[stream[this._current]], value: stream[this._current], start: this._current}); this._current++; } else if (isNum(stream[this._current])) { token = this._consumeNumber(stream); tokens.push(token); } else if (stream[this._current] === "[") { // No need to increment this._current. This happens // in _consumeLBracket token = this._consumeLBracket(stream); tokens.push(token); } else if (stream[this._current] === "\"") { start = this._current; identifier = this._consumeQuotedIdentifier(stream); tokens.push({type: TOK_QUOTEDIDENTIFIER, value: identifier, start: start}); } else if (stream[this._current] === "'") { start = this._current; identifier = this._consumeRawStringLiteral(stream); tokens.push({type: TOK_LITERAL, value: identifier, start: start}); } else if (stream[this._current] === "`") { start = this._current; var literal = this._consumeLiteral(stream); tokens.push({type: TOK_LITERAL, value: literal, start: start}); } else if (operatorStartToken[stream[this._current]] !== undefined) { tokens.push(this._consumeOperator(stream)); } else if (skipChars[stream[this._current]] !== undefined) { // Ignore whitespace. this._current++; } else if (stream[this._current] === "&") { start = this._current; this._current++; if (stream[this._current] === "&") { this._current++; tokens.push({type: TOK_AND, value: "&&", start: start}); } else { tokens.push({type: TOK_EXPREF, value: "&", start: start}); } } else if (stream[this._current] === "|") { start = this._current; this._current++; if (stream[this._current] === "|") { this._current++; tokens.push({type: TOK_OR, value: "||", start: start}); } else { tokens.push({type: TOK_PIPE, value: "|", start: start}); } } else { var error = new Error("Unknown character:" + stream[this._current]); error.name = "LexerError"; throw error; } } return tokens; }, _consumeUnquotedIdentifier: function(stream) { var start = this._current; this._current++; while (this._current < stream.length && isAlphaNum(stream[this._current])) { this._current++; } return stream.slice(start, this._current); }, _consumeQuotedIdentifier: function(stream) { var start = this._current; this._current++; var maxLength = stream.length; while (stream[this._current] !== "\"" && this._current < maxLength) { // You can escape a double quote and you can escape an escape. var current = this._current; if (stream[current] === "\\" && (stream[current + 1] === "\\" || stream[current + 1] === "\"")) { current += 2; } else { current++; } this._current = current; } this._current++; return JSON.parse(stream.slice(start, this._current)); }, _consumeRawStringLiteral: function(stream) { var start = this._current; this._current++; var maxLength = stream.length; while (stream[this._current] !== "'" && this._current < maxLength) { // You can escape a single quote and you can escape an escape. var current = this._current; if (stream[current] === "\\" && (stream[current + 1] === "\\" || stream[current + 1] === "'")) { current += 2; } else { current++; } this._current = current; } this._current++; var literal = stream.slice(start + 1, this._current - 1); return literal.replace("\\'", "'"); }, _consumeNumber: function(stream) { var start = this._current; this._current++; var maxLength = stream.length; while (isNum(stream[this._current]) && this._current < maxLength) { this._current++; } var value = parseInt(stream.slice(start, this._current)); return {type: TOK_NUMBER, value: value, start: start}; }, _consumeLBracket: function(stream) { var start = this._current; this._current++; if (stream[this._current] === "?") { this._current++; return {type: TOK_FILTER, value: "[?", start: start}; } else if (stream[this._current] === "]") { this._current++; return {type: TOK_FLATTEN, value: "[]", start: start}; } else { return {type: TOK_LBRACKET, value: "[", start: start}; } }, _consumeOperator: function(stream) { var start = this._current; var startingChar = stream[start]; this._current++; if (startingChar === "!") { if (stream[this._current] === "=") { this._current++; return {type: TOK_NE, value: "!=", start: start}; } else { return {type: TOK_NOT, value: "!", start: start}; } } else if (startingChar === "<") { if (stream[this._current] === "=") { this._current++; return {type: TOK_LTE, value: "<=", start: start}; } else { return {type: TOK_LT, value: "<", start: start}; } } else if (startingChar === ">") { if (stream[this._current] === "=") { this._current++; return {type: TOK_GTE, value: ">=", start: start}; } else { return {type: TOK_GT, value: ">", start: start}; } } else if (startingChar === "=") { if (stream[this._current] === "=") { this._current++; return {type: TOK_EQ, value: "==", start: start}; } } }, _consumeLiteral: function(stream) { this._current++; var start = this._current; var maxLength = stream.length; var literal; while(stream[this._current] !== "`" && this._current < maxLength) { // You can escape a literal char or you can escape the escape. var current = this._current; if (stream[current] === "\\" && (stream[current + 1] === "\\" || stream[current + 1] === "`")) { current += 2; } else { current++; } this._current = current; } var literalString = trimLeft(stream.slice(start, this._current)); literalString = literalString.replace("\\`", "`"); if (this._looksLikeJSON(literalString)) { literal = JSON.parse(literalString); } else { // Try to JSON parse it as "" literal = JSON.parse("\"" + literalString + "\""); } // +1 gets us to the ending "`", +1 to move on to the next char. this._current++; return literal; }, _looksLikeJSON: function(literalString) { var startingChars = "[{\""; var jsonLiterals = ["true", "false", "null"]; var numberLooking = "-0123456789"; if (literalString === "") { return false; } else if (startingChars.indexOf(literalString[0]) >= 0) { return true; } else if (jsonLiterals.indexOf(literalString) >= 0) { return true; } else if (numberLooking.indexOf(literalString[0]) >= 0) { try { JSON.parse(literalString); return true; } catch (ex) { return false; } } else { return false; } } }; var bindingPower = {}; bindingPower[TOK_EOF] = 0; bindingPower[TOK_UNQUOTEDIDENTIFIER] = 0; bindingPower[TOK_QUOTEDIDENTIFIER] = 0; bindingPower[TOK_RBRACKET] = 0; bindingPower[TOK_RPAREN] = 0; bindingPower[TOK_COMMA] = 0; bindingPower[TOK_RBRACE] = 0; bindingPower[TOK_NUMBER] = 0; bindingPower[TOK_CURRENT] = 0; bindingPower[TOK_EXPREF] = 0; bindingPower[TOK_PIPE] = 1; bindingPower[TOK_OR] = 2; bindingPower[TOK_AND] = 3; bindingPower[TOK_EQ] = 5; bindingPower[TOK_GT] = 5; bindingPower[TOK_LT] = 5; bindingPower[TOK_GTE] = 5; bindingPower[TOK_LTE] = 5; bindingPower[TOK_NE] = 5; bindingPower[TOK_FLATTEN] = 9; bindingPower[TOK_STAR] = 20; bindingPower[TOK_FILTER] = 21; bindingPower[TOK_DOT] = 40; bindingPower[TOK_NOT] = 45; bindingPower[TOK_LBRACE] = 50; bindingPower[TOK_LBRACKET] = 55; bindingPower[TOK_LPAREN] = 60; function Parser() { } Parser.prototype = { parse: function(expression) { this._loadTokens(expression); this.index = 0; var ast = this.expression(0); if (this._lookahead(0) !== TOK_EOF) { var t = this._lookaheadToken(0); var error = new Error( "Unexpected token type: " + t.type + ", value: " + t.value); error.name = "ParserError"; throw error; } return ast; }, _loadTokens: function(expression) { var lexer = new Lexer(); var tokens = lexer.tokenize(expression); tokens.push({type: TOK_EOF, value: "", start: expression.length}); this.tokens = tokens; }, expression: function(rbp) { var leftToken = this._lookaheadToken(0); this._advance(); var left = this.nud(leftToken); var currentToken = this._lookahead(0); while (rbp < bindingPower[currentToken]) { this._advance(); left = this.led(currentToken, left); currentToken = this._lookahead(0); } return left; }, _lookahead: function(number) { return this.tokens[this.index + number].type; }, _lookaheadToken: function(number) { return this.tokens[this.index + number]; }, _advance: function() { this.index++; }, nud: function(token) { var left; var right; var expression; switch (token.type) { case TOK_LITERAL: return {type: "Literal", value: token.value}; case TOK_UNQUOTEDIDENTIFIER: return {type: "Field", name: token.value}; case TOK_QUOTEDIDENTIFIER: var node = {type: "Field", name: token.value}; if (this._lookahead(0) === TOK_LPAREN) { throw new Error("Quoted identifier not allowed for function names."); } else { return node; } break; case TOK_NOT: right = this.expression(bindingPower.Not); return {type: "NotExpression", children: [right]}; case TOK_STAR: left = {type: "Identity"}; right = null; if (this._lookahead(0) === TOK_RBRACKET) { // This can happen in a multiselect, // [a, b, *] right = {type: "Identity"}; } else { right = this._parseProjectionRHS(bindingPower.Star); } return {type: "ValueProjection", children: [left, right]}; case TOK_FILTER: return this.led(token.type, {type: "Identity"}); case TOK_LBRACE: return this._parseMultiselectHash(); case TOK_FLATTEN: left = {type: TOK_FLATTEN, children: [{type: "Identity"}]}; right = this._parseProjectionRHS(bindingPower.Flatten); return {type: "Projection", children: [left, right]}; case TOK_LBRACKET: if (this._lookahead(0) === TOK_NUMBER || this._lookahead(0) === TOK_COLON) { right = this._parseIndexExpression(); return this._projectIfSlice({type: "Identity"}, right); } else if (this._lookahead(0) === TOK_STAR && this._lookahead(1) === TOK_RBRACKET) { this._advance(); this._advance(); right = this._parseProjectionRHS(bindingPower.Star); return {type: "Projection", children: [{type: "Identity"}, right]}; } else { return this._parseMultiselectList(); } break; case TOK_CURRENT: return {type: TOK_CURRENT}; case TOK_EXPREF: expression = this.expression(bindingPower.Expref); return {type: "ExpressionReference", children: [expression]}; case TOK_LPAREN: var args = []; while (this._lookahead(0) !== TOK_RPAREN) { if (this._lookahead(0) === TOK_CURRENT) { expression = {type: TOK_CURRENT}; this._advance(); } else { expression = this.expression(0); } args.push(expression); } this._match(TOK_RPAREN); return args[0]; default: this._errorToken(token); } }, led: function(tokenName, left) { var right; switch(tokenName) { case TOK_DOT: var rbp = bindingPower.Dot; if (this._lookahead(0) !== TOK_STAR) { right = this._parseDotRHS(rbp); return {type: "Subexpression", children: [left, right]}; } else { // Creating a projection. this._advance(); right = this._parseProjectionRHS(rbp); return {type: "ValueProjection", children: [left, right]}; } break; case TOK_PIPE: right = this.expression(bindingPower.Pipe); return {type: TOK_PIPE, children: [left, right]}; case TOK_OR: right = this.expression(bindingPower.Or); return {type: "OrExpression", children: [left, right]}; case TOK_AND: right = this.expression(bindingPower.And); return {type: "AndExpression", children: [left, right]}; case TOK_LPAREN: var name = left.name; var args = []; var expression, node; while (this._lookahead(0) !== TOK_RPAREN) { if (this._lookahead(0) === TOK_CURRENT) { expression = {type: TOK_CURRENT}; this._advance(); } else { expression = this.expression(0); } if (this._lookahead(0) === TOK_COMMA) { this._match(TOK_COMMA); } args.push(expression); } this._match(TOK_RPAREN); node = {type: "Function", name: name, children: args}; return node; case TOK_FILTER: var condition = this.expression(0); this._match(TOK_RBRACKET); if (this._lookahead(0) === TOK_FLATTEN) { right = {type: "Identity"}; } else { right = this._parseProjectionRHS(bindingPower.Filter); } return {type: "FilterProjection", children: [left, right, condition]}; case TOK_FLATTEN: var leftNode = {type: TOK_FLATTEN, children: [left]}; var rightNode = this._parseProjectionRHS(bindingPower.Flatten); return {type: "Projection", children: [leftNode, rightNode]}; case TOK_EQ: case TOK_NE: case TOK_GT: case TOK_GTE: case TOK_LT: case TOK_LTE: return this._parseComparator(left, tokenName); case TOK_LBRACKET: var token = this._lookaheadToken(0); if (token.type === TOK_NUMBER || token.type === TOK_COLON) { right = this._parseIndexExpression(); return this._projectIfSlice(left, right); } else { this._match(TOK_STAR); this._match(TOK_RBRACKET); right = this._parseProjectionRHS(bindingPower.Star); return {type: "Projection", children: [left, right]}; } break; default: this._errorToken(this._lookaheadToken(0)); } }, _match: function(tokenType) { if (this._lookahead(0) === tokenType) { this._advance(); } else { var t = this._lookaheadToken(0); var error = new Error("Expected " + tokenType + ", got: " + t.type); error.name = "ParserError"; throw error; } }, _errorToken: function(token) { var error = new Error("Invalid token (" + token.type + "): \"" + token.value + "\""); error.name = "ParserError"; throw error; }, _parseIndexExpression: function() { if (this._lookahead(0) === TOK_COLON || this._lookahead(1) === TOK_COLON) { return this._parseSliceExpression(); } else { var node = { type: "Index", value: this._lookaheadToken(0).value}; this._advance(); this._match(TOK_RBRACKET); return node; } }, _projectIfSlice: function(left, right) { var indexExpr = {type: "IndexExpression", children: [left, right]}; if (right.type === "Slice") { return { type: "Projection", children: [indexExpr, this._parseProjectionRHS(bindingPower.Star)] }; } else { return indexExpr; } }, _parseSliceExpression: function() { // [start:end:step] where each part is optional, as well as the last // colon. var parts = [null, null, null]; var index = 0; var currentToken = this._lookahead(0); while (currentToken !== TOK_RBRACKET && index < 3) { if (currentToken === TOK_COLON) { index++; this._advance(); } else if (currentToken === TOK_NUMBER) { parts[index] = this._lookaheadToken(0).value; this._advance(); } else { var t = this._lookahead(0); var error = new Error("Syntax error, unexpected token: " + t.value + "(" + t.type + ")"); error.name = "Parsererror"; throw error; } currentToken = this._lookahead(0); } this._match(TOK_RBRACKET); return { type: "Slice", children: parts }; }, _parseComparator: function(left, comparator) { var right = this.expression(bindingPower[comparator]); return {type: "Comparator", name: comparator, children: [left, right]}; }, _parseDotRHS: function(rbp) { var lookahead = this._lookahead(0); var exprTokens = [TOK_UNQUOTEDIDENTIFIER, TOK_QUOTEDIDENTIFIER, TOK_STAR]; if (exprTokens.indexOf(lookahead) >= 0) { return this.expression(rbp); } else if (lookahead === TOK_LBRACKET) { this._match(TOK_LBRACKET); return this._parseMultiselectList(); } else if (lookahead === TOK_LBRACE) { this._match(TOK_LBRACE); return this._parseMultiselectHash(); } }, _parseProjectionRHS: function(rbp) { var right; if (bindingPower[this._lookahead(0)] < 10) { right = {type: "Identity"}; } else if (this._lookahead(0) === TOK_LBRACKET) { right = this.expression(rbp); } else if (this._lookahead(0) === TOK_FILTER) { right = this.expression(rbp); } else if (this._lookahead(0) === TOK_DOT) { this._match(TOK_DOT); right = this._parseDotRHS(rbp); } else { var t = this._lookaheadToken(0); var error = new Error("Sytanx error, unexpected token: " + t.value + "(" + t.type + ")"); error.name = "ParserError"; throw error; } return right; }, _parseMultiselectList: function() { var expressions = []; while (this._lookahead(0) !== TOK_RBRACKET) { var expression = this.expression(0); expressions.push(expression); if (this._lookahead(0) === TOK_COMMA) { this._match(TOK_COMMA); if (this._lookahead(0) === TOK_RBRACKET) { throw new Error("Unexpected token Rbracket"); } } } this._match(TOK_RBRACKET); return {type: "MultiSelectList", children: expressions}; }, _parseMultiselectHash: function() { var pairs = []; var identifierTypes = [TOK_UNQUOTEDIDENTIFIER, TOK_QUOTEDIDENTIFIER]; var keyToken, keyName, value, node; for (;;) { keyToken = this._lookaheadToken(0); if (identifierTypes.indexOf(keyToken.type) < 0) { throw new Error("Expecting an identifier token, got: " + keyToken.type); } keyName = keyToken.value; this._advance(); this._match(TOK_COLON); value = this.expression(0); node = {type: "KeyValuePair", name: keyName, value: value}; pairs.push(node); if (this._lookahead(0) === TOK_COMMA) { this._match(TOK_COMMA); } else if (this._lookahead(0) === TOK_RBRACE) { this._match(TOK_RBRACE); break; } } return {type: "MultiSelectHash", children: pairs}; } }; function TreeInterpreter(runtime) { this.runtime = runtime; } TreeInterpreter.prototype = { search: function(node, value) { return this.visit(node, value); }, visit: function(node, value) { var matched, current, result, first, second, field, left, right, collected, i; switch (node.type) { case "Field": if (value === null ) { return null; } else if (isObject(value)) { field = value[node.name]; if (field === undefined) { return null; } else { return field; } } else { return null; } break; case "Subexpression": result = this.visit(node.children[0], value); for (i = 1; i < node.children.length; i++) { result = this.visit(node.children[1], result); if (result === null) { return null; } } return result; case "IndexExpression": left = this.visit(node.children[0], value); right = this.visit(node.children[1], left); return right; case "Index": if (!isArray(value)) { return null; } var index = node.value; if (index < 0) { index = value.length + index; } result = value[index]; if (result === undefined) { result = null; } return result; case "Slice": if (!isArray(value)) { return null; } var sliceParams = node.children.slice(0); var computed = this.computeSliceParams(value.length, sliceParams); var start = computed[0]; var stop = computed[1]; var step = computed[2]; result = []; if (step > 0) { for (i = start; i < stop; i += step) { result.push(value[i]); } } else { for (i = start; i > stop; i += step) { result.push(value[i]); } } return result; case "Projection": // Evaluate left child. var base = this.visit(node.children[0], value); if (!isArray(base)) { return null; } collected = []; for (i = 0; i < base.length; i++) { current = this.visit(node.children[1], base[i]); if (current !== null) { collected.push(current); } } return collected; case "ValueProjection": // Evaluate left child. base = this.visit(node.children[0], value); if (!isObject(base)) { return null; } collected = []; var values = objValues(base); for (i = 0; i < values.length; i++) { current = this.visit(node.children[1], values[i]); if (current !== null) { collected.push(current); } } return collected; case "FilterProjection": base = this.visit(node.children[0], value); if (!isArray(base)) { return null; } var filtered = []; var finalResults = []; for (i = 0; i < base.length; i++) { matched = this.visit(node.children[2], base[i]); if (!isFalse(matched)) { filtered.push(base[i]); } } for (var j = 0; j < filtered.length; j++) { current = this.visit(node.children[1], filtered[j]); if (current !== null) { finalResults.push(current); } } return finalResults; case "Comparator": first = this.visit(node.children[0], value); second = this.visit(node.children[1], value); switch(node.name) { case TOK_EQ: result = strictDeepEqual(first, second); break; case TOK_NE: result = !strictDeepEqual(first, second); break; case TOK_GT: result = first > second; break; case TOK_GTE: result = first >= second; break; case TOK_LT: result = first < second; break; case TOK_LTE: result = first <= second; break; default: throw new Error("Unknown comparator: " + node.name); } return result; case TOK_FLATTEN: var original = this.visit(node.children[0], value); if (!isArray(original)) { return null; } var merged = []; for (i = 0; i < original.length; i++) { current = original[i]; if (isArray(current)) { merged.push.apply(merged, current); } else { merged.push(current); } } return merged; case "Identity": return value; case "MultiSelectList": if (value === null) { return null; } collected = []; for (i = 0; i < node.children.length; i++) { collected.push(this.visit(node.children[i], value)); } return collected; case "MultiSelectHash": if (value === null) { return null; } collected = {}; var child; for (i = 0; i < node.children.length; i++) { child = node.children[i]; collected[child.name] = this.visit(child.value, value); } return collected; case "OrExpression": matched = this.visit(node.children[0], value); if (isFalse(matched)) { matched = this.visit(node.children[1], value); } return matched; case "AndExpression": first = this.visit(node.children[0], value); if (isFalse(first) === true) { return first; } return this.visit(node.children[1], value); case "NotExpression": first = this.visit(node.children[0], value); return isFalse(first); case "Literal": return node.value; case TOK_PIPE: left = this.visit(node.children[0], value); return this.visit(node.children[1], left); case TOK_CURRENT: return value; case "Function": var resolvedArgs = []; for (i = 0; i < node.children.length; i++) { resolvedArgs.push(this.visit(node.children[i], value)); } return this.runtime.callFunction(node.name, resolvedArgs); case "ExpressionReference": var refNode = node.children[0]; // Tag the node with a specific attribute so the type // checker verify the type. refNode.jmespathType = TOK_EXPREF; return refNode; default: throw new Error("Unknown node type: " + node.type); } }, computeSliceParams: function(arrayLength, sliceParams) { var start = sliceParams[0]; var stop = sliceParams[1]; var step = sliceParams[2]; var computed = [null, null, null]; if (step === null) { step = 1; } else if (step === 0) { var error = new Error("Invalid slice, step cannot be 0"); error.name = "RuntimeError"; throw error; } var stepValueNegative = step < 0 ? true : false; if (start === null) { start = stepValueNegative ? arrayLength - 1 : 0; } else { start = this.capSliceRange(arrayLength, start, step); } if (stop === null) { stop = stepValueNegative ? -1 : arrayLength; } else { stop = this.capSliceRange(arrayLength, stop, step); } computed[0] = start; computed[1] = stop; computed[2] = step; return computed; }, capSliceRange: function(arrayLength, actualValue, step) { if (actualValue < 0) { actualValue += arrayLength; if (actualValue < 0) { actualValue = step < 0 ? -1 : 0; } } else if (actualValue >= arrayLength) { actualValue = step < 0 ? arrayLength - 1 : arrayLength; } return actualValue; } }; function Runtime(interpreter) { this._interpreter = interpreter; this.functionTable = { // name: [function, ] // The can be: // // { // args: [[type1, type2], [type1, type2]], // variadic: true|false // } // // Each arg in the arg list is a list of valid types // (if the function is overloaded and supports multiple // types. If the type is "any" then no type checking // occurs on the argument. Variadic is optional // and if not provided is assumed to be false. abs: {_func: this._functionAbs, _signature: [{types: [TYPE_NUMBER]}]}, avg: {_func: this._functionAvg, _signature: [{types: [TYPE_ARRAY_NUMBER]}]}, ceil: {_func: this._functionCeil, _signature: [{types: [TYPE_NUMBER]}]}, contains: { _func: this._functionContains, _signature: [{types: [TYPE_STRING, TYPE_ARRAY]}, {types: [TYPE_ANY]}]}, "ends_with": { _func: this._functionEndsWith, _signature: [{types: [TYPE_STRING]}, {types: [TYPE_STRING]}]}, floor: {_func: this._functionFloor, _signature: [{types: [TYPE_NUMBER]}]}, length: { _func: this._functionLength, _signature: [{types: [TYPE_STRING, TYPE_ARRAY, TYPE_OBJECT]}]}, map: { _func: this._functionMap, _signature: [{types: [TYPE_EXPREF]}, {types: [TYPE_ARRAY]}]}, max: { _func: this._functionMax, _signature: [{types: [TYPE_ARRAY_NUMBER, TYPE_ARRAY_STRING]}]}, "merge": { _func: this._functionMerge, _signature: [{types: [TYPE_OBJECT], variadic: true}] }, "max_by": { _func: this._functionMaxBy, _signature: [{types: [TYPE_ARRAY]}, {types: [TYPE_EXPREF]}] }, sum: {_func: this._functionSum, _signature: [{types: [TYPE_ARRAY_NUMBER]}]}, "starts_with": { _func: this._functionStartsWith, _signature: [{types: [TYPE_STRING]}, {types: [TYPE_STRING]}]}, min: { _func: this._functionMin, _signature: [{types: [TYPE_ARRAY_NUMBER, TYPE_ARRAY_STRING]}]}, "min_by": { _func: this._functionMinBy, _signature: [{types: [TYPE_ARRAY]}, {types: [TYPE_EXPREF]}] }, type: {_func: this._functionType, _signature: [{types: [TYPE_ANY]}]}, keys: {_func: this._functionKeys, _signature: [{types: [TYPE_OBJECT]}]}, values: {_func: this._functionValues, _signature: [{types: [TYPE_OBJECT]}]}, sort: {_func: this._functionSort, _signature: [{types: [TYPE_ARRAY_STRING, TYPE_ARRAY_NUMBER]}]}, "sort_by": { _func: this._functionSortBy, _signature: [{types: [TYPE_ARRAY]}, {types: [TYPE_EXPREF]}] }, join: { _func: this._functionJoin, _signature: [ {types: [TYPE_STRING]}, {types: [TYPE_ARRAY_STRING]} ] }, reverse: { _func: this._functionReverse, _signature: [{types: [TYPE_STRING, TYPE_ARRAY]}]}, "to_array": {_func: this._functionToArray, _signature: [{types: [TYPE_ANY]}]}, "to_string": {_func: this._functionToString, _signature: [{types: [TYPE_ANY]}]}, "to_number": {_func: this._functionToNumber, _signature: [{types: [TYPE_ANY]}]}, "not_null": { _func: this._functionNotNull, _signature: [{types: [TYPE_ANY], variadic: true}] } }; } Runtime.prototype = { callFunction: function(name, resolvedArgs) { var functionEntry = this.functionTable[name]; if (functionEntry === undefined) { throw new Error("Unknown function: " + name + "()"); } this._validateArgs(name, resolvedArgs, functionEntry._signature); return functionEntry._func.call(this, resolvedArgs); }, _validateArgs: function(name, args, signature) { // Validating the args requires validating // the correct arity and the correct type of each arg. // If the last argument is declared as variadic, then we need // a minimum number of args to be required. Otherwise it has to // be an exact amount. var pluralized; if (signature[signature.length - 1].variadic) { if (args.length < signature.length) { pluralized = signature.length === 1 ? " argument" : " arguments"; throw new Error("ArgumentError: " + name + "() " + "takes at least" + signature.length + pluralized + " but received " + args.length); } } else if (args.length !== signature.length) { pluralized = signature.length === 1 ? " argument" : " arguments"; throw new Error("ArgumentError: " + name + "() " + "takes " + signature.length + pluralized + " but received " + args.length); } var currentSpec; var actualType; var typeMatched; for (var i = 0; i < signature.length; i++) { typeMatched = false; currentSpec = signature[i].types; actualType = this._getTypeName(args[i]); for (var j = 0; j < currentSpec.length; j++) { if (this._typeMatches(actualType, currentSpec[j], args[i])) { typeMatched = true; break; } } if (!typeMatched) { throw new Error("TypeError: " + name + "() " + "expected argument " + (i + 1) + " to be type " + currentSpec + " but received type " + actualType + " instead."); } } }, _typeMatches: function(actual, expected, argValue) { if (expected === TYPE_ANY) { return true; } if (expected === TYPE_ARRAY_STRING || expected === TYPE_ARRAY_NUMBER || expected === TYPE_ARRAY) { // The expected type can either just be array, // or it can require a specific subtype (array of numbers). // // The simplest case is if "array" with no subtype is specified. if (expected === TYPE_ARRAY) { return actual === TYPE_ARRAY; } else if (actual === TYPE_ARRAY) { // Otherwise we need to check subtypes. // I think this has potential to be improved. var subtype; if (expected === TYPE_ARRAY_NUMBER) { subtype = TYPE_NUMBER; } else if (expected === TYPE_ARRAY_STRING) { subtype = TYPE_STRING; } for (var i = 0; i < argValue.length; i++) { if (!this._typeMatches( this._getTypeName(argValue[i]), subtype, argValue[i])) { return false; } } return true; } } else { return actual === expected; } }, _getTypeName: function(obj) { switch (Object.prototype.toString.call(obj)) { case "[object String]": return TYPE_STRING; case "[object Number]": return TYPE_NUMBER; case "[object Array]": return TYPE_ARRAY; case "[object Boolean]": return TYPE_BOOLEAN; case "[object Null]": return TYPE_NULL; case "[object Object]": // Check if it's an expref. If it has, it's been // tagged with a jmespathType attr of 'Expref'; if (obj.jmespathType === TOK_EXPREF) { return TYPE_EXPREF; } else { return TYPE_OBJECT; } } }, _functionStartsWith: function(resolvedArgs) { return resolvedArgs[0].lastIndexOf(resolvedArgs[1]) === 0; }, _functionEndsWith: function(resolvedArgs) { var searchStr = resolvedArgs[0]; var suffix = resolvedArgs[1]; return searchStr.indexOf(suffix, searchStr.length - suffix.length) !== -1; }, _functionReverse: function(resolvedArgs) { var typeName = this._getTypeName(resolvedArgs[0]); if (typeName === TYPE_STRING) { var originalStr = resolvedArgs[0]; var reversedStr = ""; for (var i = originalStr.length - 1; i >= 0; i--) { reversedStr += originalStr[i]; } return reversedStr; } else { var reversedArray = resolvedArgs[0].slice(0); reversedArray.reverse(); return reversedArray; } }, _functionAbs: function(resolvedArgs) { return Math.abs(resolvedArgs[0]); }, _functionCeil: function(resolvedArgs) { return Math.ceil(resolvedArgs[0]); }, _functionAvg: function(resolvedArgs) { var sum = 0; var inputArray = resolvedArgs[0]; for (var i = 0; i < inputArray.length; i++) { sum += inputArray[i]; } return sum / inputArray.length; }, _functionContains: function(resolvedArgs) { return resolvedArgs[0].indexOf(resolvedArgs[1]) >= 0; }, _functionFloor: function(resolvedArgs) { return Math.floor(resolvedArgs[0]); }, _functionLength: function(resolvedArgs) { if (!isObject(resolvedArgs[0])) { return resolvedArgs[0].length; } else { // As far as I can tell, there's no way to get the length // of an object without O(n) iteration through the object. return Object.keys(resolvedArgs[0]).length; } }, _functionMap: function(resolvedArgs) { var mapped = []; var interpreter = this._interpreter; var exprefNode = resolvedArgs[0]; var elements = resolvedArgs[1]; for (var i = 0; i < elements.length; i++) { mapped.push(interpreter.visit(exprefNode, elements[i])); } return mapped; }, _functionMerge: function(resolvedArgs) { var merged = {}; for (var i = 0; i < resolvedArgs.length; i++) { var current = resolvedArgs[i]; for (var key in current) { merged[key] = current[key]; } } return merged; }, _functionMax: function(resolvedArgs) { if (resolvedArgs[0].length > 0) { var typeName = this._getTypeName(resolvedArgs[0][0]); if (typeName === TYPE_NUMBER) { return Math.max.apply(Math, resolvedArgs[0]); } else { var elements = resolvedArgs[0]; var maxElement = elements[0]; for (var i = 1; i < elements.length; i++) { if (maxElement.localeCompare(elements[i]) < 0) { maxElement = elements[i]; } } return maxElement; } } else { return null; } }, _functionMin: function(resolvedArgs) { if (resolvedArgs[0].length > 0) { var typeName = this._getTypeName(resolvedArgs[0][0]); if (typeName === TYPE_NUMBER) { return Math.min.apply(Math, resolvedArgs[0]); } else { var elements = resolvedArgs[0]; var minElement = elements[0]; for (var i = 1; i < elements.length; i++) { if (elements[i].localeCompare(minElement) < 0) { minElement = elements[i]; } } return minElement; } } else { return null; } }, _functionSum: function(resolvedArgs) { var sum = 0; var listToSum = resolvedArgs[0]; for (var i = 0; i < listToSum.length; i++) { sum += listToSum[i]; } return sum; }, _functionType: function(resolvedArgs) { switch (this._getTypeName(resolvedArgs[0])) { case TYPE_NUMBER: return "number"; case TYPE_STRING: return "string"; case TYPE_ARRAY: return "array"; case TYPE_OBJECT: return "object"; case TYPE_BOOLEAN: return "boolean"; case TYPE_EXPREF: return "expref"; case TYPE_NULL: return "null"; } }, _functionKeys: function(resolvedArgs) { return Object.keys(resolvedArgs[0]); }, _functionValues: function(resolvedArgs) { var obj = resolvedArgs[0]; var keys = Object.keys(obj); var values = []; for (var i = 0; i < keys.length; i++) { values.push(obj[keys[i]]); } return values; }, _functionJoin: function(resolvedArgs) { var joinChar = resolvedArgs[0]; var listJoin = resolvedArgs[1]; return listJoin.join(joinChar); }, _functionToArray: function(resolvedArgs) { if (this._getTypeName(resolvedArgs[0]) === TYPE_ARRAY) { return resolvedArgs[0]; } else { return [resolvedArgs[0]]; } }, _functionToString: function(resolvedArgs) { if (this._getTypeName(resolvedArgs[0]) === TYPE_STRING) { return resolvedArgs[0]; } else { return JSON.stringify(resolvedArgs[0]); } }, _functionToNumber: function(resolvedArgs) { var typeName = this._getTypeName(resolvedArgs[0]); var convertedValue; if (typeName === TYPE_NUMBER) { return resolvedArgs[0]; } else if (typeName === TYPE_STRING) { convertedValue = +resolvedArgs[0]; if (!isNaN(convertedValue)) { return convertedValue; } } return null; }, _functionNotNull: function(resolvedArgs) { for (var i = 0; i < resolvedArgs.length; i++) { if (this._getTypeName(resolvedArgs[i]) !== TYPE_NULL) { return resolvedArgs[i]; } } return null; }, _functionSort: function(resolvedArgs) { var sortedArray = resolvedArgs[0].slice(0); sortedArray.sort(); return sortedArray; }, _functionSortBy: function(resolvedArgs) { var sortedArray = resolvedArgs[0].slice(0); if (sortedArray.length === 0) { return sortedArray; } var interpreter = this._interpreter; var exprefNode = resolvedArgs[1]; var requiredType = this._getTypeName( interpreter.visit(exprefNode, sortedArray[0])); if ([TYPE_NUMBER, TYPE_STRING].indexOf(requiredType) < 0) { throw new Error("TypeError"); } var that = this; // In order to get a stable sort out of an unstable // sort algorithm, we decorate/sort/undecorate (DSU) // by creating a new list of [index, element] pairs. // In the cmp function, if the evaluated elements are // equal, then the index will be used as the tiebreaker. // After the decorated list has been sorted, it will be // undecorated to extract the original elements. var decorated = []; for (var i = 0; i < sortedArray.length; i++) { decorated.push([i, sortedArray[i]]); } decorated.sort(function(a, b) { var exprA = interpreter.visit(exprefNode, a[1]); var exprB = interpreter.visit(exprefNode, b[1]); if (that._getTypeName(exprA) !== requiredType) { throw new Error( "TypeError: expected " + requiredType + ", received " + that._getTypeName(exprA)); } else if (that._getTypeName(exprB) !== requiredType) { throw new Error( "TypeError: expected " + requiredType + ", received " + that._getTypeName(exprB)); } if (exprA > exprB) { return 1; } else if (exprA < exprB) { return -1; } else { // If they're equal compare the items by their // order to maintain relative order of equal keys // (i.e. to get a stable sort). return a[0] - b[0]; } }); // Undecorate: extract out the original list elements. for (var j = 0; j < decorated.length; j++) { sortedArray[j] = decorated[j][1]; } return sortedArray; }, _functionMaxBy: function(resolvedArgs) { var exprefNode = resolvedArgs[1]; var resolvedArray = resolvedArgs[0]; var keyFunction = this.createKeyFunction(exprefNode, [TYPE_NUMBER, TYPE_STRING]); var maxNumber = -Infinity; var maxRecord; var current; for (var i = 0; i < resolvedArray.length; i++) { current = keyFunction(resolvedArray[i]); if (current > maxNumber) { maxNumber = current; maxRecord = resolvedArray[i]; } } return maxRecord; }, _functionMinBy: function(resolvedArgs) { var exprefNode = resolvedArgs[1]; var resolvedArray = resolvedArgs[0]; var keyFunction = this.createKeyFunction(exprefNode, [TYPE_NUMBER, TYPE_STRING]); var minNumber = Infinity; var minRecord; var current; for (var i = 0; i < resolvedArray.length; i++) { current = keyFunction(resolvedArray[i]); if (current < minNumber) { minNumber = current; minRecord = resolvedArray[i]; } } return minRecord; }, createKeyFunction: function(exprefNode, allowedTypes) { var that = this; var interpreter = this._interpreter; var keyFunc = function(x) { var current = interpreter.visit(exprefNode, x); if (allowedTypes.indexOf(that._getTypeName(current)) < 0) { var msg = "TypeError: expected one of " + allowedTypes + ", received " + that._getTypeName(current); throw new Error(msg); } return current; }; return keyFunc; } }; function compile(stream) { var parser = new Parser(); var ast = parser.parse(stream); return ast; } function tokenize(stream) { var lexer = new Lexer(); return lexer.tokenize(stream); } function search(data, expression) { var parser = new Parser(); // This needs to be improved. Both the interpreter and runtime depend on // each other. The runtime needs the interpreter to support exprefs. // There's likely a clean way to avoid the cyclic dependency. var runtime = new Runtime(); var interpreter = new TreeInterpreter(runtime); runtime._interpreter = interpreter; var node = parser.parse(expression); return interpreter.search(node, data); } exports.tokenize = tokenize; exports.compile = compile; exports.search = search; exports.strictDeepEqual = strictDeepEqual; })(typeof exports === "undefined" ? this.jmespath = {} : exports);