json-rules-engine
Advanced tools
Comparing version 1.2.1 to 1.3.0
@@ -0,1 +1,22 @@ | ||
1.3.0 / 2016-10-24 | ||
================== | ||
* Rule event emissions | ||
* Rule chaining | ||
1.2.1 / 2016-10-22 | ||
================== | ||
* Use Array.indexOf instead of Array.includes for older node version compatibility | ||
1.2.0 / 2016-09-13 | ||
================== | ||
* Fact path support | ||
1.1.0 / 2016-09-11 | ||
================== | ||
* Custom operator support | ||
1.0.4 / 2016-06-18 | ||
@@ -2,0 +23,0 @@ ================== |
@@ -17,2 +17,4 @@ 'use strict'; | ||
var _events = require('events'); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
@@ -24,5 +26,11 @@ | ||
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } | ||
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } | ||
var debug = require('debug')('json-rules-engine'); | ||
var Rule = function () { | ||
var Rule = function (_EventEmitter) { | ||
_inherits(Rule, _EventEmitter); | ||
/** | ||
@@ -41,2 +49,4 @@ * returns a new Rule instance | ||
var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(Rule).call(this)); | ||
if (typeof options === 'string') { | ||
@@ -46,10 +56,17 @@ options = JSON.parse(options); | ||
if (options && options.conditions) { | ||
this.setConditions(options.conditions); | ||
_this.setConditions(options.conditions); | ||
} | ||
if (options && options.onSuccess) { | ||
_this.on('success', options.onSuccess); | ||
} | ||
if (options && options.onFailure) { | ||
_this.on('failure', options.onFailure); | ||
} | ||
var priority = options && options.priority || 1; | ||
this.setPriority(priority); | ||
_this.setPriority(priority); | ||
var event = options && options.event || { type: 'unknown' }; | ||
this.setEvent(event); | ||
_this.setEvent(event); | ||
return _this; | ||
} | ||
@@ -141,3 +158,3 @@ | ||
value: function prioritizeConditions(conditions) { | ||
var _this = this; | ||
var _this2 = this; | ||
@@ -149,3 +166,3 @@ var factSets = conditions.reduce(function (sets, condition) { | ||
if (!priority) { | ||
var fact = _this.engine.getFact(condition.fact); | ||
var fact = _this2.engine.getFact(condition.fact); | ||
priority = fact && fact.priority || 1; | ||
@@ -174,3 +191,3 @@ } | ||
var _ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee6(almanac) { | ||
var _this2 = this; | ||
var _this3 = this; | ||
@@ -189,3 +206,3 @@ var evaluateCondition, evaluateConditions, prioritizeAndRun, any, all; | ||
var _ref2 = _asyncToGenerator(regeneratorRuntime.mark(function _callee(condition) { | ||
var comparisonValue, subConditions; | ||
var comparisonValue, subConditions, passes; | ||
return regeneratorRuntime.wrap(function _callee$(_context) { | ||
@@ -236,5 +253,16 @@ while (1) { | ||
case 17: | ||
return _context.abrupt('return', condition.evaluate(comparisonValue, _this2.engine.operators)); | ||
_context.next = 19; | ||
return condition.evaluate(comparisonValue, _this3.engine.operators); | ||
case 18: | ||
case 19: | ||
passes = _context.sent; | ||
if (passes) { | ||
_this3.emit('success', _this3.event, almanac); | ||
} else { | ||
_this3.emit('failure', _this3.event, almanac); | ||
} | ||
return _context.abrupt('return', passes); | ||
case 22: | ||
case 'end': | ||
@@ -244,3 +272,3 @@ return _context.stop(); | ||
} | ||
}, _callee, _this2); | ||
}, _callee, _this3); | ||
})); | ||
@@ -287,3 +315,3 @@ | ||
} | ||
}, _callee2, _this2); | ||
}, _callee2, _this3); | ||
})); | ||
@@ -328,3 +356,3 @@ | ||
} | ||
orderedSets = _this2.prioritizeConditions(conditions); | ||
orderedSets = _this3.prioritizeConditions(conditions); | ||
cursor = Promise.resolve(); | ||
@@ -359,3 +387,3 @@ | ||
} | ||
}, _callee3, _this2); | ||
}, _callee3, _this3); | ||
})); | ||
@@ -388,3 +416,3 @@ | ||
} | ||
}, _callee4, _this2); | ||
}, _callee4, _this3); | ||
})); | ||
@@ -417,3 +445,3 @@ | ||
} | ||
}, _callee5, _this2); | ||
}, _callee5, _this3); | ||
})); | ||
@@ -461,4 +489,4 @@ | ||
return Rule; | ||
}(); | ||
}(_events.EventEmitter); | ||
exports.default = Rule; |
@@ -25,2 +25,10 @@ # Almanac | ||
### almanac.addRuntimeFact(String factId, Mixed value) | ||
Sets a constant fact mid-run. Often used in conjunction with rule and engine event emissions. | ||
```js | ||
almanac.addRuntimeFact('account-id', 1) | ||
``` | ||
## Common Use Cases | ||
@@ -105,2 +113,37 @@ | ||
}) | ||
``` | ||
``` | ||
### Rule Chaining | ||
The `almanac.addRuntimeFact()` method may be used in conjunction with event emissions to | ||
set fact values during runtime, effectively enabling _rule-chaining_. Note that ordering | ||
of rule execution is enabled via the `priority` option, and is crucial component to propertly | ||
configuring rule chaining. | ||
```js | ||
engine.addRule({ | ||
conditions, | ||
event, | ||
onSuccess: function (event, almanac) { | ||
almanac.addRuntimeFact('rule-1-passed', true) // track that the rule passed | ||
}, | ||
onFailure: function (event, almanac) { | ||
almanac.addRuntimeFact('rule-1-passed', false) // track that the rule failed | ||
}, | ||
priority: 10 // a higher priority ensures this rule will be run prior to subsequent rules | ||
}) | ||
// in a later rule: | ||
engine.addRule({ | ||
conditions: { | ||
all: [{ | ||
fact: 'rule-1-passed', | ||
operator: 'equal', | ||
value: true | ||
} | ||
}, | ||
priority: 1 // lower priority ensures this is run AFTER its predecessor | ||
} | ||
``` | ||
See the [full example](../examples/07-rule-chaining.js) |
@@ -46,3 +46,5 @@ # Engine | ||
event: {}, | ||
priority: 1 | ||
priority: 1, // optional, default: 1 | ||
onSuccess: function (event, almanac) {}, // optional | ||
onFailure: function (event, almanac) {}, // optional | ||
}) | ||
@@ -49,0 +51,0 @@ |
@@ -79,2 +79,25 @@ # Rules | ||
### Events | ||
Listen for 'success' and 'failure' events emitted when rule is evaluated. | ||
#### ```rule.on('success', Function(Object event, Almanac almanac))``` | ||
```js | ||
// whenever rule is evaluated and the conditions pass, 'success' will trigger | ||
rule.on('success', function(event, almanac) { | ||
console.log(event) // { type: 'my-event', params: { id: 1 } | ||
}) | ||
``` | ||
#### ```rule.on('failure', Function(Object event, Almanac almanac))``` | ||
Companion to 'success', except fires when the rule fails. | ||
```js | ||
engine.on('failure', function(event, almanac) { | ||
console.log(event) // { type: 'my-event', params: { id: 1 } | ||
}) | ||
``` | ||
## Conditions | ||
@@ -81,0 +104,0 @@ |
@@ -7,6 +7,6 @@ 'use strict' | ||
* Usage: | ||
* node ./examples/hello-world.js | ||
* node ./examples/01-hello-world.js | ||
* | ||
* For detailed output: | ||
* DEBUG=json-rules-engine node ./examples/hello-world.js | ||
* DEBUG=json-rules-engine node ./examples/01-hello-world.js | ||
*/ | ||
@@ -13,0 +13,0 @@ |
@@ -7,6 +7,6 @@ 'use strict' | ||
* Usage: | ||
* node ./examples/nested-boolean-logic.js | ||
* node ./examples/02-nested-boolean-logic.js | ||
* | ||
* For detailed output: | ||
* DEBUG=json-rules-engine node ./examples/nested-boolean-logic.js | ||
* DEBUG=json-rules-engine node ./examples/02-nested-boolean-logic.js | ||
*/ | ||
@@ -13,0 +13,0 @@ |
@@ -8,6 +8,6 @@ 'use strict' | ||
* Usage: | ||
* node ./examples/dynamic-facts.js | ||
* node ./examples/03-dynamic-facts.js | ||
* | ||
* For detailed output: | ||
* DEBUG=json-rules-engine node ./examples/dynamic-facts.js | ||
* DEBUG=json-rules-engine node ./examples/03-dynamic-facts.js | ||
*/ | ||
@@ -14,0 +14,0 @@ |
@@ -9,6 +9,6 @@ 'use strict' | ||
* Usage: | ||
* node ./examples/fact-dependency.js | ||
* node ./examples/04-fact-dependency.js | ||
* | ||
* For detailed output: | ||
* DEBUG=json-rules-engine node ./examples/fact-dependency.js | ||
* DEBUG=json-rules-engine node ./examples/04-fact-dependency.js | ||
*/ | ||
@@ -15,0 +15,0 @@ |
@@ -7,6 +7,6 @@ 'use strict' | ||
* Usage: | ||
* node ./examples/optimize-runtime-with-fact-priority.js | ||
* node ./examples/05-optimize-runtime-with-fact-priority.js | ||
* | ||
* For detailed output: | ||
* DEBUG=json-rules-engine node ./examples/optimize-runtime-with-fact-priority.js | ||
* DEBUG=json-rules-engine node ./examples/05-optimize-runtime-with-fact-priority.js | ||
*/ | ||
@@ -13,0 +13,0 @@ |
@@ -13,6 +13,6 @@ 'use strict' | ||
* Usage: | ||
* node ./examples/custom-operators.js | ||
* node ./examples/06-custom-operators.js | ||
* | ||
* For detailed output: | ||
* DEBUG=json-rules-engine node ./examples/custom-operators.js | ||
* DEBUG=json-rules-engine node ./examples/06-custom-operators.js | ||
*/ | ||
@@ -19,0 +19,0 @@ |
{ | ||
"name": "json-rules-engine", | ||
"version": "1.2.1", | ||
"version": "1.3.0", | ||
"description": "Rules Engine expressed in simple json", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
@@ -96,3 +96,3 @@ ![json-rules-engine](http://i.imgur.com/MAzq7l2.png) | ||
Run this example [here](./examples/nested-boolean-logic.js) | ||
This is available in the [examples](./examples/02-nested-boolean-logic.js) | ||
@@ -183,3 +183,3 @@ ## Advanced Example | ||
Run this example [here](./examples/nested-boolean-logic.js) | ||
This is available in the [examples](./examples/03-dynamic-facts) | ||
@@ -186,0 +186,0 @@ ## Docs |
'use strict' | ||
import engineFactory from '../src/index' | ||
import Almanac from '../src/almanac' | ||
import sinon from 'sinon' | ||
@@ -23,3 +25,4 @@ describe('Engine: event', () => { | ||
engine = engineFactory() | ||
let determineDrinkingAgeRule = factories.rule({ conditions, event, priority: 100 }) | ||
let ruleOptions = { conditions, event, priority: 100 } | ||
let determineDrinkingAgeRule = factories.rule(ruleOptions) | ||
engine.addRule(determineDrinkingAgeRule) | ||
@@ -29,59 +32,93 @@ engine.addFact('age', 21) | ||
it('passes the event type and params', (done) => { | ||
engine.on('success', function (a, engine) { | ||
try { | ||
expect(a).to.eql(event) | ||
expect(engine).to.eql(engine) | ||
} catch (e) { return done(e) } | ||
done() | ||
describe('engine events', () => { | ||
it('passes the event type and params', (done) => { | ||
engine.on('success', function (a, engine) { | ||
try { | ||
expect(a).to.eql(event) | ||
expect(engine).to.eql(engine) | ||
} catch (e) { return done(e) } | ||
done() | ||
}) | ||
engine.run() | ||
}) | ||
engine.run() | ||
}) | ||
it('emits using the event "type"', (done) => { | ||
engine.on('setDrinkingFlag', function (params, engine) { | ||
try { | ||
expect(params).to.eql(event.params) | ||
expect(engine).to.eql(engine) | ||
} catch (e) { return done(e) } | ||
done() | ||
it('emits using the event "type"', (done) => { | ||
engine.on('setDrinkingFlag', function (params, e) { | ||
try { | ||
expect(params).to.eql(event.params) | ||
expect(engine).to.eql(e) | ||
} catch (e) { return done(e) } | ||
done() | ||
}) | ||
engine.run() | ||
}) | ||
engine.run() | ||
}) | ||
it('allows facts to be added by the event handler, affecting subsequent rules', () => { | ||
let drinkOrderParams = { wine: 'merlot', quantity: 2 } | ||
let drinkOrderEvent = { | ||
type: 'offerDrink', | ||
params: drinkOrderParams | ||
} | ||
let drinkOrderConditions = { | ||
any: [{ | ||
fact: 'canOrderDrinks', | ||
operator: 'equal', | ||
value: true | ||
}] | ||
} | ||
let drinkOrderRule = factories.rule({ | ||
conditions: drinkOrderConditions, | ||
event: drinkOrderEvent, | ||
priority: 1 | ||
it('allows facts to be added by the event handler, affecting subsequent rules', () => { | ||
let drinkOrderParams = { wine: 'merlot', quantity: 2 } | ||
let drinkOrderEvent = { | ||
type: 'offerDrink', | ||
params: drinkOrderParams | ||
} | ||
let drinkOrderConditions = { | ||
any: [{ | ||
fact: 'canOrderDrinks', | ||
operator: 'equal', | ||
value: true | ||
}] | ||
} | ||
let drinkOrderRule = factories.rule({ | ||
conditions: drinkOrderConditions, | ||
event: drinkOrderEvent, | ||
priority: 1 | ||
}) | ||
engine.addRule(drinkOrderRule) | ||
return new Promise((resolve, reject) => { | ||
engine.on('success', function (event, almanac) { | ||
switch (event.type) { | ||
case 'setDrinkingFlag': | ||
almanac.addRuntimeFact('canOrderDrinks', event.params.canOrderDrinks) | ||
break | ||
case 'offerDrink': | ||
expect(event.params).to.eql(drinkOrderParams) | ||
break | ||
default: | ||
reject(new Error('default case not expected')) | ||
} | ||
}) | ||
engine.run().then(resolve).catch(reject) | ||
}) | ||
}) | ||
engine.addRule(drinkOrderRule) | ||
return new Promise((resolve, reject) => { | ||
engine.on('success', function (event, almanac) { | ||
switch (event.type) { | ||
case 'setDrinkingFlag': | ||
almanac.addRuntimeFact('canOrderDrinks', event.params.canOrderDrinks) | ||
break | ||
case 'offerDrink': | ||
expect(event.params).to.eql(drinkOrderParams) | ||
break | ||
default: | ||
reject(new Error('default case not expected')) | ||
} | ||
}) | ||
describe('rule events', () => { | ||
it('on-success, it passes the event type and params', (done) => { | ||
let failureSpy = sinon.spy() | ||
let rule = engine.rules[0] | ||
rule.on('success', function (e, a) { | ||
try { | ||
expect(e).to.eql(event) | ||
expect(a).to.be.an.instanceof(Almanac) | ||
expect(failureSpy.callCount).to.equal(0) | ||
} catch (err) { return done(err) } | ||
done() | ||
}) | ||
engine.run().then(resolve).catch(reject) | ||
rule.on('failure', failureSpy) | ||
engine.run() | ||
}) | ||
it('on-failure, it passes the event type and params', (done) => { | ||
let successSpy = sinon.spy() | ||
let rule = engine.rules[0] | ||
rule.on('failure', function (e, a) { | ||
try { | ||
expect(e).to.eql(event) | ||
expect(a).to.be.an.instanceof(Almanac) | ||
expect(successSpy.callCount).to.equal(0) | ||
} catch (err) { return done(err) } | ||
done() | ||
}) | ||
rule.on('success', successSpy) | ||
engine.addFact('age', 10) | ||
engine.run() | ||
}) | ||
}) | ||
}) |
@@ -55,2 +55,32 @@ 'use strict' | ||
describe('event emissions', () => { | ||
it('can emit', () => { | ||
let rule = new Rule() | ||
let successSpy = sinon.spy() | ||
rule.on('test', successSpy) | ||
rule.emit('test') | ||
expect(successSpy.callCount).to.equal(1) | ||
}) | ||
it('can be initialized with an onSuccess option', (done) => { | ||
let event = { type: 'test' } | ||
let onSuccess = function (e) { | ||
expect(e).to.equal(event) | ||
done() | ||
} | ||
let rule = new Rule({ onSuccess }) | ||
rule.emit('success', event) | ||
}) | ||
it('can be initialized with an onFailure option', (done) => { | ||
let event = { type: 'test' } | ||
let onFailure = function (e) { | ||
expect(e).to.equal(event) | ||
done() | ||
} | ||
let rule = new Rule({ onFailure }) | ||
rule.emit('failure', event) | ||
}) | ||
}) | ||
describe('setConditions()', () => { | ||
@@ -57,0 +87,0 @@ describe('validations', () => { |
190819
59
4587