Skip to content

Commit 476edef

Browse files
authored
breaking change - make the api async (#23)
1 parent 5ad068e commit 476edef

File tree

17 files changed

+1031
-992
lines changed

17 files changed

+1031
-992
lines changed

README.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ const validationContext: ValidationContext<IExampleContext, IExampleContextIgnor
8282
};
8383

8484
const functionsTable: IExampleFunctionTable = {
85-
countRange: ([min, max]: [min: number, max: number], ctx: { times: number | undefined }): boolean => {
85+
countRange: async ([min, max]: [min: number, max: number], ctx: { times: number | undefined }): Promise<boolean> => {
8686
return ctx.times === undefined ? false : ctx.times >= min && ctx.times < max;
8787
},
8888
};
@@ -115,12 +115,12 @@ const expression: IExampleExpression = {
115115
// Example usage 1
116116
const handler =
117117
new ExpressionHandler<IExampleContext, IExampleFunctionTable, IExampleContextIgnore>(expression, functionsTable);
118-
handler.validate(validationContext); // Should not throw
119-
console.log(handler.evaluate(context)); // true
118+
await handler.validate(validationContext); // Should not throw
119+
console.log(await handler.evaluate(context)); // true
120120

121121
// Example usage 2
122-
validate<IExampleContext, IExampleFunctionTable, IExampleContextIgnore>(expression, validationContext, functionsTable); // Should not throw
123-
console.log(evaluate<IExampleContext, IExampleFunctionTable, IExampleContextIgnore>(expression, context, functionsTable)); // true
122+
await validate<IExampleContext, IExampleFunctionTable, IExampleContextIgnore>(expression, validationContext, functionsTable); // Should not throw
123+
console.log(await evaluate<IExampleContext, IExampleFunctionTable, IExampleContextIgnore>(expression, context, functionsTable)); // true
124124
```
125125

126126
### Expression
@@ -129,7 +129,7 @@ There are 4 types of operators you can use (evaluated in that order of precedenc
129129
- `and` - accepts a non-empty list of expressions
130130
- `or` - accepts a non-empty list of expressions
131131
- `not` - accepts another expressions
132-
- `<user defined funcs>` - accepts any type of argument and evaluated by the user defined functions and the given context.
132+
- `<user defined funcs>` - accepts any type of argument and evaluated by the user defined functions and the given context. Can be async.
133133
- `<compare funcs>` - operates on one of the context properties and compares it to a given value.
134134
- `{property: {op: value}}`
135135
- available ops:
@@ -208,7 +208,7 @@ type IExampleFunctionTable = {
208208
}
209209

210210
type IExampleRuleFunctionTable = {
211-
userRule: (user: string, ctx: IExampleContext) => void | ResolvedConsequence<IExamplePayload>;
211+
userRule: (user: string, ctx: IExampleContext) => Promise<void | ResolvedConsequence<IExamplePayload>>;
212212
}
213213

214214
type IExampleRule = Rule<IExamplePayload, IExampleRuleFunctionTable, IExampleContext,
@@ -247,7 +247,7 @@ const functionsTable: IExampleFunctionTable = {
247247
};
248248

249249
const ruleFunctionsTable: IExampleRuleFunctionTable = {
250-
userRule: (user: string, ctx: IExampleContext): void | ResolvedConsequence<number> => {
250+
userRule: async (user: string, ctx: IExampleContext): Promise<void | ResolvedConsequence<number>> => {
251251
if (ctx.userId === user) {
252252
return {
253253
message: `Username ${user} is not allowed`,
@@ -291,12 +291,12 @@ const rules: IExampleRule[] = [
291291
// Example usage 1
292292
const engine = new RulesEngine<IExamplePayload, IExampleContext, IExampleRuleFunctionTable,
293293
IExampleFunctionTable, IExampleContextIgnore>(functionsTable, ruleFunctionsTable);
294-
engine.validate(rules, validationContext); // Should not throw
295-
console.log(JSON.stringify(engine.evaluateAll(rules, context))); // [{"message":"user a@b.com should not equal a@b.com","custom":579}]
294+
await engine.validate(rules, validationContext); // Should not throw
295+
console.log(JSON.stringify(await engine.evaluateAll(rules, context))); // [{"message":"user a@b.com should not equal a@b.com","custom":579}]
296296

297297
// Example usage 2
298-
validateRules<IExamplePayload, IExampleContext, IExampleRuleFunctionTable,
298+
await validateRules<IExamplePayload, IExampleContext, IExampleRuleFunctionTable,
299299
IExampleFunctionTable, IExampleContextIgnore>(rules, validationContext, functionsTable, ruleFunctionsTable); // Should not throw
300-
console.log(JSON.stringify(evaluateRules<IExamplePayload, IExampleContext, IExampleRuleFunctionTable,
300+
console.log(JSON.stringify(await evaluateRules<IExamplePayload, IExampleContext, IExampleRuleFunctionTable,
301301
IExampleFunctionTable, IExampleContextIgnore>(rules, context, functionsTable, ruleFunctionsTable, false))); // [{"message":"user a@b.com should not equal a@b.com","custom":579}]
302302
```

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "json-expression-eval",
3-
"version": "4.3.0",
3+
"version": "5.0.0",
44
"description": "json serializable rule engine / boolean expression evaluator",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",
@@ -49,10 +49,12 @@
4949
"devDependencies": {
5050
"@istanbuljs/nyc-config-typescript": "0.1.3",
5151
"@types/chai": "^4.2.16",
52+
"@types/chai-as-promised": "^7.1.5",
5253
"@types/mocha": "^8.2.2",
5354
"@types/node": "^14.14.37",
5455
"@types/underscore": "^1.11.1",
5556
"chai": "^4.3.4",
57+
"chai-as-promised": "^7.1.1",
5658
"mocha": "^6.2.3",
5759
"moment": "^2.29.1",
5860
"nyc": "^14.1.1",

src/examples/engine/example.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ const context: ExpressionContext = {
1313
},
1414
};
1515

16-
const run = (_rules: MyRule[], _context: ExpressionContext) => {
17-
const result = engine.evaluateAll(_rules, _context);
16+
const run = async (_rules: MyRule[], _context: ExpressionContext) => {
17+
const result = await engine.evaluateAll(_rules, _context);
1818
console.log(`Evaluating rules ${JSON.stringify(_rules)} using context ${JSON.stringify(_context)}`);
1919
console.log(`Result: ${JSON.stringify(result)}\n\n`);
2020
};
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
export const counterFunc = (maxCount: number, context: { times: number }): boolean => {
1+
export const counterFunc = async (maxCount: number, context: { times: number }): Promise<boolean> => {
22
return context.times < maxCount;
33
};
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import {ResolvedConsequence} from '../../../../types';
22

3-
export const userRule = (user: string, context: { userId: string }): void | ResolvedConsequence<number> => {
3+
export const userRule = async (user: string, context: { userId: string })
4+
: Promise<void | ResolvedConsequence<number>> => {
45
if (context.userId === user) {
56
return {
67
message: `Username ${user} is not allowed`,
78
custom: 543,
89
}
910
}
10-
};
11+
};

src/examples/evaluator/example.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ const context: ExpressionContext = {
1515
},
1616
};
1717

18-
const run = (expr: Expression<ExpressionContext, ExpressionFunction, Moment>, ctx: ExpressionContext) => {
19-
const result = getEvaluator(expression).evaluate(ctx);
18+
const run = async (expr: Expression<ExpressionContext, ExpressionFunction, Moment>, ctx: ExpressionContext) => {
19+
const result = await getEvaluator(expression).evaluate(ctx);
2020
console.log(`Evaluating expression ${JSON.stringify(expr)} using context ${JSON.stringify(ctx)}`);
2121
console.log(`Result: ${result}`);
2222
};
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
export const userFunc = (user: string, context: { userId: string }): boolean => {
1+
export const userFunc = async (user: string, context: { userId: string }): Promise<boolean> => {
22
return context.userId === user;
33
};

src/lib/engine.ts

Lines changed: 54 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,72 +4,74 @@ import {objectKeys} from './helpers';
44
import {isRuleFunction} from './typeGuards';
55
import {evaluateEngineConsequence} from './engineConsequenceEvaluator';
66

7-
function run<ConsequencePayload, C extends Context,
8-
RF extends RuleFunctionsTable<C, ConsequencePayload>, F extends FunctionsTable<C>, Ignore = never>
7+
async function run<ConsequencePayload, C extends Context,
8+
RF extends RuleFunctionsTable<C, ConsequencePayload>, F extends FunctionsTable<C>, Ignore = never>
99
(rules: Rule<ConsequencePayload, RF, C, F, Ignore>[], context: C, functionsTable: F, ruleFunctionsTable: RF,
1010
haltOnFirstMatch: boolean, validation: false)
11-
: void | ResolvedConsequence<ConsequencePayload>[]
12-
function run<ConsequencePayload, C extends Context,
13-
RF extends RuleFunctionsTable<C, ConsequencePayload>, F extends FunctionsTable<C>, Ignore = never>
11+
: Promise<void | ResolvedConsequence<ConsequencePayload>[]>
12+
async function run<ConsequencePayload, C extends Context,
13+
RF extends RuleFunctionsTable<C, ConsequencePayload>, F extends FunctionsTable<C>, Ignore = never>
1414
(rules: Rule<ConsequencePayload, RF, C, F, Ignore>[], context: ValidationContext<C, Ignore>,
1515
functionsTable: F, ruleFunctionsTable: RF,
1616
haltOnFirstMatch: boolean, validation: true)
17-
: void | ResolvedConsequence<ConsequencePayload>[]
18-
function run<ConsequencePayload, C extends Context,
19-
RF extends RuleFunctionsTable<C, ConsequencePayload>, F extends FunctionsTable<C>, Ignore = never>
17+
: Promise<void | ResolvedConsequence<ConsequencePayload>[]>
18+
async function run<ConsequencePayload, C extends Context,
19+
RF extends RuleFunctionsTable<C, ConsequencePayload>, F extends FunctionsTable<C>, Ignore = never>
2020
(rules: Rule<ConsequencePayload, RF, C, F, Ignore>[], context: C | ValidationContext<C, Ignore>, functionsTable: F,
2121
ruleFunctionsTable: RF, haltOnFirstMatch: boolean, validation: boolean)
22-
: void | ResolvedConsequence<ConsequencePayload>[] {
23-
const errors: ResolvedConsequence<ConsequencePayload>[] = [];
24-
for (const rule of rules) {
25-
const keys = objectKeys(rule);
26-
const key = keys[0];
27-
if (keys.length === 1 && key && isRuleFunction<ConsequencePayload, C, RF>(rule, ruleFunctionsTable, key)) {
28-
const consequence = ruleFunctionsTable[key](rule[key], context as C);
29-
if (consequence) {
30-
errors.push(consequence);
31-
if (haltOnFirstMatch && !validation) {
32-
return errors;
22+
: Promise<void | ResolvedConsequence<ConsequencePayload>[]> {
23+
const errors: ResolvedConsequence<ConsequencePayload>[] = [];
24+
for (const rule of rules) {
25+
const keys = objectKeys(rule);
26+
const key = keys[0];
27+
if (keys.length === 1 && key && isRuleFunction<ConsequencePayload, C, RF>(rule, ruleFunctionsTable, key)) {
28+
const consequence = await ruleFunctionsTable[key](rule[key], context as C);
29+
if (consequence) {
30+
errors.push(consequence);
31+
if (haltOnFirstMatch && !validation) {
32+
return errors;
33+
}
34+
}
35+
} else {
36+
if (!rule.condition) {
37+
throw new Error(`Missing condition for rule`);
38+
}
39+
if (!rule.consequence) {
40+
throw new Error(`Missing consequence for rule`);
41+
}
42+
if (validation) {
43+
await validate<C, F, Ignore>(rule.condition, context as ValidationContext<C, Ignore>, functionsTable);
44+
await evaluateEngineConsequence<ConsequencePayload, C, Ignore>(context as C, rule.consequence);
45+
} else {
46+
const ruleApplies = await evaluate<C, F, Ignore>(rule.condition, context as C, functionsTable);
47+
if (ruleApplies) {
48+
const consequence =
49+
await evaluateEngineConsequence<ConsequencePayload, C, Ignore>(context as C, rule.consequence);
50+
errors.push(consequence);
51+
if (haltOnFirstMatch) {
52+
return errors;
53+
}
54+
}
55+
}
3356
}
34-
}
35-
} else {
36-
if (!rule.condition) {
37-
throw new Error(`Missing condition for rule`);
38-
}
39-
if (!rule.consequence) {
40-
throw new Error(`Missing consequence for rule`);
41-
}
42-
if (validation) {
43-
validate<C, F, Ignore>(rule.condition, context as ValidationContext<C, Ignore>, functionsTable);
44-
evaluateEngineConsequence<ConsequencePayload, C, Ignore>(context as C, rule.consequence);
45-
} else {
46-
const ruleApplies = evaluate<C, F, Ignore>(rule.condition, context as C, functionsTable);
47-
if (ruleApplies) {
48-
const consequence = evaluateEngineConsequence<ConsequencePayload, C, Ignore>(context as C, rule.consequence);
49-
errors.push(consequence);
50-
if (haltOnFirstMatch) {
51-
return errors;
52-
}
53-
}
54-
}
5557
}
56-
}
57-
return errors.length ? errors : undefined;
58+
return errors.length ? errors : undefined;
5859
}
5960

60-
export const evaluateRules = <ConsequencePayload, C extends Context,
61-
RF extends RuleFunctionsTable<C, ConsequencePayload>, F extends FunctionsTable<C>, Ignore = never>
61+
export const evaluateRules = async <ConsequencePayload, C extends Context,
62+
RF extends RuleFunctionsTable<C, ConsequencePayload>, F extends FunctionsTable<C>, Ignore = never>
6263
(rules: Rule<ConsequencePayload, RF, C, F, Ignore>[], context: C, functionsTable: F, ruleFunctionsTable: RF,
6364
haltOnFirstMatch: boolean)
64-
: void | ResolvedConsequence<ConsequencePayload>[] => {
65-
return run<ConsequencePayload, C, RF, F, Ignore>(
66-
rules, context, functionsTable, ruleFunctionsTable, haltOnFirstMatch, false);
65+
: Promise<void | ResolvedConsequence<ConsequencePayload>[]> => {
66+
return run<ConsequencePayload, C, RF, F, Ignore>(
67+
rules, context, functionsTable, ruleFunctionsTable, haltOnFirstMatch, false);
6768
}
6869

69-
export const validateRules = <ConsequencePayload, C extends Context,
70-
RF extends RuleFunctionsTable<C, ConsequencePayload>, F extends FunctionsTable<C>, Ignore = never>
70+
export const validateRules = async <ConsequencePayload, C extends Context,
71+
RF extends RuleFunctionsTable<C, ConsequencePayload>, F extends FunctionsTable<C>, Ignore = never>
7172
(rules: Rule<ConsequencePayload, RF, C, F, Ignore>[], validationContext: ValidationContext<C, Ignore>,
7273
functionsTable: F, ruleFunctionsTable: RF)
73-
: void => {
74-
run<ConsequencePayload, C, RF, F, Ignore>(rules, validationContext, functionsTable, ruleFunctionsTable, false, true);
75-
}
74+
: Promise<void> => {
75+
await run<ConsequencePayload, C, RF, F, Ignore>(rules, validationContext, functionsTable,
76+
ruleFunctionsTable, false, true);
77+
}

src/lib/engineConsequenceEvaluator.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import {ResolvedConsequence, Context, RuleConsequence, RuleConsequenceMessagePart} from '../types';
22
import {getFromPath} from './helpers';
33

4-
export const evaluateEngineConsequence = <ConsequencePayload, C extends Context, Ignore = never>
4+
export const evaluateEngineConsequence = async <ConsequencePayload, C extends Context, Ignore = never>
55
(context: C, consequence: RuleConsequence<ConsequencePayload, C, Ignore>)
6-
: ResolvedConsequence<ConsequencePayload> => {
6+
: Promise<ResolvedConsequence<ConsequencePayload>> => {
77
let messageParts: RuleConsequenceMessagePart<C, Ignore>[];
88
if (typeof consequence.message === 'string') {
99
messageParts = [consequence.message];
@@ -23,4 +23,4 @@ export const evaluateEngineConsequence = <ConsequencePayload, C extends Context,
2323
}).join(' '),
2424
custom: consequence.custom,
2525
}
26-
}
26+
}

src/lib/evaluator.ts

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,10 @@ const extractValueOrRef = <C extends Context>(context: C, validation: boolean, v
5252
}
5353
}
5454

55-
function evaluateCompareOp<C extends Context>(expressionValue: ExtendedCompareOp<any, any, any>, expressionKey: string,
56-
contextValue: any, context: C, validation: boolean)
57-
: boolean {
55+
async function evaluateCompareOp<C extends Context>(expressionValue: ExtendedCompareOp<any, any, any>,
56+
expressionKey: string, contextValue: any,
57+
context: C, validation: boolean)
58+
: Promise<boolean> {
5859
if (!_isObject(expressionValue)) {
5960
return contextValue === expressionValue;
6061
}
@@ -121,36 +122,36 @@ function evaluateCompareOp<C extends Context>(expressionValue: ExtendedCompareOp
121122
}
122123
}
123124

124-
function handleAndOp<C extends Context, F extends FunctionsTable<C>, Ignore>
125-
(andExpression: Expression<C, F, Ignore>[], context: C, functionsTable: F, validation: boolean): boolean {
125+
async function handleAndOp<C extends Context, F extends FunctionsTable<C>, Ignore>
126+
(andExpression: Expression<C, F, Ignore>[], context: C, functionsTable: F, validation: boolean): Promise<boolean> {
126127
if (andExpression.length === 0) {
127128
throw new Error('Invalid expression - and operator must have at least one expression');
128129
}
129130
for (const currExpression of andExpression) {
130-
const result = run<C, F, Ignore>(currExpression, context, functionsTable, validation);
131+
const result = await run<C, F, Ignore>(currExpression, context, functionsTable, validation);
131132
if (!validation && !result) {
132133
return false;
133134
}
134135
}
135136
return true;
136137
}
137138

138-
function handleOrOp<C extends Context, F extends FunctionsTable<C>, Ignore>
139-
(orExpression: Expression<C, F, Ignore>[], context: C, functionsTable: F, validation: boolean): boolean {
139+
async function handleOrOp<C extends Context, F extends FunctionsTable<C>, Ignore>
140+
(orExpression: Expression<C, F, Ignore>[], context: C, functionsTable: F, validation: boolean): Promise<boolean> {
140141
if (orExpression.length === 0) {
141142
throw new Error('Invalid expression - or operator must have at least one expression');
142143
}
143144
for (const currExpression of orExpression) {
144-
const result = run<C, F, Ignore>(currExpression, context, functionsTable, validation);
145+
const result = await run<C, F, Ignore>(currExpression, context, functionsTable, validation);
145146
if (!validation && result) {
146147
return true;
147148
}
148149
}
149150
return false;
150151
}
151152

152-
function run<C extends Context, F extends FunctionsTable<C>, Ignore>
153-
(expression: Expression<C, F, Ignore>, context: C, functionsTable: F, validation: boolean): boolean {
153+
async function run<C extends Context, F extends FunctionsTable<C>, Ignore>
154+
(expression: Expression<C, F, Ignore>, context: C, functionsTable: F, validation: boolean): Promise<boolean> {
154155
const expressionKeys = objectKeys(expression);
155156
if (expressionKeys.length !== 1) {
156157
throw new Error('Invalid expression - too may keys');
@@ -161,9 +162,9 @@ function run<C extends Context, F extends FunctionsTable<C>, Ignore>
161162
} else if (isOrCompareOp<C, F, Ignore>(expression)) {
162163
return handleOrOp<C, F, Ignore>(expression.or, context, functionsTable, validation);
163164
} else if (isNotCompareOp<C, F, Ignore>(expression)) {
164-
return !run<C, F, Ignore>(expression.not, context, functionsTable, validation);
165+
return !(await run<C, F, Ignore>(expression.not, context, functionsTable, validation));
165166
} else if (isFunctionCompareOp<C, F, Ignore>(expression, functionsTable, expressionKey)) {
166-
return validation ? true : functionsTable[expressionKey](expression[expressionKey], context);
167+
return validation ? true : await functionsTable[expressionKey](expression[expressionKey], context);
167168
} else {
168169
const {value: contextValue, exists} = getFromPath(context, expressionKey);
169170
if (validation && !exists) {
@@ -177,13 +178,14 @@ function run<C extends Context, F extends FunctionsTable<C>, Ignore>
177178
}
178179
}
179180

180-
export const evaluate = <C extends Context, F extends FunctionsTable<C>, Ignore = never>
181-
(expression: Expression<C, F, Ignore>, context: C, functionsTable: F): boolean => {
181+
export const evaluate = async <C extends Context, F extends FunctionsTable<C>, Ignore = never>
182+
(expression: Expression<C, F, Ignore>, context: C, functionsTable: F): Promise<boolean> => {
182183
return run<C, F, Ignore>(expression, context, functionsTable, false);
183184
};
184185

185186
// Throws in case of validation error. Does not run functions or compare fields
186-
export const validate = <C extends Context, F extends FunctionsTable<C>, Ignore = never>
187-
(expression: Expression<C, F, Ignore>, validationContext: ValidationContext<C, Ignore>, functionsTable: F): void => {
188-
run<C, F, Ignore>(expression, validationContext as C, functionsTable, true);
187+
export const validate = async <C extends Context, F extends FunctionsTable<C>, Ignore = never>
188+
(expression: Expression<C, F, Ignore>, validationContext: ValidationContext<C, Ignore>, functionsTable: F)
189+
: Promise<void> => {
190+
await run<C, F, Ignore>(expression, validationContext as C, functionsTable, true);
189191
};

0 commit comments

Comments
 (0)