From 81609cc42e9fde9bc92c4b097d5eb6a0c0202c6a Mon Sep 17 00:00:00 2001 From: Simon Fan Date: Wed, 10 Mar 2021 05:53:42 -0300 Subject: [PATCH] feat: add support for async/sync interpreters on same context This commit refactors the whole codebase to better support sync/async execution. Some important changes: - add support for nested param resolution through @orioro/typing typeSpec utilities - remove function and null ParamResolvers in favor of aforementioned typeSpecs - separate interpreter functions into sub-modules (withtin dir src/interpreter) - fix isExpression method to check whether the interpreterId is actually a string before attempting to get the interpterter function (previous versions were unintentionally using JS type casting which transformed the array `[['$$value']]` into a string `'$$value'`) - modify interpreterList format to support sync/async evaluation on the same context object allowing expression interpreters to evaluate synchronoushly even if they were called in async mode (introduced to support Array.prototype.sort(customComparatorExp)) BREAKING CHANGE: `context.interpreters` format modified --- .gitignore | 1 + README.md | 20 +- TODO.md | 15 +- package.json | 4 +- spec/specUtil.ts | 64 +++ src/__snapshots__/index.spec.ts.snap | 149 +++--- src/async.spec.ts | 29 +- src/errors.ts | 29 ++ src/evaluate.spec.ts | 9 +- src/evaluate.ts | 61 ++- src/expressions/array.spec.ts | 575 ++++++++------------- src/expressions/array.ts | 219 ++++++-- src/expressions/boolean.spec.ts | 71 +-- src/expressions/boolean.ts | 4 +- src/expressions/comparison.spec.ts | 186 +++---- src/expressions/comparison.ts | 56 +- src/expressions/functional.spec.ts | 33 +- src/expressions/functional.ts | 8 +- src/expressions/logical.spec.ts | 683 ++++++++++--------------- src/expressions/logical.ts | 68 +-- src/expressions/math.spec.ts | 140 ++--- src/expressions/math.ts | 26 +- src/expressions/number.spec.ts | 79 +-- src/expressions/number.ts | 6 +- src/expressions/object.spec.ts | 538 +++++++++---------- src/expressions/object.ts | 126 ++--- src/expressions/object/objectFormat.ts | 88 ++++ src/expressions/string.spec.ts | 223 +++----- src/expressions/string.ts | 24 +- src/expressions/type.spec.ts | 121 ++--- src/expressions/type.ts | 8 +- src/expressions/value.spec.ts | 98 ++-- src/expressions/value.ts | 31 +- src/index.spec.ts | 2 +- src/index.ts | 6 +- src/interpreter.ts | 148 ------ src/interpreter/asyncInterpreter.ts | 34 ++ src/interpreter/asyncParamResolver.ts | 111 ++++ src/interpreter/interpreter.spec.ts | 203 ++++++++ src/interpreter/interpreter.ts | 79 +++ src/interpreter/syncInterpreter.ts | 51 ++ src/interpreter/syncParamResolver.ts | 117 +++++ src/types.ts | 63 ++- src/util/promiseResolveObject.ts | 24 + tsconfig.json | 3 +- yarn.lock | 261 +++++++++- 46 files changed, 2654 insertions(+), 2240 deletions(-) create mode 100644 spec/specUtil.ts create mode 100644 src/errors.ts create mode 100644 src/expressions/object/objectFormat.ts delete mode 100644 src/interpreter.ts create mode 100644 src/interpreter/asyncInterpreter.ts create mode 100644 src/interpreter/asyncParamResolver.ts create mode 100644 src/interpreter/interpreter.spec.ts create mode 100644 src/interpreter/interpreter.ts create mode 100644 src/interpreter/syncInterpreter.ts create mode 100644 src/interpreter/syncParamResolver.ts create mode 100644 src/util/promiseResolveObject.ts diff --git a/.gitignore b/.gitignore index 8fd6552..63922d3 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ node_modules dist coverage tmp +yarn-error.log diff --git a/README.md b/README.md index 5d9a8a9..1fda70b 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ TODO - [`$arrayLength(array)`](#arraylengtharray) - [`$arrayReduce(reduceExp, start, array)`](#arrayreducereduceexp-start-array) - [`$arrayMap(mapExp, array)`](#arraymapmapexp-array) -- [`$arrayEvery(everyExp, array)`](#arrayeveryeveryexp-array) +- [`$arrayEvery(testExp, array)`](#arrayeverytestexp-array) - [`$arraySome(someExp, array)`](#arraysomesomeexp-array) - [`$arrayFilter(queryExp, array)`](#arrayfilterqueryexp-array) - [`$arrayFindIndex(queryExp, array)`](#arrayfindindexqueryexp-array) @@ -169,7 +169,7 @@ any of the searched values is in the array. - `mapExp` {[Expression](#expression)} - `array` {Array} -##### `$arrayEvery(everyExp, array)` +##### `$arrayEvery(testExp, array)` `Array.prototype.every` @@ -178,7 +178,7 @@ Result is similar to logical operator `$and`. Main difference $arrayEvery exposes array iteration variables: `$$PARENT_SCOPE`, `$$VALUE`, `$$INDEX`, `$$ARRAY` -- `everyExp` {[Expression](#expression)} +- `testExp` {[Expression](#expression)} - `array` {Array} ##### `$arraySome(someExp, array)` @@ -727,14 +727,14 @@ through Catastrophic backtracking, for future study and reference: ## Value -- [`$value(pathExp, defaultExp)`](#valuepathexp-defaultexp) +- [`$value(path, defaultExp)`](#valuepath-defaultexp) - [`$literal(value)`](#literalvalue) -- [`$evaluate(expExp, scopeExp)`](#evaluateexpexp-scopeexp) +- [`$evaluate(expression, scope)`](#evaluateexpression-scope) -##### `$value(pathExp, defaultExp)` +##### `$value(path, defaultExp)` -- `pathExp` {String} +- `path` {String} - `defaultExp` {*} - Returns: `value` {*} @@ -743,8 +743,8 @@ through Catastrophic backtracking, for future study and reference: - `value` {*} - Returns: {*} -##### `$evaluate(expExp, scopeExp)` +##### `$evaluate(expression, scope)` -- `expExp` {[Expression](#expression)} -- `scopeExp` {Object | null} +- `expression` {[Expression](#expression)} +- `scope` {Object} - Returns: {*} diff --git a/TODO.md b/TODO.md index 8b13789..84d8c33 100644 --- a/TODO.md +++ b/TODO.md @@ -1 +1,14 @@ - +- $and | Add 'strict' mode option (src/expressions/logical.ts) +- array | $arrayEvery write tests with async conditions (src/expressions/array.ts) +- array | $arraySome write tests with async conditions (src/expressions/array.ts) +- array | Make it possible for the same set of expression interpreters + to be called synchronously or asynchronously. E.g. sort comparator + expression should only support Synchronous (src/expressions/array.ts) +- asyncParamResolver | ONE_OF_TYPES: handle complex cases, e.g. + oneOfTypes(['string', objectType({ key1: 'string', key2: 'number '})]) (src/interpreter/asyncParamResolver.ts) +- logical | Write test to ensure delayed evaluation (src/expressions/logical.ts) +- syncInterpreter | Handle nested object param typeSpec (src/interpreter/syncParamResolver.ts) +- syncInterpreter | Study better ways at validating evlauation results for + tupleType and indefiniteArrayOfType. Currently validation is highly redundant. (src/interpreter/syncParamResolver.ts) +- syncInterpreter | Update Interpreter type: remove function (src/interpreter/syncInterpreter.ts) +- value | Consider adding 'expression' type (src/expressions/value.ts) diff --git a/package.json b/package.json index 6d93fd5..4d855af 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "@babel/core": "^7.11.5", "@babel/preset-env": "^7.11.5", "@babel/preset-typescript": "^7.10.4", - "@orioro/jest-util": "^1.3.0", + "@orioro/jest-util": "^1.5.0", "@orioro/readme": "^1.0.1", "@rollup/plugin-babel": "^5.2.0", "@rollup/plugin-commonjs": "^15.0.0", @@ -64,7 +64,7 @@ "typescript": "^4.0.2" }, "dependencies": { - "@orioro/typing": "^3.0.0", + "@orioro/typing": "^4.2.0", "lodash": "^4.17.20" }, "config": { diff --git a/spec/specUtil.ts b/spec/specUtil.ts new file mode 100644 index 0000000..0b2a121 --- /dev/null +++ b/spec/specUtil.ts @@ -0,0 +1,64 @@ +import { evaluate, evaluateAsync } from '../src/evaluate' +import { interpreterList } from '../src/interpreter/interpreter' +import { + testCases, + asyncResult, + valueLabel, + resultLabel, + variableName, + VariableName, +} from '@orioro/jest-util' + +const _evLabel = ([value, expression], result) => + `${valueLabel(value)} | ${valueLabel(expression)} -> ${resultLabel(result)}` + +export const _prepareEvaluateTestCases = (interpreterSpecs) => { + const interpreters = interpreterList(interpreterSpecs) + + const testSyncCases = (cases) => { + testCases( + cases, + (value, expression) => + evaluate( + { + interpreters, + scope: { $$VALUE: value }, + }, + expression + ), + ([value, expression], result) => + `sync - ${_evLabel([value, expression], result)}` + ) + } + + const testAsyncCases = cases => { + testCases( + cases.map((_case) => { + const result = _case[_case.length - 1] + const args = _case.slice(0, -1) + + return [...args, asyncResult(result)] + }), + (value, expression) => + evaluateAsync( + { + interpreters, + scope: { $$VALUE: value }, + }, + expression + ), + ([value, expression], result) => + `async - ${_evLabel([value, expression], result)}` + ) + } + + const testSyncAndAsync = (cases) => { + testSyncCases(cases) + testAsyncCases(cases) + } + + testSyncAndAsync.testSyncCases = testSyncCases + testSyncAndAsync.testAsyncCases = testAsyncCases + + return testSyncAndAsync +} diff --git a/src/__snapshots__/index.spec.ts.snap b/src/__snapshots__/index.spec.ts.snap index 65a4af0..66cf65e 100644 --- a/src/__snapshots__/index.spec.ts.snap +++ b/src/__snapshots__/index.spec.ts.snap @@ -2,113 +2,118 @@ exports[`public api 1`] = ` Array [ - "ALL_EXPRESSIONS", - "$$INDEX", - "$$ARRAY", "$$ACC", + "$$ARRAY", + "$$INDEX", "$$SORT_A", "$$SORT_B", + "$$VALUE", + "$and", + "$arrayAddAt", + "$arrayAt", + "$arrayEvery", + "$arrayFilter", + "$arrayFilterAsyncParallel", + "$arrayFilterAsyncSerial", + "$arrayFind", + "$arrayFindIndex", "$arrayIncludes", "$arrayIncludesAll", "$arrayIncludesAny", + "$arrayIndexOf", + "$arrayJoin", "$arrayLength", - "$arrayReduce", "$arrayMap", - "$arrayEvery", - "$arraySome", - "$arrayFilter", - "$arrayFindIndex", - "$arrayIndexOf", - "$arrayFind", - "$arrayReverse", - "$arraySort", - "$arrayPush", "$arrayPop", - "$arrayUnshift", + "$arrayPush", + "$arrayReduce", + "$arrayRemoveAt", + "$arrayReplace", + "$arrayReverse", "$arrayShift", "$arraySlice", - "$arrayReplace", - "$arrayAddAt", - "$arrayRemoveAt", - "$arrayJoin", - "$arrayAt", - "ARRAY_EXPRESSIONS", + "$arraySome", + "$arraySort", + "$arrayUnshift", "$boolean", - "BOOLEAN_EXPRESSIONS", "$eq", - "$notEq", - "$in", - "$notIn", + "$evaluate", "$gt", "$gte", + "$if", + "$in", + "$isType", + "$literal", "$lt", "$lte", "$matches", - "COMPARISON_EXPRESSIONS", - "$pipe", - "FUNCTIONAL_EXPRESSIONS", - "$and", - "$or", - "$not", - "$nor", - "$xor", - "$if", - "$switch", - "$switchKey", - "LOGICAL_EXPRESSIONS", - "$mathSum", - "$mathSub", - "$mathMult", - "$mathDiv", - "$mathMod", - "$mathPow", "$mathAbs", + "$mathCeil", + "$mathDiv", + "$mathFloor", "$mathMax", "$mathMin", + "$mathMod", + "$mathMult", + "$mathPow", "$mathRound", - "$mathFloor", - "$mathCeil", - "MATH_EXPRESSIONS", - "$numberInt", + "$mathSub", + "$mathSum", + "$nor", + "$not", + "$notEq", + "$notIn", "$numberFloat", - "NUMBER_EXPRESSIONS", - "$objectMatches", - "$objectFormat", - "$objectDefaults", + "$numberInt", "$objectAssign", + "$objectDefaults", + "$objectFormat", "$objectKeys", - "OBJECT_EXPRESSIONS", + "$objectMatches", + "$or", + "$pipe", "$string", - "$stringStartsWith", - "$stringLength", - "$stringSubstr", "$stringConcat", - "$stringTrim", - "$stringPadStart", + "$stringInterpolate", + "$stringLength", "$stringPadEnd", - "$stringToUpperCase", + "$stringPadStart", + "$stringStartsWith", + "$stringSubstr", "$stringToLowerCase", - "$stringInterpolate", - "STRING_EXPRESSIONS", - "CORE_TYPES", - "typeExpressions", + "$stringToUpperCase", + "$stringTrim", + "$switch", + "$switchKey", "$type", - "$isType", - "TYPE_EXPRESSIONS", - "$$VALUE", "$value", - "$literal", - "$evaluate", + "$xor", + "ALL_EXPRESSIONS", + "ARRAY_EXPRESSIONS", + "BOOLEAN_EXPRESSIONS", + "COMPARISON_EXPRESSIONS", + "CORE_TYPES", + "FUNCTIONAL_EXPRESSIONS", + "LOGICAL_EXPRESSIONS", + "MATH_EXPRESSIONS", + "NUMBER_EXPRESSIONS", + "OBJECT_EXPRESSIONS", + "STRING_EXPRESSIONS", + "TYPE_EXPRESSIONS", "VALUE_EXPRESSIONS", - "TypeAlternatives", - "TypeMap", - "isExpression", + "asyncInterpreter", + "asyncParamResolver", "evaluate", + "evaluateAsync", + "evaluateSync", "evaluateTyped", "evaluateTypedAsync", + "evaluateTypedSync", + "interpreter", + "interpreterList", + "isExpression", "syncInterpreter", - "syncInterpreterList", - "asyncInterpreter", - "asyncInterpreterList", + "syncParamResolver", + "typeExpressions", ] `; diff --git a/src/async.spec.ts b/src/async.spec.ts index f899c3a..676fbeb 100644 --- a/src/async.spec.ts +++ b/src/async.spec.ts @@ -3,17 +3,22 @@ import { testCases, asyncResult } from '@orioro/jest-util' import { ALL_EXPRESSIONS } from './' -import { asyncInterpreterList } from './interpreter' +import { interpreterList } from './interpreter/interpreter' -import { evaluate } from './evaluate' +import { evaluateAsync } from './evaluate' + +import { InterpreterSpec } from './types' const wait = (ms, result) => new Promise((resolve) => setTimeout(resolve.bind(null, result), ms)) -const $asyncLoadStr = [() => wait(100, 'async-str'), []] -const $asyncLoadNum = [() => wait(100, 9), []] -const $asyncLoadArr = [() => wait(100, ['str-1', 'str-2', 'str-3']), []] -const $asyncLoadObj = [ +const $asyncLoadStr: InterpreterSpec = [() => wait(100, 'async-str'), []] +const $asyncLoadNum: InterpreterSpec = [() => wait(100, 9), []] +const $asyncLoadArr: InterpreterSpec = [ + () => wait(100, ['str-1', 'str-2', 'str-3']), + [], +] +const $asyncLoadObj: InterpreterSpec = [ () => wait(100, { key1: 'value1', @@ -21,10 +26,10 @@ const $asyncLoadObj = [ }), [], ] -const $asyncLoadTrue = [() => wait(100, true), []] -const $asyncLoadFalse = [() => wait(100, false), []] +const $asyncLoadTrue: InterpreterSpec = [() => wait(100, true), []] +const $asyncLoadFalse: InterpreterSpec = [() => wait(100, false), []] -const interpreters = asyncInterpreterList({ +const interpreters = interpreterList({ ...ALL_EXPRESSIONS, $asyncLoadStr, $asyncLoadNum, @@ -45,14 +50,14 @@ describe('async - immediate async expression', () => { ['$asyncLoadFalse', asyncResult(false)], ], (expression) => - evaluate({ interpreters, scope: { $$VALUE: null } }, [expression]) + evaluateAsync({ interpreters, scope: { $$VALUE: null } }, [expression]) ) }) describe('async - nested async expression', () => { test('simple scenario - string concat', () => { return expect( - evaluate( + evaluateAsync( { interpreters, scope: { @@ -68,7 +73,7 @@ describe('async - nested async expression', () => { describe('async - syncronous expressions only get converted to async as well', () => { test('simple scenario - string concat', () => { return expect( - evaluate( + evaluateAsync( { interpreters, scope: { diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..84347f5 --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,29 @@ +export class EvaluationError extends Error {} + +export class AsyncModeUnsupportedError extends EvaluationError { + constructor(interpreterId: string, message?: string) { + super( + message + ? message + : `Interpreter \`${interpreterId}\` does not support ASYNC mode` + ) + + this.interpreterId = interpreterId + } + + interpreterId: string +} + +export class SyncModeUnsupportedError extends EvaluationError { + constructor(interpreterId: string, message?: string) { + super( + message + ? message + : `Interpreter \`${interpreterId}\` does not support SYNC mode` + ) + + this.interpreterId = interpreterId + } + + interpreterId: string +} diff --git a/src/evaluate.spec.ts b/src/evaluate.spec.ts index 0a8c9dc..f5d1861 100644 --- a/src/evaluate.spec.ts +++ b/src/evaluate.spec.ts @@ -1,4 +1,5 @@ import { evaluateTyped } from './evaluate' +import { interpreterList } from './interpreter/interpreter' describe('evaluateTyped(expectedTypes, context, value)', () => { test('simple type - example: number', () => { @@ -57,9 +58,9 @@ describe('evaluateTyped(expectedTypes, context, value)', () => { evaluateTyped( 'array', { - interpreters: { + interpreters: interpreterList({ $someExpression: () => 'text', - }, + }), scope: { $$VALUE: 'aa' }, }, ['$someExpression'] @@ -71,9 +72,9 @@ describe('evaluateTyped(expectedTypes, context, value)', () => { evaluateTyped( 'array', { - interpreters: { + interpreters: interpreterList({ $someExpression: () => ['item-1', 'item-2'], - }, + }), scope: { $$VALUE: 'aa' }, }, ['$someExpression'] diff --git a/src/evaluate.ts b/src/evaluate.ts index ce42bf5..543ed84 100644 --- a/src/evaluate.ts +++ b/src/evaluate.ts @@ -1,21 +1,18 @@ -import { validateType } from '@orioro/typing' +import { TypeSpec, validateType } from '@orioro/typing' -import { - Expression, - ExpressionInterpreterList, - EvaluationContext, -} from './types' +import { Expression, InterpreterList, EvaluationContext } from './types' /** * @function isExpression - * @param {ExpressionInterpreterList} + * @param {InterpreterList} */ export const isExpression = ( - interpreters: ExpressionInterpreterList, + interpreters: InterpreterList, candidateExpression: any // eslint-disable-line @typescript-eslint/explicit-module-boundary-types ): boolean => Array.isArray(candidateExpression) && - typeof interpreters[candidateExpression[0]] === 'function' + typeof candidateExpression[0] === 'string' && + typeof interpreters[candidateExpression[0]] === 'object' const _maybeExpression = (value) => Array.isArray(value) && @@ -52,7 +49,9 @@ const _evaluate = ( } const [interpreterId, ...interpreterArgs] = expOrValue - const interpreter = context.interpreters[interpreterId] + const interpreter = context.async + ? context.interpreters[interpreterId].async + : context.interpreters[interpreterId].sync return interpreter(context, ...interpreterArgs) } @@ -68,6 +67,32 @@ export const evaluate = ? _evaluateDev : _evaluate +export const evaluateSync = ( + context: EvaluationContext, + expOrValue: Expression | any +): any => + evaluate( + { + ...context, + async: false, + }, + expOrValue + ) + +export const evaluateAsync = ( + context: EvaluationContext, + expOrValue: Expression | any +): Promise => + Promise.resolve( + evaluate( + { + ...context, + async: true, + }, + expOrValue + ) + ) + /** * @function evaluateTyped * @param {String | string[]} expectedTypes @@ -76,7 +101,7 @@ export const evaluate = * @returns {*} */ export const evaluateTyped = ( - expectedTypes: string | string[], + expectedTypes: TypeSpec, context: EvaluationContext, expOrValue: Expression | any ): any => { @@ -85,6 +110,16 @@ export const evaluateTyped = ( return value } +export const evaluateTypedSync = ( + expectedTypes: TypeSpec, + context: EvaluationContext, + expOrValue: Expression | any +): any => { + const value = evaluate({ ...context, async: false }, expOrValue) + validateType(expectedTypes, value) + return value +} + /** * @function evaluateTypedAsync * @param {String | string[]} expectedTypes @@ -93,11 +128,11 @@ export const evaluateTyped = ( * @returns {Promise<*>} */ export const evaluateTypedAsync = ( - expectedTypes: string | string[], + expectedTypes: TypeSpec, context: EvaluationContext, expOrValue: Expression | any ): Promise => - Promise.resolve(evaluate(context, expOrValue)).then((value) => { + evaluateAsync(context, expOrValue).then((value) => { validateType(expectedTypes, value) return value }) diff --git a/src/expressions/array.spec.ts b/src/expressions/array.spec.ts index 27ce5aa..c35f5ba 100644 --- a/src/expressions/array.spec.ts +++ b/src/expressions/array.spec.ts @@ -1,5 +1,3 @@ -import { evaluate } from '../evaluate' -import { syncInterpreterList } from '../interpreter' import { VALUE_EXPRESSIONS } from './value' import { COMPARISON_EXPRESSIONS } from './comparison' import { LOGICAL_EXPRESSIONS } from './logical' @@ -9,7 +7,9 @@ import { STRING_EXPRESSIONS } from './string' import { MATH_EXPRESSIONS } from './math' import { NUMBER_EXPRESSIONS } from './number' -const interpreters = syncInterpreterList({ +import { _prepareEvaluateTestCases } from '../../spec/specUtil' + +const EXPRESSIONS = { ...VALUE_EXPRESSIONS, ...LOGICAL_EXPRESSIONS, ...NUMBER_EXPRESSIONS, @@ -18,133 +18,93 @@ const interpreters = syncInterpreterList({ ...OBJECT_EXPRESSIONS, ...STRING_EXPRESSIONS, ...ARRAY_EXPRESSIONS, -}) +} -describe('$arrayIncludes', () => { - test('basic usage', () => { - const context = { - interpreters, - scope: { - $$VALUE: ['A', 'B', 'C', 'D'], - }, - } +const _evTestCases = _prepareEvaluateTestCases(EXPRESSIONS) - expect(evaluate(context, ['$arrayIncludes', 'A'])).toEqual(true) - expect(evaluate(context, ['$arrayIncludes', 'Z'])).toEqual(false) - }) +describe('$arrayIncludes', () => { + _evTestCases([ + [['A', 'B', 'C', 'D'], ['$arrayIncludes', 'A'], true], + [['A', 'B', 'C', 'D'], ['$arrayIncludes', 'Z'], false], + ]) }) describe('$arrayIncludesAll', () => { - test('basic usage', () => { - const context = { - interpreters, - scope: { - $$VALUE: ['A', 'B', 'C', 'D'], - }, - } - - expect(evaluate(context, ['$arrayIncludesAll', ['A', 'B']])).toEqual(true) - expect(evaluate(context, ['$arrayIncludesAll', ['A', 'Z']])).toEqual(false) - }) + _evTestCases([ + [['A', 'B', 'C', 'D'], ['$arrayIncludesAll', ['A', 'B']], true], + [['A', 'B', 'C', 'D'], ['$arrayIncludesAll', ['A', 'Z']], false], + ]) }) describe('$arrayIncludesAny', () => { - test('basic usage', () => { - const context = { - interpreters, - scope: { - $$VALUE: ['A', 'B', 'C', 'D'], - }, - } - - expect(evaluate(context, ['$arrayIncludesAny', ['A', 'B']])).toEqual(true) - expect(evaluate(context, ['$arrayIncludesAny', ['A', 'Z']])).toEqual(true) - expect(evaluate(context, ['$arrayIncludesAny', ['X', 'Y', 'Z']])).toEqual( - false - ) - }) + _evTestCases([ + [['A', 'B', 'C', 'D'], ['$arrayIncludesAny', ['A', 'B']], true], + [['A', 'B', 'C', 'D'], ['$arrayIncludesAny', ['A', 'Z']], true], + [['A', 'B', 'C', 'D'], ['$arrayIncludesAny', ['X', 'Y', 'Z']], false], + ]) }) describe('$arrayLength', () => { - test('basic usage', () => { - const context = { - interpreters, - scope: { - $$VALUE: ['A', 'B', 'C', 'D'], - }, - } - - expect(evaluate(context, ['$arrayLength'])).toEqual(4) - }) + _evTestCases([ + [['A', 'B', 'C', 'D'], ['$arrayLength'], 4], + [['A', 'B', 'C', 'D', 'E'], ['$arrayLength'], 5], + [[], ['$arrayLength'], 0], + ]) }) describe('$arrayMap', () => { - test('basic usage', () => { - const context = { - interpreters, - scope: { - $$VALUE: [-10, 0, 10, 20], - }, - } - - expect(evaluate(context, ['$arrayMap', 'SOME_VALUE'])).toEqual([ - 'SOME_VALUE', - 'SOME_VALUE', - 'SOME_VALUE', - 'SOME_VALUE', - ]) - - expect(evaluate(context, ['$arrayMap', ['$mathSum', 5]])).toEqual([ - -5, - 5, - 15, - 25, + describe('basic usage', () => { + _evTestCases([ + [ + [-10, 0, 10, 20], + ['$arrayMap', 'SOME_VALUE'], + ['SOME_VALUE', 'SOME_VALUE', 'SOME_VALUE', 'SOME_VALUE'], + ], + [ + [-10, 0, 10, 20], + ['$arrayMap', ['$mathSum', 5]], + [-5, 5, 15, 25], + ], ]) }) - test('[$value, $$ARRAY]', () => { - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: [-10, 0, 10, 20] }, - }, - ['$arrayMap', ['$mathSum', ['$arrayLength', ['$value', '$$ARRAY']]]] - ) - ).toEqual([-6, 4, 14, 24]) + describe('[$value, $$ARRAY]', () => { + _evTestCases([ + [ + [-10, 0, 10, 20], + ['$arrayMap', ['$mathSum', ['$arrayLength', ['$value', '$$ARRAY']]]], + [-6, 4, 14, 24], + ], + ]) }) - test('[$value, $$INDEX]', () => { - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: [-10, 0, 10, 20] }, - }, - ['$arrayMap', ['$mathSum', ['$value', '$$INDEX']]] - ) - ).toEqual([-10, 1, 12, 23]) + describe('[$value, $$INDEX]', () => { + _evTestCases([ + [ + [-10, 0, 10, 20], + ['$arrayMap', ['$mathSum', ['$value', '$$INDEX']]], + [-10, 1, 12, 23], + ], + ]) }) }) describe('$arrayFilter', () => { - test('testing against parent scope value', () => { - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: 2 }, - }, + describe('testing against parent scope value', () => { + _evTestCases([ + [ + 2, [ '$arrayFilter', ['$eq', 0, ['$mathMod', ['$value', '$$PARENT_SCOPE.$$VALUE']]], [1, 2, 3, 4, 5, 6], - ] - ) - ).toEqual([2, 4, 6]) + ], + [2, 4, 6], + ], + ]) }) - test('with meta evaluation', () => { + describe('with meta evaluation', () => { const OUT_OF_RANGE_COND = [ '$and', [ @@ -168,300 +128,214 @@ describe('$arrayFilter', () => { [NOT_EVEN_COND, NOT_EVEN_ERR], ] - const check = (value) => - evaluate( - { - interpreters, - scope: { $$VALUE: value }, - }, + const EXPRESSION = [ + '$arrayFilter', + ['$notEq', null], + [ + '$arrayMap', [ - '$arrayFilter', - ['$notEq', null], - [ - '$arrayMap', - [ - '$if', - ['$evaluate', ['$value', '0'], ['$value', '$$PARENT_SCOPE']], - null, - ['$value', '1'], - ], - CASES, - ], - ] - ) - - expect(check(9)).toEqual([NOT_EVEN_ERR]) - expect(check(10)).toEqual([]) - expect(check(11)).toEqual([OUT_OF_RANGE_ERR, NOT_EVEN_ERR]) - }) -}) - -describe('$arrayIndexOf vs $arrayFindIndex', () => { - test('$arrayIndexOf', () => { - const context = { - interpreters, - scope: { $$VALUE: [0, 10, 20, 40] }, - } + '$if', + ['$evaluate', ['$value', '0'], ['$value', '$$PARENT_SCOPE']], + null, + ['$value', '1'], + ], + CASES, + ], + ] - expect(evaluate(context, ['$arrayIndexOf', 20])).toEqual(2) + _evTestCases([ + [9, EXPRESSION, [NOT_EVEN_ERR]], + [10, EXPRESSION, []], + [11, EXPRESSION, [OUT_OF_RANGE_ERR, NOT_EVEN_ERR]], + ]) }) +}) - test('$arrayFindIndex', () => { - const context = { - interpreters, - scope: { $$VALUE: [0, 10, 20, 40] }, - } +describe('$arrayIndexOf', () => { + _evTestCases([[[0, 10, 20, 40], ['$arrayIndexOf', 20], 2]]) +}) - expect(evaluate(context, ['$arrayFindIndex', ['$eq', 20]])).toEqual(2) - }) +describe('$arrayFindIndex', () => { + _evTestCases([[[0, 10, 20, 40], ['$arrayFindIndex', ['$eq', 20]], 2]]) }) describe('$arrayReduce', () => { - test('basic usage', () => { - const context = { - interpreters, - scope: { $$VALUE: [0, 10, 20, 40] }, - } - - expect( - evaluate(context, ['$arrayReduce', ['$mathSum', ['$value', '$$ACC']], 0]) - ).toEqual(70) - }) + _evTestCases([ + [ + [0, 10, 20, 40], + ['$arrayReduce', ['$mathSum', ['$value', '$$ACC']], 0], + 70, + ], + ]) }) describe('$arrayReverse', () => { - test('basic usage', () => { - const context = { - interpreters, - scope: { $$VALUE: ['A', 'B', 'C', 'D'] }, - } - - expect(evaluate(context, ['$arrayReverse'])).toEqual(['D', 'C', 'B', 'A']) - }) + _evTestCases([ + [['A', 'B', 'C', 'D'], ['$arrayReverse'], ['D', 'C', 'B', 'A']], + ]) }) describe('$arraySort', () => { - test('basic usage', () => { - const context = { - interpreters, - scope: { $$VALUE: ['B', 'D', 'C', 'A'] }, - } - - expect(evaluate(context, ['$arraySort'])).toEqual(['A', 'B', 'C', 'D']) - expect(evaluate(context, ['$arraySort', 'DESC'])).toEqual([ - 'D', - 'C', - 'B', - 'A', + describe('basic usage', () => { + _evTestCases([ + [['B', 'D', 'C', 'A'], ['$arraySort'], ['A', 'B', 'C', 'D']], + [ + ['B', 'D', 'C', 'A'], + ['$arraySort', 'DESC'], + ['D', 'C', 'B', 'A'], + ], ]) }) - test('with custom comparator', () => { - const context = { - interpreters, - scope: { $$VALUE: ['9', '1', '12', '11'] }, - } - + describe('with custom comparator', () => { const SORT_NUMBERS = [ '$mathSub', ['$numberInt', 10, ['$value', '$$SORT_B']], ['$numberInt', 10, ['$value', '$$SORT_A']], ] - expect(evaluate(context, ['$arraySort'])).toEqual(['1', '11', '12', '9']) - - expect(evaluate(context, ['$arraySort', SORT_NUMBERS])).toEqual([ - '1', - '9', - '11', - '12', - ]) - - expect(evaluate(context, ['$arraySort', [SORT_NUMBERS]])).toEqual([ - '1', - '9', - '11', - '12', - ]) - - expect(evaluate(context, ['$arraySort', [SORT_NUMBERS, 'DESC']])).toEqual([ - '12', - '11', - '9', - '1', + _evTestCases([ + [['9', '1', '12', '11'], ['$arraySort'], ['1', '11', '12', '9']], + [ + ['9', '1', '12', '11'], + ['$arraySort', SORT_NUMBERS], + ['1', '9', '11', '12'], + ], + [ + ['9', '1', '12', '11'], + ['$arraySort', [SORT_NUMBERS]], + ['1', '9', '11', '12'], + ], + [ + ['9', '1', '12', '11'], + ['$arraySort', [SORT_NUMBERS, 'DESC']], + ['12', '11', '9', '1'], + ], ]) }) }) describe('$arrayPush', () => { - test('basic usage', () => { - const context = { - interpreters, - scope: { $$VALUE: ['A', 'B', 'C', 'D'] }, - } - - expect(evaluate(context, ['$arrayPush', 'E'])).toEqual([ - 'A', - 'B', - 'C', - 'D', - 'E', - ]) - }) + _evTestCases([ + [ + ['A', 'B', 'C', 'D'], + ['$arrayPush', 'E'], + ['A', 'B', 'C', 'D', 'E'], + ], + ]) }) describe('$arrayPop', () => { - test('basic usage', () => { - const context = { - interpreters, - scope: { $$VALUE: ['A', 'B', 'C', 'D'] }, - } - - expect(evaluate(context, ['$arrayPop'])).toEqual(['A', 'B', 'C']) - }) + _evTestCases([[['A', 'B', 'C', 'D'], ['$arrayPop'], ['A', 'B', 'C']]]) }) describe('$arrayUnshift', () => { - test('basic usage', () => { - const context = { - interpreters, - scope: { $$VALUE: ['A', 'B', 'C', 'D'] }, - } - - expect(evaluate(context, ['$arrayUnshift', 'Z'])).toEqual([ - 'Z', - 'A', - 'B', - 'C', - 'D', - ]) - }) + _evTestCases([ + [ + ['A', 'B', 'C', 'D'], + ['$arrayUnshift', 'Z'], + ['Z', 'A', 'B', 'C', 'D'], + ], + ]) }) describe('$arrayShift', () => { - test('basic usage', () => { - const context = { - interpreters, - scope: { $$VALUE: ['A', 'B', 'C', 'D'] }, - } - - expect(evaluate(context, ['$arrayShift'])).toEqual(['B', 'C', 'D']) - }) + _evTestCases([[['A', 'B', 'C', 'D'], ['$arrayShift'], ['B', 'C', 'D']]]) }) describe('$arraySlice', () => { - test('basic usage', () => { - const context = { - interpreters, - scope: { $$VALUE: ['A', 'B', 'C', 'D'] }, - } - - expect(evaluate(context, ['$arraySlice', 1, 3])).toEqual(['B', 'C']) - }) + _evTestCases([ + [ + ['A', 'B', 'C', 'D'], + ['$arraySlice', 1, 3], + ['B', 'C'], + ], + ]) }) describe('$arrayReplace', () => { - const context = { - interpreters, - scope: { $$VALUE: ['0', '1', '2', '3'] }, - } - - test('index', () => { - expect(evaluate(context, ['$arrayReplace', 1, 'R'])).toEqual([ - '0', - 'R', - '2', - '3', + const BASE_VALUE = ['0', '1', '2', '3'] + + describe('index', () => { + _evTestCases([ + [BASE_VALUE, ['$arrayReplace', 1, 'R'], ['0', 'R', '2', '3']], + [ + BASE_VALUE, + ['$arrayReplace', 1, ['R1', 'R2', 'R3']], + ['0', 'R1', 'R2', 'R3', '2', '3'], + ], ]) - expect( - evaluate(context, ['$arrayReplace', 1, ['R1', 'R2', 'R3']]) - ).toEqual(['0', 'R1', 'R2', 'R3', '2', '3']) }) - test('range', () => { - expect(evaluate(context, ['$arrayReplace', [1, 3], 'R'])).toEqual([ - '0', - 'R', - '3', + describe('range', () => { + _evTestCases([ + [BASE_VALUE, ['$arrayReplace', [1, 3], 'R'], ['0', 'R', '3']], + [ + BASE_VALUE, + ['$arrayReplace', [1, 3], ['R1', 'R2', 'R3']], + ['0', 'R1', 'R2', 'R3', '3'], + ], ]) - expect( - evaluate(context, ['$arrayReplace', [1, 3], ['R1', 'R2', 'R3']]) - ).toEqual(['0', 'R1', 'R2', 'R3', '3']) }) - test('empty replacement (removal)', () => { - expect(evaluate(context, ['$arrayReplace', 1, []])).toEqual(['0', '2', '3']) - - expect(evaluate(context, ['$arrayReplace', [1, 3], []])).toEqual(['0', '3']) + describe('empty replacement (removal)', () => { + _evTestCases([ + [BASE_VALUE, ['$arrayReplace', 1, []], ['0', '2', '3']], + [BASE_VALUE, ['$arrayReplace', [1, 3], []], ['0', '3']], + ]) }) - test('empty range (add)', () => { - expect(evaluate(context, ['$arrayReplace', [1, 1], 'R'])).toEqual([ - '0', - 'R', - '1', - '2', - '3', + describe('empty range (add)', () => { + _evTestCases([ + [BASE_VALUE, ['$arrayReplace', [1, 1], 'R'], ['0', 'R', '1', '2', '3']], + [ + BASE_VALUE, + ['$arrayReplace', [1, 1], ['R1', 'R2', 'R3']], + ['0', 'R1', 'R2', 'R3', '1', '2', '3'], + ], ]) - - expect( - evaluate(context, ['$arrayReplace', [1, 1], ['R1', 'R2', 'R3']]) - ).toEqual(['0', 'R1', 'R2', 'R3', '1', '2', '3']) }) }) describe('$arrayAddAt', () => { - test('basic usage', () => { - const context = { - interpreters, - scope: { $$VALUE: ['A', 'B', 'C', 'D'] }, - } - - expect( - evaluate(context, ['$arrayAddAt', 1, ['$arraySlice', 0, 4]]) - ).toEqual(['A', 'A', 'B', 'C', 'D', 'B', 'C', 'D']) - }) + _evTestCases([ + [ + ['A', 'B', 'C', 'D'], + ['$arrayAddAt', 1, ['$arraySlice', 0, 4]], + ['A', 'A', 'B', 'C', 'D', 'B', 'C', 'D'], + ], + ]) }) describe('$arrayRemoveAt', () => { - test('basic usage', () => { - const context = { - interpreters, - scope: { $$VALUE: ['A', 'B', 'C', 'D'] }, - } - - expect(evaluate(context, ['$arrayRemoveAt', 1, 2])).toEqual(['A', 'D']) - - expect(evaluate(context, ['$arrayRemoveAt', 1])).toEqual(['A', 'C', 'D']) - }) + _evTestCases([ + [ + ['A', 'B', 'C', 'D'], + ['$arrayRemoveAt', 1, 2], + ['A', 'D'], + ], + [ + ['A', 'B', 'C', 'D'], + ['$arrayRemoveAt', 1], + ['A', 'C', 'D'], + ], + ]) }) describe('$arrayJoin', () => { - test('basic usage', () => { - const context = { - interpreters, - scope: { $$VALUE: ['A', 'B', 'C', 'D'] }, - } - - expect(evaluate(context, ['$arrayJoin', '_'])).toEqual('A_B_C_D') - - expect(evaluate(context, ['$arrayJoin'])).toEqual('ABCD') - }) + _evTestCases([ + [['A', 'B', 'C', 'D'], ['$arrayJoin', '_'], 'A_B_C_D'], + [['A', 'B', 'C', 'D'], ['$arrayJoin'], 'ABCD'], + ]) }) describe('$arrayAt', () => { - test('basic usage', () => { - const context = { - interpreters, - scope: { $$VALUE: ['A', 'B', 'C', 'D'] }, - } - - expect(evaluate(context, ['$arrayAt', 0])).toEqual('A') - - expect(evaluate(context, ['$arrayAt', 3])).toEqual('D') - - expect(evaluate(context, ['$arrayAt', 4])).toEqual(undefined) - }) + _evTestCases([ + [['A', 'B', 'C', 'D'], ['$arrayAt', 0], 'A'], + [['A', 'B', 'C', 'D'], ['$arrayAt', 3], 'D'], + [['A', 'B', 'C', 'D'], ['$arrayAt', 4], undefined], + [['A', 'B', 'C', 'D'], ['$arrayAt', '0'], TypeError], + ]) }) describe('$arrayEvery vs $and (logical) - example: check for array item uniqueness', () => { @@ -471,41 +345,34 @@ describe('$arrayEvery vs $and (logical) - example: check for array item uniquene ['$arrayIndexOf', ['$value', '$$VALUE'], ['$value', '$$ARRAY']], ] - test('using $and', () => { + describe('using $and', () => { const MAP_EXP = ['$arrayMap', ITEM_IS_UNIQUE_EXP] const ITEMS_UNIQUE_EXP = ['$and', MAP_EXP] - const itemsAreUnique = (array) => - evaluate( - { - interpreters, - scope: { $$VALUE: array }, - }, - ITEMS_UNIQUE_EXP - ) - - expect(itemsAreUnique([1, 2, 3, 4])).toEqual(true) - expect(itemsAreUnique([1, 2, 3, 1])).toEqual(false) + _evTestCases([ + [[1, 2, 3, 4], ITEMS_UNIQUE_EXP, true], + [[1, 2, 3, 1], ITEMS_UNIQUE_EXP, false], + ]) }) - test('using $arrayEvery', () => { + describe('using $arrayEvery', () => { // Skips the 'map' step, // which prevents executing the ITEM_IS_UNIQUE_EXP for // every value, as $arrayEvery (Array.prototype.every) // will return at first false value const ITEMS_UNIQUE_EXP = ['$arrayEvery', ITEM_IS_UNIQUE_EXP] - const itemsAreUnique = (array) => - evaluate( - { - interpreters, - scope: { $$VALUE: array }, - }, - ITEMS_UNIQUE_EXP - ) - - expect(itemsAreUnique([1, 2, 3, 4])).toEqual(true) - expect(itemsAreUnique([1, 2, 3, 1])).toEqual(false) + _evTestCases([ + [[1, 2, 3, 4], ITEMS_UNIQUE_EXP, true], + [[1, 2, 3, 1], ITEMS_UNIQUE_EXP, false], + ]) }) }) + +describe('$arraySome', () => { + _evTestCases([ + [[1, 2, 3, 4], ['$arraySome', ['$eq', 0, ['$mathMod', 2]]], true], + [[1, 3, 5, 7], ['$arraySome', ['$eq', 0, ['$mathMod', 2]]], false], + ]) +}) diff --git a/src/expressions/array.ts b/src/expressions/array.ts index 8bde0c0..be5c711 100644 --- a/src/expressions/array.ts +++ b/src/expressions/array.ts @@ -1,12 +1,13 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { evaluate, evaluateTyped, isExpression } from '../evaluate' +import { evaluate, evaluateTypedSync, isExpression } from '../evaluate' import { EvaluationContext, Expression, - ExpressionInterpreterSpec, + InterpreterSpec, + InterpreterSpecSingle, } from '../types' -import { validateType } from '@orioro/typing' +import { validateType, anyType } from '@orioro/typing' export const $$INDEX = ['$value', '$$INDEX'] export const $$ARRAY = ['$value', '$$ARRAY'] @@ -22,7 +23,7 @@ export const $$SORT_B = ['$value', '$$SORT_B'] * @param {Array} [array=$$VALUE] * @returns {Boolean} includes */ -export const $arrayIncludes: ExpressionInterpreterSpec = [ +export const $arrayIncludes: InterpreterSpec = [ (search: any, array: any[]): boolean => array.includes(search), ['any', 'array'], ] @@ -37,7 +38,7 @@ export const $arrayIncludes: ExpressionInterpreterSpec = [ * @param {Array} [array=$$VALUE] * @returns {Boolean} includesAll */ -export const $arrayIncludesAll: ExpressionInterpreterSpec = [ +export const $arrayIncludesAll: InterpreterSpec = [ (search: any[], array: any[]): boolean => search.every((value) => array.includes(value)), ['array', 'array'], @@ -52,7 +53,7 @@ export const $arrayIncludesAll: ExpressionInterpreterSpec = [ * @param {Array} [array=$$VALUE] * @returns {Boolean} includesAny */ -export const $arrayIncludesAny: ExpressionInterpreterSpec = [ +export const $arrayIncludesAny: InterpreterSpec = [ (search: any[], array: any[]): boolean => search.some((value) => array.includes(value)), ['array', 'array'], @@ -63,7 +64,7 @@ export const $arrayIncludesAny: ExpressionInterpreterSpec = [ * @param {Array} [array=$$VALUE] * @returns {Number} length */ -export const $arrayLength: ExpressionInterpreterSpec = [ +export const $arrayLength: InterpreterSpec = [ (array: any[]): number => array.length, ['array'], ] @@ -77,7 +78,7 @@ export const $arrayLength: ExpressionInterpreterSpec = [ * @param {*} start * @param {Array} [array=$$VALUE] */ -export const $arrayReduce: ExpressionInterpreterSpec = [ +export const $arrayReduce: InterpreterSpec = [ (reduceExp: Expression, start: any, array: any[], context): any => array.reduce( ($$ACC, $$VALUE, $$INDEX, $$ARRAY) => @@ -96,26 +97,33 @@ export const $arrayReduce: ExpressionInterpreterSpec = [ ), start ), - [null, 'any', 'array'], + [anyType({ delayEvaluation: true }), 'any', 'array'], ] -const _arrayIterator = (method: string): ExpressionInterpreterSpec => [ +const _iteratorContext = ( + parentContext: EvaluationContext, + $$VALUE: any, + $$INDEX: number, + $$ARRAY: any[] +): EvaluationContext => ({ + ...parentContext, + scope: { + $$PARENT_SCOPE: parentContext.scope, + $$VALUE, + $$INDEX, + $$ARRAY, + }, +}) + +const _arraySyncIterator = (method: string): InterpreterSpecSingle => [ (iteratorExp: Expression, array: any[], context: EvaluationContext): any => array[method](($$VALUE, $$INDEX, $$ARRAY) => evaluate( - { - ...context, - scope: { - $$PARENT_SCOPE: context.scope, - $$VALUE, - $$INDEX, - $$ARRAY, - }, - }, + _iteratorContext(context, $$VALUE, $$INDEX, $$ARRAY), iteratorExp ) ), - [null, 'array'], + [anyType({ delayEvaluation: true }), 'array'], ] /** @@ -127,7 +135,25 @@ const _arrayIterator = (method: string): ExpressionInterpreterSpec => [ * `$$INDEX`, `$$ARRAY`, `$$ACC` * @param {Array} [array=$$VALUE] */ -export const $arrayMap: ExpressionInterpreterSpec = _arrayIterator('map') +export const $arrayMap: InterpreterSpec = { + sync: _arraySyncIterator('map'), + async: [ + ( + iteratorExp: Expression, + array: any[], + context: EvaluationContext + ): Promise => + Promise.all( + array.map(($$VALUE, $$INDEX, $$ARRAY) => + evaluate( + _iteratorContext(context, $$VALUE, $$INDEX, $$ARRAY), + iteratorExp + ) + ) + ), + [anyType({ delayEvaluation: true }), 'array'], + ], +} /** * `Array.prototype.every` @@ -137,43 +163,145 @@ export const $arrayMap: ExpressionInterpreterSpec = _arrayIterator('map') * $arrayEvery exposes array iteration variables: * `$$PARENT_SCOPE`, `$$VALUE`, `$$INDEX`, `$$ARRAY` * + * @todo array $arrayEvery write tests with async conditions * @function $arrayEvery - * @param {Expression} everyExp + * @param {Expression} testExp * @param {Array} [array=$$VALUE] */ -export const $arrayEvery: ExpressionInterpreterSpec = _arrayIterator('every') +export const $arrayEvery: InterpreterSpec = { + sync: _arraySyncIterator('every'), + async: [ + ( + testExp: Expression, + array: any[], + context: EvaluationContext + ): Promise => + array.reduce( + (accPromise, $$VALUE, $$INDEX, $$ARRAY) => + accPromise.then((acc) => + acc === true + ? evaluate( + _iteratorContext(context, $$VALUE, $$INDEX, $$ARRAY), + testExp + ).then((result) => Boolean(result)) + : false + ), + Promise.resolve(true) + ), + [anyType({ delayEvaluation: true }), 'array'], + ], +} /** * `Array.prototype.some` * + * @todo array $arraySome write tests with async conditions * @function $arraySome * @param {Expression} someExp * @param {Array} [array=$$VALUE] */ -export const $arraySome: ExpressionInterpreterSpec = _arrayIterator('some') +export const $arraySome: InterpreterSpec = { + sync: _arraySyncIterator('some'), + async: [ + ( + testExp: Expression, + array: any[], + context: EvaluationContext + ): Promise => + array.reduce( + (accPromise, $$VALUE, $$INDEX, $$ARRAY) => + accPromise.then((acc) => + acc === false + ? evaluate( + _iteratorContext(context, $$VALUE, $$INDEX, $$ARRAY), + testExp + ).then((result) => Boolean(result)) + : true + ), + Promise.resolve(false) + ), + [anyType({ delayEvaluation: true }), 'array'], + ], +} + +export const $arrayFilterAsyncParallel: InterpreterSpecSingle = [ + (filterExp: Expression, array: any[], context: EvaluationContext) => + Promise.all( + array.map(($$VALUE, $$INDEX, $$ARRAY) => + evaluate( + _iteratorContext(context, $$VALUE, $$INDEX, $$ARRAY), + filterExp + ) + ) + ).then((results) => + results.reduce( + (acc, result, index) => (result ? [...acc, array[index]] : acc), + [] + ) + ), + [anyType({ delayEvaluation: true }), 'array'], +] + +export const $arrayFilterAsyncSerial: InterpreterSpecSingle = [ + (filterExp: Expression, array: any[], context: EvaluationContext) => + array.reduce( + (accPromise, $$VALUE, $$INDEX, $$ARRAY) => + accPromise.then((acc) => + Promise.resolve( + evaluate( + _iteratorContext(context, $$VALUE, $$INDEX, $$ARRAY), + filterExp + ) + ).then((itemCondition) => (itemCondition ? [...acc, $$VALUE] : acc)) + ), + Promise.resolve([]) + ), + [anyType({ delayEvaluation: true }), 'array'], +] /** * @function $arrayFilter * @param {Boolean} queryExp * @param {Array} [array=$$VALUE] */ -export const $arrayFilter: ExpressionInterpreterSpec = _arrayIterator('filter') +export const $arrayFilter: InterpreterSpec = { + sync: _arraySyncIterator('filter'), + async: $arrayFilterAsyncSerial, +} /** * @function $arrayFindIndex * @param {Boolean} queryExp * @param {Array} [array=$$VALUE] */ -export const $arrayFindIndex: ExpressionInterpreterSpec = _arrayIterator( - 'findIndex' -) +export const $arrayFindIndex: InterpreterSpec = { + sync: _arraySyncIterator('findIndex'), + async: [ + (queryExp: Expression, array: any[], context: EvaluationContext) => + array.reduce((accPromise, $$VALUE, $$INDEX, $$ARRAY) => { + return accPromise.then((acc) => { + if (acc === undefined) { + return Promise.resolve( + evaluate( + _iteratorContext(context, $$VALUE, $$INDEX, $$ARRAY), + queryExp + ) + ).then((matchesQuery) => (matchesQuery ? $$INDEX : undefined)) + } else { + return acc + } + }) + }, Promise.resolve(undefined)), + [anyType({ delayEvaluation: true }), 'array'], + ], +} /** * @function $arrayIndexOf * @param {*} value * @param {Array} [array=$$VALUE] */ -export const $arrayIndexOf: ExpressionInterpreterSpec = [ +export const $arrayIndexOf: InterpreterSpec = [ (value: any, array: any[]): number => array.indexOf(value), ['any', 'array'], ] @@ -183,13 +311,13 @@ export const $arrayIndexOf: ExpressionInterpreterSpec = [ * @param {Boolean} queryExp * @param {Array} [array=$$VALUE] */ -export const $arrayFind: ExpressionInterpreterSpec = _arrayIterator('find') +export const $arrayFind: InterpreterSpec = _arraySyncIterator('find') /** * @function $arrayReverse * @param {Array} [array=$$VALUE] */ -export const $arrayReverse: ExpressionInterpreterSpec = [ +export const $arrayReverse: InterpreterSpec = [ (array: any[]): any[] => { const arr = array.slice() arr.reverse() @@ -211,11 +339,14 @@ const _sortDefault = (a, b) => { } /** + * @todo array Make it possible for the same set of expression interpreters + * to be called synchronously or asynchronously. E.g. sort comparator + * expression should only support Synchronous * @function $arraySort * @param {String | Expression | [Expression, string]} sort * @param {Array} [array=$$VALUE] */ -export const $arraySort: ExpressionInterpreterSpec = [ +export const $arraySort: InterpreterSpec = [ ( sort: string | Expression | [Expression, string] = 'ASC', array: any[], @@ -234,7 +365,7 @@ export const $arraySort: ExpressionInterpreterSpec = [ sortExp === undefined ? _sortDefault : ($$SORT_A, $$SORT_B) => - evaluateTyped( + evaluateTypedSync( 'number', { ...context, @@ -247,7 +378,7 @@ export const $arraySort: ExpressionInterpreterSpec = [ .slice() .sort(order === 'DESC' ? (a, b) => -1 * sortFn(a, b) : sortFn) }, - [null, 'array'], + [anyType({ delayEvaluation: true }), 'array'], ] /** @@ -255,7 +386,7 @@ export const $arraySort: ExpressionInterpreterSpec = [ * @param {*} valueExp * @param {Array} [array=$$VALUE] */ -export const $arrayPush: ExpressionInterpreterSpec = [ +export const $arrayPush: InterpreterSpec = [ (value: any, array: any[]): any[] => [...array, value], ['any', 'array'], ] @@ -264,7 +395,7 @@ export const $arrayPush: ExpressionInterpreterSpec = [ * @function $arrayPop * @param {Array} [array=$$VALUE] */ -export const $arrayPop: ExpressionInterpreterSpec = [ +export const $arrayPop: InterpreterSpec = [ (array: any[]) => array.slice(0, array.length - 1), ['array'], ] @@ -274,7 +405,7 @@ export const $arrayPop: ExpressionInterpreterSpec = [ * @param {*} valueExp * @param {Array} [array=$$VALUE] */ -export const $arrayUnshift: ExpressionInterpreterSpec = [ +export const $arrayUnshift: InterpreterSpec = [ (value: any, array: any[]): any[] => [value, ...array], ['any', 'array'], ] @@ -283,7 +414,7 @@ export const $arrayUnshift: ExpressionInterpreterSpec = [ * @function $arrayShift * @param {Array} [array=$$VALUE] */ -export const $arrayShift: ExpressionInterpreterSpec = [ +export const $arrayShift: InterpreterSpec = [ (array: any[]): any[] => array.slice(1, array.length), ['array'], ] @@ -295,7 +426,7 @@ export const $arrayShift: ExpressionInterpreterSpec = [ * @param {Array} [array=$$VALUE] * @returns {Array} */ -export const $arraySlice: ExpressionInterpreterSpec = [ +export const $arraySlice: InterpreterSpec = [ (start: number, end: number, array: any[]): any[] => array.slice(start, end), ['number', 'number', 'array'], ] @@ -307,7 +438,7 @@ export const $arraySlice: ExpressionInterpreterSpec = [ * @param {Array} [array=$$VALUE] * @returns {Array} */ -export const $arrayReplace: ExpressionInterpreterSpec = [ +export const $arrayReplace: InterpreterSpec = [ ( indexOrRange: number | [number, number], replacement: any, @@ -336,7 +467,7 @@ export const $arrayReplace: ExpressionInterpreterSpec = [ * @param {Array} [array=$$VALUE] * @returns {Array} resultingArray The array with items added at position */ -export const $arrayAddAt: ExpressionInterpreterSpec = [ +export const $arrayAddAt: InterpreterSpec = [ (index: number, values: any[], array: any[]) => { const head = array.slice(0, index) const tail = array.slice(index) @@ -355,7 +486,7 @@ export const $arrayAddAt: ExpressionInterpreterSpec = [ * @param {Array} [array=$$VALUE] * @returns {Array} resultingArray The array without the removed item */ -export const $arrayRemoveAt: ExpressionInterpreterSpec = [ +export const $arrayRemoveAt: InterpreterSpec = [ (position: number, count: number = 1, array: any[]): any[] => [ ...array.slice(0, position), ...array.slice(position + count), @@ -369,7 +500,7 @@ export const $arrayRemoveAt: ExpressionInterpreterSpec = [ * @param {Array} [array=$$VALUE] * @returns {String} */ -export const $arrayJoin: ExpressionInterpreterSpec = [ +export const $arrayJoin: InterpreterSpec = [ (separator: string = '', array: any[]): string => array.join(separator), [['string', 'undefined'], 'array'], ] @@ -380,7 +511,7 @@ export const $arrayJoin: ExpressionInterpreterSpec = [ * @param {Array} [array=$$VALUE] * @returns {*} value */ -export const $arrayAt: ExpressionInterpreterSpec = [ +export const $arrayAt: InterpreterSpec = [ (index: number, array: any[]): any => array[index], ['number', 'array'], ] diff --git a/src/expressions/boolean.spec.ts b/src/expressions/boolean.spec.ts index 4d23798..4befbf3 100644 --- a/src/expressions/boolean.spec.ts +++ b/src/expressions/boolean.spec.ts @@ -1,65 +1,28 @@ -import { evaluate } from '../evaluate' -import { syncInterpreterList } from '../interpreter' import { $value } from './value' import { $boolean } from './boolean' -const interpreters = syncInterpreterList({ +import { _prepareEvaluateTestCases } from '../../spec/specUtil' + +const EXPS = { $value, $boolean, -}) - -describe('$boolean', () => { - test('numbers', () => { - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: 1 }, - }, - ['$boolean'] - ) - ).toEqual(true) +} - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: 0 }, - }, - ['$boolean'] - ) - ).toEqual(false) +const _evTestCases = _prepareEvaluateTestCases(EXPS) - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: -1 }, - }, - ['$boolean'] - ) - ).toEqual(true) +describe('$boolean', () => { + describe('numbers', () => { + _evTestCases([ + [1, ['$boolean'], true], + [0, ['$boolean'], false], + [-1, ['$boolean'], true], + ]) }) - test('string', () => { - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: 'some string' }, - }, - ['$boolean'] - ) - ).toEqual(true) - - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: '' }, - }, - ['$boolean'] - ) - ).toEqual(false) + describe('string', () => { + _evTestCases([ + ['some string', ['$boolean'], true], + ['', ['$boolean'], false], + ]) }) }) diff --git a/src/expressions/boolean.ts b/src/expressions/boolean.ts index 590be23..da36ad5 100644 --- a/src/expressions/boolean.ts +++ b/src/expressions/boolean.ts @@ -1,11 +1,11 @@ -import { ExpressionInterpreterSpec } from '../types' +import { InterpreterSpec } from '../types' /** * @function $boolean * @param {*} value * @returns {Boolean} */ -export const $boolean: ExpressionInterpreterSpec = [ +export const $boolean: InterpreterSpec = [ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types (value: any): boolean => Boolean(value), ['any'], diff --git a/src/expressions/comparison.spec.ts b/src/expressions/comparison.spec.ts index 290fcad..f6dd4bb 100644 --- a/src/expressions/comparison.spec.ts +++ b/src/expressions/comparison.spec.ts @@ -1,153 +1,81 @@ -import { evaluate } from '../evaluate' -import { syncInterpreterList } from '../interpreter' import { $stringSubstr } from './string' -import { - $eq, - $notEq, - $in, - $notIn, - $gt, - $gte, - $lt, - $lte, - $matches, -} from './comparison' +import { $and } from './logical' +import { COMPARISON_EXPRESSIONS } from './comparison' import { $value } from './value' -describe('$eq / $notEq', () => { - const interpreters = syncInterpreterList({ - $value, - $stringSubstr, - $eq, - $notEq, - }) +import { _prepareEvaluateTestCases } from '../../spec/specUtil' - test('string', () => { - const context = { - interpreters, - scope: { - $$VALUE: 'SOME_STRING', - }, - } +const EXPS = { + $value, + $stringSubstr, + $and, + ...COMPARISON_EXPRESSIONS, +} - expect(evaluate(context, ['$eq', 'SOME_STRING'])).toEqual(true) - expect(evaluate(context, ['$notEq', 'SOME_STRING'])).toEqual(false) +const _evTestCases = _prepareEvaluateTestCases(EXPS) - expect(evaluate(context, ['$eq', 'OTHER_STRING'])).toEqual(false) - expect(evaluate(context, ['$notEq', 'OTHER_STRING'])).toEqual(true) - - expect( - evaluate(context, [ - '$eq', - ['$stringSubstr', 4, 15, 'PRE_SOME_STRING_POS'], - ]) - ).toEqual(true) +describe('$eq / $notEq', () => { + describe('string', () => { + _evTestCases([ + ['SOME_STRING', ['$eq', 'SOME_STRING'], true], + ['SOME_STRING', ['$notEq', 'SOME_STRING'], false], + ['SOME_STRING', ['$eq', 'OTHER_STRING'], false], + ['SOME_STRING', ['$notEq', 'OTHER_STRING'], true], + [ + 'SOME_STRING', + ['$eq', ['$stringSubstr', 4, 15, 'PRE_SOME_STRING_POS']], + true, + ], + ]) }) }) describe('$in / $notIn', () => { - const interpreters = syncInterpreterList({ - $value, - $in, - $notIn, - }) - - test('basic', () => { - const context = { - interpreters, - scope: { - $$VALUE: 'C', - }, - } - - expect(evaluate(context, ['$in', ['A', 'B', 'C']])).toEqual(true) - expect(evaluate(context, ['$notIn', ['A', 'B', 'C']])).toEqual(false) - - expect(evaluate(context, ['$in', ['X', 'Y', 'Z']])).toEqual(false) - expect(evaluate(context, ['$notIn', ['X', 'Y', 'Z']])).toEqual(true) - }) + _evTestCases([ + ['C', ['$in', ['A', 'B', 'C']], true], + ['C', ['$notIn', ['A', 'B', 'C']], false], + ['C', ['$in', ['X', 'Y', 'Z']], false], + ['C', ['$notIn', ['X', 'Y', 'Z']], true], + ]) }) describe('$gt / $gte / $lt / $lte', () => { - const interpreters = syncInterpreterList({ - $value, - $gt, - $gte, - $lt, - $lte, - }) - - const context = { - interpreters, - scope: { - $$VALUE: 20, - }, - } - - test('$gt', () => { - expect(evaluate(context, ['$gt', 10])).toEqual(true) - expect(evaluate(context, ['$gt', 20])).toEqual(false) - expect(evaluate(context, ['$gt', 30])).toEqual(false) + describe('$gt', () => { + _evTestCases([ + [20, ['$gt', 10], true], + [20, ['$gt', 20], false], + [20, ['$gt', 30], false], + ]) }) - test('$gte', () => { - expect(evaluate(context, ['$gte', 10])).toEqual(true) - expect(evaluate(context, ['$gte', 20])).toEqual(true) - expect(evaluate(context, ['$gte', 30])).toEqual(false) + describe('$gte', () => { + _evTestCases([ + [20, ['$gte', 10], true], + [20, ['$gte', 20], true], + [20, ['$gte', 30], false], + ]) }) - test('$lt', () => { - expect(evaluate(context, ['$lt', 10])).toEqual(false) - expect(evaluate(context, ['$lt', 20])).toEqual(false) - expect(evaluate(context, ['$lt', 30])).toEqual(true) + describe('$lt', () => { + _evTestCases([ + [20, ['$lt', 10], false], + [20, ['$lt', 20], false], + [20, ['$lt', 30], true], + ]) }) - test('$lte', () => { - expect(evaluate(context, ['$lte', 10])).toEqual(false) - expect(evaluate(context, ['$lte', 20])).toEqual(true) - expect(evaluate(context, ['$lte', 30])).toEqual(true) + describe('$lte', () => { + _evTestCases([ + [20, ['$lte', 10], false], + [20, ['$lte', 20], true], + [20, ['$lte', 30], true], + ]) }) }) describe('$matches', () => { - const interpreters = syncInterpreterList({ - $value, - $eq, - $notEq, - $in, - $notIn, - $gt, - $gte, - $lt, - $lte, - $matches, - }) - - test('basic', () => { - const context = { - interpreters, - scope: { - $$VALUE: 24, - }, - } - - expect( - evaluate(context, [ - '$matches', - { - $gt: 20, - $lt: 30, - }, - ]) - ).toEqual(true) - expect( - evaluate(context, [ - '$matches', - { - $gt: 20, - $lt: 24, - }, - ]) - ).toEqual(false) - }) + _evTestCases([ + [24, ['$matches', { $gt: 20, $lt: 30 }], true], + [24, ['$matches', { $gt: 20, $lt: 24 }], false], + ]) }) diff --git a/src/expressions/comparison.ts b/src/expressions/comparison.ts index 61768fa..7c29f33 100644 --- a/src/expressions/comparison.ts +++ b/src/expressions/comparison.ts @@ -2,15 +2,9 @@ import { isEqual } from 'lodash' -import { evaluate, evaluateTyped } from '../evaluate' +import { evaluate } from '../evaluate' -import { - EvaluationContext, - PlainObject, - ExpressionInterpreterSpec, -} from '../types' - -import { $$VALUE } from './value' +import { EvaluationContext, PlainObject, InterpreterSpec } from '../types' /** * Checks if the two values @@ -20,7 +14,7 @@ import { $$VALUE } from './value' * @param {*} valueExp Value being compared. * @returns {Boolean} */ -export const $eq: ExpressionInterpreterSpec = [ +export const $eq: InterpreterSpec = [ (valueB: any, valueA: any): boolean => isEqual(valueA, valueB), ['any', 'any'], ] @@ -31,7 +25,7 @@ export const $eq: ExpressionInterpreterSpec = [ * @param {*} valueExp Value being compared. * @returns {Boolean} */ -export const $notEq: ExpressionInterpreterSpec = [ +export const $notEq: InterpreterSpec = [ (valueB: any, valueA: any): boolean => !isEqual(valueA, valueB), ['any', 'any'], ] @@ -44,7 +38,7 @@ export const $notEq: ExpressionInterpreterSpec = [ * @param {*} valueExp * @returns {Boolean} */ -export const $in: ExpressionInterpreterSpec = [ +export const $in: InterpreterSpec = [ (array: any[], value: any): boolean => array.some((item) => isEqual(item, value)), ['array', 'any'], @@ -58,7 +52,7 @@ export const $in: ExpressionInterpreterSpec = [ * @param {*} valueExp * @returns {Boolean} */ -export const $notIn: ExpressionInterpreterSpec = [ +export const $notIn: InterpreterSpec = [ (array: any[], value: any): boolean => array.every((item) => !isEqual(item, value)), ['array', 'any'], @@ -72,7 +66,7 @@ export const $notIn: ExpressionInterpreterSpec = [ * @param {Number} valueExp * @returns {Boolean} */ -export const $gt: ExpressionInterpreterSpec = [ +export const $gt: InterpreterSpec = [ (reference: number, value: number): boolean => value > reference, ['number', 'number'], ] @@ -85,7 +79,7 @@ export const $gt: ExpressionInterpreterSpec = [ * @param {Number} valueExp * @returns {Boolean} */ -export const $gte: ExpressionInterpreterSpec = [ +export const $gte: InterpreterSpec = [ (reference: number, value: number): boolean => value >= reference, ['number', 'number'], ] @@ -98,7 +92,7 @@ export const $gte: ExpressionInterpreterSpec = [ * @param {Number} valueExp * @returns {Boolean} */ -export const $lt: ExpressionInterpreterSpec = [ +export const $lt: InterpreterSpec = [ (reference: number, value: number): boolean => value < reference, ['number', 'number'], ] @@ -111,7 +105,7 @@ export const $lt: ExpressionInterpreterSpec = [ * @param {Number} valueExp * @returns {Boolean} */ -export const $lte: ExpressionInterpreterSpec = [ +export const $lte: InterpreterSpec = [ (reference: number, value: number): boolean => value <= reference, ['number', 'number'], ] @@ -124,7 +118,7 @@ export const $lte: ExpressionInterpreterSpec = [ * @param {Number} valueExp * @returns {Boolean} */ -export const $matches: ExpressionInterpreterSpec = [ +export const $matches: InterpreterSpec = [ (criteria: PlainObject, value: any, context: EvaluationContext): boolean => { const criteriaKeys = Object.keys(criteria) @@ -132,26 +126,14 @@ export const $matches: ExpressionInterpreterSpec = [ throw new Error(`Invalid criteria: ${JSON.stringify(criteria)}`) } - return criteriaKeys.every((criteriaKey) => { - // - // Criteria value may be an expression. - // Evaluate the expression against the original context, not - // against the value - // - const criteriaValue = evaluate(context, criteria[criteriaKey]) - - return evaluateTyped( - 'boolean', - { - ...context, - scope: { - ...context.scope, - $$VALUE: value, - }, - }, - [criteriaKey, criteriaValue, $$VALUE] - ) - }) + return evaluate(context, [ + '$and', + criteriaKeys.map((criteriaKey) => [ + criteriaKey, + criteria[criteriaKey], + value, + ]), + ]) }, ['object', 'any'], ] diff --git a/src/expressions/functional.spec.ts b/src/expressions/functional.spec.ts index 1c36a92..b6c6bc6 100644 --- a/src/expressions/functional.spec.ts +++ b/src/expressions/functional.spec.ts @@ -1,6 +1,3 @@ -import { evaluate } from '../evaluate' -import { syncInterpreterList } from '../interpreter' - import { COMPARISON_EXPRESSIONS } from './comparison' import { VALUE_EXPRESSIONS } from './value' import { FUNCTIONAL_EXPRESSIONS } from './functional' @@ -8,35 +5,29 @@ import { ARRAY_EXPRESSIONS } from './array' import { MATH_EXPRESSIONS } from './math' import { STRING_EXPRESSIONS } from './string' -const interpreters = syncInterpreterList({ +import { _prepareEvaluateTestCases } from '../../spec/specUtil' + +const EXPS = { ...VALUE_EXPRESSIONS, ...COMPARISON_EXPRESSIONS, ...FUNCTIONAL_EXPRESSIONS, ...ARRAY_EXPRESSIONS, ...MATH_EXPRESSIONS, ...STRING_EXPRESSIONS, -}) +} -test('$pipe', () => { +const _evTestCases = _prepareEvaluateTestCases(EXPS) + +describe('$pipe', () => { const SUM_2 = ['$arrayMap', ['$mathSum', 2]] const MULT_2 = ['$arrayMap', ['$mathMult', 2]] const GREATER_THAN_50 = ['$arrayFilter', ['$gt', 50]] - const context = { - interpreters, - scope: { $$VALUE: [10, 20, 30, 40] }, - } + const VALUE = [10, 20, 30, 40] - expect(evaluate(context, ['$pipe', [SUM_2, MULT_2]])).toEqual([ - 24, - 44, - 64, - 84, + _evTestCases([ + [VALUE, ['$pipe', [SUM_2, MULT_2]], [24, 44, 64, 84]], + [VALUE, ['$pipe', [SUM_2, MULT_2, GREATER_THAN_50]], [64, 84]], + [VALUE, ['$pipe', [SUM_2, GREATER_THAN_50]], []], ]) - - expect( - evaluate(context, ['$pipe', [SUM_2, MULT_2, GREATER_THAN_50]]) - ).toEqual([64, 84]) - - expect(evaluate(context, ['$pipe', [SUM_2, GREATER_THAN_50]])).toEqual([]) }) diff --git a/src/expressions/functional.ts b/src/expressions/functional.ts index 80ea6cc..3dddade 100644 --- a/src/expressions/functional.ts +++ b/src/expressions/functional.ts @@ -1,8 +1,4 @@ -import { - EvaluationContext, - Expression, - ExpressionInterpreterSpec, -} from '../types' +import { EvaluationContext, Expression, InterpreterSpec } from '../types' import { evaluate } from '../evaluate' @@ -11,7 +7,7 @@ import { evaluate } from '../evaluate' * @param {Expression[]} expressions * @returns {*} pipeResult */ -export const $pipe: ExpressionInterpreterSpec = [ +export const $pipe: InterpreterSpec = [ (expressions: Expression[], context: EvaluationContext): any => expressions.reduce((acc, expression) => { return evaluate( diff --git a/src/expressions/logical.spec.ts b/src/expressions/logical.spec.ts index 2e0ba9a..8319241 100644 --- a/src/expressions/logical.spec.ts +++ b/src/expressions/logical.spec.ts @@ -1,5 +1,6 @@ import { evaluate } from '../evaluate' -import { syncInterpreterList } from '../interpreter' +import { SyncModeUnsupportedError } from '../errors' +import { interpreterList } from '../interpreter/interpreter' import { VALUE_EXPRESSIONS } from './value' import { BOOLEAN_EXPRESSIONS } from './boolean' import { LOGICAL_EXPRESSIONS } from './logical' @@ -8,7 +9,12 @@ import { COMPARISON_EXPRESSIONS } from './comparison' import { STRING_EXPRESSIONS } from './string' import { MATH_EXPRESSIONS } from './math' -const interpreters = syncInterpreterList({ +import { _prepareEvaluateTestCases } from '../../spec/specUtil' + +const delayValue = (value, ms = 100) => + new Promise((resolve) => setTimeout(resolve.bind(null, value), ms)) + +const EXP = { ...VALUE_EXPRESSIONS, ...BOOLEAN_EXPRESSIONS, ...LOGICAL_EXPRESSIONS, @@ -16,143 +22,45 @@ const interpreters = syncInterpreterList({ ...COMPARISON_EXPRESSIONS, ...STRING_EXPRESSIONS, ...MATH_EXPRESSIONS, -}) + $asyncValue: (context, ...args) => + delayValue(evaluate(context, ['$value', ...args])), + $asyncNum50: () => delayValue(50), + $asyncNum100: () => delayValue(100), + $asyncStr1: () => delayValue('str1'), + $asyncStr2: () => delayValue('str2'), + $asyncFalse: () => delayValue(false), + $asyncTrue: () => delayValue(true), +} + +const _evTestCases = _prepareEvaluateTestCases(EXP) describe('$and', () => { - test('error situations', () => { - expect(() => - evaluate( - { - interpreters, - scope: { $$VALUE: undefined }, - }, - ['$and'] - ) - ).toThrow(TypeError) - - expect(() => - evaluate( - { - interpreters, - scope: { $$VALUE: null }, - }, - ['$and'] - ) - ).toThrow(TypeError) - - expect(() => - evaluate( - { - interpreters, - scope: { $$VALUE: true }, - }, - ['$and'] - ) - ).toThrow(TypeError) - - expect(() => - evaluate( - { - interpreters, - scope: { $$VALUE: 8 }, - }, - ['$and'] - ) - ).toThrow(TypeError) - - expect(() => - evaluate( - { - interpreters, - scope: { $$VALUE: {} }, - }, - ['$and'] - ) - ).toThrow(TypeError) + describe('error situations', () => { + _evTestCases([ + [undefined, ['$and'], TypeError], + [null, ['$and'], TypeError], + [true, ['$and'], TypeError], + [8, ['$and'], TypeError], + [{}, ['$and'], TypeError], + ]) }) - test('basic', () => { - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: [true, true, true] }, - }, - ['$and'] - ) - ).toEqual(true) - - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: [true, false, true] }, - }, - ['$and'] - ) - ).toEqual(false) - - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: [1, 'string', true] }, - }, - ['$and', ['$arrayMap', ['$boolean']]] - ) - ).toEqual(true) - - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: [1, '', true] }, - }, - ['$and', ['$arrayMap', ['$boolean']]] - ) - ).toEqual(false) + describe('basic', () => { + _evTestCases([ + [[true, true, true], ['$and'], true], + [[true, false, true], ['$and'], false], + [[1, 'string', true], ['$and'], true], + [[1, '', true], ['$and'], false], + ]) }) - test('value coercion', () => { - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: [1, 0, true] }, - }, - ['$and'] - ) - ).toEqual(false) - - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: [1, 'some-string', true] }, - }, - ['$and'] - ) - ).toEqual(true) - - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: [1, '', true] }, - }, - ['$and'] - ) - ).toEqual(false) - - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: [1, null, true] }, - }, - ['$and'] - ) - ).toEqual(false) + describe('value coercion', () => { + _evTestCases([ + [[1, 0, true], ['$and'], false], + [[1, 'some-string', true], ['$and'], true], + [[1, '', true], ['$and'], false], + [[1, null, true], ['$and'], false], + ]) }) // eslint-disable-next-line jest/no-disabled-tests @@ -162,7 +70,7 @@ describe('$and', () => { evaluate( { - interpreters, + interpreters: interpreterList(EXP), scope: { $$VALUE: [ ['$unknownExpression', 1, 2], @@ -187,245 +95,252 @@ describe('$and', () => { console.warn = warn_ }) - test('w/ comparison', () => { - const context = { - interpreters, - scope: { - $$VALUE: { - name: 'João Maranhão', - age: 25, - }, - }, + describe('w/ comparison', () => { + const data = { + name: 'João Maranhão', + age: 25, } - expect( - evaluate(context, [ - '$and', + _evTestCases([ + [ + data, [ - ['$eq', 'João', ['$stringSubstr', 0, 4, ['$value', 'name']]], - ['$gt', 20, ['$value', 'age']], - ['$lt', 30, ['$value', 'age']], + '$and', + [ + ['$eq', 'João', ['$stringSubstr', 0, 4, ['$value', 'name']]], + ['$gt', 20, ['$value', 'age']], + ['$lt', 30, ['$value', 'age']], + ], ], - ]) - ).toEqual(true) - - expect( - evaluate(context, [ - '$and', + true, + ], + [ + data, [ - ['$gt', 20, ['$value', 'age']], - ['$lt', 30, ['$value', 'age']], + '$and', + [ + ['$gt', 20, ['$value', 'age']], + ['$lt', 30, ['$value', 'age']], + ], ], - ]) - ).toEqual(true) - - expect( - evaluate(context, [ - '$and', + true, + ], + [ + data, [ - ['$eq', 'Fernando', ['$stringSubstr', 0, 8, ['$value', 'name']]], - ['$gt', 20, ['$value', 'age']], - ['$lt', 30, ['$value', 'age']], + '$and', + [ + ['$eq', 'Fernando', ['$stringSubstr', 0, 8, ['$value', 'name']]], + ['$gt', 20, ['$value', 'age']], + ['$lt', 30, ['$value', 'age']], + ], ], - ]) - ).toEqual(false) - - expect( - evaluate(context, [ - '$and', + false, + ], + [ + data, [ - ['$gt', 20, ['$value', 'age']], - ['$lt', 30, ['$value', 'age']], + '$and', + [ + ['$gt', 20, ['$value', 'age']], + ['$lt', 30, ['$value', 'age']], + ], ], - ]) - ).toEqual(true) + true, + ], + ]) }) -}) - -describe('$or', () => { - test('basic', () => { - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: [false, true, false] }, - }, - ['$or'] - ) - ).toEqual(true) - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: [false, false, false] }, - }, - ['$or'] - ) - ).toEqual(false) - }) -}) + describe('using async getters', () => { + const MIN_VALUE = 20 + const MAX_VALUE = 50 + + // const interpreters = interpreterList({ + // ...VALUE_EXPRESSIONS, + // ...LOGICAL_EXPRESSIONS, + // ...COMPARISON_EXPRESSIONS, + // $asyncGetMinValue: () => + // new Promise((resolve) => { + // setTimeout(resolve.bind(null, MIN_VALUE), 100) + // }), + // $asyncGetMaxValue: () => + // new Promise((resolve) => { + // setTimeout(resolve.bind(null, MAX_VALUE), 100) + // }), + // }) + + const _evTestCases = _prepareEvaluateTestCases({ + ...VALUE_EXPRESSIONS, + ...LOGICAL_EXPRESSIONS, + ...COMPARISON_EXPRESSIONS, + $asyncGetMinValue: { + sync: null, + async: () => + new Promise((resolve) => { + setTimeout(resolve.bind(null, MIN_VALUE), 100) + }), + }, + $asyncGetMaxValue: { + sync: null, + async: () => + new Promise((resolve) => { + setTimeout(resolve.bind(null, MAX_VALUE), 100) + }), + }, + }) -describe('$not', () => { - test('basic', () => { - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: 'some-value' }, - }, - ['$not', ['$eq', 'some-value']] - ) - ).toEqual(false) + const exp = [ + '$and', + [ + ['$gte', ['$asyncGetMinValue']], + ['$lte', ['$asyncGetMaxValue']], + ], + ] - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: 'some-value' }, - }, - ['$not', ['$eq', 'some-other-value']] - ) - ).toEqual(true) + _evTestCases.testSyncCases([ + [30, exp, new SyncModeUnsupportedError('$asyncGetMinValue')], + [20, exp, new SyncModeUnsupportedError('$asyncGetMinValue')], + [10, exp, new SyncModeUnsupportedError('$asyncGetMinValue')], + ]) + + _evTestCases.testAsyncCases([ + [30, exp, true], + [20, exp, true], + [10, exp, false], + ]) }) }) -describe('$nor', () => { - test('basic', () => { - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: '1234567890' }, - }, +describe('$or', () => { + _evTestCases([ + [[false, true, false], ['$or'], true], + [[false, false, false], ['$or'], false], + [ + 10, + [ + '$or', [ - '$nor', - [ - ['$stringStartsWith', '123'], // true - ['$gt', 15, ['$stringLength']], // false - ], - ] - ) - ).toEqual(false) - - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: '1234567890' }, - }, + ['$eq', 0, ['$mathMod', 5]], + ['$eq', 0, ['$mathMod', 3]], + ], + ], + true, + ], + [ + 7, + [ + '$or', [ - '$nor', - [ - ['$stringStartsWith', '0000'], // false - ['$gt', 15, ['$stringLength']], // false - ], - ] - ) - ).toEqual(true) - }) + ['$eq', 0, ['$mathMod', 5]], + ['$eq', 0, ['$mathMod', 3]], + ], + ], + false, + ], + ]) }) -describe('$xor', () => { - test('basic', () => { - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: '1234567890' }, - }, +describe('$not', () => { + _evTestCases([ + ['some-value', ['$not', ['$eq', 'some-value']], false], + ['some-value', ['$not', ['$eq', 'some-other-value']], true], + ]) +}) + +describe('$nor', () => { + _evTestCases([ + [ + '1234567890', + [ + '$nor', [ - '$xor', ['$stringStartsWith', '123'], // true ['$gt', 15, ['$stringLength']], // false - ] - ) - ).toEqual(true) - - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: '1234567890' }, - }, + ], + ], + false, + ], + [ + '1234567890', + [ + '$nor', [ - '$xor', ['$stringStartsWith', '0000'], // false ['$gt', 15, ['$stringLength']], // false - ] - ) - ).toEqual(false) + ], + ], + true, + ], + ]) +}) - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: '1234567890' }, - }, - [ - '$xor', - ['$stringStartsWith', '123'], // true - ['$gt', 5, ['$stringLength']], // true - ] - ) - ).toEqual(false) - }) +describe('$xor', () => { + _evTestCases([ + [ + '1234567890', + [ + '$xor', + ['$stringStartsWith', '123'], // true + ['$gt', 15, ['$stringLength']], // false + ], + true, + ], + [ + '1234567890', + [ + '$xor', + ['$stringStartsWith', '0000'], // false + ['$gt', 15, ['$stringLength']], // false + ], + false, + ], + [ + '1234567890', + [ + '$xor', + ['$stringStartsWith', '123'], // true + ['$gt', 5, ['$stringLength']], // true + ], + false, + ], + ]) }) describe('$if', () => { - test('basic', () => { - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: 15 }, - }, - ['$if', ['$gt', 10], 100, 0] - ) - ).toEqual(100) - - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: 8 }, - }, - ['$if', ['$gt', 10], 100, 0] - ) - ).toEqual(0) + describe('basic', () => { + _evTestCases([ + [15, ['$if', ['$gt', 10], 100, 0], 100], + [8, ['$if', ['$gt', 10], 100, 0], 0], + [15, ['$if', ['$gt', 10], ['$mathMult', 10], ['$mathMult', -10]], 150], + [8, ['$if', ['$gt', 10], ['$mathMult', 10], ['$mathMult', -10]], -80], + ]) }) test('then and else expressions should be evaluated only after condition evaluation', () => { - let expAExecuted = false - let expBExecuted = false // eslint-disable-line prefer-const + const $expA = jest.fn(() => 'expA-result') + const $expB = jest.fn(() => 'expB-result') expect( evaluate( { - interpreters: { - ...interpreters, - $expA: () => { - expAExecuted = true - return 'expA-result' - }, - $expB: () => { - expAExecuted = false - return 'expB-result' - }, - }, + interpreters: interpreterList({ + ...EXP, + $expA, + $expB, + }), scope: { $$VALUE: 15 }, }, ['$if', ['$gt', 10], ['$expA'], ['$expB']] ) ).toEqual('expA-result') - expect(expAExecuted).toEqual(true) - expect(expBExecuted).toEqual(false) + expect($expA).toHaveBeenCalledTimes(1) + expect($expB).not.toHaveBeenCalled() }) }) describe('$switch', () => { - test('simple comparison', () => { + describe('simple comparison', () => { const expNoDefault = [ '$switch', [ @@ -437,39 +352,15 @@ describe('$switch', () => { const expWithDefault = [...expNoDefault, 'DEFAULT_VALUE'] - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: 'CASE_B' }, - }, - expNoDefault - ) - ).toEqual('VALUE_B') - - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: 'CASE_D' }, - }, - expNoDefault - ) - ).toEqual(undefined) - - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: 'CASE_D' }, - }, - expWithDefault - ) - ).toEqual('DEFAULT_VALUE') + _evTestCases([ + ['CASE_B', expNoDefault, 'VALUE_B'], + ['CASE_D', expNoDefault, undefined], + ['CASE_D', expWithDefault, 'DEFAULT_VALUE'], + ]) }) - test('more complex condition', () => { - const $expr = [ + describe('more complex condition', () => { + const $switchExpr = [ '$switch', [ [ @@ -506,45 +397,12 @@ describe('$switch', () => { ['$mathMult', -1], ] - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: 5 }, - }, - $expr - ) - ).toEqual(0) - - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: 15 }, - }, - $expr - ) - ).toEqual(150) - - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: 25 }, - }, - $expr - ) - ).toEqual(500) - - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: 30 }, - }, - $expr - ) - ).toEqual(-30) + _evTestCases([ + [5, $switchExpr, 0], + [15, $switchExpr, 150], + [25, $switchExpr, 500], + [30, $switchExpr, -30], + ]) }) }) @@ -554,29 +412,14 @@ describe('$switchKey', () => { key2: 'value2', key3: 'value3', } - - test('basic', () => { - const exp = ['$switchKey', options, 'DEFAULT_VALUE'] - - const expected = [ - [undefined, 'DEFAULT_VALUE'], - [null, 'DEFAULT_VALUE'], - ['key1', 'value1'], - ['key2', 'value2'], - ['key3', 'value3'], - ['key4', 'DEFAULT_VALUE'], - ] - - expected.forEach(([input, result]) => { - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: input }, - }, - exp - ) - ).toEqual(result) - }) - }) + const exp = ['$switchKey', options, 'DEFAULT_VALUE'] + + _evTestCases([ + [undefined, exp, 'DEFAULT_VALUE'], + [null, exp, 'DEFAULT_VALUE'], + ['key1', exp, 'value1'], + ['key2', exp, 'value2'], + ['key3', exp, 'value3'], + ['key4', exp, 'DEFAULT_VALUE'], + ]) }) diff --git a/src/expressions/logical.ts b/src/expressions/logical.ts index b11d692..e7d5a0f 100644 --- a/src/expressions/logical.ts +++ b/src/expressions/logical.ts @@ -1,22 +1,22 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ - +import { indefiniteArrayOfType, tupleType, anyType } from '@orioro/typing' import { evaluate } from '../evaluate' import { Expression, EvaluationContext, PlainObject, - ExpressionInterpreterSpec, + InterpreterSpec, } from '../types' /** + * @todo $and Add 'strict' mode option * @function $and * @param {Array} expressionsExp * @returns {Boolean} */ -export const $and: ExpressionInterpreterSpec = [ - (expressions: Expression[], context: EvaluationContext): boolean => - expressions.every((exp) => Boolean(evaluate(context, exp))), - [['array', 'undefined']], +export const $and: InterpreterSpec = [ + (values: any[]): boolean => values.every((value) => Boolean(value)), + [indefiniteArrayOfType('any')], ] /** @@ -24,10 +24,9 @@ export const $and: ExpressionInterpreterSpec = [ * @param {Array} expressionsExp * @returns {Boolean} */ -export const $or: ExpressionInterpreterSpec = [ - (expressions: Expression[], context: EvaluationContext): boolean => - expressions.some((exp) => Boolean(evaluate(context, exp))), - [['array', 'undefined']], +export const $or: InterpreterSpec = [ + (values: Expression[]): boolean => values.some((value) => Boolean(value)), + [indefiniteArrayOfType('any')], ] /** @@ -35,20 +34,16 @@ export const $or: ExpressionInterpreterSpec = [ * @param {Array} expressionsExp * @returns {Boolean} */ -export const $not: ExpressionInterpreterSpec = [ - (value: any): boolean => !value, - ['any'], -] +export const $not: InterpreterSpec = [(value: any): boolean => !value, ['any']] /** * @function $nor * @param {Array} expressionsExp * @returns {Boolean} */ -export const $nor: ExpressionInterpreterSpec = [ - (expressions: Expression[], context: EvaluationContext): boolean => - expressions.every((exp) => !evaluate(context, exp)), - ['array'], +export const $nor: InterpreterSpec = [ + (values: Expression[]): boolean => values.every((value) => !value), + [indefiniteArrayOfType('any')], ] /** @@ -57,7 +52,7 @@ export const $nor: ExpressionInterpreterSpec = [ * @param {Boolean} expressionB * @returns {Boolean} */ -export const $xor: ExpressionInterpreterSpec = [ +export const $xor: InterpreterSpec = [ (valueA: any, valueB: any): boolean => Boolean(valueA) !== Boolean(valueB), ['any', 'any'], ] @@ -69,40 +64,53 @@ export const $xor: ExpressionInterpreterSpec = [ * @param {Expression} elseExp * @returns {*} result */ -export const $if: ExpressionInterpreterSpec = [ +export const $if: InterpreterSpec = [ ( condition: any, thenExp: Expression, elseExp: Expression, context: EvaluationContext ): any => + // Usage of `evaluate` inside the expression does not affect + // its sync/async usage, as the logic of the expression operation + // does not depend on any evaluation ran inside itself. + // For example: condition MUST be evaluated outside expression + // interpreter logic by the argument resolvers because otherwise + // the logic for handling promises would have to be inside + // the expression interpreter. + // On the other hand, the return value is ignored by the expression interpreter: + // whether it returns a value or a promise is not important to its logic condition ? evaluate(context, thenExp) : evaluate(context, elseExp), [ 'any', - null, // Only evaluate if condition is satisfied - null, // Only evaluate if condition is not satisfied + anyType({ delayEvaluation: true }), // Only evaluate if condition is satisfied + anyType({ delayEvaluation: true }), // Only evaluate if condition is not satisfied ], ] type Case = [Expression, Expression] /** + * @todo logical Write test to ensure delayed evaluation * @function $switch * @param {Array} cases * @param {Expression} defaultExp * @returns {*} result */ -export const $switch: ExpressionInterpreterSpec = [ +export const $switch: InterpreterSpec = [ (cases: Case[], defaultExp: Expression, context: EvaluationContext): any => { - const correspondingCase = cases.find(([conditionExp]) => - Boolean(evaluate(context, conditionExp)) - ) + const correspondingCase = cases.find(([condition]) => Boolean(condition)) return correspondingCase ? evaluate(context, correspondingCase[1]) : evaluate(context, defaultExp) }, - ['array', null], + [ + indefiniteArrayOfType( + tupleType(['any', anyType({ delayEvaluation: true })]) + ), + anyType({ delayEvaluation: true }), + ], { defaultParam: -1, }, @@ -117,10 +125,10 @@ export const $switch: ExpressionInterpreterSpec = [ * @param {String} ValueExp * @returns {*} */ -export const $switchKey: ExpressionInterpreterSpec = [ +export const $switchKey: InterpreterSpec = [ ( cases: PlainObject, - defaultExp: Expression | undefined = undefined, + defaultExp: Expression, value: any, context: EvaluationContext ): any => { @@ -130,7 +138,7 @@ export const $switchKey: ExpressionInterpreterSpec = [ ? evaluate(context, correspondingCase) : evaluate(context, defaultExp) }, - ['object', null, 'any'], + ['object', anyType({ delayEvaluation: true }), 'any'], ] export const LOGICAL_EXPRESSIONS = { diff --git a/src/expressions/math.spec.ts b/src/expressions/math.spec.ts index bc82b46..00e1287 100644 --- a/src/expressions/math.spec.ts +++ b/src/expressions/math.spec.ts @@ -1,121 +1,75 @@ -import { evaluate } from '../evaluate' -import { syncInterpreterList } from '../interpreter' import { VALUE_EXPRESSIONS } from './value' import { MATH_EXPRESSIONS } from './math' +import { _prepareEvaluateTestCases } from '../../spec/specUtil' -const interpreters = syncInterpreterList({ +const EXP = { ...VALUE_EXPRESSIONS, ...MATH_EXPRESSIONS, -}) +} -describe('operations', () => { - const context = { - interpreters, - scope: { - $$VALUE: 10, - }, - } +const _evTestCases = _prepareEvaluateTestCases(EXP) - test('$mathSum', () => expect(evaluate(context, ['$mathSum', 5])).toEqual(15)) - test('$mathSub', () => expect(evaluate(context, ['$mathSub', 5])).toEqual(5)) - test('$mathMult', () => - expect(evaluate(context, ['$mathMult', 5])).toEqual(50)) - test('$mathDiv', () => expect(evaluate(context, ['$mathDiv', 5])).toEqual(2)) - test('$mathMod', () => expect(evaluate(context, ['$mathMod', 3])).toEqual(1)) - test('$mathPow', () => - expect(evaluate(context, ['$mathPow', 3])).toEqual(1000)) +describe('operations', () => { + _evTestCases([ + [10, ['$mathSum', 5], 15], + [10, ['$mathSub', 5], 5], + [10, ['$mathMult', 5], 50], + [10, ['$mathDiv', 5], 2], + [10, ['$mathMod', 3], 1], + [10, ['$mathPow', 3], 1000], + ]) }) -test('$mathAbs', () => { - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: 10 }, - }, - ['$mathAbs'] - ) - ).toEqual(10) - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: -10 }, - }, - ['$mathAbs'] - ) - ).toEqual(10) +describe('$mathAbs', () => { + _evTestCases([ + [10, ['$mathAbs'], 10], + [-10, ['$mathAbs'], 10], + ]) }) describe('$mathMax', () => { - const context = { - interpreters, - scope: { $$VALUE: 10 }, - } - - test('single value', () => { - expect(evaluate(context, ['$mathMax', 5])).toEqual(10) - expect(evaluate(context, ['$mathMax', 15])).toEqual(15) + describe('single value', () => { + _evTestCases([ + [10, ['$mathMax', 5], 10], + [10, ['$mathMax', 15], 15], + ]) }) - test('array of values', () => { - expect(evaluate(context, ['$mathMax', []])).toEqual(10) - expect(evaluate(context, ['$mathMax', [0, 5]])).toEqual(10) - expect(evaluate(context, ['$mathMax', [5, 15]])).toEqual(15) + describe('array of values', () => { + _evTestCases([ + [10, ['$mathMax', []], 10], + [10, ['$mathMax', [0, 5]], 10], + [10, ['$mathMax', [5, 15]], 15], + ]) }) }) describe('$mathMin', () => { - const context = { - interpreters, - scope: { $$VALUE: 10 }, - } - - test('single value', () => { - expect(evaluate(context, ['$mathMin', 5])).toEqual(5) - expect(evaluate(context, ['$mathMin', 15])).toEqual(10) + describe('single value', () => { + _evTestCases([ + [10, ['$mathMin', 5], 5], + [10, ['$mathMin', 15], 10], + ]) }) - test('array of values', () => { - expect(evaluate(context, ['$mathMin', []])).toEqual(10) - expect(evaluate(context, ['$mathMin', [0, 5]])).toEqual(0) - expect(evaluate(context, ['$mathMin', [5, 15]])).toEqual(5) - expect(evaluate(context, ['$mathMin', [25, 15]])).toEqual(10) + describe('array of values', () => { + _evTestCases([ + [10, ['$mathMin', []], 10], + [10, ['$mathMin', [0, 5]], 0], + [10, ['$mathMin', [5, 15]], 5], + [10, ['$mathMin', [25, 15]], 10], + ]) }) }) -test('$mathRound', () => { - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: 10.1 }, - }, - ['$mathRound'] - ) - ).toEqual(10) +describe('$mathRound', () => { + _evTestCases([[10.1, ['$mathRound'], 10]]) }) -test('$mathFloor', () => { - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: 10.1 }, - }, - ['$mathFloor'] - ) - ).toEqual(10) +describe('$mathFloor', () => { + _evTestCases([[10.1, ['$mathFloor'], 10]]) }) -test('$mathCeil', () => { - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: 10.1 }, - }, - ['$mathCeil'] - ) - ).toEqual(11) +describe('$mathCeil', () => { + _evTestCases([[10.1, ['$mathCeil'], 11]]) }) diff --git a/src/expressions/math.ts b/src/expressions/math.ts index 1798558..87514f4 100644 --- a/src/expressions/math.ts +++ b/src/expressions/math.ts @@ -1,4 +1,4 @@ -import { ExpressionInterpreterSpec } from '../types' +import { InterpreterSpec } from '../types' /** * @function $mathSum @@ -6,7 +6,7 @@ import { ExpressionInterpreterSpec } from '../types' * @param {Number} [base=$$VALUE] * @returns {Number} result */ -export const $mathSum: ExpressionInterpreterSpec = [ +export const $mathSum: InterpreterSpec = [ (sum: number, base: number): number => base + sum, ['number', 'number'], ] @@ -17,7 +17,7 @@ export const $mathSum: ExpressionInterpreterSpec = [ * @param {Number} [base=$$VALUE] * @returns {Number} result */ -export const $mathSub: ExpressionInterpreterSpec = [ +export const $mathSub: InterpreterSpec = [ (sub: number, base: number): number => base - sub, ['number', 'number'], ] @@ -28,7 +28,7 @@ export const $mathSub: ExpressionInterpreterSpec = [ * @param {Number} [base=$$VALUE] * @returns {Number} result */ -export const $mathMult: ExpressionInterpreterSpec = [ +export const $mathMult: InterpreterSpec = [ (mult: number, base: number): number => base * mult, ['number', 'number'], ] @@ -39,7 +39,7 @@ export const $mathMult: ExpressionInterpreterSpec = [ * @param {Number} dividend * @returns {Number} result */ -export const $mathDiv: ExpressionInterpreterSpec = [ +export const $mathDiv: InterpreterSpec = [ (divisor: number, dividend: number): number => dividend / divisor, ['number', 'number'], ] @@ -50,7 +50,7 @@ export const $mathDiv: ExpressionInterpreterSpec = [ * @param {Number} dividend * @returns {Number} result */ -export const $mathMod: ExpressionInterpreterSpec = [ +export const $mathMod: InterpreterSpec = [ (divisor: number, dividend: number): number => dividend % divisor, ['number', 'number'], ] @@ -61,7 +61,7 @@ export const $mathMod: ExpressionInterpreterSpec = [ * @param {Number} [base=$$VALUE] * @returns {Number} result */ -export const $mathPow: ExpressionInterpreterSpec = [ +export const $mathPow: InterpreterSpec = [ (exponent: number, base: number): number => Math.pow(base, exponent), ['number', 'number'], ] @@ -71,7 +71,7 @@ export const $mathPow: ExpressionInterpreterSpec = [ * @param {Number} [value=$$VALUE] * @returns {Number} result */ -export const $mathAbs: ExpressionInterpreterSpec = [ +export const $mathAbs: InterpreterSpec = [ (value: number): number => Math.abs(value), ['number'], ] @@ -82,7 +82,7 @@ export const $mathAbs: ExpressionInterpreterSpec = [ * @param {Number} [value=$$VALUE] * @returns {Number} result */ -export const $mathMax: ExpressionInterpreterSpec = [ +export const $mathMax: InterpreterSpec = [ (otherValue: number | number[], value: number): number => Array.isArray(otherValue) ? Math.max(value, ...otherValue) @@ -96,7 +96,7 @@ export const $mathMax: ExpressionInterpreterSpec = [ * @param {Number} [value=$$VALUE] * @returns {Number} result */ -export const $mathMin: ExpressionInterpreterSpec = [ +export const $mathMin: InterpreterSpec = [ (otherValue: number | number[], value: number): number => Array.isArray(otherValue) ? Math.min(value, ...otherValue) @@ -109,7 +109,7 @@ export const $mathMin: ExpressionInterpreterSpec = [ * @param {Number} [value=$$VALUE] * @returns {Number} result */ -export const $mathRound: ExpressionInterpreterSpec = [ +export const $mathRound: InterpreterSpec = [ (value: number): number => Math.round(value), ['number'], ] @@ -119,7 +119,7 @@ export const $mathRound: ExpressionInterpreterSpec = [ * @param {Number} [value=$$VALUE] * @returns {Number} result */ -export const $mathFloor: ExpressionInterpreterSpec = [ +export const $mathFloor: InterpreterSpec = [ (value: number): number => Math.floor(value), ['number'], ] @@ -128,7 +128,7 @@ export const $mathFloor: ExpressionInterpreterSpec = [ * @param {Number} [value=$$VALUE] * @returns {Number} result */ -export const $mathCeil: ExpressionInterpreterSpec = [ +export const $mathCeil: InterpreterSpec = [ (value: number): number => Math.ceil(value), ['number'], ] diff --git a/src/expressions/number.spec.ts b/src/expressions/number.spec.ts index 86889f1..2e32470 100644 --- a/src/expressions/number.spec.ts +++ b/src/expressions/number.spec.ts @@ -1,73 +1,26 @@ -import { evaluate } from '../evaluate' -import { syncInterpreterList } from '../interpreter' import { VALUE_EXPRESSIONS } from './value' import { NUMBER_EXPRESSIONS } from './number' +import { _prepareEvaluateTestCases } from '../../spec/specUtil' -const interpreters = syncInterpreterList({ +const EXP = { ...VALUE_EXPRESSIONS, ...NUMBER_EXPRESSIONS, -}) - -test('$numberInt', () => { - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: '10.50' }, - }, - ['$numberInt'] - ) - ).toEqual(10) +} - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: 10.5 }, - }, - ['$numberInt'] - ) - ).toEqual(10.5) +const _evTestCases = _prepareEvaluateTestCases(EXP) - expect(() => { - evaluate( - { - interpreters, - scope: { $$VALUE: true }, - }, - ['$numberInt'] - ) - }).toThrow(TypeError) +describe('$numberInt', () => { + _evTestCases([ + ['10.50', ['$numberInt'], 10], + [10.5, ['$numberInt'], 10.5], + [true, ['$numberInt'], TypeError], + ]) }) -test('$numberFloat', () => { - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: '10.50' }, - }, - ['$numberFloat'] - ) - ).toEqual(10.5) - - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: 10.5 }, - }, - ['$numberInt'] - ) - ).toEqual(10.5) - - expect(() => { - evaluate( - { - interpreters, - scope: { $$VALUE: true }, - }, - ['$numberInt'] - ) - }).toThrow(TypeError) +describe('$numberFloat', () => { + _evTestCases([ + ['10.50', ['$numberFloat'], 10.5], + [10.5, ['$numberFloat'], 10.5], + [true, ['$numberFloat'], TypeError], + ]) }) diff --git a/src/expressions/number.ts b/src/expressions/number.ts index fc410b7..3292d8a 100644 --- a/src/expressions/number.ts +++ b/src/expressions/number.ts @@ -1,4 +1,4 @@ -import { ExpressionInterpreterSpec } from '../types' +import { InterpreterSpec } from '../types' /** * @function $numberInt @@ -6,7 +6,7 @@ import { ExpressionInterpreterSpec } from '../types' * @param {*} value * @returns {Number} */ -export const $numberInt: ExpressionInterpreterSpec = [ +export const $numberInt: InterpreterSpec = [ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types (radix: number = 10, value: any): number => { if (typeof value === 'number') { @@ -25,7 +25,7 @@ export const $numberInt: ExpressionInterpreterSpec = [ * @param {*} value * @returns {Number} */ -export const $numberFloat: ExpressionInterpreterSpec = [ +export const $numberFloat: InterpreterSpec = [ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types (value: any): number => { if (typeof value === 'number') { diff --git a/src/expressions/object.spec.ts b/src/expressions/object.spec.ts index a8fc738..898e1d3 100644 --- a/src/expressions/object.spec.ts +++ b/src/expressions/object.spec.ts @@ -1,208 +1,242 @@ -import { evaluate } from '../evaluate' -import { syncInterpreterList } from '../interpreter' import { $value } from './value' import { COMPARISON_EXPRESSIONS } from './comparison' import { ARRAY_EXPRESSIONS } from './array' import { OBJECT_EXPRESSIONS } from './object' import { STRING_EXPRESSIONS } from './string' +import { LOGICAL_EXPRESSIONS } from './logical' +import { _prepareEvaluateTestCases } from '../../spec/specUtil' -const interpreters = syncInterpreterList({ +const EXP = { $value, ...STRING_EXPRESSIONS, ...COMPARISON_EXPRESSIONS, ...ARRAY_EXPRESSIONS, + ...LOGICAL_EXPRESSIONS, ...OBJECT_EXPRESSIONS, -}) - -const context = { - interpreters, - scope: { - $$VALUE: { - name: 'João Silva', - age: 50, - mother: { - name: 'Maria do Carmo', - age: 76, - }, - father: { - name: 'Galvão Queiroz', - age: 74, - }, - }, - }, } +const _evTestCases = _prepareEvaluateTestCases(EXP) + describe('$objectMatches', () => { - test('path notation', () => { - expect( - evaluate(context, [ - '$objectMatches', - { - name: 'João Silva', - 'mother.name': 'Maria do Carmo', - }, - ]) - ).toEqual(true) + const DATA = { + name: 'João Silva', + age: 50, + mother: { + name: 'Maria do Carmo', + age: 76, + }, + father: { + name: 'Galvão Queiroz', + age: 74, + }, + } + + describe('path notation', () => { + _evTestCases([ + [ + DATA, + [ + '$objectMatches', + { + name: 'João Silva', + 'mother.name': 'Maria do Carmo', + }, + ], + true, + ], + ]) }) - test('object property equality', () => { - expect( - evaluate(context, [ - '$objectMatches', - { - mother: { - $eq: { - name: 'Maria do Carmo', - age: 76, + describe('object property equality', () => { + _evTestCases([ + [ + DATA, + [ + '$objectMatches', + { + mother: { + $eq: { + name: 'Maria do Carmo', + age: 76, + }, }, }, - }, - ]) - ).toEqual(true) - - expect( - evaluate(context, [ - '$objectMatches', - { - mother: { - $eq: { - name: 'Maria do Carmo', - age: 76, - someOtherProp: 'B', + ], + true, + ], + [ + DATA, + [ + '$objectMatches', + { + mother: { + $eq: { + name: 'Maria do Carmo', + age: 76, + someOtherProp: 'B', + }, }, }, - }, - ]) - ).toEqual(false) + ], + false, + ], + ]) }) - test('comparison operators', () => { - expect( - evaluate(context, [ - '$objectMatches', - { - age: { - $gte: 50, + describe('comparison operators', () => { + _evTestCases([ + [ + DATA, + [ + '$objectMatches', + { + age: { + $gte: 50, + }, }, - }, - ]) - ).toEqual(true) - - expect( - evaluate(context, [ - '$objectMatches', - { - age: { - $gte: 51, + ], + true, + ], + [ + DATA, + [ + '$objectMatches', + { + age: { + $gte: 51, + }, }, - }, - ]) - ).toEqual(false) + ], + false, + ], + ]) }) }) describe('$objectFormat', () => { + const DATA = { + name: 'João Silva', + age: 50, + mother: { + name: 'Maria do Carmo', + age: 76, + }, + father: { + name: 'Galvão Queiroz', + age: 74, + }, + } + describe('object root', () => { - test('simple transformation', () => { - expect( - evaluate(context, [ - '$objectFormat', + describe('simple transformation', () => { + _evTestCases([ + [ + DATA, + [ + '$objectFormat', + { + fatherName: 'father.name', + motherName: 'mother.name', + }, + ], { - fatherName: 'father.name', - motherName: 'mother.name', + fatherName: 'Galvão Queiroz', + motherName: 'Maria do Carmo', }, - ]) - ).toEqual({ - fatherName: 'Galvão Queiroz', - motherName: 'Maria do Carmo', - }) + ], + ]) }) - test('basic', () => { - expect( - evaluate(context, [ - '$objectFormat', + describe('basic', () => { + _evTestCases([ + [ + DATA, + [ + '$objectFormat', + { + fatherName: 'father.name', + motherNameIsMariaDoCarmo: [ + '$eq', + 'Maria do Carmo', + ['$value', 'mother.name'], + ], + parentNames: ['father.name', 'mother.name'], + }, + ], { - fatherName: 'father.name', - motherNameIsMariaDoCarmo: [ - '$eq', - 'Maria do Carmo', - ['$value', 'mother.name'], - ], - parentNames: ['father.name', 'mother.name'], + fatherName: 'Galvão Queiroz', + motherNameIsMariaDoCarmo: true, + parentNames: ['Galvão Queiroz', 'Maria do Carmo'], }, - ]) - ).toEqual({ - fatherName: 'Galvão Queiroz', - motherNameIsMariaDoCarmo: true, - parentNames: ['Galvão Queiroz', 'Maria do Carmo'], - }) + ], + ]) }) }) describe('array root', () => { - test('basic', () => { - expect( - evaluate(context, [ - '$objectFormat', - ['name', 'father.name', 'mother.name'], - ]) - ).toEqual(['João Silva', 'Galvão Queiroz', 'Maria do Carmo']) + describe('basic', () => { + _evTestCases([ + [ + DATA, + ['$objectFormat', ['name', 'father.name', 'mother.name']], + ['João Silva', 'Galvão Queiroz', 'Maria do Carmo'], + ], + ]) }) - test('expression items', () => { - expect( - evaluate(context, [ - '$objectFormat', + describe('expression items', () => { + _evTestCases([ + [ + DATA, [ + '$objectFormat', [ - '$stringConcat', - ['$value', 'father.name'], - ['$value', 'mother.name'], + [ + '$stringConcat', + ['$value', 'father.name'], + ['$value', 'mother.name'], + ], + 'name', + 'father.name', + 'mother.name', ], - 'name', - 'father.name', - 'mother.name', ], - ]) - ).toEqual([ - 'Maria do CarmoGalvão Queiroz', - 'João Silva', - 'Galvão Queiroz', - 'Maria do Carmo', + [ + 'Maria do CarmoGalvão Queiroz', + 'João Silva', + 'Galvão Queiroz', + 'Maria do Carmo', + ], + ], ]) }) - test('with object items', () => { - expect( - evaluate(context, [ - '$objectFormat', + describe('with object items', () => { + _evTestCases([ + [ + DATA, [ - 'father.name', - { - fatherName: 'father.name', - }, + '$objectFormat', + [ + 'father.name', + { + fatherName: 'father.name', + }, + ], ], - ]) - ).toEqual(['Galvão Queiroz', { fatherName: 'Galvão Queiroz' }]) + ['Galvão Queiroz', { fatherName: 'Galvão Queiroz' }], + ], + ]) }) }) }) describe('$objectDefaults', () => { - test('simple', () => { - expect( - evaluate( - { - interpreters, - scope: { - $$VALUE: { - propA: 'valueA', - propB: 'valueB', - }, - }, - }, + describe('simple', () => { + _evTestCases([ + [ + { propA: 'valueA', propB: 'valueB' }, [ '$objectDefaults', { @@ -210,28 +244,20 @@ describe('$objectDefaults', () => { propB: 'defaultB', propC: 'defaultC', }, - ] - ) - ).toEqual({ - propA: 'valueA', - propB: 'valueB', - propC: 'defaultC', - }) + ], + { propA: 'valueA', propB: 'valueB', propC: 'defaultC' }, + ], + ]) }) - test('nested object', () => { - expect( - evaluate( + describe('nested object', () => { + _evTestCases([ + [ { - interpreters, - scope: { - $$VALUE: { - propA: 'valueA', - propB: 'valueB', - propC: { - propCA: 'valueCA', - }, - }, + propA: 'valueA', + propB: 'valueB', + propC: { + propCA: 'valueCA', }, }, [ @@ -245,69 +271,62 @@ describe('$objectDefaults', () => { }, propD: 'defaultValueD', }, - ] - ) - ).toEqual({ - propA: 'valueA', - propB: 'valueB', - propC: { - propCA: 'valueCA', - propCB: 'defaultValueCB', - }, - propD: 'defaultValueD', - }) + ], + { + propA: 'valueA', + propB: 'valueB', + propC: { + propCA: 'valueCA', + propCB: 'defaultValueCB', + }, + propD: 'defaultValueD', + }, + ], + ]) }) - test('nested array', () => { - const context = { - interpreters, - scope: { - $$VALUE: { + describe('nested array', () => { + _evTestCases([ + [ + { propA: 'valueA', propB: [{ id: 'B0' }, { id: 'B1' }, undefined, { id: 'B3' }], }, - }, - } - - const expression = [ - '$objectDefaults', - { - propA: 'defaultA', - propB: [ - { id: 'defaultB0', foo: 0 }, - { id: 'defaultB1', foo: 1 }, - { id: 'defaultB2', foo: 2 }, - { id: 'defaultB3', foo: 3 }, + [ + '$objectDefaults', + { + propA: 'defaultA', + propB: [ + { id: 'defaultB0', foo: 0 }, + { id: 'defaultB1', foo: 1 }, + { id: 'defaultB2', foo: 2 }, + { id: 'defaultB3', foo: 3 }, + ], + propC: 'defaultC', + }, ], - propC: 'defaultC', - }, - ] - - expect(evaluate(context, expression)).toEqual({ - propA: 'valueA', - propB: [ - { id: 'B0', foo: 0 }, - { id: 'B1', foo: 1 }, - { id: 'defaultB2', foo: 2 }, - { id: 'B3', foo: 3 }, + { + propA: 'valueA', + propB: [ + { id: 'B0', foo: 0 }, + { id: 'B1', foo: 1 }, + { id: 'defaultB2', foo: 2 }, + { id: 'B3', foo: 3 }, + ], + propC: 'defaultC', + }, ], - propC: 'defaultC', - }) + ]) }) }) describe('$objectAssign', () => { - test('simple', () => { - expect( - evaluate( + describe('simple', () => { + _evTestCases([ + [ { - interpreters, - scope: { - $$VALUE: { - propA: 'valueA', - propB: 'valueB', - }, - }, + propA: 'valueA', + propB: 'valueB', }, [ '$objectAssign', @@ -315,30 +334,26 @@ describe('$objectAssign', () => { propA: 'assignA', propC: 'assignC', }, - ] - ) - ).toEqual({ - propA: 'assignA', - propB: 'valueB', - propC: 'assignC', - }) + ], + { + propA: 'assignA', + propB: 'valueB', + propC: 'assignC', + }, + ], + ]) }) - test('nested', () => { - expect( - evaluate( + describe('nested', () => { + _evTestCases([ + [ { - interpreters, - scope: { - $$VALUE: { - propA: 'valueA', - propB: { - propBA: 'valueBA', - propBB: 'valueBB', - }, - propC: 'valueC', - }, + propA: 'valueA', + propB: { + propBA: 'valueBA', + propBB: 'valueBB', }, + propC: 'valueC', }, [ '$objectAssign', @@ -348,33 +363,26 @@ describe('$objectAssign', () => { propBB: 'assignBB', }, }, - ] - ) - ).toEqual({ - propA: 'assignA', - propB: { - propBA: 'valueBA', - propBB: 'assignBB', - }, - propC: 'valueC', - }) + ], + { + propA: 'assignA', + propB: { + propBA: 'valueBA', + propBB: 'assignBB', + }, + propC: 'valueC', + }, + ], + ]) }) }) -test('$objectKeys', () => { - expect( - evaluate( - { - interpreters, - scope: { - $$VALUE: { - key1: 'value1', - key2: 'value2', - key3: 'value3', - }, - }, - }, - ['$objectKeys'] - ) - ).toEqual(['key1', 'key2', 'key3']) +describe('$objectKeys', () => { + _evTestCases([ + [ + { key1: 'value1', key2: 'value2', key3: 'value3' }, + ['$objectKeys'], + ['key1', 'key2', 'key3'], + ], + ]) }) diff --git a/src/expressions/object.ts b/src/expressions/object.ts index fe16f30..ba9c5f9 100644 --- a/src/expressions/object.ts +++ b/src/expressions/object.ts @@ -1,15 +1,13 @@ -import { get, set, isPlainObject } from 'lodash' +import { get, isPlainObject } from 'lodash' -import { evaluate, isExpression } from '../evaluate' +import { evaluate } from '../evaluate' + +import { $objectFormatSync, $objectFormatAsync } from './object/objectFormat' import { objectDeepApplyDefaults } from '../util/deepApplyDefaults' import { objectDeepAssign } from '../util/deepAssign' -import { - EvaluationContext, - PlainObject, - ExpressionInterpreterSpec, -} from '../types' +import { EvaluationContext, PlainObject, InterpreterSpec } from '../types' /** * @function $objectMatches @@ -17,7 +15,7 @@ import { * @param {Object} [value=$$VALUE] * @returns {Boolean} matches */ -export const $objectMatches: ExpressionInterpreterSpec = [ +export const $objectMatches: InterpreterSpec = [ ( criteriaByPath: PlainObject, value: PlainObject, @@ -31,89 +29,49 @@ export const $objectMatches: ExpressionInterpreterSpec = [ ) } - return paths.every((path) => { - // - // pathCriteria is either: - // - a literal value to be compared against (array, string, number) - // - or an expression to be evaluated against the value - // - const pathCriteria = isPlainObject(criteriaByPath[path]) - ? criteriaByPath[path] - : { $eq: criteriaByPath[path] } - - return evaluate( - { - ...context, - scope: { $$VALUE: get(value, path) }, - }, - ['$matches', pathCriteria] - ) - }) + return evaluate(context, [ + '$and', + paths.map((path) => [ + '$matches', + isPlainObject(criteriaByPath[path]) + ? criteriaByPath[path] + : { $eq: criteriaByPath[path] }, + get(value, path), + ]), + ]) + + // return paths.every((path) => { + // // + // // pathCriteria is either: + // // - a literal value to be compared against (array, string, number) + // // - or an expression to be evaluated against the value + // // + // const pathCriteria = isPlainObject(criteriaByPath[path]) + // ? criteriaByPath[path] + // : { $eq: criteriaByPath[path] } + + // return evaluate( + // { + // ...context, + // scope: { $$VALUE: get(value, path) }, + // }, + // ['$matches', pathCriteria] + // ) + // }) }, ['object', 'object'], ] -const _formatEvaluate = (context, targetValue, source) => { - targetValue = - typeof targetValue === 'string' ? ['$value', targetValue] : targetValue - - if (isExpression(context.interpreters, targetValue)) { - return evaluate( - { - ...context, - scope: { $$VALUE: source }, - }, - targetValue - ) - } else if (Array.isArray(targetValue)) { - return _formatArray(context, targetValue, source) - } else if (isPlainObject(targetValue)) { - return _formatObject(context, targetValue, source) - } else { - throw `Invalid $objectFormat item: ${targetValue}` - } -} - -const _formatArray = ( - context: EvaluationContext, - format: any[], - source: any -): any[] => - format.map((targetValue) => _formatEvaluate(context, targetValue, source)) - -const _formatObject = ( - context: EvaluationContext, - format: PlainObject, - source: any -): PlainObject => { - const targetPaths = Object.keys(format) - - return targetPaths.reduce((acc, targetPath) => { - set(acc, targetPath, _formatEvaluate(context, format[targetPath], source)) - - return acc - }, {}) -} - /** * @function $objectFormat * @param {Object | Array} format * @param {*} [source=$$VALUE] * @returns {Object | Array} object */ -export const $objectFormat: ExpressionInterpreterSpec = [ - ( - format: PlainObject | any[], - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - source: any, - context: EvaluationContext - ): PlainObject | any[] => { - return Array.isArray(format) - ? _formatArray(context, format, source) - : _formatObject(context, format, source) - }, - [['object', 'array'], 'any'], -] +export const $objectFormat = { + sync: $objectFormatSync, + async: $objectFormatAsync, +} /** * @function $objectDefaults @@ -121,7 +79,7 @@ export const $objectFormat: ExpressionInterpreterSpec = [ * @param {Object} [base=$$VALUE] * @returns {Object} */ -export const $objectDefaults: ExpressionInterpreterSpec = [ +export const $objectDefaults: InterpreterSpec = [ (defaultValues: PlainObject, base: PlainObject): PlainObject => objectDeepApplyDefaults(base, defaultValues), ['object', 'object'], @@ -133,7 +91,7 @@ export const $objectDefaults: ExpressionInterpreterSpec = [ * @param {Object} [base=$$VALUE] * @returns {Object} */ -export const $objectAssign: ExpressionInterpreterSpec = [ +export const $objectAssign: InterpreterSpec = [ (values: PlainObject, base: PlainObject): PlainObject => objectDeepAssign(base, values), ['object', 'object'], @@ -144,7 +102,7 @@ export const $objectAssign: ExpressionInterpreterSpec = [ * @param {Object} object * @returns {String[]} */ -export const $objectKeys: ExpressionInterpreterSpec = [ +export const $objectKeys: InterpreterSpec = [ (obj: PlainObject): string[] => Object.keys(obj), ['object'], ] diff --git a/src/expressions/object/objectFormat.ts b/src/expressions/object/objectFormat.ts new file mode 100644 index 0000000..ee15e57 --- /dev/null +++ b/src/expressions/object/objectFormat.ts @@ -0,0 +1,88 @@ +import { set, isPlainObject } from 'lodash' + +import { promiseResolveObject } from '../../util/promiseResolveObject' + +import { evaluate, isExpression } from '../../evaluate' + +import { + EvaluationContext, + PlainObject, + InterpreterSpecSingle, +} from '../../types' + +const _parseFormat = (format) => + typeof format === 'string' ? ['$value', format] : format + +const _formatSync = (context, format, source) => { + format = _parseFormat(format) + + if (isExpression(context.interpreters, format)) { + return evaluate( + { + ...context, + scope: { $$VALUE: source }, + }, + format + ) + } else if (Array.isArray(format)) { + return format.map((nestedTargetValue) => + _formatSync(context, nestedTargetValue, source) + ) + } else if (isPlainObject(format)) { + const targetPaths = Object.keys(format) + + return targetPaths.reduce((acc, targetPath) => { + set(acc, targetPath, _formatSync(context, format[targetPath], source)) + + return acc + }, {}) + } else { + throw `Invalid $objectFormat item: ${format}` + } +} + +const _formatAsync = (context, format, source) => { + format = _parseFormat(format) + + if (isExpression(context.interpreters, format)) { + return evaluate( + { + ...context, + scope: { $$VALUE: source }, + }, + format + ) + } else if (Array.isArray(format)) { + return Promise.all( + format.map((nestedTargetValue) => + _formatAsync(context, nestedTargetValue, source) + ) + ) + } else if (isPlainObject(format)) { + return promiseResolveObject(format, (propertyValue) => + _formatAsync(context, propertyValue, source) + ) + } else { + throw `Invalid $objectFormat item: ${format}` + } +} + +export const $objectFormatSync: InterpreterSpecSingle = [ + ( + format: PlainObject | any[], + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + source: any, + context: EvaluationContext + ): PlainObject | any[] => _formatSync(context, format, source), + [['object', 'array'], 'any'], +] + +export const $objectFormatAsync: InterpreterSpecSingle = [ + ( + format: PlainObject | any[], + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + source: any, + context: EvaluationContext + ): PlainObject | any[] => _formatAsync(context, format, source), + [['object', 'array'], 'any'], +] diff --git a/src/expressions/string.spec.ts b/src/expressions/string.spec.ts index 705a700..7ea3264 100644 --- a/src/expressions/string.spec.ts +++ b/src/expressions/string.spec.ts @@ -1,14 +1,15 @@ -import { evaluate } from '../evaluate' -import { syncInterpreterList } from '../interpreter' import { VALUE_EXPRESSIONS } from './value' import { STRING_EXPRESSIONS } from './string' +import { _prepareEvaluateTestCases } from '../../spec/specUtil' -const interpreters = syncInterpreterList({ +const EXP = { ...VALUE_EXPRESSIONS, ...STRING_EXPRESSIONS, -}) +} + +const _evTestCases = _prepareEvaluateTestCases(EXP) -test('$string', () => { +describe('$string', () => { const expectations = [ ['some string', 'some string'], [10.5, '10.5'], @@ -36,125 +37,80 @@ test('$string', () => { [new WeakMap(), '[object WeakMap]'], ] - expectations.forEach(([input, result]) => { - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: input }, - }, - ['$string'] - ) - ).toEqual(result) - }) -}) - -test('$stringStartsWith', () => { - const context = { - interpreters, - scope: { $$VALUE: 'some_string' }, - } - - expect(evaluate(context, ['$stringStartsWith', 'some'])).toEqual(true) - expect(evaluate(context, ['$stringStartsWith', 'somethingelse'])).toEqual( - false + _evTestCases( + expectations.map(([input, result]) => [input, ['$string'], result]) ) }) -test('$stringLength', () => { - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: 'some_string' }, - }, - ['$stringLength'] - ) - ).toEqual(11) +describe('$stringStartsWith', () => { + _evTestCases([ + ['some_string', ['$stringStartsWith', 'some'], true], + ['some_string', ['$stringStartsWith', 'somethingelse'], false], + ]) }) -test('$stringSubstr', () => { - const context = { - interpreters, - scope: { $$VALUE: 'some_string' }, - } +describe('$stringLength', () => { + _evTestCases([ + ['some_string', ['$stringLength'], 11], + ['', ['$stringLength'], 0], + ]) +}) - expect(evaluate(context, ['$stringSubstr', 0, 4])).toEqual('some') - expect(evaluate(context, ['$stringSubstr', 4])).toEqual('_string') +describe('$stringSubstr', () => { + _evTestCases([ + ['some_string', ['$stringSubstr', 0, 4], 'some'], + ['some_string', ['$stringSubstr', 4], '_string'], + ]) }) describe('$stringConcat', () => { - const context = { - interpreters, - scope: { $$VALUE: 'some_string' }, - } - - test('single string', () => { - expect(evaluate(context, ['$stringConcat', '_another_string'])).toEqual( - 'some_string_another_string' - ) + describe('single string', () => { + _evTestCases([ + [ + 'some_string', + ['$stringConcat', '_another_string'], + 'some_string_another_string', + ], + ]) }) - test('array of strings', () => { - expect( - evaluate(context, ['$stringConcat', ['some', 'other', 'strings']]) - ).toEqual('some_stringsomeotherstrings') + describe('array of strings', () => { + _evTestCases([ + [ + 'some_string', + ['$stringConcat', ['some', 'other', 'strings']], + 'some_stringsomeotherstrings', + ], + ]) }) }) -test('$stringTrim', () => { - const context = { - interpreters, - scope: { $$VALUE: ' some string ' }, - } - - expect(evaluate(context, ['$stringTrim'])).toEqual('some string') +describe('$stringTrim', () => { + _evTestCases([[' some string ', ['$stringTrim'], 'some string']]) }) -test('$stringPadStart', () => { - const context = { - interpreters, - scope: { $$VALUE: '1' }, - } - - expect(evaluate(context, ['$stringPadStart', 3, '0'])).toEqual('001') +describe('$stringPadStart', () => { + _evTestCases([['1', ['$stringPadStart', 3, '0'], '001']]) }) -test('$stringPadEnd', () => { - const context = { - interpreters, - scope: { $$VALUE: '1' }, - } - - expect(evaluate(context, ['$stringPadEnd', 3, '*'])).toEqual('1**') +describe('$stringPadEnd', () => { + _evTestCases([['1', ['$stringPadEnd', 3, '*'], '1**']]) }) -test('$stringToUpperCase', () => { - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: 'String Multi Case' }, - }, - ['$stringToUpperCase'] - ) - ).toEqual('STRING MULTI CASE') +describe('$stringToUpperCase', () => { + _evTestCases([ + ['String Multi Case', ['$stringToUpperCase'], 'STRING MULTI CASE'], + ]) }) -test('$stringToLowerCase', () => { - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: 'String Multi Case' }, - }, - ['$stringToLowerCase'] - ) - ).toEqual('string multi case') +describe('$stringToLowerCase', () => { + _evTestCases([ + ['String Multi Case', ['$stringToLowerCase'], 'string multi case'], + ]) }) describe('$stringInterpolate(data, string)', () => { - test('object', () => { + describe('object', () => { const data = { name: 'João', mother: { @@ -168,20 +124,11 @@ describe('$stringInterpolate(data, string)', () => { _special2: '_____', } - const template = - 'Olá, eu sou ${ name }. Minha mãe ${ mother.name }, meu pai ${ father.name }.' - - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: template }, - }, - ['$stringInterpolate', data] - ) - ).toEqual('Olá, eu sou João. Minha mãe Maria, meu pai Guilherme.') - const expectations = [ + [ + 'Olá, eu sou ${ name }. Minha mãe ${ mother.name }, meu pai ${ father.name }.', + 'Olá, eu sou João. Minha mãe Maria, meu pai Guilherme.', + ], ['name: ${ name }', 'name: João'], ['${name}', 'João'], ['${name }', 'João'], @@ -198,35 +145,25 @@ describe('$stringInterpolate(data, string)', () => { ['${ [] }', '${ [] }'], ] - expectations.forEach(([input, expected]) => { - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: input }, - }, - ['$stringInterpolate', data] - ) - ).toEqual(expected) - }) + _evTestCases( + expectations.map(([input, result]) => [ + input, + ['$stringInterpolate', data], + result, + ]) + ) }) - test('array', () => { + describe('array', () => { const data = ['first', 'second', 'third'] const template = '1: ${0}; 2: ${1}; 3: ${2}' - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: template }, - }, - ['$stringInterpolate', data] - ) - ).toEqual('1: first; 2: second; 3: third') + _evTestCases([ + [template, ['$stringInterpolate', data], '1: first; 2: second; 3: third'], + ]) }) - test('null / undefined / other type values', () => { + describe('null / undefined / other type values', () => { const template = '1: ${0}; 2: ${1}; 3: ${2}' const expectations = [ @@ -245,16 +182,12 @@ describe('$stringInterpolate(data, string)', () => { ], ] - expectations.forEach(([input, result]) => { - expect( - evaluate( - { - interpreters, - scope: { $$VALUE: template }, - }, - ['$stringInterpolate', input] - ) - ).toEqual(result) - }) + _evTestCases( + expectations.map(([data, result]) => [ + template, + ['$stringInterpolate', data], + result, + ]) + ) }) }) diff --git a/src/expressions/string.ts b/src/expressions/string.ts index 9bbcfa3..6fe7314 100644 --- a/src/expressions/string.ts +++ b/src/expressions/string.ts @@ -1,5 +1,5 @@ import { get } from 'lodash' -import { PlainObject, ExpressionInterpreterSpec } from '../types' +import { PlainObject, InterpreterSpec } from '../types' import { getType } from '@orioro/typing' @@ -45,7 +45,7 @@ const stringifyValue = (value) => { * @param {*} [value=$$VALUE] * @returns {String} */ -export const $string: ExpressionInterpreterSpec = [ +export const $string: InterpreterSpec = [ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types (value: any): string => stringifyValue(value), ['any'], @@ -57,7 +57,7 @@ export const $string: ExpressionInterpreterSpec = [ * @param {String} [str=$$VALUE] * @returns {Boolean} */ -export const $stringStartsWith: ExpressionInterpreterSpec = [ +export const $stringStartsWith: InterpreterSpec = [ (query: string, str: string): boolean => str.startsWith(query), ['string', 'string'], ] @@ -67,7 +67,7 @@ export const $stringStartsWith: ExpressionInterpreterSpec = [ * @param {String} [str=$$VALUE] * @returns {Number} */ -export const $stringLength: ExpressionInterpreterSpec = [ +export const $stringLength: InterpreterSpec = [ (str: string): number => str.length, ['string'], ] @@ -78,7 +78,7 @@ export const $stringLength: ExpressionInterpreterSpec = [ * @param {Number} end * @param {String} [str=$$VALUE] */ -export const $stringSubstr: ExpressionInterpreterSpec = [ +export const $stringSubstr: InterpreterSpec = [ (start: number, end: number | undefined, str: string): string => str.substring(start, end), ['number', ['number', 'undefined'], 'string'], @@ -90,7 +90,7 @@ export const $stringSubstr: ExpressionInterpreterSpec = [ * @param {String} [base=$$VALUE] * @returns {String} */ -export const $stringConcat: ExpressionInterpreterSpec = [ +export const $stringConcat: InterpreterSpec = [ (concat: string | string[], base: string): string => Array.isArray(concat) ? base.concat(concat.join('')) : base.concat(concat), [['string', 'array'], 'string'], @@ -101,7 +101,7 @@ export const $stringConcat: ExpressionInterpreterSpec = [ * @param {String} [str=$$VALUE] * @returns {String} */ -export const $stringTrim: ExpressionInterpreterSpec = [ +export const $stringTrim: InterpreterSpec = [ (str: string): string => str.trim(), ['string'], ] @@ -113,7 +113,7 @@ export const $stringTrim: ExpressionInterpreterSpec = [ * @param {String} [str=$$VALUE] * @returns {String} */ -export const $stringPadStart: ExpressionInterpreterSpec = [ +export const $stringPadStart: InterpreterSpec = [ (targetLength: number, padString: string, str: string): string => str.padStart(targetLength, padString), ['number', 'string', 'string'], @@ -126,7 +126,7 @@ export const $stringPadStart: ExpressionInterpreterSpec = [ * @param {String} [str=$$VALUE] * @returns {String} */ -export const $stringPadEnd: ExpressionInterpreterSpec = [ +export const $stringPadEnd: InterpreterSpec = [ (targetLength: number, padString: string, str: string): string => str.padEnd(targetLength, padString), ['number', 'string', 'string'], @@ -137,7 +137,7 @@ export const $stringPadEnd: ExpressionInterpreterSpec = [ * @param {String} value * @returns {String} */ -export const $stringToUpperCase: ExpressionInterpreterSpec = [ +export const $stringToUpperCase: InterpreterSpec = [ (str: string): string => str.toUpperCase(), ['string'], ] @@ -147,7 +147,7 @@ export const $stringToUpperCase: ExpressionInterpreterSpec = [ * @param {String} value * @returns {String} */ -export const $stringToLowerCase: ExpressionInterpreterSpec = [ +export const $stringToLowerCase: InterpreterSpec = [ (str: string): string => str.toLowerCase(), ['string'], ] @@ -187,7 +187,7 @@ const INTERPOLATABLE_TYPES = ['string', 'number'] * only interpreted as paths to the value. No logic * supported: loops, conditionals, etc. */ -export const $stringInterpolate: ExpressionInterpreterSpec = [ +export const $stringInterpolate: InterpreterSpec = [ (data: PlainObject | any[], template: string): string => template.replace(INTERPOLATION_REGEXP, (match, path) => { const value = get(data, path) diff --git a/src/expressions/type.spec.ts b/src/expressions/type.spec.ts index 236310e..3544730 100644 --- a/src/expressions/type.spec.ts +++ b/src/expressions/type.spec.ts @@ -1,49 +1,33 @@ -import { evaluate } from '../evaluate' -import { syncInterpreterList } from '../interpreter' import { $value } from './value' import { TYPE_EXPRESSIONS, typeExpressions } from './type' -import { testCases } from '@orioro/jest-util' +import { _prepareEvaluateTestCases } from '../../spec/specUtil' -const interpreters = syncInterpreterList({ +const EXP = { $value, ...TYPE_EXPRESSIONS, -}) +} + +const _evTestCases = _prepareEvaluateTestCases(EXP) describe('$type', () => { - testCases( - [ - ['some string', 'string'], - [10, 'number'], - [true, 'boolean'], - [[], 'array'], - [{}, 'object'], - [new Map(), 'map'], - [new Set(), 'set'], - [Symbol(), 'symbol'], - ], - (value) => - evaluate( - { - interpreters, - scope: { $$VALUE: value }, - }, - ['$type'] - ), - '$type' - ) + _evTestCases([ + ['some string', ['$type'], 'string'], + [10, ['$type'], 'number'], + [true, ['$type'], 'boolean'], + [[], ['$type'], 'array'], + [{}, ['$type'], 'object'], + [new Map(), ['$type'], 'map'], + [new Set(), ['$type'], 'set'], + [Symbol(), ['$type'], 'symbol'], + ]) }) describe('$isType', () => { - testCases( - [ - ['string', 'Some str', true], - ['string', 9, false], - ['number', 9, true], - ], - (type, value) => - evaluate({ interpreters, scope: { $$VALUE: value } }, ['$isType', type]), - '$isType' - ) + _evTestCases([ + ['Some str', ['$isType', 'string'], true], + [9, ['$isType', 'string'], false], + [9, ['$isType', 'number'], true], + ]) }) describe('typeExpressions(types)', () => { @@ -57,57 +41,34 @@ describe('typeExpressions(types)', () => { normalString: (value) => typeof value === 'string', }) - const customTypeInterpreters = syncInterpreterList({ + const customTypeExps = { $customType, $customIsType, + } + + const _evTestCases = _prepareEvaluateTestCases({ + ...EXP, + ...customTypeExps, }) describe('$customType', () => { - testCases( - [ - ['abc123', 'alphaNumericString'], - ['abc123-', 'normalString'], - ['abc', 'alphaOnlyString'], - ['abc123', 'alphaNumericString'], - ['123', 'numericOnlyString'], - ], - (value) => - evaluate( - { - interpreters: { - ...interpreters, - ...customTypeInterpreters, - }, - scope: { $$VALUE: value }, - }, - ['$customType'] - ), - '$customType' - ) + _evTestCases([ + ['abc123', ['$customType'], 'alphaNumericString'], + ['abc123-', ['$customType'], 'normalString'], + ['abc', ['$customType'], 'alphaOnlyString'], + ['abc123', ['$customType'], 'alphaNumericString'], + ['123', ['$customType'], 'numericOnlyString'], + ]) }) describe('$customIsType', () => { - testCases( - [ - ['alphaNumericString', 'abc123', true], - ['alphaNumericString', 'abc123-', false], - ['alphaOnlyString', 'abc', true], - ['alphaOnlyString', 'abc123', false], - ['numericOnlyString', '123', true], - ['numericOnlyString', 'abc123', false], - ], - (type, value) => - evaluate( - { - interpreters: { - ...interpreters, - ...customTypeInterpreters, - }, - scope: { $$VALUE: value }, - }, - ['$customIsType', type] - ), - '$customIsType' - ) + _evTestCases([ + ['abc123', ['$customIsType', 'alphaNumericString'], true], + ['abc123-', ['$customIsType', 'alphaNumericString'], false], + ['abc', ['$customIsType', 'alphaOnlyString'], true], + ['abc123', ['$customIsType', 'alphaOnlyString'], false], + ['123', ['$customIsType', 'numericOnlyString'], true], + ['abc123', ['$customIsType', 'numericOnlyString'], false], + ]) }) }) diff --git a/src/expressions/type.ts b/src/expressions/type.ts index 177a4fb..4ac0cdf 100644 --- a/src/expressions/type.ts +++ b/src/expressions/type.ts @@ -1,9 +1,9 @@ import { typing, CORE_TYPES } from '@orioro/typing' -import { TypeAlternatives, TypeMap, ExpressionInterpreterSpec } from '../types' +import { TypeAlternatives, TypeMap, InterpreterSpec } from '../types' export const typeExpressions = ( types: TypeAlternatives | TypeMap -): [ExpressionInterpreterSpec, ExpressionInterpreterSpec] => { +): [InterpreterSpec, InterpreterSpec] => { const { getType, isType, @@ -32,7 +32,7 @@ export const typeExpressions = ( * - weakmap * - weakset */ - const $type: ExpressionInterpreterSpec = [ + const $type: InterpreterSpec = [ (value: any): string => getType(value), ['any'], ] @@ -43,7 +43,7 @@ export const typeExpressions = ( * @param {*} value * @returns {Boolean} */ - const $isType: ExpressionInterpreterSpec = [ + const $isType: InterpreterSpec = [ (type: string, value: any): boolean => isType(type, value), [['string', 'array', 'object'], 'any'], ] diff --git a/src/expressions/value.spec.ts b/src/expressions/value.spec.ts index 85f3971..09169c8 100644 --- a/src/expressions/value.spec.ts +++ b/src/expressions/value.spec.ts @@ -1,21 +1,22 @@ -import { evaluate } from '../evaluate' -import { syncInterpreterList } from '../interpreter' import { MATH_EXPRESSIONS } from './math' import { LOGICAL_EXPRESSIONS } from './logical' import { COMPARISON_EXPRESSIONS } from './comparison' import { ARRAY_EXPRESSIONS } from './array' import { VALUE_EXPRESSIONS } from './value' +import { _prepareEvaluateTestCases } from '../../spec/specUtil' -const interpreters = syncInterpreterList({ +const EXP = { ...MATH_EXPRESSIONS, ...LOGICAL_EXPRESSIONS, ...COMPARISON_EXPRESSIONS, ...ARRAY_EXPRESSIONS, ...VALUE_EXPRESSIONS, -}) +} + +const _evTestCases = _prepareEvaluateTestCases(EXP) describe('$value', () => { - test('basic', () => { + describe('basic', () => { const data = { key1: 'Value 1', key2: 'Value 2', @@ -24,65 +25,48 @@ describe('$value', () => { }, } - const context = { - interpreters, - scope: { - $$VALUE: data, - }, - } + _evTestCases([ + [data, ['$value'], data], + [data, ['$value', 'key1'], 'Value 1'], + [data, ['$value', 'key3.key31'], 'Value 31'], + ]) + }) - expect(evaluate(context, ['$value'])).toEqual(data) - expect(evaluate(context, ['$value', 'key1'])).toEqual('Value 1') - expect(evaluate(context, ['$value', 'key3.key31'])).toEqual('Value 31') + describe("[['$value']]", () => { + _evTestCases([ + ['TEST', ['$value'], 'TEST'], + ['TEST', [['$value']], [['$value']]], + ]) }) }) describe('$evaluate', () => { - test('basic', () => { - const check = (value) => - evaluate( - { - interpreters, - scope: { - $$VALUE: value, - }, - }, - [ - '$arrayMap', - [ - '$if', - ['$evaluate', ['$value', '0'], ['$value', '$$PARENT_SCOPE']], - ['$value', '1'], - '-', - ], - [ - [['$gte', 10], '>10'], - [['$eq', 0, ['$mathMod', 2]], 'EVEN'], - [['$notEq', 0, ['$mathMod', 2]], 'ODD'], - ], - ] - ) + const EXP = [ + '$arrayMap', + [ + '$if', + ['$evaluate', ['$value', '0'], ['$value', '$$PARENT_SCOPE']], + ['$value', '1'], + '-', + ], + [ + [['$gte', 10], '>10'], + [['$eq', 0, ['$mathMod', 2]], 'EVEN'], + [['$notEq', 0, ['$mathMod', 2]], 'ODD'], + ], + ] - expect(check(6)).toEqual(['-', 'EVEN', '-']) - expect(check(5)).toEqual(['-', '-', 'ODD']) - expect(check(11)).toEqual(['>10', '-', 'ODD']) - expect(check(12)).toEqual(['>10', 'EVEN', '-']) - }) + _evTestCases([ + [6, EXP, ['-', 'EVEN', '-']], + [5, EXP, ['-', '-', 'ODD']], + [11, EXP, ['>10', '-', 'ODD']], + [12, EXP, ['>10', 'EVEN', '-']], + ]) }) describe('$literal', () => { - test('basic', () => { - const context = { - interpreters, - scope: { - $$VALUE: 'SOME_VALUE', - }, - } - - expect(evaluate(context, ['$value', '$$VALUE'])).toEqual('SOME_VALUE') - expect(evaluate(context, ['$literal', ['$value', '$$VALUE']])).toEqual([ - '$value', - '$$VALUE', - ]) - }) + _evTestCases([ + ['SOME_VALUE', ['$value', '$$VALUE'], 'SOME_VALUE'], + ['SOME_VALUE', ['$literal', ['$value', '$$VALUE']], ['$value', '$$VALUE']], + ]) }) diff --git a/src/expressions/value.ts b/src/expressions/value.ts index c5c37c7..229a30c 100644 --- a/src/expressions/value.ts +++ b/src/expressions/value.ts @@ -1,12 +1,13 @@ import { get } from 'lodash' import { evaluate } from '../evaluate' +import { anyType } from '@orioro/typing' import { Expression, EvaluationContext, EvaluationScope, - ExpressionInterpreterSpec, + InterpreterSpec, } from '../types' const PATH_VARIABLE_RE = /^\$\$.+/ @@ -15,11 +16,11 @@ export const $$VALUE: Expression = ['$value', '$$VALUE'] /** * @function $value - * @param {String} pathExp + * @param {String} path * @param {*} defaultExp * @returns {*} value */ -export const $value: ExpressionInterpreterSpec = [ +export const $value: InterpreterSpec = [ ( path: string = '$$VALUE', defaultExp: Expression, @@ -35,7 +36,7 @@ export const $value: ExpressionInterpreterSpec = [ ? evaluate(context, defaultExp) : value }, - [['string', 'undefined'], null], + [['string', 'undefined'], anyType({ delayEvaluation: true })], { defaultParam: -1, }, @@ -46,20 +47,22 @@ export const $value: ExpressionInterpreterSpec = [ * @param {*} value * @returns {*} */ -export const $literal: ExpressionInterpreterSpec = [ +export const $literal: InterpreterSpec = [ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types (value: any): any => value, - [null], + [anyType({ delayEvaluation: true })], { defaultParam: -1 }, ] /** + * @todo value Consider adding 'expression' type + * * @function $evaluate - * @param {Expression} expExp - * @param {Object | null} scopeExp + * @param {Expression} expression + * @param {Object} scope * @returns {*} */ -export const $evaluate: ExpressionInterpreterSpec = [ +export const $evaluate: InterpreterSpec = [ ( expression: Expression, scope: EvaluationScope, @@ -72,15 +75,7 @@ export const $evaluate: ExpressionInterpreterSpec = [ }, expression ), - [ - (context: EvaluationContext, expExp: Expression | any): Expression => - evaluate(context, expExp), - ( - context: EvaluationContext, - scopeExp: Expression | null = null - ): EvaluationScope => - scopeExp === null ? context.scope : evaluate(context, scopeExp), - ], + ['any', 'object'], { defaultParam: -1 }, ] diff --git a/src/index.spec.ts b/src/index.spec.ts index 012f125..f9848eb 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -1,5 +1,5 @@ import * as api from './index' test('public api', () => { - expect(Object.keys(api)).toMatchSnapshot() + expect(Object.keys(api).sort()).toMatchSnapshot() }) diff --git a/src/index.ts b/src/index.ts index cd68df7..91235a5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,7 +33,11 @@ export const ALL_EXPRESSIONS = { export * from './types' export * from './evaluate' -export * from './interpreter' +export * from './interpreter/syncParamResolver' +export * from './interpreter/syncInterpreter' +export * from './interpreter/asyncParamResolver' +export * from './interpreter/asyncInterpreter' +export * from './interpreter/interpreter' export * from './expressions/array' export * from './expressions/boolean' export * from './expressions/comparison' diff --git a/src/interpreter.ts b/src/interpreter.ts deleted file mode 100644 index 86f4b49..0000000 --- a/src/interpreter.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { isPlainObject } from 'lodash' - -import { - EvaluationContext, - ExpressionInterpreter, - ExpressionInterpreterList, - ExpressionInterpreterFunction, - ExpressionInterpreterFunctionList, -} from './types' - -import { evaluateTyped, evaluateTypedAsync } from './evaluate' - -import { ParamResolver } from './types' - -const _paramResolverNoop = (context: EvaluationContext, arg: any): any => arg - -const _isExpectedType = (resolver: ParamResolver): boolean => - Array.isArray(resolver) || - isPlainObject(resolver) || - typeof resolver === 'string' - -// export const interpreter = ( -// interpreterFn: (...args: any[]) => any, -// paramResolvers: ParamResolver[], -// defaultScopeValue: boolean | number = true -// ) => [ -// interpreterFn, -// paramResolvers, -// { -// defaultParam: -// defaultScopeValue === false -// ? -1 -// : defaultScopeValue === true -// ? paramResolvers.length - 1 -// : defaultScopeValue, -// }, -// ] - -const _paramResolver = (evaluateTyped, resolver) => { - if (typeof resolver === 'function') { - return resolver - } else if (_isExpectedType(resolver)) { - return evaluateTyped.bind(null, resolver) - } else if (resolver === null) { - return _paramResolverNoop - } else { - throw new TypeError( - `Expected resolver to be either Function | ExpectedType | 'any' | null, but got ${typeof resolver}: ${resolver}` - ) - } -} - -/** - * @function syncInterpreter - * @returns {ExpressionInterpreter} - */ -export const syncInterpreter = ( - spec: ExpressionInterpreter -): ExpressionInterpreterFunction => { - if (typeof spec === 'function') { - return spec - } - - const [ - fn, - paramResolvers, - { defaultParam = paramResolvers.length - 1 } = {}, - ] = spec - - // - // Bring all evaluation logic that is possible - // to outside the returned interperter wrapper function - // in order to minimize expression evaluation performance - // - const _paramResolvers = paramResolvers.map((resolver) => - _paramResolver(evaluateTyped, resolver) - ) - - return (context, ...args) => - fn( - ..._paramResolvers.map((resolver, index) => { - // Last param defaults to $$VALUE - const arg = - args[index] === undefined && index === defaultParam - ? ['$value', '$$VALUE'] - : args[index] - return resolver(context, arg) - }), - context - ) -} - -export const syncInterpreterList = ( - specs: ExpressionInterpreterList -): ExpressionInterpreterFunctionList => - Object.keys(specs).reduce( - (acc, interperterId) => ({ - ...acc, - [interperterId]: syncInterpreter(specs[interperterId]), - }), - {} - ) - -export const asyncInterpreter = ( - spec: ExpressionInterpreter -): ExpressionInterpreterFunction => { - if (typeof spec === 'function') { - return spec - } - - const [ - fn, - paramResolvers, - { defaultParam = paramResolvers.length - 1 } = {}, - ] = spec - - // - // Bring all evaluation logic that is possible - // to outside the returned interperter wrapper function - // in order to minimize expression evaluation performance - // - const _paramResolvers = paramResolvers.map((resolver) => - _paramResolver(evaluateTypedAsync, resolver) - ) - - return (context, ...args) => - Promise.all( - _paramResolvers.map((resolver, index) => { - // Last param defaults to $$VALUE - const arg = - args[index] === undefined && index === defaultParam - ? ['$value', '$$VALUE'] - : args[index] - return resolver(context, arg) - }) - ).then((resolvedArgs) => fn(...resolvedArgs, context)) -} - -export const asyncInterpreterList = ( - specs: ExpressionInterpreterList -): ExpressionInterpreterFunctionList => - Object.keys(specs).reduce( - (acc, interperterId) => ({ - ...acc, - [interperterId]: asyncInterpreter(specs[interperterId]), - }), - {} - ) diff --git a/src/interpreter/asyncInterpreter.ts b/src/interpreter/asyncInterpreter.ts new file mode 100644 index 0000000..16f1c11 --- /dev/null +++ b/src/interpreter/asyncInterpreter.ts @@ -0,0 +1,34 @@ +import { InterpreterSpecSingle, InterpreterFunction } from '../types' + +import { asyncParamResolver } from './asyncParamResolver' + +export const asyncInterpreter = ( + spec: InterpreterSpecSingle +): InterpreterFunction => { + const [ + fn, + paramResolvers, + { defaultParam = paramResolvers.length - 1 } = {}, + ] = spec + + // + // Bring all evaluation logic that is possible + // to outside the returned interperter wrapper function + // in order to minimize expression evaluation performance + // + const asyncParamResolvers = paramResolvers.map((resolver) => + asyncParamResolver(resolver) + ) + + return (context, ...args) => + Promise.all( + asyncParamResolvers.map((resolver, index) => { + // Last param defaults to $$VALUE + const arg = + args[index] === undefined && index === defaultParam + ? ['$value', '$$VALUE'] + : args[index] + return resolver(context, arg) + }) + ).then((resolvedArgs) => fn(...resolvedArgs, context)) +} diff --git a/src/interpreter/asyncParamResolver.ts b/src/interpreter/asyncParamResolver.ts new file mode 100644 index 0000000..321f33d --- /dev/null +++ b/src/interpreter/asyncParamResolver.ts @@ -0,0 +1,111 @@ +import { + validateType, + castTypeSpec, + ANY_TYPE, + SINGLE_TYPE, + ONE_OF_TYPES, + ENUM_TYPE, + INDEFINITE_ARRAY_OF_TYPE, + INDEFINITE_OBJECT_OF_TYPE, + TUPLE_TYPE, + OBJECT_TYPE, +} from '@orioro/typing' + +import { TypeSpec, ParamResolver } from '../types' + +import { evaluate, evaluateTypedAsync } from '../evaluate' +import { promiseResolveObject } from '../util/promiseResolveObject' + +/** + * @function asyncParamResolver + * @todo asyncParamResolver ONE_OF_TYPES: handle complex cases, e.g. + * oneOfTypes(['string', objectType({ key1: 'string', key2: 'number '})]) + */ +export const asyncParamResolver = (typeSpec: TypeSpec): ParamResolver => { + const expectedType = castTypeSpec(typeSpec) + + if (expectedType === null) { + throw new TypeError(`Invalid typeSpec: ${JSON.stringify(typeSpec)}`) + } + + switch (expectedType.specType) { + case ANY_TYPE: + return expectedType.delayEvaluation + ? (context, value) => Promise.resolve(value) + : (context, value) => Promise.resolve(evaluate(context, value)) + case SINGLE_TYPE: + case ONE_OF_TYPES: + case ENUM_TYPE: + return evaluateTypedAsync.bind(null, expectedType) + case TUPLE_TYPE: { + const itemParamResolvers = expectedType.items.map((itemResolver) => + asyncParamResolver(itemResolver) + ) + + return (context, value) => { + return evaluateTypedAsync('array', context, value).then((array) => { + return Promise.all( + array.map((item, index) => { + return itemParamResolvers[index](context, item) + }) + ) + }) + } + } + case INDEFINITE_ARRAY_OF_TYPE: { + const itemParamResolver = asyncParamResolver(expectedType.itemType) + + return (context, value) => { + return evaluateTypedAsync('array', context, value).then((array) => { + return Promise.all( + array.map((item) => itemParamResolver(context, item)) + ) + }) + } + } + case OBJECT_TYPE: { + const propertyParamResolvers = Object.keys( + expectedType.properties + ).reduce( + (acc, key) => ({ + ...acc, + [key]: asyncParamResolver(expectedType.properties[key]), + }), + {} + ) + + return (context, value) => + evaluateTypedAsync('object', context, value) + .then((unresolvedObject) => + promiseResolveObject( + unresolvedObject, + (propertyValue, propertyKey) => + propertyParamResolvers[propertyKey](context, propertyValue) + ) + ) + .then((resolvedObject) => { + validateType(expectedType, resolvedObject) + + return resolvedObject + }) + } + case INDEFINITE_OBJECT_OF_TYPE: { + const propertyParamResolver = asyncParamResolver( + expectedType.propertyType + ) + + return (context, value) => + evaluateTypedAsync('object', context, value) + .then((object) => + promiseResolveObject(object, (propertyValue) => + propertyParamResolver(context, propertyValue) + ) + ) + .then((finalObject) => { + validateType(expectedType, finalObject) + + return finalObject + }) + } + } +} diff --git a/src/interpreter/interpreter.spec.ts b/src/interpreter/interpreter.spec.ts new file mode 100644 index 0000000..54dbec6 --- /dev/null +++ b/src/interpreter/interpreter.spec.ts @@ -0,0 +1,203 @@ +import { testCases, asyncResult, valueLabel } from '@orioro/jest-util' + +import { ALL_EXPRESSIONS } from '../' +import { + anyType, + tupleType, + indefiniteArrayOfType, + indefiniteObjectOfType, +} from '@orioro/typing' +import { interpreterList } from './interpreter' +import { syncParamResolver } from './syncParamResolver' +import { asyncParamResolver } from './asyncParamResolver' + +const interpreters = interpreterList(ALL_EXPRESSIONS) + +const _resolverTestCases = (cases, paramSpec) => { + const syncResolver = syncParamResolver(paramSpec) + const asyncResolver = asyncParamResolver(paramSpec) + + testCases( + cases, + (scopeValue, valueToResolve) => { + return syncResolver( + { + interpreters, + scope: { + $$VALUE: scopeValue, + }, + }, + valueToResolve + ) + }, + () => `sync - ${valueLabel(paramSpec)}` + ) + + testCases( + cases.map((_case) => { + const args = _case.slice(0, -1) + const result = _case[_case.length - 1] + + return [...args, asyncResult(result)] + }), + (scopeValue, valueToResolve) => { + return asyncResolver( + { + interpreters, + scope: { + $$VALUE: scopeValue, + }, + }, + valueToResolve + ) + }, + `async - ${valueLabel(paramSpec)}` + ) +} + +test('invalid type', () => { + expect(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + syncParamResolver(10) + }).toThrow('Invalid typeSpec') +}) + +describe('anyType()', () => { + _resolverTestCases( + [ + [10, 'value-b', 'value-b'], + [10, ['$value'], 10], + [10, ['$mathSum', 5], 15], + ], + 'any' + ) +}) + +describe('anyType({ delayEvaluation: true })', () => { + _resolverTestCases( + [ + [10, 'value-b', 'value-b'], + [10, ['$value'], ['$value']], + [10, ['$mathSum', 5], ['$mathSum', 5]], + ], + anyType({ delayEvaluation: true }) + ) +}) + +describe('singleType(string)', () => { + _resolverTestCases( + [ + ['some-str', 'value-b', 'value-b'], + ['some-str', ['$value'], 'some-str'], + ['some-str', 7, TypeError], + ], + 'string' + ) +}) + +describe('oneOfTypes([string, number])', () => { + _resolverTestCases( + [ + ['some-str', 'value-b', 'value-b'], + ['some-str', ['$value'], 'some-str'], + ['some-str', 7, 7], + ], + ['string', 'number'] + ) +}) + +describe('tupleType([string, number])', () => { + _resolverTestCases( + [ + [['some-str', 20], ['$value'], ['some-str', 20]], + [ + ['some-str', 20], + ['some-other-str', 15], + ['some-other-str', 15], + ], + [['some-str', 20], 'value-b', TypeError], + [['some-str', 20], 7, TypeError], + [['some-str', 20], [1, 2], TypeError], + [['some-str', 20], ['some-str', 20, 30], TypeError], + ], + tupleType(['string', 'number']) + ) +}) + +describe('indefiniteArrayOfType([string, number])', () => { + _resolverTestCases( + [ + [['some-str', 20], ['$value'], ['some-str', 20]], + [ + ['some-str', 20], + ['some-other-str', 15], + ['some-other-str', 15], + ], + [ + ['some-str', 20], + [1, 2], + [1, 2], + ], + [ + ['some-str', 20], + ['some-str', 20, 30], + ['some-str', 20, 30], + ], + [['some-str', 20], 'value-b', TypeError], + [['some-str', 20], 7, TypeError], + ], + indefiniteArrayOfType(['string', 'number']) + ) +}) + +describe('objectType(obj)', () => { + const type = { + key1: 'string', + key2: indefiniteArrayOfType(['string', 'number']), + } + + _resolverTestCases( + [ + [ + 'some-str', + { key1: 'LITERAL-STR-1', key2: ['LITERAL-STR-2', 'LITERAL-STR-3'] }, + { key1: 'LITERAL-STR-1', key2: ['LITERAL-STR-2', 'LITERAL-STR-3'] }, + ], + [ + 'some-str', + { key1: ['$value'], key2: [['$value'], 'LITERAL-STR-1'] }, + { key1: 'some-str', key2: ['some-str', 'LITERAL-STR-1'] }, + ], + ], + type + ) +}) + +describe('indefiniteObjectOfType(type)', () => { + _resolverTestCases( + [ + [ + '10.1', + { + key1: [9, ['$value']], + key2: [10, 'LITERAL-STR-1'], + key3: [['$numberFloat'], ['$value']], + }, + { + key1: [9, '10.1'], + key2: [10, 'LITERAL-STR-1'], + key3: [10.1, '10.1'], + }, + ], + [ + '10.1', + { + key1: [['$value'], ['$value']], + }, + TypeError, + ], + ], + indefiniteObjectOfType(tupleType(['number', 'string'])) + ) +}) diff --git a/src/interpreter/interpreter.ts b/src/interpreter/interpreter.ts new file mode 100644 index 0000000..acba1ae --- /dev/null +++ b/src/interpreter/interpreter.ts @@ -0,0 +1,79 @@ +import { isPlainObject } from 'lodash' +import { + InterpreterSpec, + InterpreterFunction, + InterpreterSpecList, + Interpreter, + InterpreterList, +} from '../types' + +import { SyncModeUnsupportedError, AsyncModeUnsupportedError } from '../errors' + +import { syncInterpreter } from './syncInterpreter' +import { asyncInterpreter } from './asyncInterpreter' + +const _syncUnsupported = (interperterId) => () => { + throw new SyncModeUnsupportedError(interperterId) +} + +const _asyncUnsupported = (interperterId) => () => { + throw new AsyncModeUnsupportedError(interperterId) +} + +export const interpreter = ( + spec: InterpreterSpec | InterpreterFunction, + interperterId: string = 'UNKNOWN_INTERPRETER' +): Interpreter => { + if (typeof spec === 'function') { + // Assume it is a shared function that works + // in both sync and async modes + return { + sync: spec, + async: spec, + } + } else if (Array.isArray(spec)) { + // Assume it is a shared spec that works + // in both sync and async modes + return { + sync: syncInterpreter(spec), + async: asyncInterpreter(spec), + } + } else if (isPlainObject(spec)) { + return { + sync: + spec.sync === null + ? _syncUnsupported(interperterId) + : typeof spec.sync === 'function' + ? spec.sync + : syncInterpreter(spec.sync), + async: + spec.async === null + ? _asyncUnsupported(interperterId) + : typeof spec.async === 'function' + ? spec.async + : asyncInterpreter(spec.async), + } + } else { + throw new Error(`Invalid interpreter spec ${spec}`) + } +} + +// export const asyncInterpreterList = ( +// specs: InterpreterList +// ): InterpreterFunctionList => +// Object.keys(specs).reduce( +// (acc, interperterId) => ({ +// ...acc, +// [interperterId]: asyncInterpreter(specs[interperterId]), +// }), +// {} +// ) + +export const interpreterList = (specs: InterpreterSpecList): InterpreterList => + Object.keys(specs).reduce( + (acc, interperterId) => ({ + ...acc, + [interperterId]: interpreter(specs[interperterId], interperterId), + }), + {} + ) diff --git a/src/interpreter/syncInterpreter.ts b/src/interpreter/syncInterpreter.ts new file mode 100644 index 0000000..4e68350 --- /dev/null +++ b/src/interpreter/syncInterpreter.ts @@ -0,0 +1,51 @@ +import { InterpreterSpecSingle, InterpreterFunction } from '../types' + +import { syncParamResolver } from './syncParamResolver' + +/** + * @todo syncInterpreter Update Interpreter type: remove function + * @function syncInterpreter + * @returns {Interpreter} + */ +export const syncInterpreter = ( + spec: InterpreterSpecSingle +): InterpreterFunction => { + const [ + fn, + paramTypeSpecs, + { defaultParam = paramTypeSpecs.length - 1 } = {}, + ] = spec + + // + // Bring all evaluation logic that is possible + // to outside the returned interperter wrapper function + // in order to minimize expression evaluation performance + // + const syncParamResolvers = paramTypeSpecs.map((typeSpec) => + syncParamResolver(typeSpec) + ) + + return (context, ...args) => + fn( + ...syncParamResolvers.map((typeSpec, index) => { + // Last param defaults to $$VALUE + const arg = + args[index] === undefined && index === defaultParam + ? ['$value', '$$VALUE'] + : args[index] + return typeSpec(context, arg) + }), + context + ) +} + +// export const syncInterpreterList = ( +// specs: InterpreterList +// ): InterpreterFunctionList => +// Object.keys(specs).reduce( +// (acc, interperterId) => ({ +// ...acc, +// [interperterId]: syncInterpreter(specs[interperterId]), +// }), +// {} +// ) diff --git a/src/interpreter/syncParamResolver.ts b/src/interpreter/syncParamResolver.ts new file mode 100644 index 0000000..068caf1 --- /dev/null +++ b/src/interpreter/syncParamResolver.ts @@ -0,0 +1,117 @@ +import { + validateType, + castTypeSpec, + ANY_TYPE, + SINGLE_TYPE, + ONE_OF_TYPES, + ENUM_TYPE, + INDEFINITE_ARRAY_OF_TYPE, + INDEFINITE_OBJECT_OF_TYPE, + TUPLE_TYPE, + OBJECT_TYPE, +} from '@orioro/typing' + +import { ParamResolver, TypeSpec } from '../types' + +import { evaluate, evaluateTyped } from '../evaluate' + +/** + * @todo syncInterpreter Study better ways at validating evlauation results for + * tupleType and indefiniteArrayOfType. Currently validation is highly redundant. + * @todo syncInterpreter Handle nested object param typeSpec + * @function syncParamResolver + * @private + * @param {TypeSpec} typeSpec + * @returns {ParamResolver} + */ +export const syncParamResolver = (typeSpec: TypeSpec): ParamResolver => { + const expectedType = castTypeSpec(typeSpec) + + if (expectedType === null) { + throw new TypeError(`Invalid typeSpec: ${JSON.stringify(typeSpec)}`) + } + + switch (expectedType.specType) { + case ANY_TYPE: + return expectedType.delayEvaluation ? (context, value) => value : evaluate + case SINGLE_TYPE: + case ONE_OF_TYPES: + case ENUM_TYPE: + return evaluateTyped.bind(null, expectedType) + case TUPLE_TYPE: { + const itemParamResolvers = expectedType.items.map((itemResolver) => + syncParamResolver(itemResolver) + ) + + return (context, value) => { + const array = evaluateTyped( + 'array', + context, + value + ).map((item, index) => itemParamResolvers[index](context, item)) + + validateType(expectedType, array) + + return array + } + } + case INDEFINITE_ARRAY_OF_TYPE: { + const itemParamResolver = syncParamResolver(expectedType.itemType) + + return (context, value) => { + const array = evaluateTyped('array', context, value).map((item) => + itemParamResolver(context, item) + ) + + validateType(expectedType, array) + + return array + } + } + case OBJECT_TYPE: { + const propertyParamResolvers = Object.keys( + expectedType.properties + ).reduce( + (acc, key) => ({ + ...acc, + [key]: syncParamResolver(expectedType.properties[key]), + }), + {} + ) + + return (context, value) => { + const _object = evaluateTyped('object', context, value) + const object = Object.keys(_object).reduce( + (acc, key) => ({ + ...acc, + [key]: propertyParamResolvers[key](context, _object[key]), + }), + {} + ) + + validateType(expectedType, object) + + return object + } + } + case INDEFINITE_OBJECT_OF_TYPE: + return (context, value) => { + const propertyParamResolver = syncParamResolver( + expectedType.propertyType + ) + + const _object = evaluateTyped('object', context, value) + const object = Object.keys(_object).reduce( + (acc, key) => ({ + ...acc, + [key]: propertyParamResolver(context, _object[key]), + }), + {} + ) + + validateType(expectedType, object) + + return object + } + } +} diff --git a/src/types.ts b/src/types.ts index 65f33b2..2ada025 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,8 +1,8 @@ -import { TypeAlternatives, TypeMap, ExpectedType } from '@orioro/typing' +import type { TypeAlternatives, TypeMap, TypeSpec } from '@orioro/typing' -export { TypeAlternatives, TypeMap } +export type { TypeAlternatives, TypeMap, TypeSpec } -type ParamResolverFunction = (context: EvaluationContext, arg: any) => any +export type ParamResolver = (context: EvaluationContext, arg: any) => any /** * Defines how an expression argument should be resolved @@ -32,7 +32,7 @@ type ParamResolverFunction = (context: EvaluationContext, arg: any) => any * * @typedef {Function | null | string | string[]} ParamResolver */ -export type ParamResolver = ParamResolverFunction | null | ExpectedType +// export type ParamResolver = TypeSpec /** * An expression is an array tuple with the first item @@ -48,7 +48,7 @@ export type Expression = [string, ...any[]] * `syncInterpreter`, `syncInterpreterList`, `asyncInterpreter` or * `asyncInterpreterList`) may be compatible with sync and async formats. * - * @typedef {[Function, ParamResolver[] | null, Object]} ExpressionInterpreterSpec + * @typedef {[Function, ParamResolver[] | null, Object]} InterpreterSpec * @param {Function} interpreterFn Function that executes logic for interpreting the * expression. If `paramResolvers` are not null, the * interpreterFn is invoked with the list of resolved @@ -65,26 +65,47 @@ export type Expression = [string, ...any[]] * In case the defaultParameter should not * be used, use `defaultParam = -1` */ -export type ExpressionInterpreterSpec = [ +export type InterpreterSpecSingle = [ (...args: any) => any, - ParamResolver[], + TypeSpec[], { defaultParam?: number }? ] +export type InterpreterSpec = + | InterpreterSpecSingle + | InterpreterFunction + | { + sync: InterpreterSpecSingle | InterpreterFunction | null + async: InterpreterSpecSingle | InterpreterFunction | null + } + +export type InterpreterSpecList = { + [key: string]: InterpreterSpec +} /** * Function that receives as first parameter the EvaluationContext * and should return the result for evaluating a given expression. * - * @typedef {Function} ExpressionInterpreterFunction + * @typedef {Function} InterpreterFunction */ -export type ExpressionInterpreterFunction = ( +export type InterpreterFunction = ( context: EvaluationContext, ...args: any[] ) => any -export type ExpressionInterpreter = - | ExpressionInterpreterSpec - | ExpressionInterpreterFunction +export type Interpreter = { + sync: InterpreterFunction + async: InterpreterFunction +} + +/** + * @typedef {Object} InterpreterList + * @property {Object} interpreterList + * @property {Interpreter} interpreterList.{{ expressionName }} + */ +export type InterpreterList = { + [key: string]: Interpreter +} /** * @typedef {Object} EvaluationScope @@ -109,28 +130,16 @@ export type EvaluationScope = { [key: string]: any } -/** - * @typedef {Object} ExpressionInterpreterList - * @property {Object} interpreterList - * @property {ExpressionInterpreter} interpreterList.{{ expressionName }} - */ -export type ExpressionInterpreterList = { - [key: string]: ExpressionInterpreter -} - -export type ExpressionInterpreterFunctionList = { - [key: string]: ExpressionInterpreterFunction -} - /** * @typedef {Object} EvaluationContext * @property {Object} context - * @property {ExpressionInterpreterFunctionList} context.interpreters + * @property {InterpreterFunctionList} context.interpreters * @property {EvaluationScope} context.scope */ export type EvaluationContext = { - interpreters: ExpressionInterpreterFunctionList + interpreters: InterpreterList scope: EvaluationScope + async?: boolean } /** diff --git a/src/util/promiseResolveObject.ts b/src/util/promiseResolveObject.ts new file mode 100644 index 0000000..07660b3 --- /dev/null +++ b/src/util/promiseResolveObject.ts @@ -0,0 +1,24 @@ +import { PlainObject } from '../types' + +type PropertyResolverFunction = (value: any, key: string) => any + +const _defaulrPropertyResolver: PropertyResolverFunction = (value) => value + +export const promiseResolveObject = ( + object: PlainObject, + resolver: PropertyResolverFunction = _defaulrPropertyResolver +): Promise => { + const keys = Object.keys(object) + + return Promise.all(keys.map((key) => resolver(object[key], key))).then( + (values) => + values.reduce((acc, value, index) => { + const key = keys[index] + + return { + ...acc, + [key]: value, + } + }, {}) + ) +} diff --git a/tsconfig.json b/tsconfig.json index 81be7b2..a47b3e3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,6 @@ "target": "es6" }, "exclude": [ - "node_modules", - "src/*.spec.ts" + "node_modules" ] } diff --git a/yarn.lock b/yarn.lock index 6d842d3..93a1649 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1246,10 +1246,10 @@ lodash "^4.17.20" luxon "^1.25.0" -"@orioro/jest-util@^1.3.0": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@orioro/jest-util/-/jest-util-1.3.0.tgz#8f981fbed37246aa6c95015b2cd85d7e72b4c88f" - integrity sha512-JOLvA1PkVO/ygw4pyDRM3YyOfwjWLj8XM7CxP9E1qX0W13fPUZFAIWbBWHqjNuu1ua6wzjPWtzU/3Hduwz+/cw== +"@orioro/jest-util@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@orioro/jest-util/-/jest-util-1.5.0.tgz#d1a672ebf7aa28dcd86622cc263ddbec99e994f5" + integrity sha512-3O6qgVKn9zAZxkEMwDMLeA6JXsHUIA/p8gBviPTBsGrVhsuv8nrsiG3aQcMvDlCR8ucWVciQQcZ9rCrcEZ7/Cw== dependencies: is-plain-object "^5.0.0" @@ -1271,12 +1271,13 @@ vinyl-fs "^3.0.3" yargs "^16.2.0" -"@orioro/typing@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@orioro/typing/-/typing-3.0.0.tgz#b30218d9e5075040ebc6a013ef5b419d0e2a3667" - integrity sha512-NuiteC4tN9kbraxcFzvMLlWB+2pTS1tiIVvJZdV/8MrL8M5RhI7V0kDv77O9JicuiBiUsTvYKqUXAR1OxWTTFA== +"@orioro/typing@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@orioro/typing/-/typing-4.2.0.tgz#f7cb7d2707775f64424471362c8f7bcc213038ff" + integrity sha512-tRw6jSqIqlyAtDUHPWKa+s2p4IOVOvt4JWYyyk0uv8kA8R8KcRmhgG+EiRy1wdOOIUmPhXWR19zn+ABUeT3p5A== dependencies: "@orioro/cascade" "^3.0.0" + deep-equal "^2.0.5" is-plain-object "^5.0.0" "@rollup/plugin-babel@^5.2.0": @@ -1876,6 +1877,11 @@ arr-union@^3.1.0: resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= +array-filter@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83" + integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM= + array-ify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" @@ -1945,6 +1951,13 @@ autolinker@~0.28.0: dependencies: gulp-header "^1.7.1" +available-typed-arrays@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5" + integrity sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ== + dependencies: + array-filter "^1.0.0" + aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" @@ -2938,6 +2951,27 @@ dedent@0.7.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= +deep-equal@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.5.tgz#55cd2fe326d83f9cbf7261ef0e060b3f724c5cb9" + integrity sha512-nPiRgmbAtm1a3JsnLCf6/SLfXcjyN5v8L1TXzdCmHrXJ4hx+gW/w1YCcn7z8gJtSiDArZCgYtbao3QqLm/N1Sw== + dependencies: + call-bind "^1.0.0" + es-get-iterator "^1.1.1" + get-intrinsic "^1.0.1" + is-arguments "^1.0.4" + is-date-object "^1.0.2" + is-regex "^1.1.1" + isarray "^2.0.5" + object-is "^1.1.4" + object-keys "^1.1.1" + object.assign "^4.1.2" + regexp.prototype.flags "^1.3.0" + side-channel "^1.0.3" + which-boxed-primitive "^1.0.1" + which-collection "^1.0.1" + which-typed-array "^1.1.2" + deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" @@ -3229,6 +3263,42 @@ es-abstract@^1.18.0-next.1: string.prototype.trimend "^1.0.3" string.prototype.trimstart "^1.0.3" +es-abstract@^1.18.0-next.2: + version "1.18.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0.tgz#ab80b359eecb7ede4c298000390bc5ac3ec7b5a4" + integrity sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw== + dependencies: + call-bind "^1.0.2" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + get-intrinsic "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.2" + is-callable "^1.2.3" + is-negative-zero "^2.0.1" + is-regex "^1.1.2" + is-string "^1.0.5" + object-inspect "^1.9.0" + object-keys "^1.1.1" + object.assign "^4.1.2" + string.prototype.trimend "^1.0.4" + string.prototype.trimstart "^1.0.4" + unbox-primitive "^1.0.0" + +es-get-iterator@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.2.tgz#9234c54aba713486d7ebde0220864af5e2b283f7" + integrity sha512-+DTO8GYwbMCwbywjimwZMHp8AuYXOS2JZFWoi2AlPOS3ebnII9w/NLpNZtA7A0YLaVDw+O7KFCeoIV7OPvM7hQ== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.0" + has-symbols "^1.0.1" + is-arguments "^1.1.0" + is-map "^2.0.2" + is-set "^2.0.2" + is-string "^1.0.5" + isarray "^2.0.5" + es-to-primitive@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" @@ -3751,6 +3821,11 @@ for-in@^1.0.2: resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= +foreach@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" + integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k= + forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" @@ -3912,7 +3987,7 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.0.2: +get-intrinsic@^1.0.1, get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== @@ -4162,6 +4237,11 @@ hard-rejection@^2.1.0: resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== +has-bigints@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" + integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -4172,6 +4252,11 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-symbols@^1.0.0, has-symbols@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" + integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== + has-symbols@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" @@ -4505,11 +4590,23 @@ is-accessor-descriptor@^1.0.0: dependencies: kind-of "^6.0.0" +is-arguments@^1.0.4, is-arguments@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.0.tgz#62353031dfbee07ceb34656a6bde59efecae8dd9" + integrity sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg== + dependencies: + call-bind "^1.0.0" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= +is-bigint@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.1.tgz#6923051dfcbc764278540b9ce0e6b3213aa5ebc2" + integrity sha512-J0ELF4yHFxHy0cmSxZuheDOz2luOdVvqjwmEcj8H/L1JHeuEDSDbeRP+Dk9kFVk5RTFzbucJ2Kb9F7ixY2QaCg== + is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" @@ -4517,12 +4614,19 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" +is-boolean-object@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.0.tgz#e2aaad3a3a8fca34c28f6eee135b156ed2587ff0" + integrity sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA== + dependencies: + call-bind "^1.0.0" + is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-callable@^1.1.4, is-callable@^1.2.2: +is-callable@^1.1.4, is-callable@^1.2.2, is-callable@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e" integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ== @@ -4569,7 +4673,7 @@ is-data-descriptor@^1.0.0: dependencies: kind-of "^6.0.0" -is-date-object@^1.0.1: +is-date-object@^1.0.1, is-date-object@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== @@ -4658,6 +4762,11 @@ is-installed-globally@^0.1.0: global-dirs "^0.1.0" is-path-inside "^1.0.0" +is-map@^2.0.1, is-map@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127" + integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg== + is-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" @@ -4678,6 +4787,11 @@ is-npm@^1.0.0: resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" integrity sha1-8vtjpl5JBbQGyGBydloaTceTufQ= +is-number-object@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197" + integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw== + is-number@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" @@ -4763,7 +4877,7 @@ is-reference@^1.2.1: dependencies: "@types/estree" "*" -is-regex@^1.1.1: +is-regex@^1.1.1, is-regex@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251" integrity sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg== @@ -4783,6 +4897,11 @@ is-retry-allowed@^1.0.0: resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4" integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg== +is-set@^2.0.1, is-set@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec" + integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g== + is-stream@^1.0.0, is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" @@ -4793,7 +4912,12 @@ is-stream@^2.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== -is-symbol@^1.0.2: +is-string@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" + integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== + +is-symbol@^1.0.2, is-symbol@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== @@ -4807,6 +4931,17 @@ is-text-path@^1.0.1: dependencies: text-extensions "^1.0.0" +is-typed-array@^1.1.3: + version "1.1.5" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.5.tgz#f32e6e096455e329eb7b423862456aa213f0eb4e" + integrity sha512-S+GRDgJlR3PyEbsX/Fobd9cqpZBuvUS+8asRqYDMLCb2qMzt1oz5m5oxQCxOgUDxiWsOVNi4yaF+/uvdlHlYug== + dependencies: + available-typed-arrays "^1.0.2" + call-bind "^1.0.2" + es-abstract "^1.18.0-next.2" + foreach "^2.0.5" + has-symbols "^1.0.1" + is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -4829,6 +4964,16 @@ is-valid-glob@^1.0.0: resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-1.0.0.tgz#29bf3eff701be2d4d315dbacc39bc39fe8f601aa" integrity sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao= +is-weakmap@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" + integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA== + +is-weakset@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.1.tgz#e9a0af88dbd751589f5e50d80f4c98b780884f83" + integrity sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw== + is-windows@^1.0.1, is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -4851,6 +4996,11 @@ isarray@1.0.0, isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -6653,6 +6803,14 @@ object-inspect@^1.9.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a" integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw== +object-is@^1.1.4: + version "1.1.5" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" + integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + object-keys@^1.0.12, object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -7436,6 +7594,14 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" +regexp.prototype.flags@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz#7ef352ae8d159e758c0eadca6f8fcb4eef07be26" + integrity sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + regexpp@^3.0.0, regexpp@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" @@ -7903,6 +8069,15 @@ shellwords@^0.1.1: resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== +side-channel@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" @@ -8245,6 +8420,14 @@ string.prototype.trimend@^1.0.3: call-bind "^1.0.0" define-properties "^1.1.3" +string.prototype.trimend@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" + integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + string.prototype.trimstart@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz#9b4cb590e123bb36564401d59824298de50fd5aa" @@ -8253,6 +8436,14 @@ string.prototype.trimstart@^1.0.3: call-bind "^1.0.0" define-properties "^1.1.3" +string.prototype.trimstart@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" + integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -8703,6 +8894,16 @@ umask@^1.1.0, umask@~1.1.0: resolved "https://registry.yarnpkg.com/umask/-/umask-1.1.0.tgz#f29cebf01df517912bb58ff9c4e50fde8e33320d" integrity sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0= +unbox-primitive@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.0.tgz#eeacbc4affa28e9b3d36b5eaeccc50b3251b1d3f" + integrity sha512-P/51NX+JXyxK/aigg1/ZgyccdAxm5K1+n8+tvqSntjOivPt19gvm1VC49RWYetsiub8WViUchdxl/KWHHB0kzA== + dependencies: + function-bind "^1.1.1" + has-bigints "^1.0.0" + has-symbols "^1.0.0" + which-boxed-primitive "^1.0.1" + unc-path-regex@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" @@ -9032,11 +9233,45 @@ whatwg-url@^8.0.0: tr46 "^2.0.2" webidl-conversions "^6.1.0" +which-boxed-primitive@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + +which-collection@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906" + integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A== + dependencies: + is-map "^2.0.1" + is-set "^2.0.1" + is-weakmap "^2.0.1" + is-weakset "^2.0.1" + which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= +which-typed-array@^1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.4.tgz#8fcb7d3ee5adf2d771066fba7cf37e32fe8711ff" + integrity sha512-49E0SpUe90cjpoc7BOJwyPHRqSAd12c10Qm2amdEZrJPCY2NDxaW01zHITrem+rnETY3dwrbH3UUrUwagfCYDA== + dependencies: + available-typed-arrays "^1.0.2" + call-bind "^1.0.0" + es-abstract "^1.18.0-next.1" + foreach "^2.0.5" + function-bind "^1.1.1" + has-symbols "^1.0.1" + is-typed-array "^1.1.3" + which@^1.2.14, which@^1.2.9, which@^1.3.0, which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"