regevbr/json-expression-eval

View on GitHub
src/lib/evaluator.ts

Summary

Maintainability
B
6 hrs
Test Coverage
A
100%
import {
isAndCompareOp,
isEqualCompareOp,
isBetweenCompareOp,
isGtCompareOp,
isGteCompareOp,
isLtCompareOp,
isLteCompareOp,
isNotCompareOp,
isNotEqualCompareOp,
isOrCompareOp,
_isObject,
isFunctionCompareOp,
isInqCompareOp,
isNinCompareOp,
isRegexCompareOp,
isRegexiCompareOp,
isWithRef, isMathOp,
WithRef
} from './typeGuards';
import {
Context,
Expression,
FunctionsTable,
ExtendedCompareOp,
ValidationContext,
PropertyCompareOps, Primitive, MathOp
} from '../types';
import {
assertUnreachable,
objectKeys,
getFromPath,
contextNumberAssertion,
contextStringAssertion,
expressionStringAssertion,
expressionNumberAssertion
} from './helpers';
 
const extractValueOrRef = <C extends Context>(context: C, validation: boolean, valueOrRef: Primitive | WithRef)
: Primitive => {
if (isWithRef(valueOrRef)) {
const {value, exists} = getFromPath(context, valueOrRef.ref);
if (validation && !exists) {
throw new Error(`Invalid expression - unknown context key ${valueOrRef.ref}`);
}
return value;
} else {
return valueOrRef;
}
}
 
const computeValue = <C extends Context>(context: C, validation: boolean,
value: Primitive | WithRef | MathOp<any, any>,
expressionKey: string): Primitive => {
if (isMathOp(value)) {
const lhs = extractValueOrRef<C>(context, validation, value.lhs);
const rhs = extractValueOrRef<C>(context, validation, value.rhs);
expressionNumberAssertion(expressionKey, lhs);
expressionNumberAssertion(expressionKey, rhs);
switch (value.op) {
case '+':
return lhs + rhs;
case '-':
return lhs - rhs;
case '*':
return lhs * rhs;
case '/':
return lhs / rhs;
case '%':
return lhs % rhs;
case 'pow':
return Math.pow(lhs, rhs);
default:
throw new Error(`Invalid expression - ${expressionKey} has invalid math operand ${value.op}`);
}
}
return extractValueOrRef(context, validation, value);
}
 
