From c40bea412bb1c4a890f6930a93f3c24aa613f26f Mon Sep 17 00:00:00 2001 From: Piotr Mionskowski Date: Fri, 23 Jun 2023 11:03:37 +0200 Subject: [PATCH] Add jackson module for serialization and deserialization --- buildSrc/src/main/kotlin/Dependencies.kt | 1 + buildSrc/src/main/kotlin/Versions.kt | 3 +- .../codified/enums/CodifiedEnumDecoder.kt | 6 +- jackson/.gitignore | 1 + jackson/build.gradle.kts | 16 ++ .../codified/jackson/CodifiedDeserializer.kt | 35 ++++ .../jackson/CodifiedEnumDeserializer.kt | 35 ++++ .../jackson/CodifiedEnumSerializer.kt | 12 ++ .../codified/jackson/CodifiedJacksonModule.kt | 36 ++++ .../codified/jackson/CodifiedSerializer.kt | 12 ++ .../com.fasterxml.jackson.databind.Module | 1 + .../jackson/CodifiedJacksonModuleTest.kt | 154 ++++++++++++++++++ settings.gradle.kts | 1 + 13 files changed, 308 insertions(+), 5 deletions(-) create mode 100644 jackson/.gitignore create mode 100644 jackson/build.gradle.kts create mode 100644 jackson/src/main/kotlin/dev/codified/jackson/CodifiedDeserializer.kt create mode 100644 jackson/src/main/kotlin/dev/codified/jackson/CodifiedEnumDeserializer.kt create mode 100644 jackson/src/main/kotlin/dev/codified/jackson/CodifiedEnumSerializer.kt create mode 100644 jackson/src/main/kotlin/dev/codified/jackson/CodifiedJacksonModule.kt create mode 100644 jackson/src/main/kotlin/dev/codified/jackson/CodifiedSerializer.kt create mode 100644 jackson/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module create mode 100644 jackson/src/test/kotlin/dev/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/src/main/kotlin/pl/brightinventions/codified/enums/CodifiedEnumDecoder.kt b/enums/src/main/kotlin/pl/brightinventions/codified/enums/CodifiedEnumDecoder.kt index 03e1875..4bb3ab0 100644 --- a/enums/src/main/kotlin/pl/brightinventions/codified/enums/CodifiedEnumDecoder.kt +++ b/enums/src/main/kotlin/pl/brightinventions/codified/enums/CodifiedEnumDecoder.kt @@ -3,12 +3,10 @@ package pl.brightinventions.codified.enums import pl.brightinventions.codified.Codified import java.util.concurrent.ConcurrentHashMap -@PublishedApi -internal object CodifiedEnumDecoder { +object CodifiedEnumDecoder { private val enumsSerializedNamed = ConcurrentHashMap, Map>>() - @PublishedApi - internal fun decode(value: String, clazz: Class): CodifiedEnum + fun decode(value: String, clazz: Class): CodifiedEnum where T : Enum, T : Codified { val namesForEnum = enumsSerializedNamed.getOrPut(clazz) { diff --git a/jackson/.gitignore b/jackson/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/jackson/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/jackson/build.gradle.kts b/jackson/build.gradle.kts new file mode 100644 index 0000000..a517927 --- /dev/null +++ b/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/jackson/src/main/kotlin/dev/codified/jackson/CodifiedDeserializer.kt b/jackson/src/main/kotlin/dev/codified/jackson/CodifiedDeserializer.kt new file mode 100644 index 0000000..8678ea7 --- /dev/null +++ b/jackson/src/main/kotlin/dev/codified/jackson/CodifiedDeserializer.kt @@ -0,0 +1,35 @@ +package dev.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 + +class CodifiedDeserializer() : JsonDeserializer(), + ContextualDeserializer where T : Codified, T : Enum { + private lateinit var enumType: Class + + override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): T? { + return CodifiedEnumDecoder.decode(p.valueAsString, 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 + } +} diff --git a/jackson/src/main/kotlin/dev/codified/jackson/CodifiedEnumDeserializer.kt b/jackson/src/main/kotlin/dev/codified/jackson/CodifiedEnumDeserializer.kt new file mode 100644 index 0000000..1cd104f --- /dev/null +++ b/jackson/src/main/kotlin/dev/codified/jackson/CodifiedEnumDeserializer.kt @@ -0,0 +1,35 @@ +package dev.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 T : Codified, T : Enum { + private lateinit var enumCodeType: Class + private lateinit var enumType: Class + + override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): CodifiedEnum { + return CodifiedEnumDecoder.decode(p.valueAsString, 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/jackson/src/main/kotlin/dev/codified/jackson/CodifiedEnumSerializer.kt b/jackson/src/main/kotlin/dev/codified/jackson/CodifiedEnumSerializer.kt new file mode 100644 index 0000000..425ace3 --- /dev/null +++ b/jackson/src/main/kotlin/dev/codified/jackson/CodifiedEnumSerializer.kt @@ -0,0 +1,12 @@ +package dev.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/jackson/src/main/kotlin/dev/codified/jackson/CodifiedJacksonModule.kt b/jackson/src/main/kotlin/dev/codified/jackson/CodifiedJacksonModule.kt new file mode 100644 index 0000000..cef29ad --- /dev/null +++ b/jackson/src/main/kotlin/dev/codified/jackson/CodifiedJacksonModule.kt @@ -0,0 +1,36 @@ +package dev.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/jackson/src/main/kotlin/dev/codified/jackson/CodifiedSerializer.kt b/jackson/src/main/kotlin/dev/codified/jackson/CodifiedSerializer.kt new file mode 100644 index 0000000..00717be --- /dev/null +++ b/jackson/src/main/kotlin/dev/codified/jackson/CodifiedSerializer.kt @@ -0,0 +1,12 @@ +package dev.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/jackson/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module b/jackson/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module new file mode 100644 index 0000000..594169f --- /dev/null +++ b/jackson/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module @@ -0,0 +1 @@ +dev.codified.jackson.CodifiedJacksonModule diff --git a/jackson/src/test/kotlin/dev/codified/jackson/CodifiedJacksonModuleTest.kt b/jackson/src/test/kotlin/dev/codified/jackson/CodifiedJacksonModuleTest.kt new file mode 100644 index 0000000..abb190b --- /dev/null +++ b/jackson/src/test/kotlin/dev/codified/jackson/CodifiedJacksonModuleTest.kt @@ -0,0 +1,154 @@ +package dev.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`() { + // given + val input = Colour.Blue + + // 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`() { + // given + val input = HasColour(Colour.Blue) + + // when + val output = serde(input) + + // then + output.shouldEqual(input) + } + + @Test + fun `can serialize and 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 serialize and 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 serialize and 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: 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 + } + + +} diff --git a/settings.gradle.kts b/settings.gradle.kts index e01c32f..ad83c33 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,4 @@ rootProject.name = "codified" include("enums") include("enums-serializer") +include("jackson")