Skip to content

Condition Sharing #339

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 4 commits into from
Jul 12, 2023
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
72 changes: 72 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.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-)
* [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 @@ -43,6 +45,11 @@ let engine = new Engine([Array rules], options)
an exception is thrown. Turning this option on will cause the engine to treat
undefined facts as `undefined`. (default: false)

`allowUndefinedConditions` - By default, when a running engine encounters a
condition reference that cannot be resolved an exception is thrown. Turning
this option on will cause the engine to treat unresolvable condition references
as failed conditions. (default: false)

`pathResolver` - Allows a custom object path resolution library to be used. (default: `json-path` syntax). See [custom path resolver](./rules.md#condition-helpers-custom-path-resolver) docs.

### engine.addFact(String id, Function [definitionFunc], Object [options])
Expand Down Expand Up @@ -172,6 +179,71 @@ engine.addOperator('startsWithLetter', (factValue, jsonValue) => {
engine.removeOperator('startsWithLetter');
```

### 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.

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

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

```

### engine.removeCondition(String name)

Removes the condition that was previously added.

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

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


### engine.run([Object facts], [Object options]) -> Promise ({ events: [], failureEvents: [], almanac: Almanac, results: [], failureResults: []})
Expand Down
30 changes: 27 additions & 3 deletions docs/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ Rules contain a set of _conditions_ and a single _event_. When the engine is ru
* [toJSON(Boolean stringify = true)](#tojsonboolean-stringify--true)
* [Conditions](#conditions)
* [Basic conditions](#basic-conditions)
* [Boolean expressions: all and any](#boolean-expressions-all-and-any)
* [Boolean expressions: all, any, and not](#boolean-expressions-all-any-and-not)
* [Condition Reference](#condition-reference)
* [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* have an `all` or `any` operator containing an array of conditions at its root, a `not` operator containing a single condition, or a condition reference. 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.

### Condition Reference

Rules may reference conditions based on their name.

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

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

```js
engine.setCondition('conditionName', { /* conditions */ });
```

Conditions must start with `all`, `any`, `not`, or reference a condition.

### Condition helpers: `params`

Expand Down
139 changes: 139 additions & 0 deletions examples/10-condition-sharing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
'use strict'
/*
* This is an advanced example demonstrating rules that re-use a condition defined
* in the engine.
*
* Usage:
* node ./examples/10-condition-sharing.js
*
* For detailed output:
* DEBUG=json-rules-engine node ./examples/10-condition-sharing.js
*/

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

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

/**
* Condition that will be used to determine if a user likes screwdrivers
*/
engine.setCondition('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: [
{
condition: '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: {
condition: '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.
*/
2 changes: 1 addition & 1 deletion examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
"author": "Cache Hamm <cache.hamm@gmail.com>",
"license": "ISC",
"dependencies": {
"json-rules-engine": "6.0.0-alpha-3"
"json-rules-engine": "../"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

}
}
12 changes: 11 additions & 1 deletion src/condition.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default class Condition {
} else {
this[booleanOperator] = new Condition(subConditions)
}
} else {
} else if (!Object.prototype.hasOwnProperty.call(properties, 'condition')) {
if (!Object.prototype.hasOwnProperty.call(properties, 'fact')) throw new Error('Condition: constructor "fact" property required')
if (!Object.prototype.hasOwnProperty.call(properties, 'operator')) throw new Error('Condition: constructor "operator" property required')
if (!Object.prototype.hasOwnProperty.call(properties, 'value')) throw new Error('Condition: constructor "value" property required')
Expand Down Expand Up @@ -54,6 +54,8 @@ export default class Condition {
} else {
props[oper] = this[oper].toJSON(false)
}
} else if (this.isConditionReference()) {
props.condition = this.condition
} else {
props.operator = this.operator
props.value = this.value
Expand Down Expand Up @@ -147,4 +149,12 @@ export default class Condition {
isBooleanOperator () {
return Condition.booleanOperator(this) !== undefined
}

/**
* Whether the condition represents a reference to a condition
* @returns {Boolean}
*/
isConditionReference () {
return Object.prototype.hasOwnProperty.call(this, 'condition')
}
}
28 changes: 28 additions & 0 deletions src/engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Almanac from './almanac'
import EventEmitter from 'eventemitter2'
import defaultOperators from './engine-default-operators'
import debug from './debug'
import Condition from './condition'

export const READY = 'READY'
export const RUNNING = 'RUNNING'
Expand All @@ -21,9 +22,11 @@ class Engine extends EventEmitter {
super()
this.rules = []
this.allowUndefinedFacts = options.allowUndefinedFacts || false
this.allowUndefinedConditions = options.allowUndefinedConditions || false
this.pathResolver = options.pathResolver
this.operators = new Map()
this.facts = new Map()
this.conditions = new Map()
this.status = READY
rules.map(r => this.addRule(r))
defaultOperators.map(o => this.addOperator(o))
Expand Down Expand Up @@ -92,6 +95,31 @@ class Engine extends EventEmitter {
return ruleRemoved
}

/**
* sets a condition that can be referenced by the given name.
* If a condition with the given name has already been set this will replace it.
* @param {string} name - the name of the condition to be referenced by rules.
* @param {object} conditions - the conditions to use when the condition is referenced.
*/
setCondition (name, conditions) {
if (!name) throw new Error('Engine: setCondition() requires name')
if (!conditions) throw new Error('Engine: setCondition() requires conditions')
if (!Object.prototype.hasOwnProperty.call(conditions, 'all') && !Object.prototype.hasOwnProperty.call(conditions, 'any') && !Object.prototype.hasOwnProperty.call(conditions, 'not') && !Object.prototype.hasOwnProperty.call(conditions, 'condition')) {
throw new Error('"conditions" root must contain a single instance of "all", "any", "not", or "condition"')
}
this.conditions.set(name, new Condition(conditions))
return this
}

/**
* Removes a condition that has previously been added to this engine
* @param {string} name - the name of the condition to remove.
* @returns true if the condition existed, otherwise false
*/
removeCondition (name) {
return this.conditions.delete(name)
}

/**
* Add a custom operator definition
* @param {string} operatorOrName - operator identifier within the condition; i.e. instead of 'equals', 'greaterThan', etc
Expand Down
Loading