@mihazs/rules-machine
v1.1.2
Published
A JSON-based Rules Engine. Serialize business logic into JSON to manage complexity & model larger workflows.
Downloads
3
Maintainers
Readme
Rules Machine
Rules Against The Machine 🤘
Table of Content
- Rules Machine
What's a Rules Machine
?
It's a fast, general purpose JSON Rules Engine
library for both the Browser & Node.js! 🚀
Goals
- Share business logic - move logic around the I/O layer, just like data.
- Shared validation logic (same logic from the web form to the backend)
- Push rules where they are needed: Cloud functions, CloudFlare Workers, Lambda@Edge, etc.)
- Organize complexity - isolate complex Business Rules from App Logic and state.
- Name, group and chain rules.
- Don't repeat yourself: reference common rule(s) by name. (
applySalesTax
)
- Modeling workflows - model your business logic as a series of readable steps.
- Help non-dev stakeholders (QA, Product) understand critical logic.
- Simply formatting JSON Rules sheds light on both hierarchy & steps.
Key Terms
App Logic != Business Rules
- App Logic - applies more broadly and changes less frequently than Business Rules.
- "Throw Error if ShoppingCart total is less than zero."
- "Only one discount code can be applied at a time."
- Business Rules - targeted & detailed, can change frequently.
- Supports business goals & objectives (as they evolve) from Product, Leadership, Legal, Finance, A/B Tuning, etc.
- "Premium customers can apply 3 discounts, up to 25% off."
- "If we're in lock-down, double shipping estimates."
- "If State is NY, add NY tax."
- "If State is AZ and during Daylight Savings, offset an hour."
Finding Opportunities for Rules
Typically Business Rules are better-suited to 'rules engine' style pattern.
If your Business Rules or logic changes frequently, you can get alignment benefits by moving that logic to a serializable & sharable format. Specifically, this can provide immediate benefits to mobile & native apps, as you don't have to wait for an approvals process for every change. ✨
Why Rules Engines?
Typically App Logic & Business Rules are woven together throughout the project. This co-location of logic is usually helpful, keeping things readable in small and even mid-sized projects.
This works great, until you run into one of the following challenges:
- Storing Rules
- A note taking app could let users create custom shortcuts, where typing "TODO" could load a template.
- These "shortcuts" (JSON Rules) can be stored in a local file, synced to a database, or even broadcast over a mesh network.
- Unavoidable Complexity
- In many industries like healthcare, insurance, finance, etc. it's common to find 100's or 1,000s of rules run on every transaction.
- Over time, "Hand-coded Rules" can distract & obscure from core App Logic.
- Example: Adding a feature to a
DepositTransaction
controller shouldn't require careful reading of 2,000 lines of custom rules around currency hackery & country-code checks. - Without a strategy, code eventually sprawls as logic gets duplicated & placed arbitrarily. Projects become harder to understand, risky to modify, and adding new rules become high-stakes exercises.
- Tracing Errors or Miscalculations
- Complex pricing, taxes & discount policies can be fully "covered" by unit tests, yet still fail in surprising ways.
- Determining how a customer's subtotal WAS calculated after the fact can be tedious & time consuming.
- Example: Sales tax rates and rules are defined by several layers of local government. (Mainly City, County, and State.)
- Depending on the State rules, you'll need to calculate based on the Billing Address or Shipping Address.
- Scenario: A California customer has expanded into Canada. Their new shipping destination seems to cause double taxation!?!
- In this situation, a trace of the computations can save hours of dev work, boost Customer Support' confidence issuing a partial refund, and the data team can use the raw data to understand the scope of the issue.
- Scenario: "Why did we approve a $10,000,000 loan for 'The Joker'?"
- Scenario: "How did an Ultra Sports Car ($1M+) qualify for fiscal hardship rates?"
Pros
- Uses a subset of JavaScript and structured JSON object(s).
- Easy to start using & experimenting with, larger implementations require more planning.
- Provides a
trace
, with details on each step, what happened, and the time taken.
Cons
- Sizable projects require up-front planning & design work to properly adapt this pattern. (1,000s rules, for example.)
- Possible early optimization or premature architecture decision.
- Not as easy to write compared to a native language.
Install
yarn add @elite-libs/rules-machine
# Or
npm install @elite-libs/rules-machine
Usage
import { ruleFactory } from '@elite-libs/rules-machine';
const fishRhyme = ruleFactory([
{if: 'fish == "oneFish"', then: 'fish = "twoFish"' },
{if: 'fish == "redFish"', then: 'fish = "blueFish"' },
]);
// Equivalent to:
// if (fish == "oneFish") fish = "twoFish"
// if (fish == "redFish") fish = "blueFish"
fishyRhyme({fish: 'oneFish'}); // {fish: 'twoFish'}
Examples
Example Rule: Apply Either $5 or $10 Discount
// Using "and" object style operator
[
{"if": {"and": ["price >= 25", "price <= 50"]}, "then": "discount = 5"},
{"if": "price > 50", "then": "discount = 10"},
{"return": "discount"}
]
// Using inline AND operator
[
{"if": "price >= 25 AND price <= 50", "then": "discount = 5"},
{"if": "price > 50", "then": "discount = 10"},
{"return": "discount"}
]
- if: {and: [price >= 25, price <= 50]}
then: discount = 5
- if: price > 50
then: discount = 10
- return: discount
Example Rule: Apply $15 Discount if Employee, or Premium Customer
[
{
"if": "user.plan == \"premium\"",
"then": "discount = 15"
},
{
"if": "user.employee == true",
"then": "discount = 15"
},
{
"return": "discount"
}
]
Example Rule: Multiple Conditional, Nested Rules
[
{
"if": "price <= 100",
"then": "discount = 5"
},
{
"if": {
"or": [
"price >= 100",
"user.isAdmin == true"
]
},
"then": "discount = 20"
},
{
"return": "discount"
}
]
- if: price <= 100
then: discount = 5
- if:
or: [price >= 100, user.isAdmin == true]
then: discount = 20
- return: discount
Example Rule: Use variable between rules
[
{
"if": "price <= 100",
"then": [
"discount = 5",
"user.discountApplied = true"
]
},
{
"if": {
"and": [
"price >= 90",
"user.discountApplied != true"
]
},
"then": "discount = 20"
},
{
"return": "discount"
}
]
- if: price <= 100
then:
- discount = 5
- user.discountApplied = true
- if:
and:
- price >= 90
- user.discountApplied != true
then: discount = 20
- return: discount
All Operators & Functions
Builtin Operators
!=
=
- equality check.==
- equality check.<
<=
<>
>
>=
%
-10 % 2
=>0
(tip: odd/even check)*
-42 * 10
=>420
+
-42 + 10
=>52
-
/
^
~=
AND
OR
Functions
Extended Methods
REMOVE_VALUES(matches, input)
- will remove all values matching the item(s) in the 1st argument from the 2nd argument array. (XOR operation.)INCLUDES_VALUES(matches, input)
- will ONLY INCLUDE values that are in the 1st & 2nd arguments. (Intersection operation.)
Utility Functions
- IF() -
IF(7 > 5, 8, 10)
=>8
- GET() -
GET('users[2].name', users)
=>Mary
Math Functions: Core
- AVERAGE() -
AVERAGE([10, 20, 30])
=>20
- CEIL() -
CEIL(0.1)
=>1
- FLOOR() -
FLOOR(1.9)
=>1
- ROUND() -
FLOOR(0.6)
=>1
- TRUNC() -
TRUNC(1.9)
=>1
- SUM() -
SUM([1,2,3])
=>6
- ADD() -
ADD(2, 3)
=>5
- SUB() -
SUB(2, 3)
=>-1
- DIV() -
DIV(9, 3)
=>3
- MUL() -
MUL(3, 3)
=>9
- NEG() -
NEG(ADD(1, 2))
=>-3
- NOT() -
NOT(ISPRIME(7))
=>false
- ISNAN() -
ISNAN('hai')
=>true
- ISPRIME() -
ISPRIME(7)
=>true
- MOD() -
MOD(10, 2)
=>0
- GCD() -
GCD(9, 3)
=>3
Array Functions
- SLICE() -
SLICE(1, 3, [1, 42, 69, 54])
=>[42, 69]
- LENGTH() -
LENGTH([42, 69, 54])
=>3
- SORT() -
SORT([2,2,1])
=>[1, 2, 2]
- FILTER() -
FILTER(isEven, [1,2,3,4,5,6])
=>[2, 4, 6]
- INDEX() -
INDEX([42, 69, 54], 0)
=>42
- MAP() -
MAP("NOT", [FALSE, TRUE, FALSE])
=>[true, false, true]
- MIN() -
MIN([42, 69, 54])
=>42
- MAX() -
MAX([42, 69, 54])
=>69
- HEAD() -
HEAD([42, 69, 54])
=>42
- LAST() -
LAST([42, 69, 54])
=>54
- TAIL() -
TAIL([42, 69, 54])
=>[69, 54]
- TAKE() -
TAKE(2, [42, 69, 54])
=>[42, 69]
- TAKEWHILE() -
TAKEWHILE(isEven, [0,2,4,5,6,7,8])
=>[0, 2, 4]
- DROP() -
DROP(2, [1, 42, 69, 54])
=>[69, 54]
- DROPWHILE() -
DROPWHILE(isEven, [0,2,4,5,6,7,8])
=>[5,6,7,8]
- REDUCE() -
REDUCE("ADD", 0, [1, 2, 3])
=>6
- REVERSE() -
REVERSE([1,2,2])
=>[2, 2, 1]
- CHARARRAY() -
CHARARRAY("abc")
=>['a', 'b', 'c']
- CONCAT() -
CONCAT([42, 69], [54])
=>[42, 69, 54]
- CONS() -
CONS(2, [3, 4])
=>[2, 3, 4]
- JOIN() -
JOIN(",", ["a", "b"])
=>a,b
- RANGE() -
RANGE(0, 5)
=>[0, 1, 2, 3, 4]
- UNZIPDICT() -
UNZIPDICT([["a", 1], ["b", 5]])
=>{a: 1, b: 5}
- ZIP() -
ZIP([1, 3], [2, 4])
=>[[1, 2], [3, 4]]
Object Functions
- DICT() -
DICT(["a", "b"], [1, 4])
=>{a: 1, b: 4}
- KEYS() -
KEYS(DICT(["a", "b"], [1, 4]))
=>['a', 'b']
- VALUES() -
VALUES(DICT(["a", "b"], [1, 4]))
=>[1, 4]
- UNZIP() -
UNZIP([[1, 2], [3, 4]])
=>[[1, 3], [2, 4]]
String Functions
- LOWER() -
LOWER('HELLO')
=>hello
- UPPER() -
UPPER('hello')
=>HELLO
- SPLIT() -
SPLIT(',', 'a,b')
=>['a', 'b']
- CHAR() -
CHAR(65)
=>A
- CODE() -
CODE('A')
=>65
- BIN2DEC() -
BIN2DEC('101010')
=>42
- DEC2BIN() -
DEC2BIN(42)
=>101010
- DEC2HEX() -
DEC2HEX('42')
=>2a
- DEC2STR() -
DEC2STR('42')
=>42
- HEX2DEC() -
HEX2DEC("F")
=>15
- STR2DEC() -
STR2DEC('42')
=>42
Math Functions: Advanced
- SQRT()
- CUBEROOT()
- SIGN() -
SIGN(-42)
=>-1
- ABS() -
ABS(-42)
=>42
- ACOS()
- ACOSH()
- ASIN()
- ASINH()
- ATAN()
- ATAN2()
- ATANH()
- COS()
- COSH()
- DEGREES()
- RADIANS()
- SIN()
- SINH()
- TAN()
- TANH()
- EXP()
- LN()
- LOG()
- LOG2()
More Reading & Related Projects
- Should I use a Rules Engine?
- JSON Rules Engine.
- GitHub Actions YAML conditional syntax.
TODO
- [ ] Web app to test & build rules.
- [ ] Return result by default, make trace and metadata opt-in via options.
- [x] Add arithmetic & function support to expression parser.
- Over 80 builtin functions supported.
- [x] Publish modules for CJS, ESM, AMD, UMD. (Using parcel.)
- [ ] misc: Structured Type validation.
- [x] security: NEVER use
eval
/Function('...')
parsing. - [x] misc: Simplify TS, making
Rule[]
the sole recursive type. - [x] misc: Use reduced JS syntax, scope.
- [x] misc: Use single object for input and output. (Doesn't mutate input.)
- [x] misc: Add support for multiple boolean expressions. (see:
{"and": []}
{"or": []}
). - [x] misc: Rules are serializable, and can be shared.
- [ ] rule type:
{"run": Rule[] | Rule | "ruleSetName"}
- [ ] rule type:
{"throw": "error message"}
- [ ] rule type:
{"log": "rule/value expression"}
- [ ] rule type:
{"set": "newVar = value"}
- [ ] Disallow input keys that can cause weirdness:
undefined
,valueOf
,toString
,__proto__
,constructor
.