json-rules-engine
Advanced tools
Comparing version 1.0.0-beta3 to 1.0.0-beta4
# Rules | ||
## Actions | ||
## Methods | ||
### constructor([Object options|String json]) | ||
Returns a new rule instance | ||
```js | ||
let rule = new Rule(options) | ||
``` | ||
### setConditions(Array conditions) | ||
Assigns the rule conditions to the provided argument. The root condition must be a boolean operator (```all``` or ```any```) | ||
```js | ||
rule.setConditions({ | ||
all: [ | ||
{ | ||
fact: 'revenue', | ||
operator: 'greaterThanInclusive' | ||
value: 1000000 | ||
} | ||
] | ||
}) | ||
``` | ||
### setEvent(Object event) | ||
Sets the event the engine should emit when the rule conditions pass. All events must have a ```type``` property, which denotes the event name to emit when the rule passes. | ||
Optionally, a ```params``` property may be provided as well. ```params``` will be passed to the event as an argument. | ||
```js | ||
rule.setEvent({ | ||
type: 'string', //required | ||
params: 'object' //optional | ||
}) | ||
``` | ||
### setPriority(Integer priority = 1) | ||
Sets the rule priority. Priority must be a positive, non-zero integer. The higher the priority, the sooner the rule will run. If no priority is assigned to a Rule, it will receive a default priority of 1. | ||
```js | ||
rule.setPriority(100) | ||
``` | ||
### toJSON(Boolean stringify = true) | ||
Serializes the rule into a JSON string. Usually used when persisting rules. | ||
```js | ||
let jsonString = rule.toJSON() // string: '{"conditions":{"all":[]},"priority":50 ... | ||
let rule = new Rule(jsonString) // restored rule; same conditions, priority, event | ||
// without stringifying | ||
let jsonObject = rule.toJSON(false) // object: {conditions:{ all: [] }, priority: 50 ... | ||
``` | ||
## Conditions | ||
@@ -6,0 +64,0 @@ |
{ | ||
"name": "json-rules-engine", | ||
"version": "1.0.0-beta3", | ||
"version": "1.0.0-beta4", | ||
"description": "Rules Engine expressed in simple json", | ||
@@ -18,3 +18,4 @@ "main": "dist/index.js", | ||
"rules", | ||
"engine" | ||
"engine", | ||
"rules engine" | ||
], | ||
@@ -21,0 +22,0 @@ "standard": { |
# Json Rules Engine | ||
[![js-standard-style](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) | ||
[![Build Status](https://travis-ci.org/CacheControl/json-rules-engine.svg?branch=master)](https://travis-ci.org/CacheControl/json-rules-engine) | ||
[![npm version](https://badge.fury.io/js/json-rules-engine.svg)](https://badge.fury.io/js/json-rules-engine) | ||
@@ -13,3 +14,3 @@ A rules engine expressed in JSON | ||
* Rules and Actions expressed in JSON | ||
* Rules and Events expressed in JSON | ||
* Facts provide the mechanism for pulling data asynchronously during runtime | ||
@@ -29,5 +30,5 @@ * Priority levels can be set at the rule, fact, and condition levels to optimize performance | ||
An _engine_ is composed of 4 basic building blocks: *rules*, *rule conditions*, *rule actions*, and *facts*. | ||
An _engine_ is composed of 4 basic building blocks: *rules*, *rule conditions*, *rule events*, and *facts*. | ||
_Engine_ - executes rules, emits actions, and maintains state. Most applications will have a single instance. | ||
_Engine_ - executes rules, emits events, and maintains state. Most applications will have a single instance. | ||
@@ -38,6 +39,7 @@ ```js | ||
_Rule_ - contains a set of _conditions_ and a single _action_. When the engine is run, each rule condition is evaluated. If the results are truthy, the rule's _action_ is triggered. | ||
_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. | ||
```js | ||
let rule = new Rule({ priority: 25 }) // the higher the priority, the earlier the rule will run. default=1 | ||
engine.addRule(rule) | ||
``` | ||
@@ -59,6 +61,6 @@ | ||
_Rule Action_ - Actions are event emissions triggered by the engine when conditions are met. Actions must have a _type_ property which acts as an identifier. Optionally, actions may also have _params_. | ||
_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_. | ||
```js | ||
rule.setAction({ | ||
rule.setEvent({ | ||
type: 'celebrate', | ||
@@ -71,3 +73,3 @@ params: { | ||
engine.on('celebrate', function (params) { | ||
// handle action business logic | ||
// handle event business logic | ||
// params = { balloons: true, cake: false } | ||
@@ -96,10 +98,12 @@ }) | ||
More on engines can be found [here](./docs/engine.md) | ||
### Step 2: Add Rules | ||
Rules are composed of two components: conditions and actions. _Conditions_ are a set of requirements that must be met to trigger the rule's _action_. Actions are emitted as events and may subscribed to by the application (see step 4). | ||
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_. | ||
```js | ||
let action = { | ||
let event = { | ||
type: 'young-adult-rocky-mnts', | ||
params: { // optional | ||
params: { | ||
giftCard: 'amazon', | ||
@@ -139,7 +143,7 @@ value: 50 | ||
} | ||
let rule = new Rule({ conditions, action}) | ||
let rule = new Rule({ conditions, event}) | ||
engine.addRule(rule) | ||
``` | ||
The example above demonstrates a rule that detects _male_ users between the ages of _18 and 25_. | ||
The example above demonstrates a rule for finding individuals between _18 and 25_ who live in either _Utah or Colorado_. | ||
@@ -150,3 +154,3 @@ More on rules can be found [here](./docs/rules.md) | ||
Facts are constants or pure functions that may return different results during run-time. 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! | ||
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! | ||
@@ -182,3 +186,3 @@ ```js | ||
**Important:** facts should be *pure functions*, meaning their values will always evaluate based on the ```params``` argument. By establishing facts are pure functions, it allows the rules engine to cache results throughout a ```run()```; if the same fact is called multiple times with the same ```params```, it 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](./docs/facts.md). | ||
**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](./docs/facts.md). | ||
@@ -188,9 +192,9 @@ More on facts can be found [here](./docs/facts.md) | ||
### Step 4: Handing Actions | ||
### Step 4: Handing Events | ||
When rule conditions are met, the application needs to respond to the action that is emitted. | ||
When rule conditions are met, the application needs to respond to the event that is emitted. | ||
```js | ||
// subscribe directly to the 'young-adult' action from Step 1 | ||
engine.on('young-adult', (params) => { | ||
// subscribe directly to the 'young-adult' event | ||
engine.on('young-adult-rocky-mnts', (params) => { | ||
// params: { | ||
@@ -204,7 +208,7 @@ // giftCard: 'amazon', | ||
// subscribe to any action emitted by the engine | ||
engine.on('action', function (action, engine) { | ||
// action: { | ||
// type: "young-adult", | ||
// params: { // optional | ||
// subscribe to any event emitted by the engine | ||
engine.on('event', function (event, engine) { | ||
// event: { | ||
// type: "young-adult-rocky-mnts", | ||
// params: { | ||
// giftCard: 'amazon', | ||
@@ -219,3 +223,3 @@ // value: 50 | ||
Running an engine executes the rules, and fires off action events for conditions that were met. The fact results cache will be cleared with each ```run()``` | ||
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()``` | ||
@@ -235,2 +239,16 @@ ```js | ||
## 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. | ||
```js | ||
// 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](https://github.com/mithunsatheesh/node-rules) project supports this (though be aware the capability is enabled via ``eval()``. | ||
## Debugging | ||
@@ -237,0 +255,0 @@ |
'use strict' | ||
import sinon from 'sinon' | ||
import engineFactory from '../src/json-rules-engine' | ||
import engineFactory from '../src/index' | ||
@@ -22,3 +22,3 @@ async function factSenior (params, engine) { | ||
describe('supports a single "all" condition', () => { | ||
let action = { | ||
let event = { | ||
type: 'ageTrigger', | ||
@@ -36,9 +36,9 @@ params: { | ||
} | ||
let actionSpy = sinon.spy() | ||
let eventSpy = sinon.spy() | ||
beforeEach(() => { | ||
actionSpy.reset() | ||
let rule = factories.rule({ conditions, action }) | ||
eventSpy.reset() | ||
let rule = factories.rule({ conditions, event }) | ||
engine = engineFactory() | ||
engine.addRule(rule) | ||
engine.on('action', actionSpy) | ||
engine.on('event', eventSpy) | ||
}) | ||
@@ -49,3 +49,3 @@ | ||
await engine.run() | ||
expect(actionSpy).to.have.been.calledWith(action) | ||
expect(eventSpy).to.have.been.calledWith(event) | ||
}) | ||
@@ -56,3 +56,3 @@ | ||
engine.run() | ||
expect(actionSpy).to.not.have.been.calledWith(action) | ||
expect(eventSpy).to.not.have.been.calledWith(event) | ||
}) | ||
@@ -73,3 +73,3 @@ }) | ||
} | ||
let action = { | ||
let event = { | ||
type: 'ageTrigger', | ||
@@ -80,15 +80,15 @@ params: { | ||
} | ||
let actionSpy = sinon.spy() | ||
let eventSpy = sinon.spy() | ||
beforeEach(() => { | ||
actionSpy.reset() | ||
let rule = factories.rule({ conditions, action }) | ||
eventSpy.reset() | ||
let rule = factories.rule({ conditions, event }) | ||
engine = engineFactory() | ||
engine.addRule(rule) | ||
engine.on('action', actionSpy) | ||
engine.on('event', eventSpy) | ||
}) | ||
it('emits an action when every condition is met', async () => { | ||
it('emits an event when every condition is met', async () => { | ||
engine.addFact('age', factAdult) | ||
await engine.run() | ||
expect(actionSpy).to.have.been.calledWith(action) | ||
expect(eventSpy).to.have.been.calledWith(event) | ||
}) | ||
@@ -100,3 +100,3 @@ | ||
await engine.run() | ||
expect(actionSpy).to.not.have.been.calledWith(action) | ||
expect(eventSpy).to.not.have.been.calledWith(event) | ||
}) | ||
@@ -107,3 +107,3 @@ | ||
await engine.run() | ||
expect(actionSpy).to.not.have.been.calledWith(action) | ||
expect(eventSpy).to.not.have.been.calledWith(event) | ||
}) | ||
@@ -110,0 +110,0 @@ }) |
'use strict' | ||
import sinon from 'sinon' | ||
import engineFactory from '../src/json-rules-engine' | ||
import engineFactory from '../src/index' | ||
@@ -10,3 +10,3 @@ describe('Engine: "any" conditions', () => { | ||
describe('supports a single "any" condition', () => { | ||
let action = { | ||
let event = { | ||
type: 'ageTrigger', | ||
@@ -24,11 +24,11 @@ params: { | ||
} | ||
let actionSpy = sinon.spy() | ||
let eventSpy = sinon.spy() | ||
let ageSpy = sinon.stub() | ||
beforeEach(() => { | ||
actionSpy.reset() | ||
let rule = factories.rule({ conditions, action }) | ||
eventSpy.reset() | ||
let rule = factories.rule({ conditions, event }) | ||
engine = engineFactory() | ||
engine.addRule(rule) | ||
engine.addFact('age', ageSpy) | ||
engine.on('action', actionSpy) | ||
engine.on('event', eventSpy) | ||
}) | ||
@@ -39,3 +39,3 @@ | ||
await engine.run() | ||
expect(actionSpy).to.have.been.calledWith(action) | ||
expect(eventSpy).to.have.been.calledWith(event) | ||
}) | ||
@@ -46,3 +46,3 @@ | ||
engine.run() | ||
expect(actionSpy).to.not.have.been.calledWith(action) | ||
expect(eventSpy).to.not.have.been.calledWith(event) | ||
}) | ||
@@ -63,3 +63,3 @@ }) | ||
} | ||
let action = { | ||
let event = { | ||
type: 'ageTrigger', | ||
@@ -70,10 +70,10 @@ params: { | ||
} | ||
let actionSpy = sinon.spy() | ||
let eventSpy = sinon.spy() | ||
let ageSpy = sinon.stub() | ||
let segmentSpy = sinon.stub() | ||
beforeEach(() => { | ||
actionSpy.reset() | ||
eventSpy.reset() | ||
ageSpy.reset() | ||
segmentSpy.reset() | ||
let rule = factories.rule({ conditions, action }) | ||
let rule = factories.rule({ conditions, event }) | ||
engine = engineFactory() | ||
@@ -83,10 +83,10 @@ engine.addRule(rule) | ||
engine.addFact('age', ageSpy) | ||
engine.on('action', actionSpy) | ||
engine.on('event', eventSpy) | ||
}) | ||
it('emits an action when any condition is met', async () => { | ||
it('emits an event when any condition is met', async () => { | ||
segmentSpy.returns('north-american') | ||
ageSpy.returns(25) | ||
await engine.run() | ||
expect(actionSpy).to.have.been.calledWith(action) | ||
expect(eventSpy).to.have.been.calledWith(event) | ||
@@ -96,3 +96,3 @@ segmentSpy.returns('european') | ||
await engine.run() | ||
expect(actionSpy).to.have.been.calledWith(action) | ||
expect(eventSpy).to.have.been.calledWith(event) | ||
}) | ||
@@ -104,5 +104,5 @@ | ||
await engine.run() | ||
expect(actionSpy).to.not.have.been.calledWith(action) | ||
expect(eventSpy).to.not.have.been.calledWith(event) | ||
}) | ||
}) | ||
}) |
'use strict' | ||
import engineFactory from '../src/json-rules-engine' | ||
import engineFactory from '../src/index' | ||
import sinon from 'sinon' | ||
@@ -9,4 +9,4 @@ | ||
let action = { type: 'setDrinkingFlag' } | ||
let collegeSeniorAction = { type: 'isCollegeSenior' } | ||
let event = { type: 'setDrinkingFlag' } | ||
let collegeSeniorEvent = { type: 'isCollegeSenior' } | ||
let conditions = { | ||
@@ -21,15 +21,15 @@ any: [{ | ||
let factSpy = sinon.stub().returns(22) | ||
let actionSpy = sinon.spy() | ||
let eventSpy = sinon.spy() | ||
function setup (factOptions) { | ||
factSpy.reset() | ||
actionSpy.reset() | ||
eventSpy.reset() | ||
engine = engineFactory() | ||
let determineDrinkingAge = factories.rule({ conditions, action, priority: 100 }) | ||
let determineDrinkingAge = factories.rule({ conditions, event, priority: 100 }) | ||
engine.addRule(determineDrinkingAge) | ||
let determineCollegeSenior = factories.rule({ conditions, action: collegeSeniorAction, priority: 1 }) | ||
let determineCollegeSenior = factories.rule({ conditions, event: collegeSeniorEvent, priority: 1 }) | ||
engine.addRule(determineCollegeSenior) | ||
let over20 = factories.rule({ conditions, action: collegeSeniorAction, priority: 50 }) | ||
let over20 = factories.rule({ conditions, event: collegeSeniorEvent, priority: 50 }) | ||
engine.addRule(over20) | ||
engine.addFact('age', factOptions, factSpy) | ||
engine.on('action', actionSpy) | ||
engine.addFact('age', factSpy, factOptions) | ||
engine.on('event', eventSpy) | ||
} | ||
@@ -40,3 +40,3 @@ | ||
await engine.run() | ||
expect(actionSpy).to.have.been.calledThrice | ||
expect(eventSpy).to.have.been.calledThrice | ||
expect(factSpy).to.have.been.calledOnce | ||
@@ -48,5 +48,5 @@ }) | ||
await engine.run() | ||
expect(actionSpy).to.have.been.calledThrice | ||
expect(eventSpy).to.have.been.calledThrice | ||
expect(factSpy).to.have.been.calledThrice | ||
}) | ||
}) |
'use strict' | ||
import engineFactory from '../src/json-rules-engine' | ||
import engineFactory from '../src/index' | ||
import sinon from 'sinon' | ||
@@ -8,5 +8,5 @@ | ||
let engine | ||
let action = { type: 'adult-human-admins' } | ||
let event = { type: 'adult-human-admins' } | ||
let actionSpy = sinon.spy() | ||
let eventSpy = sinon.spy() | ||
let ageStub = sinon.stub() | ||
@@ -18,3 +18,3 @@ let segmentStub = sinon.stub() | ||
segmentStub.reset() | ||
actionSpy.reset() | ||
eventSpy.reset() | ||
engine = engineFactory() | ||
@@ -29,3 +29,3 @@ | ||
} | ||
let rule = factories.rule({ conditions, action, priority: 100 }) | ||
let rule = factories.rule({ conditions, event, priority: 100 }) | ||
engine.addRule(rule) | ||
@@ -40,7 +40,7 @@ | ||
} | ||
rule = factories.rule({ conditions, action }) | ||
rule = factories.rule({ conditions, event }) | ||
engine.addRule(rule) | ||
engine.addFact('age', { priority: 100 }, ageStub) | ||
engine.addFact('segment', { priority: 50 }, segmentStub) | ||
engine.addFact('age', ageStub, { priority: 100 }) | ||
engine.addFact('segment', segmentStub, { priority: 50 }) | ||
} | ||
@@ -52,8 +52,8 @@ | ||
ageStub.returns(20) // success | ||
engine.on('action', (action, engine) => { | ||
actionSpy() | ||
engine.on('event', (event, engine) => { | ||
eventSpy() | ||
engine.stop() | ||
}) | ||
await engine.run() | ||
expect(actionSpy).to.have.been.calledOnce | ||
expect(eventSpy).to.have.been.calledOnce | ||
expect(ageStub).to.have.been.calledOnce | ||
@@ -60,0 +60,0 @@ expect(segmentStub).to.not.have.been.called |
'use strict' | ||
import engineFactory, { Fact, Rule } from '../src/json-rules-engine' | ||
import engineFactory, { Fact, Rule } from '../src/index' | ||
describe('Engine: custom properties', () => { | ||
let engine | ||
let action = { type: 'generic' } | ||
let event = { type: 'generic' } | ||
@@ -12,3 +12,3 @@ describe('all conditions', () => { | ||
engine = engineFactory() | ||
let fact = new Fact('age') | ||
let fact = new Fact('age', 12) | ||
fact.customId = 'uuid' | ||
@@ -31,3 +31,3 @@ engine.addFact(fact) | ||
} | ||
let rule = factories.rule({ conditions, action }) | ||
let rule = factories.rule({ conditions, event }) | ||
engine.addRule(rule) | ||
@@ -47,3 +47,3 @@ expect(engine.rules[0].conditions).to.have.property('customId') | ||
} | ||
let rule = factories.rule({ conditions, action }) | ||
let rule = factories.rule({ conditions, event }) | ||
engine.addRule(rule) | ||
@@ -61,3 +61,3 @@ expect(engine.rules[0].conditions['all'][0]).to.have.property('customId') | ||
.setConditions(ruleProperties.conditions) | ||
.setAction(ruleProperties.action) | ||
.setEvent(ruleProperties.event) | ||
rule.customId = 'uuid' | ||
@@ -64,0 +64,0 @@ engine.addRule(rule) |
'use strict' | ||
import engineFactory from '../src/json-rules-engine' | ||
import engineFactory from '../src/index' | ||
@@ -8,3 +8,3 @@ describe('Engine: failure', () => { | ||
let action = { type: 'generic' } | ||
let event = { type: 'generic' } | ||
let conditions = { | ||
@@ -19,3 +19,3 @@ any: [{ | ||
engine = engineFactory() | ||
let determineDrinkingAgeRule = factories.rule({ conditions, action }) | ||
let determineDrinkingAgeRule = factories.rule({ conditions, event }) | ||
engine.addRule(determineDrinkingAgeRule) | ||
@@ -22,0 +22,0 @@ engine.addFact('age', function (params, engine) { |
'use strict' | ||
import engineFactory from '../src/json-rules-engine' | ||
import engineFactory from '../src/index' | ||
import sinon from 'sinon' | ||
@@ -8,5 +8,5 @@ | ||
let engine | ||
let action = { type: 'adult-human-admins' } | ||
let event = { type: 'adult-human-admins' } | ||
let actionSpy = sinon.spy() | ||
let eventSpy = sinon.spy() | ||
let failureSpy = sinon.spy() | ||
@@ -21,12 +21,12 @@ let ageStub = sinon.stub() | ||
accountTypeStub.reset() | ||
actionSpy.reset() | ||
eventSpy.reset() | ||
failureSpy.reset() | ||
engine = engineFactory() | ||
let rule = factories.rule({ conditions, action }) | ||
let rule = factories.rule({ conditions, event }) | ||
engine.addRule(rule) | ||
engine.addFact('age', { priority: 100 }, ageStub) | ||
engine.addFact('segment', { priority: 50 }, segmentStub) | ||
engine.addFact('accountType', { priority: 25 }, accountTypeStub) | ||
engine.on('action', actionSpy) | ||
engine.addFact('age', ageStub, { priority: 100 }) | ||
engine.addFact('segment', segmentStub, { priority: 50 }) | ||
engine.addFact('accountType', accountTypeStub, { priority: 25 }) | ||
engine.on('event', eventSpy) | ||
engine.on('failure', failureSpy) | ||
@@ -57,3 +57,3 @@ } | ||
expect(failureSpy).to.have.been.called | ||
expect(actionSpy).to.not.have.been.called | ||
expect(eventSpy).to.not.have.been.called | ||
expect(ageStub).to.have.been.calledOnce | ||
@@ -70,3 +70,3 @@ expect(segmentStub).to.not.have.been.called | ||
expect(failureSpy).to.have.been.called | ||
expect(actionSpy).to.not.have.been.called | ||
expect(eventSpy).to.not.have.been.called | ||
expect(ageStub).to.have.been.calledOnce | ||
@@ -104,3 +104,3 @@ expect(segmentStub).to.have.been.calledOnce | ||
expect(failureSpy).to.have.been.called | ||
expect(actionSpy).to.not.have.been.called | ||
expect(eventSpy).to.not.have.been.called | ||
expect(ageStub).to.have.been.calledOnce | ||
@@ -133,3 +133,3 @@ expect(segmentStub).to.have.been.calledOnce | ||
await engine.run() | ||
expect(actionSpy).to.have.been.calledOnce | ||
expect(eventSpy).to.have.been.calledOnce | ||
expect(failureSpy).to.not.have.been.called | ||
@@ -146,3 +146,3 @@ expect(ageStub).to.have.been.calledOnce | ||
await engine.run() | ||
expect(actionSpy).to.have.been.calledOnce | ||
expect(eventSpy).to.have.been.calledOnce | ||
expect(failureSpy).to.not.have.been.called | ||
@@ -181,3 +181,3 @@ expect(ageStub).to.have.been.calledOnce | ||
expect(failureSpy).to.not.have.been.called | ||
expect(actionSpy).to.have.been.called | ||
expect(eventSpy).to.have.been.called | ||
expect(ageStub).to.have.been.calledOnce | ||
@@ -184,0 +184,0 @@ expect(segmentStub).to.have.been.calledOnce |
'use strict' | ||
import sinon from 'sinon' | ||
import engineFactory from '../src/json-rules-engine' | ||
import engineFactory from '../src/index' | ||
@@ -20,3 +20,3 @@ const CHILD = 14 | ||
let engine | ||
let action = { | ||
let event = { | ||
type: 'ageTrigger', | ||
@@ -38,10 +38,10 @@ params: { | ||
} | ||
let actionSpy = sinon.spy() | ||
let eventSpy = sinon.spy() | ||
function setup (conditions = baseConditions) { | ||
actionSpy.reset() | ||
eventSpy.reset() | ||
engine = engineFactory() | ||
let rule = factories.rule({ conditions, action }) | ||
let rule = factories.rule({ conditions, event }) | ||
engine.addRule(rule) | ||
engine.addFact('eligibility', eligibility) | ||
engine.on('action', actionSpy) | ||
engine.on('event', eventSpy) | ||
} | ||
@@ -53,3 +53,3 @@ | ||
await engine.run() | ||
expect(actionSpy).to.have.been.calledWith(action) | ||
expect(eventSpy).to.have.been.calledWith(event) | ||
}) | ||
@@ -62,3 +62,3 @@ | ||
await engine.run() | ||
expect(actionSpy).to.not.have.been.called | ||
expect(eventSpy).to.not.have.been.called | ||
}) | ||
@@ -79,3 +79,3 @@ }) | ||
await engine.run() | ||
expect(actionSpy).to.have.been.called | ||
expect(eventSpy).to.have.been.called | ||
}) | ||
@@ -92,3 +92,3 @@ }) | ||
await engine.run() | ||
expect(actionSpy).to.have.been.called | ||
expect(eventSpy).to.have.been.called | ||
}) | ||
@@ -103,5 +103,5 @@ | ||
await engine.run() | ||
expect(actionSpy).to.not.have.been.called | ||
expect(eventSpy).to.not.have.been.called | ||
}) | ||
}) | ||
}) |
'use strict' | ||
import engineFactory from '../src/json-rules-engine' | ||
import { Fact } from '../src/json-rules-engine' | ||
import engineFactory from '../src/index' | ||
import { Fact } from '../src/index' | ||
import sinon from 'sinon' | ||
@@ -9,3 +9,3 @@ | ||
let engine | ||
let action = { type: 'early-twenties' } | ||
let event = { type: 'early-twenties' } | ||
let conditions = { | ||
@@ -29,3 +29,3 @@ all: [{ | ||
let actionSpy = sinon.spy() | ||
let eventSpy = sinon.spy() | ||
let demographicDataSpy = sinon.spy() | ||
@@ -36,3 +36,3 @@ let demographicSpy = sinon.spy() | ||
demographicDataSpy.reset() | ||
actionSpy.reset() | ||
eventSpy.reset() | ||
@@ -56,7 +56,7 @@ let demographicsDataDefinition = async (params, engine) => { | ||
engine = engineFactory() | ||
let rule = factories.rule({ conditions, action }) | ||
let rule = factories.rule({ conditions, event }) | ||
engine.addRule(rule) | ||
engine.addFact(demographicsFact) | ||
engine.addFact(demographicsDataFact) | ||
engine.on('action', actionSpy) | ||
engine.on('event', eventSpy) | ||
}) | ||
@@ -67,3 +67,3 @@ | ||
await engine.run() | ||
expect(actionSpy).to.have.been.calledOnce | ||
expect(eventSpy).to.have.been.calledOnce | ||
expect(demographicDataSpy).to.have.been.calledOnce | ||
@@ -86,7 +86,7 @@ expect(demographicSpy).to.have.been.calledTwice | ||
} | ||
let rule = factories.rule({ conditions, action }) | ||
let rule = factories.rule({ conditions, event }) | ||
engine.addRule(rule) | ||
await engine.run() | ||
expect(actionSpy).to.have.been.calledTwice | ||
expect(eventSpy).to.have.been.calledTwice | ||
expect(demographicDataSpy).to.have.been.calledOnce | ||
@@ -93,0 +93,0 @@ expect(demographicSpy).to.have.been.calledTwice |
'use strict' | ||
import engineFactory from '../src/json-rules-engine' | ||
import engineFactory from '../src/index' | ||
import sinon from 'sinon' | ||
@@ -9,3 +9,3 @@ | ||
let action = { type: 'generic' } | ||
let event = { type: 'generic' } | ||
let conditions = { | ||
@@ -20,3 +20,3 @@ any: [{ | ||
engine = engineFactory() | ||
let determineDrinkingAgeRule = factories.rule({ conditions, action }) | ||
let determineDrinkingAgeRule = factories.rule({ conditions, event }) | ||
engine.addRule(determineDrinkingAgeRule) | ||
@@ -23,0 +23,0 @@ engine.addFact('age', 10) |
'use strict' | ||
import engineFactory from '../src/json-rules-engine' | ||
import engineFactory from '../src/index' | ||
import sinon from 'sinon' | ||
@@ -8,3 +8,3 @@ | ||
let engine | ||
let action = { type: 'early-twenties' } | ||
let event = { type: 'early-twenties' } | ||
let conditions = { | ||
@@ -26,7 +26,7 @@ all: [{ | ||
let actionSpy = sinon.spy() | ||
let eventSpy = sinon.spy() | ||
let factSpy = sinon.spy() | ||
function setup (factOptions) { | ||
factSpy.reset() | ||
actionSpy.reset() | ||
eventSpy.reset() | ||
@@ -39,6 +39,6 @@ let factDefinition = () => { | ||
engine = engineFactory() | ||
let rule = factories.rule({ conditions, action }) | ||
let rule = factories.rule({ conditions, event }) | ||
engine.addRule(rule) | ||
engine.addFact('age', factOptions, factDefinition) | ||
engine.on('action', actionSpy) | ||
engine.addFact('age', factDefinition, factOptions) | ||
engine.on('event', eventSpy) | ||
} | ||
@@ -50,3 +50,3 @@ | ||
await engine.run() | ||
expect(actionSpy).to.have.been.calledOnce | ||
expect(eventSpy).to.have.been.calledOnce | ||
expect(factSpy).to.have.been.calledThrice | ||
@@ -58,3 +58,3 @@ }) | ||
await engine.run() | ||
expect(actionSpy).to.have.been.calledOnce | ||
expect(eventSpy).to.have.been.calledOnce | ||
expect(factSpy).to.have.been.calledOnce | ||
@@ -74,7 +74,7 @@ }) | ||
} | ||
let rule = factories.rule({ conditions, action }) | ||
let rule = factories.rule({ conditions, event }) | ||
engine.addRule(rule) | ||
await engine.run() | ||
expect(actionSpy).to.have.been.calledTwice | ||
expect(eventSpy).to.have.been.calledTwice | ||
expect(factSpy).to.have.been.calledOnce | ||
@@ -81,0 +81,0 @@ }) |
'use strict' | ||
import engineFactory from '../src/json-rules-engine' | ||
import engineFactory from '../src/index' | ||
import sinon from 'sinon' | ||
@@ -8,3 +8,3 @@ | ||
let engine | ||
let action = { type: 'middle-income-adult' } | ||
let event = { type: 'middle-income-adult' } | ||
let nestedAnyCondition = { | ||
@@ -39,10 +39,10 @@ all: [ | ||
let actionSpy = sinon.spy() | ||
let eventSpy = sinon.spy() | ||
function setup (conditions = nestedAnyCondition) { | ||
actionSpy.reset() | ||
eventSpy.reset() | ||
engine = engineFactory() | ||
let rule = factories.rule({ conditions, action }) | ||
let rule = factories.rule({ conditions, event }) | ||
engine.addRule(rule) | ||
engine.on('action', actionSpy) | ||
engine.on('event', eventSpy) | ||
} | ||
@@ -57,3 +57,3 @@ | ||
await engine.run() | ||
expect(actionSpy).to.have.been.calledOnce | ||
expect(eventSpy).to.have.been.calledOnce | ||
}) | ||
@@ -67,3 +67,3 @@ | ||
await engine.run() | ||
expect(actionSpy).to.not.have.been.calledOnce | ||
expect(eventSpy).to.not.have.been.calledOnce | ||
}) | ||
@@ -108,3 +108,3 @@ }) | ||
await engine.run() | ||
expect(actionSpy).to.have.been.calledOnce | ||
expect(eventSpy).to.have.been.calledOnce | ||
}) | ||
@@ -118,3 +118,3 @@ | ||
await engine.run() | ||
expect(actionSpy).to.not.have.been.calledOnce | ||
expect(eventSpy).to.not.have.been.calledOnce | ||
}) | ||
@@ -152,3 +152,3 @@ }) | ||
await engine.run() | ||
expect(actionSpy).to.have.been.calledOnce | ||
expect(eventSpy).to.have.been.calledOnce | ||
}) | ||
@@ -161,5 +161,5 @@ | ||
await engine.run() | ||
expect(actionSpy).to.not.have.been.calledOnce | ||
expect(eventSpy).to.not.have.been.calledOnce | ||
}) | ||
}) | ||
}) |
'use strict' | ||
import engineFactory from '../src/json-rules-engine' | ||
import engineFactory from '../src/index' | ||
import sinon from 'sinon' | ||
@@ -9,4 +9,4 @@ | ||
let action = { type: 'setDrinkingFlag' } | ||
let collegeSeniorAction = { type: 'isCollegeSenior' } | ||
let event = { type: 'setDrinkingFlag' } | ||
let collegeSeniorEvent = { type: 'isCollegeSenior' } | ||
let conditions = { | ||
@@ -21,15 +21,15 @@ any: [{ | ||
let factSpy = sinon.stub().returns(22) | ||
let actionSpy = sinon.spy() | ||
let eventSpy = sinon.spy() | ||
function setup () { | ||
factSpy.reset() | ||
actionSpy.reset() | ||
eventSpy.reset() | ||
engine = engineFactory() | ||
let over20 = factories.rule({ conditions, action: collegeSeniorAction, priority: 50 }) | ||
let over20 = factories.rule({ conditions, event: collegeSeniorEvent, priority: 50 }) | ||
engine.addRule(over20) | ||
let determineDrinkingAge = factories.rule({ conditions, action, priority: 100 }) | ||
let determineDrinkingAge = factories.rule({ conditions, event, priority: 100 }) | ||
engine.addRule(determineDrinkingAge) | ||
let determineCollegeSenior = factories.rule({ conditions, action: collegeSeniorAction, priority: 1 }) | ||
let determineCollegeSenior = factories.rule({ conditions, event: collegeSeniorEvent, priority: 1 }) | ||
engine.addRule(determineCollegeSenior) | ||
engine.addFact('age', factSpy) | ||
engine.on('action', actionSpy) | ||
engine.on('event', eventSpy) | ||
} | ||
@@ -36,0 +36,0 @@ |
'use strict' | ||
import engineFactory from '../src/json-rules-engine' | ||
import engineFactory from '../src/index' | ||
import sinon from 'sinon' | ||
@@ -9,3 +9,3 @@ | ||
let action = { type: 'generic' } | ||
let event = { type: 'generic' } | ||
let conditions = { | ||
@@ -18,7 +18,7 @@ any: [{ | ||
} | ||
let actionSpy = sinon.spy() | ||
let eventSpy = sinon.spy() | ||
let factSpy = sinon.spy() | ||
beforeEach(() => { | ||
factSpy.reset() | ||
actionSpy.reset() | ||
eventSpy.reset() | ||
@@ -31,6 +31,6 @@ let factDefinition = () => { | ||
engine = engineFactory() | ||
let rule = factories.rule({ conditions, action }) | ||
let rule = factories.rule({ conditions, event }) | ||
engine.addRule(rule) | ||
engine.addFact('age', factDefinition) | ||
engine.on('action', actionSpy) | ||
engine.on('event', eventSpy) | ||
}) | ||
@@ -42,3 +42,3 @@ | ||
await engine.run() | ||
expect(actionSpy).to.have.been.calledThrice | ||
expect(eventSpy).to.have.been.calledThrice | ||
expect(factSpy).to.have.been.calledThrice | ||
@@ -51,5 +51,5 @@ }) | ||
await engine.run() | ||
expect(actionSpy).to.have.been.calledThrice | ||
expect(eventSpy).to.have.been.calledThrice | ||
expect(factSpy).to.have.been.calledTwice | ||
}) | ||
}) |
'use strict' | ||
import sinon from 'sinon' | ||
import engineFactory from '../src/json-rules-engine' | ||
import { Fact } from '../src/json-rules-engine' | ||
import { Rule } from '../src/json-rules-engine' | ||
import engineFactory from '../src/index' | ||
import { Fact } from '../src/index' | ||
import { Rule } from '../src/index' | ||
@@ -64,8 +64,8 @@ describe('Engine', () => { | ||
it('.action', () => { | ||
it('.event', () => { | ||
let rule = factories.rule() | ||
delete rule.action | ||
delete rule.event | ||
expect(() => { | ||
engine.addRule(rule) | ||
}).to.throw(/Missing key "action"/) | ||
}).to.throw(/Missing key "event"/) | ||
}) | ||
@@ -92,3 +92,3 @@ }) | ||
let options = { cache: false } | ||
engine.addFact(FACT_NAME, options, FACT_VALUE) | ||
engine.addFact(FACT_NAME, FACT_VALUE, options) | ||
assertFact(engine) | ||
@@ -109,5 +109,5 @@ expect(engine.facts.get(FACT_NAME).value).to.equal(FACT_VALUE) | ||
let options = { cache: false } | ||
engine.addFact(FACT_NAME, options, async (params, engine) => { | ||
engine.addFact(FACT_NAME, async (params, engine) => { | ||
return FACT_VALUE | ||
}) | ||
}, options) | ||
assertFact(engine) | ||
@@ -120,3 +120,3 @@ expect(engine.facts.get(FACT_NAME).options).to.eql(options) | ||
let options = { cache: false } | ||
let fact = new Fact(FACT_NAME, options) | ||
let fact = new Fact(FACT_NAME, 50, options) | ||
engine.addFact(fact) | ||
@@ -138,4 +138,4 @@ assertFact(engine) | ||
} | ||
let action = { type: 'generic' } | ||
let rule = factories.rule({ conditions, action }) | ||
let event = { type: 'generic' } | ||
let rule = factories.rule({ conditions, event }) | ||
engine.addRule(rule) | ||
@@ -151,5 +151,5 @@ engine.addFact('age', 20) | ||
it('changes the status to "RUNNING"', () => { | ||
let actionSpy = sinon.spy() | ||
engine.on('action', (action, engine) => { | ||
actionSpy() | ||
let eventSpy = sinon.spy() | ||
engine.on('event', (event, engine) => { | ||
eventSpy() | ||
expect(engine.status).to.equal('RUNNING') | ||
@@ -193,6 +193,6 @@ }) | ||
factSpy.reset() | ||
engine.addFact('foo', factOptions, async (params, facts) => { | ||
engine.addFact('foo', async (params, facts) => { | ||
factSpy() | ||
return 'unknown' | ||
}) | ||
}, factOptions) | ||
} | ||
@@ -199,0 +199,0 @@ |
@@ -5,2 +5,3 @@ 'use strict' | ||
import Rule from '../src/rule' | ||
import sinon from 'sinon' | ||
@@ -15,3 +16,3 @@ describe('Rule', () => { | ||
describe('constructor()', () => { | ||
it('can be initialized with priority, conditions, and action', () => { | ||
it('can be initialized with priority, conditions, and event', () => { | ||
let condition = { | ||
@@ -25,3 +26,3 @@ all: [ Object.assign({}, conditionBase) ] | ||
conditions: condition, | ||
action: { | ||
event: { | ||
type: 'awesome' | ||
@@ -33,3 +34,3 @@ } | ||
expect(rule.conditions).to.eql(opts.conditions) | ||
expect(rule.action).to.eql(opts.action) | ||
expect(rule.event).to.eql(opts.event) | ||
}) | ||
@@ -46,3 +47,3 @@ | ||
conditions: condition, | ||
action: { | ||
event: { | ||
type: 'awesome' | ||
@@ -55,3 +56,3 @@ } | ||
expect(rule.conditions).to.eql(opts.conditions) | ||
expect(rule.action).to.eql(opts.action) | ||
expect(rule.event).to.eql(opts.event) | ||
}) | ||
@@ -104,6 +105,6 @@ }) | ||
let engine = new Engine() | ||
engine.addFact('state', { priority: 500 }, async () => {}) | ||
engine.addFact('segment', { priority: 50 }, async () => {}) | ||
engine.addFact('accountType', { priority: 25 }, async () => {}) | ||
engine.addFact('age', { priority: 100 }, async () => {}) | ||
engine.addFact('state', async () => {}, { priority: 500 }) | ||
engine.addFact('segment', async () => {}, { priority: 50 }) | ||
engine.addFact('accountType', async () => {}, { priority: 25 }) | ||
engine.addFact('age', async () => {}, { priority: 100 }) | ||
let rule = new Rule() | ||
@@ -120,2 +121,80 @@ rule.setEngine(engine) | ||
}) | ||
describe('evaluate()', () => { | ||
it('evalutes truthy when there are no conditions', async () => { | ||
let eventSpy = sinon.spy() | ||
let engine = new Engine() | ||
let rule = new Rule() | ||
rule.setConditions({ | ||
all: [] | ||
}) | ||
engine.addRule(rule) | ||
engine.on('event', eventSpy) | ||
await engine.run() | ||
expect(eventSpy).to.have.been.calledOnce | ||
}) | ||
}) | ||
describe('toJSON() and fromJSON()', () => { | ||
let priority = 50 | ||
let event = { | ||
type: 'to-json!', | ||
params: { id: 1 } | ||
} | ||
let conditions = { | ||
priority: 1, | ||
all: [{ | ||
value: 10, | ||
operator: 'equals', | ||
fact: 'userId', | ||
params: { | ||
foo: true | ||
} | ||
}] | ||
} | ||
let rule | ||
beforeEach(() => { | ||
rule = new Rule() | ||
rule.setConditions(conditions) | ||
rule.setPriority(priority) | ||
rule.setEvent(event) | ||
}) | ||
it('serializes itself', () => { | ||
let json = rule.toJSON(false) | ||
expect(Object.keys(json).length).to.equal(3) | ||
expect(json.conditions).to.eql(conditions) | ||
expect(json.priority).to.eql(priority) | ||
expect(json.event).to.eql(event) | ||
}) | ||
it('serializes itself as json', () => { | ||
let jsonString = rule.toJSON() | ||
expect(jsonString).to.be.a('string') | ||
let json = JSON.parse(jsonString) | ||
expect(Object.keys(json).length).to.equal(3) | ||
expect(json.conditions).to.eql(conditions) | ||
expect(json.priority).to.eql(priority) | ||
expect(json.event).to.eql(event) | ||
}) | ||
it('rehydrates itself using a JSON string', () => { | ||
let jsonString = rule.toJSON() | ||
expect(jsonString).to.be.a('string') | ||
let hydratedRule = new Rule(jsonString) | ||
expect(hydratedRule.conditions).to.eql(rule.conditions) | ||
expect(hydratedRule.priority).to.eql(rule.priority) | ||
expect(hydratedRule.event).to.eql(rule.event) | ||
}) | ||
it('rehydrates itself using an object from JSON.parse()', () => { | ||
let jsonString = rule.toJSON() | ||
expect(jsonString).to.be.a('string') | ||
let json = JSON.parse(jsonString) | ||
let hydratedRule = new Rule(json) | ||
expect(hydratedRule.conditions).to.eql(rule.conditions) | ||
expect(hydratedRule.priority).to.eql(rule.priority) | ||
expect(hydratedRule.event).to.eql(rule.event) | ||
}) | ||
}) | ||
}) |
@@ -19,3 +19,3 @@ 'use strict' | ||
}, | ||
action: options.action || { | ||
event: options.event || { | ||
type: 'pointCapReached', | ||
@@ -22,0 +22,0 @@ params: { |
65269
31
1697
248