diff --git a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/extension/ToMapConversionExt.scala b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/extension/ToMapConversionExt.scala index b3bd7abd35f..77ebcb5510a 100644 --- a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/extension/ToMapConversionExt.scala +++ b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/extension/ToMapConversionExt.scala @@ -77,15 +77,16 @@ object ToMapConversion extends Conversion[JMap[_, _]] { @tailrec override def convertEither(value: Any): Either[Throwable, JMap[_, _]] = value match { - case m: JMap[_, _] => Right(m) - case a: Array[_] => convertEither(ConversionHandler.convertArrayToList(a)) - case c: JCollection[JMap[_, _] @unchecked] if canConvertToMap(c) => - val map = new JHashMap[Any, Any]() - c.forEach(e => map.put(e.get(keyName), e.get(valueName))) - Right(map) - case x => Left(new IllegalArgumentException(s"Cannot convert: $x to a Map")) + case m: JMap[_, _] => Right(m) + case a: Array[_] => convertEither(ConversionHandler.convertArrayToList(a)) + case c: JCollection[_] if canConvertToMap(c) => convertToMap(c).map(e => Right(e)).getOrElse(cannotConvertMessage(c)) + case x => cannotConvertMessage(x) } + private def cannotConvertMessage(c: Any): Left[IllegalArgumentException, Nothing] = { + Left(new IllegalArgumentException(s"Cannot convert: $c to a Map")) + } + // We could leave underlying method using convertEither as well but this implementation is faster override def canConvert(value: Any): JBoolean = value match { case _: JMap[_, _] => true @@ -94,12 +95,29 @@ object ToMapConversion extends Conversion[JMap[_, _]] { case _ => false } - private def canConvertToMap(c: JCollection[_]): Boolean = c.isEmpty || c + private def canConvertToMap(c: JCollection[_]): Boolean = c .stream() .allMatch { - case m: JMap[_, _] => m.keySet().containsAll(keyAndValueNames) - case _ => false + case m: JMap[_, _] => m.keySet().containsAll(keyAndValueNames) + case _: JMap.Entry[_, _] => true + case _ => false + } + + private def convertToMap(c: JCollection[_]): Option[JMap[_, _]] = { + val map = new JHashMap[Any, Any]() + var foundError = false + c.forEach { + case e: JMap[_, _] => map.put(e.get(keyName), e.get(valueName)) + case e: JMap.Entry[_, _] => map.put(e.getKey, e.getValue) + case _ => foundError = true + } + if (foundError) { + // internal error of this class, should not have been called with such value + None + } else { + Some(map) } + } override def appliesToConversion(clazz: Class[_]): Boolean = clazz != resultTypeClass && (clazz.isAOrChildOf(collectionClass) || clazz == unknownClass || clazz.isArray) 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 91cdd3b104b..80be87b147d 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 @@ -1663,6 +1663,36 @@ class SpelExpressionSpec extends AnyFunSuite with Matchers with ValidatedValuesD } } + test("should convert list of map entries into map") { + val result = evaluate[Any](""" + |{ + |a: "A", + |b: "B" + |}.![#this].toMap + |""".stripMargin) + result shouldBe java.util.Map.of("a", "A", "b", "B") + } + + test("should be able to convert list of map entries into map") { + val result = evaluate[Any](""" + |{ + |a: "A", + |b: "B" + |}.![#this].canBeMap + |""".stripMargin) + result shouldBe true + } + + test("should be able to convert list of map entries into map or null") { + val result = evaluate[Any](""" + |{ + |a: "A", + |b: "B" + |}.![#this].toMapOrNull + |""".stripMargin) + result shouldBe java.util.Map.of("a", "A", "b", "B") + } + test("should return error msg if record in map project does not contain required fields") { parse[Any]("#mapValue.![{invalid_key: #this.key}].toMap()", ctx).invalidValue.toList should matchPattern { case GenericFunctionError("List element must contain 'key' and 'value' fields") :: Nil =>