Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: CacheControl/json-rules-engine
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v6.4.0
Choose a base ref
...
head repository: CacheControl/json-rules-engine
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref
Loading
4 changes: 2 additions & 2 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ name: Deploy Package

on:
push:
branches: [ master ]
branches: [ master, v6 ]

jobs:
build:
@@ -14,7 +14,7 @@ jobs:

strategy:
matrix:
node-version: [14.x, 16.x, 18.x]
node-version: [18.x, 20.x, 22.x]

steps:
- uses: actions/checkout@v2
4 changes: 2 additions & 2 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ name: Node.js CI

on:
pull_request:
branches: [ master, next-major ]
branches: [ master, v6 ]

jobs:
build:
@@ -14,7 +14,7 @@ jobs:

strategy:
matrix:
node-version: [14.x, 16.x, 18.x]
node-version: [18.x, 20.x, 22.x]

steps:
- uses: actions/checkout@v2
21 changes: 21 additions & 0 deletions docs/almanac.md
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@
* [Overview](#overview)
* [Methods](#methods)
* [almanac.factValue(Fact fact, Object params, String path) -> Promise](#almanacfactvaluefact-fact-object-params-string-path---promise)
* [almanac.addFact(String id, Function [definitionFunc], Object [options])](#almanacaddfactstring-id-function-definitionfunc-object-options)
* [almanac.addRuntimeFact(String factId, Mixed value)](#almanacaddruntimefactstring-factid-mixed-value)
* [almanac.getEvents(String outcome) -> Events[]](#almanacgeteventsstring-outcome---events)
* [almanac.getResults() -> RuleResults[]](#almanacgetresults---ruleresults)
@@ -33,8 +34,28 @@ almanac
.then( value => console.log(value))
```

### almanac.addFact(String id, Function [definitionFunc], Object [options])

Sets a fact in the almanac. Used in conjunction with rule and engine event emissions.

```js
// constant facts:
engine.addFact('speed-of-light', 299792458)

// facts computed via function
engine.addFact('account-type', function getAccountType(params, almanac) {
// ...
})

// facts with options:
engine.addFact('account-type', function getAccountType(params, almanac) {
// ...
}, { cache: false, priority: 500 })
```

### almanac.addRuntimeFact(String factId, Mixed value)

**Deprecated** Use `almanac.addFact` instead
Sets a constant fact mid-run. Often used in conjunction with rule and engine event emissions.

```js
68 changes: 68 additions & 0 deletions docs/engine.md
Original file line number Diff line number Diff line change
@@ -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-)
@@ -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.removeOperatorDecorator('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.
@@ -269,6 +327,16 @@ const {
```
Link to the [Almanac documentation](./almanac.md)

Optionally, you may specify a specific almanac instance via the almanac property.

```js
// create a custom Almanac
const myCustomAlmanac = new CustomAlmanac();

// run the engine with the custom almanac
await engine.run({}, { almanac: myCustomAlmanac })
```

### engine.stop() -> Engine

Stops the rules engine from running the next priority set of Rules. All remaining rules will be resolved as undefined,
34 changes: 34 additions & 0 deletions docs/rules.md
Original file line number Diff line number Diff line change
@@ -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)

@@ -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.
6 changes: 3 additions & 3 deletions examples/07-rule-chaining.js
Original file line number Diff line number Diff line change
@@ -39,16 +39,16 @@ async function start () {
event: { type: 'drinks-screwdrivers' },
priority: 10, // IMPORTANT! Set a higher priority for the drinkRule, so it runs first
onSuccess: async function (event, almanac) {
almanac.addRuntimeFact('screwdriverAficionado', true)
almanac.addFact('screwdriverAficionado', true)

// asychronous operations can be performed within callbacks
// engine execution will not proceed until the returned promises is resolved
const accountId = await almanac.factValue('accountId')
const accountInfo = await getAccountInformation(accountId)
almanac.addRuntimeFact('accountInfo', accountInfo)
almanac.addFact('accountInfo', accountInfo)
},
onFailure: function (event, almanac) {
almanac.addRuntimeFact('screwdriverAficionado', false)
almanac.addFact('screwdriverAficionado', false)
}
}
engine.addRule(drinkRule)
94 changes: 94 additions & 0 deletions examples/12-using-custom-almanac.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
'use strict'

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

/**
* Almanac that support piping values through named functions
*/
class PipedAlmanac extends Almanac {
constructor (options) {
super(options)
this.pipes = new Map()
}

addPipe (name, pipe) {
this.pipes.set(name, pipe)
}

factValue (factId, params, path) {
let pipes = []
if (params && 'pipes' in params && Array.isArray(params.pipes)) {
pipes = params.pipes
delete params.pipes
}
return super.factValue(factId, params, path).then(value => {
return pipes.reduce((value, pipeName) => {
const pipe = this.pipes.get(pipeName)
if (pipe) {
return pipe(value)
}
return value
}, value)
})
}
}

async function start () {
const engine = new Engine()
.addRule({
conditions: {
all: [
{
fact: 'age',
params: {
// the addOne pipe adds one to the value
pipes: ['addOne']
},
operator: 'greaterThanInclusive',
value: 21
}
]
},
event: {
type: 'Over 21(ish)'
}
})

engine.on('success', async (event, almanac) => {
const name = await almanac.factValue('name')
const age = await almanac.factValue('age')
console.log(`${name} is ${age} years old and ${'is'.green} ${event.type}`)
})

engine.on('failure', async (event, almanac) => {
const name = await almanac.factValue('name')
const age = await almanac.factValue('age')
console.log(`${name} is ${age} years old and ${'is not'.red} ${event.type}`)
})

const createAlmanacWithPipes = () => {
const almanac = new PipedAlmanac()
almanac.addPipe('addOne', (v) => v + 1)
return almanac
}

// first run Bob who is less than 20
await engine.run({ name: 'Bob', age: 19 }, { almanac: createAlmanacWithPipes() })

// second run Alice who is 21
await engine.run({ name: 'Alice', age: 21 }, { almanac: createAlmanacWithPipes() })

// third run Chad who is 20
await engine.run({ name: 'Chad', age: 20 }, { almanac: createAlmanacWithPipes() })
}

start()

/*
* OUTPUT:
*
* Bob is 19 years old and is not Over 21(ish)
* Alice is 21 years old and is Over 21(ish)
* Chad is 20 years old and is Over 21(ish)
*/
Loading