Skip to content

Commit

Permalink
breaking change - make the api async (#23)
Browse files Browse the repository at this point in the history
  • Loading branch information
regevbr authored Aug 29, 2022
1 parent 5ad068e commit 476edef
Show file tree
Hide file tree
Showing 17 changed files with 1,031 additions and 992 deletions.
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ const validationContext: ValidationContext<IExampleContext, IExampleContextIgnor
};

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

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

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

type IExampleRuleFunctionTable = {
userRule: (user: string, ctx: IExampleContext) => void | ResolvedConsequence<IExamplePayload>;
userRule: (user: string, ctx: IExampleContext) => Promise<void | ResolvedConsequence<IExamplePayload>>;
}

type IExampleRule = Rule<IExamplePayload, IExampleRuleFunctionTable, IExampleContext,
Expand Down Expand Up @@ -247,7 +247,7 @@ const functionsTable: IExampleFunctionTable = {
};

const ruleFunctionsTable: IExampleRuleFunctionTable = {
userRule: (user: string, ctx: IExampleContext): void | ResolvedConsequence<number> => {
userRule: async (user: string, ctx: IExampleContext): Promise<void | ResolvedConsequence<number>> => {
if (ctx.userId === user) {
return {
message: `Username ${user} is not allowed`,
Expand Down Expand Up @@ -291,12 +291,12 @@ const rules: IExampleRule[] = [
// Example usage 1
const engine = new RulesEngine<IExamplePayload, IExampleContext, IExampleRuleFunctionTable,
IExampleFunctionTable, IExampleContextIgnore>(functionsTable, ruleFunctionsTable);
engine.validate(rules, validationContext); // Should not throw
console.log(JSON.stringify(engine.evaluateAll(rules, context))); // [{"message":"user [email protected] should not equal [email protected]","custom":579}]
await engine.validate(rules, validationContext); // Should not throw
console.log(JSON.stringify(await engine.evaluateAll(rules, context))); // [{"message":"user [email protected] should not equal [email protected]","custom":579}]

// Example usage 2
validateRules<IExamplePayload, IExampleContext, IExampleRuleFunctionTable,
await validateRules<IExamplePayload, IExampleContext, IExampleRuleFunctionTable,
IExampleFunctionTable, IExampleContextIgnore>(rules, validationContext, functionsTable, ruleFunctionsTable); // Should not throw
console.log(JSON.stringify(evaluateRules<IExamplePayload, IExampleContext, IExampleRuleFunctionTable,
console.log(JSON.stringify(await evaluateRules<IExamplePayload, IExampleContext, IExampleRuleFunctionTable,
IExampleFunctionTable, IExampleContextIgnore>(rules, context, functionsTable, ruleFunctionsTable, false))); // [{"message":"user [email protected] should not equal [email protected]","custom":579}]
```
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "json-expression-eval",
"version": "4.3.0",
"version": "5.0.0",
"description": "json serializable rule engine / boolean expression evaluator",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down Expand Up @@ -49,10 +49,12 @@
"devDependencies": {
"@istanbuljs/nyc-config-typescript": "0.1.3",
"@types/chai": "^4.2.16",
"@types/chai-as-promised": "^7.1.5",
"@types/mocha": "^8.2.2",
"@types/node": "^14.14.37",
"@types/underscore": "^1.11.1",
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"mocha": "^6.2.3",
"moment": "^2.29.1",
"nyc": "^14.1.1",
Expand Down
4 changes: 2 additions & 2 deletions src/examples/engine/example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ const context: ExpressionContext = {
},
};

const run = (_rules: MyRule[], _context: ExpressionContext) => {
const result = engine.evaluateAll(_rules, _context);
const run = async (_rules: MyRule[], _context: ExpressionContext) => {
const result = await engine.evaluateAll(_rules, _context);
console.log(`Evaluating rules ${JSON.stringify(_rules)} using context ${JSON.stringify(_context)}`);
console.log(`Result: ${JSON.stringify(result)}\n\n`);
};
Expand Down
2 changes: 1 addition & 1 deletion src/examples/engine/lib/functions/counterFunc.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export const counterFunc = (maxCount: number, context: { times: number }): boolean => {
export const counterFunc = async (maxCount: number, context: { times: number }): Promise<boolean> => {
return context.times < maxCount;
};
5 changes: 3 additions & 2 deletions src/examples/engine/lib/rules/userRule.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {ResolvedConsequence} from '../../../../types';

export const userRule = (user: string, context: { userId: string }): void | ResolvedConsequence<number> => {
export const userRule = async (user: string, context: { userId: string })
: Promise<void | ResolvedConsequence<number>> => {
if (context.userId === user) {
return {
message: `Username ${user} is not allowed`,
custom: 543,
}
}
};
};
4 changes: 2 additions & 2 deletions src/examples/evaluator/example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ const context: ExpressionContext = {
},
};

const run = (expr: Expression<ExpressionContext, ExpressionFunction, Moment>, ctx: ExpressionContext) => {
const result = getEvaluator(expression).evaluate(ctx);
const run = async (expr: Expression<ExpressionContext, ExpressionFunction, Moment>, ctx: ExpressionContext) => {
const result = await getEvaluator(expression).evaluate(ctx);
console.log(`Evaluating expression ${JSON.stringify(expr)} using context ${JSON.stringify(ctx)}`);
console.log(`Result: ${result}`);
};
Expand Down
2 changes: 1 addition & 1 deletion src/examples/evaluator/lib/functions/userFunc.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export const userFunc = (user: string, context: { userId: string }): boolean => {
export const userFunc = async (user: string, context: { userId: string }): Promise<boolean> => {
return context.userId === user;
};
106 changes: 54 additions & 52 deletions src/lib/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,72 +4,74 @@ import {objectKeys} from './helpers';
import {isRuleFunction} from './typeGuards';
import {evaluateEngineConsequence} from './engineConsequenceEvaluator';

function run<ConsequencePayload, C extends Context,
RF extends RuleFunctionsTable<C, ConsequencePayload>, F extends FunctionsTable<C>, Ignore = never>
async function run<ConsequencePayload, C extends Context,
RF extends RuleFunctionsTable<C, ConsequencePayload>, F extends FunctionsTable<C>, Ignore = never>
(rules: Rule<ConsequencePayload, RF, C, F, Ignore>[], context: C, functionsTable: F, ruleFunctionsTable: RF,
haltOnFirstMatch: boolean, validation: false)
: void | ResolvedConsequence<ConsequencePayload>[]
function run<ConsequencePayload, C extends Context,
RF extends RuleFunctionsTable<C, ConsequencePayload>, F extends FunctionsTable<C>, Ignore = never>
: Promise<void | ResolvedConsequence<ConsequencePayload>[]>
async function run<ConsequencePayload, C extends Context,
RF extends RuleFunctionsTable<C, ConsequencePayload>, F extends FunctionsTable<C>, Ignore = never>
(rules: Rule<ConsequencePayload, RF, C, F, Ignore>[], context: ValidationContext<C, Ignore>,
functionsTable: F, ruleFunctionsTable: RF,
haltOnFirstMatch: boolean, validation: true)
: void | ResolvedConsequence<ConsequencePayload>[]
function run<ConsequencePayload, C extends Context,
RF extends RuleFunctionsTable<C, ConsequencePayload>, F extends FunctionsTable<C>, Ignore = never>
: Promise<void | ResolvedConsequence<ConsequencePayload>[]>
async function run<ConsequencePayload, C extends Context,
RF extends RuleFunctionsTable<C, ConsequencePayload>, F extends FunctionsTable<C>, Ignore = never>
(rules: Rule<ConsequencePayload, RF, C, F, Ignore>[], context: C | ValidationContext<C, Ignore>, functionsTable: F,
ruleFunctionsTable: RF, haltOnFirstMatch: boolean, validation: boolean)
: void | ResolvedConsequence<ConsequencePayload>[] {
const errors: ResolvedConsequence<ConsequencePayload>[] = [];
for (const rule of rules) {
const keys = objectKeys(rule);
const key = keys[0];
if (keys.length === 1 && key && isRuleFunction<ConsequencePayload, C, RF>(rule, ruleFunctionsTable, key)) {
const consequence = ruleFunctionsTable[key](rule[key], context as C);
if (consequence) {
errors.push(consequence);
if (haltOnFirstMatch && !validation) {
return errors;
: Promise<void | ResolvedConsequence<ConsequencePayload>[]> {
const errors: ResolvedConsequence<ConsequencePayload>[] = [];
for (const rule of rules) {
const keys = objectKeys(rule);
const key = keys[0];
if (keys.length === 1 && key && isRuleFunction<ConsequencePayload, C, RF>(rule, ruleFunctionsTable, key)) {
const consequence = await ruleFunctionsTable[key](rule[key], context as C);
if (consequence) {
errors.push(consequence);
if (haltOnFirstMatch && !validation) {
return errors;
}
}
} else {
if (!rule.condition) {
throw new Error(`Missing condition for rule`);
}
if (!rule.consequence) {
throw new Error(`Missing consequence for rule`);
}
if (validation) {
await validate<C, F, Ignore>(rule.condition, context as ValidationContext<C, Ignore>, functionsTable);
await evaluateEngineConsequence<ConsequencePayload, C, Ignore>(context as C, rule.consequence);
} else {
const ruleApplies = await evaluate<C, F, Ignore>(rule.condition, context as C, functionsTable);
if (ruleApplies) {
const consequence =
await evaluateEngineConsequence<ConsequencePayload, C, Ignore>(context as C, rule.consequence);
errors.push(consequence);
if (haltOnFirstMatch) {
return errors;
}
}
}
}
}
} else {
if (!rule.condition) {
throw new Error(`Missing condition for rule`);
}
if (!rule.consequence) {
throw new Error(`Missing consequence for rule`);
}
if (validation) {
validate<C, F, Ignore>(rule.condition, context as ValidationContext<C, Ignore>, functionsTable);
evaluateEngineConsequence<ConsequencePayload, C, Ignore>(context as C, rule.consequence);
} else {
const ruleApplies = evaluate<C, F, Ignore>(rule.condition, context as C, functionsTable);
if (ruleApplies) {
const consequence = evaluateEngineConsequence<ConsequencePayload, C, Ignore>(context as C, rule.consequence);
errors.push(consequence);
if (haltOnFirstMatch) {
return errors;
}
}
}
}
}
return errors.length ? errors : undefined;
return errors.length ? errors : undefined;
}

export const evaluateRules = <ConsequencePayload, C extends Context,
RF extends RuleFunctionsTable<C, ConsequencePayload>, F extends FunctionsTable<C>, Ignore = never>
export const evaluateRules = async <ConsequencePayload, C extends Context,
RF extends RuleFunctionsTable<C, ConsequencePayload>, F extends FunctionsTable<C>, Ignore = never>
(rules: Rule<ConsequencePayload, RF, C, F, Ignore>[], context: C, functionsTable: F, ruleFunctionsTable: RF,
haltOnFirstMatch: boolean)
: void | ResolvedConsequence<ConsequencePayload>[] => {
return run<ConsequencePayload, C, RF, F, Ignore>(
rules, context, functionsTable, ruleFunctionsTable, haltOnFirstMatch, false);
: Promise<void | ResolvedConsequence<ConsequencePayload>[]> => {
return run<ConsequencePayload, C, RF, F, Ignore>(
rules, context, functionsTable, ruleFunctionsTable, haltOnFirstMatch, false);
}

export const validateRules = <ConsequencePayload, C extends Context,
RF extends RuleFunctionsTable<C, ConsequencePayload>, F extends FunctionsTable<C>, Ignore = never>
export const validateRules = async <ConsequencePayload, C extends Context,
RF extends RuleFunctionsTable<C, ConsequencePayload>, F extends FunctionsTable<C>, Ignore = never>
(rules: Rule<ConsequencePayload, RF, C, F, Ignore>[], validationContext: ValidationContext<C, Ignore>,
functionsTable: F, ruleFunctionsTable: RF)
: void => {
run<ConsequencePayload, C, RF, F, Ignore>(rules, validationContext, functionsTable, ruleFunctionsTable, false, true);
}
: Promise<void> => {
await run<ConsequencePayload, C, RF, F, Ignore>(rules, validationContext, functionsTable,
ruleFunctionsTable, false, true);
}
6 changes: 3 additions & 3 deletions src/lib/engineConsequenceEvaluator.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {ResolvedConsequence, Context, RuleConsequence, RuleConsequenceMessagePart} from '../types';
import {getFromPath} from './helpers';

export const evaluateEngineConsequence = <ConsequencePayload, C extends Context, Ignore = never>
export const evaluateEngineConsequence = async <ConsequencePayload, C extends Context, Ignore = never>
(context: C, consequence: RuleConsequence<ConsequencePayload, C, Ignore>)
: ResolvedConsequence<ConsequencePayload> => {
: Promise<ResolvedConsequence<ConsequencePayload>> => {
let messageParts: RuleConsequenceMessagePart<C, Ignore>[];
if (typeof consequence.message === 'string') {
messageParts = [consequence.message];
Expand All @@ -23,4 +23,4 @@ export const evaluateEngineConsequence = <ConsequencePayload, C extends Context,
}).join(' '),
custom: consequence.custom,
}
}
}
38 changes: 20 additions & 18 deletions src/lib/evaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,10 @@ const extractValueOrRef = <C extends Context>(context: C, validation: boolean, v
}
}

function evaluateCompareOp<C extends Context>(expressionValue: ExtendedCompareOp<any, any, any>, expressionKey: string,
contextValue: any, context: C, validation: boolean)
: boolean {
async function evaluateCompareOp<C extends Context>(expressionValue: ExtendedCompareOp<any, any, any>,
expressionKey: string, contextValue: any,
context: C, validation: boolean)
: Promise<boolean> {
if (!_isObject(expressionValue)) {
return contextValue === expressionValue;
}
Expand Down Expand Up @@ -121,36 +122,36 @@ function evaluateCompareOp<C extends Context>(expressionValue: ExtendedCompareOp
}
}

function handleAndOp<C extends Context, F extends FunctionsTable<C>, Ignore>
(andExpression: Expression<C, F, Ignore>[], context: C, functionsTable: F, validation: boolean): boolean {
async function handleAndOp<C extends Context, F extends FunctionsTable<C>, Ignore>
(andExpression: Expression<C, F, Ignore>[], context: C, functionsTable: F, validation: boolean): Promise<boolean> {
if (andExpression.length === 0) {
throw new Error('Invalid expression - and operator must have at least one expression');
}
for (const currExpression of andExpression) {
const result = run<C, F, Ignore>(currExpression, context, functionsTable, validation);
const result = await run<C, F, Ignore>(currExpression, context, functionsTable, validation);
if (!validation && !result) {
return false;
}
}
return true;
}

function handleOrOp<C extends Context, F extends FunctionsTable<C>, Ignore>
(orExpression: Expression<C, F, Ignore>[], context: C, functionsTable: F, validation: boolean): boolean {
async function handleOrOp<C extends Context, F extends FunctionsTable<C>, Ignore>
(orExpression: Expression<C, F, Ignore>[], context: C, functionsTable: F, validation: boolean): Promise<boolean> {
if (orExpression.length === 0) {
throw new Error('Invalid expression - or operator must have at least one expression');
}
for (const currExpression of orExpression) {
const result = run<C, F, Ignore>(currExpression, context, functionsTable, validation);
const result = await run<C, F, Ignore>(currExpression, context, functionsTable, validation);
if (!validation && result) {
return true;
}
}
return false;
}

function run<C extends Context, F extends FunctionsTable<C>, Ignore>
(expression: Expression<C, F, Ignore>, context: C, functionsTable: F, validation: boolean): boolean {
async function run<C extends Context, F extends FunctionsTable<C>, Ignore>
(expression: Expression<C, F, Ignore>, context: C, functionsTable: F, validation: boolean): Promise<boolean> {
const expressionKeys = objectKeys(expression);
if (expressionKeys.length !== 1) {
throw new Error('Invalid expression - too may keys');
Expand All @@ -161,9 +162,9 @@ function run<C extends Context, F extends FunctionsTable<C>, Ignore>
} else if (isOrCompareOp<C, F, Ignore>(expression)) {
return handleOrOp<C, F, Ignore>(expression.or, context, functionsTable, validation);
} else if (isNotCompareOp<C, F, Ignore>(expression)) {
return !run<C, F, Ignore>(expression.not, context, functionsTable, validation);
return !(await run<C, F, Ignore>(expression.not, context, functionsTable, validation));
} else if (isFunctionCompareOp<C, F, Ignore>(expression, functionsTable, expressionKey)) {
return validation ? true : functionsTable[expressionKey](expression[expressionKey], context);
return validation ? true : await functionsTable[expressionKey](expression[expressionKey], context);
} else {
const {value: contextValue, exists} = getFromPath(context, expressionKey);
if (validation && !exists) {
Expand All @@ -177,13 +178,14 @@ function run<C extends Context, F extends FunctionsTable<C>, Ignore>
}
}

export const evaluate = <C extends Context, F extends FunctionsTable<C>, Ignore = never>
(expression: Expression<C, F, Ignore>, context: C, functionsTable: F): boolean => {
export const evaluate = async <C extends Context, F extends FunctionsTable<C>, Ignore = never>
(expression: Expression<C, F, Ignore>, context: C, functionsTable: F): Promise<boolean> => {
return run<C, F, Ignore>(expression, context, functionsTable, false);
};

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

0 comments on commit 476edef

Please sign in to comment.