Skip to content

Commit e1c27ec

Browse files
authored
add custom function run opts (#28)
1 parent b3e6e1a commit e1c27ec

30 files changed

+2158
-1641
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
strategy:
2323
matrix:
2424
os: [ ubuntu-latest ]
25-
node-version: [ 6.x, 8.x, 10.x, 12.x, 14.x, 16.x, 18.x ]
25+
node-version: [ 14.x, 16.x, 18.x, 19.x ]
2626

2727
steps:
2828

@@ -92,7 +92,7 @@ jobs:
9292
strategy:
9393
matrix:
9494
os: [ ubuntu-latest ]
95-
node-version: [ 6.x ]
95+
node-version: [ 18.x ]
9696
needs: [ test ]
9797
if: github.event_name == 'release' && github.event.action == 'published'
9898
steps:

.run/Template Mocha.run.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<component name="ProjectRunConfigurationManager">
2+
<configuration default="true" type="mocha-javascript-test-runner">
3+
<node-interpreter>project</node-interpreter>
4+
<node-options />
5+
<working-directory>$PROJECT_DIR$</working-directory>
6+
<pass-parent-env>true</pass-parent-env>
7+
<ui />
8+
<extra-mocha-options>--config src/test/.mocharc.json</extra-mocha-options>
9+
<test-kind>DIRECTORY</test-kind>
10+
<test-directory />
11+
<recursive>false</recursive>
12+
<method v="2" />
13+
</configuration>
14+
</component>

README.md

Lines changed: 113 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,14 @@ interface IExampleContext {
4747

4848
type IExampleContextIgnore = Moment;
4949

50+
type IExampleCustomEvaluatorFuncRunOptions = {dryRun: boolean};
51+
5052
type IExampleFunctionTable = {
51-
countRange: ([min, max]: [min: number, max: number], ctx: { times: number | undefined }) => boolean;
53+
countRange: ([min, max]: [min: number, max: number], ctx: { times: number | undefined },
54+
runOpts: EvaluatorFuncRunOptions<IExampleCustomEvaluatorFuncRunOptions>) => Promise<boolean>;
5255
}
5356

54-
type IExampleExpression = Expression<IExampleContext, IExampleFunctionTable, IExampleContextIgnore>; // We pass Moment here to avoid TS exhaustion
57+
type IExampleExpression = Expression<IExampleContext, IExampleFunctionTable, IExampleContextIgnore, IExampleCustomEvaluatorFuncRunOptions>; // We pass Moment here to avoid TS exhaustion
5558

5659
const context: IExampleContext = {
5760
userId: 'a@b.com',
@@ -82,7 +85,8 @@ const validationContext: ValidationContext<IExampleContext, IExampleContextIgnor
8285
};
8386

8487
const functionsTable: IExampleFunctionTable = {
85-
countRange: async ([min, max]: [min: number, max: number], ctx: { times: number | undefined }, runOptions: EvaluatorFuncRunOptions): Promise<boolean> => {
88+
countRange: async ([min, max]: [min: number, max: number], ctx: { times: number | undefined },
89+
runOpts: EvaluatorFuncRunOptions<IExampleCustomEvaluatorFuncRunOptions>): Promise<boolean> => {
8690
return ctx.times === undefined ? false : ctx.times >= min && ctx.times < max;
8791
},
8892
};
@@ -97,7 +101,7 @@ const expression: IExampleExpression = {
97101
lte: {
98102
op: '+',
99103
lhs: {
100-
ref: 'nested.value4'
104+
ref: 'nested.value4',
101105
},
102106
rhs: 2,
103107
},
@@ -114,24 +118,28 @@ const expression: IExampleExpression = {
114118
{
115119
times: {
116120
lte: {
117-
ref: 'nested.value4'
118-
}
121+
ref: 'nested.value4',
122+
},
119123
},
120124
},
121125
],
122126
},
123127
],
124128
};
125129

