diff --git a/pom.xml b/pom.xml index fd5b1bb69..64198c718 100644 --- a/pom.xml +++ b/pom.xml @@ -328,6 +328,17 @@ org/camunda/feel/syntaxtree/Range 8001 + + + org/camunda/feel/syntaxtree/** + 7002 + scala.Function1 andThen(scala.Function1) + + + org/camunda/feel/syntaxtree/** + 7002 + scala.Function1 compose(scala.Function1) + diff --git a/src/main/scala/org/camunda/feel/FeelEngine.scala b/src/main/scala/org/camunda/feel/FeelEngine.scala index 8568a47ec..736ab641b 100644 --- a/src/main/scala/org/camunda/feel/FeelEngine.scala +++ b/src/main/scala/org/camunda/feel/FeelEngine.scala @@ -26,7 +26,7 @@ import org.camunda.feel.FeelEngine.{ import org.camunda.feel.context.{Context, FunctionProvider, VariableProvider} 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} @@ -205,9 +205,11 @@ class FeelEngine( private def eval(exp: ParsedExpression, context: EvalContext): EvalExpressionResult = { interpreter.eval(exp.expression)(context) match { - case ValError(cause) => + case ValError(cause) => Left(Failure(s"failed to evaluate expression '${exp.text}': $cause")) - case value => Right(valueMapper.unpackVal(value)) + case ValFatalError(cause) => + Left(Failure(s"failed to evaluate expression '${exp.text}': $cause")) + case value => Right(valueMapper.unpackVal(value)) } } 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 6a68b2dc2..3a6da6dfa 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,12 +34,13 @@ object BuiltinFunction { } private def error: PartialFunction[List[Val], Any] = { - case vars if (vars.exists(_.isInstanceOf[ValError])) => - vars.filter(_.isInstanceOf[ValError]).head.asInstanceOf[ValError] - case e => { - logger.warn(s"Suppressed failure: illegal arguments: $e") + 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(", ") + logger.warn(s"Suppressed failure: Illegal arguments: $argumentList") ValNull - } } } 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 b8f9d0571..5b8edfb65 100644 --- a/src/main/scala/org/camunda/feel/impl/interpreter/FeelInterpreter.scala +++ b/src/main/scala/org/camunda/feel/impl/interpreter/FeelInterpreter.scala @@ -16,103 +16,15 @@ */ package org.camunda.feel.impl.interpreter -import java.time.{Duration, Period} import org.camunda.feel.FeelEngine.UnaryTests 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.syntaxtree.{ - Addition, - ArithmeticNegation, - AtLeastOne, - ClosedConstRangeBoundary, - ClosedRangeBoundary, - Comparison, - Conjunction, - ConstBool, - ConstContext, - ConstDate, - ConstDateTime, - ConstDayTimeDuration, - ConstInputValue, - ConstList, - ConstLocalDateTime, - ConstLocalTime, - ConstNull, - ConstNumber, - ConstRange, - ConstRangeBoundary, - ConstString, - ConstTime, - ConstYearMonthDuration, - Disjunction, - Division, - Equal, - EveryItem, - Exp, - Exponentiation, - Filter, - For, - FunctionDefinition, - FunctionInvocation, - FunctionParameters, - GreaterOrEqual, - GreaterThan, - If, - In, - InputEqualTo, - InputGreaterOrEqual, - InputGreaterThan, - InputInRange, - InputLessOrEqual, - InputLessThan, - InstanceOf, - IterationContext, - JavaFunctionInvocation, - LessOrEqual, - LessThan, - Multiplication, - NamedFunctionParameters, - Not, - OpenConstRangeBoundary, - OpenRangeBoundary, - PathExpression, - PositionalFunctionParameters, - QualifiedFunctionInvocation, - RangeBoundary, - Ref, - SomeItem, - Subtraction, - UnaryTestExpression, - Val, - ValBoolean, - ValContext, - ValDate, - ValDateTime, - ValDayTimeDuration, - ValError, - ValFunction, - ValList, - ValLocalDateTime, - ValLocalTime, - ValNull, - ValNumber, - ValRange, - ValString, - ValTime, - ValYearMonthDuration, - ZonedTime -} -import org.camunda.feel.{ - Date, - DateTime, - DayTimeDuration, - LocalDateTime, - LocalTime, - Number, - Time, - YearMonthDuration, - logger -} +import org.camunda.feel.{Number, logger} + +import java.time.{Duration, Period} +import scala.reflect.ClassTag /** @author * Philipp Ossler @@ -124,7 +36,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) @@ -153,19 +65,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))) @@ -174,16 +86,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) => @@ -214,19 +127,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) } } ) @@ -239,12 +152,13 @@ class FeelInterpreter { case SomeItem(iterators, condition) => withCartesianProduct( iterators, - p => atLeastOne(p.map(vars => () => eval(condition)(context.addAll(vars))), ValBoolean) + p => + atLeastOneValue(p.map(vars => () => eval(condition)(context.addAll(vars))), ValBoolean) ) case EveryItem(iterators, condition) => withCartesianProduct( iterators, - p => all(p.map(vars => () => eval(condition)(context.addAll(vars))), ValBoolean) + p => allValues(p.map(vars => () => eval(condition)(context.addAll(vars))), ValBoolean) ) case For(iterators, exp) => withCartesianProduct( @@ -323,10 +237,12 @@ class FeelInterpreter { ) // unsupported expression - case exp => ValError(s"unsupported expression '$exp'") + case exp => ValError(s"Unsupported expression '$exp'") } + // ======== helpers ==================== + private def mapEither[T, R]( it: Iterable[T], f: T => Either[ValError, R], @@ -360,221 +276,143 @@ class FeelInterpreter { } } + // ========================================= + private def error(x: Val, message: String) = x match { case e: ValError => e case _ => ValError(message) } + 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 withValOrNull(x: Val): Val = x match { - case ValError(e) => { + case fatalError: ValFatalError => fatalError + case ValError(e) => { logger.warn(s"Suppressed failure: $e") ValNull } - case _ => x + case _ => x } - private def unaryOpDual(x: Val, y: Val, c: (Val, Val, Val) => Boolean, f: Boolean => Val)(implicit - context: EvalContext - ): Val = - withVal( - input, - _ match { - case ValNull => f(false) - case _ if (x == ValNull || y == ValNull) => f(false) - case i if (!i.isComparable) => ValError(s"$i is not comparable") - case _ if (!x.isComparable) => ValError(s"$x is not comparable") - case _ if (!y.isComparable) => ValError(s"$y is not comparable") - case i if (i.getClass != x.getClass) => - ValError(s"$i can not be compared to $x") - case i if (i.getClass != y.getClass) => - ValError(s"$i can not be compared to $y") - case i => f(c(i, x, y)) - } - ) + private def withValues(x: Val, y: Val, f: (Val, Val) => Val) = + withVal(x, valueX => withVal(y, valueY => f(valueX, valueY))) - private def withNumbers(x: Val, y: Val, f: (Number, Number) => Val): Val = - withNumber( - x, - x => { - withNumber( - y, - y => { - f(x, y) - } + 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( + value, + s"Expected ${getTypeName(tag.runtimeClass)} but found '$other'" ) - } - ) - - private def withNumber(x: Val, f: Number => Val): Val = x match { - case ValNumber(x) => f(x) - case _ => error(x, s"expected Number but found '$x'") + } } - private def withBoolean(x: Val, f: Boolean => Val): Val = x match { - case ValBoolean(x) => f(x) - case _ => error(x, 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 withBooleanOrNull(x: Val, f: Boolean => Val): Val = x match { - case ValBoolean(x) => f(x) - case _ => ValNull - } + private def isComparable(values: Val*): Boolean = values.forall(_.isComparable) - private def withBooleanOrFalse(x: Val, f: Boolean => Val): Val = x match { - case ValBoolean(x) => f(x) - case _ => { - logger.warn(s"Suppressed failure: expected Boolean but found '$x'") - f(false) - } - } + private def hasSameType(values: Val*): Boolean = values.map(_.getClass).distinct.size == 1 - private def withString(x: Val, f: String => Val): Val = x match { - case ValString(x) => f(x) - case _ => error(x, s"expected String but found '$x'") - } + // ======== type checks ==================== - private def withDates(x: Val, y: Val, f: (Date, Date) => Val): Val = - withDate( - x, - x => { - withDate( - y, - y => { - f(x, y) - } - ) - } - ) + private def withNumber(x: Val, f: Number => Val)(implicit context: EvalContext): Val = + withValueType[ValNumber](x, number => f(number.value)) - private def withDate(x: Val, f: Date => Val): Val = x match { - case ValDate(x) => f(x) - case _ => error(x, s"expected Date but found '$x'") - } + 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 withTimes(x: Val, y: Val, f: (Time, Time) => Val): Val = - withTime( - x, - x => { - withTime( - 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 withLocalTimes(x: Val, y: Val, f: (LocalTime, LocalTime) => Val): Val = - withLocalTime( - x, - x => { - withLocalTime( - y, - y => { - f(x, y) - } - ) - } - ) + private def withBooleanOrNull(x: Val, f: Boolean => Val)(implicit context: EvalContext): Val = + withBoolean(x, f) match { + case _: ValError => ValNull + case value => value + } - private def withLocalTime(x: Val, f: LocalTime => Val): Val = x match { - case ValLocalTime(x) => f(x) - case _ => error(x, s"expect Local Time but found '$x'") - } + private def withBooleanOrFalse(x: Val, f: Boolean => Val)(implicit context: EvalContext): Val = + withBoolean(x, f) match { + case _: ValError => + logger.warn(s"Suppressed failure: Expected boolean but found '$x'") + f(false) + case value => value + } - private def withTime(x: Val, f: Time => Val): Val = x match { - case ValTime(x) => f(x) - case _ => error(x, s"expect Time but found '$x'") - } + private def withFunction(x: Val, f: ValFunction => Val)(implicit context: EvalContext): Val = + withValueType[ValFunction](x, f) - private def withDateTimes(x: Val, y: Val, f: (DateTime, DateTime) => Val): Val = - withDateTime( - x, - x => { - withDateTime( - y, - y => { - f(x, y) - } - ) - } - ) + private def withList(x: Val, f: ValList => Val)(implicit context: EvalContext): Val = + withValueType[ValList](x, f) - private def withLocalDateTimes(x: Val, y: Val, f: (LocalDateTime, LocalDateTime) => Val): Val = - withLocalDateTime( - x, - x => { - withLocalDateTime( - y, - y => { - f(x, y) - } - ) - } - ) + private def withContext(x: Val, f: ValContext => Val)(implicit context: EvalContext): Val = + withValueType[ValContext](x, f) - private def withDateTime(x: Val, f: DateTime => Val): Val = x match { - case ValDateTime(x) => f(x) - case _ => error(x, s"expect Date Time but found '$x'") - } + // ========================================= - private def withLocalDateTime(x: Val, f: LocalDateTime => Val): Val = - x match { - case ValLocalDateTime(x) => f(x) - case _ => error(x, s"expect Local Date 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 withYearMonthDurations( - x: Val, - y: Val, - f: (YearMonthDuration, YearMonthDuration) => Val - ): Val = - withYearMonthDuration( - x, - x => { - withYearMonthDuration( - y, - y => { - f(x, y) - } - ) - } - ) + private def getImplicitInputValue(implicit context: EvalContext): Val = { + context.variable(getInputVariableName) + } + + 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 withDayTimeDurations( - x: Val, - y: Val, - f: (DayTimeDuration, DayTimeDuration) => Val + private def unaryOpDual(x: Val, y: Val, c: (Val, Val, Val) => Boolean, f: Boolean => Val)(implicit + context: EvalContext ): Val = - withDayTimeDuration( - x, - x => { - withDayTimeDuration( - y, - y => { - f(x, y) - } - ) + withVal( + getImplicitInputValue, + { + case ValNull => f(false) + case _ if (x == ValNull || y == ValNull) => f(false) + case i if !isComparable(i, x, y) || !hasSameType(i, x, y) => + ValError(s"Can't compare '$i' with '$x' and '$y'") + case i => f(c(i, x, y)) } ) - private def withYearMonthDuration(x: Val, f: YearMonthDuration => Val): Val = - x match { - case ValYearMonthDuration(x) => f(x) - case _ => error(x, s"expect Year-Month-Duration but found '$x'") - } - - private def withDayTimeDuration(x: Val, f: DayTimeDuration => Val): Val = - x match { - case ValDayTimeDuration(x) => f(x) - case _ => error(x, s"expect Day-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 isInRange(range: ConstRange): (Val, Val, Val) => Boolean = (i, x, y) => { val inStart: Boolean = range.start match { @@ -589,292 +427,290 @@ class FeelInterpreter { } private def atLeastOne(xs: List[Exp], f: Boolean => Val)(implicit context: EvalContext): Val = - atLeastOne(xs map (x => () => eval(x)), f) + atLeastOneValue(xs map (x => () => eval(x)), f) - private def atLeastOne(items: List[() => Val], f: Boolean => Val): Val = { + private def atLeastOneValue(items: List[() => Val], f: Boolean => Val)(implicit + 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) } } private def all(xs: List[Exp], f: Boolean => Val)(implicit context: EvalContext): Val = - all(xs map (x => () => eval(x)), f) + allValues(xs map (x => () => eval(x)), f) - private def all(items: List[() => Val], f: Boolean => Val): Val = { + private def allValues(items: List[() => Val], f: Boolean => Val)(implicit + 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) - - 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(x, 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 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( + ): Val = { + (x, y) match { + case (fatalError: ValFatalError, _) => fatalError + case (_, fatalError: ValFatalError) => fatalError + case (ValNull, _) => f(c(ValNull, y.toOption.getOrElse(ValNull))) + case (_, ValNull) => f(c(x.toOption.getOrElse(ValNull), ValNull)) + case _ => + withValues( + x, y, - y => { - if (x.size != y.items.size) { - f(false) + { + 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.items).foldRight(true) { case ((x, y), listIsEqual) => - listIsEqual && { - checkEquality(x, y, c, f) match { - case ValBoolean(itemIsEqual) => itemIsEqual - case _ => 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 + case (ValContext(x), ValContext(y)) => + val xVars = x.variableProvider.getVariables + val yVars = y.variableProvider.getVariables - if (xVars.keys != yVars.keys) { - f(false) + 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 + } 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(x, s"Can't compare '$x' with '$y'") } ) - case _ => - error( - x, - s"expected Number, Boolean, String, Date, Time, Duration, List or Context but found '$x'" - ) } + } private def dualOp(x: Val, y: Val, c: (Val, Val) => Boolean, f: Boolean => Val)(implicit context: EvalContext - ): Val = - x match { - case ValNull => withVal(y, y => ValBoolean(false)) - case _ if (y == ValNull) => withVal(x, x => ValBoolean(false)) - case _ if (!x.isComparable) => ValError(s"$x is not comparable") - case _ if (!y.isComparable) => ValError(s"$y is not comparable") - case _ if (x.getClass != y.getClass) => - ValError(s"$x can not be compared to $y") - case _ => f(c(x, y)) + ): Val = { + (x, y) match { + case (ValNull, _) => withVal(y, y => ValBoolean(false)) + case (_, ValNull) => withVal(x, x => ValBoolean(false)) + case _ => + withValues( + x, + y, + { + case _ if !isComparable(x, y) || !hasSameType(x, y) => + error(x, s"Can't compare '$x' with '$y'") + case _ => f(c(x, y)) + } + ) } + } - private def addOp(x: Val, y: Val): 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(y, s"expect Year-Month-/Day-Time-Duration but found '$x'") - } - case ValDateTime(x) => - y match { - case ValYearMonthDuration(y) => ValDateTime(x.plus(y)) - case ValDayTimeDuration(y) => ValDateTime(x.plus(y)) - case _ => - error(y, s"expect Year-Month-/Day-Time-Duration but found '$x'") - } - case ValYearMonthDuration(x) => - y match { - case ValYearMonthDuration(y) => + 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 ValLocalDateTime(y) => ValLocalDateTime(y.plus(x)) - case ValDateTime(y) => ValDateTime(y.plus(x)) - case ValDate(y) => ValDate(y.plus(x)) - case _ => - error(y, s"expect Date-Time, Date, or Year-Month-Duration but found '$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(y, s"expect Date-Time, Date, Time, or Day-Time-Duration but found '$x'") - } - case ValDate(x) => - y match { - case ValDayTimeDuration(y) => - ValDate(x.atStartOfDay().plus(y).toLocalDate()) - case ValYearMonthDuration(y) => ValDate(x.plus(y)) - case _ => - error(y, s"expect Year-Month-/Day-Time-Duration but found '$x'") - } - case _ => - error(x, s"expected Number, String, Date, Time or Duration but found '$x'") - } + 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)) - private def subOp(x: Val, y: Val): 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(y, s"expect Time, or Day-Time-Duration but found '$x'") - } - case ValTime(x) => - y match { - case ValTime(y) => ValDayTimeDuration(ZonedTime.between(x, y)) - case ValDayTimeDuration(y) => ValTime(x.minus(y)) - case _ => error(y, s"expect Time, or Day-Time-Duration but found '$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(y, s"expect Time, or Year-Month-/Day-Time-Duration but found '$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(y, s"expect Time, or Year-Month-/Day-Time-Duration but found '$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(x, 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(y, s"expect Date, or Year-Month-/Day-Time-Duration but found '$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(x, 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(x, s"expected Number, Date, Time or Duration but found '$x'") - } + ) - private def mulOp(x: Val, y: Val): 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(y, s"expect Number, or Year-Month-/Day-Time-Duration but found '$x'") - } - 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(x, s"expected Number, or Duration but found '$x'") - } + case (ValNumber(x), ValDayTimeDuration(y)) => ValDayTimeDuration(y.multipliedBy(x.intValue)) - private def divOp(x: Val, y: Val): 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(x, s"expected Number, or Duration but found '$x'") + case (ValYearMonthDuration(x), ValNumber(y)) => + ValYearMonthDuration(x.multipliedBy(y.intValue).normalized) + case (ValDayTimeDuration(x), ValNumber(y)) => ValDayTimeDuration(x.multipliedBy(y.intValue)) + + case _ => error(x, 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 _ => ValError(s"'$x / $y' is not allowed") - } + 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) - private def unaryTestExpression(x: Val)(implicit context: EvalContext): Val = - withVal( - input, - i => - if (x == ValBoolean(true)) { - ValBoolean(true) + 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)) - } else if (checkEquality(i, x, _ == _, ValBoolean) == ValBoolean(true)) { - ValBoolean(true) + case _ => error(x, s"Can't divide '$x' by '$y'") + } + ) - } else { - x match { - case ValList(ys) => ValBoolean(ys.contains(i)) - case _ => ValBoolean(false) - } + private def unaryTestExpression(expression: Exp)(implicit context: EvalContext): Val = { + withVal( + 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 other => + error( + other, + s"The unary-test should return a boolean value when the input value is applied but was '$other'." + ) + ValNull + } + case error: ValError => error + 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] => + // 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 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) + } - private def withFunction(x: Val, f: ValFunction => Val): Val = x match { - case x: ValFunction => f(x) - case _ => error(x, s"expect Function but found '$x'") + function match { + case ValError(failure) => error(function, failure) + case _ => function + } } private def invokeFunction(function: ValFunction, params: FunctionParameters)(implicit @@ -908,63 +744,36 @@ class FeelInterpreter { } function.invoke(paramList) match { - case ValError(failure) => { + case fatalError: ValFatalError => fatalError + case ValError(failure) => { // TODO (saig0): customize error handling (#260) logger.warn(s"Failed to invoke function: $failure") ValNull } - case result => context.valueMapper.toVal(result) + 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) - } - - private def withType(x: Val, f: String => ValBoolean): 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(x, s"unexpected type '${x.getClass.getName}'") - } - - private def withList(x: Val, f: ValList => Val): Val = x match { - case x: ValList => f(x) - case _ => error(x, s"expect List but found '$x'") - } - private def withLists(lists: List[(String, Val)], f: List[(String, ValList)] => Val)(implicit context: EvalContext ): 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 { @@ -976,7 +785,9 @@ class FeelInterpreter { } yield values + (name -> v) // zip } - private def filterList(list: List[Val], filter: Val => Val): Val = { + private def filterList(list: List[Val], filter: Val => Val)(implicit + context: EvalContext + ): Val = { val conditionNotFulfilled = ValString("_") val withBooleanFilter = (list: List[Val]) => @@ -1039,11 +850,6 @@ class FeelInterpreter { } } - private def withContext(x: Val, f: ValContext => Val): Val = x match { - case x: ValContext => f(x) - case _ => error(x, 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) @@ -1065,14 +871,25 @@ class FeelInterpreter { case ctx: ValContext => EvalContext.wrap(ctx.context, context.valueMapper).variable(key) match { case _: ValError => - ValError(s"context contains no entry with key '$key'") + val detailedMessage = ctx.context.variableProvider.keys match { + case Nil => "The context is empty" + case keys => s"Available keys: ${keys.map("'" + _ + "'").mkString(", ")}" + } + error( + v, + s"No context entry found with key '$key'. $detailedMessage" + ) case x: Val => x - case _ => ValError(s"context contains no entry with key '$key'") } case ValList(list) => ValList(list map (item => path(item, key))) + case ValNull => + error( + v, + s"No context entry found with key '$key'. The context is null" + ) case value => value.property(key).getOrElse { - val propertyNames: String = value.propertyNames().mkString(",") + val propertyNames: String = value.propertyNames().map("'" + _ + "'").mkString(", ") error( value, s"No property found with name '$key' of value '$value'. Available properties: $propertyNames" @@ -1080,16 +897,13 @@ 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, arguments: List[String], paramValues: List[Val], valueMapper: ValueMapper - ): Val = { + )(implicit context: EvalContext): Val = { try { val clazz = JavaClassMapper.loadClass(className) @@ -1121,21 +935,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 { - ValError(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(startValue, s"Invalid range definition '$range'") + } + } ) } @@ -1160,3 +972,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 3b12fb6d6..0846d5f80 100644 --- a/src/main/scala/org/camunda/feel/syntaxtree/Val.scala +++ b/src/main/scala/org/camunda/feel/syntaxtree/Val.scala @@ -25,11 +25,14 @@ import org.camunda.feel.{ LocalTime, Number, Time, - YearMonthDuration + YearMonthDuration, + dateTimeFormatter, + localDateTimeFormatter, + localTimeFormatter } -import java.math.BigInteger import java.time.Duration +import java.util.regex.Pattern /** FEEL supports the following datatypes: number string boolean days and time duration years and * months duration time date and time Duration and date/time datatypes have no literal syntax. They @@ -91,22 +94,30 @@ 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) } } -case class ValNumber(value: Number) extends Val +case class ValNumber(value: Number) extends Val { + override def toString: String = value.toString() +} -case class ValBoolean(value: Boolean) extends Val +case class ValBoolean(value: Boolean) extends Val { + override def toString: String = value.toString +} -case class ValString(value: String) extends Val +case class ValString(value: String) extends Val { + override def toString: String = s"\"$value\"" +} case class ValDate(value: Date) extends Val { override protected val properties: Map[String, Val] = Map( @@ -115,6 +126,8 @@ case class ValDate(value: Date) extends Val { "day" -> ValNumber(value.getDayOfMonth), "weekday" -> ValNumber(value.getDayOfWeek.getValue) ) + + override def toString: String = value.toString } case class ValLocalTime(value: LocalTime) extends Val { @@ -125,6 +138,8 @@ case class ValLocalTime(value: LocalTime) extends Val { "time offset" -> ValNull, "timezone" -> ValNull ) + + override def toString: String = value.format(localTimeFormatter) } case class ValTime(value: Time) extends Val { @@ -136,6 +151,8 @@ case class ValTime(value: Time) extends Val { ValDayTimeDuration(Duration.ofSeconds(value.getOffsetInTotalSeconds)), "timezone" -> value.getZoneId.map(ValString).getOrElse(ValNull) ) + + override def toString: String = value.format } case class ValLocalDateTime(value: LocalDateTime) extends Val { @@ -150,6 +167,8 @@ case class ValLocalDateTime(value: LocalDateTime) extends Val { "time offset" -> ValNull, "timezone" -> ValNull ) + + override def toString: String = value.format(localDateTimeFormatter) } case class ValDateTime(value: DateTime) extends Val { @@ -169,54 +188,73 @@ case class ValDateTime(value: DateTime) extends Val { ) private def hasTimeZone = !value.getOffset.equals(value.getZone) + + override def toString: String = ValDateTime.format(value) +} + +object ValDateTime { + + private val dateTimeOffsetZoneIdPattern = Pattern.compile("(.*)([+-]\\d{2}:\\d{2}|Z)(@.*)") + + def format(value: DateTime): String = { + val formattedDateTime = value.format(dateTimeFormatter) + // remove offset-id if zone-id is present + dateTimeOffsetZoneIdPattern + .matcher(formattedDateTime) + .replaceAll("$1$3") + } + } case class ValYearMonthDuration(value: YearMonthDuration) extends Val { - override def toString: String = { - def makeString(sign: String, year: Long, month: Long): String = { - val y = Option(year).filterNot(_ == 0).map(_ + "Y").getOrElse("") - val m = Option(month).filterNot(_ == 0).map(_ + "M").getOrElse("") + override def toString: String = ValYearMonthDuration.format(value) - val stringBuilder = new StringBuilder("") - stringBuilder.append(sign).append("P").append(y).append(m) - stringBuilder.toString() - } + override val properties: Map[String, Val] = Map( + "years" -> ValNumber(value.getYears), + "months" -> ValNumber(value.getMonths) + ) +} + +object ValYearMonthDuration { + def format(value: YearMonthDuration): String = { val year = value.getYears val month = value.getMonths % 12 if (year == 0 && month == 0) "P0Y" else if (year <= 0 && month <= 0) - makeString("-", -year, -month) + "-" + mkString(-year, -month) else - makeString("", year, month) + mkString(year, month) } + private def mkString(year: Long, month: Long): String = { + val y = Option(year).filterNot(_ == 0).map(_ + "Y").getOrElse("") + val m = Option(month).filterNot(_ == 0).map(_ + "M").getOrElse("") + + val stringValue = new StringBuilder("P") + stringValue.append(y).append(m) + stringValue.toString() + } + +} + +case class ValDayTimeDuration(value: DayTimeDuration) extends Val { + override def toString: String = ValDayTimeDuration.format(value) + override val properties: Map[String, Val] = Map( - "years" -> ValNumber(value.getYears), - "months" -> ValNumber(value.getMonths) + "days" -> ValNumber(value.toDays), + "hours" -> ValNumber(value.toHours % 24), + "minutes" -> ValNumber(value.toMinutes % 60), + "seconds" -> ValNumber(value.getSeconds % 60) ) } -case class ValDayTimeDuration(value: DayTimeDuration) extends Val { - override def toString: String = { - def makeString(sign: String, day: Long, hour: Long, minute: Long, second: Long): String = { - val d = Option(day).filterNot(_ == 0).map(_ + "D").getOrElse("") - val h = Option(hour).filterNot(_ == 0).map(_ + "H").getOrElse("") - val m = Option(minute).filterNot(_ == 0).map(_ + "M").getOrElse("") - val s = Option(second).filterNot(_ == 0).map(_ + "S").getOrElse("") - - val stringBuilder = new StringBuilder("") - stringBuilder.append(sign).append("P").append(d) - if (h.nonEmpty || m.nonEmpty || s.nonEmpty) { - stringBuilder.append("T") - stringBuilder.append(h).append(m).append(s) - } - stringBuilder.toString() - } +object ValDayTimeDuration { + def format(value: DayTimeDuration): String = { val day = value.toDays val hour = value.toHours % 24 val minute = value.toMinutes % 60 @@ -225,30 +263,63 @@ case class ValDayTimeDuration(value: DayTimeDuration) extends Val { if (day == 0 && hour == 0 && minute == 0 && second == 0) "P0D" else if (day <= 0 && hour <= 0 && minute <= 0 && second <= 0) - makeString("-", -day, -hour, -minute, -second) + "-" + mkString(-day, -hour, -minute, -second) else - makeString("", day, hour, minute, second) + mkString(day, hour, minute, second) } - override val properties: Map[String, Val] = Map( - "days" -> ValNumber(value.toDays), - "hours" -> ValNumber(value.toHours % 24), - "minutes" -> ValNumber(value.toMinutes % 60), - "seconds" -> ValNumber(value.getSeconds % 60) - ) + + private def mkString(day: Long, hour: Long, minute: Long, second: Long): String = { + val d = Option(day).filterNot(_ == 0).map(_ + "D").getOrElse("") + val h = Option(hour).filterNot(_ == 0).map(_ + "H").getOrElse("") + val m = Option(minute).filterNot(_ == 0).map(_ + "M").getOrElse("") + val s = Option(second).filterNot(_ == 0).map(_ + "S").getOrElse("") + + val stringValue = new StringBuilder("P") + stringValue.append(d) + if (h.nonEmpty || m.nonEmpty || s.nonEmpty) { + stringValue.append("T") + stringValue.append(h).append(m).append(s) + } + stringValue.toString() + } + } -case class ValError(error: String) extends Val +case class ValError(error: String) extends Val { + override def toString: String = s"error(\"$error\")" +} -case object ValNull extends Val +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" +} case class ValFunction(params: List[String], invoke: List[Val] => Any, hasVarArgs: Boolean = false) extends Val { val paramSet: Set[String] = params.toSet + + override def toString: String = s"function(${params.mkString(", ")})" } -case class ValContext(context: Context) extends Val +case class ValContext(context: Context) extends Val { + override def toString: String = context.variableProvider.getVariables + .map { case (key, value) => s"$key:$value" } + .mkString(start = "{", sep = ", ", end = "}") +} -case class ValList(items: List[Val]) extends Val +case class ValList(items: List[Val]) extends Val { + override def toString: String = items.mkString(start = "[", sep = ", ", end = "]") +} -case class ValRange(start: RangeBoundary, end: RangeBoundary) extends Val +case class ValRange(start: RangeBoundary, end: RangeBoundary) extends Val { + override def toString: String = { + val startSymbol = if (start.isClosed) "[" else "(" + val endSymbol = if (end.isClosed) "]" else ")" + + s"$startSymbol${start.value}..${end.value}$endSymbol" + } +} diff --git a/src/test/scala/org/camunda/feel/api/FeelEngineTest.scala b/src/test/scala/org/camunda/feel/api/FeelEngineTest.scala index 80e6cd629..372a931b1 100644 --- a/src/test/scala/org/camunda/feel/api/FeelEngineTest.scala +++ b/src/test/scala/org/camunda/feel/api/FeelEngineTest.scala @@ -66,7 +66,7 @@ class FeelEngineTest extends AnyFlatSpec with Matchers with EitherValues { engine.evalUnaryTests("< 3", variables = Map(UnaryTests.defaultInputVariable -> "2")) should be( Left( Failure( - "failed to evaluate expression '< 3': ValString(2) can not be compared to ValNumber(3)" + """failed to evaluate expression '< 3': Can't compare '"2"' with '3'""" ) ) ) diff --git a/src/test/scala/org/camunda/feel/impl/FeelEngineTest.scala b/src/test/scala/org/camunda/feel/impl/FeelEngineTest.scala index 95c8db41e..76d8349e3 100644 --- a/src/test/scala/org/camunda/feel/impl/FeelEngineTest.scala +++ b/src/test/scala/org/camunda/feel/impl/FeelEngineTest.scala @@ -16,10 +16,21 @@ */ package org.camunda.feel.impl +import org.camunda.feel.{ + Date, + DateTime, + DayTimeDuration, + LocalDateTime, + LocalTime, + Time, + YearMonthDuration +} import org.camunda.feel.FeelEngine import org.camunda.feel.FeelEngine.{EvalExpressionResult, EvalUnaryTestsResult} 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 { @@ -88,4 +99,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/interpreter/DateTimeDurationPropertiesTest.scala b/src/test/scala/org/camunda/feel/impl/interpreter/DateTimeDurationPropertiesTest.scala index 13af0592b..f7a450e79 100644 --- a/src/test/scala/org/camunda/feel/impl/interpreter/DateTimeDurationPropertiesTest.scala +++ b/src/test/scala/org/camunda/feel/impl/interpreter/DateTimeDurationPropertiesTest.scala @@ -54,7 +54,7 @@ class DateTimeDurationPropertiesTest extends AnyFlatSpec with Matchers with Feel result shouldBe a[ValError] result.asInstanceOf[ValError].error should startWith( - "No property found with name 'x' of value 'ValDate(2020-09-30)'. Available properties:" + "No property found with name 'x' of value '2020-09-30'. Available properties: 'year', 'month', 'day', 'weekday'" ) } @@ -97,7 +97,7 @@ class DateTimeDurationPropertiesTest extends AnyFlatSpec with Matchers with Feel result shouldBe a[ValError] result.asInstanceOf[ValError].error should startWith( - "No property found with name 'x' of value 'ValTime(ZonedTime(11:45:30,+02:00,None))'. Available properties:" + "No property found with name 'x' of value '11:45:30+02:00'. Available properties: 'timezone', 'second', 'time offset', 'minute', 'hour'" ) } @@ -139,7 +139,7 @@ class DateTimeDurationPropertiesTest extends AnyFlatSpec with Matchers with Feel result shouldBe a[ValError] result.asInstanceOf[ValError].error should startWith( - "No property found with name 'x' of value 'ValLocalTime(11:45:30)'. Available properties:" + "No property found with name 'x' of value '11:45:30'. Available properties: 'timezone', 'second', 'time offset', 'minute', 'hour'" ) } @@ -208,7 +208,7 @@ class DateTimeDurationPropertiesTest extends AnyFlatSpec with Matchers with Feel result shouldBe a[ValError] result.asInstanceOf[ValError].error should startWith( - "No property found with name 'x' of value 'ValDateTime(2020-09-30T22:50:30+02:00)'. Available properties:" + "No property found with name 'x' of value '2020-09-30T22:50:30+02:00'. Available properties: 'timezone', 'year', 'second', 'month', 'day', 'time offset', 'weekday', 'minute', 'hour'" ) } @@ -269,7 +269,7 @@ class DateTimeDurationPropertiesTest extends AnyFlatSpec with Matchers with Feel result shouldBe a[ValError] result.asInstanceOf[ValError].error should startWith( - "No property found with name 'x' of value 'ValLocalDateTime(2020-09-30T22:50:30)'. Available properties:" + "No property found with name 'x' of value '2020-09-30T22:50:30'. Available properties: 'timezone', 'year', 'second', 'month', 'day', 'time offset', 'weekday', 'minute', 'hour'" ) } diff --git a/src/test/scala/org/camunda/feel/impl/interpreter/InterpreterBeanExpressionTest.scala b/src/test/scala/org/camunda/feel/impl/interpreter/InterpreterBeanExpressionTest.scala index 358a8deae..421af2100 100644 --- a/src/test/scala/org/camunda/feel/impl/interpreter/InterpreterBeanExpressionTest.scala +++ b/src/test/scala/org/camunda/feel/impl/interpreter/InterpreterBeanExpressionTest.scala @@ -51,7 +51,7 @@ class InterpreterBeanExpressionTest extends AnyFlatSpec with Matchers with FeelI } eval("a.result", Map("a" -> new A(2))) should be( - ValError("context contains no entry with key 'result'") + ValError("No context entry found with key 'result'. Available keys: ") ) } @@ -62,7 +62,7 @@ class InterpreterBeanExpressionTest extends AnyFlatSpec with Matchers with FeelI } eval("a.plus", Map("a" -> new A(2))) should be( - ValError("context contains no entry with key 'plus'") + ValError("No context entry found with key 'plus'. Available keys: ") ) } @@ -70,7 +70,7 @@ class InterpreterBeanExpressionTest extends AnyFlatSpec with Matchers with FeelI class A(private val x: Int) eval("a.x", Map("a" -> new A(2))) should be( - ValError("context contains no entry with key 'x'") + ValError("No context entry found with key 'x'. Available keys: ") ) } @@ -80,7 +80,7 @@ class InterpreterBeanExpressionTest extends AnyFlatSpec with Matchers with FeelI } eval("a.result", Map("a" -> new A(2))) should be( - ValError("context contains no entry with key 'result'") + ValError("No context entry found with key 'result'. Available keys: 'x'") ) } diff --git a/src/test/scala/org/camunda/feel/impl/interpreter/InterpreterContextExpressionTest.scala b/src/test/scala/org/camunda/feel/impl/interpreter/InterpreterContextExpressionTest.scala index e2ba82e27..31c08141b 100644 --- a/src/test/scala/org/camunda/feel/impl/interpreter/InterpreterContextExpressionTest.scala +++ b/src/test/scala/org/camunda/feel/impl/interpreter/InterpreterContextExpressionTest.scala @@ -96,7 +96,7 @@ class InterpreterContextExpressionTest } it should "fail if compare to not a context" in { - evaluateExpression("{} = 1") should failWith("expect Context but found 'ValNumber(1)'") + evaluateExpression("{} = 1") should failWith("Can't compare '{}' with '1'") } it should "fail when special symbols violate context syntax" in { @@ -135,26 +135,34 @@ class InterpreterContextExpressionTest } it should "fail if the context is empty" in { - evaluateExpression("{}.x") should failWith("context contains no entry with key 'x'") + evaluateExpression("{}.x") should failWith( + "No context entry found with key 'x'. The context is empty" + ) } it should "fail if no entry exists with the key" in { - evaluateExpression("{x:1, y:2}.z") should failWith("context contains no entry with key 'z'") + evaluateExpression("{x:1, y:2}.z") should failWith( + "No context entry found with key 'z'. Available keys: 'x', 'y'" + ) } - it should "return fail if the context is null" in { + it should "fail if the context is null" in { evaluateExpression( expression = "a.b", variables = Map("a" -> null) - ) should failWith("No property found with name 'b' of value 'ValNull'") + ) should failWith("No context entry found with key 'b'. The context is null") } it should "fail if the chained context is null" in { - evaluateExpression("{a:1}.b.c") should failWith("context contains no entry with key 'b'") + evaluateExpression("{a:1}.b.c") should failWith( + "No context entry found with key 'b'. Available keys: 'a'" + ) } it should "fail if the context is empty (inside a context)" in { - evaluateExpression("{x:1, y:{}.z}") should failWith("context contains no entry with key 'z'") + evaluateExpression("{x:1, y:{}.z}") should failWith( + "No context entry found with key 'z'. The context is empty" + ) } it should "return the value of a key with whitespaces" in { 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 e602305e3..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,263 +17,284 @@ package org.camunda.feel.impl.interpreter import org.camunda.feel.FeelEngine.UnaryTests -import org.camunda.feel.impl.FeelIntegrationTest +import org.camunda.feel.impl.{EvaluationResultMatchers, FeelEngineTest} import org.camunda.feel.syntaxtree._ -import org.scalatest.matchers.should.Matchers import org.scalatest.flatspec.AnyFlatSpec +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("some = true") shouldBe a[ValError] - eval("every = true") shouldBe a[ValError] - eval("if = true") shouldBe a[ValError] - eval("then = true") shouldBe a[ValError] - eval("else = true") shouldBe a[ValError] - eval("function = true") shouldBe a[ValError] - eval("for = true") shouldBe a[ValError] - eval("between = true") shouldBe a[ValError] - eval("instance = true") shouldBe a[ValError] - eval("of = true") shouldBe a[ValError] - eval("not = true") shouldBe a[ValError] - eval("in = true") shouldBe a[ValError] - eval("satisfies = true") shouldBe a[ValError] - eval("and = true") shouldBe a[ValError] - eval("or = true") shouldBe a[ValError] - eval("return = true") shouldBe a[ValError] + 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 { + 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( @@ -300,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/InterpreterListExpressionTest.scala b/src/test/scala/org/camunda/feel/impl/interpreter/InterpreterListExpressionTest.scala index 7f3c0613f..5d184e778 100644 --- a/src/test/scala/org/camunda/feel/impl/interpreter/InterpreterListExpressionTest.scala +++ b/src/test/scala/org/camunda/feel/impl/interpreter/InterpreterListExpressionTest.scala @@ -150,7 +150,7 @@ class InterpreterListExpressionTest } it should "fail if compare to not a list" in { - evaluateExpression("[] = 1") should failWith("expect List but found 'ValNumber(1)'") + evaluateExpression("[] = 1") should failWith("Can't compare '[]' with '1'") } "A for-expression" should "iterate over a range" in { @@ -390,7 +390,7 @@ class InterpreterListExpressionTest it should "fail if the filter doesn't return a boolean or a number" in { evaluateExpression(""" [1,2,3,4]["not a valid filter"] """) should - failWith("Expected boolean filter or number but found 'ValString(not a valid filter)'") + failWith("""Expected boolean filter or number but found '"not a valid filter"'""") } it should "access an item property if the context contains a variable with the same name" in { 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 4d099b837..b87949382 100644 --- a/src/test/scala/org/camunda/feel/impl/interpreter/InterpreterUnaryTest.scala +++ b/src/test/scala/org/camunda/feel/impl/interpreter/InterpreterUnaryTest.scala @@ -16,525 +16,636 @@ */ 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 { + evaluateUnaryTests("3", inputValue = null) should returnResult(false) + } + + it should "compare null with less/greater than" in { + evaluateUnaryTests("< 3", inputValue = null) should returnResult(false) + evaluateUnaryTests("<= 3", inputValue = null) should returnResult(false) + evaluateUnaryTests("> 3", inputValue = null) should returnResult(false) + evaluateUnaryTests(">= 3", inputValue = null) should returnResult(false) + } - evalUnaryTests(null, "3") should be(ValBoolean(false)) - evalUnaryTests(null, "< 3") should be(ValBoolean(false)) - evalUnaryTests(null, "> 3") should be(ValBoolean(false)) - evalUnaryTests(null, "(0..10)") should be(ValBoolean(false)) + it should "compare null with interval" in { + evaluateUnaryTests("(0..10)", inputValue = null) should returnResult(false) } "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 + ) + evaluateUnaryTests("""time("11: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("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 false 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 returnResult(false) + } + + it should "return true if it evaluates to a list that contains the implicit value" in { - evalUnaryTests(date("2019-08-12"), """ date(now) """, Map("now" -> "2019-08-13")) should be( - ValBoolean(false) + 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 "return false if it evaluates to a list that doesn't contain the implicit value" in { + + 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 "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 "fail if the implicit value has a different type" in { + + evaluateUnaryTests(""" < @"2024-08-19" """, 5) should failWith( + "Can't compare '5' with '2024-08-19'" ) } - it should "be compared with a list value" in { + 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 false 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 returnResult(false) + evaluateUnaryTests("?", 5) should returnResult(false) + evaluateUnaryTests("? + not_existing", 5) should returnResult(false) + } + + 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) + } + + it should "return false if it evaluates to null and the implicit value is not null" in { - evalUnaryTests(2, """[1,2,3]""") should be(ValBoolean(true)) - evalUnaryTests(4, """[1,2,3]""") should be(ValBoolean(false)) + evaluateUnaryTests("null", 5) should returnResult(false) + evaluateUnaryTests("2 + not_existing", 5) 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 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 false if it evaluates to null when null is assigned to the special variable '?'" in { + + evaluateUnaryTests("? < 10", inputValue = null) should returnResult(false) + evaluateUnaryTests("odd(?)", inputValue = null) should returnResult(false) + evaluateUnaryTests("5 < ? and ? < 10", inputValue = null) should returnResult(false) + evaluateUnaryTests("5 < ? or ? < 10", inputValue = null) should returnResult(false) } }