diff --git a/docs/docs/reference/builtin-functions/feel-built-in-functions-boolean.md b/docs/docs/reference/builtin-functions/feel-built-in-functions-boolean.md index c77832cbb..93a65c72b 100644 --- a/docs/docs/reference/builtin-functions/feel-built-in-functions-boolean.md +++ b/docs/docs/reference/builtin-functions/feel-built-in-functions-boolean.md @@ -80,3 +80,53 @@ get or default(null, "default") get or default(null, null) // null ``` + +## assert(value, condition) + + + +Verify that the given condition is met. If the condition is `true`, the function returns the value. +Otherwise, the evaluation fails with an error. + +**Function signature** + +```js +assert(value: Any, condition: Any) +``` + +**Examples** + +```js +assert(x, x != null) +// "value" - if x is "value" +// error - if x is null or doesn't exist + +assert(x, x >= 0) +// 4 - if x is 4 +// error - if x is less than zero +``` + +## assert(value, condition, cause) + + + +Verify that the given condition is met. If the condition is `true`, the function returns the value. +Otherwise, the evaluation fails with an error containing the given message. + +**Function signature** + +```js +assert(value: Any, condition: Any, cause: String) +``` + +**Examples** + +```js +assert(x, x != null, "'x' should not be null") +// "value" - if x is "value" +// error('x' should not be null) - if x is null or doesn't exist + +assert(x, x >= 0, "'x' should be positive") +// 4 - if x is 4 +// error('x' should be positive) - if x is less than zero +``` diff --git a/src/main/scala/org/camunda/feel/FeelEngine.scala b/src/main/scala/org/camunda/feel/FeelEngine.scala index 3a4835b68..afd36f475 100644 --- a/src/main/scala/org/camunda/feel/FeelEngine.scala +++ b/src/main/scala/org/camunda/feel/FeelEngine.scala @@ -18,7 +18,7 @@ package org.camunda.feel import fastparse.Parsed import org.camunda.feel.FeelEngine.{Configuration, EvalExpressionResult, EvalUnaryTestsResult, Failure} -import org.camunda.feel.api.{EvaluationResult, FailedEvaluationResult, SuccessfulEvaluationResult} +import org.camunda.feel.api.{EvaluationFailure, EvaluationFailureType, EvaluationResult, FailedEvaluationResult, SuccessfulEvaluationResult} import org.camunda.feel.context.{Context, FunctionProvider} import org.camunda.feel.impl.interpreter.{BuiltinFunctions, EvalContext, FeelInterpreter} import org.camunda.feel.impl.parser.{ExpressionValidator, FeelParser} @@ -156,6 +156,13 @@ class FeelEngine( private def eval(exp: ParsedExpression, context: EvalContext): EvaluationResult = { interpreter.eval(exp.expression)(context) match { + case _ if containsAssertionError(context) => { + val failureMessage = getAssertErrorMessage(context) + FailedEvaluationResult( + failure = Failure(s"Assertion failure on evaluate the expression '${exp.text}': ${failureMessage}"), + suppressedFailures = context.failureCollector.failures + ) + } case ValError(cause) => FailedEvaluationResult( failure = Failure(s"failed to evaluate expression '${exp.text}': $cause"), @@ -169,6 +176,19 @@ class FeelEngine( } } + /** + * Check if an {@link EvaluationFailureType.ASSERT_FAILURE} error is raised during the evaluation of an expression + * @param context the context of the evaluation + * @return true if an an {@link EvaluationFailureType.ASSERT_FAILURE} is raised, false otherwise + */ + private def containsAssertionError(context: EvalContext): Boolean = { + context.failureCollector.failures.exists(_.failureType == EvaluationFailureType.ASSERT_FAILURE) + } + + private def getAssertErrorMessage(context: EvalContext): String = { + context.failureCollector.failures.find(_.failureType == EvaluationFailureType.ASSERT_FAILURE).get.failureMessage + } + // ============ public API ============ /** diff --git a/src/main/scala/org/camunda/feel/api/EvaluationFailureType.scala b/src/main/scala/org/camunda/feel/api/EvaluationFailureType.scala index 123a37ee5..d4be146bf 100644 --- a/src/main/scala/org/camunda/feel/api/EvaluationFailureType.scala +++ b/src/main/scala/org/camunda/feel/api/EvaluationFailureType.scala @@ -39,6 +39,8 @@ object EvaluationFailureType { case object FUNCTION_INVOCATION_FAILURE extends EvaluationFailureType + case object ASSERT_FAILURE extends EvaluationFailureType + } diff --git a/src/main/scala/org/camunda/feel/impl/builtin/BooleanBuiltinFunctions.scala b/src/main/scala/org/camunda/feel/impl/builtin/BooleanBuiltinFunctions.scala index 9b0862acc..3cd15933f 100644 --- a/src/main/scala/org/camunda/feel/impl/builtin/BooleanBuiltinFunctions.scala +++ b/src/main/scala/org/camunda/feel/impl/builtin/BooleanBuiltinFunctions.scala @@ -17,14 +17,15 @@ package org.camunda.feel.impl.builtin import org.camunda.feel.impl.builtin.BuiltinFunction.builtinFunction -import org.camunda.feel.syntaxtree.{Val, ValBoolean, ValError, ValNull} +import org.camunda.feel.syntaxtree.{Val, ValBoolean, ValError, ValNull, ValString} object BooleanBuiltinFunctions { def functions = Map( "not" -> List(notFunction), "is defined" -> List(isDefinedFunction), - "get or else" -> List(getOrElse) + "get or else" -> List(getOrElse), + "assert" -> List(assertFunction, assertFunction2) ) private def notFunction = @@ -49,4 +50,21 @@ object BooleanBuiltinFunctions { case List(value, _) => value } ) + + private def assertFunction = builtinFunction( + params = List("value", "condition"), + invoke = { + case List(value, ValBoolean(true)) => value + case _ => ValError("The condition is not fulfilled") + } + ) + + private def assertFunction2 = builtinFunction( + params = List("value", "condition", "cause"), + invoke = { + case List(value, ValBoolean(true), _) => value + case List(_, _, ValString(cause)) => ValError(cause) + } + ) + } diff --git a/src/main/scala/org/camunda/feel/impl/interpreter/FeelInterpreter.scala b/src/main/scala/org/camunda/feel/impl/interpreter/FeelInterpreter.scala index cc8a0f308..cdfa97100 100644 --- a/src/main/scala/org/camunda/feel/impl/interpreter/FeelInterpreter.scala +++ b/src/main/scala/org/camunda/feel/impl/interpreter/FeelInterpreter.scala @@ -207,6 +207,9 @@ class FeelInterpreter { case FunctionInvocation(name, params) => withFunction(findFunction(context, name, params), f => invokeFunction(f, params) match { + case ValError(failure) if name == "assert" => + error(EvaluationFailureType.ASSERT_FAILURE, failure) + ValError(failure) case ValError(failure) => error(EvaluationFailureType.FUNCTION_INVOCATION_FAILURE, s"Failed to invoke function '$name': $failure") ValNull diff --git a/src/test/scala/org/camunda/feel/impl/builtin/BuiltinFunctionTest.scala b/src/test/scala/org/camunda/feel/impl/builtin/BuiltinFunctionTest.scala deleted file mode 100644 index dc1a4389e..000000000 --- a/src/test/scala/org/camunda/feel/impl/builtin/BuiltinFunctionTest.scala +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH - * under one or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information regarding copyright - * ownership. Camunda licenses this file to you under the Apache License, - * Version 2.0; you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.camunda.feel.impl.builtin - -import org.camunda.feel.impl.{EvaluationResultMatchers, FeelEngineTest} -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -/** - * @author Philipp - */ -class BuiltinFunctionsTest - extends AnyFlatSpec - with Matchers - with FeelEngineTest - with EvaluationResultMatchers { - - "A built-in function" should "return null if arguments doesn't match" in { - - evaluateExpression("date(true)") should returnNull() - evaluateExpression("number(false)") should returnNull() - } - - "A not() function" should "negate Boolean" in { - - evaluateExpression(" not(true) ") should returnResult(false) - evaluateExpression(" not(false) ") should returnResult(true) - } - - "A is defined() function" should "return true if the value is present" in { - - evaluateExpression("is defined(null)") should returnResult(true) - - evaluateExpression("is defined(1)") should returnResult(true) - evaluateExpression("is defined(true)") should returnResult(true) - evaluateExpression("is defined([])") should returnResult(true) - evaluateExpression("is defined({})") should returnResult(true) - evaluateExpression(""" is defined( {"a":1}.a ) """) should returnResult(true) - } - - // see: https://github.com/camunda/feel-scala/issues/695 - ignore should "return false if a variable doesn't exist" in { - evaluateExpression("is defined(a)") should returnResult(false) - evaluateExpression("is defined(a.b)") should returnResult(false) - } - - // see: https://github.com/camunda/feel-scala/issues/695 - ignore should "return false if a context entry doesn't exist" in { - evaluateExpression("is defined({}.a)") should returnResult(false) - evaluateExpression("is defined({}.a.b)") should returnResult(false) - } - - "A get or else(value: Any, default: Any) function" should "return the value if not null" in { - - evaluateExpression("get or else(3, 1)") should returnResult(3) - evaluateExpression("""get or else("value", "default")""") should returnResult("value") - evaluateExpression("get or else(value:3, default:1)") should returnResult(3) - } - - it should "return the default param if value is null" in { - - evaluateExpression("get or else(null, 1)") should returnResult(1) - evaluateExpression("""get or else(null, "default")""") should returnResult("default") - evaluateExpression("get or else(value:null, default:1)") should returnResult(1) - } - - it should "return null if both value and default params are null" in { - - evaluateExpression("get or else(null, null)") should returnNull() - evaluateExpression("get or else(value:null, default:null)") should returnNull() - } -} diff --git a/src/test/scala/org/camunda/feel/impl/builtin/BuiltinFunctionsTest.scala b/src/test/scala/org/camunda/feel/impl/builtin/BuiltinFunctionsTest.scala new file mode 100644 index 000000000..f0c28e594 --- /dev/null +++ b/src/test/scala/org/camunda/feel/impl/builtin/BuiltinFunctionsTest.scala @@ -0,0 +1,244 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. Camunda licenses this file to you under the Apache License, + * Version 2.0; you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.camunda.feel.impl.builtin + +import org.camunda.feel.impl.EvaluationResultMatchers +import org.camunda.feel.impl.FeelEngineTest +import org.scalatest.matchers.should.Matchers +import org.scalatest.flatspec.AnyFlatSpec + +/** + * @author Philipp + */ +class BuiltinFunctionsTest + extends AnyFlatSpec + with Matchers + with FeelEngineTest + with EvaluationResultMatchers { + + "A built-in function" should "return null if arguments doesn't match" in { + + evaluateExpression( + expression = "date(true)" + ) should returnNull() + + evaluateExpression( + expression = "number(false)" + ) should returnNull() + } + + "A not() function" should "negate Boolean" in { + + evaluateExpression( + expression = "not(true)" + ) should returnResult(false) + + evaluateExpression( + expression = "not(false)" + ) should returnResult(true) + } + + "A is defined() function" should "return true if the value is present" in { + + evaluateExpression( + expression = "is defined(null)" + ) should returnResult(true) + + evaluateExpression( + expression = "is defined(1)" + ) should returnResult(true) + + evaluateExpression( + expression = "is defined(true)" + ) should returnResult(true) + + evaluateExpression( + expression = "is defined([])" + ) should returnResult(true) + + evaluateExpression( + expression = "is defined({})" + ) should returnResult(true) + + evaluateExpression( + expression = """ is defined( {"a":1}.a ) """ + ) should returnResult(true) + } + + // see: https://github.com/camunda/feel-scala/issues/695 + ignore should "return false if a variable doesn't exist" in { + + evaluateExpression( + expression = "is defined(a)" + ) should returnResult(false) + + evaluateExpression( + expression = "is defined(a.b)" + ) should returnResult(false) + } + + // see: https://github.com/camunda/feel-scala/issues/695 + ignore should "return false if a context entry doesn't exist" in { + + evaluateExpression( + expression = "is defined({}.a)" + ) should returnResult(false) + + evaluateExpression( + expression = "is defined({}.a.b)" + ) should returnResult(false) + } + + "A get or else(value: Any, default: Any) function" should "return the value if not null" in { + + evaluateExpression( + expression = "get or else(3, 1)", + ) should returnResult(3) + + evaluateExpression( + expression = """get or else("value", "default")""", + ) should returnResult("value") + + evaluateExpression( + expression = "get or else(value:3, default:1)", + ) should returnResult(3) + } + + it should "return the default param if value is null" in { + + evaluateExpression( + expression = "get or else(null, 1)", + ) should returnResult(1) + + evaluateExpression( + expression = """get or else(null, "default")""", + ) should returnResult("default") + + evaluateExpression( + expression = "get or else(value:null, default:1)", + ) should returnResult(1) + } + + it should "return null if both value and default params are null" in { + + evaluateExpression( + expression = "get or else(null, null)", + ) should returnNull() + + evaluateExpression( + expression = "get or else(value:null, default:null)", + ) should returnNull() + } + + "A assert(value: Any, condition: Any) function" should "return the value if the condition evaluated to true" in { + + evaluateExpression( + expression = """assert(x, x > 3)""", + variables = Map("x" -> 4) + ) should returnResult(4) + + evaluateExpression( + expression = """assert(x, x != null)""", + variables = Map("x" -> "value") + ) should returnResult("value") + + evaluateExpression( + expression = """assert(x, x = 3)""", + variables = Map("x" -> 3) + ) should returnResult(3) + + evaluateExpression( + expression = """assert(value: x, condition: x = 3)""", + variables = Map("x" -> 3) + ) should returnResult(3) + } + + it should "fail the evaluation if the condition is evaluated to false" in { + + evaluateExpression( + expression = """assert(x, x > 5)""", + variables = Map("x" -> 4) + ) should failWith("The condition is not fulfilled") + + evaluateExpression( + expression = """assert(x, x != null)""", + ) should failWith("The condition is not fulfilled") + + evaluateExpression( + expression = """assert(x, x > 5)""", + variables = Map("x" -> null) + ) should failWith("The condition is not fulfilled") + + evaluateExpression( + expression = """assert(x, x = 5)""", + variables = Map("x" -> 4) + ) should failWith("The condition is not fulfilled") + + evaluateExpression( + expression = """list contains(assert(my_list, my_list != null), 2)""" + ) should failWith("Assertion failure on evaluate the expression 'list contains(assert(my_list, my_list != null), 2)': The condition is not fulfilled") + } + + "A assert(value: Any, condition: Any, cause: String) function" should "return the value if the condition evaluated to true" in { + + evaluateExpression( + expression = """assert(x, x > 3, "The condition is not true")""", + variables = Map("x" -> 4) + ) should returnResult(4) + + evaluateExpression( + expression = """assert(x, x != null, "The condition is not true")""", + variables = Map("x" -> "value") + ) should returnResult("value") + + evaluateExpression( + expression = """assert(x, x = 3, "The condition is not true")""", + variables = Map("x" -> 3) + ) should returnResult(3) + + evaluateExpression( + expression = """assert(value: x, condition: x = 3, cause: "The condition is not true")""", + variables = Map("x" -> 3) + ) should returnResult(3) + } + + it should "fail the evaluation with custom message if the condition is evaluated to false" in { + + evaluateExpression( + expression = """assert(x, x > 5, "The condition is not true")""", + variables = Map("x" -> 4) + ) should failWith("The condition is not true") + + evaluateExpression( + expression = """assert(x, x != null, "The condition is not true")""", + ) should failWith("The condition is not true") + + evaluateExpression( + expression = """assert(x, x > 5, "The condition is not true")""", + variables = Map("x" -> null) + ) should failWith("The condition is not true") + + evaluateExpression( + expression = """assert(x, x = 5, "The condition is not true")""", + variables = Map("x" -> 4) + ) should failWith("The condition is not true") + + evaluateExpression( + expression = """list contains(assert(my_list, my_list != null, "The condition is not true"), 2)""" + ) should failWith("Assertion failure on evaluate the expression 'list contains(assert(my_list, my_list != null, \"The condition is not true\"), 2)': The condition is not true") + } +}