Skip to content

Commit cc5e3fd

Browse files
authored
Merge pull request #237 from CacheControl/promisify-events
2 parents f1546c2 + 3eac30c commit cc5e3fd

File tree

16 files changed

+422
-96
lines changed

16 files changed

+422
-96
lines changed

.github/workflows/node.js.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ name: Node.js CI
55

66
on:
77
push:
8-
branches: [ master ]
8+
branches: [ master, next-major ]
99
pull_request:
10-
branches: [ master ]
10+
branches: [ master, next-major ]
1111

1212
jobs:
1313
build:

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
#### 6.0. / 2020-12-XX
2+
* BREAKING CHANGES
3+
* Private `rule.event` property renamed. Use `rule.getEvent()` to avoid breaking changes in the future.
4+
* 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+
16
#### 5.3.0 / 2020-12-02
27
* Allow facts to have a value of `undefined`
38

docs/engine.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ There are two generic event emissions that trigger automatically:
176176

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

179-
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).
179+
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.
180180

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

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

189-
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).
189+
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.
190190

191191
```js
192192
engine.on('failure', function(event, almanac, ruleResult) {

docs/rules.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,24 +50,36 @@ let rule = new Rule(options)
5050

5151
**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.
5252

53-
**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.
53+
**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.
5454

55-
**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.
55+
**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.
5656

5757
**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.
5858

5959
### setConditions(Array conditions)
6060

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

63+
### getConditions() -> Object
64+
65+
Retrieves rule condition set by constructor or `setCondition()`
66+
6367
### setEvent(Object event)
6468

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

71+
### getEvent() -> Object
72+
73+
Retrieves rule event set by constructor or `setEvent()`
74+
6775
### setPriority(Integer priority = 1)
6876

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

79+
### getPriority() -> Integer
80+
81+
Retrieves rule priority set by constructor or `setPriority()`
82+
7183
### toJSON(Boolean stringify = true)
7284

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

208220
### Comparing facts
209221

210-
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.
222+
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.
211223

212224
```js
213225
// identifies whether the current widget price is above a maximum

examples/07-rule-chaining.js

Lines changed: 42 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

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

