From 610fe63bd1bdddebd9326232125e16af8af2ea81 Mon Sep 17 00:00:00 2001 From: Simon Fan Date: Mon, 1 Mar 2021 18:01:26 -0300 Subject: [PATCH 1/3] refactor: isolate evaluate and interpreter methods into separate files --- src/{expression.spec.ts => evaluate.spec.ts} | 2 +- src/evaluate.ts | 86 ++++++++++++++++++++ src/expressions/array.spec.ts | 2 +- src/expressions/array.ts | 9 +- src/expressions/boolean.spec.ts | 2 +- src/expressions/boolean.ts | 2 +- src/expressions/comparison.spec.ts | 2 +- src/expressions/comparison.ts | 3 +- src/expressions/functional.spec.ts | 2 +- src/expressions/functional.ts | 3 +- src/expressions/logical.spec.ts | 2 +- src/expressions/logical.ts | 3 +- src/expressions/math.spec.ts | 2 +- src/expressions/math.ts | 2 +- src/expressions/number.spec.ts | 2 +- src/expressions/number.ts | 2 +- src/expressions/object.spec.ts | 2 +- src/expressions/object.ts | 3 +- src/expressions/string.spec.ts | 2 +- src/expressions/string.ts | 2 +- src/expressions/type.spec.ts | 2 +- src/expressions/type.ts | 2 +- src/expressions/value.spec.ts | 2 +- src/expressions/value.ts | 3 +- src/index.ts | 3 +- src/{expression.ts => interpreter.ts} | 86 +------------------- 26 files changed, 120 insertions(+), 113 deletions(-) rename src/{expression.spec.ts => evaluate.spec.ts} (97%) create mode 100644 src/evaluate.ts rename src/{expression.ts => interpreter.ts} (68%) 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..782c703 --- /dev/null +++ b/src/evaluate.ts @@ -0,0 +1,86 @@ +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 _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 +} diff --git a/src/expressions/array.spec.ts b/src/expressions/array.spec.ts index 93a5f69..9048923 100644 --- a/src/expressions/array.spec.ts +++ b/src/expressions/array.spec.ts @@ -1,4 +1,4 @@ -import { evaluate } from '../expression' +import { evaluate } from '../evaluate' import { VALUE_EXPRESSIONS } from './value' import { COMPARISON_EXPRESSIONS } from './comparison' import { LOGICAL_EXPRESSIONS } from './logical' diff --git a/src/expressions/array.ts b/src/expressions/array.ts index c0758b3..3502a05 100644 --- a/src/expressions/array.ts +++ b/src/expressions/array.ts @@ -1,9 +1,6 @@ -import { - interpreter, - evaluate, - evaluateTyped, - isExpression, -} from '../expression' +import { evaluate, evaluateTyped, isExpression } from '../evaluate' + +import { interpreter } from '../interpreter' import { EvaluationContext, Expression } from '../types' import { validateType } from '@orioro/typing' diff --git a/src/expressions/boolean.spec.ts b/src/expressions/boolean.spec.ts index 7826b46..dd9f1c5 100644 --- a/src/expressions/boolean.spec.ts +++ b/src/expressions/boolean.spec.ts @@ -1,4 +1,4 @@ -import { evaluate } from '../expression' +import { evaluate } from '../evaluate' import { $value } from './value' import { $boolean } from './boolean' diff --git a/src/expressions/boolean.ts b/src/expressions/boolean.ts index ec8591b..6f2a6f5 100644 --- a/src/expressions/boolean.ts +++ b/src/expressions/boolean.ts @@ -1,4 +1,4 @@ -import { interpreter } from '../expression' +import { interpreter } from '../interpreter' /** * @function $boolean diff --git a/src/expressions/comparison.spec.ts b/src/expressions/comparison.spec.ts index 6ae8807..b9b3fba 100644 --- a/src/expressions/comparison.spec.ts +++ b/src/expressions/comparison.spec.ts @@ -1,4 +1,4 @@ -import { evaluate } from '../expression' +import { evaluate } from '../evaluate' import { $stringSubstr } from './string' import { $eq, diff --git a/src/expressions/comparison.ts b/src/expressions/comparison.ts index 9451a24..bc1f469 100644 --- a/src/expressions/comparison.ts +++ b/src/expressions/comparison.ts @@ -1,6 +1,7 @@ import { isEqual } from 'lodash' -import { evaluate, evaluateTyped, interpreter } from '../expression' +import { interpreter } from '../interpreter' +import { evaluate, evaluateTyped } from '../evaluate' import { EvaluationContext, PlainObject } from '../types' diff --git a/src/expressions/functional.spec.ts b/src/expressions/functional.spec.ts index 73cf5b3..dd72fd6 100644 --- a/src/expressions/functional.spec.ts +++ b/src/expressions/functional.spec.ts @@ -1,4 +1,4 @@ -import { evaluate } from '../expression' +import { evaluate } from '../evaluate' import { COMPARISON_EXPRESSIONS } from './comparison' import { VALUE_EXPRESSIONS } from './value' diff --git a/src/expressions/functional.ts b/src/expressions/functional.ts index cf1ee19..c5ed4b0 100644 --- a/src/expressions/functional.ts +++ b/src/expressions/functional.ts @@ -1,6 +1,7 @@ import { EvaluationContext, Expression } from '../types' -import { evaluate, interpreter } from '../expression' +import { evaluate } from '../evaluate' +import { interpreter } from '../interpreter' /** * @function $pipe diff --git a/src/expressions/logical.spec.ts b/src/expressions/logical.spec.ts index 4db673f..09d91f4 100644 --- a/src/expressions/logical.spec.ts +++ b/src/expressions/logical.spec.ts @@ -1,4 +1,4 @@ -import { evaluate } from '../expression' +import { evaluate } from '../evaluate' import { VALUE_EXPRESSIONS } from './value' import { BOOLEAN_EXPRESSIONS } from './boolean' import { LOGICAL_EXPRESSIONS } from './logical' diff --git a/src/expressions/logical.ts b/src/expressions/logical.ts index a3a0868..c0f20ea 100644 --- a/src/expressions/logical.ts +++ b/src/expressions/logical.ts @@ -1,4 +1,5 @@ -import { evaluate, interpreter } from '../expression' +import { interpreter } from '../interpreter' +import { evaluate } from '../evaluate' import { Expression, EvaluationContext, PlainObject } from '../types' /** diff --git a/src/expressions/math.spec.ts b/src/expressions/math.spec.ts index 5ea9f1d..38b6ec0 100644 --- a/src/expressions/math.spec.ts +++ b/src/expressions/math.spec.ts @@ -1,4 +1,4 @@ -import { evaluate } from '../expression' +import { evaluate } from '../evaluate' import { VALUE_EXPRESSIONS } from './value' import { MATH_EXPRESSIONS } from './math' diff --git a/src/expressions/math.ts b/src/expressions/math.ts index 57163c2..813d7aa 100644 --- a/src/expressions/math.ts +++ b/src/expressions/math.ts @@ -1,4 +1,4 @@ -import { interpreter } from '../expression' +import { interpreter } from '../interpreter' /** * @function $mathSum diff --git a/src/expressions/number.spec.ts b/src/expressions/number.spec.ts index 03b1b75..a102383 100644 --- a/src/expressions/number.spec.ts +++ b/src/expressions/number.spec.ts @@ -1,4 +1,4 @@ -import { evaluate } from '../expression' +import { evaluate } from '../evaluate' import { VALUE_EXPRESSIONS } from './value' import { NUMBER_EXPRESSIONS } from './number' diff --git a/src/expressions/number.ts b/src/expressions/number.ts index f7f3b11..591e51f 100644 --- a/src/expressions/number.ts +++ b/src/expressions/number.ts @@ -1,4 +1,4 @@ -import { interpreter } from '../expression' +import { interpreter } from '../interpreter' /** * @function $numberInt diff --git a/src/expressions/object.spec.ts b/src/expressions/object.spec.ts index cd3fc34..27df02b 100644 --- a/src/expressions/object.spec.ts +++ b/src/expressions/object.spec.ts @@ -1,4 +1,4 @@ -import { evaluate } from '../expression' +import { evaluate } from '../evaluate' import { $value } from './value' import { COMPARISON_EXPRESSIONS } from './comparison' import { ARRAY_EXPRESSIONS } from './array' diff --git a/src/expressions/object.ts b/src/expressions/object.ts index d1d9115..536a26c 100644 --- a/src/expressions/object.ts +++ b/src/expressions/object.ts @@ -1,6 +1,7 @@ import { get, set, isPlainObject } from 'lodash' -import { evaluate, isExpression, interpreter } from '../expression' +import { evaluate, isExpression } from '../evaluate' +import { interpreter } from '../interpreter' import { objectDeepApplyDefaults } from '../util/deepApplyDefaults' import { objectDeepAssign } from '../util/deepAssign' diff --git a/src/expressions/string.spec.ts b/src/expressions/string.spec.ts index 98a9644..0cd82d7 100644 --- a/src/expressions/string.spec.ts +++ b/src/expressions/string.spec.ts @@ -1,4 +1,4 @@ -import { evaluate } from '../expression' +import { evaluate } from '../evaluate' import { VALUE_EXPRESSIONS } from './value' import { STRING_EXPRESSIONS } from './string' diff --git a/src/expressions/string.ts b/src/expressions/string.ts index d4604e5..cf69b14 100644 --- a/src/expressions/string.ts +++ b/src/expressions/string.ts @@ -1,4 +1,4 @@ -import { interpreter } from '../expression' +import { interpreter } from '../interpreter' import { get } from 'lodash' import { PlainObject } from '../types' diff --git a/src/expressions/type.spec.ts b/src/expressions/type.spec.ts index d278944..bde7f5e 100644 --- a/src/expressions/type.spec.ts +++ b/src/expressions/type.spec.ts @@ -1,4 +1,4 @@ -import { evaluate } from '../expression' +import { evaluate } from '../evaluate' import { $value } from './value' import { TYPE_EXPRESSIONS, typeExpressions } from './type' import { testCases } from '@orioro/jest-util' diff --git a/src/expressions/type.ts b/src/expressions/type.ts index 832cada..570efb5 100644 --- a/src/expressions/type.ts +++ b/src/expressions/type.ts @@ -1,4 +1,4 @@ -import { interpreter } from '../expression' +import { interpreter } from '../interpreter' import { typing, CORE_TYPES } from '@orioro/typing' import { TypeAlternatives, TypeMap, ExpressionInterpreter } from '../types' diff --git a/src/expressions/value.spec.ts b/src/expressions/value.spec.ts index 9c0becb..8915b3e 100644 --- a/src/expressions/value.spec.ts +++ b/src/expressions/value.spec.ts @@ -1,4 +1,4 @@ -import { evaluate } from '../expression' +import { evaluate } from '../evaluate' import { MATH_EXPRESSIONS } from './math' import { LOGICAL_EXPRESSIONS } from './logical' import { COMPARISON_EXPRESSIONS } from './comparison' diff --git a/src/expressions/value.ts b/src/expressions/value.ts index e56003d..2f94e50 100644 --- a/src/expressions/value.ts +++ b/src/expressions/value.ts @@ -1,6 +1,7 @@ import { get } from 'lodash' -import { evaluate, interpreter } from '../expression' +import { evaluate } from '../evaluate' +import { interpreter } from '../interpreter' import { Expression, EvaluationContext } from '../types' 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/expression.ts b/src/interpreter.ts similarity index 68% rename from src/expression.ts rename to src/interpreter.ts index 40902f7..9fa9945 100644 --- a/src/expression.ts +++ b/src/interpreter.ts @@ -2,91 +2,9 @@ import { validateType, ExpectedType } from '@orioro/typing' import { isPlainObject } from 'lodash' -import { - Expression, - ExpressionInterpreter, - ExpressionInterpreterList, - EvaluationContext, -} from './types' +import { EvaluationContext, ExpressionInterpreter } 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 -} +import { evaluate, evaluateTyped } from './evaluate' type ParamResolverFunction = (context: EvaluationContext, arg: any) => any From 20d0165bffe80e5d8232d67ef6857cc03ebf1e8e Mon Sep 17 00:00:00 2001 From: Simon Fan Date: Mon, 1 Mar 2021 18:02:51 -0300 Subject: [PATCH 2/3] chore: remove @semantic-changelog and add @orioro/jest-util --- .gitignore | 1 + package.json | 2 -- yarn.lock | 12 +----------- 3 files changed, 2 insertions(+), 13 deletions(-) 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..db4cdd7 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,6 @@ "@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/yarn.lock b/yarn.lock index 2df56a5..2c2dcd7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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== From 8bd0db20d0e1c45e5e57ead140e60ba556b68207 Mon Sep 17 00:00:00 2001 From: Simon Fan Date: Wed, 3 Mar 2021 04:49:19 -0300 Subject: [PATCH 3/3] 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"