UNPKG

@aws-lambda-powertools/jmespath

Version:

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

284 lines (283 loc) 10.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.typeCheckArgument = exports.typeCheck = exports.sliceArray = exports.arityCheck = exports.isTruthy = 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"); /** * Check if a value is truthy. * * In JavaScript, zero is falsy while all other non-zero numbers are truthy. * In JMESPath however, zero is truthy as well as all other non-zero numbers. For * this reason we wrap the original isTruthy function from the commons package * and add a check for numbers. * * @param value The value to check */ const isTruthy = (value) => { if ((0, typeutils_1.isNumber)(value)) { return true; } return (0, typeutils_1.isTruthy)(value); }; exports.isTruthy = isTruthy; /** * @internal * Cap a slice range value to the length of an array, taking into account * negative values and whether the step is negative. * * @param arrayLength The length of the array * @param value The value to cap * @param isStepNegative Whether the step is negative */ const capSliceRange = (arrayLength, value, isStepNegative) => { let capValue = value; if (capValue < 0) { capValue += arrayLength; if (capValue < 0) { capValue = isStepNegative ? -1 : 0; } } else if (capValue >= arrayLength) { capValue = isStepNegative ? arrayLength - 1 : arrayLength; } return capValue; }; /** * Given a start, stop, and step value, the sub elements in an array are extracted as follows: * * The first element in the extracted array is the index denoted by start. * * The last element in the extracted array is the index denoted by end - 1. * * The step value determines how many indices to skip after each element is selected from the array. An array of 1 (the default step) will not skip any indices. A step value of 2 will skip every other index while extracting elements from an array. A step value of -1 will extract values in reverse order from the array. * * Slice expressions adhere to the following rules: * * If a negative start position is given, it is calculated as the total length of the array plus the given start position. * * If no start position is given, it is assumed to be 0 if the given step is greater than 0 or the end of the array if the given step is less than 0. * * If a negative stop position is given, it is calculated as the total length of the array plus the given stop position. * * If no stop position is given, it is assumed to be the length of the array if the given step is greater than 0 or 0 if the given step is less than 0. * * If the given step is omitted, it it assumed to be 1. * * If the given step is 0, an invalid-value error MUST be raised (thrown before calling the function) * * If the element being sliced is not an array, the result is null (returned before calling the function) * * If the element being sliced is an array and yields no results, the result MUST be an empty array. * * @param array The array to slice * @param start The start index * @param end The end index * @param step The step value */ const sliceArray = ({ array, start, end, step, }) => { const isStepNegative = step < 0; const length = array.length; const defaultStart = isStepNegative ? length - 1 : 0; const defaultEnd = isStepNegative ? -1 : length; start = (0, typeutils_1.isIntegerNumber)(start) ? capSliceRange(length, start, isStepNegative) : defaultStart; end = (0, typeutils_1.isIntegerNumber)(end) ? capSliceRange(length, end, isStepNegative) : defaultEnd; const result = []; if (step > 0) { for (let i = start; i < end; i += step) { result.push(array[i]); } } else { for (let i = start; i > end; i += step) { result.push(array[i]); } } return result; }; exports.sliceArray = sliceArray; /** * Checks if the number of arguments passed to a function matches the expected arity. * If the number of arguments does not match the expected arity, an ArityError is thrown. * * If the function is variadic, then the number of arguments passed to the function must be * greater than or equal to the expected arity. If the number of arguments passed to the function * is less than the expected arity, a `VariadicArityError` is thrown. * * @param args The arguments passed to the function * @param argumentsSpecs The expected types for each argument * @param decoratedFuncName The name of the function being called * @param variadic Whether the function is variadic */ const arityCheck = (args, argumentsSpecs, variadic) => { if (variadic) { if (args.length < argumentsSpecs.length) { throw new errors_js_1.VariadicArityError({ expectedArity: argumentsSpecs.length, actualArity: args.length, }); } } else if (args.length !== argumentsSpecs.length) { throw new errors_js_1.ArityError({ expectedArity: argumentsSpecs.length, actualArity: args.length, }); } }; exports.arityCheck = arityCheck; /** * Type checks the arguments passed to a function against the expected types. * * Type checking at runtime involves checking the top level type, * and in the case of arrays, potentially checking the types of * the elements in the array. * * If the list of types includes 'any', then the type check is a * no-op. * * If the list of types includes more than one type, then the * argument is checked against each type in the list. If the * argument matches any of the types, then the type check * passes. If the argument does not match any of the types, then * a JMESPathTypeError is thrown. * * @param args The arguments passed to the function * @param argumentsSpecs The expected types for each argument */ const typeCheck = (args, argumentsSpecs) => { for (const [index, argumentSpec] of argumentsSpecs.entries()) { if (argumentSpec[0] === 'any') continue; typeCheckArgument(args[index], argumentSpec); } }; exports.typeCheck = typeCheck; /** * Type checks an argument against a list of types. * * If the list of types includes more than one type, then the * argument is checked against each type in the list. If the * argument matches any of the types, then the type check * passes. If the argument does not match any of the types, then * a JMESPathTypeError is thrown. * * @param arg * @param argumentSpec */ const typeCheckArgument = (arg, argumentSpec) => { let valid = false; argumentSpec.forEach((type, index) => { if (valid) return; valid = checkIfArgumentTypeIsValid(arg, type, index, argumentSpec); }); }; exports.typeCheckArgument = typeCheckArgument; /** * Check if the argument is of the expected type. * * @param arg The argument to check * @param type The expected type * @param index The index of the type we are checking * @param argumentSpec The list of types to check against */ const checkIfArgumentTypeIsValid = (arg, type, index, argumentSpec) => { const hasMoreTypesToCheck = index < argumentSpec.length - 1; if (type.startsWith('array')) { if (!Array.isArray(arg)) { if (hasMoreTypesToCheck) { return false; } throw new errors_js_1.JMESPathTypeError({ currentValue: arg, expectedTypes: argumentSpec, actualType: (0, typeutils_1.getType)(arg), }); } checkComplexArrayType(arg, type, hasMoreTypesToCheck); return true; } if (type === 'expression') { checkExpressionType(arg, argumentSpec, hasMoreTypesToCheck); return true; } if (['string', 'number', 'boolean'].includes(type)) { typeCheckType(arg, type, argumentSpec, hasMoreTypesToCheck); // biome-ignore lint/suspicious/useValidTypeof: we know that `type` is one of 'string', 'number', or 'boolean' because we checked for that above if (typeof arg === type) return true; } else if (type === 'object') { checkObjectType(arg, argumentSpec, hasMoreTypesToCheck); return true; } return false; }; /** * Check if the argument is of the expected type. * * @param arg The argument to check * @param type The type to check against * @param argumentSpec The list of types to check against * @param hasMoreTypesToCheck Whether there are more types to check */ const typeCheckType = (arg, type, argumentSpec, hasMoreTypesToCheck) => { // biome-ignore lint/suspicious/useValidTypeof: we know that `type` is one of 'string', 'number', or 'boolean' because we checked before calling this function if (typeof arg !== type && !hasMoreTypesToCheck) { throw new errors_js_1.JMESPathTypeError({ currentValue: arg, expectedTypes: argumentSpec, actualType: (0, typeutils_1.getType)(arg), }); } }; /** * Check if the argument is an array of complex types. * * @param arg The argument to check * @param type The type to check against * @param hasMoreTypesToCheck Whether there are more types to check */ const checkComplexArrayType = (arg, type, hasMoreTypesToCheck) => { if (!type.includes('-')) return; const arrayItemsType = type.slice(6); let actualType; for (const element of arg) { try { typeCheckArgument(element, [arrayItemsType]); actualType = arrayItemsType; } catch (error) { if (!hasMoreTypesToCheck || actualType !== undefined) { throw error; } } } }; /** * Check if the argument is an expression. * * @param arg The argument to check * @param type The type to check against * @param hasMoreTypesToCheck Whether there are more types to check */ const checkExpressionType = (arg, type, hasMoreTypesToCheck) => { if (!(arg instanceof Expression_js_1.Expression) && !hasMoreTypesToCheck) { throw new errors_js_1.JMESPathTypeError({ currentValue: arg, expectedTypes: type, actualType: (0, typeutils_1.getType)(arg), }); } }; /** * Check if the argument is an object. * * @param arg The argument to check * @param type The type to check against * @param hasMoreTypesToCheck Whether there are more types to check */ const checkObjectType = (arg, type, hasMoreTypesToCheck) => { if (!(0, typeutils_1.isRecord)(arg) && !hasMoreTypesToCheck) { throw new errors_js_1.JMESPathTypeError({ currentValue: arg, expectedTypes: type, actualType: (0, typeutils_1.getType)(arg), }); } };