UNPKG

@metrichor/jmespath

Version:

Typescript implementation of the JMESPath spec (100% compliant)

1,340 lines (1,337 loc) 62.6 kB
const isObject = (obj) => { return obj !== null && Object.prototype.toString.call(obj) === '[object Object]'; }; const strictDeepEqual = (first, second) => { if (first === second) { return true; } if (typeof first !== typeof second) { return false; } if (Array.isArray(first) && Array.isArray(second)) { if (first.length !== second.length) { return false; } for (let i = 0; i < first.length; i += 1) { if (!strictDeepEqual(first[i], second[i])) { return false; } } return true; } if (isObject(first) && isObject(second)) { const firstEntries = Object.entries(first); const secondKeys = new Set(Object.keys(second)); if (firstEntries.length !== secondKeys.size) { return false; } for (const [key, value] of firstEntries) { if (!strictDeepEqual(value, second[key])) { return false; } secondKeys.delete(key); } return secondKeys.size === 0; } return false; }; const isFalse = (obj) => { if (obj === '' || obj === false || obj === null || obj === undefined) { return true; } if (Array.isArray(obj) && obj.length === 0) { return true; } if (isObject(obj)) { for (const key in obj) { if (obj.hasOwnProperty(key)) { return false; } } return true; } return false; }; const isAlpha = (ch) => { // tslint:disable-next-line: strict-comparisons return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_'; }; const isNum = (ch) => { // tslint:disable-next-line: strict-comparisons return (ch >= '0' && ch <= '9') || ch === '-'; }; const isAlphaNum = (ch) => { // tslint:disable-next-line: strict-comparisons return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch === '_'; }; var Token; (function (Token) { Token["TOK_EOF"] = "EOF"; Token["TOK_UNQUOTEDIDENTIFIER"] = "UnquotedIdentifier"; Token["TOK_QUOTEDIDENTIFIER"] = "QuotedIdentifier"; Token["TOK_RBRACKET"] = "Rbracket"; Token["TOK_RPAREN"] = "Rparen"; Token["TOK_COMMA"] = "Comma"; Token["TOK_COLON"] = "Colon"; Token["TOK_RBRACE"] = "Rbrace"; Token["TOK_NUMBER"] = "Number"; Token["TOK_CURRENT"] = "Current"; Token["TOK_ROOT"] = "Root"; Token["TOK_EXPREF"] = "Expref"; Token["TOK_PIPE"] = "Pipe"; Token["TOK_OR"] = "Or"; Token["TOK_AND"] = "And"; Token["TOK_EQ"] = "EQ"; Token["TOK_GT"] = "GT"; Token["TOK_LT"] = "LT"; Token["TOK_GTE"] = "GTE"; Token["TOK_LTE"] = "LTE"; Token["TOK_NE"] = "NE"; Token["TOK_FLATTEN"] = "Flatten"; Token["TOK_STAR"] = "Star"; Token["TOK_FILTER"] = "Filter"; Token["TOK_DOT"] = "Dot"; Token["TOK_NOT"] = "Not"; Token["TOK_LBRACE"] = "Lbrace"; Token["TOK_LBRACKET"] = "Lbracket"; Token["TOK_LPAREN"] = "Lparen"; Token["TOK_LITERAL"] = "Literal"; })(Token || (Token = {})); const basicTokens = { '(': Token.TOK_LPAREN, ')': Token.TOK_RPAREN, '*': Token.TOK_STAR, ',': Token.TOK_COMMA, '.': Token.TOK_DOT, ':': Token.TOK_COLON, '@': Token.TOK_CURRENT, ['$']: Token.TOK_ROOT, ']': Token.TOK_RBRACKET, '{': Token.TOK_LBRACE, '}': Token.TOK_RBRACE, }; const operatorStartToken = { '!': true, '<': true, '=': true, '>': true, }; const skipChars = { '\t': true, '\n': true, '\r': true, ' ': true, }; class StreamLexer { constructor() { this._current = 0; } tokenize(stream) { const tokens = []; this._current = 0; let start; let identifier; let token; while (this._current < stream.length) { if (isAlpha(stream[this._current])) { start = this._current; identifier = this.consumeUnquotedIdentifier(stream); tokens.push({ start, type: Token.TOK_UNQUOTEDIDENTIFIER, value: identifier, }); } else if (basicTokens[stream[this._current]] !== undefined) { tokens.push({ start: this._current, type: basicTokens[stream[this._current]], value: stream[this._current], }); this._current += 1; } else if (isNum(stream[this._current])) { token = this.consumeNumber(stream); tokens.push(token); } else if (stream[this._current] === '[') { token = this.consumeLBracket(stream); tokens.push(token); } else if (stream[this._current] === '"') { start = this._current; identifier = this.consumeQuotedIdentifier(stream); tokens.push({ start, type: Token.TOK_QUOTEDIDENTIFIER, value: identifier, }); } else if (stream[this._current] === `'`) { start = this._current; identifier = this.consumeRawStringLiteral(stream); tokens.push({ start, type: Token.TOK_LITERAL, value: identifier, }); } else if (stream[this._current] === '`') { start = this._current; const literal = this.consumeLiteral(stream); tokens.push({ start, type: Token.TOK_LITERAL, value: literal, }); } else if (operatorStartToken[stream[this._current]] !== undefined) { token = this.consumeOperator(stream); token && tokens.push(token); } else if (skipChars[stream[this._current]] !== undefined) { this._current += 1; } else if (stream[this._current] === '&') { start = this._current; this._current += 1; if (stream[this._current] === '&') { this._current += 1; tokens.push({ start, type: Token.TOK_AND, value: '&&' }); } else { tokens.push({ start, type: Token.TOK_EXPREF, value: '&' }); } } else if (stream[this._current] === '|') { start = this._current; this._current += 1; if (stream[this._current] === '|') { this._current += 1; tokens.push({ start, type: Token.TOK_OR, value: '||' }); } else { tokens.push({ start, type: Token.TOK_PIPE, value: '|' }); } } else { const error = new Error(`Unknown character: ${stream[this._current]}`); error.name = 'LexerError'; throw error; } } return tokens; } consumeUnquotedIdentifier(stream) { const start = this._current; this._current += 1; while (this._current < stream.length && isAlphaNum(stream[this._current])) { this._current += 1; } return stream.slice(start, this._current); } consumeQuotedIdentifier(stream) { const start = this._current; this._current += 1; const maxLength = stream.length; while (stream[this._current] !== '"' && this._current < maxLength) { let current = this._current; if (stream[current] === '\\' && (stream[current + 1] === '\\' || stream[current + 1] === '"')) { current += 2; } else { current += 1; } this._current = current; } this._current += 1; return JSON.parse(stream.slice(start, this._current)); } consumeRawStringLiteral(stream) { const start = this._current; this._current += 1; const maxLength = stream.length; while (stream[this._current] !== `'` && this._current < maxLength) { let current = this._current; if (stream[current] === '\\' && (stream[current + 1] === '\\' || stream[current + 1] === `'`)) { current += 2; } else { current += 1; } this._current = current; } this._current += 1; const literal = stream.slice(start + 1, this._current - 1); return literal.replace(`\\'`, `'`); } consumeNumber(stream) { const start = this._current; this._current += 1; const maxLength = stream.length; while (isNum(stream[this._current]) && this._current < maxLength) { this._current += 1; } const value = parseInt(stream.slice(start, this._current), 10); return { start, value, type: Token.TOK_NUMBER }; } consumeLBracket(stream) { const start = this._current; this._current += 1; if (stream[this._current] === '?') { this._current += 1; return { start, type: Token.TOK_FILTER, value: '[?' }; } if (stream[this._current] === ']') { this._current += 1; return { start, type: Token.TOK_FLATTEN, value: '[]' }; } return { start, type: Token.TOK_LBRACKET, value: '[' }; } consumeOperator(stream) { const start = this._current; const startingChar = stream[start]; this._current += 1; if (startingChar === '!') { if (stream[this._current] === '=') { this._current += 1; return { start, type: Token.TOK_NE, value: '!=' }; } return { start, type: Token.TOK_NOT, value: '!' }; } if (startingChar === '<') { if (stream[this._current] === '=') { this._current += 1; return { start, type: Token.TOK_LTE, value: '<=' }; } return { start, type: Token.TOK_LT, value: '<' }; } if (startingChar === '>') { if (stream[this._current] === '=') { this._current += 1; return { start, type: Token.TOK_GTE, value: '>=' }; } return { start, type: Token.TOK_GT, value: '>' }; } if (startingChar === '=' && stream[this._current] === '=') { this._current += 1; return { start, type: Token.TOK_EQ, value: '==' }; } } consumeLiteral(stream) { this._current += 1; const start = this._current; const maxLength = stream.length; while (stream[this._current] !== '`' && this._current < maxLength) { let current = this._current; if (stream[current] === '\\' && (stream[current + 1] === '\\' || stream[current + 1] === '`')) { current += 2; } else { current += 1; } this._current = current; } let literalString = stream.slice(start, this._current).trimLeft(); literalString = literalString.replace('\\`', '`'); const literal = this.looksLikeJSON(literalString) ? JSON.parse(literalString) : JSON.parse(`"${literalString}"`); this._current += 1; return literal; } looksLikeJSON(literalString) { const startingChars = '[{"'; const jsonLiterals = ['true', 'false', 'null']; const numberLooking = '-0123456789'; if (literalString === '') { return false; } if (startingChars.includes(literalString[0])) { return true; } if (jsonLiterals.includes(literalString)) { return true; } if (numberLooking.includes(literalString[0])) { try { JSON.parse(literalString); return true; } catch (ex) { return false; } } return false; } } const Lexer = new StreamLexer(); const bindingPower = { [Token.TOK_EOF]: 0, [Token.TOK_UNQUOTEDIDENTIFIER]: 0, [Token.TOK_QUOTEDIDENTIFIER]: 0, [Token.TOK_RBRACKET]: 0, [Token.TOK_RPAREN]: 0, [Token.TOK_COMMA]: 0, [Token.TOK_RBRACE]: 0, [Token.TOK_NUMBER]: 0, [Token.TOK_CURRENT]: 0, [Token.TOK_EXPREF]: 0, [Token.TOK_ROOT]: 0, [Token.TOK_PIPE]: 1, [Token.TOK_OR]: 2, [Token.TOK_AND]: 3, [Token.TOK_EQ]: 5, [Token.TOK_GT]: 5, [Token.TOK_LT]: 5, [Token.TOK_GTE]: 5, [Token.TOK_LTE]: 5, [Token.TOK_NE]: 5, [Token.TOK_FLATTEN]: 9, [Token.TOK_STAR]: 20, [Token.TOK_FILTER]: 21, [Token.TOK_DOT]: 40, [Token.TOK_NOT]: 45, [Token.TOK_LBRACE]: 50, [Token.TOK_LBRACKET]: 55, [Token.TOK_LPAREN]: 60, }; class TokenParser { constructor() { this.index = 0; this.tokens = []; } parse(expression) { this.loadTokens(expression); this.index = 0; const ast = this.expression(0); if (this.lookahead(0) !== Token.TOK_EOF) { const token = this.lookaheadToken(0); this.errorToken(token, `Unexpected token type: ${token.type}, value: ${token.value}`); } return ast; } loadTokens(expression) { this.tokens = [...Lexer.tokenize(expression), { type: Token.TOK_EOF, value: '', start: expression.length }]; } expression(rbp) { const leftToken = this.lookaheadToken(0); this.advance(); let left = this.nud(leftToken); let currentTokenType = this.lookahead(0); while (rbp < bindingPower[currentTokenType]) { this.advance(); left = this.led(currentTokenType, left); currentTokenType = this.lookahead(0); } return left; } lookahead(offset) { return this.tokens[this.index + offset].type; } lookaheadToken(offset) { return this.tokens[this.index + offset]; } advance() { this.index += 1; } nud(token) { let left; let right; let expression; switch (token.type) { case Token.TOK_LITERAL: return { type: 'Literal', value: token.value }; case Token.TOK_UNQUOTEDIDENTIFIER: return { type: 'Field', name: token.value }; case Token.TOK_QUOTEDIDENTIFIER: const node = { type: 'Field', name: token.value }; if (this.lookahead(0) === Token.TOK_LPAREN) { throw new Error('Quoted identifier not allowed for function names.'); } else { return node; } case Token.TOK_NOT: right = this.expression(bindingPower.Not); return { type: 'NotExpression', children: [right] }; case Token.TOK_STAR: left = { type: 'Identity' }; right = (this.lookahead(0) === Token.TOK_RBRACKET && { type: 'Identity' }) || this.parseProjectionRHS(bindingPower.Star); return { type: 'ValueProjection', children: [left, right] }; case Token.TOK_FILTER: return this.led(token.type, { type: 'Identity' }); case Token.TOK_LBRACE: return this.parseMultiselectHash(); case Token.TOK_FLATTEN: left = { type: Token.TOK_FLATTEN, children: [{ type: 'Identity' }] }; right = this.parseProjectionRHS(bindingPower.Flatten); return { type: 'Projection', children: [left, right] }; case Token.TOK_LBRACKET: if (this.lookahead(0) === Token.TOK_NUMBER || this.lookahead(0) === Token.TOK_COLON) { right = this.parseIndexExpression(); return this.projectIfSlice({ type: 'Identity' }, right); } if (this.lookahead(0) === Token.TOK_STAR && this.lookahead(1) === Token.TOK_RBRACKET) { this.advance(); this.advance(); right = this.parseProjectionRHS(bindingPower.Star); return { children: [{ type: 'Identity' }, right], type: 'Projection', }; } return this.parseMultiselectList(); case Token.TOK_CURRENT: return { type: Token.TOK_CURRENT }; case Token.TOK_ROOT: return { type: Token.TOK_ROOT }; case Token.TOK_EXPREF: expression = this.expression(bindingPower.Expref); return { type: 'ExpressionReference', children: [expression] }; case Token.TOK_LPAREN: const args = []; while (this.lookahead(0) !== Token.TOK_RPAREN) { if (this.lookahead(0) === Token.TOK_CURRENT) { expression = { type: Token.TOK_CURRENT }; this.advance(); } else { expression = this.expression(0); } args.push(expression); } this.match(Token.TOK_RPAREN); return args[0]; default: this.errorToken(token); } } led(tokenName, left) { let right; switch (tokenName) { case Token.TOK_DOT: const rbp = bindingPower.Dot; if (this.lookahead(0) !== Token.TOK_STAR) { right = this.parseDotRHS(rbp); return { type: 'Subexpression', children: [left, right] }; } this.advance(); right = this.parseProjectionRHS(rbp); return { type: 'ValueProjection', children: [left, right] }; case Token.TOK_PIPE: right = this.expression(bindingPower.Pipe); return { type: Token.TOK_PIPE, children: [left, right] }; case Token.TOK_OR: right = this.expression(bindingPower.Or); return { type: 'OrExpression', children: [left, right] }; case Token.TOK_AND: right = this.expression(bindingPower.And); return { type: 'AndExpression', children: [left, right] }; case Token.TOK_LPAREN: const name = left.name; const args = []; let expression; while (this.lookahead(0) !== Token.TOK_RPAREN) { if (this.lookahead(0) === Token.TOK_CURRENT) { expression = { type: Token.TOK_CURRENT }; this.advance(); } else { expression = this.expression(0); } if (this.lookahead(0) === Token.TOK_COMMA) { this.match(Token.TOK_COMMA); } args.push(expression); } this.match(Token.TOK_RPAREN); const node = { name, type: 'Function', children: args }; return node; case Token.TOK_FILTER: const condition = this.expression(0); this.match(Token.TOK_RBRACKET); right = (this.lookahead(0) === Token.TOK_FLATTEN && { type: 'Identity' }) || this.parseProjectionRHS(bindingPower.Filter); return { type: 'FilterProjection', children: [left, right, condition] }; case Token.TOK_FLATTEN: const leftNode = { type: Token.TOK_FLATTEN, children: [left] }; const rightNode = this.parseProjectionRHS(bindingPower.Flatten); return { type: 'Projection', children: [leftNode, rightNode] }; case Token.TOK_EQ: case Token.TOK_NE: case Token.TOK_GT: case Token.TOK_GTE: case Token.TOK_LT: case Token.TOK_LTE: return this.parseComparator(left, tokenName); case Token.TOK_LBRACKET: const token = this.lookaheadToken(0); if (token.type === Token.TOK_NUMBER || token.type === Token.TOK_COLON) { right = this.parseIndexExpression(); return this.projectIfSlice(left, right); } this.match(Token.TOK_STAR); this.match(Token.TOK_RBRACKET); right = this.parseProjectionRHS(bindingPower.Star); return { type: 'Projection', children: [left, right] }; default: return this.errorToken(this.lookaheadToken(0)); } } match(tokenType) { if (this.lookahead(0) === tokenType) { this.advance(); return; } else { const token = this.lookaheadToken(0); this.errorToken(token, `Expected ${tokenType}, got: ${token.type}`); } } errorToken(token, message = '') { const error = new Error(message || `Invalid token (${token.type}): "${token.value}"`); error.name = 'ParserError'; throw error; } parseIndexExpression() { if (this.lookahead(0) === Token.TOK_COLON || this.lookahead(1) === Token.TOK_COLON) { return this.parseSliceExpression(); } const node = { type: 'Index', value: this.lookaheadToken(0).value, }; this.advance(); this.match(Token.TOK_RBRACKET); return node; } projectIfSlice(left, right) { const indexExpr = { type: 'IndexExpression', children: [left, right] }; if (right.type === 'Slice') { return { children: [indexExpr, this.parseProjectionRHS(bindingPower.Star)], type: 'Projection', }; } return indexExpr; } parseSliceExpression() { const parts = [null, null, null]; let index = 0; let currentTokenType = this.lookahead(0); while (currentTokenType !== Token.TOK_RBRACKET && index < 3) { if (currentTokenType === Token.TOK_COLON) { index += 1; this.advance(); } else if (currentTokenType === Token.TOK_NUMBER) { parts[index] = this.lookaheadToken(0).value; this.advance(); } else { const token = this.lookaheadToken(0); this.errorToken(token, `Syntax error, unexpected token: ${token.value}(${token.type})`); } currentTokenType = this.lookahead(0); } this.match(Token.TOK_RBRACKET); return { children: parts, type: 'Slice', }; } parseComparator(left, comparator) { const right = this.expression(bindingPower[comparator]); return { type: 'Comparator', name: comparator, children: [left, right] }; } parseDotRHS(rbp) { const lookahead = this.lookahead(0); const exprTokens = [Token.TOK_UNQUOTEDIDENTIFIER, Token.TOK_QUOTEDIDENTIFIER, Token.TOK_STAR]; if (exprTokens.includes(lookahead)) { return this.expression(rbp); } if (lookahead === Token.TOK_LBRACKET) { this.match(Token.TOK_LBRACKET); return this.parseMultiselectList(); } if (lookahead === Token.TOK_LBRACE) { this.match(Token.TOK_LBRACE); return this.parseMultiselectHash(); } const token = this.lookaheadToken(0); this.errorToken(token, `Syntax error, unexpected token: ${token.value}(${token.type})`); } parseProjectionRHS(rbp) { if (bindingPower[this.lookahead(0)] < 10) { return { type: 'Identity' }; } if (this.lookahead(0) === Token.TOK_LBRACKET) { return this.expression(rbp); } if (this.lookahead(0) === Token.TOK_FILTER) { return this.expression(rbp); } if (this.lookahead(0) === Token.TOK_DOT) { this.match(Token.TOK_DOT); return this.parseDotRHS(rbp); } const token = this.lookaheadToken(0); this.errorToken(token, `Syntax error, unexpected token: ${token.value}(${token.type})`); } parseMultiselectList() { const expressions = []; while (this.lookahead(0) !== Token.TOK_RBRACKET) { const expression = this.expression(0); expressions.push(expression); if (this.lookahead(0) === Token.TOK_COMMA) { this.match(Token.TOK_COMMA); if (this.lookahead(0) === Token.TOK_RBRACKET) { throw new Error('Unexpected token Rbracket'); } } } this.match(Token.TOK_RBRACKET); return { type: 'MultiSelectList', children: expressions }; } parseMultiselectHash() { const pairs = []; const identifierTypes = [Token.TOK_UNQUOTEDIDENTIFIER, Token.TOK_QUOTEDIDENTIFIER]; let keyToken; let keyName; let value; // tslint:disable-next-line: prettier for (;;) { keyToken = this.lookaheadToken(0); if (!identifierTypes.includes(keyToken.type)) { throw new Error(`Expecting an identifier token, got: ${keyToken.type}`); } keyName = keyToken.value; this.advance(); this.match(Token.TOK_COLON); value = this.expression(0); pairs.push({ value, type: 'KeyValuePair', name: keyName }); if (this.lookahead(0) === Token.TOK_COMMA) { this.match(Token.TOK_COMMA); } else if (this.lookahead(0) === Token.TOK_RBRACE) { this.match(Token.TOK_RBRACE); break; } } return { type: 'MultiSelectHash', children: pairs }; } } const Parser = new TokenParser(); var InputArgument; (function (InputArgument) { InputArgument[InputArgument["TYPE_NUMBER"] = 0] = "TYPE_NUMBER"; InputArgument[InputArgument["TYPE_ANY"] = 1] = "TYPE_ANY"; InputArgument[InputArgument["TYPE_STRING"] = 2] = "TYPE_STRING"; InputArgument[InputArgument["TYPE_ARRAY"] = 3] = "TYPE_ARRAY"; InputArgument[InputArgument["TYPE_OBJECT"] = 4] = "TYPE_OBJECT"; InputArgument[InputArgument["TYPE_BOOLEAN"] = 5] = "TYPE_BOOLEAN"; InputArgument[InputArgument["TYPE_EXPREF"] = 6] = "TYPE_EXPREF"; InputArgument[InputArgument["TYPE_NULL"] = 7] = "TYPE_NULL"; InputArgument[InputArgument["TYPE_ARRAY_NUMBER"] = 8] = "TYPE_ARRAY_NUMBER"; InputArgument[InputArgument["TYPE_ARRAY_STRING"] = 9] = "TYPE_ARRAY_STRING"; })(InputArgument || (InputArgument = {})); class Runtime { constructor(interpreter) { this.TYPE_NAME_TABLE = { [InputArgument.TYPE_NUMBER]: 'number', [InputArgument.TYPE_ANY]: 'any', [InputArgument.TYPE_STRING]: 'string', [InputArgument.TYPE_ARRAY]: 'array', [InputArgument.TYPE_OBJECT]: 'object', [InputArgument.TYPE_BOOLEAN]: 'boolean', [InputArgument.TYPE_EXPREF]: 'expression', [InputArgument.TYPE_NULL]: 'null', [InputArgument.TYPE_ARRAY_NUMBER]: 'Array<number>', [InputArgument.TYPE_ARRAY_STRING]: 'Array<string>', }; this.functionAbs = ([inputValue]) => { return Math.abs(inputValue); }; this.functionAvg = ([inputArray]) => { let sum = 0; for (let i = 0; i < inputArray.length; i += 1) { sum += inputArray[i]; } return sum / inputArray.length; }; this.functionCeil = ([inputValue]) => { return Math.ceil(inputValue); }; this.functionContains = resolvedArgs => { const [searchable, searchValue] = resolvedArgs; return searchable.includes(searchValue); }; this.functionEndsWith = resolvedArgs => { const [searchStr, suffix] = resolvedArgs; return searchStr.includes(suffix, searchStr.length - suffix.length); }; this.functionFloor = ([inputValue]) => { return Math.floor(inputValue); }; this.functionJoin = resolvedArgs => { const [joinChar, listJoin] = resolvedArgs; return listJoin.join(joinChar); }; this.functionKeys = ([inputObject]) => { return Object.keys(inputObject); }; this.functionLength = ([inputValue]) => { if (!isObject(inputValue)) { return inputValue.length; } return Object.keys(inputValue).length; }; this.functionMap = (resolvedArgs) => { if (!this._interpreter) { return []; } const mapped = []; const interpreter = this._interpreter; const exprefNode = resolvedArgs[0]; const elements = resolvedArgs[1]; for (let i = 0; i < elements.length; i += 1) { mapped.push(interpreter.visit(exprefNode, elements[i])); } return mapped; }; this.functionMax = ([inputValue]) => { if (!inputValue.length) { return null; } const typeName = this.getTypeName(inputValue[0]); if (typeName === InputArgument.TYPE_NUMBER) { return Math.max(...inputValue); } const elements = inputValue; let maxElement = elements[0]; for (let i = 1; i < elements.length; i += 1) { if (maxElement.localeCompare(elements[i]) < 0) { maxElement = elements[i]; } } return maxElement; }; this.functionMaxBy = (resolvedArgs) => { const exprefNode = resolvedArgs[1]; const resolvedArray = resolvedArgs[0]; const keyFunction = this.createKeyFunction(exprefNode, [InputArgument.TYPE_NUMBER, InputArgument.TYPE_STRING]); let maxNumber = -Infinity; let maxRecord; let current; for (let i = 0; i < resolvedArray.length; i += 1) { current = keyFunction && keyFunction(resolvedArray[i]); if (current !== undefined && current > maxNumber) { maxNumber = current; maxRecord = resolvedArray[i]; } } return maxRecord; }; this.functionMerge = resolvedArgs => { let merged = {}; for (let i = 0; i < resolvedArgs.length; i += 1) { const current = resolvedArgs[i]; merged = Object.assign(merged, current); // for (const key in current) { // merged[key] = current[key]; // } } return merged; }; this.functionMin = ([inputValue]) => { if (!inputValue.length) { return null; } const typeName = this.getTypeName(inputValue[0]); if (typeName === InputArgument.TYPE_NUMBER) { return Math.min(...inputValue); } const elements = inputValue; let minElement = elements[0]; for (let i = 1; i < elements.length; i += 1) { if (elements[i].localeCompare(minElement) < 0) { minElement = elements[i]; } } return minElement; }; this.functionMinBy = (resolvedArgs) => { const exprefNode = resolvedArgs[1]; const resolvedArray = resolvedArgs[0]; const keyFunction = this.createKeyFunction(exprefNode, [InputArgument.TYPE_NUMBER, InputArgument.TYPE_STRING]); let minNumber = Infinity; let minRecord; let current; for (let i = 0; i < resolvedArray.length; i += 1) { current = keyFunction && keyFunction(resolvedArray[i]); if (current !== undefined && current < minNumber) { minNumber = current; minRecord = resolvedArray[i]; } } return minRecord; }; this.functionNotNull = (resolvedArgs) => { for (let i = 0; i < resolvedArgs.length; i += 1) { if (this.getTypeName(resolvedArgs[i]) !== InputArgument.TYPE_NULL) { return resolvedArgs[i]; } } return null; }; this.functionReverse = ([inputValue]) => { const typeName = this.getTypeName(inputValue); if (typeName === InputArgument.TYPE_STRING) { const originalStr = inputValue; let reversedStr = ''; for (let i = originalStr.length - 1; i >= 0; i -= 1) { reversedStr += originalStr[i]; } return reversedStr; } const reversedArray = inputValue.slice(0); reversedArray.reverse(); return reversedArray; }; this.functionSort = ([inputValue]) => { return [...inputValue].sort(); }; this.functionSortBy = (resolvedArgs) => { if (!this._interpreter) { return []; } const sortedArray = resolvedArgs[0].slice(0); if (sortedArray.length === 0) { return sortedArray; } const interpreter = this._interpreter; const exprefNode = resolvedArgs[1]; const requiredType = this.getTypeName(interpreter.visit(exprefNode, sortedArray[0])); if (requiredType !== undefined && ![InputArgument.TYPE_NUMBER, InputArgument.TYPE_STRING].includes(requiredType)) { throw new Error(`TypeError: unexpected type (${this.TYPE_NAME_TABLE[requiredType]})`); } const decorated = []; for (let i = 0; i < sortedArray.length; i += 1) { decorated.push([i, sortedArray[i]]); } decorated.sort((a, b) => { const exprA = interpreter.visit(exprefNode, a[1]); const exprB = interpreter.visit(exprefNode, b[1]); if (this.getTypeName(exprA) !== requiredType) { throw new Error(`TypeError: expected (${this.TYPE_NAME_TABLE[requiredType]}), received ${this.TYPE_NAME_TABLE[this.getTypeName(exprA)]}`); } else if (this.getTypeName(exprB) !== requiredType) { throw new Error(`TypeError: expected (${this.TYPE_NAME_TABLE[requiredType]}), received ${this.TYPE_NAME_TABLE[this.getTypeName(exprB)]}`); } if (exprA > exprB) { return 1; } return exprA < exprB ? -1 : a[0] - b[0]; }); for (let j = 0; j < decorated.length; j += 1) { sortedArray[j] = decorated[j][1]; } return sortedArray; }; this.functionStartsWith = ([searchable, searchStr]) => { return searchable.startsWith(searchStr); }; this.functionSum = ([inputValue]) => { return inputValue.reduce((x, y) => x + y, 0); }; this.functionToArray = ([inputValue]) => { if (this.getTypeName(inputValue) === InputArgument.TYPE_ARRAY) { return inputValue; } return [inputValue]; }; this.functionToNumber = ([inputValue]) => { const typeName = this.getTypeName(inputValue); let convertedValue; if (typeName === InputArgument.TYPE_NUMBER) { return inputValue; } if (typeName === InputArgument.TYPE_STRING) { convertedValue = +inputValue; if (!isNaN(convertedValue)) { return convertedValue; } } return null; }; this.functionToString = ([inputValue]) => { if (this.getTypeName(inputValue) === InputArgument.TYPE_STRING) { return inputValue; } return JSON.stringify(inputValue); }; this.functionType = ([inputValue]) => { switch (this.getTypeName(inputValue)) { case InputArgument.TYPE_NUMBER: return 'number'; case InputArgument.TYPE_STRING: return 'string'; case InputArgument.TYPE_ARRAY: return 'array'; case InputArgument.TYPE_OBJECT: return 'object'; case InputArgument.TYPE_BOOLEAN: return 'boolean'; case InputArgument.TYPE_EXPREF: return 'expref'; case InputArgument.TYPE_NULL: return 'null'; default: return; } }; this.functionValues = ([inputObject]) => { return Object.values(inputObject); }; this.functionTable = { abs: { _func: this.functionAbs, _signature: [ { types: [InputArgument.TYPE_NUMBER], }, ], }, avg: { _func: this.functionAvg, _signature: [ { types: [InputArgument.TYPE_ARRAY_NUMBER], }, ], }, ceil: { _func: this.functionCeil, _signature: [ { types: [InputArgument.TYPE_NUMBER], }, ], }, contains: { _func: this.functionContains, _signature: [ { types: [InputArgument.TYPE_STRING, InputArgument.TYPE_ARRAY], }, { types: [InputArgument.TYPE_ANY], }, ], }, ends_with: { _func: this.functionEndsWith, _signature: [ { types: [InputArgument.TYPE_STRING], }, { types: [InputArgument.TYPE_STRING], }, ], }, floor: { _func: this.functionFloor, _signature: [ { types: [InputArgument.TYPE_NUMBER], }, ], }, join: { _func: this.functionJoin, _signature: [ { types: [InputArgument.TYPE_STRING], }, { types: [InputArgument.TYPE_ARRAY_STRING], }, ], }, keys: { _func: this.functionKeys, _signature: [ { types: [InputArgument.TYPE_OBJECT], }, ], }, length: { _func: this.functionLength, _signature: [ { types: [InputArgument.TYPE_STRING, InputArgument.TYPE_ARRAY, InputArgument.TYPE_OBJECT], }, ], }, map: { _func: this.functionMap, _signature: [ { types: [InputArgument.TYPE_EXPREF], }, { types: [InputArgument.TYPE_ARRAY], }, ], }, max: { _func: this.functionMax, _signature: [ { types: [InputArgument.TYPE_ARRAY_NUMBER, InputArgument.TYPE_ARRAY_STRING], }, ], }, max_by: { _func: this.functionMaxBy, _signature: [ { types: [InputArgument.TYPE_ARRAY], }, { types: [InputArgument.TYPE_EXPREF], }, ], }, merge: { _func: this.functionMerge, _signature: [ { types: [InputArgument.TYPE_OBJECT], variadic: true, }, ], }, min: { _func: this.functionMin, _signature: [ { types: [InputArgument.TYPE_ARRAY_NUMBER, InputArgument.TYPE_ARRAY_STRING], }, ], }, min_by: { _func: this.functionMinBy, _signature: [ { types: [InputArgument.TYPE_ARRAY], }, { types: [InputArgument.TYPE_EXPREF], }, ], }, not_null: { _func: this.functionNotNull, _signature: [ { types: [InputArgument.TYPE_ANY], variadic: true, }, ], }, reverse: { _func: this.functionReverse, _signature: [ { types: [InputArgument.TYPE_STRING, InputArgument.TYPE_ARRAY], }, ], }, sort: { _func: this.functionSort, _signature: [ { types: [InputArgument.TYPE_ARRAY_STRING, InputArgument.TYPE_ARRAY_NUMBER], }, ], }, sort_by: { _func: this.functionSortBy, _signature: [ { types: [InputArgument.TYPE_ARRAY], }, { types: [InputArgument.TYPE_EXPREF], }, ], }, starts_with: { _func: this.functionStartsWith, _signature: [ { types: [InputArgument.TYPE_STRING], }, { types: [InputArgument.TYPE_STRING], }, ], }, sum: { _func: this.functionSum, _signature: [ { types: [InputArgument.TYPE_ARRAY_NUMBER], }, ], }, to_array: { _func: this.functionToArray, _signature: [ { types: [InputArgument.TYPE_ANY], }, ], }, to_number: { _func: this.functionToNumber, _signature: [ { types: [InputArgument.TYPE_ANY], }, ], }, to_string: { _func: this.functionToString, _signature: [ { types: [InputArgument.TYPE_ANY], }, ], }, type: { _func: this.functionType, _signature: [ { types: [InputArgument.TYPE_ANY], }, ], }, values: { _func: this.functionValues, _signature: [ { types: [InputArgument.TYPE_OBJECT], }, ], }, }; this._interpreter = interpreter; } registerFunction(name, customFunction, signature) { if (name in this.functionTable) { throw new Error(`Function already defined: ${name}()`); } this.functionTable[name] = { _func: customFunction.bind(this), _signature: signature, }; } callFunction(name, resolvedArgs) { const 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); } validateInputSignatures(name, signature) { for (let i = 0; i < signature.length; i += 1) { if ('variadic' in signature[i] && i !== signature.length - 1) { throw new Error(`ArgumentError: ${name}() 'variadic' argument ${i + 1} must occur last`); } } } validateArgs(name, args, signature) { var _a, _b; let pluralized; this.validateInputSignatures(name, signature); const numberOfRequiredArgs = signature.filter(argSignature => { var _a; return (_a = !argSignature.optional) !== null && _a !== void 0 ? _a : false; }).length; const lastArgIsVariadic = (_b = (_a = signature[signature.length - 1]) === null || _a === void 0 ? void 0 : _a.variadic) !== null && _b !== void 0 ? _b : false; const tooFewArgs = args.length < numberOfRequiredArgs; const tooManyArgs = args.length > signature.length; const tooFewModifier = tooFewArgs && ((!lastArgIsVariadic && numberOfRequiredArgs > 1) || lastArgIsVariadic) ? 'at least ' : ''; if ((lastArgIsVariadic && tooFewArgs) || (!lastArgIsVariadic && (tooFewArgs || tooManyArgs))) { pluralized = signature.length > 1; throw new Error(`ArgumentError: ${name}() takes ${tooFewModifier}${numberOfRequiredArgs} argument${(pluralized && 's') || ''} but received ${args.length}`); } let currentSpec; let actualType; let typeMatched; for (let i = 0; i < signature.length; i += 1) { typeMatched = false; currentSpec = signature[i].types; actualType = this.getTypeName(args[i]); let j; for (j = 0; j < currentSpec.length; j += 1) { if (actualType !== undefined && this.typeMatches(actualType, currentSpec[j], args[i])) { typeMatched = true; break; } } if (!typeMatched && actualType !== undefined) { const expected = currentSpec .map((typeIdentifier) => { return this.TYPE_NAME_TABLE[typeIdentifier]; }) .join(' | '); throw new Error(`TypeError: ${name}() expected argument ${i + 1} to be type (${expected}) but received type ${this.TYPE_NAME_TABLE[actualType]} instead.`); } } } typeMatches(actual, expected, argValue) { if (expected === InputArgument.TYPE_ANY) { return true; } if (expected === InputArgument.TYPE_ARRAY_STRING || expected === InputArgument.TYPE_ARRAY_NUMBER || expected === InputArgument.TYPE_ARRAY) { if (expected === InputArgument.TYPE_ARRAY) { return actual === InputArgument.TYPE_ARRAY; } if (actual === InputArgument.TYPE_ARRAY) { let subtype; if (expected === InputArgument.TYPE_ARRAY_NUMBER) { subtype = InputArgument.TYPE_NUMBER; } else if (expected === InputArgument.TYPE_ARRAY_STRING) { subtype = InputArgument.TYPE_STRING; } for (let i = 0; i < argValue.length; i += 1) { const typeName = this.getTypeName(argValue[i]); if (typeName !== undefined && subtype !== undefined && !this.typeMatches(typeName, subtype, argValue[i])) { return false; } } return true; } } else { return actual === expected; } return false; } getTypeName(obj) { switch (Object.prototype.toString.call(obj)) { case '[object String]': return InputArgument.TYPE_STRING; case '[object Number]': return InputArgument.TYPE_NUMBER; case '[o