Skip to content

Commit 559bd68

Browse files
authored
add math capabilities (#24)
1 parent 476edef commit 559bd68

File tree

9 files changed

+303
-41
lines changed

9 files changed

+303
-41
lines changed

README.md

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,17 @@ const expression: IExampleExpression = {
9292
{
9393
userId: 'a@b.com',
9494
},
95+
{
96+
times: {
97+
lte: {
98+
op: '+',
99+
lhs: {
100+
ref: 'nested.value4'
101+
},
102+
rhs: 2,
103+
},
104+
},
105+
},
95106
{
96107
and: [
97108
{
@@ -129,7 +140,7 @@ There are 4 types of operators you can use (evaluated in that order of precedenc
129140
- `and` - accepts a non-empty list of expressions
130141
- `or` - accepts a non-empty list of expressions
131142
- `not` - accepts another expressions
132-
- `<user defined funcs>` - accepts any type of argument and evaluated by the user defined functions and the given context. Can be async.
143+
- `<user defined funcs>` - accepts any type of argument and evaluated by the user defined functions, and the given context (can be async).
133144
- `<compare funcs>` - operates on one of the context properties and compares it to a given value.
134145
- `{property: {op: value}}`
135146
- available ops:
@@ -145,13 +156,36 @@ There are 4 types of operators you can use (evaluated in that order of precedenc
145156
- `inq: any[]` - True if in an array of values. Comparison is done using the `===` operator
146157
- `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.
147158
- `{property: value}`
148-
- compares the property to that value (shorthand to the `eq` op)
159+
- compares the property to that value (shorthand to the `eq` op, without the option to user math or refs to other properties)
149160

150161
> Nested properties in the context can also be accessed using a dot notation (see example above)
162+
151163
> In each expression level, you can only define 1 operator, and 1 only
152164
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)
165+
The right-hand side of compare (not user defined) functions can be a:
166+
- literal - number/string/boolean (depending on the left-hand side of the function)
167+
- reference to a property (or nested property) in the context.
168+
This can be achieved by using `{"ref":"<dot notation path>"}`
169+
- A math operation that can reference properties in the context.
170+
The valid operations are `+,-,*,/,%,pow`.
171+
This can be achieved by using
172+
```json
173+
{
174+
"op": "<+,-,*,/,%,pow>",
175+
"lhs": {"ref": "<dot notation path>"}, // or a number literal
176+
"rhs": {"ref": "<dot notation path>"} // or a number literal
177+
}
178+
```
179+
which will be computed as `<lhs> <op> <rhs>` where lhs is left-hand-side and rhs is right-hand-side. So for example
180+
```json
181+
{
182+
"op": "/",
183+
"lhs": 10,
184+
"rhs": 2
185+
}
186+
```
187+
will equal `10 / 2 = 5`
188+
155189

156190
Example expressions, assuming we have the `user` and `maxCount` user defined functions in place can be:
157191
```json

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "json-expression-eval",
3-
"version": "5.0.0",
3+
"version": "5.1.0",
44
"description": "json serializable rule engine / boolean expression evaluator",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",
@@ -14,7 +14,8 @@
1414
"build": "yarn lint && yarn compile",
1515
"compile": "./node_modules/.bin/tsc",
1616
"test:cover": "nyc --reporter=lcov --reporter=text-summary mocha --opts src/test/mocha.opts",
17-
"lint": "tslint -c tslint.json 'src/**/*.ts' 'test/**/*.ts'"
17+
"lint": "tslint -c tslint.json 'src/**/*.ts' 'test/**/*.ts'",
18+
"ci": "yarn lint && yarn compile && yarn test:tsd && yarn test:cover"
1819
},
1920
"repository": {
2021
"type": "git",

src/examples/engine/example.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ rules = [
5050
{user: 'a@b.com'},
5151
{maxCount: 5},
5252
{times: {eq:{ref:'nested.value'}}},
53+
{times: {lte:{op:'+', lhs: {ref:'nested.value'}, rhs: 1}}},
5354
],
5455
},
5556
consequence: {

src/examples/evaluator/example.ts

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

4646
run(expression, context);
4747

48+
expression = {
49+
and: [
50+
{user: 'a@b.com'},
51+
{times: {eq:{op:'+', lhs: {ref:'nested.value'}, rhs: 1}}},
52+
],
53+
};
54+
55+
run(expression, context);
56+
4857
expression = {
4958
and: [
5059
{user: 'a@b.com'},

src/lib/evaluator.ts

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,17 @@ import {
1414
isInqCompareOp,
1515
isNinCompareOp,
1616
isRegexCompareOp,
17-
isRegexiCompareOp
17+
isRegexiCompareOp,
18+
isWithRef, isMathOp,
19+
WithRef
1820
} from './typeGuards';
1921
import {
2022
Context,
2123
Expression,
2224
FunctionsTable,
2325
ExtendedCompareOp,
2426
ValidationContext,
25-
PropertyCompareOps, Primitive
27+
PropertyCompareOps, Primitive, MathOp
2628
} from '../types';
2729
import {
2830
assertUnreachable,
@@ -34,13 +36,8 @@ import {
3436
expressionNumberAssertion
3537
} from './helpers';
3638

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) => {
39+
const extractValueOrRef = <C extends Context>(context: C, validation: boolean, valueOrRef: Primitive | WithRef)
40+
: Primitive => {
4441
if (isWithRef(valueOrRef)) {
4542
const {value, exists} = getFromPath(context, valueOrRef.ref);
4643
if (validation && !exists) {
@@ -52,9 +49,37 @@ const extractValueOrRef = <C extends Context>(context: C, validation: boolean, v
5249
}
5350
}
5451

55-
async function evaluateCompareOp<C extends Context>(expressionValue: ExtendedCompareOp<any, any, any>,
56-
expressionKey: string, contextValue: any,
57-
context: C, validation: boolean)
52+
const computeValue = <C extends Context>(context: C, validation: boolean,
53+
value: Primitive | WithRef | MathOp<any, any>,
54+
expressionKey: string): Primitive => {
55+
if (isMathOp(value)) {
56+
const lhs = extractValueOrRef<C>(context, validation, value.lhs);
57+
const rhs = extractValueOrRef<C>(context, validation, value.rhs);
58+
expressionNumberAssertion(expressionKey, lhs);
59+
expressionNumberAssertion(expressionKey, rhs);
60+
switch (value.op) {
61+
case '+':
62+
return lhs + rhs;
63+
case '-':
64+
return lhs - rhs;
65+
case '*':
66+
return lhs * rhs;
67+
case '/':
68+
return lhs / rhs;
69+
case '%':
70+
return lhs % rhs;
71+
case 'pow':
72+
return Math.pow(lhs, rhs);
73+
default:
74+
throw new Error(`Invalid expression - ${expressionKey} has invalid math operand ${value.op}`);
75+
}
76+
}
77+
return extractValueOrRef(context, validation, value);
78+
}
79+
80+
async function evaluateCompareOp<C extends Context, Ignore>(expressionValue: ExtendedCompareOp<any, any, any>,
81+
expressionKey: string, contextValue: any,
82+
context: C, validation: boolean)
5883
: Promise<boolean> {
5984
if (!_isObject(expressionValue)) {
6085
return contextValue === expressionValue;
@@ -64,43 +89,43 @@ async function evaluateCompareOp<C extends Context>(expressionValue: ExtendedCom
6489
throw new Error('Invalid expression - too may keys');
6590
}
6691
if (isEqualCompareOp(expressionValue)) {
67-
return contextValue === extractValueOrRef(context, validation, expressionValue.eq);
92+
return contextValue === computeValue(context, validation, expressionValue.eq, expressionKey);
6893
} else if (isNotEqualCompareOp(expressionValue)) {
69-
return contextValue !== extractValueOrRef(context, validation, expressionValue.neq);
94+
return contextValue !== computeValue(context, validation, expressionValue.neq, expressionKey);
7095
} else if (isInqCompareOp(expressionValue)) {
71-
return expressionValue.inq.map((value) => extractValueOrRef(context, validation, value))
96+
return expressionValue.inq.map((value) => computeValue(context, validation, value, expressionKey))
7297
.indexOf(contextValue) >= 0;
7398
} else if (isNinCompareOp(expressionValue)) {
74-
return expressionValue.nin.map((value) => extractValueOrRef(context, validation, value))
99+
return expressionValue.nin.map((value) => computeValue(context, validation, value, expressionKey))
75100
.indexOf(contextValue) < 0;
76101
} else if (isRegexCompareOp(expressionValue)) {
77102
contextStringAssertion(expressionKey, contextValue);
78-
const regexpValue = extractValueOrRef(context, validation, expressionValue.regexp);
103+
const regexpValue = computeValue(context, validation, expressionValue.regexp, expressionKey);
79104
expressionStringAssertion(expressionKey, regexpValue);
80105
return Boolean(contextValue.match(new RegExp(regexpValue)));
81106
} else if (isRegexiCompareOp(expressionValue)) {
82107
contextStringAssertion(expressionKey, contextValue);
83-
const regexpiValue = extractValueOrRef(context, validation, expressionValue.regexpi);
108+
const regexpiValue = computeValue(context, validation, expressionValue.regexpi, expressionKey);
84109
expressionStringAssertion(expressionKey, regexpiValue);
85110
return Boolean(contextValue.match(new RegExp(regexpiValue, `i`)));
86111
} else if (isGtCompareOp(expressionValue)) {
87112
contextNumberAssertion(expressionKey, contextValue);
88-
const gtValue = extractValueOrRef(context, validation, expressionValue.gt);
113+
const gtValue = computeValue(context, validation, expressionValue.gt, expressionKey);
89114
expressionNumberAssertion(expressionKey, gtValue);
90115
return contextValue > gtValue;
91116
} else if (isGteCompareOp(expressionValue)) {
92117
contextNumberAssertion(expressionKey, contextValue);
93-
const gteValue = extractValueOrRef(context, validation, expressionValue.gte);
118+
const gteValue = computeValue(context, validation, expressionValue.gte, expressionKey);
94119
expressionNumberAssertion(expressionKey, gteValue);
95120
return contextValue >= gteValue;
96121
} else if (isLteCompareOp(expressionValue)) {
97122
contextNumberAssertion(expressionKey, contextValue);
98-
const lteValue = extractValueOrRef(context, validation, expressionValue.lte);
123+
const lteValue = computeValue(context, validation, expressionValue.lte, expressionKey);
99124
expressionNumberAssertion(expressionKey, lteValue);
100125
return contextValue <= lteValue;
101126
} else if (isLtCompareOp(expressionValue)) {
102127
contextNumberAssertion(expressionKey, contextValue);
103-
const ltValue = extractValueOrRef(context, validation, expressionValue.lt);
128+
const ltValue = computeValue(context, validation, expressionValue.lt, expressionKey);
104129
expressionNumberAssertion(expressionKey, ltValue);
105130
return contextValue < ltValue;
106131
} else if (isBetweenCompareOp(expressionValue)) {
@@ -109,8 +134,8 @@ async function evaluateCompareOp<C extends Context>(expressionValue: ExtendedCom
109134
throw new Error(`Invalid expression - ${expressionKey}.length must be 2`);
110135
}
111136
const [lowRaw, highRaw] = expressionValue.between;
112-
const low = extractValueOrRef(context, validation, lowRaw);
113-
const high = extractValueOrRef(context, validation, highRaw);
137+
const low = computeValue(context, validation, lowRaw, expressionKey);
138+
const high = computeValue(context, validation, highRaw, expressionKey);
114139
expressionNumberAssertion(`${expressionKey}[0]`, low);
115140
expressionNumberAssertion(`${expressionKey}[1]`, high);
116141
if (low > high) {
@@ -170,7 +195,7 @@ async function run<C extends Context, F extends FunctionsTable<C>, Ignore>
170195
if (validation && !exists) {
171196
throw new Error(`Invalid expression - unknown context key ${expressionKey}`);
172197
}
173-
return evaluateCompareOp<C>(
198+
return evaluateCompareOp<C, Ignore>(
174199
(expression as PropertyCompareOps<C, Ignore>)
175200
[expressionKey as any as keyof PropertyCompareOps<C, Ignore>] as
176201
unknown as ExtendedCompareOp<any, any, any>,

src/lib/typeGuards.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
RegexiCompareOp,
1313
NotCompareOp,
1414
NotEqualCompareOp,
15-
OrCompareOp, InqCompareOp, NinCompareOp, RuleFunctionsTable, RuleFunctionsParams, Primitive
15+
OrCompareOp, InqCompareOp, NinCompareOp, RuleFunctionsTable, RuleFunctionsParams, Primitive, MathOp
1616
} from '../types';
1717

1818
export const _isObject = (obj: unknown): boolean => {
@@ -103,3 +103,10 @@ export const isNinCompareOp = (op: ExtendedCompareOp<any, any, any>)
103103
: op is NinCompareOp<any, any, any> => {
104104
return Array.isArray((op as NinCompareOp<any, any, any>).nin);
105105
}
106+
107+
export type WithRef = {
108+
ref: string
109+
}
110+
111+
export const isWithRef = (x: unknown): x is WithRef => Boolean((x as WithRef).ref);
112+
export const isMathOp = (x: unknown): x is MathOp<any, any> => Boolean((x as MathOp<any, any>).op);

0 commit comments

Comments
 (0)