Skip to content

Commit

Permalink
Add documentation around shared conditions
Browse files Browse the repository at this point in the history
Add documentation covering the use and
advantages of shared conditions. Specifically on
the ability to re-use the same condition across multiple
rules.
  • Loading branch information
Christopher Pardy committed Jul 10, 2023
1 parent b19cff2 commit 2e4ec69
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 2 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ A rules engine expressed in JSON

* Rules expressed in simple, easy to read JSON
* Full support for ```ALL``` and ```ANY``` boolean operators, including recursive nesting
* Rule composition / inheritance with the use of shared conditions
* Fast by default, faster with configuration; priority levels and cache settings for fine tuning performance
* Secure; no use of eval()
* Isomorphic; runs in node and browser
Expand Down
67 changes: 67 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.addSharedCondition(String name, Object conditions)](#engineaddsharedconditionsstring-name-object-conditions)
* [engine.removeSharedCondition(String name)](#engineremovesharedcondtionsstring-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-)
* [engine.stop() -> Engine](#enginestop---engine)
* [engine.on('success', Function(Object event, Almanac almanac, RuleResult ruleResult))](#engineonsuccess-functionobject-event-almanac-almanac-ruleresult-ruleresult)
Expand Down Expand Up @@ -172,6 +174,71 @@ engine.addOperator('startsWithLetter', (factValue, jsonValue) => {
engine.removeOperator('startsWithLetter');
```

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

Adds a shared condition to the engine. Rules may include references to this shared condition.

```javascript
engine.addSharedCondition('validLogin', {
all: [
{
operator: 'notEqual',
fact: 'loginToken',
value: null
},
{
operator: 'greaterThan',
fact: 'loginToken',
path: '$.expirationTime',
value: { fact: 'now' }
}
]
});

engine.addRule({
condtions: {
all: [
{
uses: 'validLogin'
},
{
operator: 'contains',
fact: 'loginToken',
path: '$.role',
value: 'admin'
}
]
},
event: {
type: 'AdminAccessAllowed'
}
})

```

### engine.removeSharedCondition(String name)

Removes the shared condition that was previously added.

```javascript
engine.addSharedCondition('validLogin', {
all: [
{
operator: 'notEqual',
fact: 'loginToken',
value: null
},
{
operator: 'greaterThan',
fact: 'loginToken',
path: '$.expirationTime',
value: { fact: 'now' }
}
]
});

engine.removeSharedCondition('validLogin');
```


### engine.run([Object facts], [Object options]) -> Promise ({ events: [], failureEvents: [], almanac: Almanac, results: [], failureResults: []})
Expand Down
28 changes: 26 additions & 2 deletions docs/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Rules contain a set of _conditions_ and a single _event_. When the engine is ru
* [Conditions](#conditions)
* [Basic conditions](#basic-conditions)
* [Boolean expressions: all, any, and not](#boolean-expressions-all-any-and-not)
* [Shared conditions](#shared-conditions)
* [Condition helpers: params](#condition-helpers-params)
* [Condition helpers: path](#condition-helpers-path)
* [Condition helpers: custom path resolver](#condition-helpers-custom-path-resolver)
Expand Down Expand Up @@ -136,7 +137,7 @@ See the [hello-world](../examples/01-hello-world.js) example.

### Boolean expressions: `all`, `any`, and `not`

Each rule's conditions *must* have an `all` or `any` operator containing an array of conditions at its root or a `not` operator containing a single condition. The `all` operator specifies that all conditions contained within must be truthy for the rule to be considered a `success`. The `any` operator only requires one condition to be truthy for the rule to succeed. The `not` operator will negate whatever condition it contains.
Each rule's conditions *must* reference a shared condition or have an `all` or `any` operator containing an array of conditions at its root or a `not` operator containing a single condition. The `all` operator specifies that all conditions contained within must be truthy for the rule to be considered a `success`. The `any` operator only requires one condition to be truthy for the rule to succeed. The `not` operator will negate whatever condition it contains.

```js
// all:
Expand Down Expand Up @@ -174,7 +175,30 @@ let rule = new Rule({
})
```

Notice in the second example how `all`, `any`, and 'not' can be nested within one another to produce complex boolean expressions. See the [nested-boolean-logic](../examples/02-nested-boolean-logic.js) example.
Notice in the second example how `all`, `any`, and `not` can be nested within one another to produce complex boolean expressions. See the [nested-boolean-logic](../examples/02-nested-boolean-logic.js) example.

### Shared Conditions

Rules my reference shared conditions based on their name.

```js
let rule = new Rule({
conditions: {
all: [
{ uses: 'sharedCondition' },
{ /* additional condition */ }
]
}
})
```

Before running the rule the shared condition should be added to the engine.

```js
engine.addSharedCondition('sharedCondition', { /* conditions */ });
```

Shared conditions must start with `all`, `any`, `not`, or reference a shared condition.

### Condition helpers: `params`

Expand Down
140 changes: 140 additions & 0 deletions examples/10-shared-conditions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
'use strict'
/*
* This is an advanced example demonstrating rules that passed based off the
* results of other rules by adding runtime facts. It also demonstrates
* accessing the runtime facts after engine execution.
*
* Usage:
* node ./examples/10-shared-conditions.js
*
* For detailed output:
* DEBUG=json-rules-engine node ./examples/10-shared-conditions.js
*/

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

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

/**
* Shared condition that will be used to determine if a user likes screwdrivers
*/
engine.addSharedCondition('screwdriverAficionado', {
all: [
{
fact: 'drinksOrangeJuice',
operator: 'equal',
value: true
},
{
fact: 'enjoysVodka',
operator: 'equal',
value: true
}
]
})

/**
* Rule for identifying people who should be invited to a screwdriver social
* - Only invite people who enjoy screw drivers
* - Only invite people who are sociable
*/
const inviteRule = {
conditions: {
all: [
{
uses: 'screwdriverAficionado'
},
{
fact: 'isSociable',
operator: 'equal',
value: true
}
]
},
event: { type: 'invite-to-screwdriver-social' }
}
engine.addRule(inviteRule)

/**
* Rule for identifying people who should be invited to the other social
* - Only invite people who don't enjoy screw drivers
* - Only invite people who are sociable
*/
const otherInviteRule = {
conditions: {
all: [
{
not: {
uses: 'screwdriverAficionado'
}
},
{
fact: 'isSociable',
operator: 'equal',
value: true
}
]
},
event: { type: 'invite-to-other-social' }
}
engine.addRule(otherInviteRule)

/**
* Register listeners with the engine for rule success and failure
*/
engine
.on('success', async (event, almanac) => {
const accountId = await almanac.factValue('accountId')
console.log(
`${accountId}` +
'DID'.green +
` meet conditions for the ${event.type.underline} rule.`
)
})
.on('failure', async (event, almanac) => {
const accountId = await almanac.factValue('accountId')
console.log(
`${accountId} did ` +
'NOT'.red +
` meet conditions for the ${event.type.underline} rule.`
)
})

// define fact(s) known at runtime
let facts = {
accountId: 'washington',
drinksOrangeJuice: true,
enjoysVodka: true,
isSociable: true
}

// first run, using washington's facts
await engine.run(facts)

facts = {
accountId: 'jefferson',
drinksOrangeJuice: true,
enjoysVodka: false,
isSociable: true,
accountInfo: {}
}

// second run, using jefferson's facts; facts & evaluation are independent of the first run
await engine.run(facts)
}

start()

/*
* OUTPUT:
*
* washington DID meet conditions for the invite-to-screwdriver-social rule.
* washington did NOT meet conditions for the invite-to-other-social rule.
* jefferson did NOT meet conditions for the invite-to-screwdriver-social rule.
* jefferson DID meet conditions for the invite-to-other-social rule.
*/

0 comments on commit 2e4ec69

Please sign in to comment.