Skip to content

Commit

Permalink
import lodash // new Rule() // exposing { Rools, Rule } // new featur…
Browse files Browse the repository at this point in the history
…e `Rule.extend`
  • Loading branch information
frankthelen committed Jan 21, 2018
1 parent 0d4ae07 commit acd5d61
Show file tree
Hide file tree
Showing 28 changed files with 555 additions and 305 deletions.
113 changes: 66 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# rools

This is a small rule engine for Node.
A small rule engine for Node.

[![build status](https://img.shields.io/travis/frankthelen/rools.svg)](http://travis-ci.org/frankthelen/rools)
[![Coverage Status](https://coveralls.io/repos/github/frankthelen/rools/badge.svg?branch=master)](https://coveralls.io/github/frankthelen/rools?branch=master)
Expand Down Expand Up @@ -32,8 +32,8 @@ npm install --save rools
This is a basic example.

```javascript
// import Rools
const Rools = require('rools');
// import
const Rools, Rule } = require('rools');

// facts
const facts = {
Expand All @@ -49,14 +49,14 @@ const facts = {
};

// rules
const ruleMoodGreat = {
const ruleMoodGreat = new Rule({
name: 'mood is great if 200 stars or more',
when: facts => facts.user.stars >= 200,
then: (facts) => {
facts.user.mood = 'great';
},
};
const ruleGoWalking = {
});
const ruleGoWalking = new Rule({
name: 'go for a walk if mood is great and the weather is fine',
when: [
facts => facts.user.mood === 'great',
Expand All @@ -66,7 +66,7 @@ const ruleGoWalking = {
then: (facts) => {
facts.goWalking = true;
},
};
});

// evaluation
const rools = new Rools();
Expand All @@ -91,7 +91,8 @@ The engine does forward-chaining and works in the usual match-resolve-act cycle.

Facts are plain JavaScript or JSON objects or objects from ES6 classes with getters and setters.

Rules are specified in pure JavaScript, i.e., they have premises (`when`) and actions (`then`).
Rules are specified in pure JavaScript via `new Rule()`.
They have premises (`when`) and actions (`then`).
Both are JavaScript functions, i.e., classic functions or ES6 arrow functions.
Actions can also be asynchronous.

Expand Down Expand Up @@ -120,26 +121,26 @@ actions (`then`) can be synchronous or asynchronous.

Example: asynchronous action using async/await
```javascript
const rule = {
const rule = new Rule({
name: 'check availability',
when: facts => facts.user.address.country === 'germany',
then: async (facts) => {
facts.products = await availabilityCheck(facts.user.address);
},
};
});
```

Example: asynchronous action using promises
```javascript
const rule = {
const rule = new Rule({
name: 'check availability',
when: facts => facts.user.address.country === 'germany',
then: facts =>
availabilityCheck(facts.user.address)
.then((result) => {
facts.products = result;
}),
};
});
```

### Optimization I
Expand All @@ -152,57 +153,57 @@ Both options are working fine.
Example 1: by reference
```javascript
const isApplicable = facts => facts.user.salery >= 2000;
const rule1 = {
const rule1 = new Rule({
when: [
isApplicable,
...
],
...
};
const rule2 = {
});
const rule2 = new Rule({
when: [
isApplicable,
...
],
...
};
});
```

Example 2: repeat premise
```javascript
const rule1 = {
const rule1 = new Rule({
when: [
facts => facts.user.salery >= 2000,
...
],
...
};
const rule2 = {
});
const rule2 = new Rule({
when: [
facts => facts.user.salery >= 2000,
...
],
...
};
});
```

Furthermore, it is recommended to de-compose premises with AND relations (`&&`).
For example:

```javascript
// this version works...
const rule = {
const rule = new Rule({
when: facts => facts.user.salery >= 2000 && facts.user.age > 25,
...
};
});
// however, it's better to write it like this...
const rule = {
const rule = new Rule({
when: [
facts => facts.user.salery >= 2000,
facts => facts.user.age > 25,
],
...
};
});
```

One last thing. Look at the example below.
Expand All @@ -212,15 +213,15 @@ Later on, at evaluation time (`evaluate()`), both rules are clearly identical.

```javascript
let value = 2000;
const rule1 = {
const rule1 = new Rule({
when: facts => facts.user.salery >= value,
...
};
});
value = 3000;
const rule2 = {
const rule2 = new Rule({
when: facts => facts.user.salery >= value,
...
};
});
```

*TL;DR* -- Technically, this is achieved by hashing the premise functions (remember, functions are "first-class" objects in JavaScript). This can be a classic function or an ES6 arrow function; it can be a reference or the function directly.
Expand Down Expand Up @@ -260,14 +261,14 @@ You usually do this once for a given set of rules.

Example:
```javascript
const Rools = require('rools');
const Rools } = require('rools');
const rools = new Rools();
...
```

### Register rules: `register()`

Rules are plain JavaScript objects with the following properties:
Rules are created through `new Rule()` with the following properties:

| Property | Required | Default | Description |
|-------------|----------|---------|-------------|
Expand All @@ -291,14 +292,15 @@ If this happens, the affected Rools instance is inconsistent and should no longe

Example:
```javascript
const ruleMoodGreat = {
constRools, Rule } = require('rools');
const ruleMoodGreat = new Rule({
name: 'mood is great if 200 stars or more',
when: facts => facts.user.stars >= 200,
then: (facts) => {
facts.user.mood = 'great';
},
};
const ruleGoWalking = {
});
const ruleGoWalking = new Rule({
name: 'go for a walk if mood is great and the weather is fine',
when: [
facts => facts.user.mood === 'great',
Expand All @@ -308,7 +310,7 @@ const ruleGoWalking = {
then: (facts) => {
facts.goWalking = true;
},
};
});
const rools = new Rools();
await rools.register([ruleMoodGreat, ruleGoWalking]);
```
Expand Down Expand Up @@ -366,31 +368,48 @@ const rools = new Rools({

### Version 1.x.x to Version 2.x.x

There are two breaking changes that require some little changes to your code.
There are a few breaking changes that require changes to your code.

`register()` takes the rules to register as an array now.
Reason is to allow a second options parameter in future releases.
Rools exposes now two classes, `Rools` and `Rule`.

Version 1.x.x
```javascript
await register(rule1, rule2, rule3);
// Version 1.x.x
const Rools = require('rools');
// Version 2.x.x
const { Rools, Rule } = require('rools');
```

Version 2.x.x
Rules must now be created with `new Rule()`.

```javascript
await register([rule1, rule2, rule3]);
// Version 1.x.x
const rule = {
name: 'my rule',
...
};
// Version 2.x.x
const rule = new Rule({
name: 'my rule',
...
});
```

`evaluate()` does not return the facts which was only for convenience anyway.
Instead, it returns an object with some useful information about what it was actually doing. `updated` lists the names of the fact segments that were actually updated during evaluation. `fired` is number of rules that were fired. `elapsed` is the number of milliseconds needed.
`register()` takes the rules to register as an array now.
Reason is to allow a second options parameter for future releases.

Version 1.x.x
```javascript
const facts = await evaluate({ user, weather });
// Version 1.x.x
await register(rule1, rule2, rule3);
// Version 2.x.x
await register([rule1, rule2, rule3]);
```

Version 2.x.x
`evaluate()` does not return the facts anymore - which was only for convenience anyway. Instead, it returns an object with some useful information about what it was actually doing. `updated` lists the names of the fact segments that were actually updated during evaluation. `fired` is the number of rules that were fired. `elapsed` is the number of milliseconds needed.

```javascript
// Version 1.x.x
const facts = await evaluate({ user, weather });
// Version 2.x.x
const { updated, fired, elapsed } = await evaluate({ user, weather });
console.log(updated, fired, elapsed); // e.g., ["user"] 26 536
console.log(updated, fired, elapsed); // e.g., ["user"] 26 187
```
8 changes: 1 addition & 7 deletions package-lock.json

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

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "rools",
"version": "1.2.0",
"description": "This is a small rule engine for Node.",
"description": "A small rule engine for Node.",
"main": "src/index.js",
"author": "Frank Thelen",
"license": "MIT",
Expand Down Expand Up @@ -43,7 +43,7 @@
},
"dependencies": {
"bluebird": "^3.5.1",
"lodash.intersection": "^4.4.0",
"lodash": "^4.17.4",
"md5": "^2.2.1",
"uniqueid": "^1.0.0"
}
Expand Down
4 changes: 2 additions & 2 deletions src/Rools.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const _ = require('lodash');
const assert = require('assert');
const intersection = require('lodash.intersection');
const Promise = require('bluebird');
const RuleSet = require('./RuleSet');
const Logger = require('./Logger');
Expand Down Expand Up @@ -160,7 +160,7 @@ class Rools {
resolveBySpecificity(actions) {
const isMoreSpecific = (action, rhs) =>
action.premises.length > rhs.premises.length &&
intersection(action.premises, rhs.premises).length === rhs.premises.length;
_.intersection(action.premises, rhs.premises).length === rhs.premises.length;
const isMostSpecific = (action, all) =>
all.reduce((acc, other) => acc && !isMoreSpecific(other, action), true);
const selected = actions.filter(action => isMostSpecific(action, actions));
Expand Down
Loading

0 comments on commit acd5d61

Please sign in to comment.