1718
/**
1819
* Setup a new engine
@@ -36,8 +37,14 @@ const drinkRule = {
3637
},
3738
event: { type: 'drinks-screwdrivers' },
3839
priority: 10, // IMPORTANT! Set a higher priority for the drinkRule, so it runs first
39-
onSuccess: function (event, almanac) {
40+
onSuccess: async function (event, almanac) {
4041
almanac.addRuntimeFact('screwdriverAficionado', true)
42+
43+
// asychronous operations can be performed within callbacks
44+
// engine execution will not proceed until the returned promises is resolved
45+
const accountId = await almanac.factValue('accountId')
46+
const accountInfo = await getAccountInformation(accountId)
47+
almanac.addRuntimeFact('accountInfo', accountInfo)
4148
},
4249
onFailure: function (event, almanac) {
4350
almanac.addRuntimeFact('screwdriverAficionado', false)
@@ -60,6 +67,11 @@ const inviteRule = {
6067
fact: 'isSociable',
6168
operator: 'equal',
6269
value: true
70+
}, {
71+
fact: 'accountInfo',
72+
path: '$.company',
73+
operator: 'equal',
74+
value: 'microsoft'
6375
}]
6476
},
6577
event: { type: 'invite-to-screwdriver-social' },
@@ -70,46 +82,43 @@ engine.addRule(inviteRule)
7082
/**
7183
* Register listeners with the engine for rule success and failure
7284
*/
73-
let facts
7485
engine
75-
.on('success', (event, almanac) => {
76-
console.log(facts.accountId + ' DID '.green + 'meet conditions for the ' + event.type.underline + ' rule.')
86+
.on('success', async (event, almanac) => {
87+
const accountInfo = await almanac.factValue('accountInfo')
88+
const accountId = await almanac.factValue('accountId')
89+
console.log(`${accountId}(${accountInfo.company}) ` + 'DID'.green + ` meet conditions for the ${event.type.underline} rule.`)
7790
})
78-
.on('failure', event => {
79-
console.log(facts.accountId + ' did ' + 'NOT'.red + ' meet conditions for the ' + event.type.underline + ' rule.')
91+
.on('failure', async (event, almanac) => {
92+
const accountId = await almanac.factValue('accountId')
93+
console.log(`${accountId} did ` + 'NOT'.red + ` meet conditions for the ${event.type.underline} rule.`)
8094
})
8195

82-
// define fact(s) known at runtime
83-
facts = { accountId: 'washington', drinksOrangeJuice: true, enjoysVodka: true, isSociable: true }
84-
engine
85-
.run(facts) // first run, using washington's facts
86-
.then((results) => {
87-
// access whether washington is a screwdriverAficionado,
88-
// which was determined at runtime via the rules `drinkRules`
89-
return results.almanac.factValue('screwdriverAficionado')
90-
})
91-
.then(isScrewdriverAficionado => {
92-
console.log(`${facts.accountId} ${isScrewdriverAficionado ? 'IS'.green : 'IS NOT'.red} a screwdriver aficionado`)
93-
})
94-
.then(() => {
95-
facts = { accountId: 'jefferson', drinksOrangeJuice: true, enjoysVodka: false, isSociable: true }
96-
return engine.run(facts) // second run, using jefferson's facts; facts & evaluation are independent of the first run
97-
})
98-
.then((results) => {
99-
// access whether jefferson is a screwdriverAficionado,
100-
// which was determined at runtime via the rules `drinkRules`
101-
return results.almanac.factValue('screwdriverAficionado')
102-
})
103-
.then(isScrewdriverAficionado => {
104-
console.log(`${facts.accountId} ${isScrewdriverAficionado ? 'IS'.green : 'IS NOT'.red} a screwdriver aficionado`)
105-
})
106-
.catch(console.log)
96+
async function run () {
97+
// define fact(s) known at runtime
98+
let facts = { accountId: 'washington', drinksOrangeJuice: true, enjoysVodka: true, isSociable: true, accountInfo: {} }
99+
100+
// first run, using washington's facts
101+
let results = await engine.run(facts)
102+
103+
// isScrewdriverAficionado was a fact set by engine.run()
104+
let isScrewdriverAficionado = results.almanac.factValue('screwdriverAficionado')
105+
console.log(`${facts.accountId} ${isScrewdriverAficionado ? 'IS'.green : 'IS NOT'.red} a screwdriver aficionado`)
106+
107+
facts = { accountId: 'jefferson', drinksOrangeJuice: true, enjoysVodka: false, isSociable: true, accountInfo: {} }
108+
results = await engine.run(facts) // second run, using jefferson's facts; facts & evaluation are independent of the first run
109+
110+
isScrewdriverAficionado = await results.almanac.factValue('screwdriverAficionado')
111+
console.log(`${facts.accountId} ${isScrewdriverAficionado ? 'IS'.green : 'IS NOT'.red} a screwdriver aficionado`)
112+
}
113+
114+
run().catch(console.log)
107115

108116
/*
109117
* OUTPUT:
110118
*
111-
* washington DID meet conditions for the drinks-screwdrivers rule.
112-
* washington DID meet conditions for the invite-to-screwdriver-social rule.
119+
* loading account information for "washington"
120+
* washington(microsoft) DID meet conditions for the drinks-screwdrivers rule.
121+
* washington(microsoft) DID meet conditions for the invite-to-screwdriver-social rule.
113122
* washington IS a screwdriver aficionado
114123
* jefferson did NOT meet conditions for the drinks-screwdrivers rule.
115124
* jefferson did NOT meet conditions for the invite-to-screwdriver-social rule.

package-lock.json

Lines changed: 14 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "json-rules-engine",
3-
"version": "5.3.0",
3+
"version": "6.0.0-alpha-3",
44
"description": "Rules Engine expressed in simple json",
55
"main": "dist/index.js",
66
"types": "types/index.d.ts",
@@ -49,6 +49,7 @@
4949
],
5050
"file": "./test/support/bootstrap.js",
5151
"checkLeaks": true,
52+
"recursive": true,
5253
"globals": [
5354
"expect"
5455
]
@@ -82,7 +83,7 @@
8283
},
8384
"dependencies": {
8485
"clone": "^2.1.2",
85-
"events": "^3.2.0",
86+
"eventemitter2": "^6.4.3",
8687
"hash-it": "^4.0.5",
8788
"jsonpath-plus": "^4.0.0",
8889
"lodash.isobjectlike": "^4.0.0"

src/almanac.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export default class Almanac {
114114
.then(factValue => {
115115
if (isObjectLike(factValue)) {
116116
const pathValue = JSONPath({ path, json: factValue, wrap: false })
117-
debug(`condition::evaluate extracting object property ${path}, received: ${pathValue}`)
117+
debug(`condition::evaluate extracting object property ${path}, received: ${JSON.stringify(pathValue)}`)
118118
return pathValue
119119
} else {
120120
debug(`condition::evaluate could not compute object path(${path}) of non-object: ${factValue} <${typeof factValue}>; continuing with ${factValue}`)

src/condition.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ export default class Condition {
101101
return almanac.factValue(this.fact, this.params, this.path)
102102
.then(leftHandSideValue => {
103103
const result = op.evaluate(leftHandSideValue, rightHandSideValue)
104-
debug(`condition::evaluate <${leftHandSideValue} ${this.operator} ${rightHandSideValue}?> (${result})`)
104+
debug(`condition::evaluate <${JSON.stringify(leftHandSideValue)} ${this.operator} ${JSON.stringify(rightHandSideValue)}?> (${result})`)
105105
return { result, leftHandSideValue, rightHandSideValue, operator: this.operator }
106106
})
107107
})

src/engine.js

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Fact from './fact'
44
import Rule from './rule'
55
import Operator from './operator'
66
import Almanac from './almanac'
7-
import { EventEmitter } from 'events'
7+
import EventEmitter from 'eventemitter2'
88
import { SuccessEventFact } from './engine-facts'
99
import defaultOperators from './engine-default-operators'
1010
import debug from './debug'
@@ -40,13 +40,14 @@ class Engine extends EventEmitter {
4040
*/
4141
addRule (properties) {
4242
if (!properties) throw new Error('Engine: addRule() requires options')
43-
if (!Object.prototype.hasOwnProperty.call(properties, 'conditions')) throw new Error('Engine: addRule() argument requires "conditions" property')
44-
if (!Object.prototype.hasOwnProperty.call(properties, 'event')) throw new Error('Engine: addRule() argument requires "event" property')
4543

4644
let rule
4745
if (properties instanceof Rule) {
4846
rule = properties
4947
} else {
48+
if (!Object.prototype.hasOwnProperty.call(properties, 'event')) throw new Error('Engine: addRule() argument requires "event" property')
49+
if (!Object.prototype.hasOwnProperty.call(properties, 'conditions')) throw new Error('Engine: addRule() argument requires "conditions" property')
50+
5051
rule = new Rule(properties)
5152
}
5253
rule.setEngine(this)
@@ -191,11 +192,12 @@ class Engine extends EventEmitter {
191192
return rule.evaluate(almanac).then((ruleResult) => {
192193
debug(`engine::run ruleResult:${ruleResult.result}`)
193194
if (ruleResult.result) {
194-
this.emit('success', rule.event, almanac, ruleResult)
195-
this.emit(rule.event.type, rule.event.params, almanac, ruleResult)
196-
almanac.factValue('success-events', { event: rule.event })
195+
return Promise.all([
196+
almanac.factValue('success-events', { event: ruleResult.event }),
197+
this.emitAsync('success', ruleResult.event, almanac, ruleResult)
198+
]).then(() => this.emitAsync(ruleResult.event.type, ruleResult.event.params, almanac, ruleResult))
197199
} else {
198-
this.emit('failure', rule.event, almanac, ruleResult)
200+
return this.emitAsync('failure', ruleResult.event, almanac, ruleResult)
199201
}
200202
})
201203
}))
@@ -209,7 +211,6 @@ class Engine extends EventEmitter {
209211
*/
210212
run (runtimeFacts = {}) {
211213
debug('engine::run started')
212-
debug('engine::run runtimeFacts:', runtimeFacts)
213214
runtimeFacts['success-events'] = new Fact('success-events', SuccessEventFact(), { cache: false })
214215
this.status = RUNNING
215216
const almanac = new Almanac(this.facts, runtimeFacts, { allowUndefinedFacts: this.allowUndefinedFacts })

0 commit comments

Comments
 (0)