From 3c1975e3bd3d4a754feac03e74227c528bfad632 Mon Sep 17 00:00:00 2001 From: Chris Pardy Date: Mon, 6 Nov 2023 01:07:09 +0200 Subject: [PATCH] Support passing a custom Almanac Support for passing a custom almanac to the run options in the engine. --- docs/engine.md | 10 +++ examples/12-using-custom-almanac.js | 94 +++++++++++++++++++++++++++++ src/engine.js | 9 +-- src/json-rules-engine.js | 3 +- test/engine-run.test.js | 20 ++++++ types/index.d.ts | 14 ++++- 6 files changed, 142 insertions(+), 8 deletions(-) create mode 100644 examples/12-using-custom-almanac.js diff --git a/docs/engine.md b/docs/engine.md index 9712b5e..8546656 100644 --- a/docs/engine.md +++ b/docs/engine.md @@ -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, diff --git a/examples/12-using-custom-almanac.js b/examples/12-using-custom-almanac.js new file mode 100644 index 0000000..c94c398 --- /dev/null +++ b/examples/12-using-custom-almanac.js @@ -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) + */ diff --git a/src/engine.js b/src/engine.js index 9536da6..c67f3a2 100644 --- a/src/engine.js +++ b/src/engine.js @@ -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) }) diff --git a/src/json-rules-engine.js b/src/json-rules-engine.js index 339c3c0..6f3b149 100644 --- a/src/json-rules-engine.js +++ b/src/json-rules-engine.js @@ -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) } diff --git a/test/engine-run.test.js b/test/engine-run.test.js index 18b8155..a96d950 100644 --- a/test/engine-run.test.js +++ b/test/engine-run.test.js @@ -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') + }) + }) + }) }) diff --git a/types/index.d.ts b/types/index.d.ts index c57423d..c6ace7f 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -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 { @@ -48,7 +55,7 @@ export class Engine { on(eventName: "failure", handler: EventHandler): this; on(eventName: string, handler: EventHandler): this; - run(facts?: Record): Promise; + run(facts?: Record, runOptions?: RunOptions): Promise; stop(): this; } @@ -66,6 +73,7 @@ export class Operator { } export class Almanac { + constructor(options?: AlmanacOptions); factValue( factId: string, params?: Record,