Skip to content

Commit

Permalink
add better validation logic (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
regevbr authored Dec 4, 2022
1 parent 49a26b8 commit b3e6e1a
Show file tree
Hide file tree
Showing 8 changed files with 25 additions and 16 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ on:
- '**'

env:
PRIMARY_NODE_VERSION: 16.x
PRIMARY_NODE_VERSION: 18.x
PRIMARY_OS: ubuntu-latest
REGISTRY: https://registry.npmjs.org/

Expand Down
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ yarn add json-expression-eval
*Please see tests and examples dir for more usages and examples (under /src)*

```typescript
import {evaluate, Expression, ExpressionHandler, validate, ValidationContext} from 'json-expression-eval';
import {evaluate, Expression, ExpressionHandler, validate, ValidationContext, EvaluatorFuncRunOptions} from 'json-expression-eval';
import {Moment} from 'moment';
import moment = require('moment');

Expand Down Expand Up @@ -82,7 +82,7 @@ const validationContext: ValidationContext<IExampleContext, IExampleContextIgnor
};

const functionsTable: IExampleFunctionTable = {
countRange: async ([min, max]: [min: number, max: number], ctx: { times: number | undefined }): Promise<boolean> => {
countRange: async ([min, max]: [min: number, max: number], ctx: { times: number | undefined }, runOptions: EvaluatorFuncRunOptions): Promise<boolean> => {
return ctx.times === undefined ? false : ctx.times >= min && ctx.times < max;
},
};
Expand Down Expand Up @@ -140,7 +140,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).
- `<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).
- `<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,7 +217,7 @@ 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} from 'json-expression-eval';
import {ValidationContext, validateRules, evaluateRules, RulesEngine, Rule, ResolvedConsequence, EngineRuleFuncRunOptions, EvaluatorFuncRunOptions} from 'json-expression-eval';
import {Moment} from 'moment';
import moment = require('moment');

Expand All @@ -238,11 +238,11 @@ type IExampleContextIgnore = Moment;
type IExamplePayload = number;

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 }, runOptions: EvaluatorFuncRunOptions) => boolean;
}

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

type IExampleRule = Rule<IExamplePayload, IExampleRuleFunctionTable, IExampleContext,
Expand Down Expand Up @@ -275,13 +275,13 @@ const validationContext: ValidationContext<IExampleContext, IExampleContextIgnor
};

const functionsTable: IExampleFunctionTable = {
countRange: ([min, max]: [min: number, max: number], ctx: { times: number | undefined }): boolean => {
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;
},
};

const ruleFunctionsTable: IExampleRuleFunctionTable = {
userRule: async (user: string, ctx: IExampleContext): Promise<void | ResolvedConsequence<number>> => {
userRule: async (user: string, ctx: IExampleContext, runOptions: EngineRuleFuncRunOptions): Promise<void | ResolvedConsequence<number>> => {
if (ctx.userId === user) {
return {
message: `Username ${user} is not allowed`,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "json-expression-eval",
"version": "5.1.2",
"version": "6.0.0",
"description": "json serializable rule engine / boolean expression evaluator",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
2 changes: 1 addition & 1 deletion src/lib/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ async function run<ConsequencePayload, C extends Context,
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);
const consequence = await ruleFunctionsTable[key](rule[key], context as C, {validation});
if (consequence) {
errors.push(consequence);
if (haltOnFirstMatch && !validation) {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/evaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ async function run<C extends Context, F extends FunctionsTable<C>, Ignore>
} else if (isNotCompareOp<C, F, Ignore>(expression)) {
return !(await run<C, F, Ignore>(expression.not, context, functionsTable, validation));
} else if (isFunctionCompareOp<C, F, Ignore>(expression, functionsTable, expressionKey)) {
return validation ? true : await functionsTable[expressionKey](expression[expressionKey], context);
return functionsTable[expressionKey](expression[expressionKey], context, {validation});
} else {
const {value: contextValue, exists} = getFromPath(context, expressionKey);
if (validation && !exists) {
Expand Down
6 changes: 4 additions & 2 deletions src/test/evaluator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1307,7 +1307,8 @@ describe('evaluator', () => {
],
};
await validate(expression, context, fnTable);
expect(fnCounter).to.eql(0);
expect(fnCounter).to.eql(4);
fnCounter = 0;
await evaluate(expression, context, fnTable);
expect(fnCounter).to.eql(3);
});
Expand Down Expand Up @@ -1338,7 +1339,8 @@ describe('evaluator', () => {
};
const context = {};
await validate<typeof context, typeof fnTable>(expression, context, fnTable);
expect(fnCounter).to.eql(0);
expect(fnCounter).to.eql(4);
fnCounter = 0;
await evaluate(expression, context, fnTable);
expect(fnCounter).to.eql(3);
});
Expand Down
6 changes: 5 additions & 1 deletion src/types/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,12 @@ export interface RuleDefinition<ConsequencePayload, C extends Context, F extends
consequence: RuleConsequence<ConsequencePayload, C, Ignore>;
}

export type EngineRuleFuncRunOptions = {
validation: boolean;
}

export type RuleFunc<C, ConsequencePayload> = (
param: any, context: C) => void | ResolvedConsequence<ConsequencePayload>
param: any, context: C, runOptions: EngineRuleFuncRunOptions) => void | ResolvedConsequence<ConsequencePayload>
| Promise<(void | ResolvedConsequence<ConsequencePayload>)>;

export type RuleFunctionsTable<C, ConsequencePayload> = Record<string, RuleFunc<C, ConsequencePayload>>;
Expand Down
5 changes: 4 additions & 1 deletion src/types/evaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,10 @@ export type FullExpression<C extends Context, F extends FunctionsTable<C>, Ignor
export type Expression<C extends Context, F extends FunctionsTable<C>, Ignore = never> =
RequireOnlyOne<FullExpression<C, F, Ignore>>;

export type Func<T> = (param: any, context: T) => boolean | Promise<boolean>;
export type EvaluatorFuncRunOptions = {
validation: boolean;
}
export type Func<T> = (param: any, context: T, runOptions: EvaluatorFuncRunOptions) => boolean | Promise<boolean>;

export type FunctionsTable<T> = Record<string, Func<T>>;

Expand Down

0 comments on commit b3e6e1a

Please sign in to comment.