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

* Supports PluginData store with json format.

* Reformat code.
This commit is contained in:
NoMathExpectation 2023-03-22 23:36:54 +08:00 committed by GitHub
parent baf9ee4bf7
commit cb603adbfc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 229 additions and 16 deletions

View File

@ -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;
}

View File

@ -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>

View File

@ -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 {
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()

View File

@ -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())
}
}