Skip to content

Commit 1d2968b

Browse files
author
Cache Hamm
committed
allowUndefinedFacts option
1 parent 2645750 commit 1d2968b

7 files changed

Lines changed: 103 additions & 34 deletions

File tree

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
"/dist"
2525
],
2626
"globals": [
27+
"context",
28+
"xcontext",
2729
"describe",
2830
"xdescribe",
2931
"it",

src/almanac.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ let debug = require('debug')('json-rules-engine')
44
let verbose = require('debug')('json-rules-engine-verbose')
55

66
import Fact from './fact'
7+
import { UndefinedFactError } from './errors'
78

89
/**
910
* Fact results lookup
@@ -36,7 +37,7 @@ export default class Almanac {
3637
_getFact (factId) {
3738
let fact = this.factMap.get(factId)
3839
if (fact === undefined) {
39-
throw new Error(`Undefined fact: ${factId}`)
40+
throw new UndefinedFactError(`Undefined fact: ${factId}`)
4041
}
4142
return fact
4243
}

src/engine.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ class Engine extends EventEmitter {
2020
* Returns a new Engine instance
2121
* @param {Rule[]} rules - array of rules to initialize with
2222
*/
23-
constructor (rules = []) {
23+
constructor (rules = [], options = {}) {
2424
super()
2525
this.rules = []
26+
this.allowUndefinedFacts = options.allowUndefinedFacts || false
2627
this.operators = new Map()
2728
this.facts = new Map()
2829
this.status = READY
@@ -166,7 +167,7 @@ class Engine extends EventEmitter {
166167
debug(`engine::run runtimeFacts:`, runtimeFacts)
167168
runtimeFacts['success-events'] = new Fact('success-events', SuccessEventFact(), { cache: false })
168169
this.status = RUNNING
169-
let almanac = new Almanac(this.facts, runtimeFacts)
170+
let almanac = new Almanac(this.facts, runtimeFacts, this.options)
170171
let orderedSets = this.prioritizeRules()
171172
let cursor = Promise.resolve()
172173
// for each rule set, evaluate in parallel,

src/errors.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
'use strict'
2+
3+
export class UndefinedFactError extends Error {
4+
constructor (...props) {
5+
super(...props)
6+
this.code = 'UNDEFINED_FACT'
7+
}
8+
}

src/json-rules-engine.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ import Rule from './rule'
44
import Operator from './operator'
55

66
export { Fact, Rule, Operator, Engine }
7-
export default function (rules) {
8-
return new Engine(rules)
7+
export default function (rules, options) {
8+
return new Engine(rules, options)
99
}

src/rule.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ class Rule extends EventEmitter {
134134
*/
135135
let evaluateCondition = async (condition) => {
136136
let comparisonValue
137+
let passes
137138
if (condition.isBooleanOperator()) {
138139
let subConditions = condition[condition.operator]
139140
if (condition.operator === 'all') {
@@ -142,9 +143,16 @@ class Rule extends EventEmitter {
142143
comparisonValue = await any(subConditions)
143144
}
144145
} else {
145-
comparisonValue = await almanac.factValue(condition.fact, condition.params)
146+
try {
147+
comparisonValue = await almanac.factValue(condition.fact, condition.params)
148+
} catch (err) {
149+
if (this.engine.allowUndefinedFacts && err.code === 'UNDEFINED_FACT') passes = false
150+
else throw err
151+
}
152+
}
153+
if (passes === undefined) {
154+
passes = await condition.evaluate(comparisonValue, this.engine.operators)
146155
}
147-
let passes = await condition.evaluate(comparisonValue, this.engine.operators)
148156
if (passes) {
149157
this.emit('success', this.event, almanac)
150158
} else {

test/engine-fact.test.js

Lines changed: 76 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -42,41 +42,90 @@ describe('Engine: fact evaluation', () => {
4242
demographic: 'under50'
4343
}
4444
}
45-
let baseConditions = {
46-
any: [{
47-
fact: 'eligibilityField',
48-
operator: 'lessThan',
49-
params: {
50-
eligibilityId: 1,
51-
field: 'age'
52-
},
53-
value: 50
54-
}]
45+
function baseConditions () {
46+
return {
47+
any: [{
48+
fact: 'eligibilityField',
49+
operator: 'lessThan',
50+
params: {
51+
eligibilityId: 1,
52+
field: 'age'
53+
},
54+
value: 50
55+
}]
56+
}
5557
}
56-
let eventSpy = sinon.spy()
57-
function setup (conditions = baseConditions) {
58-
eventSpy.reset()
59-
engine = engineFactory()
58+
let successSpy = sinon.spy()
59+
let failureSpy = sinon.spy()
60+
function setup (conditions = baseConditions(), engineOptions = {}) {
61+
successSpy.reset()
62+
failureSpy.reset()
63+
engine = engineFactory([], engineOptions)
6064
let rule = factories.rule({ conditions, event })
6165
engine.addRule(rule)
6266
engine.addFact('eligibilityField', eligibilityField)
6367
engine.addFact('eligibilityData', eligibilityData)
64-
engine.on('success', eventSpy)
68+
engine.on('success', successSpy)
69+
engine.on('failure', failureSpy)
6570
}
6671

72+
describe('options', () => {
73+
describe('options.allowUndefinedFacts', () => {
74+
it('throws when fact is undefined by default', async () => {
75+
let conditions = Object.assign({}, baseConditions())
76+
conditions.any.push({
77+
fact: 'undefined-fact',
78+
operator: 'equal',
79+
value: true
80+
})
81+
setup(conditions)
82+
return expect(engine.run()).to.be.rejectedWith(/Undefined fact: undefined-fact/)
83+
})
84+
85+
context('treats undefined facts as falsey when allowUndefinedFacts is set', () => {
86+
it('emits "success" when the condition succeeds', async () => {
87+
let conditions = Object.assign({}, baseConditions())
88+
conditions.any.push({
89+
fact: 'undefined-fact',
90+
operator: 'equal',
91+
value: true
92+
})
93+
setup(conditions, { allowUndefinedFacts: true })
94+
await engine.run()
95+
expect(successSpy).to.have.been.called
96+
expect(failureSpy).to.not.have.been.called
97+
})
98+
99+
it('emits "failure" when the condition fails', async () => {
100+
let conditions = Object.assign({}, baseConditions())
101+
conditions.any.push({
102+
fact: 'undefined-fact',
103+
operator: 'equal',
104+
value: true
105+
})
106+
conditions.any[0].params.eligibilityId = 2
107+
setup(conditions, { allowUndefinedFacts: true })
108+
await engine.run()
109+
expect(successSpy).to.not.have.been.called
110+
expect(failureSpy).to.have.been.called
111+
})
112+
})
113+
})
114+
})
115+
67116
describe('params', () => {
68117
it('emits when the condition is met', async () => {
69118
setup()
70119
await engine.run()
71-
expect(eventSpy).to.have.been.calledWith(event)
120+
expect(successSpy).to.have.been.calledWith(event)
72121
})
73122

74123
it('does not emit when the condition fails', async () => {
75-
let conditions = Object.assign({}, baseConditions)
124+
let conditions = Object.assign({}, baseConditions())
76125
conditions.any[0].params.eligibilityId = 2
77126
setup(conditions)
78127
await engine.run()
79-
expect(eventSpy).to.not.have.been.called
128+
expect(successSpy).to.not.have.been.called
80129
})
81130
})
82131

@@ -97,15 +146,15 @@ describe('Engine: fact evaluation', () => {
97146
it('emits when the condition is met', async () => {
98147
setup(conditions())
99148
await engine.run()
100-
expect(eventSpy).to.have.been.calledWith(event)
149+
expect(successSpy).to.have.been.calledWith(event)
101150
})
102151

103152
it('does not emit when the condition fails', async () => {
104153
let failureCondition = conditions()
105154
failureCondition.any[0].params.eligibilityId = 2
106155
setup(failureCondition)
107156
await engine.run()
108-
expect(eventSpy).to.not.have.been.called
157+
expect(successSpy).to.not.have.been.called
109158
})
110159

111160
it('emits when complex object paths meet the conditions', async () => {
@@ -115,7 +164,7 @@ describe('Engine: fact evaluation', () => {
115164
complexCondition.any[0].operator = 'equal'
116165
setup(complexCondition)
117166
await engine.run()
118-
expect(eventSpy).to.have.been.calledWith(event)
167+
expect(successSpy).to.have.been.calledWith(event)
119168
})
120169

121170
it('does not emit when complex object paths fail the condition', async () => {
@@ -125,7 +174,7 @@ describe('Engine: fact evaluation', () => {
125174
complexCondition.any[0].operator = 'equal'
126175
setup(complexCondition)
127176
await engine.run()
128-
expect(eventSpy).to.not.have.been.calledWith(event)
177+
expect(successSpy).to.not.have.been.calledWith(event)
129178
})
130179

131180
it('treats invalid object paths as undefined', async () => {
@@ -135,7 +184,7 @@ describe('Engine: fact evaluation', () => {
135184
complexCondition.any[0].operator = 'equal'
136185
setup(complexCondition)
137186
await engine.run()
138-
expect(eventSpy).to.have.been.calledWith(event)
187+
expect(successSpy).to.have.been.calledWith(event)
139188
})
140189

141190
it('ignores "path" when facts return non-objects', async () => {
@@ -145,7 +194,7 @@ describe('Engine: fact evaluation', () => {
145194
}
146195
engine.addFact('eligibilityData', eligibilityData)
147196
await engine.run()
148-
expect(eventSpy).to.have.been.calledWith(event)
197+
expect(successSpy).to.have.been.calledWith(event)
149198
})
150199
})
151200

@@ -161,7 +210,7 @@ describe('Engine: fact evaluation', () => {
161210
}
162211
engine.addFact('eligibilityField', eligibilityField)
163212
await engine.run()
164-
expect(eventSpy).to.have.been.called
213+
expect(successSpy).to.have.been.called
165214
})
166215
})
167216

@@ -173,7 +222,7 @@ describe('Engine: fact evaluation', () => {
173222
}
174223
engine.addFact('eligibilityField', eligibilityField)
175224
await engine.run()
176-
expect(eventSpy).to.have.been.called
225+
expect(successSpy).to.have.been.called
177226
})
178227

179228
it('works with synchronous, non-promise evaluations that are falsey', async () => {
@@ -183,7 +232,7 @@ describe('Engine: fact evaluation', () => {
183232
}
184233
engine.addFact('eligibilityField', eligibilityField)
185234
await engine.run()
186-
expect(eventSpy).to.not.have.been.called
235+
expect(successSpy).to.not.have.been.called
187236
})
188237
})
189238
})

0 commit comments

Comments
 (0)