json-rules-engine
Advanced tools
Comparing version 2.1.0 to 2.2.0-alpha1
@@ -19,4 +19,2 @@ 'use strict'; | ||
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"); }); }; } | ||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } | ||
@@ -68,7 +66,3 @@ | ||
value: function _getFact(factId) { | ||
var fact = this.factMap.get(factId); | ||
if (fact === undefined) { | ||
throw new _errors.UndefinedFactError('Undefined fact: ' + factId); | ||
} | ||
return fact; | ||
return this.factMap.get(factId); | ||
} | ||
@@ -100,5 +94,2 @@ | ||
var factValue = Promise.resolve(value); | ||
factValue.then(function (val) { | ||
return debug('almanac::factValue fact:' + fact.id + ' calculated as: ' + JSON.stringify(val) + '<' + (typeof val === 'undefined' ? 'undefined' : _typeof(val)) + '>'); | ||
}); | ||
if (cacheKey) { | ||
@@ -134,64 +125,38 @@ this.factResultsCache.set(cacheKey, factValue); | ||
key: 'factValue', | ||
value: function () { | ||
var _ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee(factId) { | ||
var params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
var path = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : ''; | ||
var factValue, fact, cacheKey, cacheVal; | ||
return regeneratorRuntime.wrap(function _callee$(_context) { | ||
while (1) { | ||
switch (_context.prev = _context.next) { | ||
case 0: | ||
factValue = void 0; | ||
fact = this._getFact(factId); | ||
cacheKey = fact.getCacheKey(params); | ||
cacheVal = cacheKey && this.factResultsCache.get(cacheKey); | ||
value: function factValue(factId) { | ||
var params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
var path = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : ''; | ||
if (!cacheVal) { | ||
_context.next = 11; | ||
break; | ||
} | ||
_context.next = 7; | ||
return cacheVal; | ||
case 7: | ||
factValue = _context.sent; | ||
debug('almanac::factValue cache hit for fact:' + factId + ' value: ' + JSON.stringify(factValue) + '<' + (typeof factValue === 'undefined' ? 'undefined' : _typeof(factValue)) + '>'); | ||
_context.next = 15; | ||
break; | ||
case 11: | ||
verbose('almanac::factValue cache miss for fact:' + factId + '; calculating'); | ||
_context.next = 14; | ||
return this._setFactValue(fact, params, fact.calculate(params, this)); | ||
case 14: | ||
factValue = _context.sent; | ||
case 15: | ||
if (path) { | ||
if (isObjectLike(factValue)) { | ||
factValue = selectn(path)(factValue); | ||
debug('condition::evaluate extracting object property ' + path + ', received: ' + factValue); | ||
} else { | ||
warn('condition::evaluate could not compute object path(' + path + ') of non-object: ' + factValue + ' <' + (typeof factValue === 'undefined' ? 'undefined' : _typeof(factValue)) + '>; continuing with ' + factValue); | ||
} | ||
} | ||
return _context.abrupt('return', factValue); | ||
case 17: | ||
case 'end': | ||
return _context.stop(); | ||
} | ||
var factValuePromise = void 0; | ||
var fact = this._getFact(factId); | ||
if (fact === undefined) { | ||
return Promise.reject(new _errors.UndefinedFactError('Undefined fact: ' + factId)); | ||
} | ||
if (fact.isConstant()) { | ||
factValuePromise = Promise.resolve(fact.calculate(params, this)); | ||
} else { | ||
var cacheKey = fact.getCacheKey(params); | ||
var cacheVal = cacheKey && this.factResultsCache.get(cacheKey); | ||
if (cacheVal) { | ||
factValuePromise = Promise.resolve(cacheVal); | ||
debug('almanac::factValue cache hit for fact:' + factId); | ||
} else { | ||
verbose('almanac::factValue cache miss for fact:' + factId + '; calculating'); | ||
factValuePromise = this._setFactValue(fact, params, fact.calculate(params, this)); | ||
} | ||
} | ||
if (path) { | ||
return factValuePromise.then(function (factValue) { | ||
if (isObjectLike(factValue)) { | ||
var pathValue = selectn(path)(factValue); | ||
debug('condition::evaluate extracting object property ' + path + ', received: ' + pathValue); | ||
return pathValue; | ||
} else { | ||
warn('condition::evaluate could not compute object path(' + path + ') of non-object: ' + factValue + ' <' + (typeof factValue === 'undefined' ? 'undefined' : _typeof(factValue)) + '>; continuing with ' + factValue); | ||
return factValue; | ||
} | ||
}, _callee, this); | ||
})); | ||
function factValue(_x2) { | ||
return _ref.apply(this, arguments); | ||
}); | ||
} | ||
return factValue; | ||
}() | ||
return factValuePromise; | ||
} | ||
}]); | ||
@@ -198,0 +163,0 @@ |
@@ -9,4 +9,2 @@ 'use strict'; | ||
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"); }); }; } | ||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } | ||
@@ -98,40 +96,11 @@ | ||
key: '_getValue', | ||
value: function () { | ||
var _ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee(almanac) { | ||
var value; | ||
return regeneratorRuntime.wrap(function _callee$(_context) { | ||
while (1) { | ||
switch (_context.prev = _context.next) { | ||
case 0: | ||
value = this.value; | ||
if (!(isObjectLike(value) && value.hasOwnProperty('fact'))) { | ||
_context.next = 5; | ||
break; | ||
} | ||
_context.next = 4; | ||
return almanac.factValue(value.fact, value.params, value.path); | ||
case 4: | ||
value = _context.sent; | ||
case 5: | ||
return _context.abrupt('return', value); | ||
case 6: | ||
case 'end': | ||
return _context.stop(); | ||
} | ||
} | ||
}, _callee, this); | ||
})); | ||
function _getValue(_x2) { | ||
return _ref.apply(this, arguments); | ||
value: function _getValue(almanac) { | ||
var value = this.value; | ||
if (isObjectLike(value) && value.hasOwnProperty('fact')) { | ||
// value: { fact: 'xyz' } | ||
return almanac.factValue(value.fact, value.params, value.path); | ||
} | ||
return Promise.resolve(value); | ||
} | ||
return _getValue; | ||
}() | ||
/** | ||
@@ -149,73 +118,22 @@ * Takes the fact result and compares it to the condition 'value', using the operator | ||
key: 'evaluate', | ||
value: function () { | ||
var _ref2 = _asyncToGenerator(regeneratorRuntime.mark(function _callee2(almanac, operatorMap) { | ||
var op, rightHandSideValue, leftHandSideValue, result; | ||
return regeneratorRuntime.wrap(function _callee2$(_context2) { | ||
while (1) { | ||
switch (_context2.prev = _context2.next) { | ||
case 0: | ||
if (almanac) { | ||
_context2.next = 2; | ||
break; | ||
} | ||
value: function evaluate(almanac, operatorMap) { | ||
var _this = this; | ||
throw new Error('almanac required'); | ||
if (!almanac) return Promise.reject(new Error('almanac required')); | ||
if (!operatorMap) return Promise.reject(new Error('operatorMap required')); | ||
if (this.isBooleanOperator()) return Promise.reject(new Error('Cannot evaluate() a boolean condition')); | ||
case 2: | ||
if (operatorMap) { | ||
_context2.next = 4; | ||
break; | ||
} | ||
var op = operatorMap.get(this.operator); | ||
if (!op) return Promise.reject(new Error('Unknown operator: ' + this.operator)); | ||
throw new Error('operatorMap required'); | ||
return this._getValue(almanac) // todo - parallelize | ||
.then(function (rightHandSideValue) { | ||
return almanac.factValue(_this.fact, _this.params, _this.path).then(function (leftHandSideValue) { | ||
var result = op.evaluate(leftHandSideValue, rightHandSideValue); | ||
debug('condition::evaluate <' + leftHandSideValue + ' ' + _this.operator + ' ' + rightHandSideValue + '?> (' + result + ')'); | ||
return { result: result, leftHandSideValue: leftHandSideValue, rightHandSideValue: rightHandSideValue, operator: _this.operator }; | ||
}); | ||
}); | ||
} | ||
case 4: | ||
if (!this.isBooleanOperator()) { | ||
_context2.next = 6; | ||
break; | ||
} | ||
throw new Error('Cannot evaluate() a boolean condition'); | ||
case 6: | ||
op = operatorMap.get(this.operator); | ||
if (op) { | ||
_context2.next = 9; | ||
break; | ||
} | ||
throw new Error('Unknown operator: ' + this.operator); | ||
case 9: | ||
_context2.next = 11; | ||
return this._getValue(almanac); | ||
case 11: | ||
rightHandSideValue = _context2.sent; | ||
_context2.next = 14; | ||
return almanac.factValue(this.fact, this.params, this.path); | ||
case 14: | ||
leftHandSideValue = _context2.sent; | ||
result = op.evaluate(leftHandSideValue, rightHandSideValue); | ||
debug('condition::evaluate <' + leftHandSideValue + ' ' + this.operator + ' ' + rightHandSideValue + '?> (' + result + ')'); | ||
return _context2.abrupt('return', { result: result, leftHandSideValue: leftHandSideValue, rightHandSideValue: rightHandSideValue, operator: this.operator }); | ||
case 18: | ||
case 'end': | ||
return _context2.stop(); | ||
} | ||
} | ||
}, _callee2, this); | ||
})); | ||
function evaluate(_x3, _x4) { | ||
return _ref2.apply(this, arguments); | ||
} | ||
return evaluate; | ||
}() | ||
/** | ||
@@ -222,0 +140,0 @@ * Returns the boolean operator for the condition |
@@ -36,4 +36,2 @@ 'use strict'; | ||
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"); }); }; } | ||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } | ||
@@ -213,42 +211,23 @@ | ||
key: 'evaluateRules', | ||
value: function () { | ||
var _ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee(ruleArray, almanac) { | ||
var _this2 = this; | ||
value: function evaluateRules(ruleArray, almanac) { | ||
var _this2 = this; | ||
return regeneratorRuntime.wrap(function _callee$(_context) { | ||
while (1) { | ||
switch (_context.prev = _context.next) { | ||
case 0: | ||
return _context.abrupt('return', Promise.all(ruleArray.map(function (rule) { | ||
if (_this2.status !== RUNNING) { | ||
debug('engine::run status:' + _this2.status + '; skipping remaining rules'); | ||
return; | ||
} | ||
return rule.evaluate(almanac).then(function (ruleResult) { | ||
debug('engine::run ruleResult:' + ruleResult.result); | ||
if (ruleResult.result) { | ||
_this2.emit('success', rule.event, almanac, ruleResult); | ||
_this2.emit(rule.event.type, rule.event.params, almanac, ruleResult); | ||
almanac.factValue('success-events', { event: rule.event }); | ||
} else { | ||
_this2.emit('failure', rule.event, almanac, ruleResult); | ||
} | ||
}); | ||
}))); | ||
case 1: | ||
case 'end': | ||
return _context.stop(); | ||
} | ||
return Promise.all(ruleArray.map(function (rule) { | ||
if (_this2.status !== RUNNING) { | ||
debug('engine::run status:' + _this2.status + '; skipping remaining rules'); | ||
return; | ||
} | ||
return rule.evaluate(almanac).then(function (ruleResult) { | ||
debug('engine::run ruleResult:' + ruleResult.result); | ||
if (ruleResult.result) { | ||
_this2.emit('success', rule.event, almanac, ruleResult); | ||
_this2.emit(rule.event.type, rule.event.params, almanac, ruleResult); | ||
almanac.factValue('success-events', { event: rule.event }); | ||
} else { | ||
_this2.emit('failure', rule.event, almanac, ruleResult); | ||
} | ||
}, _callee, this); | ||
}); | ||
})); | ||
} | ||
function evaluateRules(_x3, _x4) { | ||
return _ref.apply(this, arguments); | ||
} | ||
return evaluateRules; | ||
}() | ||
/** | ||
@@ -263,50 +242,30 @@ * Runs the rules engine | ||
key: 'run', | ||
value: function () { | ||
var _ref2 = _asyncToGenerator(regeneratorRuntime.mark(function _callee2() { | ||
var _this3 = this; | ||
value: function run() { | ||
var _this3 = this; | ||
var runtimeFacts = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; | ||
var almanac, orderedSets, cursor; | ||
return regeneratorRuntime.wrap(function _callee2$(_context2) { | ||
while (1) { | ||
switch (_context2.prev = _context2.next) { | ||
case 0: | ||
debug('engine::run started'); | ||
debug('engine::run runtimeFacts:', runtimeFacts); | ||
runtimeFacts['success-events'] = new _fact2.default('success-events', (0, _engineFacts.SuccessEventFact)(), { cache: false }); | ||
this.status = RUNNING; | ||
almanac = new _almanac2.default(this.facts, runtimeFacts); | ||
orderedSets = this.prioritizeRules(); | ||
cursor = Promise.resolve(); | ||
// for each rule set, evaluate in parallel, | ||
// before proceeding to the next priority set. | ||
var runtimeFacts = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; | ||
return _context2.abrupt('return', new Promise(function (resolve, reject) { | ||
orderedSets.map(function (set) { | ||
cursor = cursor.then(function () { | ||
return _this3.evaluateRules(set, almanac); | ||
}).catch(reject); | ||
return cursor; | ||
}); | ||
cursor.then(function () { | ||
_this3.status = FINISHED; | ||
debug('engine::run completed'); | ||
resolve(almanac.factValue('success-events')); | ||
}).catch(reject); | ||
})); | ||
case 8: | ||
case 'end': | ||
return _context2.stop(); | ||
} | ||
} | ||
}, _callee2, this); | ||
})); | ||
function run() { | ||
return _ref2.apply(this, arguments); | ||
} | ||
return run; | ||
}() | ||
debug('engine::run started'); | ||
debug('engine::run runtimeFacts:', runtimeFacts); | ||
runtimeFacts['success-events'] = new _fact2.default('success-events', (0, _engineFacts.SuccessEventFact)(), { cache: false }); | ||
this.status = RUNNING; | ||
var almanac = new _almanac2.default(this.facts, runtimeFacts); | ||
var orderedSets = this.prioritizeRules(); | ||
var cursor = Promise.resolve(); | ||
// for each rule set, evaluate in parallel, | ||
// before proceeding to the next priority set. | ||
return new Promise(function (resolve, reject) { | ||
orderedSets.map(function (set) { | ||
cursor = cursor.then(function () { | ||
return _this3.evaluateRules(set, almanac); | ||
}).catch(reject); | ||
return cursor; | ||
}); | ||
cursor.then(function () { | ||
_this3.status = FINISHED; | ||
debug('engine::run completed'); | ||
resolve(almanac.factValue('success-events')); | ||
}).catch(reject); | ||
}); | ||
} | ||
}]); | ||
@@ -313,0 +272,0 @@ |
@@ -38,4 +38,6 @@ 'use strict'; | ||
this.value = valueOrMethod; | ||
this.type = this.constructor.CONSTANT; | ||
} else { | ||
this.calculationMethod = valueOrMethod; | ||
this.type = this.constructor.DYNAMIC; | ||
} | ||
@@ -54,11 +56,21 @@ | ||
/** | ||
* Return the fact value, based on provided parameters | ||
* @param {object} params | ||
* @param {Almanac} almanac | ||
* @return {any} calculation method results | ||
*/ | ||
_createClass(Fact, [{ | ||
key: 'isConstant', | ||
value: function isConstant() { | ||
return this.type === this.constructor.CONSTANT; | ||
} | ||
}, { | ||
key: 'isDynamic', | ||
value: function isDynamic() { | ||
return this.type === this.constructor.DYNAMIC; | ||
} | ||
/** | ||
* Return the fact value, based on provided parameters | ||
* @param {object} params | ||
* @param {Almanac} almanac | ||
* @return {any} calculation method results | ||
*/ | ||
_createClass(Fact, [{ | ||
}, { | ||
key: 'calculate', | ||
@@ -107,3 +119,4 @@ value: function calculate(params, almanac) { | ||
var cacheProperties = this.cacheKeyMethod(this.id, params); | ||
return Fact.hashFromObject(cacheProperties); | ||
var _hash = Fact.hashFromObject(cacheProperties); | ||
return _hash; | ||
} | ||
@@ -122,2 +135,5 @@ } | ||
Fact.CONSTANT = 'CONSTANT'; | ||
Fact.DYNAMIC = 'DYNAMIC'; | ||
exports.default = Fact; |
'use strict'; | ||
require('./generator-runtime'); | ||
module.exports = require('./json-rules-engine'); |
440
dist/rule.js
@@ -21,4 +21,2 @@ 'use strict'; | ||
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"); }); }; } | ||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } | ||
@@ -189,324 +187,142 @@ | ||
key: 'evaluate', | ||
value: function () { | ||
var _ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee6(almanac) { | ||
var _this3 = this; | ||
value: function evaluate(almanac) { | ||
var _this3 = this; | ||
var ruleResult, evaluateCondition, evaluateConditions, prioritizeAndRun, any, all, processResult, result, _result; | ||
var ruleResult = new _ruleResult2.default(this.conditions, this.event, this.priority); | ||
return regeneratorRuntime.wrap(function _callee6$(_context6) { | ||
while (1) { | ||
switch (_context6.prev = _context6.next) { | ||
case 0: | ||
ruleResult = new _ruleResult2.default(this.conditions, this.event, this.priority); | ||
/** | ||
* Evaluates the rule conditions | ||
* @param {Condition} condition - condition to evaluate | ||
* @return {Promise(true|false)} - resolves with the result of the condition evaluation | ||
*/ | ||
var evaluateCondition = function evaluateCondition(condition) { | ||
if (condition.isBooleanOperator()) { | ||
var subConditions = condition[condition.operator]; | ||
var comparisonPromise = void 0; | ||
if (condition.operator === 'all') { | ||
comparisonPromise = all(subConditions); | ||
} else { | ||
comparisonPromise = any(subConditions); | ||
} | ||
// for booleans, rule passing is determined by the all/any result | ||
return comparisonPromise.then(function (comparisonValue) { | ||
var passes = comparisonValue === true; | ||
condition.result = passes; | ||
return passes; | ||
}); | ||
} else { | ||
return condition.evaluate(almanac, _this3.engine.operators).then(function (evaluationResult) { | ||
var passes = evaluationResult.result; | ||
condition.factResult = evaluationResult.leftHandSideValue; | ||
condition.result = passes; | ||
return passes; | ||
}).catch(function (err) { | ||
// any condition raising an undefined fact error is considered falsey when allowUndefinedFacts is enabled | ||
if (_this3.engine.allowUndefinedFacts && err.code === 'UNDEFINED_FACT') return false; | ||
throw err; | ||
}); | ||
} | ||
}; | ||
/** | ||
* Evaluates the rule conditions | ||
* @param {Condition} condition - condition to evaluate | ||
* @return {Promise(true|false)} - resolves with the result of the condition evaluation | ||
*/ | ||
/** | ||
* Evalutes an array of conditions, using an 'every' or 'some' array operation | ||
* @param {Condition[]} conditions | ||
* @param {string(every|some)} array method to call for determining result | ||
* @return {Promise(boolean)} whether conditions evaluated truthy or falsey based on condition evaluation + method | ||
*/ | ||
var evaluateConditions = function evaluateConditions(conditions, method) { | ||
if (!Array.isArray(conditions)) conditions = [conditions]; | ||
evaluateCondition = function () { | ||
var _ref2 = _asyncToGenerator(regeneratorRuntime.mark(function _callee(condition) { | ||
var comparisonValue, passes, subConditions, evaluationResult; | ||
return regeneratorRuntime.wrap(function _callee$(_context) { | ||
while (1) { | ||
switch (_context.prev = _context.next) { | ||
case 0: | ||
comparisonValue = void 0; | ||
passes = void 0; | ||
return Promise.all(conditions.map(function (condition) { | ||
return evaluateCondition(condition); | ||
})).then(function (conditionResults) { | ||
debug('rule::evaluateConditions results', conditionResults); | ||
return method.call(conditionResults, function (result) { | ||
return result === true; | ||
}); | ||
}); | ||
}; | ||
if (!condition.isBooleanOperator()) { | ||
_context.next = 16; | ||
break; | ||
} | ||
/** | ||
* Evaluates a set of conditions based on an 'all' or 'any' operator. | ||
* First, orders the top level conditions based on priority | ||
* Iterates over each priority set, evaluating each condition | ||
* If any condition results in the rule to be guaranteed truthy or falsey, | ||
* it will short-circuit and not bother evaluating any additional rules | ||
* @param {Condition[]} conditions - conditions to be evaluated | ||
* @param {string('all'|'any')} operator | ||
* @return {Promise(boolean)} rule evaluation result | ||
*/ | ||
var prioritizeAndRun = function prioritizeAndRun(conditions, operator) { | ||
if (conditions.length === 0) { | ||
return Promise.resolve(true); | ||
} | ||
var method = Array.prototype.some; | ||
if (operator === 'all') { | ||
method = Array.prototype.every; | ||
} | ||
var orderedSets = _this3.prioritizeConditions(conditions); | ||
var cursor = Promise.resolve(); | ||
orderedSets.forEach(function (set) { | ||
var stop = false; | ||
cursor = cursor.then(function (setResult) { | ||
// after the first set succeeds, don't fire off the remaining promises | ||
if (operator === 'any' && setResult === true || stop) { | ||
debug('prioritizeAndRun::detected truthy result; skipping remaining conditions'); | ||
stop = true; | ||
return true; | ||
} | ||
subConditions = condition[condition.operator]; | ||
// after the first set fails, don't fire off the remaining promises | ||
if (operator === 'all' && setResult === false || stop) { | ||
debug('prioritizeAndRun::detected falsey result; skipping remaining conditions'); | ||
stop = true; | ||
return false; | ||
} | ||
// all conditions passed; proceed with running next set in parallel | ||
return evaluateConditions(set, method); | ||
}); | ||
}); | ||
return cursor; | ||
}; | ||
if (!(condition.operator === 'all')) { | ||
_context.next = 10; | ||
break; | ||
} | ||
/** | ||
* Runs an 'any' boolean operator on an array of conditions | ||
* @param {Condition[]} conditions to be evaluated | ||
* @return {Promise(boolean)} condition evaluation result | ||
*/ | ||
var any = function any(conditions) { | ||
return prioritizeAndRun(conditions, 'any'); | ||
}; | ||
_context.next = 7; | ||
return all(subConditions); | ||
/** | ||
* Runs an 'all' boolean operator on an array of conditions | ||
* @param {Condition[]} conditions to be evaluated | ||
* @return {Promise(boolean)} condition evaluation result | ||
*/ | ||
var all = function all(conditions) { | ||
return prioritizeAndRun(conditions, 'all'); | ||
}; | ||
case 7: | ||
comparisonValue = _context.sent; | ||
_context.next = 13; | ||
break; | ||
/** | ||
* Emits based on rule evaluation result, and decorates ruleResult with 'result' property | ||
* @param {Boolean} result | ||
*/ | ||
var processResult = function processResult(result) { | ||
ruleResult.setResult(result); | ||
case 10: | ||
_context.next = 12; | ||
return any(subConditions); | ||
if (result) _this3.emit('success', ruleResult.event, almanac, ruleResult);else _this3.emit('failure', ruleResult.event, almanac, ruleResult); | ||
return ruleResult; | ||
}; | ||
case 12: | ||
comparisonValue = _context.sent; | ||
case 13: | ||
// for booleans, rule passing is determined by the all/any result | ||
passes = comparisonValue === true; | ||
_context.next = 31; | ||
break; | ||
case 16: | ||
_context.prev = 16; | ||
_context.next = 19; | ||
return condition.evaluate(almanac, _this3.engine.operators, comparisonValue); | ||
case 19: | ||
evaluationResult = _context.sent; | ||
passes = evaluationResult.result; | ||
condition.factResult = evaluationResult.leftHandSideValue; | ||
_context.next = 31; | ||
break; | ||
case 24: | ||
_context.prev = 24; | ||
_context.t0 = _context['catch'](16); | ||
if (!(_this3.engine.allowUndefinedFacts && _context.t0.code === 'UNDEFINED_FACT')) { | ||
_context.next = 30; | ||
break; | ||
} | ||
passes = false; | ||
_context.next = 31; | ||
break; | ||
case 30: | ||
throw _context.t0; | ||
case 31: | ||
condition.result = passes; | ||
return _context.abrupt('return', passes); | ||
case 33: | ||
case 'end': | ||
return _context.stop(); | ||
} | ||
} | ||
}, _callee, _this3, [[16, 24]]); | ||
})); | ||
return function evaluateCondition(_x3) { | ||
return _ref2.apply(this, arguments); | ||
}; | ||
}(); | ||
/** | ||
* Evalutes an array of conditions, using an 'every' or 'some' array operation | ||
* @param {Condition[]} conditions | ||
* @param {string(every|some)} array method to call for determining result | ||
* @return {Promise(boolean)} whether conditions evaluated truthy or falsey based on condition evaluation + method | ||
*/ | ||
evaluateConditions = function () { | ||
var _ref3 = _asyncToGenerator(regeneratorRuntime.mark(function _callee2(conditions, method) { | ||
var conditionResults; | ||
return regeneratorRuntime.wrap(function _callee2$(_context2) { | ||
while (1) { | ||
switch (_context2.prev = _context2.next) { | ||
case 0: | ||
if (!Array.isArray(conditions)) conditions = [conditions]; | ||
_context2.next = 3; | ||
return Promise.all(conditions.map(function (condition) { | ||
return evaluateCondition(condition); | ||
})); | ||
case 3: | ||
conditionResults = _context2.sent; | ||
debug('rule::evaluateConditions results', conditionResults); | ||
return _context2.abrupt('return', method.call(conditionResults, function (result) { | ||
return result === true; | ||
})); | ||
case 6: | ||
case 'end': | ||
return _context2.stop(); | ||
} | ||
} | ||
}, _callee2, _this3); | ||
})); | ||
return function evaluateConditions(_x4, _x5) { | ||
return _ref3.apply(this, arguments); | ||
}; | ||
}(); | ||
/** | ||
* Evaluates a set of conditions based on an 'all' or 'any' operator. | ||
* First, orders the top level conditions based on priority | ||
* Iterates over each priority set, evaluating each condition | ||
* If any condition results in the rule to be guaranteed truthy or falsey, | ||
* it will short-circuit and not bother evaluating any additional rules | ||
* @param {Condition[]} conditions - conditions to be evaluated | ||
* @param {string('all'|'any')} operator | ||
* @return {Promise(boolean)} rule evaluation result | ||
*/ | ||
prioritizeAndRun = function () { | ||
var _ref4 = _asyncToGenerator(regeneratorRuntime.mark(function _callee3(conditions, operator) { | ||
var method, orderedSets, cursor; | ||
return regeneratorRuntime.wrap(function _callee3$(_context3) { | ||
while (1) { | ||
switch (_context3.prev = _context3.next) { | ||
case 0: | ||
if (!(conditions.length === 0)) { | ||
_context3.next = 2; | ||
break; | ||
} | ||
return _context3.abrupt('return', true); | ||
case 2: | ||
method = Array.prototype.some; | ||
if (operator === 'all') { | ||
method = Array.prototype.every; | ||
} | ||
orderedSets = _this3.prioritizeConditions(conditions); | ||
cursor = Promise.resolve(); | ||
orderedSets.forEach(function (set) { | ||
var stop = false; | ||
cursor = cursor.then(function (setResult) { | ||
// after the first set succeeds, don't fire off the remaining promises | ||
if (operator === 'any' && setResult === true || stop) { | ||
debug('prioritizeAndRun::detected truthy result; skipping remaining conditions'); | ||
stop = true; | ||
return true; | ||
} | ||
// after the first set fails, don't fire off the remaining promises | ||
if (operator === 'all' && setResult === false || stop) { | ||
debug('prioritizeAndRun::detected falsey result; skipping remaining conditions'); | ||
stop = true; | ||
return false; | ||
} | ||
// all conditions passed; proceed with running next set in parallel | ||
return evaluateConditions(set, method); | ||
}); | ||
}); | ||
return _context3.abrupt('return', cursor); | ||
case 8: | ||
case 'end': | ||
return _context3.stop(); | ||
} | ||
} | ||
}, _callee3, _this3); | ||
})); | ||
return function prioritizeAndRun(_x6, _x7) { | ||
return _ref4.apply(this, arguments); | ||
}; | ||
}(); | ||
/** | ||
* Runs an 'any' boolean operator on an array of conditions | ||
* @param {Condition[]} conditions to be evaluated | ||
* @return {Promise(boolean)} condition evaluation result | ||
*/ | ||
any = function () { | ||
var _ref5 = _asyncToGenerator(regeneratorRuntime.mark(function _callee4(conditions) { | ||
return regeneratorRuntime.wrap(function _callee4$(_context4) { | ||
while (1) { | ||
switch (_context4.prev = _context4.next) { | ||
case 0: | ||
return _context4.abrupt('return', prioritizeAndRun(conditions, 'any')); | ||
case 1: | ||
case 'end': | ||
return _context4.stop(); | ||
} | ||
} | ||
}, _callee4, _this3); | ||
})); | ||
return function any(_x8) { | ||
return _ref5.apply(this, arguments); | ||
}; | ||
}(); | ||
/** | ||
* Runs an 'all' boolean operator on an array of conditions | ||
* @param {Condition[]} conditions to be evaluated | ||
* @return {Promise(boolean)} condition evaluation result | ||
*/ | ||
all = function () { | ||
var _ref6 = _asyncToGenerator(regeneratorRuntime.mark(function _callee5(conditions) { | ||
return regeneratorRuntime.wrap(function _callee5$(_context5) { | ||
while (1) { | ||
switch (_context5.prev = _context5.next) { | ||
case 0: | ||
return _context5.abrupt('return', prioritizeAndRun(conditions, 'all')); | ||
case 1: | ||
case 'end': | ||
return _context5.stop(); | ||
} | ||
} | ||
}, _callee5, _this3); | ||
})); | ||
return function all(_x9) { | ||
return _ref6.apply(this, arguments); | ||
}; | ||
}(); | ||
/** | ||
* Emits based on rule evaluation result, and decorates ruleResult with 'result' property | ||
* @param {Boolean} result | ||
*/ | ||
processResult = function processResult(result) { | ||
ruleResult.setResult(result); | ||
if (result) _this3.emit('success', ruleResult.event, almanac, ruleResult);else _this3.emit('failure', ruleResult.event, almanac, ruleResult); | ||
return ruleResult; | ||
}; | ||
if (!ruleResult.conditions.any) { | ||
_context6.next = 14; | ||
break; | ||
} | ||
_context6.next = 10; | ||
return any(ruleResult.conditions.any); | ||
case 10: | ||
result = _context6.sent; | ||
return _context6.abrupt('return', processResult(result)); | ||
case 14: | ||
_context6.next = 16; | ||
return all(ruleResult.conditions.all); | ||
case 16: | ||
_result = _context6.sent; | ||
return _context6.abrupt('return', processResult(_result)); | ||
case 18: | ||
case 'end': | ||
return _context6.stop(); | ||
} | ||
} | ||
}, _callee6, this); | ||
})); | ||
function evaluate(_x2) { | ||
return _ref.apply(this, arguments); | ||
if (ruleResult.conditions.any) { | ||
return any(ruleResult.conditions.any).then(function (result) { | ||
return processResult(result); | ||
}); | ||
} else { | ||
return all(ruleResult.conditions.all).then(function (result) { | ||
return processResult(result); | ||
}); | ||
} | ||
return evaluate; | ||
}() | ||
} | ||
}]); | ||
@@ -513,0 +329,0 @@ |
{ | ||
"name": "json-rules-engine", | ||
"version": "2.1.0", | ||
"version": "2.2.0-alpha1", | ||
"description": "Rules Engine expressed in simple json", | ||
@@ -10,4 +10,4 @@ "main": "dist/index.js", | ||
"lint:fix": "standard --fix", | ||
"prepublish": "npm run compile", | ||
"compile": "babel --stage 1 -d dist/ src/ && regenerator --no-cache-dir --include-runtime src/generator-runtime.js > dist/generator-runtime.js" | ||
"prepublishOnly": "npm run build", | ||
"build": "babel --stage 1 -d dist/ src/" | ||
}, | ||
@@ -41,3 +41,3 @@ "repository": { | ||
}, | ||
"author": "Cache Hamm <cdhamm@gmail.com>", | ||
"author": "Cache Hamm <cache.hamm@gmail.com>", | ||
"license": "ISC", | ||
@@ -53,3 +53,2 @@ "bugs": { | ||
"babel-loader": "7.1.2", | ||
"babel-plugin-transform-async-to-generator": "^6.4.6", | ||
"babel-polyfill": "6.26.0", | ||
@@ -59,12 +58,12 @@ "babel-preset-es2015": "~6.24.1", | ||
"babel-register": "6.26.0", | ||
"chai": "4.1.2", | ||
"chai-as-promised": "7.1.1", | ||
"chai": "^4.1.2", | ||
"chai-as-promised": "^7.1.1", | ||
"colors": "~1.1.2", | ||
"dirty-chai": "2.0.1", | ||
"mocha": "4.0.1", | ||
"regenerator": "0.11.0", | ||
"sinon": "4.1.2", | ||
"sinon-chai": "2.14.0", | ||
"mocha": "^5.0.5", | ||
"perfy": "^1.1.2", | ||
"sinon": "^4.1.2", | ||
"sinon-chai": "^2.14.0", | ||
"snazzy": "^7.0.0", | ||
"standard": "10.0.3" | ||
"standard": "^10.0.3" | ||
}, | ||
@@ -71,0 +70,0 @@ "dependencies": { |
@@ -54,3 +54,3 @@ import { Fact } from '../src/index' | ||
it('throws an exception if it encounters an undefined fact', () => { | ||
return expect(almanac.factValue('bar')).to.eventually.be.rejectedWith(/Undefined fact: bar/) | ||
return expect(almanac.factValue('bar')).to.be.rejectedWith(/Undefined fact: bar/) | ||
}) | ||
@@ -84,7 +84,2 @@ }) | ||
}) | ||
it('raises an exception if fact DNE', () => { | ||
almanac = new Almanac(new Map()) | ||
expect(almanac._getFact.bind(almanac, 'unknown')).to.throw(/Undefined fact/) | ||
}) | ||
}) | ||
@@ -91,0 +86,0 @@ |
@@ -6,7 +6,6 @@ 'use strict' | ||
describe('Fact', () => { | ||
function subject (id, definition, options) { | ||
return new Fact(id, definition, options) | ||
} | ||
describe('Fact::constructor', () => { | ||
function subject (id, definition, options) { | ||
return new Fact(id, definition, options) | ||
} | ||
it('works for constant facts', () => { | ||
@@ -40,2 +39,16 @@ let fact = subject('factId', 10) | ||
}) | ||
describe('Fact::types', () => { | ||
it('initializes facts with method values as dynamic', () => { | ||
let fact = subject('factId', () => {}) | ||
expect(fact.type).to.equal(Fact.DYNAMIC) | ||
expect(fact.isDynamic()).to.be.true() | ||
}) | ||
it('initializes facts with non-methods as constant', () => { | ||
let fact = subject('factId', 2) | ||
expect(fact.type).to.equal(Fact.CONSTANT) | ||
expect(fact.isConstant()).to.be.true() | ||
}) | ||
}) | ||
}) |
Sorry, the diff of this file is not supported yet
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
18
65
226393
5283
1