Skip to content

Bug fixes #378

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 2 commits into from
Oct 15, 2024
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
5 changes: 2 additions & 3 deletions examples/13-using-operator-decorators.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,11 @@ async function start () {
}
}

engine.addRule(caseInsensitiveValidTags);
engine.addRule(caseInsensitiveValidTags)

// third run with a tag that is valid if case insensitive
facts = { tags: ['dev', 'PROD'] }
await engine.run(facts);

await engine.run(facts)
}
start()

Expand Down
14 changes: 7 additions & 7 deletions src/almanac.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export default class Almanac {
} else {
fact = new Fact(id, valueOrMethod, options)
}
debug(`almanac::addFact id:${factId}`)
debug('almanac::addFact', { id: factId })
this.factMap.set(factId, fact)
if (fact.isConstant()) {
this._setFactValue(fact, {}, fact.value)
Expand All @@ -121,7 +121,7 @@ export default class Almanac {
* @param {Mixed} value - constant value of the fact
*/
addRuntimeFact (factId, value) {
debug(`almanac::addRuntimeFact id:${factId}`)
debug('almanac::addRuntimeFact', { id: factId })
const fact = new Fact(factId, value)
return this._addConstantFact(fact)
}
Expand Down Expand Up @@ -151,22 +151,22 @@ export default class Almanac {
const cacheVal = cacheKey && this.factResultsCache.get(cacheKey)
if (cacheVal) {
factValuePromise = Promise.resolve(cacheVal)
debug(`almanac::factValue cache hit for fact:${factId}`)
debug('almanac::factValue cache hit for fact', { id: factId })
} else {
debug(`almanac::factValue cache miss for fact:${factId}; calculating`)
debug('almanac::factValue cache miss, calculating', { id: factId })
factValuePromise = this._setFactValue(fact, params, fact.calculate(params, this))
}
}
if (path) {
debug(`condition::evaluate extracting object property ${path}`)
debug('condition::evaluate extracting object', { property: path })
return factValuePromise
.then(factValue => {
if (isObjectLike(factValue)) {
const pathValue = this.pathResolver(factValue, path)
debug(`condition::evaluate extracting object property ${path}, received: ${JSON.stringify(pathValue)}`)
debug('condition::evaluate extracting object', { property: path, received: pathValue })
return pathValue
} else {
debug(`condition::evaluate could not compute object path(${path}) of non-object: ${factValue} <${typeof factValue}>; continuing with ${factValue}`)
debug('condition::evaluate could not compute object path of non-object', { path, factValue, type: typeof factValue })
return factValue
}
})
Expand Down
9 changes: 6 additions & 3 deletions src/condition.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,12 @@ export default class Condition {
]).then(([rightHandSideValue, leftHandSideValue]) => {
const result = op.evaluate(leftHandSideValue, rightHandSideValue)
debug(
`condition::evaluate <${JSON.stringify(leftHandSideValue)} ${
this.operator
} ${JSON.stringify(rightHandSideValue)}?> (${result})`
'condition::evaluate', {
leftHandSideValue,
operator: this.operator,
rightHandSideValue,
result
}
)
return {
result,
Expand Down
8 changes: 6 additions & 2 deletions src/debug.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
export default function debug (message) {

function createDebug () {
try {
if ((typeof process !== 'undefined' && process.env && process.env.DEBUG && process.env.DEBUG.match(/json-rules-engine/)) ||
(typeof window !== 'undefined' && window.localStorage && window.localStorage.debug && window.localStorage.debug.match(/json-rules-engine/))) {
console.log(message)
return console.debug.bind(console)
}
} catch (ex) {
// Do nothing
}
return () => {}
}

export default createDebug()
8 changes: 4 additions & 4 deletions src/engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ class Engine extends EventEmitter {
} else {
fact = new Fact(id, valueOrMethod, options)
}
debug(`engine::addFact id:${factId}`)
debug('engine::addFact', { id: factId })
this.facts.set(factId, fact)
return this
}
Expand Down Expand Up @@ -241,11 +241,11 @@ class Engine extends EventEmitter {
evaluateRules (ruleArray, almanac) {
return Promise.all(ruleArray.map((rule) => {
if (this.status !== RUNNING) {
debug(`engine::run status:${this.status}; skipping remaining rules`)
debug('engine::run, skipping remaining rules', { status: this.status })
return Promise.resolve()
}
return rule.evaluate(almanac).then((ruleResult) => {
debug(`engine::run ruleResult:${ruleResult.result}`)
debug('engine::run', { ruleResult: ruleResult.result })
almanac.addResult(ruleResult)
if (ruleResult.result) {
almanac.addEvent(ruleResult.event, 'success')
Expand Down Expand Up @@ -286,7 +286,7 @@ class Engine extends EventEmitter {
}

almanac.addFact(fact)
debug(`engine::run initialized runtime fact:${fact.id} with ${fact.value}<${typeof fact.value}>`)
debug('engine::run initialized runtime fact', { id: fact.id, value: fact.value, type: typeof fact.value })
}
const orderedSets = this.prioritizeRules()
let cursor = Promise.resolve()
Expand Down
8 changes: 4 additions & 4 deletions src/operator-map.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default class OperatorMap {
} else {
operator = new Operator(operatorOrName, cb)
}
debug(`operatorMap::addOperator name:${operator.name}`)
debug('operatorMap::addOperator', { name: operator.name })
this.operators.set(operator.name, operator)
}

Expand Down Expand Up @@ -64,7 +64,7 @@ export default class OperatorMap {
} else {
decorator = new OperatorDecorator(decoratorOrName, cb)
}
debug(`operatorMap::addOperatorDecorator name:${decorator.name}`)
debug('operatorMap::addOperatorDecorator', { name: decorator.name })
this.decorators.set(decorator.name, decorator)
}

Expand Down Expand Up @@ -110,15 +110,15 @@ export default class OperatorMap {
const decoratorName = opName.slice(0, firstDecoratorIndex)
const decorator = this.decorators.get(decoratorName)
if (!decorator) {
debug(`operatorMap::get invalid decorator named ${decoratorName}`)
debug('operatorMap::get invalid decorator', { name: decoratorName })
return null
}
// we're going to apply this later, use unshift since we'll apply in reverse order
decorators.unshift(decorator)
// continue looking for a known operator with the rest of the name
opName = opName.slice(firstDecoratorIndex + 1)
} else {
debug(`operatorMap::get no operator named ${opName}`)
debug('operatorMap::get no operator', { name: opName })
return null
}
}
Expand Down
32 changes: 6 additions & 26 deletions src/rule.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ class Rule extends EventEmitter {
return Promise.all(
conditions.map((condition) => evaluateCondition(condition))
).then((conditionResults) => {
debug('rule::evaluateConditions results', conditionResults)
debug('rule::evaluateConditions', { results: conditionResults })
return method.call(conditionResults, (result) => result === true)
})
}
Expand All @@ -274,36 +274,16 @@ class Rule extends EventEmitter {
// this also covers the 'not' case which should only ever have a single condition
return evaluateCondition(conditions[0])
}
let method = Array.prototype.some
if (operator === 'all') {
method = Array.prototype.every
}
const orderedSets = this.prioritizeConditions(conditions)
let cursor = Promise.resolve()
let cursor = Promise.resolve(operator === 'all')
// use for() loop over Array.forEach to support IE8 without polyfill
for (let i = 0; i < orderedSets.length; i++) {
const set = orderedSets[i]
let stop = false
cursor = cursor.then((setResult) => {
// after the first set succeeds, don't fire off the remaining promises
if ((operator === 'any' && setResult === true) || stop) {
debug(
'prioritizeAndRun::detected truthy result; skipping remaining conditions'
)
stop = true
return true
}

// after the first set fails, don't fire off the remaining promises
if ((operator === 'all' && setResult === false) || stop) {
debug(
'prioritizeAndRun::detected falsey result; skipping remaining conditions'
)
stop = true
return false
}
// all conditions passed; proceed with running next set in parallel
return evaluateConditions(set, method)
// rely on the short-circuiting behavior of || and && to avoid evaluating subsequent conditions
return operator === 'any'
? (setResult || evaluateConditions(set, Array.prototype.some))
: (setResult && evaluateConditions(set, Array.prototype.every))
})
}
return cursor
Expand Down
6 changes: 3 additions & 3 deletions test/engine-operator-map.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,10 @@ describe('Engine Operator Map', () => {
})

it('the swap decorator', () => {
const factValue = 1;
const jsonValue = [1, 2, 3];
const factValue = 1
const jsonValue = [1, 2, 3]

const op = engine.operators.get('swap:contains');
const op = engine.operators.get('swap:contains')
expect(op.evaluate(factValue, jsonValue)).to.be.true()
})
})
Loading