Skip to content

Commit

Permalink
add custom function run opts (#28)
Browse files Browse the repository at this point in the history
  • Loading branch information
regevbr authored Feb 27, 2023
1 parent b3e6e1a commit e1c27ec
Show file tree
Hide file tree
Showing 30 changed files with 2,158 additions and 1,641 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
strategy:
matrix:
os: [ ubuntu-latest ]
node-version: [ 6.x, 8.x, 10.x, 12.x, 14.x, 16.x, 18.x ]
node-version: [ 14.x, 16.x, 18.x, 19.x ]

steps:

Expand Down Expand Up @@ -92,7 +92,7 @@ jobs:
strategy:
matrix:
os: [ ubuntu-latest ]
node-version: [ 6.x ]
node-version: [ 18.x ]
needs: [ test ]
if: github.event_name == 'release' && github.event.action == 'published'
steps:
Expand Down
14 changes: 14 additions & 0 deletions .run/Template Mocha.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<component name="ProjectRunConfigurationManager">
<configuration default="true" type="mocha-javascript-test-runner">
<node-interpreter>project</node-interpreter>
<node-options />
<working-directory>$PROJECT_DIR$</working-directory>
<pass-parent-env>true</pass-parent-env>
<ui />
<extra-mocha-options>--config src/test/.mocharc.json</extra-mocha-options>
<test-kind>DIRECTORY</test-kind>
<test-directory />
<recursive>false</recursive>
<method v="2" />
</configuration>
</component>
205 changes: 113 additions & 92 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,14 @@ interface IExampleContext {

type IExampleContextIgnore = Moment;

type IExampleCustomEvaluatorFuncRunOptions = {dryRun: boolean};

type IExampleFunctionTable = {
countRange: ([min, max]: [min: number, max: number], ctx: { times: number | undefined }) => boolean;
countRange: ([min, max]: [min: number, max: number], ctx: { times: number | undefined },
runOpts: EvaluatorFuncRunOptions<IExampleCustomEvaluatorFuncRunOptions>) => Promise<boolean>;
}

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

const context: IExampleContext = {
userId: 'a@b.com',
Expand Down Expand Up @@ -82,7 +85,8 @@ const validationContext: ValidationContext<IExampleContext, IExampleContextIgnor
};

const functionsTable: IExampleFunctionTable = {
countRange: async ([min, max]: [min: number, max: number], ctx: { times: number | undefined }, runOptions: EvaluatorFuncRunOptions): Promise<boolean> => {
countRange: async ([min, max]: [min: number, max: number], ctx: { times: number | undefined },
runOpts: EvaluatorFuncRunOptions<IExampleCustomEvaluatorFuncRunOptions>): Promise<boolean> => {
return ctx.times === undefined ? false : ctx.times >= min && ctx.times < max;
},
};
Expand All @@ -97,7 +101,7 @@ const expression: IExampleExpression = {
lte: {
op: '+',
lhs: {
ref: 'nested.value4'
ref: 'nested.value4',
},
rhs: 2,
},
Expand All @@ -114,24 +118,28 @@ const expression: IExampleExpression = {
{
times: {
lte: {
ref: 'nested.value4'
}
ref: 'nested.value4',
},
},
},
],
},
],
};

// Example usage 1
const handler =
new ExpressionHandler<IExampleContext, IExampleFunctionTable, IExampleContextIgnore>(expression, functionsTable);
await handler.validate(validationContext); // Should not throw
console.log(await handler.evaluate(context)); // true

