From 768229b4aca9b5bd10b4a1f323a6fc168e1c1f79 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 12 Mar 2024 11:01:05 +0100 Subject: [PATCH 1/9] feat(jmespath): add Expression and utils --- packages/jmespath/src/Expression.ts | 28 +++ packages/jmespath/src/TreeInterpreter.ts | 10 + packages/jmespath/src/utils.ts | 254 +++++++++++++++++++++++ 3 files changed, 292 insertions(+) create mode 100644 packages/jmespath/src/Expression.ts create mode 100644 packages/jmespath/src/TreeInterpreter.ts create mode 100644 packages/jmespath/src/utils.ts diff --git a/packages/jmespath/src/Expression.ts b/packages/jmespath/src/Expression.ts new file mode 100644 index 0000000000..94c69ef3c4 --- /dev/null +++ b/packages/jmespath/src/Expression.ts @@ -0,0 +1,28 @@ +import type { TreeInterpreter } from './TreeInterpreter.js'; +import type { JSONObject, Node } from './types.js'; + +/** + * Apply a JMESPath expression to a JSON value. + */ +class Expression { + readonly #expression: Node; + readonly #interpreter: TreeInterpreter; + + public constructor(expression: Node, interpreter: TreeInterpreter) { + this.#expression = expression; + this.#interpreter = interpreter; + } + + /** + * Evaluate the expression against a JSON value. + * + * @param value The JSON value to apply the expression to. + * @param node The node to visit. + * @returns The result of applying the expression to the value. + */ + public visit(value: JSONObject, node?: Node): JSONObject { + return this.#interpreter.visit(node ?? this.#expression, value); + } +} + +export { Expression }; diff --git a/packages/jmespath/src/TreeInterpreter.ts b/packages/jmespath/src/TreeInterpreter.ts new file mode 100644 index 0000000000..29fcfcf0ca --- /dev/null +++ b/packages/jmespath/src/TreeInterpreter.ts @@ -0,0 +1,10 @@ +import type { Node, JSONObject } from './types.js'; + +// This is a placeholder for the real class. The actual implementation will be added in a subsequent PR. +export class TreeInterpreter { + public iAmAPlaceholder = true; + + public visit(_node: Node, _value: JSONObject): JSONObject | null { + return this.iAmAPlaceholder; + } +} diff --git a/packages/jmespath/src/utils.ts b/packages/jmespath/src/utils.ts new file mode 100644 index 0000000000..77936fc625 --- /dev/null +++ b/packages/jmespath/src/utils.ts @@ -0,0 +1,254 @@ +import { + getType, + isIntegerNumber, + isRecord, + isTruthy as isTruthyJS, + isNumber, +} from '@aws-lambda-powertools/commons/typeutils'; +import { Expression } from './Expression.js'; +import { ArityError, JMESPathTypeError, VariadicArityError } from './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: unknown): boolean => { + if (isNumber(value)) { + return true; + } else { + return isTruthyJS(value); + } +}; + +/** + * @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: number, + value: number, + isStepNegative: boolean +): number => { + if (value < 0) { + value += arrayLength; + if (value < 0) { + value = isStepNegative ? -1 : 0; + } + } else if (value >= arrayLength) { + value = isStepNegative ? arrayLength - 1 : arrayLength; + } + + return value; +}; + +/** + * 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, +}: { + array: T[]; + start?: number; + end?: number; + step: number; +}): T[] | null => { + const isStepNegative = step < 0; + const length = array.length; + const defaultStart = isStepNegative ? length - 1 : 0; + const defaultEnd = isStepNegative ? -1 : length; + + start = isIntegerNumber(start) + ? capSliceRange(length, start, isStepNegative) + : defaultStart; + + end = isIntegerNumber(end) + ? capSliceRange(length, end, isStepNegative) + : defaultEnd; + + const result: T[] = []; + 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; +}; + +/** + * 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: unknown[], + argumentsSpecs: Array>, + variadic?: boolean +): void => { + if (variadic) { + if (args.length < argumentsSpecs.length) { + throw new VariadicArityError({ + expectedArity: argumentsSpecs.length, + actualArity: args.length, + }); + } + } else if (args.length !== argumentsSpecs.length) { + throw new ArityError({ + expectedArity: argumentsSpecs.length, + actualArity: args.length, + }); + } +}; + +/** + * Type checks the arguments passed to a function against the expected types. + * + * @param args The arguments passed to the function + * @param argumentsSpecs The expected types for each argument + */ +const typeCheck = ( + args: unknown[], + argumentsSpecs: Array> +): void => { + argumentsSpecs.forEach((argumentSpec, index) => { + typeCheckArgument(args[index], argumentSpec); + }); +}; + +/** + * Type checks an argument against a list of 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 arg + * @param argumentSpec + */ +const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { + if (argumentSpec.length === 0 || argumentSpec[0] === 'any') { + return; + } + const entryCount = argumentSpec.length; + let hasMoreTypesToCheck = argumentSpec.length > 1; + for (const [index, type] of argumentSpec.entries()) { + hasMoreTypesToCheck = index < entryCount - 1; + if (type.startsWith('array')) { + if (!Array.isArray(arg)) { + if (hasMoreTypesToCheck) { + continue; + } + throw new JMESPathTypeError({ + currentValue: arg, + expectedTypes: argumentSpec, + actualType: getType(arg), + }); + } + if (type.includes('-')) { + const arrayItemsType = type.slice(6); + let actualType: string | undefined; + for (const element of arg) { + try { + typeCheckArgument(element, [arrayItemsType]); + actualType = arrayItemsType; + } catch (error) { + if (!hasMoreTypesToCheck || actualType !== undefined) { + throw error; + } + } + } + } + break; + } + if (type === 'expression') { + if (!(arg instanceof Expression)) { + if (!hasMoreTypesToCheck) { + throw new JMESPathTypeError({ + currentValue: arg, + expectedTypes: argumentSpec, + actualType: getType(arg), + }); + } + } + break; + } else if (type === 'string' || type === 'number' || type === 'boolean') { + if (typeof arg !== type) { + if (!hasMoreTypesToCheck) { + throw new JMESPathTypeError({ + currentValue: arg, + expectedTypes: argumentSpec, + actualType: getType(arg), + }); + } + continue; + } + break; + } else if (type === 'object') { + if (!isRecord(arg)) { + if (index === entryCount - 1) { + throw new JMESPathTypeError({ + currentValue: arg, + expectedTypes: argumentSpec, + actualType: getType(arg), + }); + } + } + break; + } + } +}; + +export { isTruthy, arityCheck, sliceArray, typeCheck, typeCheckArgument }; From 72bbcba1dce6ff8a0d5a8bd9104958ca5eb6dc1d Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 12 Mar 2024 12:28:14 +0100 Subject: [PATCH 2/9] chore: remove line from comments --- packages/jmespath/src/utils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/jmespath/src/utils.ts b/packages/jmespath/src/utils.ts index 77936fc625..e517c0a5b6 100644 --- a/packages/jmespath/src/utils.ts +++ b/packages/jmespath/src/utils.ts @@ -27,7 +27,6 @@ const isTruthy = (value: unknown): boolean => { }; /** - * @internal * Cap a slice range value to the length of an array, taking into account * negative values and whether the step is negative. * From cbd0c0ea5752758e7b64186336bfa9b4ffe54252 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 12 Mar 2024 15:25:31 +0100 Subject: [PATCH 3/9] chore: reduce complexity --- packages/jmespath/src/utils.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/jmespath/src/utils.ts b/packages/jmespath/src/utils.ts index e517c0a5b6..d164176798 100644 --- a/packages/jmespath/src/utils.ts +++ b/packages/jmespath/src/utils.ts @@ -27,6 +27,7 @@ const isTruthy = (value: unknown): boolean => { }; /** + * @internal * Cap a slice range value to the length of an array, taking into account * negative values and whether the step is negative. * @@ -158,6 +159,15 @@ const typeCheck = ( }); }; +/** + * Predicate function that checks if a type check is needed for an argument. + * + * @param argumentSpec The expected types for an argument + */ +const needsTypeCheck = (argumentSpec: Array): boolean => { + return argumentSpec.length > 0 && argumentSpec[0] !== 'any'; +}; + /** * Type checks an argument against a list of types. * @@ -174,11 +184,11 @@ const typeCheck = ( * passes. If the argument does not match any of the types, then * a JMESPathTypeError is thrown. * - * @param arg - * @param argumentSpec + * @param arg The argument to type check + * @param argumentSpec The expected types for the argument */ const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { - if (argumentSpec.length === 0 || argumentSpec[0] === 'any') { + if (!needsTypeCheck(argumentSpec)) { return; } const entryCount = argumentSpec.length; @@ -199,7 +209,7 @@ const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { if (type.includes('-')) { const arrayItemsType = type.slice(6); let actualType: string | undefined; - for (const element of arg) { + for (const element of arg as Array) { try { typeCheckArgument(element, [arrayItemsType]); actualType = arrayItemsType; From 38b37973ee63270a172f3b21bd44eaabda2d433c Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 12 Mar 2024 16:39:27 +0100 Subject: [PATCH 4/9] chore: decrease complexity --- packages/jmespath/src/Expression.ts | 288 ++++++++++++++++++++++++++-- 1 file changed, 268 insertions(+), 20 deletions(-) diff --git a/packages/jmespath/src/Expression.ts b/packages/jmespath/src/Expression.ts index 94c69ef3c4..35f0df7792 100644 --- a/packages/jmespath/src/Expression.ts +++ b/packages/jmespath/src/Expression.ts @@ -1,28 +1,276 @@ -import type { TreeInterpreter } from './TreeInterpreter.js'; -import type { JSONObject, Node } from './types.js'; +import { + getType, + isIntegerNumber, + isRecord, + isTruthy as isTruthyJS, + isNumber, +} from '@aws-lambda-powertools/commons/typeutils'; +import { Expression } from './Expression.js'; +import { ArityError, JMESPathTypeError, VariadicArityError } from './errors.js'; /** - * Apply a JMESPath expression to a JSON value. + * 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 */ -class Expression { - readonly #expression: Node; - readonly #interpreter: TreeInterpreter; +const isTruthy = (value: unknown): boolean => { + if (isNumber(value)) { + return true; + } else { + return isTruthyJS(value); + } +}; + +/** + * @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: number, + value: number, + isStepNegative: boolean +): number => { + if (value < 0) { + value += arrayLength; + if (value < 0) { + value = isStepNegative ? -1 : 0; + } + } else if (value >= arrayLength) { + value = isStepNegative ? arrayLength - 1 : arrayLength; + } + + return value; +}; + +/** + * 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, +}: { + array: T[]; + start?: number; + end?: number; + step: number; +}): T[] | null => { + const isStepNegative = step < 0; + const length = array.length; + const defaultStart = isStepNegative ? length - 1 : 0; + const defaultEnd = isStepNegative ? -1 : length; + + start = isIntegerNumber(start) + ? capSliceRange(length, start, isStepNegative) + : defaultStart; + + end = isIntegerNumber(end) + ? capSliceRange(length, end, isStepNegative) + : defaultEnd; + + const result: T[] = []; + 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; +}; + +/** + * 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: unknown[], + argumentsSpecs: Array>, + variadic?: boolean +): void => { + if (variadic) { + if (args.length < argumentsSpecs.length) { + throw new VariadicArityError({ + expectedArity: argumentsSpecs.length, + actualArity: args.length, + }); + } + } else if (args.length !== argumentsSpecs.length) { + throw new ArityError({ + expectedArity: argumentsSpecs.length, + actualArity: args.length, + }); + } +}; + +/** + * Type checks the arguments passed to a function against the expected types. + * + * @param args The arguments passed to the function + * @param argumentsSpecs The expected types for each argument + */ +const typeCheck = ( + args: unknown[], + argumentsSpecs: Array> +): void => { + for (const [index, argumentSpec] of argumentsSpecs.entries()) { + if (argumentSpec[0] === 'any') continue; + typeCheckArgument(args[index], argumentSpec); + } +}; + +/** + * Type checks an argument against a list of 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 arg + * @param argumentSpec + */ +const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { + const entryCount = argumentSpec.length; + let hasMoreTypesToCheck = argumentSpec.length > 1; + for (const [index, type] of argumentSpec.entries()) { + hasMoreTypesToCheck = index < entryCount - 1; + if (type.startsWith('array')) { + if (!Array.isArray(arg)) { + if (hasMoreTypesToCheck) { + continue; + } + throw new JMESPathTypeError({ + currentValue: arg, + expectedTypes: argumentSpec, + actualType: getType(arg), + }); + } + if (type.includes('-')) { + checkComplexArrayType(arg, type, hasMoreTypesToCheck); + } + break; + } + if (type === 'expression') { + checkExpressionType(arg, argumentSpec, hasMoreTypesToCheck); + break; + } else if (type === 'string' || type === 'number' || type === 'boolean') { + if (typeof arg !== type) { + if (!hasMoreTypesToCheck) { + throw new JMESPathTypeError({ + currentValue: arg, + expectedTypes: argumentSpec, + actualType: getType(arg), + }); + } + continue; + } + break; + } else if (type === 'object') { + checkObjectType(arg, argumentSpec, hasMoreTypesToCheck); + break; + } + } +}; + +const checkComplexArrayType = ( + arg: unknown[], + type: string, + hasMoreTypesToCheck: boolean +): void => { + const arrayItemsType = type.slice(6); + let actualType: string | undefined; + for (const element of arg) { + try { + typeCheckArgument(element, [arrayItemsType]); + actualType = arrayItemsType; + } catch (error) { + if (!hasMoreTypesToCheck || actualType !== undefined) { + throw error; + } + } + } +}; + +/* const checkBaseType = (arg: unknown, type: string, hasMoreTypesToCheck: boolean): void => { + +} */ - public constructor(expression: Node, interpreter: TreeInterpreter) { - this.#expression = expression; - this.#interpreter = interpreter; +const checkExpressionType = ( + arg: unknown, + type: string[], + hasMoreTypesToCheck: boolean +): void => { + if (!(arg instanceof Expression) && !hasMoreTypesToCheck) { + throw new JMESPathTypeError({ + currentValue: arg, + expectedTypes: type, + actualType: getType(arg), + }); } +}; - /** - * Evaluate the expression against a JSON value. - * - * @param value The JSON value to apply the expression to. - * @param node The node to visit. - * @returns The result of applying the expression to the value. - */ - public visit(value: JSONObject, node?: Node): JSONObject { - return this.#interpreter.visit(node ?? this.#expression, value); +const checkObjectType = ( + arg: unknown, + type: string[], + hasMoreTypesToCheck: boolean +): void => { + if (!isRecord(arg) && !hasMoreTypesToCheck) { + throw new JMESPathTypeError({ + currentValue: arg, + expectedTypes: type, + actualType: getType(arg), + }); } -} +}; -export { Expression }; +export { isTruthy, arityCheck, sliceArray, typeCheck, typeCheckArgument }; From 76da0768db62ee59e45e50d276e86ece0c128c45 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 12 Mar 2024 16:42:17 +0100 Subject: [PATCH 5/9] revert changes to Expression.ts --- packages/jmespath/src/Expression.ts | 288 ++-------------------------- 1 file changed, 20 insertions(+), 268 deletions(-) diff --git a/packages/jmespath/src/Expression.ts b/packages/jmespath/src/Expression.ts index 35f0df7792..94c69ef3c4 100644 --- a/packages/jmespath/src/Expression.ts +++ b/packages/jmespath/src/Expression.ts @@ -1,276 +1,28 @@ -import { - getType, - isIntegerNumber, - isRecord, - isTruthy as isTruthyJS, - isNumber, -} from '@aws-lambda-powertools/commons/typeutils'; -import { Expression } from './Expression.js'; -import { ArityError, JMESPathTypeError, VariadicArityError } from './errors.js'; +import type { TreeInterpreter } from './TreeInterpreter.js'; +import type { JSONObject, Node } from './types.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 + * Apply a JMESPath expression to a JSON value. */ -const isTruthy = (value: unknown): boolean => { - if (isNumber(value)) { - return true; - } else { - return isTruthyJS(value); - } -}; - -/** - * @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: number, - value: number, - isStepNegative: boolean -): number => { - if (value < 0) { - value += arrayLength; - if (value < 0) { - value = isStepNegative ? -1 : 0; - } - } else if (value >= arrayLength) { - value = isStepNegative ? arrayLength - 1 : arrayLength; - } - - return value; -}; - -/** - * 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, -}: { - array: T[]; - start?: number; - end?: number; - step: number; -}): T[] | null => { - const isStepNegative = step < 0; - const length = array.length; - const defaultStart = isStepNegative ? length - 1 : 0; - const defaultEnd = isStepNegative ? -1 : length; - - start = isIntegerNumber(start) - ? capSliceRange(length, start, isStepNegative) - : defaultStart; - - end = isIntegerNumber(end) - ? capSliceRange(length, end, isStepNegative) - : defaultEnd; - - const result: T[] = []; - 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; -}; - -/** - * 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: unknown[], - argumentsSpecs: Array>, - variadic?: boolean -): void => { - if (variadic) { - if (args.length < argumentsSpecs.length) { - throw new VariadicArityError({ - expectedArity: argumentsSpecs.length, - actualArity: args.length, - }); - } - } else if (args.length !== argumentsSpecs.length) { - throw new ArityError({ - expectedArity: argumentsSpecs.length, - actualArity: args.length, - }); - } -}; - -/** - * Type checks the arguments passed to a function against the expected types. - * - * @param args The arguments passed to the function - * @param argumentsSpecs The expected types for each argument - */ -const typeCheck = ( - args: unknown[], - argumentsSpecs: Array> -): void => { - for (const [index, argumentSpec] of argumentsSpecs.entries()) { - if (argumentSpec[0] === 'any') continue; - typeCheckArgument(args[index], argumentSpec); - } -}; - -/** - * Type checks an argument against a list of 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 arg - * @param argumentSpec - */ -const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { - const entryCount = argumentSpec.length; - let hasMoreTypesToCheck = argumentSpec.length > 1; - for (const [index, type] of argumentSpec.entries()) { - hasMoreTypesToCheck = index < entryCount - 1; - if (type.startsWith('array')) { - if (!Array.isArray(arg)) { - if (hasMoreTypesToCheck) { - continue; - } - throw new JMESPathTypeError({ - currentValue: arg, - expectedTypes: argumentSpec, - actualType: getType(arg), - }); - } - if (type.includes('-')) { - checkComplexArrayType(arg, type, hasMoreTypesToCheck); - } - break; - } - if (type === 'expression') { - checkExpressionType(arg, argumentSpec, hasMoreTypesToCheck); - break; - } else if (type === 'string' || type === 'number' || type === 'boolean') { - if (typeof arg !== type) { - if (!hasMoreTypesToCheck) { - throw new JMESPathTypeError({ - currentValue: arg, - expectedTypes: argumentSpec, - actualType: getType(arg), - }); - } - continue; - } - break; - } else if (type === 'object') { - checkObjectType(arg, argumentSpec, hasMoreTypesToCheck); - break; - } - } -}; - -const checkComplexArrayType = ( - arg: unknown[], - type: string, - hasMoreTypesToCheck: boolean -): void => { - const arrayItemsType = type.slice(6); - let actualType: string | undefined; - for (const element of arg) { - try { - typeCheckArgument(element, [arrayItemsType]); - actualType = arrayItemsType; - } catch (error) { - if (!hasMoreTypesToCheck || actualType !== undefined) { - throw error; - } - } - } -}; - -/* const checkBaseType = (arg: unknown, type: string, hasMoreTypesToCheck: boolean): void => { - -} */ +class Expression { + readonly #expression: Node; + readonly #interpreter: TreeInterpreter; -const checkExpressionType = ( - arg: unknown, - type: string[], - hasMoreTypesToCheck: boolean -): void => { - if (!(arg instanceof Expression) && !hasMoreTypesToCheck) { - throw new JMESPathTypeError({ - currentValue: arg, - expectedTypes: type, - actualType: getType(arg), - }); + public constructor(expression: Node, interpreter: TreeInterpreter) { + this.#expression = expression; + this.#interpreter = interpreter; } -}; -const checkObjectType = ( - arg: unknown, - type: string[], - hasMoreTypesToCheck: boolean -): void => { - if (!isRecord(arg) && !hasMoreTypesToCheck) { - throw new JMESPathTypeError({ - currentValue: arg, - expectedTypes: type, - actualType: getType(arg), - }); + /** + * Evaluate the expression against a JSON value. + * + * @param value The JSON value to apply the expression to. + * @param node The node to visit. + * @returns The result of applying the expression to the value. + */ + public visit(value: JSONObject, node?: Node): JSONObject { + return this.#interpreter.visit(node ?? this.#expression, value); } -}; +} -export { isTruthy, arityCheck, sliceArray, typeCheck, typeCheckArgument }; +export { Expression }; From dfb1d3f3c3451657c6461e7211cb96e1acef64f0 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 12 Mar 2024 16:42:37 +0100 Subject: [PATCH 6/9] chore: reduce complexity --- packages/jmespath/src/utils.ts | 105 ++++++++++++++++++--------------- 1 file changed, 59 insertions(+), 46 deletions(-) diff --git a/packages/jmespath/src/utils.ts b/packages/jmespath/src/utils.ts index d164176798..35f0df7792 100644 --- a/packages/jmespath/src/utils.ts +++ b/packages/jmespath/src/utils.ts @@ -154,18 +154,10 @@ const typeCheck = ( args: unknown[], argumentsSpecs: Array> ): void => { - argumentsSpecs.forEach((argumentSpec, index) => { + for (const [index, argumentSpec] of argumentsSpecs.entries()) { + if (argumentSpec[0] === 'any') continue; typeCheckArgument(args[index], argumentSpec); - }); -}; - -/** - * Predicate function that checks if a type check is needed for an argument. - * - * @param argumentSpec The expected types for an argument - */ -const needsTypeCheck = (argumentSpec: Array): boolean => { - return argumentSpec.length > 0 && argumentSpec[0] !== 'any'; + } }; /** @@ -184,13 +176,10 @@ const needsTypeCheck = (argumentSpec: Array): boolean => { * passes. If the argument does not match any of the types, then * a JMESPathTypeError is thrown. * - * @param arg The argument to type check - * @param argumentSpec The expected types for the argument + * @param arg + * @param argumentSpec */ const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { - if (!needsTypeCheck(argumentSpec)) { - return; - } const entryCount = argumentSpec.length; let hasMoreTypesToCheck = argumentSpec.length > 1; for (const [index, type] of argumentSpec.entries()) { @@ -207,31 +196,12 @@ const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { }); } if (type.includes('-')) { - const arrayItemsType = type.slice(6); - let actualType: string | undefined; - for (const element of arg as Array) { - try { - typeCheckArgument(element, [arrayItemsType]); - actualType = arrayItemsType; - } catch (error) { - if (!hasMoreTypesToCheck || actualType !== undefined) { - throw error; - } - } - } + checkComplexArrayType(arg, type, hasMoreTypesToCheck); } break; } if (type === 'expression') { - if (!(arg instanceof Expression)) { - if (!hasMoreTypesToCheck) { - throw new JMESPathTypeError({ - currentValue: arg, - expectedTypes: argumentSpec, - actualType: getType(arg), - }); - } - } + checkExpressionType(arg, argumentSpec, hasMoreTypesToCheck); break; } else if (type === 'string' || type === 'number' || type === 'boolean') { if (typeof arg !== type) { @@ -246,18 +216,61 @@ const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { } break; } else if (type === 'object') { - if (!isRecord(arg)) { - if (index === entryCount - 1) { - throw new JMESPathTypeError({ - currentValue: arg, - expectedTypes: argumentSpec, - actualType: getType(arg), - }); - } - } + checkObjectType(arg, argumentSpec, hasMoreTypesToCheck); break; } } }; +const checkComplexArrayType = ( + arg: unknown[], + type: string, + hasMoreTypesToCheck: boolean +): void => { + const arrayItemsType = type.slice(6); + let actualType: string | undefined; + for (const element of arg) { + try { + typeCheckArgument(element, [arrayItemsType]); + actualType = arrayItemsType; + } catch (error) { + if (!hasMoreTypesToCheck || actualType !== undefined) { + throw error; + } + } + } +}; + +/* const checkBaseType = (arg: unknown, type: string, hasMoreTypesToCheck: boolean): void => { + +} */ + +const checkExpressionType = ( + arg: unknown, + type: string[], + hasMoreTypesToCheck: boolean +): void => { + if (!(arg instanceof Expression) && !hasMoreTypesToCheck) { + throw new JMESPathTypeError({ + currentValue: arg, + expectedTypes: type, + actualType: getType(arg), + }); + } +}; + +const checkObjectType = ( + arg: unknown, + type: string[], + hasMoreTypesToCheck: boolean +): void => { + if (!isRecord(arg) && !hasMoreTypesToCheck) { + throw new JMESPathTypeError({ + currentValue: arg, + expectedTypes: type, + actualType: getType(arg), + }); + } +}; + export { isTruthy, arityCheck, sliceArray, typeCheck, typeCheckArgument }; From 5f3d6516fb2963aa7ee575cfa22a26de5a3e0dd4 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 12 Mar 2024 16:50:25 +0100 Subject: [PATCH 7/9] chore: reduce complexity --- packages/jmespath/src/utils.ts | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/packages/jmespath/src/utils.ts b/packages/jmespath/src/utils.ts index 35f0df7792..e787652bb3 100644 --- a/packages/jmespath/src/utils.ts +++ b/packages/jmespath/src/utils.ts @@ -203,18 +203,17 @@ const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { if (type === 'expression') { checkExpressionType(arg, argumentSpec, hasMoreTypesToCheck); break; - } else if (type === 'string' || type === 'number' || type === 'boolean') { - if (typeof arg !== type) { - if (!hasMoreTypesToCheck) { - throw new JMESPathTypeError({ - currentValue: arg, - expectedTypes: argumentSpec, - actualType: getType(arg), - }); - } - continue; + } else if (['string', 'number', 'boolean'].includes(type)) { + if (typeof arg !== type && !hasMoreTypesToCheck) { + throw new JMESPathTypeError({ + currentValue: arg, + expectedTypes: argumentSpec, + actualType: getType(arg), + }); + } + if (typeof arg === type) { + break; } - break; } else if (type === 'object') { checkObjectType(arg, argumentSpec, hasMoreTypesToCheck); break; @@ -241,10 +240,6 @@ const checkComplexArrayType = ( } }; -/* const checkBaseType = (arg: unknown, type: string, hasMoreTypesToCheck: boolean): void => { - -} */ - const checkExpressionType = ( arg: unknown, type: string[], From 5e80a22ec6f8a2843b24997b78afc4a55bb4b504 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 12 Mar 2024 19:25:14 +0100 Subject: [PATCH 8/9] refactor: reduce code complexity --- packages/jmespath/src/utils.ts | 141 +++++++++++++++++++++++---------- 1 file changed, 98 insertions(+), 43 deletions(-) diff --git a/packages/jmespath/src/utils.ts b/packages/jmespath/src/utils.ts index e787652bb3..567ec0bac5 100644 --- a/packages/jmespath/src/utils.ts +++ b/packages/jmespath/src/utils.ts @@ -147,6 +147,19 @@ const 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 */ @@ -163,13 +176,6 @@ const typeCheck = ( /** * Type checks an argument against a list of 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 @@ -180,52 +186,87 @@ const typeCheck = ( * @param argumentSpec */ const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { - const entryCount = argumentSpec.length; - let hasMoreTypesToCheck = argumentSpec.length > 1; - for (const [index, type] of argumentSpec.entries()) { - hasMoreTypesToCheck = index < entryCount - 1; - if (type.startsWith('array')) { - if (!Array.isArray(arg)) { - if (hasMoreTypesToCheck) { - continue; - } - throw new JMESPathTypeError({ - currentValue: arg, - expectedTypes: argumentSpec, - actualType: getType(arg), - }); - } - if (type.includes('-')) { - checkComplexArrayType(arg, type, hasMoreTypesToCheck); - } - break; - } - if (type === 'expression') { - checkExpressionType(arg, argumentSpec, hasMoreTypesToCheck); - break; - } else if (['string', 'number', 'boolean'].includes(type)) { - if (typeof arg !== type && !hasMoreTypesToCheck) { - throw new JMESPathTypeError({ - currentValue: arg, - expectedTypes: argumentSpec, - actualType: getType(arg), - }); - } - if (typeof arg === type) { - break; + let valid = false; + argumentSpec.forEach((type, index) => { + if (valid) return; + valid = check(arg, type, index, argumentSpec); + }); +}; + +const check = ( + arg: unknown, + type: string, + index: number, + argumentSpec: string[] +): boolean => { + const hasMoreTypesToCheck = index < argumentSpec.length - 1; + if (type.startsWith('array')) { + if (!Array.isArray(arg)) { + if (hasMoreTypesToCheck) { + return false; } - } else if (type === 'object') { - checkObjectType(arg, argumentSpec, hasMoreTypesToCheck); - break; + throw new JMESPathTypeError({ + currentValue: arg, + expectedTypes: argumentSpec, + actualType: getType(arg), + }); } + checkComplexArrayType(arg, type, hasMoreTypesToCheck); + + return true; + } + if (type === 'expression') { + checkExpressionType(arg, argumentSpec, hasMoreTypesToCheck); + + return true; + } else if (['string', 'number', 'boolean'].includes(type)) { + typeCheckType(arg, type, argumentSpec, hasMoreTypesToCheck); + 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: unknown, + type: string, + argumentSpec: string[], + hasMoreTypesToCheck: boolean +): void => { + if (typeof arg !== type && !hasMoreTypesToCheck) { + throw new JMESPathTypeError({ + currentValue: arg, + expectedTypes: argumentSpec, + actualType: 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: unknown[], type: string, hasMoreTypesToCheck: boolean ): void => { + if (!type.includes('-')) return; const arrayItemsType = type.slice(6); let actualType: string | undefined; for (const element of arg) { @@ -240,6 +281,13 @@ const checkComplexArrayType = ( } }; +/** + * 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: unknown, type: string[], @@ -254,6 +302,13 @@ const checkExpressionType = ( } }; +/** + * 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: unknown, type: string[], From 4944a8d44e72ff716a981703f0fd9dcd7d205096 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 12 Mar 2024 19:26:25 +0100 Subject: [PATCH 9/9] docs: add missing jsdoc --- packages/jmespath/src/utils.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/jmespath/src/utils.ts b/packages/jmespath/src/utils.ts index 567ec0bac5..fe115f3098 100644 --- a/packages/jmespath/src/utils.ts +++ b/packages/jmespath/src/utils.ts @@ -189,11 +189,19 @@ const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { let valid = false; argumentSpec.forEach((type, index) => { if (valid) return; - valid = check(arg, type, index, argumentSpec); + valid = checkIfArgumentTypeIsValid(arg, type, index, argumentSpec); }); }; -const check = ( +/** + * 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: unknown, type: string, index: number,