Sfoglia il codice sorgente

[console] 支持使用 json 保存与读取 `PluginData` 与 `PluginConfig` (#2498)

* Supports PluginData store with json format.

* Reformat code.
NoMathExpectation 3 anni fa
parent
commit
cb603adbfc

+ 1 - 0
mirai-console/backend/mirai-console/compatibility-validation/jvm/api/jvm.api

@@ -1108,6 +1108,7 @@ public abstract interface class net/mamoe/mirai/console/data/PluginConfig : net/
 
 public abstract interface class net/mamoe/mirai/console/data/PluginData {
 	public abstract fun getSaveName ()Ljava/lang/String;
+	public fun getSaveType ()Lnet/mamoe/mirai/console/data/PluginData$SaveType;
 	public abstract fun getSerializersModule ()Lkotlinx/serialization/modules/SerializersModule;
 	public abstract fun getUpdaterSerializer ()Lkotlinx/serialization/KSerializer;
 }

+ 15 - 0
mirai-console/backend/mirai-console/src/data/PluginData.kt

@@ -122,6 +122,21 @@ public interface PluginData {
     @ConsoleExperimentalApi
     public val saveName: String
 
+    /**
+     * [PluginData] 序列化时使用的格式的枚举.
+     */
+    @ConsoleExperimentalApi
+    public enum class SaveType(@ConsoleExperimentalApi public val extension: String) {
+        YAML("yml"), JSON("json")
+    }
+
+    /**
+     * 决定这个 [PluginData] 序列化时使用的格式, 默认为 YAML.
+     * 具体实现格式由 [PluginDataStorage] 决定.
+     */
+    @ConsoleExperimentalApi
+    public val saveType: SaveType get() = SaveType.YAML
+
     @ConsoleExperimentalApi
     public val updaterSerializer: KSerializer<Unit>
 

+ 55 - 17
mirai-console/backend/mirai-console/src/internal/data/MultiFilePluginDataStorageImpl.kt

@@ -40,9 +40,18 @@ internal open class MultiFilePluginDataStorageImpl(
         val file = getPluginDataFile(holder, instance)
         val text = file.readText().removePrefix("\uFEFF")
         if (text.isNotBlank()) {
-            val yaml = createYaml(instance)
             try {
-                yaml.decodeFromString(instance.updaterSerializer, text)
+                when (instance.saveType) {
+                    PluginData.SaveType.YAML -> {
+                        val yaml = createYaml(instance)
+                        yaml.decodeFromString(instance.updaterSerializer, text)
+                    }
+
+                    PluginData.SaveType.JSON -> {
+                        val json = createJson(instance)
+                        json.decodeFromString(instance.updaterSerializer, text)
+                    }
+                }
             } catch (cause: Throwable) {
                 // backup data file
                 file.copyTo(file.resolveSibling("${file.name}.${currentTimeMillis()}.bak"))
@@ -67,7 +76,7 @@ internal open class MultiFilePluginDataStorageImpl(
         }
         dir.mkdir()
 
-        val file = dir.resolve("$name.yml")
+        val file = dir.resolve("$name.${instance.saveType.extension}")
         if (file.isDirectory) {
             error("Target File $file is occupied by a directory therefore data ${instance::class.qualifiedNameOrTip} can't be saved.")
         }
@@ -82,27 +91,41 @@ internal open class MultiFilePluginDataStorageImpl(
     public override fun store(holder: PluginDataHolder, instance: PluginData) {
         getPluginDataFile(holder, instance).writeText(
             kotlin.runCatching {
-                createYaml(instance).encodeToString(instance.updaterSerializer, Unit).also {
-                    Yaml.decodeAnyFromString(it) // test yaml
+                when (instance.saveType) {
+                    PluginData.SaveType.YAML -> {
+                        val yaml = createYaml(instance)
+                        yaml.encodeToString(instance.updaterSerializer, Unit).also {
+                            yaml.decodeAnyFromString(it) // test yaml
+                        }
+                    }
+
+                    PluginData.SaveType.JSON -> {
+                        val json = createJson(instance)
+                        json.encodeToString(instance.updaterSerializer, Unit).also {
+                            json.decodeFromString(instance.updaterSerializer, it) // test json
+                        }
+                    }
                 }
             }.recoverCatching {
                 logger.warning(
-                    "Could not save ${instance.saveName} in YAML format due to exception in YAML encoder. " +
+                    "Could not save ${instance.saveName} in ${instance.saveType.name} format due to exception in ${instance.saveType.name} encoder. " +
                             "Please report this exception and relevant configurations to https://github.com/mamoe/mirai/issues/new/choose",
                     it
                 )
-                @Suppress("JSON_FORMAT_REDUNDANT")
-                Json {
-                    serializersModule = MessageSerializers.serializersModule + instance.serializersModule
-
-                    prettyPrint = true
-                    ignoreUnknownKeys = true
-                    isLenient = true
-                    allowStructuredMapKeys = true
-                    encodeDefaults = true
-                }.encodeToString(instance.updaterSerializer, Unit)
+
+                if (instance.saveType == PluginData.SaveType.JSON) {
+                    throw it
+                }
+
+                val json = createJson(instance)
+                json.encodeToString(instance.updaterSerializer, Unit).also { string ->
+                    json.decodeFromString(instance.updaterSerializer, string) // test json
+                }
             }.getOrElse {
-                throw IllegalStateException("Exception while saving $instance, saveName=${instance.saveName}", it)
+                throw IllegalStateException(
+                    "Exception while saving $instance, saveName=${instance.saveName} in json format",
+                    it
+                )
             }
         )
 //        logger.verbose { "Successfully saved PluginData: ${instance.saveName} (containing ${instance.castOrNull<AbstractPluginData>()?.valueNodes?.size} properties)" }
@@ -114,6 +137,21 @@ internal open class MultiFilePluginDataStorageImpl(
                 MessageSerializers.serializersModule + instance.serializersModule // MessageSerializers.serializersModule is dynamic
         }
     }
+
+    private fun createJson(instance: PluginData): Json {
+        return Json {
+            serializersModule =
+                MessageSerializers.serializersModule + instance.serializersModule // MessageSerializers.serializersModule is dynamic
+
+            prettyPrint = true
+            ignoreUnknownKeys = true
+            isLenient = true
+            allowStructuredMapKeys = true
+            encodeDefaults = true
+
+            classDiscriminator = "#class"
+        }
+    }
 }
 
 internal fun Path.mkdir(): Boolean = this.toFile().mkdir()

+ 159 - 0
mirai-console/backend/mirai-console/test/data/MultiFilePluginDataStorageImplTests.kt

@@ -0,0 +1,159 @@
+/*
+ * Copyright 2019-2022 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.console.data
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.JsonClassDiscriminator
+import net.mamoe.mirai.console.internal.data.MultiFilePluginDataStorageImpl
+import net.mamoe.mirai.console.testFramework.AbstractConsoleInstanceTest
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.io.TempDir
+import java.nio.file.Path
+import kotlin.test.assertEquals
+
+internal class MultiFilePluginDataStorageImplTests : AbstractConsoleInstanceTest() {
+    @TempDir
+    internal lateinit var storePath: Path
+
+    @Serializable
+    @JsonClassDiscriminator("base_type")
+    internal sealed class Base // not using interface, see https://github.com/Kotlin/kotlinx.serialization/issues/2181
+
+    @Serializable
+    @SerialName("DerivedA")
+    internal data class DerivedA(val valueA: Double) : Base()
+
+    @Serializable
+    @SerialName("DerivedB")
+    internal data class DerivedB(val valueB: String) : Base()
+
+    @Serializable
+    @SerialName("DerivedC")
+    internal object DerivedC : Base() {
+        @Suppress("unused")
+        const val valueC: Int = 42
+    }
+
+    private class YamlPluginData : AutoSavePluginData("test_yaml") {
+        var int by value(1)
+        val map: MutableMap<String, String> by value()
+        val map2: MutableMap<String, MutableMap<String, String>> by value()
+
+        companion object {
+            val string = """
+                int: 2
+                map: 
+                  key1: value1
+                  key2: value2
+                map2: 
+                  key1: 
+                    key1: value1
+                    key2: value2
+                  key2: 
+                    key1: value1
+                    key2: value2
+            """.trimIndent()
+        }
+    }
+
+    private class JsonPluginData : AutoSavePluginData("test_json") {
+        override val saveType = PluginData.SaveType.JSON
+
+        val baseMap: MutableMap<String, Base> by value()
+
+        companion object {
+            val string = """
+                {
+                    "baseMap": {
+                        "A": {
+                            "base_type": "DerivedA",
+                            "valueA": 11.4514
+                        },
+                        "B": {
+                            "base_type": "DerivedB",
+                            "valueB": "mamoe.mirai"
+                        },
+                        "C": {
+                            "base_type": "DerivedC"
+                        }
+                    }
+                }
+            """.trimIndent()
+        }
+    }
+
+    private val dataStorage by lazy { MultiFilePluginDataStorageImpl(storePath) }
+
+    @Test
+    fun testYamlLoad() {
+        val data = YamlPluginData()
+        dataStorage.load(mockPlugin, data)
+        dataStorage.getPluginDataFileInternal(mockPlugin, data).writeText(YamlPluginData.string)
+        dataStorage.load(mockPlugin, data)
+
+        assertEquals(2, data.int)
+        assertEquals(mapOf("key1" to "value1", "key2" to "value2"), data.map)
+        assertEquals(
+            mapOf(
+                "key1" to mapOf("key1" to "value1", "key2" to "value2"),
+                "key2" to mapOf("key1" to "value1", "key2" to "value2")
+            ), data.map2
+        )
+    }
+
+    @Test
+    fun testYamlStore() {
+        val data = YamlPluginData()
+        dataStorage.load(mockPlugin, data)
+
+        data.int = 2
+        data.map["key1"] = "value1"
+        data.map["key2"] = "value2"
+        data.map2["key1"] = mutableMapOf("key1" to "value1", "key2" to "value2")
+        data.map2["key2"] = mutableMapOf("key1" to "value1", "key2" to "value2")
+
+        dataStorage.store(mockPlugin, data)
+
+        val file = dataStorage.getPluginDataFileInternal(mockPlugin, data)
+        assertEquals(YamlPluginData.string, file.readText())
+    }
+
+    @Test
+    fun testJsonLoad() {
+        val data = JsonPluginData()
+        dataStorage.load(mockPlugin, data)
+        dataStorage.getPluginDataFileInternal(mockPlugin, data).writeText(JsonPluginData.string)
+        dataStorage.load(mockPlugin, data)
+
+        assertEquals(
+            mapOf(
+                "A" to DerivedA(11.4514),
+                "B" to DerivedB("mamoe.mirai"),
+                "C" to DerivedC
+            ), data.baseMap
+        )
+    }
+
+    @Test
+    fun testJsonStore() {
+        val data = JsonPluginData()
+        dataStorage.load(mockPlugin, data)
+
+        data.baseMap["A"] = DerivedA(11.4514)
+        data.baseMap["B"] = DerivedB("mamoe.mirai")
+        data.baseMap["C"] = DerivedC
+
+        dataStorage.store(mockPlugin, data)
+
+        val file = dataStorage.getPluginDataFileInternal(mockPlugin, data)
+        assertEquals(JsonPluginData.string, file.readText())
+    }
+}