// Example usage 2
await validate<IExampleContext, IExampleFunctionTable, IExampleContextIgnore>(expression, validationContext, functionsTable); // Should not throw
console.log(await evaluate<IExampleContext, IExampleFunctionTable, IExampleContextIgnore>(expression, context, functionsTable)); // true
(async () => {
// Example usage 1
const handler =
new ExpressionHandler<IExampleContext, IExampleFunctionTable, IExampleContextIgnore,
IExampleCustomEvaluatorFuncRunOptions>(expression, functionsTable);
await handler.validate(validationContext, {dryRun: false}); // Should not throw
console.log(await handler.evaluate(context, {dryRun: true})); // true

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

### Expression
Expand All @@ -140,7 +148,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 (can be async) and run options (i.e. validation).
- `<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).
- `<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 @@ -217,120 +225,133 @@ Example expressions, assuming we have the `user` and `maxCount` user defined fun
*Please see tests and examples dir for more usages and examples (under /src)*

```typescript
import {ValidationContext, validateRules, evaluateRules, RulesEngine, Rule, ResolvedConsequence, EngineRuleFuncRunOptions, EvaluatorFuncRunOptions} from 'json-expression-eval';
import {ValidationContext, validateRules, evaluateRules, RulesEngine, Rule, ResolvedConsequence, EngineRuleFuncRunOptions} from 'json-expression-eval';
import {Moment} from 'moment';
import moment = require('moment');

interface IExampleContext {
userId: string;
times: number | undefined;
date: Moment;
nested: {
value: number | null;
nested2: {
value2?: number;
value3: boolean;
userId: string;
times: number | undefined;
date: Moment;
nested: {
value: number | null;
nested2: {
value2?: number;
value3: boolean;
};
};
};
}

type IExampleContextIgnore = Moment;

type IExampleCustomEngineRuleFuncRunOptions = {dryRun: boolean};

type IExamplePayload = number;

type IExampleFunctionTable = {
countRange: ([min, max]: [min: number, max: number], ctx: { times: number | undefined }, runOptions: EvaluatorFuncRunOptions) => boolean;
countRange: ([min, max]: [min: number, max: number], ctx: { times: number | undefined },
runOpts: EngineRuleFuncRunOptions<IExampleCustomEngineRuleFuncRunOptions>) => boolean;
}

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

type IExampleRule = Rule<IExamplePayload, IExampleRuleFunctionTable, IExampleContext,
IExampleFunctionTable, IExampleContextIgnore>;
IExampleFunctionTable, IExampleContextIgnore, IExampleCustomEngineRuleFuncRunOptions>;

const context: IExampleContext = {
userId: 'a@b.com',
times: 3,
date: moment(),
nested: {
value: null,
nested2: {
value3: true,
userId: 'a@b.com',
times: 3,
date: moment(),
nested: {
value: null,
nested2: {
value3: true,
},
},
},
};

// For validation we must provide a full example context
const validationContext: ValidationContext<IExampleContext, IExampleContextIgnore> = {
userId: 'a@b.com',
times: 3,
date: moment(),
nested: {
value: 5,
nested2: {
value2: 6,
value3: true,
userId: 'a@b.com',
times: 3,
date: moment(),
nested: {
value: 5,
nested2: {
value2: 6,
value3: true,
},
},
},
};

const functionsTable: IExampleFunctionTable = {
countRange: ([min, max]: [min: number, max: number], ctx: { times: number | undefined }, runOptions: EvaluatorFuncRunOptions): boolean => {
return ctx.times === undefined ? false : ctx.times >= min && ctx.times < max;
},
countRange: ([min, max]: [min: number, max: number], ctx: { times: number | undefined },
runOptions: EngineRuleFuncRunOptions<IExampleCustomEngineRuleFuncRunOptions>): boolean => {
return ctx.times === undefined ? false : ctx.times >= min && ctx.times < max;
},
};

const ruleFunctionsTable: IExampleRuleFunctionTable = {
userRule: async (user: string, ctx: IExampleContext, runOptions: EngineRuleFuncRunOptions): Promise<void | ResolvedConsequence<number>> => {
if (ctx.userId === user) {
return {
message: `Username ${user} is not allowed`,
custom: 543,
}
}
},
userRule: async (user: string, ctx: IExampleContext,
runOptions: EngineRuleFuncRunOptions<IExampleCustomEngineRuleFuncRunOptions>)
: Promise<void | ResolvedConsequence<number>> => {
if (ctx.userId === user) {
return {
message: `Username ${user} is not allowed`,
custom: 543,
}
}
},
};

const rules: IExampleRule[] = [
{
condition: {
or: [
{
userId: 'a@b.com',
{
condition: {
or: [
{
userId: 'a@b.com',
},
{
and: [
{
countRange: [2, 6],
},
{
'nested.nested2.value3': true,
},
],
},
],
},
{
and: [
{
countRange: [2, 6],
},
{
'nested.nested2.value3': true,
},
],
consequence: {
message: ['user', {
ref: 'userId',
}, 'should not equal a@b.com'],
custom: 579,
},
],
},
consequence: {
message: ['user', {
ref: 'userId',
}, 'should not equal a@b.com'],
custom: 579,
{
userRule: 'b@c.com',
},
},
{
userRule: 'b@c.com',
},
];

// Example usage 1
const engine = new RulesEngine<IExamplePayload, IExampleContext, IExampleRuleFunctionTable,
IExampleFunctionTable, IExampleContextIgnore>(functionsTable, ruleFunctionsTable);
await engine.validate(rules, validationContext); // Should not throw
console.log(JSON.stringify(await engine.evaluateAll(rules, context))); // [{"message":"user a@b.com should not equal a@b.com","custom":579}]

// Example usage 2
await validateRules<IExamplePayload, IExampleContext, IExampleRuleFunctionTable,
IExampleFunctionTable, IExampleContextIgnore>(rules, validationContext, functionsTable, ruleFunctionsTable); // Should not throw
console.log(JSON.stringify(await evaluateRules<IExamplePayload, IExampleContext, IExampleRuleFunctionTable,
IExampleFunctionTable, IExampleContextIgnore>(rules, context, functionsTable, ruleFunctionsTable, false))); // [{"message":"user a@b.com should not equal a@b.com","custom":579}]
(async () => {
// Example usage 1
const engine = new RulesEngine<IExamplePayload, IExampleContext, IExampleRuleFunctionTable,
IExampleFunctionTable, IExampleContextIgnore, IExampleCustomEngineRuleFuncRunOptions>(
functionsTable, ruleFunctionsTable);
await engine.validate(rules, validationContext, {dryRun: false}); // Should not throw
console.log(JSON.stringify(await engine.evaluateAll(rules, context, {dryRun: false}))); // [{"message":"user a@b.com should not equal a@b.com","custom":579}]

// Example usage 2
await validateRules<IExamplePayload, IExampleContext, IExampleRuleFunctionTable,
IExampleFunctionTable, IExampleContextIgnore, IExampleCustomEngineRuleFuncRunOptions>(
rules, validationContext, functionsTable, ruleFunctionsTable, {dryRun: false}); // Should not throw
console.log(JSON.stringify(await evaluateRules<IExamplePayload, IExampleContext, IExampleRuleFunctionTable,
IExampleFunctionTable, IExampleContextIgnore, IExampleCustomEngineRuleFuncRunOptions>(rules, context, functionsTable, ruleFunctionsTable, false, {dryRun: false}))); // [{"message":"user a@b.com should not equal a@b.com","custom":579}]
})();
```
35 changes: 16 additions & 19 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
{
"name": "json-expression-eval",
"version": "6.0.0",
"version": "7.0.0",
"description": "json serializable rule engine / boolean expression evaluator",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"engines": {
"node": "^6 || ^8 || ^10 || ^12 || ^14 || ^16 || ^18"
"node": "^14 || ^16 || ^18 || ^19"
},
"scripts": {
"test": "yarn lint && yarn test:tsd && yarn test:cover",
"test:tsd": "tsd",
"test:unit": "mocha --opts src/test/mocha.opts",
"test:unit": "mocha --config src/test/.mocharc.json",
"build": "yarn lint && yarn compile",
"compile": "./node_modules/.bin/tsc",
"test:cover": "nyc --reporter=lcov --reporter=text-summary mocha --opts src/test/mocha.opts",
"test:cover": "nyc --reporter=lcov --reporter=text-summary mocha --config src/test/.mocharc.json",
"lint": "tslint -c tslint.json 'src/**/*.ts' 'test/**/*.ts'",
"ci": "yarn lint && yarn compile && yarn test:tsd && yarn test:cover"
},
Expand Down Expand Up @@ -48,27 +48,24 @@
"directory": "src/test/types"
},
"devDependencies": {
"@istanbuljs/nyc-config-typescript": "0.1.3",
"@types/chai": "^4.2.16",
"@istanbuljs/nyc-config-typescript": "1.0.2",
"@types/chai": "^4.3.4",
"@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",
"@types/mocha": "^10.0.1",
"@types/node": "^18.14.2",
"@types/underscore": "^1.11.4",
"chai": "^4.3.7",
"chai-as-promised": "^7.1.1",
"mocha": "^6.2.3",
"moment": "^2.29.1",
"nyc": "^14.1.1",
"mocha": "^10.2.0",
"moment": "^2.29.4",
"nyc": "^15.1.0",
"source-map-support": "^0.5.21",
"ts-node": "^8.10.2",
"tsd": "^0.21.0",
"ts-node": "^10.9.1",
"tsd": "^0.25.0",
"tslint": "^6.1.3",
"typescript": "4.6.4"
"typescript": "^4.9.5"
},
"dependencies": {
"ts-toolbelt": "^9.6.0"
},
"resolutions": {
"merge2": "1.3.0"
}
}
Loading

0 comments on commit e1c27ec

Please sign in to comment.