Skip to content

Commit 92c8c51

Browse files
Add Support for Operator Decorators
Operator Decorators "decorate" an operator and return a new operator. Create some defaults including: - someFact -> for fact values that are arrays return if one of the values matches the decorated operator - someValue -> same but for json values that are arrays - everyFact - everyValue - not
1 parent d008b88 commit 92c8c51

12 files changed

+475
-71
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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+
start()
63+
64+
/*
65+
* OUTPUT:
66+
*
67+
* dev, prod WERE all valid tags
68+
* dev, deleted WERE NOT all valid tags
69+
*/

examples/package-lock.json

Lines changed: 41 additions & 41 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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('not', (factValue, jsonValue, next) => !next(factValue, jsonValue)))
12+
13+
export default OperatorDecorators

src/engine.js

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22

33
import Fact from './fact'
44
import Rule from './rule'
5-
import Operator from './operator'
65
import Almanac from './almanac'
76
import EventEmitter from 'eventemitter2'
87
import defaultOperators from './engine-default-operators'
8+
import defaultDecorators from './engine-default-operator-decorators'
99
import debug from './debug'
1010
import Condition from './condition'
11+
import OperatorMap from './operator-map'
1112

1213
export const READY = 'READY'
1314
export const RUNNING = 'RUNNING'
@@ -25,12 +26,13 @@ class Engine extends EventEmitter {
2526
this.allowUndefinedConditions = options.allowUndefinedConditions || false
2627
this.replaceFactsInEventParams = options.replaceFactsInEventParams || false
2728
this.pathResolver = options.pathResolver
28-
this.operators = new Map()
29+
this.operators = new OperatorMap()
2930
this.facts = new Map()
3031
this.conditions = new Map()
3132
this.status = READY
3233
rules.map(r => this.addRule(r))
3334
defaultOperators.map(o => this.addOperator(o))
35+
defaultDecorators.map(d => this.addOperatorDecorator(d))
3436
}
3537

3638
/**
@@ -127,30 +129,32 @@ class Engine extends EventEmitter {
127129
* @param {function(factValue, jsonValue)} callback - the method to execute when the operator is encountered.
128130
*/
129131
addOperator (operatorOrName, cb) {
130-
let operator
131-
if (operatorOrName instanceof Operator) {
132-
operator = operatorOrName
133-
} else {
134-
operator = new Operator(operatorOrName, cb)
135-
}
136-
debug(`engine::addOperator name:${operator.name}`)
137-
this.operators.set(operator.name, operator)
132+
this.operators.addOperator(operatorOrName, cb)
138133
}
139134

140135
/**
141136
* Remove a custom operator definition
142137
* @param {string} operatorOrName - operator identifier within the condition; i.e. instead of 'equals', 'greaterThan', etc
143-
* @param {function(factValue, jsonValue)} callback - the method to execute when the operator is encountered.
144138
*/
145139
removeOperator (operatorOrName) {
146-
let operatorName
147-
if (operatorOrName instanceof Operator) {
148-
operatorName = operatorOrName.name
149-
} else {
150-
operatorName = operatorOrName
151-
}
140+
return this.operators.removeOperator(operatorOrName)
141+
}
142+
143+
/**
144+
* Add a custom operator definition
145+
* @param {string} decoratorOrName - operator identifier within the condition; i.e. instead of 'equals', 'greaterThan', etc
146+
* @param {function(factValue, jsonValue, next)} callback - the method to execute when the decorator is encountered.
147+
*/
148+
addOperatorDecorator (decoratorOrName, cb) {
149+
this.operators.addOperatorDecorator(decoratorOrName, cb)
150+
}
152151

153-
return this.operators.delete(operatorName)
152+
/**
153+
* Remove a custom operator definition
154+
* @param {string} decoratorOrName - operator identifier within the condition; i.e. instead of 'equals', 'greaterThan', etc
155+
*/
156+
removeOperatorDecorator (decoratorOrName) {
157+
return this.operators.removeOperatorDecorator(decoratorOrName)
154158
}
155159

156160
/**

src/json-rules-engine.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import Fact from './fact'
33
import Rule from './rule'
44
import Operator from './operator'
55
import Almanac from './almanac'
6+
import OperatorDecorator from './operator-decorator'
67

7-
export { Fact, Rule, Operator, Engine, Almanac }
8+
export { Fact, Rule, Operator, Engine, Almanac, OperatorDecorator }
89
export default function (rules, options) {
910
return new Engine(rules, options)
1011
}

src/operator-decorator.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
'use strict'
2+
3+
import Operator from './operator'
4+
5+
export default class OperatorDecorator {
6+
/**
7+
* Constructor
8+
* @param {string} name - decorator identifier
9+
* @param {function(factValue, jsonValue, next)} callback - callback that takes the next operator as a parameter
10+
* @param {function} [factValueValidator] - optional validator for asserting the data type of the fact
11+
* @returns {OperatorDecorator} - instance
12+
*/
13+
constructor (name, cb, factValueValidator) {
14+
this.name = String(name)
15+
if (!name) throw new Error('Missing decorator name')
16+
if (typeof cb !== 'function') throw new Error('Missing decorator callback')
17+
this.cb = cb
18+
this.factValueValidator = factValueValidator
19+
if (!this.factValueValidator) this.factValueValidator = () => true
20+
}
21+
22+
/**
23+
* Takes the fact result and compares it to the condition 'value', using the callback
24+
* @param {Operator} operator - fact result
25+
* @returns {Operator} - whether the values pass the operator test
26+
*/
27+
decorate (operator) {
28+
const next = operator.evaluate.bind(operator)
29+
return new Operator(
30+
`${this.name}:${operator.name}`,
31+
(factValue, jsonValue) => {
32+
return this.cb(factValue, jsonValue, next)
33+
},
34+
this.factValueValidator
35+
)
36+
}
37+
}

0 commit comments

Comments
 (0)