From 180e3915d0fbf9e5d80252d61265e56145ab1080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Mon, 30 Sep 2024 22:32:55 +0200 Subject: [PATCH 01/13] Added deno and changed js to cjs --- .vscode/settings.json | 3 +++ deno.json | 19 +++++++++++++++++++ jmespath.js => jmespath.cjs | 0 mod.ts | 3 +++ package.json | 2 +- 5 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 .vscode/settings.json create mode 100644 deno.json rename jmespath.js => jmespath.cjs (100%) create mode 100644 mod.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..cbac569 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "deno.enable": true +} diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..dd3302f --- /dev/null +++ b/deno.json @@ -0,0 +1,19 @@ +{ + "name": "@halvardm/jmespath", + "version": "0.17.0", + "exports": { + ".":"./mod.ts" + }, + "tasks": { + "test": "deno test --allow-read", + "test:inspect": "deno task test --inspect-wait" + }, + "license": "Apache-2.0", + "imports": { + "@std/assert": "jsr:@std/assert@1", + "@std/path": "jsr:@std/path@^1" + }, + "fmt": { + "exclude": ["test/compliance"] + } +} diff --git a/jmespath.js b/jmespath.cjs similarity index 100% rename from jmespath.js rename to jmespath.cjs diff --git a/mod.ts b/mod.ts new file mode 100644 index 0000000..9f2efc4 --- /dev/null +++ b/mod.ts @@ -0,0 +1,3 @@ +import { search } from "./jmespath.cjs"; + +export { search } diff --git a/package.json b/package.json index 8c1d27c..67a8d0e 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "mocha": "^2.1.0" }, "dependencies": {}, - "main": "jmespath.js", + "main": "jmespath.cjs", "directories": { "test": "test" }, From 2497743f517eeb2d59e7ca742fa246c3c5165445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Tue, 1 Oct 2024 00:11:25 +0200 Subject: [PATCH 02/13] Refactoredthe compliance test --- deno.lock | 47 +++++++++++++++++++++++++++++ test/compliance.test.ts | 66 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 deno.lock create mode 100644 test/compliance.test.ts diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..6c52be6 --- /dev/null +++ b/deno.lock @@ -0,0 +1,47 @@ +{ + "version": "3", + "packages": { + "specifiers": { + "jsr:@std/assert@1": "jsr:@std/assert@1.0.6", + "jsr:@std/internal@^1.0.4": "jsr:@std/internal@1.0.4", + "jsr:@std/path@^1": "jsr:@std/path@1.0.6", + "npm:@types/node": "npm:@types/node@18.16.19" + }, + "jsr": { + "@std/assert@1.0.6": { + "integrity": "1904c05806a25d94fe791d6d883b685c9e2dcd60e4f9fc30f4fc5cf010c72207", + "dependencies": [ + "jsr:@std/internal@^1.0.4" + ] + }, + "@std/internal@1.0.4": { + "integrity": "62e8e4911527e5e4f307741a795c0b0a9e6958d0b3790716ae71ce085f755422" + }, + "@std/path@1.0.6": { + "integrity": "ab2c55f902b380cf28e0eec501b4906e4c1960d13f00e11cfbcd21de15f18fed" + } + }, + "npm": { + "@types/node@18.16.19": { + "integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==", + "dependencies": {} + } + } + }, + "remote": {}, + "workspace": { + "dependencies": [ + "jsr:@std/assert@1", + "jsr:@std/path@^1" + ], + "packageJson": { + "dependencies": [ + "npm:grunt-contrib-jshint@^0.11.0", + "npm:grunt-contrib-uglify@^0.11.1", + "npm:grunt-eslint@^17.3.1", + "npm:grunt@^0.4.5", + "npm:mocha@^2.1.0" + ] + } + } +} diff --git a/test/compliance.test.ts b/test/compliance.test.ts new file mode 100644 index 0000000..b07ff3a --- /dev/null +++ b/test/compliance.test.ts @@ -0,0 +1,66 @@ +import { join, resolve } from "@std/path"; +import { assertEquals, assertThrows } from "@std/assert"; + +import { createRequire } from "node:module"; +const require = createRequire(import.meta.url); +const { search } = require("../jmespath.cjs"); + + +type ComplianceTestCases = { + expression: string; + result: unknown; + error?: string; +}; +type ComplianceTest = { + // deno-lint-ignore no-explicit-any + given: Record; + cases: ComplianceTestCases[]; +}; +type ComplianceTestFile = ComplianceTest[]; + +// Compliance tests that aren't supported yet. +const notImplementedYet: string[] = []; + +const testSrcPath = resolve("test/compliance"); + +const listings = Deno.readDirSync("test/compliance"); + +for (const listing of listings) { + if ( + listing.isFile && listing.name.endsWith(".json") && + !notImplementedYet.includes(listing.name) + ) { + const filename = join(testSrcPath, listing.name); + + const contentRaw = Deno.readTextFileSync(filename); + const content: ComplianceTestFile = JSON.parse(contentRaw); + + for (const [i, spec] of content.entries()) { + Deno.test(`Compliance > suite ${i} for '${listing.name}'`, async (t) => { + const { given, cases } = spec; + for (const [j, testCase] of cases.entries()) { + if (testCase.error) { + await t.step("should throw error for test " + j, () => { + assertThrows( + () => { + search(given, testCase.expression); + }, + Error, + ); + }); + } else { + await t.step( + `should pass test ${j} expression: ${testCase.expression}`, + () => { + assertEquals( + search(given, testCase.expression), + testCase.result, + ); + }, + ); + } + } + }); + } + } +} From b3ce340d6b24f99d288feed5100d881e778189ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Tue, 1 Oct 2024 00:32:25 +0200 Subject: [PATCH 03/13] split code into multiple files and refactored from cjs to mjs --- jmespath.cjs | 1672 --------------------------------------- lib/lexer.js | 247 ++++++ lib/mod.js | 27 + lib/parser.js | 365 +++++++++ lib/runtime.js | 529 +++++++++++++ lib/tree-interpreter.js | 274 +++++++ lib/utils.js | 251 ++++++ mod.ts | 2 +- test/compliance.test.ts | 5 +- 9 files changed, 1695 insertions(+), 1677 deletions(-) delete mode 100644 jmespath.cjs create mode 100644 lib/lexer.js create mode 100644 lib/mod.js create mode 100644 lib/parser.js create mode 100644 lib/runtime.js create mode 100644 lib/tree-interpreter.js create mode 100644 lib/utils.js diff --git a/jmespath.cjs b/jmespath.cjs deleted file mode 100644 index 0a36691..0000000 --- a/jmespath.cjs +++ /dev/null @@ -1,1672 +0,0 @@ -(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 TYPE_NAME_TABLE = { - 0: 'number', - 1: 'any', - 2: 'string', - 3: 'array', - 4: 'object', - 5: 'boolean', - 6: 'expression', - 7: 'null', - 8: 'Array', - 9: 'Array' - }; - - 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."); - } - return node; - 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]}; - } - return this._parseMultiselectList(); - 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]}; - } - // Creating a projection. - this._advance(); - right = this._parseProjectionRHS(rbp); - return {type: "ValueProjection", children: [left, right]}; - 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); - } - this._match(TOK_STAR); - this._match(TOK_RBRACKET); - right = this._parseProjectionRHS(bindingPower.Star); - return {type: "Projection", children: [left, right]}; - 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 && isObject(value)) { - field = value[node.name]; - if (field === undefined) { - return null; - } else { - return field; - } - } - return null; - 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) { - var expected = currentSpec - .map(function(typeIdentifier) { - return TYPE_NAME_TABLE[typeIdentifier]; - }) - .join(','); - throw new Error("TypeError: " + name + "() " + - "expected argument " + (i + 1) + - " to be type " + expected + - " but received type " + - TYPE_NAME_TABLE[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); diff --git a/lib/lexer.js b/lib/lexer.js new file mode 100644 index 0000000..b22d9ef --- /dev/null +++ b/lib/lexer.js @@ -0,0 +1,247 @@ +import { + TYPE_ANY, + TOK_AND, + TOK_COLON, + TOK_COMMA, + TOK_CURRENT, + TYPE_ARRAY, + TOK_DOT,TOK_EOF,TOK_EQ,TOK_EXPREF,TOK_FILTER,TOK_FLATTEN,TOK_GT,TOK_GTE,TOK_LBRACE,TOK_LBRACKET,TOK_LITERAL,TOK_LPAREN,TOK_LT,TOK_LTE,TOK_NE,TOK_NOT,TOK_NUMBER,TOK_OR,TOK_PIPE,TOK_QUOTEDIDENTIFIER,TOK_RBRACE,TOK_RBRACKET,TOK_RPAREN,TOK_STAR,TOK_UNQUOTEDIDENTIFIER,TYPE_ARRAY_NUMBER,TYPE_ARRAY_STRING,TYPE_BOOLEAN,TYPE_EXPREF,TYPE_NAME_TABLE,TYPE_NULL,TYPE_NUMBER,TYPE_OBJECT,TYPE_STRING,isAlpha,isAlphaNum,isArray,isFalse,isNum,isObject,basicTokens,bindingPower,objValues,operatorStartToken,skipChars,strictDeepEqual + } from "./utils.js" + + export 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 = stream.slice(start, this._current).trimStart(); + 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; + } + } +}; \ No newline at end of file diff --git a/lib/mod.js b/lib/mod.js new file mode 100644 index 0000000..fe86e87 --- /dev/null +++ b/lib/mod.js @@ -0,0 +1,27 @@ +import {Lexer} from "./lexer.js" +import {Parser} from "./parser.js" +import {Runtime} from "./runtime.js" +import {TreeInterpreter} from "./tree-interpreter.js" + +export function compile(stream) { + var parser = new Parser(); + var ast = parser.parse(stream); + return ast; + } + + export function tokenize(stream) { + var lexer = new Lexer(); + return lexer.tokenize(stream); + } + + export 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); + } diff --git a/lib/parser.js b/lib/parser.js new file mode 100644 index 0000000..42ea390 --- /dev/null +++ b/lib/parser.js @@ -0,0 +1,365 @@ +import { + TYPE_ANY, + TOK_AND, + TOK_COLON, + TOK_COMMA, + TOK_CURRENT, + TYPE_ARRAY, + TOK_DOT,TOK_EOF,TOK_EQ,TOK_EXPREF,TOK_FILTER,TOK_FLATTEN,TOK_GT,TOK_GTE,TOK_LBRACE,TOK_LBRACKET,TOK_LITERAL,TOK_LPAREN,TOK_LT,TOK_LTE,TOK_NE,TOK_NOT,TOK_NUMBER,TOK_OR,TOK_PIPE,TOK_QUOTEDIDENTIFIER,TOK_RBRACE,TOK_RBRACKET,TOK_RPAREN,TOK_STAR,TOK_UNQUOTEDIDENTIFIER,TYPE_ARRAY_NUMBER,TYPE_ARRAY_STRING,TYPE_BOOLEAN,TYPE_EXPREF,TYPE_NAME_TABLE,TYPE_NULL,TYPE_NUMBER,TYPE_OBJECT,TYPE_STRING,isAlpha,isAlphaNum,isArray,isFalse,isNum,isObject,basicTokens,bindingPower,objValues,operatorStartToken,skipChars,strictDeepEqual +} from "./utils.js" +import { Lexer } from "./lexer.js"; +export 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."); + } + return node; + 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]}; + } + return this._parseMultiselectList(); + 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]}; + } + // Creating a projection. + this._advance(); + right = this._parseProjectionRHS(rbp); + return {type: "ValueProjection", children: [left, right]}; + 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); + } + this._match(TOK_STAR); + this._match(TOK_RBRACKET); + right = this._parseProjectionRHS(bindingPower.Star); + return {type: "Projection", children: [left, right]}; + 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}; + } +}; \ No newline at end of file diff --git a/lib/runtime.js b/lib/runtime.js new file mode 100644 index 0000000..221910d --- /dev/null +++ b/lib/runtime.js @@ -0,0 +1,529 @@ +import { + TYPE_ANY, + TOK_AND, + TOK_COLON, + TOK_COMMA, + TOK_CURRENT, + TYPE_ARRAY, + TOK_DOT,TOK_EOF,TOK_EQ,TOK_EXPREF,TOK_FILTER,TOK_FLATTEN,TOK_GT,TOK_GTE,TOK_LBRACE,TOK_LBRACKET,TOK_LITERAL,TOK_LPAREN,TOK_LT,TOK_LTE,TOK_NE,TOK_NOT,TOK_NUMBER,TOK_OR,TOK_PIPE,TOK_QUOTEDIDENTIFIER,TOK_RBRACE,TOK_RBRACKET,TOK_RPAREN,TOK_STAR,TOK_UNQUOTEDIDENTIFIER,TYPE_ARRAY_NUMBER,TYPE_ARRAY_STRING,TYPE_BOOLEAN,TYPE_EXPREF,TYPE_NAME_TABLE,TYPE_NULL,TYPE_NUMBER,TYPE_OBJECT,TYPE_STRING,isAlpha,isAlphaNum,isArray,isFalse,isNum,isObject,basicTokens,bindingPower,objValues,operatorStartToken,skipChars,strictDeepEqual +} from "./utils.js" + +export 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) { + var expected = currentSpec + .map(function(typeIdentifier) { + return TYPE_NAME_TABLE[typeIdentifier]; + }) + .join(','); + throw new Error("TypeError: " + name + "() " + + "expected argument " + (i + 1) + + " to be type " + expected + + " but received type " + + TYPE_NAME_TABLE[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; + } + + }; \ No newline at end of file diff --git a/lib/tree-interpreter.js b/lib/tree-interpreter.js new file mode 100644 index 0000000..d1bd6ec --- /dev/null +++ b/lib/tree-interpreter.js @@ -0,0 +1,274 @@ +import { + TYPE_ANY, + TOK_AND, + TOK_COLON, + TOK_COMMA, + TOK_CURRENT, + TYPE_ARRAY, + TOK_DOT,TOK_EOF,TOK_EQ,TOK_EXPREF,TOK_FILTER,TOK_FLATTEN,TOK_GT,TOK_GTE,TOK_LBRACE,TOK_LBRACKET,TOK_LITERAL,TOK_LPAREN,TOK_LT,TOK_LTE,TOK_NE,TOK_NOT,TOK_NUMBER,TOK_OR,TOK_PIPE,TOK_QUOTEDIDENTIFIER,TOK_RBRACE,TOK_RBRACKET,TOK_RPAREN,TOK_STAR,TOK_UNQUOTEDIDENTIFIER,TYPE_ARRAY_NUMBER,TYPE_ARRAY_STRING,TYPE_BOOLEAN,TYPE_EXPREF,TYPE_NAME_TABLE,TYPE_NULL,TYPE_NUMBER,TYPE_OBJECT,TYPE_STRING,isAlpha,isAlphaNum,isArray,isFalse,isNum,isObject,basicTokens,bindingPower,objValues,operatorStartToken,skipChars,strictDeepEqual +} from "./utils.js" + +export 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 && isObject(value)) { + field = value[node.name]; + if (field === undefined) { + return null; + } else { + return field; + } + } + return null; + 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; + } + + }; \ No newline at end of file diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..51edcc2 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,251 @@ +export function isArray(obj) { + if (obj !== null) { + return Object.prototype.toString.call(obj) === "[object Array]"; + } else { + return false; + } + } + + export function isObject(obj) { + if (obj !== null) { + return Object.prototype.toString.call(obj) === "[object Object]"; + } else { + return false; + } + } + + export 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; + } + + export 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; + } + } + + export 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; + } + + export 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; + } + + // Type constants used to define functions. + export var TYPE_NUMBER = 0; + export var TYPE_ANY = 1; + export var TYPE_STRING = 2; + export var TYPE_ARRAY = 3; + export var TYPE_OBJECT = 4; + export var TYPE_BOOLEAN = 5; + export var TYPE_EXPREF = 6; + export var TYPE_NULL = 7; + export var TYPE_ARRAY_NUMBER = 8; + export var TYPE_ARRAY_STRING = 9; + export var TYPE_NAME_TABLE = { + 0: 'number', + 1: 'any', + 2: 'string', + 3: 'array', + 4: 'object', + 5: 'boolean', + 6: 'expression', + 7: 'null', + 8: 'Array', + 9: 'Array' + }; + + export var TOK_EOF = "EOF"; + export var TOK_UNQUOTEDIDENTIFIER = "UnquotedIdentifier"; + export var TOK_QUOTEDIDENTIFIER = "QuotedIdentifier"; + export var TOK_RBRACKET = "Rbracket"; + export var TOK_RPAREN = "Rparen"; + export var TOK_COMMA = "Comma"; + export var TOK_COLON = "Colon"; + export var TOK_RBRACE = "Rbrace"; + export var TOK_NUMBER = "Number"; + export var TOK_CURRENT = "Current"; + export var TOK_EXPREF = "Expref"; + export var TOK_PIPE = "Pipe"; + export var TOK_OR = "Or"; + export var TOK_AND = "And"; + export var TOK_EQ = "EQ"; + export var TOK_GT = "GT"; + export var TOK_LT = "LT"; + export var TOK_GTE = "GTE"; + export var TOK_LTE = "LTE"; + export var TOK_NE = "NE"; + export var TOK_FLATTEN = "Flatten"; + export var TOK_STAR = "Star"; + export var TOK_FILTER = "Filter"; + export var TOK_DOT = "Dot"; + export var TOK_NOT = "Not"; + export var TOK_LBRACE = "Lbrace"; + export var TOK_LBRACKET = "Lbracket"; + export var TOK_LPAREN= "Lparen"; + export var TOK_LITERAL= "Literal"; + + // The "&", "[", "<", ">" tokens + // are not in basicToken because + // there are two token variants + // ("&&", "[?", "<=", ">="). This is specially handled + // below. + + export var basicTokens = { + ".": TOK_DOT, + "*": TOK_STAR, + ",": TOK_COMMA, + ":": TOK_COLON, + "{": TOK_LBRACE, + "}": TOK_RBRACE, + "]": TOK_RBRACKET, + "(": TOK_LPAREN, + ")": TOK_RPAREN, + "@": TOK_CURRENT + }; + + export var operatorStartToken = { + "<": true, + ">": true, + "=": true, + "!": true + }; + + export var skipChars = { + " ": true, + "\t": true, + "\n": true + }; + + + export function isAlpha(ch) { + return (ch >= "a" && ch <= "z") || + (ch >= "A" && ch <= "Z") || + ch === "_"; + } + + export function isNum(ch) { + return (ch >= "0" && ch <= "9") || + ch === "-"; + } + export function isAlphaNum(ch) { + return (ch >= "a" && ch <= "z") || + (ch >= "A" && ch <= "Z") || + (ch >= "0" && ch <= "9") || + ch === "_"; + } + + + + export 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; diff --git a/mod.ts b/mod.ts index 9f2efc4..7c4d089 100644 --- a/mod.ts +++ b/mod.ts @@ -1,3 +1,3 @@ -import { search } from "./jmespath.cjs"; +import { search } from "./lib/mod.js"; export { search } diff --git a/test/compliance.test.ts b/test/compliance.test.ts index b07ff3a..02ffe8c 100644 --- a/test/compliance.test.ts +++ b/test/compliance.test.ts @@ -1,10 +1,7 @@ import { join, resolve } from "@std/path"; import { assertEquals, assertThrows } from "@std/assert"; -import { createRequire } from "node:module"; -const require = createRequire(import.meta.url); -const { search } = require("../jmespath.cjs"); - +import { search } from "../lib/mod.js"; type ComplianceTestCases = { expression: string; From b38197124abd23d26aee858f21bfe1152f55365a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Tue, 1 Oct 2024 01:01:00 +0200 Subject: [PATCH 04/13] refactored benchmark test --- lib/parser.bench.ts | 25 +++++++++++++++++++++++++ perf.js | 33 --------------------------------- 2 files changed, 25 insertions(+), 33 deletions(-) create mode 100644 lib/parser.bench.ts delete mode 100644 perf.js diff --git a/lib/parser.bench.ts b/lib/parser.bench.ts new file mode 100644 index 0000000..a3d13d6 --- /dev/null +++ b/lib/parser.bench.ts @@ -0,0 +1,25 @@ +import { compile } from "./mod.js"; + +Deno.bench("Parser > single_expr", () => { + compile("foo"); +}); + +Deno.bench("Parser > single_subexpr", function () { + compile("foo.bar"); +}); + +Deno.bench("Parser > deeply_nested_50", function () { + compile( + "j49.j48.j47.j46.j45.j44.j43.j42.j41.j40.j39.j38.j37.j36.j35.j34.j33.j32.j31.j30.j29.j28.j27.j26.j25.j24.j23.j22.j21.j20.j19.j18.j17.j16.j15.j14.j13.j12.j11.j10.j9.j8.j7.j6.j5.j4.j3.j2.j1.j0", + ); +}); + +Deno.bench("Parser > deeply_nested_50_index", function () { + compile( + "[49][48][47][46][45][44][43][42][41][40][39][38][37][36][35][34][33][32][31][30][29][28][27][26][25][24][23][22][21][20][19][18][17][16][15][14][13][12][11][10][9][8][7][6][5][4][3][2][1][0]", + ); +}); + +Deno.bench("Parser > basic_list_projection", function () { + compile("foo[*].bar"); +}); diff --git a/perf.js b/perf.js deleted file mode 100644 index 58794ec..0000000 --- a/perf.js +++ /dev/null @@ -1,33 +0,0 @@ -var jmespath = require('./jmespath') -var Benchmark = require('benchmark'); -var suite = new Benchmark.Suite; - -// add tests -suite.add('Parser#single_expr', function() { - jmespath.compile("foo"); -}) -.add('Parser#single_subexpr', function() { - jmespath.compile("foo.bar"); -}) -.add('Parser#deeply_nested_50', function() { - jmespath.compile("j49.j48.j47.j46.j45.j44.j43.j42.j41.j40.j39.j38.j37.j36.j35.j34.j33.j32.j31.j30.j29.j28.j27.j26.j25.j24.j23.j22.j21.j20.j19.j18.j17.j16.j15.j14.j13.j12.j11.j10.j9.j8.j7.j6.j5.j4.j3.j2.j1.j0"); - -}) -.add('Parser#deeply_nested_50_index', function() { - jmespath.compile("[49][48][47][46][45][44][43][42][41][40][39][38][37][36][35][34][33][32][31][30][29][28][27][26][25][24][23][22][21][20][19][18][17][16][15][14][13][12][11][10][9][8][7][6][5][4][3][2][1][0]"); -}) -.add('Parser#basic_list_projection', function() { - jmespath.compile("foo[*].bar"); -}) -.on('cycle', function(event) { - var bench = event.target; - var mean = bench.stats.mean * 1000; - var variance = bench.stats.variance * 1000000; - var result = 'Mean time: ' + mean.toFixed(6) + 'msec '; - result += event.target.toString(); - console.log(result); -}) -.on('complete', function() { -}) -// run async -.run({ 'async': false }); From d6f25567e33eb77b235dcf2c8ce59a12e380f80d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Tue, 1 Oct 2024 01:03:25 +0200 Subject: [PATCH 05/13] removed old node files --- .eslintrc | 10 ------- .gitignore | 2 -- .npmignore | 6 ----- .travis.yml | 6 ----- Gruntfile.js | 36 ------------------------- artifacts/jmespath.min.js | 2 -- bower.json | 24 ----------------- package.json | 45 ------------------------------- test/compliance.js | 56 --------------------------------------- 9 files changed, 187 deletions(-) delete mode 100644 .eslintrc delete mode 100644 .gitignore delete mode 100644 .npmignore delete mode 100644 .travis.yml delete mode 100644 Gruntfile.js delete mode 100644 artifacts/jmespath.min.js delete mode 100644 bower.json delete mode 100644 package.json delete mode 100644 test/compliance.js diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 93c7190..0000000 --- a/.eslintrc +++ /dev/null @@ -1,10 +0,0 @@ -{ - "env": { - "browser": true, - "node": true - }, - "globals": { - "toString": true, - "hasOwnProperty": true - } -} diff --git a/.gitignore b/.gitignore deleted file mode 100644 index eeaf59e..0000000 --- a/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules -/yarn.lock diff --git a/.npmignore b/.npmignore deleted file mode 100644 index d532157..0000000 --- a/.npmignore +++ /dev/null @@ -1,6 +0,0 @@ -test/ -*.* -!*.js -!*.json -perf.js -Gruntfile.js diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 97bccea..0000000 --- a/.travis.yml +++ /dev/null @@ -1,6 +0,0 @@ -language: node_js -node_js: - - "0.12" - - "0.11" - - "0.10" - - "iojs" diff --git a/Gruntfile.js b/Gruntfile.js deleted file mode 100644 index 614a61b..0000000 --- a/Gruntfile.js +++ /dev/null @@ -1,36 +0,0 @@ -module.exports = function(grunt) { - - // Project configuration. - grunt.initConfig({ - pkg: grunt.file.readJSON('package.json'), - uglify: { - options: { - banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n', - mangleProperties: {regex: /^_/} - }, - build: { - src: '<%= pkg.name %>.js', - dest: 'artifacts/<%= pkg.name %>.min.js' - } - }, - jshint: { - ignore_warning: { - options: { - '-W083': true - }, - src: ['jmespath.js', 'test/*.js', 'Gruntfile.js'] - } - }, - eslint: { - target: ['jmespath.js'] - } - }); - - grunt.loadNpmTasks('grunt-contrib-uglify'); - grunt.loadNpmTasks('grunt-contrib-jshint'); - grunt.loadNpmTasks('grunt-eslint'); - - // Default task(s). - grunt.registerTask('default', ['uglify', 'jshint', 'eslint']); - -}; diff --git a/artifacts/jmespath.min.js b/artifacts/jmespath.min.js deleted file mode 100644 index ae76a6d..0000000 --- a/artifacts/jmespath.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! jmespath 2016-03-22 */ -!function(a){"use strict";function b(a){return null!==a?"[object Array]"===Object.prototype.toString.call(a):!1}function c(a){return null!==a?"[object Object]"===Object.prototype.toString.call(a):!1}function d(a,e){if(a===e)return!0;var f=Object.prototype.toString.call(a);if(f!==Object.prototype.toString.call(e))return!1;if(b(a)===!0){if(a.length!==e.length)return!1;for(var g=0;g="a"&&"z">=a||a>="A"&&"Z">=a||"_"===a}function h(a){return a>="0"&&"9">=a||"-"===a}function i(a){return a>="a"&&"z">=a||a>="A"&&"Z">=a||a>="0"&&"9">=a||"_"===a}function j(){}function k(){}function l(a){this.runtime=a}function m(a){this.a=a,this.functionTable={abs:{b:this.c,d:[{types:[r]}]},avg:{b:this.e,d:[{types:[z]}]},ceil:{b:this.f,d:[{types:[r]}]},contains:{b:this.g,d:[{types:[t,u]},{types:[s]}]},ends_with:{b:this.h,d:[{types:[t]},{types:[t]}]},floor:{b:this.i,d:[{types:[r]}]},length:{b:this.j,d:[{types:[t,u,v]}]},map:{b:this.k,d:[{types:[x]},{types:[u]}]},max:{b:this.l,d:[{types:[z,A]}]},merge:{b:this.m,d:[{types:[v],variadic:!0}]},max_by:{b:this.n,d:[{types:[u]},{types:[x]}]},sum:{b:this.o,d:[{types:[z]}]},starts_with:{b:this.p,d:[{types:[t]},{types:[t]}]},min:{b:this.q,d:[{types:[z,A]}]},min_by:{b:this.r,d:[{types:[u]},{types:[x]}]},type:{b:this.s,d:[{types:[s]}]},keys:{b:this.t,d:[{types:[v]}]},values:{b:this.u,d:[{types:[v]}]},sort:{b:this.v,d:[{types:[A,z]}]},sort_by:{b:this.w,d:[{types:[u]},{types:[x]}]},join:{b:this.x,d:[{types:[t]},{types:[A]}]},reverse:{b:this.y,d:[{types:[t,u]}]},to_array:{b:this.z,d:[{types:[s]}]},to_string:{b:this.A,d:[{types:[s]}]},to_number:{b:this.B,d:[{types:[s]}]},not_null:{b:this.C,d:[{types:[s],variadic:!0}]}}}function n(a){var b=new k,c=b.parse(a);return c}function o(a){var b=new j;return b.tokenize(a)}function p(a,b){var c=new k,d=new m,e=new l(d);d.a=e;var f=c.parse(b);return e.search(f,a)}var q;q="function"==typeof String.prototype.trimLeft?function(a){return a.trimLeft()}:function(a){return a.match(/^\s*(.*)/)[1]};var r=0,s=1,t=2,u=3,v=4,w=5,x=6,y=7,z=8,A=9,B="EOF",C="UnquotedIdentifier",D="QuotedIdentifier",E="Rbracket",F="Rparen",G="Comma",H="Colon",I="Rbrace",J="Number",K="Current",L="Expref",M="Pipe",N="Or",O="And",P="EQ",Q="GT",R="LT",S="GTE",T="LTE",U="NE",V="Flatten",W="Star",X="Filter",Y="Dot",Z="Not",$="Lbrace",_="Lbracket",aa="Lparen",ba="Literal",ca={".":Y,"*":W,",":G,":":H,"{":$,"}":I,"]":E,"(":aa,")":F,"@":K},da={"<":!0,">":!0,"=":!0,"!":!0},ea={" ":!0," ":!0,"\n":!0};j.prototype={tokenize:function(a){var b=[];this.D=0;for(var c,d,e;this.D"===c?"="===a[this.D]?(this.D++,{type:S,value:">=",start:b}):{type:Q,value:">",start:b}:"="===c&&"="===a[this.D]?(this.D++,{type:P,value:"==",start:b}):void 0},J:function(a){this.D++;for(var b,c=this.D,d=a.length;"`"!==a[this.D]&&this.D=0)return!0;if(c.indexOf(a)>=0)return!0;if(!(d.indexOf(a[0])>=0))return!1;try{return JSON.parse(a),!0}catch(e){return!1}}};var fa={};fa[B]=0,fa[C]=0,fa[D]=0,fa[E]=0,fa[F]=0,fa[G]=0,fa[I]=0,fa[J]=0,fa[K]=0,fa[L]=0,fa[M]=1,fa[N]=2,fa[O]=3,fa[P]=5,fa[Q]=5,fa[R]=5,fa[S]=5,fa[T]=5,fa[U]=5,fa[V]=9,fa[W]=20,fa[X]=21,fa[Y]=40,fa[Z]=45,fa[$]=50,fa[_]=55,fa[aa]=60,k.prototype={parse:function(a){this.M(a),this.index=0;var b=this.expression(0);if(this.N(0)!==B){var c=this.O(0),d=new Error("Unexpected token type: "+c.type+", value: "+c.value);throw d.name="ParserError",d}return b},M:function(a){var b=new j,c=b.tokenize(a);c.push({type:B,value:"",start:a.length}),this.tokens=c},expression:function(a){var b=this.O(0);this.P();for(var c=this.nud(b),d=this.N(0);ab;){if(c===H)b++,this.P();else{if(c!==J){var d=this.N(0),e=new Error("Syntax error, unexpected token: "+d.value+"("+d.type+")");throw e.name="Parsererror",e}a[b]=this.O(0).value,this.P()}c=this.N(0)}return this.V(E),{type:"Slice",children:a}},Y:function(a,b){var c=this.expression(fa[b]);return{type:"Comparator",name:b,children:[a,c]}},X:function(a){var b=this.N(0),c=[C,D,W];return c.indexOf(b)>=0?this.expression(a):b===_?(this.V(_),this.U()):b===$?(this.V($),this.R()):void 0},Q:function(a){var b;if(fa[this.N(0)]<10)b={type:"Identity"};else if(this.N(0)===_)b=this.expression(a);else if(this.N(0)===X)b=this.expression(a);else{if(this.N(0)!==Y){var c=this.O(0),d=new Error("Sytanx error, unexpected token: "+c.value+"("+c.type+")");throw d.name="ParserError",d}this.V(Y),b=this.X(a)}return b},U:function(){for(var a=[];this.N(0)!==E;){var b=this.expression(0);if(a.push(b),this.N(0)===G&&(this.V(G),this.N(0)===E))throw new Error("Unexpected token Rbracket")}return this.V(E),{type:"MultiSelectList",children:a}},R:function(){for(var a,b,c,d,e=[],f=[C,D];;){if(a=this.O(0),f.indexOf(a.type)<0)throw new Error("Expecting an identifier token, got: "+a.type);if(b=a.value,this.P(),this.V(H),c=this.expression(0),d={type:"KeyValuePair",name:b,value:c},e.push(d),this.N(0)===G)this.V(G);else if(this.N(0)===I){this.V(I);break}}return{type:"MultiSelectHash",children:e}}},l.prototype={search:function(a,b){return this.visit(a,b)},visit:function(a,g){var h,i,j,k,l,m,n,o,p,q;switch(a.type){case"Field":return null===g?null:c(g)?(m=g[a.name],void 0===m?null:m):null;case"Subexpression":for(j=this.visit(a.children[0],g),q=1;qr&&(r=g.length+r),j=g[r],void 0===j&&(j=null),j;case"Slice":if(!b(g))return null;var s=a.children.slice(0),t=this.computeSliceParams(g.length,s),u=t[0],v=t[1],w=t[2];if(j=[],w>0)for(q=u;v>q;q+=w)j.push(g[q]);else for(q=u;q>v;q+=w)j.push(g[q]);return j;case"Projection":var x=this.visit(a.children[0],g);if(!b(x))return null;for(p=[],q=0;ql;break;case S:j=k>=l;break;case R:j=l>k;break;case T:j=l>=k;break;default:throw new Error("Unknown comparator: "+a.name)}return j;case V:var C=this.visit(a.children[0],g);if(!b(C))return null;var D=[];for(q=0;qe?!0:!1;return c=null===c?h?a-1:0:this.capSliceRange(a,c,e),d=null===d?h?-1:a:this.capSliceRange(a,d,e),f[0]=c,f[1]=d,f[2]=e,f},capSliceRange:function(a,b,c){return 0>b?(b+=a,0>b&&(b=0>c?-1:0)):b>=a&&(b=0>c?a-1:a),b}},m.prototype={callFunction:function(a,b){var c=this.functionTable[a];if(void 0===c)throw new Error("Unknown function: "+a+"()");return this.$(a,b,c.d),c.b.call(this,b)},$:function(a,b,c){var d;if(c[c.length-1].variadic){if(b.length=0;e--)d+=c[e];return d}var f=a[0].slice(0);return f.reverse(),f},c:function(a){return Math.abs(a[0])},f:function(a){return Math.ceil(a[0])},e:function(a){for(var b=0,c=a[0],d=0;d=0},i:function(a){return Math.floor(a[0])},j:function(a){return c(a[0])?Object.keys(a[0]).length:a[0].length},k:function(a){for(var b=[],c=this.a,d=a[0],e=a[1],f=0;f0){var b=this._(a[0][0]);if(b===r)return Math.max.apply(Math,a[0]);for(var c=a[0],d=c[0],e=1;e0){var b=this._(a[0][0]);if(b===r)return Math.min.apply(Math,a[0]);for(var c=a[0],d=c[0],e=1;eh?1:h>g?-1:a[0]-b[0]});for(var i=0;ig&&(g=c,b=e[h]);return b},r:function(a){for(var b,c,d=a[1],e=a[0],f=this.createKeyFunction(d,[r,t]),g=1/0,h=0;hc&&(g=c,b=e[h]);return b},createKeyFunction:function(a,b){var c=this,d=this.a,e=function(e){var f=d.visit(a,e);if(b.indexOf(c._(f))<0){var g="TypeError: expected one of "+b+", received "+c._(f);throw new Error(g)}return f};return e}},a.tokenize=o,a.compile=n,a.search=p,a.strictDeepEqual=d}("undefined"==typeof exports?this.jmespath={}:exports); \ No newline at end of file diff --git a/bower.json b/bower.json deleted file mode 100644 index f0f626c..0000000 --- a/bower.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "jmespath.js", - "main": "jmespath.js", - "version": "0.11.0", - "homepage": "https://github.com/jmespath/jmespath.js", - "authors": [ - "James Saryerwinnie " - ], - "description": "JMESPath implementation in javascript", - "moduleType": [ - "node" - ], - "keywords": [ - "jmespath" - ], - "license": "MIT", - "ignore": [ - "**/.*", - "node_modules", - "bower_components", - "test", - "tests" - ] -} diff --git a/package.json b/package.json deleted file mode 100644 index 67a8d0e..0000000 --- a/package.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "name": "jmespath", - "description": "JMESPath implementation in javascript", - "version": "0.16.0", - "author": { - "name": "James Saryerwinnie", - "email": "js@jamesls.com", - "url": "http://jamesls.com/" - }, - "homepage": "https://github.com/jmespath/jmespath.js", - "contributors": [], - "devDependencies": { - "grunt": "^0.4.5", - "grunt-contrib-jshint": "^0.11.0", - "grunt-contrib-uglify": "^0.11.1", - "grunt-eslint": "^17.3.1", - "mocha": "^2.1.0" - }, - "dependencies": {}, - "main": "jmespath.cjs", - "directories": { - "test": "test" - }, - "engines": { - "node": ">= 0.6.0" - }, - "repository": { - "type": "git", - "url": "git://github.com/jmespath/jmespath.js" - }, - "bugs": { - "url": "http://github.com/jmespath/jmespath.js/issues", - "mail": "" - }, - "license": "Apache-2.0", - "keywords": [ - "jmespath", - "jsonpath", - "json", - "xpath" - ], - "scripts": { - "test": "mocha test/" - } -} diff --git a/test/compliance.js b/test/compliance.js deleted file mode 100644 index 6298e9e..0000000 --- a/test/compliance.js +++ /dev/null @@ -1,56 +0,0 @@ -var fs = require('fs'); -var path = require('path'); -var assert = require('assert'); -var jmespath = require('../jmespath'); -var search = jmespath.search; - -// Compliance tests that aren't supported yet. -var notImplementedYet = []; - -function endsWith(str, suffix) { - return str.indexOf(suffix, str.length - suffix.length) !== -1; -} - -var listing = fs.readdirSync('test/compliance'); -for (var i = 0; i < listing.length; i++) { - var filename = 'test/compliance/' + listing[i]; - if (fs.statSync(filename).isFile() && endsWith(filename, '.json') && - notImplementedYet.indexOf(path.basename(filename)) === -1) { - addTestSuitesFromFile(filename); - } -} -function addTestSuitesFromFile(filename) { - describe(filename, function() { - var spec = JSON.parse(fs.readFileSync(filename, 'utf-8')); - var errorMsg; - for (var i = 0; i < spec.length; i++) { - var msg = "suite " + i + " for filename " + filename; - describe(msg, function() { - var given = spec[i].given; - var cases = spec[i].cases; - for (var j = 0; j < cases.length; j++) { - var testcase = cases[j]; - if (testcase.error !== undefined) { - // For now just verify that an error is thrown - // for error tests. - (function(testcase, given) { - it('should throw error for test ' + j, function() { - assert.throws( - function() { - search(given, testcase.expression); - }, Error, testcase.expression); - }); - })(testcase, given); - } else { - (function(testcase, given) { - it('should pass test ' + j + " expression: " + testcase.expression, function() { - assert.deepEqual(search(given, testcase.expression), - testcase.result); - }); - })(testcase, given); - } - } - }); - } - }); -} From 147703ecd0fee2196f385d6d34ddb18e4a000207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Tue, 1 Oct 2024 09:31:09 +0200 Subject: [PATCH 06/13] Renamed js files to ts --- lib/{lexer.js => lexer.ts} | 0 lib/{mod.js => mod.ts} | 0 lib/{parser.js => parser.ts} | 0 lib/{runtime.js => runtime.ts} | 0 lib/{tree-interpreter.js => tree-interpreter.ts} | 0 lib/{utils.js => utils.ts} | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename lib/{lexer.js => lexer.ts} (100%) rename lib/{mod.js => mod.ts} (100%) rename lib/{parser.js => parser.ts} (100%) rename lib/{runtime.js => runtime.ts} (100%) rename lib/{tree-interpreter.js => tree-interpreter.ts} (100%) rename lib/{utils.js => utils.ts} (100%) diff --git a/lib/lexer.js b/lib/lexer.ts similarity index 100% rename from lib/lexer.js rename to lib/lexer.ts diff --git a/lib/mod.js b/lib/mod.ts similarity index 100% rename from lib/mod.js rename to lib/mod.ts diff --git a/lib/parser.js b/lib/parser.ts similarity index 100% rename from lib/parser.js rename to lib/parser.ts diff --git a/lib/runtime.js b/lib/runtime.ts similarity index 100% rename from lib/runtime.js rename to lib/runtime.ts diff --git a/lib/tree-interpreter.js b/lib/tree-interpreter.ts similarity index 100% rename from lib/tree-interpreter.js rename to lib/tree-interpreter.ts diff --git a/lib/utils.js b/lib/utils.ts similarity index 100% rename from lib/utils.js rename to lib/utils.ts From daaf9985c5ad5364b2ef635e23fe5940efc124a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Tue, 1 Oct 2024 09:40:56 +0200 Subject: [PATCH 07/13] Refactored codebase to typescript --- lib/errors.ts | 82 +++ lib/lexer.ts | 521 ++++++++++-------- lib/mod.ts | 27 - lib/parser.ts | 718 +++++++++++++------------ lib/runtime.ts | 1135 ++++++++++++++++++++++----------------- lib/structs.ts | 171 ++++++ lib/tree-interpreter.ts | 608 ++++++++++++--------- lib/utils.ts | 312 +++-------- mod.ts | 19 +- 9 files changed, 2011 insertions(+), 1582 deletions(-) create mode 100644 lib/errors.ts delete mode 100644 lib/mod.ts create mode 100644 lib/structs.ts diff --git a/lib/errors.ts b/lib/errors.ts new file mode 100644 index 0000000..a2cf988 --- /dev/null +++ b/lib/errors.ts @@ -0,0 +1,82 @@ +import type { TokenObj } from "./structs.ts"; + +/** + * Is raised when an invalid type is encountered during the evaluation process. + */ +export class InvalidTypeError extends Error { + constructor(message: string) { + const msg = `[invalid-type] ${message}`; + super(msg); + this.name = "InvalidTypeError"; + } +} + +/** + * Is raised when an invalid value is encountered during the evaluation process. + */ +export class InvalidValueError extends Error { + constructor(message: string) { + const msg = `[invalid-value] ${message}`; + super(msg); + this.name = "InvalidValueError"; + } +} + +/** + * Is raised when an unknown function is encountered during the evaluation process. + */ +export class UnknownFunctionError extends Error { + constructor(message: string) { + const msg = `[unknown-function] ${message}`; + super(msg); + this.name = "UnknownFunctionError"; + } +} + +/** + * Is raised when an invalid number of function arguments is encountered during the evaluation process. + */ +export class InvalidArityError extends Error { + constructor(message: string) { + const msg = `[invalid-arity] ${message}`; + super(msg); + this.name = "InvalidArityError"; + } +} + +/** + * Is raised when an invalid number of function arguments is encountered during the evaluation process. + */ +export class InvalidSyntaxError extends Error { + constructor(message: string) { + const msg = `[syntax] ${message}`; + super(msg); + this.name = "InvalidSyntaxError"; + } +} + +export const fError = { + invalidToken: (token: TokenObj) => + `Invalid token '${token.type}'(${token.value})`, + unexpectedToken: (token: TokenObj) => + `Unexpected token '${token.type}'(${token.value})`, + expectedValue: (expected: string, actual: string) => + `Expected ${expected}, got: ${actual}`, + expectedAguments: ( + name: string, + lengthExpected: number, + lengthActual: number, + ) => + `ArgumentError: ${name}() takes at least ${lengthExpected} argument(s) but received ${lengthActual},`, +} as const; + +export const ErrorCodeToErrorMap: Record< + string, + // deno-lint-ignore no-explicit-any + new (...args: any[]) => Error +> = { + "invalid-type": InvalidTypeError, + "invalid-value": InvalidValueError, + "unknown-function": UnknownFunctionError, + "invalid-arity": InvalidArityError, +} as const; diff --git a/lib/lexer.ts b/lib/lexer.ts index b22d9ef..ab09ac9 100644 --- a/lib/lexer.ts +++ b/lib/lexer.ts @@ -1,247 +1,300 @@ +import { InvalidValueError } from "./errors.ts"; import { - TYPE_ANY, - TOK_AND, - TOK_COLON, - TOK_COMMA, - TOK_CURRENT, - TYPE_ARRAY, - TOK_DOT,TOK_EOF,TOK_EQ,TOK_EXPREF,TOK_FILTER,TOK_FLATTEN,TOK_GT,TOK_GTE,TOK_LBRACE,TOK_LBRACKET,TOK_LITERAL,TOK_LPAREN,TOK_LT,TOK_LTE,TOK_NE,TOK_NOT,TOK_NUMBER,TOK_OR,TOK_PIPE,TOK_QUOTEDIDENTIFIER,TOK_RBRACE,TOK_RBRACKET,TOK_RPAREN,TOK_STAR,TOK_UNQUOTEDIDENTIFIER,TYPE_ARRAY_NUMBER,TYPE_ARRAY_STRING,TYPE_BOOLEAN,TYPE_EXPREF,TYPE_NAME_TABLE,TYPE_NULL,TYPE_NUMBER,TYPE_OBJECT,TYPE_STRING,isAlpha,isAlphaNum,isArray,isFalse,isNum,isObject,basicTokens,bindingPower,objValues,operatorStartToken,skipChars,strictDeepEqual - } from "./utils.js" - - export 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; - }, + isBasicToken, + isOperatorStartToken, + isSkipChars, + type JSONValue, + TOKEN, + type TokenObj, + TOKENS_BASIC_MAP, +} from "./structs.ts"; +import { isAlpha, isAlphaNum, isNum } from "./utils.ts"; - _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); - }, +export class Lexer { + readonly expression: string; - _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)); - }, + #index = 0; - _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("\\'", "'"); - }, + get #currentChar(): string { + return this.expression[this.#index]; + } - _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}; - }, + constructor(expression: string) { + this.expression = expression; + } - _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}; + tokenize(): TokenObj[] { + const tokens: TokenObj[] = []; + this.#index = 0; + let start: number; + let identifier; + let token; + while (this.#index < this.expression.length) { + if (isAlpha(this.#currentChar)) { + start = this.#index; + identifier = this.#consumeUnquotedIdentifier(); + tokens.push({ + type: TOKEN.UnquotedIdentifier, + value: identifier, + start: start, + }); + } else if (isBasicToken(this.#currentChar)) { + tokens.push({ + type: TOKENS_BASIC_MAP[this.#currentChar], + value: this.#currentChar, + start: this.#index, + }); + this.#index++; + } else if (isNum(this.#currentChar)) { + token = this.#consumeNumber(); + tokens.push(token); + } else if (this.#currentChar === "[") { + // No need to increment this._current. This happens + // in _consumeLBracket + token = this.#consumeLBracket(); + tokens.push(token); + } else if (this.#currentChar === '"') { + start = this.#index; + identifier = this.#consumeQuotedIdentifier(); + tokens.push({ + type: TOKEN.QuotedIdentifier, + value: identifier, + start: start, + }); + } else if (this.#currentChar === "'") { + start = this.#index; + identifier = this.#consumeRawStringLiteral(); + tokens.push({ + type: TOKEN.Literal, + value: identifier, + start: start, + }); + } else if (this.#currentChar === "`") { + start = this.#index; + const literal = this.#consumeLiteral(); + tokens.push({ + type: TOKEN.Literal, + value: literal, + start: start, + }); + } else if (isOperatorStartToken(this.#currentChar)) { + const token = this.#consumeOperator(); + if (token) { + tokens.push(token); } - }, - - _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; + } else if (isSkipChars(this.#currentChar)) { + // Ignore whitespace. + this.#index++; + } else if (this.#currentChar === "&") { + start = this.#index; + this.#index++; + if (this.#currentChar === "&") { + this.#index++; + tokens.push({ + type: TOKEN.And, + value: "&&", + start: start, + }); + } else { + tokens.push({ + type: TOKEN.Expref, + value: "&", + start: start, + }); } - var literalString = stream.slice(start, this._current).trimStart(); - literalString = literalString.replace("\\`", "`"); - if (this._looksLikeJSON(literalString)) { - literal = JSON.parse(literalString); + } else if (this.#currentChar === "|") { + start = this.#index; + this.#index++; + if (this.#currentChar === "|") { + this.#index++; + tokens.push({ + type: TOKEN.Or, + value: "||", + start: start, + }); } else { - // Try to JSON parse it as "" - literal = JSON.parse("\"" + literalString + "\""); + tokens.push({ + type: TOKEN.Pipe, + value: "|", + start: start, + }); } - // +1 gets us to the ending "`", +1 to move on to the next char. - this._current++; - return literal; - }, + } else { + throw new InvalidValueError( + `Unknown character: '${this.#currentChar}'`, + ); + } + } + return tokens; + } - _looksLikeJSON: function(literalString) { - var startingChars = "[{\""; - var jsonLiterals = ["true", "false", "null"]; - var numberLooking = "-0123456789"; + #consumeUnquotedIdentifier(): string { + const start = this.#index; + this.#index++; + while ( + this.#index < this.expression.length && + isAlphaNum(this.expression[this.#index]) + ) { + this.#index++; + } + return this.expression.slice(start, this.#index); + } - 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; - } + #consumeQuotedIdentifier(): JSONValue { + const start = this.#index; + this.#index++; + const maxLength = this.expression.length; + while (this.expression[this.#index] !== '"' && this.#index < maxLength) { + // You can escape a double quote and you can escape an escape. + let current = this.#index; + if ( + this.expression[current] === "\\" && + (this.expression[current + 1] === "\\" || + this.expression[current + 1] === '"') + ) { + current += 2; + } else { + current++; + } + this.#index = current; } -}; \ No newline at end of file + this.#index++; + return JSON.parse(this.expression.slice(start, this.#index)); + } + + #consumeRawStringLiteral(): string { + const start = this.#index; + this.#index++; + const maxLength = this.expression.length; + while (this.expression[this.#index] !== "'" && this.#index < maxLength) { + // You can escape a single quote and you can escape an escape. + let current = this.#index; + if ( + this.expression[current] === "\\" && + (this.expression[current + 1] === "\\" || + this.expression[current + 1] === "'") + ) { + current += 2; + } else { + current++; + } + this.#index = current; + } + this.#index++; + const literal = this.expression.slice(start + 1, this.#index - 1); + return literal.replace("\\'", "'"); + } + + #consumeNumber(): TokenObj { + const start = this.#index; + this.#index++; + const maxLength = this.expression.length; + while (isNum(this.expression[this.#index]) && this.#index < maxLength) { + this.#index++; + } + const value = parseInt(this.expression.slice(start, this.#index)); + return { type: TOKEN.Number, value: value, start: start }; + } + + #consumeLBracket(): TokenObj { + const start = this.#index; + this.#index++; + if (this.expression[this.#index] === "?") { + this.#index++; + return { type: TOKEN.Filter, value: "[?", start: start }; + } else if (this.expression[this.#index] === "]") { + this.#index++; + return { type: TOKEN.Flatten, value: "[]", start: start }; + } else { + return { type: TOKEN.Lbracket, value: "[", start: start }; + } + } + + #consumeOperator(): TokenObj | undefined { + const start = this.#index; + const startingChar = this.expression[start]; + this.#index++; + if (startingChar === "!") { + if (this.expression[this.#index] === "=") { + this.#index++; + return { type: TOKEN.Ne, value: "!=", start: start }; + } else { + return { type: TOKEN.Not, value: "!", start: start }; + } + } else if (startingChar === "<") { + if (this.expression[this.#index] === "=") { + this.#index++; + return { type: TOKEN.Lte, value: "<=", start: start }; + } else { + return { type: TOKEN.Lt, value: "<", start: start }; + } + } else if (startingChar === ">") { + if (this.expression[this.#index] === "=") { + this.#index++; + return { type: TOKEN.Gte, value: ">=", start: start }; + } else { + return { type: TOKEN.Gt, value: ">", start: start }; + } + } else if (startingChar === "=") { + if (this.expression[this.#index] === "=") { + this.#index++; + return { type: TOKEN.Eq, value: "==", start: start }; + } + } + } + + #consumeLiteral(): JSONValue { + this.#index++; + const start = this.#index; + const maxLength = this.expression.length; + let literal: JSONValue; + while (this.expression[this.#index] !== "`" && this.#index < maxLength) { + // You can escape a literal char or you can escape the escape. + let current = this.#index; + if ( + this.expression[current] === "\\" && + (this.expression[current + 1] === "\\" || + this.expression[current + 1] === "`") + ) { + current += 2; + } else { + current++; + } + this.#index = current; + } + const literalString = this.expression.slice(start, this.#index).trimStart() + .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.#index++; + return literal; + } + + #looksLikeJSON(literalString: string): boolean { + const startingChars = '[{"'; + const jsonLiterals = ["true", "false", "null"]; + const 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 { + return false; + } + } else { + return false; + } + } +} diff --git a/lib/mod.ts b/lib/mod.ts deleted file mode 100644 index fe86e87..0000000 --- a/lib/mod.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {Lexer} from "./lexer.js" -import {Parser} from "./parser.js" -import {Runtime} from "./runtime.js" -import {TreeInterpreter} from "./tree-interpreter.js" - -export function compile(stream) { - var parser = new Parser(); - var ast = parser.parse(stream); - return ast; - } - - export function tokenize(stream) { - var lexer = new Lexer(); - return lexer.tokenize(stream); - } - - export 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); - } diff --git a/lib/parser.ts b/lib/parser.ts index 42ea390..7303e63 100644 --- a/lib/parser.ts +++ b/lib/parser.ts @@ -1,365 +1,413 @@ +import { fError, InvalidTypeError, InvalidValueError } from "./errors.ts"; +import { Lexer } from "./lexer.ts"; import { - TYPE_ANY, - TOK_AND, - TOK_COLON, - TOK_COMMA, - TOK_CURRENT, - TYPE_ARRAY, - TOK_DOT,TOK_EOF,TOK_EQ,TOK_EXPREF,TOK_FILTER,TOK_FLATTEN,TOK_GT,TOK_GTE,TOK_LBRACE,TOK_LBRACKET,TOK_LITERAL,TOK_LPAREN,TOK_LT,TOK_LTE,TOK_NE,TOK_NOT,TOK_NUMBER,TOK_OR,TOK_PIPE,TOK_QUOTEDIDENTIFIER,TOK_RBRACE,TOK_RBRACKET,TOK_RPAREN,TOK_STAR,TOK_UNQUOTEDIDENTIFIER,TYPE_ARRAY_NUMBER,TYPE_ARRAY_STRING,TYPE_BOOLEAN,TYPE_EXPREF,TYPE_NAME_TABLE,TYPE_NULL,TYPE_NUMBER,TYPE_OBJECT,TYPE_STRING,isAlpha,isAlphaNum,isArray,isFalse,isNum,isObject,basicTokens,bindingPower,objValues,operatorStartToken,skipChars,strictDeepEqual -} from "./utils.js" -import { Lexer } from "./lexer.js"; -export function Parser() { -} + BindingPowerToken, + type JSONValue, + TOKEN, + type Token, + type TokenObj, +} from "./structs.ts"; -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; - }, +export type ParserAst = { + type: string; + jmespathType?: typeof TOKEN.Expref; + value?: number; + name?: string; + children?: (JSONValue)[]; +}; - _loadTokens: function(expression) { - var lexer = new Lexer(); - var tokens = lexer.tokenize(expression); - tokens.push({type: TOK_EOF, value: "", start: expression.length}); - this.tokens = tokens; - }, +export class Parser { + readonly lexer: Lexer; + readonly tokens: TokenObj[]; - 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; - }, + #index: number = 0; - _lookahead: function(number) { - return this.tokens[this.index + number].type; - }, + constructor(expression: string) { + this.lexer = new Lexer(expression); - _lookaheadToken: function(number) { - return this.tokens[this.index + number]; - }, + this.tokens = this.lexer.tokenize(); + this.tokens.push({ + type: TOKEN.Eof, + value: "", + start: expression.length, + }); + } - _advance: function() { - this.index++; - }, + parse(): ParserAst { + this.#index = 0; + const ast = this.#expression(0); + if (this.#lookahead() !== TOKEN.Eof) { + const t = this.#lookaheadToken(); + throw new InvalidValueError(fError.unexpectedToken(t)); + } + return ast; + } - 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."); - } - return node; - 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"}; + #expression(rbp: number): ParserAst { + const leftToken = this.#lookaheadToken(); + this.#advance(); + + let left = this.#nud(leftToken); + + let currentToken = this.#lookahead(); + while (rbp < BindingPowerToken[currentToken]) { + this.#advance(); + left = this.#led(currentToken, left); + currentToken = this.#lookahead(); + } + return left; + } + + #lookahead(number: number = 0): Token { + return this.tokens[this.#index + number].type; + } + + #lookaheadToken(number: number = 0): TokenObj { + return this.tokens[this.#index + number]; + } + + #advance(): void { + this.#index++; + } + + #nud(token: TokenObj): ParserAst { + switch (token.type) { + case TOKEN.Literal: + return { type: "Literal", value: token.value as number }; + case TOKEN.UnquotedIdentifier: + return { type: "Field", name: token.value as string }; + case TOKEN.QuotedIdentifier: { + const node = { type: "Field", name: token.value as string }; + if (this.#lookahead() === TOKEN.Lparen) { + throw new InvalidValueError( + "Quoted identifier not allowed for function names.", + ); + } + return node; + } + case TOKEN.Not: { + const right = this.#expression(BindingPowerToken.Not); + return { type: "NotExpression", children: [right] }; + } + case TOKEN.Star: { + const left = { type: "Identity" }; + let right = null; + if (this.#lookahead() === TOKEN.Rbracket) { + // This can happen in a multiselect, + // [a, b, *] + right = { type: "Identity" }; + } else { + right = this.#parseProjectionRHS(BindingPowerToken.Star); + } + return { type: "ValueProjection", children: [left, right] }; + } + case TOKEN.Filter: + return this.#led(token.type, { type: "Identity" }); + case TOKEN.Lbrace: + return this.#parseMultiselectHash(); + case TOKEN.Flatten: { + const left = { type: TOKEN.Flatten, children: [{ type: "Identity" }] }; + const right = this.#parseProjectionRHS(BindingPowerToken.Flatten); + return { type: "Projection", children: [left, right] }; + } + case TOKEN.Lbracket: + if ( + this.#lookahead() === TOKEN.Number || + this.#lookahead() === TOKEN.Colon + ) { + const right = this.#parseIndexExpression(); + return this.#projectIfSlice({ type: "Identity" }, right); + } else if ( + this.#lookahead() === TOKEN.Star && + this.#lookahead(1) === TOKEN.Rbracket + ) { + this.#advance(); + this.#advance(); + const right = this.#parseProjectionRHS(BindingPowerToken.Star); + return { + type: "Projection", + children: [{ type: "Identity" }, right], + }; + } + return this.#parseMultiselectList(); + case TOKEN.Current: + return { type: TOKEN.Current }; + case TOKEN.Expref: { + const expression = this.#expression(BindingPowerToken.Expref); + return { type: "ExpressionReference", children: [expression] }; + } + case TOKEN.Lparen: { + const args = []; + let expression: ParserAst; + while (this.#lookahead() !== TOKEN.Rparen) { + if (this.#lookahead() === TOKEN.Current) { + expression = { type: TOKEN.Current }; + this.#advance(); } else { - right = this._parseProjectionRHS(bindingPower.Star); + expression = this.#expression(0); } - 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]}; - } - return this._parseMultiselectList(); - 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); + args.push(expression); + } + this.#match(TOKEN.Rparen); + return args[0]; } - }, + default: + throw new InvalidValueError(fError.invalidToken(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]}; - } - // Creating a projection. - this._advance(); - right = this._parseProjectionRHS(rbp); - return {type: "ValueProjection", children: [left, right]}; - 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"}; + #led(tokenName: Token, left: ParserAst): ParserAst { + switch (tokenName) { + case TOKEN.Dot: { + const rbp = BindingPowerToken.Dot; + if (this.#lookahead() !== TOKEN.Star) { + const right = this.#parseDotRHS(rbp); + return { type: "Subexpression", children: [left, right] }; + } + // Creating a projection. + this.#advance(); + const right = this.#parseProjectionRHS(rbp); + return { type: "ValueProjection", children: [left, right] }; + } + case TOKEN.Pipe: { + const right = this.#expression(BindingPowerToken.Pipe); + return { type: TOKEN.Pipe, children: [left, right] }; + } + case TOKEN.Or: { + const right = this.#expression(BindingPowerToken.Or); + return { type: "OrExpression", children: [left, right] }; + } + case TOKEN.And: { + const right = this.#expression(BindingPowerToken.And); + return { type: "AndExpression", children: [left, right] }; + } + case TOKEN.Lparen: { + const name = (left as { type: string; name: JSONValue }).name; + const args = []; + let expression; + while (this.#lookahead() !== TOKEN.Rparen) { + if (this.#lookahead() === TOKEN.Current) { + expression = { type: TOKEN.Current }; + this.#advance(); } else { - right = this._parseProjectionRHS(bindingPower.Filter); + expression = this.#expression(0); } - 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); + if (this.#lookahead() === TOKEN.Comma) { + this.#match(TOKEN.Comma); } - this._match(TOK_STAR); - this._match(TOK_RBRACKET); - right = this._parseProjectionRHS(bindingPower.Star); - return {type: "Projection", children: [left, right]}; - default: - this._errorToken(this._lookaheadToken(0)); + args.push(expression); + } + this.#match(TOKEN.Rparen); + const node = { type: "Function", name: name as string, children: args }; + return node; } - }, - - _match: function(tokenType) { - if (this._lookahead(0) === tokenType) { - this._advance(); + case TOKEN.Filter: { + const condition = this.#expression(0); + this.#match(TOKEN.Rbracket); + let right: ParserAst | undefined; + if (this.#lookahead() === TOKEN.Flatten) { + right = { type: "Identity" }; } else { - var t = this._lookaheadToken(0); - var error = new Error("Expected " + tokenType + ", got: " + t.type); - error.name = "ParserError"; - throw error; + right = this.#parseProjectionRHS(BindingPowerToken.Filter); } - }, - - _errorToken: function(token) { - var error = new Error("Invalid token (" + - token.type + "): \"" + - token.value + "\""); - error.name = "ParserError"; - throw error; - }, + return { + type: "FilterProjection", + children: [left, right, condition], + }; + } + case TOKEN.Flatten: { + const leftNode = { type: TOKEN.Flatten, children: [left] }; + const rightNode = this.#parseProjectionRHS( + BindingPowerToken.Flatten, + ); + return { type: "Projection", children: [leftNode, rightNode] }; + } + case TOKEN.Eq: + case TOKEN.Ne: + case TOKEN.Gt: + case TOKEN.Gte: + case TOKEN.Lt: + case TOKEN.Lte: + return this.#parseComparator(left, tokenName); + case TOKEN.Lbracket: { + const token = this.#lookaheadToken(); + if (token.type === TOKEN.Number || token.type === TOKEN.Colon) { + const right = this.#parseIndexExpression(); + return this.#projectIfSlice(left, right); + } + this.#match(TOKEN.Star); + this.#match(TOKEN.Rbracket); + const right = this.#parseProjectionRHS(BindingPowerToken.Star); + return { type: "Projection", children: [left, right] }; + } + default: + throw new InvalidValueError( + fError.invalidToken(this.#lookaheadToken()), + ); + } + } + #match(tokenType: Token): void { + if (this.#lookahead() === tokenType) { + this.#advance(); + } else { + const t = this.#lookaheadToken(); + throw new InvalidTypeError(fError.expectedValue(tokenType, t.type)); + } + } - _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; - } - }, + #parseIndexExpression(): ParserAst { + if ( + this.#lookahead() === TOKEN.Colon || + this.#lookahead(1) === TOKEN.Colon + ) { + return this.#parseSliceExpression(); + } else { + const node = { + type: "Index", + value: this.#lookaheadToken().value as number, + }; + this.#advance(); + this.#match(TOKEN.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; - } - }, + #projectIfSlice(left: ParserAst, right: ParserAst): ParserAst { + const indexExpr = { type: "IndexExpression", children: [left, right] }; + if (right.type === "Slice") { + return { + type: "Projection", + children: [ + indexExpr, + this.#parseProjectionRHS(BindingPowerToken.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 - }; - }, + #parseSliceExpression(): ParserAst { + // [start:end:step] where each part is optional, as well as the last + // colon. + const parts: [number | null, number | null, number | null] = [ + null, + null, + null, + ]; + let index = 0; + let currentToken = this.#lookahead(); + while (currentToken !== TOKEN.Rbracket && index < 3) { + if (currentToken === TOKEN.Colon) { + index++; + this.#advance(); + } else if (currentToken === TOKEN.Number) { + parts[index] = this.#lookaheadToken().value as number; + this.#advance(); + } else { + const t = this.#lookaheadToken(); + throw new InvalidValueError(fError.unexpectedToken(t)); + } + currentToken = this.#lookahead(); + } + this.#match(TOKEN.Rbracket); + return { + type: "Slice", + children: parts, + }; + } - _parseComparator: function(left, comparator) { - var right = this.expression(bindingPower[comparator]); - return {type: "Comparator", name: comparator, children: [left, right]}; - }, + #parseComparator(left: ParserAst, comparator: Token): ParserAst { + const right = this.#expression(BindingPowerToken[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(); - } - }, + #parseDotRHS(rbp: number): ParserAst | undefined { + const lookahead = this.#lookaheadToken(); + const exprTokens: string[] = [ + TOKEN.UnquotedIdentifier, + TOKEN.QuotedIdentifier, + TOKEN.Star, + ]; + if (exprTokens.includes(lookahead.type)) { + return this.#expression(rbp); + } else if (lookahead.type === TOKEN.Lbracket) { + this.#match(TOKEN.Lbracket); + return this.#parseMultiselectList(); + } else if (lookahead.type === TOKEN.Lbrace) { + this.#match(TOKEN.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; - }, + #parseProjectionRHS(rbp: number): ParserAst | undefined { + let right: ParserAst | undefined; + if (BindingPowerToken[this.#lookahead()] < 10) { + right = { type: "Identity" }; + } else if (this.#lookahead() === TOKEN.Lbracket) { + right = this.#expression(rbp); + } else if (this.#lookahead() === TOKEN.Filter) { + right = this.#expression(rbp); + } else if (this.#lookahead() === TOKEN.Dot) { + this.#match(TOKEN.Dot); + right = this.#parseDotRHS(rbp); + } else { + const t = this.#lookaheadToken(); + throw new InvalidValueError(fError.unexpectedToken(t)); + } + 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"); - } - } + #parseMultiselectList(): ParserAst { + const expressions = []; + while (this.#lookahead() !== TOKEN.Rbracket) { + const expression = this.#expression(0); + expressions.push(expression); + if (this.#lookahead() === TOKEN.Comma) { + this.#match(TOKEN.Comma); + const token = this.#lookaheadToken(); + if (token.type === TOKEN.Rbracket) { + throw new InvalidValueError(fError.unexpectedToken(token)); } - this._match(TOK_RBRACKET); - return {type: "MultiSelectList", children: expressions}; - }, + } + } + this.#match(TOKEN.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; - } + #parseMultiselectHash(): ParserAst { + const pairs = []; + const identifierTypes: string[] = [ + TOKEN.UnquotedIdentifier, + TOKEN.QuotedIdentifier, + ]; + let keyToken, keyName, value, node; + while (true) { + keyToken = this.#lookaheadToken(); + if (!identifierTypes.includes(keyToken.type)) { + throw new InvalidValueError(fError.expectedValue( + "an identifier token", + keyToken.type, + )); + } + keyName = keyToken.value; + this.#advance(); + this.#match(TOKEN.Colon); + value = this.#expression(0); + node = { type: "KeyValuePair", name: keyName, value: value }; + pairs.push(node); + if (this.#lookahead() === TOKEN.Comma) { + this.#match(TOKEN.Comma); + } else if (this.#lookahead() === TOKEN.Rbrace) { + this.#match(TOKEN.Rbrace); + break; } - return {type: "MultiSelectHash", children: pairs}; } -}; \ No newline at end of file + return { type: "MultiSelectHash", children: pairs }; + } +} diff --git a/lib/runtime.ts b/lib/runtime.ts index 221910d..08afca7 100644 --- a/lib/runtime.ts +++ b/lib/runtime.ts @@ -1,529 +1,702 @@ +import { unreachable } from "@std/assert"; import { - TYPE_ANY, - TOK_AND, - TOK_COLON, - TOK_COMMA, - TOK_CURRENT, - TYPE_ARRAY, - TOK_DOT,TOK_EOF,TOK_EQ,TOK_EXPREF,TOK_FILTER,TOK_FLATTEN,TOK_GT,TOK_GTE,TOK_LBRACE,TOK_LBRACKET,TOK_LITERAL,TOK_LPAREN,TOK_LT,TOK_LTE,TOK_NE,TOK_NOT,TOK_NUMBER,TOK_OR,TOK_PIPE,TOK_QUOTEDIDENTIFIER,TOK_RBRACE,TOK_RBRACKET,TOK_RPAREN,TOK_STAR,TOK_UNQUOTEDIDENTIFIER,TYPE_ARRAY_NUMBER,TYPE_ARRAY_STRING,TYPE_BOOLEAN,TYPE_EXPREF,TYPE_NAME_TABLE,TYPE_NULL,TYPE_NUMBER,TYPE_OBJECT,TYPE_STRING,isAlpha,isAlphaNum,isArray,isFalse,isNum,isObject,basicTokens,bindingPower,objValues,operatorStartToken,skipChars,strictDeepEqual -} from "./utils.js" - -export 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); + fError, + InvalidArityError, + InvalidTypeError, + UnknownFunctionError, +} from "./errors.ts"; +import { + type JSONArray, + type JSONObject, + type JSONValue, + mapTypeCodeToName, + TOKEN, + TYPE_CODE, + TYPE_NAME, + type TypeCode, +} from "./structs.ts"; +import type { TreeInterpreter } from "./tree-interpreter.ts"; +import { assertIsArray, isObject } from "./utils.ts"; +import type { ParserAst } from "./parser.ts"; + +export type RuntimeFunctionTableElementSignature = { + types: TypeCode[]; + constiadic?: true; +}; +export type RuntimeFunctionTableElement = { + // deno-lint-ignore no-explicit-any + func: (resolvedArgs: any) => any; + signature: RuntimeFunctionTableElementSignature[]; +}; +export type RuntimeFunctionTable = Record; + +export class Runtime { + readonly _interpreter: TreeInterpreter; + + readonly functionTable: RuntimeFunctionTable = { + // name: [function, ] + // The can be: + // + // { + // args: [[type1, type2], [type1, type2]], + // constiadic: 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. constiadic is optional + // and if not provided is assumed to be false. + abs: { + func: this.#functionAbs, + signature: [{ types: [TYPE_CODE.Number] }], }, - - _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) { - var expected = currentSpec - .map(function(typeIdentifier) { - return TYPE_NAME_TABLE[typeIdentifier]; - }) - .join(','); - throw new Error("TypeError: " + name + "() " + - "expected argument " + (i + 1) + - " to be type " + expected + - " but received type " + - TYPE_NAME_TABLE[actualType] + " instead."); - } - } + avg: { + func: this.#functionAvg, + signature: [{ types: [TYPE_CODE.ArrayNumber] }], }, - - _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; - } + ceil: { + func: this.#functionCeil, + signature: [{ types: [TYPE_CODE.Number] }], }, - _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; - } - } + contains: { + func: this.#functionContains, + signature: [ + { types: [TYPE_CODE.String, TYPE_CODE.Array] }, + { types: [TYPE_CODE.Any] }, + ], }, - - _functionStartsWith: function(resolvedArgs) { - return resolvedArgs[0].lastIndexOf(resolvedArgs[1]) === 0; + "ends_with": { + func: this.#functionEndsWith, + signature: [ + { types: [TYPE_CODE.String] }, + { types: [TYPE_CODE.String] }, + ], }, - - _functionEndsWith: function(resolvedArgs) { - var searchStr = resolvedArgs[0]; - var suffix = resolvedArgs[1]; - return searchStr.indexOf(suffix, searchStr.length - suffix.length) !== -1; + floor: { + func: this.#functionFloor, + signature: [{ types: [TYPE_CODE.Number] }], }, - - _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; - } + length: { + func: this.#functionLength, + signature: [{ + types: [ + TYPE_CODE.String, + TYPE_CODE.Array, + TYPE_CODE.Object, + ], + }], }, - - _functionAbs: function(resolvedArgs) { - return Math.abs(resolvedArgs[0]); + map: { + func: this.#functionMap, + signature: [{ types: [TYPE_CODE.Expref] }, { + types: [TYPE_CODE.Array], + }], }, - - _functionCeil: function(resolvedArgs) { - return Math.ceil(resolvedArgs[0]); + max: { + func: this.#functionMax, + signature: [{ + types: [ + TYPE_CODE.ArrayNumber, + TYPE_CODE.ArrayString, + ], + }], }, - - _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; + "merge": { + func: this.#functionMerge, + signature: [{ types: [TYPE_CODE.Object], constiadic: true }], }, - - _functionContains: function(resolvedArgs) { - return resolvedArgs[0].indexOf(resolvedArgs[1]) >= 0; + "max_by": { + func: this.#functionMaxBy, + signature: [{ types: [TYPE_CODE.Array] }, { + types: [TYPE_CODE.Expref], + }], }, - - _functionFloor: function(resolvedArgs) { - return Math.floor(resolvedArgs[0]); + sum: { + func: this.#functionSum, + signature: [{ types: [TYPE_CODE.ArrayNumber] }], }, - - _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; - } + "starts_with": { + func: this.#functionStartsWith, + signature: [{ types: [TYPE_CODE.String] }, { + types: [TYPE_CODE.String], + }], }, - - _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; + min: { + func: this.#functionMin, + signature: [{ + types: [ + TYPE_CODE.ArrayNumber, + TYPE_CODE.ArrayString, + ], + }], + }, + "min_by": { + func: this.#functionMinBy, + signature: [ + { types: [TYPE_CODE.Array] }, + { types: [TYPE_CODE.Expref] }, + ], + }, + type: { + func: this.#functionType, + signature: [{ types: [TYPE_CODE.Any] }], + }, + keys: { + func: this.#functionKeys, + signature: [{ types: [TYPE_CODE.Object] }], + }, + values: { + func: this.#functionValues, + signature: [{ types: [TYPE_CODE.Object] }], + }, + sort: { + func: this.#functionSort, + signature: [ + { + types: [ + TYPE_CODE.ArrayString, + TYPE_CODE.ArrayNumber, + ], + }, + ], + }, + "sort_by": { + func: this.#functionSortBy, + signature: [ + { types: [TYPE_CODE.Array] }, + { types: [TYPE_CODE.Expref] }, + ], }, + join: { + func: this.#functionJoin, + signature: [ + { types: [TYPE_CODE.String] }, + { types: [TYPE_CODE.ArrayString] }, + ], + }, + reverse: { + func: this.#functionReverse, + signature: [ + { types: [TYPE_CODE.String, TYPE_CODE.Array] }, + ], + }, + "to_array": { + func: this.#functionToArray, + signature: [{ types: [TYPE_CODE.Any] }], + }, + "to_string": { + func: this.#functionToString, + signature: [{ types: [TYPE_CODE.Any] }], + }, + "to_number": { + func: this.#functionToNumber, + signature: [{ types: [TYPE_CODE.Any] }], + }, + "not_null": { + func: this.#functionNotNull, + signature: [{ types: [TYPE_CODE.Any], constiadic: true }], + }, + } as const; - _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]; + constructor(interpreter: TreeInterpreter) { + this._interpreter = interpreter; + } + + callFunction(name: string, resolvedArgs: JSONValue[]): JSONValue { + const functionEntry = this.functionTable[name]; + if (functionEntry === undefined) { + throw new UnknownFunctionError(`${name}()`); + } + this.#validateArgs(name, resolvedArgs, functionEntry.signature); + return functionEntry.func.call(this, resolvedArgs); + } + + /** + * Validating the args requires validating + * the correct arity and the correct type of each arg. + * If the last argument is declared as constiadic, then we need + * a minimum number of args to be required. Otherwise it has to + * be an exact amount. + */ + #validateArgs( + name: string, + args: JSONValue[], + signature: RuntimeFunctionTableElementSignature[], + ) { + if ( + ( + signature[signature.length - 1].constiadic && + args.length < signature.length + ) || + ( + !signature[signature.length - 1].constiadic && + args.length !== signature.length + ) + ) { + throw new InvalidArityError(fError.expectedAguments( + name, + signature.length, + args.length, + )); + } + let currentSpec; + let actualType: TypeCode; + let typeMatched; + for (const [i, sign] of signature.entries()) { + typeMatched = false; + currentSpec = sign.types; + actualType = this.#getTypeName(args[i]); + for (const spec of currentSpec) { + if (this.#typeMatches(actualType, spec, args[i])) { + typeMatched = true; + break; } } - return merged; - }, + if (!typeMatched) { + const expected = currentSpec + .map(function (typeIdentifier) { + return TYPE_NAME[typeIdentifier]; + }) + .join(","); + throw new InvalidTypeError( + fError.expectedValue( + `${name}() argument ${(i + 1)} to be type ${expected}`, + TYPE_NAME[actualType], + ), + ); + } + } + } - _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]); + #typeMatches( + actual: TypeCode, + expected: TypeCode, + argValue: JSONValue, + ): boolean { + if (expected === TYPE_CODE.Any) { + return true; + } + if ( + expected === TYPE_CODE.ArrayString || + expected === TYPE_CODE.ArrayNumber || + expected === TYPE_CODE.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_CODE.Array) { + return actual === TYPE_CODE.Array; + } else if (actual === TYPE_CODE.Array) { + // Otherwise we need to check subtypes. + // I think this has potential to be improved. + let subtype: TypeCode; + if (expected === TYPE_CODE.ArrayNumber) { + subtype = TYPE_CODE.Number; + } else if (expected === TYPE_CODE.ArrayString) { + subtype = TYPE_CODE.String; } 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 false; + } + + assertIsArray(argValue); + + for (const arg of argValue) { + if ( + !this.#typeMatches( + this.#getTypeName(arg), + subtype, + arg, + ) + ) { + return false; } - return maxElement; } + return true; } else { - return null; + return false; } - }, + } else { + return actual === expected; + } + } - _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]); + #getTypeName(obj: JSONValue): TypeCode { + const objToString = Object.prototype.toString.call(obj); + switch (objToString) { + case "[object String]": + return TYPE_CODE.String; + case "[object Number]": + return TYPE_CODE.Number; + case "[object Array]": + return TYPE_CODE.Array; + case "[object Boolean]": + return TYPE_CODE.Boolean; + case "[object Null]": + return TYPE_CODE.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 as { jmespathType: string }).jmespathType === + TOKEN.Expref + ) { + return TYPE_CODE.Expref; } 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; + return TYPE_CODE.Object; } - } else { - return null; - } - }, + default: + unreachable(`Unexpected object type ${objToString}`); + } + } - _functionSum: function(resolvedArgs) { - var sum = 0; - var listToSum = resolvedArgs[0]; - for (var i = 0; i < listToSum.length; i++) { - sum += listToSum[i]; - } - return sum; - }, + #functionStartsWith(resolvedArgs: [string, string]): boolean { + const searchStr = resolvedArgs[0]; + const prefix = resolvedArgs[1]; + return searchStr.lastIndexOf(prefix) === 0; + } - _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"; - } - }, + #functionEndsWith(resolvedArgs: [string, string]): boolean { + const searchStr = resolvedArgs[0]; + const suffix = resolvedArgs[1]; + return searchStr.indexOf(suffix, searchStr.length - suffix.length) !== + -1; + } - _functionKeys: function(resolvedArgs) { - return Object.keys(resolvedArgs[0]); - }, + #functionReverse(resolvedArgs: JSONValue[]): JSONValue { + const typeName = this.#getTypeName(resolvedArgs[0]); + if (typeName === TYPE_CODE.String) { + const original = resolvedArgs[0] as string; + return original.split("").reverse().join(""); + } else { + const original = resolvedArgs[0] as JSONValue[]; + return original.toReversed(); + } + } - _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; - }, + #functionAbs(resolvedArgs: [number]): number { + return Math.abs(resolvedArgs[0]); + } - _functionJoin: function(resolvedArgs) { - var joinChar = resolvedArgs[0]; - var listJoin = resolvedArgs[1]; - return listJoin.join(joinChar); - }, + #functionCeil(resolvedArgs: [number]): number { + return Math.ceil(resolvedArgs[0]); + } - _functionToArray: function(resolvedArgs) { - if (this._getTypeName(resolvedArgs[0]) === TYPE_ARRAY) { - return resolvedArgs[0]; - } else { - return [resolvedArgs[0]]; - } - }, + #functionAvg(resolvedArgs: [number[]]): number { + const inputArray = resolvedArgs[0]; + const sum = inputArray.reduce((a, b) => a + b, 0); + return (sum / inputArray.length) || 0; + } - _functionToString: function(resolvedArgs) { - if (this._getTypeName(resolvedArgs[0]) === TYPE_STRING) { - return resolvedArgs[0]; - } else { - return JSON.stringify(resolvedArgs[0]); - } - }, + #functionContains(resolvedArgs: [JSONArray, string]): boolean { + return resolvedArgs[0].indexOf(resolvedArgs[1]) >= 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; - }, + #functionFloor(resolvedArgs: [number]): number { + return Math.floor(resolvedArgs[0]); + } - _functionNotNull: function(resolvedArgs) { - for (var i = 0; i < resolvedArgs.length; i++) { - if (this._getTypeName(resolvedArgs[i]) !== TYPE_NULL) { - return resolvedArgs[i]; - } - } - return null; - }, + #functionLength(resolvedArgs: [string | JSONArray | JSONObject]): number { + 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; + } + } - _functionSort: function(resolvedArgs) { - var sortedArray = resolvedArgs[0].slice(0); - sortedArray.sort(); - return sortedArray; - }, + #functionMap(resolvedArgs: [ParserAst, JSONArray]): ParserAst[] { + const mapped = []; + const interpreter = this._interpreter; + const exprefNode = resolvedArgs[0]; + const elements = resolvedArgs[1]; + for (const element of elements) { + mapped.push( + interpreter.visit(exprefNode, element), + ); + } + return mapped as ParserAst[]; + } - _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]; + #functionMerge(resolvedArgs: JSONObject[]): JSONObject { + const merged: JSONObject = {}; + for (const current of resolvedArgs) { + for (const key of Object.keys(current)) { + merged[key] = current[key]; + } + } + return merged; + } + + #functionMax(resolvedArgs: [JSONArray]): string | number | null { + if (resolvedArgs[0].length > 0) { + const typeName = this.#getTypeName(resolvedArgs[0][0]); + if (typeName === TYPE_CODE.Number) { + return Math.max(...resolvedArgs[0] as number[]); + } else { + const elements = resolvedArgs[0] as string[]; + let maxElement = elements[0] as string; + for (const element of elements) { + if (maxElement.localeCompare(element) < 0) { + maxElement = element; } - }); - // Undecorate: extract out the original list elements. - for (var j = 0; j < decorated.length; j++) { - sortedArray[j] = decorated[j][1]; } - return sortedArray; - }, + return maxElement; + } + } else { + return null; + } + } - _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]; + #functionMin(resolvedArgs: [JSONArray]): string | number | null { + const arg1 = resolvedArgs[0]; + if (arg1.length > 0) { + const typeName = this.#getTypeName(arg1[0]); + if (typeName === TYPE_CODE.Number) { + return Math.min(...arg1 as number[]); + } else { + const elements = arg1 as string[]; + let minElement = elements[0] as string; + for (const element of elements) { + if (element.localeCompare(minElement) < 0) { + minElement = element; + } } + return minElement; } - return maxRecord; - }, + } else { + return null; + } + } - _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]; - } + #functionSum(resolvedArgs: [number[]]) { + const inputArray = resolvedArgs[0]; + return inputArray.reduce((a, b) => a + b, 0); + } + + #functionType(resolvedArgs: JSONValue[]): string { + const typeName = this.#getTypeName(resolvedArgs[0]); + switch (typeName) { + case TYPE_CODE.Number: + return "number"; + case TYPE_CODE.String: + return "string"; + case TYPE_CODE.Array: + return "array"; + case TYPE_CODE.Object: + return "object"; + case TYPE_CODE.Boolean: + return "boolean"; + case TYPE_CODE.Expref: + return "expref"; + case TYPE_CODE.Null: + return "null"; + default: + unreachable(`Unexpected type name '${typeName}'`); + } + } + + #functionKeys(resolvedArgs: [JSONObject]): string[] { + return Object.keys(resolvedArgs[0]); + } + + #functionValues(resolvedArgs: [JSONObject]): JSONValue[] { + const obj = resolvedArgs[0]; + const keys = Object.keys(obj); + const values: JSONValue[] = []; + for (const key of keys) { + values.push(obj[key]); + } + return values; + } + + #functionJoin(resolvedArgs: [string, JSONArray]): string { + const joinChar = resolvedArgs[0]; + const listJoin = resolvedArgs[1]; + return listJoin.join(joinChar); + } + + #functionToArray(resolvedArgs: JSONValue[]): JSONArray { + if (this.#getTypeName(resolvedArgs[0]) === TYPE_CODE.Array) { + return resolvedArgs[0] as JSONArray; + } else { + return [resolvedArgs[0]]; + } + } + + #functionToString(resolvedArgs: JSONValue[]): string { + if (this.#getTypeName(resolvedArgs[0]) === TYPE_CODE.String) { + return resolvedArgs[0] as string; + } else { + return JSON.stringify(resolvedArgs[0]); + } + } + + #functionToNumber(resolvedArgs: JSONValue[]): number | null { + const typeName = this.#getTypeName(resolvedArgs[0]); + if (typeName === TYPE_CODE.Number) { + return resolvedArgs[0] as number; + } else if (typeName === TYPE_CODE.String) { + const convertedValue = parseFloat(resolvedArgs[0] as string); + if (!Number.isNaN(convertedValue)) { + return convertedValue; } - return minRecord; - }, + } + return null; + } - 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; + #functionNotNull(resolvedArgs: JSONValue[]): JSONValue | null { + for (const arg of resolvedArgs) { + if (this.#getTypeName(arg) !== TYPE_CODE.Null) { + return arg; + } + } + return null; + } + + #functionSort(resolvedArgs: JSONArray[]): JSONArray { + const sortedArray = resolvedArgs[0].slice(0); + sortedArray.sort(); + return sortedArray; + } + + #functionSortBy(resolvedArgs: [JSONArray, ParserAst]): JSONArray { + 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]), + ); + const validCodeTypes = [TYPE_CODE.Number, TYPE_CODE.String]; + if ( + validCodeTypes.indexOf(requiredType as 0) < 0 + ) { + throw new InvalidTypeError( + fError.expectedValue( + `one of [${mapTypeCodeToName(...validCodeTypes).toString()}]`, + mapTypeCodeToName(requiredType).toString(), + ), + ); + } + // 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. + const decorated: JSONValue[][] = []; + for (const [i, el] of sortedArray.entries()) { + decorated.push([i, el]); + } + 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 InvalidTypeError( + fError.expectedValue( + mapTypeCodeToName(requiredType).toString(), + mapTypeCodeToName(this.#getTypeName(exprA)).toString(), + ), + ); + } else if (this.#getTypeName(exprB) !== requiredType) { + throw new InvalidTypeError( + fError.expectedValue( + mapTypeCodeToName(requiredType).toString(), + mapTypeCodeToName(this.#getTypeName(exprB)).toString(), + ), + ); + } + 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 as number[])[0] - (b as number[])[0]; + } + }); + // Undecorate: extract out the original list elements. + for (const [i, el] of decorated.entries()) { + sortedArray[i] = el[1]; + } + return sortedArray; + } - }; \ No newline at end of file + #functionMaxBy(resolvedArgs: [JSONArray, ParserAst]): JSONValue { + const exprefNode = resolvedArgs[1]; + const resolvedArray = resolvedArgs[0]; + const keyFunction = this.createKeyFunction(exprefNode, [ + TYPE_CODE.Number, + TYPE_CODE.String, + ]); + let maxNumber: number = -Infinity; + let maxRecord: JSONValue = null; + let current: number; + for (const el of resolvedArray) { + current = keyFunction( + el, + ) as unknown as number; + if (current > maxNumber) { + maxNumber = current; + maxRecord = el; + } + } + return maxRecord; + } + + #functionMinBy(resolvedArgs: [JSONArray, ParserAst]): JSONValue { + const exprefNode = resolvedArgs[1]; + const resolvedArray = resolvedArgs[0]; + const keyFunction = this.createKeyFunction(exprefNode, [ + TYPE_CODE.Number, + TYPE_CODE.String, + ]); + let minNumber: number = Infinity; + let minRecord: JSONValue = null; + let current: number; + for (const el of resolvedArray) { + current = keyFunction( + el, + ) as unknown as number; + if (current < minNumber) { + minNumber = current; + minRecord = el; + } + } + return minRecord; + } + + createKeyFunction( + exprefNode: ParserAst, + allowedTypes: TypeCode[], + ): (x: JSONValue) => ParserAst { + const interpreter = this._interpreter; + const keyFunc = (x: JSONValue): ParserAst => { + const current = interpreter.visit(exprefNode, x); + if (allowedTypes.indexOf(this.#getTypeName(current)) < 0) { + throw new InvalidTypeError( + fError.expectedValue( + `one of [${mapTypeCodeToName(...allowedTypes).toString()}]`, + mapTypeCodeToName(this.#getTypeName(current)).toString(), + ), + ); + } + return current as ParserAst; + }; + return keyFunc; + } +} diff --git a/lib/structs.ts b/lib/structs.ts new file mode 100644 index 0000000..8294da3 --- /dev/null +++ b/lib/structs.ts @@ -0,0 +1,171 @@ +export type JSONValue = + | string + | number + | boolean + | null + | undefined + | JSONValue[] + | { [key: string]: JSONValue }; + +export interface JSONObject { + [k: string]: JSONValue; +} +export interface JSONArray extends Array {} + +export type TokenObj = { + type: Token; + value: JSONValue; + start: number; +}; + +export const TYPE_CODE = { + Number: 0, + Any: 1, + String: 2, + Array: 3, + Object: 4, + Boolean: 5, + Expref: 6, + Null: 7, + ArrayNumber: 8, + ArrayString: 9, +} as const; + +export type TypeCode = typeof TYPE_CODE[keyof typeof TYPE_CODE]; + +export const TYPE_NAME = { + [TYPE_CODE.Number]: "number", + [TYPE_CODE.Any]: "any", + [TYPE_CODE.String]: "string", + [TYPE_CODE.Array]: "array", + [TYPE_CODE.Object]: "object", + [TYPE_CODE.Boolean]: "boolean", + [TYPE_CODE.Expref]: "expression", + [TYPE_CODE.Null]: "null", + [TYPE_CODE.ArrayNumber]: "Array", + [TYPE_CODE.ArrayString]: "Array", +}; + +export type TypeName = typeof TYPE_NAME[keyof typeof TYPE_NAME]; + +export function mapTypeCodeToName(...codes: TypeCode[]): TypeName[] { + return codes.map((c) => TYPE_NAME[c]); +} + +export const TOKEN = { + Eof: "EOF", + UnquotedIdentifier: "UnquotedIdentifier", + QuotedIdentifier: "QuotedIdentifier", + Rbracket: "Rbracket", + Rparen: "Rparen", + Comma: "Comma", + Colon: "Colon", + Rbrace: "Rbrace", + Number: "Number", + Current: "Current", + Expref: "Expref", + Pipe: "Pipe", + Or: "Or", + And: "And", + Eq: "EQ", + Gt: "GT", + Lt: "LT", + Gte: "GTE", + Lte: "LTE", + Ne: "NE", + Flatten: "Flatten", + Star: "Star", + Filter: "Filter", + Dot: "Dot", + Not: "Not", + Lbrace: "Lbrace", + Lbracket: "Lbracket", + Lparen: "Lparen", + Literal: "Literal", +} as const; + +export type Token = typeof TOKEN[keyof typeof TOKEN]; + +/** + * The "&", "[", "<", ">" tokens + * are not in basicToken because + * there are two token constiants + * ("&&", "[?", "<=", ">="). This is specially handled + * below. + */ +export const TOKENS_BASIC_MAP = { + ".": TOKEN.Dot, + "*": TOKEN.Star, + ",": TOKEN.Comma, + ":": TOKEN.Colon, + "{": TOKEN.Lbrace, + "}": TOKEN.Rbrace, + "]": TOKEN.Rbracket, + "(": TOKEN.Lparen, + ")": TOKEN.Rparen, + "@": TOKEN.Current, +} as const; + +export type BasicTokens = keyof typeof TOKENS_BASIC_MAP; + +export function isBasicToken(ch: string): ch is BasicTokens { + return Object.keys(TOKENS_BASIC_MAP).includes(ch); +} + +export const TOKEN_OPERATOR_START = { + LeftAngleBracket: "<", + RightAngleBracket: ">", + Equals: "=", + Not: "!", +} as const; + +export type OperatorStartTokens = + typeof TOKEN_OPERATOR_START[keyof typeof TOKEN_OPERATOR_START]; + +export function isOperatorStartToken(ch: string): ch is OperatorStartTokens { + return Object.values(TOKEN_OPERATOR_START).includes(ch); +} + +export const SKIP_CHARS = { + Space: " ", + Tab: "\t", + Newline: "\n", +} as const; + +export type SkipChars = typeof SKIP_CHARS[keyof typeof SKIP_CHARS]; + +export function isSkipChars(ch: string): ch is SkipChars { + return Object.values(SKIP_CHARS).includes(ch); +} + +export const BindingPowerToken: Record = { + [TOKEN.Eof]: 0, + [TOKEN.UnquotedIdentifier]: 0, + [TOKEN.QuotedIdentifier]: 0, + [TOKEN.Rbracket]: 0, + [TOKEN.Rparen]: 0, + [TOKEN.Comma]: 0, + [TOKEN.Rbrace]: 0, + [TOKEN.Number]: 0, + [TOKEN.Current]: 0, + [TOKEN.Expref]: 0, + [TOKEN.Colon]: 0, + [TOKEN.Literal]: 0, + [TOKEN.Pipe]: 1, + [TOKEN.Or]: 2, + [TOKEN.And]: 3, + [TOKEN.Eq]: 5, + [TOKEN.Gt]: 5, + [TOKEN.Lt]: 5, + [TOKEN.Gte]: 5, + [TOKEN.Lte]: 5, + [TOKEN.Ne]: 5, + [TOKEN.Flatten]: 9, + [TOKEN.Star]: 20, + [TOKEN.Filter]: 21, + [TOKEN.Dot]: 40, + [TOKEN.Not]: 45, + [TOKEN.Lbrace]: 50, + [TOKEN.Lbracket]: 55, + [TOKEN.Lparen]: 60, +}; diff --git a/lib/tree-interpreter.ts b/lib/tree-interpreter.ts index d1bd6ec..530549d 100644 --- a/lib/tree-interpreter.ts +++ b/lib/tree-interpreter.ts @@ -1,274 +1,370 @@ import { - TYPE_ANY, - TOK_AND, - TOK_COLON, - TOK_COMMA, - TOK_CURRENT, - TYPE_ARRAY, - TOK_DOT,TOK_EOF,TOK_EQ,TOK_EXPREF,TOK_FILTER,TOK_FLATTEN,TOK_GT,TOK_GTE,TOK_LBRACE,TOK_LBRACKET,TOK_LITERAL,TOK_LPAREN,TOK_LT,TOK_LTE,TOK_NE,TOK_NOT,TOK_NUMBER,TOK_OR,TOK_PIPE,TOK_QUOTEDIDENTIFIER,TOK_RBRACE,TOK_RBRACKET,TOK_RPAREN,TOK_STAR,TOK_UNQUOTEDIDENTIFIER,TYPE_ARRAY_NUMBER,TYPE_ARRAY_STRING,TYPE_BOOLEAN,TYPE_EXPREF,TYPE_NAME_TABLE,TYPE_NULL,TYPE_NUMBER,TYPE_OBJECT,TYPE_STRING,isAlpha,isAlphaNum,isArray,isFalse,isNum,isObject,basicTokens,bindingPower,objValues,operatorStartToken,skipChars,strictDeepEqual -} from "./utils.js" + InvalidSyntaxError, + InvalidTypeError, + InvalidValueError, +} from "./errors.ts"; +import type { Parser, ParserAst } from "./parser.ts"; +import { Runtime } from "./runtime.ts"; +import { type JSONValue, TOKEN } from "./structs.ts"; +import { isFalsy, isObject } from "./utils.ts"; +import { equal } from "@std/assert"; -export function TreeInterpreter(runtime) { - this.runtime = runtime; +export class TreeInterpreter { + runtime: Runtime; + readonly data: JSONValue; + + constructor(data: JSONValue) { + this.data = data; + this.runtime = new Runtime(this); } - TreeInterpreter.prototype = { - search: function(node, value) { - return this.visit(node, value); - }, + search(parser: Parser): JSONValue { + const ast = parser.parse(); + return this.visit(ast, this.data); + } - visit: function(node, value) { - var matched, current, result, first, second, field, left, right, collected, i; - switch (node.type) { - case "Field": - if (value !== null && isObject(value)) { - field = value[node.name]; - if (field === undefined) { - return null; - } else { - return field; - } - } - return null; - 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); + visit(node: ParserAst | undefined, value: JSONValue): JSONValue { + if (!node) { + throw new InvalidSyntaxError(`Node was not defined (${typeof node})`); + } - 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); + switch (node.type) { + case "Field": + if (value !== null && isObject(value)) { + const field = value[node.name!]; + if (field === undefined) { + return null; + } else { + return field; } - }, + } + return null; + case "Subexpression": { + const children = node.children as ParserAst[]; - 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; + let result = this.visit(children[0], value); + for (let i = 1; i < children.length; i++) { + result = this.visit(children[1], result!); + if (result === null) { + return null; + } } - var stepValueNegative = step < 0 ? true : false; + return result; + } + case "IndexExpression": { + const children = node.children as ParserAst[]; - if (start === null) { - start = stepValueNegative ? arrayLength - 1 : 0; + const left = this.visit(children[0], value); + const right = this.visit(children[1], left!); + return right; + } + case "Index": { + if (!Array.isArray(value)) { + return null; + } + let index = node.value!; + if (index < 0) { + index = value.length + index; + } + let result = value[index]; + if (result === undefined) { + result = null; + } + return result; + } + case "Slice": { + if (!Array.isArray(value)) { + return null; + } + const sliceParams = node.children!.slice( + 0, + ) as number[]; + const computed = this.computeSliceParams( + value.length, + sliceParams, + ); + const start = computed[0]; + const stop = computed[1]; + const step = computed[2]; + const result: number[] = []; + if (step > 0) { + for (let i = start; i < stop; i += step) { + result.push(value[i] as number); + } } else { - start = this.capSliceRange(arrayLength, start, step); + for (let i = start; i > stop; i += step) { + result.push(value[i] as number); + } + } + return result; + } + case "Projection": { + const children = node.children as ParserAst[]; + + // Evaluate left child. + const base = this.visit( + children[0] as ParserAst, + value, + ); + if (!Array.isArray(base)) { + return null; + } + const collected = []; + for (const el of base) { + const current = this.visit( + children[1], + el, + ); + if (current !== null) { + collected.push(current); + } } + return collected; + } + case "ValueProjection": { + const children = node.children as ParserAst[]; - if (stop === null) { - stop = stepValueNegative ? -1 : arrayLength; - } else { - stop = this.capSliceRange(arrayLength, stop, step); + // Evaluate left child. + const base = this.visit( + children[0], + value, + ); + if (!isObject(base)) { + return null; + } + const collected = []; + const values = Object.values(base); + for (const value of values) { + const current = this.visit( + children[1], + value, + ); + if (current !== null) { + collected.push(current); + } } - computed[0] = start; - computed[1] = stop; - computed[2] = step; - return computed; - }, + return collected; + } + case "FilterProjection": { + const children = node.children as ParserAst[]; - 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; + const base = this.visit( + children[0], + value, + ); + if (!Array.isArray(base)) { + return null; + } + const filtered = []; + const finalResults = []; + for (const el of base) { + const matched = this.visit( + children[2], + el, + ); + if (!isFalsy(matched)) { + filtered.push(el); + } + } + for (const el of filtered) { + const current = this.visit( + children[1], + el, + ); + if (current !== null) { + finalResults.push(current); + } + } + return finalResults; + } + case "Comparator": { + const children = node.children as ParserAst[]; + const first = this.visit( + children[0], + value, + ) as number; + const second = this.visit( + children[1], + value, + ) as number; + switch (node.name) { + case TOKEN.Eq: + return equal(first, second); + case TOKEN.Ne: + return !equal(first, second); + case TOKEN.Gt: + return first > second; + case TOKEN.Gte: + return first >= second; + case TOKEN.Lt: + return first < second; + case TOKEN.Lte: + return first <= second; + default: + throw new InvalidValueError( + "Unknown comparator: " + node.name, + ); + } + } + case TOKEN.Flatten: { + const children = node.children as ParserAst[]; + const original = this.visit( + children[0], + value, + ); + if (!Array.isArray(original)) { + return null; + } + const merged = []; + for (const current of original) { + if (Array.isArray(current)) { + merged.push(...current); + } else { + merged.push(current); } - return actualValue; + } + return merged; + } + case "Identity": + return value; + case "MultiSelectList": { + if (value === null) { + return null; + } + const children = node.children as ParserAst[]; + const collected = []; + for (const child of children) { + collected.push( + this.visit(child, value), + ); + } + return collected; + } + case "MultiSelectHash": { + if (value === null) { + return null; + } + const children = node.children as ParserAst[]; + const collected: Record = {}; + for (const child of children) { + collected[child.name!] = this.visit( + child.value as unknown as ParserAst, + value, + ); + } + return collected; + } + case "OrExpression": { + const children = node.children as ParserAst[]; + const matched = this.visit( + children[0], + value, + ); + if (isFalsy(matched)) { + return this.visit(children[1], value); + } + return matched; + } + case "AndExpression": { + const children = node.children as ParserAst[]; + const first = this.visit( + children[0], + value, + ); + + if (isFalsy(first)) { + return first; + } + return this.visit(children[1], value); + } + case "NotExpression": { + const children = node.children as ParserAst[]; + const first = this.visit( + children[0], + value, + ); + return isFalsy(first); + } + case "Literal": + return node.value; + case TOKEN.Pipe: { + const children = node.children as ParserAst[]; + const left = this.visit( + children[0], + value, + ); + return this.visit(children[1], left); + } + case TOKEN.Current: + return value; + case "Function": { + const children = node.children as ParserAst[]; + const resolvedArgs = []; + for (const child of children) { + resolvedArgs.push( + this.visit(child, value), + ); + } + return this.runtime.callFunction( + node.name!, + resolvedArgs, + ); + } + case "ExpressionReference": { + const children = node.children as ParserAst[]; + const refNode = children[0]; + // Tag the node with a specific attribute so the type + // checker verify the type. + refNode!.jmespathType = TOKEN.Expref; + return refNode as JSONValue; } + default: + throw new InvalidTypeError( + "Unknown node type: " + node.type, + ); + } + } + + computeSliceParams(arrayLength: number, sliceParams: number[]): number[] { + let start = sliceParams[0]; + let stop = sliceParams[1]; + let step = sliceParams[2]; + if (step === null) { + step = 1; + } else if (step === 0) { + throw new InvalidValueError( + "Invalid slice, step cannot be 0", + ); + } + const stepValueNegative = step < 0 ? true : false; - }; \ No newline at end of file + 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); + } + return [start, stop, step]; + } + + capSliceRange( + arrayLength: number, + actualValue: number, + step: number, + ): number { + 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; + } +} diff --git a/lib/utils.ts b/lib/utils.ts index 51edcc2..d95d5a0 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,251 +1,69 @@ -export function isArray(obj) { - if (obj !== null) { - return Object.prototype.toString.call(obj) === "[object Array]"; - } else { - return false; - } - } - - export function isObject(obj) { - if (obj !== null) { - return Object.prototype.toString.call(obj) === "[object Object]"; - } else { - return false; - } - } - - export 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; - } +import type { JSONObject } from "./structs.ts"; +import { InvalidTypeError } from "./errors.ts"; + +/** + * Checks if a value is an object + */ +export function isObject(obj: unknown): obj is JSONObject { + return typeof obj === "object" && !Array.isArray(obj) && obj !== null; +} + +/** + * Checking if character is an alfabetic character or an underscore + */ +export function isAlpha(ch: string): boolean { + return (ch >= "a" && ch <= "z") || + (ch >= "A" && ch <= "Z") || + ch === "_"; +} + +/** + * Checking if character is a numeric character or an dash + */ +export function isNum(ch: string): boolean { + return (ch >= "0" && ch <= "9") || + ch === "-"; +} + +/** + * Checking if character is an alfanumeric character or an underscore + */ +export function isAlphaNum(ch: string): boolean { + return isAlpha(ch) || + (ch >= "0" && ch <= "9"); +} + +/** + * Checks if the value is falsy + * + * From the spec: + * A false value corresponds to the following values: + * - Empty list + * - Empty object + * - Empty string + * - False boolean + * - null value + */ +export function isFalsy(obj: unknown): boolean { + // First check the scalar values. + if ( + obj === "" || obj === false || obj === null || obj === undefined + ) { + return true; + } else if (Array.isArray(obj)) { + // Check for an empty array. + return obj.length < 1; + } else if (isObject(obj)) { + // Check for an empty object. + return Object.keys(obj).length < 1; + } else { return false; } +} - export 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; - } +// deno-lint-ignore no-explicit-any +export function assertIsArray(value: unknown): asserts value is any[] { + if (!Array.isArray(value)) { + throw new InvalidTypeError(`Value is not array, but '${typeof value}'`); } - - export 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; - } - - export 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; - } - - // Type constants used to define functions. - export var TYPE_NUMBER = 0; - export var TYPE_ANY = 1; - export var TYPE_STRING = 2; - export var TYPE_ARRAY = 3; - export var TYPE_OBJECT = 4; - export var TYPE_BOOLEAN = 5; - export var TYPE_EXPREF = 6; - export var TYPE_NULL = 7; - export var TYPE_ARRAY_NUMBER = 8; - export var TYPE_ARRAY_STRING = 9; - export var TYPE_NAME_TABLE = { - 0: 'number', - 1: 'any', - 2: 'string', - 3: 'array', - 4: 'object', - 5: 'boolean', - 6: 'expression', - 7: 'null', - 8: 'Array', - 9: 'Array' - }; - - export var TOK_EOF = "EOF"; - export var TOK_UNQUOTEDIDENTIFIER = "UnquotedIdentifier"; - export var TOK_QUOTEDIDENTIFIER = "QuotedIdentifier"; - export var TOK_RBRACKET = "Rbracket"; - export var TOK_RPAREN = "Rparen"; - export var TOK_COMMA = "Comma"; - export var TOK_COLON = "Colon"; - export var TOK_RBRACE = "Rbrace"; - export var TOK_NUMBER = "Number"; - export var TOK_CURRENT = "Current"; - export var TOK_EXPREF = "Expref"; - export var TOK_PIPE = "Pipe"; - export var TOK_OR = "Or"; - export var TOK_AND = "And"; - export var TOK_EQ = "EQ"; - export var TOK_GT = "GT"; - export var TOK_LT = "LT"; - export var TOK_GTE = "GTE"; - export var TOK_LTE = "LTE"; - export var TOK_NE = "NE"; - export var TOK_FLATTEN = "Flatten"; - export var TOK_STAR = "Star"; - export var TOK_FILTER = "Filter"; - export var TOK_DOT = "Dot"; - export var TOK_NOT = "Not"; - export var TOK_LBRACE = "Lbrace"; - export var TOK_LBRACKET = "Lbracket"; - export var TOK_LPAREN= "Lparen"; - export var TOK_LITERAL= "Literal"; - - // The "&", "[", "<", ">" tokens - // are not in basicToken because - // there are two token variants - // ("&&", "[?", "<=", ">="). This is specially handled - // below. - - export var basicTokens = { - ".": TOK_DOT, - "*": TOK_STAR, - ",": TOK_COMMA, - ":": TOK_COLON, - "{": TOK_LBRACE, - "}": TOK_RBRACE, - "]": TOK_RBRACKET, - "(": TOK_LPAREN, - ")": TOK_RPAREN, - "@": TOK_CURRENT - }; - - export var operatorStartToken = { - "<": true, - ">": true, - "=": true, - "!": true - }; - - export var skipChars = { - " ": true, - "\t": true, - "\n": true - }; - - - export function isAlpha(ch) { - return (ch >= "a" && ch <= "z") || - (ch >= "A" && ch <= "Z") || - ch === "_"; - } - - export function isNum(ch) { - return (ch >= "0" && ch <= "9") || - ch === "-"; - } - export function isAlphaNum(ch) { - return (ch >= "a" && ch <= "z") || - (ch >= "A" && ch <= "Z") || - (ch >= "0" && ch <= "9") || - ch === "_"; - } - - - - export 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; +} diff --git a/mod.ts b/mod.ts index 7c4d089..684ec2d 100644 --- a/mod.ts +++ b/mod.ts @@ -1,3 +1,18 @@ -import { search } from "./lib/mod.js"; +import { Parser } from "./lib/parser.ts"; +import type { JSONValue } from "./lib/structs.ts"; +import { TreeInterpreter } from "./lib/tree-interpreter.ts"; -export { search } +export class JmesPath { + data: JSONValue; + interpreter: TreeInterpreter; + + constructor(data: JSONValue) { + this.data = data; + + this.interpreter = new TreeInterpreter(this.data); + } + search(expression: string): JSONValue { + const parser = new Parser(expression); + return this.interpreter.search(parser); + } +} From 92f24ea3d2a5c3628f95b1860741e92d506ceb67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Tue, 1 Oct 2024 09:41:34 +0200 Subject: [PATCH 08/13] renamed jp.js to bin.ts --- jp.js => bin.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename jp.js => bin.ts (100%) diff --git a/jp.js b/bin.ts similarity index 100% rename from jp.js rename to bin.ts From 57ecc9f5e6110143193de64c65895ac42ccb0f8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Tue, 1 Oct 2024 09:42:00 +0200 Subject: [PATCH 09/13] Refactored bin.ts to typescript --- bin.ts | 39 ++++++++++++++++++++------------------- deno.json | 3 ++- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/bin.ts b/bin.ts index c152108..063b928 100755 --- a/bin.ts +++ b/bin.ts @@ -1,23 +1,24 @@ -#!/usr/bin/env node -jmespath = require('./jmespath'); +#!/usr/bin/env -S deno run -process.stdin.setEncoding('utf-8'); +import { JmesPath } from "./mod.ts"; +if (Deno.args.length < 1) { + console.error("Must provide a jmespath expression."); + Deno.exit(1); +} + +const inputJSON: string[] = []; -if (process.argv.length < 2) { - console.log("Must provide a jmespath expression."); - process.exit(1); +const decoder = new TextDecoder(); +for await (const chunk of Deno.stdin.readable) { + const text = decoder.decode(chunk); + inputJSON.push(text); } -var inputJSON = ""; - -process.stdin.on('readable', function() { - var chunk = process.stdin.read(); - if (chunk !== null) { - inputJSON += chunk; - } -}); - -process.stdin.on('end', function() { - var parsedInput = JSON.parse(inputJSON); - console.log(JSON.stringify(jmespath.search(parsedInput, process.argv[2]))); -}); + +const expression = Deno.args[0]; + +const parsedInput = JSON.parse(inputJSON.join("")); + +const res = new JmesPath(parsedInput).search(expression); + +console.info(JSON.stringify(res)); diff --git a/deno.json b/deno.json index dd3302f..38e4052 100644 --- a/deno.json +++ b/deno.json @@ -2,7 +2,8 @@ "name": "@halvardm/jmespath", "version": "0.17.0", "exports": { - ".":"./mod.ts" + ".": "./mod.ts", + "./bin": "./bin.ts" }, "tasks": { "test": "deno test --allow-read", From bf133a4d6b6350495b862652df02b48fd07c13be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Tue, 1 Oct 2024 09:51:54 +0200 Subject: [PATCH 10/13] Updated and refactored tests --- lib/errors.test.ts | 47 +++++ lib/lexer.test.ts | 362 +++++++++++++++++++++++++++++++++++ lib/parser.bench.ts | 12 +- lib/parser.test.ts | 29 +++ lib/runtime.test.ts | 16 ++ lib/tree-interpreter.test.ts | 19 ++ lib/utils.test.ts | 63 ++++++ test/compliance.test.ts | 15 +- test/jmespath.js | 234 ---------------------- 9 files changed, 551 insertions(+), 246 deletions(-) create mode 100644 lib/errors.test.ts create mode 100644 lib/lexer.test.ts create mode 100644 lib/parser.test.ts create mode 100644 lib/runtime.test.ts create mode 100644 lib/tree-interpreter.test.ts create mode 100644 lib/utils.test.ts delete mode 100644 test/jmespath.js diff --git a/lib/errors.test.ts b/lib/errors.test.ts new file mode 100644 index 0000000..e4b4845 --- /dev/null +++ b/lib/errors.test.ts @@ -0,0 +1,47 @@ +import { assertThrows } from "@std/assert"; +import { + InvalidArityError, + InvalidTypeError, + InvalidValueError, + UnknownFunctionError, +} from "./errors.ts"; + +Deno.test("InvalidTypeError > throws", () => { + assertThrows( + () => { + throw new InvalidTypeError("test"); + }, + InvalidTypeError, + "[invalid-type] test", + ); +}); + +Deno.test("InvalidValueError > throws", () => { + assertThrows( + () => { + throw new InvalidValueError("test"); + }, + InvalidValueError, + "[invalid-value] test", + ); +}); + +Deno.test("UnknownFunctionError > throws", () => { + assertThrows( + () => { + throw new UnknownFunctionError("test"); + }, + UnknownFunctionError, + "[unknown-function] test", + ); +}); + +Deno.test("InvalidArityError > throws", () => { + assertThrows( + () => { + throw new InvalidArityError("test"); + }, + InvalidArityError, + "[invalid-arity] test", + ); +}); diff --git a/lib/lexer.test.ts b/lib/lexer.test.ts new file mode 100644 index 0000000..0d99e5e --- /dev/null +++ b/lib/lexer.test.ts @@ -0,0 +1,362 @@ +import { assertEquals, assertThrows } from "@std/assert"; +import { Lexer } from "./lexer.ts"; +import type { TokenObj } from "./structs.ts"; +import { InvalidValueError } from "./errors.ts"; + +Deno.test("Lexer > single_expr", () => { + const expected: TokenObj[] = [ + { + start: 0, + type: "UnquotedIdentifier", + value: "foo", + }, + ]; + const l = new Lexer("foo"); + assertEquals(l.tokenize(), expected); +}); + +Deno.test("Lexer > single_subexpr", () => { + const expected: TokenObj[] = [ + { + start: 0, + type: "UnquotedIdentifier", + value: "foo", + }, + { + start: 3, + type: "Dot", + value: ".", + }, + { + start: 4, + type: "UnquotedIdentifier", + value: "bar", + }, + ]; + const l = new Lexer("foo.bar"); + assertEquals(l.tokenize(), expected); +}); + +Deno.test("Lexer > should tokenize unquoted identifier with underscore", () => { + const expected: TokenObj[] = [ + { + type: "UnquotedIdentifier", + value: "_underscore", + start: 0, + }, + ]; + const l = new Lexer("_underscore"); + assertEquals(l.tokenize(), expected); +}); +Deno.test("Lexer > should tokenize unquoted identifier with numbers", () => { + const expected: TokenObj[] = [ + { + type: "UnquotedIdentifier", + value: "foo123", + start: 0, + }, + ]; + const l = new Lexer("foo123"); + assertEquals(l.tokenize(), expected); +}); + +Deno.test("Lexer > should tokenize numbers", () => { + const expected: TokenObj[] = [ + { type: "UnquotedIdentifier", value: "foo", start: 0 }, + { type: "Lbracket", value: "[", start: 3 }, + { type: "Number", value: 0, start: 4 }, + { type: "Rbracket", value: "]", start: 5 }, + ]; + const l = new Lexer("foo[0]"); + assertEquals(l.tokenize(), expected); +}); +Deno.test("Lexer > should tokenize numbers with multiple digits", () => { + const expected: TokenObj[] = [ + { type: "Number", value: 12345, start: 0 }, + ]; + const l = new Lexer("12345"); + assertEquals(l.tokenize(), expected); +}); +Deno.test("Lexer > should tokenize negative numbers", () => { + const expected: TokenObj[] = [ + { type: "Number", value: -12345, start: 0 }, + ]; + const l = new Lexer("-12345"); + assertEquals(l.tokenize(), expected); +}); +Deno.test("Lexer > should tokenize quoted identifier", () => { + const expected: TokenObj[] = [ + { + type: "QuotedIdentifier", + value: "foo", + start: 0, + }, + ]; + const l = new Lexer('"foo"'); + assertEquals(l.tokenize(), expected); +}); +Deno.test("Lexer > should tokenize quoted identifier with unicode escape", () => { + const expected: TokenObj[] = [ + { + type: "QuotedIdentifier", + value: "✓", + start: 0, + }, + ]; + const l = new Lexer('"\\u2713"'); + assertEquals(l.tokenize(), expected); +}); +Deno.test("Lexer > should tokenize literal lists", () => { + const expected: TokenObj[] = [ + { + type: "Literal", + value: [0, 1], + start: 0, + }, + ]; + const l = new Lexer("`[0, 1]`"); + assertEquals(l.tokenize(), expected); +}); +Deno.test("Lexer > should tokenize literal dict", () => { + const expected: TokenObj[] = [ + { + type: "Literal", + value: { "foo": "bar" }, + start: 0, + }, + ]; + const l = new Lexer('`{"foo": "bar"}`'); + assertEquals(l.tokenize(), expected); +}); +Deno.test("Lexer > should tokenize literal strings", () => { + const expected: TokenObj[] = [ + { + type: "Literal", + value: "foo", + start: 0, + }, + ]; + const l = new Lexer('`"foo"`'); + assertEquals(l.tokenize(), expected); +}); +Deno.test("Lexer > should tokenize json literals", () => { + const expected: TokenObj[] = [ + { + type: "Literal", + value: true, + start: 0, + }, + ]; + const l = new Lexer("`true`"); + assertEquals(l.tokenize(), expected); +}); +Deno.test("Lexer > should not requiring surrounding quotes for strings", () => { + const expected: TokenObj[] = [ + { + type: "Literal", + value: "foo", + start: 0, + }, + ]; + const l = new Lexer("`foo`"); + assertEquals(l.tokenize(), expected); +}); +Deno.test("Lexer > should not requiring surrounding quotes for numbers", () => { + const expected: TokenObj[] = [ + { + type: "Literal", + value: 20, + start: 0, + }, + ]; + const l = new Lexer("`20`"); + assertEquals(l.tokenize(), expected); +}); +Deno.test("Lexer > should tokenize literal lists with chars afterwards", () => { + const expected: TokenObj[] = [ + { type: "Literal", value: [0, 1], start: 0 }, + { type: "Lbracket", value: "[", start: 8 }, + { type: "Number", value: 0, start: 9 }, + { type: "Rbracket", value: "]", start: 10 }, + ]; + const l = new Lexer("`[0, 1]`[0]"); + assertEquals(l.tokenize(), expected); +}); +Deno.test("Lexer > should tokenize two char tokens with shared prefix", () => { + const expected: TokenObj[] = [ + { type: "Filter", value: "[?", start: 0 }, + { type: "UnquotedIdentifier", value: "foo", start: 2 }, + { type: "Rbracket", value: "]", start: 5 }, + ]; + const l = new Lexer("[?foo]"); + assertEquals(l.tokenize(), expected); +}); +Deno.test("Lexer > should tokenize flatten operator", () => { + const expected: TokenObj[] = [ + { type: "Flatten", value: "[]", start: 0 }, + ]; + const l = new Lexer("[]"); + assertEquals(l.tokenize(), expected); +}); +Deno.test("Lexer > should tokenize comparators", () => { + const expected: TokenObj[] = [ + { type: "LT", value: "<", start: 0 }, + ]; + const l = new Lexer("<"); + assertEquals(l.tokenize(), expected); +}); +Deno.test("Lexer > should tokenize two char tokens without shared prefix", () => { + const expected: TokenObj[] = [ + { type: "EQ", value: "==", start: 0 }, + ]; + const l = new Lexer("=="); + assertEquals(l.tokenize(), expected); +}); +Deno.test("Lexer > should tokenize not equals", () => { + const expected: TokenObj[] = [ + { type: "NE", value: "!=", start: 0 }, + ]; + const l = new Lexer("!="); + assertEquals(l.tokenize(), expected); +}); +Deno.test("Lexer > should tokenize the OR token", () => { + const expected: TokenObj[] = [ + { type: "UnquotedIdentifier", value: "a", start: 0 }, + { type: "Or", value: "||", start: 1 }, + { type: "UnquotedIdentifier", value: "b", start: 3 }, + ]; + const l = new Lexer("a||b"); + assertEquals(l.tokenize(), expected); +}); +Deno.test("Lexer > should tokenize function calls", () => { + const expected: TokenObj[] = [ + { type: "UnquotedIdentifier", value: "abs", start: 0 }, + { type: "Lparen", value: "(", start: 3 }, + { type: "Current", value: "@", start: 4 }, + { type: "Rparen", value: ")", start: 5 }, + ]; + const l = new Lexer("abs(@)"); + assertEquals(l.tokenize(), expected); +}); + +Deno.test("Lexer > index", () => { + const expected: TokenObj[] = [ + { + start: 0, + type: "Lbracket", + value: "[", + }, + { + start: 1, + type: "Number", + value: 1, + }, + { + start: 2, + type: "Rbracket", + value: "]", + }, + { + start: 3, + type: "Lbracket", + value: "[", + }, + { + start: 4, + type: "Number", + value: 0, + }, + { + start: 5, + type: "Rbracket", + value: "]", + }, + ]; + const l = new Lexer( + "[1][0]", + ); + assertEquals(l.tokenize(), expected); +}); + +Deno.test("Lexer > all paths", () => { + const expected: TokenObj[] = [ + { + start: 0, + type: "UnquotedIdentifier", + value: "f", + }, + { + start: 1, + type: "Lbracket", + value: "[", + }, + { + start: 2, + type: "Number", + value: 1, + }, + { + start: 3, + type: "Rbracket", + value: "]", + }, + { + start: 4, + type: "QuotedIdentifier", + value: "a", + }, + { + start: 7, + type: "Literal", + value: "b", + }, + { + start: 10, + type: "Literal", + value: "c", + }, + { + start: 13, + type: "And", + value: "&&", + }, + { + start: 15, + type: "LT", + value: "<", + }, + { + start: 16, + type: "Pipe", + value: "|", + }, + { + start: 17, + type: "UnquotedIdentifier", + value: "a", + }, + { + start: 18, + type: "Expref", + value: "&", + }, + { + start: 19, + type: "Or", + value: "||", + }, + ]; + const l = new Lexer("f[1]\"a\"'b'`c`&&<|a&||"); + assertEquals(l.tokenize(), expected); +}); + +Deno.test("Lexer > throws", () => { + const l = new Lexer("%"); + assertThrows( + () => { + l.tokenize(); + }, + InvalidValueError, + "[invalid-value] Unknown character: '%'", + ); +}); diff --git a/lib/parser.bench.ts b/lib/parser.bench.ts index a3d13d6..f4608d7 100644 --- a/lib/parser.bench.ts +++ b/lib/parser.bench.ts @@ -1,25 +1,25 @@ -import { compile } from "./mod.js"; +import { Parser } from "./parser.ts"; Deno.bench("Parser > single_expr", () => { - compile("foo"); + new Parser("foo"); }); Deno.bench("Parser > single_subexpr", function () { - compile("foo.bar"); + new Parser("foo.bar"); }); Deno.bench("Parser > deeply_nested_50", function () { - compile( + new Parser( "j49.j48.j47.j46.j45.j44.j43.j42.j41.j40.j39.j38.j37.j36.j35.j34.j33.j32.j31.j30.j29.j28.j27.j26.j25.j24.j23.j22.j21.j20.j19.j18.j17.j16.j15.j14.j13.j12.j11.j10.j9.j8.j7.j6.j5.j4.j3.j2.j1.j0", ); }); Deno.bench("Parser > deeply_nested_50_index", function () { - compile( + new Parser( "[49][48][47][46][45][44][43][42][41][40][39][38][37][36][35][34][33][32][31][30][29][28][27][26][25][24][23][22][21][20][19][18][17][16][15][14][13][12][11][10][9][8][7][6][5][4][3][2][1][0]", ); }); Deno.bench("Parser > basic_list_projection", function () { - compile("foo[*].bar"); + new Parser("foo[*].bar"); }); diff --git a/lib/parser.test.ts b/lib/parser.test.ts new file mode 100644 index 0000000..c50f7c2 --- /dev/null +++ b/lib/parser.test.ts @@ -0,0 +1,29 @@ +import { assertEquals } from "@std/assert/equals"; +import { Parser } from "./parser.ts"; + +Deno.test("Parser > nested", function () { + const p = new Parser("foo.bar"); + assertEquals(p.tokens, [ + { start: 0, type: "UnquotedIdentifier", value: "foo" }, + { start: 3, type: "Dot", value: "." }, + { start: 4, type: "UnquotedIdentifier", value: "bar" }, + { start: 7, type: "EOF", value: "" }, + ]); + const t = p.parse(); + assertEquals( + t, + { + children: [ + { + name: "foo", + type: "Field", + }, + { + name: "bar", + type: "Field", + }, + ], + type: "Subexpression", + }, + ); +}); diff --git a/lib/runtime.test.ts b/lib/runtime.test.ts new file mode 100644 index 0000000..f66b1a2 --- /dev/null +++ b/lib/runtime.test.ts @@ -0,0 +1,16 @@ +import { assert, assertFalse, assertThrows } from "@std/assert"; +import { TreeInterpreter } from "./tree-interpreter.ts"; +import { InvalidArityError } from "./errors.ts"; + +Deno.test("Runtime > call not_null", () => { + const t = new TreeInterpreter({}); + assert(t.runtime.callFunction("not_null", ["null"])); + assertFalse(t.runtime.callFunction("not_null", [null])); + assertThrows( + () => { + t.runtime.callFunction("not_null", []); + }, + InvalidArityError, + "[invalid-arity] ArgumentError: not_null() takes at least 1 argument(s) but received 0", + ); +}); diff --git a/lib/tree-interpreter.test.ts b/lib/tree-interpreter.test.ts new file mode 100644 index 0000000..c5164e0 --- /dev/null +++ b/lib/tree-interpreter.test.ts @@ -0,0 +1,19 @@ +import { assertThrows } from "@std/assert"; +import { Parser } from "./parser.ts"; +import { TreeInterpreter } from "./tree-interpreter.ts"; +import { InvalidTypeError } from "./errors.ts"; + +Deno.test( + "TreeInterpreter > should throw a readable error when invalid arguments are provided to a function", + () => { + const parser = new Parser("length(`null`)"); + const interpreter = new TreeInterpreter([]); + assertThrows( + () => { + interpreter.search(parser); + }, + InvalidTypeError, + "[invalid-type] Expected length() argument 1 to be type string,array,object, got: null", + ); + }, +); diff --git a/lib/utils.test.ts b/lib/utils.test.ts new file mode 100644 index 0000000..4d50e2c --- /dev/null +++ b/lib/utils.test.ts @@ -0,0 +1,63 @@ +import { assert, assertFalse } from "@std/assert"; +import { isAlpha, isAlphaNum, isFalsy, isNum } from "./utils.ts"; + +Deno.test("isAlpha", () => { + assert(isAlpha("a")); + assert(isAlpha("m")); + assert(isAlpha("z")); + assert(isAlpha("A")); + assert(isAlpha("M")); + assert(isAlpha("Z")); + assert(isAlpha("_")); + assertFalse(isAlpha("-")); + assertFalse(isAlpha("?")); + assertFalse(isAlpha("@")); + assertFalse(isAlpha("[")); + assertFalse(isAlpha("{")); +}); + +Deno.test("isNum", () => { + assert(isNum("0")); + assert(isNum("5")); + assert(isNum("9")); + assert(isNum("-")); + assertFalse(isNum("/")); + assertFalse(isNum("_")); + assertFalse(isNum(":")); +}); + +Deno.test("isAlphaNum", () => { + assert(isAlphaNum("0")); + assert(isAlphaNum("5")); + assert(isAlphaNum("9")); + assertFalse(isAlphaNum("-")); + assertFalse(isAlphaNum("/")); + assertFalse(isAlphaNum(":")); + + assert(isAlphaNum("a")); + assert(isAlphaNum("m")); + assert(isAlphaNum("z")); + assert(isAlphaNum("A")); + assert(isAlphaNum("M")); + assert(isAlphaNum("Z")); + assert(isAlphaNum("_")); + assertFalse(isAlphaNum("?")); + assertFalse(isAlphaNum("@")); + assertFalse(isAlphaNum("[")); + assertFalse(isAlphaNum("{")); +}); + +Deno.test("isFalsy", () => { + assert(isFalsy("")); + assert(isFalsy(false)); + assert(isFalsy(null)); + assert(isFalsy(undefined)); + assert(isFalsy([])); + assert(isFalsy({})); + + assertFalse(isFalsy("a")); + assertFalse(isFalsy("false")); + assertFalse(isFalsy(true)); + assertFalse(isFalsy([0])); + assertFalse(isFalsy({ 0: 1 })); +}); diff --git a/test/compliance.test.ts b/test/compliance.test.ts index 02ffe8c..196cd3b 100644 --- a/test/compliance.test.ts +++ b/test/compliance.test.ts @@ -1,11 +1,12 @@ import { join, resolve } from "@std/path"; import { assertEquals, assertThrows } from "@std/assert"; - -import { search } from "../lib/mod.js"; +import { JmesPath } from "../mod.ts"; +import type { JSONValue } from "../lib/structs.ts"; +import { ErrorCodeToErrorMap } from "../lib/errors.ts"; type ComplianceTestCases = { expression: string; - result: unknown; + result: JSONValue; error?: string; }; type ComplianceTest = { @@ -40,9 +41,11 @@ for (const listing of listings) { await t.step("should throw error for test " + j, () => { assertThrows( () => { - search(given, testCase.expression); + new JmesPath(given).search(testCase.expression); }, - Error, + ErrorCodeToErrorMap[testCase.error!], + testCase.error, + `failed for ${testCase.expression} expected ${testCase.error}`, ); }); } else { @@ -50,7 +53,7 @@ for (const listing of listings) { `should pass test ${j} expression: ${testCase.expression}`, () => { assertEquals( - search(given, testCase.expression), + new JmesPath(given).search(testCase.expression), testCase.result, ); }, diff --git a/test/jmespath.js b/test/jmespath.js deleted file mode 100644 index 98d4ead..0000000 --- a/test/jmespath.js +++ /dev/null @@ -1,234 +0,0 @@ -var assert = require('assert'); -var jmespath = require('../jmespath'); -var tokenize = jmespath.tokenize; -var compile = jmespath.compile; -var strictDeepEqual = jmespath.strictDeepEqual; - - -describe('tokenize', function() { - it('should tokenize unquoted identifier', function() { - assert.deepEqual(tokenize('foo'), - [{type: "UnquotedIdentifier", - value: "foo", - start: 0}]); - }); - it('should tokenize unquoted identifier with underscore', function() { - assert.deepEqual(tokenize('_underscore'), - [{type: "UnquotedIdentifier", - value: "_underscore", - start: 0}]); - }); - it('should tokenize unquoted identifier with numbers', function() { - assert.deepEqual(tokenize('foo123'), - [{type: "UnquotedIdentifier", - value: "foo123", - start: 0}]); - }); - it('should tokenize dotted lookups', function() { - assert.deepEqual( - tokenize('foo.bar'), - [{type: "UnquotedIdentifier", value: "foo", start: 0}, - {type: "Dot", value: ".", start: 3}, - {type: "UnquotedIdentifier", value: "bar", start: 4}, - ]); - }); - it('should tokenize numbers', function() { - assert.deepEqual( - tokenize('foo[0]'), - [{type: "UnquotedIdentifier", value: "foo", start: 0}, - {type: "Lbracket", value: "[", start: 3}, - {type: "Number", value: 0, start: 4}, - {type: "Rbracket", value: "]", start: 5}, - ]); - }); - it('should tokenize numbers with multiple digits', function() { - assert.deepEqual( - tokenize("12345"), - [{type: "Number", value: 12345, start: 0}]); - }); - it('should tokenize negative numbers', function() { - assert.deepEqual( - tokenize("-12345"), - [{type: "Number", value: -12345, start: 0}]); - }); - it('should tokenize quoted identifier', function() { - assert.deepEqual(tokenize('"foo"'), - [{type: "QuotedIdentifier", - value: "foo", - start: 0}]); - }); - it('should tokenize quoted identifier with unicode escape', function() { - assert.deepEqual(tokenize('"\\u2713"'), - [{type: "QuotedIdentifier", - value: "✓", - start: 0}]); - }); - it('should tokenize literal lists', function() { - assert.deepEqual(tokenize("`[0, 1]`"), - [{type: "Literal", - value: [0, 1], - start: 0}]); - }); - it('should tokenize literal dict', function() { - assert.deepEqual(tokenize("`{\"foo\": \"bar\"}`"), - [{type: "Literal", - value: {"foo": "bar"}, - start: 0}]); - }); - it('should tokenize literal strings', function() { - assert.deepEqual(tokenize("`\"foo\"`"), - [{type: "Literal", - value: "foo", - start: 0}]); - }); - it('should tokenize json literals', function() { - assert.deepEqual(tokenize("`true`"), - [{type: "Literal", - value: true, - start: 0}]); - }); - it('should not requiring surrounding quotes for strings', function() { - assert.deepEqual(tokenize("`foo`"), - [{type: "Literal", - value: "foo", - start: 0}]); - }); - it('should not requiring surrounding quotes for numbers', function() { - assert.deepEqual(tokenize("`20`"), - [{type: "Literal", - value: 20, - start: 0}]); - }); - it('should tokenize literal lists with chars afterwards', function() { - assert.deepEqual( - tokenize("`[0, 1]`[0]"), [ - {type: "Literal", value: [0, 1], start: 0}, - {type: "Lbracket", value: "[", start: 8}, - {type: "Number", value: 0, start: 9}, - {type: "Rbracket", value: "]", start: 10} - ]); - }); - it('should tokenize two char tokens with shared prefix', function() { - assert.deepEqual( - tokenize("[?foo]"), - [{type: "Filter", value: "[?", start: 0}, - {type: "UnquotedIdentifier", value: "foo", start: 2}, - {type: "Rbracket", value: "]", start: 5}] - ); - }); - it('should tokenize flatten operator', function() { - assert.deepEqual( - tokenize("[]"), - [{type: "Flatten", value: "[]", start: 0}]); - }); - it('should tokenize comparators', function() { - assert.deepEqual(tokenize("<"), - [{type: "LT", - value: "<", - start: 0}]); - }); - it('should tokenize two char tokens without shared prefix', function() { - assert.deepEqual( - tokenize("=="), - [{type: "EQ", value: "==", start: 0}] - ); - }); - it('should tokenize not equals', function() { - assert.deepEqual( - tokenize("!="), - [{type: "NE", value: "!=", start: 0}] - ); - }); - it('should tokenize the OR token', function() { - assert.deepEqual( - tokenize("a||b"), - [ - {type: "UnquotedIdentifier", value: "a", start: 0}, - {type: "Or", value: "||", start: 1}, - {type: "UnquotedIdentifier", value: "b", start: 3} - ] - ); - }); - it('should tokenize function calls', function() { - assert.deepEqual( - tokenize("abs(@)"), - [ - {type: "UnquotedIdentifier", value: "abs", start: 0}, - {type: "Lparen", value: "(", start: 3}, - {type: "Current", value: "@", start: 4}, - {type: "Rparen", value: ")", start: 5} - ] - ); - }); - -}); - - -describe('parsing', function() { - it('should parse field node', function() { - assert.deepEqual(compile('foo'), - {type: 'Field', name: 'foo'}); - }); -}); - -describe('strictDeepEqual', function() { - it('should compare scalars', function() { - assert.strictEqual(strictDeepEqual('a', 'a'), true); - }); - it('should be false for different types', function() { - assert.strictEqual(strictDeepEqual('a', 2), false); - }); - it('should be false for arrays of different lengths', function() { - assert.strictEqual(strictDeepEqual([0, 1], [1, 2, 3]), false); - }); - it('should be true for identical arrays', function() { - assert.strictEqual(strictDeepEqual([0, 1], [0, 1]), true); - }); - it('should be true for nested arrays', function() { - assert.strictEqual( - strictDeepEqual([[0, 1], [2, 3]], [[0, 1], [2, 3]]), true); - }); - it('should be true for nested arrays of strings', function() { - assert.strictEqual( - strictDeepEqual([["a", "b"], ["c", "d"]], - [["a", "b"], ["c", "d"]]), true); - }); - it('should be false for different arrays of the same length', function() { - assert.strictEqual(strictDeepEqual([0, 1], [1, 2]), false); - }); - it('should handle object literals', function() { - assert.strictEqual(strictDeepEqual({a: 1, b: 2}, {a: 1, b: 2}), true); - }); - it('should handle keys in first not in second', function() { - assert.strictEqual(strictDeepEqual({a: 1, b: 2}, {a: 1}), false); - }); - it('should handle keys in second not in first', function() { - assert.strictEqual(strictDeepEqual({a: 1}, {a: 1, b: 2}), false); - }); - it('should handle nested objects', function() { - assert.strictEqual( - strictDeepEqual({a: {b: [1, 2]}}, - {a: {b: [1, 2]}}), true); - }); - it('should handle nested objects that are not equal', function() { - assert.strictEqual( - strictDeepEqual({a: {b: [1, 2]}}, - {a: {b: [1, 4]}}), false); - }); -}); - -describe('search', function() { - it( - 'should throw a readable error when invalid arguments are provided to a function', - function() { - try { - jmespath.search([], 'length(`null`)'); - } catch (e) { - assert(e.message.search( - 'expected argument 1 to be type string,array,object' - ), e.message); - assert(e.message.search('received type null'), e.message); - } - } - ); -}); From 2a04a2ffbdaaec1f12a5d56ee06272f68aa0592b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Tue, 1 Oct 2024 09:52:00 +0200 Subject: [PATCH 11/13] updated readme --- README.md | 85 +++++++++++++++++++++++++++---------------------------- 1 file changed, 41 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index b1110c2..6f908e8 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,54 @@ -# jmespath.js +# jmespath.ts -[![Build Status](https://travis-ci.org/jmespath/jmespath.js.png?branch=master)](https://travis-ci.org/jmespath/jmespath.js) +jmespath.ts is a TypeScript implementation of JMESPath, which is a query +language for JSON. It will take a JSON document and transform it into another +JSON document through a JMESPath expression. -jmespath.js is a javascript implementation of JMESPath, -which is a query language for JSON. It will take a JSON -document and transform it into another JSON document -through a JMESPath expression. +Using jmespath.ts is really easy. There's a single class you use, `JmesPath`: -Using jmespath.js is really easy. There's a single function -you use, `jmespath.search`: +```ts +import { JmesPath } from "jsr:@halvardm/jmespath"; - -``` -> var jmespath = require('jmespath'); -> jmespath.search({foo: {bar: {baz: [0, 1, 2, 3, 4]}}}, "foo.bar.baz[2]") -2 +const jp = new JmesPath({ foo: { bar: { baz: [0, 1, 2, 3, 4] } } }); +console.log(jp.search("foo.bar.baz[2]")); // 2 ``` -In the example we gave the ``search`` function input data of -`{foo: {bar: {baz: [0, 1, 2, 3, 4]}}}` as well as the JMESPath -expression `foo.bar.baz[2]`, and the `search` function evaluated -the expression against the input data to produce the result ``2``. - -The JMESPath language can do a lot more than select an element -from a list. Here are a few more examples: - -``` -> jmespath.search({foo: {bar: {baz: [0, 1, 2, 3, 4]}}}, "foo.bar") -{ baz: [ 0, 1, 2, 3, 4 ] } - -> jmespath.search({"foo": [{"first": "a", "last": "b"}, - {"first": "c", "last": "d"}]}, - "foo[*].first") -[ 'a', 'c' ] - -> jmespath.search({"foo": [{"age": 20}, {"age": 25}, - {"age": 30}, {"age": 35}, - {"age": 40}]}, - "foo[?age > `30`]") -[ { age: 35 }, { age: 40 } ] +The JMESPath language can do a lot more than select an element from a list. Here +are a few more examples: + +```ts +import { JmesPath } from "jsr:@halvardm/jmespath"; + +const jp = new JmesPath({ foo: { bar: { baz: [0, 1, 2, 3, 4] } } }); +console.log(jp.search("foo.bar")); +// { baz: [ 0, 1, 2, 3, 4 ] } + +const jp = new JmesPath({ + foo: [ + { first: "a", last: "b" }, + { first: "c", last: "d" }, + ], +}); +console.log(jp.search("foo[*].first")); +// [ 'a', 'c' ] + +const jp = new JmesPath({ + foo: [{ age: 20 }, { age: 25 }, { age: 30 }, { age: 35 }, { age: 40 }], +}); +console.log(jp.search("foo[?age > `30`]")); +// [ { age: 35 }, { age: 40 } ] ``` ## More Resources -The example above only show a small amount of what -a JMESPath expression can do. If you want to take a -tour of the language, the *best* place to go is the +The example above only show a small amount of what a JMESPath expression can do. +If you want to take a tour of the language, the _best_ place to go is the [JMESPath Tutorial](http://jmespath.org/tutorial.html). -One of the best things about JMESPath is that it is -implemented in many different programming languages including -python, ruby, php, lua, etc. To see a complete list of libraries, -check out the [JMESPath libraries page](http://jmespath.org/libraries.html). +One of the best things about JMESPath is that it is implemented in many +different programming languages including python, ruby, php, lua, etc. To see a +complete list of libraries, check out the +[JMESPath libraries page](http://jmespath.org/libraries.html). -And finally, the full JMESPath specification can be found -on the [JMESPath site](http://jmespath.org/specification.html). +And finally, the full JMESPath specification can be found on the +[JMESPath site](http://jmespath.org/specification.html). From a9c22c038bba757e385efa2a2efe5cd15d50218b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Tue, 1 Oct 2024 09:52:08 +0200 Subject: [PATCH 12/13] updated lock --- deno.lock | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/deno.lock b/deno.lock index 6c52be6..97e166b 100644 --- a/deno.lock +++ b/deno.lock @@ -33,15 +33,6 @@ "dependencies": [ "jsr:@std/assert@1", "jsr:@std/path@^1" - ], - "packageJson": { - "dependencies": [ - "npm:grunt-contrib-jshint@^0.11.0", - "npm:grunt-contrib-uglify@^0.11.1", - "npm:grunt-eslint@^17.3.1", - "npm:grunt@^0.4.5", - "npm:mocha@^2.1.0" - ] - } + ] } } From 3b0387fc4b1ffc25b99a03842c10dece5cf5e2b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Tue, 1 Oct 2024 09:52:39 +0200 Subject: [PATCH 13/13] fmt --- test/compliance/basic.json | 183 +++-- test/compliance/current.json | 46 +- test/compliance/escape.json | 88 +-- test/compliance/filters.json | 392 +++++++---- test/compliance/functions.json | 203 +++--- test/compliance/indices.json | 702 ++++++++++--------- test/compliance/literal.json | 374 +++++----- test/compliance/multiselect.json | 750 ++++++++++---------- test/compliance/pipe.json | 12 +- test/compliance/slice.json | 9 +- test/compliance/syntax.json | 1131 +++++++++++++++--------------- test/compliance/unicode.json | 72 +- test/compliance/wildcard.json | 905 ++++++++++++------------ 13 files changed, 2557 insertions(+), 2310 deletions(-) diff --git a/test/compliance/basic.json b/test/compliance/basic.json index d550e96..def2bcc 100644 --- a/test/compliance/basic.json +++ b/test/compliance/basic.json @@ -1,96 +1,89 @@ [{ - "given": - {"foo": {"bar": {"baz": "correct"}}}, - "cases": [ - { - "expression": "foo", - "result": {"bar": {"baz": "correct"}} - }, - { - "expression": "foo.bar", - "result": {"baz": "correct"} - }, - { - "expression": "foo.bar.baz", - "result": "correct" - }, - { - "expression": "foo\n.\nbar\n.baz", - "result": "correct" - }, - { - "expression": "foo.bar.baz.bad", - "result": null - }, - { - "expression": "foo.bar.bad", - "result": null - }, - { - "expression": "foo.bad", - "result": null - }, - { - "expression": "bad", - "result": null - }, - { - "expression": "bad.morebad.morebad", - "result": null - } - ] -}, -{ - "given": - {"foo": {"bar": ["one", "two", "three"]}}, - "cases": [ - { - "expression": "foo", - "result": {"bar": ["one", "two", "three"]} - }, - { - "expression": "foo.bar", - "result": ["one", "two", "three"] - } - ] -}, -{ - "given": ["one", "two", "three"], - "cases": [ - { - "expression": "one", - "result": null - }, - { - "expression": "two", - "result": null - }, - { - "expression": "three", - "result": null - }, - { - "expression": "one.two", - "result": null - } - ] -}, -{ - "given": - {"foo": {"1": ["one", "two", "three"], "-1": "bar"}}, - "cases": [ - { - "expression": "foo.\"1\"", - "result": ["one", "two", "three"] - }, - { - "expression": "foo.\"1\"[0]", - "result": "one" - }, - { - "expression": "foo.\"-1\"", - "result": "bar" - } - ] -} -] + "given": { "foo": { "bar": { "baz": "correct" } } }, + "cases": [ + { + "expression": "foo", + "result": { "bar": { "baz": "correct" } } + }, + { + "expression": "foo.bar", + "result": { "baz": "correct" } + }, + { + "expression": "foo.bar.baz", + "result": "correct" + }, + { + "expression": "foo\n.\nbar\n.baz", + "result": "correct" + }, + { + "expression": "foo.bar.baz.bad", + "result": null + }, + { + "expression": "foo.bar.bad", + "result": null + }, + { + "expression": "foo.bad", + "result": null + }, + { + "expression": "bad", + "result": null + }, + { + "expression": "bad.morebad.morebad", + "result": null + } + ] +}, { + "given": { "foo": { "bar": ["one", "two", "three"] } }, + "cases": [ + { + "expression": "foo", + "result": { "bar": ["one", "two", "three"] } + }, + { + "expression": "foo.bar", + "result": ["one", "two", "three"] + } + ] +}, { + "given": ["one", "two", "three"], + "cases": [ + { + "expression": "one", + "result": null + }, + { + "expression": "two", + "result": null + }, + { + "expression": "three", + "result": null + }, + { + "expression": "one.two", + "result": null + } + ] +}, { + "given": { "foo": { "1": ["one", "two", "three"], "-1": "bar" } }, + "cases": [ + { + "expression": "foo.\"1\"", + "result": ["one", "two", "three"] + }, + { + "expression": "foo.\"1\"[0]", + "result": "one" + }, + { + "expression": "foo.\"-1\"", + "result": "bar" + } + ] +}] diff --git a/test/compliance/current.json b/test/compliance/current.json index 0c26248..ef4677a 100644 --- a/test/compliance/current.json +++ b/test/compliance/current.json @@ -1,25 +1,25 @@ [ - { - "given": { - "foo": [{"name": "a"}, {"name": "b"}], - "bar": {"baz": "qux"} - }, - "cases": [ - { - "expression": "@", - "result": { - "foo": [{"name": "a"}, {"name": "b"}], - "bar": {"baz": "qux"} - } - }, - { - "expression": "@.bar", - "result": {"baz": "qux"} - }, - { - "expression": "@.foo[0]", - "result": {"name": "a"} - } - ] - } + { + "given": { + "foo": [{ "name": "a" }, { "name": "b" }], + "bar": { "baz": "qux" } + }, + "cases": [ + { + "expression": "@", + "result": { + "foo": [{ "name": "a" }, { "name": "b" }], + "bar": { "baz": "qux" } + } + }, + { + "expression": "@.bar", + "result": { "baz": "qux" } + }, + { + "expression": "@.foo[0]", + "result": { "name": "a" } + } + ] + } ] diff --git a/test/compliance/escape.json b/test/compliance/escape.json index 4a62d95..e70542f 100644 --- a/test/compliance/escape.json +++ b/test/compliance/escape.json @@ -1,46 +1,46 @@ [{ - "given": { - "foo.bar": "dot", - "foo bar": "space", - "foo\nbar": "newline", - "foo\"bar": "doublequote", - "c:\\\\windows\\path": "windows", - "/unix/path": "unix", - "\"\"\"": "threequotes", - "bar": {"baz": "qux"} - }, - "cases": [ - { - "expression": "\"foo.bar\"", - "result": "dot" - }, - { - "expression": "\"foo bar\"", - "result": "space" - }, - { - "expression": "\"foo\\nbar\"", - "result": "newline" - }, - { - "expression": "\"foo\\\"bar\"", - "result": "doublequote" - }, - { - "expression": "\"c:\\\\\\\\windows\\\\path\"", - "result": "windows" - }, - { - "expression": "\"/unix/path\"", - "result": "unix" - }, - { - "expression": "\"\\\"\\\"\\\"\"", - "result": "threequotes" - }, - { - "expression": "\"bar\".\"baz\"", - "result": "qux" - } - ] + "given": { + "foo.bar": "dot", + "foo bar": "space", + "foo\nbar": "newline", + "foo\"bar": "doublequote", + "c:\\\\windows\\path": "windows", + "/unix/path": "unix", + "\"\"\"": "threequotes", + "bar": { "baz": "qux" } + }, + "cases": [ + { + "expression": "\"foo.bar\"", + "result": "dot" + }, + { + "expression": "\"foo bar\"", + "result": "space" + }, + { + "expression": "\"foo\\nbar\"", + "result": "newline" + }, + { + "expression": "\"foo\\\"bar\"", + "result": "doublequote" + }, + { + "expression": "\"c:\\\\\\\\windows\\\\path\"", + "result": "windows" + }, + { + "expression": "\"/unix/path\"", + "result": "unix" + }, + { + "expression": "\"\\\"\\\"\\\"\"", + "result": "threequotes" + }, + { + "expression": "\"bar\".\"baz\"", + "result": "qux" + } + ] }] diff --git a/test/compliance/filters.json b/test/compliance/filters.json index 5b9f52b..cc39c2c 100644 --- a/test/compliance/filters.json +++ b/test/compliance/filters.json @@ -1,16 +1,16 @@ [ { - "given": {"foo": [{"name": "a"}, {"name": "b"}]}, + "given": { "foo": [{ "name": "a" }, { "name": "b" }] }, "cases": [ { "comment": "Matching a literal", "expression": "foo[?name == 'a']", - "result": [{"name": "a"}] + "result": [{ "name": "a" }] } ] }, { - "given": {"foo": [0, 1], "bar": [2, 3]}, + "given": { "foo": [0, 1], "bar": [2, 3] }, "cases": [ { "comment": "Matching a literal", @@ -20,14 +20,18 @@ ] }, { - "given": {"foo": [{"first": "foo", "last": "bar"}, - {"first": "foo", "last": "foo"}, - {"first": "foo", "last": "baz"}]}, + "given": { + "foo": [ + { "first": "foo", "last": "bar" }, + { "first": "foo", "last": "foo" }, + { "first": "foo", "last": "baz" } + ] + }, "cases": [ { "comment": "Matching an expression", "expression": "foo[?first == last]", - "result": [{"first": "foo", "last": "foo"}] + "result": [{ "first": "foo", "last": "foo" }] }, { "comment": "Verify projection created from filter", @@ -37,18 +41,16 @@ ] }, { - "given": {"foo": [{"age": 20}, - {"age": 25}, - {"age": 30}]}, + "given": { "foo": [{ "age": 20 }, { "age": 25 }, { "age": 30 }] }, "cases": [ { "comment": "Greater than with a number", "expression": "foo[?age > `25`]", - "result": [{"age": 30}] + "result": [{ "age": 30 }] }, { "expression": "foo[?age >= `25`]", - "result": [{"age": 25}, {"age": 30}] + "result": [{ "age": 25 }, { "age": 30 }] }, { "comment": "Greater than with a number", @@ -58,12 +60,12 @@ { "comment": "Greater than with a number", "expression": "foo[?age < `25`]", - "result": [{"age": 20}] + "result": [{ "age": 20 }] }, { "comment": "Greater than with a number", "expression": "foo[?age <= `25`]", - "result": [{"age": 20}, {"age": 25}] + "result": [{ "age": 20 }, { "age": 25 }] }, { "comment": "Greater than with a number", @@ -72,216 +74,343 @@ }, { "expression": "foo[?age == `20`]", - "result": [{"age": 20}] + "result": [{ "age": 20 }] }, { "expression": "foo[?age != `20`]", - "result": [{"age": 25}, {"age": 30}] + "result": [{ "age": 25 }, { "age": 30 }] } ] }, { - "given": {"foo": [{"top": {"name": "a"}}, - {"top": {"name": "b"}}]}, + "given": { + "foo": [{ "top": { "name": "a" } }, { "top": { "name": "b" } }] + }, "cases": [ { "comment": "Filter with subexpression", "expression": "foo[?top.name == 'a']", - "result": [{"top": {"name": "a"}}] + "result": [{ "top": { "name": "a" } }] } ] }, { - "given": {"foo": [{"top": {"first": "foo", "last": "bar"}}, - {"top": {"first": "foo", "last": "foo"}}, - {"top": {"first": "foo", "last": "baz"}}]}, + "given": { + "foo": [ + { "top": { "first": "foo", "last": "bar" } }, + { "top": { "first": "foo", "last": "foo" } }, + { "top": { "first": "foo", "last": "baz" } } + ] + }, "cases": [ { "comment": "Matching an expression", "expression": "foo[?top.first == top.last]", - "result": [{"top": {"first": "foo", "last": "foo"}}] + "result": [{ "top": { "first": "foo", "last": "foo" } }] }, { "comment": "Matching a JSON array", "expression": "foo[?top == `{\"first\": \"foo\", \"last\": \"bar\"}`]", - "result": [{"top": {"first": "foo", "last": "bar"}}] + "result": [{ "top": { "first": "foo", "last": "bar" } }] } ] }, { - "given": {"foo": [ - {"key": true}, - {"key": false}, - {"key": 0}, - {"key": 1}, - {"key": [0]}, - {"key": {"bar": [0]}}, - {"key": null}, - {"key": [1]}, - {"key": {"a":2}} - ]}, + "given": { + "foo": [ + { "key": true }, + { "key": false }, + { "key": 0 }, + { "key": 1 }, + { "key": [0] }, + { "key": { "bar": [0] } }, + { "key": null }, + { "key": [1] }, + { "key": { "a": 2 } } + ] + }, "cases": [ { "expression": "foo[?key == `true`]", - "result": [{"key": true}] + "result": [{ "key": true }] }, { "expression": "foo[?key == `false`]", - "result": [{"key": false}] + "result": [{ "key": false }] }, { "expression": "foo[?key == `0`]", - "result": [{"key": 0}] + "result": [{ "key": 0 }] }, { "expression": "foo[?key == `1`]", - "result": [{"key": 1}] + "result": [{ "key": 1 }] }, { "expression": "foo[?key == `[0]`]", - "result": [{"key": [0]}] + "result": [{ "key": [0] }] }, { "expression": "foo[?key == `{\"bar\": [0]}`]", - "result": [{"key": {"bar": [0]}}] + "result": [{ "key": { "bar": [0] } }] }, { "expression": "foo[?key == `null`]", - "result": [{"key": null}] + "result": [{ "key": null }] }, { "expression": "foo[?key == `[1]`]", - "result": [{"key": [1]}] + "result": [{ "key": [1] }] }, { "expression": "foo[?key == `{\"a\":2}`]", - "result": [{"key": {"a":2}}] + "result": [{ "key": { "a": 2 } }] }, { "expression": "foo[?`true` == key]", - "result": [{"key": true}] + "result": [{ "key": true }] }, { "expression": "foo[?`false` == key]", - "result": [{"key": false}] + "result": [{ "key": false }] }, { "expression": "foo[?`0` == key]", - "result": [{"key": 0}] + "result": [{ "key": 0 }] }, { "expression": "foo[?`1` == key]", - "result": [{"key": 1}] + "result": [{ "key": 1 }] }, { "expression": "foo[?`[0]` == key]", - "result": [{"key": [0]}] + "result": [{ "key": [0] }] }, { "expression": "foo[?`{\"bar\": [0]}` == key]", - "result": [{"key": {"bar": [0]}}] + "result": [{ "key": { "bar": [0] } }] }, { "expression": "foo[?`null` == key]", - "result": [{"key": null}] + "result": [{ "key": null }] }, { "expression": "foo[?`[1]` == key]", - "result": [{"key": [1]}] + "result": [{ "key": [1] }] }, { "expression": "foo[?`{\"a\":2}` == key]", - "result": [{"key": {"a":2}}] + "result": [{ "key": { "a": 2 } }] }, { "expression": "foo[?key != `true`]", - "result": [{"key": false}, {"key": 0}, {"key": 1}, {"key": [0]}, - {"key": {"bar": [0]}}, {"key": null}, {"key": [1]}, {"key": {"a":2}}] + "result": [ + { "key": false }, + { "key": 0 }, + { "key": 1 }, + { "key": [0] }, + { "key": { "bar": [0] } }, + { "key": null }, + { "key": [1] }, + { "key": { "a": 2 } } + ] }, { "expression": "foo[?key != `false`]", - "result": [{"key": true}, {"key": 0}, {"key": 1}, {"key": [0]}, - {"key": {"bar": [0]}}, {"key": null}, {"key": [1]}, {"key": {"a":2}}] + "result": [ + { "key": true }, + { "key": 0 }, + { "key": 1 }, + { "key": [0] }, + { "key": { "bar": [0] } }, + { "key": null }, + { "key": [1] }, + { "key": { "a": 2 } } + ] }, { "expression": "foo[?key != `0`]", - "result": [{"key": true}, {"key": false}, {"key": 1}, {"key": [0]}, - {"key": {"bar": [0]}}, {"key": null}, {"key": [1]}, {"key": {"a":2}}] + "result": [ + { "key": true }, + { "key": false }, + { "key": 1 }, + { "key": [0] }, + { "key": { "bar": [0] } }, + { "key": null }, + { "key": [1] }, + { "key": { "a": 2 } } + ] }, { "expression": "foo[?key != `1`]", - "result": [{"key": true}, {"key": false}, {"key": 0}, {"key": [0]}, - {"key": {"bar": [0]}}, {"key": null}, {"key": [1]}, {"key": {"a":2}}] + "result": [ + { "key": true }, + { "key": false }, + { "key": 0 }, + { "key": [0] }, + { "key": { "bar": [0] } }, + { "key": null }, + { "key": [1] }, + { "key": { "a": 2 } } + ] }, { "expression": "foo[?key != `null`]", - "result": [{"key": true}, {"key": false}, {"key": 0}, {"key": 1}, {"key": [0]}, - {"key": {"bar": [0]}}, {"key": [1]}, {"key": {"a":2}}] + "result": [ + { "key": true }, + { "key": false }, + { "key": 0 }, + { "key": 1 }, + { "key": [0] }, + { "key": { "bar": [0] } }, + { "key": [1] }, + { "key": { "a": 2 } } + ] }, { "expression": "foo[?key != `[1]`]", - "result": [{"key": true}, {"key": false}, {"key": 0}, {"key": 1}, {"key": [0]}, - {"key": {"bar": [0]}}, {"key": null}, {"key": {"a":2}}] + "result": [ + { "key": true }, + { "key": false }, + { "key": 0 }, + { "key": 1 }, + { "key": [0] }, + { "key": { "bar": [0] } }, + { "key": null }, + { "key": { "a": 2 } } + ] }, { "expression": "foo[?key != `{\"a\":2}`]", - "result": [{"key": true}, {"key": false}, {"key": 0}, {"key": 1}, {"key": [0]}, - {"key": {"bar": [0]}}, {"key": null}, {"key": [1]}] + "result": [ + { "key": true }, + { "key": false }, + { "key": 0 }, + { "key": 1 }, + { "key": [0] }, + { "key": { "bar": [0] } }, + { "key": null }, + { "key": [1] } + ] }, { "expression": "foo[?`true` != key]", - "result": [{"key": false}, {"key": 0}, {"key": 1}, {"key": [0]}, - {"key": {"bar": [0]}}, {"key": null}, {"key": [1]}, {"key": {"a":2}}] + "result": [ + { "key": false }, + { "key": 0 }, + { "key": 1 }, + { "key": [0] }, + { "key": { "bar": [0] } }, + { "key": null }, + { "key": [1] }, + { "key": { "a": 2 } } + ] }, { "expression": "foo[?`false` != key]", - "result": [{"key": true}, {"key": 0}, {"key": 1}, {"key": [0]}, - {"key": {"bar": [0]}}, {"key": null}, {"key": [1]}, {"key": {"a":2}}] + "result": [ + { "key": true }, + { "key": 0 }, + { "key": 1 }, + { "key": [0] }, + { "key": { "bar": [0] } }, + { "key": null }, + { "key": [1] }, + { "key": { "a": 2 } } + ] }, { "expression": "foo[?`0` != key]", - "result": [{"key": true}, {"key": false}, {"key": 1}, {"key": [0]}, - {"key": {"bar": [0]}}, {"key": null}, {"key": [1]}, {"key": {"a":2}}] + "result": [ + { "key": true }, + { "key": false }, + { "key": 1 }, + { "key": [0] }, + { "key": { "bar": [0] } }, + { "key": null }, + { "key": [1] }, + { "key": { "a": 2 } } + ] }, { "expression": "foo[?`1` != key]", - "result": [{"key": true}, {"key": false}, {"key": 0}, {"key": [0]}, - {"key": {"bar": [0]}}, {"key": null}, {"key": [1]}, {"key": {"a":2}}] + "result": [ + { "key": true }, + { "key": false }, + { "key": 0 }, + { "key": [0] }, + { "key": { "bar": [0] } }, + { "key": null }, + { "key": [1] }, + { "key": { "a": 2 } } + ] }, { "expression": "foo[?`null` != key]", - "result": [{"key": true}, {"key": false}, {"key": 0}, {"key": 1}, {"key": [0]}, - {"key": {"bar": [0]}}, {"key": [1]}, {"key": {"a":2}}] + "result": [ + { "key": true }, + { "key": false }, + { "key": 0 }, + { "key": 1 }, + { "key": [0] }, + { "key": { "bar": [0] } }, + { "key": [1] }, + { "key": { "a": 2 } } + ] }, { "expression": "foo[?`[1]` != key]", - "result": [{"key": true}, {"key": false}, {"key": 0}, {"key": 1}, {"key": [0]}, - {"key": {"bar": [0]}}, {"key": null}, {"key": {"a":2}}] + "result": [ + { "key": true }, + { "key": false }, + { "key": 0 }, + { "key": 1 }, + { "key": [0] }, + { "key": { "bar": [0] } }, + { "key": null }, + { "key": { "a": 2 } } + ] }, { "expression": "foo[?`{\"a\":2}` != key]", - "result": [{"key": true}, {"key": false}, {"key": 0}, {"key": 1}, {"key": [0]}, - {"key": {"bar": [0]}}, {"key": null}, {"key": [1]}] + "result": [ + { "key": true }, + { "key": false }, + { "key": 0 }, + { "key": 1 }, + { "key": [0] }, + { "key": { "bar": [0] } }, + { "key": null }, + { "key": [1] } + ] } ] }, { - "given": {"reservations": [ - {"instances": [ - {"foo": 1, "bar": 2}, {"foo": 1, "bar": 3}, - {"foo": 1, "bar": 2}, {"foo": 2, "bar": 1}]}]}, + "given": { + "reservations": [ + { + "instances": [ + { "foo": 1, "bar": 2 }, + { "foo": 1, "bar": 3 }, + { "foo": 1, "bar": 2 }, + { "foo": 2, "bar": 1 } + ] + } + ] + }, "cases": [ { "expression": "reservations[].instances[?bar==`1`]", - "result": [[{"foo": 2, "bar": 1}]] + "result": [[{ "foo": 2, "bar": 1 }]] }, { "expression": "reservations[*].instances[?bar==`1`]", - "result": [[{"foo": 2, "bar": 1}]] + "result": [[{ "foo": 2, "bar": 1 }]] }, { "expression": "reservations[].instances[?bar==`1`][]", - "result": [{"foo": 2, "bar": 1}] + "result": [{ "foo": 2, "bar": 1 }] } ] }, @@ -289,7 +418,11 @@ "given": { "baz": "other", "foo": [ - {"bar": 1}, {"bar": 2}, {"bar": 3}, {"bar": 4}, {"bar": 1, "baz": 2} + { "bar": 1 }, + { "bar": 2 }, + { "bar": 3 }, + { "bar": 4 }, + { "bar": 1, "baz": 2 } ] }, "cases": [ @@ -302,11 +435,11 @@ { "given": { "foo": [ - {"a": 1, "b": {"c": "x"}}, - {"a": 1, "b": {"c": "y"}}, - {"a": 1, "b": {"c": "z"}}, - {"a": 2, "b": {"c": "z"}}, - {"a": 1, "baz": 2} + { "a": 1, "b": { "c": "x" } }, + { "a": 1, "b": { "c": "y" } }, + { "a": 1, "b": { "c": "z" } }, + { "a": 2, "b": { "c": "z" } }, + { "a": 1, "baz": 2 } ] }, "cases": [ @@ -317,30 +450,30 @@ ] }, { - "given": {"foo": [{"name": "a"}, {"name": "b"}, {"name": "c"}]}, + "given": { "foo": [{ "name": "a" }, { "name": "b" }, { "name": "c" }] }, "cases": [ { "comment": "Filter with or expression", "expression": "foo[?name == 'a' || name == 'b']", - "result": [{"name": "a"}, {"name": "b"}] + "result": [{ "name": "a" }, { "name": "b" }] }, { "expression": "foo[?name == 'a' || name == 'e']", - "result": [{"name": "a"}] + "result": [{ "name": "a" }] }, { "expression": "foo[?name == 'a' || name == 'b' || name == 'c']", - "result": [{"name": "a"}, {"name": "b"}, {"name": "c"}] + "result": [{ "name": "a" }, { "name": "b" }, { "name": "c" }] } ] }, { - "given": {"foo": [{"a": 1, "b": 2}, {"a": 1, "b": 3}]}, + "given": { "foo": [{ "a": 1, "b": 2 }, { "a": 1, "b": 3 }] }, "cases": [ { "comment": "Filter with and expression", "expression": "foo[?a == `1` && b == `2`]", - "result": [{"a": 1, "b": 2}] + "result": [{ "a": 1, "b": 2 }] }, { "expression": "foo[?a == `1` && b == `4`]", @@ -349,46 +482,46 @@ ] }, { - "given": {"foo": [{"a": 1, "b": 2, "c": 3}, {"a": 3, "b": 4}]}, + "given": { "foo": [{ "a": 1, "b": 2, "c": 3 }, { "a": 3, "b": 4 }] }, "cases": [ { "comment": "Filter with Or and And expressions", "expression": "foo[?c == `3` || a == `1` && b == `4`]", - "result": [{"a": 1, "b": 2, "c": 3}] + "result": [{ "a": 1, "b": 2, "c": 3 }] }, { "expression": "foo[?b == `2` || a == `3` && b == `4`]", - "result": [{"a": 1, "b": 2, "c": 3}, {"a": 3, "b": 4}] + "result": [{ "a": 1, "b": 2, "c": 3 }, { "a": 3, "b": 4 }] }, { "expression": "foo[?a == `3` && b == `4` || b == `2`]", - "result": [{"a": 1, "b": 2, "c": 3}, {"a": 3, "b": 4}] + "result": [{ "a": 1, "b": 2, "c": 3 }, { "a": 3, "b": 4 }] }, { "expression": "foo[?(a == `3` && b == `4`) || b == `2`]", - "result": [{"a": 1, "b": 2, "c": 3}, {"a": 3, "b": 4}] + "result": [{ "a": 1, "b": 2, "c": 3 }, { "a": 3, "b": 4 }] }, { "expression": "foo[?((a == `3` && b == `4`)) || b == `2`]", - "result": [{"a": 1, "b": 2, "c": 3}, {"a": 3, "b": 4}] + "result": [{ "a": 1, "b": 2, "c": 3 }, { "a": 3, "b": 4 }] }, { "expression": "foo[?a == `3` && (b == `4` || b == `2`)]", - "result": [{"a": 3, "b": 4}] + "result": [{ "a": 3, "b": 4 }] }, { "expression": "foo[?a == `3` && ((b == `4` || b == `2`))]", - "result": [{"a": 3, "b": 4}] + "result": [{ "a": 3, "b": 4 }] } ] }, { - "given": {"foo": [{"a": 1, "b": 2, "c": 3}, {"a": 3, "b": 4}]}, + "given": { "foo": [{ "a": 1, "b": 2, "c": 3 }, { "a": 3, "b": 4 }] }, "cases": [ { "comment": "Verify precedence of or/and expressions", "expression": "foo[?a == `1` || b ==`2` && c == `5`]", - "result": [{"a": 1, "b": 2, "c": 3}] + "result": [{ "a": 1, "b": 2, "c": 3 }] }, { "comment": "Parentheses can alter precedence", @@ -398,23 +531,23 @@ { "comment": "Not expressions combined with and/or", "expression": "foo[?!(a == `1` || b ==`2`)]", - "result": [{"a": 3, "b": 4}] + "result": [{ "a": 3, "b": 4 }] } ] }, { "given": { "foo": [ - {"key": true}, - {"key": false}, - {"key": []}, - {"key": {}}, - {"key": [0]}, - {"key": {"a": "b"}}, - {"key": 0}, - {"key": 1}, - {"key": null}, - {"notkey": true} + { "key": true }, + { "key": false }, + { "key": [] }, + { "key": {} }, + { "key": [0] }, + { "key": { "a": "b" } }, + { "key": 0 }, + { "key": 1 }, + { "key": null }, + { "notkey": true } ] }, "cases": [ @@ -422,23 +555,30 @@ "comment": "Unary filter expression", "expression": "foo[?key]", "result": [ - {"key": true}, {"key": [0]}, {"key": {"a": "b"}}, - {"key": 0}, {"key": 1} + { "key": true }, + { "key": [0] }, + { "key": { "a": "b" } }, + { "key": 0 }, + { "key": 1 } ] }, { "comment": "Unary not filter expression", "expression": "foo[?!key]", "result": [ - {"key": false}, {"key": []}, {"key": {}}, - {"key": null}, {"notkey": true} + { "key": false }, + { "key": [] }, + { "key": {} }, + { "key": null }, + { "notkey": true } ] }, { "comment": "Equality with null RHS", "expression": "foo[?key == `null`]", "result": [ - {"key": null}, {"notkey": true} + { "key": null }, + { "notkey": true } ] } ] diff --git a/test/compliance/functions.json b/test/compliance/functions.json index 8b8db36..ae0a387 100644 --- a/test/compliance/functions.json +++ b/test/compliance/functions.json @@ -1,6 +1,5 @@ [{ - "given": - { + "given": { "foo": -1, "zero": 0, "numbers": [-1, 3, 4, 5], @@ -11,7 +10,7 @@ "false": false, "empty_list": [], "empty_hash": {}, - "objects": {"foo": "bar", "bar": "baz"}, + "objects": { "foo": "bar", "bar": "baz" }, "null_key": null }, "cases": [ @@ -253,15 +252,15 @@ }, { "expression": "merge(`{\"a\": 1}`, `{\"b\": 2}`)", - "result": {"a": 1, "b": 2} + "result": { "a": 1, "b": 2 } }, { "expression": "merge(`{\"a\": 1}`, `{\"a\": 2}`)", - "result": {"a": 2} + "result": { "a": 2 } }, { "expression": "merge(`{\"a\": 1, \"b\": 2}`, `{\"a\": 2, \"c\": 3}`, `{\"d\": 4}`)", - "result": {"a": 2, "b": 2, "c": 3, "d": 4} + "result": { "a": 2, "b": 2, "c": 3, "d": 4 } }, { "expression": "min(numbers)", @@ -465,7 +464,7 @@ }, { "expression": "to_array(objects)", - "result": [{"foo": "bar", "bar": "baz"}] + "result": [{ "foo": "bar", "bar": "baz" }] }, { "expression": "to_array(`[1, 2, 3]`)", @@ -583,14 +582,13 @@ } ] }, { - "given": - { + "given": { "foo": [ - {"b": "b", "a": "a"}, - {"c": "c", "b": "b"}, - {"d": "d", "c": "c"}, - {"e": "e", "d": "d"}, - {"f": "f", "e": "e"} + { "b": "b", "a": "a" }, + { "c": "c", "b": "b" }, + { "d": "d", "c": "c" }, + { "e": "e", "d": "d" }, + { "f": "f", "e": "e" } ] }, "cases": [ @@ -601,14 +599,19 @@ } ] }, { - "given": - { + "given": { "people": [ - {"age": 20, "age_str": "20", "bool": true, "name": "a", "extra": "foo"}, - {"age": 40, "age_str": "40", "bool": false, "name": "b", "extra": "bar"}, - {"age": 30, "age_str": "30", "bool": true, "name": "c"}, - {"age": 50, "age_str": "50", "bool": false, "name": "d"}, - {"age": 10, "age_str": "10", "bool": true, "name": 3} + { "age": 20, "age_str": "20", "bool": true, "name": "a", "extra": "foo" }, + { + "age": 40, + "age_str": "40", + "bool": false, + "name": "b", + "extra": "bar" + }, + { "age": 30, "age_str": "30", "bool": true, "name": "c" }, + { "age": 50, "age_str": "50", "bool": false, "name": "d" }, + { "age": 10, "age_str": "10", "bool": true, "name": 3 } ] }, "cases": [ @@ -616,32 +619,68 @@ "description": "sort by field expression", "expression": "sort_by(people, &age)", "result": [ - {"age": 10, "age_str": "10", "bool": true, "name": 3}, - {"age": 20, "age_str": "20", "bool": true, "name": "a", "extra": "foo"}, - {"age": 30, "age_str": "30", "bool": true, "name": "c"}, - {"age": 40, "age_str": "40", "bool": false, "name": "b", "extra": "bar"}, - {"age": 50, "age_str": "50", "bool": false, "name": "d"} + { "age": 10, "age_str": "10", "bool": true, "name": 3 }, + { + "age": 20, + "age_str": "20", + "bool": true, + "name": "a", + "extra": "foo" + }, + { "age": 30, "age_str": "30", "bool": true, "name": "c" }, + { + "age": 40, + "age_str": "40", + "bool": false, + "name": "b", + "extra": "bar" + }, + { "age": 50, "age_str": "50", "bool": false, "name": "d" } ] }, { "expression": "sort_by(people, &age_str)", "result": [ - {"age": 10, "age_str": "10", "bool": true, "name": 3}, - {"age": 20, "age_str": "20", "bool": true, "name": "a", "extra": "foo"}, - {"age": 30, "age_str": "30", "bool": true, "name": "c"}, - {"age": 40, "age_str": "40", "bool": false, "name": "b", "extra": "bar"}, - {"age": 50, "age_str": "50", "bool": false, "name": "d"} + { "age": 10, "age_str": "10", "bool": true, "name": 3 }, + { + "age": 20, + "age_str": "20", + "bool": true, + "name": "a", + "extra": "foo" + }, + { "age": 30, "age_str": "30", "bool": true, "name": "c" }, + { + "age": 40, + "age_str": "40", + "bool": false, + "name": "b", + "extra": "bar" + }, + { "age": 50, "age_str": "50", "bool": false, "name": "d" } ] }, { "description": "sort by function expression", "expression": "sort_by(people, &to_number(age_str))", "result": [ - {"age": 10, "age_str": "10", "bool": true, "name": 3}, - {"age": 20, "age_str": "20", "bool": true, "name": "a", "extra": "foo"}, - {"age": 30, "age_str": "30", "bool": true, "name": "c"}, - {"age": 40, "age_str": "40", "bool": false, "name": "b", "extra": "bar"}, - {"age": 50, "age_str": "50", "bool": false, "name": "d"} + { "age": 10, "age_str": "10", "bool": true, "name": 3 }, + { + "age": 20, + "age_str": "20", + "bool": true, + "name": "a", + "extra": "foo" + }, + { "age": 30, "age_str": "30", "bool": true, "name": "c" }, + { + "age": 40, + "age_str": "40", + "bool": false, + "name": "b", + "extra": "bar" + }, + { "age": 50, "age_str": "50", "bool": false, "name": "d" } ] }, { @@ -675,11 +714,11 @@ }, { "expression": "max_by(people, &age)", - "result": {"age": 50, "age_str": "50", "bool": false, "name": "d"} + "result": { "age": 50, "age_str": "50", "bool": false, "name": "d" } }, { "expression": "max_by(people, &age_str)", - "result": {"age": 50, "age_str": "50", "bool": false, "name": "d"} + "result": { "age": 50, "age_str": "50", "bool": false, "name": "d" } }, { "expression": "max_by(people, &bool)", @@ -691,15 +730,15 @@ }, { "expression": "max_by(people, &to_number(age_str))", - "result": {"age": 50, "age_str": "50", "bool": false, "name": "d"} + "result": { "age": 50, "age_str": "50", "bool": false, "name": "d" } }, { "expression": "min_by(people, &age)", - "result": {"age": 10, "age_str": "10", "bool": true, "name": 3} + "result": { "age": 10, "age_str": "10", "bool": true, "name": 3 } }, { "expression": "min_by(people, &age_str)", - "result": {"age": 10, "age_str": "10", "bool": true, "name": 3} + "result": { "age": 10, "age_str": "10", "bool": true, "name": 3 } }, { "expression": "min_by(people, &bool)", @@ -711,24 +750,23 @@ }, { "expression": "min_by(people, &to_number(age_str))", - "result": {"age": 10, "age_str": "10", "bool": true, "name": 3} + "result": { "age": 10, "age_str": "10", "bool": true, "name": 3 } } ] }, { - "given": - { + "given": { "people": [ - {"age": 10, "order": "1"}, - {"age": 10, "order": "2"}, - {"age": 10, "order": "3"}, - {"age": 10, "order": "4"}, - {"age": 10, "order": "5"}, - {"age": 10, "order": "6"}, - {"age": 10, "order": "7"}, - {"age": 10, "order": "8"}, - {"age": 10, "order": "9"}, - {"age": 10, "order": "10"}, - {"age": 10, "order": "11"} + { "age": 10, "order": "1" }, + { "age": 10, "order": "2" }, + { "age": 10, "order": "3" }, + { "age": 10, "order": "4" }, + { "age": 10, "order": "5" }, + { "age": 10, "order": "6" }, + { "age": 10, "order": "7" }, + { "age": 10, "order": "8" }, + { "age": 10, "order": "9" }, + { "age": 10, "order": "10" }, + { "age": 10, "order": "11" } ] }, "cases": [ @@ -736,33 +774,32 @@ "description": "stable sort order", "expression": "sort_by(people, &age)", "result": [ - {"age": 10, "order": "1"}, - {"age": 10, "order": "2"}, - {"age": 10, "order": "3"}, - {"age": 10, "order": "4"}, - {"age": 10, "order": "5"}, - {"age": 10, "order": "6"}, - {"age": 10, "order": "7"}, - {"age": 10, "order": "8"}, - {"age": 10, "order": "9"}, - {"age": 10, "order": "10"}, - {"age": 10, "order": "11"} + { "age": 10, "order": "1" }, + { "age": 10, "order": "2" }, + { "age": 10, "order": "3" }, + { "age": 10, "order": "4" }, + { "age": 10, "order": "5" }, + { "age": 10, "order": "6" }, + { "age": 10, "order": "7" }, + { "age": 10, "order": "8" }, + { "age": 10, "order": "9" }, + { "age": 10, "order": "10" }, + { "age": 10, "order": "11" } ] } ] }, { - "given": - { + "given": { "people": [ - {"a": 10, "b": 1, "c": "z"}, - {"a": 10, "b": 2, "c": null}, - {"a": 10, "b": 3}, - {"a": 10, "b": 4, "c": "z"}, - {"a": 10, "b": 5, "c": null}, - {"a": 10, "b": 6}, - {"a": 10, "b": 7, "c": "z"}, - {"a": 10, "b": 8, "c": null}, - {"a": 10, "b": 9} + { "a": 10, "b": 1, "c": "z" }, + { "a": 10, "b": 2, "c": null }, + { "a": 10, "b": 3 }, + { "a": 10, "b": 4, "c": "z" }, + { "a": 10, "b": 5, "c": null }, + { "a": 10, "b": 6 }, + { "a": 10, "b": 7, "c": "z" }, + { "a": 10, "b": 8, "c": null }, + { "a": 10, "b": 9 } ], "empty": [] }, @@ -788,15 +825,16 @@ "given": { "array": [ { - "foo": {"bar": "yes1"} + "foo": { "bar": "yes1" } }, { - "foo": {"bar": "yes2"} + "foo": { "bar": "yes2" } }, { - "foo1": {"bar": "no"} + "foo1": { "bar": "no" } } - ]}, + ] + }, "cases": [ { "expression": "map(&foo.bar, array)", @@ -821,5 +859,4 @@ "result": [[1, 2, 3, 4], [5, 6, 7, 8, 9]] } ] -} -] +}] diff --git a/test/compliance/indices.json b/test/compliance/indices.json index aa03b35..cf56d04 100644 --- a/test/compliance/indices.json +++ b/test/compliance/indices.json @@ -1,346 +1,410 @@ [{ - "given": - {"foo": {"bar": ["zero", "one", "two"]}}, - "cases": [ - { - "expression": "foo.bar[0]", - "result": "zero" - }, - { - "expression": "foo.bar[1]", - "result": "one" - }, - { - "expression": "foo.bar[2]", - "result": "two" - }, - { - "expression": "foo.bar[3]", - "result": null - }, - { - "expression": "foo.bar[-1]", - "result": "two" - }, - { - "expression": "foo.bar[-2]", - "result": "one" - }, - { - "expression": "foo.bar[-3]", - "result": "zero" - }, - { - "expression": "foo.bar[-4]", - "result": null - } - ] -}, -{ - "given": - {"foo": [{"bar": "one"}, {"bar": "two"}, {"bar": "three"}, {"notbar": "four"}]}, - "cases": [ - { - "expression": "foo.bar", - "result": null - }, - { - "expression": "foo[0].bar", - "result": "one" - }, - { - "expression": "foo[1].bar", - "result": "two" - }, - { - "expression": "foo[2].bar", - "result": "three" - }, - { - "expression": "foo[3].notbar", - "result": "four" - }, - { - "expression": "foo[3].bar", - "result": null - }, - { - "expression": "foo[0]", - "result": {"bar": "one"} - }, - { - "expression": "foo[1]", - "result": {"bar": "two"} - }, - { - "expression": "foo[2]", - "result": {"bar": "three"} - }, - { - "expression": "foo[3]", - "result": {"notbar": "four"} - }, - { - "expression": "foo[4]", - "result": null - } - ] -}, -{ - "given": [ - "one", "two", "three" - ], - "cases": [ - { - "expression": "[0]", - "result": "one" - }, - { - "expression": "[1]", - "result": "two" - }, - { - "expression": "[2]", - "result": "three" - }, - { - "expression": "[-1]", - "result": "three" - }, - { - "expression": "[-2]", - "result": "two" - }, - { - "expression": "[-3]", - "result": "one" - } - ] -}, -{ - "given": {"reservations": [ - {"instances": [{"foo": 1}, {"foo": 2}]} - ]}, - "cases": [ - { - "expression": "reservations[].instances[].foo", - "result": [1, 2] - }, - { - "expression": "reservations[].instances[].bar", - "result": [] - }, - { - "expression": "reservations[].notinstances[].foo", - "result": [] - }, - { - "expression": "reservations[].notinstances[].foo", - "result": [] - } + "given": { "foo": { "bar": ["zero", "one", "two"] } }, + "cases": [ + { + "expression": "foo.bar[0]", + "result": "zero" + }, + { + "expression": "foo.bar[1]", + "result": "one" + }, + { + "expression": "foo.bar[2]", + "result": "two" + }, + { + "expression": "foo.bar[3]", + "result": null + }, + { + "expression": "foo.bar[-1]", + "result": "two" + }, + { + "expression": "foo.bar[-2]", + "result": "one" + }, + { + "expression": "foo.bar[-3]", + "result": "zero" + }, + { + "expression": "foo.bar[-4]", + "result": null + } + ] +}, { + "given": { + "foo": [ + { "bar": "one" }, + { "bar": "two" }, + { "bar": "three" }, + { "notbar": "four" } ] -}, -{ - "given": {"reservations": [{ - "instances": [ - {"foo": [{"bar": 1}, {"bar": 2}, {"notbar": 3}, {"bar": 4}]}, - {"foo": [{"bar": 5}, {"bar": 6}, {"notbar": [7]}, {"bar": 8}]}, - {"foo": "bar"}, - {"notfoo": [{"bar": 20}, {"bar": 21}, {"notbar": [7]}, {"bar": 22}]}, - {"bar": [{"baz": [1]}, {"baz": [2]}, {"baz": [3]}, {"baz": [4]}]}, - {"baz": [{"baz": [1, 2]}, {"baz": []}, {"baz": []}, {"baz": [3, 4]}]}, - {"qux": [{"baz": []}, {"baz": [1, 2, 3]}, {"baz": [4]}, {"baz": []}]} - ], - "otherkey": {"foo": [{"bar": 1}, {"bar": 2}, {"notbar": 3}, {"bar": 4}]} - }, { - "instances": [ - {"a": [{"bar": 1}, {"bar": 2}, {"notbar": 3}, {"bar": 4}]}, - {"b": [{"bar": 5}, {"bar": 6}, {"notbar": [7]}, {"bar": 8}]}, - {"c": "bar"}, - {"notfoo": [{"bar": 23}, {"bar": 24}, {"notbar": [7]}, {"bar": 25}]}, - {"qux": [{"baz": []}, {"baz": [1, 2, 3]}, {"baz": [4]}, {"baz": []}]} - ], - "otherkey": {"foo": [{"bar": 1}, {"bar": 2}, {"notbar": 3}, {"bar": 4}]} - } - ]}, - "cases": [ - { - "expression": "reservations[].instances[].foo[].bar", - "result": [1, 2, 4, 5, 6, 8] - }, - { - "expression": "reservations[].instances[].foo[].baz", - "result": [] - }, - { - "expression": "reservations[].instances[].notfoo[].bar", - "result": [20, 21, 22, 23, 24, 25] - }, - { - "expression": "reservations[].instances[].notfoo[].notbar", - "result": [[7], [7]] - }, - { - "expression": "reservations[].notinstances[].foo", - "result": [] - }, + }, + "cases": [ + { + "expression": "foo.bar", + "result": null + }, + { + "expression": "foo[0].bar", + "result": "one" + }, + { + "expression": "foo[1].bar", + "result": "two" + }, + { + "expression": "foo[2].bar", + "result": "three" + }, + { + "expression": "foo[3].notbar", + "result": "four" + }, + { + "expression": "foo[3].bar", + "result": null + }, + { + "expression": "foo[0]", + "result": { "bar": "one" } + }, + { + "expression": "foo[1]", + "result": { "bar": "two" } + }, + { + "expression": "foo[2]", + "result": { "bar": "three" } + }, + { + "expression": "foo[3]", + "result": { "notbar": "four" } + }, + { + "expression": "foo[4]", + "result": null + } + ] +}, { + "given": [ + "one", + "two", + "three" + ], + "cases": [ + { + "expression": "[0]", + "result": "one" + }, + { + "expression": "[1]", + "result": "two" + }, + { + "expression": "[2]", + "result": "three" + }, + { + "expression": "[-1]", + "result": "three" + }, + { + "expression": "[-2]", + "result": "two" + }, + { + "expression": "[-3]", + "result": "one" + } + ] +}, { + "given": { + "reservations": [ + { "instances": [{ "foo": 1 }, { "foo": 2 }] } + ] + }, + "cases": [ + { + "expression": "reservations[].instances[].foo", + "result": [1, 2] + }, + { + "expression": "reservations[].instances[].bar", + "result": [] + }, + { + "expression": "reservations[].notinstances[].foo", + "result": [] + }, + { + "expression": "reservations[].notinstances[].foo", + "result": [] + } + ] +}, { + "given": { + "reservations": [{ + "instances": [ + { "foo": [{ "bar": 1 }, { "bar": 2 }, { "notbar": 3 }, { "bar": 4 }] }, { - "expression": "reservations[].instances[].foo[].notbar", - "result": [3, [7]] + "foo": [{ "bar": 5 }, { "bar": 6 }, { "notbar": [7] }, { "bar": 8 }] }, + { "foo": "bar" }, { - "expression": "reservations[].instances[].bar[].baz", - "result": [[1], [2], [3], [4]] + "notfoo": [ + { "bar": 20 }, + { "bar": 21 }, + { "notbar": [7] }, + { "bar": 22 } + ] }, { - "expression": "reservations[].instances[].baz[].baz", - "result": [[1, 2], [], [], [3, 4]] + "bar": [ + { "baz": [1] }, + { "baz": [2] }, + { "baz": [3] }, + { "baz": [4] } + ] }, { - "expression": "reservations[].instances[].qux[].baz", - "result": [[], [1, 2, 3], [4], [], [], [1, 2, 3], [4], []] + "baz": [ + { "baz": [1, 2] }, + { "baz": [] }, + { "baz": [] }, + { "baz": [3, 4] } + ] }, { - "expression": "reservations[].instances[].qux[].baz[]", - "result": [1, 2, 3, 4, 1, 2, 3, 4] + "qux": [ + { "baz": [] }, + { "baz": [1, 2, 3] }, + { "baz": [4] }, + { "baz": [] } + ] } - ] -}, -{ - "given": { - "foo": [ - [["one", "two"], ["three", "four"]], - [["five", "six"], ["seven", "eight"]], - [["nine"], ["ten"]] - ] - }, - "cases": [ - { - "expression": "foo[]", - "result": [["one", "two"], ["three", "four"], ["five", "six"], - ["seven", "eight"], ["nine"], ["ten"]] - }, - { - "expression": "foo[][0]", - "result": ["one", "three", "five", "seven", "nine", "ten"] - }, - { - "expression": "foo[][1]", - "result": ["two", "four", "six", "eight"] - }, + ], + "otherkey": { + "foo": [{ "bar": 1 }, { "bar": 2 }, { "notbar": 3 }, { "bar": 4 }] + } + }, { + "instances": [ + { "a": [{ "bar": 1 }, { "bar": 2 }, { "notbar": 3 }, { "bar": 4 }] }, + { "b": [{ "bar": 5 }, { "bar": 6 }, { "notbar": [7] }, { "bar": 8 }] }, + { "c": "bar" }, { - "expression": "foo[][0][0]", - "result": [] - }, - { - "expression": "foo[][2][2]", - "result": [] - }, - { - "expression": "foo[][0][0][100]", - "result": [] - } - ] -}, -{ - "given": { - "foo": [{ - "bar": [ - { - "qux": 2, - "baz": 1 - }, - { - "qux": 4, - "baz": 3 - } + "notfoo": [ + { "bar": 23 }, + { "bar": 24 }, + { "notbar": [7] }, + { "bar": 25 } ] }, { - "bar": [ - { - "qux": 6, - "baz": 5 - }, - { - "qux": 8, - "baz": 7 - } + "qux": [ + { "baz": [] }, + { "baz": [1, 2, 3] }, + { "baz": [4] }, + { "baz": [] } ] } + ], + "otherkey": { + "foo": [{ "bar": 1 }, { "bar": 2 }, { "notbar": 3 }, { "bar": 4 }] + } + }] + }, + "cases": [ + { + "expression": "reservations[].instances[].foo[].bar", + "result": [1, 2, 4, 5, 6, 8] + }, + { + "expression": "reservations[].instances[].foo[].baz", + "result": [] + }, + { + "expression": "reservations[].instances[].notfoo[].bar", + "result": [20, 21, 22, 23, 24, 25] + }, + { + "expression": "reservations[].instances[].notfoo[].notbar", + "result": [[7], [7]] + }, + { + "expression": "reservations[].notinstances[].foo", + "result": [] + }, + { + "expression": "reservations[].instances[].foo[].notbar", + "result": [3, [7]] + }, + { + "expression": "reservations[].instances[].bar[].baz", + "result": [[1], [2], [3], [4]] + }, + { + "expression": "reservations[].instances[].baz[].baz", + "result": [[1, 2], [], [], [3, 4]] + }, + { + "expression": "reservations[].instances[].qux[].baz", + "result": [[], [1, 2, 3], [4], [], [], [1, 2, 3], [4], []] + }, + { + "expression": "reservations[].instances[].qux[].baz[]", + "result": [1, 2, 3, 4, 1, 2, 3, 4] + } + ] +}, { + "given": { + "foo": [ + [["one", "two"], ["three", "four"]], + [["five", "six"], ["seven", "eight"]], + [["nine"], ["ten"]] + ] + }, + "cases": [ + { + "expression": "foo[]", + "result": [ + ["one", "two"], + ["three", "four"], + ["five", "six"], + ["seven", "eight"], + ["nine"], + ["ten"] ] }, - "cases": [ - { - "expression": "foo", - "result": [{"bar": [{"qux": 2, "baz": 1}, {"qux": 4, "baz": 3}]}, - {"bar": [{"qux": 6, "baz": 5}, {"qux": 8, "baz": 7}]}] - }, + { + "expression": "foo[][0]", + "result": ["one", "three", "five", "seven", "nine", "ten"] + }, + { + "expression": "foo[][1]", + "result": ["two", "four", "six", "eight"] + }, + { + "expression": "foo[][0][0]", + "result": [] + }, + { + "expression": "foo[][2][2]", + "result": [] + }, + { + "expression": "foo[][0][0][100]", + "result": [] + } + ] +}, { + "given": { + "foo": [{ + "bar": [ { - "expression": "foo[]", - "result": [{"bar": [{"qux": 2, "baz": 1}, {"qux": 4, "baz": 3}]}, - {"bar": [{"qux": 6, "baz": 5}, {"qux": 8, "baz": 7}]}] + "qux": 2, + "baz": 1 }, { - "expression": "foo[].bar", - "result": [[{"qux": 2, "baz": 1}, {"qux": 4, "baz": 3}], - [{"qux": 6, "baz": 5}, {"qux": 8, "baz": 7}]] - }, + "qux": 4, + "baz": 3 + } + ] + }, { + "bar": [ { - "expression": "foo[].bar[]", - "result": [{"qux": 2, "baz": 1}, {"qux": 4, "baz": 3}, - {"qux": 6, "baz": 5}, {"qux": 8, "baz": 7}] + "qux": 6, + "baz": 5 }, { - "expression": "foo[].bar[].baz", - "result": [1, 3, 5, 7] + "qux": 8, + "baz": 7 } - ] -}, -{ - "given": { - "string": "string", - "hash": {"foo": "bar", "bar": "baz"}, - "number": 23, - "nullvalue": null - }, - "cases": [ - { - "expression": "string[]", - "result": null - }, - { - "expression": "hash[]", - "result": null - }, - { - "expression": "number[]", - "result": null - }, - { - "expression": "nullvalue[]", - "result": null - }, - { - "expression": "string[].foo", - "result": null - }, - { - "expression": "hash[].foo", - "result": null - }, - { - "expression": "number[].foo", - "result": null - }, - { - "expression": "nullvalue[].foo", - "result": null - }, - { - "expression": "nullvalue[].foo[].bar", - "result": null - } - ] -} -] + ] + }] + }, + "cases": [ + { + "expression": "foo", + "result": [ + { "bar": [{ "qux": 2, "baz": 1 }, { "qux": 4, "baz": 3 }] }, + { "bar": [{ "qux": 6, "baz": 5 }, { "qux": 8, "baz": 7 }] } + ] + }, + { + "expression": "foo[]", + "result": [ + { "bar": [{ "qux": 2, "baz": 1 }, { "qux": 4, "baz": 3 }] }, + { "bar": [{ "qux": 6, "baz": 5 }, { "qux": 8, "baz": 7 }] } + ] + }, + { + "expression": "foo[].bar", + "result": [ + [{ "qux": 2, "baz": 1 }, { "qux": 4, "baz": 3 }], + [{ "qux": 6, "baz": 5 }, { "qux": 8, "baz": 7 }] + ] + }, + { + "expression": "foo[].bar[]", + "result": [ + { "qux": 2, "baz": 1 }, + { "qux": 4, "baz": 3 }, + { "qux": 6, "baz": 5 }, + { "qux": 8, "baz": 7 } + ] + }, + { + "expression": "foo[].bar[].baz", + "result": [1, 3, 5, 7] + } + ] +}, { + "given": { + "string": "string", + "hash": { "foo": "bar", "bar": "baz" }, + "number": 23, + "nullvalue": null + }, + "cases": [ + { + "expression": "string[]", + "result": null + }, + { + "expression": "hash[]", + "result": null + }, + { + "expression": "number[]", + "result": null + }, + { + "expression": "nullvalue[]", + "result": null + }, + { + "expression": "string[].foo", + "result": null + }, + { + "expression": "hash[].foo", + "result": null + }, + { + "expression": "number[].foo", + "result": null + }, + { + "expression": "nullvalue[].foo", + "result": null + }, + { + "expression": "nullvalue[].foo[].bar", + "result": null + } + ] +}] diff --git a/test/compliance/literal.json b/test/compliance/literal.json index b796d36..e2afb59 100644 --- a/test/compliance/literal.json +++ b/test/compliance/literal.json @@ -1,190 +1,190 @@ [ - { - "given": { - "foo": [{"name": "a"}, {"name": "b"}], - "bar": {"baz": "qux"} - }, - "cases": [ - { - "expression": "`\"foo\"`", - "result": "foo" - }, - { - "comment": "Interpret escaped unicode.", - "expression": "`\"\\u03a6\"`", - "result": "Φ" - }, - { - "expression": "`\"✓\"`", - "result": "✓" - }, - { - "expression": "`[1, 2, 3]`", - "result": [1, 2, 3] - }, - { - "expression": "`{\"a\": \"b\"}`", - "result": {"a": "b"} - }, - { - "expression": "`true`", - "result": true - }, - { - "expression": "`false`", - "result": false - }, - { - "expression": "`null`", - "result": null - }, - { - "expression": "`0`", - "result": 0 - }, - { - "expression": "`1`", - "result": 1 - }, - { - "expression": "`2`", - "result": 2 - }, - { - "expression": "`3`", - "result": 3 - }, - { - "expression": "`4`", - "result": 4 - }, - { - "expression": "`5`", - "result": 5 - }, - { - "expression": "`6`", - "result": 6 - }, - { - "expression": "`7`", - "result": 7 - }, - { - "expression": "`8`", - "result": 8 - }, - { - "expression": "`9`", - "result": 9 - }, - { - "comment": "Escaping a backtick in quotes", - "expression": "`\"foo\\`bar\"`", - "result": "foo`bar" - }, - { - "comment": "Double quote in literal", - "expression": "`\"foo\\\"bar\"`", - "result": "foo\"bar" - }, - { - "expression": "`\"1\\`\"`", - "result": "1`" - }, - { - "comment": "Multiple literal expressions with escapes", - "expression": "`\"\\\\\"`.{a:`\"b\"`}", - "result": {"a": "b"} - }, - { - "comment": "literal . identifier", - "expression": "`{\"a\": \"b\"}`.a", - "result": "b" - }, - { - "comment": "literal . identifier . identifier", - "expression": "`{\"a\": {\"b\": \"c\"}}`.a.b", - "result": "c" - }, - { - "comment": "literal . identifier bracket-expr", - "expression": "`[0, 1, 2]`[1]", - "result": 1 - } - ] + { + "given": { + "foo": [{ "name": "a" }, { "name": "b" }], + "bar": { "baz": "qux" } }, - { - "comment": "Literals", - "given": {"type": "object"}, - "cases": [ - { - "comment": "Literal with leading whitespace", - "expression": "` {\"foo\": true}`", - "result": {"foo": true} - }, - { - "comment": "Literal with trailing whitespace", - "expression": "`{\"foo\": true} `", - "result": {"foo": true} - }, - { - "comment": "Literal on RHS of subexpr not allowed", - "expression": "foo.`\"bar\"`", - "error": "syntax" - } - ] - }, - { - "comment": "Raw String Literals", - "given": {}, - "cases": [ - { - "expression": "'foo'", - "result": "foo" - }, - { - "expression": "' foo '", - "result": " foo " - }, - { - "expression": "'0'", - "result": "0" - }, - { - "expression": "'newline\n'", - "result": "newline\n" - }, - { - "expression": "'\n'", - "result": "\n" - }, - { - "expression": "'✓'", - "result": "✓" - }, - { - "expression": "'𝄞'", - "result": "𝄞" - }, - { - "expression": "' [foo] '", - "result": " [foo] " - }, - { - "expression": "'[foo]'", - "result": "[foo]" - }, - { - "comment": "Do not interpret escaped unicode.", - "expression": "'\\u03a6'", - "result": "\\u03a6" - }, - { - "comment": "Can escape the single quote", - "expression": "'foo\\'bar'", - "result": "foo'bar" - } - ] - } + "cases": [ + { + "expression": "`\"foo\"`", + "result": "foo" + }, + { + "comment": "Interpret escaped unicode.", + "expression": "`\"\\u03a6\"`", + "result": "Φ" + }, + { + "expression": "`\"✓\"`", + "result": "✓" + }, + { + "expression": "`[1, 2, 3]`", + "result": [1, 2, 3] + }, + { + "expression": "`{\"a\": \"b\"}`", + "result": { "a": "b" } + }, + { + "expression": "`true`", + "result": true + }, + { + "expression": "`false`", + "result": false + }, + { + "expression": "`null`", + "result": null + }, + { + "expression": "`0`", + "result": 0 + }, + { + "expression": "`1`", + "result": 1 + }, + { + "expression": "`2`", + "result": 2 + }, + { + "expression": "`3`", + "result": 3 + }, + { + "expression": "`4`", + "result": 4 + }, + { + "expression": "`5`", + "result": 5 + }, + { + "expression": "`6`", + "result": 6 + }, + { + "expression": "`7`", + "result": 7 + }, + { + "expression": "`8`", + "result": 8 + }, + { + "expression": "`9`", + "result": 9 + }, + { + "comment": "Escaping a backtick in quotes", + "expression": "`\"foo\\`bar\"`", + "result": "foo`bar" + }, + { + "comment": "Double quote in literal", + "expression": "`\"foo\\\"bar\"`", + "result": "foo\"bar" + }, + { + "expression": "`\"1\\`\"`", + "result": "1`" + }, + { + "comment": "Multiple literal expressions with escapes", + "expression": "`\"\\\\\"`.{a:`\"b\"`}", + "result": { "a": "b" } + }, + { + "comment": "literal . identifier", + "expression": "`{\"a\": \"b\"}`.a", + "result": "b" + }, + { + "comment": "literal . identifier . identifier", + "expression": "`{\"a\": {\"b\": \"c\"}}`.a.b", + "result": "c" + }, + { + "comment": "literal . identifier bracket-expr", + "expression": "`[0, 1, 2]`[1]", + "result": 1 + } + ] + }, + { + "comment": "Literals", + "given": { "type": "object" }, + "cases": [ + { + "comment": "Literal with leading whitespace", + "expression": "` {\"foo\": true}`", + "result": { "foo": true } + }, + { + "comment": "Literal with trailing whitespace", + "expression": "`{\"foo\": true} `", + "result": { "foo": true } + }, + { + "comment": "Literal on RHS of subexpr not allowed", + "expression": "foo.`\"bar\"`", + "error": "syntax" + } + ] + }, + { + "comment": "Raw String Literals", + "given": {}, + "cases": [ + { + "expression": "'foo'", + "result": "foo" + }, + { + "expression": "' foo '", + "result": " foo " + }, + { + "expression": "'0'", + "result": "0" + }, + { + "expression": "'newline\n'", + "result": "newline\n" + }, + { + "expression": "'\n'", + "result": "\n" + }, + { + "expression": "'✓'", + "result": "✓" + }, + { + "expression": "'𝄞'", + "result": "𝄞" + }, + { + "expression": "' [foo] '", + "result": " [foo] " + }, + { + "expression": "'[foo]'", + "result": "[foo]" + }, + { + "comment": "Do not interpret escaped unicode.", + "expression": "'\\u03a6'", + "result": "\\u03a6" + }, + { + "comment": "Can escape the single quote", + "expression": "'foo\\'bar'", + "result": "foo'bar" + } + ] + } ] diff --git a/test/compliance/multiselect.json b/test/compliance/multiselect.json index 8f2a481..b7ed80a 100644 --- a/test/compliance/multiselect.json +++ b/test/compliance/multiselect.json @@ -1,393 +1,407 @@ [{ - "given": { - "foo": { - "bar": "bar", - "baz": "baz", - "qux": "qux", - "nested": { - "one": { - "a": "first", - "b": "second", - "c": "third" - }, - "two": { - "a": "first", - "b": "second", - "c": "third" - }, - "three": { - "a": "first", - "b": "second", - "c": {"inner": "third"} - } + "given": { + "foo": { + "bar": "bar", + "baz": "baz", + "qux": "qux", + "nested": { + "one": { + "a": "first", + "b": "second", + "c": "third" + }, + "two": { + "a": "first", + "b": "second", + "c": "third" + }, + "three": { + "a": "first", + "b": "second", + "c": { "inner": "third" } } - }, - "bar": 1, - "baz": 2, - "qux\"": 3 - }, - "cases": [ - { - "expression": "foo.{bar: bar}", - "result": {"bar": "bar"} - }, - { - "expression": "foo.{\"bar\": bar}", - "result": {"bar": "bar"} - }, - { - "expression": "foo.{\"foo.bar\": bar}", - "result": {"foo.bar": "bar"} - }, - { - "expression": "foo.{bar: bar, baz: baz}", - "result": {"bar": "bar", "baz": "baz"} - }, - { - "expression": "foo.{\"bar\": bar, \"baz\": baz}", - "result": {"bar": "bar", "baz": "baz"} - }, - { - "expression": "{\"baz\": baz, \"qux\\\"\": \"qux\\\"\"}", - "result": {"baz": 2, "qux\"": 3} - }, - { - "expression": "foo.{bar:bar,baz:baz}", - "result": {"bar": "bar", "baz": "baz"} - }, - { - "expression": "foo.{bar: bar,qux: qux}", - "result": {"bar": "bar", "qux": "qux"} - }, - { - "expression": "foo.{bar: bar, noexist: noexist}", - "result": {"bar": "bar", "noexist": null} - }, - { - "expression": "foo.{noexist: noexist, alsonoexist: alsonoexist}", - "result": {"noexist": null, "alsonoexist": null} - }, - { - "expression": "foo.badkey.{nokey: nokey, alsonokey: alsonokey}", - "result": null - }, - { - "expression": "foo.nested.*.{a: a,b: b}", - "result": [{"a": "first", "b": "second"}, - {"a": "first", "b": "second"}, - {"a": "first", "b": "second"}] - }, - { - "expression": "foo.nested.three.{a: a, cinner: c.inner}", - "result": {"a": "first", "cinner": "third"} - }, - { - "expression": "foo.nested.three.{a: a, c: c.inner.bad.key}", - "result": {"a": "first", "c": null} - }, - { - "expression": "foo.{a: nested.one.a, b: nested.two.b}", - "result": {"a": "first", "b": "second"} - }, - { - "expression": "{bar: bar, baz: baz}", - "result": {"bar": 1, "baz": 2} - }, - { - "expression": "{bar: bar}", - "result": {"bar": 1} - }, - { - "expression": "{otherkey: bar}", - "result": {"otherkey": 1} - }, - { - "expression": "{no: no, exist: exist}", - "result": {"no": null, "exist": null} - }, - { - "expression": "foo.[bar]", - "result": ["bar"] - }, - { - "expression": "foo.[bar,baz]", - "result": ["bar", "baz"] - }, - { - "expression": "foo.[bar,qux]", - "result": ["bar", "qux"] - }, - { - "expression": "foo.[bar,noexist]", - "result": ["bar", null] - }, - { - "expression": "foo.[noexist,alsonoexist]", - "result": [null, null] - } - ] + } + }, + "bar": 1, + "baz": 2, + "qux\"": 3 + }, + "cases": [ + { + "expression": "foo.{bar: bar}", + "result": { "bar": "bar" } + }, + { + "expression": "foo.{\"bar\": bar}", + "result": { "bar": "bar" } + }, + { + "expression": "foo.{\"foo.bar\": bar}", + "result": { "foo.bar": "bar" } + }, + { + "expression": "foo.{bar: bar, baz: baz}", + "result": { "bar": "bar", "baz": "baz" } + }, + { + "expression": "foo.{\"bar\": bar, \"baz\": baz}", + "result": { "bar": "bar", "baz": "baz" } + }, + { + "expression": "{\"baz\": baz, \"qux\\\"\": \"qux\\\"\"}", + "result": { "baz": 2, "qux\"": 3 } + }, + { + "expression": "foo.{bar:bar,baz:baz}", + "result": { "bar": "bar", "baz": "baz" } + }, + { + "expression": "foo.{bar: bar,qux: qux}", + "result": { "bar": "bar", "qux": "qux" } + }, + { + "expression": "foo.{bar: bar, noexist: noexist}", + "result": { "bar": "bar", "noexist": null } + }, + { + "expression": "foo.{noexist: noexist, alsonoexist: alsonoexist}", + "result": { "noexist": null, "alsonoexist": null } + }, + { + "expression": "foo.badkey.{nokey: nokey, alsonokey: alsonokey}", + "result": null + }, + { + "expression": "foo.nested.*.{a: a,b: b}", + "result": [ + { "a": "first", "b": "second" }, + { "a": "first", "b": "second" }, + { "a": "first", "b": "second" } + ] + }, + { + "expression": "foo.nested.three.{a: a, cinner: c.inner}", + "result": { "a": "first", "cinner": "third" } + }, + { + "expression": "foo.nested.three.{a: a, c: c.inner.bad.key}", + "result": { "a": "first", "c": null } + }, + { + "expression": "foo.{a: nested.one.a, b: nested.two.b}", + "result": { "a": "first", "b": "second" } + }, + { + "expression": "{bar: bar, baz: baz}", + "result": { "bar": 1, "baz": 2 } + }, + { + "expression": "{bar: bar}", + "result": { "bar": 1 } + }, + { + "expression": "{otherkey: bar}", + "result": { "otherkey": 1 } + }, + { + "expression": "{no: no, exist: exist}", + "result": { "no": null, "exist": null } + }, + { + "expression": "foo.[bar]", + "result": ["bar"] + }, + { + "expression": "foo.[bar,baz]", + "result": ["bar", "baz"] + }, + { + "expression": "foo.[bar,qux]", + "result": ["bar", "qux"] + }, + { + "expression": "foo.[bar,noexist]", + "result": ["bar", null] + }, + { + "expression": "foo.[noexist,alsonoexist]", + "result": [null, null] + } + ] }, { - "given": { - "foo": {"bar": 1, "baz": [2, 3, 4]} - }, - "cases": [ - { - "expression": "foo.{bar:bar,baz:baz}", - "result": {"bar": 1, "baz": [2, 3, 4]} - }, - { - "expression": "foo.[bar,baz[0]]", - "result": [1, 2] - }, - { - "expression": "foo.[bar,baz[1]]", - "result": [1, 3] - }, - { - "expression": "foo.[bar,baz[2]]", - "result": [1, 4] - }, - { - "expression": "foo.[bar,baz[3]]", - "result": [1, null] - }, - { - "expression": "foo.[bar[0],baz[3]]", - "result": [null, null] - } - ] + "given": { + "foo": { "bar": 1, "baz": [2, 3, 4] } + }, + "cases": [ + { + "expression": "foo.{bar:bar,baz:baz}", + "result": { "bar": 1, "baz": [2, 3, 4] } + }, + { + "expression": "foo.[bar,baz[0]]", + "result": [1, 2] + }, + { + "expression": "foo.[bar,baz[1]]", + "result": [1, 3] + }, + { + "expression": "foo.[bar,baz[2]]", + "result": [1, 4] + }, + { + "expression": "foo.[bar,baz[3]]", + "result": [1, null] + }, + { + "expression": "foo.[bar[0],baz[3]]", + "result": [null, null] + } + ] }, { - "given": { - "foo": {"bar": 1, "baz": 2} - }, - "cases": [ - { - "expression": "foo.{bar: bar, baz: baz}", - "result": {"bar": 1, "baz": 2} - }, - { - "expression": "foo.[bar,baz]", - "result": [1, 2] - } - ] + "given": { + "foo": { "bar": 1, "baz": 2 } + }, + "cases": [ + { + "expression": "foo.{bar: bar, baz: baz}", + "result": { "bar": 1, "baz": 2 } + }, + { + "expression": "foo.[bar,baz]", + "result": [1, 2] + } + ] }, { - "given": { - "foo": { - "bar": {"baz": [{"common": "first", "one": 1}, - {"common": "second", "two": 2}]}, - "ignoreme": 1, - "includeme": true - } + "given": { + "foo": { + "bar": { + "baz": [ + { "common": "first", "one": 1 }, + { "common": "second", "two": 2 } + ] + }, + "ignoreme": 1, + "includeme": true + } + }, + "cases": [ + { + "expression": "foo.{bar: bar.baz[1],includeme: includeme}", + "result": { "bar": { "common": "second", "two": 2 }, "includeme": true } }, - "cases": [ - { - "expression": "foo.{bar: bar.baz[1],includeme: includeme}", - "result": {"bar": {"common": "second", "two": 2}, "includeme": true} - }, - { - "expression": "foo.{\"bar.baz.two\": bar.baz[1].two, includeme: includeme}", - "result": {"bar.baz.two": 2, "includeme": true} - }, - { - "expression": "foo.[includeme, bar.baz[*].common]", - "result": [true, ["first", "second"]] - }, - { - "expression": "foo.[includeme, bar.baz[*].none]", - "result": [true, []] - }, - { - "expression": "foo.[includeme, bar.baz[].common]", - "result": [true, ["first", "second"]] - } - ] + { + "expression": "foo.{\"bar.baz.two\": bar.baz[1].two, includeme: includeme}", + "result": { "bar.baz.two": 2, "includeme": true } + }, + { + "expression": "foo.[includeme, bar.baz[*].common]", + "result": [true, ["first", "second"]] + }, + { + "expression": "foo.[includeme, bar.baz[*].none]", + "result": [true, []] + }, + { + "expression": "foo.[includeme, bar.baz[].common]", + "result": [true, ["first", "second"]] + } + ] }, { - "given": { - "reservations": [{ - "instances": [ - {"id": "id1", - "name": "first"}, - {"id": "id2", - "name": "second"} - ]}, { - "instances": [ - {"id": "id3", - "name": "third"}, - {"id": "id4", - "name": "fourth"} - ]} - ]}, - "cases": [ - { - "expression": "reservations[*].instances[*].{id: id, name: name}", - "result": [[{"id": "id1", "name": "first"}, {"id": "id2", "name": "second"}], - [{"id": "id3", "name": "third"}, {"id": "id4", "name": "fourth"}]] - }, - { - "expression": "reservations[].instances[].{id: id, name: name}", - "result": [{"id": "id1", "name": "first"}, - {"id": "id2", "name": "second"}, - {"id": "id3", "name": "third"}, - {"id": "id4", "name": "fourth"}] - }, - { - "expression": "reservations[].instances[].[id, name]", - "result": [["id1", "first"], - ["id2", "second"], - ["id3", "third"], - ["id4", "fourth"]] - } - ] -}, -{ - "given": { - "foo": [{ - "bar": [ - { - "qux": 2, - "baz": 1 - }, - { - "qux": 4, - "baz": 3 - } - ] - }, - { - "bar": [ - { - "qux": 6, - "baz": 5 - }, - { - "qux": 8, - "baz": 7 - } - ] - } + "given": { + "reservations": [{ + "instances": [ + { "id": "id1", "name": "first" }, + { "id": "id2", "name": "second" } + ] + }, { + "instances": [ + { "id": "id3", "name": "third" }, + { "id": "id4", "name": "fourth" } + ] + }] + }, + "cases": [ + { + "expression": "reservations[*].instances[*].{id: id, name: name}", + "result": [ + [{ "id": "id1", "name": "first" }, { "id": "id2", "name": "second" }], + [{ "id": "id3", "name": "third" }, { "id": "id4", "name": "fourth" }] ] }, - "cases": [ - { - "expression": "foo", - "result": [{"bar": [{"qux": 2, "baz": 1}, {"qux": 4, "baz": 3}]}, - {"bar": [{"qux": 6, "baz": 5}, {"qux": 8, "baz": 7}]}] - }, - { - "expression": "foo[]", - "result": [{"bar": [{"qux": 2, "baz": 1}, {"qux": 4, "baz": 3}]}, - {"bar": [{"qux": 6, "baz": 5}, {"qux": 8, "baz": 7}]}] - }, - { - "expression": "foo[].bar", - "result": [[{"qux": 2, "baz": 1}, {"qux": 4, "baz": 3}], - [{"qux": 6, "baz": 5}, {"qux": 8, "baz": 7}]] - }, + { + "expression": "reservations[].instances[].{id: id, name: name}", + "result": [ + { "id": "id1", "name": "first" }, + { "id": "id2", "name": "second" }, + { "id": "id3", "name": "third" }, + { "id": "id4", "name": "fourth" } + ] + }, + { + "expression": "reservations[].instances[].[id, name]", + "result": [ + ["id1", "first"], + ["id2", "second"], + ["id3", "third"], + ["id4", "fourth"] + ] + } + ] +}, { + "given": { + "foo": [{ + "bar": [ { - "expression": "foo[].bar[]", - "result": [{"qux": 2, "baz": 1}, {"qux": 4, "baz": 3}, - {"qux": 6, "baz": 5}, {"qux": 8, "baz": 7}] + "qux": 2, + "baz": 1 }, { - "expression": "foo[].bar[].[baz, qux]", - "result": [[1, 2], [3, 4], [5, 6], [7, 8]] - }, + "qux": 4, + "baz": 3 + } + ] + }, { + "bar": [ { - "expression": "foo[].bar[].[baz]", - "result": [[1], [3], [5], [7]] + "qux": 6, + "baz": 5 }, { - "expression": "foo[].bar[].[baz, qux][]", - "result": [1, 2, 3, 4, 5, 6, 7, 8] - } - ] -}, -{ - "given": { - "foo": { - "baz": [ - { - "bar": "abc" - }, { - "bar": "def" - } - ], - "qux": ["zero"] + "qux": 8, + "baz": 7 } + ] + }] + }, + "cases": [ + { + "expression": "foo", + "result": [ + { "bar": [{ "qux": 2, "baz": 1 }, { "qux": 4, "baz": 3 }] }, + { "bar": [{ "qux": 6, "baz": 5 }, { "qux": 8, "baz": 7 }] } + ] }, - "cases": [ - { - "expression": "foo.[baz[*].bar, qux[0]]", - "result": [["abc", "def"], "zero"] - } - ] -}, -{ - "given": { - "foo": { - "baz": [ - { - "bar": "a", - "bam": "b", - "boo": "c" - }, { - "bar": "d", - "bam": "e", - "boo": "f" - } - ], - "qux": ["zero"] - } + { + "expression": "foo[]", + "result": [ + { "bar": [{ "qux": 2, "baz": 1 }, { "qux": 4, "baz": 3 }] }, + { "bar": [{ "qux": 6, "baz": 5 }, { "qux": 8, "baz": 7 }] } + ] }, - "cases": [ - { - "expression": "foo.[baz[*].[bar, boo], qux[0]]", - "result": [[["a", "c" ], ["d", "f" ]], "zero"] - } - ] -}, -{ - "given": { - "foo": { - "baz": [ - { - "bar": "a", - "bam": "b", - "boo": "c" - }, { - "bar": "d", - "bam": "e", - "boo": "f" - } - ], - "qux": ["zero"] - } + { + "expression": "foo[].bar", + "result": [ + [{ "qux": 2, "baz": 1 }, { "qux": 4, "baz": 3 }], + [{ "qux": 6, "baz": 5 }, { "qux": 8, "baz": 7 }] + ] }, - "cases": [ + { + "expression": "foo[].bar[]", + "result": [ + { "qux": 2, "baz": 1 }, + { "qux": 4, "baz": 3 }, + { "qux": 6, "baz": 5 }, + { "qux": 8, "baz": 7 } + ] + }, + { + "expression": "foo[].bar[].[baz, qux]", + "result": [[1, 2], [3, 4], [5, 6], [7, 8]] + }, + { + "expression": "foo[].bar[].[baz]", + "result": [[1], [3], [5], [7]] + }, + { + "expression": "foo[].bar[].[baz, qux][]", + "result": [1, 2, 3, 4, 5, 6, 7, 8] + } + ] +}, { + "given": { + "foo": { + "baz": [ { - "expression": "foo.[baz[*].not_there || baz[*].bar, qux[0]]", - "result": [["a", "d"], "zero"] + "bar": "abc" + }, + { + "bar": "def" } - ] -}, -{ - "given": {"type": "object"}, - "cases": [ + ], + "qux": ["zero"] + } + }, + "cases": [ + { + "expression": "foo.[baz[*].bar, qux[0]]", + "result": [["abc", "def"], "zero"] + } + ] +}, { + "given": { + "foo": { + "baz": [ + { + "bar": "a", + "bam": "b", + "boo": "c" + }, { - "comment": "Nested multiselect", - "expression": "[[*],*]", - "result": [null, ["object"]] + "bar": "d", + "bam": "e", + "boo": "f" } - ] -}, -{ - "given": [], - "cases": [ + ], + "qux": ["zero"] + } + }, + "cases": [ + { + "expression": "foo.[baz[*].[bar, boo], qux[0]]", + "result": [[["a", "c"], ["d", "f"]], "zero"] + } + ] +}, { + "given": { + "foo": { + "baz": [ + { + "bar": "a", + "bam": "b", + "boo": "c" + }, { - "comment": "Nested multiselect", - "expression": "[[*]]", - "result": [[]] + "bar": "d", + "bam": "e", + "boo": "f" } - ] -} -] + ], + "qux": ["zero"] + } + }, + "cases": [ + { + "expression": "foo.[baz[*].not_there || baz[*].bar, qux[0]]", + "result": [["a", "d"], "zero"] + } + ] +}, { + "given": { "type": "object" }, + "cases": [ + { + "comment": "Nested multiselect", + "expression": "[[*],*]", + "result": [null, ["object"]] + } + ] +}, { + "given": [], + "cases": [ + { + "comment": "Nested multiselect", + "expression": "[[*]]", + "result": [[]] + } + ] +}] diff --git a/test/compliance/pipe.json b/test/compliance/pipe.json index b10c0a4..dfcd2b1 100644 --- a/test/compliance/pipe.json +++ b/test/compliance/pipe.json @@ -67,7 +67,7 @@ "cases": [ { "expression": "foo | bar", - "result": {"baz": "one"} + "result": { "baz": "one" } }, { "expression": "foo | bar | baz", @@ -87,15 +87,15 @@ }, { "expression": "[foo.bar, foo.other] | [0]", - "result": {"baz": "one"} + "result": { "baz": "one" } }, { "expression": "{\"a\": foo.bar, \"b\": foo.other} | a", - "result": {"baz": "one"} + "result": { "baz": "one" } }, { "expression": "{\"a\": foo.bar, \"b\": foo.other} | b", - "result": {"baz": "two"} + "result": { "baz": "two" } }, { "expression": "foo.bam || foo.bar | baz", @@ -103,7 +103,7 @@ }, { "expression": "foo | not_there || bar", - "result": {"baz": "one"} + "result": { "baz": "one" } } ] }, { @@ -125,7 +125,7 @@ "cases": [ { "expression": "foo[*].bar[*] | [0][0]", - "result": {"baz": "one"} + "result": { "baz": "one" } } ] }] diff --git a/test/compliance/slice.json b/test/compliance/slice.json index 3594772..34f20ef 100644 --- a/test/compliance/slice.json +++ b/test/compliance/slice.json @@ -133,9 +133,8 @@ ] }, { "given": { - "foo": [{"a": 1}, {"a": 2}, {"a": 3}], - "bar": [{"a": {"b": 1}}, {"a": {"b": 2}}, - {"a": {"b": 3}}], + "foo": [{ "a": 1 }, { "a": 2 }, { "a": 3 }], + "bar": [{ "a": { "b": 1 } }, { "a": { "b": 2 } }, { "a": { "b": 3 } }], "baz": 50 }, "cases": [ @@ -165,11 +164,11 @@ } ] }, { - "given": [{"a": 1}, {"a": 2}, {"a": 3}], + "given": [{ "a": 1 }, { "a": 2 }, { "a": 3 }], "cases": [ { "expression": "[:]", - "result": [{"a": 1}, {"a": 2}, {"a": 3}] + "result": [{ "a": 1 }, { "a": 2 }, { "a": 3 }] }, { "expression": "[:2].a", diff --git a/test/compliance/syntax.json b/test/compliance/syntax.json index 003c294..122f4ff 100644 --- a/test/compliance/syntax.json +++ b/test/compliance/syntax.json @@ -1,6 +1,6 @@ [{ "comment": "Dot syntax", - "given": {"type": "object"}, + "given": { "type": "object" }, "cases": [ { "expression": "foo.bar", @@ -43,574 +43,561 @@ "error": "syntax" } ] -}, - { - "comment": "Simple token errors", - "given": {"type": "object"}, - "cases": [ - { - "expression": ".", - "error": "syntax" - }, - { - "expression": ":", - "error": "syntax" - }, - { - "expression": ",", - "error": "syntax" - }, - { - "expression": "]", - "error": "syntax" - }, - { - "expression": "[", - "error": "syntax" - }, - { - "expression": "}", - "error": "syntax" - }, - { - "expression": "{", - "error": "syntax" - }, - { - "expression": ")", - "error": "syntax" - }, - { - "expression": "(", - "error": "syntax" - }, - { - "expression": "((&", - "error": "syntax" - }, - { - "expression": "a[", - "error": "syntax" - }, - { - "expression": "a]", - "error": "syntax" - }, - { - "expression": "a][", - "error": "syntax" - }, - { - "expression": "!", - "error": "syntax" - } - ] - }, - { - "comment": "Boolean syntax errors", - "given": {"type": "object"}, - "cases": [ - { - "expression": "![!(!", - "error": "syntax" - } - ] - }, - { - "comment": "Wildcard syntax", - "given": {"type": "object"}, - "cases": [ - { - "expression": "*", - "result": ["object"] - }, - { - "expression": "*.*", - "result": [] - }, - { - "expression": "*.foo", - "result": [] - }, - { - "expression": "*[0]", - "result": [] - }, - { - "expression": ".*", - "error": "syntax" - }, - { - "expression": "*foo", - "error": "syntax" - }, - { - "expression": "*0", - "error": "syntax" - }, - { - "expression": "foo[*]bar", - "error": "syntax" - }, - { - "expression": "foo[*]*", - "error": "syntax" - } - ] - }, - { - "comment": "Flatten syntax", - "given": {"type": "object"}, - "cases": [ - { - "expression": "[]", - "result": null - } - ] - }, - { - "comment": "Simple bracket syntax", - "given": {"type": "object"}, - "cases": [ - { - "expression": "[0]", - "result": null - }, - { - "expression": "[*]", - "result": null - }, - { - "expression": "*.[0]", - "error": "syntax" - }, - { - "expression": "*.[\"0\"]", - "result": [[null]] - }, - { - "expression": "[*].bar", - "result": null - }, - { - "expression": "[*][0]", - "result": null - }, - { - "expression": "foo[#]", - "error": "syntax" - } - ] - }, - { - "comment": "Multi-select list syntax", - "given": {"type": "object"}, - "cases": [ - { - "expression": "foo[0]", - "result": null - }, - { - "comment": "Valid multi-select of a list", - "expression": "foo[0, 1]", - "error": "syntax" - }, - { - "expression": "foo.[0]", - "error": "syntax" - }, - { - "expression": "foo.[*]", - "result": null - }, - { - "comment": "Multi-select of a list with trailing comma", - "expression": "foo[0, ]", - "error": "syntax" - }, - { - "comment": "Multi-select of a list with trailing comma and no close", - "expression": "foo[0,", - "error": "syntax" - }, - { - "comment": "Multi-select of a list with trailing comma and no close", - "expression": "foo.[a", - "error": "syntax" - }, - { - "comment": "Multi-select of a list with extra comma", - "expression": "foo[0,, 1]", - "error": "syntax" - }, - { - "comment": "Multi-select of a list using an identifier index", - "expression": "foo[abc]", - "error": "syntax" - }, - { - "comment": "Multi-select of a list using identifier indices", - "expression": "foo[abc, def]", - "error": "syntax" - }, - { - "comment": "Multi-select of a list using an identifier index", - "expression": "foo[abc, 1]", - "error": "syntax" - }, - { - "comment": "Multi-select of a list using an identifier index with trailing comma", - "expression": "foo[abc, ]", - "error": "syntax" - }, - { - "comment": "Valid multi-select of a hash using an identifier index", - "expression": "foo.[abc]", - "result": null - }, - { - "comment": "Valid multi-select of a hash", - "expression": "foo.[abc, def]", - "result": null - }, - { - "comment": "Multi-select of a hash using a numeric index", - "expression": "foo.[abc, 1]", - "error": "syntax" - }, - { - "comment": "Multi-select of a hash with a trailing comma", - "expression": "foo.[abc, ]", - "error": "syntax" - }, - { - "comment": "Multi-select of a hash with extra commas", - "expression": "foo.[abc,, def]", - "error": "syntax" - }, - { - "comment": "Multi-select of a hash using number indices", - "expression": "foo.[0, 1]", - "error": "syntax" - } - ] - }, - { - "comment": "Multi-select hash syntax", - "given": {"type": "object"}, - "cases": [ - { - "comment": "No key or value", - "expression": "a{}", - "error": "syntax" - }, - { - "comment": "No closing token", - "expression": "a{", - "error": "syntax" - }, - { - "comment": "Not a key value pair", - "expression": "a{foo}", - "error": "syntax" - }, - { - "comment": "Missing value and closing character", - "expression": "a{foo:", - "error": "syntax" - }, - { - "comment": "Missing closing character", - "expression": "a{foo: 0", - "error": "syntax" - }, - { - "comment": "Missing value", - "expression": "a{foo:}", - "error": "syntax" - }, - { - "comment": "Trailing comma and no closing character", - "expression": "a{foo: 0, ", - "error": "syntax" - }, - { - "comment": "Missing value with trailing comma", - "expression": "a{foo: ,}", - "error": "syntax" - }, - { - "comment": "Accessing Array using an identifier", - "expression": "a{foo: bar}", - "error": "syntax" - }, - { - "expression": "a{foo: 0}", - "error": "syntax" - }, - { - "comment": "Missing key-value pair", - "expression": "a.{}", - "error": "syntax" - }, - { - "comment": "Not a key-value pair", - "expression": "a.{foo}", - "error": "syntax" - }, - { - "comment": "Missing value", - "expression": "a.{foo:}", - "error": "syntax" - }, - { - "comment": "Missing value with trailing comma", - "expression": "a.{foo: ,}", - "error": "syntax" - }, - { - "comment": "Valid multi-select hash extraction", - "expression": "a.{foo: bar}", - "result": null - }, - { - "comment": "Valid multi-select hash extraction", - "expression": "a.{foo: bar, baz: bam}", - "result": null - }, - { - "comment": "Trailing comma", - "expression": "a.{foo: bar, }", - "error": "syntax" - }, - { - "comment": "Missing key in second key-value pair", - "expression": "a.{foo: bar, baz}", - "error": "syntax" - }, - { - "comment": "Missing value in second key-value pair", - "expression": "a.{foo: bar, baz:}", - "error": "syntax" - }, - { - "comment": "Trailing comma", - "expression": "a.{foo: bar, baz: bam, }", - "error": "syntax" - }, - { - "comment": "Nested multi select", - "expression": "{\"\\\\\":{\" \":*}}", - "result": {"\\": {" ": ["object"]}} - } - ] - }, - { - "comment": "Or expressions", - "given": {"type": "object"}, - "cases": [ - { - "expression": "foo || bar", - "result": null - }, - { - "expression": "foo ||", - "error": "syntax" - }, - { - "expression": "foo.|| bar", - "error": "syntax" - }, - { - "expression": " || foo", - "error": "syntax" - }, - { - "expression": "foo || || foo", - "error": "syntax" - }, - { - "expression": "foo.[a || b]", - "result": null - }, - { - "expression": "foo.[a ||]", - "error": "syntax" - }, - { - "expression": "\"foo", - "error": "syntax" - } - ] - }, - { - "comment": "Filter expressions", - "given": {"type": "object"}, - "cases": [ - { - "expression": "foo[?bar==`\"baz\"`]", - "result": null - }, - { - "expression": "foo[? bar == `\"baz\"` ]", - "result": null - }, - { - "expression": "foo[ ?bar==`\"baz\"`]", - "error": "syntax" - }, - { - "expression": "foo[?bar==]", - "error": "syntax" - }, - { - "expression": "foo[?==]", - "error": "syntax" - }, - { - "expression": "foo[?==bar]", - "error": "syntax" - }, - { - "expression": "foo[?bar==baz?]", - "error": "syntax" - }, - { - "expression": "foo[?a.b.c==d.e.f]", - "result": null - }, - { - "expression": "foo[?bar==`[0, 1, 2]`]", - "result": null - }, - { - "expression": "foo[?bar==`[\"a\", \"b\", \"c\"]`]", - "result": null - }, - { - "comment": "Literal char not escaped", - "expression": "foo[?bar==`[\"foo`bar\"]`]", - "error": "syntax" - }, - { - "comment": "Literal char escaped", - "expression": "foo[?bar==`[\"foo\\`bar\"]`]", - "result": null - }, - { - "comment": "Unknown comparator", - "expression": "foo[?bar<>baz]", - "error": "syntax" - }, - { - "comment": "Unknown comparator", - "expression": "foo[?bar^baz]", - "error": "syntax" - }, - { - "expression": "foo[bar==baz]", - "error": "syntax" - }, - { - "comment": "Quoted identifier in filter expression no spaces", - "expression": "[?\"\\\\\">`\"foo\"`]", - "result": null - }, - { - "comment": "Quoted identifier in filter expression with spaces", - "expression": "[?\"\\\\\" > `\"foo\"`]", - "result": null - } - ] - }, - { - "comment": "Filter expression errors", - "given": {"type": "object"}, - "cases": [ - { - "expression": "bar.`\"anything\"`", - "error": "syntax" - }, - { - "expression": "bar.baz.noexists.`\"literal\"`", - "error": "syntax" - }, - { - "comment": "Literal wildcard projection", - "expression": "foo[*].`\"literal\"`", - "error": "syntax" - }, - { - "expression": "foo[*].name.`\"literal\"`", - "error": "syntax" - }, - { - "expression": "foo[].name.`\"literal\"`", - "error": "syntax" - }, - { - "expression": "foo[].name.`\"literal\"`.`\"subliteral\"`", - "error": "syntax" - }, - { - "comment": "Projecting a literal onto an empty list", - "expression": "foo[*].name.noexist.`\"literal\"`", - "error": "syntax" - }, - { - "expression": "foo[].name.noexist.`\"literal\"`", - "error": "syntax" - }, - { - "expression": "twolen[*].`\"foo\"`", - "error": "syntax" - }, - { - "comment": "Two level projection of a literal", - "expression": "twolen[*].threelen[*].`\"bar\"`", - "error": "syntax" - }, - { - "comment": "Two level flattened projection of a literal", - "expression": "twolen[].threelen[].`\"bar\"`", - "error": "syntax" - } - ] - }, - { - "comment": "Identifiers", - "given": {"type": "object"}, - "cases": [ - { - "expression": "foo", - "result": null - }, - { - "expression": "\"foo\"", - "result": null - }, - { - "expression": "\"\\\\\"", - "result": null - } - ] - }, - { - "comment": "Combined syntax", - "given": [], - "cases": [ - { - "expression": "*||*|*|*", - "result": null - }, - { - "expression": "*[]||[*]", - "result": [] - }, - { - "expression": "[*.*]", - "result": [null] - } - ] - } -] +}, { + "comment": "Simple token errors", + "given": { "type": "object" }, + "cases": [ + { + "expression": ".", + "error": "syntax" + }, + { + "expression": ":", + "error": "syntax" + }, + { + "expression": ",", + "error": "syntax" + }, + { + "expression": "]", + "error": "syntax" + }, + { + "expression": "[", + "error": "syntax" + }, + { + "expression": "}", + "error": "syntax" + }, + { + "expression": "{", + "error": "syntax" + }, + { + "expression": ")", + "error": "syntax" + }, + { + "expression": "(", + "error": "syntax" + }, + { + "expression": "((&", + "error": "syntax" + }, + { + "expression": "a[", + "error": "syntax" + }, + { + "expression": "a]", + "error": "syntax" + }, + { + "expression": "a][", + "error": "syntax" + }, + { + "expression": "!", + "error": "syntax" + } + ] +}, { + "comment": "Boolean syntax errors", + "given": { "type": "object" }, + "cases": [ + { + "expression": "![!(!", + "error": "syntax" + } + ] +}, { + "comment": "Wildcard syntax", + "given": { "type": "object" }, + "cases": [ + { + "expression": "*", + "result": ["object"] + }, + { + "expression": "*.*", + "result": [] + }, + { + "expression": "*.foo", + "result": [] + }, + { + "expression": "*[0]", + "result": [] + }, + { + "expression": ".*", + "error": "syntax" + }, + { + "expression": "*foo", + "error": "syntax" + }, + { + "expression": "*0", + "error": "syntax" + }, + { + "expression": "foo[*]bar", + "error": "syntax" + }, + { + "expression": "foo[*]*", + "error": "syntax" + } + ] +}, { + "comment": "Flatten syntax", + "given": { "type": "object" }, + "cases": [ + { + "expression": "[]", + "result": null + } + ] +}, { + "comment": "Simple bracket syntax", + "given": { "type": "object" }, + "cases": [ + { + "expression": "[0]", + "result": null + }, + { + "expression": "[*]", + "result": null + }, + { + "expression": "*.[0]", + "error": "syntax" + }, + { + "expression": "*.[\"0\"]", + "result": [[null]] + }, + { + "expression": "[*].bar", + "result": null + }, + { + "expression": "[*][0]", + "result": null + }, + { + "expression": "foo[#]", + "error": "syntax" + } + ] +}, { + "comment": "Multi-select list syntax", + "given": { "type": "object" }, + "cases": [ + { + "expression": "foo[0]", + "result": null + }, + { + "comment": "Valid multi-select of a list", + "expression": "foo[0, 1]", + "error": "syntax" + }, + { + "expression": "foo.[0]", + "error": "syntax" + }, + { + "expression": "foo.[*]", + "result": null + }, + { + "comment": "Multi-select of a list with trailing comma", + "expression": "foo[0, ]", + "error": "syntax" + }, + { + "comment": "Multi-select of a list with trailing comma and no close", + "expression": "foo[0,", + "error": "syntax" + }, + { + "comment": "Multi-select of a list with trailing comma and no close", + "expression": "foo.[a", + "error": "syntax" + }, + { + "comment": "Multi-select of a list with extra comma", + "expression": "foo[0,, 1]", + "error": "syntax" + }, + { + "comment": "Multi-select of a list using an identifier index", + "expression": "foo[abc]", + "error": "syntax" + }, + { + "comment": "Multi-select of a list using identifier indices", + "expression": "foo[abc, def]", + "error": "syntax" + }, + { + "comment": "Multi-select of a list using an identifier index", + "expression": "foo[abc, 1]", + "error": "syntax" + }, + { + "comment": "Multi-select of a list using an identifier index with trailing comma", + "expression": "foo[abc, ]", + "error": "syntax" + }, + { + "comment": "Valid multi-select of a hash using an identifier index", + "expression": "foo.[abc]", + "result": null + }, + { + "comment": "Valid multi-select of a hash", + "expression": "foo.[abc, def]", + "result": null + }, + { + "comment": "Multi-select of a hash using a numeric index", + "expression": "foo.[abc, 1]", + "error": "syntax" + }, + { + "comment": "Multi-select of a hash with a trailing comma", + "expression": "foo.[abc, ]", + "error": "syntax" + }, + { + "comment": "Multi-select of a hash with extra commas", + "expression": "foo.[abc,, def]", + "error": "syntax" + }, + { + "comment": "Multi-select of a hash using number indices", + "expression": "foo.[0, 1]", + "error": "syntax" + } + ] +}, { + "comment": "Multi-select hash syntax", + "given": { "type": "object" }, + "cases": [ + { + "comment": "No key or value", + "expression": "a{}", + "error": "syntax" + }, + { + "comment": "No closing token", + "expression": "a{", + "error": "syntax" + }, + { + "comment": "Not a key value pair", + "expression": "a{foo}", + "error": "syntax" + }, + { + "comment": "Missing value and closing character", + "expression": "a{foo:", + "error": "syntax" + }, + { + "comment": "Missing closing character", + "expression": "a{foo: 0", + "error": "syntax" + }, + { + "comment": "Missing value", + "expression": "a{foo:}", + "error": "syntax" + }, + { + "comment": "Trailing comma and no closing character", + "expression": "a{foo: 0, ", + "error": "syntax" + }, + { + "comment": "Missing value with trailing comma", + "expression": "a{foo: ,}", + "error": "syntax" + }, + { + "comment": "Accessing Array using an identifier", + "expression": "a{foo: bar}", + "error": "syntax" + }, + { + "expression": "a{foo: 0}", + "error": "syntax" + }, + { + "comment": "Missing key-value pair", + "expression": "a.{}", + "error": "syntax" + }, + { + "comment": "Not a key-value pair", + "expression": "a.{foo}", + "error": "syntax" + }, + { + "comment": "Missing value", + "expression": "a.{foo:}", + "error": "syntax" + }, + { + "comment": "Missing value with trailing comma", + "expression": "a.{foo: ,}", + "error": "syntax" + }, + { + "comment": "Valid multi-select hash extraction", + "expression": "a.{foo: bar}", + "result": null + }, + { + "comment": "Valid multi-select hash extraction", + "expression": "a.{foo: bar, baz: bam}", + "result": null + }, + { + "comment": "Trailing comma", + "expression": "a.{foo: bar, }", + "error": "syntax" + }, + { + "comment": "Missing key in second key-value pair", + "expression": "a.{foo: bar, baz}", + "error": "syntax" + }, + { + "comment": "Missing value in second key-value pair", + "expression": "a.{foo: bar, baz:}", + "error": "syntax" + }, + { + "comment": "Trailing comma", + "expression": "a.{foo: bar, baz: bam, }", + "error": "syntax" + }, + { + "comment": "Nested multi select", + "expression": "{\"\\\\\":{\" \":*}}", + "result": { "\\": { " ": ["object"] } } + } + ] +}, { + "comment": "Or expressions", + "given": { "type": "object" }, + "cases": [ + { + "expression": "foo || bar", + "result": null + }, + { + "expression": "foo ||", + "error": "syntax" + }, + { + "expression": "foo.|| bar", + "error": "syntax" + }, + { + "expression": " || foo", + "error": "syntax" + }, + { + "expression": "foo || || foo", + "error": "syntax" + }, + { + "expression": "foo.[a || b]", + "result": null + }, + { + "expression": "foo.[a ||]", + "error": "syntax" + }, + { + "expression": "\"foo", + "error": "syntax" + } + ] +}, { + "comment": "Filter expressions", + "given": { "type": "object" }, + "cases": [ + { + "expression": "foo[?bar==`\"baz\"`]", + "result": null + }, + { + "expression": "foo[? bar == `\"baz\"` ]", + "result": null + }, + { + "expression": "foo[ ?bar==`\"baz\"`]", + "error": "syntax" + }, + { + "expression": "foo[?bar==]", + "error": "syntax" + }, + { + "expression": "foo[?==]", + "error": "syntax" + }, + { + "expression": "foo[?==bar]", + "error": "syntax" + }, + { + "expression": "foo[?bar==baz?]", + "error": "syntax" + }, + { + "expression": "foo[?a.b.c==d.e.f]", + "result": null + }, + { + "expression": "foo[?bar==`[0, 1, 2]`]", + "result": null + }, + { + "expression": "foo[?bar==`[\"a\", \"b\", \"c\"]`]", + "result": null + }, + { + "comment": "Literal char not escaped", + "expression": "foo[?bar==`[\"foo`bar\"]`]", + "error": "syntax" + }, + { + "comment": "Literal char escaped", + "expression": "foo[?bar==`[\"foo\\`bar\"]`]", + "result": null + }, + { + "comment": "Unknown comparator", + "expression": "foo[?bar<>baz]", + "error": "syntax" + }, + { + "comment": "Unknown comparator", + "expression": "foo[?bar^baz]", + "error": "syntax" + }, + { + "expression": "foo[bar==baz]", + "error": "syntax" + }, + { + "comment": "Quoted identifier in filter expression no spaces", + "expression": "[?\"\\\\\">`\"foo\"`]", + "result": null + }, + { + "comment": "Quoted identifier in filter expression with spaces", + "expression": "[?\"\\\\\" > `\"foo\"`]", + "result": null + } + ] +}, { + "comment": "Filter expression errors", + "given": { "type": "object" }, + "cases": [ + { + "expression": "bar.`\"anything\"`", + "error": "syntax" + }, + { + "expression": "bar.baz.noexists.`\"literal\"`", + "error": "syntax" + }, + { + "comment": "Literal wildcard projection", + "expression": "foo[*].`\"literal\"`", + "error": "syntax" + }, + { + "expression": "foo[*].name.`\"literal\"`", + "error": "syntax" + }, + { + "expression": "foo[].name.`\"literal\"`", + "error": "syntax" + }, + { + "expression": "foo[].name.`\"literal\"`.`\"subliteral\"`", + "error": "syntax" + }, + { + "comment": "Projecting a literal onto an empty list", + "expression": "foo[*].name.noexist.`\"literal\"`", + "error": "syntax" + }, + { + "expression": "foo[].name.noexist.`\"literal\"`", + "error": "syntax" + }, + { + "expression": "twolen[*].`\"foo\"`", + "error": "syntax" + }, + { + "comment": "Two level projection of a literal", + "expression": "twolen[*].threelen[*].`\"bar\"`", + "error": "syntax" + }, + { + "comment": "Two level flattened projection of a literal", + "expression": "twolen[].threelen[].`\"bar\"`", + "error": "syntax" + } + ] +}, { + "comment": "Identifiers", + "given": { "type": "object" }, + "cases": [ + { + "expression": "foo", + "result": null + }, + { + "expression": "\"foo\"", + "result": null + }, + { + "expression": "\"\\\\\"", + "result": null + } + ] +}, { + "comment": "Combined syntax", + "given": [], + "cases": [ + { + "expression": "*||*|*|*", + "result": null + }, + { + "expression": "*[]||[*]", + "result": [] + }, + { + "expression": "[*.*]", + "result": [null] + } + ] +}] diff --git a/test/compliance/unicode.json b/test/compliance/unicode.json index 6b07b0b..3b6d29d 100644 --- a/test/compliance/unicode.json +++ b/test/compliance/unicode.json @@ -1,38 +1,38 @@ [ - { - "given": {"foo": [{"✓": "✓"}, {"✓": "✗"}]}, - "cases": [ - { - "expression": "foo[].\"✓\"", - "result": ["✓", "✗"] - } - ] - }, - { - "given": {"☯": true}, - "cases": [ - { - "expression": "\"☯\"", - "result": true - } - ] - }, - { - "given": {"♪♫•*¨*•.¸¸❤¸¸.•*¨*•♫♪": true}, - "cases": [ - { - "expression": "\"♪♫•*¨*•.¸¸❤¸¸.•*¨*•♫♪\"", - "result": true - } - ] - }, - { - "given": {"☃": true}, - "cases": [ - { - "expression": "\"☃\"", - "result": true - } - ] - } + { + "given": { "foo": [{ "✓": "✓" }, { "✓": "✗" }] }, + "cases": [ + { + "expression": "foo[].\"✓\"", + "result": ["✓", "✗"] + } + ] + }, + { + "given": { "☯": true }, + "cases": [ + { + "expression": "\"☯\"", + "result": true + } + ] + }, + { + "given": { "♪♫•*¨*•.¸¸❤¸¸.•*¨*•♫♪": true }, + "cases": [ + { + "expression": "\"♪♫•*¨*•.¸¸❤¸¸.•*¨*•♫♪\"", + "result": true + } + ] + }, + { + "given": { "☃": true }, + "cases": [ + { + "expression": "\"☃\"", + "result": true + } + ] + } ] diff --git a/test/compliance/wildcard.json b/test/compliance/wildcard.json index 3bcec30..679f1d2 100644 --- a/test/compliance/wildcard.json +++ b/test/compliance/wildcard.json @@ -1,460 +1,473 @@ [{ - "given": { - "foo": { - "bar": { - "baz": "val" - }, - "other": { - "baz": "val" - }, - "other2": { - "baz": "val" - }, - "other3": { - "notbaz": ["a", "b", "c"] - }, - "other4": { - "notbaz": ["a", "b", "c"] - }, - "other5": { - "other": { - "a": 1, - "b": 1, - "c": 1 - } - } + "given": { + "foo": { + "bar": { + "baz": "val" + }, + "other": { + "baz": "val" + }, + "other2": { + "baz": "val" + }, + "other3": { + "notbaz": ["a", "b", "c"] + }, + "other4": { + "notbaz": ["a", "b", "c"] + }, + "other5": { + "other": { + "a": 1, + "b": 1, + "c": 1 } + } + } + }, + "cases": [ + { + "expression": "foo.*.baz", + "result": ["val", "val", "val"] }, - "cases": [ - { - "expression": "foo.*.baz", - "result": ["val", "val", "val"] - }, - { - "expression": "foo.bar.*", - "result": ["val"] - }, - { - "expression": "foo.*.notbaz", - "result": [["a", "b", "c"], ["a", "b", "c"]] - }, - { - "expression": "foo.*.notbaz[0]", - "result": ["a", "a"] - }, - { - "expression": "foo.*.notbaz[-1]", - "result": ["c", "c"] - } - ] + { + "expression": "foo.bar.*", + "result": ["val"] + }, + { + "expression": "foo.*.notbaz", + "result": [["a", "b", "c"], ["a", "b", "c"]] + }, + { + "expression": "foo.*.notbaz[0]", + "result": ["a", "a"] + }, + { + "expression": "foo.*.notbaz[-1]", + "result": ["c", "c"] + } + ] }, { - "given": { - "foo": { - "first-1": { - "second-1": "val" - }, - "first-2": { - "second-1": "val" - }, - "first-3": { - "second-1": "val" - } - } + "given": { + "foo": { + "first-1": { + "second-1": "val" + }, + "first-2": { + "second-1": "val" + }, + "first-3": { + "second-1": "val" + } + } + }, + "cases": [ + { + "expression": "foo.*", + "result": [ + { "second-1": "val" }, + { "second-1": "val" }, + { "second-1": "val" } + ] + }, + { + "expression": "foo.*.*", + "result": [["val"], ["val"], ["val"]] + }, + { + "expression": "foo.*.*.*", + "result": [[], [], []] + }, + { + "expression": "foo.*.*.*.*", + "result": [[], [], []] + } + ] +}, { + "given": { + "foo": { + "bar": "one" + }, + "other": { + "bar": "one" + }, + "nomatch": { + "notbar": "three" + } + }, + "cases": [ + { + "expression": "*.bar", + "result": ["one", "one"] + } + ] +}, { + "given": { + "top1": { + "sub1": { "foo": "one" } + }, + "top2": { + "sub1": { "foo": "one" } + } + }, + "cases": [ + { + "expression": "*", + "result": [{ "sub1": { "foo": "one" } }, { "sub1": { "foo": "one" } }] + }, + { + "expression": "*.sub1", + "result": [{ "foo": "one" }, { "foo": "one" }] + }, + { + "expression": "*.*", + "result": [[{ "foo": "one" }], [{ "foo": "one" }]] + }, + { + "expression": "*.*.foo[]", + "result": ["one", "one"] }, - "cases": [ - { - "expression": "foo.*", - "result": [{"second-1": "val"}, {"second-1": "val"}, - {"second-1": "val"}] - }, - { - "expression": "foo.*.*", - "result": [["val"], ["val"], ["val"]] - }, - { - "expression": "foo.*.*.*", - "result": [[], [], []] - }, - { - "expression": "foo.*.*.*.*", - "result": [[], [], []] - } + { + "expression": "*.sub1.foo", + "result": ["one", "one"] + } + ] +}, { + "given": { + "foo": [ + { "bar": "one" }, + { "bar": "two" }, + { "bar": "three" }, + { "notbar": "four" } ] + }, + "cases": [ + { + "expression": "foo[*].bar", + "result": ["one", "two", "three"] + }, + { + "expression": "foo[*].notbar", + "result": ["four"] + } + ] }, { - "given": { - "foo": { - "bar": "one" - }, - "other": { - "bar": "one" - }, - "nomatch": { - "notbar": "three" - } + "given": [ + { "bar": "one" }, + { "bar": "two" }, + { "bar": "three" }, + { "notbar": "four" } + ], + "cases": [ + { + "expression": "[*]", + "result": [ + { "bar": "one" }, + { "bar": "two" }, + { "bar": "three" }, + { "notbar": "four" } + ] + }, + { + "expression": "[*].bar", + "result": ["one", "two", "three"] + }, + { + "expression": "[*].notbar", + "result": ["four"] + } + ] +}, { + "given": { + "foo": { + "bar": [ + { "baz": ["one", "two", "three"] }, + { "baz": ["four", "five", "six"] }, + { "baz": ["seven", "eight", "nine"] } + ] + } + }, + "cases": [ + { + "expression": "foo.bar[*].baz", + "result": [ + ["one", "two", "three"], + ["four", "five", "six"], + ["seven", "eight", "nine"] + ] + }, + { + "expression": "foo.bar[*].baz[0]", + "result": ["one", "four", "seven"] + }, + { + "expression": "foo.bar[*].baz[1]", + "result": ["two", "five", "eight"] + }, + { + "expression": "foo.bar[*].baz[2]", + "result": ["three", "six", "nine"] + }, + { + "expression": "foo.bar[*].baz[3]", + "result": [] + } + ] +}, { + "given": { + "foo": { + "bar": [["one", "two"], ["three", "four"]] + } + }, + "cases": [ + { + "expression": "foo.bar[*]", + "result": [["one", "two"], ["three", "four"]] + }, + { + "expression": "foo.bar[0]", + "result": ["one", "two"] + }, + { + "expression": "foo.bar[0][0]", + "result": "one" + }, + { + "expression": "foo.bar[0][0][0]", + "result": null + }, + { + "expression": "foo.bar[0][0][0][0]", + "result": null + }, + { + "expression": "foo[0][0]", + "result": null + } + ] +}, { + "given": { + "foo": [ + { "bar": [{ "kind": "basic" }, { "kind": "intermediate" }] }, + { "bar": [{ "kind": "advanced" }, { "kind": "expert" }] }, + { "bar": "string" } + ] + }, + "cases": [ + { + "expression": "foo[*].bar[*].kind", + "result": [["basic", "intermediate"], ["advanced", "expert"]] }, - "cases": [ - { - "expression": "*.bar", - "result": ["one", "one"] - } + { + "expression": "foo[*].bar[0].kind", + "result": ["basic", "advanced"] + } + ] +}, { + "given": { + "foo": [ + { "bar": { "kind": "basic" } }, + { "bar": { "kind": "intermediate" } }, + { "bar": { "kind": "advanced" } }, + { "bar": { "kind": "expert" } }, + { "bar": "string" } ] + }, + "cases": [ + { + "expression": "foo[*].bar.kind", + "result": ["basic", "intermediate", "advanced", "expert"] + } + ] }, { - "given": { - "top1": { - "sub1": {"foo": "one"} - }, - "top2": { - "sub1": {"foo": "one"} - } + "given": { + "foo": [ + { "bar": ["one", "two"] }, + { "bar": ["three", "four"] }, + { "bar": ["five"] } + ] + }, + "cases": [ + { + "expression": "foo[*].bar[0]", + "result": ["one", "three", "five"] + }, + { + "expression": "foo[*].bar[1]", + "result": ["two", "four"] + }, + { + "expression": "foo[*].bar[2]", + "result": [] + } + ] +}, { + "given": { + "foo": [{ "bar": [] }, { "bar": [] }, { "bar": [] }] + }, + "cases": [ + { + "expression": "foo[*].bar[0]", + "result": [] + } + ] +}, { + "given": { + "foo": [["one", "two"], ["three", "four"], ["five"]] + }, + "cases": [ + { + "expression": "foo[*][0]", + "result": ["one", "three", "five"] }, - "cases": [ - { - "expression": "*", - "result": [{"sub1": {"foo": "one"}}, - {"sub1": {"foo": "one"}}] - }, - { - "expression": "*.sub1", - "result": [{"foo": "one"}, - {"foo": "one"}] - }, - { - "expression": "*.*", - "result": [[{"foo": "one"}], - [{"foo": "one"}]] - }, - { - "expression": "*.*.foo[]", - "result": ["one", "one"] - }, - { - "expression": "*.sub1.foo", - "result": ["one", "one"] - } + { + "expression": "foo[*][1]", + "result": ["two", "four"] + } + ] +}, { + "given": { + "foo": [ + [ + ["one", "two"], + ["three", "four"] + ], + [ + ["five", "six"], + ["seven", "eight"] + ], + [ + ["nine"], + ["ten"] + ] ] -}, -{ - "given": - {"foo": [{"bar": "one"}, {"bar": "two"}, {"bar": "three"}, {"notbar": "four"}]}, - "cases": [ - { - "expression": "foo[*].bar", - "result": ["one", "two", "three"] - }, - { - "expression": "foo[*].notbar", - "result": ["four"] - } - ] -}, -{ - "given": - [{"bar": "one"}, {"bar": "two"}, {"bar": "three"}, {"notbar": "four"}], - "cases": [ - { - "expression": "[*]", - "result": [{"bar": "one"}, {"bar": "two"}, {"bar": "three"}, {"notbar": "four"}] - }, - { - "expression": "[*].bar", - "result": ["one", "two", "three"] - }, - { - "expression": "[*].notbar", - "result": ["four"] - } - ] -}, -{ - "given": { - "foo": { - "bar": [ - {"baz": ["one", "two", "three"]}, - {"baz": ["four", "five", "six"]}, - {"baz": ["seven", "eight", "nine"]} - ] - } + }, + "cases": [ + { + "expression": "foo[*][0]", + "result": [["one", "two"], ["five", "six"], ["nine"]] }, - "cases": [ - { - "expression": "foo.bar[*].baz", - "result": [["one", "two", "three"], ["four", "five", "six"], ["seven", "eight", "nine"]] - }, - { - "expression": "foo.bar[*].baz[0]", - "result": ["one", "four", "seven"] - }, - { - "expression": "foo.bar[*].baz[1]", - "result": ["two", "five", "eight"] - }, - { - "expression": "foo.bar[*].baz[2]", - "result": ["three", "six", "nine"] - }, - { - "expression": "foo.bar[*].baz[3]", - "result": [] - } - ] -}, -{ - "given": { - "foo": { - "bar": [["one", "two"], ["three", "four"]] - } + { + "expression": "foo[*][1]", + "result": [["three", "four"], ["seven", "eight"], ["ten"]] + }, + { + "expression": "foo[*][0][0]", + "result": ["one", "five", "nine"] + }, + { + "expression": "foo[*][1][0]", + "result": ["three", "seven", "ten"] + }, + { + "expression": "foo[*][0][1]", + "result": ["two", "six"] + }, + { + "expression": "foo[*][1][1]", + "result": ["four", "eight"] + }, + { + "expression": "foo[*][2]", + "result": [] }, - "cases": [ - { - "expression": "foo.bar[*]", - "result": [["one", "two"], ["three", "four"]] - }, - { - "expression": "foo.bar[0]", - "result": ["one", "two"] - }, - { - "expression": "foo.bar[0][0]", - "result": "one" - }, - { - "expression": "foo.bar[0][0][0]", - "result": null - }, - { - "expression": "foo.bar[0][0][0][0]", - "result": null - }, - { - "expression": "foo[0][0]", - "result": null - } - ] -}, -{ - "given": { - "foo": [ - {"bar": [{"kind": "basic"}, {"kind": "intermediate"}]}, - {"bar": [{"kind": "advanced"}, {"kind": "expert"}]}, - {"bar": "string"} - ] - - }, - "cases": [ - { - "expression": "foo[*].bar[*].kind", - "result": [["basic", "intermediate"], ["advanced", "expert"]] - }, - { - "expression": "foo[*].bar[0].kind", - "result": ["basic", "advanced"] - } - ] -}, -{ - "given": { - "foo": [ - {"bar": {"kind": "basic"}}, - {"bar": {"kind": "intermediate"}}, - {"bar": {"kind": "advanced"}}, - {"bar": {"kind": "expert"}}, - {"bar": "string"} - ] - }, - "cases": [ - { - "expression": "foo[*].bar.kind", - "result": ["basic", "intermediate", "advanced", "expert"] - } - ] -}, -{ - "given": { - "foo": [{"bar": ["one", "two"]}, {"bar": ["three", "four"]}, {"bar": ["five"]}] - }, - "cases": [ - { - "expression": "foo[*].bar[0]", - "result": ["one", "three", "five"] - }, - { - "expression": "foo[*].bar[1]", - "result": ["two", "four"] - }, - { - "expression": "foo[*].bar[2]", - "result": [] - } - ] -}, -{ - "given": { - "foo": [{"bar": []}, {"bar": []}, {"bar": []}] - }, - "cases": [ - { - "expression": "foo[*].bar[0]", - "result": [] - } - ] -}, -{ - "given": { - "foo": [["one", "two"], ["three", "four"], ["five"]] - }, - "cases": [ - { - "expression": "foo[*][0]", - "result": ["one", "three", "five"] - }, - { - "expression": "foo[*][1]", - "result": ["two", "four"] - } - ] -}, -{ - "given": { - "foo": [ - [ - ["one", "two"], ["three", "four"] - ], [ - ["five", "six"], ["seven", "eight"] - ], [ - ["nine"], ["ten"] - ] - ] - }, - "cases": [ - { - "expression": "foo[*][0]", - "result": [["one", "two"], ["five", "six"], ["nine"]] - }, - { - "expression": "foo[*][1]", - "result": [["three", "four"], ["seven", "eight"], ["ten"]] - }, - { - "expression": "foo[*][0][0]", - "result": ["one", "five", "nine"] - }, - { - "expression": "foo[*][1][0]", - "result": ["three", "seven", "ten"] - }, - { - "expression": "foo[*][0][1]", - "result": ["two", "six"] - }, - { - "expression": "foo[*][1][1]", - "result": ["four", "eight"] - }, - { - "expression": "foo[*][2]", - "result": [] - }, - { - "expression": "foo[*][2][2]", - "result": [] - }, - { - "expression": "bar[*]", - "result": null - }, - { - "expression": "bar[*].baz[*]", - "result": null - } - ] -}, -{ - "given": { - "string": "string", - "hash": {"foo": "bar", "bar": "baz"}, - "number": 23, - "nullvalue": null - }, - "cases": [ - { - "expression": "string[*]", - "result": null - }, - { - "expression": "hash[*]", - "result": null - }, - { - "expression": "number[*]", - "result": null - }, - { - "expression": "nullvalue[*]", - "result": null - }, - { - "expression": "string[*].foo", - "result": null - }, - { - "expression": "hash[*].foo", - "result": null - }, - { - "expression": "number[*].foo", - "result": null - }, - { - "expression": "nullvalue[*].foo", - "result": null - }, - { - "expression": "nullvalue[*].foo[*].bar", - "result": null - } - ] -}, -{ - "given": { - "string": "string", - "hash": {"foo": "val", "bar": "val"}, - "number": 23, - "array": [1, 2, 3], - "nullvalue": null - }, - "cases": [ - { - "expression": "string.*", - "result": null - }, - { - "expression": "hash.*", - "result": ["val", "val"] - }, - { - "expression": "number.*", - "result": null - }, - { - "expression": "array.*", - "result": null - }, - { - "expression": "nullvalue.*", - "result": null - } - ] -}, -{ - "given": { - "a": [0, 1, 2], - "b": [0, 1, 2] - }, - "cases": [ - { - "expression": "*[0]", - "result": [0, 0] - } - ] -} -] + { + "expression": "foo[*][2][2]", + "result": [] + }, + { + "expression": "bar[*]", + "result": null + }, + { + "expression": "bar[*].baz[*]", + "result": null + } + ] +}, { + "given": { + "string": "string", + "hash": { "foo": "bar", "bar": "baz" }, + "number": 23, + "nullvalue": null + }, + "cases": [ + { + "expression": "string[*]", + "result": null + }, + { + "expression": "hash[*]", + "result": null + }, + { + "expression": "number[*]", + "result": null + }, + { + "expression": "nullvalue[*]", + "result": null + }, + { + "expression": "string[*].foo", + "result": null + }, + { + "expression": "hash[*].foo", + "result": null + }, + { + "expression": "number[*].foo", + "result": null + }, + { + "expression": "nullvalue[*].foo", + "result": null + }, + { + "expression": "nullvalue[*].foo[*].bar", + "result": null + } + ] +}, { + "given": { + "string": "string", + "hash": { "foo": "val", "bar": "val" }, + "number": 23, + "array": [1, 2, 3], + "nullvalue": null + }, + "cases": [ + { + "expression": "string.*", + "result": null + }, + { + "expression": "hash.*", + "result": ["val", "val"] + }, + { + "expression": "number.*", + "result": null + }, + { + "expression": "array.*", + "result": null + }, + { + "expression": "nullvalue.*", + "result": null + } + ] +}, { + "given": { + "a": [0, 1, 2], + "b": [0, 1, 2] + }, + "cases": [ + { + "expression": "*[0]", + "result": [0, 0] + } + ] +}]