Data-driven logic for ultra-configurable applications, implemented in Javascript
This library allows you to safely evaluate logic user-defined logic. Through this, you can (lazy) load decision making logic from various sources, and analyze them in very interesting ways. As an application, think of dynamically configurable alerts based on analytics, tuning an algorithm that decides when to re-engage the user by e-mail, or defining some simple operations that need to be done during the migration of a data model. This library is inspired by JSON logic, but a lot cleaner, more extensible, customizable and debuggable. Also, not using symbols like !=
and >=
makes this much more readable when storing logic in YAML format.
npm install user-logic --save
import { UserLogic } from 'user-logic'
const willReceiveWiskeyBottleLogic = new UserLogic({definition: {
and: [
{gt: ['$user.years-of-membership', 5]},
{gt: ['$user.age', 21]},
'$user.likes.alcohol',
]
}})
console.log(willReceiveWiskeyBottleLogic.evaluate({user: {
'years-of-membership': 6,
'age': 52,
'likes': {flowers: false, alcohol: true}
}}))
There are operations that take one or multiple operands, but they all work in the way above.
You can find the whole list of supported operations in the unit tests.
You can pass in custom operations to the constructor of UserLogic
:
import { UserLogic } from 'user-logic'
import { unaryOperator } from 'user-logic/lib/operations'
const customLogic = new UserLogic({definition: {bla: 'Hello'}, operations: {
bla: unaryOperator(value => value() + '!!!')
}})
console.log(customLogic.evaluate({})) // Hello!!!
The simplest way is to use either unaryOperator
as shown above, or binaryOperator
. Both take functions that evaluate the passed in expressions when called, allowing for short-circuit operators. The binaryOperator
is used like this, and supports operators with more than one operand:
import { UserLogic } from 'user-logic'
import { binaryOperator } from 'user-logic/lib/operations'
const customLogic = new UserLogic({definition: {add: [3, 5, 7]}, operations: {
add: binaryOperator((left, right) => left() + right())
}})
console.log(customLogic.evaluate({})) // 15
If you want to do more advanced stuff, you can construct custom nodes:
const customLogic = new UserLogic({definition: {'if': [true', 'True!', 'False!']}, operations: {
ifNode: ({definition, parse}) => {
const [condition, ifTrue, ifFalse] = definition.map(parse)
return {
evaluate: context => {
if (condition.evaluate(context)) {
return ifTrue.evaluate(context)
} else {
return ifFalse.evaluate(context)
}
}
}
}
})
To override lookup behavior, you can override the valueTemplate operation which receives the path without a $
sign as the definition. This is a very simple implementation:
import * as objectPath from 'object-path'
export function valueTemplateNode({definition, reportLookup}) {
const path = definition.split('.')
return {
evaluate: context => {
return objectPath['withInheritedProps'].get(context, path)
}
}
}
Sometimes, you need to construct an object with a few values including logic, of which a few might be conditional. For this use case, we have logic maps:
import { UserLogic, makeLogicMap, evalLogicMap } from 'user-logic'
const logicMap = makeLogicMap({
foo: {gt: ['$age', 18]},
bar: {gt: ['$age', 21]}
})
console.log(evalLogicMap(logicMap, {age: 20})) // {foo: true, bar: false}
const conditionalLogicMap = makeLogicMap([
{name: 'Joe'},
{if: {lt: ['$age', 18]}
minor: true}
])
console.log(evalLogicMap(conditionalLogicMap, {age: 20})) // {name: 'Joe'}
console.log(evalLogicMap(conditionalLogicMap, {age: 15})) // {name: 'Joe', minor: true}
If you need to know which variables your logic can access, pass in the reportLookup
option to the constructor:
const lookups = []
const logic = new UserLogic({definition: {eq: ['$foo.test', 5]}, reportLookup: path => lookups.push(path)})
expect(lookups).to.deep.equal([
['foo', 'test']
])
This library has the goal of making applications more flexible with user-defined business logic portable across various languages, while being super debuggable and useful to work on complex logic with inter-disciplinary teams. In this content, I'd like to see the following features:
{and: ['$foo', '$bar']}
with $foo and $bar being booleans would generate [{foo: true, bar: true}, {foo: true, bar: false}, {foo: false, bar: true}, {foo: false, bar: false}]
.