Coder Social home page Coder Social logo

json-rules-engine-simplified's Introduction

Build Status Coverage Status npm version

json-rules-engine-simplified

A simple rules engine expressed in JSON

The primary goal of this project is to be an alternative of json-rules-engine for react-jsonschema-form-conditionals, as such it has similar interface and configuration, but simplified predicate language, similar to SQL.

Features

  • Optional schema and rules validation
  • Basic boolean operations (and or and not) that allow to have any arbitrary complexity
  • Rules expressed in simple, easy to read JSON
  • Declarative conditional logic with predicates
  • Relevant conditional logic support
  • Support of nested structures with selectn including composite arrays
  • Secure - no use of eval()

Installation

Install json-rules-engine-simplified by running:

npm install --s json-rules-engine-simplified

Usage

The simplest example of using json-rules-engine-simplified

import Engine from 'json-rules-engine-simplified'

let rules = [{
    conditions: {
      firstName: "empty"
    },
    event: {
        type: "remove",
        params: { 
            field: "password"
        },
    }
}];

/**
 * Setup a new engine
 */
let engine = new Engine(rules);

let formData = {
  lastName: "Smit"
}

// Run the engine to evaluate
engine
  .run(formData)
  .then(events => { // run() returns remove event
    events.map(event => console.log(event.type));
  })

Rules engine expects to know all the rules in advance, it effectively drops builder pattern, but keeps the interface.

Appending rule to existing engine

You don't have to specify all rules at the construction time, you can add rules in different time in process. In order to add new rules to the Engine use addRule function.

For example, following declarations are the same

import Engine from 'json-rules-engine-simplified';

let engineA = new Engine();

let rule = {
    conditions: {
      firstName: "empty"
    },
    event: {
        type: "remove",
        params: { 
            field: "password"
        },
    }
};

engineA.addRule(rule);

let engineB = new Engine(rule);

In this case engineA and engineB will give the same results.

Validation

In order to prevent most common errors, Engine does initial validation on the schema, during construction. Validation is done automatically if you specify schema during construction.

let rules = [{
    conditions: {
      firstName: "empty"
    },
    event: {
        type: "remove",
        params: { field: "password" },
    }
}];

let schema = {
    properties: {
        firstName: { type: "string" },
        lastName: { type: "string" }
    }
}

let engine = new Engine(rules, schema);

Types of errors

  • Conditions field validation (conditions use fields that are not part of the schema)
  • Predicate validation (used predicates are not part of the predicates library and most likely wrong)

Validation is done only during development, validation is disabled by default in production.

WARNING!!! Currently validation does not support nested structures, so be extra careful, when using those.

Conditional logic

Conditional logic is based on public predicate library with boolean logic extension.

Predicate library has a lot of predicates that we found more, than sufficient for our use cases.

To showcase conditional logic, we'll be using simple registration schema

let schema = {
  definitions: {
    hobby: {
      type: "object",
      properties: {
        name: { type: "string" },
        durationInMonth: { type: "integer" },
      }
    }
  },
  title: "A registration form",
  description: "A simple form example.",
  type: "object",
  required: [
    "firstName",
    "lastName"
  ],
  properties: {
    firstName: {
      type: "string",
      title: "First name"
    },
    lastName: {
      type: "string",
      title: "Last name"
    },
    age: {
      type: "integer",
      title: "Age",
    },
    bio: {
      type: "string",
      title: "Bio",
    },
    country: {
      type: "string",
      title: "Country" 
    },
    state: {
      type: "string",
      title: "State" 
    },
    zip: {
      type: "string",
      title: "ZIP" 
    },
    password: {
      type: "string",
      title: "Password",
      minLength: 3
    },
    telephone: {
      type: "string",
      title: "Telephone",
      minLength: 10
    },
    work: { "$ref": "#/definitions/hobby" },
    hobbies: {
        type: "array",
        items: { "$ref": "#/definitions/hobby" }
    }
  }
}

Assuming action part is taken from react-jsonschema-form-conditionals

Single line conditionals

