diff --git a/src/__snapshots__/index.spec.ts.snap b/src/__snapshots__/index.spec.ts.snap index 3c0aa46..691846a 100644 --- a/src/__snapshots__/index.spec.ts.snap +++ b/src/__snapshots__/index.spec.ts.snap @@ -86,6 +86,7 @@ Array [ "$stringTrim", "$switch", "$switchKey", + "$try", "$type", "$value", "$xor", diff --git a/src/expressions/functional.spec.ts b/src/expressions/functional.spec.ts index b6c6bc6..27a0874 100644 --- a/src/expressions/functional.spec.ts +++ b/src/expressions/functional.spec.ts @@ -5,8 +5,33 @@ import { ARRAY_EXPRESSIONS } from './array' import { MATH_EXPRESSIONS } from './math' import { STRING_EXPRESSIONS } from './string' +import { + SyncModeUnsupportedError, + AsyncModeUnsupportedError, + EvaluationError, +} from '../errors' + import { _prepareEvaluateTestCases } from '../../spec/specUtil' +const _mockFailSync = [ + (err) => { + throw err || new Error('MOCK_ERROR') + }, + ['any'], + { defaultParam: -1 }, +] + +const _mockFailAsync = [ + (err) => + new Promise((resolve, reject) => { + setTimeout(() => { + reject(err || new Error('MOCK_ERROR')) + }, 100) + }), + ['any'], + { defaultParam: -1 }, +] + const EXPS = { ...VALUE_EXPRESSIONS, ...COMPARISON_EXPRESSIONS, @@ -14,10 +39,27 @@ const EXPS = { ...ARRAY_EXPRESSIONS, ...MATH_EXPRESSIONS, ...STRING_EXPRESSIONS, + + $mockFailSync: { sync: _mockFailSync }, + $mockFailAsync: { async: _mockFailAsync }, + $mockFailIsomorphic: { + sync: _mockFailSync, + async: _mockFailAsync, + }, + $mockFailImplicitSync: _mockFailSync, } const _evTestCases = _prepareEvaluateTestCases(EXPS) +class CustomError extends Error { + constructor(code: string) { + super(`CustomError message: the error code was "${code}"`) + this.code = code + } + + code: string +} + describe('$pipe', () => { const SUM_2 = ['$arrayMap', ['$mathSum', 2]] const MULT_2 = ['$arrayMap', ['$mathMult', 2]] @@ -31,3 +73,94 @@ describe('$pipe', () => { [VALUE, ['$pipe', [SUM_2, GREATER_THAN_50]], []], ]) }) + +describe('$try', () => { + describe('sync/async isomorphism', () => { + const VALUE = 'any-value' + + _evTestCases([ + [ + VALUE, + ['$try', ['$mockFailIsomorphic']], + { error: true, message: 'MOCK_ERROR' }, + ], + + // Custom error code + [ + VALUE, + ['$try', ['$mockFailIsomorphic', 'CUSTOM_ERROR_CODE']], + { error: true, code: 'CUSTOM_ERROR_CODE' }, + ], + + // Custom error object + [ + VALUE, + [ + '$try', + ['$mockFailIsomorphic', { error: true, code: 'CUSTOM_ERROR_CODE' }], + ], + { error: true, code: 'CUSTOM_ERROR_CODE' }, + ], + + // CustomError instance + [ + VALUE, + ['$try', ['$mockFailIsomorphic', new CustomError('CUSTOM_ERROR_CODE')]], + { + error: true, + code: 'CUSTOM_ERROR_CODE', + message: + 'CustomError message: the error code was "CUSTOM_ERROR_CODE"', + }, + ], + + // Invalid error + [VALUE, ['$try', ['$mockFailIsomorphic', 9]], EvaluationError], + + // Catch value + [VALUE, ['$try', ['$mockFailIsomorphic'], 'FAIL_VALUE'], 'FAIL_VALUE'], + + // Catch expression + [ + VALUE, + [ + '$try', + ['$mockFailIsomorphic'], + ['$stringConcat', ['::FAILED::', ['$value', '$$ERROR.message']]], + ], + 'any-value::FAILED::MOCK_ERROR', + ], + ]) + }) + + describe('sync only', () => { + const VALUE = 'any-value' + + _evTestCases.testSyncCases([ + [ + VALUE, + ['$try', ['$mockFailSync']], + { error: true, message: 'MOCK_ERROR' }, + ], + [VALUE, ['$try', ['$mockFailAsync']], SyncModeUnsupportedError], + ]) + }) + + describe('async only', () => { + const VALUE = 'any-value' + + _evTestCases.testAsyncCases([ + [VALUE, ['$try', ['$mockFailSync']], AsyncModeUnsupportedError], + [ + VALUE, + ['$try', ['$mockFailImplicitSync']], + { error: true, message: 'MOCK_ERROR' }, + ], + [ + VALUE, + ['$try', ['$mockFailAsync']], + { error: true, message: 'MOCK_ERROR' }, + ], + ]) + }) +}) diff --git a/src/expressions/functional.ts b/src/expressions/functional.ts index 3dddade..cf790cc 100644 --- a/src/expressions/functional.ts +++ b/src/expressions/functional.ts @@ -1,6 +1,17 @@ -import { EvaluationContext, Expression, InterpreterSpec } from '../types' +import { + EvaluationContext, + Expression, + InterpreterSpec, + InterpreterSpecSingle, +} from '../types' -import { evaluate } from '../evaluate' +import { EvaluationError } from '../errors' + +import { isPlainObject } from 'lodash' + +import { evaluate, evaluateAsync } from '../evaluate' + +import { anyType } from '@orioro/typing' /** * @function $pipe @@ -21,6 +32,93 @@ export const $pipe: InterpreterSpec = [ ['array'], ] +const _serializableError = (err) => { + if (err instanceof Error) { + return { + error: true, + ...err, + message: err.message, + } + } else if (typeof err === 'string') { + return { + error: true, + code: err, + } + } else if (isPlainObject(err)) { + return { + error: true, + ...err, + } + } else { + throw new EvaluationError('$try', `Invalid error object: ${err}`) + } +} + +const _tryHandleError = (catchExpressionOrValue, context, err) => { + if (err instanceof EvaluationError) { + throw err + } + + const $$ERROR = _serializableError(err) + + return catchExpressionOrValue === undefined + ? $$ERROR + : evaluate( + { + ...context, + scope: { + ...context.scope, + $$ERROR, + }, + }, + catchExpressionOrValue + ) +} + +const _trySync: InterpreterSpecSingle = [ + ( + expressionOrValue: Expression | any, + catchExpressionOrValue: undefined | Expression | any, + context: EvaluationContext + ): any => { + try { + return evaluate(context, expressionOrValue) + } catch (err) { + return _tryHandleError(catchExpressionOrValue, context, err) + } + }, + [anyType({ skipEvaluation: true }), anyType({ skipEvaluation: true })], + { + defaultParam: -1, + }, +] + +const _tryAsync: InterpreterSpecSingle = [ + ( + expressionOrValue: Expression | any, + catchExpressionOrValue: undefined | Expression | any, + context: EvaluationContext + ): Promise => + evaluateAsync(context, expressionOrValue).catch((err) => + _tryHandleError(catchExpressionOrValue, context, err) + ), + [anyType({ skipEvaluation: true }), anyType({ skipEvaluation: true })], + { + defaultParam: -1, + }, +] + +/** + * @function $try + * @param {Expression | *} expressionOrValue + * @param {Expression | *} [catchExpressionOrValue=$$ERROR] + */ +export const $try: InterpreterSpec = { + sync: _trySync, + async: _tryAsync, +} + export const FUNCTIONAL_EXPRESSIONS = { $pipe, + $try, } diff --git a/src/types.ts b/src/types.ts index 2ada025..72e9817 100644 --- a/src/types.ts +++ b/src/types.ts @@ -127,6 +127,7 @@ export type EvaluationScope = { $$ACC?: any $$SORT_A?: any $$SORT_B?: any + $$ERROR?: PlainObject [key: string]: any }