Code Monkey home page Code Monkey logo

json-expression-eval's Introduction

Npm Version node Build status Test Coverage Maintainability Known Vulnerabilities

json-expression-eval (and rule engine)

A Fully typed Node.js module that evaluates a json described boolean expressions using dynamic functions and a given context. Expressions can also be evaluated in a rule engine manner.

The module is strictly typed, ensuring that passed expressions are 100% valid at compile time.

This module is especially useful if you need to serialize complex expressions / rules (to be saved in a DB for example)

Installation

npm install json-expression-eval

Or

yarn add json-expression-eval

Usage

Please see tests and examples dir for more usages and examples (under /src)

import {evaluate, Expression, ExpressionHandler, validate, ValidationContext, EvaluatorFuncRunOptions} 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;
        value4: number;
        nested2: {
            value2?: number;
            value3: boolean;
        };
    };
}

type IExampleContextIgnore = Moment;

type IExampleCustomEvaluatorFuncRunOptions = {dryRun: boolean};

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

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

const context: IExampleContext = {
    userId: '[email protected]',
    times: 3,
    date: moment(),
    nested: {
        value: null,
        value4: 5,
        nested2: {
            value3: true,
        },
    },
};

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

const functionsTable: IExampleFunctionTable = {
    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;
    },
};

const expression: IExampleExpression = {
    or: [
        {
            userId: '[email protected]',
        },
        {
            times: {
                lte: {
                    op: '+',
                    lhs: {
                        ref: 'nested.value4',
                    },
                    rhs: 2,
                },
            },
        },
        {
            and: [
                {
                    countRange: [2, 6],
                },
                {
                    'nested.nested2.value3': true,
                },
                {
                    times: {
                        lte: {
                            ref: 'nested.value4',
                        },
                    },
                },
            ],
        },
    ],
};

(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

There are 4 types of operators you can use (evaluated in that order of precedence):

  • 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 + custom defined value).
  • <compare funcs> - operates on one of the context properties and compares it to a given value.
    • {property: {op: value}}
      • available ops:
        • gt - >
        • gte - >=
        • lt - <
        • lte - <=
        • eq - ===
        • neq - !==
        • regexp: RegExp - True if matches the compiled regular expression.
        • regexpi: RegExp - True if matches the compiled regular expression with the i flag set.
        • nin: any[] - True if not in an array of values. Comparison is done using the === operator
        • inq: any[] - True if in an array of values. Comparison is done using the === operator
        • between: readonly [number, number] (as const) - True if the value is between the two specified values: greater than or equal to first value and less than or equal to second value.
    • {property: value}
      • compares the property to that value (shorthand to the eq op, without the option to user math or refs to other properties)

Nested properties in the context can also be accessed using a dot notation (see example above)

In each expression level, you can only define 1 operator, and 1 only

The right-hand side of compare (not user defined) functions can be a:

  • literal - number/string/boolean (depending on the left-hand side of the function)
  • reference to a property (or nested property) in the context.
    This can be achieved by using {"ref":"<dot notation path>"}
  • A math operation that can reference properties in the context.
    The valid operations are +,-,*,/,%,pow.
    This can be achieved by using
    {
      "op": "<+,-,*,/,%,pow>",
      "lhs": {"ref": "<dot notation path>"}, // or a number literal
      "rhs": {"ref": "<dot notation path>"} // or a number literal
    }
    which will be computed as <lhs> <op> <rhs> where lhs is left-hand-side and rhs is right-hand-side. So for example
    {
      "op": "/",
      "lhs": 10,
      "rhs": 2
    }
    will equal 10 / 2 = 5

Example expressions, assuming we have the user and maxCount user defined functions in place can be:

{  
   "or":[  
      {  
         "not":{  
            "user":"[email protected]"
         }
      },
      {  
         "maxCount":1
      },
      {  
         "times": { "eq" : 5}
      },
      {  
         "times": { "eq" : { "ref": "nested.preoprty"}}
      },
      {  
         "country": "USA"
      }
   ]
}

Rule Engine

Please see tests and examples dir for more usages and examples (under /src)

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;
        };
    };
}

type IExampleContextIgnore = Moment;

type IExampleCustomEngineRuleFuncRunOptions = {dryRun: boolean};

type IExamplePayload = number;

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

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

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

const context: IExampleContext = {
    userId: '[email protected]',
    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: '[email protected]',
    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: 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<IExampleCustomEngineRuleFuncRunOptions>)
        : Promise<void | ResolvedConsequence<number>> => {
        if (ctx.userId === user) {
            return {
                message: `Username ${user} is not allowed`,
                custom: 543,
            }
        }
    },
};

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

(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 [email protected] should not equal [email protected]","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 [email protected] should not equal [email protected]","custom":579}]
})();

json-expression-eval's People

Contributors

dependabot[bot] avatar regevbr avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

json-expression-eval's Issues

Implement short-circuit evaluation

in the handleOrOp and handleAndOp functions forEach calls a provided callback function once for each element but it's not necessary to call all the elements if we already got true in one of the elements evaluate.

suggested solution -
At handleOrOp and handleAndOp functions need to return result earlier at forEach method if the result variable is true at handleOrOp or false at handleAndOp.

I would like to create PR to fix it.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.