diff --git a/.gitignore b/.gitignore index 8c8cc8b..8fd6552 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules *.bundle.js dist coverage +tmp diff --git a/package.json b/package.json index c799de1..6d93fd5 100644 --- a/package.json +++ b/package.json @@ -42,12 +42,11 @@ "@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", "@rollup/plugin-node-resolve": "^9.0.0", - "@semantic-release/changelog": "^5.0.1", "@types/jest": "^26.0.14", "@typescript-eslint/eslint-plugin": "^4.15.0", "@typescript-eslint/parser": "^4.15.0", @@ -77,7 +76,6 @@ "plugins": [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", - "@semantic-release/changelog", "@semantic-release/npm", "@semantic-release/github" ] 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/expression.spec.ts b/src/evaluate.spec.ts similarity index 97% rename from src/expression.spec.ts rename to src/evaluate.spec.ts index 6a9c31e..0a8c9dc 100644 --- a/src/expression.spec.ts +++ b/src/evaluate.spec.ts @@ -1,4 +1,4 @@ -import { evaluateTyped } from './expression' +import { evaluateTyped } from './evaluate' describe('evaluateTyped(expectedTypes, context, value)', () => { test('simple type - example: number', () => { diff --git a/src/evaluate.ts b/src/evaluate.ts new file mode 100644 index 0000000..ce42bf5 --- /dev/null +++ b/src/evaluate.ts @@ -0,0 +1,103 @@ +import { validateType } from '@orioro/typing' + +import { + Expression, + ExpressionInterpreterList, + EvaluationContext, +} from './types' + +/** + * @function isExpression + * @param {ExpressionInterpreterList} + */ +export const isExpression = ( + interpreters: ExpressionInterpreterList, + candidateExpression: any // eslint-disable-line @typescript-eslint/explicit-module-boundary-types +): boolean => + Array.isArray(candidateExpression) && + typeof interpreters[candidateExpression[0]] === 'function' + +const _maybeExpression = (value) => + Array.isArray(value) && + typeof value[0] === 'string' && + value[0].startsWith('$') + +const _ellipsis = (str, maxlen = 50) => + str.length > maxlen ? str.substr(0, maxlen - 1).concat('...') : str + +const _evaluateDev = ( + context: EvaluationContext, + expOrValue: Expression | any +): any => { + if ( + !isExpression(context.interpreters, expOrValue) && + _maybeExpression(expOrValue) + ) { + console.warn( + `Possible missing expression error: ${_ellipsis( + JSON.stringify(expOrValue) + )}. No interpreter was found for '${expOrValue[0]}'` + ) + } + + 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) +} + +/** + * @function evaluate + * @param {EvaluationContext} context + * @param {Expression | *} expOrValue + * @returns {*} + */ +export const evaluate = + process && process.env && process.env.NODE_ENV !== 'production' + ? _evaluateDev + : _evaluate + +/** + * @function evaluateTyped + * @param {String | string[]} expectedTypes + * @param {EvaluationContext} context + * @param {Expression | any} expOrValue + * @returns {*} + */ +export const evaluateTyped = ( + expectedTypes: string | string[], + context: EvaluationContext, + expOrValue: Expression | any +): any => { + const value = evaluate(context, expOrValue) + 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/expression.ts b/src/expression.ts deleted file mode 100644 index 40902f7..0000000 --- a/src/expression.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { validateType, ExpectedType } from '@orioro/typing' - -import { isPlainObject } from 'lodash' - -import { - Expression, - ExpressionInterpreter, - ExpressionInterpreterList, - EvaluationContext, -} from './types' - -/** - * @function isExpression - * @param {ExpressionInterpreterList} - */ -export const isExpression = ( - interpreters: ExpressionInterpreterList, - candidateExpression: any // eslint-disable-line @typescript-eslint/explicit-module-boundary-types -): boolean => - Array.isArray(candidateExpression) && - typeof interpreters[candidateExpression[0]] === 'function' - -const _maybeExpression = (value) => - Array.isArray(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 - -const _evaluateDev = ( - context: EvaluationContext, - expOrValue: Expression | any -): any => { - if ( - !isExpression(context.interpreters, expOrValue) && - _maybeExpression(expOrValue) - ) { - console.warn( - `Possible missing expression error: ${_ellipsis( - JSON.stringify(expOrValue) - )}. No interpreter was found for '${expOrValue[0]}'` - ) - } - - return _evaluateProd(context, expOrValue) -} - -/** - * @function evaluate - * @param {EvaluationContext} context - * @param {Expression | *} expOrValue - * @returns {*} - */ -export const evaluate = - process && process.env && process.env.NODE_ENV !== 'production' - ? _evaluateDev - : _evaluateProd - -/** - * @function evaluateTyped - * @param {String | string[]} expectedTypes - * @param {EvaluationContext} context - * @param {Expression | any} expOrValue - * @returns {*} - */ -export const evaluateTyped = ( - expectedTypes: string | string[], - context: EvaluationContext, - expOrValue: Expression | any -): any => { - const value = evaluate(context, expOrValue) - validateType(expectedTypes, value) - return value -} - -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 - | ExpectedType[] - -const _paramResolverNoop = (context: EvaluationContext, arg: any): any => arg - -const _isExpectedType = (resolver: ParamResolver): boolean => - Array.isArray(resolver) || - isPlainObject(resolver) || - typeof resolver === 'string' - -/** - * @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 - * @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))) - } -} diff --git a/src/expressions/array.spec.ts b/src/expressions/array.spec.ts index 93a5f69..27ce5aa 100644 --- a/src/expressions/array.spec.ts +++ b/src/expressions/array.spec.ts @@ -1,4 +1,5 @@ -import { evaluate } from '../expression' +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 c0758b3..8bde0c0 100644 --- a/src/expressions/array.ts +++ b/src/expressions/array.ts @@ -1,10 +1,11 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ + +import { evaluate, evaluateTyped, isExpression } from '../evaluate' import { - interpreter, - evaluate, - evaluateTyped, - isExpression, -} from '../expression' -import { EvaluationContext, Expression } from '../types' + EvaluationContext, + Expression, + ExpressionInterpreterSpec, +} from '../types' import { validateType } from '@orioro/typing' export const $$INDEX = ['$value', '$$INDEX'] @@ -21,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 @@ -36,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 @@ -51,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 @@ -76,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) => @@ -95,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 @@ -127,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` @@ -141,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` @@ -150,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) { @@ -213,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[], @@ -245,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 @@ -293,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 @@ -305,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, @@ -322,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. @@ -334,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) @@ -343,8 +345,8 @@ export const $arrayAddAt = interpreter( ? [...head, ...values, ...tail] : [...head, values, ...tail] }, - ['number', 'any', 'array'] -) + ['number', 'any', 'array'], +] /** * @function $arrayRemoveAt @@ -353,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 @@ -367,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 @@ -378,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 7826b46..4d23798 100644 --- a/src/expressions/boolean.spec.ts +++ b/src/expressions/boolean.spec.ts @@ -1,13 +1,14 @@ -import { evaluate } from '../expression' +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 ec8591b..590be23 100644 --- a/src/expressions/boolean.ts +++ b/src/expressions/boolean.ts @@ -1,13 +1,15 @@ -import { interpreter } from '../expression' +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 6ae8807..290fcad 100644 --- a/src/expressions/comparison.spec.ts +++ b/src/expressions/comparison.spec.ts @@ -1,4 +1,5 @@ -import { evaluate } from '../expression' +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 9451a24..61768fa 100644 --- a/src/expressions/comparison.ts +++ b/src/expressions/comparison.ts @@ -1,13 +1,17 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ + import { isEqual } from 'lodash' -import { evaluate, evaluateTyped, interpreter } from '../expression' +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 * @@ -16,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 @@ -27,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. @@ -37,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. @@ -51,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` @@ -61,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` @@ -74,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` @@ -87,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` @@ -100,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. @@ -113,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) @@ -142,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 73cf5b3..1c36a92 100644 --- a/src/expressions/functional.spec.ts +++ b/src/expressions/functional.spec.ts @@ -1,4 +1,5 @@ -import { evaluate } from '../expression' +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 cf1ee19..80ea6cc 100644 --- a/src/expressions/functional.ts +++ b/src/expressions/functional.ts @@ -1,13 +1,17 @@ -import { EvaluationContext, Expression } from '../types' +import { + EvaluationContext, + Expression, + ExpressionInterpreterSpec, +} from '../types' -import { evaluate, interpreter } from '../expression' +import { evaluate } from '../evaluate' /** * @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( @@ -18,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 4db673f..2e0ba9a 100644 --- a/src/expressions/logical.spec.ts +++ b/src/expressions/logical.spec.ts @@ -1,4 +1,5 @@ -import { evaluate } from '../expression' +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 a3a0868..b11d692 100644 --- a/src/expressions/logical.ts +++ b/src/expressions/logical.ts @@ -1,45 +1,55 @@ -import { evaluate, interpreter } from '../expression' -import { Expression, EvaluationContext, PlainObject } from '../types' +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ + +import { evaluate } from '../evaluate' +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 @@ -47,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 @@ -59,7 +69,7 @@ export const $xor = interpreter( * @param {Expression} elseExp * @returns {*} result */ -export const $if = interpreter( +export const $if: ExpressionInterpreterSpec = [ ( condition: any, thenExp: Expression, @@ -71,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] @@ -82,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)) @@ -93,8 +103,10 @@ export const $switch = interpreter( : evaluate(context, defaultExp) }, ['array', null], - false -) + { + defaultParam: -1, + }, +] /** * @function $switchKey @@ -105,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, @@ -118,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 5ea9f1d..bc82b46 100644 --- a/src/expressions/math.spec.ts +++ b/src/expressions/math.spec.ts @@ -1,11 +1,12 @@ -import { evaluate } from '../expression' +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 57163c2..1798558 100644 --- a/src/expressions/math.ts +++ b/src/expressions/math.ts @@ -1,4 +1,4 @@ -import { interpreter } from '../expression' +import { ExpressionInterpreterSpec } from '../types' /** * @function $mathSum @@ -6,10 +6,10 @@ import { interpreter } from '../expression' * @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 03b1b75..86889f1 100644 --- a/src/expressions/number.spec.ts +++ b/src/expressions/number.spec.ts @@ -1,11 +1,12 @@ -import { evaluate } from '../expression' +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 f7f3b11..fc410b7 100644 --- a/src/expressions/number.ts +++ b/src/expressions/number.ts @@ -1,4 +1,4 @@ -import { interpreter } from '../expression' +import { ExpressionInterpreterSpec } from '../types' /** * @function $numberInt @@ -6,7 +6,8 @@ import { interpreter } from '../expression' * @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 cd3fc34..a8fc738 100644 --- a/src/expressions/object.spec.ts +++ b/src/expressions/object.spec.ts @@ -1,18 +1,19 @@ -import { evaluate } from '../expression' +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 d1d9115..fe16f30 100644 --- a/src/expressions/object.ts +++ b/src/expressions/object.ts @@ -1,13 +1,15 @@ import { get, set, isPlainObject } from 'lodash' -import { evaluate, isExpression, interpreter } from '../expression' +import { evaluate, isExpression } from '../evaluate' 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 @@ -15,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, @@ -39,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 = @@ -99,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[] => { @@ -109,8 +112,8 @@ export const $objectFormat = interpreter( ? _formatArray(context, format, source) : _formatObject(context, format, source) }, - [['object', 'array'], 'any'] -) + [['object', 'array'], 'any'], +] /** * @function $objectDefaults @@ -118,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 @@ -130,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 98a9644..705a700 100644 --- a/src/expressions/string.spec.ts +++ b/src/expressions/string.spec.ts @@ -1,11 +1,12 @@ -import { evaluate } from '../expression' +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 d4604e5..9bbcfa3 100644 --- a/src/expressions/string.ts +++ b/src/expressions/string.ts @@ -1,6 +1,5 @@ -import { interpreter } from '../expression' 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 d278944..236310e 100644 --- a/src/expressions/type.spec.ts +++ b/src/expressions/type.spec.ts @@ -1,12 +1,13 @@ -import { evaluate } from '../expression' +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 832cada..177a4fb 100644 --- a/src/expressions/type.ts +++ b/src/expressions/type.ts @@ -1,10 +1,9 @@ -import { interpreter } from '../expression' 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 9c0becb..85f3971 100644 --- a/src/expressions/value.spec.ts +++ b/src/expressions/value.spec.ts @@ -1,17 +1,18 @@ -import { evaluate } from '../expression' +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 e56003d..c5c37c7 100644 --- a/src/expressions/value.ts +++ b/src/expressions/value.ts @@ -1,8 +1,13 @@ import { get } from 'lodash' -import { evaluate, interpreter } from '../expression' +import { evaluate } from '../evaluate' -import { Expression, EvaluationContext } from '../types' +import { + Expression, + EvaluationContext, + EvaluationScope, + ExpressionInterpreterSpec, +} from '../types' const PATH_VARIABLE_RE = /^\$\$.+/ @@ -14,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, @@ -31,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 @@ -47,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, @@ -57,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/index.ts b/src/index.ts index c6a863b..cd68df7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,7 +32,8 @@ export const ALL_EXPRESSIONS = { } export * from './types' -export * from './expression' +export * from './evaluate' +export * from './interpreter' export * from './expressions/array' export * from './expressions/boolean' export * from './expressions/comparison' diff --git a/src/interpreter.ts b/src/interpreter.ts new file mode 100644 index 0000000..86f4b49 --- /dev/null +++ b/src/interpreter.ts @@ -0,0 +1,148 @@ +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/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 2df56a5..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" @@ -1321,16 +1321,6 @@ estree-walker "^1.0.1" picomatch "^2.2.2" -"@semantic-release/changelog@^5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@semantic-release/changelog/-/changelog-5.0.1.tgz#50a84b63e5d391b7debfe021421589fa2bcdafe4" - integrity sha512-unvqHo5jk4dvAf2nZ3aw4imrlwQ2I50eVVvq9D47Qc3R+keNqepx1vDYwkjF8guFXnOYaYcR28yrZWno1hFbiw== - dependencies: - "@semantic-release/error" "^2.1.0" - aggregate-error "^3.0.0" - fs-extra "^9.0.0" - lodash "^4.17.4" - "@semantic-release/commit-analyzer@^8.0.0": version "8.0.1" resolved "https://registry.yarnpkg.com/@semantic-release/commit-analyzer/-/commit-analyzer-8.0.1.tgz#5d2a37cd5a3312da0e3ac05b1ca348bf60b90bca" @@ -1344,7 +1334,7 @@ lodash "^4.17.4" micromatch "^4.0.2" -"@semantic-release/error@^2.1.0", "@semantic-release/error@^2.2.0": +"@semantic-release/error@^2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@semantic-release/error/-/error-2.2.0.tgz#ee9d5a09c9969eade1ec864776aeda5c5cddbbf0" integrity sha512-9Tj/qn+y2j+sjCI3Jd+qseGtHjOAeg7dU2/lVcqIQ9TV3QDaDXDYXcoOHU+7o2Hwh8L8ymL4gfuO7KxDs3q2zg==