Let's say we want to remove password , when firstName is missing, we can expressed it like this:

let rules = [{
    conditions: {
      firstName: "empty"
    },
    event: {
      type: "remove",
      params: {
        field: "password"
      }
    }
}]

This translates into - when firstName is empty, trigger remove event.

Empty keyword is equal in predicate library and required event will be performed only when predicate.empty(registration.firstName) is true.

Conditionals with arguments

Let's say we need to require zip, when age is less than 16, because the service we are using is legal only after 16 in some countries

let rules = [{
    conditions: {
      age: { less : 16 }
    },
    event: {
      type: "require",
      params: {
        field: "zip"
      }
    }
}]

This translates into - when age is less than 16, require zip.

Less keyword is less in predicate and required event will be returned only when predicate.empty(registration.age, 5) is true.

Boolean operations on a single field

AND

For the field AND is a default behavior.

Looking at previous rule, we decide that we want to change the rule and require zip, when age is between 16 and 70, so it would be available only to people older, than 16 and younger than 70.

let rules = [{
    conditions: {
        age: {
          greater: 16,
          less : 70,
        }
    },
    event: {
      type: "require",
      params: {
        field: "zip"
      }
    }
}]

By default action will be applied only when both field conditions are true. In this case, when age is greater than 16 and less than 70.

NOT

Let's say we want to change the logic to opposite, and trigger event only when age is lesser then 16 or greater than 70,

let rules = [{
  conditions: {
    age: {
      not: {
          greater: 16,
          less : 70,
      }
    }
  },
  event: {
    type: "require",
    params: {
      field: "zip"
    }
  }
}]

This does it, since the final result will be opposite of the previous condition.

OR

The previous example works, but it's a bit hard to understand, luckily we can express it differently with or conditional.

let rules = [{
  conditions: { age: { 
      or: [
        { lessEq : 5 },
        { greaterEq: 70 }
      ]
    }
  },
  event: {
    type: "require",
    params: {
      field: "zip"
    }
  }
}]

The result is the same as NOT, but easier to grasp.

Boolean operations on multi fields

To support cases, when action depends on more, than one field meeting criteria we introduced multi fields boolean operations.

Default AND operation

Let's say, when age is less than 70 and country is USA we want to require bio.

let rules = [{
  conditions: {
    age: { less : 70 },
    country: { is: "USA" }
  },
  event: { 
    type: "require",
    params: { fields: [ "bio" ]}
  }
}]

This is the way we can express this. By default each field is treated as a separate condition and all conditions must be meet.

OR

In addition to previous rule we need bio, if state is NY.

let rules = [{
  conditions: {
    or: [
      {
        age: { less : 70 },
        country: { is: "USA" }
      },
      {
        state: { is: "NY"}
      }
    ]
  },
  event: { 
    type: "require",
    params: { fields: [ "bio" ]}
  }
}]

NOT

When we don't require bio we need zip code.

let rules = [{
    conditions: {
      not: {
        or: [
          {
            age: { less : 70 },
            country: { is: "USA" }
          },
          {
            state: { is: "NY"}
          }
        ]
      }
    },
    event: { 
      type: "require",
      params: { fields: [ "zip" ]}
    }
}]

Nested object queries

Rules engine supports querying inside nested objects, with selectn, any data query that works in selectn, will work in here

Let's say we need to require state, when work has a name congressman, this is how we can do this:

let rules = [{
    conditions: {
      "work.name": { is: "congressman" }
    },
    event: { 
      type: "require",
      params: { fields: [ "state" ]}
    }
}]

Nested arrays object queries

Sometimes we need to make changes to the form if some nested condition is true.

For example if one of the hobbies is baseball, we need to make state required. This can be expressed like this:

let rules = [{
    conditions: {
      hobbies: {
        name: { is: "baseball" },
      }
    },
    event: { 
      type: "require",
      params: { fields: [ "state" ]}
    }
}]

Rules engine will go through all the elements in the array and trigger require if any of the elements meet the criteria.

Extending available predicates

If for some reason the list of predicates is insufficient for your needs, you can extend them pretty easy, by specifying additional predicates in global import object.

