Skip to content

Operator decorators #376

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading