Skip to content

Commit

Permalink
Support passing a custom Almanac
Browse files Browse the repository at this point in the history
Support for passing a custom almanac to the run options in the engine.
  • Loading branch information
chris-pardy committed Nov 5, 2023
1 parent 25f9f7f commit 3c1975e
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 8 deletions.
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
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)
*/
9 changes: 5 additions & 4 deletions src/engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -261,14 +261,15 @@ 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
}
const almanac = new Almanac(almanacOptions)
})

this.facts.forEach(fact => {
almanac.addFact(fact)
})
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)
}
20 changes: 20 additions & 0 deletions test/engine-run.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,24 @@ describe('Engine: run', () => {
})
})
})

describe('custom alamanc', () => {
class CapitalAlmanac extends Almanac {
factValue (factId, params, path) {
return super.factValue(factId, params, path).then(value => {
if (typeof value === 'string') {
return value.toUpperCase()
}
return value
})
}
}

it('returns the capitalized value when using the CapitalAlamanc', () => {
return engine.run({ greeting: 'hello', age: 30 }, { almanac: new CapitalAlmanac() }).then((results) => {
const fact = results.almanac.factValue('greeting')
return expect(fact).to.eventually.equal('HELLO')
})
})
})
})
14 changes: 11 additions & 3 deletions types/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
export interface EngineOptions {
export interface AlmanacOptions {
allowUndefinedFacts?: boolean;
pathResolver?: PathResolver;
}

export interface EngineOptions extends AlmanacOptions {
allowUndefinedConditions?: boolean;
replaceFactsInEventParams?: boolean;
pathResolver?: PathResolver;
}

export interface RunOptions {
almanac?: Almanac;
}

export interface EngineResult {
Expand Down Expand Up @@ -48,7 +55,7 @@ export class Engine {
on(eventName: "failure", handler: EventHandler): this;
on(eventName: string, handler: EventHandler): this;

run(facts?: Record<string, any>): Promise<EngineResult>;
run(facts?: Record<string, any>, runOptions?: RunOptions): Promise<EngineResult>;
stop(): this;
}

Expand All @@ -66,6 +73,7 @@ export class Operator<A = unknown, B = unknown> {
}

export class Almanac {
constructor(options?: AlmanacOptions);
factValue<T>(
factId: string,
params?: Record<string, any>,
Expand Down

0 comments on commit 3c1975e

Please sign in to comment.