Skip to content
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
4 changes: 2 additions & 2 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ name: Node.js CI

on:
push:
branches: [ master ]
branches: [ master, next-major ]
pull_request:
branches: [ master ]
branches: [ master, next-major ]

jobs:
build:
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
#### 6.0. / 2020-12-XX
* BREAKING CHANGES
* Private `rule.event` property renamed. Use `rule.getEvent()` to avoid breaking changes in the future.
* 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.

#### 5.3.0 / 2020-12-02
* Allow facts to have a value of `undefined`

Expand Down
4 changes: 2 additions & 2 deletions docs/engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ There are two generic event emissions that trigger automatically:

#### ```engine.on('success', Function(Object event, Almanac almanac, RuleResult ruleResult))```

Fires when a rule passes. The callback will receive the event object, the current [Almanac](./almanac.md), and the [Rule Result](./rules.md#rule-results).
Fires when a rule passes. The callback will receive the event object, the current [Almanac](./almanac.md), and the [Rule Result](./rules.md#rule-results). Any promise returned by the callback will be waited on to resolve before execution continues.

```js
engine.on('success', function(event, almanac, ruleResult) {
Expand All @@ -186,7 +186,7 @@ engine.on('success', function(event, almanac, ruleResult) {

#### ```engine.on('failure', Function(Object event, Almanac almanac, RuleResult ruleResult))```

Companion to 'success', except fires when a rule fails. The callback will receive the event object, the current [Almanac](./almanac.md), and the [Rule Result](./rules.md#rule-results).
Companion to 'success', except fires when a rule fails. The callback will receive the event object, the current [Almanac](./almanac.md), and the [Rule Result](./rules.md#rule-results). Any promise returned by the callback will be waited on to resolve before execution continues.

```js
engine.on('failure', function(event, almanac, ruleResult) {
Expand Down
18 changes: 15 additions & 3 deletions docs/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,24 +50,36 @@ let rule = new Rule(options)

**options.priority** : `[Number, default 1]` Dictates when rule should be run, relative to other rules. Higher priority rules are run before lower priority rules. Rules with the same priority are run in parallel. Priority must be a positive, non-zero integer.

**options.onSuccess** : `[Function(Object event, Almanac almanac)]` Registers callback with the rule's `on('success')` listener. The rule's `event` property and the current [Almanac](./almanac.md) are passed as arguments.
**options.onSuccess** : `[Function(Object event, Almanac almanac)]` Registers callback with the rule's `on('success')` listener. The rule's `event` property and the current [Almanac](./almanac.md) are passed as arguments. Any promise returned by the callback will be waited on to resolve before execution continues.

**options.onFailure** : `[Function(Object event, Almanac almanac)]` Registers callback with the rule's `on('failure')` listener. The rule's `event` property and the current [Almanac](./almanac.md) are passed as arguments.
**options.onFailure** : `[Function(Object event, Almanac almanac)]` Registers callback with the rule's `on('failure')` listener. The rule's `event` property and the current [Almanac](./almanac.md) are passed as arguments. Any promise returned by the callback will be waited on to resolve before execution continues.

**options.name** : `[Any]` A way of naming your rules, allowing them to be easily identifiable in [Rule Results](#rule-results). This is usually of type `String`, but could also be `Object`, `Array`, or `Number`. Note that the name need not be unique, and that it has no impact on execution of the rule.

### setConditions(Array conditions)

Helper for setting rule conditions. Alternative to passing the `conditions` option to the rule constructor.

### getConditions() -> Object

Retrieves rule condition set by constructor or `setCondition()`

### setEvent(Object event)

Helper for setting rule event. Alternative to passing the `event` option to the rule constructor.

### getEvent() -> Object

Retrieves rule event set by constructor or `setEvent()`

### setPriority(Integer priority = 1)

Helper for setting rule priority. Alternative to passing the `priority` option to the rule constructor.

### getPriority() -> Integer

Retrieves rule priority set by constructor or `setPriority()`

### toJSON(Boolean stringify = true)

Serializes the rule into a JSON string. Often used when persisting rules.
Expand Down Expand Up @@ -207,7 +219,7 @@ For an example, see [fact-dependency](../examples/04-fact-dependency.js)

### Comparing facts

Sometimes it is necessary to compare facts against others facts. This can be accomplished by nesting the second fact within the `value` property. This second fact has access to the same `params` and `path` helpers as the primary fact.
Sometimes it is necessary to compare facts against other facts. This can be accomplished by nesting the second fact within the `value` property. This second fact has access to the same `params` and `path` helpers as the primary fact.

```js
// identifies whether the current widget price is above a maximum
Expand Down
75 changes: 42 additions & 33 deletions examples/07-rule-chaining.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

require('colors')
const { Engine } = require('json-rules-engine')
const { getAccountInformation } = require('./support/account-api-client')

/**
* Setup a new engine
Expand All @@ -36,8 +37,14 @@ const drinkRule = {
},
event: { type: 'drinks-screwdrivers' },
priority: 10, // IMPORTANT! Set a higher priority for the drinkRule, so it runs first
onSuccess: function (event, almanac) {
onSuccess: async function (event, almanac) {
almanac.addRuntimeFact('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)
},
onFailure: function (event, almanac) {
almanac.addRuntimeFact('screwdriverAficionado', false)
Expand All @@ -60,6 +67,11 @@ const inviteRule = {
fact: 'isSociable',
operator: 'equal',
value: true
}, {
fact: 'accountInfo',
path: '$.company',
operator: 'equal',
value: 'microsoft'
}]
},
event: { type: 'invite-to-screwdriver-social' },
Expand All @@ -70,46 +82,43 @@ engine.addRule(inviteRule)
/**
* Register listeners with the engine for rule success and failure
*/
let facts
engine
.on('success', (event, almanac) => {
console.log(facts.accountId + ' DID '.green + 'meet conditions for the ' + event.type.underline + ' rule.')
.on('success', async (event, almanac) => {
const accountInfo = await almanac.factValue('accountInfo')
const accountId = await almanac.factValue('accountId')
console.log(`${accountId}(${accountInfo.company}) ` + 'DID'.green + ` meet conditions for the ${event.type.underline} rule.`)
})
.on('failure', event => {
console.log(facts.accountId + ' did ' + 'NOT'.red + ' meet conditions for the ' + event.type.underline + ' rule.')
.on('failure', async (event, almanac) => {
const accountId = await almanac.factValue('accountId')
console.log(`${accountId} did ` + 'NOT'.red + ` meet conditions for the ${event.type.underline} rule.`)
})

// define fact(s) known at runtime
facts = { accountId: 'washington', drinksOrangeJuice: true, enjoysVodka: true, isSociable: true }
engine
.run(facts) // first run, using washington's facts
.then((results) => {
// access whether washington is a screwdriverAficionado,
// which was determined at runtime via the rules `drinkRules`
return results.almanac.factValue('screwdriverAficionado')
})
.then(isScrewdriverAficionado => {
console.log(`${facts.accountId} ${isScrewdriverAficionado ? 'IS'.green : 'IS NOT'.red} a screwdriver aficionado`)
})
.then(() => {
facts = { accountId: 'jefferson', drinksOrangeJuice: true, enjoysVodka: false, isSociable: true }
return engine.run(facts) // second run, using jefferson's facts; facts & evaluation are independent of the first run
})
.then((results) => {
// access whether jefferson is a screwdriverAficionado,
// which was determined at runtime via the rules `drinkRules`
return results.almanac.factValue('screwdriverAficionado')
})
.then(isScrewdriverAficionado => {
console.log(`${facts.accountId} ${isScrewdriverAficionado ? 'IS'.green : 'IS NOT'.red} a screwdriver aficionado`)
})
.catch(console.log)
async function run () {
// define fact(s) known at runtime
let facts = { accountId: 'washington', drinksOrangeJuice: true, enjoysVodka: true, isSociable: true, accountInfo: {} }

// first run, using washington's facts
let results = await engine.run(facts)

// isScrewdriverAficionado was a fact set by engine.run()
let isScrewdriverAficionado = results.almanac.factValue('screwdriverAficionado')
console.log(`${facts.accountId} ${isScrewdriverAficionado ? 'IS'.green : 'IS NOT'.red} a screwdriver aficionado`)

facts = { accountId: 'jefferson', drinksOrangeJuice: true, enjoysVodka: false, isSociable: true, accountInfo: {} }
results = await engine.run(facts) // second run, using jefferson's facts; facts & evaluation are independent of the first run

isScrewdriverAficionado = await results.almanac.factValue('screwdriverAficionado')
console.log(`${facts.accountId} ${isScrewdriverAficionado ? 'IS'.green : 'IS NOT'.red} a screwdriver aficionado`)
}

run().catch(console.log)

/*
* OUTPUT:
*
* washington DID meet conditions for the drinks-screwdrivers rule.
* washington DID meet conditions for the invite-to-screwdriver-social rule.
* loading account information for "washington"
* washington(microsoft) DID meet conditions for the drinks-screwdrivers rule.
* washington(microsoft) DID meet conditions for the invite-to-screwdriver-social rule.
* washington IS a screwdriver aficionado
* jefferson did NOT meet conditions for the drinks-screwdrivers rule.
* jefferson did NOT meet conditions for the invite-to-screwdriver-social rule.
Expand Down
28 changes: 14 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "json-rules-engine",
"version": "5.3.0",
"version": "6.0.0-alpha-3",
"description": "Rules Engine expressed in simple json",
"main": "dist/index.js",
"types": "types/index.d.ts",
Expand Down Expand Up @@ -49,6 +49,7 @@
],
"file": "./test/support/bootstrap.js",
"checkLeaks": true,
"recursive": true,
"globals": [
"expect"
]
Expand Down Expand Up @@ -82,7 +83,7 @@
},
"dependencies": {
"clone": "^2.1.2",
"events": "^3.2.0",
"eventemitter2": "^6.4.3",
"hash-it": "^4.0.5",
"jsonpath-plus": "^4.0.0",
"lodash.isobjectlike": "^4.0.0"
Expand Down
2 changes: 1 addition & 1 deletion src/almanac.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export default class Almanac {
.then(factValue => {
if (isObjectLike(factValue)) {
const pathValue = JSONPath({ path, json: factValue, wrap: false })
debug(`condition::evaluate extracting object property ${path}, received: ${pathValue}`)
debug(`condition::evaluate extracting object property ${path}, received: ${JSON.stringify(pathValue)}`)
return pathValue
} else {
debug(`condition::evaluate could not compute object path(${path}) of non-object: ${factValue} <${typeof factValue}>; continuing with ${factValue}`)
Expand Down
2 changes: 1 addition & 1 deletion src/condition.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export default class Condition {
return almanac.factValue(this.fact, this.params, this.path)
.then(leftHandSideValue => {
const result = op.evaluate(leftHandSideValue, rightHandSideValue)
debug(`condition::evaluate <${leftHandSideValue} ${this.operator} ${rightHandSideValue}?> (${result})`)
debug(`condition::evaluate <${JSON.stringify(leftHandSideValue)} ${this.operator} ${JSON.stringify(rightHandSideValue)}?> (${result})`)
return { result, leftHandSideValue, rightHandSideValue, operator: this.operator }
})
})
Expand Down
17 changes: 9 additions & 8 deletions src/engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Fact from './fact'
import Rule from './rule'
import Operator from './operator'
import Almanac from './almanac'
import { EventEmitter } from 'events'
import EventEmitter from 'eventemitter2'
import { SuccessEventFact } from './engine-facts'
import defaultOperators from './engine-default-operators'
import debug from './debug'
Expand Down Expand Up @@ -40,13 +40,14 @@ class Engine extends EventEmitter {
*/
addRule (properties) {
if (!properties) throw new Error('Engine: addRule() requires options')
if (!Object.prototype.hasOwnProperty.call(properties, 'conditions')) throw new Error('Engine: addRule() argument requires "conditions" property')
if (!Object.prototype.hasOwnProperty.call(properties, 'event')) throw new Error('Engine: addRule() argument requires "event" property')

let rule
if (properties instanceof Rule) {
rule = properties
} else {
if (!Object.prototype.hasOwnProperty.call(properties, 'event')) throw new Error('Engine: addRule() argument requires "event" property')
if (!Object.prototype.hasOwnProperty.call(properties, 'conditions')) throw new Error('Engine: addRule() argument requires "conditions" property')

rule = new Rule(properties)
}
rule.setEngine(this)
Expand Down Expand Up @@ -191,11 +192,12 @@ class Engine extends EventEmitter {
return rule.evaluate(almanac).then((ruleResult) => {
debug(`engine::run ruleResult:${ruleResult.result}`)
if (ruleResult.result) {
this.emit('success', rule.event, almanac, ruleResult)
this.emit(rule.event.type, rule.event.params, almanac, ruleResult)
almanac.factValue('success-events', { event: rule.event })
return Promise.all([
almanac.factValue('success-events', { event: ruleResult.event }),
this.emitAsync('success', ruleResult.event, almanac, ruleResult)
]).then(() => this.emitAsync(ruleResult.event.type, ruleResult.event.params, almanac, ruleResult))
} else {
this.emit('failure', rule.event, almanac, ruleResult)
return this.emitAsync('failure', ruleResult.event, almanac, ruleResult)
}
})
}))
Expand All @@ -209,7 +211,6 @@ class Engine extends EventEmitter {
*/
run (runtimeFacts = {}) {
debug('engine::run started')
debug('engine::run runtimeFacts:', runtimeFacts)
runtimeFacts['success-events'] = new Fact('success-events', SuccessEventFact(), { cache: false })
this.status = RUNNING
const almanac = new Almanac(this.facts, runtimeFacts, { allowUndefinedFacts: this.allowUndefinedFacts })
Expand Down
Loading