UNPKG

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