diff --git a/src/main/scala/org/camunda/feel/FeelEngine.scala b/src/main/scala/org/camunda/feel/FeelEngine.scala index e794a126a..46efab5e1 100644 --- a/src/main/scala/org/camunda/feel/FeelEngine.scala +++ b/src/main/scala/org/camunda/feel/FeelEngine.scala @@ -33,7 +33,7 @@ import org.camunda.feel.api.{ import org.camunda.feel.context.{Context, FunctionProvider} import org.camunda.feel.impl.interpreter.{BuiltinFunctions, EvalContext, FeelInterpreter} import org.camunda.feel.impl.parser.{ExpressionValidator, FeelParser} -import org.camunda.feel.syntaxtree.{Exp, ParsedExpression, ValError} +import org.camunda.feel.syntaxtree.{Exp, ParsedExpression, ValError, ValFatalError} import org.camunda.feel.valuemapper.ValueMapper.CompositeValueMapper import org.camunda.feel.valuemapper.{CustomValueMapper, ValueMapper} @@ -181,7 +181,14 @@ class FeelEngine( failure = Failure(s"failed to evaluate expression '${exp.text}': $cause"), suppressedFailures = context.failureCollector.failures ) - case value => + + case ValFatalError(cause) => + FailedEvaluationResult( + failure = Failure(s"failed to evaluate expression '${exp.text}': $cause"), + suppressedFailures = context.failureCollector.failures + ) + + case value => SuccessfulEvaluationResult( result = valueMapper.unpackVal(value), suppressedFailures = context.failureCollector.failures diff --git a/src/main/scala/org/camunda/feel/impl/DefaultValueMapper.scala b/src/main/scala/org/camunda/feel/impl/DefaultValueMapper.scala index 2598adc19..6b98e579b 100644 --- a/src/main/scala/org/camunda/feel/impl/DefaultValueMapper.scala +++ b/src/main/scala/org/camunda/feel/impl/DefaultValueMapper.scala @@ -35,6 +35,7 @@ import org.camunda.feel.syntaxtree.{ ValDateTime, ValDayTimeDuration, ValError, + ValFatalError, ValFunction, ValList, ValLocalDateTime, @@ -170,8 +171,9 @@ class DefaultValueMapper extends CustomValueMapper { }.toMap ) - case f: ValFunction => Some(f) - case e: ValError => Some(e) + case f: ValFunction => Some(f) + case e: ValError => Some(e) + case fatalError: ValFatalError => Some(fatalError) case _ => None } diff --git a/src/main/scala/org/camunda/feel/impl/builtin/BuiltinFunction.scala b/src/main/scala/org/camunda/feel/impl/builtin/BuiltinFunction.scala index ad79bc964..007fd632b 100644 --- a/src/main/scala/org/camunda/feel/impl/builtin/BuiltinFunction.scala +++ b/src/main/scala/org/camunda/feel/impl/builtin/BuiltinFunction.scala @@ -17,7 +17,7 @@ package org.camunda.feel.impl.builtin import org.camunda.feel.logger -import org.camunda.feel.syntaxtree.{Val, ValError, ValFunction, ValNull} +import org.camunda.feel.syntaxtree.{Val, ValError, ValFatalError, ValFunction, ValNull} object BuiltinFunction { @@ -34,9 +34,10 @@ object BuiltinFunction { } private def error: PartialFunction[List[Val], Any] = { - case args if (args.exists(_.isInstanceOf[ValError])) => - args.filter(_.isInstanceOf[ValError]).head.asInstanceOf[ValError] - case args => + case args if args.exists(_.isInstanceOf[ValFatalError]) => + args.find(_.isInstanceOf[ValFatalError]) + case args if args.exists(_.isInstanceOf[ValError]) => args.find(_.isInstanceOf[ValError]) + case args => val argumentList = args.map("'" + _ + "'").mkString(", ") ValError(s"Illegal arguments: $argumentList") } 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 54f4b14fe..0628b1f77 100644 --- a/src/main/scala/org/camunda/feel/impl/interpreter/FeelInterpreter.scala +++ b/src/main/scala/org/camunda/feel/impl/interpreter/FeelInterpreter.scala @@ -19,6 +19,7 @@ package org.camunda.feel.impl.interpreter import org.camunda.feel.FeelEngine.UnaryTests import org.camunda.feel.api.EvaluationFailureType import org.camunda.feel.context.Context +import org.camunda.feel.impl.interpreter.FeelInterpreter.INPUT_VALUE_SYMBOL import org.camunda.feel.syntaxtree._ import org.camunda.feel.valuemapper.ValueMapper import org.camunda.feel.{ @@ -33,6 +34,7 @@ import org.camunda.feel.{ } import java.time.{Duration, Period} +import scala.reflect.ClassTag /** @author * Philipp Ossler @@ -49,7 +51,7 @@ class FeelInterpreter { // literals case ConstNull => ValNull - case ConstInputValue => input + case ConstInputValue => getInputValueBySymbol case ConstNumber(x) => ValNumber(x) case ConstBool(b) => ValBoolean(b) case ConstString(s) => ValString(s) @@ -78,19 +80,19 @@ class FeelInterpreter { // simple unary tests case InputEqualTo(x) => - withVal(input, i => checkEquality(i, eval(x), _ == _, ValBoolean)) + withVal(getImplicitInputValue, i => checkEquality(i, eval(x), _ == _, ValBoolean)) case InputLessThan(x) => - withVal(input, i => dualOp(i, eval(x), _ < _, ValBoolean)) + withVal(getImplicitInputValue, i => dualOp(i, eval(x), _ < _, ValBoolean)) case InputLessOrEqual(x) => - withVal(input, i => dualOp(i, eval(x), _ <= _, ValBoolean)) + withVal(getImplicitInputValue, i => dualOp(i, eval(x), _ <= _, ValBoolean)) case InputGreaterThan(x) => - withVal(input, i => dualOp(i, eval(x), _ > _, ValBoolean)) + withVal(getImplicitInputValue, i => dualOp(i, eval(x), _ > _, ValBoolean)) case InputGreaterOrEqual(x) => - withVal(input, i => dualOp(i, eval(x), _ >= _, ValBoolean)) + withVal(getImplicitInputValue, i => dualOp(i, eval(x), _ >= _, ValBoolean)) case InputInRange(range @ ConstRange(start, end)) => unaryOpDual(eval(start.value), eval(end.value), isInRange(range), ValBoolean) - case UnaryTestExpression(x) => withVal(eval(x), unaryTestExpression) + case UnaryTestExpression(x) => unaryTestExpression(x) // arithmetic operations case Addition(x, y) => withValOrNull(addOp(eval(x), eval(y))) @@ -99,16 +101,17 @@ class FeelInterpreter { case Division(x, y) => withValOrNull(divOp(eval(x), eval(y))) case Exponentiation(x, y) => withValOrNull( - dualNumericOp( + withNumbers( eval(x), eval(y), - (x, y) => - if (y.isWhole) { + (x, y) => { + val result: Number = if (y.isWhole) { x.pow(y.toInt) } else { math.pow(x.toDouble, y.toDouble) - }, - ValNumber + } + ValNumber(result) + } ) ) case ArithmeticNegation(x) => @@ -139,19 +142,19 @@ class FeelInterpreter { } ) case In(x, test) => - withVal(eval(x), x => eval(test)(context.add(inputKey -> x))) + withVal(eval(x), x => eval(test)(context.add(getInputVariableName -> x))) case InstanceOf(x, typeName) => withVal( eval(x), x => { + val valueType = getTypeName(x.getClass) + typeName match { - case "Any" if x != ValNull => ValBoolean(true) - case "years and months duration" => - withType(x, t => ValBoolean(t == "year-month-duration")) - case "days and time duration" => - withType(x, t => ValBoolean(t == "day-time-duration")) - case "date and time" => withType(x, t => ValBoolean(t == "date time")) - case _ => withType(x, t => ValBoolean(t == typeName)) + case "Any" => ValBoolean(x != ValNull) + case "date time" => ValBoolean("date and time" == valueType) + case "year-month-duration" => ValBoolean("years and months duration" == valueType) + case "day-time-duration" => ValBoolean("days and time duration" == valueType) + case _ => ValBoolean(typeName == valueType) } } ) @@ -228,21 +231,23 @@ class FeelInterpreter { // functions 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 - case result => result - } + withValOrNull( + 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 + case result => result + } + ) ) case QualifiedFunctionInvocation(path, name, params) => withContext( @@ -276,6 +281,8 @@ class FeelInterpreter { } } + // ======== helpers ==================== + private def mapEither[T, R]( it: Iterable[T], f: T => Either[ValError, R], @@ -309,6 +316,8 @@ class FeelInterpreter { } } + // ========================================= + private def error(failureType: EvaluationFailureType, failureMessage: String)(implicit context: EvalContext ): ValError = { @@ -316,127 +325,131 @@ class FeelInterpreter { ValError(failureMessage) } - private def withValOrNull(x: Val): Val = x match { - case _: ValError => ValNull - case _ => x + private def withVal(x: Val, f: Val => Val): Val = x match { + case fatalError: ValFatalError => fatalError + case error: ValError => error + case value => f(value) } - private def unaryOpDual(x: Val, y: Val, c: (Val, Val, Val) => Boolean, f: Boolean => Val)(implicit - context: EvalContext - ): Val = - withVal( - input, - { - case i if !isComparable(i, x, y) || !hasSameType(i, x, y) => - error(EvaluationFailureType.NOT_COMPARABLE, s"Can't compare '$input' with '$x' and '$y'") - ValNull - case i => f(c(i, x, y)) - } - ) + private def withValOrNull(x: Val): Val = x.toOption.getOrElse(ValNull) - private def withNumbers(x: Val, y: Val, f: (Number, Number) => Val)(implicit - context: EvalContext - ): Val = - withNumber( - x, - x => { - withNumber( - y, - y => { - f(x, y) - } - ) - } - ) + private def withValues(x: Val, y: Val, f: (Val, Val) => Val) = + withVal(x, valueX => withVal(y, valueY => f(valueX, valueY))) - private def withNumber(x: Val, f: Number => Val)(implicit context: EvalContext): Val = x match { - case ValNumber(x) => f(x) - case _ => error(EvaluationFailureType.INVALID_TYPE, s"Expected number but found '$x'") + private def withValueType[T <: Val](value: Val, f: T => Val)(implicit + context: EvalContext, + tag: ClassTag[T] + ): Val = { + value match { + case fatalError: ValFatalError => fatalError + case error: ValError => error + case v: T if tag.runtimeClass.isInstance(value) => f(v) + case other => + error( + EvaluationFailureType.INVALID_TYPE, + s"Expected ${getTypeName(tag.runtimeClass)} but found '$other'" + ) + } } - private def withBoolean(x: Val, f: Boolean => Val)(implicit context: EvalContext): Val = x match { - case ValBoolean(x) => f(x) - case _ => error(EvaluationFailureType.INVALID_TYPE, s"Expected boolean but found '$x'") + private def getTypeName(valueType: Class[_]): String = valueType match { + case _ if valueType == ValNull.getClass => "null" + case _ if valueType == classOf[ValNumber] => "number" + case _ if valueType == classOf[ValBoolean] => "boolean" + case _ if valueType == classOf[ValString] => "string" + case _ if valueType == classOf[ValDate] => "date" + case _ if valueType == classOf[ValTime] => "time" + case _ if valueType == classOf[ValLocalTime] => "time" + case _ if valueType == classOf[ValDateTime] => "date and time" + case _ if valueType == classOf[ValLocalDateTime] => "date and time" + case _ if valueType == classOf[ValYearMonthDuration] => "years and months duration" + case _ if valueType == classOf[ValDayTimeDuration] => "days and time duration" + case _ if valueType == classOf[ValList] => "list" + case _ if valueType == classOf[ValContext] => "context" + case _ if valueType == classOf[ValFunction] => "function" + case _ if valueType == classOf[ValRange] => "range" + case _ if valueType == classOf[ValError] => "error" + case _ if valueType == classOf[ValFatalError] => "fatal error" + case other => other.getSimpleName } + private def isComparable(values: Val*): Boolean = values.forall(_.isComparable) + + private def hasSameType(values: Val*): Boolean = values.map(_.getClass).distinct.size == 1 + + // ======== type checks ==================== + + private def withNumber(x: Val, f: Number => Val)(implicit context: EvalContext): Val = + withValueType[ValNumber](x, number => f(number.value)) + + private def withNumbers(x: Val, y: Val, f: (Number, Number) => Val)(implicit + context: EvalContext + ): Val = + withNumber(x, x => withNumber(y, y => f(x, y))) + + private def withBoolean(x: Val, f: Boolean => Val)(implicit context: EvalContext): Val = + withValueType[ValBoolean](x, boolean => f(boolean.value)) + private def withBooleanOrNull(x: Val, f: Boolean => Val)(implicit context: EvalContext): Val = - x match { - case ValBoolean(x) => f(x) - case _ => - error(EvaluationFailureType.INVALID_TYPE, s"Expected boolean but found '$x'") - ValNull + withBoolean(x, f) match { + case _: ValError => ValNull + case value => value } private def withBooleanOrFalse(x: Val, f: Boolean => Val)(implicit context: EvalContext): Val = - x match { - case ValBoolean(x) => f(x) - case _ => - error(EvaluationFailureType.INVALID_TYPE, s"Expected boolean but found '$x'") - f(false) + withBoolean(x, f) match { + case _: ValError => f(false) + case value => value } - private def withString(x: Val, f: String => Val)(implicit context: EvalContext): Val = x match { - case ValString(x) => f(x) - case _ => error(EvaluationFailureType.INVALID_TYPE, s"Expected string but found '$x'") - } + private def withFunction(x: Val, f: ValFunction => Val)(implicit context: EvalContext): Val = + withValueType[ValFunction](x, f) - private def withDate(x: Val, f: Date => Val)(implicit context: EvalContext): Val = x match { - case ValDate(x) => f(x) - case _ => error(EvaluationFailureType.INVALID_TYPE, s"Expected date but found '$x'") - } + private def withList(x: Val, f: ValList => Val)(implicit context: EvalContext): Val = + withValueType[ValList](x, f) - private def withLocalTime(x: Val, f: LocalTime => Val)(implicit context: EvalContext): Val = - x match { - case ValLocalTime(x) => f(x) - case _ => error(EvaluationFailureType.INVALID_TYPE, s"Expected local time but found '$x'") - } + private def withContext(x: Val, f: ValContext => Val)(implicit context: EvalContext): Val = + withValueType[ValContext](x, f) - private def withTime(x: Val, f: Time => Val)(implicit context: EvalContext): Val = x match { - case ValTime(x) => f(x) - case _ => error(EvaluationFailureType.INVALID_TYPE, s"Expected time but found '$x'") - } + // ========================================= - private def withDateTime(x: Val, f: DateTime => Val)(implicit context: EvalContext): Val = - x match { - case ValDateTime(x) => f(x) - case _ => error(EvaluationFailureType.INVALID_TYPE, s"Expected date-and-time but found '$x'") + private def getInputVariableName(implicit context: EvalContext): String = { + context.variable(UnaryTests.inputVariable) match { + case ValString(inputVariableName) => inputVariableName + case _ => UnaryTests.defaultInputVariable } + } - private def withLocalDateTime(x: Val, f: LocalDateTime => Val)(implicit - context: EvalContext - ): Val = - x match { - case ValLocalDateTime(x) => f(x) - case _ => - error(EvaluationFailureType.INVALID_TYPE, s"Expected local date-and-time but found '$x'") + private def getImplicitInputValue(implicit context: EvalContext): Val = { + context.variable(getInputVariableName).toOption.getOrElse { + error(EvaluationFailureType.NO_VARIABLE_FOUND, "No input value found.") + ValNull } + } - private def withYearMonthDuration(x: Val, f: YearMonthDuration => Val)(implicit - context: EvalContext - ): Val = - x match { - case ValYearMonthDuration(x) => f(x) - case _ => - error(EvaluationFailureType.INVALID_TYPE, s"Expected years-months-duration but found '$x'") + private def getInputValueBySymbol(implicit context: EvalContext): Val = { + context.variable(INPUT_VALUE_SYMBOL).toOption.getOrElse { + ValFatalError( + s"No input value available. '$INPUT_VALUE_SYMBOL' can only be used inside an unary-test expression." + ) } + } - private def withDayTimeDuration(x: Val, f: DayTimeDuration => Val)(implicit + private def unaryOpDual(x: Val, y: Val, c: (Val, Val, Val) => Boolean, f: Boolean => Val)(implicit context: EvalContext ): Val = - x match { - case ValDayTimeDuration(x) => f(x) - case _ => - error(EvaluationFailureType.INVALID_TYPE, s"Expected days-time-duration but found '$x'") - } - - private def withVal(x: Val, f: Val => Val): Val = x match { - case e: ValError => e - case _ => f(x) - } - - private def isComparable(values: Val*): Boolean = values.forall(_.isComparable) - - private def hasSameType(values: Val*): Boolean = values.map(_.getClass).distinct.size == 1 + withVal( + getImplicitInputValue, + { + case inputValue if !isComparable(inputValue, x, y) || !hasSameType(inputValue, x, y) => + error( + EvaluationFailureType.NOT_COMPARABLE, + s"Can't compare '$getImplicitInputValue' with '$x' and '$y'" + ) + ValNull + case inputValue => f(c(inputValue, x, y)) + } + ) private def isInRange(range: ConstRange): (Val, Val, Val) => Boolean = (i, x, y) => { @@ -458,13 +471,15 @@ class FeelInterpreter { context: EvalContext ): Val = { items.foldLeft(f(false)) { - case (ValBoolean(true), _) => f(true) - case (ValNull, item) => + case (ValBoolean(true), _) => f(true) + case (fatalError: ValFatalError, _) => fatalError + case (ValNull, item) => item() match { - case ValBoolean(true) => f(true) - case _ => ValNull + case ValBoolean(true) => f(true) + case fatalError: ValFatalError => fatalError + case _ => ValNull } - case (_, item) => withBooleanOrNull(item(), f) + case (_, item) => withBooleanOrNull(item(), f) } } @@ -475,277 +490,250 @@ class FeelInterpreter { context: EvalContext ): Val = { items.foldLeft(f(true)) { - case (ValBoolean(false), _) => f(false) - case (ValNull, item) => + case (ValBoolean(false), _) => f(false) + case (fatalError: ValFatalError, _) => fatalError + case (ValNull, item) => item() match { - case ValBoolean(false) => f(false) - case _ => ValNull + case ValBoolean(false) => f(false) + case fatalError: ValFatalError => fatalError + case _ => ValNull } - case (_, item) => withBooleanOrNull(item(), f) + case (_, item) => withBooleanOrNull(item(), f) } } - private def inputKey(implicit context: EvalContext): String = - context.variable(UnaryTests.inputVariable) match { - case ValString(inputVariableName) => inputVariableName - case _ => UnaryTests.defaultInputVariable - } - - private def input(implicit context: EvalContext): Val = - context.variable(inputKey) match { - case _: ValError => - error(EvaluationFailureType.NO_VARIABLE_FOUND, s"No input value found.") - ValNull - case inputValue => inputValue - } - - private def dualNumericOp(x: Val, y: Val, op: (Number, Number) => Number, f: Number => Val)( - implicit context: EvalContext - ): Val = - x match { - case ValNumber(x) => withNumber(y, y => f(op(x, y))) - case _ => error(EvaluationFailureType.INVALID_TYPE, s"Expected number but found '$x'") - } - private def checkEquality(x: Val, y: Val, c: (Any, Any) => Boolean, f: Boolean => Val)(implicit context: EvalContext ): Val = - x match { - case ValNull => f(c(ValNull, y.toOption.getOrElse(ValNull))) - case x if (y == ValNull) => f(c(x.toOption.getOrElse(ValNull), ValNull)) - case _: ValError => f(c(ValNull, y.toOption.getOrElse(ValNull))) - case _ if (y.isInstanceOf[ValError]) => f(c(ValNull, x.toOption.getOrElse(ValNull))) - case _ if !hasSameType(x, y) => - error(EvaluationFailureType.NOT_COMPARABLE, s"Can't compare '$x' with '$y'") - ValNull - case ValNumber(x) => withNumber(y, y => f(c(x, y))) - case ValBoolean(x) => withBoolean(y, y => f(c(x, y))) - case ValString(x) => withString(y, y => f(c(x, y))) - case ValDate(x) => withDate(y, y => f(c(x, y))) - case ValLocalTime(x) => withLocalTime(y, y => f(c(x, y))) - case ValTime(x) => withTime(y, y => f(c(x, y))) - case ValLocalDateTime(x) => withLocalDateTime(y, y => f(c(x, y))) - case ValDateTime(x) => withDateTime(y, y => f(c(x, y))) - case ValYearMonthDuration(x) => withYearMonthDuration(y, y => f(c(x, y))) - case ValDayTimeDuration(x) => withDayTimeDuration(y, y => f(c(x, y))) - case ValList(x) => - withList( - y, - y => { - if (x.size != y.items.size) { - f(false) - - } else { - val isEqual = x.zip(y.items).foldRight(true) { case ((x, y), listIsEqual) => - listIsEqual && { - checkEquality(x, y, c, f) match { - case ValBoolean(itemIsEqual) => itemIsEqual - case _ => false - } + withValues( + x, + y, + { + case (ValNull, _) => f(c(ValNull, y.toOption.getOrElse(ValNull))) + case (_, ValNull) => f(c(x.toOption.getOrElse(ValNull), ValNull)) + case (ValNumber(x), ValNumber(y)) => f(c(x, y)) + case (ValBoolean(x), ValBoolean(y)) => f(c(x, y)) + case (ValString(x), ValString(y)) => f(c(x, y)) + case (ValDate(x), ValDate(y)) => f(c(x, y)) + case (ValLocalTime(x), ValLocalTime(y)) => f(c(x, y)) + case (ValTime(x), ValTime(y)) => f(c(x, y)) + case (ValLocalDateTime(x), ValLocalDateTime(y)) => f(c(x, y)) + case (ValDateTime(x), ValDateTime(y)) => f(c(x, y)) + case (ValYearMonthDuration(x), ValYearMonthDuration(y)) => f(c(x, y)) + case (ValDayTimeDuration(x), ValDayTimeDuration(y)) => f(c(x, y)) + case (ValList(x), ValList(y)) => + if (x.size != y.size) { + f(false) + + } else { + val isEqual = x.zip(y).foldRight(true) { case ((x, y), listIsEqual) => + listIsEqual && { + checkEquality(x, y, c, f) match { + case ValBoolean(itemIsEqual) => itemIsEqual + case _ => false } } - f(isEqual) } + f(isEqual) } - ) - case ValContext(x) => - withContext( - y, - y => { - val xVars = x.variableProvider.getVariables - val yVars = y.context.variableProvider.getVariables - - if (xVars.keys != yVars.keys) { - f(false) - - } else { - val isEqual = xVars.keys.foldRight(true) { case (key, contextIsEqual) => - contextIsEqual && { - val xVal = context.valueMapper.toVal(xVars(key)) - val yVal = context.valueMapper.toVal(yVars(key)) - - checkEquality(xVal, yVal, c, f) match { - case ValBoolean(entryIsEqual) => entryIsEqual - case _ => false - } + case (ValContext(x), ValContext(y)) => + val xVars = x.variableProvider.getVariables + val yVars = y.variableProvider.getVariables + + if (xVars.keys != yVars.keys) { + f(false) + + } else { + val isEqual = xVars.keys.foldRight(true) { case (key, contextIsEqual) => + contextIsEqual && { + val xVal = context.valueMapper.toVal(xVars(key)) + val yVal = context.valueMapper.toVal(yVars(key)) + + checkEquality(xVal, yVal, c, f) match { + case ValBoolean(entryIsEqual) => entryIsEqual + case _ => false } } - f(isEqual) } + f(isEqual) } - ) - case _ => - error(EvaluationFailureType.NOT_COMPARABLE, s"Can't compare '$x' with '$y'") - ValNull - } + case _ => + error(EvaluationFailureType.NOT_COMPARABLE, s"Can't compare '$x' with '$y'") + ValNull + } + ) private def dualOp(x: Val, y: Val, c: (Val, Val) => Boolean, f: Boolean => Val)(implicit context: EvalContext - ): Val = - x match { - case _ if !isComparable(x, y) || !hasSameType(x, y) => - error(EvaluationFailureType.NOT_COMPARABLE, s"Can't compare '$x' with '$y'") - ValNull - case _ => f(c(x, y)) - } - - private def addOp(x: Val, y: Val)(implicit context: EvalContext): Val = x match { - case ValNumber(x) => withNumber(y, y => ValNumber(x + y)) - case ValString(x) => withString(y, y => ValString(x + y)) - case ValLocalTime(x) => withDayTimeDuration(y, y => ValLocalTime(x.plus(y))) - case ValTime(x) => withDayTimeDuration(y, y => ValTime(x.plus(y))) - case ValLocalDateTime(x) => - y match { - case ValYearMonthDuration(y) => ValLocalDateTime(x.plus(y)) - case ValDayTimeDuration(y) => ValLocalDateTime(x.plus(y)) - case _ => error(EvaluationFailureType.INVALID_TYPE, s"Can't add '$y' to '$x'") - } - case ValDateTime(x) => - y match { - case ValYearMonthDuration(y) => ValDateTime(x.plus(y)) - case ValDayTimeDuration(y) => ValDateTime(x.plus(y)) - case _ => error(EvaluationFailureType.INVALID_TYPE, s"Can't add '$y' to '$x'") - } - case ValYearMonthDuration(x) => - y match { - case ValYearMonthDuration(y) => - ValYearMonthDuration(x.plus(y).normalized) - case ValLocalDateTime(y) => ValLocalDateTime(y.plus(x)) - case ValDateTime(y) => ValDateTime(y.plus(x)) - case ValDate(y) => ValDate(y.plus(x)) - case _ => error(EvaluationFailureType.INVALID_TYPE, s"Can't add '$y' to '$x'") - } - case ValDayTimeDuration(x) => - y match { - case ValDayTimeDuration(y) => ValDayTimeDuration(x.plus(y)) - case ValLocalDateTime(y) => ValLocalDateTime(y.plus(x)) - case ValDateTime(y) => ValDateTime(y.plus(x)) - case ValLocalTime(y) => ValLocalTime(y.plus(x)) - case ValTime(y) => ValTime(y.plus(x)) - case ValDate(y) => ValDate(y.atStartOfDay().plus(x).toLocalDate()) - case _ => error(EvaluationFailureType.INVALID_TYPE, s"Can't add '$y' to '$x'") - } - case ValDate(x) => - y match { - case ValDayTimeDuration(y) => - ValDate(x.atStartOfDay().plus(y).toLocalDate()) - case ValYearMonthDuration(y) => ValDate(x.plus(y)) - case _ => error(EvaluationFailureType.INVALID_TYPE, s"Can't add '$y' to '$x'") + ): Val = { + withValues( + x, + y, + { + case _ if !isComparable(x, y) || !hasSameType(x, y) => + error(EvaluationFailureType.NOT_COMPARABLE, s"Can't compare '$x' with '$y'") + ValNull + case _ => f(c(x, y)) } - case _ => error(EvaluationFailureType.INVALID_TYPE, s"Can't add '$y' to '$x'") + ) } - private def subOp(x: Val, y: Val)(implicit context: EvalContext): Val = x match { - case ValNumber(x) => withNumber(y, y => ValNumber(x - y)) - case ValLocalTime(x) => - y match { - case ValLocalTime(y) => ValDayTimeDuration(Duration.between(y, x)) - case ValDayTimeDuration(y) => ValLocalTime(x.minus(y)) - case _ => error(EvaluationFailureType.INVALID_TYPE, s"Can't subtract '$y' from '$x'") - } - case ValTime(x) => - y match { - case ValTime(y) => ValDayTimeDuration(ZonedTime.between(x, y)) - case ValDayTimeDuration(y) => ValTime(x.minus(y)) - case _ => error(EvaluationFailureType.INVALID_TYPE, s"Can't subtract '$y' from '$x'") - } - case ValLocalDateTime(x) => - y match { - case ValLocalDateTime(y) => ValDayTimeDuration(Duration.between(y, x)) - case ValYearMonthDuration(y) => ValLocalDateTime(x.minus(y)) - case ValDayTimeDuration(y) => ValLocalDateTime(x.minus(y)) - case _ => error(EvaluationFailureType.INVALID_TYPE, s"Can't subtract '$y' from '$x'") - } - case ValDateTime(x) => - y match { - case ValDateTime(y) => ValDayTimeDuration(Duration.between(y, x)) - case ValYearMonthDuration(y) => ValDateTime(x.minus(y)) - case ValDayTimeDuration(y) => ValDateTime(x.minus(y)) - case _ => error(EvaluationFailureType.INVALID_TYPE, s"Can't subtract '$y' from '$x'") + private def addOp(x: Val, y: Val)(implicit context: EvalContext): Val = + withValues( + x, + y, + { + case (ValNumber(x), ValNumber(y)) => ValNumber(x + y) + case (ValString(x), ValString(y)) => ValString(x + y) + + case (ValLocalTime(x), ValDayTimeDuration(y)) => ValLocalTime(x.plus(y)) + case (ValTime(x), ValDayTimeDuration(y)) => ValTime(x.plus(y)) + + case (ValLocalDateTime(x), ValYearMonthDuration(y)) => ValLocalDateTime(x.plus(y)) + case (ValLocalDateTime(x), ValDayTimeDuration(y)) => ValLocalDateTime(x.plus(y)) + case (ValDateTime(x), ValYearMonthDuration(y)) => ValDateTime(x.plus(y)) + case (ValDateTime(x), ValDayTimeDuration(y)) => ValDateTime(x.plus(y)) + + case (ValYearMonthDuration(x), ValYearMonthDuration(y)) => + ValYearMonthDuration(x.plus(y).normalized) + case (ValYearMonthDuration(x), ValLocalDateTime(y)) => ValLocalDateTime(y.plus(x)) + case (ValYearMonthDuration(x), ValDateTime(y)) => ValDateTime(y.plus(x)) + case (ValYearMonthDuration(x), ValDate(y)) => ValDate(y.plus(x)) + + case (ValDayTimeDuration(x), ValDayTimeDuration(y)) => ValDayTimeDuration(x.plus(y)) + case (ValDayTimeDuration(x), ValLocalDateTime(y)) => ValLocalDateTime(y.plus(x)) + case (ValDayTimeDuration(x), ValDateTime(y)) => ValDateTime(y.plus(x)) + case (ValDayTimeDuration(x), ValLocalTime(y)) => ValLocalTime(y.plus(x)) + case (ValDayTimeDuration(x), ValTime(y)) => ValTime(y.plus(x)) + case (ValDayTimeDuration(x), ValDate(y)) => ValDate(y.atStartOfDay().plus(x).toLocalDate) + + case (ValDate(x), ValDayTimeDuration(y)) => ValDate(x.atStartOfDay().plus(y).toLocalDate) + case (ValDate(x), ValYearMonthDuration(y)) => ValDate(x.plus(y)) + + case _ => error(EvaluationFailureType.INVALID_TYPE, s"Can't add '$y' to '$x'") } - case ValDate(x) => - y match { - case ValDate(y) => + ) + + private def subOp(x: Val, y: Val)(implicit context: EvalContext): Val = + withValues( + x, + y, + { + case (ValNumber(x), ValNumber(y)) => ValNumber(x - y) + + case (ValLocalTime(x), ValLocalTime(y)) => ValDayTimeDuration(Duration.between(y, x)) + case (ValLocalTime(x), ValDayTimeDuration(y)) => ValLocalTime(x.minus(y)) + + case (ValTime(x), ValTime(y)) => ValDayTimeDuration(ZonedTime.between(x, y)) + case (ValTime(x), ValDayTimeDuration(y)) => ValTime(x.minus(y)) + + case (ValLocalDateTime(x), ValLocalDateTime(y)) => + ValDayTimeDuration(Duration.between(y, x)) + case (ValLocalDateTime(x), ValYearMonthDuration(y)) => ValLocalDateTime(x.minus(y)) + case (ValLocalDateTime(x), ValDayTimeDuration(y)) => ValLocalDateTime(x.minus(y)) + + case (ValDateTime(x), ValDateTime(y)) => ValDayTimeDuration(Duration.between(y, x)) + case (ValDateTime(x), ValYearMonthDuration(y)) => ValDateTime(x.minus(y)) + case (ValDateTime(x), ValDayTimeDuration(y)) => ValDateTime(x.minus(y)) + + case (ValDate(x), ValDate(y)) => ValDayTimeDuration(Duration.between(y.atStartOfDay, x.atStartOfDay)) - case ValYearMonthDuration(y) => ValDate(x.minus(y)) - case ValDayTimeDuration(y) => - ValDate(x.atStartOfDay.minus(y).toLocalDate()) - case _ => error(EvaluationFailureType.INVALID_TYPE, s"Can't subtract '$y' from '$x'") + case (ValDate(x), ValYearMonthDuration(y)) => ValDate(x.minus(y)) + case (ValDate(x), ValDayTimeDuration(y)) => ValDate(x.atStartOfDay.minus(y).toLocalDate) + + case (ValYearMonthDuration(x), ValYearMonthDuration(y)) => + ValYearMonthDuration(x.minus(y).normalized) + case (ValDayTimeDuration(x), ValDayTimeDuration(y)) => ValDayTimeDuration(x.minus(y)) + + case _ => error(EvaluationFailureType.INVALID_TYPE, s"Can't subtract '$y' from '$x'") } - case ValYearMonthDuration(x) => - withYearMonthDuration(y, y => ValYearMonthDuration(x.minus(y).normalized)) - case ValDayTimeDuration(x) => - withDayTimeDuration(y, y => ValDayTimeDuration(x.minus(y))) - case _ => error(EvaluationFailureType.INVALID_TYPE, s"Can't subtract '$y' from '$x'") - } + ) - private def mulOp(x: Val, y: Val)(implicit context: EvalContext): Val = x match { - case ValNumber(x) => - y match { - case ValNumber(y) => ValNumber(x * y) - case ValYearMonthDuration(y) => + private def mulOp(x: Val, y: Val)(implicit context: EvalContext): Val = + withValues( + x, + y, + { + case (ValNumber(x), ValNumber(y)) => ValNumber(x * y) + case (ValNumber(x), ValYearMonthDuration(y)) => ValYearMonthDuration(y.multipliedBy(x.intValue).normalized) - case ValDayTimeDuration(y) => - ValDayTimeDuration(y.multipliedBy(x.intValue)) - case _ => error(EvaluationFailureType.INVALID_TYPE, s"Can't multiply '$x' by '$y'") - } - case ValYearMonthDuration(x) => - withNumber(y, y => ValYearMonthDuration(x.multipliedBy(y.intValue).normalized)) - case ValDayTimeDuration(x) => - withNumber(y, y => ValDayTimeDuration(x.multipliedBy(y.intValue))) - case _ => error(EvaluationFailureType.INVALID_TYPE, s"Can't multiply '$x' by '$y'") - } + case (ValNumber(x), ValDayTimeDuration(y)) => ValDayTimeDuration(y.multipliedBy(x.intValue)) + + case (ValYearMonthDuration(x), ValNumber(y)) => + ValYearMonthDuration(x.multipliedBy(y.intValue).normalized) + case (ValDayTimeDuration(x), ValNumber(y)) => ValDayTimeDuration(x.multipliedBy(y.intValue)) - private def divOp(x: Val, y: Val)(implicit context: EvalContext): Val = y match { - case ValNumber(y) if (y != 0) => - x match { - case ValNumber(x) => ValNumber(x / y) - case ValYearMonthDuration(x) => - ValYearMonthDuration(Period.ofMonths((x.toTotalMonths() / y).intValue).normalized) - case ValDayTimeDuration(x) => - ValDayTimeDuration(Duration.ofMillis((x.toMillis() / y).intValue)) - case _ => error(EvaluationFailureType.INVALID_TYPE, s"Can't divide '$x' by '$y'") + case _ => error(EvaluationFailureType.INVALID_TYPE, s"Can't multiply '$x' by '$y'") } + ) - case ValYearMonthDuration(y) if (!y.isZero) => - withYearMonthDuration(x, x => ValNumber(x.toTotalMonths / y.toTotalMonths)) - case ValDayTimeDuration(y) if (!y.isZero) => - withDayTimeDuration(x, x => ValNumber(x.toMillis / y.toMillis)) + private def divOp(x: Val, y: Val)(implicit context: EvalContext): Val = + withValues( + x, + y, + { + case (ValNumber(x), ValNumber(y)) if (y != 0) => ValNumber(x / y) - case _ => error(EvaluationFailureType.INVALID_TYPE, s"Can't divide '$x' by '$y'") - } + case (ValYearMonthDuration(x), ValNumber(y)) if (y != 0) => + ValYearMonthDuration(Period.ofMonths((x.toTotalMonths / y).intValue).normalized) + case (ValYearMonthDuration(x), ValYearMonthDuration(y)) if (!y.isZero) => + ValNumber(x.toTotalMonths / y.toTotalMonths) + + case (ValDayTimeDuration(x), ValDayTimeDuration(y)) if (!y.isZero) => + ValNumber(x.toMillis / y.toMillis) + case (ValDayTimeDuration(x), ValNumber(y)) if (y != 0) => + ValDayTimeDuration(Duration.ofMillis((x.toMillis / y).intValue)) - private def unaryTestExpression(x: Val)(implicit context: EvalContext): Val = + case _ => error(EvaluationFailureType.INVALID_TYPE, s"Can't divide '$x' by '$y'") + } + ) + + private def unaryTestExpression(expression: Exp)(implicit context: EvalContext): Val = { withVal( - input, - i => - x match { - case ValBoolean(true) => ValBoolean(true) // the expression is true - case ValList(ys) if ys.contains(i) => - ValBoolean(true) // the expression contains the input value - case _ => - checkEquality(i, x, _ == _, ValBoolean) match { - case ValBoolean(true) => ValBoolean(true) // the expression is the input value - case _ if x == ValBoolean(false) => ValBoolean(false) // the expression is false + getImplicitInputValue, + inputValue => + eval(expression) match { + case _: ValFatalError => + eval(expression)(context.add(INPUT_VALUE_SYMBOL -> inputValue)) match { + case ValBoolean(true) => ValBoolean(true) + case ValBoolean(false) => ValBoolean(false) + case _ => ValNull + } + case ValBoolean(true) => ValBoolean(true) + case ValList(ys) if ys.contains(inputValue) => + // the expression contains the input value + ValBoolean(true) + case x => + checkEquality(inputValue, x, _ == _, ValBoolean) match { + case ValBoolean(true) => + // the expression is the input value + ValBoolean(true) case _ if x.isInstanceOf[ValList] => - ValBoolean(false) // the expression is a list but doesn't contain the input value - case ValNull => ValNull // the expression can't be compared to the input value - case _ => ValBoolean(false) // the expression is not the input value + // the expression is a list but doesn't contain the input value + ValBoolean(false) + case ValNull => ValNull + case _ => + // the expression is not the input value + ValBoolean(false) } } ) + } - private def withFunction(x: Val, f: ValFunction => Val)(implicit context: EvalContext): Val = - x match { - case x: ValFunction => f(x) - case ValError(failure) => - error(EvaluationFailureType.NO_FUNCTION_FOUND, failure) - ValNull - case _ => - error(EvaluationFailureType.INVALID_TYPE, s"Expected function but found '$x'") - ValNull + private def findFunction(ctx: EvalContext, name: String, params: FunctionParameters)(implicit + context: EvalContext + ): Val = { + val function = params match { + case PositionalFunctionParameters(params) => ctx.function(name, params.size) + case NamedFunctionParameters(params) => ctx.function(name, params.keySet) } + function match { + case ValError(failure) => error(EvaluationFailureType.NO_FUNCTION_FOUND, failure) + case _ => function + } + } + private def invokeFunction(function: ValFunction, params: FunctionParameters)(implicit context: EvalContext ): Val = { @@ -777,40 +765,10 @@ class FeelInterpreter { } function.invoke(paramList) match { - case e: ValError => e - case result => context.valueMapper.toVal(result) - } - } - - private def findFunction(ctx: EvalContext, name: String, params: FunctionParameters): Val = - params match { - case PositionalFunctionParameters(params) => ctx.function(name, params.size) - case NamedFunctionParameters(params) => ctx.function(name, params.keySet) + case fatalError: ValFatalError => fatalError + case e: ValError => e + case result => context.valueMapper.toVal(result) } - - private def withType(x: Val, f: String => ValBoolean)(implicit context: EvalContext): Val = - x match { - case ValNumber(_) => f("number") - case ValBoolean(_) => f("boolean") - case ValString(_) => f("string") - case ValDate(_) => f("date") - case ValLocalTime(_) => f("time") - case ValTime(_) => f("time") - case ValLocalDateTime(_) => f("date time") - case ValDateTime(_) => f("date time") - case ValYearMonthDuration(_) => f("year-month-duration") - case ValDayTimeDuration(_) => f("day-time-duration") - case ValNull => f("null") - case ValList(_) => f("list") - case ValContext(_) => f("context") - case ValFunction(_, _, _) => f("function") - case _ => - error(EvaluationFailureType.INVALID_TYPE, s"Unknown type '${x.getClass.getName}' of '$x'") - } - - private def withList(x: Val, f: ValList => Val)(implicit context: EvalContext): Val = x match { - case x: ValList => f(x) - case _ => error(EvaluationFailureType.INVALID_TYPE, s"Expected list but found '$x'") } private def withLists(lists: List[(String, Val)], f: List[(String, ValList)] => Val)(implicit @@ -818,20 +776,21 @@ class FeelInterpreter { ): Val = { lists .map { case (name, it) => name -> withList(it, list => list) } - .find(_._2.isInstanceOf[ValError]) match { - case Some(Tuple2(_, e: Val)) => e - case None => f(lists.asInstanceOf[List[(String, ValList)]]) + .find { case (_, value) => !value.isInstanceOf[ValList] } match { + case Some(Tuple2(_, error: Val)) => error + case None => f(lists.asInstanceOf[List[(String, ValList)]]) } } private def withCartesianProduct( iterators: List[(String, Exp)], f: List[Map[String, Val]] => Val - )(implicit context: EvalContext): Val = + )(implicit context: EvalContext): Val = { withLists( iterators.map { case (name, it) => name -> eval(it) }, lists => f(flattenAndZipLists(lists)) ) + } private def flattenAndZipLists(lists: List[(String, ValList)]): List[Map[String, Val]] = lists match { @@ -909,12 +868,6 @@ class FeelInterpreter { } } - private def withContext(x: Val, f: ValContext => Val)(implicit context: EvalContext): Val = - x match { - case x: ValContext => f(x) - case _ => error(EvaluationFailureType.INVALID_TYPE, s"Expect context but found '$x'") - } - private def filterContext(x: Val)(implicit context: EvalContext): EvalContext = x match { case ValContext(ctx: Context) => context.add("item" -> x).merge(ctx) @@ -966,9 +919,6 @@ class FeelInterpreter { } } - private def evalContextEntry(key: String, exp: Exp)(implicit context: EvalContext): Val = - withVal(eval(exp), value => value) - private def invokeJavaFunction( className: String, methodName: String, @@ -1012,21 +962,19 @@ class FeelInterpreter { } private def toRange(range: ConstRange)(implicit context: EvalContext): Val = { - withVal( + withValues( eval(range.start.value), - startValue => - withVal( - eval(range.end.value), - endValue => - if (isValidRange(startValue, endValue)) { - ValRange( - start = toRangeBoundary(range.start, startValue), - end = toRangeBoundary(range.end, endValue) - ) - } else { - error(EvaluationFailureType.INVALID_TYPE, s"Invalid range definition '$range'") - } - ) + eval(range.end.value), + (startValue, endValue) => { + if (isValidRange(startValue, endValue)) { + ValRange( + start = toRangeBoundary(range.start, startValue), + end = toRangeBoundary(range.end, endValue) + ) + } else { + error(EvaluationFailureType.INVALID_TYPE, s"Invalid range definition '$range'") + } + } ) } @@ -1051,3 +999,9 @@ class FeelInterpreter { } } + +object FeelInterpreter { + + val INPUT_VALUE_SYMBOL: String = "?" + +} diff --git a/src/main/scala/org/camunda/feel/syntaxtree/Val.scala b/src/main/scala/org/camunda/feel/syntaxtree/Val.scala index fda5327a3..0846d5f80 100644 --- a/src/main/scala/org/camunda/feel/syntaxtree/Val.scala +++ b/src/main/scala/org/camunda/feel/syntaxtree/Val.scala @@ -94,13 +94,15 @@ sealed trait Val extends Ordered[Val] { } def toEither: Either[ValError, Val] = this match { - case e: ValError => Left(e) - case v => Right(v) + case e: ValError => Left(e) + case e: ValFatalError => Left(ValError(e.toString)) + case v => Right(v) } def toOption: Option[Val] = this match { - case e: ValError => None - case v => Some(v) + case _: ValError => None + case fatalError: ValFatalError => Some(fatalError) + case v => Some(v) } } @@ -287,6 +289,10 @@ case class ValError(error: String) extends Val { override def toString: String = s"error(\"$error\")" } +case class ValFatalError(error: String) extends Val { + override def toString: String = s"fatal error(\"$error\")" +} + case object ValNull extends Val { override def toString: String = "null" } diff --git a/src/test/scala/org/camunda/feel/api/StringRepresentationTypeTest.scala b/src/test/scala/org/camunda/feel/api/StringRepresentationTypeTest.scala index 817d50b4f..1b259e3f2 100644 --- a/src/test/scala/org/camunda/feel/api/StringRepresentationTypeTest.scala +++ b/src/test/scala/org/camunda/feel/api/StringRepresentationTypeTest.scala @@ -25,6 +25,7 @@ import org.camunda.feel.syntaxtree.{ ValDateTime, ValDayTimeDuration, ValError, + ValFatalError, ValFunction, ValList, ValLocalDateTime, @@ -201,4 +202,10 @@ class StringRepresentationTypeTest extends AnyFlatSpec with Matchers { error.toString should be("error(\"something wrong\")") } + "A fatal error" should """return 'fatal error("something wrong")' """ in { + val fatalError = ValFatalError("something wrong") + + fatalError.toString should be("fatal error(\"something wrong\")") + } + } diff --git a/src/test/scala/org/camunda/feel/impl/FeelEngineTest.scala b/src/test/scala/org/camunda/feel/impl/FeelEngineTest.scala index 54a13381d..7ad81fc8d 100644 --- a/src/test/scala/org/camunda/feel/impl/FeelEngineTest.scala +++ b/src/test/scala/org/camunda/feel/impl/FeelEngineTest.scala @@ -16,15 +16,26 @@ */ package org.camunda.feel.impl +import org.camunda.feel.{ + Date, + DateTime, + DayTimeDuration, + LocalDateTime, + LocalTime, + Time, + YearMonthDuration +} import org.camunda.feel.api.{ EvaluationResult, + FailedEvaluationResult, FeelEngineApi, FeelEngineBuilder, - SuccessfulEvaluationResult, - FailedEvaluationResult + SuccessfulEvaluationResult } import org.camunda.feel.context.Context -import org.camunda.feel.syntaxtree.ValFunction +import org.camunda.feel.syntaxtree.{ValFunction, ZonedTime} + +import java.time.{Duration, LocalDate, LocalDateTime, LocalTime, Period, ZonedDateTime} trait FeelEngineTest { @@ -97,4 +108,18 @@ trait FeelEngineTest { } } + def date(x: String): Date = LocalDate.parse(x) + + def localTime(x: String): LocalTime = LocalTime.parse(x) + + def time(x: String): Time = ZonedTime.parse(x) + + def dateTime(x: String): DateTime = ZonedDateTime.parse(x) + + def localDateTime(x: String): LocalDateTime = LocalDateTime.parse(x) + + def yearMonthDuration(x: String): YearMonthDuration = Period.parse(x) + + def dayTimeDuration(x: String): DayTimeDuration = Duration.parse(x) + } diff --git a/src/test/scala/org/camunda/feel/impl/SuppressedFailuresTest.scala b/src/test/scala/org/camunda/feel/impl/SuppressedFailuresTest.scala index ab66f09b7..310c20be5 100644 --- a/src/test/scala/org/camunda/feel/impl/SuppressedFailuresTest.scala +++ b/src/test/scala/org/camunda/feel/impl/SuppressedFailuresTest.scala @@ -65,7 +65,7 @@ class SuppressedFailuresTest it should "report a suppressed failure if an addition has incompatible values" in { evaluateExpression("2 + true") should reportFailure( failureType = EvaluationFailureType.INVALID_TYPE, - failureMessage = "Expected number but found 'true'" + failureMessage = "Can't add 'true' to '2'" ) } @@ -109,7 +109,7 @@ class SuppressedFailuresTest ), EvaluationFailure( failureType = EvaluationFailureType.INVALID_TYPE, - failureMessage = "Expected number but found 'null'" + failureMessage = "Can't add 'null' to '1'" ) ) } diff --git a/src/test/scala/org/camunda/feel/impl/interpreter/InterpreterExpressionTest.scala b/src/test/scala/org/camunda/feel/impl/interpreter/InterpreterExpressionTest.scala index f20c29dc6..5eab431b1 100644 --- a/src/test/scala/org/camunda/feel/impl/interpreter/InterpreterExpressionTest.scala +++ b/src/test/scala/org/camunda/feel/impl/interpreter/InterpreterExpressionTest.scala @@ -17,8 +17,7 @@ package org.camunda.feel.impl.interpreter import org.camunda.feel.FeelEngine.UnaryTests -import org.camunda.feel.impl.FeelIntegrationTest -import org.camunda.feel.impl.interpreter.EvaluationErrorMatcher._ +import org.camunda.feel.impl.{EvaluationResultMatchers, FeelEngineTest} import org.camunda.feel.syntaxtree._ import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers @@ -26,261 +25,276 @@ import org.scalatest.matchers.should.Matchers /** @author * Philipp Ossler */ -class InterpreterExpressionTest extends AnyFlatSpec with Matchers with FeelIntegrationTest { +class InterpreterExpressionTest + extends AnyFlatSpec + with Matchers + with FeelEngineTest + with EvaluationResultMatchers { "An expression" should "be an if-then-else (with parentheses)" in { val exp = """ if (x < 5) then "low" else "high" """ - eval(exp, Map("x" -> 2)) should be(ValString("low")) - eval(exp, Map("x" -> 7)) should be(ValString("high")) + evaluateExpression(exp, Map("x" -> 2)) should returnResult("low") + evaluateExpression(exp, Map("x" -> 7)) should returnResult("high") - eval(exp, Map("x" -> "foo")) should be(ValString("high")) + evaluateExpression(exp, Map("x" -> "foo")) should returnResult("high") } it should "be an if-then-else (without parentheses)" in { - eval("if x < 5 then 1 else 2", Map("x" -> 2)) should be(ValNumber(1)) + evaluateExpression("if x < 5 then 1 else 2", Map("x" -> 2)) should returnResult(1) } it should "be an if-then-else (with literal)" in { - eval("if true then 1 else 2") should be(ValNumber(1)) + evaluateExpression("if true then 1 else 2") should returnResult(1) } it should "be an if-then-else (with path)" in { - eval("if {a: true}.a then 1 else 2") should be(ValNumber(1)) + evaluateExpression("if {a: true}.a then 1 else 2") should returnResult(1) } it should "be an if-then-else (with filter)" in { - eval("if [true][1] then 1 else 2") should be(ValNumber(1)) + evaluateExpression("if [true][1] then 1 else 2") should returnResult(1) } it should "be an if-then-else (with conjunction)" in { - eval("if true and true then 1 else 2") should be(ValNumber(1)) + evaluateExpression("if true and true then 1 else 2") should returnResult(1) } it should "be an if-then-else (with disjunction)" in { - eval("if false or true then 1 else 2") should be(ValNumber(1)) + evaluateExpression("if false or true then 1 else 2") should returnResult(1) } it should "be an if-then-else (with in-test)" in { - eval("if 1 in < 5 then 1 else 2") should be(ValNumber(1)) + evaluateExpression("if 1 in < 5 then 1 else 2") should returnResult(1) } it should "be an if-then-else (with instance of)" in { - eval("if 1 instance of number then 1 else 2") should be(ValNumber(1)) + evaluateExpression("if 1 instance of number then 1 else 2") should returnResult(1) } it should "be an if-then-else (with variable and function call -> then)" in { - eval("if 7 > var then flatten(xs) else []", Map("xs" -> List(1, 2), "var" -> 3)) should be( - ValList(List(ValNumber(1), ValNumber(2))) - ) + evaluateExpression( + "if 7 > var then flatten(xs) else []", + Map("xs" -> List(1, 2), "var" -> 3) + ) should returnResult(List(1, 2)) } it should "be an if-then-else (with variable and function call -> else)" in { - eval("if false then var else flatten(xs)", Map("xs" -> List(1, 2), "var" -> 3)) should be( - ValList(List(ValNumber(1), ValNumber(2))) - ) + evaluateExpression( + "if false then var else flatten(xs)", + Map("xs" -> List(1, 2), "var" -> 3) + ) should returnResult(List(1, 2)) } it should "be a simple positive unary test" in { - eval("< 3", Map(UnaryTests.defaultInputVariable -> 2)) should be(ValBoolean(true)) + evaluateExpression("< 3", Map(UnaryTests.defaultInputVariable -> 2)) should returnResult(true) - eval("(2 .. 4)", Map(UnaryTests.defaultInputVariable -> 5)) should be(ValBoolean(false)) + evaluateExpression("(2 .. 4)", Map(UnaryTests.defaultInputVariable -> 5)) should returnResult( + false + ) } it should "be an instance of (literal)" in { - eval("x instance of number", Map("x" -> 1)) should be(ValBoolean(true)) - eval("x instance of number", Map("x" -> "NaN")) should be(ValBoolean(false)) + evaluateExpression("x instance of number", Map("x" -> 1)) should returnResult(true) + evaluateExpression("x instance of number", Map("x" -> "NaN")) should returnResult(false) - eval("x instance of boolean", Map("x" -> true)) should be(ValBoolean(true)) - eval("x instance of boolean", Map("x" -> 0)) should be(ValBoolean(false)) + evaluateExpression("x instance of boolean", Map("x" -> true)) should returnResult(true) + evaluateExpression("x instance of boolean", Map("x" -> 0)) should returnResult(false) - eval("x instance of string", Map("x" -> "yes")) should be(ValBoolean(true)) - eval("x instance of string", Map("x" -> 0)) should be(ValBoolean(false)) + evaluateExpression("x instance of string", Map("x" -> "yes")) should returnResult(true) + evaluateExpression("x instance of string", Map("x" -> 0)) should returnResult(false) } it should "be an instance of (duration)" in { - eval("""duration("P3M") instance of years and months duration""") should be(ValBoolean(true)) - eval("""duration("PT4H") instance of days and time duration""") should be(ValBoolean(true)) - eval("""null instance of years and months duration""") should be(ValBoolean(false)) - eval("""null instance of days and time duration""") should be(ValBoolean(false)) + evaluateExpression( + """duration("P3M") instance of years and months duration""" + ) should returnResult(true) + evaluateExpression( + """duration("PT4H") instance of days and time duration""" + ) should returnResult(true) + evaluateExpression("""null instance of years and months duration""") should returnResult(false) + evaluateExpression("""null instance of days and time duration""") should returnResult(false) } it should "be an instance of (date)" in { - eval("""date("2023-03-07") instance of date""") should be(ValBoolean(true)) - eval(""" @"2023-03-07" instance of date""") should be(ValBoolean(true)) - eval("1 instance of date") should be(ValBoolean(false)) + evaluateExpression("""date("2023-03-07") instance of date""") should returnResult(true) + evaluateExpression(""" @"2023-03-07" instance of date""") should returnResult(true) + evaluateExpression("1 instance of date") should returnResult(false) } it should "be an instance of (time)" in { - eval("""time("11:27:00") instance of time""") should be(ValBoolean(true)) - eval(""" @"11:27:00" instance of time""") should be(ValBoolean(true)) - eval("1 instance of time") should be(ValBoolean(false)) + evaluateExpression("""time("11:27:00") instance of time""") should returnResult(true) + evaluateExpression(""" @"11:27:00" instance of time""") should returnResult(true) + evaluateExpression("1 instance of time") should returnResult(false) } it should "be an instance of (date and time)" in { - eval("""date and time("2023-03-07T11:27:00") instance of date and time""") should be( - ValBoolean(true) + evaluateExpression( + """date and time("2023-03-07T11:27:00") instance of date and time""" + ) should returnResult(true) + + evaluateExpression(""" @"2023-03-07T11:27:00" instance of date and time""") should returnResult( + true ) - eval(""" @"2023-03-07T11:27:00" instance of date and time""") should be(ValBoolean(true)) - eval("1 instance of date and time") should be(ValBoolean(false)) + evaluateExpression("1 instance of date and time") should returnResult(false) } it should "be an instance of (list)" in { - eval("[1,2,3] instance of list") should be(ValBoolean(true)) - eval("[] instance of list") should be(ValBoolean(true)) - eval("1 instance of list") should be(ValBoolean(false)) + evaluateExpression("[1,2,3] instance of list") should returnResult(true) + evaluateExpression("[] instance of list") should returnResult(true) + evaluateExpression("1 instance of list") should returnResult(false) } it should "be an instance of (context)" in { - eval("{x:1} instance of context") should be(ValBoolean(true)) - eval("{} instance of context") should be(ValBoolean(true)) - eval("1 instance of context") should be(ValBoolean(false)) + evaluateExpression("{x:1} instance of context") should returnResult(true) + evaluateExpression("{} instance of context") should returnResult(true) + evaluateExpression("1 instance of context") should returnResult(false) } it should "be an instance of (multiplication)" in { - eval("2 * 3 instance of number") should be(ValBoolean(true)) + evaluateExpression("2 * 3 instance of number") should returnResult(true) } it should "be an instance of (function definition)" in { - eval(""" (function() "foo") instance of function """) should be(ValBoolean(true)) - eval("""1 instance of function""") should be(ValBoolean(false)) + evaluateExpression(""" (function() "foo") instance of function """) should returnResult(true) + evaluateExpression("""1 instance of function""") should returnResult(false) } it should "be a instance of Any should always pass" in { - eval("x instance of Any", Map("x" -> "yes")) should be(ValBoolean(true)) - eval("x instance of Any", Map("x" -> 1)) should be(ValBoolean(true)) - eval("x instance of Any", Map("x" -> true)) should be(ValBoolean(true)) - eval("x instance of Any", Map("x" -> null)) should be(ValBoolean(false)) + evaluateExpression("x instance of Any", Map("x" -> "yes")) should returnResult(true) + evaluateExpression("x instance of Any", Map("x" -> 1)) should returnResult(true) + evaluateExpression("x instance of Any", Map("x" -> true)) should returnResult(true) + evaluateExpression("x instance of Any", Map("x" -> null)) should returnResult(false) } it should "be an escaped identifier" in { // regular identifier - eval(" `x` ", Map("x" -> "foo")) should be(ValString("foo")) + evaluateExpression(" `x` ", Map("x" -> "foo")) should returnResult("foo") // with whitespace - eval(" `a b` ", Map("a b" -> "foo")) should be(ValString("foo")) + evaluateExpression(" `a b` ", Map("a b" -> "foo")) should returnResult("foo") // with operator - eval(" `a-b` ", Map("a-b" -> 3)) should be(ValNumber(3)) + evaluateExpression(" `a-b` ", Map("a-b" -> 3)) should returnResult(3) } it should "contains parentheses" in { - eval("(1 + 2)") should be(ValNumber(3)) - eval("(1 + 2) + 3") should be(ValNumber(6)) - eval("1 + (2 + 3)") should be(ValNumber(6)) + evaluateExpression("(1 + 2)") should returnResult(3) + evaluateExpression("(1 + 2) + 3") should returnResult(6) + evaluateExpression("1 + (2 + 3)") should returnResult(6) - eval("([1,2,3])[1]") should be(ValNumber(1)) - eval("({x:1}).x") should be(ValNumber(1)) - eval("{x:(1)}.x") should be(ValNumber(1)) + evaluateExpression("([1,2,3])[1]") should returnResult(1) + evaluateExpression("({x:1}).x") should returnResult(1) + evaluateExpression("{x:(1)}.x") should returnResult(1) - eval("[1,2,3,4][(1)]") should be(ValNumber(1)) + evaluateExpression("[1,2,3,4][(1)]") should returnResult(1) } it should "contain parentheses in a context literal" in { val context = Map("xs" -> List(1, 2, 3)) - eval("{x:(xs[1])}.x", context) should be(ValNumber(1)) - eval("{x:(xs)[1]}.x", context) should be(ValNumber(1)) - eval("{x:(xs)}.x", context) should be(ValList(List(ValNumber(1), ValNumber(2), ValNumber(3)))) + evaluateExpression("{x:(xs[1])}.x", context) should returnResult(1) + evaluateExpression("{x:(xs)[1]}.x", context) should returnResult(1) + evaluateExpression("{x:(xs)}.x", context) should returnResult(List(1, 2, 3)) } it should "contains nested filter expressions" in { - eval("[1,2,3,4][item > 2][1]") should be(ValNumber(3)) - eval("([1,2,3,4])[item > 2][1]") should be(ValNumber(3)) - eval("([1,2,3,4][item > 2])[1]") should be(ValNumber(3)) + evaluateExpression("[1,2,3,4][item > 2][1]") should returnResult(3) + evaluateExpression("([1,2,3,4])[item > 2][1]") should returnResult(3) + evaluateExpression("([1,2,3,4][item > 2])[1]") should returnResult(3) } it should "contains nested path expressions" in { - eval("{x:{y:1}}.x.y") should be(ValNumber(1)) - eval("{x:{y:{z:1}}}.x.y.z") should be(ValNumber(1)) + evaluateExpression("{x:{y:1}}.x.y") should returnResult(1) + evaluateExpression("{x:{y:{z:1}}}.x.y.z") should returnResult(1) - eval("({x:{y:{z:1}}}).x.y.z") should be(ValNumber(1)) - eval("({x:{y:{z:1}}}.x).y.z") should be(ValNumber(1)) - eval("({x:{y:{z:1}}}.x.y).z") should be(ValNumber(1)) + evaluateExpression("({x:{y:{z:1}}}).x.y.z") should returnResult(1) + evaluateExpression("({x:{y:{z:1}}}.x).y.z") should returnResult(1) + evaluateExpression("({x:{y:{z:1}}}.x.y).z") should returnResult(1) } it should "contains nested filter and path expressions" in { - eval("[{x:{y:1}},{x:{y:2}},{x:{y:3}}].x.y[2]") should be(ValNumber(2)) - eval("([{x:{y:1}},{x:{y:2}},{x:{y:3}}]).x.y[2]") should be(ValNumber(2)) - eval("([{x:{y:1}},{x:{y:2}},{x:{y:3}}].x).y[2]") should be(ValNumber(2)) - eval("([{x:{y:1}},{x:{y:2}},{x:{y:3}}].x.y)[2]") should be(ValNumber(2)) + evaluateExpression("[{x:{y:1}},{x:{y:2}},{x:{y:3}}].x.y[2]") should returnResult(2) + evaluateExpression("([{x:{y:1}},{x:{y:2}},{x:{y:3}}]).x.y[2]") should returnResult(2) + evaluateExpression("([{x:{y:1}},{x:{y:2}},{x:{y:3}}].x).y[2]") should returnResult(2) + evaluateExpression("([{x:{y:1}},{x:{y:2}},{x:{y:3}}].x.y)[2]") should returnResult(2) - eval("([{x:{y:1}},{x:{y:2}},{x:{y:3}}]).x[2].y") should be(ValNumber(2)) - eval("([{x:{y:1}},{x:{y:2}},{x:{y:3}}])[2].x.y") should be(ValNumber(2)) + evaluateExpression("([{x:{y:1}},{x:{y:2}},{x:{y:3}}]).x[2].y") should returnResult(2) + evaluateExpression("([{x:{y:1}},{x:{y:2}},{x:{y:3}}])[2].x.y") should returnResult(2) - eval("[{x:[1,2]},{x:[3,4]},{x:[5,6]}][2].x[1]") should be(ValNumber(3)) + evaluateExpression("[{x:[1,2]},{x:[3,4]},{x:[5,6]}][2].x[1]") should returnResult(3) - eval("([{x:[1,2]},{x:[3,4]},{x:[5,6]}]).x[2][1]") should be(ValNumber(3)) - eval("([{x:[1,2]},{x:[3,4]},{x:[5,6]}].x)[2][1]") should be(ValNumber(3)) - eval("([{x:[1,2]},{x:[3,4]},{x:[5,6]}].x[2])[1]") should be(ValNumber(3)) + evaluateExpression("([{x:[1,2]},{x:[3,4]},{x:[5,6]}]).x[2][1]") should returnResult(3) + evaluateExpression("([{x:[1,2]},{x:[3,4]},{x:[5,6]}].x)[2][1]") should returnResult(3) + evaluateExpression("([{x:[1,2]},{x:[3,4]},{x:[5,6]}].x[2])[1]") should returnResult(3) } "Null" should "compare to null" in { - eval("null = null") should be(ValBoolean(true)) - eval("null != null") should be(ValBoolean(false)) + evaluateExpression("null = null") should returnResult(true) + evaluateExpression("null != null") should returnResult(false) } it should "compare to nullable variable" in { - eval("null = x", Map("x" -> ValNull)) should be(ValBoolean(true)) - eval("null = x", Map("x" -> 1)) should be(ValBoolean(false)) + evaluateExpression("null = x", Map("x" -> ValNull)) should returnResult(true) + evaluateExpression("null = x", Map("x" -> 1)) should returnResult(false) - eval("null != x", Map("x" -> ValNull)) should be(ValBoolean(false)) - eval("null != x", Map("x" -> 1)) should be(ValBoolean(true)) + evaluateExpression("null != x", Map("x" -> ValNull)) should returnResult(false) + evaluateExpression("null != x", Map("x" -> 1)) should returnResult(true) } it should "compare to nullable context entry" in { - eval("null = {x: null}.x") should be(ValBoolean(true)) - eval("null = {x: 1}.x") should be(ValBoolean(false)) + evaluateExpression("null = {x: null}.x") should returnResult(true) + evaluateExpression("null = {x: 1}.x") should returnResult(false) - eval("null != {x: null}.x") should be(ValBoolean(false)) - eval("null != {x: 1}.x") should be(ValBoolean(true)) + evaluateExpression("null != {x: null}.x") should returnResult(false) + evaluateExpression("null != {x: 1}.x") should returnResult(true) } it should "compare to not existing variable" in { - eval("null = x") should be(ValBoolean(true)) - eval("null = x.y") should be(ValBoolean(true)) + evaluateExpression("null = x") should returnResult(true) + evaluateExpression("null = x.y") should returnResult(true) - eval("x = null") should be(ValBoolean(true)) - eval("x.y = null") should be(ValBoolean(true)) + evaluateExpression("x = null") should returnResult(true) + evaluateExpression("x.y = null") should returnResult(true) } it should "compare to not existing context entry" in { - eval("null = {}.x") should be(ValBoolean(true)) - eval("null = {x: null}.x.y") should be(ValBoolean(true)) + evaluateExpression("null = {}.x") should returnResult(true) + evaluateExpression("null = {x: null}.x.y") should returnResult(true) - eval("{}.x = null") should be(ValBoolean(true)) - eval("{x: null}.x.y = null") should be(ValBoolean(true)) + evaluateExpression("{}.x = null") should returnResult(true) + evaluateExpression("{x: null}.x.y = null") should returnResult(true) } "A variable name" should "not be a key-word" in { - eval("{ null: 1 }.null") shouldBe aParseError - eval("{ true: 1}.true") shouldBe aParseError - eval("{ false: 1}.false") shouldBe aParseError - eval("function") shouldBe aParseError - eval("in") shouldBe aParseError - eval("return") shouldBe aParseError - eval("then") shouldBe aParseError - eval("else") shouldBe aParseError - eval("satisfies") shouldBe aParseError - eval("and") shouldBe aParseError - eval("or") shouldBe aParseError + evaluateExpression("{ null: 1 }.null") should failToParse() + evaluateExpression("{ true: 1}.true") should failToParse() + evaluateExpression("{ false: 1}.false") should failToParse() + evaluateExpression("function") should failToParse() + evaluateExpression("in") should failToParse() + evaluateExpression("return") should failToParse() + evaluateExpression("then") should failToParse() + evaluateExpression("else") should failToParse() + evaluateExpression("satisfies") should failToParse() + evaluateExpression("and") should failToParse() + evaluateExpression("or") should failToParse() } // Ignored as these keywords are not listed as reserved keywords yet ignore should "not be a key-word (ignored)" in { - eval("some") shouldBe aParseError - eval("every") shouldBe aParseError - eval("if") shouldBe aParseError - eval("for") shouldBe aParseError - eval("between") shouldBe aParseError - eval("instance") shouldBe aParseError - eval("of") shouldBe aParseError - eval("not") shouldBe aParseError + evaluateExpression("some") should failToParse() + evaluateExpression("every") should failToParse() + evaluateExpression("if") should failToParse() + evaluateExpression("for") should failToParse() + evaluateExpression("between") should failToParse() + evaluateExpression("instance") should failToParse() + evaluateExpression("of") should failToParse() + evaluateExpression("not") should failToParse() } List( @@ -307,32 +321,47 @@ class InterpreterExpressionTest extends AnyFlatSpec with Matchers with FeelInteg ).foreach { variableName => it should s"contain a key-word ($variableName)" in { - eval(s"$variableName = true", Map(variableName -> true)) should be(ValBoolean(true)) + evaluateExpression(s"$variableName = true", Map(variableName -> true)) should returnResult( + true + ) } } "A comment" should "be written as end of line comments //" in { - eval(""" [1,2,3][1] // the first item """) should be(ValNumber(1)) + evaluateExpression(""" [1,2,3][1] // the first item """) should returnResult(1) } it should "be written as trailing comments /* .. */" in { - eval(""" [1,2,3][1] /* the first item */ """) should be(ValNumber(1)) + evaluateExpression(""" [1,2,3][1] /* the first item */ """) should returnResult(1) } it should "be written as single line comments /* .. */" in { - eval(""" + evaluateExpression(""" /* the first item */ [1,2,3][1] - """) should be(ValNumber(1)) + """) should returnResult(1) } it should "be written as block comments /* .. */" in { - eval(""" + evaluateExpression(""" /* * the first item */ [1,2,3][1] - """) should be(ValNumber(1)) + """) should returnResult(1) + } + + "The special variable '?' (input value)" should "be available in an unary-test" in { + + evaluateExpression("5 in ? < 10") should returnResult(true) + evaluateExpression("5 in ? < 3") should returnResult(false) + } + + it should "not be available outside an unary-test" in { + + evaluateExpression("? < 10") should failWith( + """failed to evaluate expression '? < 10': No input value available. '?' can only be used inside an unary-test expression.""" + ) } } diff --git a/src/test/scala/org/camunda/feel/impl/interpreter/InterpreterUnaryTest.scala b/src/test/scala/org/camunda/feel/impl/interpreter/InterpreterUnaryTest.scala index 35816ea1b..c5db4f30e 100644 --- a/src/test/scala/org/camunda/feel/impl/interpreter/InterpreterUnaryTest.scala +++ b/src/test/scala/org/camunda/feel/impl/interpreter/InterpreterUnaryTest.scala @@ -16,532 +16,637 @@ */ package org.camunda.feel.impl.interpreter -import org.camunda.feel.impl.FeelIntegrationTest -import org.camunda.feel.syntaxtree._ -import org.scalatest.matchers.should.Matchers +import org.camunda.feel.impl.{EvaluationResultMatchers, FeelEngineTest} import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers /** @author * Philipp Ossler */ -class InterpreterUnaryTest extends AnyFlatSpec with Matchers with FeelIntegrationTest { +class InterpreterUnaryTest + extends AnyFlatSpec + with Matchers + with FeelEngineTest + with EvaluationResultMatchers { "A number" should "compare with '<'" in { - evalUnaryTests(2, "< 3") should be(ValBoolean(true)) - evalUnaryTests(3, "< 3") should be(ValBoolean(false)) - evalUnaryTests(4, "< 3") should be(ValBoolean(false)) + evaluateUnaryTests("< 3", 2) should returnResult(true) + evaluateUnaryTests("< 3", 3) should returnResult(false) + evaluateUnaryTests("< 3", 4) should returnResult(false) } it should "compare with '<='" in { - evalUnaryTests(2, "<= 3") should be(ValBoolean(true)) - evalUnaryTests(3, "<= 3") should be(ValBoolean(true)) - evalUnaryTests(4, "<= 3") should be(ValBoolean(false)) + evaluateUnaryTests("<= 3", 2) should returnResult(true) + evaluateUnaryTests("<= 3", 3) should returnResult(true) + evaluateUnaryTests("<= 3", 4) should returnResult(false) } it should "compare with '>'" in { - evalUnaryTests(2, "> 3") should be(ValBoolean(false)) - evalUnaryTests(3, "> 3") should be(ValBoolean(false)) - evalUnaryTests(4, "> 3") should be(ValBoolean(true)) + evaluateUnaryTests("> 3", 2) should returnResult(false) + evaluateUnaryTests("> 3", 3) should returnResult(false) + evaluateUnaryTests("> 3", 4) should returnResult(true) } it should "compare with '>='" in { - evalUnaryTests(2, ">= 3") should be(ValBoolean(false)) - evalUnaryTests(3, ">= 3") should be(ValBoolean(true)) - evalUnaryTests(4, ">= 3") should be(ValBoolean(true)) + evaluateUnaryTests(">= 3", 2) should returnResult(false) + evaluateUnaryTests(">= 3", 3) should returnResult(true) + evaluateUnaryTests(">= 3", 4) should returnResult(true) } it should "be equal to another number" in { - evalUnaryTests(2, "3") should be(ValBoolean(false)) - evalUnaryTests(3, "3") should be(ValBoolean(true)) + evaluateUnaryTests("3", 2) should returnResult(false) + evaluateUnaryTests("3", 3) should returnResult(true) - evalUnaryTests(-1, "-1") should be(ValBoolean(true)) - evalUnaryTests(0, "-1") should be(ValBoolean(false)) + evaluateUnaryTests("-1", -1) should returnResult(true) + evaluateUnaryTests("-1", 0) should returnResult(false) } it should "be in interval '(2..4)'" in { - evalUnaryTests(2, "(2..4)") should be(ValBoolean(false)) - evalUnaryTests(3, "(2..4)") should be(ValBoolean(true)) - evalUnaryTests(4, "(2..4)") should be(ValBoolean(false)) + evaluateUnaryTests("(2..4)", 2) should returnResult(false) + evaluateUnaryTests("(2..4)", 3) should returnResult(true) + evaluateUnaryTests("(2..4)", 4) should returnResult(false) } it should "be in interval '[2..4]'" in { - evalUnaryTests(2, "[2..4]") should be(ValBoolean(true)) - evalUnaryTests(3, "[2..4]") should be(ValBoolean(true)) - evalUnaryTests(4, "[2..4]") should be(ValBoolean(true)) + evaluateUnaryTests("[2..4]", 2) should returnResult(true) + evaluateUnaryTests("[2..4]", 3) should returnResult(true) + evaluateUnaryTests("[2..4]", 4) should returnResult(true) } it should "be in one of two intervals (disjunction)" in { - evalUnaryTests(3, "[1..5], [6..10]") should be(ValBoolean(true)) - evalUnaryTests(6, "[1..5], [6..10]") should be(ValBoolean(true)) - evalUnaryTests(11, "[1..5], [6..10]") should be(ValBoolean(false)) + evaluateUnaryTests("[1..5], [6..10]", 3) should returnResult(true) + evaluateUnaryTests("[1..5], [6..10]", 6) should returnResult(true) + evaluateUnaryTests("[1..5], [6..10]", 11) should returnResult(false) } it should "be in '2,3'" in { - evalUnaryTests(2, "2,3") should be(ValBoolean(true)) - evalUnaryTests(3, "2,3") should be(ValBoolean(true)) - evalUnaryTests(4, "2,3") should be(ValBoolean(false)) + evaluateUnaryTests("2,3", 2) should returnResult(true) + evaluateUnaryTests("2,3", 3) should returnResult(true) + evaluateUnaryTests("2,3", 4) should returnResult(false) } it should "be not equal 'not(3)'" in { - evalUnaryTests(2, "not(3)") should be(ValBoolean(true)) - evalUnaryTests(3, "not(3)") should be(ValBoolean(false)) - evalUnaryTests(4, "not(3)") should be(ValBoolean(true)) + evaluateUnaryTests("not(3)", 2) should returnResult(true) + evaluateUnaryTests("not(3)", 3) should returnResult(false) + evaluateUnaryTests("not(3)", 4) should returnResult(true) } it should "be not in 'not(2,3)'" in { - evalUnaryTests(2, "not(2,3)") should be(ValBoolean(false)) - evalUnaryTests(3, "not(2,3)") should be(ValBoolean(false)) - evalUnaryTests(4, "not(2,3)") should be(ValBoolean(true)) + evaluateUnaryTests("not(2,3)", 2) should returnResult(false) + evaluateUnaryTests("not(2,3)", 3) should returnResult(false) + evaluateUnaryTests("not(2,3)", 4) should returnResult(true) } it should "compare to a variable (qualified name)" in { - evalUnaryTests(2, "var", Map("var" -> 3)) should be(ValBoolean(false)) - evalUnaryTests(3, "var", Map("var" -> 3)) should be(ValBoolean(true)) + evaluateUnaryTests("var", 2, Map("var" -> 3)) should returnResult(false) + evaluateUnaryTests("var", 3, Map("var" -> 3)) should returnResult(true) - evalUnaryTests(2, "< var", Map("var" -> 3)) should be(ValBoolean(true)) - evalUnaryTests(3, "< var", Map("var" -> 3)) should be(ValBoolean(false)) + evaluateUnaryTests("< var", 2, Map("var" -> 3)) should returnResult(true) + evaluateUnaryTests("< var", 3, Map("var" -> 3)) should returnResult(false) } it should "compare to a field of a bean" in { class A(val b: Int) - evalUnaryTests(3, "a.b", Map("a" -> new A(3))) should be(ValBoolean(true)) - evalUnaryTests(3, "a.b", Map("a" -> new A(4))) should be(ValBoolean(false)) + evaluateUnaryTests("a.b", 3, Map("a" -> new A(3))) should returnResult(true) + evaluateUnaryTests("a.b", 3, Map("a" -> new A(4))) should returnResult(false) - evalUnaryTests(3, "< a.b", Map("a" -> new A(4))) should be(ValBoolean(true)) - evalUnaryTests(3, "< a.b", Map("a" -> new A(2))) should be(ValBoolean(false)) + evaluateUnaryTests("< a.b", 3, Map("a" -> new A(4))) should returnResult(true) + evaluateUnaryTests("< a.b", 3, Map("a" -> new A(2))) should returnResult(false) } it should "compare to null" in { - evalUnaryTests(null, "3") should be(ValBoolean(false)) + evaluateUnaryTests("3", inputValue = null) should returnResult(false) } it should "compare null with less/greater than" in { - evalUnaryTests(null, "< 3") should be(ValNull) - evalUnaryTests(null, "<= 3") should be(ValNull) - evalUnaryTests(null, "> 3") should be(ValNull) - evalUnaryTests(null, ">= 3") should be(ValNull) + evaluateUnaryTests("< 3", inputValue = null) should returnNull() + evaluateUnaryTests("<= 3", inputValue = null) should returnNull() + evaluateUnaryTests("> 3", inputValue = null) should returnNull() + evaluateUnaryTests(">= 3", inputValue = null) should returnNull() } it should "compare null with interval" in { - evalUnaryTests(null, "(0..10)") should be(ValNull) + evaluateUnaryTests("(0..10)", inputValue = null) should returnNull() } "A string" should "be equal to another string" in { - evalUnaryTests("a", """ "b" """) should be(ValBoolean(false)) - evalUnaryTests("b", """ "b" """) should be(ValBoolean(true)) + evaluateUnaryTests(""" "b" """, "a") should returnResult(false) + evaluateUnaryTests(""" "b" """, "b") should returnResult(true) } it should "compare to null" in { - evalUnaryTests(null, """ "a" """) should be(ValBoolean(false)) + evaluateUnaryTests(""" "a" """, inputValue = null) should returnResult(false) } it should """be in '"a","b"' """ in { - evalUnaryTests("a", """ "a","b" """) should be(ValBoolean(true)) - evalUnaryTests("b", """ "a","b" """) should be(ValBoolean(true)) - evalUnaryTests("c", """ "a","b" """) should be(ValBoolean(false)) + evaluateUnaryTests(""" "a","b" """, "a") should returnResult(true) + evaluateUnaryTests(""" "a","b" """, "b") should returnResult(true) + evaluateUnaryTests(""" "a","b" """, "c") should returnResult(false) } "A boolean" should "be equal to another boolean" in { - evalUnaryTests(false, "true") should be(ValBoolean(false)) - evalUnaryTests(true, "false") should be(ValBoolean(false)) + evaluateUnaryTests("true", false) should returnResult(false) + evaluateUnaryTests("false", true) should returnResult(false) - evalUnaryTests(false, "false") should be(ValBoolean(true)) - evalUnaryTests(true, "true") should be(ValBoolean(true)) + evaluateUnaryTests("false", false) should returnResult(true) + evaluateUnaryTests("true", true) should returnResult(true) } it should "compare to null" in { - evalUnaryTests(null, "true") should be(ValBoolean(false)) - evalUnaryTests(null, "false") should be(ValBoolean(false)) + evaluateUnaryTests("true", inputValue = null) should returnResult(false) + evaluateUnaryTests("false", inputValue = null) should returnResult(false) } it should "compare to a boolean comparison (numeric)" in { - evalUnaryTests(true, "1 < 2") should be(ValBoolean(true)) - evalUnaryTests(true, "2 < 1") should be(ValBoolean(false)) + evaluateUnaryTests("1 < 2", true) should returnResult(true) + evaluateUnaryTests("2 < 1", true) should returnResult(false) } it should "compare to a boolean comparison (string)" in { - evalUnaryTests(true, """ "a" = "a" """) should be(ValBoolean(true)) - evalUnaryTests(true, """ "a" = "b" """) should be(ValBoolean(false)) + evaluateUnaryTests(""" "a" = "a" """, true) should returnResult(true) + evaluateUnaryTests(""" "a" = "b" """, true) should returnResult(false) } it should "compare to a conjunction (and)" in { // it is uncommon to use a conjunction in a unary-tests but the engine should be able to parse - evalUnaryTests(true, "true and true") shouldBe ValBoolean(true) - evalUnaryTests(true, "false and true") shouldBe ValBoolean(false) + evaluateUnaryTests("true and true", true) should returnResult(true) + evaluateUnaryTests("false and true", true) should returnResult(false) - evalUnaryTests(true, "true and null") shouldBe ValBoolean(false) - evalUnaryTests(true, "false and null") shouldBe ValBoolean(false) + evaluateUnaryTests("true and null", true) should returnResult(false) + evaluateUnaryTests("false and null", true) should returnResult(false) - evalUnaryTests(true, """true and "otherwise" """) shouldBe ValBoolean(false) - evalUnaryTests(true, """false and "otherwise" """) shouldBe ValBoolean(false) + evaluateUnaryTests("""true and "otherwise" """, true) should returnResult(false) + evaluateUnaryTests("""false and "otherwise" """, true) should returnResult(false) } it should "compare to a disjunction (or)" in { // it is uncommon to use a disjunction in a unary-tests but the engine should be able to parse - evalUnaryTests(true, "true or true") shouldBe ValBoolean(true) - evalUnaryTests(true, "false or true") shouldBe ValBoolean(true) - evalUnaryTests(true, "false or false") shouldBe ValBoolean(false) + evaluateUnaryTests("true or true", true) should returnResult(true) + evaluateUnaryTests("false or true", true) should returnResult(true) + evaluateUnaryTests("false or false", true) should returnResult(false) - evalUnaryTests(true, "true or null") shouldBe ValBoolean(true) - evalUnaryTests(true, "false or null") shouldBe ValBoolean(false) + evaluateUnaryTests("true or null", true) should returnResult(true) + evaluateUnaryTests("false or null", true) should returnResult(false) - evalUnaryTests(true, """true or "otherwise" """) shouldBe ValBoolean(true) - evalUnaryTests(true, """false or "otherwise" """) shouldBe ValBoolean(false) + evaluateUnaryTests("""true or "otherwise" """, true) should returnResult(true) + evaluateUnaryTests("""false or "otherwise" """, true) should returnResult(false) } "A date" should "compare with '<'" in { - evalUnaryTests(date("2015-09-17"), """< date("2015-09-18")""") should be(ValBoolean(true)) - evalUnaryTests(date("2015-09-18"), """< date("2015-09-18")""") should be(ValBoolean(false)) - evalUnaryTests(date("2015-09-19"), """< date("2015-09-18")""") should be(ValBoolean(false)) + evaluateUnaryTests("""< date("2015-09-18")""", date("2015-09-17")) should returnResult(true) + evaluateUnaryTests("""< date("2015-09-18")""", date("2015-09-18")) should returnResult(false) + evaluateUnaryTests("""< date("2015-09-18")""", date("2015-09-19")) should returnResult(false) } it should "compare with '<='" in { - evalUnaryTests(date("2015-09-17"), """<= date("2015-09-18")""") should be(ValBoolean(true)) - evalUnaryTests(date("2015-09-18"), """<= date("2015-09-18")""") should be(ValBoolean(true)) - evalUnaryTests(date("2015-09-19"), """<= date("2015-09-18")""") should be(ValBoolean(false)) + evaluateUnaryTests("""<= date("2015-09-18")""", date("2015-09-17")) should returnResult(true) + evaluateUnaryTests("""<= date("2015-09-18")""", date("2015-09-18")) should returnResult(true) + evaluateUnaryTests("""<= date("2015-09-18")""", date("2015-09-19")) should returnResult(false) } it should "compare with '>'" in { - evalUnaryTests(date("2015-09-17"), """> date("2015-09-18")""") should be(ValBoolean(false)) - evalUnaryTests(date("2015-09-18"), """> date("2015-09-18")""") should be(ValBoolean(false)) - evalUnaryTests(date("2015-09-19"), """> date("2015-09-18")""") should be(ValBoolean(true)) + evaluateUnaryTests("""> date("2015-09-18")""", date("2015-09-17")) should returnResult(false) + evaluateUnaryTests("""> date("2015-09-18")""", date("2015-09-18")) should returnResult(false) + evaluateUnaryTests("""> date("2015-09-18")""", date("2015-09-19")) should returnResult(true) } it should "compare with '>='" in { - evalUnaryTests(date("2015-09-17"), """>= date("2015-09-18")""") should be(ValBoolean(false)) - evalUnaryTests(date("2015-09-18"), """>= date("2015-09-18")""") should be(ValBoolean(true)) - evalUnaryTests(date("2015-09-19"), """>= date("2015-09-18")""") should be(ValBoolean(true)) + evaluateUnaryTests(""">= date("2015-09-18")""", date("2015-09-17")) should returnResult(false) + evaluateUnaryTests(""">= date("2015-09-18")""", date("2015-09-18")) should returnResult(true) + evaluateUnaryTests(""">= date("2015-09-18")""", date("2015-09-19")) should returnResult(true) } it should "be equal to another date" in { - evalUnaryTests(date("2015-09-17"), """date("2015-09-18")""") should be(ValBoolean(false)) - evalUnaryTests(date("2015-09-18"), """date("2015-09-18")""") should be(ValBoolean(true)) + evaluateUnaryTests("""date("2015-09-18")""", date("2015-09-17")) should returnResult(false) + evaluateUnaryTests("""date("2015-09-18")""", date("2015-09-18")) should returnResult(true) } it should """be in interval '(date("2015-09-17")..date("2015-09-19")]'""" in { - evalUnaryTests(date("2015-09-17"), """(date("2015-09-17")..date("2015-09-19"))""") should be( - ValBoolean(false) - ) - evalUnaryTests(date("2015-09-18"), """(date("2015-09-17")..date("2015-09-19"))""") should be( - ValBoolean(true) - ) - evalUnaryTests(date("2015-09-19"), """(date("2015-09-17")..date("2015-09-19"))""") should be( - ValBoolean(false) - ) + evaluateUnaryTests( + """(date("2015-09-17")..date("2015-09-19"))""", + date("2015-09-17") + ) should returnResult(false) + evaluateUnaryTests( + """(date("2015-09-17")..date("2015-09-19"))""", + date("2015-09-18") + ) should returnResult(true) + evaluateUnaryTests( + """(date("2015-09-17")..date("2015-09-19"))""", + date("2015-09-19") + ) should returnResult(false) } it should """be in interval '[date("2015-09-17")..date("2015-09-19")]'""" in { - evalUnaryTests(date("2015-09-17"), """[date("2015-09-17")..date("2015-09-19")]""") should be( - ValBoolean(true) - ) - evalUnaryTests(date("2015-09-18"), """[date("2015-09-17")..date("2015-09-19")]""") should be( - ValBoolean(true) - ) - evalUnaryTests(date("2015-09-19"), """[date("2015-09-17")..date("2015-09-19")]""") should be( - ValBoolean(true) - ) + evaluateUnaryTests( + """[date("2015-09-17")..date("2015-09-19")]""", + date("2015-09-17") + ) should returnResult(true) + evaluateUnaryTests( + """[date("2015-09-17")..date("2015-09-19")]""", + date("2015-09-18") + ) should returnResult(true) + evaluateUnaryTests( + """[date("2015-09-17")..date("2015-09-19")]""", + date("2015-09-19") + ) should returnResult(true) } "A time" should "compare with '<'" in { - evalUnaryTests(localTime("08:31:14"), """< time("10:00:00")""") should be(ValBoolean(true)) - evalUnaryTests(localTime("10:10:00"), """< time("10:00:00")""") should be(ValBoolean(false)) - evalUnaryTests(localTime("11:31:14"), """< time("10:00:00")""") should be(ValBoolean(false)) + evaluateUnaryTests("""< time("10:00:00")""", localTime("08:31:14")) should returnResult(true) + evaluateUnaryTests("""< time("10:00:00")""", localTime("10:10:00")) should returnResult(false) + evaluateUnaryTests("""< time("10:00:00")""", localTime("11:31:14")) should returnResult(false) - evalUnaryTests(time("10:00:00+01:00"), """< time("11:00:00+01:00")""") should be( - ValBoolean(true) + evaluateUnaryTests("""< time("11:00:00+01:00")""", time("10:00:00+01:00")) should returnResult( + true ) - evalUnaryTests(time("10:00:00+01:00"), """< time("10:00:00+01:00")""") should be( - ValBoolean(false) + evaluateUnaryTests("""< time("10:00:00+01:00")""", time("10:00:00+01:00")) should returnResult( + false ) } it should "be equal to another time" in { - evalUnaryTests(localTime("08:31:14"), """time("10:00:00")""") should be(ValBoolean(false)) - evalUnaryTests(localTime("08:31:14"), """time("08:31:14")""") should be(ValBoolean(true)) + evaluateUnaryTests("""time("10:00:00")""", localTime("08:31:14")) should returnResult(false) + evaluateUnaryTests("""time("08:31:14")""", localTime("08:31:14")) should returnResult(true) - evalUnaryTests(time("10:00:00+01:00"), """time("10:00:00+02:00")""") should be( - ValBoolean(false) + evaluateUnaryTests("""time("10:00:00+02:00")""", time("10:00:00+01:00")) should returnResult( + false ) - evalUnaryTests(time("10:00:00+01:00"), """time("11:00:00+02:00")""") should be( - ValBoolean(false) + evaluateUnaryTests("""time("11:00:00+02:00")""", time("10:00:00+01:00")) should returnResult( + false + ) + evaluateUnaryTests("""time("10:00:00+01:00")""", time("10:00:00+01:00")) should returnResult( + true ) - evalUnaryTests(time("10:00:00+01:00"), """time("10:00:00+01:00")""") should be(ValBoolean(true)) } it should """be in interval '[time("08:00:00")..time("10:00:00")]'""" in { - evalUnaryTests(localTime("07:45:10"), """[time("08:00:00")..time("10:00:00")]""") should be( - ValBoolean(false) - ) - evalUnaryTests(localTime("09:15:20"), """[time("08:00:00")..time("10:00:00")]""") should be( - ValBoolean(true) - ) - evalUnaryTests(localTime("11:30:30"), """[time("08:00:00")..time("10:00:00")]""") should be( - ValBoolean(false) - ) - - evalUnaryTests( - time("11:30:00+01:00"), - """[time("08:00:00+01:00")..time("10:00:00+01:00")]""" - ) should be(ValBoolean(false)) - evalUnaryTests( - time("09:30:00+01:00"), - """[time("08:00:00+01:00")..time("10:00:00+01:00")]""" - ) should be(ValBoolean(true)) + evaluateUnaryTests( + """[time("08:00:00")..time("10:00:00")]""", + localTime("07:45:10") + ) should returnResult(false) + evaluateUnaryTests( + """[time("08:00:00")..time("10:00:00")]""", + localTime("09:15:20") + ) should returnResult(true) + evaluateUnaryTests( + """[time("08:00:00")..time("10:00:00")]""", + localTime("11:30:30") + ) should returnResult(false) + + evaluateUnaryTests( + """[time("08:00:00+01:00")..time("10:00:00+01:00")]""", + time("11:30:00+01:00") + ) should returnResult(false) + evaluateUnaryTests( + """[time("08:00:00+01:00")..time("10:00:00+01:00")]""", + time("09:30:00+01:00") + ) should returnResult(true) } "A date-time" should "compare with '<'" in { - evalUnaryTests( - localDateTime("2015-09-17T08:31:14"), - """< date and time("2015-09-17T10:00:00")""" - ) should be(ValBoolean(true)) - evalUnaryTests( - localDateTime("2015-09-17T10:10:00"), - """< date and time("2015-09-17T10:00:00")""" - ) should be(ValBoolean(false)) - evalUnaryTests( - localDateTime("2015-09-17T11:31:14"), - """< date and time("2015-09-17T10:00:00")""" - ) should be(ValBoolean(false)) - - evalUnaryTests( - dateTime("2015-09-17T10:00:00+01:00"), - """< date and time("2015-09-17T12:00:00+01:00")""" - ) should be(ValBoolean(true)) - evalUnaryTests( - dateTime("2015-09-17T10:00:00+01:00"), - """< date and time("2015-09-17T09:00:00+01:00")""" - ) should be(ValBoolean(false)) + evaluateUnaryTests( + """< date and time("2015-09-17T10:00:00")""", + localDateTime("2015-09-17T08:31:14") + ) should returnResult(true) + evaluateUnaryTests( + """< date and time("2015-09-17T10:00:00")""", + localDateTime("2015-09-17T10:10:00") + ) should returnResult(false) + evaluateUnaryTests( + """< date and time("2015-09-17T10:00:00")""", + localDateTime("2015-09-17T11:31:14") + ) should returnResult(false) + + evaluateUnaryTests( + """< date and time("2015-09-17T12:00:00+01:00")""", + dateTime("2015-09-17T10:00:00+01:00") + ) should returnResult(true) + evaluateUnaryTests( + """< date and time("2015-09-17T09:00:00+01:00")""", + dateTime("2015-09-17T10:00:00+01:00") + ) should returnResult(false) } it should "be equal to another date-time" in { - evalUnaryTests( - localDateTime("2015-09-17T08:31:14"), - """date and time("2015-09-17T10:00:00")""" - ) should be(ValBoolean(false)) - evalUnaryTests( - localDateTime("2015-09-17T08:31:14"), - """date and time("2015-09-17T08:31:14")""" - ) should be(ValBoolean(true)) - - evalUnaryTests( - dateTime("2015-09-17T08:30:00+01:00"), - """date and time("2015-09-17T09:30:00+01:00")""" - ) should be(ValBoolean(false)) - evalUnaryTests( - dateTime("2015-09-17T08:30:00+01:00"), - """date and time("2015-09-17T08:30:00+02:00")""" - ) should be(ValBoolean(false)) - evalUnaryTests( - dateTime("2015-09-17T08:30:00+01:00"), - """date and time("2015-09-17T08:30:00+01:00")""" - ) should be(ValBoolean(true)) + evaluateUnaryTests( + """date and time("2015-09-17T10:00:00")""", + localDateTime("2015-09-17T08:31:14") + ) should returnResult(false) + evaluateUnaryTests( + """date and time("2015-09-17T08:31:14")""", + localDateTime("2015-09-17T08:31:14") + ) should returnResult(true) + + evaluateUnaryTests( + """date and time("2015-09-17T09:30:00+01:00")""", + dateTime("2015-09-17T08:30:00+01:00") + ) should returnResult(false) + evaluateUnaryTests( + """date and time("2015-09-17T08:30:00+02:00")""", + dateTime("2015-09-17T08:30:00+01:00") + ) should returnResult(false) + evaluateUnaryTests( + """date and time("2015-09-17T08:30:00+01:00")""", + dateTime("2015-09-17T08:30:00+01:00") + ) should returnResult(true) } it should """be in interval '[dante and time("2015-09-17T08:00:00")..date and time("2015-09-17T10:00:00")]'""" in { - evalUnaryTests( - localDateTime("2015-09-17T07:45:10"), - """[date and time("2015-09-17T08:00:00")..date and time("2015-09-17T10:00:00")]""" - ) should be(ValBoolean(false)) - evalUnaryTests( - localDateTime("2015-09-17T09:15:20"), - """[date and time("2015-09-17T08:00:00")..date and time("2015-09-17T10:00:00")]""" - ) should be(ValBoolean(true)) - evalUnaryTests( - localDateTime("2015-09-17T11:30:30"), - """[date and time("2015-09-17T08:00:00")..date and time("2015-09-17T10:00:00")]""" - ) should be(ValBoolean(false)) - - evalUnaryTests( - dateTime("2015-09-17T08:30:00+01:00"), - """[date and time("2015-09-17T09:00:00+01:00")..date and time("2015-09-17T10:00:00+01:00")]""" - ) should be(ValBoolean(false)) - evalUnaryTests( - dateTime("2015-09-17T08:30:00+01:00"), - """[date and time("2015-09-17T08:00:00+01:00")..date and time("2015-09-17T10:00:00+01:00")]""" - ) should be(ValBoolean(true)) + evaluateUnaryTests( + """[date and time("2015-09-17T08:00:00")..date and time("2015-09-17T10:00:00")]""", + localDateTime("2015-09-17T07:45:10") + ) should returnResult(false) + evaluateUnaryTests( + """[date and time("2015-09-17T08:00:00")..date and time("2015-09-17T10:00:00")]""", + localDateTime("2015-09-17T09:15:20") + ) should returnResult(true) + evaluateUnaryTests( + """[date and time("2015-09-17T08:00:00")..date and time("2015-09-17T10:00:00")]""", + localDateTime("2015-09-17T11:30:30") + ) should returnResult(false) + + evaluateUnaryTests( + """[date and time("2015-09-17T09:00:00+01:00")..date and time("2015-09-17T10:00:00+01:00")]""", + dateTime("2015-09-17T08:30:00+01:00") + ) should returnResult(false) + evaluateUnaryTests( + """[date and time("2015-09-17T08:00:00+01:00")..date and time("2015-09-17T10:00:00+01:00")]""", + dateTime("2015-09-17T08:30:00+01:00") + ) should returnResult(true) } "A year-month-duration" should "compare with '<'" in { - evalUnaryTests(yearMonthDuration("P1Y"), """< duration("P2Y")""") should be(ValBoolean(true)) - evalUnaryTests(yearMonthDuration("P1Y"), """< duration("P1Y")""") should be(ValBoolean(false)) - evalUnaryTests(yearMonthDuration("P1Y2M"), """< duration("P1Y")""") should be(ValBoolean(false)) + evaluateUnaryTests("""< duration("P2Y")""", yearMonthDuration("P1Y")) should returnResult(true) + evaluateUnaryTests("""< duration("P1Y")""", yearMonthDuration("P1Y")) should returnResult(false) + evaluateUnaryTests("""< duration("P1Y")""", yearMonthDuration("P1Y2M")) should returnResult( + false + ) } it should "be equal to another duration" in { - evalUnaryTests(yearMonthDuration("P1Y4M"), """duration("P1Y3M")""") should be(ValBoolean(false)) - evalUnaryTests(yearMonthDuration("P1Y4M"), """duration("P1Y4M")""") should be(ValBoolean(true)) + evaluateUnaryTests("""duration("P1Y3M")""", yearMonthDuration("P1Y4M")) should returnResult( + false + ) + evaluateUnaryTests("""duration("P1Y4M")""", yearMonthDuration("P1Y4M")) should returnResult( + true + ) } it should """be in interval '[duration("P1Y")..duration("P2Y")]'""" in { - evalUnaryTests(yearMonthDuration("P6M"), """[duration("P1Y")..duration("P2Y")]""") should be( - ValBoolean(false) - ) - evalUnaryTests(yearMonthDuration("P1Y8M"), """[duration("P1Y")..duration("P2Y")]""") should be( - ValBoolean(true) - ) - evalUnaryTests(yearMonthDuration("P2Y1M"), """[duration("P1Y")..duration("P2Y")]""") should be( - ValBoolean(false) - ) + evaluateUnaryTests( + """[duration("P1Y")..duration("P2Y")]""", + yearMonthDuration("P6M") + ) should returnResult(false) + evaluateUnaryTests( + """[duration("P1Y")..duration("P2Y")]""", + yearMonthDuration("P1Y8M") + ) should returnResult(true) + evaluateUnaryTests( + """[duration("P1Y")..duration("P2Y")]""", + yearMonthDuration("P2Y1M") + ) should returnResult(false) } "A day-time-duration" should "compare with '<'" in { - evalUnaryTests(dayTimeDuration("P1DT4H"), """< duration("P2DT4H")""") should be( - ValBoolean(true) + evaluateUnaryTests("""< duration("P2DT4H")""", dayTimeDuration("P1DT4H")) should returnResult( + true ) - evalUnaryTests(dayTimeDuration("P2DT4H"), """< duration("P2DT4H")""") should be( - ValBoolean(false) + evaluateUnaryTests("""< duration("P2DT4H")""", dayTimeDuration("P2DT4H")) should returnResult( + false ) - evalUnaryTests(dayTimeDuration("P2DT8H"), """< duration("P2DT4H")""") should be( - ValBoolean(false) + evaluateUnaryTests("""< duration("P2DT4H")""", dayTimeDuration("P2DT8H")) should returnResult( + false ) } it should "be equal to another duration" in { - evalUnaryTests(dayTimeDuration("P1DT4H"), """duration("P2DT4H")""") should be(ValBoolean(false)) - evalUnaryTests(dayTimeDuration("P2DT4H"), """duration("P2DT4H")""") should be(ValBoolean(true)) + evaluateUnaryTests("""duration("P2DT4H")""", dayTimeDuration("P1DT4H")) should returnResult( + false + ) + evaluateUnaryTests("""duration("P2DT4H")""", dayTimeDuration("P2DT4H")) should returnResult( + true + ) } it should """be in interval '[duration("P1D")..duration("P2D")]'""" in { - evalUnaryTests(dayTimeDuration("PT4H"), """[duration("P1D")..duration("P2D")]""") should be( - ValBoolean(false) - ) - evalUnaryTests(dayTimeDuration("P1DT4H"), """[duration("P1D")..duration("P2D")]""") should be( - ValBoolean(true) - ) - evalUnaryTests(dayTimeDuration("P2DT4H"), """[duration("P1D")..duration("P2D")]""") should be( - ValBoolean(false) - ) + evaluateUnaryTests( + """[duration("P1D")..duration("P2D")]""", + dayTimeDuration("PT4H") + ) should returnResult(false) + evaluateUnaryTests( + """[duration("P1D")..duration("P2D")]""", + dayTimeDuration("P1DT4H") + ) should returnResult(true) + evaluateUnaryTests( + """[duration("P1D")..duration("P2D")]""", + dayTimeDuration("P2DT4H") + ) should returnResult(false) } "A list" should "be equal to another list" in { - evalUnaryTests(List.empty, "[]") should be(ValBoolean(true)) - evalUnaryTests(List(1, 2), "[1,2]") should be(ValBoolean(true)) + evaluateUnaryTests("[]", List.empty) should returnResult(true) + evaluateUnaryTests("[1,2]", List(1, 2)) should returnResult(true) - evalUnaryTests(List(1, 2), "[]") should be(ValBoolean(false)) - evalUnaryTests(List(1, 2), "[1]") should be(ValBoolean(false)) - evalUnaryTests(List(1, 2), "[2,1]") should be(ValBoolean(false)) - evalUnaryTests(List(1, 2), "[1,2,3]") should be(ValBoolean(false)) + evaluateUnaryTests("[]", List(1, 2)) should returnResult(false) + evaluateUnaryTests("[1]", List(1, 2)) should returnResult(false) + evaluateUnaryTests("[2,1]", List(1, 2)) should returnResult(false) + evaluateUnaryTests("[1,2,3]", List(1, 2)) should returnResult(false) } it should "be checked in an every expression" in { - evalUnaryTests(List(1, 2, 3), "every x in ? satisfies x > 3") should be(ValBoolean(false)) - evalUnaryTests(List(4, 5, 6), "every x in ? satisfies x > 3") should be(ValBoolean(true)) + evaluateUnaryTests("every x in ? satisfies x > 3", List(1, 2, 3)) should returnResult(false) + evaluateUnaryTests("every x in ? satisfies x > 3", List(4, 5, 6)) should returnResult(true) } it should "be checked in a some expression" in { - evalUnaryTests(List(1, 2, 3), "some x in ? satisfies x > 4") should be(ValBoolean(false)) - evalUnaryTests(List(4, 5, 6), "some x in ? satisfies x > 4") should be(ValBoolean(true)) + evaluateUnaryTests("some x in ? satisfies x > 4", List(1, 2, 3)) should returnResult(false) + evaluateUnaryTests("some x in ? satisfies x > 4", List(4, 5, 6)) should returnResult(true) } "A context" should "be equal to another context" in { - evalUnaryTests(Map.empty, "{}") should be(ValBoolean(true)) - evalUnaryTests(Map("x" -> 1), "{x:1}") should be(ValBoolean(true)) + evaluateUnaryTests("{}", Map.empty) should returnResult(true) + evaluateUnaryTests("{x:1}", Map("x" -> 1)) should returnResult(true) - evalUnaryTests(Map("x" -> 1), "{}") should be(ValBoolean(false)) - evalUnaryTests(Map("x" -> 1), "{x:2}") should be(ValBoolean(false)) - evalUnaryTests(Map("x" -> 1), "{y:1}") should be(ValBoolean(false)) - evalUnaryTests(Map("x" -> 1), "{x:1,y:2}") should be(ValBoolean(false)) + evaluateUnaryTests("{}", Map("x" -> 1)) should returnResult(false) + evaluateUnaryTests("{x:2}", Map("x" -> 1)) should returnResult(false) + evaluateUnaryTests("{y:1}", Map("x" -> 1)) should returnResult(false) + evaluateUnaryTests("{x:1,y:2}", Map("x" -> 1)) should returnResult(false) } "An empty expression ('-')" should "be always true" in { - evalUnaryTests(None, "-") should be(ValBoolean(true)) + evaluateUnaryTests("-", None) should returnResult(true) } "A null expression" should "compare to null" in { - evalUnaryTests(1, "null") should be(ValBoolean(false)) - evalUnaryTests(true, "null") should be(ValBoolean(false)) - evalUnaryTests("a", "null") should be(ValBoolean(false)) + evaluateUnaryTests("null", 1) should returnResult(false) + evaluateUnaryTests("null", true) should returnResult(false) + evaluateUnaryTests("null", "a") should returnResult(false) - evalUnaryTests(null, "null") should be(ValBoolean(true)) + evaluateUnaryTests("null", inputValue = null) should returnResult(true) } "A function" should "be invoked with ? (input value)" in { - evalUnaryTests("foo", """ starts with(?, "f") """) should be(ValBoolean(true)) - evalUnaryTests("foo", """ starts with(?, "b") """) should be(ValBoolean(false)) + evaluateUnaryTests(""" starts with(?, "f") """, "foo") should returnResult(true) + evaluateUnaryTests(""" starts with(?, "b") """, "foo") should returnResult(false) } it should "be invoked as endpoint" in { - evalUnaryTests(2, "< max(1,2,3)") should be(ValBoolean(true)) - evalUnaryTests(2, "< min(1,2,3)") should be(ValBoolean(false)) + evaluateUnaryTests("< max(1,2,3)", 2) should returnResult(true) + evaluateUnaryTests("< min(1,2,3)", 2) should returnResult(false) } - "An expression" should "be compared with equals" in { + "A unary-tests expression" should "return true if it evaluates to a value that is equal to the implicit value" in { - evalUnaryTests(2, """number("2")""") should be(ValBoolean(true)) + evaluateUnaryTests("5", 5) should returnResult(true) + evaluateUnaryTests("2 + 3", 5) should returnResult(true) + evaluateUnaryTests("x", 5, Map("x" -> 5)) should returnResult(true) } - it should "be compared with a boolean" in { + it should "return false if it evaluates to a value that is not equal to the implicit value" in { - evalUnaryTests(false, """(5 < 4)""") should be(ValBoolean(true)) - evalUnaryTests(true, """(5 < 4)""") should be(ValBoolean(false)) + evaluateUnaryTests("3", 5) should returnResult(false) + evaluateUnaryTests("1 + 2", 5) should returnResult(false) + evaluateUnaryTests("x", 5, Map("x" -> 3)) should returnResult(false) } - it should "be compared to literal" in { + it should "return null if it evaluates to a value that has a different type than the implicit value" in { - evalUnaryTests(date("2019-08-12"), """ date(now) """, Map("now" -> "2019-08-12")) should be( - ValBoolean(true) - ) + evaluateUnaryTests(""" @"2024-08-19" """, 5) should returnNull() + } - evalUnaryTests(date("2019-08-12"), """ date(now) """, Map("now" -> "2019-08-13")) should be( - ValBoolean(false) - ) + it should "return true if it evaluates to a list that contains the implicit value" in { + + evaluateUnaryTests("[4,5,6]", 5) should returnResult(true) + evaluateUnaryTests("concatenate([1,2,3], [4,5,6])", 5) should returnResult(true) + evaluateUnaryTests("x", 5, Map("x" -> List(4, 5, 6))) should returnResult(true) } - it should "be compared with a list value" in { + it should "return false if it evaluates to a list that doesn't contain the implicit value" in { - evalUnaryTests(2, """[1,2,3]""") should be(ValBoolean(true)) - evalUnaryTests(4, """[1,2,3]""") should be(ValBoolean(false)) + evaluateUnaryTests("[1,2,3]", 5) should returnResult(false) + evaluateUnaryTests("concatenate([1,2], [3])", 5) should returnResult(false) + evaluateUnaryTests("x", 5, Map("x" -> List(1, 2, 3))) should returnResult(false) } - it should "be compared with a list variable" in { - evalUnaryTests(2, "x", Map("x" -> List(1, 2, 3))) should be(ValBoolean(true)) - evalUnaryTests(4, "x", Map("x" -> List(1, 2, 3))) should be(ValBoolean(false)) + it should "return true if it evaluates to true when the implicit value is applied to it" in { + + evaluateUnaryTests("< 10", 5) should returnResult(true) + evaluateUnaryTests("[1..10]", 5) should returnResult(true) + evaluateUnaryTests("> x", 5, Map("x" -> 3)) should returnResult(true) + } + + it should "return false if it evaluates to false when the implicit value is applied to it" in { + + evaluateUnaryTests("< 3", 5) should returnResult(false) + evaluateUnaryTests("[1..3]", 5) should returnResult(false) + evaluateUnaryTests("> x", 5, Map("x" -> 10)) should returnResult(false) + } + + it should "return null if it evaluates to null when the implicit value is applied to it" in { + + evaluateUnaryTests(""" < @"2024-08-19" """, 5) should returnNull() + evaluateUnaryTests(""" < @"2024-08-19" """, inputValue = null) should returnNull() + } + + it should "return true if it evaluates to true when the implicit value is assigned to the special variable '?'" in { + + evaluateUnaryTests("odd(?)", 5) should returnResult(true) + evaluateUnaryTests("abs(?) < 10", 5) should returnResult(true) + evaluateUnaryTests("? > x", 5, Map("x" -> 3)) should returnResult(true) + } + + it should "return false if it evaluates to false when the implicit value is assigned to the special variable '?'" in { + + evaluateUnaryTests("even(?)", 5) should returnResult(false) + evaluateUnaryTests("abs(?) < 3", 5) should returnResult(false) + evaluateUnaryTests("? > x", 5, Map("x" -> 10)) should returnResult(false) + } + + it should "return null if it evaluates to a value that is not a boolean when the implicit value is assigned to the special variable '?'" in { + + evaluateUnaryTests("abs(?)", 5) should returnNull() + evaluateUnaryTests("?", 5) should returnNull() + evaluateUnaryTests("? + not_existing", 5) should returnNull() + } + + it should "return true if it evaluates to null and the implicit value is null" in { + + evaluateUnaryTests("null", inputValue = null) should returnResult(true) + evaluateUnaryTests("2 + not_existing", inputValue = null) should returnResult(true) + evaluateUnaryTests("not_existing", inputValue = null) should returnResult(true) + } + + it should "return false if it evaluates to null and the implicit value is not null" in { + + evaluateUnaryTests("null", 5) should returnResult(false) + evaluateUnaryTests("2 + not_existing", 5) should returnResult(false) + evaluateUnaryTests("not_existing", 5) should returnResult(false) + } + + it should "return true if it evaluates to true when null is assigned to the special variable '?'" in { + + evaluateUnaryTests("? = null", inputValue = null) should returnResult(true) + evaluateUnaryTests("odd(?) or ? = null", inputValue = null) should returnResult(true) + } + + it should "return false if it evaluates to false when null is assigned to the special variable '?'" in { + + evaluateUnaryTests("? != null", inputValue = null) should returnResult(false) + evaluateUnaryTests("odd(?) and ? != null", inputValue = null) should returnResult(false) + } + + it should "return null if it evaluates to null when null is assigned to the special variable '?'" in { + + evaluateUnaryTests("? < 10", inputValue = null) should returnNull() + evaluateUnaryTests("odd(?)", inputValue = null) should returnNull() + evaluateUnaryTests("5 < ? and ? < 10", inputValue = null) should returnNull() + evaluateUnaryTests("5 < ? or ? < 10", inputValue = null) should returnNull() } }