diff --git a/src/Lexer.ts b/src/Lexer.ts index ca3d574..9b3d48a 100644 --- a/src/Lexer.ts +++ b/src/Lexer.ts @@ -34,7 +34,7 @@ export enum Token { TOK_LITERAL = 'Literal', } -export type LexerTokenValue = string | number | JSONValue; +export type LexerTokenValue = JSONValue; export interface LexerToken { type: Token; @@ -108,7 +108,9 @@ export interface ComparitorNode { jmespathType?: Token; } -export const basicTokens: {[key: string]: Token} = { +export type ExpressionNodeTree = ASTNode | ExpressionNode | FieldNode | ValueNode; + +export const basicTokens: Record = { '(': Token.TOK_LPAREN, ')': Token.TOK_RPAREN, '*': Token.TOK_STAR, @@ -122,14 +124,14 @@ export const basicTokens: {[key: string]: Token} = { '}': Token.TOK_RBRACE, }; -const operatorStartToken: {[key: string]: true} = { +const operatorStartToken: Record = { '!': true, '<': true, '=': true, '>': true, }; -const skipChars: {[key: string]: true} = { +const skipChars: Record = { '\t': true, '\n': true, '\r': true, diff --git a/src/Parser.ts b/src/Parser.ts index fa409e7..9ed037d 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -4,16 +4,15 @@ import { ExpressionNode, IndexNode, SliceNode, - ASTNode, FieldNode, KeyValuePairNode, LexerToken, ValueNode, - Token, + ASTNode, } from './Lexer.ts'; -import Lexer from './Lexer.ts'; +import Lexer, { Token } from './Lexer.ts'; -const bindingPower: { [token: string]: number } = { +const bindingPower: Record = { [Token.TOK_EOF]: 0, [Token.TOK_UNQUOTEDIDENTIFIER]: 0, [Token.TOK_QUOTEDIDENTIFIER]: 0, diff --git a/src/Runtime.ts b/src/Runtime.ts index d1cb4e3..b7f7844 100644 --- a/src/Runtime.ts +++ b/src/Runtime.ts @@ -1,8 +1,9 @@ import type { TreeInterpreter } from './TreeInterpreter.ts'; -import { Token, ExpressionNode } from './Lexer.ts'; +import type { ExpressionNode } from './Lexer.ts'; +import type { JSONValue, JSONObject, JSONArray, ObjectDict } from './index.ts'; +import { Token } from './Lexer.ts'; import { isObject } from './utils/index.ts'; -import type { JSONValue, JSONObject, JSONArray, ObjectDict } from './index.ts'; export enum InputArgument { TYPE_NUMBER = 0, @@ -188,7 +189,9 @@ export class Runtime { exprefNode: ExpressionNode, allowedTypes: InputArgument[], ): ((x: JSONValue) => JSONValue) | undefined { - if (!this._interpreter) return; + if (!this._interpreter) { + return; + } const interpreter = this._interpreter; const keyFunc = (x: JSONValue): JSONValue => { const current = interpreter.visit(exprefNode, x) as JSONValue; @@ -249,8 +252,10 @@ export class Runtime { return Object.keys(inputValue).length; }; - private functionMap = (resolvedArgs: any[]) => { - if (!this._interpreter) return []; + private functionMap = (resolvedArgs: any[]): any[] => { + if (!this._interpreter) { + return []; + } const mapped = []; const interpreter = this._interpreter; const exprefNode = resolvedArgs[0]; @@ -262,7 +267,9 @@ export class Runtime { }; private functionMax: RuntimeFunction<[(string | number)[]], string | number | null> = ([inputValue]) => { - if (!inputValue.length) return null; + if (!inputValue.length) { + return null; + } const typeName = this.getTypeName(inputValue[0]); if (typeName === InputArgument.TYPE_NUMBER) { @@ -309,7 +316,9 @@ export class Runtime { }; private functionMin: RuntimeFunction<[(string | number)[]], string | number | null> = ([inputValue]) => { - if (!inputValue.length) return null; + if (!inputValue.length) { + return null; + } const typeName = this.getTypeName(inputValue[0]); if (typeName === InputArgument.TYPE_NUMBER) { @@ -343,7 +352,7 @@ export class Runtime { return minRecord; }; - private functionNotNull = (resolvedArgs: any[]) => { + private functionNotNull = (resolvedArgs: any[]): JSONValue => { for (let i = 0; i < resolvedArgs.length; i += 1) { if (this.getTypeName(resolvedArgs[i]) !== InputArgument.TYPE_NULL) { return resolvedArgs[i]; @@ -372,7 +381,9 @@ export class Runtime { }; private functionSortBy = (resolvedArgs: [number[] | string[], ExpressionNode]): number[] | string[] => { - if (!this._interpreter) return []; + if (!this._interpreter) { + return []; + } const sortedArray = resolvedArgs[0].slice(0); if (sortedArray.length === 0) { return sortedArray; diff --git a/src/TreeInterpreter.ts b/src/TreeInterpreter.ts index 40398ac..6bbd7a7 100644 --- a/src/TreeInterpreter.ts +++ b/src/TreeInterpreter.ts @@ -1,15 +1,16 @@ -import { - Token, - ASTNode, +import type { + ExpressionNodeTree, FieldNode, ExpressionNode, ValueNode, ComparitorNode, KeyValuePairNode, + ASTNode, } from './Lexer.ts'; import { isFalse, isObject, strictDeepEqual } from './utils/index.ts'; +import { Token } from './Lexer.ts'; import { Runtime } from './Runtime.ts'; -import type { JSONValue, JSONObject } from './index.ts'; +import type { JSONValue } from './index.ts'; export class TreeInterpreter { runtime: Runtime; diff --git a/test/compliance.spec.js b/test/compliance.spec.js index 735c93b..8e0a41f 100644 --- a/test/compliance.spec.js +++ b/test/compliance.spec.js @@ -1,4 +1,5 @@ -/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable @typescript-eslint/no-var-requires */ import { search } from '../src/index.ts'; import { basename, join } from 'https://deno.land/std@0.71.0/path/mod.ts'; diff --git a/test/deno-shim.js b/test/deno-shim.js index 905a3e0..8d2872b 100644 --- a/test/deno-shim.js +++ b/test/deno-shim.js @@ -20,6 +20,7 @@ export function expect(actual) { return { toMatchObject(expected) { assertEquals(actual, expected) }, toEqual(expected) { assertEquals(actual, expected) }, + toBe(expected) { assertEquals(actual, expected) }, toStrictEqual(expected) { assertStrictEquals(actual, expected) }, toThrow(message) { assertThrows(actual, Error, message) }, toContain(slice) { assertStringContains(actual, slice) }, diff --git a/test/jmespath.extensions.spec.js b/test/jmespath.extensions.spec.js new file mode 100644 index 0000000..aa85864 --- /dev/null +++ b/test/jmespath.extensions.spec.js @@ -0,0 +1,208 @@ +import jmespath, { search, registerFunction } from '../src/index.ts'; + +import { describe, it, expect } from './deno-shim.js'; + +describe('registerFunction', () => { + it('register a customFunction', () => { + expect(() => + search( + { + foo: 60, + bar: 10, + }, + 'divide(foo, bar)', + ), + ).toThrow('Unknown function: divide()'); + jmespath.registerFunction( + 'divide', + resolvedArgs => { + const [dividend, divisor] = resolvedArgs; + return dividend / divisor; + }, + [{ types: [jmespath.TYPE_NUMBER] }, { types: [jmespath.TYPE_NUMBER] }], + ); + expect(() => + search( + { + foo: 60, + bar: 10, + }, + 'divide(foo, bar)', + ), + ).not.toThrow(); + expect( + search( + { + foo: 60, + bar: 10, + }, + 'divide(foo, bar)', + ), + ).toEqual(6); + }); + it("won't register a customFunction if one already exists", () => { + expect(() => + registerFunction( + 'sum', + () => { + /* EMPTY FUNCTION */ + }, + [], + ), + ).toThrow('Function already defined: sum()'); + }); + it('alerts too few arguments', () => { + registerFunction( + 'tooFewArgs', + () => { + /* EMPTY FUNCTION */ + }, + [{ types: [jmespath.TYPE_ANY] }, { types: [jmespath.TYPE_NUMBER], optional: true }], + ); + expect(() => + search( + { + foo: 60, + bar: 10, + }, + 'tooFewArgs()', + ), + ).toThrow('ArgumentError: tooFewArgs() takes 1 arguments but received 0'); + expect(() => + search( + { + foo: 60, + bar: 10, + }, + 'tooFewArgs(foo, @)', + ), + ).toThrow('TypeError: tooFewArgs() expected argument 2 to be type (number) but received type object instead.'); + expect(() => + search( + { + foo: 60, + bar: 10, + }, + 'tooFewArgs(foo, `2`, @)', + ), + ).toThrow('ArgumentError: tooFewArgs() takes 1 arguments but received 3'); + }); + it('alerts too many arguments', () => { + registerFunction( + 'tooManyArgs', + () => { + /* EMPTY FUNCTION */ + }, + [], + ); + expect(() => + search( + { + foo: 60, + bar: 10, + }, + 'tooManyArgs(foo)', + ), + ).toThrow('ArgumentError: tooManyArgs() takes 0 argument but received 1'); + }); + + it('alerts optional variadic arguments', () => { + registerFunction( + 'optionalVariadic', + () => { + /* EMPTY FUNCTION */ + }, + [{ types: [jmespath.TYPE_ANY], optional: true, variadic: true }], + ); + expect(() => + search( + { + foo: 60, + bar: 10, + }, + 'optionalVariadic(foo)', + ), + ).not.toThrow(); + }); + + it('alerts variadic is always last argument', () => { + registerFunction( + 'variadicAlwaysLast', + () => { + /* EMPTY FUNCTION */ + }, + [ + { types: [jmespath.TYPE_ANY], variadic: true }, + { types: [jmespath.TYPE_ANY], optional: true }, + ], + ); + expect(() => + search( + { + foo: 60, + bar: 10, + }, + 'variadicAlwaysLast(foo)', + ), + ).toThrow("ArgumentError: variadicAlwaysLast() 'variadic' argument 1 must occur last"); + }); + + it('accounts for optional arguments', () => { + registerFunction( + 'optionalArgs', + ([first, second, third]) => { + return { first, second: second ?? 'default[2]', third: third ?? 'default[3]' }; + }, + [{ types: [jmespath.TYPE_ANY] }, { types: [jmespath.TYPE_ANY], optional: true }], + ); + expect( + search( + { + foo: 60, + bar: 10, + }, + 'optionalArgs(foo)', + ), + ).toEqual({ first: 60, second: 'default[2]', third: 'default[3]' }); + expect( + search( + { + foo: 60, + bar: 10, + }, + 'optionalArgs(foo, bar)', + ), + ).toEqual({ first: 60, second: 10, third: 'default[3]' }); + expect(() => + search( + { + foo: 60, + bar: 10, + }, + 'optionalArgs(foo, bar, [foo, bar])', + ), + ).toThrow('ArgumentError: optionalArgs() takes 1 arguments but received 3'); + }); +}); + +describe('root', () => { + it('$ should give access to the root value', () => { + const value = search({ foo: { bar: 1 } }, 'foo.{ value: $.foo.bar }'); + expect(value.value).toBe(1); + }); + it('$ should give access to the root value after pipe', () => { + const value = search({ foo: { bar: 1 } }, 'foo | $.foo.bar'); + expect(value).toEqual(1); + }); + it('$ should give access in expressions', () => { + const value = search([{ foo: { bar: 1 } }, { foo: { bar: 99 } }], 'map(&foo.{boo: bar, root: $}, @)'); + expect(value).toEqual([ + { boo: 1, root: [{ foo: { bar: 1 } }, { foo: { bar: 99 } }] }, + { boo: 99, root: [{ foo: { bar: 1 } }, { foo: { bar: 99 } }] }, + ]); + }); + it('$ can be used in parallel', () => { + const value = search([{ foo: { bar: 1 } }, { foo: { bar: 99 } }], '[$[0].foo.bar, $[1].foo.bar]'); + expect(value).toEqual([1, 99]); + }); +}); diff --git a/test/jmespath.spec.js b/test/jmespath.spec.js index e918b2a..8cd72e9 100644 --- a/test/jmespath.spec.js +++ b/test/jmespath.spec.js @@ -1,4 +1,4 @@ -import jmespath, { search, tokenize, compile, registerFunction, TreeInterpreter } from '../src/index.ts'; +import { search, tokenize, compile, TreeInterpreter } from '../src/index.ts'; import { strictDeepEqual } from '../src/utils/index.ts'; import { describe, it, expect } from './deno-shim.js'; @@ -186,208 +186,3 @@ describe('search', () => { } }); }); - -describe('registerFunction', () => { - it('register a customFunction', () => { - expect(() => - search( - { - foo: 60, - bar: 10, - }, - 'divide(foo, bar)', - ), - ).toThrow('Unknown function: divide()'); - jmespath.registerFunction( - 'divide', - resolvedArgs => { - const [dividend, divisor] = resolvedArgs; - return dividend / divisor; - }, - [{ types: [jmespath.TYPE_NUMBER] }, { types: [jmespath.TYPE_NUMBER] }], - ); - expect(() => - search( - { - foo: 60, - bar: 10, - }, - 'divide(foo, bar)', - ), - ).not.toThrow(); - expect( - search( - { - foo: 60, - bar: 10, - }, - 'divide(foo, bar)', - ), - ).toEqual(6); - }); - it("won't register a customFunction if one already exists", () => { - expect(() => - registerFunction( - 'sum', - () => { - /* EMPTY FUNCTION */ - }, - [], - ), - ).toThrow('Function already defined: sum()'); - }); - it('alerts too few arguments', () => { - registerFunction( - 'tooFewArgs', - () => { - /* EMPTY FUNCTION */ - }, - [{ types: [jmespath.TYPE_ANY] }, { types: [jmespath.TYPE_NUMBER], optional: true }], - ); - expect(() => - search( - { - foo: 60, - bar: 10, - }, - 'tooFewArgs()', - ), - ).toThrow('ArgumentError: tooFewArgs() takes 1 arguments but received 0'); - expect(() => - search( - { - foo: 60, - bar: 10, - }, - 'tooFewArgs(foo, @)', - ), - ).toThrow('TypeError: tooFewArgs() expected argument 2 to be type (number) but received type object instead.'); - expect(() => - search( - { - foo: 60, - bar: 10, - }, - 'tooFewArgs(foo, `2`, @)', - ), - ).toThrow('ArgumentError: tooFewArgs() takes 1 arguments but received 3'); - }); - it('alerts too many arguments', () => { - registerFunction( - 'tooManyArgs', - () => { - /* EMPTY FUNCTION */ - }, - [], - ); - expect(() => - search( - { - foo: 60, - bar: 10, - }, - 'tooManyArgs(foo)', - ), - ).toThrow('ArgumentError: tooManyArgs() takes 0 argument but received 1'); - }); - - it('alerts optional variadic arguments', () => { - registerFunction( - 'optionalVariadic', - () => { - /* EMPTY FUNCTION */ - }, - [{ types: [jmespath.TYPE_ANY], optional: true, variadic: true }], - ); - expect(() => - search( - { - foo: 60, - bar: 10, - }, - 'optionalVariadic(foo)', - ), - ).not.toThrow(); - }); - - it('alerts variadic is always last argument', () => { - registerFunction( - 'variadicAlwaysLast', - () => { - /* EMPTY FUNCTION */ - }, - [ - { types: [jmespath.TYPE_ANY], variadic: true }, - { types: [jmespath.TYPE_ANY], optional: true }, - ], - ); - expect(() => - search( - { - foo: 60, - bar: 10, - }, - 'variadicAlwaysLast(foo)', - ), - ).toThrow("ArgumentError: variadicAlwaysLast() 'variadic' argument 1 must occur last"); - }); - - it('accounts for optional arguments', () => { - registerFunction( - 'optionalArgs', - ([first, second, third]) => { - return { first, second: second ?? 'default[2]', third: third ?? 'default[3]' }; - }, - [{ types: [jmespath.TYPE_ANY] }, { types: [jmespath.TYPE_ANY], optional: true }], - ); - expect( - search( - { - foo: 60, - bar: 10, - }, - 'optionalArgs(foo)', - ), - ).toEqual({ first: 60, second: 'default[2]', third: 'default[3]' }); - expect( - search( - { - foo: 60, - bar: 10, - }, - 'optionalArgs(foo, bar)', - ), - ).toEqual({ first: 60, second: 10, third: 'default[3]' }); - expect(() => - search( - { - foo: 60, - bar: 10, - }, - 'optionalArgs(foo, bar, [foo, bar])', - ), - ).toThrow('ArgumentError: optionalArgs() takes 1 arguments but received 3'); - }); -}); - -describe('root', () => { - it('$ should give access to the root value', () => { - var value = search({ foo: { bar: 1 } }, 'foo.{ value: $.foo.bar }'); - expect(value.value).toEqual(1); - }); - it('$ should give access to the root value after pipe', () => { - var value = search({ foo: { bar: 1 } }, 'foo | $.foo.bar'); - expect(value).toEqual(1); - }); - it('$ should give access in expressions', () => { - var value = search([{ foo: { bar: 1 } }, { foo: { bar: 99 } }], 'map(&foo.{boo: bar, root: $}, @)'); - expect(value).toEqual([ - { boo: 1, root: [{ foo: { bar: 1 } }, { foo: { bar: 99 } }] }, - { boo: 99, root: [{ foo: { bar: 1 } }, { foo: { bar: 99 } }] }, - ]); - }); - it('$ can be used in parallel', () => { - var value = search([{ foo: { bar: 1 } }, { foo: { bar: 99 } }], '[$[0].foo.bar, $[1].foo.bar]'); - expect(value).toEqual([1, 99]); - }); -});