Skip to content

Commit 194ebdb

Browse files
authored
Merge pull request #376 from chris-pardy/operator-decorators
Operator decorators
2 parents b75acfd + 87afb2f commit 194ebdb

14 files changed

+614
-75
lines changed

docs/engine.md

+58
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ The Engine stores and executes rules, emits events, and maintains state.
1212
* [engine.removeRule(Rule instance | String ruleId)](#engineremoverulerule-instance)
1313
* [engine.addOperator(String operatorName, Function evaluateFunc(factValue, jsonValue))](#engineaddoperatorstring-operatorname-function-evaluatefuncfactvalue-jsonvalue)
1414
* [engine.removeOperator(String operatorName)](#engineremoveoperatorstring-operatorname)
15+
* [engine.addOperatorDecorator(String decoratorName, Function evaluateFunc(factValue, jsonValue, next))](#engineaddoperatordecoratorstring-decoratorname-function-evaluatefuncfactvalue-jsonvalue-next)
16+
* [engine.removeOperatorDecorator(String decoratorName)](#engineremoveoperatordecoratorstring-decoratorname)
1517
* [engine.setCondition(String name, Object conditions)](#enginesetconditionstring-name-object-conditions)
1618
* [engine.removeCondition(String name)](#engineremovecondtionstring-name)
1719
* [engine.run([Object facts], [Object options]) -> Promise ({ events: [], failureEvents: [], almanac: Almanac, results: [], failureResults: []})](#enginerunobject-facts-object-options---promise--events--failureevents--almanac-almanac-results--failureresults-)
@@ -181,6 +183,62 @@ engine.addOperator('startsWithLetter', (factValue, jsonValue) => {
181183
engine.removeOperator('startsWithLetter');
182184
```
183185

186+
### engine.addOperatorDecorator(String decoratorName, Function evaluateFunc(factValue, jsonValue, next))
187+
188+
Adds a custom operator decorator to the engine.
189+
190+
```js
191+
/*
192+
* decoratorName - operator decorator identifier used in the rule condition
193+
* evaluateFunc(factValue, jsonValue, next) - uses the decorated operator to compare the fact result to the condition 'value'
194+
* factValue - the value returned from the fact
195+
* jsonValue - the "value" property stored in the condition itself
196+
* next - the evaluateFunc of the decorated operator
197+
*/
198+
engine.addOperatorDecorator('first', (factValue, jsonValue, next) => {
199+
if (!factValue.length) return false
200+
return next(factValue[0], jsonValue)
201+
})
202+
203+
engine.addOperatorDecorator('caseInsensitive', (factValue, jsonValue, next) => {
204+
return next(factValue.toLowerCase(), jsonValue.toLowerCase())
205+
})
206+
207+
// and to use the decorator...
208+
let rule = new Rule(
209+
conditions: {
210+
all: [
211+
{
212+
fact: 'username',
213+
operator: 'first:caseInsensitive:equal', // reference the decorator:operator in the rule
214+
value: 'a'
215+
}
216+
]
217+
}
218+
)
219+
```
220+
221+
See the [operator decorator example](../examples/13-using-operator-decorators.js)
222+
223+
224+
225+
### engine.removeOperatorDecorator(String decoratorName)
226+
227+
Removes a operator decorator from the engine
228+
229+
```javascript
230+
engine.addOperatorDecorator('first', (factValue, jsonValue, next) => {
231+
if (!factValue.length) return false
232+
return next(factValue[0], jsonValue)
233+
})
234+
235+
engine.addOperatorDecorator('caseInsensitive', (factValue, jsonValue, next) => {
236+
return next(factValue.toLowerCase(), jsonValue.toLowerCase())
237+
})
238+
239+
engine.removeOperator('first');
240+
```
241+
184242
### engine.setCondition(String name, Object conditions)
185243

186244
Adds or updates a condition to the engine. Rules may include references to this condition. Conditions must start with `all`, `any`, `not`, or reference a condition.

docs/rules.md

+34
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ Rules contain a set of _conditions_ and a single _event_. When the engine is ru
2727
* [String and Numeric operators:](#string-and-numeric-operators)
2828
* [Numeric operators:](#numeric-operators)
2929
* [Array operators:](#array-operators)
30+
* [Operator Decorators](#operator-decorators)
31+
* [Array decorators:](#array-decorators)
32+
* [Logical decorators:](#logical-decorators)
33+
* [Utility decorators:](#utility-decorators)
34+
* [Decorator composition:](#decorator-composition)
3035
* [Rule Results](#rule-results)
3136
* [Persisting](#persisting)
3237

@@ -406,6 +411,35 @@ The ```operator``` compares the value returned by the ```fact``` to what is stor
406411
407412
```doesNotContain``` - _fact_ (an array) must not include _value_
408413
414+
## Operator Decorators
415+
416+
Operator Decorators modify the behavior of an operator either by changing the input or the output. To specify one or more decorators prefix the name of the operator with them in the ```operator``` field and use the colon (```:```) symbol to separate decorators and the operator. For instance ```everyFact:greaterThan``` will produce an operator that checks that every element of the _fact_ is greater than the value.
417+
418+
See [12-using-operator-decorators.js](../examples/13-using-operator-decorators.js) for an example.
419+
420+
### Array Decorators:
421+
422+
```everyFact``` - _fact_ (an array) must have every element pass the decorated operator for _value_
423+
424+
```everyValue``` - _fact_ must pass the decorated operator for every element of _value_ (an array)
425+
426+
```someFact``` - _fact_ (an array) must have at-least one element pass the decorated operator for _value_
427+
428+
```someValue``` - _fact_ must pass the decorated operator for at-least one element of _value_ (an array)
429+
430+
### Logical Decorators
431+
432+
```not``` - negate the result of the decorated operator
433+
434+
### Utility Decorators
435+
```swap``` - Swap _fact_ and _value_ for the decorated operator
436+
437+
### Decorator Composition
438+
439+
Operator Decorators can be composed by chaining them together with the colon to separate them. For example if you wanted to ensure that every number in an array was less than every number in another array you could use ```everyFact:everyValue:lessThan```.
440+
441+
```swap``` and ```not``` are useful when there are not symmetric or negated versions of custom operators, for instance you could check if a _value_ does not start with a letter contained in a _fact_ using the decorated custom operator ```swap:not:startsWithLetter```. This allows a single custom operator to have 4 permutations.
442+
409443
## Rule Results
410444
411445
After a rule is evaluated, a `rule result` object is provided to the `success` and `failure` events. This argument is similar to a regular rule, and contains additional metadata about how the rule was evaluated. Rule results can be used to extract the results of individual conditions, computed fact values, and boolean logic results. `name` can be used to easily identify a given rule.
+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
'use strict'
2+
/*
3+
* This example demonstrates using operator decorators.
4+
*
5+
* In this example, a fact contains a list of strings and we want to check if any of these are valid.
6+
*
7+
* Usage:
8+
* node ./examples/12-using-operator-decorators.js
9+
*
10+
* For detailed output:
11+
* DEBUG=json-rules-engine node ./examples/12-using-operator-decorators.js
12+
*/
13+
14+
require('colors')
15+
const { Engine } = require('json-rules-engine')
16+
17+
async function start () {
18+
/**
19+
* Setup a new engine
20+
*/
21+
const engine = new Engine()
22+
23+
/**
24+
* Add a rule for validating a tag (fact)
25+
* against a set of tags that are valid (also a fact)
26+
*/
27+
const validTags = {
28+
conditions: {
29+
all: [{
30+
fact: 'tags',
31+
operator: 'everyFact:in',
32+
value: { fact: 'validTags' }
33+
}]
34+
},
35+
event: {
36+
type: 'valid tags'
37+
}
38+
}
39+
40+
engine.addRule(validTags)
41+
42+
engine.addFact('validTags', ['dev', 'staging', 'load', 'prod'])
43+
44+
let facts
45+
46+
engine
47+
.on('success', event => {
48+
console.log(facts.tags.join(', ') + ' WERE'.green + ' all ' + event.type)
49+
})
50+
.on('failure', event => {
51+
console.log(facts.tags.join(', ') + ' WERE NOT'.red + ' all ' + event.type)
52+
})
53+
54+
// first run with valid tags
55+
facts = { tags: ['dev', 'prod'] }
56+
await engine.run(facts)
57+
58+
// second run with an invalid tag
59+
facts = { tags: ['dev', 'deleted'] }
60+
await engine.run(facts)
61+
62+
// add a new decorator to allow for a case-insensitive match
63+
engine.addOperatorDecorator('caseInsensitive', (factValue, jsonValue, next) => {
64+
return next(factValue.toLowerCase(), jsonValue.toLowerCase())
65+
})
66+
67+
// new rule for case-insensitive validation
68+
const caseInsensitiveValidTags = {
69+
conditions: {
70+
all: [{
71+
fact: 'tags',
72+
// everyFact has someValue that caseInsensitive is equal
73+
operator: 'everyFact:someValue:caseInsensitive:equal',
74+
value: { fact: 'validTags' }
75+
}]
76+
},
77+
event: {
78+
type: 'valid tags (case insensitive)'
79+
}
80+
}
81+
82+
engine.addRule(caseInsensitiveValidTags);
83+
84+
// third run with a tag that is valid if case insensitive
85+
facts = { tags: ['dev', 'PROD'] }
86+
await engine.run(facts);
87+
88+
}
89+
start()
90+
91+
/*
92+
* OUTPUT:
93+
*
94+
* dev, prod WERE all valid tags
95+
* dev, deleted WERE NOT all valid tags
96+
* dev, PROD WERE NOT all valid tags
97+
* dev, PROD WERE all valid tags (case insensitive)
98+
*/

examples/package-lock.json

+41-41
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
'use strict'
2+
3+
import OperatorDecorator from './operator-decorator'
4+
5+
const OperatorDecorators = []
6+
7+
OperatorDecorators.push(new OperatorDecorator('someFact', (factValue, jsonValue, next) => factValue.some(fv => next(fv, jsonValue)), Array.isArray))
8+
OperatorDecorators.push(new OperatorDecorator('someValue', (factValue, jsonValue, next) => jsonValue.some(jv => next(factValue, jv))))
9+
OperatorDecorators.push(new OperatorDecorator('everyFact', (factValue, jsonValue, next) => factValue.every(fv => next(fv, jsonValue)), Array.isArray))
10+
OperatorDecorators.push(new OperatorDecorator('everyValue', (factValue, jsonValue, next) => jsonValue.every(jv => next(factValue, jv))))
11+
OperatorDecorators.push(new OperatorDecorator('swap', (factValue, jsonValue, next) => next(jsonValue, factValue)))
12+
OperatorDecorators.push(new OperatorDecorator('not', (factValue, jsonValue, next) => !next(factValue, jsonValue)))
13+
14+
export default OperatorDecorators

0 commit comments

Comments
 (0)