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")
+ }
+}