diff --git a/mirai-core-utils/src/commonMain/kotlin/Either.kt b/mirai-core-utils/src/commonMain/kotlin/Either.kt new file mode 100644 index 000000000..785b39264 --- /dev/null +++ b/mirai-core-utils/src/commonMain/kotlin/Either.kt @@ -0,0 +1,140 @@ +/* + * 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/master/LICENSE + */ + +@file:Suppress("NOTHING_TO_INLINE", "unused") + +package net.mamoe.mirai.utils + +/** + * Safe union of two types. + */ +@JvmInline +public value class Either private constructor( + @PublishedApi + @JvmField + internal val value: Any? +) { + override fun toString(): String = value.toString() + + public companion object { + /////////////////////////////////////////////////////////////////////////// + // constructors + /////////////////////////////////////////////////////////////////////////// + + @PublishedApi + internal object CheckedTypes + + @PublishedApi + internal fun CheckedTypes.new(value: Any?): Either = Either(value) + + @PublishedApi + internal inline fun checkTypes(value: Any?): CheckedTypes { + if (!(value is R).xor(value is L)) { + throw IllegalArgumentException("value(${getTypeHint(value)}) must be either L(${getTypeHint()}) or R(${getTypeHint()}), and must not be both of them.") + } + return CheckedTypes + } + + /** + * Create a [Either] whose value is [left]. + * @throws IllegalArgumentException if [left] satisfies both types [L] and [R]. + */ + @JvmName("left") + @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") + @kotlin.internal.LowPriorityInOverloadResolution + public inline operator fun invoke(left: L): Either = + checkTypes(left).new(left) + + /** + * Create a [Either] whose value is [right]. + * @throws IllegalArgumentException if [right] satisfies both types [L] and [R]. + */ + @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") + @kotlin.internal.LowPriorityInOverloadResolution + @JvmName("right") + public inline operator fun invoke(right: R): Either = + checkTypes(right).new(right) + + + /////////////////////////////////////////////////////////////////////////// + // functions + /////////////////////////////////////////////////////////////////////////// + + + public inline val Either.rightOrNull: R? get() = value.safeCast() + public inline val Either.right: R get() = value.cast() + + public inline val Either.leftOrNull: L? get() = value.safeCast() + public inline val Either.left: L get() = value.cast() + + public inline val Either.isLeft: Boolean get() = value is L + public inline val Either.isRight: Boolean get() = value is R + + + public inline fun Either.ifLeft(block: (L) -> T): T? = + this.leftOrNull?.let(block) + + public inline fun Either.ifRight(block: (R) -> T): T? = + this.rightOrNull?.let(block) + + + public inline fun Either.onLeft(block: (L) -> Unit): Either { + this.leftOrNull?.let(block) + return this + } + + public inline fun Either.onRight(block: (R) -> Unit): Either { + this.rightOrNull?.let(block) + return this + } + + + public inline fun Either.mapLeft(block: (L) -> T): Either { + @Suppress("RemoveExplicitTypeArguments") + return this.fold( + onLeft = { invoke(block(it)) }, + onRight = { invoke(right) } + ) + } + + public inline fun Either.mapRight(block: (R) -> T): Either { + @Suppress("RemoveExplicitTypeArguments") + return this.fold( + onLeft = { invoke(left) }, + onRight = { invoke(right.let(block)) } + ) + } + + + public inline fun Either.fold( + onLeft: (L) -> T, + onRight: (R) -> T, + ): T { + this.leftOrNull?.let { return onLeft(it) } + this.rightOrNull?.let { return onRight(it) } + error("value(${getTypeHint(this.value)}) is neither left(${getTypeHint()}) or right(${getTypeHint()}).") + } + + public inline fun Either.toResult(): Result = this.fold( + onLeft = { Result.failure(it) }, + onRight = { Result.success(it) } + ) + + @PublishedApi + internal fun getTypeHint(value: Any?): String { + return if (value == null) "null" + else value::class.run { simpleName ?: toString() } + } + + @PublishedApi + internal inline fun getTypeHint(): String { + return T::class.run { simpleName ?: toString() } + } + } +} diff --git a/mirai-core-utils/src/commonTest/kotlin/net/mamoe/mirai/utils/EitherTest.kt b/mirai-core-utils/src/commonTest/kotlin/net/mamoe/mirai/utils/EitherTest.kt new file mode 100644 index 000000000..e0ddf7802 --- /dev/null +++ b/mirai-core-utils/src/commonTest/kotlin/net/mamoe/mirai/utils/EitherTest.kt @@ -0,0 +1,173 @@ +/* + * 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/master/LICENSE + */ + +@file:OptIn(ExperimentalStdlibApi::class) + +package net.mamoe.mirai.utils + +import net.mamoe.mirai.utils.Either.Companion.fold +import net.mamoe.mirai.utils.Either.Companion.ifLeft +import net.mamoe.mirai.utils.Either.Companion.ifRight +import net.mamoe.mirai.utils.Either.Companion.left +import net.mamoe.mirai.utils.Either.Companion.leftOrNull +import net.mamoe.mirai.utils.Either.Companion.mapLeft +import net.mamoe.mirai.utils.Either.Companion.mapRight +import net.mamoe.mirai.utils.Either.Companion.onLeft +import net.mamoe.mirai.utils.Either.Companion.onRight +import net.mamoe.mirai.utils.Either.Companion.right +import net.mamoe.mirai.utils.Either.Companion.rightOrNull +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import kotlin.reflect.KType +import kotlin.reflect.typeOf +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertIs + +internal class EitherTest { + + @Test + fun `type check`() { + Either("") + Either(1) + + assertThrows { Either.invoke("") } + assertThrows { Either.invoke("") } + } + + @Test + fun `type variance`() { + val p: Either = Either("") // type is + assertIs(p.left) + } + + @Test + fun `get left`() { + assertEquals("", Either("").leftOrNull) + assertEquals(null, Either(1).leftOrNull) + + assertFailsWith { Either(1).left } + } + + @Test + fun `get right`() { + assertEquals(null, Either("").rightOrNull) + assertEquals(1, Either(1).rightOrNull) + + assertFailsWith { Either("").right } + } + + @Test + fun `can fold`() { + assertEquals( + true, + Either("").fold( + onLeft = { true }, + onRight = { false } + ) + ) + assertEquals( + false, + Either(1).fold( + onLeft = { true }, + onRight = { false } + ) + ) + } + + @Test + fun `can map left`() { + assertTypeIs(typeOf>(), Either("").mapLeft { true }) + + // left is not null, so block will be called + assertEquals(true, Either("").mapLeft { true }.leftOrNull) + assertEquals(null, Either("").mapLeft { true }.rightOrNull) + + // right is null, so map will also be null + assertEquals( + null, + Either(1) + .mapLeft { + throw AssertionError("should not be called") + }.leftOrNull + ) + assertEquals( + 1, + Either(1) + .mapLeft { + throw AssertionError("should not be called") + }.rightOrNull + ) + } + + @Test + fun `can map right`() { + assertTypeIs(typeOf>(), Either(1).mapRight { true }) + + // right is not null, so block will be called + assertEquals(null, Either(1).mapRight { true }.leftOrNull) + assertEquals(true, Either(1).mapRight { true }.rightOrNull) + + // right is null, so map will also be null + assertEquals( + null, + Either("") + .mapRight { + throw AssertionError("should not be called") + }.rightOrNull + ) + assertEquals( + "", + Either("") + .mapRight { + throw AssertionError("should not be called") + }.leftOrNull + ) + } + + @Test + fun `can call onRight`() { + var called = false + Either(1).onRight { called = true } + assertEquals(true, called) + + Either("").onRight { + throw AssertionError("should not be called") + } + } + + @Test + fun `can call onLeft`() { + var called = false + Either("").onLeft { called = true } + assertEquals(true, called) + + Either(1).onLeft { + throw AssertionError("should not be called") + } + } + + @Test + fun `can call ifRight`() { + assertEquals(true, Either(1).ifRight { true }) + @Suppress("IMPLICIT_NOTHING_TYPE_ARGUMENT_IN_RETURN_POSITION") + Either("").ifRight { throw AssertionError("should not be called") } + } + + @Test + fun `can call ifLeft`() { + assertEquals(true, Either("").ifLeft { true }) + @Suppress("IMPLICIT_NOTHING_TYPE_ARGUMENT_IN_RETURN_POSITION") + Either(1).ifLeft { throw AssertionError("should not be called") } + } + + private inline fun assertTypeIs(expected: KType, @Suppress("UNUSED_PARAMETER") value: V) { + assertEquals(expected, typeOf()) + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/components/PacketCodec.kt b/mirai-core/src/commonMain/kotlin/network/components/PacketCodec.kt index ba4842cf9..55ff9cfb9 100644 --- a/mirai-core/src/commonMain/kotlin/network/components/PacketCodec.kt +++ b/mirai-core/src/commonMain/kotlin/network/components/PacketCodec.kt @@ -259,10 +259,10 @@ internal class PacketCodecImpl : PacketCodec { } }.fold( onSuccess = { packet -> - IncomingPacket(input.commandName, input.sequenceId, packet, null) + IncomingPacket(input.commandName, input.sequenceId, packet) }, onFailure = { exception: Throwable -> - IncomingPacket(input.commandName, input.sequenceId, null, exception) + IncomingPacket(input.commandName, input.sequenceId, exception) } ) } diff --git a/mirai-core/src/commonMain/kotlin/network/components/PacketLoggingStrategy.kt b/mirai-core/src/commonMain/kotlin/network/components/PacketLoggingStrategy.kt index f3ff21595..189fe64c2 100644 --- a/mirai-core/src/commonMain/kotlin/network/components/PacketLoggingStrategy.kt +++ b/mirai-core/src/commonMain/kotlin/network/components/PacketLoggingStrategy.kt @@ -19,6 +19,7 @@ import net.mamoe.mirai.internal.network.ParseErrorPacket import net.mamoe.mirai.internal.network.component.ComponentKey import net.mamoe.mirai.internal.network.protocol.packet.IncomingPacket import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket +import net.mamoe.mirai.utils.Either.Companion.fold import net.mamoe.mirai.utils.MiraiLogger import net.mamoe.mirai.utils.systemProp import net.mamoe.mirai.utils.verbose @@ -44,22 +45,27 @@ internal class PacketLoggingStrategyImpl( } override fun logReceived(logger: MiraiLogger, incomingPacket: IncomingPacket) { - incomingPacket.exception?.let { - if (it is CancellationException) return - logger.error("Exception in decoding packet.", it) - return - } - val packet = incomingPacket.data ?: return - if (!bot.logger.isEnabled && !logger.isEnabled) return - if (packet is ParseErrorPacket) { - packet.direction.getLogger(bot).error("Exception in parsing packet.", packet.error) - } - if (incomingPacket.data is MultiPacket<*>) { - for (d in incomingPacket.data) { - logReceivedImpl(d, incomingPacket, logger) + incomingPacket.result.fold( + onLeft = { e -> + if (e is CancellationException) return + logger.error("Exception in decoding packet.", e) + }, + onRight = { packet -> + packet ?: return + if (!bot.logger.isEnabled && !logger.isEnabled) return + if (packet is ParseErrorPacket) { + packet.direction.getLogger(bot).error("Exception in parsing packet.", packet.error) + } + + if (packet is MultiPacket<*>) { + for (d in packet) { + logReceivedImpl(d, incomingPacket, logger) + } + } + + logReceivedImpl(packet, incomingPacket, logger) } - } - logReceivedImpl(packet, incomingPacket, logger) + ) } private fun logReceivedImpl(packet: Packet, incomingPacket: IncomingPacket, logger: MiraiLogger) { @@ -74,7 +80,7 @@ internal class PacketLoggingStrategyImpl( else -> { if (incomingPacket.commandName in blacklist) return if (SHOW_PACKET_DETAILS) { - logger.verbose { "Recv: ${incomingPacket.commandName} ${incomingPacket.data}".replaceMagicCodes() } + logger.verbose { "Recv: ${incomingPacket.commandName} ${incomingPacket.result}".replaceMagicCodes() } } else { logger.verbose { "Recv: ${incomingPacket.commandName}".replaceMagicCodes() } } diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/OutgoingPacketAndroid.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/OutgoingPacketAndroid.kt index c795d0429..c366f0999 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/OutgoingPacketAndroid.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/OutgoingPacketAndroid.kt @@ -21,6 +21,8 @@ import net.mamoe.mirai.internal.utils.io.encryptAndWrite import net.mamoe.mirai.internal.utils.io.writeHex import net.mamoe.mirai.internal.utils.io.writeIntLVPacket import net.mamoe.mirai.utils.EMPTY_BYTE_ARRAY +import net.mamoe.mirai.utils.Either +import net.mamoe.mirai.utils.Either.Companion.fold import net.mamoe.mirai.utils.KEY_16_ZEROS @kotlin.Suppress("unused") @@ -42,27 +44,29 @@ internal open class OutgoingPacket constructor( val displayName: String = if (name == null) commandName else "$commandName($name)" } -internal class IncomingPacket constructor( +internal class IncomingPacket private constructor( val commandName: String, val sequenceId: Int, - val data: Packet?, - /** - * If not `null`, [data] is `null` - */ - val exception: Throwable?, // may complete with exception (thrown by decoders) + val result: Either ) { - init { - if (exception != null) require(data == null) { "When exception is not null, data must be null." } - if (data != null) require(exception == null) { "When data is not null, exception must be null." } + companion object { + operator fun invoke(commandName: String, sequenceId: Int, data: Packet?) = + IncomingPacket(commandName, sequenceId, Either(data)) + + operator fun invoke(commandName: String, sequenceId: Int, throwable: Throwable) = + IncomingPacket(commandName, sequenceId, Either(throwable)) } override fun toString(): String { - return if (exception == null) { - "IncomingPacket(cmd=$commandName, seq=$sequenceId, SUCCESS, r=$data)" - } else { - "IncomingPacket(cmd=$commandName, seq=$sequenceId, FAILURE, e=$exception)" - } + return result.fold( + onLeft = { + "IncomingPacket(cmd=$commandName, seq=$sequenceId, FAILURE, e=$it)" + }, + onRight = { + "IncomingPacket(cmd=$commandName, seq=$sequenceId, SUCCESS, r=$it)" + } + ) } }