Skip to content

Commit 0d53cdb

Browse files
author
Cache Hamm
committed
Overhaul rules.md and add fact-comparison docs
1 parent a62d1ee commit 0d53cdb

6 files changed

Lines changed: 231 additions & 85 deletions

File tree

docs/almanac.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ The almanac for the current engine run is available as arguments passed to the f
1212

1313
## Methods
1414

15-
### almanac.factValue(Fact fact, Object params) -> Promise
15+
### almanac.factValue(Fact fact, Object params, String path) -> Promise
1616

17-
Computes the value of the provided fact + params.
17+
Computes the value of the provided fact + params. If "path" is provided, it will be used as a property accessor on the fact's return object.
1818

1919
```js
2020
almanac
21-
.factValue('account-information', { accountId: 1 })
22-
.then( values => console.log(values))
21+
.factValue('account-information', { accountId: 1 }, '.balance')
22+
.then( value => console.log(value))
2323
```
2424

2525
### almanac.addRuntimeFact(String factId, Mixed value)
@@ -37,7 +37,7 @@ almanac.addRuntimeFact('account-id', 1)
3737
The most common use of the almanac is to access data computed by other facts during runtime. This allows
3838
leveraging the engine's caching mechanisms to design more efficient rules.
3939

40-
The [computed-facts](../examples/computed-facts) example demonstrates a real world application of this technique.
40+
The [fact-dependency](../examples/04-fact-dependency.js) example demonstrates a real world application of this technique.
4141

4242
For example, say there were two facts: _is-funded-account_ and _account-balance_. Both facts depend on the same _account-information_ data set.
4343
Using the Almanac, each fact can be defined to call a **base** fact responsible for loading the data. This causes the engine
@@ -97,7 +97,7 @@ engine.run({ accountId: 1 })
9797
When a rule evalutes truthy and its ```event``` is called, new facts may be defined by the event handler.
9898
Note that with this technique, the rule priority becomes important; if a rule is expected to
9999
define a fact value, it's important that rule be run prior to other rules that reference the fact. To
100-
learn more about setting rule priorties, see the [rule documentation](./rule.md).
100+
learn more about setting rule priorities, see the [rule documentation](./rules.md).
101101
102102
```js
103103
engine.on('success', (event, almanac) => {

docs/engine.md

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -82,18 +82,20 @@ engine.addOperator('startsWithLetter', (factValue, jsonValue) => {
8282
})
8383

8484
// and to use the operator...
85-
rule.setConditions({
86-
all: [
87-
{
88-
fact: 'username',
89-
operator: 'startsWithLetter' // reference the operator name in the rule
90-
value: 'a'
91-
}
92-
]
93-
})
85+
let rule = new Rule(
86+
conditions: {
87+
all: [
88+
{
89+
fact: 'username',
90+
operator: 'startsWithLetter', // reference the operator name in the rule
91+
value: 'a'
92+
}
93+
]
94+
}
95+
)
9496
```
9597

96-
See the [operator example](../examples/custom-operators.js)
98+
See the [operator example](../examples/06-custom-operators.js)
9799

98100
### engine.run([Object facts], [Object options]) -> Promise (Events)
99101

@@ -128,19 +130,21 @@ engine.stop()
128130

129131
### engine.on(String event, Function callback) -> Engine
130132

