Skip to content

Commit fff169d

Browse files
authored
Merge pull request #357 from chris-pardy/custom-almanac
Custom almanac
2 parents 0de797c + 3c1975e commit fff169d

11 files changed

+250
-52
lines changed

docs/almanac.md

+21
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* [Overview](#overview)
44
* [Methods](#methods)
55
* [almanac.factValue(Fact fact, Object params, String path) -> Promise](#almanacfactvaluefact-fact-object-params-string-path---promise)
6+
* [almanac.addFact(String id, Function [definitionFunc], Object [options])](#almanacaddfactstring-id-function-definitionfunc-object-options)
67
* [almanac.addRuntimeFact(String factId, Mixed value)](#almanacaddruntimefactstring-factid-mixed-value)
78
* [almanac.getEvents(String outcome) -> Events[]](#almanacgeteventsstring-outcome---events)
89
* [almanac.getResults() -> RuleResults[]](#almanacgetresults---ruleresults)
@@ -33,8 +34,28 @@ almanac
3334
.then( value => console.log(value))
3435
```
3536

37+
### almanac.addFact(String id, Function [definitionFunc], Object [options])
38+
39+
Sets a fact in the almanac. Used in conjunction with rule and engine event emissions.
40+
41+
```js
42+
// constant facts:
43+
engine.addFact('speed-of-light', 299792458)
44+
45+
// facts computed via function
46+
engine.addFact('account-type', function getAccountType(params, almanac) {
47+
// ...
48+
})
49+
50+
// facts with options:
51+
engine.addFact('account-type', function getAccountType(params, almanac) {
52+
// ...
53+
}, { cache: false, priority: 500 })
54+
```
55+
3656
### almanac.addRuntimeFact(String factId, Mixed value)
3757

58+
**Deprecated** Use `almanac.addFact` instead
3859
Sets a constant fact mid-run. Often used in conjunction with rule and engine event emissions.
3960

4061
```js

docs/engine.md

+10
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,16 @@ const {
269269
```
270270
Link to the [Almanac documentation](./almanac.md)
271271

272+
Optionally, you may specify a specific almanac instance via the almanac property.
273+
274+
```js
275+
// create a custom Almanac
276+
const myCustomAlmanac = new CustomAlmanac();
277+
278+
// run the engine with the custom almanac
279+
await engine.run({}, { almanac: myCustomAlmanac })
280+
```
281+
272282
### engine.stop() -> Engine
273283

274284
Stops the rules engine from running the next priority set of Rules. All remaining rules will be resolved as undefined,

examples/07-rule-chaining.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,16 @@ async function start () {
3939
event: { type: 'drinks-screwdrivers' },
4040
priority: 10, // IMPORTANT! Set a higher priority for the drinkRule, so it runs first
4141
onSuccess: async function (event, almanac) {
42-
almanac.addRuntimeFact('screwdriverAficionado', true)
42+
almanac.addFact('screwdriverAficionado', true)
4343

4444
// asychronous operations can be performed within callbacks
4545
// engine execution will not proceed until the returned promises is resolved
4646
const accountId = await almanac.factValue('accountId')
4747
const accountInfo = await getAccountInformation(accountId)
48-
almanac.addRuntimeFact('accountInfo', accountInfo)
48+
almanac.addFact('accountInfo', accountInfo)
4949
},
5050
onFailure: function (event, almanac) {
51-
almanac.addRuntimeFact('screwdriverAficionado', false)
51+
almanac.addFact('screwdriverAficionado', false)
5252
}
5353
}
5454
engine.addRule(drinkRule)

examples/12-using-custom-almanac.js

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
'use strict'
2+
3+
require('colors')
4+
const { Almanac, Engine } = require('json-rules-engine')
5+
6+
/**
7+
* Almanac that support piping values through named functions
8+
*/
9+
class PipedAlmanac extends Almanac {
10+
constructor (options) {
11+
super(options)
12+
this.pipes = new Map()
13+
}
14+
15+
addPipe (name, pipe) {
16+
this.pipes.set(name, pipe)
17+
}
18+
19+
factValue (factId, params, path) {
20+
let pipes = []
21+
if (params && 'pipes' in params && Array.isArray(params.pipes)) {
22+
pipes = params.pipes
23+
delete params.pipes
24+
}
25+
return super.factValue(factId, params, path).then(value => {
26+
return pipes.reduce((value, pipeName) => {
27+
const pipe = this.pipes.get(pipeName)
28+
if (pipe) {
29+
return pipe(value)
30+
}
31+
return value
32+
}, value)
33+
})
34+
}
35+
}
36+
37+
async function start () {
38+
const engine = new Engine()
39+
.addRule({
40+
conditions: {
41+
all: [
42+
{
43+
fact: 'age',
44+
params: {
45+
// the addOne pipe adds one to the value
46+
pipes: ['addOne']
47+
},
48+
operator: 'greaterThanInclusive',
49+
value: 21
50+
}
51+
]
52+
},
53+
event: {
54+
type: 'Over 21(ish)'
55+
}
56+
})
57+
58+
engine.on('success', async (event, almanac) => {
59+
const name = await almanac.factValue('name')
60+
const age = await almanac.factValue('age')
61+
console.log(`${name} is ${age} years old and ${'is'.green} ${event.type}`)
62+
})
63+
64+
engine.on('failure', async (event, almanac) => {
65+
const name = await almanac.factValue('name')
66+
const age = await almanac.factValue('age')
67+
console.log(`${name} is ${age} years old and ${'is not'.red} ${event.type}`)
68+
})
69+
70+
const createAlmanacWithPipes = () => {
71+
const almanac = new PipedAlmanac()
72+
almanac.addPipe('addOne', (v) => v + 1)
73+
return almanac
74+
}
75+
76+
// first run Bob who is less than 20
77+
await engine.run({ name: 'Bob', age: 19 }, { almanac: createAlmanacWithPipes() })
78+
79+
// second run Alice who is 21
80+
await engine.run({ name: 'Alice', age: 21 }, { almanac: createAlmanacWithPipes() })
81+
82+
// third run Chad who is 20
83+
await engine.run({ name: 'Chad', age: 20 }, { almanac: createAlmanacWithPipes() })
84+
}
85+
86+
start()
87+
88+
/*
89+
* OUTPUT:
90+
*
91+
* Bob is 19 years old and is not Over 21(ish)
92+
* Alice is 21 years old and is Over 21(ish)
93+
* Chad is 20 years old and is Over 21(ish)
94+
*/

src/almanac.js

+26-14
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,13 @@ function defaultPathResolver (value, path) {
1717
* A new almanac is used for every engine run()
1818
*/
1919
export default class Almanac {
20-
constructor (factMap, runtimeFacts = {}, options = {}) {
21-
this.factMap = new Map(factMap)
20+
constructor (options = {}) {
21+
this.factMap = new Map()
2222
this.factResultsCache = new Map() // { cacheKey: Promise<factValu> }
2323
this.allowUndefinedFacts = Boolean(options.allowUndefinedFacts)
2424
this.pathResolver = options.pathResolver || defaultPathResolver
2525
this.events = { success: [], failure: [] }
2626
this.ruleResults = []
27-
28-
for (const factId in runtimeFacts) {
29-
let fact
30-
if (runtimeFacts[factId] instanceof Fact) {
31-
fact = runtimeFacts[factId]
32-
} else {
33-
fact = new Fact(factId, runtimeFacts[factId])
34-
}
35-
36-
this._addConstantFact(fact)
37-
debug(`almanac::constructor initialized runtime fact:${fact.id} with ${fact.value}<${typeof fact.value}>`)
38-
}
3927
}
4028

4129
/**
@@ -103,8 +91,32 @@ export default class Almanac {
10391
return factValue
10492
}
10593

94+
/**
95+
* Add a fact definition to the engine. Facts are called by rules as they are evaluated.
96+
* @param {object|Fact} id - fact identifier or instance of Fact
97+
* @param {function} definitionFunc - function to be called when computing the fact value for a given rule
98+
* @param {Object} options - options to initialize the fact with. used when "id" is not a Fact instance
99+
*/
100+
addFact (id, valueOrMethod, options) {
101+
let factId = id
102+
let fact
103+
if (id instanceof Fact) {
104+
factId = id.id
105+
fact = id
106+
} else {
107+
fact = new Fact(id, valueOrMethod, options)
108+
}
109+
debug(`almanac::addFact id:${factId}`)
110+
this.factMap.set(factId, fact)
111+
if (fact.isConstant()) {
112+
this._setFactValue(fact, {}, fact.value)
113+
}
114+
return this
115+
}
116+
106117
/**
107118
* Adds a constant fact during runtime. Can be used mid-run() to add additional information
119+
* @deprecated use addFact
108120
* @param {String} fact - fact identifier
109121
* @param {Mixed} value - constant value of the fact
110122
*/

src/engine.js

+18-3
Original file line numberDiff line numberDiff line change
@@ -261,14 +261,29 @@ class Engine extends EventEmitter {
261261
* @param {Object} runOptions - run options
262262
* @return {Promise} resolves when the engine has completed running
263263
*/
264-
run (runtimeFacts = {}) {
264+
run (runtimeFacts = {}, runOptions = {}) {
265265
debug('engine::run started')
266266
this.status = RUNNING
267-
const almanacOptions = {
267+
268+
const almanac = runOptions.almanac || new Almanac({
268269
allowUndefinedFacts: this.allowUndefinedFacts,
269270
pathResolver: this.pathResolver
271+
})
272+
273+
this.facts.forEach(fact => {
274+
almanac.addFact(fact)
275+
})
276+
for (const factId in runtimeFacts) {
277+
let fact
278+
if (runtimeFacts[factId] instanceof Fact) {
279+
fact = runtimeFacts[factId]
280+
} else {
281+
fact = new Fact(factId, runtimeFacts[factId])
282+
}
283+
284+
almanac.addFact(fact)
285+
debug(`engine::run initialized runtime fact:${fact.id} with ${fact.value}<${typeof fact.value}>`)
270286
}
271-
const almanac = new Almanac(this.facts, runtimeFacts, almanacOptions)
272287
const orderedSets = this.prioritizeRules()
273288
let cursor = Promise.resolve()
274289
// for each rule set, evaluate in parallel,

src/json-rules-engine.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import Engine from './engine'
22
import Fact from './fact'
33
import Rule from './rule'
44
import Operator from './operator'
5+
import Almanac from './almanac'
56

6-
export { Fact, Rule, Operator, Engine }
7+
export { Fact, Rule, Operator, Engine, Almanac }
78
export default function (rules, options) {
89
return new Engine(rules, options)
910
}

test/almanac.test.js

+27-19
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,33 @@ describe('Almanac', () => {
2323
})
2424

2525
it('adds runtime facts', () => {
26-
almanac = new Almanac(new Map(), { modelId: 'XYZ' })
26+
almanac = new Almanac()
27+
almanac.addFact('modelId', 'XYZ')
2728
expect(almanac.factMap.get('modelId').value).to.equal('XYZ')
2829
})
2930
})
3031

31-
describe('constructor', () => {
32+
describe('addFact', () => {
3233
it('supports runtime facts as key => values', () => {
33-
almanac = new Almanac(new Map(), { fact1: 3 })
34+
almanac = new Almanac()
35+
almanac.addFact('fact1', 3)
3436
return expect(almanac.factValue('fact1')).to.eventually.equal(3)
3537
})
3638

39+
it('supporrts runtime facts as dynamic callbacks', async () => {
40+
almanac = new Almanac()
41+
almanac.addFact('fact1', () => {
42+
factSpy()
43+
return Promise.resolve(3)
44+
})
45+
await expect(almanac.factValue('fact1')).to.eventually.equal(3)
46+
await expect(factSpy).to.have.been.calledOnce()
47+
})
48+
3749
it('supports runtime fact instances', () => {
3850
const fact = new Fact('fact1', 3)
39-
almanac = new Almanac(new Map(), { fact1: fact })
51+
almanac = new Almanac()
52+
almanac.addFact(fact)
4053
return expect(almanac.factValue('fact1')).to.eventually.equal(fact.value)
4154
})
4255
})
@@ -69,9 +82,8 @@ describe('Almanac', () => {
6982
if (params.userId) return params.userId
7083
return 'unknown'
7184
})
72-
const factMap = new Map()
73-
factMap.set(fact.id, fact)
74-
almanac = new Almanac(factMap)
85+
almanac = new Almanac()
86+
almanac.addFact(fact)
7587
})
7688

7789
it('allows parameters to be passed to the fact', async () => {
@@ -106,10 +118,9 @@ describe('Almanac', () => {
106118

107119
describe('_getFact', _ => {
108120
it('retrieves the fact object', () => {
109-
const facts = new Map()
110121
const fact = new Fact('id', 1)
111-
facts.set(fact.id, fact)
112-
almanac = new Almanac(facts)
122+
almanac = new Almanac()
123+
almanac.addFact(fact)
113124
expect(almanac._getFact('id')).to.equal(fact)
114125
})
115126
})
@@ -124,9 +135,8 @@ describe('Almanac', () => {
124135

125136
function setup (f = new Fact('id', 1)) {
126137
fact = f
127-
const facts = new Map()
128-
facts.set(fact.id, fact)
129-
almanac = new Almanac(facts)
138+
almanac = new Almanac()
139+
almanac.addFact(fact)
130140
}
131141
let fact
132142
const FACT_VALUE = 2
@@ -154,9 +164,8 @@ describe('Almanac', () => {
154164
name: 'Thomas'
155165
}]
156166
})
157-
const factMap = new Map()
158-
factMap.set(fact.id, fact)
159-
almanac = new Almanac(factMap)
167+
almanac = new Almanac()
168+
almanac.addFact(fact)
160169
const result = await almanac.factValue('foo', null, '$..name')
161170
expect(result).to.deep.equal(['George', 'Thomas'])
162171
})
@@ -167,9 +176,8 @@ describe('Almanac', () => {
167176
factSpy()
168177
return 'unknown'
169178
}, factOptions)
170-
const factMap = new Map()
171-
factMap.set(fact.id, fact)
172-
almanac = new Almanac(factMap)
179+
almanac = new Almanac()
180+
almanac.addFact(fact)
173181
}
174182

175183
it('evaluates the fact every time when fact caching is off', () => {

0 commit comments

Comments
 (0)