Skip to content

Custom almanac #357

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/almanac.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* [Overview](#overview)
* [Methods](#methods)
* [almanac.factValue(Fact fact, Object params, String path) -> Promise](#almanacfactvaluefact-fact-object-params-string-path---promise)
* [almanac.addFact(String id, Function [definitionFunc], Object [options])](#almanacaddfactstring-id-function-definitionfunc-object-options)
* [almanac.addRuntimeFact(String factId, Mixed value)](#almanacaddruntimefactstring-factid-mixed-value)
* [almanac.getEvents(String outcome) -> Events[]](#almanacgeteventsstring-outcome---events)
* [almanac.getResults() -> RuleResults[]](#almanacgetresults---ruleresults)
Expand Down Expand Up @@ -33,8 +34,28 @@ almanac
.then( value => console.log(value))
```

### almanac.addFact(String id, Function [definitionFunc], Object [options])

Sets a fact in the almanac. Used in conjunction with rule and engine event emissions.

```js
// constant facts:
engine.addFact('speed-of-light', 299792458)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a mistake, engine.addFact should be almanac.addFact


// facts computed via function
engine.addFact('account-type', function getAccountType(params, almanac) {
// ...
})

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

### almanac.addRuntimeFact(String factId, Mixed value)

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

```js
Expand Down
10 changes: 10 additions & 0 deletions docs/engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,16 @@ const {
```
Link to the [Almanac documentation](./almanac.md)

Optionally, you may specify a specific almanac instance via the almanac property.

```js
// create a custom Almanac
const myCustomAlmanac = new CustomAlmanac();

// run the engine with the custom almanac
await engine.run({}, { almanac: myCustomAlmanac })
```

### engine.stop() -> Engine

Stops the rules engine from running the next priority set of Rules. All remaining rules will be resolved as undefined,
Expand Down
6 changes: 3 additions & 3 deletions examples/07-rule-chaining.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,16 @@ async function start () {
event: { type: 'drinks-screwdrivers' },
priority: 10, // IMPORTANT! Set a higher priority for the drinkRule, so it runs first
onSuccess: async function (event, almanac) {
almanac.addRuntimeFact('screwdriverAficionado', true)
almanac.addFact('screwdriverAficionado', true)

// asychronous operations can be performed within callbacks
// engine execution will not proceed until the returned promises is resolved
const accountId = await almanac.factValue('accountId')
const accountInfo = await getAccountInformation(accountId)
almanac.addRuntimeFact('accountInfo', accountInfo)
almanac.addFact('accountInfo', accountInfo)
},
onFailure: function (event, almanac) {
almanac.addRuntimeFact('screwdriverAficionado', false)
almanac.addFact('screwdriverAficionado', false)
}
}
engine.addRule(drinkRule)
Expand Down
94 changes: 94 additions & 0 deletions examples/12-using-custom-almanac.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
'use strict'

require('colors')
const { Almanac, Engine } = require('json-rules-engine')

/**
* Almanac that support piping values through named functions
*/
class PipedAlmanac extends Almanac {
constructor (options) {
super(options)
this.pipes = new Map()
}

addPipe (name, pipe) {
this.pipes.set(name, pipe)
}

factValue (factId, params, path) {
let pipes = []
if (params && 'pipes' in params && Array.isArray(params.pipes)) {
pipes = params.pipes
delete params.pipes
}
return super.factValue(factId, params, path).then(value => {
return pipes.reduce((value, pipeName) => {
const pipe = this.pipes.get(pipeName)
if (pipe) {
return pipe(value)
}
return value
}, value)
})
}
}

async function start () {
const engine = new Engine()
.addRule({
conditions: {
all: [
{
fact: 'age',
params: {
// the addOne pipe adds one to the value
pipes: ['addOne']
},
operator: 'greaterThanInclusive',
value: 21
}
]
},
event: {
type: 'Over 21(ish)'
}
})

engine.on('success', async (event, almanac) => {
const name = await almanac.factValue('name')
const age = await almanac.factValue('age')
console.log(`${name} is ${age} years old and ${'is'.green} ${event.type}`)
})

engine.on('failure', async (event, almanac) => {
const name = await almanac.factValue('name')
const age = await almanac.factValue('age')
console.log(`${name} is ${age} years old and ${'is not'.red} ${event.type}`)
})

const createAlmanacWithPipes = () => {
const almanac = new PipedAlmanac()
almanac.addPipe('addOne', (v) => v + 1)
return almanac
}

// first run Bob who is less than 20
await engine.run({ name: 'Bob', age: 19 }, { almanac: createAlmanacWithPipes() })

// second run Alice who is 21
await engine.run({ name: 'Alice', age: 21 }, { almanac: createAlmanacWithPipes() })

// third run Chad who is 20
await engine.run({ name: 'Chad', age: 20 }, { almanac: createAlmanacWithPipes() })
}

start()

/*
* OUTPUT:
*
* Bob is 19 years old and is not Over 21(ish)
* Alice is 21 years old and is Over 21(ish)
* Chad is 20 years old and is Over 21(ish)
*/
40 changes: 26 additions & 14 deletions src/almanac.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,13 @@ function defaultPathResolver (value, path) {
* A new almanac is used for every engine run()
*/
export default class Almanac {
constructor (factMap, runtimeFacts = {}, options = {}) {
this.factMap = new Map(factMap)
constructor (options = {}) {
this.factMap = new Map()
this.factResultsCache = new Map() // { cacheKey: Promise<factValu> }
this.allowUndefinedFacts = Boolean(options.allowUndefinedFacts)
this.pathResolver = options.pathResolver || defaultPathResolver
this.events = { success: [], failure: [] }
this.ruleResults = []

for (const factId in runtimeFacts) {
let fact
if (runtimeFacts[factId] instanceof Fact) {
fact = runtimeFacts[factId]
} else {
fact = new Fact(factId, runtimeFacts[factId])
}

this._addConstantFact(fact)
debug(`almanac::constructor initialized runtime fact:${fact.id} with ${fact.value}<${typeof fact.value}>`)
}
}

/**
Expand Down Expand Up @@ -103,8 +91,32 @@ export default class Almanac {
return factValue
}

/**
* Add a fact definition to the engine. Facts are called by rules as they are evaluated.
* @param {object|Fact} id - fact identifier or instance of Fact
* @param {function} definitionFunc - function to be called when computing the fact value for a given rule
* @param {Object} options - options to initialize the fact with. used when "id" is not a Fact instance
*/
addFact (id, valueOrMethod, options) {
let factId = id
let fact
if (id instanceof Fact) {
factId = id.id
fact = id
} else {
fact = new Fact(id, valueOrMethod, options)
}
debug(`almanac::addFact id:${factId}`)
this.factMap.set(factId, fact)
if (fact.isConstant()) {
this._setFactValue(fact, {}, fact.value)
}
return this
}

/**
* Adds a constant fact during runtime. Can be used mid-run() to add additional information
* @deprecated use addFact
* @param {String} fact - fact identifier
* @param {Mixed} value - constant value of the fact
*/
Expand Down
21 changes: 18 additions & 3 deletions src/engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -261,14 +261,29 @@ class Engine extends EventEmitter {
* @param {Object} runOptions - run options
* @return {Promise} resolves when the engine has completed running
*/
run (runtimeFacts = {}) {
run (runtimeFacts = {}, runOptions = {}) {
debug('engine::run started')
this.status = RUNNING
const almanacOptions = {

const almanac = runOptions.almanac || new Almanac({
allowUndefinedFacts: this.allowUndefinedFacts,
pathResolver: this.pathResolver
})

this.facts.forEach(fact => {
almanac.addFact(fact)
})
for (const factId in runtimeFacts) {
let fact
if (runtimeFacts[factId] instanceof Fact) {
fact = runtimeFacts[factId]
} else {
fact = new Fact(factId, runtimeFacts[factId])
}

almanac.addFact(fact)
debug(`engine::run initialized runtime fact:${fact.id} with ${fact.value}<${typeof fact.value}>`)
}
const almanac = new Almanac(this.facts, runtimeFacts, almanacOptions)
const orderedSets = this.prioritizeRules()
let cursor = Promise.resolve()
// for each rule set, evaluate in parallel,
Expand Down
3 changes: 2 additions & 1 deletion src/json-rules-engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import Engine from './engine'
import Fact from './fact'
import Rule from './rule'
import Operator from './operator'
import Almanac from './almanac'

export { Fact, Rule, Operator, Engine }
export { Fact, Rule, Operator, Engine, Almanac }
export default function (rules, options) {
return new Engine(rules, options)
}
46 changes: 27 additions & 19 deletions test/almanac.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,33 @@ describe('Almanac', () => {
})

it('adds runtime facts', () => {
almanac = new Almanac(new Map(), { modelId: 'XYZ' })
almanac = new Almanac()
almanac.addFact('modelId', 'XYZ')
expect(almanac.factMap.get('modelId').value).to.equal('XYZ')
})
})

describe('constructor', () => {
describe('addFact', () => {
it('supports runtime facts as key => values', () => {
almanac = new Almanac(new Map(), { fact1: 3 })
almanac = new Almanac()
almanac.addFact('fact1', 3)
return expect(almanac.factValue('fact1')).to.eventually.equal(3)
})

it('supporrts runtime facts as dynamic callbacks', async () => {
almanac = new Almanac()
almanac.addFact('fact1', () => {
factSpy()
return Promise.resolve(3)
})
await expect(almanac.factValue('fact1')).to.eventually.equal(3)
await expect(factSpy).to.have.been.calledOnce()
})

it('supports runtime fact instances', () => {
const fact = new Fact('fact1', 3)
almanac = new Almanac(new Map(), { fact1: fact })
almanac = new Almanac()
almanac.addFact(fact)
return expect(almanac.factValue('fact1')).to.eventually.equal(fact.value)
})
})
Expand Down Expand Up @@ -69,9 +82,8 @@ describe('Almanac', () => {
if (params.userId) return params.userId
return 'unknown'
})
const factMap = new Map()
factMap.set(fact.id, fact)
almanac = new Almanac(factMap)
almanac = new Almanac()
almanac.addFact(fact)
})

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

describe('_getFact', _ => {
it('retrieves the fact object', () => {
const facts = new Map()
const fact = new Fact('id', 1)
facts.set(fact.id, fact)
almanac = new Almanac(facts)
almanac = new Almanac()
almanac.addFact(fact)
expect(almanac._getFact('id')).to.equal(fact)
})
})
Expand All @@ -124,9 +135,8 @@ describe('Almanac', () => {

function setup (f = new Fact('id', 1)) {
fact = f
const facts = new Map()
facts.set(fact.id, fact)
almanac = new Almanac(facts)
almanac = new Almanac()
almanac.addFact(fact)
}
let fact
const FACT_VALUE = 2
Expand Down Expand Up @@ -154,9 +164,8 @@ describe('Almanac', () => {
name: 'Thomas'
}]
})
const factMap = new Map()
factMap.set(fact.id, fact)
almanac = new Almanac(factMap)
almanac = new Almanac()
almanac.addFact(fact)
const result = await almanac.factValue('foo', null, '$..name')
expect(result).to.deep.equal(['George', 'Thomas'])
})
Expand All @@ -167,9 +176,8 @@ describe('Almanac', () => {
factSpy()
return 'unknown'
}, factOptions)
const factMap = new Map()
factMap.set(fact.id, fact)
almanac = new Almanac(factMap)
almanac = new Almanac()
almanac.addFact(fact)
}

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