For example, if we want to add range predicate, that would verify, that integer value is in range, we can do it like this:

import predicate from "predicate";
import Engine from "json-rules-engine-simplified";

predicate.range = predicate.curry((val, range) => {
  return predicate.num(val) &&
    predicate.array(range) &&
    predicate.equal(range.length, 2) &&
    predicate.num(range[0]) &&
    predicate.num(range[1]) &&
    predicate.greaterEq(val, range[0]) &&
    predicate.lessEq(val, range[1]);
});

let engine = new Engine([{
  conditions: { age: { range: [ 20, 40 ] } },
  event: "hit"
}]);

Validation will automatically catch new extension and work as expected.

Logic on nested objects

Support of nested structures with selectn, so basically any query you can define in selectn you can use here.

For example if in previous example, age would be a part of person object, we could work with it like this:

    let rules = [ { conditions: { "person.age": { range: [ 20, 40 ] } } } ]; 

Also in order to support systems where keys with "." not allowed (for example if you would like to store data in mongo), you can use $ to separate references:

For example, this is the same condition, but instead of . it uses $:

    let rules = [ { conditions: { "person$age": { range: [ 20, 40 ] } } } ]; 

Relevant conditional logic

Sometimes you would want to validate formData fields one against the other. You can do this simply by appending $ to the beginning of reference.

For example, you want to trigger event only when a is less then b, when you don't know ahead a or b values

let schema = {
  type: "object",
  properties: {
    a: { type: "number" },
    b: { type: "number" }
  }
}

let rules = [{
  conditions: {
    a: { less: "$b" }
  },
  event: "some"
}]

let engine = new Engine(schema, rules);

This is how you do it, in run time $b will be replaces with field b value.

Relevant fields work on nested objects as well as on any field condition.

Events

Framework does not put any restrictions on event object, that will be triggered, in case conditions are meet

For example, event can be a string:

let rules = [{
    conditions: { ... },
    event: "require"
}]

Or number

let rules = [{
    conditions: { ... },
    event: 4
}]

Or an object

let rules = [{
    conditions: { ... },
    event: { 
      type: "require",
      params: { fields: [ "state" ]}
    }
}]

You can even return an array of events, each of which will be added to final array of results

let rules = [{
    conditions: { ... },
    event: [
      { 
        type: "require",
        params: { field: "state"}
      },
      { 
        type: "remove",
        params: { fields: "fake" }
      },
    ]
}]

License

The project is licensed under the Apache Licence 2.0.

json-rules-engine-simplified's People

Contributors

mavarazy avatar mrclay avatar sjockers avatar steffan-ennis avatar unormatov-rxnt avatar vidyaprabha avatar

Stargazers

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

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

json-rules-engine-simplified's Issues

BUG: "matches" predicate is throwing rgx.test is not a function

Matches predicate which helps to do regex check is throwing and rgx.test is not a function.
Rule and whole Engine run I used:

var Engine = require("json-rules-engine-simplified").default
let regexp = new RegExp('g')
console.log(regexp, "regular expression")
let rules = [{
conditions: {
"firstName": {
"matches": regexp
}
},
event: {
"ignore": "fjbehdsj"
}
}];
let input = {"firstName": "ugvdfvgfd"}
let engine = new Engine(rules)
engine.run(input).then((events) => {
console.log("events", events)
})

Error :

TypeError: rgx.test is not a function
in predicate/lib/predicates.js — line 182
at Object.curried (as matches) in predicate/lib/utils.js — line 22
in json-rules-engine-simplified/lib/checkField.js — line 39
at Array.every in ECMAScript

Creating rules for collection

I have a use case where, I have to create rules for collections.
var fact ={ Person:{ Name:"ricky", phoneNumber:"1234567890", areaCode:"EA_001" }, address:[ {streetName:"georgeSt", BuildingName:"SHORE Villa", flatNumber:"007, flag:"red"}, {streetName:"Hardy St", BuildingName:"FRANKLIN", flatNumber:"008, flag:"blue"} ] }

