@aws-lambda-powertools/jmespath
Version:
A type safe and modern jmespath module to parse and extract data from JSON documents using JMESPath
605 lines (604 loc) • 20 kB
JavaScript
;
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;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Functions = void 0;
const typeutils_1 = require("@aws-lambda-powertools/commons/typeutils");
const errors_js_1 = require("./errors.js");
const utils_js_1 = require("./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 ((0, typeutils_1.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 ((0, typeutils_1.isNumber)(arg[0])) {
return Math.max(...arg);
}
// local compare function to handle string comparison
return arg.reduce((a, b) => (a > b ? a : b));
}
/**
* 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 = (0, typeutils_1.getType)(current.visited);
if (type !== 'string' && type !== 'number') {
throw new errors_js_1.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 ((0, typeutils_1.isNumber)(arg[0])) {
return Math.min(...arg);
}
return arg.reduce((a, b) => (a < b ? a : b));
}
/**
* 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 = (0, typeutils_1.getType)(current.visited);
if (type !== 'string' && type !== 'number') {
throw new errors_js_1.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 = (0, typeutils_1.getType)(visited);
if (type !== 'string' && type !== 'number') {
throw new errors_js_1.JMESPathTypeError({
currentValue: visited,
expectedTypes: ['string'],
actualType: (0, typeutils_1.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 (0, typeutils_1.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;
(0, utils_js_1.arityCheck)(args, argumentsSpecs, variadic);
(0, utils_js_1.typeCheck)(args, argumentsSpecs);
return originalMethod.apply(this, args);
};
return descriptor;
};
}
}
exports.Functions = Functions;
__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);