UNPKG

@aws-lambda-powertools/jmespath

Version:

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

603 lines (602 loc) 20 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import { getType, isNumber, isRecord, } from '@aws-lambda-powertools/commons/typeutils'; import { JMESPathTypeError } from './errors.js'; import { arityCheck, typeCheck } from './utils.js'; /** * A class that contains the built-in JMESPath functions. * * The built-in functions are implemented as methods on the Functions class. * Each method is decorated with the `@Function.signature()` decorator to enforce the * arity and types of the arguments passed to the function at runtime. * * You can extend the Functions class to add custom functions by creating a new class * that extends Functions and adding new methods to it. * * @example * ```typescript * import { search } from '@aws-lambda-powertools/jmespath'; * import { Functions } from '@aws-lambda-powertools/jmespath/functions'; * * class MyFunctions extends Functions { * ⁣@Functions.signature({ * argumentsSpecs: [['number'], ['number']], * variadic: true, * }) * public funcMyMethod(args: Array<number>): unknown { * // ... * } * } * * const myFunctions = new MyFunctions(); * * search('myMethod(@)', {}, { customFunctions: new MyFunctions() }); * ``` */ class Functions { /** * A set of all the custom functions available in this and all child classes. */ methods = new Set(); /** * Get the absolute value of the provided number. * * @param args - The number to get the absolute value of */ funcAbs(args) { return Math.abs(args); } /** * Calculate the average of the numbers in the provided array. * * @param args - The numbers to average */ funcAvg(args) { return args.reduce((a, b) => a + b, 0) / args.length; } /** * Get the ceiling of the provided number. * * @param args - The number to get the ceiling of */ funcCeil(args) { return Math.ceil(args); } /** * Determine if the given value is contained in the provided array or string. * * @param haystack - The array or string to check * @param needle - The value to check for */ funcContains(haystack, needle) { return haystack.includes(needle); } /** * Determines if the provided string ends with the provided suffix. * * @param str - The string to check * @param suffix - The suffix to check for */ funcEndsWith(str, suffix) { return str.endsWith(suffix); } /** * Get the floor of the provided number. * * @param args - The number to get the floor of */ funcFloor(args) { return Math.floor(args); } /** * Join the provided array into a single string. * * @param separator - The separator to use * @param items - The array of itmes to join */ funcJoin(separator, items) { return items.join(separator); } /** * Get the keys of the provided object. * * @param arg - The object to get the keys of */ funcKeys(arg) { return Object.keys(arg); } /** * Get the number of items in the provided item. * * @param arg - The array to get the length of */ funcLength(arg) { if (isRecord(arg)) { return Object.keys(arg).length; } return arg.length; } /** * Map the provided function over the provided array. * * @param expression - The expression to map over the array * @param args - The array to map the expression over */ funcMap(expression, args) { return args.map((arg) => { return expression.visit(arg) || null; }); } /** * Get the maximum value in the provided array. * * @param arg - The array to get the maximum value of */ funcMax(arg) { if (arg.length === 0) { return null; // The signature decorator already enforces that all elements are of the same type } if (isNumber(arg[0])) { return Math.max(...arg); } // Math.max doesn't work with strings (returns NaN), so we use reduce for lexicographic comparison return arg.reduce((a, b) => (a > b ? a : b)); // NOSONAR - Math.max only works with numbers } /** * Get the item in the provided array that has the maximum value when the provided expression is evaluated. * * @param args - The array of items to get the maximum value of * @param expression - The expression to evaluate for each item in the array */ funcMaxBy(args, expression) { if (args.length === 0) { return null; } const visitedArgs = args.map((arg) => ({ arg, visited: expression.visit(arg), })); const max = visitedArgs.reduce((max, current) => { const type = getType(current.visited); if (type !== 'string' && type !== 'number') { throw new JMESPathTypeError({ currentValue: current.visited, expectedTypes: ['string'], actualType: type, }); } if (max.visited === current.visited) { return max; } // We can safely cast visited to number | string here because we've already // checked the type at runtime above and we know that it's either a number or a string return max.visited > current.visited ? max : current; }, visitedArgs[0]); return max.arg; } /** * Merge the provided objects into a single object. * * Note that this is a shallow merge and will not merge nested objects. * * @param args - The objects to merge */ funcMerge(...args) { // biome-ignore lint/performance/noAccumulatingSpread: This is a shallow merge so the tradeoff is acceptable return args.reduce((a, b) => ({ ...a, ...b }), {}); } /** * Get the minimum value in the provided array. * * @param arg - The array to get the minimum value of */ funcMin(arg) { if (arg.length === 0) { return null; // The signature decorator already enforces that all elements are of the same type } if (isNumber(arg[0])) { return Math.min(...arg); } // Math.min doesn't work with strings (returns NaN), so we use reduce for lexicographic comparison return arg.reduce((a, b) => (a < b ? a : b)); // NOSONAR - Math.min only works with numbers } /** * Get the item in the provided array that has the minimum value when the provided expression is evaluated. * * @param args - The array of items to get the minimum value of * @param expression - The expression to evaluate for each item in the array */ funcMinBy(args, expression) { if (args.length === 0) { return null; } const visitedArgs = args.map((arg) => ({ arg, visited: expression.visit(arg), })); const min = visitedArgs.reduce((min, current) => { const type = getType(current.visited); if (type !== 'string' && type !== 'number') { throw new JMESPathTypeError({ currentValue: current.visited, expectedTypes: ['string'], actualType: type, }); } if (min.visited === current.visited) { return min; } // We can safely cast visited to number | string here because we've already // checked the type at runtime above and we know that it's either a number or a string return min.visited < current.visited ? min : current; }, visitedArgs[0]); return min.arg; } /** * Get the first argument that does not evaluate to null. * If all arguments evaluate to null, then null is returned. * * @param args - The keys of the items to check */ funcNotNull(...args) { return args.find((arg) => !Object.is(arg, null)) || null; } /** * Reverses the provided string or array. * * @param arg - The string or array to reverse */ funcReverse(arg) { return Array.isArray(arg) ? arg.reverse() : arg.split('').reverse().join(''); } /** * Sort the provided array. * * @param arg - The array to sort */ funcSort(arg) { return arg.sort((a, b) => { if (typeof a === 'string') { // We can safely cast a and b to string here because the signature decorator // already enforces that all elements are of the same type return a.localeCompare(b); } // We can safely cast a and b to number here because the signature decorator // already enforces that all elements are of the same type, so if they're not strings // then they must be numbers return a - b; }); } /** * Sort the provided array by the provided expression. * * @param args - The array to sort * @param expression - The expression to sort by */ funcSortBy(args, expression) { return args .map((value, index) => { const visited = expression.visit(value); const type = getType(visited); if (type !== 'string' && type !== 'number') { throw new JMESPathTypeError({ currentValue: visited, expectedTypes: ['string'], actualType: getType(visited), }); } return { value, index, visited, }; }) .sort((a, b) => { if (a.visited === b.visited) { return a.index - b.index; // Make the sort stable } // We can safely cast visited to number | string here because we've already // checked the type at runtime above and we know that it's either a number or a string return a.visited > b.visited ? 1 : -1; }) .map(({ value }) => value); // Extract the original values } /** * Determines if the provided string starts with the provided prefix. * * @param str - The string to check * @param prefix - The prefix to check for */ funcStartsWith(str, prefix) { return str.startsWith(prefix); } /** * Sum the provided numbers. * * @param args - The numbers to sum */ funcSum(args) { return args.reduce((a, b) => a + b, 0); } /** * Convert the provided value to an array. * * If the provided value is an array, then it is returned. * Otherwise, the value is wrapped in an array and returned. * * @param arg - The items to convert to an array */ funcToArray(arg) { return Array.isArray(arg) ? arg : [arg]; } /** * Convert the provided value to a number. * * If the provided value is a number, then it is returned. * Otherwise, the value is converted to a number and returned. * * If the value cannot be converted to a number, then null is returned. * * @param arg - The value to convert to a number */ funcToNumber(arg) { if (typeof arg === 'number') { return arg; } if (typeof arg === 'string') { const num = Number(arg); return Number.isNaN(num) ? null : num; } return null; } /** * Convert the provided value to a string. * * If the provided value is a string, then it is returned. * Otherwise, the value is converted to a string and returned. * * @param arg - The value to convert to a string */ funcToString(arg) { return typeof arg === 'string' ? arg : JSON.stringify(arg); } /** * Get the type of the provided value. * * @param arg - The value to check the type of */ funcType(arg) { return getType(arg); } /** * Get the values of the provided object. * * @param arg - The object to get the values of */ funcValues(arg) { return Object.values(arg); } /** * Lazily introspects the methods of the class instance and all child classes * to get the names of the methods that correspond to JMESPath functions. * * This method is used to get the names of the custom functions that are available * in the class instance and all child classes. The names of the functions are used * to create the custom function map that is passed to the JMESPath search function. * * The method traverses the inheritance chain going from the leaf class to the root class * and stops when it reaches the `Functions` class, which is the root class. * * In doing so, it collects the names of the methods that start with `func` and adds them * to the `methods` set. Finally, when the recursion collects back to the current instance, * it adds the collected methods to the `this.methods` set so that they can be accessed later. * * @param scope - The scope of the class instance to introspect */ introspectMethods(scope) { const prototype = Object.getPrototypeOf(this); const methods = new Set(); if (this instanceof Functions) { for (const method of prototype.introspectMethods(scope)) { methods.add(method); } } else { return methods; } // This block is executed for every class in the inheritance chain for (const method of Object.getOwnPropertyNames(Object.getPrototypeOf(this))) { method !== 'constructor' && method.startsWith('func') && methods.add(method); } // This block will be executed only if the scope is the outermost class if (this.methods) { for (const method of methods) { this.methods.add(method); } } return methods; } /** * Decorator to enforce the signature of a function at runtime. * * The signature decorator enforces the arity and types of the arguments * passed to a function at runtime. If the arguments do not match the * expected arity or types errors are thrown. * * @example * ```typescript * import { PowertoolsFunctions } from '@aws-lambda-powertools/jmespath/functions'; * * class MyFunctions extends Functions { * ⁣@Functions.signature({ * argumentsSpecs: [['number'], ['number']], * variadic: true, * }) * public funcMyMethod(args: Array<number>): unknown { * // ... * } * } * ``` * * @param options - The options for the signature decorator */ static signature(options) { return (_target, _propertyKey, descriptor) => { const originalMethod = descriptor.value; // Use a function() {} instead of an () => {} arrow function so that we can // access `myClass` as `this` in a decorated `myClass.myMethod()`. descriptor.value = function (args) { const { variadic, argumentsSpecs } = options; arityCheck(args, argumentsSpecs, variadic); typeCheck(args, argumentsSpecs); return originalMethod.apply(this, args); }; return descriptor; }; } } __decorate([ Functions.signature({ argumentsSpecs: [['number']] }) ], Functions.prototype, "funcAbs", null); __decorate([ Functions.signature({ argumentsSpecs: [['array-number']], }) ], Functions.prototype, "funcAvg", null); __decorate([ Functions.signature({ argumentsSpecs: [['number']] }) ], Functions.prototype, "funcCeil", null); __decorate([ Functions.signature({ argumentsSpecs: [['array', 'string'], ['any']], }) ], Functions.prototype, "funcContains", null); __decorate([ Functions.signature({ argumentsSpecs: [['string'], ['string']], }) ], Functions.prototype, "funcEndsWith", null); __decorate([ Functions.signature({ argumentsSpecs: [['number']] }) ], Functions.prototype, "funcFloor", null); __decorate([ Functions.signature({ argumentsSpecs: [['string'], ['array-string']], }) ], Functions.prototype, "funcJoin", null); __decorate([ Functions.signature({ argumentsSpecs: [['object']], }) ], Functions.prototype, "funcKeys", null); __decorate([ Functions.signature({ argumentsSpecs: [['array', 'string', 'object']], }) ], Functions.prototype, "funcLength", null); __decorate([ Functions.signature({ argumentsSpecs: [['any'], ['array']], }) ], Functions.prototype, "funcMap", null); __decorate([ Functions.signature({ argumentsSpecs: [['array-number', 'array-string']], }) ], Functions.prototype, "funcMax", null); __decorate([ Functions.signature({ argumentsSpecs: [['array'], ['expression']], }) ], Functions.prototype, "funcMaxBy", null); __decorate([ Functions.signature({ argumentsSpecs: [['object']], variadic: true, }) ], Functions.prototype, "funcMerge", null); __decorate([ Functions.signature({ argumentsSpecs: [['array-number', 'array-string']], }) ], Functions.prototype, "funcMin", null); __decorate([ Functions.signature({ argumentsSpecs: [['array'], ['expression']], }) ], Functions.prototype, "funcMinBy", null); __decorate([ Functions.signature({ argumentsSpecs: [[]], variadic: true, }) ], Functions.prototype, "funcNotNull", null); __decorate([ Functions.signature({ argumentsSpecs: [['string', 'array']], }) ], Functions.prototype, "funcReverse", null); __decorate([ Functions.signature({ argumentsSpecs: [['array-number', 'array-string']], }) ], Functions.prototype, "funcSort", null); __decorate([ Functions.signature({ argumentsSpecs: [['array'], ['expression']], }) ], Functions.prototype, "funcSortBy", null); __decorate([ Functions.signature({ argumentsSpecs: [['string'], ['string']], }) ], Functions.prototype, "funcStartsWith", null); __decorate([ Functions.signature({ argumentsSpecs: [['array-number']], }) ], Functions.prototype, "funcSum", null); __decorate([ Functions.signature({ argumentsSpecs: [['any']], }) ], Functions.prototype, "funcToArray", null); __decorate([ Functions.signature({ argumentsSpecs: [['any']], }) ], Functions.prototype, "funcToNumber", null); __decorate([ Functions.signature({ argumentsSpecs: [['any']], }) ], Functions.prototype, "funcToString", null); __decorate([ Functions.signature({ argumentsSpecs: [['any']], }) ], Functions.prototype, "funcType", null); __decorate([ Functions.signature({ argumentsSpecs: [['object']], }) ], Functions.prototype, "funcValues", null); export { Functions };