Function `evaluateCompareOp` has a Cognitive Complexity of 19 (exceeds 5 allowed). Consider refactoring.
Function `evaluateCompareOp` has 64 lines of code (exceeds 25 allowed). Consider refactoring.
Function `evaluateCompareOp` has 5 arguments (exceeds 4 allowed). Consider refactoring.
async function evaluateCompareOp<C extends Context, Ignore>(expressionValue: ExtendedCompareOp<any, any, any>,
expressionKey: string, contextValue: any,
context: C, validation: boolean)
: Promise<boolean> {
if (!_isObject(expressionValue)) {
return contextValue === expressionValue;
}
const compareKeys = objectKeys(expressionValue);
if (compareKeys.length !== 1) {
throw new Error('Invalid expression - too may keys');
}
if (isEqualCompareOp(expressionValue)) {
return contextValue === computeValue(context, validation, expressionValue.eq, expressionKey);
} else if (isNotEqualCompareOp(expressionValue)) {
return contextValue !== computeValue(context, validation, expressionValue.neq, expressionKey);
} else if (isInqCompareOp(expressionValue)) {
return expressionValue.inq.map((value) => computeValue(context, validation, value, expressionKey))
.indexOf(contextValue) >= 0;
} else if (isNinCompareOp(expressionValue)) {
Avoid too many `return` statements within this function.
return expressionValue.nin.map((value) => computeValue(context, validation, value, expressionKey))
.indexOf(contextValue) < 0;
} else if (isRegexCompareOp(expressionValue)) {
contextStringAssertion(expressionKey, contextValue);
const regexpValue = computeValue(context, validation, expressionValue.regexp, expressionKey);
expressionStringAssertion(expressionKey, regexpValue);
Avoid too many `return` statements within this function.
return Boolean(contextValue.match(new RegExp(regexpValue)));
} else if (isRegexiCompareOp(expressionValue)) {
contextStringAssertion(expressionKey, contextValue);
const regexpiValue = computeValue(context, validation, expressionValue.regexpi, expressionKey);
expressionStringAssertion(expressionKey, regexpiValue);
Avoid too many `return` statements within this function.
return Boolean(contextValue.match(new RegExp(regexpiValue, `i`)));
Similar blocks of code found in 4 locations. Consider refactoring.
} else if (isGtCompareOp(expressionValue)) {
contextNumberAssertion(expressionKey, contextValue);
const gtValue = computeValue(context, validation, expressionValue.gt, expressionKey);
expressionNumberAssertion(expressionKey, gtValue);
Avoid too many `return` statements within this function.
return contextValue > gtValue;
Similar blocks of code found in 4 locations. Consider refactoring.
} else if (isGteCompareOp(expressionValue)) {
contextNumberAssertion(expressionKey, contextValue);
const gteValue = computeValue(context, validation, expressionValue.gte, expressionKey);
expressionNumberAssertion(expressionKey, gteValue);
Avoid too many `return` statements within this function.
return contextValue >= gteValue;
Similar blocks of code found in 4 locations. Consider refactoring.
} else if (isLteCompareOp(expressionValue)) {
contextNumberAssertion(expressionKey, contextValue);
const lteValue = computeValue(context, validation, expressionValue.lte, expressionKey);
expressionNumberAssertion(expressionKey, lteValue);
Avoid too many `return` statements within this function.
return contextValue <= lteValue;
Similar blocks of code found in 4 locations. Consider refactoring.
} else if (isLtCompareOp(expressionValue)) {
contextNumberAssertion(expressionKey, contextValue);
const ltValue = computeValue(context, validation, expressionValue.lt, expressionKey);
expressionNumberAssertion(expressionKey, ltValue);
Avoid too many `return` statements within this function.
return contextValue < ltValue;
} else if (isBetweenCompareOp(expressionValue)) {
contextNumberAssertion(expressionKey, contextValue);
if (expressionValue.between.length !== 2) {
throw new Error(`Invalid expression - ${expressionKey}.length must be 2`);
}
const [lowRaw, highRaw] = expressionValue.between;
const low = computeValue(context, validation, lowRaw, expressionKey);
const high = computeValue(context, validation, highRaw, expressionKey);
expressionNumberAssertion(`${expressionKey}[0]`, low);
expressionNumberAssertion(`${expressionKey}[1]`, high);
if (low > high) {
throw new Error(`Invalid expression - ${expressionKey} first value is higher than second value`);
}
Avoid too many `return` statements within this function.
return contextValue >= low && contextValue <= high;
} else {
Avoid too many `return` statements within this function.
return assertUnreachable(expressionValue, `Invalid expression - unknown op ${compareKeys[0]}`);
}
}
 
Similar blocks of code found in 2 locations. Consider refactoring.
async function handleAndOp<C extends Context, F extends FunctionsTable<C, CustomEvaluatorFuncRunOptions>,
Ignore, CustomEvaluatorFuncRunOptions>
Function `handleAndOp` has 5 arguments (exceeds 4 allowed). Consider refactoring.
(andExpression: Expression<C, F, Ignore, CustomEvaluatorFuncRunOptions>[], context: C, functionsTable: F,
validation: boolean, runOptions: CustomEvaluatorFuncRunOptions): Promise<boolean> {
if (andExpression.length === 0) {
throw new Error('Invalid expression - and operator must have at least one expression');
}
for (const currExpression of andExpression) {
const result = await run<C, F, Ignore, CustomEvaluatorFuncRunOptions>(currExpression,
context, functionsTable, validation, runOptions);
if (!validation && !result) {
return false;
}
}
return true;
}
 
