Skip to content

Commit 96f5c17

Browse files
author
Christopher Pardy
committed
Add Condition / ConditionReference Support
Add Support for Conditions which are added to the engine and then referenced in various rules. Condition references can be used as top-level conditions in a rule and can be used recursively.
1 parent 1d5d304 commit 96f5c17

File tree

6 files changed

+369
-13
lines changed

6 files changed

+369
-13
lines changed

src/condition.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export default class Condition {
2121
} else {
2222
this[booleanOperator] = new Condition(subConditions)
2323
}
24-
} else {
24+
} else if (!Object.prototype.hasOwnProperty.call(properties, 'condition')) {
2525
if (!Object.prototype.hasOwnProperty.call(properties, 'fact')) throw new Error('Condition: constructor "fact" property required')
2626
if (!Object.prototype.hasOwnProperty.call(properties, 'operator')) throw new Error('Condition: constructor "operator" property required')
2727
if (!Object.prototype.hasOwnProperty.call(properties, 'value')) throw new Error('Condition: constructor "value" property required')
@@ -54,6 +54,8 @@ export default class Condition {
5454
} else {
5555
props[oper] = this[oper].toJSON(false)
5656
}
57+
} else if (this.isConditionReference()) {
58+
props.condition = this.condition
5759
} else {
5860
props.operator = this.operator
5961
props.value = this.value
@@ -147,4 +149,12 @@ export default class Condition {
147149
isBooleanOperator () {
148150
return Condition.booleanOperator(this) !== undefined
149151
}
152+
153+
/**
154+
* Whether the condition represents a reference to a condition
155+
* @returns {Boolean}
156+
*/
157+
isConditionReference () {
158+
return Object.prototype.hasOwnProperty.call(this, 'condition')
159+
}
150160
}

src/engine.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import Almanac from './almanac'
77
import EventEmitter from 'eventemitter2'
88
import defaultOperators from './engine-default-operators'
99
import debug from './debug'
10+
import Condition from './condition'
1011

1112
export const READY = 'READY'
1213
export const RUNNING = 'RUNNING'
@@ -21,9 +22,11 @@ class Engine extends EventEmitter {
2122
super()
2223
this.rules = []
2324
this.allowUndefinedFacts = options.allowUndefinedFacts || false
25+
this.allowUndefinedConditions = options.allowUndefinedConditions || false
2426
this.pathResolver = options.pathResolver
2527
this.operators = new Map()
2628
this.facts = new Map()
29+
this.conditions = new Map()
2730
this.status = READY
2831
rules.map(r => this.addRule(r))
2932
defaultOperators.map(o => this.addOperator(o))
@@ -92,6 +95,31 @@ class Engine extends EventEmitter {
9295
return ruleRemoved
9396
}
9497

98+
/**
99+
* sets a condition that can be referenced by the given name.
100+
* If a condition with the given name has already been set this will replace it.
101+
* @param {string} name - the name of the condition to be referenced by rules.
102+
* @param {object} conditions - the conditions to use when the condition is referenced.
103+
*/
104+
setCondition (name, conditions) {
105+
if (!name) throw new Error('Engine: setCondition() requires name')
106+
if (!conditions) throw new Error('Engine: setCondition() requires conditions')
107+
if (!Object.prototype.hasOwnProperty.call(conditions, 'all') && !Object.prototype.hasOwnProperty.call(conditions, 'any') && !Object.prototype.hasOwnProperty.call(conditions, 'not') && !Object.prototype.hasOwnProperty.call(conditions, 'condition')) {
108+
throw new Error('"conditions" root must contain a single instance of "all", "any", "not", or "condition"')
109+
}
110+
this.conditions.set(name, new Condition(conditions))
111+
return this
112+
}
113+
114+
/**
115+
* Removes a condition that has previously been added to this engine
116+
* @param {string} name - the name of the condition to remove.
117+
* @returns true if the condition existed, otherwise false
118+
*/
119+
removeCondition (name) {
120+
return this.conditions.delete(name)
121+
}
122+
95123
/**
96124
* Add a custom operator definition
97125
* @param {string} operatorOrName - operator identifier within the condition; i.e. instead of 'equals', 'greaterThan', etc

src/rule.js

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import Condition from './condition'
44
import RuleResult from './rule-result'
55
import debug from './debug'
6+
import deepClone from 'clone'
67
import EventEmitter from 'eventemitter2'
78

89
class Rule extends EventEmitter {
@@ -70,8 +71,8 @@ class Rule extends EventEmitter {
7071
* @param {object} conditions - conditions, root element must be a boolean operator
7172
*/
7273
setConditions (conditions) {
73-
if (!Object.prototype.hasOwnProperty.call(conditions, 'all') && !Object.prototype.hasOwnProperty.call(conditions, 'any') && !Object.prototype.hasOwnProperty.call(conditions, 'not')) {
74-
throw new Error('"conditions" root must contain a single instance of "all", "any", or "not"')
74+
if (!Object.prototype.hasOwnProperty.call(conditions, 'all') && !Object.prototype.hasOwnProperty.call(conditions, 'any') && !Object.prototype.hasOwnProperty.call(conditions, 'not') && !Object.prototype.hasOwnProperty.call(conditions, 'condition')) {
75+
throw new Error('"conditions" root must contain a single instance of "all", "any", "not", or "condition"')
7576
}
7677
this.conditions = new Condition(conditions)
7778
return this
@@ -188,7 +189,9 @@ class Rule extends EventEmitter {
188189
* @return {Promise(true|false)} - resolves with the result of the condition evaluation
189190
*/
190191
const evaluateCondition = (condition) => {
191-
if (condition.isBooleanOperator()) {
192+
if (condition.isConditionReference()) {
193+
return realize(this.engine.conditions.get(condition.condition), condition)
194+
} else if (condition.isBooleanOperator()) {
192195
const subConditions = condition[condition.operator]
193196
let comparisonPromise
194197
if (condition.operator === 'all') {
@@ -309,6 +312,23 @@ class Rule extends EventEmitter {
309312
return prioritizeAndRun([condition], 'not').then(result => !result)
310313
}
311314

315+
const realize = (condition, conditionReference) => {
316+
if (!condition) {
317+
if (this.engine.allowUndefinedConditions) {
318+
// undefined conditions always fail
319+
conditionReference.result = false
320+
return Promise.resolve(false)
321+
} else {
322+
throw new Error(`No condition ${conditionReference.condition} exists`)
323+
}
324+
} else {
325+
// project the referenced condition onto reference object and evaluate it.
326+
delete conditionReference.condition
327+
Object.assign(conditionReference, deepClone(condition))
328+
return evaluateCondition(conditionReference)
329+
}
330+
}
331+
312332
/**
313333
* Emits based on rule evaluation result, and decorates ruleResult with 'result' property
314334
* @param {RuleResult} ruleResult

0 commit comments

Comments
 (0)