Skip to content

Commit 5ad068e

Browse files
authored
add ability to refrence context on right hand side of expressions (#21)
1 parent 8bd849d commit 5ad068e

File tree

9 files changed

+756
-386
lines changed

9 files changed

+756
-386
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ interface IExampleContext {
3737
date: Moment;
3838
nested: {
3939
value: number | null;
40+
value4: number;
4041
nested2: {
4142
value2?: number;
4243
value3: boolean;
@@ -58,6 +59,7 @@ const context: IExampleContext = {
5859
date: moment(),
5960
nested: {
6061
value: null,
62+
value4: 5,
6163
nested2: {
6264
value3: true,
6365
},
@@ -71,6 +73,7 @@ const validationContext: ValidationContext<IExampleContext, IExampleContextIgnor
7173
date: moment(),
7274
nested: {
7375
value: 5,
76+
value4: 6,
7477
nested2: {
7578
value2: 6,
7679
value3: true,
@@ -97,6 +100,13 @@ const expression: IExampleExpression = {
97100
{
98101
'nested.nested2.value3': true,
99102
},
103+
{
104+
times: {
105+
lte: {
106+
ref: 'nested.value4'
107+
}
108+
},
109+
},
100110
],
101111
},
102112
],
@@ -136,9 +146,13 @@ There are 4 types of operators you can use (evaluated in that order of precedenc
136146
- `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.
137147
- `{property: value}`
138148
- compares the property to that value (shorthand to the `eq` op)
149+
139150
> Nested properties in the context can also be accessed using a dot notation (see example above)
140151
> In each expression level, you can only define 1 operator, and 1 only
141152
153+
> You can reference values (and nested values) from the context using the {"ref":"<dot notation path>"}
154+
> (see example above) on the right-hand side of expressions (not in parameters to user defined functions though)
155+
142156
Example expressions, assuming we have the `user` and `maxCount` user defined functions in place can be:
143157
```json
144158
{
@@ -154,6 +168,9 @@ Example expressions, assuming we have the `user` and `maxCount` user defined fun
154168
{
155169
"times": { "eq" : 5}
156170
},
171+
{
172+
"times": { "eq" : { "ref": "nested.preoprty"}}
173+
},
157174
{
158175
"country": "USA"
159176
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "json-expression-eval",
3-
"version": "4.2.1",
3+
"version": "4.3.0",
44
"description": "json serializable rule engine / boolean expression evaluator",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

src/examples/engine/example.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ rules = [
4949
and: [
5050
{user: 'a@b.com'},
5151
{maxCount: 5},
52+
{times: {eq:{ref:'nested.value'}}},
5253
],
5354
},
5455
consequence: {

src/examples/evaluator/example.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ expression = {
3636

3737
run(expression, context);
3838

39+
expression = {
40+
and: [
41+
{user: 'a@b.com'},
42+
{times: {eq:{ref:'nested.value'}}},
43+
],
44+
};
45+
46+
run(expression, context);
47+
3948
expression = {
4049
and: [
4150
{user: 'a@b.com'},

src/lib/evaluator.ts

Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
FunctionsTable,
2323
ExtendedCompareOp,
2424
ValidationContext,
25-
PropertyCompareOps
25+
PropertyCompareOps, Primitive
2626
} from '../types';
2727
import {
2828
assertUnreachable,
@@ -34,7 +34,27 @@ import {
3434
expressionNumberAssertion
3535
} from './helpers';
3636

37-
function evaluateCompareOp(expressionValue: ExtendedCompareOp, expressionKey: string, contextValue: any): boolean {
37+
type WithRef = {
38+
ref: string
39+
}
40+
41+
const isWithRef = (x: unknown): x is WithRef => Boolean((x as WithRef).ref);
42+
43+
const extractValueOrRef = <C extends Context>(context: C, validation: boolean, valueOrRef: Primitive | WithRef) => {
44+
if (isWithRef(valueOrRef)) {
45+
const {value, exists} = getFromPath(context, valueOrRef.ref);
46+
if (validation && !exists) {
47+
throw new Error(`Invalid expression - unknown context key ${valueOrRef.ref}`);
48+
}
49+
return value;
50+
} else {
51+
return valueOrRef;
52+
}
53+
}
54+
55+
function evaluateCompareOp<C extends Context>(expressionValue: ExtendedCompareOp<any, any, any>, expressionKey: string,
56+
contextValue: any, context: C, validation: boolean)
57+
: boolean {
3858
if (!_isObject(expressionValue)) {
3959
return contextValue === expressionValue;
4060
}
@@ -43,46 +63,55 @@ function evaluateCompareOp(expressionValue: ExtendedCompareOp, expressionKey: st
4363
throw new Error('Invalid expression - too may keys');
4464
}
4565
if (isEqualCompareOp(expressionValue)) {
46-
return contextValue === expressionValue.eq;
66+
return contextValue === extractValueOrRef(context, validation, expressionValue.eq);
4767
} else if (isNotEqualCompareOp(expressionValue)) {
48-
return contextValue !== expressionValue.neq;
68+
return contextValue !== extractValueOrRef(context, validation, expressionValue.neq);
4969
} else if (isInqCompareOp(expressionValue)) {
50-
return expressionValue.inq.indexOf(contextValue) >= 0;
70+
return expressionValue.inq.map((value) => extractValueOrRef(context, validation, value))
71+
.indexOf(contextValue) >= 0;
5172
} else if (isNinCompareOp(expressionValue)) {
52-
return expressionValue.nin.indexOf(contextValue) < 0;
73+
return expressionValue.nin.map((value) => extractValueOrRef(context, validation, value))
74+
.indexOf(contextValue) < 0;
5375
} else if (isRegexCompareOp(expressionValue)) {
5476
contextStringAssertion(expressionKey, contextValue);
55-
expressionStringAssertion(expressionKey, expressionValue.regexp);
56-
return Boolean(contextValue.match(new RegExp(expressionValue.regexp)));
77+
const regexpValue = extractValueOrRef(context, validation, expressionValue.regexp);
78+
expressionStringAssertion(expressionKey, regexpValue);
79+
return Boolean(contextValue.match(new RegExp(regexpValue)));
5780
} else if (isRegexiCompareOp(expressionValue)) {
5881
contextStringAssertion(expressionKey, contextValue);
59-
expressionStringAssertion(expressionKey, expressionValue.regexpi);
60-
return Boolean(contextValue.match(new RegExp(expressionValue.regexpi, `i`)));
82+
const regexpiValue = extractValueOrRef(context, validation, expressionValue.regexpi);
83+
expressionStringAssertion(expressionKey, regexpiValue);
84+
return Boolean(contextValue.match(new RegExp(regexpiValue, `i`)));
6185
} else if (isGtCompareOp(expressionValue)) {
6286
contextNumberAssertion(expressionKey, contextValue);
63-
expressionNumberAssertion(expressionKey, expressionValue.gt);
64-
return contextValue > expressionValue.gt;
87+
const gtValue = extractValueOrRef(context, validation, expressionValue.gt);
88+
expressionNumberAssertion(expressionKey, gtValue);
89+
return contextValue > gtValue;
6590
} else if (isGteCompareOp(expressionValue)) {
6691
contextNumberAssertion(expressionKey, contextValue);
67-
expressionNumberAssertion(expressionKey, expressionValue.gte);
68-
return contextValue >= expressionValue.gte;
92+
const gteValue = extractValueOrRef(context, validation, expressionValue.gte);
93+
expressionNumberAssertion(expressionKey, gteValue);
94+
return contextValue >= gteValue;
6995
} else if (isLteCompareOp(expressionValue)) {
7096
contextNumberAssertion(expressionKey, contextValue);
71-
expressionNumberAssertion(expressionKey, expressionValue.lte);
72-
return contextValue <= expressionValue.lte;
97+
const lteValue = extractValueOrRef(context, validation, expressionValue.lte);
98+
expressionNumberAssertion(expressionKey, lteValue);
99+
return contextValue <= lteValue;
73100
} else if (isLtCompareOp(expressionValue)) {
74101
contextNumberAssertion(expressionKey, contextValue);
75-
expressionNumberAssertion(expressionKey, expressionValue.lt);
76-
return contextValue < expressionValue.lt;
102+
const ltValue = extractValueOrRef(context, validation, expressionValue.lt);
103+
expressionNumberAssertion(expressionKey, ltValue);
104+
return contextValue < ltValue;
77105
} else if (isBetweenCompareOp(expressionValue)) {
78106
contextNumberAssertion(expressionKey, contextValue);
79107
if (expressionValue.between.length !== 2) {
80108
throw new Error(`Invalid expression - ${expressionKey}.length must be 2`);
81109
}
82-
expressionValue.between.forEach((value, ind) => {
83-
expressionNumberAssertion(`${expressionKey}[${ind}]`, value);
84-
});
85-
const [low, high] = expressionValue.between;
110+
const [lowRaw, highRaw] = expressionValue.between;
111+
const low = extractValueOrRef(context, validation, lowRaw);
112+
const high = extractValueOrRef(context, validation, highRaw);
113+
expressionNumberAssertion(`${expressionKey}[0]`, low);
114+
expressionNumberAssertion(`${expressionKey}[1]`, high);
86115
if (low > high) {
87116
throw new Error(`Invalid expression - ${expressionKey} first value is higher than second value`);
88117
}
@@ -140,10 +169,11 @@ function run<C extends Context, F extends FunctionsTable<C>, Ignore>
140169
if (validation && !exists) {
141170
throw new Error(`Invalid expression - unknown context key ${expressionKey}`);
142171
}
143-
return evaluateCompareOp(
172+
return evaluateCompareOp<C>(
144173
(expression as PropertyCompareOps<C, Ignore>)
145-
[expressionKey as any as keyof PropertyCompareOps<C, Ignore>] as ExtendedCompareOp,
146-
expressionKey, contextValue);
174+
[expressionKey as any as keyof PropertyCompareOps<C, Ignore>] as
175+
unknown as ExtendedCompareOp<any, any, any>,
176+
expressionKey, contextValue, context, validation);
147177
}
148178
}
149179

src/lib/typeGuards.ts

Lines changed: 73 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -12,94 +12,94 @@ import {
1212
RegexiCompareOp,
1313
NotCompareOp,
1414
NotEqualCompareOp,
15-
OrCompareOp, InqCompareOp, NinCompareOp, RuleFunctionsTable, RuleFunctionsParams
16-
} from '../types';
15+
OrCompareOp, InqCompareOp, NinCompareOp, RuleFunctionsTable, RuleFunctionsParams, Primitive
16+
} from '../types';
1717

18-
export const _isObject = (obj: unknown): boolean => {
18+
export const _isObject = (obj: unknown): boolean => {
1919
const type = typeof obj;
2020
return type === 'function' || type === 'object' && !!obj;
21-
};
21+
};
2222

23-
export const isFunctionCompareOp =
23+
export const isFunctionCompareOp =
2424
<C extends Context, F extends FunctionsTable<C>, Ignore>(expression: unknown, functionsTable: F, key: string):
25-
expression is FuncCompares<C, F> => {
26-
return key in functionsTable;
25+
expression is FuncCompares<C, F> => {
26+
return key in functionsTable;
2727
}
2828

29-
export const isRuleFunction =
29+
export const isRuleFunction =
3030
<ConsequencePayload, C extends Context, RF extends RuleFunctionsTable<C, ConsequencePayload>>(
31-
expression: unknown, ruleFunctionsTable: RF, key: string):
32-
expression is RuleFunctionsParams<ConsequencePayload, C, RF> => {
33-
return key in ruleFunctionsTable;
31+
expression: unknown, ruleFunctionsTable: RF, key: string):
32+
expression is RuleFunctionsParams<ConsequencePayload, C, RF> => {
33+
return key in ruleFunctionsTable;
3434
}
3535

36-
export const isAndCompareOp =
36+
export const isAndCompareOp =
3737
<C extends Context, F extends FunctionsTable<C>, Ignore>(expression: unknown):
38-
expression is AndCompareOp<C, F, Ignore> => {
39-
return Array.isArray((expression as AndCompareOp<C, F, Ignore>).and);
38+
expression is AndCompareOp<C, F, Ignore> => {
39+
return Array.isArray((expression as AndCompareOp<C, F, Ignore>).and);
4040
}
4141

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

47-
export const isNotCompareOp = <C extends Context, F extends FunctionsTable<C>, Ignore>(expression: unknown):
47+
export const isNotCompareOp = <C extends Context, F extends FunctionsTable<C>, Ignore>(expression: unknown):
4848
expression is NotCompareOp<C, F, Ignore> => {
4949
return _isObject((expression as NotCompareOp<C, F, Ignore>).not);
50-
}
51-
52-
export const isBetweenCompareOp = (op: ExtendedCompareOp)
53-
: op is BetweenCompareOp => {
54-
return Array.isArray((op as BetweenCompareOp).between);
55-
}
56-
57-
export const isGtCompareOp = (op: ExtendedCompareOp)
58-
: op is GtCompareOp => {
59-
return (op as GtCompareOp).gt !== undefined;
60-
}
61-
62-
export const isGteCompareOp = (op: ExtendedCompareOp)
63-
: op is GteCompareOp => {
64-
return (op as GteCompareOp).gte !== undefined;
65-
}
66-
67-
export const isLteCompareOp = (op: ExtendedCompareOp)
68-
: op is LteCompareOp => {
69-
return (op as LteCompareOp).lte !== undefined;
70-
}
71-
72-
export const isLtCompareOp = (op: ExtendedCompareOp)
73-
: op is LtCompareOp => {
74-
return (op as LtCompareOp).lt !== undefined;
75-
}
76-
77-
export const isRegexCompareOp = (op: ExtendedCompareOp)
78-
: op is RegexCompareOp => {
79-
return (op as RegexCompareOp).regexp !== undefined;
80-
}
81-
82-
export const isRegexiCompareOp = (op: ExtendedCompareOp)
83-
: op is RegexiCompareOp => {
84-
return (op as RegexiCompareOp).regexpi !== undefined;
85-
}
86-
87-
export const isEqualCompareOp = <V>(op: ExtendedCompareOp)
88-
: op is EqualCompareOp<V> => {
89-
return (op as EqualCompareOp<V>).eq !== undefined;
90-
}
91-
92-
export const isNotEqualCompareOp = <V>(op: ExtendedCompareOp)
93-
: op is NotEqualCompareOp<V> => {
94-
return (op as NotEqualCompareOp<V>).neq !== undefined;
95-
}
96-
97-
export const isInqCompareOp = <V>(op: ExtendedCompareOp)
98-
: op is InqCompareOp<V> => {
99-
return Array.isArray((op as InqCompareOp<V>).inq);
100-
}
101-
102-
export const isNinCompareOp = <V>(op: ExtendedCompareOp)
103-
: op is NinCompareOp<V> => {
104-
return Array.isArray((op as NinCompareOp<V>).nin);
105-
}
50+
}
51+
52+
export const isBetweenCompareOp = (op: ExtendedCompareOp<any, any, any>)
53+
: op is BetweenCompareOp<any, any> => {
54+
return Array.isArray((op as BetweenCompareOp<any, any>).between);
55+
}
56+
57+
export const isGtCompareOp = (op: ExtendedCompareOp<any, any, any>)
58+
: op is GtCompareOp<any, any> => {
59+
return (op as GtCompareOp<any, any>).gt !== undefined;
60+
}
61+
62+
export const isGteCompareOp = (op: ExtendedCompareOp<any, any, any>)
63+
: op is GteCompareOp<any, any> => {
64+
return (op as GteCompareOp<any, any>).gte !== undefined;
65+
}
66+
67+
export const isLteCompareOp = (op: ExtendedCompareOp<any, any, any>)
68+
: op is LteCompareOp<any, any> => {
69+
return (op as LteCompareOp<any, any>).lte !== undefined;
70+
}
71+
72+
export const isLtCompareOp = (op: ExtendedCompareOp<any, any, any>)
73+
: op is LtCompareOp<any, any> => {
74+
return (op as LtCompareOp<any, any>).lt !== undefined;
75+
}
76+
77+
export const isRegexCompareOp = (op: ExtendedCompareOp<any, any, any>)
78+
: op is RegexCompareOp<any, any> => {
79+
return (op as RegexCompareOp<any, any>).regexp !== undefined;
80+
}
81+
82+
export const isRegexiCompareOp = (op: ExtendedCompareOp<any, any, any>)
83+
: op is RegexiCompareOp<any, any> => {
84+
return (op as RegexiCompareOp<any, any>).regexpi !== undefined;
85+
}
86+
87+
export const isEqualCompareOp = (op: ExtendedCompareOp<any, any, any>)
88+
: op is EqualCompareOp<any, any, any> => {
89+
return (op as EqualCompareOp<any, any, any>).eq !== undefined;
90+
}
91+
92+
export const isNotEqualCompareOp = (op: ExtendedCompareOp<any, any, any>)
93+
: op is NotEqualCompareOp<any, any, any> => {
94+
return (op as NotEqualCompareOp<any, any, any>).neq !== undefined;
95+
}
96+
97+
export const isInqCompareOp = (op: ExtendedCompareOp<any, any, any>)
98+
: op is InqCompareOp<any, any, any> => {
99+
return Array.isArray((op as InqCompareOp<any, any, any>).inq);
100+
}
101+
102+
export const isNinCompareOp = (op: ExtendedCompareOp<any, any, any>)
103+
: op is NinCompareOp<any, any, any> => {
104+
return Array.isArray((op as NinCompareOp<any, any, any>).nin);
105+
}

0 commit comments

Comments
 (0)