From 6f6edfbfd56ca244ebb452bbb8eefb0c94d0cf57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Jourdan-Weil?= Date: Wed, 19 May 2021 17:45:54 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=9A=80=20Support=20parameterized?= =?UTF-8?q?=20types=20in=20ClassTagExtensions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/scala/ScalaObjectMapper.scala | 2 +- .../module/scala/ClassTagExtensions.scala | 211 ++++++++++++++---- .../module/scala/ClassTagExtensionsTest.scala | 48 ++-- 3 files changed, 200 insertions(+), 61 deletions(-) diff --git a/src/main/scala-2.+/com/fasterxml/jackson/module/scala/ScalaObjectMapper.scala b/src/main/scala-2.+/com/fasterxml/jackson/module/scala/ScalaObjectMapper.scala index b480e9a6f..9a9fc6795 100644 --- a/src/main/scala-2.+/com/fasterxml/jackson/module/scala/ScalaObjectMapper.scala +++ b/src/main/scala-2.+/com/fasterxml/jackson/module/scala/ScalaObjectMapper.scala @@ -15,7 +15,7 @@ object ScalaObjectMapper { extends JsonMapper(mapper.rebuild().build()) with ScalaObjectMapper } -@deprecated("ScalaObjectMapper is deprecated because Manifests are not supported in Scala3", "2.12.1") +@deprecated("ScalaObjectMapper is deprecated because Manifests are not supported in Scala3, you might want to use ClassTagExtensions as a replacement", "2.12.1") trait ScalaObjectMapper { self: ObjectMapper => diff --git a/src/main/scala/com/fasterxml/jackson/module/scala/ClassTagExtensions.scala b/src/main/scala/com/fasterxml/jackson/module/scala/ClassTagExtensions.scala index d035cbea2..8606b7fdf 100644 --- a/src/main/scala/com/fasterxml/jackson/module/scala/ClassTagExtensions.scala +++ b/src/main/scala/com/fasterxml/jackson/module/scala/ClassTagExtensions.scala @@ -2,6 +2,7 @@ package com.fasterxml.jackson.module.scala import com.fasterxml.jackson.core.{JsonParser, TreeNode} import com.fasterxml.jackson.databind._ +import com.fasterxml.jackson.databind.`type`.TypeFactory import com.fasterxml.jackson.databind.json.JsonMapper import java.io.{File, InputStream, Reader} @@ -15,11 +16,8 @@ object ClassTagExtensions { } /** - * Experimental ClassTag equivalent of ScalaObjectMapper. This does not do a good job with - * reference types that wrap primitives, eg Option[Int], Seq[Boolean]. - * - * This is because ClassTags only provide access to the Java class and information - * about the wrapped types is lost due to type erasure. + * Experimental ClassTag equivalent of ScalaObjectMapper. + * This only works with non parameterized types or parameterized types up to 5 type parameters. */ trait ClassTagExtensions { self: ObjectMapper => @@ -56,6 +54,15 @@ trait ClassTagExtensions { ********************************************************** */ + /** + * Convenience method for constructing [[com.fasterxml.jackson.databind.JavaType]] out of given + * type (typically java.lang.Class), but without explicit + * context. + */ + def constructType[T: JavaTypeable]: JavaType = { + implicitly[JavaTypeable[T]].asJavaType(getTypeFactory) + } + /* ********************************************************** * Public API (from ObjectCodec): deserialization @@ -71,8 +78,8 @@ trait ClassTagExtensions { * and specifically needs to be used if the root type is a * parameterized (generic) container type. */ - def readValue[T: ClassTag](jp: JsonParser): T = { - readValue(jp, classFor[T]) + def readValue[T: JavaTypeable](jp: JsonParser): T = { + readValue(jp, constructType[T]) } /** @@ -87,8 +94,8 @@ trait ClassTagExtensions { *

* Note that [[com.fasterxml.jackson.databind.ObjectReader]] has more complete set of variants. */ - def readValues[T: ClassTag](jp: JsonParser): MappingIterator[T] = { - readValues(jp, classFor[T]) + def readValues[T: JavaTypeable](jp: JsonParser): MappingIterator[T] = { + readValues(jp, constructType[T]) } /* @@ -116,64 +123,64 @@ trait ClassTagExtensions { * convenience methods ********************************************************** */ - def readValue[T: ClassTag](src: File): T = { - readValue(src, classFor[T]) + def readValue[T: JavaTypeable](src: File): T = { + readValue(src, constructType[T]) } - def readValue[T: ClassTag](src: URL): T = { - readValue(src, classFor[T]) + def readValue[T: JavaTypeable](src: URL): T = { + readValue(src, constructType[T]) } - def readValue[T: ClassTag](content: String): T = { - readValue(content, classFor[T]) + def readValue[T: JavaTypeable](content: String): T = { + readValue(content, constructType[T]) } - def readValue[T: ClassTag](src: Reader): T = { - readValue(src, classFor[T]) + def readValue[T: JavaTypeable](src: Reader): T = { + readValue(src, constructType[T]) } - def readValue[T: ClassTag](src: InputStream): T = { - readValue(src, classFor[T]) + def readValue[T: JavaTypeable](src: InputStream): T = { + readValue(src, constructType[T]) } - def readValue[T: ClassTag](src: Array[Byte]): T = { - readValue(src, classFor[T]) + def readValue[T: JavaTypeable](src: Array[Byte]): T = { + readValue(src, constructType[T]) } - def readValue[T: ClassTag](src: Array[Byte], offset: Int, len: Int): T = { - readValue(src, offset, len, classFor[T]) + def readValue[T: JavaTypeable](src: Array[Byte], offset: Int, len: Int): T = { + readValue(src, offset, len, constructType[T]) } - def updateValue[T: ClassTag](valueToUpdate: T, src: File): T = { + def updateValue[T: JavaTypeable](valueToUpdate: T, src: File): T = { objectReaderFor(valueToUpdate).readValue(src) } - def updateValue[T: ClassTag](valueToUpdate: T, src: URL): T = { + def updateValue[T: JavaTypeable](valueToUpdate: T, src: URL): T = { objectReaderFor(valueToUpdate).readValue(src) } - def updateValue[T: ClassTag](valueToUpdate: T, content: String): T = { + def updateValue[T: JavaTypeable](valueToUpdate: T, content: String): T = { objectReaderFor(valueToUpdate).readValue(content) } - def updateValue[T: ClassTag](valueToUpdate: T, src: Reader): T = { + def updateValue[T: JavaTypeable](valueToUpdate: T, src: Reader): T = { objectReaderFor(valueToUpdate).readValue(src) } - def updateValue[T: ClassTag](valueToUpdate: T, src: InputStream): T = { + def updateValue[T: JavaTypeable](valueToUpdate: T, src: InputStream): T = { objectReaderFor(valueToUpdate).readValue(src) } - def updateValue[T: ClassTag](valueToUpdate: T, src: Array[Byte]): T = { + def updateValue[T: JavaTypeable](valueToUpdate: T, src: Array[Byte]): T = { objectReaderFor(valueToUpdate).readValue(src) } - def updateValue[T: ClassTag](valueToUpdate: T, src: Array[Byte], offset: Int, len: Int): T = { + def updateValue[T: JavaTypeable](valueToUpdate: T, src: Array[Byte], offset: Int, len: Int): T = { objectReaderFor(valueToUpdate).readValue(src, offset, len) } - private def objectReaderFor[T: ClassTag](valueToUpdate: T): ObjectReader = { - readerForUpdating(valueToUpdate).forType(classFor[T]) + private def objectReaderFor[T: JavaTypeable](valueToUpdate: T): ObjectReader = { + readerForUpdating(valueToUpdate).forType(constructType[T]) } /* @@ -202,8 +209,8 @@ trait ClassTagExtensions { * * @since 2.5 */ - def writerFor[T: ClassTag]: ObjectWriter = { - writerFor(classFor[T]) + def writerFor[T: JavaTypeable]: ObjectWriter = { + writerFor(constructType[T]) } /* @@ -217,8 +224,8 @@ trait ClassTagExtensions { * Factory method for constructing [[com.fasterxml.jackson.databind.ObjectReader]] that will * read or update instances of specified type */ - def readerFor[T: ClassTag]: ObjectReader = { - readerFor(classFor[T]) + def readerFor[T: JavaTypeable]: ObjectReader = { + readerFor(constructType[T]) } /** @@ -247,11 +254,139 @@ trait ClassTagExtensions { * if so, root cause will contain underlying checked exception data binding * functionality threw */ - def convertValue[T: ClassTag](fromValue: Any): T = { - convertValue(fromValue, classFor[T]) + def convertValue[T: JavaTypeable](fromValue: Any): T = { + convertValue(fromValue, constructType[T]) } private def classFor[T: ClassTag]: Class[T] = { implicitly[ClassTag[T]].runtimeClass.asInstanceOf[Class[T]] } } + +trait JavaTypeable[T] { + def asJavaType(typeFactory: TypeFactory): JavaType +} + +object JavaTypeable { + + implicit val anyJavaTypeable: JavaTypeable[Any] = { + new JavaTypeable[Any] { + override def asJavaType(typeFactory: TypeFactory): JavaType = { + val typeArgs: Array[JavaType] = Array() + typeFactory.constructParametricType(classOf[Object], typeArgs: _*) + } + } + } + + implicit def optionJavaTypeable[T : JavaTypeable]: JavaTypeable[Option[T]] = { + new JavaTypeable[Option[T]] { + override def asJavaType(typeFactory: TypeFactory): JavaType = { + val typeArg0 = implicitly[JavaTypeable[T]].asJavaType(typeFactory) + typeFactory.constructReferenceType(classOf[Option[_]], typeArg0) + } + } + } + + implicit def arrayJavaTypeable[T : JavaTypeable]: JavaTypeable[Array[T]] = { + new JavaTypeable[Array[T]] { + override def asJavaType(typeFactory: TypeFactory): JavaType = { + val typeArg0 = implicitly[JavaTypeable[T]].asJavaType(typeFactory) + typeFactory.constructArrayType(typeArg0) + } + } + } + + implicit def mapJavaTypeable[M[_,_] <: Map[_,_], K : JavaTypeable, V: JavaTypeable](implicit ct: ClassTag[M[K,V]]): JavaTypeable[M[K, V]] = { + new JavaTypeable[M[K, V]] { + override def asJavaType(typeFactory: TypeFactory): JavaType = { + val typeArg0 = implicitly[JavaTypeable[K]].asJavaType(typeFactory) + val typeArg1 = implicitly[JavaTypeable[V]].asJavaType(typeFactory) + typeFactory.constructMapLikeType(ct.runtimeClass, typeArg0, typeArg1) + } + } + } + + implicit def collectionJavaTypeable[I[_] <: Iterable[_], T : JavaTypeable](implicit ct: ClassTag[I[T]]): JavaTypeable[I[T]] = { + new JavaTypeable[I[T]] { + override def asJavaType(typeFactory: TypeFactory): JavaType = { + val typeArg0 = implicitly[JavaTypeable[T]].asJavaType(typeFactory) + typeFactory.constructCollectionLikeType(ct.runtimeClass, typeArg0) + } + } + } + + implicit def gen5JavaTypeable[T[_, _, _, _, _], A: JavaTypeable, B: JavaTypeable, C: JavaTypeable, D: JavaTypeable, E: JavaTypeable](implicit ct: ClassTag[T[A, B, C, D, E]]): JavaTypeable[T[A, B, C, D, E]] = { + new JavaTypeable[T[A, B, C, D, E]] { + override def asJavaType(typeFactory: TypeFactory): JavaType = { + val typeArgs: Array[JavaType] = Array( + implicitly[JavaTypeable[A]].asJavaType(typeFactory), + implicitly[JavaTypeable[B]].asJavaType(typeFactory), + implicitly[JavaTypeable[C]].asJavaType(typeFactory), + implicitly[JavaTypeable[D]].asJavaType(typeFactory), + implicitly[JavaTypeable[E]].asJavaType(typeFactory) + ) + typeFactory.constructParametricType(ct.runtimeClass, typeArgs: _*) + } + } + } + + implicit def gen4JavaTypeable[T[_, _, _, _], A: JavaTypeable, B: JavaTypeable, C: JavaTypeable, D: JavaTypeable](implicit ct: ClassTag[T[A, B, C, D]]): JavaTypeable[T[A, B, C, D]] = { + new JavaTypeable[T[A, B, C, D]] { + override def asJavaType(typeFactory: TypeFactory): JavaType = { + val typeArgs: Array[JavaType] = Array( + implicitly[JavaTypeable[A]].asJavaType(typeFactory), + implicitly[JavaTypeable[B]].asJavaType(typeFactory), + implicitly[JavaTypeable[C]].asJavaType(typeFactory), + implicitly[JavaTypeable[D]].asJavaType(typeFactory) + ) + typeFactory.constructParametricType(ct.runtimeClass, typeArgs: _*) + } + } + } + + implicit def gen3JavaTypeable[T[_, _, _], A: JavaTypeable, B: JavaTypeable, C: JavaTypeable](implicit ct: ClassTag[T[A, B, C]]): JavaTypeable[T[A, B, C]] = { + new JavaTypeable[T[A, B, C]] { + override def asJavaType(typeFactory: TypeFactory): JavaType = { + val typeArgs: Array[JavaType] = Array( + implicitly[JavaTypeable[A]].asJavaType(typeFactory), + implicitly[JavaTypeable[B]].asJavaType(typeFactory), + implicitly[JavaTypeable[C]].asJavaType(typeFactory) + ) + typeFactory.constructParametricType(ct.runtimeClass, typeArgs: _*) + } + } + } + + implicit def gen2JavaTypeable[T[_, _], A: JavaTypeable, B: JavaTypeable](implicit ct: ClassTag[T[A, B]]): JavaTypeable[T[A, B]] = { + new JavaTypeable[T[A, B]] { + override def asJavaType(typeFactory: TypeFactory): JavaType = { + val typeArgs: Array[JavaType] = Array( + implicitly[JavaTypeable[A]].asJavaType(typeFactory), + implicitly[JavaTypeable[B]].asJavaType(typeFactory) + ) + typeFactory.constructParametricType(ct.runtimeClass, typeArgs: _*) + } + } + } + + implicit def gen1JavaTypeable[T[_], A: JavaTypeable](implicit ct: ClassTag[T[A]]): JavaTypeable[T[A]] = { + new JavaTypeable[T[A]] { + override def asJavaType(typeFactory: TypeFactory): JavaType = { + val typeArgs: Array[JavaType] = Array( + implicitly[JavaTypeable[A]].asJavaType(typeFactory) + ) + typeFactory.constructParametricType(ct.runtimeClass, typeArgs: _*) + } + } + } + + implicit def gen0JavaTypeable[T](implicit ct: ClassTag[T]): JavaTypeable[T] = { + new JavaTypeable[T] { + override def asJavaType(typeFactory: TypeFactory): JavaType = { + val typeArgs: Array[JavaType] = Array() + typeFactory.constructParametricType(ct.runtimeClass, typeArgs: _*) + } + } + } + +} diff --git a/src/test/scala/com/fasterxml/jackson/module/scala/ClassTagExtensionsTest.scala b/src/test/scala/com/fasterxml/jackson/module/scala/ClassTagExtensionsTest.scala index 12e2178a9..d694dfd37 100644 --- a/src/test/scala/com/fasterxml/jackson/module/scala/ClassTagExtensionsTest.scala +++ b/src/test/scala/com/fasterxml/jackson/module/scala/ClassTagExtensionsTest.scala @@ -61,6 +61,11 @@ class ClassTagExtensionsTest extends JacksonTest { } } + it should "construct the proper java type" in { + val result = mapper.constructType[Target] + result should equal(mapper.constructType(classOf[Target])) + } + it should "read value from json parser" in { val parser = mapper.getFactory.createParser(genericJson) val result = mapper.readValue[GenericTestClass[Int]](parser) @@ -144,10 +149,9 @@ class ClassTagExtensionsTest extends JacksonTest { result should equal(Target.apply("foo", 0)) } - it should "convert between types" ignore { - val result = mapper.convertValue[GenericTestClass[BigInt]](GenericTestClass("42")) - //assert fails with unboxing error - result.t.intValue should equal(genericInt.t) + it should "convert between types" in { + val result = mapper.convertValue[GenericTestClass[Int]](GenericTestClass("42")) + result should equal(genericInt) } it should "read values as Array from a JSON array" in { @@ -155,17 +159,17 @@ class ClassTagExtensionsTest extends JacksonTest { result should equal(listGenericInt.toArray) } - it should "read values as Seq from a JSON array" ignore { + it should "read values as Seq from a JSON array" in { val result = mapper.readValue[Seq[GenericTestClass[Int]]](toplevelArrayJson) result should equal(listGenericInt) } - it should "read values as List from a JSON array" ignore { + it should "read values as List from a JSON array" in { val result = mapper.readValue[List[GenericTestClass[Int]]](toplevelArrayJson) result should equal(listGenericInt) } - it should "read values as Set from a JSON array" ignore { + it should "read values as Set from a JSON array" in { val result = mapper.readValue[Set[GenericTestClass[Int]]](toplevelArrayJson) result should equal(listGenericInt.toSet) } @@ -186,7 +190,7 @@ class ClassTagExtensionsTest extends JacksonTest { result should equal(Map("first" -> "firstVal", "second" -> 2)) } - it should "fail to read a Map from JSON with invalid types" ignore { + it should "fail to read a Map from JSON with invalid types" in { an [InvalidFormatException] should be thrownBy { mapper.readValue[Map[String, Int]](genericTwoFieldJson) } @@ -197,7 +201,7 @@ class ClassTagExtensionsTest extends JacksonTest { assert(result.isInstanceOf[collection.Map[_, _]]) } - it should "read option values into List from a JSON array" ignore { + it should "read option values into List from a JSON array" in { val result = mapper.readValue[java.util.ArrayList[Option[String]]](toplevelOptionArrayJson).asScala result(0) should equal(Some("some")) result(1) should equal(None) @@ -209,48 +213,48 @@ class ClassTagExtensionsTest extends JacksonTest { result(1) should equal(None) } - it should "update value from file" ignore { + it should "update value from file" in { withFile(toplevelArrayJson) { file => val result = mapper.updateValue(List.empty[GenericTestClass[Int]], file) result should equal(listGenericInt) } } - it should "update value from URL" ignore { + it should "update value from URL" in { withFile(toplevelArrayJson) { file => val result = mapper.updateValue(List.empty[GenericTestClass[Int]], file.toURI.toURL) result should equal(listGenericInt) } } - it should "update value from string" ignore { - val result = mapper.updateValue(List.empty[GenericTestClass[BigInt]], toplevelArrayJson) + it should "update value from string" in { + val result = mapper.updateValue(List.empty[GenericTestClass[Int]], toplevelArrayJson) result should equal(listGenericInt) } - it should "update value from Reader" ignore { + it should "update value from Reader" in { val reader = new InputStreamReader(new ByteArrayInputStream(toplevelArrayJson.getBytes(StandardCharsets.UTF_8))) - val result = mapper.updateValue(List.empty[GenericTestClass[BigInt]], reader) + val result = mapper.updateValue(List.empty[GenericTestClass[Int]], reader) result should equal(listGenericInt) } - it should "update value from stream" ignore { + it should "update value from stream" in { val stream = new ByteArrayInputStream(toplevelArrayJson.getBytes(StandardCharsets.UTF_8)) - val result = mapper.updateValue(List.empty[GenericTestClass[BigInt]], stream) + val result = mapper.updateValue(List.empty[GenericTestClass[Int]], stream) result should equal(listGenericInt) } - it should "update value from byte array" ignore { - val result = mapper.updateValue(List.empty[GenericTestClass[BigInt]], toplevelArrayJson.getBytes(StandardCharsets.UTF_8)) + it should "update value from byte array" in { + val result = mapper.updateValue(List.empty[GenericTestClass[Int]], toplevelArrayJson.getBytes(StandardCharsets.UTF_8)) result should equal(listGenericInt) } - it should "update value from subset of byte array" ignore { - val result = mapper.updateValue(List.empty[GenericTestClass[BigInt]], toplevelArrayJson.getBytes(StandardCharsets.UTF_8), 0, toplevelArrayJson.length) + it should "update value from subset of byte array" in { + val result = mapper.updateValue(List.empty[GenericTestClass[Int]], toplevelArrayJson.getBytes(StandardCharsets.UTF_8), 0, toplevelArrayJson.length) result should equal(listGenericInt) } - it should "deserialize a type param wrapped option" ignore { + it should "deserialize a type param wrapped option" in { val json: String = """{"t": {"bar": "baz"}}""" val result = mapper.readValue[Wrapper[Option[Foo]]](json) result.t.get.isInstanceOf[Foo] should be(true)