Skip to content

Commit 6b295f1

Browse files
author
Cache Hamm
authored
Merge pull request #31 from CacheControl/fact-comparison
Fact comparison
2 parents 06c6ef0 + 40d6a97 commit 6b295f1

31 files changed

Lines changed: 946 additions & 387 deletions

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
1.5.0 / 2017-03-12
2+
==================
3+
4+
* Add fact comparison conditions
5+
16
1.4.0 / 2017-01-23
27
==================
38

dist/almanac.js

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons
2222

2323
var debug = require('debug')('json-rules-engine');
2424
var verbose = require('debug')('json-rules-engine-verbose');
25+
var selectn = require('selectn');
26+
var isPlainObject = require('lodash.isplainobject');
27+
var warn = require('debug')('json-rules-engine:warn');
2528

2629
/**
2730
* Fact results lookup
@@ -118,6 +121,7 @@ var Almanac = function () {
118121
* by the engine, which cache's fact computations based on parameters provided
119122
* @param {string} factId - fact identifier
120123
* @param {Object} params - parameters to feed into the fact. By default, these will also be used to compute the cache key
124+
* @param {String} path - object
121125
* @return {Promise} a promise which will resolve with the fact computation.
122126
*/
123127

@@ -126,7 +130,8 @@ var Almanac = function () {
126130
value: function () {
127131
var _ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee(factId) {
128132
var params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
129-
var fact, cacheKey, cacheVal;
133+
var path = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '';
134+
var fact, cacheKey, cacheVal, factValue;
130135
return regeneratorRuntime.wrap(function _callee$(_context) {
131136
while (1) {
132137
switch (_context.prev = _context.next) {
@@ -147,9 +152,23 @@ var Almanac = function () {
147152

148153
case 6:
149154
verbose('almanac::factValue cache miss for fact:' + factId + '; calculating');
150-
return _context.abrupt('return', this._setFactValue(fact, params, fact.calculate(params, this)));
155+
_context.next = 9;
156+
return this._setFactValue(fact, params, fact.calculate(params, this));
157+
158+
case 9:
159+
factValue = _context.sent;
160+
161+
if (path) {
162+
if (isPlainObject(factValue) || Array.isArray(factValue)) {
163+
factValue = selectn(path)(factValue);
164+
debug('condition::evaluate extracting object property ' + path + ', received: ' + factValue);
165+
} else {
166+
warn('condition::evaluate could not compute object path(' + path + ') of non-object: ' + factValue + ' <' + (typeof factValue === 'undefined' ? 'undefined' : _typeof(factValue)) + '>; continuing with ' + factValue);
167+
}
168+
}
169+
return _context.abrupt('return', factValue);
151170

152-
case 8:
171+
case 12:
153172
case 'end':
154173
return _context.stop();
155174
}

dist/condition.js

Lines changed: 120 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,15 @@ Object.defineProperty(exports, "__esModule", {
44
value: true
55
});
66

7-
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
8-
97
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
108

11-
var _params = require('params');
12-
13-
var _params2 = _interopRequireDefault(_params);
14-
15-
var _selectn = require('selectn');
16-
17-
var _selectn2 = _interopRequireDefault(_selectn);
18-
19-
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
9+
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) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
2010

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

13+
var params = require('params');
2314
var debug = require('debug')('json-rules-engine');
24-
var warn = require('debug')('json-rules-engine:warn');
15+
var isPlainObject = require('lodash.isplainobject');
2516

2617
var Condition = function () {
2718
function Condition(properties) {
@@ -41,7 +32,7 @@ var Condition = function () {
4132
return new Condition(c);
4233
});
4334
} else {
44-
properties = (0, _params2.default)(properties).require(['fact', 'operator', 'value']);
35+
properties = params(properties).require(['fact', 'operator', 'value']);
4536
// a non-boolean condition does not have a priority by default. this allows
4637
// priority to be dictated by the fact definition
4738
if (properties.hasOwnProperty('priority')) {
@@ -88,38 +79,132 @@ var Condition = function () {
8879
return props;
8980
}
9081

82+
/**
83+
* Interprets .value as either a primitive, or if a fact, retrieves the fact value
84+
*/
85+
86+
}, {
87+
key: '_getValue',
88+
value: function () {
89+
var _ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee(almanac) {
90+
var value;
91+
return regeneratorRuntime.wrap(function _callee$(_context) {
92+
while (1) {
93+
switch (_context.prev = _context.next) {
94+
case 0:
95+
value = this.value;
96+
97+
if (!(isPlainObject(value) && value.hasOwnProperty('fact'))) {
98+
_context.next = 5;
99+
break;
100+
}
101+
102+
_context.next = 4;
103+
return almanac.factValue(value.fact, value.params, value.path);
104+
105+
case 4:
106+
value = _context.sent;
107+
108+
case 5:
109+
return _context.abrupt('return', value);
110+
111+
case 6:
112+
case 'end':
113+
return _context.stop();
114+
}
115+
}
116+
}, _callee, this);
117+
}));
118+
119+
function _getValue(_x2) {
120+
return _ref.apply(this, arguments);
121+
}
122+
123+
return _getValue;
124+
}()
125+
91126
/**
92127
* Takes the fact result and compares it to the condition 'value', using the operator
93-
* @param {mixed} comparisonValue - fact result
128+
* LHS OPER RHS
129+
* <fact + params + path> <operator> <value>
130+
*
131+
* @param {Almanac} almanac
94132
* @param {Map} operatorMap - map of available operators, keyed by operator name
95133
* @returns {Boolean} - evaluation result
96134
*/
97135

98136
}, {
99137
key: 'evaluate',
100-
value: function evaluate(comparisonValue, operatorMap) {
101-
// for any/all, simply comparisonValue that the sub-condition array evaluated truthy
102-
if (this.isBooleanOperator()) return comparisonValue === true;
103-
104-
// if the fact has provided an object, and a path is specified, retrieve the object property
105-
if (this.path) {
106-
if ((typeof comparisonValue === 'undefined' ? 'undefined' : _typeof(comparisonValue)) === 'object') {
107-
comparisonValue = (0, _selectn2.default)(this.path)(comparisonValue);
108-
debug('condition::evaluate extracting object property ' + this.path + ', received: ' + comparisonValue);
109-
} else {
110-
warn('condition::evaluate could not compute object path(' + this.path + ') of non-object: ' + comparisonValue + ' <' + (typeof comparisonValue === 'undefined' ? 'undefined' : _typeof(comparisonValue)) + '>; continuing with ' + comparisonValue);
111-
}
138+
value: function () {
139+
var _ref2 = _asyncToGenerator(regeneratorRuntime.mark(function _callee2(almanac, operatorMap) {
140+
var op, rightHandSideValue, leftHandSideValue, evaluationResult;
141+
return regeneratorRuntime.wrap(function _callee2$(_context2) {
142+
while (1) {
143+
switch (_context2.prev = _context2.next) {
144+
case 0:
145+
if (almanac) {
146+
_context2.next = 2;
147+
break;
148+
}
149+
150+
throw new Error('almanac required');
151+
152+
case 2:
153+
if (operatorMap) {
154+
_context2.next = 4;
155+
break;
156+
}
157+
158+
throw new Error('operatorMap required');
159+
160+
case 4:
161+
if (!this.isBooleanOperator()) {
162+
_context2.next = 6;
163+
break;
164+
}
165+
166+
throw new Error('Cannot evaluate() a boolean condition');
167+
168+
case 6:
169+
op = operatorMap.get(this.operator);
170+
171+
if (op) {
172+
_context2.next = 9;
173+
break;
174+
}
175+
176+
throw new Error('Unknown operator: ' + this.operator);
177+
178+
case 9:
179+
_context2.next = 11;
180+
return this._getValue(almanac);
181+
182+
case 11:
183+
rightHandSideValue = _context2.sent;
184+
_context2.next = 14;
185+
return almanac.factValue(this.fact, this.params, this.path);
186+
187+
case 14:
188+
leftHandSideValue = _context2.sent;
189+
evaluationResult = op.evaluate(leftHandSideValue, rightHandSideValue);
190+
191+
debug('condition::evaluate <' + leftHandSideValue + ' ' + this.operator + ' ' + rightHandSideValue + '?> (' + evaluationResult + ')');
192+
return _context2.abrupt('return', evaluationResult);
193+
194+
case 18:
195+
case 'end':
196+
return _context2.stop();
197+
}
198+
}
199+
}, _callee2, this);
200+
}));
201+
202+
function evaluate(_x3, _x4) {
203+
return _ref2.apply(this, arguments);
112204
}
113205

114-
var op = operatorMap.get(this.operator);
115-
if (!op) throw new Error('Unknown operator: ' + this.operator);
116-
117-
var evaluationResult = op.evaluate(comparisonValue, this.value);
118-
if (!this.isBooleanOperator()) {
119-
debug('condition::evaluate <' + comparisonValue + ' ' + this.operator + ' ' + this.value + '?> (' + evaluationResult + ')');
120-
}
121-
return evaluationResult;
122-
}
206+
return evaluate;
207+
}()
123208

124209
/**
125210
* Returns the boolean operator for the condition

dist/engine.js

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -161,22 +161,18 @@ var Engine = function (_EventEmitter) {
161161
}, {
162162
key: 'prioritizeRules',
163163
value: function prioritizeRules() {
164-
var _this2 = this;
165-
166164
if (!this.prioritizedRules) {
167-
(function () {
168-
var ruleSets = _this2.rules.reduce(function (sets, rule) {
169-
var priority = rule.priority;
170-
if (!sets[priority]) sets[priority] = [];
171-
sets[priority].push(rule);
172-
return sets;
173-
}, {});
174-
_this2.prioritizedRules = Object.keys(ruleSets).sort(function (a, b) {
175-
return Number(a) > Number(b) ? -1 : 1; // order highest priority -> lowest
176-
}).map(function (priority) {
177-
return ruleSets[priority];
178-
});
179-
})();
165+
var ruleSets = this.rules.reduce(function (sets, rule) {
166+
var priority = rule.priority;
167+
if (!sets[priority]) sets[priority] = [];
168+
sets[priority].push(rule);
169+
return sets;
170+
}, {});
171+
this.prioritizedRules = Object.keys(ruleSets).sort(function (a, b) {
172+
return Number(a) > Number(b) ? -1 : 1; // order highest priority -> lowest
173+
}).map(function (priority) {
174+
return ruleSets[priority];
175+
});
180176
}
181177
return this.prioritizedRules;
182178
}
@@ -217,25 +213,25 @@ var Engine = function (_EventEmitter) {
217213
key: 'evaluateRules',
218214
value: function () {
219215
var _ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee(ruleArray, almanac) {
220-
var _this3 = this;
216+
var _this2 = this;
221217

222218
return regeneratorRuntime.wrap(function _callee$(_context) {
223219
while (1) {
224220
switch (_context.prev = _context.next) {
225221
case 0:
226222
return _context.abrupt('return', Promise.all(ruleArray.map(function (rule) {
227-
if (_this3.status !== RUNNING) {
228-
debug('engine::run status:' + _this3.status + '; skipping remaining rules');
223+
if (_this2.status !== RUNNING) {
224+
debug('engine::run status:' + _this2.status + '; skipping remaining rules');
229225
return;
230226
}
231227
return rule.evaluate(almanac).then(function (rulePasses) {
232228
debug('engine::run ruleResult:' + rulePasses);
233229
if (rulePasses) {
234-
_this3.emit('success', rule.event, almanac);
235-
_this3.emit(rule.event.type, rule.event.params, _this3);
230+
_this2.emit('success', rule.event, almanac);
231+
_this2.emit(rule.event.type, rule.event.params, _this2);
236232
almanac.factValue('success-events', { event: rule.event });
237233
}
238-
if (!rulePasses) _this3.emit('failure', rule, almanac);
234+
if (!rulePasses) _this2.emit('failure', rule, almanac);
239235
});
240236
})));
241237

@@ -265,7 +261,7 @@ var Engine = function (_EventEmitter) {
265261
key: 'run',
266262
value: function () {
267263
var _ref2 = _asyncToGenerator(regeneratorRuntime.mark(function _callee2() {
268-
var _this4 = this;
264+
var _this3 = this;
269265

270266
var runtimeFacts = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
271267
var almanac, orderedSets, cursor;
@@ -286,12 +282,12 @@ var Engine = function (_EventEmitter) {
286282
return _context2.abrupt('return', new Promise(function (resolve, reject) {
287283
orderedSets.map(function (set) {
288284
cursor = cursor.then(function () {
289-
return _this4.evaluateRules(set, almanac);
285+
return _this3.evaluateRules(set, almanac);
290286
}).catch(reject);
291287
return cursor;
292288
});
293289
cursor.then(function () {
294-
_this4.status = FINISHED;
290+
_this3.status = FINISHED;
295291
debug('engine::run completed');
296292
resolve(almanac.factValue('success-events'));
297293
}).catch(reject);

0 commit comments

Comments
 (0)