Browse Source

Avoid boxing in `TypeSafeMap` and support serialization

Him188 4 năm trước cách đây
mục cha
commit
f5b2dbc65d

+ 6 - 0
mirai-core-utils/build.gradle.kts

@@ -67,6 +67,12 @@ kotlin {
             }
         }
 
+        val commonTest by getting {
+            dependencies {
+                api(yamlkt)
+            }
+        }
+
         if (isAndroidSDKAvailable) {
             val androidMain by getting {
                 //

+ 23 - 11
mirai-core-utils/src/commonMain/kotlin/TypeSafeMap.kt

@@ -11,24 +11,30 @@
 
 package net.mamoe.mirai.utils
 
+import kotlinx.serialization.Serializable
 import java.util.concurrent.ConcurrentHashMap
 import kotlin.contracts.InvocationKind
 import kotlin.contracts.contract
 
+@Serializable
 @JvmInline
-public value class TypeKey<out T>(public val name: String) {
+public value class TypeKey<T>(public val name: String) {
     override fun toString(): String = "Key($name)"
 
-    public inline infix fun <T> to(value: T): TypeSafeMap = buildTypeSafeMap { set(this@TypeKey, value) }
+    public inline infix fun to(value: T): TypeSafeMap = buildTypeSafeMap { set(this@TypeKey, value) }
 }
 
-public interface TypeSafeMap {
+/**
+ * @see buildTypeSafeMap
+ */
+public sealed interface TypeSafeMap {
     public val size: Int
 
     public operator fun <T> get(key: TypeKey<T>): T
     public operator fun <T> contains(key: TypeKey<T>): Boolean = get(key) != null
 
-    public fun toMap(): Map<TypeKey<*>, Any?>
+    public fun toMapBoxed(): Map<TypeKey<*>, Any?>
+    public fun toMap(): Map<String, Any?>
 
     public companion object {
         public val EMPTY: TypeSafeMap = TypeSafeMapImpl(emptyMap())
@@ -46,15 +52,16 @@ public operator fun TypeSafeMap.plus(other: TypeSafeMap): TypeSafeMap {
     }
 }
 
-public interface MutableTypeSafeMap : TypeSafeMap {
+public sealed interface MutableTypeSafeMap : TypeSafeMap {
     public operator fun <T> set(key: TypeKey<T>, value: T)
     public fun <T> remove(key: TypeKey<T>): T?
     public fun setAll(other: TypeSafeMap)
 }
 
 
+@PublishedApi
 internal open class TypeSafeMapImpl(
-    internal open val map: Map<TypeKey<*>, Any?> = ConcurrentHashMap()
+    @PublishedApi internal open val map: Map<String, Any?> = ConcurrentHashMap()
 ) : TypeSafeMap {
     override val size: Int get() = map.size
 
@@ -67,16 +74,17 @@ internal open class TypeSafeMapImpl(
     }
 
     override operator fun <T> get(key: TypeKey<T>): T =
-        map[key]?.uncheckedCast() ?: throw NoSuchElementException(key.toString())
+        map[key.name]?.uncheckedCast() ?: throw NoSuchElementException(key.toString())
 
     override operator fun <T> contains(key: TypeKey<T>): Boolean = get(key) != null
 
-    override fun toMap(): Map<TypeKey<*>, Any?> = map
+    override fun toMapBoxed(): Map<TypeKey<*>, Any?> = map.mapKeys { TypeKey<Any?>(it.key) }
+    override fun toMap(): Map<String, Any?> = map
 }
 
 @PublishedApi
 internal class MutableTypeSafeMapImpl(
-    override val map: MutableMap<TypeKey<*>, Any?> = ConcurrentHashMap()
+    @PublishedApi override val map: MutableMap<String, Any?> = ConcurrentHashMap()
 ) : TypeSafeMap, MutableTypeSafeMap, TypeSafeMapImpl(map) {
     override fun equals(other: Any?): Boolean {
         return other is MutableTypeSafeMapImpl && other.map == this.map
@@ -87,7 +95,7 @@ internal class MutableTypeSafeMapImpl(
     }
 
     override operator fun <T> set(key: TypeKey<T>, value: T) {
-        map[key] = value
+        map[key.name] = value
     }
 
     override fun setAll(other: TypeSafeMap) {
@@ -98,9 +106,13 @@ internal class MutableTypeSafeMapImpl(
         }
     }
 
-    override fun <T> remove(key: TypeKey<T>): T? = map.remove(key)?.uncheckedCast()
+    override fun <T> remove(key: TypeKey<T>): T? = map.remove(key.name)?.uncheckedCast()
 }
 
+public inline fun MutableTypeSafeMap(): MutableTypeSafeMap = MutableTypeSafeMapImpl()
+public inline fun MutableTypeSafeMap(map: Map<String, Any?>): MutableTypeSafeMap =
+    MutableTypeSafeMapImpl().also { it.map.putAll(map) }
+
 public inline fun buildTypeSafeMap(block: MutableTypeSafeMap.() -> Unit): MutableTypeSafeMap {
     contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
     return MutableTypeSafeMapImpl().apply(block)

+ 79 - 0
mirai-core-utils/src/commonTest/kotlin/net/mamoe/mirai/utils/TypeSafeMapTest.kt

@@ -0,0 +1,79 @@
+/*
+ * Copyright 2019-2021 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.utils
+
+import net.mamoe.yamlkt.Yaml
+import net.mamoe.yamlkt.YamlBuilder
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+internal class TypeSafeMapTest {
+
+    private val myKey = TypeKey<String>("test")
+    private val myKey2 = TypeKey<CharSequence>("test2")
+
+    @Test
+    fun `can set get`() {
+        val map = MutableTypeSafeMap()
+        map[myKey] = "str"
+        map[myKey2] = "str2"
+        assertEquals(2, map.size)
+        assertEquals("str", map[myKey])
+        assertEquals("str2", map[myKey2])
+    }
+
+    @Test
+    fun `key is inlined`() {
+        val map = MutableTypeSafeMap()
+        map[TypeKey<String>("test")] = "str"
+        map[TypeKey<String>("test")] = "str2"
+        assertEquals(1, map.size)
+        assertEquals("str2", map[TypeKey("test")])
+    }
+
+    @Test
+    fun `can toMap`() {
+        val map = MutableTypeSafeMap()
+        map[myKey] = "str"
+        map[myKey2] = "str2"
+        assertEquals(2, map.size)
+
+        val map1 = map.toMapBoxed()
+
+        assertEquals(2, map1.size)
+        assertEquals("str", map1[myKey])
+        assertEquals("str2", map1[myKey2])
+    }
+
+    @Test
+    fun `test serialization`() {
+        val map = MutableTypeSafeMap()
+        map[myKey] = "str"
+        map[myKey2] = "str2"
+        assertEquals(2, map.size)
+
+        val map1 = map.toMap()
+
+        // Json does not support reflective serialization, so we use Yaml in JSON format
+        val yaml = Yaml {
+            classSerialization = YamlBuilder.MapSerialization.FLOW_MAP
+            mapSerialization = YamlBuilder.MapSerialization.FLOW_MAP
+            listSerialization = YamlBuilder.ListSerialization.FLOW_SEQUENCE
+            stringSerialization = YamlBuilder.StringSerialization.DOUBLE_QUOTATION
+            encodeDefaultValues = true
+        }
+
+        val string = yaml.encodeToString(map1)
+        println(string) // { "test2": "str2" ,"test": "str" }
+
+        val result = MutableTypeSafeMap(Yaml.decodeMapFromString(string).cast())
+        assertEquals(map, result)
+    }
+}