diff --git a/mirai-console/backend/mirai-console/src/data/AbstractPluginData.kt b/mirai-console/backend/mirai-console/src/data/AbstractPluginData.kt index a9d20ef5a..011b5aeb1 100644 --- a/mirai-console/backend/mirai-console/src/data/AbstractPluginData.kt +++ b/mirai-console/backend/mirai-console/src/data/AbstractPluginData.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 Mamoe Technologies and contributors. + * 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. @@ -78,9 +78,7 @@ public abstract class AbstractPluginData : PluginData, PluginDataImpl() { public final override val updaterSerializer: KSerializer<Unit> get() = super.updaterSerializer - @ConsoleExperimentalApi - public override val serializersModule: SerializersModule = EmptySerializersModule - + public override val serializersModule: SerializersModule get() = EmptySerializersModule /** * 当所属于这个 [PluginData] 的 [Value] 的 [值][Value.value] 被修改时被调用. */ diff --git a/mirai-console/backend/mirai-console/src/data/PluginData.kt b/mirai-console/backend/mirai-console/src/data/PluginData.kt index f9f410994..1aa826fb7 100644 --- a/mirai-console/backend/mirai-console/src/data/PluginData.kt +++ b/mirai-console/backend/mirai-console/src/data/PluginData.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 Mamoe Technologies and contributors. + * 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. @@ -19,7 +19,6 @@ package net.mamoe.mirai.console.data import kotlinx.serialization.KSerializer import kotlinx.serialization.modules.SerializersModule -import kotlinx.serialization.modules.serializersModuleOf import net.mamoe.mirai.console.compiler.common.ResolveContext import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.RESTRICTED_NO_ARG_CONSTRUCTOR import net.mamoe.mirai.console.data.java.JAutoSavePluginData @@ -30,6 +29,9 @@ import net.mamoe.mirai.console.plugin.jvm.AbstractJvmPlugin import net.mamoe.mirai.console.plugin.jvm.JvmPlugin import net.mamoe.mirai.console.plugin.jvm.reloadPluginData import net.mamoe.mirai.console.util.ConsoleExperimentalApi +import net.mamoe.mirai.message.MessageSerializers +import net.mamoe.mirai.message.data.MessageChain +import net.mamoe.mirai.utils.NotStableForInheritance import kotlin.internal.LowPriorityInOverloadResolution import kotlin.reflect.KClass import kotlin.reflect.KType @@ -108,10 +110,13 @@ import kotlin.reflect.typeOf * ## 实现注意 * 此类型处于实验性阶段. 使用其中定义的属性和函数是安全的, 但将来可能会新增成员抽象函数. * + * 继承 [AbstractPluginData] 比继承 [PluginData] 更安全, 尽管 [AbstractPluginData] 也不稳定. + * * @see AbstractJvmPlugin.reloadPluginData 通过 [JvmPlugin] 获取指定 [PluginData] 实例. * @see PluginDataStorage [PluginData] 存储仓库 * @see PluginDataExtensions 相关 [SerializerAwareValue] 映射函数 */ +@NotStableForInheritance public interface PluginData { /** * 这个 [PluginData] 保存时使用的名称. @@ -130,13 +135,56 @@ public interface PluginData { public fun onValueChanged(value: Value<*>) /** - * 用于支持多态序列化. + * 序列化本对象数据时使用的 [SerializersModule]. 用于支持多态序列化等. + * 在序列化时会先使用 [PluginData.serializersModule], 再对无法找到 serializer 的类型使用 [MessageSerializers.serializersModule]. + * + * ### 使用示例 + * + * 假设你编写了一个类型 `ChatHistory` 用来存储一个群的消息记录: + * ``` + * data class ChatHistory( + * val groupId: Long, + * val chain: List<MessageChain>, + * ) + * ``` + * + * 要在 [PluginData] 中支持它, 需要首先为 `ChatHistory` 编写 [KSerializer]. + * + * 一种方式是为其添加 [kotlinx.serialization.Serializable]: + * + * ``` + * @Serializable + * data class ChatHistory( + * val groupId: Long, + * val chain: List<MessageChain>, + * ) + * ``` + * + * 编译器将会自动生成一个 [KSerializer], 可通过 `ChatHistory.Companion.serializer()` 获取. + * + * 然后在 [PluginData] 定义中添加该 [KSerializer]: + * ``` + * object MyData : AutoSavePluginData("save") { + * // 注意, serializersModule 需要早于其他属性定义初始化 + * override val serializersModule = SerializersModule { + * contextual(ChatHistory::class, ChatHistory.serializers()) // 为 ChatHistory 指定 KSerializer + * } + * + * val histories: Map<Long, ChatHistory> by value() + * } + * ``` + * + * 然而, 即使不覆盖 `serializersModule` 提供 [KSerializer], mirai 也会通过反射尝试获取. + * + * 但对于不是使用 `@Serializable` 注解方式, 或者是 `interface`, `abstract class` 等的抽象类型, 则必须覆盖 `serializersModule` 并提供其 [KSerializer]. + * + * * * @see SerializersModule - * @see serializersModuleOf + * + * @since 2.11 */ - @ConsoleExperimentalApi - public val serializersModule: SerializersModule + public val serializersModule: SerializersModule // 该属性在 2.0 增加, 但在 2.11 才正式支持并删除 @MiraiExperimentalApi /** * 当这个 [PluginData] 被放入一个 [PluginDataStorage] 时调用 @@ -200,28 +248,67 @@ public fun PluginData.value(default: String): SerializerAwareValue<String> = val /** * 通过具体化类型创建一个 [SerializerAwareValue], 并设置初始值. * - * @param T 具体化参数类型 T. 仅支持: + * 2.11 起, 本函数会优先根据返回值推断类型. 如下示例: + * ``` + * var singleMessage: SingleMessage by value(PlainText("str")) // value 的类型为 SerializerAwareValue<SingleMessage> + * ``` + * 这符合正常的类型定义逻辑. + * + * @param T 具体化参数类型 T. 在 2.11 以前, 支持: * - 基础数据类型 * - 标准库集合类型 ([List], [Map], [Set]) * - 标准库数据类型 ([Map.Entry], [Pair], [Triple]) - * - 和使用 [kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) 的 [Serializable] 标记的 + * - 使用 [kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) 的 [Serializable] 标记的, 可以通过反射获取 [KSerializer] 的类型 + * + * 2.11 起, 还支持: + * - [MessageSerializers] 支持的所有类型, 如 [MessageChain]. + * - 在 [PluginData.serializersModule] 自定义支持的类型 */ @Suppress("UNCHECKED_CAST") @LowPriorityInOverloadResolution public inline fun <reified T> PluginData.value( default: T, crossinline apply: T.() -> Unit = {}, -): SerializerAwareValue<T> = - valueFromKType(typeOf<T>(), default).also { it.value.apply() } +): SerializerAwareValue<@kotlin.internal.Exact T> { + /* + * 使用 `@Exact` 的 trick (自 2.11.0-RC) + * + * 使用前: + * + * ``` + * var singleMessage: SingleMessage by value(PlainText("str")) + * ``` + * + * `value` 的 reified [T] 根据其参数推断为 [PlainText], 则会使用 `PlainText.serializer()`. + * + * 可以通过序列化后的 YAML 文本直观地感受问题: + * ```yaml + * singleMessage: + * content: str + * ``` + * 那么将来若 `singleMessage` 的值变更为非 [PlainText] 类型, 将会无法序列化. + * + * 附使用 `@Exact` 时的正确结果 (使用 [SingleMessage] 的序列化器 (来自 [MessageSerializers.serializersModule])): + * + * ```yaml + * singleMessage: + * type: PlainText + * value: + * content: str + * ``` + * + * 相关测试: [net.mamoe.mirai.console.data.PluginDataTest.supports message chain] + */ + return valueFromKType(typeOf<T>(), default).also { it.value.apply() } +} /** * 通过具体化类型创建一个 [SerializerAwareValue]. - * @see valueFromKType 查看更多实现信息 */ @ResolveContext(RESTRICTED_NO_ARG_CONSTRUCTOR) @LowPriorityInOverloadResolution public inline fun <@ResolveContext(RESTRICTED_NO_ARG_CONSTRUCTOR) reified T> - PluginData.value(apply: T.() -> Unit = {}): SerializerAwareValue<T> = + PluginData.value(apply: T.() -> Unit = {}): SerializerAwareValue<@kotlin.internal.Exact T> = valueImpl<T>(typeOf<T>(), T::class).also { it.value.apply() } @Suppress("UNCHECKED_CAST") 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 acca0be75..721fe920f 100644 --- a/mirai-console/backend/mirai-console/src/internal/data/MultiFilePluginDataStorageImpl.kt +++ b/mirai-console/backend/mirai-console/src/internal/data/MultiFilePluginDataStorageImpl.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 Mamoe Technologies and contributors. + * 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. @@ -10,12 +10,14 @@ package net.mamoe.mirai.console.internal.data import kotlinx.serialization.json.Json -import net.mamoe.mirai.console.data.* +import kotlinx.serialization.modules.plus +import net.mamoe.mirai.console.data.MultiFilePluginDataStorage +import net.mamoe.mirai.console.data.PluginData +import net.mamoe.mirai.console.data.PluginDataHolder +import net.mamoe.mirai.console.data.PluginDataStorage import net.mamoe.mirai.console.util.ConsoleExperimentalApi +import net.mamoe.mirai.message.MessageSerializers import net.mamoe.mirai.utils.MiraiLogger -import net.mamoe.mirai.utils.SilentLogger -import net.mamoe.mirai.utils.debug -import net.mamoe.mirai.utils.warning import net.mamoe.yamlkt.Yaml import java.io.File import java.nio.file.Path @@ -23,7 +25,7 @@ import java.nio.file.Path @Suppress("RedundantVisibilityModifier") // might be public in the future internal open class MultiFilePluginDataStorageImpl( public final override val directoryPath: Path, - private val logger: MiraiLogger = SilentLogger, + private val logger: MiraiLogger = MiraiLogger.Factory.create(MultiFilePluginDataStorageImpl::class), ) : PluginDataStorage, MultiFilePluginDataStorage { init { directoryPath.mkdir() @@ -35,12 +37,15 @@ internal open class MultiFilePluginDataStorageImpl( // 0xFEFF is BOM, handle UTF8-BOM val text = getPluginDataFile(holder, instance).readText().removePrefix("\uFEFF") if (text.isNotBlank()) { - logger.warning { "Deserializing $text" } - Yaml.decodeFromString(instance.updaterSerializer, text) + createYaml(instance).decodeFromString(instance.updaterSerializer, text) } else { this.store(holder, instance) // save an initial copy } - logger.debug { "Successfully loaded PluginData: ${instance.saveName} (containing ${instance.castOrNull<AbstractPluginData>()?.valueNodes?.size} properties)" } +// logger.verbose { "Successfully loaded PluginData: ${instance.saveName} (containing ${instance.castOrNull<AbstractPluginData>()?.valueNodes?.size} properties)" } + } + + internal fun getPluginDataFileInternal(holder: PluginDataHolder, instance: PluginData): File { + return getPluginDataFile(holder, instance) } protected open fun getPluginDataFile(holder: PluginDataHolder, instance: PluginData): File { @@ -56,39 +61,45 @@ internal open class MultiFilePluginDataStorageImpl( if (file.isDirectory) { error("Target File $file is occupied by a directory therefore data ${instance::class.qualifiedNameOrTip} can't be saved.") } - logger.debug { "File allocated for ${instance.saveName}: $file" } +// logger.verbose { "File allocated for ${instance.saveName}: $file" } return file.toFile().also { it.createNewFile() } } - private val json = Json { - prettyPrint = true - ignoreUnknownKeys = true - isLenient = true - allowStructuredMapKeys = true - encodeDefaults = true - } - - private val yaml = Yaml - @ConsoleExperimentalApi public override fun store(holder: PluginDataHolder, instance: PluginData) { getPluginDataFile(holder, instance).writeText( kotlin.runCatching { - yaml.encodeToString(instance.updaterSerializer, Unit).also { - yaml.decodeAnyFromString(it) // test yaml + createYaml(instance).encodeToString(instance.updaterSerializer, Unit).also { + Yaml.decodeAnyFromString(it) // test yaml } }.recoverCatching { logger.warning( "Could not save ${instance.saveName} in YAML format due to exception in YAML encoder. " + - "Please report this exception and relevant configurations to https://github.com/mamoe/mirai-console/issues/new", + "Please report this exception and relevant configurations to https://github.com/mamoe/mirai/issues/new/choose", it ) - json.encodeToString(instance.updaterSerializer, Unit) + @Suppress("JSON_FORMAT_REDUNDANT") + Json { + serializersModule = MessageSerializers.serializersModule + instance.serializersModule + + prettyPrint = true + ignoreUnknownKeys = true + isLenient = true + allowStructuredMapKeys = true + encodeDefaults = true + }.encodeToString(instance.updaterSerializer, Unit) }.getOrElse { throw IllegalStateException("Exception while saving $instance, saveName=${instance.saveName}", it) } ) - logger.debug { "Successfully saved PluginData: ${instance.saveName} (containing ${instance.castOrNull<AbstractPluginData>()?.valueNodes?.size} properties)" } +// logger.verbose { "Successfully saved PluginData: ${instance.saveName} (containing ${instance.castOrNull<AbstractPluginData>()?.valueNodes?.size} properties)" } + } + + private fun createYaml(instance: PluginData): Yaml { + return Yaml { + this.serializersModule = + MessageSerializers.serializersModule + instance.serializersModule // MessageSerializers.serializersModule is dynamic + } } } diff --git a/mirai-console/backend/mirai-console/src/internal/data/serializerHelper.kt b/mirai-console/backend/mirai-console/src/internal/data/serializerHelper.kt index 995519d1b..9a01ccd82 100644 --- a/mirai-console/backend/mirai-console/src/internal/data/serializerHelper.kt +++ b/mirai-console/backend/mirai-console/src/internal/data/serializerHelper.kt @@ -1,29 +1,29 @@ /* - * Copyright 2019-2021 Mamoe Technologies and contributors. + * 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. + * 此源代码的使用受 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/master/LICENSE + * https://github.com/mamoe/mirai/blob/dev/LICENSE */ @file:Suppress("unused") package net.mamoe.mirai.console.internal.data -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.KSerializer -import kotlinx.serialization.builtins.* +import kotlinx.serialization.builtins.ArraySerializer +import kotlinx.serialization.builtins.nullable import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.serializer +import kotlinx.serialization.serializerOrNull +import net.mamoe.mirai.message.MessageSerializers import net.mamoe.yamlkt.YamlDynamicSerializer import net.mamoe.yamlkt.YamlNullableDynamicSerializer import java.lang.reflect.Modifier -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.ConcurrentMap import kotlin.reflect.KClass import kotlin.reflect.KType @@ -32,41 +32,22 @@ import kotlin.reflect.KType * Copied from kotlinx.serialization, modifications are marked with "/* mamoe modify */" * Copyright 2017-2020 JetBrains s.r.o. */ -@OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class) -@Suppress( - "UNCHECKED_CAST", - "NO_REFLECTION_IN_CLASS_PATH", - "UNSUPPORTED", - "INVISIBLE_MEMBER", - "INVISIBLE_REFERENCE", - "IMPLICIT_CAST_TO_ANY" -) -internal fun serializerMirai(type: KType): KSerializer<Any?> { - fun serializerByKTypeImpl(type: KType): KSerializer<Any> { +@Suppress("UNCHECKED_CAST") +internal fun SerializersModule.serializerMirai(type: KType): KSerializer<Any?> { + fun serializerByKTypeImpl(type: KType): KSerializer<*> { val rootClass = type.classifierAsKClass() + this.serializerOrNull(type)?.let { return it } // Kotlin builtin and user-defined + MessageSerializers.serializersModule.serializerOrNull(type)?.let { return it } // Mirai Messages + val typeArguments = type.arguments .map { requireNotNull(it.type) { "Star projections in type arguments are not allowed, but had $type" } } return when { - typeArguments.isEmpty() -> rootClass.serializer() + typeArguments.isEmpty() -> this.serializer(type) else -> { - val serializers = typeArguments - .map(::serializerMirai) - // Array is not supported, see KT-32839 + val serializers = typeArguments.map(::serializerMirai) when (rootClass) { - List::class, MutableList::class, ArrayList::class -> ListSerializer(serializers[0]) - HashSet::class -> SetSerializer(serializers[0]) - Set::class, MutableSet::class, LinkedHashSet::class -> SetSerializer(serializers[0]) - HashMap::class -> MapSerializer(serializers[0], serializers[1]) - ConcurrentHashMap::class, ConcurrentMap::class -> MapSerializer(serializers[0], serializers[1]).map( - serializer = { HashMap(it as Map<*, *>) }, - deserializer = { ConcurrentHashMap(it) } - ) - Map::class, MutableMap::class, LinkedHashMap::class -> MapSerializer(serializers[0], serializers[1]) - Map.Entry::class -> MapEntrySerializer(serializers[0], serializers[1]) - Pair::class -> PairSerializer(serializers[0], serializers[1]) - Triple::class -> TripleSerializer(serializers[0], serializers[1], serializers[2]) - /* mamoe modify */ Any::class -> if (type.isMarkedNullable) YamlNullableDynamicSerializer else YamlDynamicSerializer + Any::class -> if (type.isMarkedNullable) YamlNullableDynamicSerializer else YamlDynamicSerializer else -> { if (rootClass.java.isArray) { return ArraySerializer( @@ -81,10 +62,10 @@ internal fun serializerMirai(type: KType): KSerializer<Any?> { } } } - }.cast() + } } - val result = serializerByKTypeImpl(type) + val result = serializerByKTypeImpl(type) as KSerializer<Any> return if (type.isMarkedNullable) result.nullable else result.cast() } diff --git a/mirai-console/backend/mirai-console/src/internal/data/valueFromKTypeImpl.kt b/mirai-console/backend/mirai-console/src/internal/data/valueFromKTypeImpl.kt index 23ba837aa..a254998bf 100644 --- a/mirai-console/backend/mirai-console/src/internal/data/valueFromKTypeImpl.kt +++ b/mirai-console/backend/mirai-console/src/internal/data/valueFromKTypeImpl.kt @@ -1,10 +1,10 @@ /* - * Copyright 2019-2021 Mamoe Technologies and contributors. + * 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. + * 此源代码的使用受 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/master/LICENSE + * https://github.com/mamoe/mirai/blob/dev/LICENSE */ @file:Suppress("NOTHING_TO_INLINE", "INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") @@ -68,7 +68,7 @@ internal fun PluginData.valueFromKTypeImpl(type: KType): SerializerAwareValue<*> }, kToValue = { k -> valueFromKType(type.arguments[0].type!!, k) }, vToValue = { v -> valueFromKType(type.arguments[1].type!!, v) } - ).serializableValueWith(serializerMirai(type) as KSerializer<Map<Any?, Any?>>) // erased + ).serializableValueWith(serializersModule.serializerMirai(type) as KSerializer<Map<Any?, Any?>>) // erased } } MutableList::class, @@ -84,7 +84,7 @@ internal fun PluginData.valueFromKTypeImpl(type: KType): SerializerAwareValue<*> TODO() } else { return createCompositeListValueImpl<Any?> { v -> valueFromKType(type.arguments[0].type!!, v) } - .serializableValueWith(serializerMirai(type) as KSerializer<List<Any?>>) + .serializableValueWith(serializersModule.serializerMirai(type) as KSerializer<List<Any?>>) } } MutableSet::class, @@ -101,11 +101,11 @@ internal fun PluginData.valueFromKTypeImpl(type: KType): SerializerAwareValue<*> TODO() } else { return createCompositeSetValueImpl<Any?> { v -> valueFromKType(type.arguments[0].type!!, v) } - .serializableValueWith(serializerMirai(type) as KSerializer<Set<Any?>>) + .serializableValueWith(serializersModule.serializerMirai(type) as KSerializer<Set<Any?>>) } } else -> { - val serializer = serializerMirai(type) + val serializer = serializersModule.serializerMirai(type) return LazyReferenceValueImpl<Any?>().serializableValueWith(serializer) } } diff --git a/mirai-console/backend/mirai-console/test/data/SettingTest.kt b/mirai-console/backend/mirai-console/test/data/PluginDataTest.kt similarity index 52% rename from mirai-console/backend/mirai-console/test/data/SettingTest.kt rename to mirai-console/backend/mirai-console/test/data/PluginDataTest.kt index e627593f6..8bf0b6a27 100644 --- a/mirai-console/backend/mirai-console/test/data/SettingTest.kt +++ b/mirai-console/backend/mirai-console/test/data/PluginDataTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 Mamoe Technologies and contributors. + * 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. @@ -9,14 +9,26 @@ package net.mamoe.mirai.console.data +import kotlinx.serialization.builtins.serializer import kotlinx.serialization.json.Json -import net.mamoe.mirai.console.util.ConsoleInternalApi +import kotlinx.serialization.modules.SerializersModule +import net.mamoe.mirai.console.internal.data.MultiFilePluginDataStorageImpl +import net.mamoe.mirai.console.testFramework.AbstractConsoleInstanceTest +import net.mamoe.mirai.message.data.MessageChain +import net.mamoe.mirai.message.data.PlainText +import net.mamoe.mirai.message.data.SingleMessage +import net.mamoe.mirai.message.data.messageChainOf +import net.mamoe.mirai.utils.mapPrimitive import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path import kotlin.test.assertEquals import kotlin.test.assertSame -@OptIn(ConsoleInternalApi::class) -internal class PluginDataTest { +internal class PluginDataTest : AbstractConsoleInstanceTest() { + @TempDir + lateinit var tempDir: Path + class MyPluginData : AutoSavePluginData("test") { var int by value(1) val map: MutableMap<String, String> by value() @@ -128,4 +140,86 @@ internal class PluginDataTest { assertSame(reference(), delegation()) // check shadowing } + + + class SupportsMessageChain : AutoSavePluginData("test") { + val chain: MessageChain by value(messageChainOf(PlainText("str"))) + } + + @Test + fun `supports message chain`() { + assertEquals( + """ + chain: + - type: PlainText + value: + content: str + """.trimIndent(), serializePluginData(SupportsMessageChain()) + ) + serializeAndRereadPluginData(SupportsMessageChain()) + } + + class SupportsPolymorphicCorrectly : AutoSavePluginData("test") { + val singleMessage: SingleMessage by value(PlainText("str")) + val plainText: PlainText by value(PlainText("str")) + } + + @Test + fun `supports polymorphic correctly`() { + assertEquals( + """ + singleMessage: + type: PlainText + value: + content: str + plainText: + content: str + """.trimIndent(), serializePluginData(SupportsPolymorphicCorrectly()) + ) + serializeAndRereadPluginData(SupportsPolymorphicCorrectly()) + } + + class SupportsSerializersModule : AutoSavePluginData("test") { + override val serializersModule: SerializersModule = SerializersModule { + contextual(MyClass::class, myClassSerializer) + } + + val v: MyClass by value(MyClass("test")) + + data class MyClass( + val str: String + ) + + companion object { + private val myClassSerializer = String.serializer().mapPrimitive("MyClass", + { MyClass(it) }, + { it.str } + ) + } + } + + @Test + fun `supports serializers module`() { + assertEquals( + """ + v: test + """.trimIndent(), serializePluginData(SupportsSerializersModule()) + ) + serializeAndRereadPluginData(SupportsSerializersModule()) + } + + + private fun serializePluginData(data: PluginData): String { + val storage = MultiFilePluginDataStorageImpl(tempDir) + storage.store(mockPlugin, data) + return storage.getPluginDataFileInternal(mockPlugin, data).readText() + } + + private fun serializeAndRereadPluginData(data: PluginData) { + val storage = MultiFilePluginDataStorageImpl(tempDir) + storage.store(mockPlugin, data) + val serialized = storage.getPluginDataFileInternal(mockPlugin, data).readText() + storage.load(mockPlugin, data) + assertEquals(serialized, storage.getPluginDataFileInternal(mockPlugin, data).readText()) + } } diff --git a/mirai-console/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/UsingDerivedMap.kt b/mirai-console/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/UsingDerivedMap.kt index 728287321..712e3968a 100644 --- a/mirai-console/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/UsingDerivedMap.kt +++ b/mirai-console/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/UsingDerivedMap.kt @@ -1,10 +1,10 @@ /* - * Copyright 2019-2021 Mamoe Technologies and contributors. + * 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. + * 此源代码的使用受 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/master/LICENSE + * https://github.com/mamoe/mirai/blob/dev/LICENSE */ package org.example.myplugin @@ -12,7 +12,10 @@ package org.example.myplugin import net.mamoe.mirai.console.data.AutoSavePluginConfig import net.mamoe.mirai.console.data.ReadOnlyPluginConfig import net.mamoe.mirai.console.data.value +import net.mamoe.mirai.message.data.MessageChain +import net.mamoe.mirai.message.data.messageChainOf import org.example.myplugin.DataTest1.provideDelegate +import java.util.* import java.util.concurrent.ConcurrentHashMap @@ -23,6 +26,9 @@ object UsingDerivedMap : AutoSavePluginConfig("data") { var p4 by value<HashMap<String, String>>() var p3 by value<ConcurrentHashMap<String, String>>() + var p8 by value<List<String>>() var p5 by value<ArrayList<String>>() - var p6 by value<AbstractList<String>>() + var p7 by value<AbstractList<String>>() + + var p6 by value<MessageChain>(messageChainOf()) // no error } diff --git a/mirai-console/tools/intellij-plugin/src/diagnostics/PluginDataValuesChecker.kt b/mirai-console/tools/intellij-plugin/src/diagnostics/PluginDataValuesChecker.kt index c12edee57..2997a6ac0 100644 --- a/mirai-console/tools/intellij-plugin/src/diagnostics/PluginDataValuesChecker.kt +++ b/mirai-console/tools/intellij-plugin/src/diagnostics/PluginDataValuesChecker.kt @@ -1,10 +1,10 @@ /* - * Copyright 2019-2021 Mamoe Technologies and contributors. + * 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. + * 此源代码的使用受 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/master/LICENSE + * https://github.com/mamoe/mirai/blob/dev/LICENSE */ @file:Suppress("MemberVisibilityCanBePrivate") @@ -31,6 +31,7 @@ import org.jetbrains.kotlin.resolve.calls.checkers.CallCheckerContext import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall import org.jetbrains.kotlin.resolve.checkers.DeclarationChecker import org.jetbrains.kotlin.resolve.checkers.DeclarationCheckerContext +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe import org.jetbrains.kotlin.resolve.descriptorUtil.isSubclassOf import org.jetbrains.kotlin.types.KotlinType import org.jetbrains.kotlin.types.SimpleType @@ -114,6 +115,9 @@ class PluginDataValuesChecker : CallChecker, DeclarationChecker { val factory = when { jetTypeFqn == "java.util.concurrent.ConcurrentHashMap" -> MiraiConsoleErrors.USING_DERIVED_CONCURRENT_MAP_TYPE + classDescriptor.fqNameSafe.asString() + .startsWith("net.mamoe.mirai.message.data.") -> null // Don't report for MessageChain + classDescriptor.isSubclassOf(builtIns.list) && jetTypeFqn != "kotlin.collections.List" -> { if (classDescriptor.isSubclassOf(builtIns.mutableList)) { if (jetTypeFqn != "kotlin.collections.MutableList" && jetTypeFqn != "java.util.List") {