If areaCode is "EA_001" and the flag is red then we should get a an error that
ERROR: "Please be careful for animals"
Is this some thing we can achieve.

extending predicate not working

Hello,

I am a little bit frustrated because I cannot explain what is happening here.

Problem: extending predicate and then using the predicate in the conditions simply does not work.

import predicate from "predicate";
import Engine from 'json-rules-engine-simplified'
predicate.range = predicate.curry((val, range) => {
    console.log(val, range)
});
[
  {
    "conditions": { "age": { "range": [ 20, 40 ] } },
    "event": "test"
  }
]

So when running this, I receive the following error in the console, and my range is never used.

Uncaught ReferenceError: Rule contains invalid predicates 0,1
at toError (utils.js:52)
at validatePredicates (validation.js:120)
at Engine.validate (Engine.js:26)
at Engine.addRule (Engine.js:40)

This actually makes sense to me, as here https://github.com/RxNT/json-rules-engine-simplified/blob/948d16247e67aab907acc69bdd5aff3256bbe3b5/src/validation.js#L25 it is always checked if it is a object. And at the time when the range params are coming in, it will evaluate [20, 40] to true as array is always typeof [] === "object" true. Therefore then of course Object.keys(rule) makes [20, 40] to 0,1...

What I now completely do not understand is, how it is possible that the tests run perfectly in this repo? It seems in the repo, as within the test cases, this check in the validation is never hit. How can this be?

Thanks

Can conditions run on individual objects in an array?

We have an array of objects, and need conditions to apply based on fields in the same array element. Something like:

{
  conditions: {
    'arr[x].foo': { equal: true }},
  },
  event: {
    type: 'remove',
    params: { field: 'arr[x].bar' }
  }
};

Fail to handle string[]

I'm not sure if this is intentional behaviour. If it is it would be a good feature to be able to compare arrays and not just their items

