Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix TypingResult.valueOpt for the purpose of SpEL validator (fixed after rollback) #6436

Merged
merged 14 commits into from
Jul 31, 2024

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,10 @@ object TypeEncoders {
objTypeEncoded.+:(tagEncoded)
case TypedObjectWithValue(underlying, value) =>
val objTypeEncoded = encodeTypingResult(underlying)
val dataEncoded: (String, Json) = "value" -> SimpleObjectEncoder
.encode(underlying, value)
val dataEncoded: (String, Json) = "value" -> ValueEncoder
.encodeValue(value)
.getOrElse(throw new IllegalStateException(s"Not supported data value: $value"))

objTypeEncoded.+:(dataEncoded)
case cl: TypedClass => encodeTypedClass(cl)
}
Expand Down Expand Up @@ -129,7 +130,7 @@ class TypingResultDecoder(loadClass: String => Class[_]) {

private def typedObjectWithValue(obj: HCursor): Decoder.Result[TypingResult] = for {
valueClass <- typedClass(obj)
value <- SimpleObjectEncoder.decode(valueClass, obj.downField("value"))
value <- ValueDecoder.decodeValue(valueClass, obj.downField("value"))
} yield TypedObjectWithValue(valueClass, value)

private def typedObjectTypingResult(obj: HCursor): Decoder.Result[TypingResult] = for {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package pl.touk.nussknacker.engine.api.typed

import cats.implicits.toTraverseOps
import io.circe.{ACursor, Decoder, DecodingFailure, Json}
import pl.touk.nussknacker.engine.api.typed.typing._

import java.math.BigInteger
import scala.jdk.CollectionConverters._

object ValueDecoder {
private val intClass = Typed.typedClass[Int]
private val shortClass = Typed.typedClass[Short]
private val longClass = Typed.typedClass[Long]
private val floatClass = Typed.typedClass[Float]
private val doubleClass = Typed.typedClass[Double]
private val booleanClass = Typed.typedClass[Boolean]
private val stringClass = Typed.typedClass[String]
private val byteClass = Typed.typedClass[Byte]
private val bigIntegerClass = Typed.typedClass[BigInteger]
private val bigDecimalClass = Typed.typedClass[java.math.BigDecimal]

def decodeValue(typ: TypingResult, obj: ACursor): Decoder.Result[Any] = typ match {
case TypedObjectWithValue(_, value) => Right(value)
case TypedNull => Right(null)
case `intClass` => obj.as[Int]
case `shortClass` => obj.as[Short]
case `longClass` => obj.as[Long]
case `floatClass` => obj.as[Float]
case `doubleClass` => obj.as[Double]
case `booleanClass` => obj.as[Boolean]
case `stringClass` => obj.as[String]
case `byteClass` => obj.as[Byte]
case `bigIntegerClass` => obj.as[BigInteger]
case `bigDecimalClass` => obj.as[java.math.BigDecimal]
case TypedClass(klass, List(elementType: TypingResult)) if klass == classOf[java.util.List[_]] =>
obj.values match {
case Some(values) =>
values.toList
.traverse(v => decodeValue(elementType, v.hcursor))
.map(_.asJava)
case None =>
Left(DecodingFailure(s"Expected encoded List to be a Json array", List()))
}
case record: TypedObjectTypingResult =>
for {
fieldsJson <- obj.as[Map[String, Json]]
decodedFields <- record.fields.toList.traverse { case (fieldName, fieldType) =>
fieldsJson.get(fieldName) match {
case Some(fieldJson) => decodeValue(fieldType, fieldJson.hcursor).map(fieldName -> _)
case None =>
Left(
DecodingFailure(
s"Record field '$fieldName' isn't present in encoded Record fields: $fieldsJson",
List()
)
)
}
}
} yield decodedFields.toMap.asJava
case typ => Left(DecodingFailure(s"Decoding of type [$typ] is not supported.", List()))
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package pl.touk.nussknacker.engine.api.typed

import cats.data.ValidatedNel
import cats.implicits.{catsSyntaxValidatedId, toTraverseOps}
import io.circe.Json
import io.circe.Json.{fromBigDecimal, fromBigInt, fromBoolean, fromDouble, fromFloat, fromInt, fromLong, fromString}

import java.math.BigInteger
import scala.jdk.CollectionConverters._

object ValueEncoder {

def encodeValue(value: Any): ValidatedNel[String, Json] = value match {
case null => Json.Null.validNel
case value: Int => fromInt(value).validNel
case value: Short => fromInt(value).validNel
case value: Long => fromLong(value).validNel
case value: Boolean => fromBoolean(value).validNel
case value: String => fromString(value).validNel
case value: Byte => fromInt(value).validNel
case value: BigInteger => fromBigInt(value).validNel
case value: java.math.BigDecimal => fromBigDecimal(value).validNel
case value: Float =>
fromFloat(value).map(_.validNel).getOrElse(s"Could not encode $value as json.".invalidNel)
case value: Double =>
fromDouble(value).map(_.validNel).getOrElse(s"Could not encode $value as json.".invalidNel)

case vals: java.util.Collection[_] =>
val encodedValues = vals.asScala.map(elem => encodeValue(elem)).toList.sequence
encodedValues.map(values => Json.fromValues(values))
case vals: java.util.Map[_, _] =>
val encodedFields = vals.asScala.toList.map { case (key, value) =>
encodeValue(key).andThen(encodedKey =>
encodedKey.asString match {
case Some(encodedKeyString) =>
encodeValue(value).map(encodedValue => encodedKeyString -> encodedValue)
case None =>
s"Failed to encode Record key '$encodedKey' as String".invalidNel
}
)
}

encodedFields.sequence.map(values => Json.fromFields(values))
case value =>
s"Encoding of value [$value] of class [${value.getClass.getName}] is not supported".invalidNel
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ object typing {
objType: TypedClass,
additionalInfo: Map[String, AdditionalDataValue] = Map.empty
) extends SingleTypingResult {
override def valueOpt: Option[Map[String, Any]] =
fields.map { case (k, v) => v.valueOpt.map((k, _)) }.toList.sequence.map(Map(_: _*))
override def valueOpt: Option[java.util.Map[String, Any]] =
fields.map { case (k, v) => v.valueOpt.map((k, _)) }.toList.sequence.map(Map(_: _*).asJava)

override def withoutValue: TypedObjectTypingResult =
TypedObjectTypingResult(fields.mapValuesNow(_.withoutValue), objType, additionalInfo)
Expand Down Expand Up @@ -116,12 +116,16 @@ object typing {

override def withoutValue: SingleTypingResult = underlying.withoutValue

override def display: String = {
val dataString = value.toString
val shortenedDataString =
if (dataString.length <= maxDataDisplaySize) dataString
else dataString.take(maxDataDisplaySizeWithDots) ++ "..."
s"${underlying.display}($shortenedDataString)"
override def display: String = s"${underlying.display}($shortenedDataString)"

private def shortenedDataString = {
val dataString = value match {
case l: java.util.List[_] => l.asScala.mkString("{", ", ", "}")
case _ => value.toString
}

if (dataString.length <= maxDataDisplaySize) dataString
else dataString.take(maxDataDisplaySizeWithDots) ++ "..."
}

}
Expand Down Expand Up @@ -257,6 +261,15 @@ object typing {
cl
}

def typedListWithElementValues[T](
elementType: TypingResult,
elementValues: java.util.List[T]
): TypedObjectWithValue =
TypedObjectWithValue(
Typed.genericTypeClass(classOf[java.util.List[_]], List(elementType)),
elementValues
)

private def toRuntime[T: ClassTag]: Class[_] = implicitly[ClassTag[T]].runtimeClass

// parameters - None if you are not in generic aware context, Some - otherwise
Expand Down Expand Up @@ -328,12 +341,16 @@ object typing {
case list: List[_] =>
genericTypeClass(classOf[List[_]], List(supertypeOfElementTypes(list)))
case javaList: java.util.List[_] =>
genericTypeClass(classOf[java.util.List[_]], List(supertypeOfElementTypes(javaList.asScala.toList)))
typedListWithElementValues(
supertypeOfElementTypes(javaList.asScala.toList).withoutValue,
javaList
)
case typeFromInstance: TypedFromInstance => typeFromInstance.typingResult
// TODO: handle more types, for example Set
case other =>
Typed(other.getClass) match {
case typedClass: TypedClass =>
SimpleObjectEncoder.encode(typedClass, other) match {
ValueEncoder.encodeValue(other) match {
case Valid(_) => TypedObjectWithValue(typedClass, other)
case Invalid(_) => typedClass
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import org.scalatest.LoneElement
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
import org.scalatest.prop.TableDrivenPropertyChecks
import pl.touk.nussknacker.engine.api.typed.typing._
import pl.touk.nussknacker.engine.api.typed.typing.Typed.typedListWithElementValues
import pl.touk.nussknacker.engine.api.typed.typing.{Unknown, _}

class TypedFromInstanceTest extends AnyFunSuite with Matchers with LoneElement with TableDrivenPropertyChecks {

Expand Down Expand Up @@ -68,12 +69,20 @@ class TypedFromInstanceTest extends AnyFunSuite with Matchers with LoneElement w
val typingResult = Typed.fromInstance(obj)

typingResult.canBeSubclassOf(Typed(klass)) shouldBe true
typingResult.asInstanceOf[TypedClass].params.loneElement.canBeSubclassOf(paramTypingResult) shouldBe true
typingResult.withoutValue
.asInstanceOf[TypedClass]
.params
.loneElement
.canBeSubclassOf(paramTypingResult) shouldBe true
}

def checkNotASubclassOfOtherParamTypingResult(obj: Any, otherParamTypingResult: TypingResult): Unit = {
val typingResult = Typed.fromInstance(obj)
typingResult.asInstanceOf[TypedClass].params.loneElement.canBeSubclassOf(otherParamTypingResult) shouldBe false
typingResult.withoutValue
.asInstanceOf[TypedClass]
.params
.loneElement
.canBeSubclassOf(otherParamTypingResult) shouldBe false
}

val listOfSimpleObjects = List[Any](1.1, 2)
Expand All @@ -98,14 +107,28 @@ class TypedFromInstanceTest extends AnyFunSuite with Matchers with LoneElement w
)
}

test("should find element type for lists of different elements") {
test("should find element type for scala lists of different elements") {
Typed.fromInstance(List[Any](4L, 6.35, 8.47)) shouldBe Typed.genericTypeClass(
classOf[List[_]],
List(Typed.typedClass[Number])
)
Typed.fromInstance(List(3, "t")) shouldBe Typed.genericTypeClass(classOf[List[_]], List(Unknown))
}

test("should find element type and keep values for java lists of different elements") {
val numberList = List(4L, 6.35, 8.47).asJava
Typed.fromInstance(numberList) shouldBe typedListWithElementValues(
Typed.typedClass[Double],
numberList
)

val anyList = List(3, "t").asJava
Typed.fromInstance(anyList) shouldBe typedListWithElementValues(
Unknown,
anyList
)
}

test("should fallback to object's class") {
Typed.fromInstance("abc") shouldBe TypedObjectWithValue(Typed.typedClass[String], "abc")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import com.typesafe.scalalogging.LazyLogging
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks
import pl.touk.nussknacker.engine.api.typed.typing.Typed.typedListWithElementValues
import pl.touk.nussknacker.engine.api.typed.typing._
import pl.touk.nussknacker.test.EitherValuesDetailedMessage

import scala.jdk.CollectionConverters._

class TypingResultDecoderSpec
extends AnyFunSuite
with ScalaCheckDrivenPropertyChecks
Expand Down Expand Up @@ -42,9 +45,20 @@ class TypingResultDecoderSpec
Map("field1" -> Typed[String]),
Typed.typedClass[Map[String, Any]],
Map[String, AdditionalDataValue]("ad1" -> "aaa", "ad2" -> 22L, "ad3" -> true)
),
typedListWithElementValues(Typed[Int], List(1, 2, 3).asJava),
typedListWithElementValues(Typed[String], List("foo", "bar").asJava),
typedListWithElementValues(Typed.record(Map.empty), List(Map.empty.asJava).asJava),
typedListWithElementValues(
Typed.record(
Map("a" -> TypedObjectWithValue(Typed.typedClass[Int], 1))
),
List(Map("a" -> 1).asJava).asJava
)
).foreach { typing =>
decoder.decodeTypingResults.decodeJson(TypeEncoders.typingResultEncoder(typing)).rightValue shouldBe typing
val encoded = TypeEncoders.typingResultEncoder(typing)

decoder.decodeTypingResults.decodeJson(encoded).rightValue shouldBe typing
}

}
Expand Down
Loading
Loading