From 8f4975659693449c42c71aac535b9716a2d66f3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luiz=20H=2E=20Rapat=C3=A3o?= Date: Fri, 17 Nov 2023 17:33:01 +0000 Subject: [PATCH] feat: parent onFailure configuration (#14) 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. --- .../projects/ruleset/engine/Evaluator.kt | 37 +++-- .../engine/evaluator/rhino/RhinoContext.kt | 13 +- .../projects/ruleset/engine/EvaluatorTest.kt | 17 +++ .../ruleset/engine/cases/OnFailureCases.kt | 137 ++++++++++++++++++ .../projects/ruleset/engine/cases/TestData.kt | 6 + .../projects/ruleset/engine/helper/Helper.kt | 2 +- 6 files changed, 191 insertions(+), 21 deletions(-) create mode 100644 src/test/kotlin/com/rapatao/projects/ruleset/engine/cases/OnFailureCases.kt diff --git a/src/main/kotlin/com/rapatao/projects/ruleset/engine/Evaluator.kt b/src/main/kotlin/com/rapatao/projects/ruleset/engine/Evaluator.kt index ded8288..e99c27e 100644 --- a/src/main/kotlin/com/rapatao/projects/ruleset/engine/Evaluator.kt +++ b/src/main/kotlin/com/rapatao/projects/ruleset/engine/Evaluator.kt @@ -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. @@ -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 + } } } @@ -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 + } + } + } } diff --git a/src/main/kotlin/com/rapatao/projects/ruleset/engine/evaluator/rhino/RhinoContext.kt b/src/main/kotlin/com/rapatao/projects/ruleset/engine/evaluator/rhino/RhinoContext.kt index f28c5be..961f934 100644 --- a/src/main/kotlin/com/rapatao/projects/ruleset/engine/evaluator/rhino/RhinoContext.kt +++ b/src/main/kotlin/com/rapatao/projects/ruleset/engine/evaluator/rhino/RhinoContext.kt @@ -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 @@ -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 { diff --git a/src/test/kotlin/com/rapatao/projects/ruleset/engine/EvaluatorTest.kt b/src/test/kotlin/com/rapatao/projects/ruleset/engine/EvaluatorTest.kt index 3ffb897..2640b30 100644 --- a/src/test/kotlin/com/rapatao/projects/ruleset/engine/EvaluatorTest.kt +++ b/src/test/kotlin/com/rapatao/projects/ruleset/engine/EvaluatorTest.kt @@ -26,6 +26,9 @@ internal class EvaluatorTest { @JvmStatic fun tests() = TestData.allCases() + + @JvmStatic + fun onFailure() = TestData.onFailureCases() } @ParameterizedTest @@ -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 { + doEvaluationTest(engine, ruleSet, expected) + } + } else { + doEvaluationTest(engine, ruleSet, expected) + } + } + @Test @Disabled fun `runs the last test scenario`() { diff --git a/src/test/kotlin/com/rapatao/projects/ruleset/engine/cases/OnFailureCases.kt b/src/test/kotlin/com/rapatao/projects/ruleset/engine/cases/OnFailureCases.kt new file mode 100644 index 0000000..cc4903c --- /dev/null +++ b/src/test/kotlin/com/rapatao/projects/ruleset/engine/cases/OnFailureCases.kt @@ -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 = anyMatchCases() + allMatchCases() + noneMatchCases() + + private fun anyMatchCases(): List = 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 = 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 = 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, + ), + ) +} diff --git a/src/test/kotlin/com/rapatao/projects/ruleset/engine/cases/TestData.kt b/src/test/kotlin/com/rapatao/projects/ruleset/engine/cases/TestData.kt index 460b139..92f1871 100644 --- a/src/test/kotlin/com/rapatao/projects/ruleset/engine/cases/TestData.kt +++ b/src/test/kotlin/com/rapatao/projects/ruleset/engine/cases/TestData.kt @@ -28,4 +28,10 @@ object TestData { Arguments.of(engine.get().first { it is EvalEngine }, *it.get()) } } + + fun onFailureCases(): List = (OnFailureCases.cases()).flatMap { + engines().map { engine -> + Arguments.of(engine.get().first { it is EvalEngine }, *it.get()) + } + } } diff --git a/src/test/kotlin/com/rapatao/projects/ruleset/engine/helper/Helper.kt b/src/test/kotlin/com/rapatao/projects/ruleset/engine/helper/Helper.kt index 59a2fdc..bb18ea4 100644 --- a/src/test/kotlin/com/rapatao/projects/ruleset/engine/helper/Helper.kt +++ b/src/test/kotlin/com/rapatao/projects/ruleset/engine/helper/Helper.kt @@ -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) ) }