UNPKG

13.6 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6
7var _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; }; }();
8
9var _condition = require('./condition');
10
11var _condition2 = _interopRequireDefault(_condition);
12
13var _ruleResult = require('./rule-result');
14
15var _ruleResult2 = _interopRequireDefault(_ruleResult);
16
17var _events = require('events');
18
19var _debug = require('./debug');
20
21var _debug2 = _interopRequireDefault(_debug);
22
23function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
24
25function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
26
27function _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; }
28
29function _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; }
30
31var Rule = function (_EventEmitter) {
32 _inherits(Rule, _EventEmitter);
33
34 /**
35 * returns a new Rule instance
36 * @param {object,string} options, or json string that can be parsed into options
37 * @param {integer} options.priority (>1) - higher runs sooner.
38 * @param {Object} options.event - event to fire when rule evaluates as successful
39 * @param {string} options.event.type - name of event to emit
40 * @param {string} options.event.params - parameters to pass to the event listener
41 * @param {Object} options.conditions - conditions to evaluate when processing this rule
42 * @param {any} options.name - identifier for a particular rule, particularly valuable in RuleResult output
43 * @return {Rule} instance
44 */
45 function Rule(options) {
46 _classCallCheck(this, Rule);
47
48 var _this = _possibleConstructorReturn(this, (Rule.__proto__ || Object.getPrototypeOf(Rule)).call(this));
49
50 if (typeof options === 'string') {
51 options = JSON.parse(options);
52 }
53 if (options && options.conditions) {
54 _this.setConditions(options.conditions);
55 }
56 if (options && options.onSuccess) {
57 _this.on('success', options.onSuccess);
58 }
59 if (options && options.onFailure) {
60 _this.on('failure', options.onFailure);
61 }
62 if (options && (options.name || options.name === 0)) {
63 _this.setName(options.name);
64 }
65
66 var priority = options && options.priority || 1;
67 _this.setPriority(priority);
68
69 var event = options && options.event || { type: 'unknown' };
70 _this.setEvent(event);
71 return _this;
72 }
73
74 /**
75 * Sets the priority of the rule
76 * @param {integer} priority (>=1) - increasing the priority causes the rule to be run prior to other rules
77 */
78
79
80 _createClass(Rule, [{
81 key: 'setPriority',
82 value: function setPriority(priority) {
83 priority = parseInt(priority, 10);
84 if (priority <= 0) throw new Error('Priority must be greater than zero');
85 this.priority = priority;
86 return this;
87 }
88
89 /**
90 * Sets the name of the rule
91 * @param {any} name - any truthy input and zero is allowed
92 */
93
94 }, {
95 key: 'setName',
96 value: function setName(name) {
97 if (!name && name !== 0) {
98 throw new Error('Rule "name" must be defined');
99 }
100 this.name = name;
101 return this;
102 }
103
104 /**
105 * Sets the conditions to run when evaluating the rule.
106 * @param {object} conditions - conditions, root element must be a boolean operator
107 */
108
109 }, {
110 key: 'setConditions',
111 value: function setConditions(conditions) {
112 if (!Object.prototype.hasOwnProperty.call(conditions, 'all') && !Object.prototype.hasOwnProperty.call(conditions, 'any')) {
113 throw new Error('"conditions" root must contain a single instance of "all" or "any"');
114 }
115 this.conditions = new _condition2.default(conditions);
116 return this;
117 }
118
119 /**
120 * Sets the event to emit when the conditions evaluate truthy
121 * @param {object} event - event to emit
122 * @param {string} event.type - event name to emit on
123 * @param {string} event.params - parameters to emit as the argument of the event emission
124 */
125
126 }, {
127 key: 'setEvent',
128 value: function setEvent(event) {
129 if (!event) throw new Error('Rule: setEvent() requires event object');
130 if (!Object.prototype.hasOwnProperty.call(event, 'type')) throw new Error('Rule: setEvent() requires event object with "type" property');
131 this.event = {
132 type: event.type
133 };
134 if (event.params) this.event.params = event.params;
135 return this;
136 }
137
138 /**
139 * Sets the engine to run the rules under
140 * @param {object} engine
141 * @returns {Rule}
142 */
143
144 }, {
145 key: 'setEngine',
146 value: function setEngine(engine) {
147 this.engine = engine;
148 return this;
149 }
150 }, {
151 key: 'toJSON',
152 value: function toJSON() {
153 var stringify = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
154
155 var props = {
156 conditions: this.conditions.toJSON(false),
157 priority: this.priority,
158 event: this.event,
159 name: this.name
160 };
161 if (stringify) {
162 return JSON.stringify(props);
163 }
164 return props;
165 }
166
167 /**
168 * Priorizes an array of conditions based on "priority"
169 * When no explicit priority is provided on the condition itself, the condition's priority is determine by its fact
170 * @param {Condition[]} conditions
171 * @return {Condition[][]} prioritized two-dimensional array of conditions
172 * Each outer array element represents a single priority(integer). Inner array is
173 * all conditions with that priority.
174 */
175
176 }, {
177 key: 'prioritizeConditions',
178 value: function prioritizeConditions(conditions) {
179 var _this2 = this;
180
181 var factSets = conditions.reduce(function (sets, condition) {
182 // if a priority has been set on this specific condition, honor that first
183 // otherwise, use the fact's priority
184 var priority = condition.priority;
185 if (!priority) {
186 var fact = _this2.engine.getFact(condition.fact);
187 priority = fact && fact.priority || 1;
188 }
189 if (!sets[priority]) sets[priority] = [];
190 sets[priority].push(condition);
191 return sets;
192 }, {});
193 return Object.keys(factSets).sort(function (a, b) {
194 return Number(a) > Number(b) ? -1 : 1; // order highest priority -> lowest
195 }).map(function (priority) {
196 return factSets[priority];
197 });
198 }
199
200 /**
201 * Evaluates the rule, starting with the root boolean operator and recursing down
202 * All evaluation is done within the context of an almanac
203 * @return {Promise(RuleResult)} rule evaluation result
204 */
205
206 }, {
207 key: 'evaluate',
208 value: function evaluate(almanac) {
209 var _this3 = this;
210
211 var ruleResult = new _ruleResult2.default(this.conditions, this.event, this.priority, this.name);
212
213 /**
214 * Evaluates the rule conditions
215 * @param {Condition} condition - condition to evaluate
216 * @return {Promise(true|false)} - resolves with the result of the condition evaluation
217 */
218 var evaluateCondition = function evaluateCondition(condition) {
219 if (condition.isBooleanOperator()) {
220 var subConditions = condition[condition.operator];
221 var comparisonPromise = void 0;
222 if (condition.operator === 'all') {
223 comparisonPromise = all(subConditions);
224 } else {
225 comparisonPromise = any(subConditions);
226 }
227 // for booleans, rule passing is determined by the all/any result
228 return comparisonPromise.then(function (comparisonValue) {
229 var passes = comparisonValue === true;
230 condition.result = passes;
231 return passes;
232 });
233 } else {
234 return condition.evaluate(almanac, _this3.engine.operators).then(function (evaluationResult) {
235 var passes = evaluationResult.result;
236 condition.factResult = evaluationResult.leftHandSideValue;
237 condition.result = passes;
238 return passes;
239 });
240 }
241 };
242
243 /**
244 * Evalutes an array of conditions, using an 'every' or 'some' array operation
245 * @param {Condition[]} conditions
246 * @param {string(every|some)} array method to call for determining result
247 * @return {Promise(boolean)} whether conditions evaluated truthy or falsey based on condition evaluation + method
248 */
249 var evaluateConditions = function evaluateConditions(conditions, method) {
250 if (!Array.isArray(conditions)) conditions = [conditions];
251
252 return Promise.all(conditions.map(function (condition) {
253 return evaluateCondition(condition);
254 })).then(function (conditionResults) {
255 (0, _debug2.default)('rule::evaluateConditions results', conditionResults);
256 return method.call(conditionResults, function (result) {
257 return result === true;
258 });
259 });
260 };
261
262 /**
263 * Evaluates a set of conditions based on an 'all' or 'any' operator.
264 * First, orders the top level conditions based on priority
265 * Iterates over each priority set, evaluating each condition
266 * If any condition results in the rule to be guaranteed truthy or falsey,
267 * it will short-circuit and not bother evaluating any additional rules
268 * @param {Condition[]} conditions - conditions to be evaluated
269 * @param {string('all'|'any')} operator
270 * @return {Promise(boolean)} rule evaluation result
271 */
272 var prioritizeAndRun = function prioritizeAndRun(conditions, operator) {
273 if (conditions.length === 0) {
274 return Promise.resolve(true);
275 }
276 var method = Array.prototype.some;
277 if (operator === 'all') {
278 method = Array.prototype.every;
279 }
280 var orderedSets = _this3.prioritizeConditions(conditions);
281 var cursor = Promise.resolve();
282 // use for() loop over Array.forEach to support IE8 without polyfill
283
284 var _loop = function _loop(i) {
285 var set = orderedSets[i];
286 var stop = false;
287 cursor = cursor.then(function (setResult) {
288 // after the first set succeeds, don't fire off the remaining promises
289 if (operator === 'any' && setResult === true || stop) {
290 (0, _debug2.default)('prioritizeAndRun::detected truthy result; skipping remaining conditions');
291 stop = true;
292 return true;
293 }
294
295 // after the first set fails, don't fire off the remaining promises
296 if (operator === 'all' && setResult === false || stop) {
297 (0, _debug2.default)('prioritizeAndRun::detected falsey result; skipping remaining conditions');
298 stop = true;
299 return false;
300 }
301 // all conditions passed; proceed with running next set in parallel
302 return evaluateConditions(set, method);
303 });
304 };
305
306 for (var i = 0; i < orderedSets.length; i++) {
307 _loop(i);
308 }
309 return cursor;
310 };
311
312 /**
313 * Runs an 'any' boolean operator on an array of conditions
314 * @param {Condition[]} conditions to be evaluated
315 * @return {Promise(boolean)} condition evaluation result
316 */
317 var any = function any(conditions) {
318 return prioritizeAndRun(conditions, 'any');
319 };
320
321 /**
322 * Runs an 'all' boolean operator on an array of conditions
323 * @param {Condition[]} conditions to be evaluated
324 * @return {Promise(boolean)} condition evaluation result
325 */
326 var all = function all(conditions) {
327 return prioritizeAndRun(conditions, 'all');
328 };
329
330 /**
331 * Emits based on rule evaluation result, and decorates ruleResult with 'result' property
332 * @param {Boolean} result
333 */
334 var processResult = function processResult(result) {
335 ruleResult.setResult(result);
336
337 if (result) _this3.emit('success', ruleResult.event, almanac, ruleResult);else _this3.emit('failure', ruleResult.event, almanac, ruleResult);
338 return ruleResult;
339 };
340
341 if (ruleResult.conditions.any) {
342 return any(ruleResult.conditions.any).then(function (result) {
343 return processResult(result);
344 });
345 } else {
346 return all(ruleResult.conditions.all).then(function (result) {
347 return processResult(result);
348 });
349 }
350 }
351 }]);
352
353 return Rule;
354}(_events.EventEmitter);
355
356exports.default = Rule;
\No newline at end of file