131-
Listens for events emitted as rules are being evaluated. "event" is determined by [rule.setEvent](./rules.md#seteventobject-event).
133+
Listens for events emitted as rules are being evaluated. "event" is determined by the [rule event](./rules.md#Events).
132134

133135
```js
134-
rule.setEvent({
135-
type: 'my-event',
136-
params: {
137-
id: 1
136+
let rule = new Rule({
137+
event: {
138+
type: 'my-event',
139+
params: {
140+
customValue: 'my-custom-value'
141+
}
138142
}
139143
})
140144

141-
// whenever rule is evaluated and the conditions pass, 'my-event' will trigger
145+
// whenever rule is evaluated and conditions pass, 'my-event' will trigger
142146
engine.on('my-event', function(params) {
143-
console.log(params) // id: 1
147+
console.log(params) // { customValue: 'my-custom-value' }
144148
})
145149
```
146150

docs/facts.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
# Facts
22

3-
Facts are methods or constants registered with the engine prior to runtime and referenced within rule conditions. Each fact method should be a pure function that may return a computed value or promise.
3+
Facts are methods or constants registered with the engine prior to runtime and referenced within rule conditions. Each fact method should be a pure function that may return a either computed value, or promise that resolves to a computed value.
44
As rule conditions are evaluated during runtime, they retrieve fact values dynamically and use the condition _operator_ to compare the fact result with the condition _value_.
55

66
## Methods
77

8-
### constructor(String id, Constant|Function, [Object options]) -> instance
8+
### constructor(String id, Constant|Function(Object params, Almanac almanac), [Object options]) -> instance
99

1010
```js
1111
// constant value facts
1212
let fact = new Fact('apiKey', '4feca34f9d67e99b8af2')
1313

1414
// dynamic facts
15-
let fact = new Fact('account-type', function getAccountType() {
15+
let fact = new Fact('account-type', (params, almanac) => {
1616
// ...
1717
})
1818

1919
// facts with options:
20-
engine.addFact('account-type', function getAccountType() {
20+
engine.addFact('account-type', (params, almanac) => {
2121
// ...
2222
}, { cache: false, priority: 500 })
2323
```

docs/rules.md

Lines changed: 182 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,76 +9,217 @@ Rules contain a set of _conditions_ and a single _event_. When the engine is ru
99
Returns a new rule instance
1010

1111
```js
12+
let options = {
13+
conditions: {
14+
all: [
15+
{
16+
fact: 'my-fact',
17+
operator: 'equal',
18+
value: 'some-value'
19+
}
20+
]
21+
},
22+
event: {
23+
type: 'my-event',
24+
params: {
25+
customProperty: 'customValue'
26+
}
27+
},
28+
priority: 1, // optional, default: 1
29+
onSuccess: function (event, almanac) {}, // optional
30+
onFailure: function (event, almanac) {}, // optional
31+
}
1232
let rule = new Rule(options)
1333
```
1434

35+
**options.conditions** : `[Object]` Rule conditions object
36+
37+
**options.event** : `[Object]` Sets the `.on('success')` and `on('failure')` event argument emitted whenever the rule passes. Event objects must have a ```type``` property, and an optional ```params``` property.
38+
39+
**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.
40+
41+
**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.
42+
43+
**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.
44+
1545
### setConditions(Array conditions)
1646

17-
Assigns the rule conditions to the provided argument. The root condition must be a boolean operator (```all``` or ```any```)
47+
Helper for setting rule conditions. Alternative to passing the `conditions` option to the rule constructor.
48+
49+
### setEvent(Object event)
50+
51+
Helper for setting rule event. Alternative to passing the `event` option to the rule constructor.
52+
53+
### setPriority(Integer priority = 1)
54+
55+
Helper for setting rule priority. Alternative to passing the `priority` option to the rule constructor.
56+
57+
### toJSON(Boolean stringify = true)
58+
59+
Serializes the rule into a JSON string. Often used when persisting rules.
1860

1961
```js
20-
rule.setConditions({
21-
all: [
22-
{
23-
fact: 'revenue',
24-
operator: 'greaterThanInclusive'
25-
value: 1000000
26-
}
27-
]
28-
})
62+
let jsonString = rule.toJSON() // string: '{"conditions":{"all":[]},"priority":50 ...
2963

30-
// if fact returns an object or array, providing a "path" key can be used for property traversal
31-
rule.setConditions({
32-
all: [
33-
{
34-
fact: 'userData', // 'userData' fact returns { profile: { addresses: [{ city: 'new york' }]}}
35-
operator: 'equal'
36-
value: 'new york',
37-
path: '.profile.addresses[0].city' // "path" navigates the data structure, down to the "city" property
38-
}
39-
]
40-
})
64+
let rule = new Rule(jsonString) // restored rule; same conditions, priority, event
65+
66+
// without stringifying
67+
let jsonObject = rule.toJSON(false) // object: {conditions:{ all: [] }, priority: 50 ...
4168
```
4269

43-
See the [fact dependency example](../examples/fact-dependency.js)
70+
## Conditions
4471

45-
### setEvent(Object event)
72+
Rule conditions are a combination of facts, operators, and values that determine whether the rule is a `success` or a `failure`.
4673

47-
Sets the event the engine should emit when the rule conditions pass. All events must have a ```type``` property, which denotes the event name to emit when the rule passes.
74+
### Basic conditions
4875

49-
Optionally, a ```params``` property may be provided as well. ```params``` will be passed to the event as an argument.
76+
The simplest form of a condition consists of a `fact`, an `operator`, and a `value`. When the engine runs, the operator is used to compare the fact against the value.
5077

5178
```js
52-
rule.setEvent({
53-
type: 'string', //required
54-
params: { object } //optional
79+
// my-fact <= 1
80+
let rule = new Rule({
81+
conditions: {
82+
all: [
83+
{
84+
fact: 'my-fact',
85+
operator: 'lessThanInclusive',
86+
value: 1
87+
}
88+
]
89+
}
5590
})
5691
```
5792

58-
### setPriority(Integer priority = 1)
93+
See the [hello-world](../examples/01-hello-world.js) example.
94+
95+
### Boolean expressions: `all` and `any`
5996

60-
Sets the rule priority. Priority must be a positive, non-zero integer. The higher the priority, the sooner the rule will run. If no priority is assigned to a Rule, it will receive a default priority of 1.
97+
Each rule's conditions *must* have either an `all` or an `any` operator at its root, containing an array of conditions. The `all` operator specifies that all conditions contained within must be truthy for the rule to be considered a `success`. The `any` operator only requires one condition to be truthy for the rule to succeed.
6198

6299
```js
63-
rule.setPriority(100)
100+
// all:
101+
let rule = new Rule({
102+
conditions: {
103+
all: [
104+
{ /* condition 1 */ },
105+
{ /* condition 2 */ },
106+
{ /* condition n */ },
107+
]
108+
}
109+
})
110+
111+
// any:
112+
let rule = new Rule({
113+
conditions: {
114+
any: [
115+
{ /* condition 1 */ },
116+
{ /* condition 2 */ },
117+
{ /* condition n */ },
118+
{
119+
all: [ /* more conditions */ ]
120+
}
121+
]
122+
}
123+
})
64124
```
65125

66-
### toJSON(Boolean stringify = true)
126+
Notice in the second example how `all` and `any` can be nested within one another to produce complex boolean expressions. See the [nested-boolean-logic](../examples/02-nested-boolean-logic.js) example.
67127

68-
Serializes the rule into a JSON string. Usually used when persisting rules.
128+
### Condition helpers: `params`
129+
130+
Sometimes facts require additional input to perform calculations. For this, the `params` property is passed as an argument to the fact handler. `params` essentially functions as fact arguments, enabling fact handlers to be more generic and reusable.
69131

70132
```js
71-
let jsonString = rule.toJSON() // string: '{"conditions":{"all":[]},"priority":50 ...
133+
// product-price retrieves any product's price based on the "productId" in "params"
134+
engine.addFact('product-price', function (params, almanac) {
135+
return productLoader(params.productId) // loads the "widget" product
136+
.then(product => product.price)
137+
})
72138

73-
let rule = new Rule(jsonString) // restored rule; same conditions, priority, event
139+
// identifies whether the current widget price is above $100
140+
let rule = new Rule({
141+
conditions: {
142+
all: [
143+
{
144+
fact: 'product-price',
145+
params: {
146+
productId: 'widget' // specifies which product to load
147+
},
148+
operator: 'greaterThan',
149+
value: 100
150+
}
151+
]
152+
}
153+
})
154+
```
74155

75-
// without stringifying
76-
let jsonObject = rule.toJSON(false) // object: {conditions:{ all: [] }, priority: 50 ...
156+
See the [dynamic-facts](../examples/03-dynamic-facts) example
157+
158+
### Condition helpers: `path`
159+
160+
In the `params` example above, the dynamic fact handler loads an object, then returns a specific object property. For more complex data structures, writing a separate fact handler for each object property can sometimes become unwieldy.
161+
162+
To alleviate this overhead, a `path` property is provided for traversing objects and arrays returned by facts. The example above becomes simpler, and only one fact handler must be written by the developer to handle any number of properties.
163+
164+
```js
165+
166+
// product-price retrieves any product's price based on the "productId" in "params"
167+
engine.addFact('product-price', function (params, almanac) {
168+
// NOTE: `then` is not required; .price is specified via "path" below
169+
return productLoader(params.productId)
170+
})
171+
172+
// identifies whether the current widget price is above $100
173+
let rule = new Rule({
174+
conditions: {
175+
all: [
176+
{
177+
fact: 'product-price',
178+
params: {
179+
productId: 'widget',
180+
// Complex accessor are supported, e.g. '.profile.addresses[0].city'
181+
path: '.price'
182+
},
183+
operator: 'greaterThan',
184+
value: 100
185+
}
186+
]
187+
}
188+
})
77189
```
190+
See the [fact-dependency](../examples/04-fact-dependency.js) example
78191

79-
### Events
192+
### Comparing facts
80193

81-
Listen for 'success' and 'failure' events emitted when rule is evaluated.
194+
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.
195+
196+
```js
197+
// identifies whether the current widget price is above a maximum
198+
let rule = new Rule({
199+
conditions: {
200+
all: [
201+
// widget-price > budget
202+
{
203+
fact: 'product-price',
204+
params: {
205+
productId: 'widget',
206+
path: '.price'
207+
},
208+
operator: 'greaterThan',
209+
// "value" contains a fact
210+
value: {
211+
fact: 'budget' // "params" and "path" helpers are available as well
212+
}
213+
}
214+
]
215+
}
216+
})
217+
```
218+
See the [fact-comparison](../examples/08-fact-comparison.js) example
219+
220+
## Events
221+
222+
Listen for `success` and `failure` events emitted when rule is evaluated.
82223

83224
#### ```rule.on('success', Function(Object event, Almanac almanac))```
84225

@@ -91,15 +232,15 @@ rule.on('success', function(event, almanac) {
91232

92233
#### ```rule.on('failure', Function(Object event, Almanac almanac))```
93234

94-
Companion to 'success', except fires when the rule fails.
235+
Companion to `success`, except fires when the rule fails.
95236

96237
```js
97238
engine.on('failure', function(event, almanac) {
98239
console.log(event) // { type: 'my-event', params: { id: 1 }
99240
})
100241
```
101242

102-
## Conditions
243+
## Operators
103244

104245
Each rule condition must begin with a boolean operator(```all``` or ```any```) at its root.
105246

0 commit comments

Comments
 (0)