From 8872189618f4cb9edac12613d9bdc16afd93d8ed Mon Sep 17 00:00:00 2001 From: Danielle Voznyy Date: Tue, 26 Dec 2023 20:18:56 -0500 Subject: [PATCH] Expand on ComponentListAsMapSerializer to support any polymorphic serializer --- .../mineinabyss/geary/prefabs/PrefabLoader.kt | 6 +- ...r.kt => PolymorphicListAsMapSerializer.kt} | 61 ++++++++++---- .../geary/prefabs/SerializerTest.kt | 83 +++++++++++++++++++ .../serialization/ComponentSerializers.kt | 3 +- .../geary/serialization/SerializersByMap.kt | 14 +--- 5 files changed, 135 insertions(+), 32 deletions(-) rename addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/serializers/{ComponentListAsMapSerializer.kt => PolymorphicListAsMapSerializer.kt} (58%) create mode 100644 addons/geary-prefabs/src/jvmTest/kotlin/com/mineinabyss/geary/prefabs/SerializerTest.kt diff --git a/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/PrefabLoader.kt b/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/PrefabLoader.kt index df2a4e1c4..c1ed11bfb 100644 --- a/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/PrefabLoader.kt +++ b/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/PrefabLoader.kt @@ -4,12 +4,14 @@ import co.touchlab.kermit.Severity import com.benasher44.uuid.Uuid import com.mineinabyss.geary.components.relations.NoInherit import com.mineinabyss.geary.datatypes.Entity +import com.mineinabyss.geary.datatypes.GearyComponent import com.mineinabyss.geary.helpers.entity import com.mineinabyss.geary.modules.geary import com.mineinabyss.geary.prefabs.configuration.components.Prefab import com.mineinabyss.geary.prefabs.helpers.inheritPrefabs -import com.mineinabyss.geary.prefabs.serializers.ComponentListAsMapSerializer +import com.mineinabyss.geary.prefabs.serializers.PolymorphicListAsMapSerializer import com.mineinabyss.geary.serialization.dsl.serializableComponents +import kotlinx.serialization.PolymorphicSerializer import okio.Path class PrefabLoader { @@ -58,7 +60,7 @@ class PrefabLoader { /** Registers an entity with components defined in a [path], adding a [Prefab] component. */ fun loadFromPath(namespace: String, path: Path, writeTo: Entity? = null): Result { return runCatching { - val serializer = ComponentListAsMapSerializer() + val serializer = PolymorphicListAsMapSerializer.of(PolymorphicSerializer(GearyComponent::class)) val ext = path.name.substringAfterLast('.') val decoded = formats[ext]?.decodeFromFile(serializer, path) ?: throw IllegalArgumentException("Unknown file format $ext") diff --git a/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/serializers/ComponentListAsMapSerializer.kt b/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/serializers/PolymorphicListAsMapSerializer.kt similarity index 58% rename from addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/serializers/ComponentListAsMapSerializer.kt rename to addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/serializers/PolymorphicListAsMapSerializer.kt index 7266ad32d..faf3b3174 100644 --- a/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/serializers/ComponentListAsMapSerializer.kt +++ b/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/serializers/PolymorphicListAsMapSerializer.kt @@ -1,9 +1,9 @@ package com.mineinabyss.geary.prefabs.serializers import com.mineinabyss.geary.datatypes.GearyComponent -import com.mineinabyss.geary.serialization.dsl.serializableComponents import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.KSerializer +import kotlinx.serialization.PolymorphicSerializer import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.builtins.MapSerializer import kotlinx.serialization.builtins.serializer @@ -30,10 +30,16 @@ class GearyComponentSerializer : KSerializer { } -class ComponentListAsMapSerializer( - val namespaces: List = listOf(), - val prefix: String = "", -) : KSerializer> { +class PolymorphicListAsMapSerializer internal constructor( + serializer: KSerializer, +) : KSerializer> { + // We need primary constructor to be a single serializer for generic serialization to work, use of() if manually creating + private var namespaces: List = listOf() + private var prefix: String = "" + + + val polymorphicSerializer = serializer as? PolymorphicSerializer ?: error("Serializer is not polymorphic") + val keySerializer = String.serializer() val valueSerializer = GearyComponentSerializer() @@ -46,9 +52,9 @@ class ComponentListAsMapSerializer( return decodeSerializableElement(newDescriptor, newIndex, valueSerializer) } - override fun deserialize(decoder: Decoder): List { + override fun deserialize(decoder: Decoder): List { val namespaces = namespaces.toMutableList() - val components = mutableListOf() + val components = mutableListOf() val compositeDecoder = decoder.beginStructure(descriptor) while (true) { val index = compositeDecoder.decodeElementIndex(descriptor) @@ -64,16 +70,13 @@ class ComponentListAsMapSerializer( } key.endsWith("*") -> { - val innerSerializer = ComponentListAsMapSerializer(namespaces, key.removeSuffix("*")) + val innerSerializer = of(polymorphicSerializer, namespaces, key.removeSuffix("*")) components.addAll(compositeDecoder.decodeMapValue(innerSerializer)) } else -> { - val foundValueSerializer = serializableComponents.serializers - .getSerializerFor("$prefix$key".fromCamelCaseToSnakeCase(), GearyComponent::class, namespaces) as? KSerializer - ?: error("No component serializer registered for $key") - - val decodedValue = compositeDecoder.decodeMapValue(foundValueSerializer) + val decodedValue = + compositeDecoder.decodeMapValue(findSerializerFor(compositeDecoder, namespaces, key)) components += decodedValue } } @@ -82,15 +85,43 @@ class ComponentListAsMapSerializer( return components.toList() } + @OptIn(InternalSerializationApi::class) + fun findSerializerFor( + decoder: CompositeDecoder, + namespaces: List, + key: String, + ): KSerializer { + val parsedKey = "$prefix$key".fromCamelCaseToSnakeCase() + return (if (parsedKey.hasNamespace()) polymorphicSerializer.findPolymorphicSerializerOrNull(decoder, parsedKey) + else namespaces.firstNotNullOfOrNull { namespace -> + polymorphicSerializer.findPolymorphicSerializerOrNull(decoder, "$namespace:$parsedKey") + } ?: error("No serializer found for $parsedKey in any of the namespaces $namespaces")) + as? KSerializer ?: error("Serializer for $parsedKey is not a component serializer") + } - override fun serialize(encoder: Encoder, value: List) { + private fun String.hasNamespace(): Boolean = contains(":") + + override fun serialize(encoder: Encoder, value: List) { TODO("Not implemented") } - companion object{ + companion object { private val camelRegex = Regex("([A-Z])") fun String.fromCamelCaseToSnakeCase(): String { return this.replace(camelRegex, "_$1").removePrefix("_").lowercase() } + + + fun of( + serializer: PolymorphicSerializer, + namespaces: List = listOf(), + prefix: String = "" + ): + PolymorphicListAsMapSerializer { + return PolymorphicListAsMapSerializer(serializer).apply { + this.namespaces = namespaces + this.prefix = prefix + } + } } } diff --git a/addons/geary-prefabs/src/jvmTest/kotlin/com/mineinabyss/geary/prefabs/SerializerTest.kt b/addons/geary-prefabs/src/jvmTest/kotlin/com/mineinabyss/geary/prefabs/SerializerTest.kt new file mode 100644 index 000000000..ad5752213 --- /dev/null +++ b/addons/geary-prefabs/src/jvmTest/kotlin/com/mineinabyss/geary/prefabs/SerializerTest.kt @@ -0,0 +1,83 @@ +package com.mineinabyss.geary.prefabs + +import com.mineinabyss.geary.prefabs.serializers.PolymorphicListAsMapSerializer +import io.kotest.matchers.shouldBe +import kotlinx.serialization.Polymorphic +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.polymorphic +import kotlinx.serialization.modules.subclass +import org.junit.jupiter.api.Test + +class SerializerTest { + interface Components + + @SerialName("test:thing.a") + @Serializable + object A : Components + + @SerialName("test:thing.b") + @Serializable + object B : Components + + private val json = Json { + serializersModule = SerializersModule { + polymorphic(Components::class) { + subclass(A::class) + subclass(B::class) + } + } + } + + @Serializable + class SerializerTest(val components: @Serializable(with = PolymorphicListAsMapSerializer::class) List<@Polymorphic Components>) + + @Test + fun `should serialize via @Serializable`() { + json.decodeFromString( + SerializerTest.serializer(), + """ + { + "components": { + "test:thing.a": {}, + "test:thing.b": {} + } + } + """.trimIndent() + ).components shouldBe listOf(A, B) + } + + @Test + fun `should support subkey syntax`() { + json.decodeFromString( + SerializerTest.serializer(), + """ + { + "components": { + "test:thing.*": { + "a": {}, + "b": {} + } + } + } + """.trimIndent() + ).components shouldBe listOf(A, B) + } + @Test + fun `should support importing namespaces`() { + json.decodeFromString( + SerializerTest.serializer(), + """ + { + "components": { + "namespaces": ["test"], + "thing.a": {}, + "thing.b": {} + } + } + """.trimIndent() + ).components shouldBe listOf(A, B) + } +} diff --git a/addons/geary-serialization/src/commonMain/kotlin/com/mineinabyss/geary/serialization/ComponentSerializers.kt b/addons/geary-serialization/src/commonMain/kotlin/com/mineinabyss/geary/serialization/ComponentSerializers.kt index 16179016a..09b77503f 100644 --- a/addons/geary-serialization/src/commonMain/kotlin/com/mineinabyss/geary/serialization/ComponentSerializers.kt +++ b/addons/geary-serialization/src/commonMain/kotlin/com/mineinabyss/geary/serialization/ComponentSerializers.kt @@ -13,8 +13,7 @@ interface ComponentSerializers { fun getSerializerFor( key: String, - baseClass: KClass, - namespaces: List = emptyList() + baseClass: KClass ): DeserializationStrategy? fun getSerializerFor(kClass: KClass): DeserializationStrategy? diff --git a/addons/geary-serialization/src/commonMain/kotlin/com/mineinabyss/geary/serialization/SerializersByMap.kt b/addons/geary-serialization/src/commonMain/kotlin/com/mineinabyss/geary/serialization/SerializersByMap.kt index 560d96284..68433cb6d 100644 --- a/addons/geary-serialization/src/commonMain/kotlin/com/mineinabyss/geary/serialization/SerializersByMap.kt +++ b/addons/geary-serialization/src/commonMain/kotlin/com/mineinabyss/geary/serialization/SerializersByMap.kt @@ -21,15 +21,8 @@ class SerializersByMap( override fun getSerializerFor( key: String, baseClass: KClass, - namespaces: List, ): DeserializationStrategy? = - if (key.hasNamespace()) - module.getPolymorphic(baseClass = baseClass, serializedClassName = key) - else { - namespaces.firstNotNullOfOrNull { namespace -> - module.getPolymorphic(baseClass = baseClass, serializedClassName = "$namespace:$key") - } ?: error("No serializer found for $key in any of the namespaces $namespaces") - } + module.getPolymorphic(baseClass = baseClass, serializedClassName = key) override fun getSerializerFor(kClass: KClass): DeserializationStrategy? { val serialName = getSerialNameFor(kClass) ?: return null @@ -40,9 +33,4 @@ class SerializersByMap( override fun getSerialNameFor(kClass: KClass): String? = component2serialName[kClass] - private fun String.hasNamespace(): Boolean = contains(":") - private fun String.prefixNamespaceIfNotPrefixed(): String = - if (!hasNamespace()) - "geary:${this}" - else this }