Similar blocks of code found in 2 locations. Consider refactoring.
async function handleOrOp<C extends Context, F extends FunctionsTable<C, CustomEvaluatorFuncRunOptions>,
Ignore, CustomEvaluatorFuncRunOptions>(
Function `handleOrOp` has 5 arguments (exceeds 4 allowed). Consider refactoring.
orExpression: Expression<C, F, Ignore, CustomEvaluatorFuncRunOptions>[],
context: C, functionsTable: F, validation: boolean, runOptions: CustomEvaluatorFuncRunOptions)
: Promise<boolean> {
if (orExpression.length === 0) {
throw new Error('Invalid expression - or operator must have at least one expression');
}
for (const currExpression of orExpression) {
const result = await run<C, F, Ignore, CustomEvaluatorFuncRunOptions>(
currExpression, context, functionsTable, validation, runOptions);
if (!validation && result) {
return true;
}
}
return false;
}
 
Function `run` has 31 lines of code (exceeds 25 allowed). Consider refactoring.
Function `run` has a Cognitive Complexity of 9 (exceeds 5 allowed). Consider refactoring.
async function run<C extends Context, F extends FunctionsTable<C, CustomEvaluatorFuncRunOptions>,
Ignore, CustomEvaluatorFuncRunOptions>(
Function `run` has 5 arguments (exceeds 4 allowed). Consider refactoring.
expression: Expression<C, F, Ignore, CustomEvaluatorFuncRunOptions>, context: C,
functionsTable: F, validation: boolean, runOptions: CustomEvaluatorFuncRunOptions): Promise<boolean> {
const expressionKeys = objectKeys(expression);
if (expressionKeys.length !== 1) {
throw new Error('Invalid expression - too may keys');
}
const expressionKey = expressionKeys[0];
if (isAndCompareOp<C, F, Ignore, CustomEvaluatorFuncRunOptions>(expression)) {
return handleAndOp<C, F, Ignore, CustomEvaluatorFuncRunOptions>(expression.and,
context, functionsTable, validation, runOptions);
} else if (isOrCompareOp<C, F, Ignore, CustomEvaluatorFuncRunOptions>(expression)) {
return handleOrOp<C, F, Ignore, CustomEvaluatorFuncRunOptions>(expression.or, context,
functionsTable, validation, runOptions);
} else if (isNotCompareOp<C, F, Ignore, CustomEvaluatorFuncRunOptions>(expression)) {
return !(await run<C, F, Ignore, CustomEvaluatorFuncRunOptions>(expression.not, context,
functionsTable, validation, runOptions));
} else if (isFunctionCompareOp<C, F, Ignore, CustomEvaluatorFuncRunOptions>(expression,
functionsTable, expressionKey)) {
return functionsTable[expressionKey](expression[expressionKey], context, {
custom: runOptions,
validation,
});
} else {
const {value: contextValue, exists} = getFromPath(context, expressionKey);
if (validation && !exists) {
throw new Error(`Invalid expression - unknown context key ${expressionKey}`);
}
Avoid too many `return` statements within this function.
return evaluateCompareOp<C, Ignore>(
(expression as PropertyCompareOps<C, Ignore>)
[expressionKey as any as keyof PropertyCompareOps<C, Ignore>] as
unknown as ExtendedCompareOp<any, any, any>,
expressionKey, contextValue, context, validation);
}
}
 
export const evaluate = async <C extends Context, F extends FunctionsTable<C, CustomEvaluatorFuncRunOptions>,
Ignore = never, CustomEvaluatorFuncRunOptions = undefined>(
expression: Expression<C, F, Ignore, CustomEvaluatorFuncRunOptions>, context: C, functionsTable: F
, runOptions: CustomEvaluatorFuncRunOptions)
: Promise<boolean> => {
return run<C, F, Ignore, CustomEvaluatorFuncRunOptions>(expression, context,
functionsTable, false, runOptions);
};
 
// Throws in case of validation error. Does not run functions or compare fields
export const validate = async <C extends Context, F extends FunctionsTable<C, CustomEvaluatorFuncRunOptions>,
Ignore = never, CustomEvaluatorFuncRunOptions = undefined>(
expression: Expression<C, F, Ignore, CustomEvaluatorFuncRunOptions>,
validationContext: ValidationContext<C, Ignore>, functionsTable: F, runOptions: CustomEvaluatorFuncRunOptions)
: Promise<void> => {
await run<C, F, Ignore, CustomEvaluatorFuncRunOptions>(expression,
validationContext as C, functionsTable, true, runOptions);
};