Skip to content

Commit 15f7033

Browse files
author
Cache Hamm
committed
Add engine pathResolver option; remove selectn automatic fallback
1 parent 75353df commit 15f7033

File tree

6 files changed

+62
-43
lines changed

6 files changed

+62
-43
lines changed

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
#### 6.0. / 2020-12-XX
22
* BREAKING CHANGES
3-
* Private `rule.event` property renamed. Use `rule.getEvent()` to avoid breaking changes in the future.
3+
* `path` support using `selectn` should use the `pathResolver` feature. Read more [here](). To continue using selectn, add the following to the engine constructor:
4+
```js
5+
const pathResolver = (value, path) => {
6+
return selectn(path)(value)
7+
}
8+
const engine = new Engine(rules, { pathResolver })
9+
```
410
* Engine and Rule events `on('success')`, `on('failure')`, and Rule callbacks `onSuccess` and `onFailure` now honor returned promises; any event handler that returns a promise will be waited upon to resolve before engine execution continues. (fixes #235)
11+
* Private `rule.event` property renamed. Use `rule.getEvent()` to avoid breaking changes in the future.
512
* The 'success-events' fact used to store successful events has been converted to an internal data structure and will no longer appear in the almanac's facts. (fixes #187)
613
714
#### 5.3.0 / 2020-12-02

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
"chai-as-promised": "^7.1.1",
7474
"colors": "~1.4.0",
7575
"dirty-chai": "2.0.1",
76+
"lodash": "4.17.20",
7677
"mocha": "^8.1.3",
7778
"perfy": "^1.1.5",
7879
"sinon": "^9.0.3",

src/almanac.js

Lines changed: 18 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import debug from './debug'
77
import { JSONPath } from 'jsonpath-plus'
88
import isObjectLike from 'lodash.isobjectlike'
99

10+
function defaultPathResolver (value, path) {
11+
return JSONPath({ path, json: value, wrap: false })
12+
}
13+
1014
/**
1115
* Fact results lookup
1216
* Triggers fact computations and saves the results
@@ -17,6 +21,7 @@ export default class Almanac {
1721
this.factMap = new Map(factMap)
1822
this.factResultsCache = new Map() // { cacheKey: Promise<factValu> }
1923
this.allowUndefinedFacts = Boolean(options.allowUndefinedFacts)
24+
this.pathResolver = options.pathResolver || defaultPathResolver
2025
this.successEvents = []
2126

2227
for (const factId in runtimeFacts) {
@@ -122,44 +127,19 @@ export default class Almanac {
122127
factValuePromise = this._setFactValue(fact, params, fact.calculate(params, this))
123128
}
124129
}
125-
if (path) { // selectn supports arrays and strings as a 'path'
126-
// strings starting with '$' denotes json path. otherwise fall back to deprecated 'selectn' syntax
127-
if (typeof path === 'string' && path.startsWith('$')) {
128-
debug(`condition::evaluate extracting object property ${path}`)
129-
return factValuePromise
130-
.then(factValue => {
131-
if (isObjectLike(factValue)) {
132-
const pathValue = JSONPath({ path, json: factValue, wrap: false })
133-
debug(`condition::evaluate extracting object property ${path}, received: ${JSON.stringify(pathValue)}`)
134-
return pathValue
135-
} else {
136-
debug(`condition::evaluate could not compute object path(${path}) of non-object: ${factValue} <${typeof factValue}>; continuing with ${factValue}`)
137-
return factValue
138-
}
139-
})
140-
} else {
141-
let selectn
142-
try {
143-
selectn = require('selectn')
144-
} catch (err) {
145-
console.error('Oops! Looks like you\'re trying to use the deprecated syntax for the ".path" property.')
146-
console.error('Please convert your "path" properties to JsonPath syntax (ensure your path starts with "$")')
147-
console.error('Alternatively, if you wish to continue using old syntax (provided by selectn), you may "npm install selectn" as a direct dependency.')
148-
console.error('See https://github.com/CacheControl/json-rules-engine/blob/master/CHANGELOG.md#500--2019-10-27 for more information.')
149-
throw new Error('json-rules-engine: Unmet peer dependency "selectn" required for use of deprecated ".path" syntax. please "npm install selectn" or convert to json-path syntax')
150-
}
151-
return factValuePromise
152-
.then(factValue => {
153-
if (isObjectLike(factValue)) {
154-
const pathValue = selectn(path)(factValue)
155-
debug(`condition::evaluate extracting object property ${path}, received: ${pathValue}`)
156-
return pathValue
157-
} else {
158-
debug(`condition::evaluate could not compute object path(${path}) of non-object: ${factValue} <${typeof factValue}>; continuing with ${factValue}`)
159-
return factValue
160-
}
161-
})
162-
}
130+
if (path) {
131+
debug(`condition::evaluate extracting object property ${path}`)
132+
return factValuePromise
133+
.then(factValue => {
134+
if (isObjectLike(factValue)) {
135+
const pathValue = this.pathResolver(factValue, path)
136+
debug(`condition::evaluate extracting object property ${path}, received: ${JSON.stringify(pathValue)}`)
137+
return pathValue
138+
} else {
139+
debug(`condition::evaluate could not compute object path(${path}) of non-object: ${factValue} <${typeof factValue}>; continuing with ${factValue}`)
140+
return factValue
141+
}
142+
})
163143
}
164144

165145
return factValuePromise

src/engine.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class Engine extends EventEmitter {
2121
super()
2222
this.rules = []
2323
this.allowUndefinedFacts = options.allowUndefinedFacts || false
24+
this.pathResolver = options.pathResolver
2425
this.operators = new Map()
2526
this.facts = new Map()
2627
this.status = READY
@@ -210,7 +211,11 @@ class Engine extends EventEmitter {
210211
run (runtimeFacts = {}) {
211212
debug('engine::run started')
212213
this.status = RUNNING
213-
const almanac = new Almanac(this.facts, runtimeFacts, { allowUndefinedFacts: this.allowUndefinedFacts })
214+
const almanacOptions = {
215+
allowUndefinedFacts: this.allowUndefinedFacts,
216+
pathResolver: this.pathResolver
217+
}
218+
const almanac = new Almanac(this.facts, runtimeFacts, almanacOptions)
214219
const orderedSets = this.prioritizeRules()
215220
let cursor = Promise.resolve()
216221
// for each rule set, evaluate in parallel,

test/engine-fact.test.js

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict'
22

33
import sinon from 'sinon'
4+
import { get } from 'lodash'
45
import engineFactory from '../src/index'
56

67
const CHILD = 14
@@ -224,9 +225,6 @@ describe('Engine: fact evaluation', () => {
224225
value: 1
225226
}]
226227
}
227-
const event = {
228-
type: 'runtimeEvent'
229-
}
230228

231229
engine = engineFactory([])
232230
const rule = factories.rule({ conditions, event })
@@ -268,6 +266,33 @@ describe('Engine: fact evaluation', () => {
268266
await engine.run()
269267
expect(successSpy).to.have.been.calledWith(event)
270268
})
269+
270+
describe('pathResolver', () => {
271+
it('allows a custom path resolver to be registered which interprets the path property', async () => {
272+
const fact = { x: { y: [99] }, a: 2 }
273+
const conditions = {
274+
all: [{
275+
fact: 'x',
276+
path: 'y[0]',
277+
operator: 'equal',
278+
value: 99
279+
}]
280+
}
281+
const pathResolver = (value, path) => {
282+
return get(value, path)
283+
}
284+
285+
engine = engineFactory([], { pathResolver })
286+
const rule = factories.rule({ conditions, event })
287+
await engine.run()
288+
engine.addRule(rule)
289+
engine.on('success', successSpy)
290+
engine.on('failure', failureSpy)
291+
await engine.run(fact)
292+
expect(successSpy).to.have.been.calledWith(event)
293+
expect(failureSpy).to.not.have.been.calledWith(event)
294+
})
295+
})
271296
})
272297

273298
describe('promises', () => {

types/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export interface EngineOptions {
22
allowUndefinedFacts: boolean;
3+
pathResolver: string;
34
}
45

56
export interface EngineResult {

0 commit comments

Comments
 (0)