Skip to content

Commit df85a37

Browse files
author
Cache Hamm
committed
rule success/failure event emissions
1 parent d8dc079 commit df85a37

4 files changed

Lines changed: 178 additions & 69 deletions

File tree

dist/rule.js

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,23 @@ var _condition = require('./condition');
1414

1515
var _condition2 = _interopRequireDefault(_condition);
1616

17+
var _events = require('events');
18+
1719
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
1820

1921
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { return step("next", value); }, function (err) { return step("throw", err); }); } } return step("next"); }); }; }
2022

2123
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
2224

25+
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; }
26+
27+
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; }
28+
2329
var debug = require('debug')('json-rules-engine');
2430

25-
var Rule = function () {
31+
var Rule = function (_EventEmitter) {
32+
_inherits(Rule, _EventEmitter);
33+
2634
/**
2735
* returns a new Rule instance
2836
* @param {object,string} options, or json string that can be parsed into options
@@ -36,18 +44,27 @@ var Rule = function () {
3644
function Rule(options) {
3745
_classCallCheck(this, Rule);
3846

47+
var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(Rule).call(this));
48+
3949
if (typeof options === 'string') {
4050
options = JSON.parse(options);
4151
}
4252
if (options && options.conditions) {
43-
this.setConditions(options.conditions);
53+
_this.setConditions(options.conditions);
54+
}
55+
if (options && options.onSuccess) {
56+
_this.on('success', options.onSuccess);
57+
}
58+
if (options && options.onFailure) {
59+
_this.on('failure', options.onFailure);
4460
}
4561

4662
var priority = options && options.priority || 1;
47-
this.setPriority(priority);
63+
_this.setPriority(priority);
4864

4965
var event = options && options.event || { type: 'unknown' };
50-
this.setEvent(event);
66+
_this.setEvent(event);
67+
return _this;
5168
}
5269

5370
/**
@@ -134,14 +151,14 @@ var Rule = function () {
134151
}, {
135152
key: 'prioritizeConditions',
136153
value: function prioritizeConditions(conditions) {
137-
var _this = this;
154+
var _this2 = this;
138155

139156
var factSets = conditions.reduce(function (sets, condition) {
140157
// if a priority has been set on this specific condition, honor that first
141158
// otherwise, use the fact's priority
142159
var priority = condition.priority;
143160
if (!priority) {
144-
var fact = _this.engine.getFact(condition.fact);
161+
var fact = _this2.engine.getFact(condition.fact);
145162
priority = fact && fact.priority || 1;
146163
}
147164
if (!sets[priority]) sets[priority] = [];
@@ -165,7 +182,7 @@ var Rule = function () {
165182
key: 'evaluate',
166183
value: function () {
167184
var _ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee6(almanac) {
168-
var _this2 = this;
185+
var _this3 = this;
169186

170187
var evaluateCondition, evaluateConditions, prioritizeAndRun, any, all;
171188
return regeneratorRuntime.wrap(function _callee6$(_context6) {
@@ -179,7 +196,7 @@ var Rule = function () {
179196
*/
180197
evaluateCondition = function () {
181198
var _ref2 = _asyncToGenerator(regeneratorRuntime.mark(function _callee(condition) {
182-
var comparisonValue, subConditions;
199+
var comparisonValue, subConditions, passes;
183200
return regeneratorRuntime.wrap(function _callee$(_context) {
184201
while (1) {
185202
switch (_context.prev = _context.next) {
@@ -225,14 +242,25 @@ var Rule = function () {
225242
comparisonValue = _context.sent;
226243

227244
case 17:
228-
return _context.abrupt('return', condition.evaluate(comparisonValue, _this2.engine.operators));
245+
_context.next = 19;
246+
return condition.evaluate(comparisonValue, _this3.engine.operators);
247+
248+
case 19:
249+
passes = _context.sent;
250+
251+
if (passes) {
252+
_this3.emit('success', _this3.event, almanac);
253+
} else {
254+
_this3.emit('failure', _this3.event, almanac);
255+
}
256+
return _context.abrupt('return', passes);
229257

230-
case 18:
258+
case 22:
231259
case 'end':
232260
return _context.stop();
233261
}
234262
}
235-
}, _callee, _this2);
263+
}, _callee, _this3);
236264
}));
237265

238266
return function evaluateCondition(_x3) {
@@ -274,7 +302,7 @@ var Rule = function () {
274302
return _context2.stop();
275303
}
276304
}
277-
}, _callee2, _this2);
305+
}, _callee2, _this3);
278306
}));
279307

