mirror of
https://github.com/mamoe/mirai.git
synced 2025-02-11 09:58:03 +08:00
Add Either
This commit is contained in:
parent
32ac910538
commit
11ffb324c9
140
mirai-core-utils/src/commonMain/kotlin/Either.kt
Normal file
140
mirai-core-utils/src/commonMain/kotlin/Either.kt
Normal 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() }
|
||||
}
|
||||
}
|
||||
}
|
@ -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>())
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -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() }
|
||||
}
|
||||
|
@ -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)"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user