UNPKG

@aws-lambda-powertools/jmespath

Version:

A type safe and modern jmespath module to parse and extract data from JSON documents using JMESPath

438 lines (437 loc) 14.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TreeInterpreter = void 0; const typeutils_1 = require("@aws-lambda-powertools/commons/typeutils"); const Expression_js_1 = require("./Expression.js"); const Functions_js_1 = require("./Functions.js"); const errors_js_1 = require("./errors.js"); const utils_js_1 = require("./utils.js"); /** * * A tree interpreter for JMESPath ASTs. * * The tree interpreter is responsible for visiting nodes in the AST and * evaluating them to produce a result. * * @internal */ class TreeInterpreter { #functions; /** * @param options The options to use for the interpreter. */ constructor(options) { if (options?.customFunctions) { this.#functions = options.customFunctions; } else { this.#functions = new Functions_js_1.Functions(); } this.#functions.introspectMethods(); } /** * Visit a node in the AST. * * The function will call the appropriate method to visit the node based on its type. * * @param node The node to visit. * @param value The current value to visit. */ visit(node, value) { const nodeType = node.type; const visitMethods = { subexpression: this.#visitSubexpressionOrIndexExpressionOrPipe, field: this.#visitField, comparator: this.#visitComparator, current: this.#visitCurrent, expref: this.#visitExpref, function_expression: this.#visitFunctionExpression, filter_projection: this.#visitFilterProjection, flatten: this.#visitFlatten, identity: this.#visitIdentity, index: this.#visitIndex, index_expression: this.#visitSubexpressionOrIndexExpressionOrPipe, slice: this.#visitSlice, key_val_pair: this.#visitKeyValPair, literal: this.#visitLiteral, multi_select_object: this.#visitMultiSelectObject, multi_select_list: this.#visitMultiSelectList, or_expression: this.#visitOrExpression, and_expression: this.#visitAndExpression, not_expression: this.#visitNotExpression, pipe: this.#visitSubexpressionOrIndexExpressionOrPipe, projection: this.#visitProjection, value_projection: this.#visitValueProjection, }; const visitMethod = visitMethods[nodeType]; if (visitMethod) { return visitMethod.call(this, node, value); } throw new errors_js_1.JMESPathError(`Not Implemented: Invalid node type: ${node.type}`); } /** * Visit a subexpression, index expression, or pipe node. * * This method is shared between subexpression, index expression, and pipe * since they all behave the same way in the context of an expression. * * They all visit their children and return the result of the last child. * * @param node The node to visit. * @param value The current value to visit. */ #visitSubexpressionOrIndexExpressionOrPipe(node, value) { let result = value; for (const child of node.children) { result = this.visit(child, result); } return result; } /** * Visit a field node. * * @param node The field node to visit. * @param value The current value to visit. */ #visitField(node, value) { if (!node.value) return null; if ((0, typeutils_1.isRecord)(value) && typeof node.value === 'string' && node.value in value) { return value[node.value]; } return null; } /** * Visit a comparator node. * * @param node The comparator node to visit. * @param value The current value to visit. */ #visitComparator(node, value) { const comparator = node.value; const left = this.visit(node.children[0], value); const right = this.visit(node.children[1], value); if (typeof comparator === 'string' && ['eq', 'ne', 'gt', 'gte', 'lt', 'lte'].includes(comparator)) { // Common cases: comparator is == or != if (comparator === 'eq') { return (0, typeutils_1.isStrictEqual)(left, right); } if (comparator === 'ne') { return !(0, typeutils_1.isStrictEqual)(left, right); } if (typeof left === 'number' && typeof right === 'number') { // Ordering operators only work on numbers. Evaluating them on other // types will return null. if (comparator === 'lt') { return left < right; } if (comparator === 'lte') { return left <= right; } if (comparator === 'gt') { return left > right; } return left >= right; } } else { throw new errors_js_1.JMESPathError(`Invalid comparator: ${comparator}`); } } /** * Visit a current node. * * @param node The current node to visit. * @param value The current value to visit. */ #visitCurrent(_node, value) { return value; } /** * Visit an expref node. * * @param node The expref node to visit. * @param value The current value to visit. */ #visitExpref(node, _value) { return new Expression_js_1.Expression(node.children[0], this); } /** * Visit a function expression node. * * @param node The function expression node to visit. * @param value The current value to visit. */ #visitFunctionExpression(node, value) { const args = []; for (const child of node.children) { args.push(this.visit(child, value)); } // check that method name is a string if (typeof node.value !== 'string') { throw new errors_js_1.JMESPathError(`Function name must be a string, got ${node.value}`); } // convert snake_case to camelCase const normalizedFunctionName = node.value.replace(/_([a-z])/g, (g) => g[1].toUpperCase()); // capitalize first letter & add `func` prefix const funcName = `func${normalizedFunctionName.charAt(0).toUpperCase() + normalizedFunctionName.slice(1)}`; if (!this.#functions.methods.has(funcName)) { throw new errors_js_1.UnknownFunctionError(node.value); } try { // We know that methodName is a key of this.#functions, but TypeScript // doesn't know that, so we have to use @ts-ignore to tell it that it's // okay. We could use a type assertion like `as keyof Functions`, but // we also want to keep the args generic, so for now we'll just ignore it. // @ts-ignore-next-line return this.#functions[funcName](args); } catch (error) { if (error instanceof errors_js_1.JMESPathTypeError || error instanceof errors_js_1.VariadicArityError || error instanceof errors_js_1.ArityError) { error.setEvaluatedFunctionName(node.value); throw error; } } } /** * Visit a filter projection node. * * @param node The filter projection node to visit. * @param value The current value to visit. */ #visitFilterProjection(node, value) { const base = this.visit(node.children[0], value); if (!Array.isArray(base)) { return null; } const comparatorNode = node.children[2]; const collected = []; for (const item of base) { if ((0, utils_js_1.isTruthy)(this.visit(comparatorNode, item))) { const current = this.visit(node.children[1], item); if (current !== null) { collected.push(current); } } } return collected; } /** * Visit a flatten node. * * @param node The flatten node to visit. * @param value The current value to visit. */ #visitFlatten(node, value) { const base = this.visit(node.children[0], value); if (!Array.isArray(base)) { return null; } const mergedList = []; for (const item of base) { if (Array.isArray(item)) { mergedList.push(...item); } else { mergedList.push(item); } } return mergedList; } /** * Visit an identity node. * * @param node The identity node to visit. * @param value The current value to visit. */ #visitIdentity(_node, value) { return value; } /** * Visit an index node. * * @param node The index node to visit. * @param value The current value to visit. */ #visitIndex(node, value) { if (!Array.isArray(value)) { return null; } // The Python implementation doesn't support string indexing // even though we could, so we won't either for now. if (typeof node.value !== 'number') { throw new errors_js_1.JMESPathError(`Invalid index: ${node.value}`); } const index = node.value < 0 ? value.length + node.value : node.value; const found = value[index]; if (found === undefined) { return null; } return found; } /** * Visit a slice node. * * @param node The slice node to visit. * @param value The current value to visit. */ #visitSlice(node, value) { const step = (0, typeutils_1.isIntegerNumber)(node.children[2]) ? node.children[2] : 1; if (step === 0) { throw new Error('Invalid slice, step cannot be 0'); } if (!Array.isArray(value)) { return null; } if (value.length === 0) { return []; } return (0, utils_js_1.sliceArray)({ array: value, start: node.children[0], end: node.children[1], step, }); } /** * Visit a key-value pair node. * * @param node The key-value pair node to visit. * @param value The current value to visit. */ #visitKeyValPair(node, value) { return this.visit(node.children[0], value); } /** * Visit a literal node. * * @param node The literal node to visit. * @param value The current value to visit. */ #visitLiteral(node, _value) { return node.value; } /** * Visit a multi-select object node. * * @param node The multi-select object node to visit. * @param value The current value to visit. */ #visitMultiSelectObject(node, value) { if (Object.is(value, null)) { return null; } const collected = {}; for (const child of node.children) { if (typeof child.value === 'string') { collected[child.value] = this.visit(child, value); } } return collected; } /** * Visit a multi-select list node. * * @param node The multi-select list node to visit. * @param value The current value to visit. */ #visitMultiSelectList(node, value) { if (Object.is(value, null)) { return null; } const collected = []; for (const child of node.children) { collected.push(this.visit(child, value)); } return collected; } /** * Visit an or expression node. * * @param node The or expression node to visit. * @param value The current value to visit. */ #visitOrExpression(node, value) { const matched = this.visit(node.children[0], value); if (!(0, utils_js_1.isTruthy)(matched)) { return this.visit(node.children[1], value); } return matched; } /** * Visit an and expression node. * * @param node The and expression node to visit. * @param value The current value to visit. */ #visitAndExpression(node, value) { const matched = this.visit(node.children[0], value); if (!(0, utils_js_1.isTruthy)(matched)) { return matched; } return this.visit(node.children[1], value); } /** * Visit a not expression node. * * @param node The not expression node to visit. * @param value The current value to visit. */ #visitNotExpression(node, value) { const originalResult = this.visit(node.children[0], value); if (typeof originalResult === 'number' && originalResult === 0) { // Special case for 0, !0 should be false, not true. // 0 is not a special cased integer in jmespath. return false; } return !(0, utils_js_1.isTruthy)(originalResult); } /** * Visit a projection node. * * @param node The projection node to visit. * @param value The current value to visit. */ #visitProjection(node, value) { const base = this.visit(node.children[0], value); if (!Array.isArray(base)) { return null; } const collected = []; for (const item of base) { const current = this.visit(node.children[1], item); if (current !== null) { collected.push(current); } } return collected; } /** * Visit a value projection node. * * @param node The value projection node to visit. * @param value The current value to visit. */ #visitValueProjection(node, value) { const base = this.visit(node.children[0], value); if (!(0, typeutils_1.isRecord)(base)) { return null; } const values = Object.values(base); const collected = []; for (const item of values) { const current = this.visit(node.children[1], item); if (current !== null) { collected.push(current); } } return collected; } } exports.TreeInterpreter = TreeInterpreter;