280308
return function evaluateConditions(_x4, _x5) {
@@ -314,7 +342,7 @@ var Rule = function () {
314342
if (operator === 'all') {
315343
method = Array.prototype.every;
316344
}
317-
orderedSets = _this2.prioritizeConditions(conditions);
345+
orderedSets = _this3.prioritizeConditions(conditions);
318346
cursor = Promise.resolve();
319347

320348
orderedSets.forEach(function (set) {
@@ -344,7 +372,7 @@ var Rule = function () {
344372
return _context3.stop();
345373
}
346374
}
347-
}, _callee3, _this2);
375+
}, _callee3, _this3);
348376
}));
349377

350378
return function prioritizeAndRun(_x6, _x7) {
@@ -372,7 +400,7 @@ var Rule = function () {
372400
return _context4.stop();
373401
}
374402
}
375-
}, _callee4, _this2);
403+
}, _callee4, _this3);
376404
}));
377405

378406
return function any(_x8) {
@@ -400,7 +428,7 @@ var Rule = function () {
400428
return _context5.stop();
401429
}
402430
}
403-
}, _callee5, _this2);
431+
}, _callee5, _this3);
404432
}));
405433

406434
return function all(_x9) {
@@ -443,6 +471,6 @@ var Rule = function () {
443471
}]);
444472

445473
return Rule;
446-
}();
474+
}(_events.EventEmitter);
447475

448476
exports.default = Rule;

src/rule.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
import params from 'params'
44
import Condition from './condition'
5+
import { EventEmitter } from 'events'
56

67
let debug = require('debug')('json-rules-engine')
78

