Skip to content

Commit

Permalink
add ability to refrence context on right hand side of expressions (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
regevbr authored Aug 29, 2022
1 parent 8bd849d commit 5ad068e
Show file tree
Hide file tree
Showing 9 changed files with 756 additions and 386 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ interface IExampleContext {
date: Moment;
nested: {
value: number | null;
value4: number;
nested2: {
value2?: number;
value3: boolean;
Expand All @@ -58,6 +59,7 @@ const context: IExampleContext = {
date: moment(),
nested: {
value: null,
value4: 5,
nested2: {
value3: true,
},
Expand All @@ -71,6 +73,7 @@ const validationContext: ValidationContext<IExampleContext, IExampleContextIgnor
date: moment(),
nested: {
value: 5,
value4: 6,
nested2: {
value2: 6,
value3: true,
Expand All @@ -97,6 +100,13 @@ const expression: IExampleExpression = {
{
'nested.nested2.value3': true,
},
{
times: {
lte: {
ref: 'nested.value4'
}
},
},
],
},
],
Expand Down Expand Up @@ -136,9 +146,13 @@ There are 4 types of operators you can use (evaluated in that order of precedenc
- `between: readonly [number, number] (as const)` - True if the value is between the two specified values: greater than or equal to first value and less than or equal to second value.
- `{property: value}`
- compares the property to that value (shorthand to the `eq` op)

> Nested properties in the context can also be accessed using a dot notation (see example above)
> In each expression level, you can only define 1 operator, and 1 only
> You can reference values (and nested values) from the context using the {"ref":"<dot notation path>"}
> (see example above) on the right-hand side of expressions (not in parameters to user defined functions though)
Example expressions, assuming we have the `user` and `maxCount` user defined functions in place can be:
```json
{
Expand All @@ -154,6 +168,9 @@ Example expressions, assuming we have the `user` and `maxCount` user defined fun
{
"times": { "eq" : 5}
},
{
"times": { "eq" : { "ref": "nested.preoprty"}}
},
{
"country": "USA"
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "json-expression-eval",
"version": "4.2.1",
"version": "4.3.0",
"description": "json serializable rule engine / boolean expression evaluator",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
1 change: 1 addition & 0 deletions src/examples/engine/example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ rules = [
and: [
{user: 'a@b.com'},
{maxCount: 5},
{times: {eq:{ref:'nested.value'}}},
],
},
consequence: {
Expand Down
9 changes: 9 additions & 0 deletions src/examples/evaluator/example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ expression = {

run(expression, context);

expression = {
and: [
{user: 'a@b.com'},
{times: {eq:{ref:'nested.value'}}},
],
};

run(expression, context);

expression = {
and: [
{user: 'a@b.com'},
Expand Down
80 changes: 55 additions & 25 deletions src/lib/evaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
FunctionsTable,
ExtendedCompareOp,
ValidationContext,
PropertyCompareOps
PropertyCompareOps, Primitive
} from '../types';
import {
assertUnreachable,
Expand All @@ -34,7 +34,27 @@ import {
expressionNumberAssertion
} from './helpers';

function evaluateCompareOp(expressionValue: ExtendedCompareOp, expressionKey: string, contextValue: any): boolean {
type WithRef = {
ref: string
}

const isWithRef = (x: unknown): x is WithRef => Boolean((x as WithRef).ref);

const extractValueOrRef = <C extends Context>(context: C, validation: boolean, valueOrRef: Primitive | WithRef) => {
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;
}
}

function evaluateCompareOp<C extends Context>(expressionValue: ExtendedCompareOp<any, any, any>, expressionKey: string,
contextValue: any, context: C, validation: boolean)
: boolean {
if (!_isObject(expressionValue)) {
return contextValue === expressionValue;
}
Expand All @@ -43,46 +63,55 @@ function evaluateCompareOp(expressionValue: ExtendedCompareOp, expressionKey: st
throw new Error('Invalid expression - too may keys');
}
if (isEqualCompareOp(expressionValue)) {
return contextValue === expressionValue.eq;
return contextValue === extractValueOrRef(context, validation, expressionValue.eq);
} else if (isNotEqualCompareOp(expressionValue)) {
return contextValue !== expressionValue.neq;
return contextValue !== extractValueOrRef(context, validation, expressionValue.neq);
} else if (isInqCompareOp(expressionValue)) {
return expressionValue.inq.indexOf(contextValue) >= 0;
return expressionValue.inq.map((value) => extractValueOrRef(context, validation, value))
.indexOf(contextValue) >= 0;
} else if (isNinCompareOp(expressionValue)) {
return expressionValue.nin.indexOf(contextValue) < 0;
return expressionValue.nin.map((value) => extractValueOrRef(context, validation, value))
.indexOf(contextValue) < 0;
} else if (isRegexCompareOp(expressionValue)) {
contextStringAssertion(expressionKey, contextValue);
expressionStringAssertion(expressionKey, expressionValue.regexp);
return Boolean(contextValue.match(new RegExp(expressionValue.regexp)));
const regexpValue = extractValueOrRef(context, validation, expressionValue.regexp);
expressionStringAssertion(expressionKey, regexpValue);
return Boolean(contextValue.match(new RegExp(regexpValue)));
} else if (isRegexiCompareOp(expressionValue)) {
contextStringAssertion(expressionKey, contextValue);
expressionStringAssertion(expressionKey, expressionValue.regexpi);
return Boolean(contextValue.match(new RegExp(expressionValue.regexpi, `i`)));
const regexpiValue = extractValueOrRef(context, validation, expressionValue.regexpi);
expressionStringAssertion(expressionKey, regexpiValue);
return Boolean(contextValue.match(new RegExp(regexpiValue, `i`)));
} else if (isGtCompareOp(expressionValue)) {
contextNumberAssertion(expressionKey, contextValue);
expressionNumberAssertion(expressionKey, expressionValue.gt);
return contextValue > expressionValue.gt;
const gtValue = extractValueOrRef(context, validation, expressionValue.gt);
expressionNumberAssertion(expressionKey, gtValue);
return contextValue > gtValue;
} else if (isGteCompareOp(expressionValue)) {
contextNumberAssertion(expressionKey, contextValue);
expressionNumberAssertion(expressionKey, expressionValue.gte);
return contextValue >= expressionValue.gte;
const gteValue = extractValueOrRef(context, validation, expressionValue.gte);
expressionNumberAssertion(expressionKey, gteValue);
return contextValue >= gteValue;
} else if (isLteCompareOp(expressionValue)) {
contextNumberAssertion(expressionKey, contextValue);
expressionNumberAssertion(expressionKey, expressionValue.lte);
return contextValue <= expressionValue.lte;
const lteValue = extractValueOrRef(context, validation, expressionValue.lte);
expressionNumberAssertion(expressionKey, lteValue);
return contextValue <= lteValue;
} else if (isLtCompareOp(expressionValue)) {
contextNumberAssertion(expressionKey, contextValue);
expressionNumberAssertion(expressionKey, expressionValue.lt);
return contextValue < expressionValue.lt;
const ltValue = extractValueOrRef(context, validation, expressionValue.lt);
expressionNumberAssertion(expressionKey, ltValue);
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`);
}
expressionValue.between.forEach((value, ind) => {
expressionNumberAssertion(`${expressionKey}[${ind}]`, value);
});
const [low, high] = expressionValue.between;
const [lowRaw, highRaw] = expressionValue.between;
const low = extractValueOrRef(context, validation, lowRaw);
const high = extractValueOrRef(context, validation, highRaw);
expressionNumberAssertion(`${expressionKey}[0]`, low);
expressionNumberAssertion(`${expressionKey}[1]`, high);
if (low > high) {
throw new Error(`Invalid expression - ${expressionKey} first value is higher than second value`);
}
Expand Down Expand Up @@ -140,10 +169,11 @@ function run<C extends Context, F extends FunctionsTable<C>, Ignore>
if (validation && !exists) {
throw new Error(`Invalid expression - unknown context key ${expressionKey}`);
}
return evaluateCompareOp(
return evaluateCompareOp<C>(
(expression as PropertyCompareOps<C, Ignore>)
[expressionKey as any as keyof PropertyCompareOps<C, Ignore>] as ExtendedCompareOp,
expressionKey, contextValue);
[expressionKey as any as keyof PropertyCompareOps<C, Ignore>] as
unknown as ExtendedCompareOp<any, any, any>,
expressionKey, contextValue, context, validation);
}
}

Expand Down
146 changes: 73 additions & 73 deletions src/lib/typeGuards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,94 +12,94 @@ import {
RegexiCompareOp,
NotCompareOp,
NotEqualCompareOp,
OrCompareOp, InqCompareOp, NinCompareOp, RuleFunctionsTable, RuleFunctionsParams
} from '../types';
OrCompareOp, InqCompareOp, NinCompareOp, RuleFunctionsTable, RuleFunctionsParams, Primitive
} from '../types';

export const _isObject = (obj: unknown): boolean => {
export const _isObject = (obj: unknown): boolean => {
const type = typeof obj;
return type === 'function' || type === 'object' && !!obj;
};
};

export const isFunctionCompareOp =
export const isFunctionCompareOp =
<C extends Context, F extends FunctionsTable<C>, Ignore>(expression: unknown, functionsTable: F, key: string):
expression is FuncCompares<C, F> => {
return key in functionsTable;
expression is FuncCompares<C, F> => {
return key in functionsTable;
}

export const isRuleFunction =
export const isRuleFunction =
<ConsequencePayload, C extends Context, RF extends RuleFunctionsTable<C, ConsequencePayload>>(
expression: unknown, ruleFunctionsTable: RF, key: string):
expression is RuleFunctionsParams<ConsequencePayload, C, RF> => {
return key in ruleFunctionsTable;
expression: unknown, ruleFunctionsTable: RF, key: string):
expression is RuleFunctionsParams<ConsequencePayload, C, RF> => {
return key in ruleFunctionsTable;
}

export const isAndCompareOp =
export const isAndCompareOp =
<C extends Context, F extends FunctionsTable<C>, Ignore>(expression: unknown):
expression is AndCompareOp<C, F, Ignore> => {
return Array.isArray((expression as AndCompareOp<C, F, Ignore>).and);
expression is AndCompareOp<C, F, Ignore> => {
return Array.isArray((expression as AndCompareOp<C, F, Ignore>).and);
}

export const isOrCompareOp = <C extends Context, F extends FunctionsTable<C>, Ignore>(expression: unknown):
export const isOrCompareOp = <C extends Context, F extends FunctionsTable<C>, Ignore>(expression: unknown):
expression is OrCompareOp<C, F, Ignore> => {
return Array.isArray((expression as OrCompareOp<C, F, Ignore>).or);
}
}

export const isNotCompareOp = <C extends Context, F extends FunctionsTable<C>, Ignore>(expression: unknown):
export const isNotCompareOp = <C extends Context, F extends FunctionsTable<C>, Ignore>(expression: unknown):
expression is NotCompareOp<C, F, Ignore> => {
return _isObject((expression as NotCompareOp<C, F, Ignore>).not);
}

export const isBetweenCompareOp = (op: ExtendedCompareOp)
: op is BetweenCompareOp => {
return Array.isArray((op as BetweenCompareOp).between);
}

export const isGtCompareOp = (op: ExtendedCompareOp)
: op is GtCompareOp => {
return (op as GtCompareOp).gt !== undefined;
}

export const isGteCompareOp = (op: ExtendedCompareOp)
: op is GteCompareOp => {
return (op as GteCompareOp).gte !== undefined;
}

export const isLteCompareOp = (op: ExtendedCompareOp)
: op is LteCompareOp => {
return (op as LteCompareOp).lte !== undefined;
}

export const isLtCompareOp = (op: ExtendedCompareOp)
: op is LtCompareOp => {
return (op as LtCompareOp).lt !== undefined;
}

export const isRegexCompareOp = (op: ExtendedCompareOp)
: op is RegexCompareOp => {
return (op as RegexCompareOp).regexp !== undefined;
}

export const isRegexiCompareOp = (op: ExtendedCompareOp)
: op is RegexiCompareOp => {
return (op as RegexiCompareOp).regexpi !== undefined;
}

export const isEqualCompareOp = <V>(op: ExtendedCompareOp)
: op is EqualCompareOp<V> => {
return (op as EqualCompareOp<V>).eq !== undefined;
}

export const isNotEqualCompareOp = <V>(op: ExtendedCompareOp)
: op is NotEqualCompareOp<V> => {
return (op as NotEqualCompareOp<V>).neq !== undefined;
}

export const isInqCompareOp = <V>(op: ExtendedCompareOp)
: op is InqCompareOp<V> => {
return Array.isArray((op as InqCompareOp<V>).inq);
}

export const isNinCompareOp = <V>(op: ExtendedCompareOp)
: op is NinCompareOp<V> => {
return Array.isArray((op as NinCompareOp<V>).nin);
}
}

export const isBetweenCompareOp = (op: ExtendedCompareOp<any, any, any>)
: op is BetweenCompareOp<any, any> => {
return Array.isArray((op as BetweenCompareOp<any, any>).between);
}

export const isGtCompareOp = (op: ExtendedCompareOp<any, any, any>)
: op is GtCompareOp<any, any> => {
return (op as GtCompareOp<any, any>).gt !== undefined;
}

export const isGteCompareOp = (op: ExtendedCompareOp<any, any, any>)
: op is GteCompareOp<any, any> => {
return (op as GteCompareOp<any, any>).gte !== undefined;
}

export const isLteCompareOp = (op: ExtendedCompareOp<any, any, any>)
: op is LteCompareOp<any, any> => {
return (op as LteCompareOp<any, any>).lte !== undefined;
}

export const isLtCompareOp = (op: ExtendedCompareOp<any, any, any>)
: op is LtCompareOp<any, any> => {
return (op as LtCompareOp<any, any>).lt !== undefined;
}

export const isRegexCompareOp = (op: ExtendedCompareOp<any, any, any>)
: op is RegexCompareOp<any, any> => {
return (op as RegexCompareOp<any, any>).regexp !== undefined;
}

export const isRegexiCompareOp = (op: ExtendedCompareOp<any, any, any>)
: op is RegexiCompareOp<any, any> => {
return (op as RegexiCompareOp<any, any>).regexpi !== undefined;
}

export const isEqualCompareOp = (op: ExtendedCompareOp<any, any, any>)
: op is EqualCompareOp<any, any, any> => {
return (op as EqualCompareOp<any, any, any>).eq !== undefined;
}

export const isNotEqualCompareOp = (op: ExtendedCompareOp<any, any, any>)
: op is NotEqualCompareOp<any, any, any> => {
return (op as NotEqualCompareOp<any, any, any>).neq !== undefined;
}

export const isInqCompareOp = (op: ExtendedCompareOp<any, any, any>)
: op is InqCompareOp<any, any, any> => {
return Array.isArray((op as InqCompareOp<any, any, any>).inq);
}

export const isNinCompareOp = (op: ExtendedCompareOp<any, any, any>)
: op is NinCompareOp<any, any, any> => {
return Array.isArray((op as NinCompareOp<any, any, any>).nin);
}
Loading

0 comments on commit 5ad068e

Please sign in to comment.