Json Rules Engine
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 })
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.
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) {
})
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) {
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!
let stateFact = function(params, engine) {
return stateLookupByZip(params.country, engine.factValue('zip-code'))
}
engine.addFact('state', stateFact)
let ageFact = function(params, engine) {
return engine.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
Step 4: Handing Events
When rule conditions are met, the application needs to respond to the event that is emitted.
engine.on('young-adult-rocky-mnts', (params) => {
})
engine.on('event', function (event, engine) {
})
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()
engine.run()
engine.run({ userId: 1 })
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.
let jsonString = rule.toJSON()
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