diff --git a/mirai-console/backend/mirai-console/compatibility-validation/jvm/api/jvm.api b/mirai-console/backend/mirai-console/compatibility-validation/jvm/api/jvm.api index 75b870bd4..3fc767b38 100644 --- a/mirai-console/backend/mirai-console/compatibility-validation/jvm/api/jvm.api +++ b/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; } diff --git a/mirai-console/backend/mirai-console/src/data/PluginData.kt b/mirai-console/backend/mirai-console/src/data/PluginData.kt index f5a13059e..06d09ff63 100644 --- a/mirai-console/backend/mirai-console/src/data/PluginData.kt +++ b/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 diff --git a/mirai-console/backend/mirai-console/src/internal/data/MultiFilePluginDataStorageImpl.kt b/mirai-console/backend/mirai-console/src/internal/data/MultiFilePluginDataStorageImpl.kt index 18e688aec..b5a17eea3 100644 --- a/mirai-console/backend/mirai-console/src/internal/data/MultiFilePluginDataStorageImpl.kt +++ b/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()?.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() diff --git a/mirai-console/backend/mirai-console/test/data/MultiFilePluginDataStorageImplTests.kt b/mirai-console/backend/mirai-console/test/data/MultiFilePluginDataStorageImplTests.kt new file mode 100644 index 000000000..49afec3bc --- /dev/null +++ b/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 by value() + val map2: MutableMap> 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 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()) + } +} \ No newline at end of file