UNPKG

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