126-
// Example usage 1
127-
const handler =
128-
new ExpressionHandler<IExampleContext, IExampleFunctionTable, IExampleContextIgnore>(expression, functionsTable);
129-
await handler.validate(validationContext); // Should not throw
130-
console.log(await handler.evaluate(context)); // true
131-
132-
// Example usage 2
133-
await validate<IExampleContext, IExampleFunctionTable, IExampleContextIgnore>(expression, validationContext, functionsTable); // Should not throw
134-
console.log(await evaluate<IExampleContext, IExampleFunctionTable, IExampleContextIgnore>(expression, context, functionsTable)); // true
130+
(async () => {
131+
// Example usage 1
132+
const handler =
133+
new ExpressionHandler<IExampleContext, IExampleFunctionTable, IExampleContextIgnore,
134+
IExampleCustomEvaluatorFuncRunOptions>(expression, functionsTable);
135+
await handler.validate(validationContext, {dryRun: false}); // Should not throw
136+
console.log(await handler.evaluate(context, {dryRun: true})); // true
137+
138+
// Example usage 2
139+
await validate<IExampleContext, IExampleFunctionTable, IExampleContextIgnore,
140+
IExampleCustomEvaluatorFuncRunOptions>(expression, validationContext, functionsTable, {dryRun: true}); // Should not throw
141+
console.log(await evaluate<IExampleContext, IExampleFunctionTable, IExampleContextIgnore, IExampleCustomEvaluatorFuncRunOptions>(expression, context, functionsTable, {dryRun: true})); // true
142+
})()
135143
```
136144

137145
### Expression
@@ -140,7 +148,7 @@ There are 4 types of operators you can use (evaluated in that order of precedenc
140148
- `and` - accepts a non-empty list of expressions
141149
- `or` - accepts a non-empty list of expressions
142150
- `not` - accepts another expressions
143-
- `<user defined funcs>` - accepts any type of argument and evaluated by the user defined functions, and the given context (can be async) and run options (i.e. validation).
151+
- `<user defined funcs>` - accepts any type of argument and evaluated by the user defined functions, and the given context (can be async) and run options (i.e. validation + custom defined value).
144152
- `<compare funcs>` - operates on one of the context properties and compares it to a given value.
145153
- `{property: {op: value}}`
146154
- available ops:
@@ -217,120 +225,133 @@ Example expressions, assuming we have the `user` and `maxCount` user defined fun
217225
*Please see tests and examples dir for more usages and examples (under /src)*
218226

219227
```typescript
220-
import {ValidationContext, validateRules, evaluateRules, RulesEngine, Rule, ResolvedConsequence, EngineRuleFuncRunOptions, EvaluatorFuncRunOptions} from 'json-expression-eval';
228+
import {ValidationContext, validateRules, evaluateRules, RulesEngine, Rule, ResolvedConsequence, EngineRuleFuncRunOptions} from 'json-expression-eval';
221229
import {Moment} from 'moment';
222230
import moment = require('moment');
223231