Found a bad condition check on line 7 of conditionsMeet.js
line 7-8
if (!isObject(conditions) || !isObject(formData)) { toError(Rule ${conditions} with ${formData} can't be processed); }

line 21
return refVal.some(val => conditionsMeet(refCondition, val));

can call with bad arguments

Example condition, targets is a string[]
"conditions": { "targets": "_empty" }

screen shot 2017-07-26 at 12 54 39 pm

screen shot 2017-07-26 at 12 55 19 pm

Bug: "includes" predicate doesn't work and causes exception

I use this rule:

[{
	"conditions": {
		"not": {
			"and": [{
				"field_1": {
					"includes": ["opt1", "opt2"]
				}
			}]
		}
	},
	"event": {
		"type": "remove",
		"params": {
			"field": "field_2"
		}
	}
}]

The engine fails with the following error:

Uncaught ReferenceError: Rule contains invalid predicates 0,1
at toError (vendors.chunk.js:281881)
at validatePredicates (vendors.chunk.js:282058)
at Engine.validate (vendors.chunk.js:281566)
at Engine.addRule (vendors.chunk.js:281580)
at vendors.chunk.js:348680
at Array.forEach ()
at rulesRunner (vendors.chunk.js:348679)
at applyRules (vendors.chunk.js:348486)
at new FormRenderer (:9001/app/content-management~space.chunk.js:1665)
at new FormRenderer (eval at ES6ProxyComponentFactory (vendors.chunk.js:1), :5:7)

create-react-app build process broken

See Predicate issue #10

I'm using json-rules-engine-simplified paired with react-jsonschema-form-conditionals for a web app that I'm building using create-react-app.

The form works perfectly well when in development mode. However, I'm unable to build the app for deployment because create-react-app does not minify ES6 features. This issue affects this module because it depends on Predicate, which uses ES6.

I spoke with the maintainer of that project, and they are hoping to stick with ES6 as their entry point. So I'm posting here to suggest importing predicate/dist/predicate instead of just predicate. This file is just Predicate transpiled to ES5.

The affected files are:

  • src/checkField.js
  • src/validation.js

Failure to minify predicate dependency

This issue was introduced when upgrading from version 0.1.16 to 0.1.17.

After upgrading to 0.1.17, I get this issue when I run npm run build

Failed to minify the code from this file: 
 	./node_modules/predicate/lib/utils.js:7 

The line in question is

  curried.toString = () => src.toString();

Presumably, this fails to minify because it's using an ES6 feature (an arrow function). This is related to issue #23

Usage in browser?

I would like to use your library in a browser. Is this possible?
I tried to browserify without success. Any suggestions?

conditionsMeet fails on array of strings

If the form data is an array, conditionsMeet first tries to see if any elements in the array pass the condition:

https://github.com/RxNT/json-rules-engine-simplified/blob/c271482533324b275341e0598f24bc8c9667c23d/src/conditionsMeet.js#L38-L40

But this means a string gets passed as val to conditionsMeet, which it can't handle.

I think the inner function handling refVal.some() should just return false if val isn't an object rather than letting conditionsMeet throw.

Support reference in conditions

Following #12 comment, we want to be able to do comparison between fields in the object like this

conditions: {
    a: { less: "$b" }
}

And also support arbitrary predicates:

conditions: {
     cost: {
        decreasedByMoreThanPercent: {
             average: "averages_monthly.cost",
             target: 20
         }
     }
 }

How to fail based on a condition?

I'm creating a date range, so the end has to be later than the start. I can create an event based on conditions referencing the fields, but I basically need a "failValidation" event. Or I suppose an event that adds a minimum on the end date. Any ideas here?

Why is `.run` a promise?

The async nature of .run does not seem necessary. The function applicableActions does no asynchronous work, so it's really a promise for no reason.

I wouldn't normally care, but I'm trying to implement my own rules runner and it would be much easier to write if I could rely on everything to be synchronous.

conditionsMeet fails on array of numbers

See #18, although its patch doesn't fix this this.

// throws error
test("handles array of non-objects", () => {
  let condition = {
    options: {
      contains: 2,
    },
  };
  expect(conditionsMeet(condition, { options: [1] })).toBeFalsy();
  expect(conditionsMeet(condition, { options: [] })).toBeFalsy();
  expect(conditionsMeet(condition, { options: [1, 2] })).toBeTruthy();
});

Cannot associate an array of events with a condition

According to the docs, it should be possible to specify a list of events for a condition. If I specify a single event, everything works as expected

  const rules = [
    {
      conditions: {
        hasBenefitsReference: {is: true}
      },
      event:
        {
          type: 'require',
          params: {
            field: ['hasBD2Reference', 'BD2Reference']
          }
        }      
    }
  ];

But if I use an array of events instead:

      event: [
        {
          type: 'require',
          params: {
            field: ['hasBD2Reference', 'BD2Reference']
          }
        } 
      ]

I get this error

Warning: Failed props type: Invalid props `rules[0].event` of type `array` supplied to 
`react-jsonschema-form-manager`, expected `object`.

Uncaught ReferenceError: Rule contains invalid action "undefined"
    at toError (utils.js:25)
    at applyRules.js:67
    at Array.map (<anonymous>)
    at applyRules (applyRules.js:59)

I realise it doesn't make much sense to specify an array of events containing just a single event, but the same error occurs when a larger event array is used.

The schema for the example above is:

  const schema = {
    type: 'object',
    properties: {
      hasBenefitsReference: {
        title: 'Do you have a Benefits Reference Number?',
        type: 'boolean'
      },
      benefitsReference: {
        title: 'Benefits Reference Number',
        type: 'string'
      },
      hasBD2Reference: {
        title: 'Do you have a BD2 Number?',
        type: 'boolean'
      },
      BD2Reference: {
        title: "BD2 Number",
        type: 'string',
      }
    }
  };

How to concatenate nested conditions with static values?

Sample Data:

{
    "DisplayName": {
        "LocalizedLabels": [
            {
                "Label": "Customer"
            }
        ]
    }
}

Condition: DisplayName.LocalizedLabels.0.Label_STATIC_VALUE
When using this, it returns the exact condition as text instead of Customer_STATIC_VALUE

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.