Add Either

This commit is contained in:
Him188 2021-06-25 19:38:42 +08:00
parent 32ac910538
commit 11ffb324c9
5 changed files with 355 additions and 32 deletions

View File

@ -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<out L : Any, out R : Any?> 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 <L : Any, R> CheckedTypes.new(value: Any?): Either<L, R> = Either(value)
@PublishedApi
internal inline fun <reified L, reified R> checkTypes(value: Any?): CheckedTypes {
if (!(value is R).xor(value is L)) {
throw IllegalArgumentException("value(${getTypeHint(value)}) must be either L(${getTypeHint<L>()}) or R(${getTypeHint<R>()}), 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 <reified L : Any, reified R> invoke(left: L): Either<L, R> =
checkTypes<L, R>(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 <reified L : Any, reified R> invoke(right: R): Either<L, R> =
checkTypes<L, R>(right).new(right)
///////////////////////////////////////////////////////////////////////////
// functions
///////////////////////////////////////////////////////////////////////////
public inline val <L : Any, reified R> Either<L, R>.rightOrNull: R? get() = value.safeCast()
public inline val <L : Any, reified R> Either<L, R>.right: R get() = value.cast()
public inline val <reified L : Any, R> Either<L, R>.leftOrNull: L? get() = value.safeCast()
public inline val <reified L : Any, R> Either<L, R>.left: L get() = value.cast()
public inline val <reified L : Any, R> Either<L, R>.isLeft: Boolean get() = value is L
public inline val <L : Any, reified R> Either<L, R>.isRight: Boolean get() = value is R
public inline fun <reified L : Any, reified R, T> Either<L, R>.ifLeft(block: (L) -> T): T? =
this.leftOrNull?.let(block)
public inline fun <L : Any, reified R, T> Either<L, R>.ifRight(block: (R) -> T): T? =
this.rightOrNull?.let(block)
public inline fun <reified L : Any, reified R> Either<L, R>.onLeft(block: (L) -> Unit): Either<L, R> {
this.leftOrNull?.let(block)
return this
}
public inline fun <L : Any, reified R> Either<L, R>.onRight(block: (R) -> Unit): Either<L, R> {
this.rightOrNull?.let(block)
return this
}
public inline fun <reified L : Any, reified R, reified T : Any> Either<L, R>.mapLeft(block: (L) -> T): Either<T, R> {
@Suppress("RemoveExplicitTypeArguments")
return this.fold(
onLeft = { invoke<T, R>(block(it)) },
onRight = { invoke<T, R>(right) }
)
}
public inline fun <reified L : Any, reified R, reified T : Any> Either<L, R>.mapRight(block: (R) -> T): Either<L, T> {
@Suppress("RemoveExplicitTypeArguments")
return this.fold(
onLeft = { invoke<L, T>(left) },
onRight = { invoke<L, T>(right.let(block)) }
)
}
public inline fun <reified L : Any, reified R, T> Either<L, R>.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<L>()}) or right(${getTypeHint<R>()}).")
}
public inline fun <reified T> Either<Throwable, T>.toResult(): Result<T> = 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 <reified T> getTypeHint(): String {
return T::class.run { simpleName ?: toString() }
}
}
}

View File

@ -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<CharSequence, Int>("")
Either<CharSequence, Int>(1)
assertThrows<IllegalArgumentException> { Either.invoke<CharSequence, String>("") }
assertThrows<IllegalArgumentException> { Either.invoke<String, CharSequence>("") }
}
@Test
fun `type variance`() {
val p: Either<CharSequence, Int> = Either("") // type is <String, Int>
assertIs<String>(p.left)
}
@Test
fun `get left`() {
assertEquals("", Either<CharSequence, Int>("").leftOrNull)
assertEquals(null, Either<CharSequence, Int>(1).leftOrNull)
assertFailsWith<ClassCastException> { Either<CharSequence, Int>(1).left }
}
@Test
fun `get right`() {
assertEquals(null, Either<CharSequence, Int>("").rightOrNull)
assertEquals(1, Either<CharSequence, Int>(1).rightOrNull)
assertFailsWith<ClassCastException> { Either<CharSequence, Int>("").right }
}
@Test
fun `can fold`() {
assertEquals(
true,
Either<CharSequence, Int>("").fold(
onLeft = { true },
onRight = { false }
)
)
assertEquals(
false,
Either<CharSequence, Int>(1).fold(
onLeft = { true },
onRight = { false }
)
)
}
@Test
fun `can map left`() {
assertTypeIs(typeOf<Either<Boolean, Int>>(), Either<CharSequence, Int>("").mapLeft { true })
// left is not null, so block will be called
assertEquals(true, Either<CharSequence, Int>("").mapLeft { true }.leftOrNull)
assertEquals(null, Either<CharSequence, Int>("").mapLeft { true }.rightOrNull)
// right is null, so map will also be null
assertEquals(
null,
Either<CharSequence, Int>(1)
.mapLeft<CharSequence, Int, Boolean> {
throw AssertionError("should not be called")
}.leftOrNull
)
assertEquals(
1,
Either<CharSequence, Int>(1)
.mapLeft<CharSequence, Int, Boolean> {
throw AssertionError("should not be called")
}.rightOrNull
)
}
@Test
fun `can map right`() {
assertTypeIs(typeOf<Either<CharSequence, Boolean>>(), Either<CharSequence, Int>(1).mapRight { true })
// right is not null, so block will be called
assertEquals(null, Either<CharSequence, Int>(1).mapRight { true }.leftOrNull)
assertEquals(true, Either<CharSequence, Int>(1).mapRight { true }.rightOrNull)
// right is null, so map will also be null
assertEquals(
null,
Either<CharSequence, Int>("")
.mapRight<CharSequence, Int, Boolean> {
throw AssertionError("should not be called")
}.rightOrNull
)
assertEquals(
"",
Either<CharSequence, Int>("")
.mapRight<CharSequence, Int, Boolean> {
throw AssertionError("should not be called")
}.leftOrNull
)
}
@Test
fun `can call onRight`() {
var called = false
Either<CharSequence, Int>(1).onRight { called = true }
assertEquals(true, called)
Either<CharSequence, Int>("").onRight {
throw AssertionError("should not be called")
}
}
@Test
fun `can call onLeft`() {
var called = false
Either<CharSequence, Int>("").onLeft { called = true }
assertEquals(true, called)
Either<CharSequence, Int>(1).onLeft {
throw AssertionError("should not be called")
}
}
@Test
fun `can call ifRight`() {
assertEquals(true, Either<CharSequence, Int>(1).ifRight { true })
@Suppress("IMPLICIT_NOTHING_TYPE_ARGUMENT_IN_RETURN_POSITION")
Either<CharSequence, Int>("").ifRight { throw AssertionError("should not be called") }
}
@Test
fun `can call ifLeft`() {
assertEquals(true, Either<CharSequence, Int>("").ifLeft { true })
@Suppress("IMPLICIT_NOTHING_TYPE_ARGUMENT_IN_RETURN_POSITION")
Either<CharSequence, Int>(1).ifLeft { throw AssertionError("should not be called") }
}
private inline fun <reified V> assertTypeIs(expected: KType, @Suppress("UNUSED_PARAMETER") value: V) {
assertEquals(expected, typeOf<V>())
}
}

View File

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

View File

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

View File

@ -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<Throwable, Packet?>
) {
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)"
}
)
}
}