@modernpoacher/rules-runner
Version:
A business rules engine for Node
128 lines (99 loc) • 3 kB
JavaScript
import debug from 'debug'
import objectPath from 'object-path'
import {
isBoolean,
isObject
} from './common/index.mjs'
import * as comparators from './comparators/index.mjs'
const log = debug('@modernpoacher/rules-runner')
log('`@modernpoacher/rules-runner` is awake')
const hasArrayKey = (key) => /^(.*)\[\]$/.test(key)
function getArrayKey (key) {
const [,
arrayKey
] = key.match(/^(.*)\[\]$/)
return arrayKey
}
export default class RulesRunner {
constructor (config) {
this.config = config
}
run (values = {}, decisions = {}) {
return (
Object
.values(this.config)
.reduce((accumulator, rule) => {
if (Reflect.has(rule, 'if')) {
const IF = Reflect.get(rule, 'if')
if (this.runTests(IF, values)) {
if (Reflect.has(rule, 'then')) {
const THEN = Reflect.get(rule, 'then')
return this.runOutcomes(THEN, accumulator)
}
} else {
if (Reflect.has(rule, 'otherwise')) {
const OTHERWISE = Reflect.get(rule, 'otherwise')
return this.runOutcomes(OTHERWISE, accumulator)
}
}
return accumulator
}
throw new Error('A rule must have an `if` and a `then`')
}, decisions)
)
}
runTests (expectations = {}, values = {}) {
return (
Object
.entries(expectations)
.every(([key, expectation]) => {
if (objectPath.has(values, key)) {
const actual = objectPath.get(values, key)
return this.runTest(expectation, actual)
}
throw new Error(`Unknown path "${key}"`)
})
)
}
runTest (expectation, ...actual) {
if (expectation === null) throw new Error('Expectation is `null`')
if (expectation === undefined) throw new Error('Expectation is `undefined`')
if (isObject(expectation)) {
return (
Object
.entries(expectation)
.some(([key, value]) => {
if (Reflect.has(comparators, key)) {
const comparator = Reflect.get(comparators, key)
return comparator.call(this, { [key]: value }, ...actual)
}
throw new Error(`Unknown comparator "${key}"`)
})
)
}
if (isBoolean(expectation)) {
const {
boolean
} = comparators
return boolean.call(this, expectation, ...actual)
}
const {
equals
} = comparators
return equals.call(this, expectation, ...actual)
}
runOutcomes (outcomes = {}, accumulator = {}) {
return (
Object
.entries(outcomes)
.reduce((accumulator, [key, outcome]) => {
if (hasArrayKey(key)) {
objectPath.push(accumulator, getArrayKey(key), outcome)
} else {
objectPath.set(accumulator, key, outcome)
}
return accumulator
}, accumulator)
)
}
}