Skip to content

Commit

Permalink
add isMap and isList
Browse files Browse the repository at this point in the history
  • Loading branch information
Łukasz Bigorajski committed Oct 22, 2024
1 parent e236e1b commit 2718203
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 41 deletions.
4 changes: 2 additions & 2 deletions docs/Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -40,23 +61,30 @@ 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
}

}

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),
Expand All @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@ class ExtensionMethodsSpec extends AnyFunSuite with Matchers {
"canCastTo",
"castTo",
"castToOrNull",
"isMap",
"toMap",
"toMapOrNull",
"isList",
"toList",
"toListOrNull"
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}

Expand All @@ -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
}
}

Expand Down

0 comments on commit 2718203

Please sign in to comment.