Skip to content

Commit

Permalink
Merge pull request #376 from chris-pardy/operator-decorators
Browse files Browse the repository at this point in the history
Operator decorators
  • Loading branch information
chris-pardy authored Oct 15, 2024
2 parents b75acfd + 87afb2f commit 194ebdb
Show file tree
Hide file tree
Showing 14 changed files with 614 additions and 75 deletions.
58 changes: 58 additions & 0 deletions docs/engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ The Engine stores and executes rules, emits events, and maintains state.
* [engine.removeRule(Rule instance | String ruleId)](#engineremoverulerule-instance)
* [engine.addOperator(String operatorName, Function evaluateFunc(factValue, jsonValue))](#engineaddoperatorstring-operatorname-function-evaluatefuncfactvalue-jsonvalue)
* [engine.removeOperator(String operatorName)](#engineremoveoperatorstring-operatorname)
* [engine.addOperatorDecorator(String decoratorName, Function evaluateFunc(factValue, jsonValue, next))](#engineaddoperatordecoratorstring-decoratorname-function-evaluatefuncfactvalue-jsonvalue-next)
* [engine.removeOperatorDecorator(String decoratorName)](#engineremoveoperatordecoratorstring-decoratorname)
* [engine.setCondition(String name, Object conditions)](#enginesetconditionstring-name-object-conditions)
* [engine.removeCondition(String name)](#engineremovecondtionstring-name)
* [engine.run([Object facts], [Object options]) -> Promise ({ events: [], failureEvents: [], almanac: Almanac, results: [], failureResults: []})](#enginerunobject-facts-object-options---promise--events--failureevents--almanac-almanac-results--failureresults-)
Expand Down Expand Up @@ -181,6 +183,62 @@ engine.addOperator('startsWithLetter', (factValue, jsonValue) => {
engine.removeOperator('startsWithLetter');
```

### engine.addOperatorDecorator(String decoratorName, Function evaluateFunc(factValue, jsonValue, next))

Adds a custom operator decorator to the engine.

```js
/*
* decoratorName - operator decorator identifier used in the rule condition
* evaluateFunc(factValue, jsonValue, next) - uses the decorated operator to compare the fact result to the condition 'value'
* factValue - the value returned from the fact
* jsonValue - the "value" property stored in the condition itself
* next - the evaluateFunc of the decorated operator
*/
engine.addOperatorDecorator('first', (factValue, jsonValue, next) => {
if (!factValue.length) return false
return next(factValue[0], jsonValue)
})

engine.addOperatorDecorator('caseInsensitive', (factValue, jsonValue, next) => {
return next(factValue.toLowerCase(), jsonValue.toLowerCase())
})

// and to use the decorator...
let rule = new Rule(
conditions: {
all: [
{
fact: 'username',
operator: 'first:caseInsensitive:equal', // reference the decorator:operator in the rule
value: 'a'
}
]
}
)
```

See the [operator decorator example](../examples/13-using-operator-decorators.js)



### engine.removeOperatorDecorator(String decoratorName)

Removes a operator decorator from the engine

```javascript
engine.addOperatorDecorator('first', (factValue, jsonValue, next) => {
if (!factValue.length) return false
return next(factValue[0], jsonValue)
})

engine.addOperatorDecorator('caseInsensitive', (factValue, jsonValue, next) => {
return next(factValue.toLowerCase(), jsonValue.toLowerCase())
})

engine.removeOperator('first');
```

### engine.setCondition(String name, Object conditions)

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.
Expand Down
34 changes: 34 additions & 0 deletions docs/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ Rules contain a set of _conditions_ and a single _event_. When the engine is ru
* [String and Numeric operators:](#string-and-numeric-operators)
* [Numeric operators:](#numeric-operators)
* [Array operators:](#array-operators)
* [Operator Decorators](#operator-decorators)
* [Array decorators:](#array-decorators)
* [Logical decorators:](#logical-decorators)
* [Utility decorators:](#utility-decorators)
* [Decorator composition:](#decorator-composition)
* [Rule Results](#rule-results)
* [Persisting](#persisting)

Expand Down Expand Up @@ -406,6 +411,35 @@ The ```operator``` compares the value returned by the ```fact``` to what is stor
```doesNotContain``` - _fact_ (an array) must not include _value_
## Operator Decorators
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.
See [12-using-operator-decorators.js](../examples/13-using-operator-decorators.js) for an example.
### Array Decorators:
```everyFact``` - _fact_ (an array) must have every element pass the decorated operator for _value_
```everyValue``` - _fact_ must pass the decorated operator for every element of _value_ (an array)
```someFact``` - _fact_ (an array) must have at-least one element pass the decorated operator for _value_
```someValue``` - _fact_ must pass the decorated operator for at-least one element of _value_ (an array)
### Logical Decorators
```not``` - negate the result of the decorated operator
### Utility Decorators
```swap``` - Swap _fact_ and _value_ for the decorated operator
### Decorator Composition
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```.
```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.
## Rule Results
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.
Expand Down
98 changes: 98 additions & 0 deletions examples/13-using-operator-decorators.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
'use strict'
/*
* This example demonstrates using operator decorators.
*
* In this example, a fact contains a list of strings and we want to check if any of these are valid.
*
* Usage:
* node ./examples/12-using-operator-decorators.js
*
* For detailed output:
* DEBUG=json-rules-engine node ./examples/12-using-operator-decorators.js
*/

require('colors')
const { Engine } = require('json-rules-engine')

async function start () {
/**
* Setup a new engine
*/
const engine = new Engine()

/**
* Add a rule for validating a tag (fact)
* against a set of tags that are valid (also a fact)
*/
const validTags = {
conditions: {
all: [{
fact: 'tags',
operator: 'everyFact:in',
value: { fact: 'validTags' }
}]
},
event: {
type: 'valid tags'
}
}

engine.addRule(validTags)

engine.addFact('validTags', ['dev', 'staging', 'load', 'prod'])

let facts

engine
.on('success', event => {
console.log(facts.tags.join(', ') + ' WERE'.green + ' all ' + event.type)
})
.on('failure', event => {
console.log(facts.tags.join(', ') + ' WERE NOT'.red + ' all ' + event.type)
})

// first run with valid tags
facts = { tags: ['dev', 'prod'] }
await engine.run(facts)

// second run with an invalid tag
facts = { tags: ['dev', 'deleted'] }
await engine.run(facts)

// add a new decorator to allow for a case-insensitive match
engine.addOperatorDecorator('caseInsensitive', (factValue, jsonValue, next) => {
return next(factValue.toLowerCase(), jsonValue.toLowerCase())
})

// new rule for case-insensitive validation
const caseInsensitiveValidTags = {
conditions: {
all: [{
fact: 'tags',
// everyFact has someValue that caseInsensitive is equal
operator: 'everyFact:someValue:caseInsensitive:equal',
value: { fact: 'validTags' }
}]
},
event: {
type: 'valid tags (case insensitive)'
}
}

engine.addRule(caseInsensitiveValidTags);

Check failure on line 82 in examples/13-using-operator-decorators.js

View workflow job for this annotation

GitHub Actions / build (14.x)

Extra semicolon

Check failure on line 82 in examples/13-using-operator-decorators.js

View workflow job for this annotation

GitHub Actions / build (14.x)

Extra semicolon

Check failure on line 82 in examples/13-using-operator-decorators.js

View workflow job for this annotation

GitHub Actions / build (16.x)

Extra semicolon

Check failure on line 82 in examples/13-using-operator-decorators.js

View workflow job for this annotation

GitHub Actions / build (16.x)

Extra semicolon

Check failure on line 82 in examples/13-using-operator-decorators.js

View workflow job for this annotation

GitHub Actions / build (18.x)

Extra semicolon

Check failure on line 82 in examples/13-using-operator-decorators.js

View workflow job for this annotation

GitHub Actions / build (18.x)

Extra semicolon

// third run with a tag that is valid if case insensitive
facts = { tags: ['dev', 'PROD'] }
await engine.run(facts);

Check failure on line 86 in examples/13-using-operator-decorators.js

View workflow job for this annotation

GitHub Actions / build (14.x)

Extra semicolon

Check failure on line 86 in examples/13-using-operator-decorators.js

View workflow job for this annotation

GitHub Actions / build (14.x)

Block must not be padded by blank lines

Check failure on line 86 in examples/13-using-operator-decorators.js

View workflow job for this annotation

GitHub Actions / build (14.x)

Extra semicolon

Check failure on line 86 in examples/13-using-operator-decorators.js

View workflow job for this annotation

GitHub Actions / build (14.x)

Block must not be padded by blank lines

Check failure on line 86 in examples/13-using-operator-decorators.js

View workflow job for this annotation

GitHub Actions / build (16.x)

Extra semicolon

Check failure on line 86 in examples/13-using-operator-decorators.js

View workflow job for this annotation

GitHub Actions / build (16.x)

Block must not be padded by blank lines

Check failure on line 86 in examples/13-using-operator-decorators.js

View workflow job for this annotation

GitHub Actions / build (16.x)

Extra semicolon

Check failure on line 86 in examples/13-using-operator-decorators.js

View workflow job for this annotation

GitHub Actions / build (16.x)

Block must not be padded by blank lines

Check failure on line 86 in examples/13-using-operator-decorators.js

View workflow job for this annotation

GitHub Actions / build (18.x)

Extra semicolon

Check failure on line 86 in examples/13-using-operator-decorators.js

View workflow job for this annotation

GitHub Actions / build (18.x)

Block must not be padded by blank lines

Check failure on line 86 in examples/13-using-operator-decorators.js

View workflow job for this annotation

GitHub Actions / build (18.x)

Extra semicolon

Check failure on line 86 in examples/13-using-operator-decorators.js

View workflow job for this annotation

GitHub Actions / build (18.x)

Block must not be padded by blank lines

}
start()

/*
* OUTPUT:
*
* dev, prod WERE all valid tags
* dev, deleted WERE NOT all valid tags
* dev, PROD WERE NOT all valid tags
* dev, PROD WERE all valid tags (case insensitive)
*/
82 changes: 41 additions & 41 deletions examples/package-lock.json

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

14 changes: 14 additions & 0 deletions src/engine-default-operator-decorators.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use strict'

import OperatorDecorator from './operator-decorator'

const OperatorDecorators = []

OperatorDecorators.push(new OperatorDecorator('someFact', (factValue, jsonValue, next) => factValue.some(fv => next(fv, jsonValue)), Array.isArray))
OperatorDecorators.push(new OperatorDecorator('someValue', (factValue, jsonValue, next) => jsonValue.some(jv => next(factValue, jv))))
OperatorDecorators.push(new OperatorDecorator('everyFact', (factValue, jsonValue, next) => factValue.every(fv => next(fv, jsonValue)), Array.isArray))
OperatorDecorators.push(new OperatorDecorator('everyValue', (factValue, jsonValue, next) => jsonValue.every(jv => next(factValue, jv))))
OperatorDecorators.push(new OperatorDecorator('swap', (factValue, jsonValue, next) => next(jsonValue, factValue)))
OperatorDecorators.push(new OperatorDecorator('not', (factValue, jsonValue, next) => !next(factValue, jsonValue)))

export default OperatorDecorators
Loading

0 comments on commit 194ebdb

Please sign in to comment.