From 8bd0db20d0e1c45e5e57ead140e60ba556b68207 Mon Sep 17 00:00:00 2001 From: Simon Fan Date: Wed, 3 Mar 2021 04:49:19 -0300 Subject: [PATCH] feat(async): add support for async expressions and modify expression interpreter definition api Previously the expression interpreter structure allowed only for evaluation of synchronous expressions. This commit modifies the way interpreters are prepared in order to support both synchronous and asynchronous execution. BREAKING CHANGE: remove interpreter method, add syncInterpreter, syncInterpreterList, asyncInterpreter and asyncInterpreterList methods --- package.json | 2 +- src/__snapshots__/index.spec.ts.snap | 6 +- src/async.spec.ts | 82 +++++++++ src/evaluate.ts | 49 ++++-- src/expressions/array.spec.ts | 5 +- src/expressions/array.ts | 169 +++++++++--------- src/expressions/boolean.spec.ts | 11 +- src/expressions/boolean.ts | 10 +- src/expressions/comparison.spec.ts | 17 +- src/expressions/comparison.ts | 64 ++++--- src/expressions/functional.spec.ts | 5 +- src/expressions/functional.ts | 13 +- src/expressions/logical.spec.ts | 5 +- src/expressions/logical.ts | 59 ++++--- src/expressions/math.spec.ts | 5 +- src/expressions/math.ts | 74 ++++---- src/expressions/number.spec.ts | 5 +- src/expressions/number.ts | 16 +- src/expressions/object.spec.ts | 5 +- src/expressions/object.ts | 44 ++--- src/expressions/string.spec.ts | 5 +- src/expressions/string.ts | 72 ++++---- src/expressions/type.spec.ts | 16 +- src/expressions/type.ts | 20 ++- src/expressions/value.spec.ts | 5 +- src/expressions/value.ts | 43 +++-- src/interpreter.ts | 252 ++++++++++++++------------- src/types.ts | 79 ++++++++- yarn.lock | 8 +- 29 files changed, 700 insertions(+), 446 deletions(-) create mode 100644 src/async.spec.ts diff --git a/package.json b/package.json index db4cdd7..6d93fd5 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.2.0", + "@orioro/jest-util": "^1.3.0", "@orioro/readme": "^1.0.1", "@rollup/plugin-babel": "^5.2.0", "@rollup/plugin-commonjs": "^15.0.0", diff --git a/src/__snapshots__/index.spec.ts.snap b/src/__snapshots__/index.spec.ts.snap index 468d5c4..65a4af0 100644 --- a/src/__snapshots__/index.spec.ts.snap +++ b/src/__snapshots__/index.spec.ts.snap @@ -105,6 +105,10 @@ Array [ "isExpression", "evaluate", "evaluateTyped", - "interpreter", + "evaluateTypedAsync", + "syncInterpreter", + "syncInterpreterList", + "asyncInterpreter", + "asyncInterpreterList", ] `; diff --git a/src/async.spec.ts b/src/async.spec.ts new file mode 100644 index 0000000..f899c3a --- /dev/null +++ b/src/async.spec.ts @@ -0,0 +1,82 @@ +import { testCases, asyncResult } from '@orioro/jest-util' +// import { validateType } from '@orioro/typing' + +import { ALL_EXPRESSIONS } from './' + +import { asyncInterpreterList } from './interpreter' + +import { evaluate } from './evaluate' + +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 = [ + () => + wait(100, { + key1: 'value1', + key2: 'value2', + }), + [], +] +const $asyncLoadTrue = [() => wait(100, true), []] +const $asyncLoadFalse = [() => wait(100, false), []] + +const interpreters = asyncInterpreterList({ + ...ALL_EXPRESSIONS, + $asyncLoadStr, + $asyncLoadNum, + $asyncLoadArr, + $asyncLoadObj, + $asyncLoadTrue, + $asyncLoadFalse, +}) + +describe('async - immediate async expression', () => { + testCases( + [ + ['$asyncLoadStr', asyncResult('async-str')], + ['$asyncLoadNum', asyncResult(9)], + ['$asyncLoadArr', asyncResult(['str-1', 'str-2', 'str-3'])], + ['$asyncLoadObj', asyncResult({ key1: 'value1', key2: 'value2' })], + ['$asyncLoadTrue', asyncResult(true)], + ['$asyncLoadFalse', asyncResult(false)], + ], + (expression) => + evaluate({ interpreters, scope: { $$VALUE: null } }, [expression]) + ) +}) + +describe('async - nested async expression', () => { + test('simple scenario - string concat', () => { + return expect( + evaluate( + { + interpreters, + scope: { + $$VALUE: 'value-', + }, + }, + ['$stringConcat', ['$asyncLoadStr']] + ) + ).resolves.toEqual('value-async-str') + }) +}) + +describe('async - syncronous expressions only get converted to async as well', () => { + test('simple scenario - string concat', () => { + return expect( + evaluate( + { + interpreters, + scope: { + $$VALUE: 'value-', + }, + }, + ['$stringConcat', 'sync-value'] + ) + ).resolves.toEqual('value-sync-value') + }) +}) diff --git a/src/evaluate.ts b/src/evaluate.ts index 782c703..ce42bf5 100644 --- a/src/evaluate.ts +++ b/src/evaluate.ts @@ -22,20 +22,6 @@ const _maybeExpression = (value) => typeof value[0] === 'string' && value[0].startsWith('$') -const _evaluateProd = ( - context: EvaluationContext, - expOrValue: Expression | any -): any => { - if (!isExpression(context.interpreters, expOrValue)) { - return expOrValue - } - - const [interpreterId, ...interpreterArgs] = expOrValue - const interpreter = context.interpreters[interpreterId] - - return interpreter(context, ...interpreterArgs) -} - const _ellipsis = (str, maxlen = 50) => str.length > maxlen ? str.substr(0, maxlen - 1).concat('...') : str @@ -54,7 +40,21 @@ const _evaluateDev = ( ) } - return _evaluateProd(context, expOrValue) + return _evaluate(context, expOrValue) +} + +const _evaluate = ( + context: EvaluationContext, + expOrValue: Expression | any +): any => { + if (!isExpression(context.interpreters, expOrValue)) { + return expOrValue + } + + const [interpreterId, ...interpreterArgs] = expOrValue + const interpreter = context.interpreters[interpreterId] + + return interpreter(context, ...interpreterArgs) } /** @@ -66,7 +66,7 @@ const _evaluateDev = ( export const evaluate = process && process.env && process.env.NODE_ENV !== 'production' ? _evaluateDev - : _evaluateProd + : _evaluate /** * @function evaluateTyped @@ -84,3 +84,20 @@ export const evaluateTyped = ( validateType(expectedTypes, value) return value } + +/** + * @function evaluateTypedAsync + * @param {String | string[]} expectedTypes + * @param {EvaluationContext} context + * @param {Expression | any} expOrValue + * @returns {Promise<*>} + */ +export const evaluateTypedAsync = ( + expectedTypes: string | string[], + context: EvaluationContext, + expOrValue: Expression | any +): Promise => + Promise.resolve(evaluate(context, expOrValue)).then((value) => { + validateType(expectedTypes, value) + return value + }) diff --git a/src/expressions/array.spec.ts b/src/expressions/array.spec.ts index 9048923..27ce5aa 100644 --- a/src/expressions/array.spec.ts +++ b/src/expressions/array.spec.ts @@ -1,4 +1,5 @@ import { evaluate } from '../evaluate' +import { syncInterpreterList } from '../interpreter' import { VALUE_EXPRESSIONS } from './value' import { COMPARISON_EXPRESSIONS } from './comparison' import { LOGICAL_EXPRESSIONS } from './logical' @@ -8,7 +9,7 @@ import { STRING_EXPRESSIONS } from './string' import { MATH_EXPRESSIONS } from './math' import { NUMBER_EXPRESSIONS } from './number' -const interpreters = { +const interpreters = syncInterpreterList({ ...VALUE_EXPRESSIONS, ...LOGICAL_EXPRESSIONS, ...NUMBER_EXPRESSIONS, @@ -17,7 +18,7 @@ const interpreters = { ...OBJECT_EXPRESSIONS, ...STRING_EXPRESSIONS, ...ARRAY_EXPRESSIONS, -} +}) describe('$arrayIncludes', () => { test('basic usage', () => { diff --git a/src/expressions/array.ts b/src/expressions/array.ts index 3502a05..8bde0c0 100644 --- a/src/expressions/array.ts +++ b/src/expressions/array.ts @@ -1,7 +1,11 @@ -import { evaluate, evaluateTyped, isExpression } from '../evaluate' +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { interpreter } from '../interpreter' -import { EvaluationContext, Expression } from '../types' +import { evaluate, evaluateTyped, isExpression } from '../evaluate' +import { + EvaluationContext, + Expression, + ExpressionInterpreterSpec, +} from '../types' import { validateType } from '@orioro/typing' export const $$INDEX = ['$value', '$$INDEX'] @@ -18,10 +22,10 @@ export const $$SORT_B = ['$value', '$$SORT_B'] * @param {Array} [array=$$VALUE] * @returns {Boolean} includes */ -export const $arrayIncludes = interpreter( +export const $arrayIncludes: ExpressionInterpreterSpec = [ (search: any, array: any[]): boolean => array.includes(search), - ['any', 'array'] -) + ['any', 'array'], +] /** * Similar to `$arrayIncludes`, but receives an array @@ -33,11 +37,11 @@ export const $arrayIncludes = interpreter( * @param {Array} [array=$$VALUE] * @returns {Boolean} includesAll */ -export const $arrayIncludesAll = interpreter( +export const $arrayIncludesAll: ExpressionInterpreterSpec = [ (search: any[], array: any[]): boolean => search.every((value) => array.includes(value)), - ['array', 'array'] -) + ['array', 'array'], +] /** * Similar to `$arrayIncludes`, but returns true if @@ -48,21 +52,21 @@ export const $arrayIncludesAll = interpreter( * @param {Array} [array=$$VALUE] * @returns {Boolean} includesAny */ -export const $arrayIncludesAny = interpreter( +export const $arrayIncludesAny: ExpressionInterpreterSpec = [ (search: any[], array: any[]): boolean => search.some((value) => array.includes(value)), - ['array', 'array'] -) + ['array', 'array'], +] /** * @function $arrayLength * @param {Array} [array=$$VALUE] * @returns {Number} length */ -export const $arrayLength = interpreter( +export const $arrayLength: ExpressionInterpreterSpec = [ (array: any[]): number => array.length, - ['array'] -) + ['array'], +] /** * @function $arrayReduce @@ -73,7 +77,7 @@ export const $arrayLength = interpreter( * @param {*} start * @param {Array} [array=$$VALUE] */ -export const $arrayReduce = interpreter( +export const $arrayReduce: ExpressionInterpreterSpec = [ (reduceExp: Expression, start: any, array: any[], context): any => array.reduce( ($$ACC, $$VALUE, $$INDEX, $$ARRAY) => @@ -92,28 +96,27 @@ export const $arrayReduce = interpreter( ), start ), - [null, 'any', 'array'] -) - -const _arrayIterator = (method: string) => - interpreter( - (iteratorExp: Expression, array: any[], context: EvaluationContext): any => - array[method](($$VALUE, $$INDEX, $$ARRAY) => - evaluate( - { - ...context, - scope: { - $$PARENT_SCOPE: context.scope, - $$VALUE, - $$INDEX, - $$ARRAY, - }, + [null, 'any', 'array'], +] + +const _arrayIterator = (method: string): ExpressionInterpreterSpec => [ + (iteratorExp: Expression, array: any[], context: EvaluationContext): any => + array[method](($$VALUE, $$INDEX, $$ARRAY) => + evaluate( + { + ...context, + scope: { + $$PARENT_SCOPE: context.scope, + $$VALUE, + $$INDEX, + $$ARRAY, }, - iteratorExp - ) - ), - [null, 'array'] - ) + }, + iteratorExp + ) + ), + [null, 'array'], +] /** * @function $arrayMap @@ -124,7 +127,7 @@ const _arrayIterator = (method: string) => * `$$INDEX`, `$$ARRAY`, `$$ACC` * @param {Array} [array=$$VALUE] */ -export const $arrayMap = _arrayIterator('map') +export const $arrayMap: ExpressionInterpreterSpec = _arrayIterator('map') /** * `Array.prototype.every` @@ -138,7 +141,7 @@ export const $arrayMap = _arrayIterator('map') * @param {Expression} everyExp * @param {Array} [array=$$VALUE] */ -export const $arrayEvery = _arrayIterator('every') +export const $arrayEvery: ExpressionInterpreterSpec = _arrayIterator('every') /** * `Array.prototype.some` @@ -147,51 +150,53 @@ export const $arrayEvery = _arrayIterator('every') * @param {Expression} someExp * @param {Array} [array=$$VALUE] */ -export const $arraySome = _arrayIterator('some') +export const $arraySome: ExpressionInterpreterSpec = _arrayIterator('some') /** * @function $arrayFilter * @param {Boolean} queryExp * @param {Array} [array=$$VALUE] */ -export const $arrayFilter = _arrayIterator('filter') +export const $arrayFilter: ExpressionInterpreterSpec = _arrayIterator('filter') /** * @function $arrayFindIndex * @param {Boolean} queryExp * @param {Array} [array=$$VALUE] */ -export const $arrayFindIndex = _arrayIterator('findIndex') +export const $arrayFindIndex: ExpressionInterpreterSpec = _arrayIterator( + 'findIndex' +) /** * @function $arrayIndexOf * @param {*} value * @param {Array} [array=$$VALUE] */ -export const $arrayIndexOf = interpreter( +export const $arrayIndexOf: ExpressionInterpreterSpec = [ (value: any, array: any[]): number => array.indexOf(value), - ['any', 'array'] -) + ['any', 'array'], +] /** * @function $arrayFind * @param {Boolean} queryExp * @param {Array} [array=$$VALUE] */ -export const $arrayFind = _arrayIterator('find') +export const $arrayFind: ExpressionInterpreterSpec = _arrayIterator('find') /** * @function $arrayReverse * @param {Array} [array=$$VALUE] */ -export const $arrayReverse = interpreter( +export const $arrayReverse: ExpressionInterpreterSpec = [ (array: any[]): any[] => { const arr = array.slice() arr.reverse() return arr }, - ['array'] -) + ['array'], +] const _sortDefault = (a, b) => { if (a === undefined) { @@ -210,7 +215,7 @@ const _sortDefault = (a, b) => { * @param {String | Expression | [Expression, string]} sort * @param {Array} [array=$$VALUE] */ -export const $arraySort = interpreter( +export const $arraySort: ExpressionInterpreterSpec = [ ( sort: string | Expression | [Expression, string] = 'ASC', array: any[], @@ -242,46 +247,46 @@ export const $arraySort = interpreter( .slice() .sort(order === 'DESC' ? (a, b) => -1 * sortFn(a, b) : sortFn) }, - [null, 'array'] -) + [null, 'array'], +] /** * @function $arrayPush * @param {*} valueExp * @param {Array} [array=$$VALUE] */ -export const $arrayPush = interpreter( +export const $arrayPush: ExpressionInterpreterSpec = [ (value: any, array: any[]): any[] => [...array, value], - ['any', 'array'] -) + ['any', 'array'], +] /** * @function $arrayPop * @param {Array} [array=$$VALUE] */ -export const $arrayPop = interpreter( +export const $arrayPop: ExpressionInterpreterSpec = [ (array: any[]) => array.slice(0, array.length - 1), - ['array'] -) + ['array'], +] /** * @function $arrayUnshift * @param {*} valueExp * @param {Array} [array=$$VALUE] */ -export const $arrayUnshift = interpreter( +export const $arrayUnshift: ExpressionInterpreterSpec = [ (value: any, array: any[]): any[] => [value, ...array], - ['any', 'array'] -) + ['any', 'array'], +] /** * @function $arrayShift * @param {Array} [array=$$VALUE] */ -export const $arrayShift = interpreter( +export const $arrayShift: ExpressionInterpreterSpec = [ (array: any[]): any[] => array.slice(1, array.length), - ['array'] -) + ['array'], +] /** * @function $arraySlice @@ -290,10 +295,10 @@ export const $arrayShift = interpreter( * @param {Array} [array=$$VALUE] * @returns {Array} */ -export const $arraySlice = interpreter( +export const $arraySlice: ExpressionInterpreterSpec = [ (start: number, end: number, array: any[]): any[] => array.slice(start, end), - ['number', 'number', 'array'] -) + ['number', 'number', 'array'], +] /** * @function $arrayReplace @@ -302,7 +307,7 @@ export const $arraySlice = interpreter( * @param {Array} [array=$$VALUE] * @returns {Array} */ -export const $arrayReplace = interpreter( +export const $arrayReplace: ExpressionInterpreterSpec = [ ( indexOrRange: number | [number, number], replacement: any, @@ -319,8 +324,8 @@ export const $arrayReplace = interpreter( ? [...head, ...replacement, ...tail] : [...head, replacement, ...tail] }, - [['number', 'array'], 'any', 'array'] -) + [['number', 'array'], 'any', 'array'], +] /** * Adds items at the given position. @@ -331,7 +336,7 @@ export const $arrayReplace = interpreter( * @param {Array} [array=$$VALUE] * @returns {Array} resultingArray The array with items added at position */ -export const $arrayAddAt = interpreter( +export const $arrayAddAt: ExpressionInterpreterSpec = [ (index: number, values: any[], array: any[]) => { const head = array.slice(0, index) const tail = array.slice(index) @@ -340,8 +345,8 @@ export const $arrayAddAt = interpreter( ? [...head, ...values, ...tail] : [...head, values, ...tail] }, - ['number', 'any', 'array'] -) + ['number', 'any', 'array'], +] /** * @function $arrayRemoveAt @@ -350,13 +355,13 @@ export const $arrayAddAt = interpreter( * @param {Array} [array=$$VALUE] * @returns {Array} resultingArray The array without the removed item */ -export const $arrayRemoveAt = interpreter( +export const $arrayRemoveAt: ExpressionInterpreterSpec = [ (position: number, count: number = 1, array: any[]): any[] => [ ...array.slice(0, position), ...array.slice(position + count), ], - ['number', ['number', 'undefined'], 'array'] -) + ['number', ['number', 'undefined'], 'array'], +] /** * @function $arrayJoin @@ -364,10 +369,10 @@ export const $arrayRemoveAt = interpreter( * @param {Array} [array=$$VALUE] * @returns {String} */ -export const $arrayJoin = interpreter( +export const $arrayJoin: ExpressionInterpreterSpec = [ (separator: string = '', array: any[]): string => array.join(separator), - [['string', 'undefined'], 'array'] -) + [['string', 'undefined'], 'array'], +] /** * @function $arrayAt @@ -375,10 +380,10 @@ export const $arrayJoin = interpreter( * @param {Array} [array=$$VALUE] * @returns {*} value */ -export const $arrayAt = interpreter( +export const $arrayAt: ExpressionInterpreterSpec = [ (index: number, array: any[]): any => array[index], - ['number', 'array'] -) + ['number', 'array'], +] export const ARRAY_EXPRESSIONS = { $arrayIncludes, diff --git a/src/expressions/boolean.spec.ts b/src/expressions/boolean.spec.ts index dd9f1c5..4d23798 100644 --- a/src/expressions/boolean.spec.ts +++ b/src/expressions/boolean.spec.ts @@ -1,13 +1,14 @@ import { evaluate } from '../evaluate' +import { syncInterpreterList } from '../interpreter' import { $value } from './value' import { $boolean } from './boolean' -describe('$boolean', () => { - const interpreters = { - $value, - $boolean, - } +const interpreters = syncInterpreterList({ + $value, + $boolean, +}) +describe('$boolean', () => { test('numbers', () => { expect( evaluate( diff --git a/src/expressions/boolean.ts b/src/expressions/boolean.ts index 6f2a6f5..590be23 100644 --- a/src/expressions/boolean.ts +++ b/src/expressions/boolean.ts @@ -1,13 +1,15 @@ -import { interpreter } from '../interpreter' +import { ExpressionInterpreterSpec } from '../types' /** * @function $boolean * @param {*} value * @returns {Boolean} */ -export const $boolean = interpreter((value: any): boolean => Boolean(value), [ - 'any', -]) +export const $boolean: ExpressionInterpreterSpec = [ + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + (value: any): boolean => Boolean(value), + ['any'], +] export const BOOLEAN_EXPRESSIONS = { $boolean, diff --git a/src/expressions/comparison.spec.ts b/src/expressions/comparison.spec.ts index b9b3fba..290fcad 100644 --- a/src/expressions/comparison.spec.ts +++ b/src/expressions/comparison.spec.ts @@ -1,4 +1,5 @@ import { evaluate } from '../evaluate' +import { syncInterpreterList } from '../interpreter' import { $stringSubstr } from './string' import { $eq, @@ -14,12 +15,12 @@ import { import { $value } from './value' describe('$eq / $notEq', () => { - const interpreters = { + const interpreters = syncInterpreterList({ $value, $stringSubstr, $eq, $notEq, - } + }) test('string', () => { const context = { @@ -45,11 +46,11 @@ describe('$eq / $notEq', () => { }) describe('$in / $notIn', () => { - const interpreters = { + const interpreters = syncInterpreterList({ $value, $in, $notIn, - } + }) test('basic', () => { const context = { @@ -68,13 +69,13 @@ describe('$in / $notIn', () => { }) describe('$gt / $gte / $lt / $lte', () => { - const interpreters = { + const interpreters = syncInterpreterList({ $value, $gt, $gte, $lt, $lte, - } + }) const context = { interpreters, @@ -109,7 +110,7 @@ describe('$gt / $gte / $lt / $lte', () => { }) describe('$matches', () => { - const interpreters = { + const interpreters = syncInterpreterList({ $value, $eq, $notEq, @@ -120,7 +121,7 @@ describe('$matches', () => { $lt, $lte, $matches, - } + }) test('basic', () => { const context = { diff --git a/src/expressions/comparison.ts b/src/expressions/comparison.ts index bc1f469..61768fa 100644 --- a/src/expressions/comparison.ts +++ b/src/expressions/comparison.ts @@ -1,14 +1,17 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ + import { isEqual } from 'lodash' -import { interpreter } from '../interpreter' import { evaluate, evaluateTyped } from '../evaluate' -import { EvaluationContext, PlainObject } from '../types' +import { + EvaluationContext, + PlainObject, + ExpressionInterpreterSpec, +} from '../types' import { $$VALUE } from './value' -const _negation = (fn) => (...args): boolean => !fn(...args) - /** * Checks if the two values * @@ -17,10 +20,10 @@ const _negation = (fn) => (...args): boolean => !fn(...args) * @param {*} valueExp Value being compared. * @returns {Boolean} */ -export const $eq = interpreter( +export const $eq: ExpressionInterpreterSpec = [ (valueB: any, valueA: any): boolean => isEqual(valueA, valueB), - ['any', 'any'] -) + ['any', 'any'], +] /** * @function $notEq @@ -28,7 +31,10 @@ export const $eq = interpreter( * @param {*} valueExp Value being compared. * @returns {Boolean} */ -export const $notEq = _negation($eq) +export const $notEq: ExpressionInterpreterSpec = [ + (valueB: any, valueA: any): boolean => !isEqual(valueA, valueB), + ['any', 'any'], +] /** * Checks whether the value is in the given array. @@ -38,11 +44,11 @@ export const $notEq = _negation($eq) * @param {*} valueExp * @returns {Boolean} */ -export const $in = interpreter( +export const $in: ExpressionInterpreterSpec = [ (array: any[], value: any): boolean => array.some((item) => isEqual(item, value)), - ['array', 'any'] -) + ['array', 'any'], +] /** * Checks whether the value is **not** in the given array. @@ -52,7 +58,11 @@ export const $in = interpreter( * @param {*} valueExp * @returns {Boolean} */ -export const $notIn = _negation($in) +export const $notIn: ExpressionInterpreterSpec = [ + (array: any[], value: any): boolean => + array.every((item) => !isEqual(item, value)), + ['array', 'any'], +] /** * Greater than `value > threshold` @@ -62,10 +72,10 @@ export const $notIn = _negation($in) * @param {Number} valueExp * @returns {Boolean} */ -export const $gt = interpreter( +export const $gt: ExpressionInterpreterSpec = [ (reference: number, value: number): boolean => value > reference, - ['number', 'number'] -) + ['number', 'number'], +] /** * Greater than or equal `value >= threshold` @@ -75,10 +85,10 @@ export const $gt = interpreter( * @param {Number} valueExp * @returns {Boolean} */ -export const $gte = interpreter( +export const $gte: ExpressionInterpreterSpec = [ (reference: number, value: number): boolean => value >= reference, - ['number', 'number'] -) + ['number', 'number'], +] /** * Lesser than `value < threshold` @@ -88,10 +98,10 @@ export const $gte = interpreter( * @param {Number} valueExp * @returns {Boolean} */ -export const $lt = interpreter( +export const $lt: ExpressionInterpreterSpec = [ (reference: number, value: number): boolean => value < reference, - ['number', 'number'] -) + ['number', 'number'], +] /** * Lesser than or equal `value <= threshold` @@ -101,10 +111,10 @@ export const $lt = interpreter( * @param {Number} valueExp * @returns {Boolean} */ -export const $lte = interpreter( +export const $lte: ExpressionInterpreterSpec = [ (reference: number, value: number): boolean => value <= reference, - ['number', 'number'] -) + ['number', 'number'], +] /** * Checks if the value matches the set of criteria. @@ -114,7 +124,7 @@ export const $lte = interpreter( * @param {Number} valueExp * @returns {Boolean} */ -export const $matches = interpreter( +export const $matches: ExpressionInterpreterSpec = [ (criteria: PlainObject, value: any, context: EvaluationContext): boolean => { const criteriaKeys = Object.keys(criteria) @@ -143,8 +153,8 @@ export const $matches = interpreter( ) }) }, - ['object', 'any'] -) + ['object', 'any'], +] export const COMPARISON_EXPRESSIONS = { $eq, diff --git a/src/expressions/functional.spec.ts b/src/expressions/functional.spec.ts index dd72fd6..1c36a92 100644 --- a/src/expressions/functional.spec.ts +++ b/src/expressions/functional.spec.ts @@ -1,4 +1,5 @@ import { evaluate } from '../evaluate' +import { syncInterpreterList } from '../interpreter' import { COMPARISON_EXPRESSIONS } from './comparison' import { VALUE_EXPRESSIONS } from './value' @@ -7,14 +8,14 @@ import { ARRAY_EXPRESSIONS } from './array' import { MATH_EXPRESSIONS } from './math' import { STRING_EXPRESSIONS } from './string' -const interpreters = { +const interpreters = syncInterpreterList({ ...VALUE_EXPRESSIONS, ...COMPARISON_EXPRESSIONS, ...FUNCTIONAL_EXPRESSIONS, ...ARRAY_EXPRESSIONS, ...MATH_EXPRESSIONS, ...STRING_EXPRESSIONS, -} +}) test('$pipe', () => { const SUM_2 = ['$arrayMap', ['$mathSum', 2]] diff --git a/src/expressions/functional.ts b/src/expressions/functional.ts index c5ed4b0..80ea6cc 100644 --- a/src/expressions/functional.ts +++ b/src/expressions/functional.ts @@ -1,14 +1,17 @@ -import { EvaluationContext, Expression } from '../types' +import { + EvaluationContext, + Expression, + ExpressionInterpreterSpec, +} from '../types' import { evaluate } from '../evaluate' -import { interpreter } from '../interpreter' /** * @function $pipe * @param {Expression[]} expressions * @returns {*} pipeResult */ -export const $pipe = interpreter( +export const $pipe: ExpressionInterpreterSpec = [ (expressions: Expression[], context: EvaluationContext): any => expressions.reduce((acc, expression) => { return evaluate( @@ -19,8 +22,8 @@ export const $pipe = interpreter( expression ) }, context.scope.$$VALUE), - ['array'] -) + ['array'], +] export const FUNCTIONAL_EXPRESSIONS = { $pipe, diff --git a/src/expressions/logical.spec.ts b/src/expressions/logical.spec.ts index 09d91f4..2e0ba9a 100644 --- a/src/expressions/logical.spec.ts +++ b/src/expressions/logical.spec.ts @@ -1,4 +1,5 @@ import { evaluate } from '../evaluate' +import { syncInterpreterList } from '../interpreter' import { VALUE_EXPRESSIONS } from './value' import { BOOLEAN_EXPRESSIONS } from './boolean' import { LOGICAL_EXPRESSIONS } from './logical' @@ -7,7 +8,7 @@ import { COMPARISON_EXPRESSIONS } from './comparison' import { STRING_EXPRESSIONS } from './string' import { MATH_EXPRESSIONS } from './math' -const interpreters = { +const interpreters = syncInterpreterList({ ...VALUE_EXPRESSIONS, ...BOOLEAN_EXPRESSIONS, ...LOGICAL_EXPRESSIONS, @@ -15,7 +16,7 @@ const interpreters = { ...COMPARISON_EXPRESSIONS, ...STRING_EXPRESSIONS, ...MATH_EXPRESSIONS, -} +}) describe('$and', () => { test('error situations', () => { diff --git a/src/expressions/logical.ts b/src/expressions/logical.ts index c0f20ea..b11d692 100644 --- a/src/expressions/logical.ts +++ b/src/expressions/logical.ts @@ -1,46 +1,55 @@ -import { interpreter } from '../interpreter' +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ + import { evaluate } from '../evaluate' -import { Expression, EvaluationContext, PlainObject } from '../types' +import { + Expression, + EvaluationContext, + PlainObject, + ExpressionInterpreterSpec, +} from '../types' /** * @function $and * @param {Array} expressionsExp * @returns {Boolean} */ -export const $and = interpreter( +export const $and: ExpressionInterpreterSpec = [ (expressions: Expression[], context: EvaluationContext): boolean => expressions.every((exp) => Boolean(evaluate(context, exp))), - [['array', 'undefined']] -) + [['array', 'undefined']], +] /** * @function $or * @param {Array} expressionsExp * @returns {Boolean} */ -export const $or = interpreter( +export const $or: ExpressionInterpreterSpec = [ (expressions: Expression[], context: EvaluationContext): boolean => expressions.some((exp) => Boolean(evaluate(context, exp))), - [['array', 'undefined']] -) + [['array', 'undefined']], +] /** * @function $not * @param {Array} expressionsExp * @returns {Boolean} */ -export const $not = interpreter((value: any): boolean => !value, ['any']) +export const $not: ExpressionInterpreterSpec = [ + (value: any): boolean => !value, + ['any'], +] /** * @function $nor * @param {Array} expressionsExp * @returns {Boolean} */ -export const $nor = interpreter( +export const $nor: ExpressionInterpreterSpec = [ (expressions: Expression[], context: EvaluationContext): boolean => expressions.every((exp) => !evaluate(context, exp)), - ['array'] -) + ['array'], +] /** * @function $xor @@ -48,10 +57,10 @@ export const $nor = interpreter( * @param {Boolean} expressionB * @returns {Boolean} */ -export const $xor = interpreter( +export const $xor: ExpressionInterpreterSpec = [ (valueA: any, valueB: any): boolean => Boolean(valueA) !== Boolean(valueB), - ['any', 'any'] -) + ['any', 'any'], +] /** * @function $if @@ -60,7 +69,7 @@ export const $xor = interpreter( * @param {Expression} elseExp * @returns {*} result */ -export const $if = interpreter( +export const $if: ExpressionInterpreterSpec = [ ( condition: any, thenExp: Expression, @@ -72,8 +81,8 @@ export const $if = interpreter( 'any', null, // Only evaluate if condition is satisfied null, // Only evaluate if condition is not satisfied - ] -) + ], +] type Case = [Expression, Expression] @@ -83,7 +92,7 @@ type Case = [Expression, Expression] * @param {Expression} defaultExp * @returns {*} result */ -export const $switch = interpreter( +export const $switch: ExpressionInterpreterSpec = [ (cases: Case[], defaultExp: Expression, context: EvaluationContext): any => { const correspondingCase = cases.find(([conditionExp]) => Boolean(evaluate(context, conditionExp)) @@ -94,8 +103,10 @@ export const $switch = interpreter( : evaluate(context, defaultExp) }, ['array', null], - false -) + { + defaultParam: -1, + }, +] /** * @function $switchKey @@ -106,7 +117,7 @@ export const $switch = interpreter( * @param {String} ValueExp * @returns {*} */ -export const $switchKey = interpreter( +export const $switchKey: ExpressionInterpreterSpec = [ ( cases: PlainObject, defaultExp: Expression | undefined = undefined, @@ -119,8 +130,8 @@ export const $switchKey = interpreter( ? evaluate(context, correspondingCase) : evaluate(context, defaultExp) }, - ['object', null, 'any'] -) + ['object', null, 'any'], +] export const LOGICAL_EXPRESSIONS = { $and, diff --git a/src/expressions/math.spec.ts b/src/expressions/math.spec.ts index 38b6ec0..bc82b46 100644 --- a/src/expressions/math.spec.ts +++ b/src/expressions/math.spec.ts @@ -1,11 +1,12 @@ import { evaluate } from '../evaluate' +import { syncInterpreterList } from '../interpreter' import { VALUE_EXPRESSIONS } from './value' import { MATH_EXPRESSIONS } from './math' -const interpreters = { +const interpreters = syncInterpreterList({ ...VALUE_EXPRESSIONS, ...MATH_EXPRESSIONS, -} +}) describe('operations', () => { const context = { diff --git a/src/expressions/math.ts b/src/expressions/math.ts index 813d7aa..1798558 100644 --- a/src/expressions/math.ts +++ b/src/expressions/math.ts @@ -1,4 +1,4 @@ -import { interpreter } from '../interpreter' +import { ExpressionInterpreterSpec } from '../types' /** * @function $mathSum @@ -6,10 +6,10 @@ import { interpreter } from '../interpreter' * @param {Number} [base=$$VALUE] * @returns {Number} result */ -export const $mathSum = interpreter( +export const $mathSum: ExpressionInterpreterSpec = [ (sum: number, base: number): number => base + sum, - ['number', 'number'] -) + ['number', 'number'], +] /** * @function $mathSub @@ -17,10 +17,10 @@ export const $mathSum = interpreter( * @param {Number} [base=$$VALUE] * @returns {Number} result */ -export const $mathSub = interpreter( +export const $mathSub: ExpressionInterpreterSpec = [ (sub: number, base: number): number => base - sub, - ['number', 'number'] -) + ['number', 'number'], +] /** * @function $mathMult @@ -28,10 +28,10 @@ export const $mathSub = interpreter( * @param {Number} [base=$$VALUE] * @returns {Number} result */ -export const $mathMult = interpreter( +export const $mathMult: ExpressionInterpreterSpec = [ (mult: number, base: number): number => base * mult, - ['number', 'number'] -) + ['number', 'number'], +] /** * @function $mathDiv @@ -39,10 +39,10 @@ export const $mathMult = interpreter( * @param {Number} dividend * @returns {Number} result */ -export const $mathDiv = interpreter( +export const $mathDiv: ExpressionInterpreterSpec = [ (divisor: number, dividend: number): number => dividend / divisor, - ['number', 'number'] -) + ['number', 'number'], +] /** * @function $mathMod @@ -50,10 +50,10 @@ export const $mathDiv = interpreter( * @param {Number} dividend * @returns {Number} result */ -export const $mathMod = interpreter( +export const $mathMod: ExpressionInterpreterSpec = [ (divisor: number, dividend: number): number => dividend % divisor, - ['number', 'number'] -) + ['number', 'number'], +] /** * @function $mathPow @@ -61,20 +61,20 @@ export const $mathMod = interpreter( * @param {Number} [base=$$VALUE] * @returns {Number} result */ -export const $mathPow = interpreter( +export const $mathPow: ExpressionInterpreterSpec = [ (exponent: number, base: number): number => Math.pow(base, exponent), - ['number', 'number'] -) + ['number', 'number'], +] /** * @function $mathAbs * @param {Number} [value=$$VALUE] * @returns {Number} result */ -export const $mathAbs = interpreter( +export const $mathAbs: ExpressionInterpreterSpec = [ (value: number): number => Math.abs(value), - ['number'] -) + ['number'], +] /** * @function $mathMax @@ -82,13 +82,13 @@ export const $mathAbs = interpreter( * @param {Number} [value=$$VALUE] * @returns {Number} result */ -export const $mathMax = interpreter( +export const $mathMax: ExpressionInterpreterSpec = [ (otherValue: number | number[], value: number): number => Array.isArray(otherValue) ? Math.max(value, ...otherValue) : Math.max(value, otherValue), - [['number', 'array'], 'number'] -) + [['number', 'array'], 'number'], +] /** * @function $mathMin @@ -96,42 +96,42 @@ export const $mathMax = interpreter( * @param {Number} [value=$$VALUE] * @returns {Number} result */ -export const $mathMin = interpreter( +export const $mathMin: ExpressionInterpreterSpec = [ (otherValue: number | number[], value: number): number => Array.isArray(otherValue) ? Math.min(value, ...otherValue) : Math.min(value, otherValue), - [['number', 'array'], 'number'] -) + [['number', 'array'], 'number'], +] /** * @function $mathRound * @param {Number} [value=$$VALUE] * @returns {Number} result */ -export const $mathRound = interpreter( +export const $mathRound: ExpressionInterpreterSpec = [ (value: number): number => Math.round(value), - ['number'] -) + ['number'], +] /** * @function $mathFloor * @param {Number} [value=$$VALUE] * @returns {Number} result */ -export const $mathFloor = interpreter( +export const $mathFloor: ExpressionInterpreterSpec = [ (value: number): number => Math.floor(value), - ['number'] -) + ['number'], +] /** * @function $mathCeil * @param {Number} [value=$$VALUE] * @returns {Number} result */ -export const $mathCeil = interpreter( +export const $mathCeil: ExpressionInterpreterSpec = [ (value: number): number => Math.ceil(value), - ['number'] -) + ['number'], +] export const MATH_EXPRESSIONS = { $mathSum, diff --git a/src/expressions/number.spec.ts b/src/expressions/number.spec.ts index a102383..86889f1 100644 --- a/src/expressions/number.spec.ts +++ b/src/expressions/number.spec.ts @@ -1,11 +1,12 @@ import { evaluate } from '../evaluate' +import { syncInterpreterList } from '../interpreter' import { VALUE_EXPRESSIONS } from './value' import { NUMBER_EXPRESSIONS } from './number' -const interpreters = { +const interpreters = syncInterpreterList({ ...VALUE_EXPRESSIONS, ...NUMBER_EXPRESSIONS, -} +}) test('$numberInt', () => { expect( diff --git a/src/expressions/number.ts b/src/expressions/number.ts index 591e51f..fc410b7 100644 --- a/src/expressions/number.ts +++ b/src/expressions/number.ts @@ -1,4 +1,4 @@ -import { interpreter } from '../interpreter' +import { ExpressionInterpreterSpec } from '../types' /** * @function $numberInt @@ -6,7 +6,8 @@ import { interpreter } from '../interpreter' * @param {*} value * @returns {Number} */ -export const $numberInt = interpreter( +export const $numberInt: ExpressionInterpreterSpec = [ + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types (radix: number = 10, value: any): number => { if (typeof value === 'number') { return value @@ -16,15 +17,16 @@ export const $numberInt = interpreter( throw new TypeError(`Invalid value ${JSON.stringify(value)}`) } }, - [['number', 'undefined'], 'any'] -) + [['number', 'undefined'], 'any'], +] /** * @function $numberFloat * @param {*} value * @returns {Number} */ -export const $numberFloat = interpreter( +export const $numberFloat: ExpressionInterpreterSpec = [ + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types (value: any): number => { if (typeof value === 'number') { return value @@ -34,8 +36,8 @@ export const $numberFloat = interpreter( throw new TypeError(`Invalid value ${JSON.stringify(value)}`) } }, - ['any'] -) + ['any'], +] export const NUMBER_EXPRESSIONS = { $numberInt, diff --git a/src/expressions/object.spec.ts b/src/expressions/object.spec.ts index 27df02b..a8fc738 100644 --- a/src/expressions/object.spec.ts +++ b/src/expressions/object.spec.ts @@ -1,18 +1,19 @@ 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' -const interpreters = { +const interpreters = syncInterpreterList({ $value, ...STRING_EXPRESSIONS, ...COMPARISON_EXPRESSIONS, ...ARRAY_EXPRESSIONS, ...OBJECT_EXPRESSIONS, -} +}) const context = { interpreters, diff --git a/src/expressions/object.ts b/src/expressions/object.ts index 536a26c..fe16f30 100644 --- a/src/expressions/object.ts +++ b/src/expressions/object.ts @@ -1,14 +1,15 @@ import { get, set, isPlainObject } from 'lodash' import { evaluate, isExpression } from '../evaluate' -import { interpreter } from '../interpreter' import { objectDeepApplyDefaults } from '../util/deepApplyDefaults' import { objectDeepAssign } from '../util/deepAssign' -import { EvaluationContext, PlainObject } from '../types' - -import { $matches } from './comparison' +import { + EvaluationContext, + PlainObject, + ExpressionInterpreterSpec, +} from '../types' /** * @function $objectMatches @@ -16,7 +17,7 @@ import { $matches } from './comparison' * @param {Object} [value=$$VALUE] * @returns {Boolean} matches */ -export const $objectMatches = interpreter( +export const $objectMatches: ExpressionInterpreterSpec = [ ( criteriaByPath: PlainObject, value: PlainObject, @@ -40,17 +41,17 @@ export const $objectMatches = interpreter( ? criteriaByPath[path] : { $eq: criteriaByPath[path] } - return $matches( + return evaluate( { ...context, scope: { $$VALUE: get(value, path) }, }, - pathCriteria + ['$matches', pathCriteria] ) }) }, - ['object', 'object'] -) + ['object', 'object'], +] const _formatEvaluate = (context, targetValue, source) => { targetValue = @@ -100,9 +101,10 @@ const _formatObject = ( * @param {*} [source=$$VALUE] * @returns {Object | Array} object */ -export const $objectFormat = interpreter( +export const $objectFormat: ExpressionInterpreterSpec = [ ( format: PlainObject | any[], + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types source: any, context: EvaluationContext ): PlainObject | any[] => { @@ -110,8 +112,8 @@ export const $objectFormat = interpreter( ? _formatArray(context, format, source) : _formatObject(context, format, source) }, - [['object', 'array'], 'any'] -) + [['object', 'array'], 'any'], +] /** * @function $objectDefaults @@ -119,11 +121,11 @@ export const $objectFormat = interpreter( * @param {Object} [base=$$VALUE] * @returns {Object} */ -export const $objectDefaults = interpreter( +export const $objectDefaults: ExpressionInterpreterSpec = [ (defaultValues: PlainObject, base: PlainObject): PlainObject => objectDeepApplyDefaults(base, defaultValues), - ['object', 'object'] -) + ['object', 'object'], +] /** * @function $objectAssign @@ -131,21 +133,21 @@ export const $objectDefaults = interpreter( * @param {Object} [base=$$VALUE] * @returns {Object} */ -export const $objectAssign = interpreter( +export const $objectAssign: ExpressionInterpreterSpec = [ (values: PlainObject, base: PlainObject): PlainObject => objectDeepAssign(base, values), - ['object', 'object'] -) + ['object', 'object'], +] /** * @function $objectKeys * @param {Object} object * @returns {String[]} */ -export const $objectKeys = interpreter( +export const $objectKeys: ExpressionInterpreterSpec = [ (obj: PlainObject): string[] => Object.keys(obj), - ['object'] -) + ['object'], +] export const OBJECT_EXPRESSIONS = { $objectMatches, diff --git a/src/expressions/string.spec.ts b/src/expressions/string.spec.ts index 0cd82d7..705a700 100644 --- a/src/expressions/string.spec.ts +++ b/src/expressions/string.spec.ts @@ -1,11 +1,12 @@ import { evaluate } from '../evaluate' +import { syncInterpreterList } from '../interpreter' import { VALUE_EXPRESSIONS } from './value' import { STRING_EXPRESSIONS } from './string' -const interpreters = { +const interpreters = syncInterpreterList({ ...VALUE_EXPRESSIONS, ...STRING_EXPRESSIONS, -} +}) test('$string', () => { const expectations = [ diff --git a/src/expressions/string.ts b/src/expressions/string.ts index cf69b14..9bbcfa3 100644 --- a/src/expressions/string.ts +++ b/src/expressions/string.ts @@ -1,6 +1,5 @@ -import { interpreter } from '../interpreter' import { get } from 'lodash' -import { PlainObject } from '../types' +import { PlainObject, ExpressionInterpreterSpec } from '../types' import { getType } from '@orioro/typing' @@ -46,10 +45,11 @@ const stringifyValue = (value) => { * @param {*} [value=$$VALUE] * @returns {String} */ -export const $string = interpreter( +export const $string: ExpressionInterpreterSpec = [ + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types (value: any): string => stringifyValue(value), - ['any'] -) + ['any'], +] /** * @function $stringStartsWith @@ -57,19 +57,20 @@ export const $string = interpreter( * @param {String} [str=$$VALUE] * @returns {Boolean} */ -export const $stringStartsWith = interpreter( +export const $stringStartsWith: ExpressionInterpreterSpec = [ (query: string, str: string): boolean => str.startsWith(query), - ['string', 'string'] -) + ['string', 'string'], +] /** * @function $stringLength * @param {String} [str=$$VALUE] * @returns {Number} */ -export const $stringLength = interpreter((str: string): number => str.length, [ - 'string', -]) +export const $stringLength: ExpressionInterpreterSpec = [ + (str: string): number => str.length, + ['string'], +] /** * @function $stringSubstr @@ -77,11 +78,11 @@ export const $stringLength = interpreter((str: string): number => str.length, [ * @param {Number} end * @param {String} [str=$$VALUE] */ -export const $stringSubstr = interpreter( +export const $stringSubstr: ExpressionInterpreterSpec = [ (start: number, end: number | undefined, str: string): string => str.substring(start, end), - ['number', ['number', 'undefined'], 'string'] -) + ['number', ['number', 'undefined'], 'string'], +] /** * @function $stringConcat @@ -89,20 +90,21 @@ export const $stringSubstr = interpreter( * @param {String} [base=$$VALUE] * @returns {String} */ -export const $stringConcat = interpreter( +export const $stringConcat: ExpressionInterpreterSpec = [ (concat: string | string[], base: string): string => Array.isArray(concat) ? base.concat(concat.join('')) : base.concat(concat), - [['string', 'array'], 'string'] -) + [['string', 'array'], 'string'], +] /** * @function $stringTrim * @param {String} [str=$$VALUE] * @returns {String} */ -export const $stringTrim = interpreter((str: string): string => str.trim(), [ - 'string', -]) +export const $stringTrim: ExpressionInterpreterSpec = [ + (str: string): string => str.trim(), + ['string'], +] /** * @function $stringPadStart @@ -111,11 +113,11 @@ export const $stringTrim = interpreter((str: string): string => str.trim(), [ * @param {String} [str=$$VALUE] * @returns {String} */ -export const $stringPadStart = interpreter( +export const $stringPadStart: ExpressionInterpreterSpec = [ (targetLength: number, padString: string, str: string): string => str.padStart(targetLength, padString), - ['number', 'string', 'string'] -) + ['number', 'string', 'string'], +] /** * @function $stringPadEnd @@ -124,31 +126,31 @@ export const $stringPadStart = interpreter( * @param {String} [str=$$VALUE] * @returns {String} */ -export const $stringPadEnd = interpreter( +export const $stringPadEnd: ExpressionInterpreterSpec = [ (targetLength: number, padString: string, str: string): string => str.padEnd(targetLength, padString), - ['number', 'string', 'string'] -) + ['number', 'string', 'string'], +] /** * @function $stringToUpperCase * @param {String} value * @returns {String} */ -export const $stringToUpperCase = interpreter( +export const $stringToUpperCase: ExpressionInterpreterSpec = [ (str: string): string => str.toUpperCase(), - ['string'] -) + ['string'], +] /** * @function $stringToLowerCase * @param {String} value * @returns {String} */ -export const $stringToLowerCase = interpreter( +export const $stringToLowerCase: ExpressionInterpreterSpec = [ (str: string): string => str.toLowerCase(), - ['string'] -) + ['string'], +] /** * /\$\{\s*([\w$.]+)\s*\}/g @@ -185,15 +187,15 @@ const INTERPOLATABLE_TYPES = ['string', 'number'] * only interpreted as paths to the value. No logic * supported: loops, conditionals, etc. */ -export const $stringInterpolate = interpreter( +export const $stringInterpolate: ExpressionInterpreterSpec = [ (data: PlainObject | any[], template: string): string => template.replace(INTERPOLATION_REGEXP, (match, path) => { const value = get(data, path) return INTERPOLATABLE_TYPES.includes(typeof value) ? value : '' }), - [['object', 'array'], 'string'] -) + [['object', 'array'], 'string'], +] export const STRING_EXPRESSIONS = { $string, diff --git a/src/expressions/type.spec.ts b/src/expressions/type.spec.ts index bde7f5e..236310e 100644 --- a/src/expressions/type.spec.ts +++ b/src/expressions/type.spec.ts @@ -1,12 +1,13 @@ import { evaluate } from '../evaluate' +import { syncInterpreterList } from '../interpreter' import { $value } from './value' import { TYPE_EXPRESSIONS, typeExpressions } from './type' import { testCases } from '@orioro/jest-util' -const interpreters = { +const interpreters = syncInterpreterList({ $value, ...TYPE_EXPRESSIONS, -} +}) describe('$type', () => { testCases( @@ -56,6 +57,11 @@ describe('typeExpressions(types)', () => { normalString: (value) => typeof value === 'string', }) + const customTypeInterpreters = syncInterpreterList({ + $customType, + $customIsType, + }) + describe('$customType', () => { testCases( [ @@ -70,8 +76,7 @@ describe('typeExpressions(types)', () => { { interpreters: { ...interpreters, - $customType, - $customIsType, + ...customTypeInterpreters, }, scope: { $$VALUE: value }, }, @@ -96,8 +101,7 @@ describe('typeExpressions(types)', () => { { interpreters: { ...interpreters, - $customType, - $customIsType, + ...customTypeInterpreters, }, scope: { $$VALUE: value }, }, diff --git a/src/expressions/type.ts b/src/expressions/type.ts index 570efb5..177a4fb 100644 --- a/src/expressions/type.ts +++ b/src/expressions/type.ts @@ -1,10 +1,9 @@ -import { interpreter } from '../interpreter' import { typing, CORE_TYPES } from '@orioro/typing' -import { TypeAlternatives, TypeMap, ExpressionInterpreter } from '../types' +import { TypeAlternatives, TypeMap, ExpressionInterpreterSpec } from '../types' export const typeExpressions = ( types: TypeAlternatives | TypeMap -): [ExpressionInterpreter, ExpressionInterpreter] => { +): [ExpressionInterpreterSpec, ExpressionInterpreterSpec] => { const { getType, isType, @@ -33,7 +32,10 @@ export const typeExpressions = ( * - weakmap * - weakset */ - const $type = interpreter((value: any): string => getType(value), ['any']) + const $type: ExpressionInterpreterSpec = [ + (value: any): string => getType(value), + ['any'], + ] /** * @function $isType @@ -41,19 +43,19 @@ export const typeExpressions = ( * @param {*} value * @returns {Boolean} */ - const $isType = interpreter( + const $isType: ExpressionInterpreterSpec = [ (type: string, value: any): boolean => isType(type, value), - [['string', 'array', 'object'], 'any'] - ) + [['string', 'array', 'object'], 'any'], + ] // // The usage of $validateType expression is not clear, we've opted for not // exposing it for now // - // const $validateType = interpreter( + // const $validateType = [ // (type: string, value: any): void => validateType(type, value), // [['string', 'array', 'object'], 'any'] - // ) + // ] return [$type, $isType] } diff --git a/src/expressions/value.spec.ts b/src/expressions/value.spec.ts index 8915b3e..85f3971 100644 --- a/src/expressions/value.spec.ts +++ b/src/expressions/value.spec.ts @@ -1,17 +1,18 @@ 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' -const interpreters = { +const interpreters = syncInterpreterList({ ...MATH_EXPRESSIONS, ...LOGICAL_EXPRESSIONS, ...COMPARISON_EXPRESSIONS, ...ARRAY_EXPRESSIONS, ...VALUE_EXPRESSIONS, -} +}) describe('$value', () => { test('basic', () => { diff --git a/src/expressions/value.ts b/src/expressions/value.ts index 2f94e50..c5c37c7 100644 --- a/src/expressions/value.ts +++ b/src/expressions/value.ts @@ -1,9 +1,13 @@ import { get } from 'lodash' import { evaluate } from '../evaluate' -import { interpreter } from '../interpreter' -import { Expression, EvaluationContext } from '../types' +import { + Expression, + EvaluationContext, + EvaluationScope, + ExpressionInterpreterSpec, +} from '../types' const PATH_VARIABLE_RE = /^\$\$.+/ @@ -15,7 +19,7 @@ export const $$VALUE: Expression = ['$value', '$$VALUE'] * @param {*} defaultExp * @returns {*} value */ -export const $value = interpreter( +export const $value: ExpressionInterpreterSpec = [ ( path: string = '$$VALUE', defaultExp: Expression, @@ -32,15 +36,22 @@ export const $value = interpreter( : value }, [['string', 'undefined'], null], - false -) + { + defaultParam: -1, + }, +] /** * @function $literal * @param {*} value * @returns {*} */ -export const $literal = interpreter((value: any): any => value, [null], false) +export const $literal: ExpressionInterpreterSpec = [ + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + (value: any): any => value, + [null], + { defaultParam: -1 }, +] /** * @function $evaluate @@ -48,8 +59,12 @@ export const $literal = interpreter((value: any): any => value, [null], false) * @param {Object | null} scopeExp * @returns {*} */ -export const $evaluate = interpreter( - (expression: Expression, scope, context: EvaluationContext): any => +export const $evaluate: ExpressionInterpreterSpec = [ + ( + expression: Expression, + scope: EvaluationScope, + context: EvaluationContext + ): any => evaluate( { ...context, @@ -58,12 +73,16 @@ export const $evaluate = interpreter( expression ), [ - (context, expExp) => evaluate(context, expExp), - (context, scopeExp = null) => + (context: EvaluationContext, expExp: Expression | any): Expression => + evaluate(context, expExp), + ( + context: EvaluationContext, + scopeExp: Expression | null = null + ): EvaluationScope => scopeExp === null ? context.scope : evaluate(context, scopeExp), ], - false -) + { defaultParam: -1 }, +] export const VALUE_EXPRESSIONS = { $value, diff --git a/src/interpreter.ts b/src/interpreter.ts index 9fa9945..86f4b49 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -1,46 +1,16 @@ -import { validateType, ExpectedType } from '@orioro/typing' - import { isPlainObject } from 'lodash' -import { EvaluationContext, ExpressionInterpreter } from './types' - -import { evaluate, evaluateTyped } from './evaluate' +import { + EvaluationContext, + ExpressionInterpreter, + ExpressionInterpreterList, + ExpressionInterpreterFunction, + ExpressionInterpreterFunctionList, +} from './types' -type ParamResolverFunction = (context: EvaluationContext, arg: any) => any +import { evaluateTyped, evaluateTypedAsync } from './evaluate' -/** - * Defines how an expression argument should be resolved - * before being passed onto the expression interpreter function. - * - function(context, argumentValue): a function to be invoked with the evaluation context - * and the argument value - * - null: argument is passed on just as received - * - string: argument is evaluated and the result is checked for its type. Possible values: - * - string - * - regexp - * - number - * - bigint - * - nan - * - null - * - undefined - * - boolean - * - function - * - object - * - array - * - date - * - symbol - * - map - * - set - * - weakmap - * - weakset - * - any - * - * @typedef {Function | null | string | string[]} ParamResolver - */ -export type ParamResolver = - | ParamResolverFunction - | null - | ExpectedType - | ExpectedType[] +import { ParamResolver } from './types' const _paramResolverNoop = (context: EvaluationContext, arg: any): any => arg @@ -49,90 +19,130 @@ const _isExpectedType = (resolver: ParamResolver): boolean => 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 interpreter - * @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 - * parameters + the evaluation context as last - * argument. - * @param {ParamResolver[] | null} paramResolvers A list of resolvers that will - * convert the arguments given to the - * expression into the parameters - * of the interpreter function - * @param {Boolean} [defaultScopeValue=true] Whether the last argument of the - * expression should default to the - * context's scope's value - * ['$value', '$$VALUE']. If given a number, - * indicates the index of the parameter - * that should get the scope value by default + * @function syncInterpreter * @returns {ExpressionInterpreter} */ -export const interpreter = ( - interpreterFn: (...args: any[]) => any, - paramResolvers: ParamResolver[] | null, - defaultScopeValue: boolean | number = true -): ExpressionInterpreter => { - validateType(['array', 'null'], paramResolvers) - - if (Array.isArray(paramResolvers)) { - // - // 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) => { - if (typeof resolver === 'function') { - return resolver - } else if (_isExpectedType(resolver)) { - return resolver === 'any' - ? evaluate - : 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}` - ) - } - }) - - defaultScopeValue = - defaultScopeValue === true - ? _paramResolvers.length - 1 - : defaultScopeValue - - // - // For difference between `argument` and `parameter` definitions, see: - // https://developer.mozilla.org/en-US/docs/Glossary/Parameter - // - return typeof defaultScopeValue === 'number' - ? (context: EvaluationContext, ...args) => { - return interpreterFn( - ..._paramResolvers.map((resolver, index) => { - // Last param defaults to $$VALUE - const arg = - args[index] === undefined && index === defaultScopeValue - ? ['$value', '$$VALUE'] - : args[index] - return resolver(context, arg) - }), - context - ) - } - : (context: EvaluationContext, ...args) => - interpreterFn( - ..._paramResolvers.map((resolver, index) => - resolver(context, args[index]) - ), - context - ) - } else { - // - // By default all arguments are evaluated before - // being passed on to the interpreter - // - return (context: EvaluationContext, ...args) => - interpreterFn(...args.map((arg) => evaluate(context, arg))) +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/types.ts b/src/types.ts index 1b1ebb0..65f33b2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,7 +1,39 @@ -import { TypeAlternatives, TypeMap } from '@orioro/typing' +import { TypeAlternatives, TypeMap, ExpectedType } from '@orioro/typing' export { TypeAlternatives, TypeMap } +type ParamResolverFunction = (context: EvaluationContext, arg: any) => any + +/** + * Defines how an expression argument should be resolved + * before being passed onto the expression interpreter function. + * - function(context, argumentValue): a function to be invoked with the evaluation context + * and the argument value + * - null: argument is passed on just as received + * - string: argument is evaluated and the result is checked for its type. Possible values: + * - string + * - regexp + * - number + * - bigint + * - nan + * - null + * - undefined + * - boolean + * - function + * - object + * - array + * - date + * - symbol + * - map + * - set + * - weakmap + * - weakset + * - any + * + * @typedef {Function | null | string | string[]} ParamResolver + */ +export type ParamResolver = ParamResolverFunction | null | ExpectedType + /** * An expression is an array tuple with the first item * as a string identifier. @@ -10,17 +42,50 @@ export { TypeAlternatives, TypeMap } */ export type Expression = [string, ...any[]] +/** + * Specification of an expression interpreter. In this format + * the resulting expression interpreter (prepared through either + * `syncInterpreter`, `syncInterpreterList`, `asyncInterpreter` or + * `asyncInterpreterList`) may be compatible with sync and async formats. + * + * @typedef {[Function, ParamResolver[] | null, Object]} ExpressionInterpreterSpec + * @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 + * parameters + the evaluation context as last + * argument. + * @param {ParamResolver[] | null} paramResolvers A list of resolvers that will + * convert the arguments given to the + * expression into the parameters + * of the interpreter function + * @param {Object} [options={ defaultParam = paramResolvers.length}] The index of the + * argument that should receive + * the default value (`scope.$$VALUE`). + * By default, is the last parameter. + * In case the defaultParameter should not + * be used, use `defaultParam = -1` + */ +export type ExpressionInterpreterSpec = [ + (...args: any) => any, + ParamResolver[], + { defaultParam?: number }? +] + /** * Function that receives as first parameter the EvaluationContext * and should return the result for evaluating a given expression. * - * @typedef {Function} ExpressionInterpreter + * @typedef {Function} ExpressionInterpreterFunction */ -export type ExpressionInterpreter = ( +export type ExpressionInterpreterFunction = ( context: EvaluationContext, ...args: any[] ) => any +export type ExpressionInterpreter = + | ExpressionInterpreterSpec + | ExpressionInterpreterFunction + /** * @typedef {Object} EvaluationScope * @property {Object} scope @@ -53,14 +118,18 @@ export type ExpressionInterpreterList = { [key: string]: ExpressionInterpreter } +export type ExpressionInterpreterFunctionList = { + [key: string]: ExpressionInterpreterFunction +} + /** * @typedef {Object} EvaluationContext * @property {Object} context - * @property {ExpressionInterpreterList} context.interpreters + * @property {ExpressionInterpreterFunctionList} context.interpreters * @property {EvaluationScope} context.scope */ export type EvaluationContext = { - interpreters: ExpressionInterpreterList + interpreters: ExpressionInterpreterFunctionList scope: EvaluationScope } diff --git a/yarn.lock b/yarn.lock index 2c2dcd7..6d842d3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1246,10 +1246,10 @@ lodash "^4.17.20" luxon "^1.25.0" -"@orioro/jest-util@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@orioro/jest-util/-/jest-util-1.2.0.tgz#259106451a41dcc99e30b745a30a1479b5b630be" - integrity sha512-8eU4CI0D0iGYBWvTb0uOGMj4i7kSISX8O8BlhGnrtdyCuh21+oEUOygsdgazv9vZRqUm5V2RnbjbJ2AvxLcXDQ== +"@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== dependencies: is-plain-object "^5.0.0"