224232
interface IExampleContext {
225-
userId: string;
226-
times: number | undefined;
227-
date: Moment;
228-
nested: {
229-
value: number | null;
230-
nested2: {
231-
value2?: number;
232-
value3: boolean;
233+
userId: string;
234+
times: number | undefined;
235+
date: Moment;
236+
nested: {
237+
value: number | null;
238+
nested2: {
239+
value2?: number;
240+
value3: boolean;
241+
};
233242
};
234-
};
235243
}
236244

237245
type IExampleContextIgnore = Moment;
246+
247+
type IExampleCustomEngineRuleFuncRunOptions = {dryRun: boolean};
248+
238249
type IExamplePayload = number;
239250

240251
type IExampleFunctionTable = {
241-
countRange: ([min, max]: [min: number, max: number], ctx: { times: number | undefined }, runOptions: EvaluatorFuncRunOptions) => boolean;
252+
countRange: ([min, max]: [min: number, max: number], ctx: { times: number | undefined },
253+
runOpts: EngineRuleFuncRunOptions<IExampleCustomEngineRuleFuncRunOptions>) => boolean;
242254
}
243255

244256
type IExampleRuleFunctionTable = {
245-
userRule: (user: string, ctx: IExampleContext, runOptions: EngineRuleFuncRunOptions) => Promise<void | ResolvedConsequence<IExamplePayload>>;
257+
userRule: (user: string, ctx: IExampleContext,
258+
runOpts: EngineRuleFuncRunOptions<IExampleCustomEngineRuleFuncRunOptions>) =>
259+
Promise<void | ResolvedConsequence<IExamplePayload>>;
246260
}
247261

248262
type IExampleRule = Rule<IExamplePayload, IExampleRuleFunctionTable, IExampleContext,
249-
IExampleFunctionTable, IExampleContextIgnore>;
263+
IExampleFunctionTable, IExampleContextIgnore, IExampleCustomEngineRuleFuncRunOptions>;
250264

251265
const context: IExampleContext = {
252-
userId: 'a@b.com',
253-
times: 3,
254-
date: moment(),
255-
nested: {
256-
value: null,
257-
nested2: {
258-
value3: true,
266+
userId: 'a@b.com',
267+
times: 3,
268+
date: moment(),
269+
nested: {
270+
value: null,
271+
nested2: {
272+
value3: true,
273+
},
259274
},
260-
},
261275
};
262276

263277
// For validation we must provide a full example context
264278
const validationContext: ValidationContext<IExampleContext, IExampleContextIgnore> = {
265-
userId: 'a@b.com',
266-
times: 3,
267-
date: moment(),
268-
nested: {
269-
value: 5,
270-
nested2: {
271-
value2: 6,
272-
value3: true,
279+
userId: 'a@b.com',
280+
times: 3,
281+
date: moment(),
282+
nested: {
283+
value: 5,
284+
nested2: {
285+
value2: 6,
286+
value3: true,
287+
},
273288
},
274-
},
275289
};
276290

277291
const functionsTable: IExampleFunctionTable = {
278-
countRange: ([min, max]: [min: number, max: number], ctx: { times: number | undefined }, runOptions: EvaluatorFuncRunOptions): boolean => {
279-
return ctx.times === undefined ? false : ctx.times >= min && ctx.times < max;
280-
},
292+
countRange: ([min, max]: [min: number, max: number], ctx: { times: number | undefined },
293+
runOptions: EngineRuleFuncRunOptions<IExampleCustomEngineRuleFuncRunOptions>): boolean => {
294+
return ctx.times === undefined ? false : ctx.times >= min && ctx.times < max;
295+
},
281296
};
282297

283298
const ruleFunctionsTable: IExampleRuleFunctionTable = {
284-
userRule: async (user: string, ctx: IExampleContext, runOptions: EngineRuleFuncRunOptions): Promise<void | ResolvedConsequence<number>> => {
285-
if (ctx.userId === user) {
286-
return {
287-
message: `Username ${user} is not allowed`,
288-
custom: 543,
289-
}
290-
}
291-
},
299+
userRule: async (user: string, ctx: IExampleContext,
300+
runOptions: EngineRuleFuncRunOptions<IExampleCustomEngineRuleFuncRunOptions>)
301+
: Promise<void | ResolvedConsequence<number>> => {
302+
if (ctx.userId === user) {
303+
return {
304+
message: `Username ${user} is not allowed`,
305+
custom: 543,
306+
}
307+
}
308+
},
292309
};
293310

294311
const rules: IExampleRule[] = [
295-
{
296-
condition: {
297-
or: [
298-
{
299-
userId: 'a@b.com',
312+
{
313+
condition: {
314+
or: [
315+
{
316+
userId: 'a@b.com',
317+
},
318+
{
319+
and: [
320+
{
321+
countRange: [2, 6],
322+
},
323+
{
324+
'nested.nested2.value3': true,
325+
},
326+
],
327+
},
328+
],
300329
},
301-
{
302-
and: [
303-
{
304-
countRange: [2, 6],
305-
},
306-
{
307-
'nested.nested2.value3': true,
308-
},
309-
],
330+
consequence: {
331+
message: ['user', {
332+
ref: 'userId',
333+
}, 'should not equal a@b.com'],
334+
custom: 579,
310335
},
311-
],
312336
},
313-
consequence: {
314-
message: ['user', {
315-
ref: 'userId',
316-
}, 'should not equal a@b.com'],
317-
custom: 579,
337+
{
338+
userRule: 'b@c.com',
318339
},
319-
},
320-
{
321-
userRule: 'b@c.com',
322-
},
323340
];
324341

325-
// Example usage 1
326-
const engine = new RulesEngine<IExamplePayload, IExampleContext, IExampleRuleFunctionTable,
327-
IExampleFunctionTable, IExampleContextIgnore>(functionsTable, ruleFunctionsTable);
328-
await engine.validate(rules, validationContext); // Should not throw
329-
console.log(JSON.stringify(await engine.evaluateAll(rules, context))); // [{"message":"user a@b.com should not equal a@b.com","custom":579}]
330-
331-
// Example usage 2
332-
await validateRules<IExamplePayload, IExampleContext, IExampleRuleFunctionTable,
333-
IExampleFunctionTable, IExampleContextIgnore>(rules, validationContext, functionsTable, ruleFunctionsTable); // Should not throw
334-
console.log(JSON.stringify(await evaluateRules<IExamplePayload, IExampleContext, IExampleRuleFunctionTable,
335-
IExampleFunctionTable, IExampleContextIgnore>(rules, context, functionsTable, ruleFunctionsTable, false))); // [{"message":"user a@b.com should not equal a@b.com","custom":579}]
342+
(async () => {
343+
// Example usage 1
344+
const engine = new RulesEngine<IExamplePayload, IExampleContext, IExampleRuleFunctionTable,
345+
IExampleFunctionTable, IExampleContextIgnore, IExampleCustomEngineRuleFuncRunOptions>(
346+
functionsTable, ruleFunctionsTable);
347+
await engine.validate(rules, validationContext, {dryRun: false}); // Should not throw
348+
console.log(JSON.stringify(await engine.evaluateAll(rules, context, {dryRun: false}))); // [{"message":"user a@b.com should not equal a@b.com","custom":579}]
349+
350+
// Example usage 2
351+
await validateRules<IExamplePayload, IExampleContext, IExampleRuleFunctionTable,
352+
IExampleFunctionTable, IExampleContextIgnore, IExampleCustomEngineRuleFuncRunOptions>(
353+
rules, validationContext, functionsTable, ruleFunctionsTable, {dryRun: false}); // Should not throw
354+
console.log(JSON.stringify(await evaluateRules<IExamplePayload, IExampleContext, IExampleRuleFunctionTable,
355+
IExampleFunctionTable, IExampleContextIgnore, IExampleCustomEngineRuleFuncRunOptions>(rules, context, functionsTable, ruleFunctionsTable, false, {dryRun: false}))); // [{"message":"user a@b.com should not equal a@b.com","custom":579}]
356+
})();
336357
```

package.json

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
{
22
"name": "json-expression-eval",
3-
"version": "6.0.0",
3+
"version": "7.0.0",
44
"description": "json serializable rule engine / boolean expression evaluator",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",
77
"engines": {
8-
"node": "^6 || ^8 || ^10 || ^12 || ^14 || ^16 || ^18"
8+
"node": "^14 || ^16 || ^18 || ^19"
99
},
1010
"scripts": {
1111
"test": "yarn lint && yarn test:tsd && yarn test:cover",
1212
"test:tsd": "tsd",
13-
"test:unit": "mocha --opts src/test/mocha.opts",
13+
"test:unit": "mocha --config src/test/.mocharc.json",
1414
"build": "yarn lint && yarn compile",
1515
"compile": "./node_modules/.bin/tsc",
16-
"test:cover": "nyc --reporter=lcov --reporter=text-summary mocha --opts src/test/mocha.opts",
16+
"test:cover": "nyc --reporter=lcov --reporter=text-summary mocha --config src/test/.mocharc.json",
1717
"lint": "tslint -c tslint.json 'src/**/*.ts' 'test/**/*.ts'",
1818
"ci": "yarn lint && yarn compile && yarn test:tsd && yarn test:cover"
1919
},
@@ -48,27 +48,24 @@
4848
"directory": "src/test/types"
4949
},
5050
"devDependencies": {
51-
"@istanbuljs/nyc-config-typescript": "0.1.3",
52-
"@types/chai": "^4.2.16",
51+
"@istanbuljs/nyc-config-typescript": "1.0.2",
52+
"@types/chai": "^4.3.4",
5353
"@types/chai-as-promised": "^7.1.5",
54-
"@types/mocha": "^8.2.2",
55-
"@types/node": "^14.14.37",
56-
"@types/underscore": "^1.11.1",
57-
"chai": "^4.3.4",
54+
"@types/mocha": "^10.0.1",
55+
"@types/node": "^18.14.2",
56+
"@types/underscore": "^1.11.4",
57+
"chai": "^4.3.7",
5858
"chai-as-promised": "^7.1.1",
59-
"mocha": "^6.2.3",
60-
"moment": "^2.29.1",
61-
"nyc": "^14.1.1",
59+
"mocha": "^10.2.0",
60+
"moment": "^2.29.4",
61+
"nyc": "^15.1.0",
6262
"source-map-support": "^0.5.21",
63-
"ts-node": "^8.10.2",
64-
"tsd": "^0.21.0",
63+
"ts-node": "^10.9.1",
64+
"tsd": "^0.25.0",
6565
"tslint": "^6.1.3",
66-
"typescript": "4.6.4"
66+
"typescript": "^4.9.5"
6767
},
6868
"dependencies": {
6969
"ts-toolbelt": "^9.6.0"
70-
},
71-
"resolutions": {
72-
"merge2": "1.3.0"
7370
}
7471
}

0 commit comments

Comments
 (0)