From 2eb18efe5e7a7429b8e98df79afa869f8fe5e63a Mon Sep 17 00:00:00 2001 From: Piotr Mionskowski Date: Wed, 28 Jun 2023 01:17:22 +0200 Subject: [PATCH] Add jackson module for serialization and deserialization (#3) Add jackson module for serialization and deserialization --- buildSrc/src/main/kotlin/Dependencies.kt | 1 + buildSrc/src/main/kotlin/Versions.kt | 3 +- enums-jackson/.gitignore | 1 + enums-jackson/build.gradle.kts | 16 ++ .../codified/jackson/CodifiedDeserializer.kt | 39 ++++ .../jackson/CodifiedEnumDeserializer.kt | 36 +++ .../jackson/CodifiedEnumSerializer.kt | 12 + .../codified/jackson/CodifiedJacksonModule.kt | 36 +++ .../codified/jackson/CodifiedSerializer.kt | 12 + .../com.fasterxml.jackson.databind.Module | 1 + .../jackson/CodifiedJacksonModuleTest.kt | 212 ++++++++++++++++++ .../codified/enums/CodifiedEnumDecoder.kt | 12 +- settings.gradle.kts | 1 + 13 files changed, 374 insertions(+), 8 deletions(-) create mode 100644 enums-jackson/.gitignore create mode 100644 enums-jackson/build.gradle.kts create mode 100644 enums-jackson/src/main/kotlin/pl/brightinventions/codified/jackson/CodifiedDeserializer.kt create mode 100644 enums-jackson/src/main/kotlin/pl/brightinventions/codified/jackson/CodifiedEnumDeserializer.kt create mode 100644 enums-jackson/src/main/kotlin/pl/brightinventions/codified/jackson/CodifiedEnumSerializer.kt create mode 100644 enums-jackson/src/main/kotlin/pl/brightinventions/codified/jackson/CodifiedJacksonModule.kt create mode 100644 enums-jackson/src/main/kotlin/pl/brightinventions/codified/jackson/CodifiedSerializer.kt create mode 100644 enums-jackson/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module create mode 100644 enums-jackson/src/test/kotlin/pl/brightinventions/codified/jackson/CodifiedJacksonModuleTest.kt diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index 18af253..1dd6177 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -3,4 +3,5 @@ object Dependencies { const val shouldko = "com.github.miensol:shouldko:${Versions.shouldko}" const val serializationCore = "org.jetbrains.kotlinx:kotlinx-serialization-core:${Versions.serialization}" const val serializationJson = "org.jetbrains.kotlinx:kotlinx-serialization-json:${Versions.serialization}" + const val jackson = "com.fasterxml.jackson.core:jackson-databind:${Versions.jackson}" } diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 8f0ae8a..531d580 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -3,4 +3,5 @@ object Versions { const val junit = "5.6.2" const val shouldko = "0.2.2" const val serialization = "1.3.2" -} \ No newline at end of file + const val jackson = "2.15.2" +} diff --git a/enums-jackson/.gitignore b/enums-jackson/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/enums-jackson/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/enums-jackson/build.gradle.kts b/enums-jackson/build.gradle.kts new file mode 100644 index 0000000..a517927 --- /dev/null +++ b/enums-jackson/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + kotlin("jvm") + id("default-config") + id("default-java-publish") +} + +dependencies { + api(rootProject) + api(project(":enums")) + + implementation(kotlin("stdlib")) + implementation(Dependencies.jackson) + + testImplementation(Dependencies.junit) + testImplementation(Dependencies.shouldko) +} diff --git a/enums-jackson/src/main/kotlin/pl/brightinventions/codified/jackson/CodifiedDeserializer.kt b/enums-jackson/src/main/kotlin/pl/brightinventions/codified/jackson/CodifiedDeserializer.kt new file mode 100644 index 0000000..5ff01d2 --- /dev/null +++ b/enums-jackson/src/main/kotlin/pl/brightinventions/codified/jackson/CodifiedDeserializer.kt @@ -0,0 +1,39 @@ +package pl.brightinventions.codified.jackson + +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.BeanProperty +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JavaType +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.deser.ContextualDeserializer +import pl.brightinventions.codified.Codified +import pl.brightinventions.codified.enums.CodifiedEnumDecoder +import java.lang.reflect.ParameterizedType + +class CodifiedDeserializer() : JsonDeserializer(), + ContextualDeserializer where TEnum : Codified, TEnum : Enum { + private lateinit var enumCodeType: Class + private lateinit var enumType: Class + + override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): TEnum? { + @Suppress("UNCHECKED_CAST") + return CodifiedEnumDecoder.decode(p.readValueAs(enumCodeType), enumType).knownOrNull() + } + + override fun createContextual( + ctxt: DeserializationContext, + property: BeanProperty? + ): JsonDeserializer { + return CodifiedDeserializer(ctxt.contextualType) + } + + override fun handledType(): Class<*> { + return Codified::class.java + } + + @Suppress("UNCHECKED_CAST") + constructor(contextualType: JavaType) : this() { + this.enumType = contextualType.rawClass as Class + this.enumCodeType = (enumType.genericInterfaces[0] as ParameterizedType).actualTypeArguments[0] as Class + } +} diff --git a/enums-jackson/src/main/kotlin/pl/brightinventions/codified/jackson/CodifiedEnumDeserializer.kt b/enums-jackson/src/main/kotlin/pl/brightinventions/codified/jackson/CodifiedEnumDeserializer.kt new file mode 100644 index 0000000..bb353e5 --- /dev/null +++ b/enums-jackson/src/main/kotlin/pl/brightinventions/codified/jackson/CodifiedEnumDeserializer.kt @@ -0,0 +1,36 @@ +package pl.brightinventions.codified.jackson + +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.BeanProperty +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JavaType +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.deser.ContextualDeserializer +import pl.brightinventions.codified.Codified +import pl.brightinventions.codified.enums.CodifiedEnum +import pl.brightinventions.codified.enums.CodifiedEnumDecoder +import java.io.Serializable + +internal class CodifiedEnumDeserializer() : JsonDeserializer>(), + ContextualDeserializer where TEnum : Codified, TEnum : Enum { + private lateinit var enumCodeType: Class + private lateinit var enumType: Class + + override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): CodifiedEnum { + @Suppress("UNCHECKED_CAST") + return CodifiedEnumDecoder.decode(p.readValueAs(enumCodeType), enumType) + } + + override fun createContextual( + ctxt: DeserializationContext, + property: BeanProperty? + ): JsonDeserializer> { + return CodifiedEnumDeserializer(ctxt.contextualType) + } + + @Suppress("UNCHECKED_CAST") + constructor(contextualType: JavaType) : this() { + this.enumType = contextualType.bindings.getBoundType(0).rawClass as Class + this.enumCodeType = contextualType.bindings.getBoundType(1).rawClass as Class + } +} diff --git a/enums-jackson/src/main/kotlin/pl/brightinventions/codified/jackson/CodifiedEnumSerializer.kt b/enums-jackson/src/main/kotlin/pl/brightinventions/codified/jackson/CodifiedEnumSerializer.kt new file mode 100644 index 0000000..feb03b2 --- /dev/null +++ b/enums-jackson/src/main/kotlin/pl/brightinventions/codified/jackson/CodifiedEnumSerializer.kt @@ -0,0 +1,12 @@ +package pl.brightinventions.codified.jackson + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.ser.std.StdSerializer +import pl.brightinventions.codified.enums.CodifiedEnum + +internal class CodifiedEnumSerializer : StdSerializer>(CodifiedEnum::class.java) { + override fun serialize(value: CodifiedEnum<*, *>, gen: JsonGenerator, serializers: SerializerProvider) { + serializers.defaultSerializeValue(value.code(), gen) + } +} diff --git a/enums-jackson/src/main/kotlin/pl/brightinventions/codified/jackson/CodifiedJacksonModule.kt b/enums-jackson/src/main/kotlin/pl/brightinventions/codified/jackson/CodifiedJacksonModule.kt new file mode 100644 index 0000000..fe12342 --- /dev/null +++ b/enums-jackson/src/main/kotlin/pl/brightinventions/codified/jackson/CodifiedJacksonModule.kt @@ -0,0 +1,36 @@ +package pl.brightinventions.codified.jackson + +import com.fasterxml.jackson.core.Version +import com.fasterxml.jackson.databind.Module +import com.fasterxml.jackson.databind.module.SimpleDeserializers +import com.fasterxml.jackson.databind.module.SimpleSerializers +import pl.brightinventions.codified.Codified +import pl.brightinventions.codified.enums.CodifiedEnum + +class CodifiedJacksonModule : Module() { + override fun version(): Version { + return Version.unknownVersion() + } + + override fun getModuleName(): String { + return CodifiedJacksonModule::class.qualifiedName!! + } + + override fun setupModule(context: SetupContext) { + context.addSerializers( + SimpleSerializers( + listOf( + CodifiedEnumSerializer(), + CodifiedSerializer() + ) + ) + ) + + val simpleDeserializers = SimpleDeserializers().apply { + addDeserializer(CodifiedEnum::class.java, CodifiedEnumDeserializer::class.java.newInstance()) + addDeserializer(Codified::class.java, CodifiedDeserializer::class.java.newInstance()) + } + context.addDeserializers(simpleDeserializers) + } +} + diff --git a/enums-jackson/src/main/kotlin/pl/brightinventions/codified/jackson/CodifiedSerializer.kt b/enums-jackson/src/main/kotlin/pl/brightinventions/codified/jackson/CodifiedSerializer.kt new file mode 100644 index 0000000..53d89bb --- /dev/null +++ b/enums-jackson/src/main/kotlin/pl/brightinventions/codified/jackson/CodifiedSerializer.kt @@ -0,0 +1,12 @@ +package pl.brightinventions.codified.jackson + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.ser.std.StdSerializer +import pl.brightinventions.codified.Codified + +internal class CodifiedSerializer : StdSerializer>(Codified::class.java) { + override fun serialize(value: Codified<*>, gen: JsonGenerator, serializers: SerializerProvider) { + serializers.defaultSerializeValue(value.code, gen) + } +} diff --git a/enums-jackson/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module b/enums-jackson/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module new file mode 100644 index 0000000..eaefa27 --- /dev/null +++ b/enums-jackson/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module @@ -0,0 +1 @@ +pl.brightinventions.codified.jackson.CodifiedJacksonModule diff --git a/enums-jackson/src/test/kotlin/pl/brightinventions/codified/jackson/CodifiedJacksonModuleTest.kt b/enums-jackson/src/test/kotlin/pl/brightinventions/codified/jackson/CodifiedJacksonModuleTest.kt new file mode 100644 index 0000000..cb9a3f7 --- /dev/null +++ b/enums-jackson/src/test/kotlin/pl/brightinventions/codified/jackson/CodifiedJacksonModuleTest.kt @@ -0,0 +1,212 @@ +package pl.brightinventions.codified.jackson + +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import org.junit.jupiter.api.Test +import pl.brightinventions.codified.Codified +import pl.brightinventions.codified.enums.CodifiedEnum +import pl.miensol.shouldko.shouldEqual + +class CodifiedJacksonModuleTest { + val objectMapper = ObjectMapper().findAndRegisterModules() + + @Test + fun `can serialize and deserialize known value to codified enum with string`() { + // given + val input = Colour.Blue + + // when + val output = serdeCodifiedEnum(input) + + // then + output.knownOrNull().shouldEqual(input) + } + + @Test + fun `can serialize and deserialize known value to codified enum with int`() { + // given + val input = Weight.Heavy + + // when + val output = serdeCodifiedEnum(input) + + // then + output.knownOrNull().shouldEqual(input) + } + + @Test + fun `can serialize and deserialize not known value to codified enum`() { + // given + val input = "pink" + + // when + val output = serdeCodifiedEnum(input) + + // then + output.knownOrNull().shouldEqual(null) + output.code().shouldEqual("pink") + } + + @Test + fun `can serialize and deserialize known value to enum property with string`() { + // given + val input = HasColour(Colour.Blue) + + // when + val output = serde(input) + + // then + output.shouldEqual(input) + } + + @Test + fun `can serialize and deserialize known value to enum property with int`() { + // given + val input = HasWeight().apply { weight = Weight.Medium } + + // when + val output = serde(input) + + // then + output.shouldEqual(input) + } + + @Test + fun `can deserialize not known value to enum property`() { + // given + val input = """{"colour": "pink"}""" + // when + val output = objectMapper.readValue(input, HasColour::class.java) + // then + output.colour.shouldEqual(null) + } + + @Test + fun `can deserialize known value to enum property`() { + // given + val input = """{"colour": "blue"}""" + // when + val output = objectMapper.readValue(input, HasColour::class.java) + // then + output.colour.shouldEqual(Colour.Blue) + } + + @Test + fun `can deserialize known value to codified enum property`() { + // given + val input = HasCodifiedColour(Colour.Blue) + + // when + val output = serde(input) + + // then + output.shouldEqual(input) + } + + @Test + fun `can deserialize not known value to codified enum property`() { + // given + val input = HasCodifiedColour("pink") + // when + val output = serde(input) + // then + output.colour!!.knownOrNull().shouldEqual(null) + output.colour!!.code().shouldEqual("pink") + } + + private fun serdeCodifiedEnum(input: Colour): CodifiedEnum { + val json = objectMapper.writer().writeValueAsString(input) + return objectMapper.readValue(json, object : TypeReference>() {}) + } + + private fun serdeCodifiedEnum(input: Weight): CodifiedEnum { + val json = objectMapper.writer().writeValueAsString(input) + return objectMapper.readValue(json, object : TypeReference>() {}) + } + + private fun serdeCodifiedEnum(input: String): CodifiedEnum { + val json = objectMapper.writer().writeValueAsString(input) + return objectMapper.readValue(json, object : TypeReference>() {}) + } + + private inline fun serde(input: T): T { + val json = objectMapper.writer().writeValueAsString(input) + return objectMapper.readValue(json, T::class.java) + } +} + + +@JsonDeserialize(using = CodifiedDeserializer::class) +enum class Colour(override val code: String) : Codified { + Red("red"), Green("green"), Blue("blue"); +} + +class HasColour() { + var colour: Colour? = null + + constructor(colour: Colour) : this() { + this.colour = colour + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as HasColour + + return colour == other.colour + } + + override fun hashCode(): Int { + return colour?.hashCode() ?: 0 + } +} + +class HasCodifiedColour() { + var colour: CodifiedEnum? = null + + constructor(colour: Colour) : this() { + this.colour = CodifiedEnum.Known(colour) + } + + constructor(colour: String) : this() { + this.colour = CodifiedEnum.Unknown(colour) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as HasCodifiedColour + + return colour == other.colour + } + + override fun hashCode(): Int { + return colour?.hashCode() ?: 0 + } +} + + +@JsonDeserialize(using = CodifiedDeserializer::class) +enum class Weight(override val code: Int) : Codified { + Light(200), Medium(400), Heavy(60); +} + +class HasWeight { + var weight: Weight? = null + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as HasWeight + + return weight == other.weight + } + + override fun hashCode(): Int { + return weight?.hashCode() ?: 0 + } +} + diff --git a/enums/src/main/kotlin/pl/brightinventions/codified/enums/CodifiedEnumDecoder.kt b/enums/src/main/kotlin/pl/brightinventions/codified/enums/CodifiedEnumDecoder.kt index 03e1875..0bb0aae 100644 --- a/enums/src/main/kotlin/pl/brightinventions/codified/enums/CodifiedEnumDecoder.kt +++ b/enums/src/main/kotlin/pl/brightinventions/codified/enums/CodifiedEnumDecoder.kt @@ -3,13 +3,11 @@ package pl.brightinventions.codified.enums import pl.brightinventions.codified.Codified import java.util.concurrent.ConcurrentHashMap -@PublishedApi -internal object CodifiedEnumDecoder { - private val enumsSerializedNamed = ConcurrentHashMap, Map>>() +object CodifiedEnumDecoder { + private val enumsSerializedNamed = ConcurrentHashMap, Map>>() - @PublishedApi - internal fun decode(value: String, clazz: Class): CodifiedEnum - where T : Enum, T : Codified { + fun decode(value: TCode, clazz: Class): CodifiedEnum + where TEnum : Enum, TEnum : Codified { val namesForEnum = enumsSerializedNamed.getOrPut(clazz) { clazz.enumConstants.associateBy { it.code } @@ -17,7 +15,7 @@ internal object CodifiedEnumDecoder { val enumValue = namesForEnum[value] return if (enumValue != null) { @Suppress("UNCHECKED_CAST") - (CodifiedEnum.Known(enumValue as T)) + (CodifiedEnum.Known(enumValue as TEnum)) } else { CodifiedEnum.Unknown(value) } diff --git a/settings.gradle.kts b/settings.gradle.kts index e01c32f..f0ef10c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,4 @@ rootProject.name = "codified" include("enums") include("enums-serializer") +include("enums-jackson")