Skip to content

lamjar/json-rules-engine

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

97 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Json Rules Engine

js-standard-style Build Status npm version

A rules engine expressed in JSON

Synopsis

json-rules-engine is a powerful, lightweight rules engine. Rules are composed of simple json structures, making them human readable and easy to persist. Performance controls and built-in caching mechanisms help make the engine sufficiently performant to handle most use cases.

Features

  • Rules and Events expressed in JSON
  • Facts provide the mechanism for pulling data asynchronously during runtime
  • Priority levels can be set at the rule, fact, and condition levels to optimize performance
  • Full support for ALL and ANY boolean operators, including recursive nesting
  • Comparison operators: equal, notEqual, in, notIn, lessThan, lessThanInclusive, greaterThan, greaterThanInclusive
  • Lightweight & extendable; less than 500 lines of javascript w/few dependencies

Installation

$ npm install json-rules-engine

Conceptual Overview

An engine is composed of 4 basic building blocks: rules, rule conditions, rule events, and facts.

Engine - executes rules, emits events, and maintains state. Most applications will have a single instance.

let engine = new Engine()

Rule - contains a set of conditions and a single event. When the engine is run, each rule condition is evaluated. If the results are truthy, the rule's event is triggered.

let rule = new Rule({ priority: 25 })  // the higher the priority, the earlier the rule will run.  default=1
engine.addRule(rule)

Rule Condition - Each condition consists of a constant value, an operator, a fact, and (optionally) fact params. The operator compares the fact result to the value.

// engine will call the "new-years" method at runtime with "params" and compare the results to "true"
rule.setConditions({
  fact: 'new-years',
  params: {
    calendar: 'gregorian'
  }
  operator: 'equal',
  value: true
})

Rule Event - Defines an event emitter that is triggered when conditions are met. Events must have a type property which acts as an identifier. Optionally, events may also have params.

rule.setEvent({
  type: 'celebrate',
  params: {
    balloons: true,
    cake: false
  }
})
engine.on('celebrate', function (params) {
  // handle event business logic
  // params = { balloons: true, cake: false }
})

Fact - Methods or constants registered with the engine prior to runtime, and referenced within rule conditions. Each fact method is a pure function that may return a computed value or promise. As rule conditions are evaluated during runtime, they retrieve fact values dynamically and use the condition operator to compare the fact result with the condition value.

let fact = function(params, engine) {
  // business logic for computing fact value based on params
  return dayOfYearByCalendar(params.calendar)
}
engine.addFact('year', fact)

Usage Example

Step 1: Create an Engine

  let Engine = require('json-rules-engine')
  let engine = new Engine()

More on engines can be found here

Step 2: Add Rules

Rules are composed of two components: conditions and events. Conditions are a set of requirements that must be met to trigger the rule's event.

let event = {
  type: 'young-adult-rocky-mnts',
  params: {
    giftCard: 'amazon',
    value: 50
  }
}
let conditions = {
  all: [
    {
      fact: 'age',
      operator: 'greaterThanInclusive',
      value: 18
    }, {
      fact: 'age',
      operator: 'lessThanInclusive',
      value: 25
    },
    any: [
      {
        fact: 'state',
        params: {
          country: 'us'
        },
        operator: 'equal',
        value: 'colorado'
      }, {
        fact: 'state',
        params: {
          country: 'us'
        },
        operator: 'equal',
        value: 'utah'
      }
    ]
  ]
}
let rule = new Rule({ conditions, event})
engine.addRule(rule)

The example above demonstrates a rule for finding individuals between 18 and 25 who live in either Utah or Colorado.

More on rules can be found here

Step 3: Define Facts

Facts are constant values or pure functions. Using the current example, if the engine were to be run, it would throw an error: "Undefined fact: 'age'". So let's define some facts!

/*
 * Define the 'state' fact
 */
let stateFact = function(params, almanac) {
  // rule "params" value is passed to the fact
  // 'almanac' can be used to lookup other facts
  // via almanac.factValue()
  return stateLookupByZip(params.country, almanac.factValue('zip-code'))
}
engine.addFact('state', stateFact)

/*
 * Define the 'age' fact
 */
let ageFact = function(params, almanac) {
  // facts may return a promise when performing asynchronous operations
  // such as database calls, http requests, etc to gather data
  return almanac.factValue('userId').then((userId) => {
    return db.getUser(userId)
  }).then((user) => {
    return user.age
  })
}
engine.addFact('age', ageFact)

Now when the engine is run, it will call the methods above whenever it encounters the fact: "age" or fact: "state" properties.

Important: facts should be pure functions; their computed values will vary based on the params argument. By establishing facts as pure functions, it allows the rules engine to cache results throughout each run(); facts called multiple times with the same params will trigger the computation once and cache the results for future calls. If fact caching not desired, this behavior can be turned off via the options; see the docs.

More on facts can be found here More on almanacs can be found here

Step 4: Handing Events

When rule conditions are met, the application needs to respond to the event that is emitted.

// subscribe directly to the 'young-adult' event
engine.on('young-adult-rocky-mnts', (params) => {
  // params: {
  //   giftCard: 'amazon',
  //   value: 50
  // }
})

// - OR -

// subscribe to any event emitted by the engine
engine.on('success', function (event, engine) {
  // event: {
  //   type: "young-adult-rocky-mnts",
  //   params: {
  //     giftCard: 'amazon',
  //     value: 50
  //   }
  // }
})

Step 5: Run the engine

Running an engine executes the rules, and fires off event events for conditions that were met. The fact results cache will be cleared with each run()

// evaluate the rules
engine.run()

// Optionally, constant facts may be provided
engine.run({ userId: 1 })  // any time a rule condition requires 'userId', '1' will be returned

// run() returns a promise
engine.run().then(() => {
  console.log('all rules executed')
})

Persisting Rules

Rules may be easily converted to JSON and persisted to a database, file system, or elsewhere. To convert a rule to JSON, simply call the rule.toJSON() method. Later, a rule may be restored by feeding the json into the Rule constructor.

// save somewhere...
let jsonString = rule.toJSON()

// ...later:
let rule = new Rule(jsonString)

Why aren't "fact" methods persistable? This is by design, for several reasons. Firstly, facts are by definition business logic bespoke to your application, and therefore lie outside the scope of this library. Secondly, many times this request indicates a design smell; try thinking of other ways to compose the rules and facts to accomplish the same objective. Finally, persisting fact methods would involve serializing javascript code, and restoring it later via eval(). If you have a strong desire for this feature, the node-rules project supports this (though be aware the capability is enabled via eval().

Debugging

To see what the engine is doing under the hood, debug output can be turned on via:

DEBUG=json-rules-engine

About

A rules engine expressed in JSON

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • JavaScript 100.0%