json-rules-engine
Advanced tools
Comparing version 1.0.4 to 1.1.0
@@ -29,3 +29,2 @@ 'use strict'; | ||
*/ | ||
var Almanac = function () { | ||
@@ -127,3 +126,3 @@ function Almanac(factMap) { | ||
value: function () { | ||
var ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee(factId) { | ||
var _ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee(factId) { | ||
var params = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; | ||
@@ -162,3 +161,3 @@ var fact, cacheKey, cacheVal; | ||
function factValue(_x2, _x3) { | ||
return ref.apply(this, arguments); | ||
return _ref.apply(this, arguments); | ||
} | ||
@@ -165,0 +164,0 @@ |
@@ -44,2 +44,9 @@ 'use strict'; | ||
/** | ||
* Converts the condition into a json-friendly structure | ||
* @param {Boolean} stringify - whether to return as a json string | ||
* @returns {string,object} json string or json-friendly object | ||
*/ | ||
_createClass(Condition, [{ | ||
@@ -72,53 +79,19 @@ key: 'toJSON', | ||
} | ||
/** | ||
* Takes the fact result and compares it to the condition 'value', using the operator | ||
* @param {mixed} comparisonValue - fact result | ||
* @param {Map} operatorMap - map of available operators, keyed by operator name | ||
* @returns {Boolean} - evaluation result | ||
*/ | ||
}, { | ||
key: 'validateComparisonValue', | ||
value: function validateComparisonValue(comparisonValue) { | ||
switch (this.operator) { | ||
case 'contains': | ||
case 'doesNotContain': | ||
return Array.isArray(comparisonValue); | ||
case 'lessThan': | ||
case 'lessThanInclusive': | ||
case 'greaterThan': | ||
case 'greaterThanInclusive': | ||
return Number.parseFloat(comparisonValue).toString() !== 'NaN'; | ||
default: | ||
return true; | ||
} | ||
} | ||
}, { | ||
key: 'evaluate', | ||
value: function evaluate(comparisonValue) { | ||
if (!this.validateComparisonValue(comparisonValue)) { | ||
return false; | ||
} | ||
switch (this.operator) { | ||
case 'equal': | ||
return comparisonValue === this.value; | ||
case 'notEqual': | ||
return comparisonValue !== this.value; | ||
case 'in': | ||
return this.value.includes(comparisonValue); | ||
case 'notIn': | ||
return !this.value.includes(comparisonValue); | ||
case 'contains': | ||
return comparisonValue.includes(this.value); | ||
case 'doesNotContain': | ||
return !comparisonValue.includes(this.value); | ||
case 'lessThan': | ||
return comparisonValue < this.value; | ||
case 'lessThanInclusive': | ||
return comparisonValue <= this.value; | ||
case 'greaterThan': | ||
return comparisonValue > this.value; | ||
case 'greaterThanInclusive': | ||
return comparisonValue >= this.value; | ||
// for any/all, simply comparisonValue that the sub-condition array evaluated truthy | ||
case 'any': | ||
return comparisonValue === true; | ||
case 'all': | ||
return comparisonValue === true; | ||
default: | ||
throw new Error('Unknown operator: ' + this.operator); | ||
} | ||
value: function evaluate(comparisonValue, operatorMap) { | ||
// for any/all, simply comparisonValue that the sub-condition array evaluated truthy | ||
if (this.isBooleanOperator()) return comparisonValue === true; | ||
var op = operatorMap.get(this.operator); | ||
if (!op) throw new Error('Unknown operator: ' + this.operator); | ||
return op.evaluate(comparisonValue, this.value); | ||
} | ||
@@ -134,5 +107,18 @@ | ||
key: 'booleanOperator', | ||
/** | ||
* Returns the condition's boolean operator | ||
* Instance version of Condition.isBooleanOperator | ||
* @returns {string,undefined} - 'any', 'all', or undefined (if not a boolean condition) | ||
*/ | ||
value: function booleanOperator() { | ||
return Condition.booleanOperator(this); | ||
} | ||
/** | ||
* Whether the operator is boolean ('all', 'any') | ||
* @returns {Boolean} | ||
*/ | ||
}, { | ||
@@ -139,0 +125,0 @@ key: 'isBooleanOperator', |
@@ -22,2 +22,6 @@ 'use strict'; | ||
var _operator = require('./operator'); | ||
var _operator2 = _interopRequireDefault(_operator); | ||
var _almanac = require('./almanac'); | ||
@@ -31,2 +35,6 @@ | ||
var _engineDefaultOperators = require('./engine-default-operators'); | ||
var _engineDefaultOperators2 = _interopRequireDefault(_engineDefaultOperators); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
@@ -55,3 +63,2 @@ | ||
*/ | ||
function Engine() { | ||
@@ -65,7 +72,11 @@ var rules = arguments.length <= 0 || arguments[0] === undefined ? [] : arguments[0]; | ||
_this.rules = []; | ||
_this.operators = new Map(); | ||
_this.facts = new Map(); | ||
_this.status = READY; | ||
rules.map(function (r) { | ||
return _this.addRule(r); | ||
}); | ||
_this.facts = new Map(); | ||
_this.status = READY; | ||
_engineDefaultOperators2.default.map(function (o) { | ||
return _this.addOperator(o); | ||
}); | ||
return _this; | ||
@@ -104,2 +115,21 @@ } | ||
/** | ||
* Add a custom operator definition | ||
* @param {string} operatorOrName - operator identifier within the condition; i.e. instead of 'equals', 'greaterThan', etc | ||
* @param {function(factValue, jsonValue)} callback - the method to execute when the operator is encountered. | ||
*/ | ||
}, { | ||
key: 'addOperator', | ||
value: function addOperator(operatorOrName, cb) { | ||
debug('engine::addOperator name:' + operatorOrName); | ||
var operator = void 0; | ||
if (operatorOrName instanceof _operator2.default) { | ||
operator = operatorOrName; | ||
} else { | ||
operator = new _operator2.default(operatorOrName, cb); | ||
} | ||
this.operators.set(operator.name, operator); | ||
} | ||
/** | ||
* Add a fact definition to the engine. Facts are called by rules as they are evaluated. | ||
@@ -192,3 +222,3 @@ * @param {object|Fact} id - fact identifier or instance of Fact | ||
value: function () { | ||
var ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee(ruleArray, almanac) { | ||
var _ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee(ruleArray, almanac) { | ||
var _this3 = this; | ||
@@ -225,3 +255,3 @@ | ||
function evaluateRules(_x2, _x3) { | ||
return ref.apply(this, arguments); | ||
return _ref.apply(this, arguments); | ||
} | ||
@@ -242,3 +272,3 @@ | ||
value: function () { | ||
var ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee2() { | ||
var _ref2 = _asyncToGenerator(regeneratorRuntime.mark(function _callee2() { | ||
var _this4 = this; | ||
@@ -285,3 +315,3 @@ | ||
function run(_x4) { | ||
return ref.apply(this, arguments); | ||
return _ref2.apply(this, arguments); | ||
} | ||
@@ -288,0 +318,0 @@ |
@@ -28,3 +28,2 @@ 'use strict'; | ||
*/ | ||
function Fact(id, valueOrMethod, options) { | ||
@@ -31,0 +30,0 @@ _classCallCheck(this, Fact); |
@@ -6,3 +6,3 @@ 'use strict'; | ||
}); | ||
exports.Engine = exports.Rule = exports.Fact = undefined; | ||
exports.Engine = exports.Operator = exports.Rule = exports.Fact = undefined; | ||
@@ -25,2 +25,6 @@ exports.default = function (rules) { | ||
var _operator = require('./operator'); | ||
var _operator2 = _interopRequireDefault(_operator); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
@@ -30,2 +34,3 @@ | ||
exports.Rule = _rule2.default; | ||
exports.Operator = _operator2.default; | ||
exports.Engine = _engine2.default; |
@@ -36,3 +36,2 @@ 'use strict'; | ||
*/ | ||
function Rule(options) { | ||
@@ -169,3 +168,3 @@ _classCallCheck(this, Rule); | ||
value: function () { | ||
var ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee6(almanac) { | ||
var _ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee6(almanac) { | ||
var _this2 = this; | ||
@@ -183,5 +182,4 @@ | ||
*/ | ||
evaluateCondition = function () { | ||
var ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee(condition) { | ||
var _ref2 = _asyncToGenerator(regeneratorRuntime.mark(function _callee(condition) { | ||
var comparisonValue, subConditions, conditionResult; | ||
@@ -233,3 +231,3 @@ return regeneratorRuntime.wrap(function _callee$(_context) { | ||
case 17: | ||
conditionResult = condition.evaluate(comparisonValue); | ||
conditionResult = condition.evaluate(comparisonValue, _this2.engine.operators); | ||
@@ -250,3 +248,3 @@ if (!condition.isBooleanOperator()) { | ||
return function evaluateCondition(_x3) { | ||
return ref.apply(this, arguments); | ||
return _ref2.apply(this, arguments); | ||
}; | ||
@@ -264,3 +262,3 @@ }(); | ||
evaluateConditions = function () { | ||
var ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee2(conditions, method) { | ||
var _ref3 = _asyncToGenerator(regeneratorRuntime.mark(function _callee2(conditions, method) { | ||
var conditionResults; | ||
@@ -294,3 +292,3 @@ return regeneratorRuntime.wrap(function _callee2$(_context2) { | ||
return function evaluateConditions(_x4, _x5) { | ||
return ref.apply(this, arguments); | ||
return _ref3.apply(this, arguments); | ||
}; | ||
@@ -312,3 +310,3 @@ }(); | ||
prioritizeAndRun = function () { | ||
var ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee3(conditions, operator) { | ||
var _ref4 = _asyncToGenerator(regeneratorRuntime.mark(function _callee3(conditions, operator) { | ||
var method, orderedSets, cursor; | ||
@@ -366,3 +364,3 @@ return regeneratorRuntime.wrap(function _callee3$(_context3) { | ||
return function prioritizeAndRun(_x6, _x7) { | ||
return ref.apply(this, arguments); | ||
return _ref4.apply(this, arguments); | ||
}; | ||
@@ -379,3 +377,3 @@ }(); | ||
any = function () { | ||
var ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee4(conditions) { | ||
var _ref5 = _asyncToGenerator(regeneratorRuntime.mark(function _callee4(conditions) { | ||
return regeneratorRuntime.wrap(function _callee4$(_context4) { | ||
@@ -396,3 +394,3 @@ while (1) { | ||
return function any(_x8) { | ||
return ref.apply(this, arguments); | ||
return _ref5.apply(this, arguments); | ||
}; | ||
@@ -409,3 +407,3 @@ }(); | ||
all = function () { | ||
var ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee5(conditions) { | ||
var _ref6 = _asyncToGenerator(regeneratorRuntime.mark(function _callee5(conditions) { | ||
return regeneratorRuntime.wrap(function _callee5$(_context5) { | ||
@@ -426,3 +424,3 @@ while (1) { | ||
return function all(_x9) { | ||
return ref.apply(this, arguments); | ||
return _ref6.apply(this, arguments); | ||
}; | ||
@@ -458,3 +456,3 @@ }(); | ||
function evaluate(_x2) { | ||
return ref.apply(this, arguments); | ||
return _ref.apply(this, arguments); | ||
} | ||
@@ -461,0 +459,0 @@ |
@@ -52,2 +52,30 @@ # Engine | ||
### engine.addOperator(String operatorName, Function evaluateFunc(factValue, jsonValue)) | ||
Adds a custom operator to the engine. For situations that require going beyond the generic, built-in operators (`equal`, `greaterThan`, etc). | ||
```js | ||
/* | ||
* operatorName - operator identifier mentioned in the rule condition | ||
* evaluateFunc(factValue, jsonValue) - compares fact result to the condition 'value', returning boolean | ||
* factValue - the value returned from the fact | ||
* jsonValue - the "value" property stored in the condition itself | ||
*/ | ||
engine.addOperator('startsWithLetter', (factValue, jsonValue) => { | ||
if (!factValue.length) return false | ||
return factValue[0].toLowerCase() === jsonValue.toLowerCase() | ||
}) | ||
// and to use the operator... | ||
rule.setConditions({ | ||
all: [ | ||
{ | ||
fact: 'username', | ||
operator: 'startsWithLetter' // reference the operator name in the rule | ||
value: 'a' | ||
} | ||
] | ||
}) | ||
``` | ||
### engine.run([Object facts], [Object options]) -> Promise (Events) | ||
@@ -54,0 +82,0 @@ |
@@ -26,4 +26,4 @@ 'use strict' | ||
* Rule for identifying microsoft employees that have been terminated. | ||
* - Demonstates re-using a same fact with different parameters | ||
* - Demonstates calling a base fact, which serves to load data once and reuse later | ||
* - Demonstrates re-using a same fact with different parameters | ||
* - Demonstrates calling a base fact, which serves to load data once and reuse later | ||
*/ | ||
@@ -54,4 +54,4 @@ var microsoftRule = { | ||
* Rule for identifying accounts older than 5 years | ||
* - Demonstates calling a base fact, also shared by the account-information-field fact | ||
* - Demonstates performing computations on data retrieved by base fact | ||
* - Demonstrates calling a base fact, also shared by the account-information-field fact | ||
* - Demonstrates performing computations on data retrieved by base fact | ||
*/ | ||
@@ -84,3 +84,3 @@ var tenureRule = { | ||
* 'account-information' fact executes an api call and retrieves account data | ||
* - Demonstates facts called only by other facts and never mentioned directly in a rule | ||
* - Demonstrates facts called only by other facts and never mentioned directly in a rule | ||
*/ | ||
@@ -87,0 +87,0 @@ engine.addFact('account-information', function (params, almanac) { |
{ | ||
"name": "json-rules-engine", | ||
"version": "1.0.4", | ||
"version": "1.1.0", | ||
"description": "Rules Engine expressed in simple json", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
'use strict' | ||
import Condition from '../src/condition' | ||
import defaultOperators from '../src/engine-default-operators' | ||
let operators = new Map() | ||
defaultOperators.forEach(o => operators.set(o.name, o)) | ||
function condition () { | ||
@@ -30,4 +34,4 @@ return { | ||
setup({ operator: 'equal' }) | ||
expect(condition.evaluate(50)).to.equal(true) | ||
expect(condition.evaluate(5)).to.equal(false) | ||
expect(condition.evaluate(50, operators)).to.equal(true) | ||
expect(condition.evaluate(5, operators)).to.equal(false) | ||
}) | ||
@@ -37,4 +41,4 @@ | ||
setup({ operator: 'notEqual' }) | ||
expect(condition.evaluate(50)).to.equal(false) | ||
expect(condition.evaluate(5)).to.equal(true) | ||
expect(condition.evaluate(50, operators)).to.equal(false) | ||
expect(condition.evaluate(5, operators)).to.equal(true) | ||
}) | ||
@@ -47,4 +51,4 @@ | ||
}) | ||
expect(condition.evaluate(15)).to.equal(true) | ||
expect(condition.evaluate(99)).to.equal(false) | ||
expect(condition.evaluate(15, operators)).to.equal(true) | ||
expect(condition.evaluate(99, operators)).to.equal(false) | ||
}) | ||
@@ -57,4 +61,4 @@ | ||
}) | ||
expect(condition.evaluate([5, 10, 15])).to.equal(true) | ||
expect(condition.evaluate([1, 2, 3])).to.equal(false) | ||
expect(condition.evaluate([5, 10, 15], operators)).to.equal(true) | ||
expect(condition.evaluate([1, 2, 3], operators)).to.equal(false) | ||
}) | ||
@@ -67,4 +71,4 @@ | ||
}) | ||
expect(condition.evaluate([5, 10, 15])).to.equal(false) | ||
expect(condition.evaluate([1, 2, 3])).to.equal(true) | ||
expect(condition.evaluate([5, 10, 15], operators)).to.equal(false) | ||
expect(condition.evaluate([1, 2, 3], operators)).to.equal(true) | ||
}) | ||
@@ -77,4 +81,4 @@ | ||
}) | ||
expect(condition.evaluate(15)).to.equal(false) | ||
expect(condition.evaluate(99)).to.equal(true) | ||
expect(condition.evaluate(15, operators)).to.equal(false) | ||
expect(condition.evaluate(99, operators)).to.equal(true) | ||
}) | ||
@@ -84,5 +88,5 @@ | ||
setup({ operator: 'lessThan' }) | ||
expect(condition.evaluate(49)).to.equal(true) | ||
expect(condition.evaluate(50)).to.equal(false) | ||
expect(condition.evaluate(51)).to.equal(false) | ||
expect(condition.evaluate(49, operators)).to.equal(true) | ||
expect(condition.evaluate(50, operators)).to.equal(false) | ||
expect(condition.evaluate(51, operators)).to.equal(false) | ||
}) | ||
@@ -92,17 +96,17 @@ | ||
setup({ operator: 'lessThanInclusive' }) | ||
expect(condition.evaluate(49)).to.equal(true) | ||
expect(condition.evaluate(50)).to.equal(true) | ||
expect(condition.evaluate(51)).to.equal(false) | ||
expect(condition.evaluate(49, operators)).to.equal(true) | ||
expect(condition.evaluate(50, operators)).to.equal(true) | ||
expect(condition.evaluate(51, operators)).to.equal(false) | ||
}) | ||
it('evaluates "greaterThan"', () => { | ||
setup({ operator: 'greaterThan' }) | ||
expect(condition.evaluate(51)).to.equal(true) | ||
expect(condition.evaluate(49)).to.equal(false) | ||
expect(condition.evaluate(50)).to.equal(false) | ||
expect(condition.evaluate(51, operators)).to.equal(true) | ||
expect(condition.evaluate(49, operators)).to.equal(false) | ||
expect(condition.evaluate(50, operators)).to.equal(false) | ||
}) | ||
it('evaluates "greaterThanInclusive"', () => { | ||
setup({operator: 'greaterThanInclusive'}) | ||
expect(condition.evaluate(51)).to.equal(true) | ||
expect(condition.evaluate(50)).to.equal(true) | ||
expect(condition.evaluate(49)).to.equal(false) | ||
expect(condition.evaluate(51, operators)).to.equal(true) | ||
expect(condition.evaluate(50, operators)).to.equal(true) | ||
expect(condition.evaluate(49, operators)).to.equal(false) | ||
}) | ||
@@ -113,5 +117,5 @@ | ||
setup({operator: 'contains'}) | ||
expect(condition.evaluate(null)).to.equal(false) | ||
expect(condition.evaluate(null, operators)).to.equal(false) | ||
setup({operator: 'doesNotContain'}) | ||
expect(condition.evaluate(null)).to.equal(false) | ||
expect(condition.evaluate(null, operators)).to.equal(false) | ||
}) | ||
@@ -121,9 +125,9 @@ | ||
setup({operator: 'lessThan'}) | ||
expect(condition.evaluate(null)).to.equal(false) | ||
expect(condition.evaluate(null, operators)).to.equal(false) | ||
setup({operator: 'lessThanInclusive'}) | ||
expect(condition.evaluate(null)).to.equal(false) | ||
expect(condition.evaluate(null, operators)).to.equal(false) | ||
setup({operator: 'greaterThan'}) | ||
expect(condition.evaluate(null)).to.equal(false) | ||
expect(condition.evaluate(null, operators)).to.equal(false) | ||
setup({operator: 'greaterThanInclusive'}) | ||
expect(condition.evaluate(null)).to.equal(false) | ||
expect(condition.evaluate(null, operators)).to.equal(false) | ||
}) | ||
@@ -133,9 +137,9 @@ | ||
setup({operator: 'lessThan'}) | ||
expect(condition.evaluate('non-number')).to.equal(false) | ||
expect(condition.evaluate('non-number', operators)).to.equal(false) | ||
setup({operator: 'lessThan'}) | ||
expect(condition.evaluate(undefined)).to.equal(false) | ||
expect(condition.evaluate(undefined, operators)).to.equal(false) | ||
setup({operator: 'lessThan'}) | ||
expect(condition.evaluate([])).to.equal(false) | ||
expect(condition.evaluate([], operators)).to.equal(false) | ||
setup({operator: 'lessThan'}) | ||
expect(condition.evaluate({})).to.equal(false) | ||
expect(condition.evaluate({}, operators)).to.equal(false) | ||
}) | ||
@@ -142,0 +146,0 @@ }) |
@@ -7,2 +7,3 @@ 'use strict' | ||
import { Rule } from '../src/index' | ||
import { Operator } from '../src/index' | ||
@@ -17,2 +18,3 @@ describe('Engine', () => { | ||
expect(engine).to.have.property('addRule') | ||
expect(engine).to.have.property('addOperator') | ||
expect(engine).to.have.property('addFact') | ||
@@ -24,4 +26,6 @@ expect(engine).to.have.property('run') | ||
describe('constructor', () => { | ||
it('begins in status "READY"', () => { | ||
it('initializes with the default state', () => { | ||
expect(engine.status).to.equal('READY') | ||
expect(engine.rules.length).to.equal(0) | ||
expect(engine.operators.size).to.equal(10) | ||
}) | ||
@@ -76,2 +80,22 @@ | ||
describe('addOperator()', () => { | ||
it('adds the operator', () => { | ||
expect(engine.operators.size).to.equal(10) | ||
engine.addOperator('startsWithLetter', (factValue, jsonValue) => { | ||
return factValue[0] === jsonValue | ||
}) | ||
expect(engine.operators.size).to.equal(11) | ||
expect(engine.operators.get('startsWithLetter')).to.exist | ||
expect(engine.operators.get('startsWithLetter')).to.be.an.instanceof(Operator) | ||
}) | ||
it('accepts an operator instance', () => { | ||
expect(engine.operators.size).to.equal(10) | ||
let op = new Operator('my-operator', _ => true) | ||
engine.addOperator(op) | ||
expect(engine.operators.size).to.equal(11) | ||
expect(engine.operators.get('my-operator')).to.equal(op) | ||
}) | ||
}) | ||
describe('addFact()', () => { | ||
@@ -78,0 +102,0 @@ const FACT_NAME = 'FACT_NAME' |
170180
56
4154