diff --git a/src/AST.type.ts b/src/AST.type.ts new file mode 100644 index 0000000..d80fd48 --- /dev/null +++ b/src/AST.type.ts @@ -0,0 +1,102 @@ +import { JSONValue } from './JSON.type'; + +export interface FieldNode { + readonly type: 'Field'; + readonly name: string; +} + +export interface LiteralNode { + readonly type: 'Literal'; + readonly value: JSONValue; +} + +export interface IndexNode { + readonly type: 'Index'; + readonly value: number; +} + +export interface FilterProjectionNode { + readonly type: 'FilterProjection'; + readonly left: ExpressionNode; + readonly right: ExpressionNode; + readonly condition: ExpressionNode; +} + +export interface SliceNode { + readonly type: 'Slice'; + readonly start: number | null; + readonly stop: number | null; + readonly step: number | null; +} + +export type ComparatorType = 'GT' | 'LT' | 'GTE' | 'LTE' | 'NE' | 'EQ'; + +export interface ComparatorNode { + readonly type: 'Comparator'; + readonly name: ComparatorType; + readonly left: ExpressionNode; + readonly right: ExpressionNode; +} + +export interface KeyValuePairNode { + readonly type: 'KeyValuePair'; + readonly name: string; + readonly value: ExpressionNode; +} + +export interface MultiSelectHashNode { + type: 'MultiSelectHash'; + children: KeyValuePairNode[]; +} + +export interface MultiSelectListNode { + type: 'MultiSelectList'; + children: ExpressionNode[]; +} + +export interface FunctionNode { + readonly type: 'Function'; + readonly name: string; + readonly children: ExpressionNode[]; +} + +type BinaryExpressionType = + | 'Subexpression' + | 'Pipe' + | 'ValueProjection' + | 'IndexExpression' + | 'Projection' + | 'OrExpression' + | 'AndExpression'; + +type UnaryExpressionType = 'NotExpression' | 'Flatten' | 'ExpressionReference'; +type SimpleExpressionType = 'Identity' | 'Current' | 'Root'; + +export interface SimpleExpressionNode { + readonly type: T; +} +export interface UnaryExpressionNode { + readonly type: T; + readonly child: ExpressionNode; +} +export interface BinaryExpressionNode { + readonly type: T; + readonly left: ExpressionNode; + readonly right: ExpressionNode; +} + +export type ExpressionNode = + | SimpleExpressionNode + | UnaryExpressionNode + | BinaryExpressionNode + | ComparatorNode + | SliceNode + | FilterProjectionNode + | IndexNode + | LiteralNode + | FieldNode + | MultiSelectHashNode + | MultiSelectListNode + | FunctionNode; + +export type ExpressionReference = { expref: true } & ExpressionNode; diff --git a/src/JSON.type.ts b/src/JSON.type.ts new file mode 100644 index 0000000..cb9807e --- /dev/null +++ b/src/JSON.type.ts @@ -0,0 +1,6 @@ +export type ObjectDict = Record; + +export type JSONPrimitive = string | number | boolean | null; +export type JSONValue = JSONPrimitive | JSONObject | JSONArray; +export type JSONObject = { [member: string]: JSONValue }; +export type JSONArray = JSONValue[]; diff --git a/src/Lexer.ts b/src/Lexer.ts index 06ccad5..b4f378c 100644 --- a/src/Lexer.ts +++ b/src/Lexer.ts @@ -1,71 +1,6 @@ +import { JSONValue } from './JSON.type'; +import { LexerToken, Token } from './Lexer.type'; import { isAlpha, isNum, isAlphaNum } from './utils/index'; -import type { JSONValue } from './index'; - -export enum Token { - TOK_EOF = 'EOF', - TOK_UNQUOTEDIDENTIFIER = 'UnquotedIdentifier', - TOK_QUOTEDIDENTIFIER = 'QuotedIdentifier', - TOK_RBRACKET = 'Rbracket', - TOK_RPAREN = 'Rparen', - TOK_COMMA = 'Comma', - TOK_COLON = 'Colon', - TOK_RBRACE = 'Rbrace', - TOK_NUMBER = 'Number', - TOK_CURRENT = 'Current', - TOK_ROOT = 'Root', - TOK_EXPREF = 'Expref', - TOK_PIPE = 'Pipe', - TOK_OR = 'Or', - TOK_AND = 'And', - TOK_EQ = 'EQ', - TOK_GT = 'GT', - TOK_LT = 'LT', - TOK_GTE = 'GTE', - TOK_LTE = 'LTE', - TOK_NE = 'NE', - TOK_FLATTEN = 'Flatten', - TOK_STAR = 'Star', - TOK_FILTER = 'Filter', - TOK_DOT = 'Dot', - TOK_NOT = 'Not', - TOK_LBRACE = 'Lbrace', - TOK_LBRACKET = 'Lbracket', - TOK_LPAREN = 'Lparen', - TOK_LITERAL = 'Literal', -} - -export type LexerTokenValue = JSONValue; - -export interface LexerToken { - type: Token; - value: LexerTokenValue; - start: number; -} - -export interface ASTNode { - type: string; -} - -export interface ValueNode extends ASTNode { - value: T; -} - -export interface FieldNode extends ASTNode { - name: LexerTokenValue; -} - -export type KeyValuePairNode = FieldNode & ValueNode; - -export interface ExpressionNode extends ASTNode { - children: T[]; - jmespathType?: Token; -} - -export interface ComparitorNode extends ExpressionNode { - name: Token; -} - -export type ExpressionNodeTree = ASTNode | ExpressionNode | FieldNode | ValueNode; export const basicTokens: Record = { '(': Token.TOK_LPAREN, @@ -323,7 +258,7 @@ class StreamLexer { try { JSON.parse(literalString); return true; - } catch (ex) { + } catch { return false; } } diff --git a/src/Lexer.type.ts b/src/Lexer.type.ts new file mode 100644 index 0000000..8d3d651 --- /dev/null +++ b/src/Lexer.type.ts @@ -0,0 +1,42 @@ +import { JSONValue } from './JSON.type'; + +export enum Token { + TOK_EOF = 'EOF', + TOK_UNQUOTEDIDENTIFIER = 'UnquotedIdentifier', + TOK_QUOTEDIDENTIFIER = 'QuotedIdentifier', + TOK_RBRACKET = 'Rbracket', + TOK_RPAREN = 'Rparen', + TOK_COMMA = 'Comma', + TOK_COLON = 'Colon', + TOK_RBRACE = 'Rbrace', + TOK_NUMBER = 'Number', + TOK_CURRENT = 'Current', + TOK_ROOT = 'Root', + TOK_EXPREF = 'Expref', + TOK_PIPE = 'Pipe', + TOK_OR = 'Or', + TOK_AND = 'And', + TOK_EQ = 'EQ', + TOK_GT = 'GT', + TOK_LT = 'LT', + TOK_GTE = 'GTE', + TOK_LTE = 'LTE', + TOK_NE = 'NE', + TOK_FLATTEN = 'Flatten', + TOK_STAR = 'Star', + TOK_FILTER = 'Filter', + TOK_DOT = 'Dot', + TOK_NOT = 'Not', + TOK_LBRACE = 'Lbrace', + TOK_LBRACKET = 'Lbracket', + TOK_LPAREN = 'Lparen', + TOK_LITERAL = 'Literal', +} + +export type LexerTokenValue = JSONValue; + +export interface LexerToken { + type: Token; + value: LexerTokenValue; + start: number; +} diff --git a/src/Parser.ts b/src/Parser.ts index 516e557..290b30a 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -1,14 +1,16 @@ -import { - ComparitorNode, +import type { + BinaryExpressionNode, + ComparatorNode, + ComparatorType, ExpressionNode, - ExpressionNodeTree, - FieldNode, + FunctionNode, + IndexNode, KeyValuePairNode, - LexerToken, - ValueNode, - ASTNode, -} from './Lexer'; -import Lexer, { Token } from './Lexer'; + SliceNode, + UnaryExpressionNode, +} from './AST.type'; +import Lexer from './Lexer'; +import { LexerToken, Token } from './Lexer.type'; const bindingPower: Record = { [Token.TOK_EOF]: 0, @@ -44,7 +46,7 @@ const bindingPower: Record = { class TokenParser { index = 0; tokens: LexerToken[] = []; - parse(expression: string): ASTNode { + parse(expression: string): ExpressionNode { this.loadTokens(expression); this.index = 0; const ast = this.expression(0); @@ -59,7 +61,7 @@ class TokenParser { this.tokens = [...Lexer.tokenize(expression), { type: Token.TOK_EOF, value: '', start: expression.length }]; } - expression(rbp: number): ASTNode { + expression(rbp: number): ExpressionNode { const leftToken = this.lookaheadToken(0); this.advance(); let left = this.nud(leftToken); @@ -84,66 +86,68 @@ class TokenParser { this.index += 1; } - nud(token: LexerToken): ASTNode { - let left; - let right; - let expression; + nud(token: LexerToken): ExpressionNode { switch (token.type) { case Token.TOK_LITERAL: - return { type: 'Literal', value: token.value } as ValueNode; + return { type: 'Literal', value: token.value }; case Token.TOK_UNQUOTEDIDENTIFIER: - return { type: 'Field', name: token.value } as FieldNode; + return { type: 'Field', name: token.value as string }; case Token.TOK_QUOTEDIDENTIFIER: - const node: FieldNode = { type: 'Field', name: token.value }; if (this.lookahead(0) === Token.TOK_LPAREN) { throw new Error('Quoted identifier not allowed for function names.'); } else { - return node; + return { type: 'Field', name: token.value as string }; } - case Token.TOK_NOT: - right = this.expression(bindingPower.Not); - return { type: 'NotExpression', children: [right] } as ExpressionNode; - case Token.TOK_STAR: - left = { type: 'Identity' }; - right = - (this.lookahead(0) === Token.TOK_RBRACKET && { type: 'Identity' }) || - this.parseProjectionRHS(bindingPower.Star); - return { type: 'ValueProjection', children: [left, right] } as ExpressionNode; + case Token.TOK_NOT: { + const child = this.expression(bindingPower.Not); + return { type: 'NotExpression', child }; + } + case Token.TOK_STAR: { + const left: ExpressionNode = { type: 'Identity' }; + const right: ExpressionNode = + this.lookahead(0) === Token.TOK_RBRACKET ? left : this.parseProjectionRHS(bindingPower.Star); + return { type: 'ValueProjection', left, right }; + } case Token.TOK_FILTER: - return this.led(token.type, { type: 'Identity' } as ASTNode); + return this.led(token.type, { type: 'Identity' }); case Token.TOK_LBRACE: return this.parseMultiselectHash(); - case Token.TOK_FLATTEN: - left = { type: Token.TOK_FLATTEN, children: [{ type: 'Identity' }] }; - right = this.parseProjectionRHS(bindingPower.Flatten); - return { type: 'Projection', children: [left, right] } as ExpressionNode; - case Token.TOK_LBRACKET: + case Token.TOK_FLATTEN: { + const left: ExpressionNode = { type: 'Flatten', child: { type: 'Identity' } }; + const right: ExpressionNode = this.parseProjectionRHS(bindingPower.Flatten); + return { type: 'Projection', left, right }; + } + case Token.TOK_LBRACKET: { if (this.lookahead(0) === Token.TOK_NUMBER || this.lookahead(0) === Token.TOK_COLON) { - right = this.parseIndexExpression(); + const right = this.parseIndexExpression(); return this.projectIfSlice({ type: 'Identity' }, right); } if (this.lookahead(0) === Token.TOK_STAR && this.lookahead(1) === Token.TOK_RBRACKET) { this.advance(); this.advance(); - right = this.parseProjectionRHS(bindingPower.Star); + const right = this.parseProjectionRHS(bindingPower.Star); return { - children: [{ type: 'Identity' }, right], + left: { type: 'Identity' }, + right, type: 'Projection', - } as ExpressionNode; + }; } return this.parseMultiselectList(); + } case Token.TOK_CURRENT: return { type: Token.TOK_CURRENT }; case Token.TOK_ROOT: return { type: Token.TOK_ROOT }; - case Token.TOK_EXPREF: - expression = this.expression(bindingPower.Expref); - return { type: 'ExpressionReference', children: [expression] } as ExpressionNode; - case Token.TOK_LPAREN: - const args: ASTNode[] = []; + case Token.TOK_EXPREF: { + const child = this.expression(bindingPower.Expref); + return { type: 'ExpressionReference', child }; + } + case Token.TOK_LPAREN: { + const args: ExpressionNode[] = []; while (this.lookahead(0) !== Token.TOK_RPAREN) { + let expression: ExpressionNode; if (this.lookahead(0) === Token.TOK_CURRENT) { - expression = { type: Token.TOK_CURRENT } as ASTNode; + expression = { type: Token.TOK_CURRENT }; this.advance(); } else { expression = this.expression(0); @@ -152,37 +156,43 @@ class TokenParser { } this.match(Token.TOK_RPAREN); return args[0]; + } default: this.errorToken(token); } } - led(tokenName: string, left: ExpressionNodeTree): ExpressionNode | ComparitorNode { - let right: ExpressionNodeTree; + led(tokenName: string, left: ExpressionNode): ExpressionNode { switch (tokenName) { - case Token.TOK_DOT: + case Token.TOK_DOT: { const rbp = bindingPower.Dot; if (this.lookahead(0) !== Token.TOK_STAR) { - right = this.parseDotRHS(rbp); - return { type: 'Subexpression', children: [left, right] }; + const right = this.parseDotRHS(rbp); + return { type: 'Subexpression', left, right }; } this.advance(); - right = this.parseProjectionRHS(rbp); - return { type: 'ValueProjection', children: [left, right] }; - - case Token.TOK_PIPE: - right = this.expression(bindingPower.Pipe); - return { type: Token.TOK_PIPE, children: [left, right] }; - case Token.TOK_OR: - right = this.expression(bindingPower.Or); - return { type: 'OrExpression', children: [left, right] }; - case Token.TOK_AND: - right = this.expression(bindingPower.And); - return { type: 'AndExpression', children: [left, right] }; - case Token.TOK_LPAREN: - const name = (left as FieldNode).name; - const args: ASTNode[] = []; - let expression: ASTNode; + const right = this.parseProjectionRHS(rbp); + return { type: 'ValueProjection', left, right }; + } + case Token.TOK_PIPE: { + const right = this.expression(bindingPower.Pipe); + return { type: 'Pipe', left, right }; + } + case Token.TOK_OR: { + const right = this.expression(bindingPower.Or); + return { type: 'OrExpression', left, right }; + } + case Token.TOK_AND: { + const right = this.expression(bindingPower.And); + return { type: 'AndExpression', left, right }; + } + case Token.TOK_LPAREN: { + if (left.type !== 'Field') { + throw new Error('Expected a Field node'); + } + const name = left.name; + const args: ExpressionNode[] = []; + let expression: ExpressionNode; while (this.lookahead(0) !== Token.TOK_RPAREN) { if (this.lookahead(0) === Token.TOK_CURRENT) { expression = { type: Token.TOK_CURRENT }; @@ -196,19 +206,21 @@ class TokenParser { args.push(expression); } this.match(Token.TOK_RPAREN); - const node = { name, type: 'Function', children: args }; + const node: FunctionNode = { name, type: 'Function', children: args }; return node; - case Token.TOK_FILTER: + } + case Token.TOK_FILTER: { const condition = this.expression(0); this.match(Token.TOK_RBRACKET); - right = - (this.lookahead(0) === Token.TOK_FLATTEN && { type: 'Identity' }) || - this.parseProjectionRHS(bindingPower.Filter); - return { type: 'FilterProjection', children: [left, right, condition] }; - case Token.TOK_FLATTEN: - const leftNode = { type: Token.TOK_FLATTEN, children: [left] }; - const rightNode = this.parseProjectionRHS(bindingPower.Flatten); - return { type: 'Projection', children: [leftNode, rightNode] }; + const right: ExpressionNode = + this.lookahead(0) === Token.TOK_FLATTEN ? { type: 'Identity' } : this.parseProjectionRHS(bindingPower.Filter); + return { type: 'FilterProjection', left, right, condition }; + } + case Token.TOK_FLATTEN: { + const leftNode: UnaryExpressionNode = { type: 'Flatten', child: left }; + const right = this.parseProjectionRHS(bindingPower.Flatten); + return { type: 'Projection', left: leftNode, right }; + } case Token.TOK_EQ: case Token.TOK_NE: case Token.TOK_GT: @@ -216,16 +228,17 @@ class TokenParser { case Token.TOK_LT: case Token.TOK_LTE: return this.parseComparator(left, tokenName); - case Token.TOK_LBRACKET: + case Token.TOK_LBRACKET: { const token = this.lookaheadToken(0); if (token.type === Token.TOK_NUMBER || token.type === Token.TOK_COLON) { - right = this.parseIndexExpression(); + const right = this.parseIndexExpression(); return this.projectIfSlice(left, right); } this.match(Token.TOK_STAR); this.match(Token.TOK_RBRACKET); - right = this.parseProjectionRHS(bindingPower.Star); - return { type: 'Projection', children: [left, right] }; + const right = this.parseProjectionRHS(bindingPower.Star); + return { type: 'Projection', left, right }; + } default: return this.errorToken(this.lookaheadToken(0)); @@ -248,60 +261,72 @@ class TokenParser { throw error; } - private parseIndexExpression(): ValueNode | ExpressionNode { + private parseIndexExpression(): SliceNode | IndexNode { if (this.lookahead(0) === Token.TOK_COLON || this.lookahead(1) === Token.TOK_COLON) { return this.parseSliceExpression(); } - const node: ValueNode = { - type: 'Index', - value: this.lookaheadToken(0).value, - }; + const value = Number(this.lookaheadToken(0).value); this.advance(); this.match(Token.TOK_RBRACKET); - return node; + return { type: 'Index', value }; } - private projectIfSlice(left: ASTNode, right: ASTNode): ExpressionNode { - const indexExpr: ExpressionNode = { type: 'IndexExpression', children: [left, right] }; + private projectIfSlice( + left: ExpressionNode, + right: ExpressionNode, + ): BinaryExpressionNode<'Projection' | 'IndexExpression'> { + const indexExpr: BinaryExpressionNode<'IndexExpression'> = { type: 'IndexExpression', left, right }; if (right.type === 'Slice') { return { - children: [indexExpr, this.parseProjectionRHS(bindingPower.Star)], + left: indexExpr, + right: this.parseProjectionRHS(bindingPower.Star), type: 'Projection', }; } return indexExpr; } - private parseSliceExpression(): ExpressionNode { - const parts: (number | null)[] = [null, null, null]; - let index = 0; - let currentTokenType = this.lookahead(0); - while (currentTokenType !== Token.TOK_RBRACKET && index < 3) { - if (currentTokenType === Token.TOK_COLON) { - index += 1; - this.advance(); - } else if (currentTokenType === Token.TOK_NUMBER) { - parts[index] = this.lookaheadToken(0).value as number; + private parseSliceExpression(): SliceNode { + const parts = []; + + for (let i = 0; i < 3; i += 1) { + let next = this.lookaheadToken(0); + if (next.type === Token.TOK_RBRACKET) { + break; + } + if (next.type === Token.TOK_NUMBER) { this.advance(); + parts.push(next.value as number); + next = this.lookaheadToken(0); } else { - const token = this.lookaheadToken(0); - this.errorToken(token, `Syntax error, unexpected token: ${token.value}(${token.type})`); + parts.push(null); } - currentTokenType = this.lookahead(0); + + // COLON/RBRACKET + // WARN technically allows for trailing colon + if (next.type !== Token.TOK_COLON) { + if (next.type !== Token.TOK_RBRACKET) { + this.errorToken(next, `Syntax error, unexpected token: ${next.value}(${next.type})`); + } + break; + } + + this.advance(); } + this.match(Token.TOK_RBRACKET); - return { - children: parts, - type: 'Slice', - }; + + const [start = null, stop = null, step = null] = parts; + + return { type: 'Slice', start, stop, step }; } - private parseComparator(left: ASTNode, comparator: Token): ComparitorNode { + private parseComparator(left: ExpressionNode, comparator: ComparatorType): ComparatorNode { const right = this.expression(bindingPower[comparator]); - return { type: 'Comparator', name: comparator, children: [left, right] }; + return { type: 'Comparator', name: comparator, left, right }; } - private parseDotRHS(rbp: number): ASTNode { + private parseDotRHS(rbp: number): ExpressionNode { const lookahead = this.lookahead(0); const exprTokens = [Token.TOK_UNQUOTEDIDENTIFIER, Token.TOK_QUOTEDIDENTIFIER, Token.TOK_STAR]; if (exprTokens.includes(lookahead)) { @@ -319,7 +344,7 @@ class TokenParser { this.errorToken(token, `Syntax error, unexpected token: ${token.value}(${token.type})`); } - private parseProjectionRHS(rbp: number): ASTNode { + private parseProjectionRHS(rbp: number): ExpressionNode { if (bindingPower[this.lookahead(0)] < 10) { return { type: 'Identity' }; } @@ -338,7 +363,7 @@ class TokenParser { } private parseMultiselectList(): ExpressionNode { - const expressions: ASTNode[] = []; + const expressions: ExpressionNode[] = []; while (this.lookahead(0) !== Token.TOK_RBRACKET) { const expression = this.expression(0); expressions.push(expression); @@ -358,7 +383,7 @@ class TokenParser { const identifierTypes = [Token.TOK_UNQUOTEDIDENTIFIER, Token.TOK_QUOTEDIDENTIFIER]; let keyToken; let keyName: string; - let value: ASTNode; + let value: ExpressionNode; // tslint:disable-next-line: prettier for (;;) { keyToken = this.lookaheadToken(0); diff --git a/src/Runtime.ts b/src/Runtime.ts index 26ee6f4..0847257 100644 --- a/src/Runtime.ts +++ b/src/Runtime.ts @@ -1,9 +1,6 @@ +import type { ExpressionNode } from './AST.type'; +import type { JSONArray, JSONObject, JSONValue, ObjectDict } from './JSON.type'; import type { TreeInterpreter } from './TreeInterpreter'; -import type { ExpressionNode } from './Lexer'; -import type { JSONValue, JSONObject, JSONArray, ObjectDict } from '.'; -import { Token } from './Lexer'; - -import { isObject } from './utils'; export enum InputArgument { TYPE_NUMBER = 0, @@ -64,7 +61,7 @@ export class Runtime { }; } - callFunction(name: string, resolvedArgs: any): unknown { + callFunction(name: string, resolvedArgs: any): JSONValue { const functionEntry = this.functionTable[name]; if (functionEntry === undefined) { throw new Error(`Unknown function: ${name}()`); @@ -175,7 +172,7 @@ export class Runtime { case '[object Null]': return InputArgument.TYPE_NULL; case '[object Object]': - if ((obj as ObjectDict).jmespathType === Token.TOK_EXPREF) { + if ((obj as ObjectDict).expref) { return InputArgument.TYPE_EXPREF; } return InputArgument.TYPE_OBJECT; @@ -246,7 +243,7 @@ export class Runtime { }; private functionLength: RuntimeFunction<[string | JSONArray | JSONObject], number> = ([inputValue]) => { - if (!isObject(inputValue)) { + if (typeof inputValue === 'string' || Array.isArray(inputValue)) { return inputValue.length; } return Object.keys(inputValue).length; diff --git a/src/TreeInterpreter.ts b/src/TreeInterpreter.ts index adc0bfb..28fe2ba 100644 --- a/src/TreeInterpreter.ts +++ b/src/TreeInterpreter.ts @@ -1,15 +1,7 @@ -import type { - ExpressionNodeTree, - FieldNode, - ExpressionNode, - ValueNode, - ComparitorNode, - KeyValuePairNode, -} from './Lexer'; -import { isFalse, isObject, strictDeepEqual } from './utils'; -import { Token } from './Lexer'; +import { isFalse, strictDeepEqual } from './utils'; import { Runtime } from './Runtime'; -import type { JSONValue } from '.'; +import type { ExpressionNode, ExpressionReference, SliceNode } from './AST.type'; +import type { JSONArray, JSONObject, JSONValue } from './JSON.type'; export class TreeInterpreter { runtime: Runtime; @@ -19,232 +11,184 @@ export class TreeInterpreter { this.runtime = new Runtime(this); } - search(node: ExpressionNodeTree, value: JSONValue): JSONValue { + search(node: ExpressionNode, value: JSONValue): JSONValue { this._rootValue = value; return this.visit(node, value) as JSONValue; } - visit(node: ExpressionNodeTree, value: JSONValue | ExpressionNodeTree): JSONValue | ExpressionNodeTree { - let matched; - let current; - let result; - let first; - let second; - let field; - let left; - let right; - let collected; - let i; - let base; + visit(node: ExpressionNode, value: JSONValue | ExpressionNode): JSONValue | ExpressionNode | ExpressionReference { switch (node.type) { case 'Field': - if (value === null) { + if (value === null || typeof value !== 'object' || Array.isArray(value)) { return null; } - if (isObject(value)) { - field = value[(node as FieldNode).name as string]; - if (field === undefined) { - return null; - } - return field; - } - return null; - case 'Subexpression': - result = this.visit((node as ExpressionNode).children[0], value); - for (i = 1; i < (node as ExpressionNode).children.length; i += 1) { - result = this.visit((node as ExpressionNode).children[1], result); - if (result === null) { - return null; - } - } - return result; + return value[node.name] ?? null; case 'IndexExpression': - left = this.visit((node as ExpressionNode).children[0], value); - right = this.visit((node as ExpressionNode).children[1], left); - return right; - case 'Index': + case 'Subexpression': + return this.visit(node.right, this.visit(node.left, value)); + case 'Index': { if (!Array.isArray(value)) { return null; } - let index = (node as ValueNode).value; - if (index < 0) { - index = value.length + index; - } - result = value[index]; - if (result === undefined) { - result = null; - } - return result; - case 'Slice': + const index = node.value < 0 ? value.length + node.value : node.value; + return value[index] ?? null; + } + case 'Slice': { if (!Array.isArray(value)) { return null; } - const sliceParams = [...(node as ExpressionNode).children]; - const computed = this.computeSliceParams(value.length, sliceParams); - const [start, stop, step] = computed; - result = []; + const { start, stop, step } = this.computeSliceParams(value.length, node); + const result = []; + if (step > 0) { - for (i = start; i < stop; i += step) { + for (let i = start; i < stop; i += step) { result.push(value[i]); } } else { - for (i = start; i > stop; i += step) { + for (let i = start; i > stop; i += step) { result.push(value[i]); } } return result; - case 'Projection': - base = this.visit((node as ExpressionNode).children[0], value); + } + case 'Projection': { + const { left, right } = node; + const base = this.visit(left, value); if (!Array.isArray(base)) { return null; } - collected = []; - for (i = 0; i < base.length; i += 1) { - current = this.visit((node as ExpressionNode).children[1], base[i]); + const collected: JSONArray = []; + for (const elem of base) { + const current = this.visit(right, elem) as JSONValue; if (current !== null) { collected.push(current); } } return collected as JSONValue; - case 'ValueProjection': - base = this.visit((node as ExpressionNode).children[0], value); - if (!isObject(base)) { + } + case 'ValueProjection': { + const { left, right } = node; + + const base = this.visit(left, value); + if (base === null || typeof base !== 'object' || Array.isArray(base)) { return null; } - collected = []; + const collected: JSONArray = []; const values = Object.values(base); - for (i = 0; i < values.length; i += 1) { - current = this.visit((node as ExpressionNode).children[1], values[i]); + for (const elem of values) { + const current = this.visit(right, elem) as JSONValue; if (current !== null) { collected.push(current); } } - return collected as JSONValue; - case 'FilterProjection': - base = this.visit((node as ExpressionNode).children[0], value); + return collected; + } + case 'FilterProjection': { + const { left, right, condition } = node; + + const base = this.visit(left, value); if (!Array.isArray(base)) { return null; } - const filtered = []; - const finalResults = []; - for (i = 0; i < base.length; i += 1) { - matched = this.visit((node as ExpressionNode).children[2], base[i]); - if (!isFalse(matched)) { - filtered.push(base[i]); + + const results: JSONArray = []; + for (const elem of base) { + const matched = this.visit(condition, elem); + if (isFalse(matched)) { + continue; } - } - for (let j = 0; j < filtered.length; j += 1) { - current = this.visit((node as ExpressionNode).children[1], filtered[j]); - if (current !== null) { - finalResults.push(current); + const result = this.visit(right, elem) as JSONValue; + if (result !== null) { + results.push(result); } } - return finalResults as JSONValue; - case 'Comparator': - first = this.visit((node as ExpressionNode).children[0], value); - second = this.visit((node as ExpressionNode).children[1], value); - switch ((node as ComparitorNode).name) { - case Token.TOK_EQ: - result = strictDeepEqual(first, second); - break; - case Token.TOK_NE: - result = !strictDeepEqual(first, second); - break; - case Token.TOK_GT: - result = (first as number) > (second as number); - break; - case Token.TOK_GTE: - result = (first as number) >= (second as number); - break; - case Token.TOK_LT: - result = (first as number) < (second as number); - break; - case Token.TOK_LTE: - result = (first as number) <= (second as number); - break; - default: - throw new Error(`Unknown comparator: ${(node as ComparitorNode).name}`); - } - return result; - case Token.TOK_FLATTEN: - const original = this.visit((node as ExpressionNode).children[0], value); - if (!Array.isArray(original)) { - return null; - } - let merged: JSONValue[] = []; - for (i = 0; i < original.length; i += 1) { - current = original[i]; - if (Array.isArray(current)) { - merged = [...merged, ...current]; - } else { - merged.push(current); - } + return results; + } + case 'Comparator': { + const first = this.visit(node.left, value); + const second = this.visit(node.right, value); + switch (node.name) { + case 'EQ': + return strictDeepEqual(first, second); + case 'NE': + return !strictDeepEqual(first, second); + case 'GT': + return (first as number) > (second as number); + case 'GTE': + return (first as number) >= (second as number); + case 'LT': + return (first as number) < (second as number); + case 'LTE': + return (first as number) <= (second as number); } - return merged; - case 'Identity': - return value; - case 'MultiSelectList': + } + case 'Flatten': { + const original = this.visit(node.child, value); + return Array.isArray(original) ? original.flat() : null; + } + case 'Root': + return this._rootValue; + case 'MultiSelectList': { if (value === null) { return null; } - collected = []; - for (i = 0; i < (node as ExpressionNode).children.length; i += 1) { - collected.push(this.visit((node as ExpressionNode).children[i], value)); + const collected: JSONArray = []; + for (const child of node.children) { + collected.push(this.visit(child, value) as JSONValue); } - return collected as JSONValue; - case 'MultiSelectHash': + return collected; + } + case 'MultiSelectHash': { if (value === null) { return null; } - collected = {}; - let child: KeyValuePairNode; - for (i = 0; i < (node as ExpressionNode).children.length; i += 1) { - child = (node as ExpressionNode).children[i]; - collected[child.name as string] = this.visit(child.value, value); + const collected: JSONObject = {}; + for (const child of node.children) { + collected[child.name] = this.visit(child.value, value) as JSONValue; } return collected; - case 'OrExpression': - matched = this.visit((node as ExpressionNode).children[0], value); - if (isFalse(matched)) { - matched = this.visit((node as ExpressionNode).children[1], value); + } + case 'OrExpression': { + const result = this.visit(node.left, value); + if (isFalse(result)) { + return this.visit(node.right, value); } - return matched; - case 'AndExpression': - first = this.visit((node as ExpressionNode).children[0], value); - - if (isFalse(first)) { - return first; + return result; + } + case 'AndExpression': { + const result = this.visit(node.left, value); + if (isFalse(result)) { + return result; } - return this.visit((node as ExpressionNode).children[1], value); + return this.visit(node.right, value); + } case 'NotExpression': - first = this.visit((node as ExpressionNode).children[0], value); - return isFalse(first); + return isFalse(this.visit(node.child, value)); case 'Literal': - return (node as ValueNode).value; - case Token.TOK_PIPE: - left = this.visit((node as ExpressionNode).children[0], value); - return this.visit((node as ExpressionNode).children[1], left); - case Token.TOK_CURRENT: - return value; - case Token.TOK_ROOT: - return this._rootValue; - case 'Function': - const resolvedArgs: JSONValue[] = []; - for (let j = 0; j < (node as ExpressionNode).children.length; j += 1) { - resolvedArgs.push(this.visit((node as ExpressionNode).children[j], value) as JSONValue); - } - return this.runtime.callFunction((node as FieldNode).name as string, resolvedArgs) as JSONValue; + return node.value; + case 'Pipe': + return this.visit(node.right, this.visit(node.left, value)); + case 'Function': { + const args: JSONArray = []; + for (const child of node.children) { + args.push(this.visit(child, value) as JSONValue); + } + return this.runtime.callFunction(node.name, args); + } case 'ExpressionReference': - const refNode = (node as ExpressionNode).children[0] as ExpressionNode; - refNode.jmespathType = Token.TOK_EXPREF; - return refNode; - default: - throw new Error(`Unknown node type: ${node.type}`); + return { + expref: true, + ...node.child, + }; + case 'Current': + case 'Identity': + return value; } } - computeSliceParams(arrayLength: number, sliceParams: number[]): number[] { - let [start, stop, step] = sliceParams; + computeSliceParams(arrayLength: number, sliceNode: SliceNode): { start: number; stop: number; step: number } { + let { start, stop, step } = sliceNode; + if (step === null) { step = 1; } else if (step === 0) { @@ -252,11 +196,11 @@ export class TreeInterpreter { error.name = 'RuntimeError'; throw error; } - const stepValueNegative = step < 0 ? true : false; - start = start === null ? (stepValueNegative ? arrayLength - 1 : 0) : this.capSliceRange(arrayLength, start, step); - stop = stop === null ? (stepValueNegative ? -1 : arrayLength) : this.capSliceRange(arrayLength, stop, step); - return [start, stop, step]; + start = start === null ? (step < 0 ? arrayLength - 1 : 0) : this.capSliceRange(arrayLength, start, step); + stop = stop === null ? (step < 0 ? -1 : arrayLength) : this.capSliceRange(arrayLength, stop, step); + + return { start, stop, step }; } capSliceRange(arrayLength: number, actualValue: number, step: number): number { diff --git a/src/index.ts b/src/index.ts index 0244e61..42c2955 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,16 +1,12 @@ import Parser from './Parser'; import Lexer from './Lexer'; import TreeInterpreterInst from './TreeInterpreter'; -import { ExpressionNodeTree, LexerToken } from './Lexer'; import { InputArgument, RuntimeFunction, InputSignature } from './Runtime'; +import { JSONValue } from './JSON.type'; +import { LexerToken } from './Lexer.type'; +import { ExpressionNode } from './AST.type'; export type { FunctionSignature, RuntimeFunction, InputSignature } from './Runtime'; -export type ObjectDict = Record; - -export type JSONPrimitive = string | number | boolean | null; -export type JSONValue = JSONPrimitive | JSONObject | JSONArray; -export type JSONObject = { [member: string]: JSONValue }; -export type JSONArray = JSONValue[]; export const TYPE_ANY = InputArgument.TYPE_ANY; export const TYPE_ARRAY = InputArgument.TYPE_ARRAY; @@ -23,7 +19,7 @@ export const TYPE_NUMBER = InputArgument.TYPE_NUMBER; export const TYPE_OBJECT = InputArgument.TYPE_OBJECT; export const TYPE_STRING = InputArgument.TYPE_STRING; -export function compile(expression: string): ExpressionNodeTree { +export function compile(expression: string): ExpressionNode { const nodeTree = Parser.parse(expression); return nodeTree; } diff --git a/src/utils/index.ts b/src/utils/index.ts index 1783fb0..a6af016 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -38,21 +38,20 @@ export const strictDeepEqual = (first: unknown, second: unknown): boolean => { }; export const isFalse = (obj: unknown): boolean => { - if (obj === '' || obj === false || obj === null || obj === undefined) { - return true; - } - if (Array.isArray(obj) && obj.length === 0) { - return true; - } - if (isObject(obj)) { - for (const key in obj) { - if (obj.hasOwnProperty(key)) { - return false; - } + if (typeof obj === 'object') { + if (obj === null) { + return true; + } + if (Array.isArray(obj)) { + return obj.length === 0; + } + // eslint-disable-next-line @typescript-eslint/naming-convention + for (const _key in obj) { + return false; } return true; } - return false; + return !(typeof obj === 'number' || obj); }; export const isAlpha = (ch: string): boolean => {