diff --git a/docs/Changelog.md b/docs/Changelog.md index 470d4d775e6..b19dd34db13 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -77,8 +77,8 @@ * [#6988](https://github.com/TouK/nussknacker/pull/6988) Remove unused API classes: `MultiMap`, `TimestampedEvictableStateFunction` * [#7000](https://github.com/TouK/nussknacker/pull/7000) Show all possible options for dictionary editor on open. * [#7042](https://github.com/TouK/nussknacker/pull/7042) SpeL: added extension methods: - * toList/toListOrNull - a collection or unknown collection can be converted to a list. - * toMap/toMapOrNull - the list of key-value pairs or unknown map can be converted to a map. + * toList/toListOrNull/isList - a collection or unknown collection can be converted to a list. + * toMap/toMapOrNull/isMap - the list of key-value pairs or unknown map can be converted to a map. ## 1.17 diff --git a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/extension/CollectionConversionExt.scala b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/extension/CollectionConversionExt.scala index 2b010f9a2f8..f15c6839968 100644 --- a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/extension/CollectionConversionExt.scala +++ b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/extension/CollectionConversionExt.scala @@ -4,27 +4,48 @@ import cats.data.ValidatedNel import cats.implicits.catsSyntaxValidatedId import pl.touk.nussknacker.engine.api.generics.{GenericFunctionTypingError, MethodTypeInfo} import pl.touk.nussknacker.engine.api.typed.typing.{Typed, TypedClass, TypedObjectTypingResult, TypingResult, Unknown} -import pl.touk.nussknacker.engine.definition.clazz.{ClassDefinitionSet, FunctionalMethodDefinition, MethodDefinition} -import pl.touk.nussknacker.engine.extension.CollectionConversionExt.{key, value} +import pl.touk.nussknacker.engine.definition.clazz.{ + ClassDefinitionSet, + FunctionalMethodDefinition, + MethodDefinition, + StaticMethodDefinition +} +import pl.touk.nussknacker.engine.extension.CollectionConversionExt.{collectionClass, key, keyAndValueKeyes, value} import pl.touk.nussknacker.engine.util.classes.Extensions.ClassExtensions -import java.util.{ArrayList => JArrayList, Collection => JCollection, HashMap => JHashMap, List => JList, Map => JMap} +import java.util.{ + ArrayList => JArrayList, + Collection => JCollection, + HashMap => JHashMap, + List => JList, + Map => JMap, + Set => JSet +} class CollectionConversionExt(target: Any) { + def isList(): Boolean = target.getClass.isAOrChildOf(collectionClass) + def toList[T](): JList[T] = convertToList[T] match { case Right(value) => value case Left(ex) => throw ex } - def toMap[K, V](): JMap[K, V] = convertToMap[K, V] match { + def toListOrNull[T](): JList[T] = convertToList[T] match { case Right(value) => value - case Left(ex) => throw ex + case Left(_) => null } - def toListOrNull[T](): JList[T] = convertToList[T] match { + def isMap(): Boolean = + target match { + case c: JCollection[_] => canConvertToMap(c) + case _: JMap[_, _] => true + case _ => false + } + + def toMap[K, V](): JMap[K, V] = convertToMap[K, V] match { case Right(value) => value - case Left(_) => null + case Left(ex) => throw ex } def toMapOrNull[K, V](): JMap[K, V] = convertToMap[K, V] match { @@ -40,16 +61,22 @@ class CollectionConversionExt(target: Any) { } } - private def convertToMap[K, V]: Either[Throwable, JMap[K, V]] = { + private def convertToMap[K, V]: Either[Throwable, JMap[K, V]] = target match { - case c: JCollection[JMap[String, Any] @unchecked] => + case c: JCollection[JMap[_, _] @unchecked] if canConvertToMap(c) => val map = new JHashMap[K, V]() c.forEach(e => map.put(e.get(key).asInstanceOf[K], e.get(value).asInstanceOf[V])) Right(map) case m: JMap[K, V] @unchecked => Right(m) case x => Left(new IllegalArgumentException(s"Cannot convert $x to a Map")) } - } + + private def canConvertToMap(c: JCollection[_]): Boolean = c.isEmpty || c + .stream() + .allMatch { + case m: JMap[_, _] if !m.isEmpty => m.keySet().containsAll(keyAndValueKeyes) + case _ => false + } } @@ -57,6 +84,7 @@ object CollectionConversionExt extends ExtensionMethodsHandler { private val collectionClass = classOf[JCollection[_]] private val unknownClass = classOf[Object] + private val booleanTyping = Typed.typedClass[Boolean] private val toMapDefinition = FunctionalMethodDefinition( typeFunction = (invocationTarget, _) => toMapTypeFunction(invocationTarget), @@ -80,15 +108,28 @@ object CollectionConversionExt extends ExtensionMethodsHandler { description = Option("Convert to a list or throw exception in case of failure") ) + private val isMethodDefinition = StaticMethodDefinition( + signature = MethodTypeInfo( + noVarArgs = Nil, + varArg = None, + result = booleanTyping + ), + name = "", + description = None + ) + private val definitions = List( + isMethodDefinition.copy(name = "isMap", description = Some("Check whether can be convert to a map")), toMapDefinition, toMapDefinition.copy(name = "toMapOrNull", description = Some("Convert to a map or null in case of failure")), + isMethodDefinition.copy(name = "isList", description = Some("Check whether can be convert to a list")), toListDefinition, toListDefinition.copy(name = "toListOrNull", description = Some("Convert to a list or null in case of failure")), ).groupBy(_.name) - private val key = "key" - private val value = "value" + private val key = "key" + private val value = "value" + private val keyAndValueKeyes = JSet.of(key, value) override type ExtensionMethodInvocationTarget = CollectionConversionExt override val invocationTargetClass: Class[CollectionConversionExt] = classOf[CollectionConversionExt] diff --git a/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/extension/ExtensionMethodsSpec.scala b/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/extension/ExtensionMethodsSpec.scala index 3a025e22818..46e06b43fae 100644 --- a/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/extension/ExtensionMethodsSpec.scala +++ b/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/extension/ExtensionMethodsSpec.scala @@ -39,8 +39,10 @@ class ExtensionMethodsSpec extends AnyFunSuite with Matchers { "canCastTo", "castTo", "castToOrNull", + "isMap", "toMap", "toMapOrNull", + "isList", "toList", "toListOrNull" ), diff --git a/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSpec.scala b/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSpec.scala index 6d721b73187..46062496a10 100644 --- a/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSpec.scala +++ b/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSpec.scala @@ -242,8 +242,8 @@ class SpelExpressionSpec extends AnyFunSuite with Matchers with ValidatedValuesD ClassDefinitionTestUtils.createDefinitionForClassesWithExtensions(typesFromGlobalVariables ++ customClasses: _*) } - private def evaluate[T: TypeTag](expr: String): T = - parse[T](expr = expr, context = ctx).validExpression.evaluateSync[T](ctx) + private def evaluate[T: TypeTag](expr: String, context: Context = ctx): T = + parse[T](expr = expr, context = context).validExpression.evaluateSync[T](context) test("parsing first selection on array") { parse[Any]("{1,2,3,4,5,6,7,8,9,10}.^[(#this%2==0)]").validExpression @@ -1529,33 +1529,34 @@ class SpelExpressionSpec extends AnyFunSuite with Matchers with ValidatedValuesD result } val mapWithDifferentValueTypes = Map("foo" -> "bar", "baz" -> 1).asJava + val customCtx = ctx + .withVariable("stringMap", stringMap) + .withVariable("mapWithDifferentValueTypes", mapWithDifferentValueTypes) + .withVariable("nullableMap", nullableMap) forAll( Table( - ("expression", "ctx", "expectedType", "expectedResult"), + ("expression", "expectedType", "expectedResult"), ( - "#mapVal.![{key: #this.key + '_k', value: #this.value + '_v'}].toMap()", - ctx.withVariable("mapVal", stringMap), + "#stringMap.![{key: #this.key + '_k', value: #this.value + '_v'}].toMap()", mapStringStringType, Map("foo_k" -> "bar_v", "baz_k" -> "qux_v").asJava ), ( - "#mapVal.![{key: #this.key, value: #this.value}].toMap()", - ctx.withVariable("mapVal", mapWithDifferentValueTypes), + "#mapWithDifferentValueTypes.![{key: #this.key, value: #this.value}].toMap()", mapStringUnknownType, mapWithDifferentValueTypes ), ( - "#mapVal.![{key: #this.key, value: #this.value}].toMap()", - ctx.withVariable("mapVal", nullableMap), + "#nullableMap.![{key: #this.key, value: #this.value}].toMap()", mapStringStringType, nullableMap ) ) - ) { (expression, ctx, expectedType, expectedResult) => - val parsed = parse[Any](expr = expression, context = ctx).validValue + ) { (expression, expectedType, expectedResult) => + val parsed = parse[Any](expr = expression, context = customCtx).validValue parsed.returnType.withoutValue shouldBe expectedType - parsed.expression.evaluateSync[Any](ctx) shouldBe expectedResult + parsed.expression.evaluateSync[Any](customCtx) shouldBe expectedResult } } @@ -1565,25 +1566,62 @@ class SpelExpressionSpec extends AnyFunSuite with Matchers with ValidatedValuesD } } - test("should convert unknown to a appropriate type") { - val map = Map("a" -> "b").asJava - val list = List("a").asJava - val mapTyping = Typed.genericTypeClass[JMap[_, _]](List(Unknown, Unknown)) - val listTyping = Typed.genericTypeClass[JList[_]](List(Unknown)) - val mapContext = ctx.withVariable("unknownMap", ContainerOfUnknown(map)) - val listContext = ctx.withVariable("unknownList", ContainerOfUnknown(list)) + test("should convert unknown to a given type") { + val mapTyping = Typed.genericTypeClass[JMap[_, _]](List(Unknown, Unknown)) + val listTyping = Typed.genericTypeClass[JList[_]](List(Unknown)) + val map = Map("a" -> "b").asJava + val emptyMap = Map().asJava + val list = List("a").asJava + val listOfTuples = List(Map("key" -> "a", "value" -> "b").asJava).asJava + val emptyList = List().asJava + val emptyTuplesList = List(Map().asJava).asJava + val customContext = ctx + .withVariable("unknownMap", ContainerOfUnknown(map)) + .withVariable("unknownList", ContainerOfUnknown(list)) + .withVariable("unknownListOfTuples", ContainerOfUnknown(listOfTuples)) + .withVariable("unknownEmptyList", ContainerOfUnknown(emptyList)) + .withVariable("unknownEmptyTuplesList", ContainerOfUnknown(emptyTuplesList)) forAll( Table( - ("expression", "ctx", "expectedType", "expectedResult"), - ("#unknownMap.value.toMap()", mapContext, mapTyping, map), - ("#unknownMap.value.toMapOrNull()", mapContext, mapTyping, map), - ("#unknownList.value.toList()", listContext, listTyping, list), - ("#unknownList.value.toListOrNull()", listContext, listTyping, list), + ("expression", "expectedType", "expectedResult"), + ("#unknownMap.value.toMap()", mapTyping, map), + ("#unknownMap.value.toMapOrNull()", mapTyping, map), + ("#unknownList.value.toList()", listTyping, list), + ("#unknownList.value.toListOrNull()", listTyping, list), + ("#unknownListOfTuples.value.toMap()", mapTyping, map), + ("#unknownListOfTuples.value.toMapOrNull()", mapTyping, map), + ("#unknownEmptyList.value.toMap()", mapTyping, emptyMap), + ("#unknownEmptyList.value.toList()", listTyping, emptyList), + ("#unknownEmptyTuplesList.value.toMapOrNull()", mapTyping, null), + ("#unknownEmptyTuplesList.value.toList()", listTyping, emptyTuplesList), ) - ) { (expression, ctx, expectedType, expectedResult) => - val parsed = parse[Any](expr = expression, context = ctx).validValue + ) { (expression, expectedType, expectedResult) => + val parsed = parse[Any](expr = expression, context = customContext).validValue parsed.returnType.withoutValue shouldBe expectedType - parsed.expression.evaluateSync[Any](ctx) shouldBe expectedResult + parsed.expression.evaluateSync[Any](customContext) shouldBe expectedResult + } + } + + test("should check if unknown can be converted to a given type") { + val map = Map("a" -> "b").asJava + val list = List("a").asJava + val tuplesList = List(Map("key" -> "a", "value" -> "b").asJava).asJava + val customCtx = ctx + .withVariable("unknownList", ContainerOfUnknown(list)) + .withVariable("unknownListOfTuples", ContainerOfUnknown(tuplesList)) + .withVariable("unknownMap", ContainerOfUnknown(map)) + forAll( + Table( + ("expression", "result"), + ("#unknownList.value.isList()", true), + ("#unknownList.value.isMap()", false), + ("#unknownMap.value.isList()", false), + ("#unknownMap.value.isMap()", true), + ("#unknownListOfTuples.value.isList()", true), + ("#unknownListOfTuples.value.isMap()", true), + ) + ) { (expression, result) => + evaluate[Any](expression, customCtx) shouldBe result } }