Skip to content

Commit 48e50e9

Browse files
author
Christopher Pardy
committed
Fix Top Level Condition Reference Failure
There was no test for the top-level condition reference and this was failing because it was being treated as a `not` add a test and fix the bug.
1 parent 37b4342 commit 48e50e9

File tree

3 files changed

+112
-30
lines changed

3 files changed

+112
-30
lines changed

Diff for: package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "json-rules-engine",
3-
"version": "6.3.0",
3+
"version": "6.3.1",
44
"description": "Rules Engine expressed in simple json",
55
"main": "dist/index.js",
66
"types": "types/index.d.ts",

Diff for: src/rule.js

+70-29
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,15 @@ class Rule extends EventEmitter {
7171
* @param {object} conditions - conditions, root element must be a boolean operator
7272
*/
7373
setConditions (conditions) {
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"')
74+
if (
75+
!Object.prototype.hasOwnProperty.call(conditions, 'all') &&
76+
!Object.prototype.hasOwnProperty.call(conditions, 'any') &&
77+
!Object.prototype.hasOwnProperty.call(conditions, 'not') &&
78+
!Object.prototype.hasOwnProperty.call(conditions, 'condition')
79+
) {
80+
throw new Error(
81+
'"conditions" root must contain a single instance of "all", "any", "not", or "condition"'
82+
)
7683
}
7784
this.conditions = new Condition(conditions)
7885
return this
@@ -86,7 +93,11 @@ class Rule extends EventEmitter {
8693
*/
8794
setEvent (event) {
8895
if (!event) throw new Error('Rule: setEvent() requires event object')
89-
if (!Object.prototype.hasOwnProperty.call(event, 'type')) throw new Error('Rule: setEvent() requires event object with "type" property')
96+
if (!Object.prototype.hasOwnProperty.call(event, 'type')) {
97+
throw new Error(
98+
'Rule: setEvent() requires event object with "type" property'
99+
)
100+
}
90101
this.ruleEvent = {
91102
type: event.type
92103
}
@@ -170,9 +181,11 @@ class Rule extends EventEmitter {
170181
sets[priority].push(condition)
171182
return sets
172183
}, {})
173-
return Object.keys(factSets).sort((a, b) => {
174-
return Number(a) > Number(b) ? -1 : 1 // order highest priority -> lowest
175-
}).map((priority) => factSets[priority])
184+
return Object.keys(factSets)
185+
.sort((a, b) => {
186+
return Number(a) > Number(b) ? -1 : 1 // order highest priority -> lowest
187+
})
188+
.map((priority) => factSets[priority])
176189
}
177190

178191
/**
@@ -181,7 +194,12 @@ class Rule extends EventEmitter {
181194
* @return {Promise(RuleResult)} rule evaluation result
182195
*/
183196
evaluate (almanac) {
184-
const ruleResult = new RuleResult(this.conditions, this.ruleEvent, this.priority, this.name)
197+
const ruleResult = new RuleResult(
198+
this.conditions,
199+
this.ruleEvent,
200+
this.priority,
201+
this.name
202+
)
185203

186204
/**
187205
* Evaluates the rule conditions
@@ -190,7 +208,7 @@ class Rule extends EventEmitter {
190208
*/
191209
const evaluateCondition = (condition) => {
192210
if (condition.isConditionReference()) {
193-
return realize(this.engine.conditions.get(condition.condition), condition)
211+
return realize(condition)
194212
} else if (condition.isBooleanOperator()) {
195213
const subConditions = condition[condition.operator]
196214
let comparisonPromise
@@ -202,14 +220,15 @@ class Rule extends EventEmitter {
202220
comparisonPromise = not(subConditions)
203221
}
204222
// for booleans, rule passing is determined by the all/any/not result
205-
return comparisonPromise.then(comparisonValue => {
223+
return comparisonPromise.then((comparisonValue) => {
206224
const passes = comparisonValue === true
207225
condition.result = passes
208226
return passes
209227
})
210228
} else {
211-
return condition.evaluate(almanac, this.engine.operators)
212-
.then(evaluationResult => {
229+
return condition
230+
.evaluate(almanac, this.engine.operators)
231+
.then((evaluationResult) => {
213232
const passes = evaluationResult.result
214233
condition.factResult = evaluationResult.leftHandSideValue
215234
condition.result = passes
@@ -225,13 +244,14 @@ class Rule extends EventEmitter {
225244
* @return {Promise(boolean)} whether conditions evaluated truthy or falsey based on condition evaluation + method
226245
*/
227246
const evaluateConditions = (conditions, method) => {
228-
if (!(Array.isArray(conditions))) conditions = [conditions]
247+
if (!Array.isArray(conditions)) conditions = [conditions]
229248

230-
return Promise.all(conditions.map((condition) => evaluateCondition(condition)))
231-
.then(conditionResults => {
232-
debug('rule::evaluateConditions results', conditionResults)
233-
return method.call(conditionResults, (result) => result === true)
234-
})
249+
return Promise.all(
250+
conditions.map((condition) => evaluateCondition(condition))
251+
).then((conditionResults) => {
252+
debug('rule::evaluateConditions results', conditionResults)
253+
return method.call(conditionResults, (result) => result === true)
254+
})
235255
}
236256

237257
/**
@@ -267,14 +287,18 @@ class Rule extends EventEmitter {
267287
cursor = cursor.then((setResult) => {
268288
// after the first set succeeds, don't fire off the remaining promises
269289
if ((operator === 'any' && setResult === true) || stop) {
270-
debug('prioritizeAndRun::detected truthy result; skipping remaining conditions')
290+
debug(
291+
'prioritizeAndRun::detected truthy result; skipping remaining conditions'
292+
)
271293
stop = true
272294
return true
273295
}
274296

275297
// after the first set fails, don't fire off the remaining promises
276298
if ((operator === 'all' && setResult === false) || stop) {
277-
debug('prioritizeAndRun::detected falsey result; skipping remaining conditions')
299+
debug(
300+
'prioritizeAndRun::detected falsey result; skipping remaining conditions'
301+
)
278302
stop = true
279303
return false
280304
}
@@ -309,17 +333,25 @@ class Rule extends EventEmitter {
309333
* @return {Promise(boolean)} condition evaluation result
310334
*/
311335
const not = (condition) => {
312-
return prioritizeAndRun([condition], 'not').then(result => !result)
336+
return prioritizeAndRun([condition], 'not').then((result) => !result)
313337
}
314338

315-
const realize = (condition, conditionReference) => {
339+
/**
340+
* Dereferences the condition reference and then evaluates it.
341+
* @param {Condition} conditionReference
342+
* @returns {Promise(boolean)} condition evaluation result
343+
*/
344+
const realize = (conditionReference) => {
345+
const condition = this.engine.conditions.get(conditionReference.condition)
316346
if (!condition) {
317347
if (this.engine.allowUndefinedConditions) {
318348
// undefined conditions always fail
319349
conditionReference.result = false
320350
return Promise.resolve(false)
321351
} else {
322-
throw new Error(`No condition ${conditionReference.condition} exists`)
352+
throw new Error(
353+
`No condition ${conditionReference.condition} exists`
354+
)
323355
}
324356
} else {
325357
// project the referenced condition onto reference object and evaluate it.
@@ -336,18 +368,27 @@ class Rule extends EventEmitter {
336368
const processResult = (result) => {
337369
ruleResult.setResult(result)
338370
const event = result ? 'success' : 'failure'
339-
return this.emitAsync(event, ruleResult.event, almanac, ruleResult).then(() => ruleResult)
371+
return this.emitAsync(event, ruleResult.event, almanac, ruleResult).then(
372+
() => ruleResult
373+
)
340374
}
341375

342376
if (ruleResult.conditions.any) {
343-
return any(ruleResult.conditions.any)
344-
.then(result => processResult(result))
377+
return any(ruleResult.conditions.any).then((result) =>
378+
processResult(result)
379+
)
345380
} else if (ruleResult.conditions.all) {
346-
return all(ruleResult.conditions.all)
347-
.then(result => processResult(result))
381+
return all(ruleResult.conditions.all).then((result) =>
382+
processResult(result)
383+
)
384+
} else if (ruleResult.conditions.not) {
385+
return not(ruleResult.conditions.not).then((result) =>
386+
processResult(result)
387+
)
348388
} else {
349-
return not(ruleResult.conditions.not)
350-
.then(result => processResult(result))
389+
return realize(
390+
ruleResult.conditions
391+
).then((result) => processResult(result))
351392
}
352393
}
353394
}

Diff for: test/engine-condition.test.js

+41
Original file line numberDiff line numberDiff line change
@@ -277,4 +277,45 @@ describe('Engine: condition', () => {
277277
expect(results[0]).to.nested.include(nestedCondition)
278278
})
279279
})
280+
281+
describe('top-level condition reference', () => {
282+
const sendEvent = {
283+
type: 'checkSending',
284+
params: {
285+
sendRetirementPayment: true
286+
}
287+
}
288+
289+
const retiredName = 'retired'
290+
const retiredCondition = {
291+
all: [
292+
{ fact: 'isRetired', operator: 'equal', value: true }
293+
]
294+
}
295+
296+
const sendConditions = {
297+
condition: retiredName
298+
}
299+
300+
let eventSpy
301+
beforeEach(() => {
302+
eventSpy = sandbox.spy()
303+
const sendRule = factories.rule({
304+
conditions: sendConditions,
305+
event: sendEvent
306+
})
307+
engine = engineFactory()
308+
309+
engine.addRule(sendRule)
310+
engine.setCondition(retiredName, retiredCondition)
311+
312+
engine.addFact('isRetired', true)
313+
engine.on('success', eventSpy)
314+
})
315+
316+
it('evaluates top level conditions correctly', async () => {
317+
await engine.run()
318+
expect(eventSpy).to.have.been.called()
319+
})
320+
})
280321
})

0 commit comments

Comments
 (0)