Skip to content

Commit

Permalink
Expand on ComponentListAsMapSerializer to support any polymorphic ser…
Browse files Browse the repository at this point in the history
…ializer
  • Loading branch information
0ffz committed Dec 27, 2023
1 parent e0e63c5 commit 8872189
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<Entity> {
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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -30,10 +30,16 @@ class GearyComponentSerializer : KSerializer<GearyComponent> {

}

class ComponentListAsMapSerializer(
val namespaces: List<String> = listOf(),
val prefix: String = "",
) : KSerializer<List<GearyComponent>> {
class PolymorphicListAsMapSerializer<T : Any> internal constructor(
serializer: KSerializer<T>,
) : KSerializer<List<T>> {
// We need primary constructor to be a single serializer for generic serialization to work, use of() if manually creating
private var namespaces: List<String> = listOf()
private var prefix: String = ""


val polymorphicSerializer = serializer as? PolymorphicSerializer<T> ?: error("Serializer is not polymorphic")

val keySerializer = String.serializer()
val valueSerializer = GearyComponentSerializer()

Expand All @@ -46,9 +52,9 @@ class ComponentListAsMapSerializer(
return decodeSerializableElement(newDescriptor, newIndex, valueSerializer)
}

override fun deserialize(decoder: Decoder): List<GearyComponent> {
override fun deserialize(decoder: Decoder): List<T> {
val namespaces = namespaces.toMutableList()
val components = mutableListOf<GearyComponent>()
val components = mutableListOf<T>()
val compositeDecoder = decoder.beginStructure(descriptor)
while (true) {
val index = compositeDecoder.decodeElementIndex(descriptor)
Expand All @@ -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<Any>
?: error("No component serializer registered for $key")

val decodedValue = compositeDecoder.decodeMapValue(foundValueSerializer)
val decodedValue =
compositeDecoder.decodeMapValue(findSerializerFor(compositeDecoder, namespaces, key))
components += decodedValue
}
}
Expand All @@ -82,15 +85,43 @@ class ComponentListAsMapSerializer(
return components.toList()
}

@OptIn(InternalSerializationApi::class)
fun findSerializerFor(
decoder: CompositeDecoder,
namespaces: List<String>,
key: String,
): KSerializer<T> {
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<T> ?: error("Serializer for $parsedKey is not a component serializer")
}

override fun serialize(encoder: Encoder, value: List<GearyComponent>) {
private fun String.hasNamespace(): Boolean = contains(":")

override fun serialize(encoder: Encoder, value: List<T>) {
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 <T : Any> of(
serializer: PolymorphicSerializer<T>,
namespaces: List<String> = listOf(),
prefix: String = ""
):
PolymorphicListAsMapSerializer<T> {
return PolymorphicListAsMapSerializer(serializer).apply {
this.namespaces = namespaces
this.prefix = prefix
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ interface ComponentSerializers {

fun <T : Component> getSerializerFor(
key: String,
baseClass: KClass<in T>,
namespaces: List<String> = emptyList()
baseClass: KClass<in T>
): DeserializationStrategy<T>?

fun <T : Component> getSerializerFor(kClass: KClass<in T>): DeserializationStrategy<out T>?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,8 @@ class SerializersByMap(
override fun <T : Component> getSerializerFor(
key: String,
baseClass: KClass<in T>,
namespaces: List<String>,
): DeserializationStrategy<T>? =
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 <T : Component> getSerializerFor(kClass: KClass<in T>): DeserializationStrategy<out T>? {
val serialName = getSerialNameFor(kClass) ?: return null
Expand All @@ -40,9 +33,4 @@ class SerializersByMap(
override fun getSerialNameFor(kClass: KClass<out Component>): String? =
component2serialName[kClass]

private fun String.hasNamespace(): Boolean = contains(":")
private fun String.prefixNamespaceIfNotPrefixed(): String =
if (!hasNamespace())
"geary:${this}"
else this
}

0 comments on commit 8872189

Please sign in to comment.