Skip to content

Commit

Permalink
feat: parent onFailure configuration (#14)
Browse files Browse the repository at this point in the history
Today, if an expression evaluation fails, it is only possible to set a
behavior at the expression level, which means that in the grouped
expression, each of them needs to have its own `onFailure` attribute
set.

Within this change, the `onFailure` on the parent expression is now
respected, so, if the evaluation of any expression fails, the parent
configuration will be used when the expression doesn't override its
default throwing behavior.
  • Loading branch information
rapatao authored Nov 17, 2023
1 parent f043f65 commit 8f49756
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 21 deletions.
37 changes: 28 additions & 9 deletions src/main/kotlin/com/rapatao/projects/ruleset/engine/Evaluator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.rapatao.projects.ruleset.engine.context.EvalContext
import com.rapatao.projects.ruleset.engine.context.EvalEngine
import com.rapatao.projects.ruleset.engine.evaluator.rhino.RhinoEvalEngine
import com.rapatao.projects.ruleset.engine.types.Expression
import com.rapatao.projects.ruleset.engine.types.OnFailure

/**
* The Evaluator class is used to evaluate a given rule expression against input data.
Expand All @@ -17,18 +18,20 @@ class Evaluator(
/**
* Evaluates the given rule expression against the provided input data.
*
* @param rule The expression to be evaluated.
* @param expression The expression to be evaluated.
* @param inputData The input data to be used in the evaluation.
* @return `true` if the rule expression evaluates to `true`, `false` otherwise.
*/
fun evaluate(rule: Expression, inputData: Any): Boolean {
return engine.call(inputData) { context ->
val processIsTrue = rule.takeIf { v -> v.parseable() }?.processExpression(context) ?: true
val processNoneMatch = rule.noneMatch?.processNoneMatch(context) ?: true
val processAnyMatch = rule.anyMatch?.processAnyMatch(context) ?: true
val processAllMatch = rule.allMatch?.processAllMatch(context) ?: true
fun evaluate(expression: Expression, inputData: Any): Boolean {
return usingFailureWrapper(expression.onFailure) {
engine.call(inputData) { context ->
val processIsTrue = expression.takeIf { v -> v.parseable() }?.processExpression(context) ?: true
val processNoneMatch = expression.noneMatch?.processNoneMatch(context) ?: true
val processAnyMatch = expression.anyMatch?.processAnyMatch(context) ?: true
val processAllMatch = expression.allMatch?.processAllMatch(context) ?: true

processIsTrue && processNoneMatch && processAnyMatch && processAllMatch
processIsTrue && processNoneMatch && processAnyMatch && processAllMatch
}
}
}

Expand Down Expand Up @@ -77,5 +80,21 @@ class Evaluator(
return allMatch
}

private fun Expression.processExpression(context: EvalContext): Boolean = context.process(this)
private fun Expression.processExpression(context: EvalContext): Boolean {
return usingFailureWrapper(this.onFailure) {
context.process(this)
}
}

private fun usingFailureWrapper(onFailure: OnFailure, block: () -> Boolean): Boolean {
return try {
block()
} catch (@SuppressWarnings("TooGenericExceptionCaught") e: Exception) {
when (onFailure) {
OnFailure.TRUE -> true
OnFailure.FALSE -> false
OnFailure.THROW -> throw e
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.rapatao.projects.ruleset.engine.evaluator.rhino

import com.rapatao.projects.ruleset.engine.context.EvalContext
import com.rapatao.projects.ruleset.engine.types.Expression
import com.rapatao.projects.ruleset.engine.types.OnFailure
import org.mozilla.javascript.Context
import org.mozilla.javascript.Script
import org.mozilla.javascript.ScriptableObject
Expand All @@ -27,16 +26,8 @@ class RhinoContext(
* @throws Exception if the expression processing fails and onFailure is set to THROW
*/
override fun process(expression: Expression): Boolean {
return try {
true == expression.asScript(context)
.exec(context, scope)
} catch (@SuppressWarnings("TooGenericExceptionCaught") e: Exception) {
when (expression.onFailure) {
OnFailure.TRUE -> true
OnFailure.FALSE -> false
OnFailure.THROW -> throw e
}
}
return true == expression.asScript(context)
.exec(context, scope)
}

private fun Expression.asScript(context: Context): Script {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ internal class EvaluatorTest {

@JvmStatic
fun tests() = TestData.allCases()

@JvmStatic
fun onFailure() = TestData.onFailureCases()
}

@ParameterizedTest
Expand All @@ -36,6 +39,20 @@ internal class EvaluatorTest {
doEvaluationTest(engine, ruleSet, expected)
}

@ParameterizedTest
@MethodSource("onFailure")
fun `assert onFailure`(engine: EvalEngine, ruleSet: Expression, expected: Boolean, isError: Boolean) {
println(ruleSet)

if (isError) {
assertThrows<Exception> {
doEvaluationTest(engine, ruleSet, expected)
}
} else {
doEvaluationTest(engine, ruleSet, expected)
}
}

@Test
@Disabled
fun `runs the last test scenario`() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package com.rapatao.projects.ruleset.engine.cases

import com.rapatao.projects.ruleset.engine.types.Expression
import com.rapatao.projects.ruleset.engine.types.OnFailure
import com.rapatao.projects.ruleset.engine.types.builder.equalsTo
import org.junit.jupiter.params.provider.Arguments

object OnFailureCases {

fun cases(): List<Arguments> = anyMatchCases() + allMatchCases() + noneMatchCases()

private fun anyMatchCases(): List<Arguments> = listOf(
Arguments.of(
Expression(
anyMatch = listOf(
"item.non.existing.field" equalsTo 10,
)
),
false,
true,
),
Arguments.of(
Expression(
onFailure = OnFailure.THROW,
anyMatch = listOf(
"item.non.existing.field" equalsTo 10,
)
),
false,
true,
),
Arguments.of(
Expression(
onFailure = OnFailure.FALSE,
anyMatch = listOf(
"item.non.existing.field" equalsTo 10,
)
),
false,
false,
),
Arguments.of(
Expression(
onFailure = OnFailure.TRUE,
anyMatch = listOf(
"item.non.existing.field" equalsTo 10,
)
),
true,
false,
),
)

private fun allMatchCases(): List<Arguments> = listOf(
Arguments.of(
Expression(
allMatch = listOf(
"item.non.existing.field" equalsTo 10,
)
),
false,
true,
),
Arguments.of(
Expression(
onFailure = OnFailure.THROW,
allMatch = listOf(
"item.non.existing.field" equalsTo 10,
)
),
false,
true,
),
Arguments.of(
Expression(
onFailure = OnFailure.FALSE,
allMatch = listOf(
"item.non.existing.field" equalsTo 10,
)
),
false,
false,
),
Arguments.of(
Expression(
onFailure = OnFailure.TRUE,
allMatch = listOf(
"item.non.existing.field" equalsTo 10,
)
),
true,
false,
),
)

private fun noneMatchCases(): List<Arguments> = listOf(
Arguments.of(
Expression(
noneMatch = listOf(
"item.non.existing.field" equalsTo 10,
)
),
false,
true,
),
Arguments.of(
Expression(
onFailure = OnFailure.THROW,
noneMatch = listOf(
"item.non.existing.field" equalsTo 10,
)
),
false,
true,
),
Arguments.of(
Expression(
onFailure = OnFailure.FALSE,
noneMatch = listOf(
"item.non.existing.field" equalsTo 10,
)
),
false,
false,
),
Arguments.of(
Expression(
onFailure = OnFailure.TRUE,
noneMatch = listOf(
"item.non.existing.field" equalsTo 10,
)
),
true,
false,
),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,10 @@ object TestData {
Arguments.of(engine.get().first { it is EvalEngine }, *it.get())
}
}

fun onFailureCases(): List<Arguments> = (OnFailureCases.cases()).flatMap {
engines().map { engine ->
Arguments.of(engine.get().first { it is EvalEngine }, *it.get())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ object Helper {
val evaluator = Evaluator(engine = engine)

assertThat(
evaluator.evaluate(rule = ruleSet, inputData = TestData.inputData),
evaluator.evaluate(expression = ruleSet, inputData = TestData.inputData),
equalTo(expected)
)
}
Expand Down

0 comments on commit 8f49756

Please sign in to comment.