8-
class Rule {
9+
class Rule extends EventEmitter {
910
/**
1011
* returns a new Rule instance
1112
* @param {object,string} options, or json string that can be parsed into options
@@ -17,12 +18,19 @@ class Rule {
1718
* @return {Rule} instance
1819
*/
1920
constructor (options) {
21+
super()
2022
if (typeof options === 'string') {
2123
options = JSON.parse(options)
2224
}
2325
if (options && options.conditions) {
2426
this.setConditions(options.conditions)
2527
}
28+
if (options && options.onSuccess) {
29+
this.on('success', options.onSuccess)
30+
}
31+
if (options && options.onFailure) {
32+
this.on('failure', options.onFailure)
33+
}
2634

2735
let priority = (options && options.priority) || 1
2836
this.setPriority(priority)
@@ -136,7 +144,13 @@ class Rule {
136144
} else {
137145
comparisonValue = await almanac.factValue(condition.fact, condition.params)
138146
}
139-
return condition.evaluate(comparisonValue, this.engine.operators)
147+
let passes = await condition.evaluate(comparisonValue, this.engine.operators)
148+
if (passes) {
149+
this.emit('success', this.event, almanac)
150+
} else {
151+
this.emit('failure', this.event, almanac)
152+
}
153+
return passes
140154
}
141155

142156
/**

test/engine-event.test.js

Lines changed: 87 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
'use strict'
22

33
import engineFactory from '../src/index'
4+
import Almanac from '../src/almanac'
5+
import sinon from 'sinon'
46

57
describe('Engine: event', () => {
68
let engine
@@ -20,66 +22,101 @@ describe('Engine: event', () => {
2022
}
2123
beforeEach(() => {
2224
engine = engineFactory()
23-
let determineDrinkingAgeRule = factories.rule({ conditions, event, priority: 100 })
25+
let ruleOptions = { conditions, event, priority: 100 }
26+
let determineDrinkingAgeRule = factories.rule(ruleOptions)
2427
engine.addRule(determineDrinkingAgeRule)
2528
engine.addFact('age', 21)
2629
})
2730

28-
it('passes the event type and params', (done) => {
29-
engine.on('success', function (a, engine) {
30-
try {
31-
expect(a).to.eql(event)
32-
expect(engine).to.eql(engine)
33-
} catch (e) { return done(e) }
34-
done()
31+
describe('engine events', () => {
32+
it('passes the event type and params', (done) => {
33+
engine.on('success', function (a, engine) {
34+
try {
35+
expect(a).to.eql(event)
36+
expect(engine).to.eql(engine)
37+
} catch (e) { return done(e) }
38+
done()
39+
})
40+
engine.run()
3541
})
36-
engine.run()
37-
})
3842

39-
it('emits using the event "type"', (done) => {
40-
engine.on('setDrinkingFlag', function (params, engine) {
41-
try {
42-
expect(params).to.eql(event.params)
43-
expect(engine).to.eql(engine)
44-
} catch (e) { return done(e) }
45-
done()
43+
it('emits using the event "type"', (done) => {
44+
engine.on('setDrinkingFlag', function (params, e) {
45+
try {
46+
expect(params).to.eql(event.params)
47+
expect(engine).to.eql(e)
48+
} catch (e) { return done(e) }
49+
done()
50+
})
51+
engine.run()
4652
})
47-
engine.run()
48-
})
4953

50-
it('allows facts to be added by the event handler, affecting subsequent rules', () => {
51-
let drinkOrderParams = { wine: 'merlot', quantity: 2 }
52-
let drinkOrderEvent = {
53-
type: 'offerDrink',
54-
params: drinkOrderParams
55-
}
56-
let drinkOrderConditions = {
57-
any: [{
58-
fact: 'canOrderDrinks',
59-
operator: 'equal',
60-
value: true
61-
}]
62-
}
63-
let drinkOrderRule = factories.rule({
64-
conditions: drinkOrderConditions,
65-
event: drinkOrderEvent,
66-
priority: 1
54+
it('allows facts to be added by the event handler, affecting subsequent rules', () => {
55+
let drinkOrderParams = { wine: 'merlot', quantity: 2 }
56+
let drinkOrderEvent = {
57+
type: 'offerDrink',
58+
params: drinkOrderParams
59+
}
60+
let drinkOrderConditions = {
61+
any: [{
62+
fact: 'canOrderDrinks',
63+
operator: 'equal',
64+
value: true
65+
}]
66+
}
67+
let drinkOrderRule = factories.rule({
68+
conditions: drinkOrderConditions,
69+
event: drinkOrderEvent,
70+
priority: 1
71+
})
72+
engine.addRule(drinkOrderRule)
73+
return new Promise((resolve, reject) => {
74+
engine.on('success', function (event, almanac) {
75+
switch (event.type) {
76+
case 'setDrinkingFlag':
77+
almanac.addRuntimeFact('canOrderDrinks', event.params.canOrderDrinks)
78+
break
79+
case 'offerDrink':
80+
expect(event.params).to.eql(drinkOrderParams)
81+
break
82+
default:
83+
reject(new Error('default case not expected'))
84+
}
85+
})
86+
engine.run().then(resolve).catch(reject)
87+
})
88+
})
89+
})
90+
describe('rule events', () => {
91+
it('on-success, it passes the event type and params', (done) => {
92+
let failureSpy = sinon.spy()
93+
let rule = engine.rules[0]
94+
rule.on('success', function (e, a) {
95+
try {
96+
expect(e).to.eql(event)
97+
expect(a).to.be.an.instanceof(Almanac)
98+
expect(failureSpy.callCount).to.equal(0)
99+
} catch (err) { return done(err) }
100+
done()
101+
})
102+
rule.on('failure', failureSpy)
103+
engine.run()
67104
})
68-
engine.addRule(drinkOrderRule)
69-
return new Promise((resolve, reject) => {
70-
engine.on('success', function (event, almanac) {
71-
switch (event.type) {
72-
case 'setDrinkingFlag':
73-
almanac.addRuntimeFact('canOrderDrinks', event.params.canOrderDrinks)
74-
break
75-
case 'offerDrink':
76-
expect(event.params).to.eql(drinkOrderParams)
77-
break
78-
default:
79-
reject(new Error('default case not expected'))
80-
}
105+
106+
it('on-failure, it passes the event type and params', (done) => {
107+
let successSpy = sinon.spy()
108+
let rule = engine.rules[0]
109+
rule.on('failure', function (e, a) {
110+
try {
111+
expect(e).to.eql(event)
112+
expect(a).to.be.an.instanceof(Almanac)
113+
expect(successSpy.callCount).to.equal(0)
114+
} catch (err) { return done(err) }
115+
done()
81116
})
82-
engine.run().then(resolve).catch(reject)
117+
rule.on('success', successSpy)
118+
engine.addFact('age', 10)
119+
engine.run()
83120
})
84121
})
85122
})

0 commit comments

Comments
 (0)