@jmespath-community/jmespath
Version:
Typescript implementation of the JMESPath Community specification
1,312 lines (1,306 loc) • 89.6 kB
JavaScript
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 (typeof obj === 'object') {
if (obj === null) {
return true;
}
if (Array.isArray(obj)) {
return obj.length === 0;
}
// eslint-disable-next-line @typescript-eslint/naming-convention
for (const _key in obj) {
return false;
}
return true;
}
return !(typeof obj === 'number' || obj);
};
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 === '_';
};
const ensureInteger = (value) => {
if (!(typeof value === 'number') || Math.floor(value) !== value) {
throw new Error('invalid-value: expecting an integer.');
}
return value;
};
const ensurePositiveInteger = (value) => {
if (!(typeof value === 'number') || value < 0 || Math.floor(value) !== value) {
throw new Error('invalid-value: expecting a non-negative integer.');
}
return value;
};
const ensureNumbers = (...operands) => {
for (let i = 0; i < operands.length; i++) {
if (operands[i] === null || operands[i] === undefined) {
throw new Error('not-a-number: undefined');
}
if (typeof operands[i] !== 'number') {
throw new Error('not-a-number');
}
}
};
const notZero = (n) => {
n = +n; // coerce to number
if (!n) {
// matches -0, +0, NaN
throw new Error('not-a-number: divide by zero');
}
return n;
};
const add = (left, right) => {
ensureNumbers(left, right);
const result = left + right;
return result;
};
const sub = (left, right) => {
ensureNumbers(left, right);
const result = left - right;
return result;
};
const mul = (left, right) => {
ensureNumbers(left, right);
const result = left * right;
return result;
};
const divide = (left, right) => {
ensureNumbers(left, right);
const result = left / notZero(right);
return result;
};
const div = (left, right) => {
ensureNumbers(left, right);
const result = Math.floor(left / notZero(right));
return result;
};
const mod = (left, right) => {
ensureNumbers(left, right);
const result = left % right;
return result;
};
const findFirst = (subject, sub, start, end) => {
if (!subject || !sub) {
return null;
}
start = Math.max(ensureInteger((start = start || 0)), 0);
end = Math.min(ensureInteger((end = end || subject.length)), subject.length);
const offset = subject.slice(start, end).indexOf(sub);
return offset === -1 ? null : offset + start;
};
const findLast = (subject, sub, start, end) => {
if (!subject || !sub) {
return null;
}
start = Math.max(ensureInteger((start = start || 0)), 0);
end = Math.min(ensureInteger((end = end || subject.length)), subject.length);
const offset = subject.slice(start, end).lastIndexOf(sub);
const result = offset === -1 ? null : offset + start;
return result;
};
const lower = (subject) => subject.toLowerCase();
const ensurePadFuncParams = (name, width, padding) => {
padding = padding || ' ';
if (padding.length > 1) {
throw new Error(`invalid value, ${name} expects its 'pad' parameter to be a valid string with a single codepoint`);
}
ensurePositiveInteger(width);
return padding;
};
const padLeft = (subject, width, padding) => {
padding = ensurePadFuncParams('pad_left', width, padding);
return (subject && subject.padStart(width, padding)) || '';
};
const padRight = (subject, width, padding) => {
padding = ensurePadFuncParams('pad_right', width, padding);
return (subject && subject.padEnd(width, padding)) || '';
};
const replace = (subject, string, by, count) => {
if (count === 0) {
return subject;
}
if (!count) {
// emulating es2021: String.prototype.replaceAll()
return subject.split(string).join(by);
}
ensurePositiveInteger(count);
[...Array(count).keys()].map(() => (subject = subject.replace(string, by)));
return subject;
};
const split = (subject, search, count) => {
if (subject.length == 0 && search.length === 0) {
return [];
}
if (count === null || count === undefined) {
return subject.split(search);
}
ensurePositiveInteger(count);
if (count === 0) {
return [subject];
}
const split = subject.split(search);
return [...split.slice(0, count), split.slice(count).join(search)];
};
const trim = (subject, chars) => {
return trimLeft(trimRight(subject, chars), chars);
};
const trimLeft = (subject, chars) => {
return trimImpl(subject, list => new RegExp(`^[${list}]*(.*?)`), chars);
};
const trimRight = (subject, chars) => {
return trimImpl(subject, list => new RegExp(`(.*?)[${list}]*\$`), chars);
};
const trimImpl = (subject, regExper, chars) => {
const pattern = chars ? chars.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') : '\\s\u0085';
return subject.replace(regExper(pattern), '$1');
};
const upper = (subject) => subject.toUpperCase();
var Token;
(function (Token) {
Token["TOK_EOF"] = "EOF";
Token["TOK_VARIABLE"] = "Variable";
Token["TOK_ASSIGN"] = "Assign";
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_PLUS"] = "Plus";
Token["TOK_MINUS"] = "Minus";
Token["TOK_MULTIPLY"] = "Multiply";
Token["TOK_DIVIDE"] = "Divide";
Token["TOK_MODULO"] = "Modulo";
Token["TOK_DIV"] = "Div";
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_RBRACKET,
'{': Token.TOK_LBRACE,
'}': Token.TOK_RBRACE,
'+': Token.TOK_PLUS,
'%': Token.TOK_MODULO,
'\u2212': Token.TOK_MINUS,
'\u00d7': Token.TOK_MULTIPLY,
'\u00f7': Token.TOK_DIVIDE,
};
const operatorStartToken = {
'!': true,
'<': true,
'=': true,
'>': true,
'&': true,
'|': true,
'/': true,
};
const skipChars = {
'\t': true,
'\n': true,
'\r': true,
' ': true,
};
class StreamLexer {
constructor() {
this._current = 0;
this._enable_legacy_literals = false;
}
tokenize(stream, options) {
const tokens = [];
this._current = 0;
this._enable_legacy_literals = (options === null || options === void 0 ? void 0 : options.enable_legacy_literals) || false;
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 (stream[this._current] === '$') {
start = this._current;
if (this._current + 1 < stream.length && isAlpha(stream[this._current + 1])) {
this._current += 1;
identifier = this.consumeUnquotedIdentifier(stream);
tokens.push({
start,
type: Token.TOK_VARIABLE,
value: identifier,
});
}
else {
tokens.push({
start: start,
type: Token.TOK_ROOT,
value: stream[this._current],
});
this._current += 1;
}
}
else if (stream[this._current] === '-') {
if (this._current + 1 < stream.length && isNum(stream[this._current + 1])) {
const token = this.consumeNumber(stream);
token && tokens.push(token);
}
else {
const token = {
start: this._current,
type: Token.TOK_MINUS,
value: '-',
};
tokens.push(token);
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 {
const error = new Error(`Syntax 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;
const [value, ok] = this.parseJSON(stream.slice(start, this._current));
if (!ok) {
const error = new Error(`syntax: unexpected end of JSON input`);
error.name = 'LexerError';
throw error;
}
return value;
}
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 replace(replace(literal, `\\\\`, `\\`), `\\'`, `'`);
}
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: '[' };
}
consumeOrElse(stream, peek, token, orElse) {
const start = this._current;
this._current += 1;
if (this._current < stream.length && stream[this._current] === peek) {
this._current += 1;
return {
start: start,
type: orElse,
value: stream.slice(start, this._current),
};
}
return { start: start, type: token, value: stream[start] };
}
consumeOperator(stream) {
const start = this._current;
const startingChar = stream[start];
switch (startingChar) {
case '!':
return this.consumeOrElse(stream, '=', Token.TOK_NOT, Token.TOK_NE);
case '<':
return this.consumeOrElse(stream, '=', Token.TOK_LT, Token.TOK_LTE);
case '>':
return this.consumeOrElse(stream, '=', Token.TOK_GT, Token.TOK_GTE);
case '=':
return this.consumeOrElse(stream, '=', Token.TOK_ASSIGN, Token.TOK_EQ);
case '&':
return this.consumeOrElse(stream, '&', Token.TOK_EXPREF, Token.TOK_AND);
case '|':
return this.consumeOrElse(stream, '|', Token.TOK_PIPE, Token.TOK_OR);
case '/':
return this.consumeOrElse(stream, '/', Token.TOK_DIVIDE, Token.TOK_DIV);
}
}
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).trimStart();
literalString = literalString.replace('\\`', '`');
let literal = null;
let ok = false;
// attempts to detect and parse valid JSON
if (this.looksLikeJSON(literalString)) {
[literal, ok] = this.parseJSON(literalString);
}
// invalid JSON values should be converted to quoted
// JSON strings during the JEP-12 deprecation period.
if (!ok && this._enable_legacy_literals) {
[literal, ok] = this.parseJSON(`"${literalString}"`);
}
if (!ok) {
const error = new Error(`Syntax error: unexpected end of JSON input or invalid format for a JSON literal: ${stream[this._current]}`);
error.name = 'LexerError';
throw error;
}
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])) {
// eslint-disable-next-line @typescript-eslint/naming-convention
const [_, ok] = this.parseJSON(literalString);
return ok;
}
return false;
}
parseJSON(text) {
try {
const json = JSON.parse(text);
return [json, true];
}
catch (_a) {
return [null, false];
}
}
}
const Lexer = new StreamLexer();
const bindingPower = {
[Token.TOK_EOF]: 0,
[Token.TOK_VARIABLE]: 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_ASSIGN]: 1,
[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_MINUS]: 6,
[Token.TOK_PLUS]: 6,
[Token.TOK_DIV]: 7,
[Token.TOK_DIVIDE]: 7,
[Token.TOK_MODULO]: 7,
[Token.TOK_MULTIPLY]: 7,
[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, options) {
this.loadTokens(expression, options || { enable_legacy_literals: false });
this.index = 0;
const ast = this.expression(0);
if (this.lookahead(0) !== Token.TOK_EOF) {
const token = this.lookaheadToken(0);
this.errorToken(token, `Syntax error: unexpected token type: ${token.type}, value: ${token.value}`);
}
return ast;
}
loadTokens(expression, options) {
this.tokens = [
...Lexer.tokenize(expression, options),
{ 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) {
switch (token.type) {
case Token.TOK_VARIABLE:
return { type: 'Variable', name: token.value };
case Token.TOK_LITERAL:
return { type: 'Literal', value: token.value };
case Token.TOK_UNQUOTEDIDENTIFIER: {
if (TokenParser.isKeyword(token, 'let') && this.lookahead(0) === Token.TOK_VARIABLE) {
return this.parseLetExpression();
}
else {
return { type: 'Field', name: token.value };
}
}
case Token.TOK_QUOTEDIDENTIFIER:
if (this.lookahead(0) === Token.TOK_LPAREN) {
throw new Error('Syntax error: quoted identifier not allowed for function names.');
}
else {
return { type: 'Field', name: token.value };
}
case Token.TOK_NOT: {
const child = this.expression(bindingPower.Not);
return { type: 'NotExpression', child };
}
case Token.TOK_MINUS: {
const child = this.expression(bindingPower.Minus);
return {
type: 'Unary',
operator: token.type,
operand: child,
};
}
case Token.TOK_PLUS: {
const child = this.expression(bindingPower.Plus);
return {
type: 'Unary',
operator: token.type,
operand: child,
};
}
case Token.TOK_STAR: {
const left = { type: 'Identity' };
const right = this.lookahead(0) === Token.TOK_RBRACKET ? left : this.parseProjectionRHS(bindingPower.Star);
return { type: 'ValueProjection', left, right };
}
case Token.TOK_FILTER:
return this.led(token.type, { type: 'Identity' });
case Token.TOK_LBRACE:
return this.parseMultiselectHash();
case Token.TOK_FLATTEN: {
const left = {
type: 'Flatten',
child: { type: 'Identity' },
};
const right = this.parseProjectionRHS(bindingPower.Flatten);
return { type: 'Projection', left, right };
}
case Token.TOK_LBRACKET: {
if (this.lookahead(0) === Token.TOK_NUMBER || this.lookahead(0) === Token.TOK_COLON) {
const 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();
const right = this.parseProjectionRHS(bindingPower.Star);
return {
left: { 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: {
const child = this.expression(bindingPower.Expref);
return { type: 'ExpressionReference', child };
}
case Token.TOK_LPAREN: {
const args = [];
let expression = this.expression(0);
args.push(expression);
this.match(Token.TOK_RPAREN);
return args[0];
}
default:
this.errorToken(token);
}
}
led(tokenName, left) {
switch (tokenName) {
case Token.TOK_DOT: {
const rbp = bindingPower.Dot;
if (this.lookahead(0) !== Token.TOK_STAR) {
const right = this.parseDotRHS(rbp);
return { type: 'Subexpression', left, right };
}
this.advance();
const right = this.parseProjectionRHS(rbp);
return { type: 'ValueProjection', left, right };
}
case Token.TOK_PIPE: {
const right = this.expression(bindingPower.Pipe);
return { type: 'Pipe', left, right };
}
case Token.TOK_OR: {
const right = this.expression(bindingPower.Or);
return { type: 'OrExpression', left, right };
}
case Token.TOK_AND: {
const right = this.expression(bindingPower.And);
return { type: 'AndExpression', left, right };
}
case Token.TOK_LPAREN: {
if (left.type !== 'Field') {
throw new Error('Syntax error: expected a Field node');
}
const name = left.name;
const args = this.parseCommaSeparatedExpressionsUntilToken(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);
const right = this.lookahead(0) === Token.TOK_FLATTEN ? { type: 'Identity' } : this.parseProjectionRHS(bindingPower.Filter);
return { type: 'FilterProjection', left, right, condition };
}
case Token.TOK_FLATTEN: {
const leftNode = { type: 'Flatten', child: left };
const right = this.parseProjectionRHS(bindingPower.Flatten);
return { type: 'Projection', left: leftNode, right };
}
case Token.TOK_ASSIGN: {
const leftNode = left;
const right = this.expression(0);
return {
type: 'Binding',
variable: leftNode.name,
reference: right,
};
}
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_PLUS:
case Token.TOK_MINUS:
case Token.TOK_MULTIPLY:
case Token.TOK_STAR:
case Token.TOK_DIVIDE:
case Token.TOK_MODULO:
case Token.TOK_DIV:
return this.parseArithmetic(left, tokenName);
case Token.TOK_LBRACKET: {
const token = this.lookaheadToken(0);
if (token.type === Token.TOK_NUMBER || token.type === Token.TOK_COLON) {
const right = this.parseIndexExpression();
return this.projectIfSlice(left, right);
}
this.match(Token.TOK_STAR);
this.match(Token.TOK_RBRACKET);
const right = this.parseProjectionRHS(bindingPower.Star);
return { type: 'Projection', left, right };
}
default:
return this.errorToken(this.lookaheadToken(0));
}
}
static isKeyword(token, keyword) {
return token.type === Token.TOK_UNQUOTEDIDENTIFIER && token.value === keyword;
}
match(tokenType) {
if (this.lookahead(0) === tokenType) {
this.advance();
return;
}
else {
const token = this.lookaheadToken(0);
this.errorToken(token, `Syntax error: expected ${tokenType}, got: ${token.type}`);
}
}
errorToken(token, message = '') {
const error = new Error(message || `Syntax error: 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 value = Number(this.lookaheadToken(0).value);
this.advance();
this.match(Token.TOK_RBRACKET);
return { type: 'Index', value };
}
projectIfSlice(left, right) {
const indexExpr = {
type: 'IndexExpression',
left,
right,
};
if (right.type === 'Slice') {
return {
left: indexExpr,
right: this.parseProjectionRHS(bindingPower.Star),
type: 'Projection',
};
}
return indexExpr;
}
parseSliceExpression() {
const parts = [null, null, null];
let index = 0;
let current = this.lookaheadToken(0);
while (current.type != Token.TOK_RBRACKET && index < 3) {
if (current.type === Token.TOK_COLON) {
index++;
if (index === 3) {
this.errorToken(this.lookaheadToken(0), 'Syntax error, too many colons in slice expression');
}
this.advance();
}
else if (current.type === Token.TOK_NUMBER) {
const part = this.lookaheadToken(0).value;
parts[index] = part;
this.advance();
}
else {
const next = this.lookaheadToken(0);
this.errorToken(next, `Syntax error, unexpected token: ${next.value}(${next.type})`);
}
current = this.lookaheadToken(0);
}
this.match(Token.TOK_RBRACKET);
const [start, stop, step] = parts;
return { type: 'Slice', start, stop, step };
}
parseLetExpression() {
const separated = this.parseCommaSeparatedExpressionsUntilKeyword('in');
const expression = this.expression(0);
const bindings = separated.map(binding => binding);
return {
type: 'LetExpression',
bindings: bindings,
expression: expression,
};
}
parseCommaSeparatedExpressionsUntilKeyword(keyword) {
return this.parseCommaSeparatedExpressionsUntil(() => {
return TokenParser.isKeyword(this.lookaheadToken(0), keyword);
}, () => {
this.advance();
});
}
parseCommaSeparatedExpressionsUntilToken(token) {
return this.parseCommaSeparatedExpressionsUntil(() => {
return this.lookahead(0) === token;
}, () => {
return this.match(token);
});
}
parseCommaSeparatedExpressionsUntil(isEndToken, matchEndToken) {
const args = [];
let expression;
while (!isEndToken()) {
expression = this.expression(0);
if (this.lookahead(0) === Token.TOK_COMMA) {
this.match(Token.TOK_COMMA);
}
args.push(expression);
}
matchEndToken();
return args;
}
parseComparator(left, comparator) {
const right = this.expression(bindingPower[comparator]);
return { type: 'Comparator', name: comparator, left, right };
}
parseArithmetic(left, operator) {
const right = this.expression(bindingPower[operator]);
return { type: 'Arithmetic', operator: operator, 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('Syntax 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(`Syntax 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();
class Text {
constructor(text) {
this._text = text;
}
get string() {
return this._text;
}
get length() {
return this.codePoints.length;
}
compareTo(other) {
return Text.compare(this, new Text(other));
}
static get comparer() {
const stringComparer = (left, right) => {
return new Text(left).compareTo(right);
};
return stringComparer;
}
static compare(left, right) {
const leftCp = left.codePoints;
const rightCp = right.codePoints;
for (let index = 0; index < Math.min(leftCp.length, rightCp.length); index++) {
if (leftCp[index] === rightCp[index]) {
continue;
}
return leftCp[index] - rightCp[index] > 0 ? 1 : -1;
}
return leftCp.length - rightCp.length > 0 ? 1 : -1;
}
reverse() {
return String.fromCodePoint(...this.codePoints.reverse());
}
get codePoints() {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const array = [...this._text].map(s => s.codePointAt(0));
return array;
}
}
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["TYPE_ARRAY_OBJECT"] = 10] = "TYPE_ARRAY_OBJECT";
InputArgument[InputArgument["TYPE_ARRAY_ARRAY"] = 11] = "TYPE_ARRAY_ARRAY";
})(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_OBJECT]: 'Array<object>',
[InputArgument.TYPE_ARRAY_STRING]: 'Array<string>',
[InputArgument.TYPE_ARRAY_ARRAY]: 'Array<Array<any>>',
};
this.functionAbs = ([inputValue]) => {
return Math.abs(inputValue);
};
this.functionAvg = ([inputArray]) => {
if (!inputArray || inputArray.length == 0) {
return null;
}
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 = ([searchable, searchValue,]) => {
if (Array.isArray(searchable)) {
const array = searchable;
return array.includes(searchValue);
}
if (typeof searchable === 'string') {
const text = searchable;
if (typeof searchValue === 'string') {
return text.includes(searchValue);
}
}
return null;
};
this.functionEndsWith = resolvedArgs => {
const [searchStr, suffix] = resolvedArgs;
return searchStr.includes(suffix, searchStr.length - suffix.length);
};
this.functionFindFirst = resolvedArgs => {
const subject = resolvedArgs[0];
const search = resolvedArgs[1];
const start = (resolvedArgs.length > 2 && resolvedArgs[2]) || undefined;
const end = (resolvedArgs.length > 3 && resolvedArgs[3]) || undefined;
return findFirst(subject, search, start, end);
};
this.functionFindLast = resolvedArgs => {
const subject = resolvedArgs[0];
const search = resolvedArgs[1];
const start = (resolvedArgs.length > 2 && resolvedArgs[2]) || undefined;
const end = (resolvedArgs.length > 3 && resolvedArgs[3]) || undefined;
return findLast(subject, search, start, end);
};
this.functionFloor = ([inputValue]) => {
return Math.floor(inputValue);
};
this.functionFromItems = ([array]) => {
array.map((pair) => {
if (pair.length != 2 || typeof pair[0] !== 'string') {
throw new Error('invalid value, each array must contain two elements, a pair of string and value');
}
});
return Object.fromEntries(array);
};
this.functionGroupBy = ([array, exprefNode]) => {
const keyFunction = this.createKeyFunction(exprefNode, [InputArgument.TYPE_STRING]);
return array.reduce((acc, cur) => {
const k = keyFunction(cur !== null && cur !== void 0 ? cur : {});
const target = (acc[k] = acc[k] || []);
target.push(cur);
return acc;
}, {});
};
this.functionItems = ([inputValue]) => {
return Object.entries(inputValue);
};
this.functionJoin = resolvedArgs => {
const [joinChar, listJoin] = resolvedArgs;
return listJoin.join(joinChar);
};
this.functionKeys = ([inputObject]) => {
return Object.keys(inputObject);
};
this.functionLength = ([inputValue]) => {
if (typeof inputValue === 'string') {
return new Text(inputValue).length;
}
if (Array.isArray(inputValue)) {
return inputValue.length;
}
return Object.keys(inputValue).length;
};
this.functionLower = ([subject]) => {
return lower(subject);
};
this.functionMap = ([exprefNode, elements]) => {
if (!this._interpreter) {
return [];
}
const mapped = [];
const interpreter = this._interpreter;
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 || null;
};
this.functionMerge = resolvedArgs => {
let merged = {};
for (let i = 0; i < resolvedArgs.length; i += 1) {
const current = resolvedArgs[i];
merged = Object.assign(merged, current);
}
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 || null;
};
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.functionPadLeft = resolvedArgs => {
const subject = resolvedArgs[0];
const width = resolvedArgs[1];
const padding = (resolvedArgs.length > 2 && resolvedArgs[2]) || undefined;
return padLeft(subject, width, padding);
};
this.functionPadRight = resolvedArgs => {
const subject = resolvedArgs[0];
const width = resolvedArgs[1];
const padding = (resolvedArgs.length > 2 && resolvedArgs[2]) || undefined;
return padRight(subject, width, padding);
};
this.functionReplace = resolvedArgs => {
const subject = resolvedArgs[0];
const string = resolvedArgs[1];
const by = resolvedArgs[2];
return replace(subject, string, by, resolvedArgs.length > 3 ? resolvedArgs[3] : undefined);
};
this.functionSplit = resolvedArgs => {
const subject = resolvedArgs[0];
const search = resolvedArgs[1];
return split(subject, search, resolvedArgs.length > 2 ? resolvedArgs[2] : undefined);
};
this.functionReverse = ([inputValue]) => {
const typeName = this.getTypeName(inputValue);
if (typeName === InputArgument.TYPE_STRING) {
return new Text(inputValue).reverse();
}
const reversedArray = inputValue.slice(0);
reversedArray.reverse();
return reversedArray;
};
this.functionSort = ([inputValue]) => {
if (inputValue.length == 0) {
return inputValue;
}
if (typeof inputValue[0] === 'string') {
return [...inputValue].sort(Text.comparer);
}
return [...inputValue].sort();
};
this.functionSortBy = resolvedArgs => {
const sortedArray = resolvedArgs[0].slice(0);
if (sortedArray.length === 0) {
retu