@aws-lambda-powertools/jmespath
Version:
A type safe and modern jmespath module to parse and extract data from JSON documents using JMESPath
441 lines (440 loc) • 14.6 kB
JavaScript
"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 errors_js_1 = require("./errors.js");
const Functions_js_1 = require("./Functions.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);
}
/* v8 ignore else -- @preserve */
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 suppress the issue 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-expect-error
return this.#functions[funcName](args);
}
catch (error) {
/* v8 ignore else -- @preserve */
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) {
/* v8 ignore else -- @preserve */
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;