-
Notifications
You must be signed in to change notification settings - Fork 154
/
Copy pathutils.ts
337 lines (316 loc) · 10.3 KB
/
utils.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
import {
getType,
isIntegerNumber,
isNumber,
isRecord,
isTruthy as isTruthyJS,
} 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;
}
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 => {
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 = <T>({
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<Array<string>>,
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.
*
* 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: unknown[],
argumentsSpecs: Array<Array<string>>
): 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.
*
* 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<string>): void => {
let valid = false;
argumentSpec.forEach((type, index) => {
if (valid) return;
valid = checkIfArgumentTypeIsValid(arg, type, index, argumentSpec);
});
};
/**
* 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,
argumentSpec: string[]
): boolean => {
const hasMoreTypesToCheck = index < argumentSpec.length - 1;
if (type.startsWith('array')) {
if (!Array.isArray(arg)) {
if (hasMoreTypesToCheck) {
return false;
}
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;
}
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: unknown,
type: string,
argumentSpec: string[],
hasMoreTypesToCheck: boolean
): void => {
// 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 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) {
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: unknown,
type: string[],
hasMoreTypesToCheck: boolean
): void => {
if (!(arg instanceof Expression) && !hasMoreTypesToCheck) {
throw new JMESPathTypeError({
currentValue: arg,
expectedTypes: type,
actualType: 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: 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 };