Skip to content

Commit e20c899

Browse files
author
Cache Hamm
committed
fact-fact comparison conditions
1 parent 06c6ef0 commit e20c899

7 files changed

Lines changed: 300 additions & 118 deletions

File tree

docs/engine.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@ undefined facts as falsey conditions. (default: false)
3434
engine.addFact('speed-of-light', 299792458)
3535

3636
// facts computed via function
37-
engine.addFact('account-type', function getAccountType() {
37+
engine.addFact('account-type', function getAccountType(params, almanac) {
3838
// ...
3939
})
4040

4141
// facts with options:
42-
engine.addFact('account-type', function getAccountType() {
42+
engine.addFact('account-type', function getAccountType(params, almanac) {
4343
// ...
4444
}, { cache: false, priority: 500 })
4545
```

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
"chai": "3.4.1",
5656
"chai-as-promised": "^5.2.0",
5757
"colors": "~1.1.2",
58-
"mocha": "2.3.4",
58+
"mocha": "3.2.0",
5959
"regenerator": "~0.8.46",
6060
"sinon": "^1.17.2",
6161
"sinon-chai": "^2.8.0",
@@ -64,6 +64,7 @@
6464
},
6565
"dependencies": {
6666
"debug": "2.2.0",
67+
"lodash.isplainobject": "4.0.6",
6768
"object-hash": "1.1.5",
6869
"params": "0.1.1",
6970
"selectn": "1.1.1"

src/almanac.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
let debug = require('debug')('json-rules-engine')
44
let verbose = require('debug')('json-rules-engine-verbose')
5+
let selectn = require('selectn')
6+
let isPlainObject = require('lodash.isplainobject')
7+
let warn = require('debug')('json-rules-engine:warn')
58

69
import Fact from './fact'
710
import { UndefinedFactError } from './errors'
@@ -82,9 +85,10 @@ export default class Almanac {
8285
* by the engine, which cache's fact computations based on parameters provided
8386
* @param {string} factId - fact identifier
8487
* @param {Object} params - parameters to feed into the fact. By default, these will also be used to compute the cache key
88+
* @param {String} path - object
8589
* @return {Promise} a promise which will resolve with the fact computation.
8690
*/
87-
async factValue (factId, params = {}) {
91+
async factValue (factId, params = {}, path = '') {
8892
let fact = this._getFact(factId)
8993
let cacheKey = fact.getCacheKey(params)
9094
let cacheVal = cacheKey && this.factResultsCache.get(cacheKey)
@@ -93,6 +97,15 @@ export default class Almanac {
9397
return cacheVal
9498
}
9599
verbose(`almanac::factValue cache miss for fact:${factId}; calculating`)
96-
return this._setFactValue(fact, params, fact.calculate(params, this))
100+
let factValue = await this._setFactValue(fact, params, fact.calculate(params, this))
101+
if (path) {
102+
if (isPlainObject(factValue) || Array.isArray(factValue)) {
103+
factValue = selectn(path)(factValue)
104+
debug(`condition::evaluate extracting object property ${path}, received: ${factValue}`)
105+
} else {
106+
warn(`condition::evaluate could not compute object path(${path}) of non-object: ${factValue} <${typeof factValue}>; continuing with ${factValue}`)
107+
}
108+
}
109+
return factValue
97110
}
98111
}

src/condition.js

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
'use strict'
22

3-
import params from 'params'
4-
import selectn from 'selectn'
5-
3+
let params = require('params')
64
let debug = require('debug')('json-rules-engine')
7-
let warn = require('debug')('json-rules-engine:warn')
5+
let isPlainObject = require('lodash.isplainobject')
86

97
export default class Condition {
108
constructor (properties) {
@@ -61,33 +59,39 @@ export default class Condition {
6159
return props
6260
}
6361

62+
/**
63+
* Interprets .value as either a primitive, or if a fact, retrieves the fact value
64+
*/
65+
async _getValue(almanac) {
66+
let value = this.value
67+
if (isPlainObject(value) && value.hasOwnProperty('fact')) { // value: { fact: 'xyz' }
68+
value = await almanac.factValue(value.fact, value.params, value.path)
69+
}
70+
return value
71+
}
72+
6473
/**
6574
* Takes the fact result and compares it to the condition 'value', using the operator
66-
* @param {mixed} comparisonValue - fact result
75+
* LHS OPER RHS
76+
* <fact + params + path> <operator> <value>
77+
*
78+
* @param {Almanac} almanac
6779
* @param {Map} operatorMap - map of available operators, keyed by operator name
6880
* @returns {Boolean} - evaluation result
6981
*/
70-
evaluate (comparisonValue, operatorMap) {
71-
// for any/all, simply comparisonValue that the sub-condition array evaluated truthy
72-
if (this.isBooleanOperator()) return comparisonValue === true
73-
74-
// if the fact has provided an object, and a path is specified, retrieve the object property
75-
if (this.path) {
76-
if (typeof comparisonValue === 'object') {
77-
comparisonValue = selectn(this.path)(comparisonValue)
78-
debug(`condition::evaluate extracting object property ${this.path}, received: ${comparisonValue}`)
79-
} else {
80-
warn(`condition::evaluate could not compute object path(${this.path}) of non-object: ${comparisonValue} <${typeof comparisonValue}>; continuing with ${comparisonValue}`)
81-
}
82-
}
82+
async evaluate (almanac, operatorMap) {
83+
if (!almanac) throw new Error('almanac required')
84+
if (!operatorMap) throw new Error('operatorMap required')
85+
if (this.isBooleanOperator()) throw new Error('Cannot evaluate() a boolean condition')
8386

8487
let op = operatorMap.get(this.operator)
8588
if (!op) throw new Error(`Unknown operator: ${this.operator}`)
8689

87-
let evaluationResult = op.evaluate(comparisonValue, this.value)
88-
if (!this.isBooleanOperator()) {
89-
debug(`condition::evaluate <${comparisonValue} ${this.operator} ${this.value}?> (${evaluationResult})`)
90-
}
90+
let rightHandSideValue = await this._getValue(almanac)
91+
let leftHandSideValue = await almanac.factValue(this.fact, this.params, this.path)
92+
93+
let evaluationResult = op.evaluate(leftHandSideValue, rightHandSideValue)
94+
debug(`condition::evaluate <${leftHandSideValue} ${this.operator} ${rightHandSideValue}?> (${evaluationResult})`)
9195
return evaluationResult
9296
}
9397

src/rule.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,17 +142,18 @@ class Rule extends EventEmitter {
142142
} else {
143143
comparisonValue = await any(subConditions)
144144
}
145+
// for booleans, rule passing is determined by the all/any result
146+
passes = comparisonValue === true
145147
} else {
146148
try {
147-
comparisonValue = await almanac.factValue(condition.fact, condition.params)
149+
passes = await condition.evaluate(almanac, this.engine.operators, comparisonValue)
148150
} catch (err) {
151+
// any condition raising an undefined fact error is considered falsey when allowUndefinedFacts is enabled
149152
if (this.engine.allowUndefinedFacts && err.code === 'UNDEFINED_FACT') passes = false
150153
else throw err
151154
}
152155
}
153-
if (passes === undefined) {
154-
passes = await condition.evaluate(comparisonValue, this.engine.operators)
155-
}
156+
156157
if (passes) {
157158
this.emit('success', this.event, almanac)
158159
} else {

0 commit comments

Comments
 (0)