mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-27 00:30:17 +08:00
parent
448acef198
commit
0f03a10d0c
@ -23,6 +23,9 @@ import net.mamoe.mirai.contact.*
|
|||||||
import net.mamoe.mirai.message.data.Image
|
import net.mamoe.mirai.message.data.Image
|
||||||
import net.mamoe.mirai.message.data.MessageContent
|
import net.mamoe.mirai.message.data.MessageContent
|
||||||
import net.mamoe.mirai.message.data.PlainText
|
import net.mamoe.mirai.message.data.PlainText
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
import kotlin.collections.HashSet
|
||||||
import kotlin.contracts.InvocationKind.EXACTLY_ONCE
|
import kotlin.contracts.InvocationKind.EXACTLY_ONCE
|
||||||
import kotlin.contracts.contract
|
import kotlin.contracts.contract
|
||||||
import kotlin.internal.LowPriorityInOverloadResolution
|
import kotlin.internal.LowPriorityInOverloadResolution
|
||||||
@ -75,34 +78,53 @@ public interface CommandArgumentContext {
|
|||||||
public val EMPTY: CommandArgumentContext = EmptyCommandArgumentContext
|
public val EMPTY: CommandArgumentContext = EmptyCommandArgumentContext
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private object EnumCommandArgumentContext : CommandArgumentContext {
|
||||||
|
private val cache = WeakHashMap<Class<*>, CommandValueArgumentParser<*>>()
|
||||||
|
private val enumKlass = Enum::class
|
||||||
|
override fun <T : Any> get(kClass: KClass<T>): CommandValueArgumentParser<T>? {
|
||||||
|
return if (kClass.isSubclassOf(enumKlass)) {
|
||||||
|
val jclass = kClass.java.asSubclass(Enum::class.java)
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
(cache[jclass] ?: kotlin.run {
|
||||||
|
EnumValueArgumentParser(jclass).also { cache[jclass] = it }
|
||||||
|
}) as CommandValueArgumentParser<T>
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toList(): List<ParserPair<*>> = emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 内建的默认 [CommandValueArgumentParser]
|
* 内建的默认 [CommandValueArgumentParser]
|
||||||
*/
|
*/
|
||||||
public object Builtins : CommandArgumentContext by (buildCommandArgumentContext {
|
public object Builtins : CommandArgumentContext by listOf(
|
||||||
Int::class with IntValueArgumentParser
|
EnumCommandArgumentContext,
|
||||||
Byte::class with ByteValueArgumentParser
|
buildCommandArgumentContext {
|
||||||
Short::class with ShortValueArgumentParser
|
Int::class with IntValueArgumentParser
|
||||||
Boolean::class with BooleanValueArgumentParser
|
Byte::class with ByteValueArgumentParser
|
||||||
String::class with StringValueArgumentParser
|
Short::class with ShortValueArgumentParser
|
||||||
Long::class with LongValueArgumentParser
|
Boolean::class with BooleanValueArgumentParser
|
||||||
Double::class with DoubleValueArgumentParser
|
String::class with StringValueArgumentParser
|
||||||
Float::class with FloatValueArgumentParser
|
Long::class with LongValueArgumentParser
|
||||||
|
Double::class with DoubleValueArgumentParser
|
||||||
|
Float::class with FloatValueArgumentParser
|
||||||
|
|
||||||
Image::class with ImageValueArgumentParser
|
Image::class with ImageValueArgumentParser
|
||||||
PlainText::class with PlainTextValueArgumentParser
|
PlainText::class with PlainTextValueArgumentParser
|
||||||
|
|
||||||
Contact::class with ExistingContactValueArgumentParser
|
Contact::class with ExistingContactValueArgumentParser
|
||||||
User::class with ExistingUserValueArgumentParser
|
User::class with ExistingUserValueArgumentParser
|
||||||
Member::class with ExistingMemberValueArgumentParser
|
Member::class with ExistingMemberValueArgumentParser
|
||||||
Group::class with ExistingGroupValueArgumentParser
|
Group::class with ExistingGroupValueArgumentParser
|
||||||
Friend::class with ExistingFriendValueArgumentParser
|
Friend::class with ExistingFriendValueArgumentParser
|
||||||
Bot::class with ExistingBotValueArgumentParser
|
Bot::class with ExistingBotValueArgumentParser
|
||||||
|
|
||||||
PermissionId::class with PermissionIdValueArgumentParser
|
PermissionId::class with PermissionIdValueArgumentParser
|
||||||
PermitteeId::class with PermitteeIdValueArgumentParser
|
PermitteeId::class with PermitteeIdValueArgumentParser
|
||||||
|
|
||||||
MessageContent::class with RawContentValueArgumentParser
|
MessageContent::class with RawContentValueArgumentParser
|
||||||
})
|
},
|
||||||
|
).fold(EmptyCommandArgumentContext, CommandArgumentContext::plus)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -127,7 +149,7 @@ public object EmptyCommandArgumentContext : CommandArgumentContext by SimpleComm
|
|||||||
* 合并两个 [buildCommandArgumentContext], [replacer] 将会替换 [this] 中重复的 parser.
|
* 合并两个 [buildCommandArgumentContext], [replacer] 将会替换 [this] 中重复的 parser.
|
||||||
*/
|
*/
|
||||||
public operator fun CommandArgumentContext.plus(replacer: CommandArgumentContext): CommandArgumentContext {
|
public operator fun CommandArgumentContext.plus(replacer: CommandArgumentContext): CommandArgumentContext {
|
||||||
if (replacer == EmptyCommandArgumentContext) return this
|
if (replacer === EmptyCommandArgumentContext) return this
|
||||||
if (this == EmptyCommandArgumentContext) return replacer
|
if (this == EmptyCommandArgumentContext) return replacer
|
||||||
return object : CommandArgumentContext {
|
return object : CommandArgumentContext {
|
||||||
override fun <T : Any> get(kClass: KClass<T>): CommandValueArgumentParser<T>? =
|
override fun <T : Any> get(kClass: KClass<T>): CommandValueArgumentParser<T>? =
|
||||||
@ -142,7 +164,7 @@ public operator fun CommandArgumentContext.plus(replacer: CommandArgumentContext
|
|||||||
*/
|
*/
|
||||||
public operator fun CommandArgumentContext.plus(replacer: List<ParserPair<*>>): CommandArgumentContext {
|
public operator fun CommandArgumentContext.plus(replacer: List<ParserPair<*>>): CommandArgumentContext {
|
||||||
if (replacer.isEmpty()) return this
|
if (replacer.isEmpty()) return this
|
||||||
if (this == EmptyCommandArgumentContext) return SimpleCommandArgumentContext(replacer)
|
if (this === EmptyCommandArgumentContext) return SimpleCommandArgumentContext(replacer)
|
||||||
return object : CommandArgumentContext {
|
return object : CommandArgumentContext {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T : Any> get(kClass: KClass<T>): CommandValueArgumentParser<T>? =
|
override fun <T : Any> get(kClass: KClass<T>): CommandValueArgumentParser<T>? =
|
||||||
|
@ -364,6 +364,102 @@ public object RawContentValueArgumentParser : CommandValueArgumentParser<Message
|
|||||||
override fun parse(raw: MessageContent, sender: CommandSender): MessageContent = raw
|
override fun parse(raw: MessageContent, sender: CommandSender): MessageContent = raw
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析参数为枚举 [T]
|
||||||
|
*
|
||||||
|
* 注:
|
||||||
|
* - 当枚举值大小写无冲突时会尝试忽略大小写
|
||||||
|
* - 当大小写驼峰可用时会尝试使用大小写驼峰
|
||||||
|
*
|
||||||
|
* 例如:
|
||||||
|
* ```
|
||||||
|
* enum class StdType { STD_IN, STD_OUT, STD_ERR }
|
||||||
|
* ```
|
||||||
|
* 对于 StdType 有以下值可用:
|
||||||
|
* - `STD_IN`, `STD_OUT`, `STD_ERR` (忽视大小写)
|
||||||
|
* - `stdIn`, `stdOut`, `stdErr` (不忽视大小写)
|
||||||
|
*
|
||||||
|
* @since 2.1.0
|
||||||
|
*/
|
||||||
|
public class EnumValueArgumentParser<T : Enum<T>>(
|
||||||
|
private val type: Class<T>,
|
||||||
|
) : InternalCommandValueArgumentParserExtensions<T>() {
|
||||||
|
// 此 Exception 仅用于中断 enum 搜索, 不需要使用堆栈信息
|
||||||
|
private object NoEnumException : RuntimeException()
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
check(Enum::class.java.isAssignableFrom(type)) {
|
||||||
|
"$type not a enum class"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> Sequence<T>.hasDuplicates(): Boolean = iterator().hasDuplicates()
|
||||||
|
private fun <T> Iterator<T>.hasDuplicates(): Boolean {
|
||||||
|
val observed = HashSet<T>()
|
||||||
|
for (elem in this) {
|
||||||
|
if (!observed.add(elem))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
|
private inline fun noConstant(): Nothing {
|
||||||
|
throw NoEnumException
|
||||||
|
}
|
||||||
|
|
||||||
|
private val delegate: (String) -> T = kotlin.run {
|
||||||
|
val enums = type.enumConstants.asSequence()
|
||||||
|
// step 1: 分析是否能够忽略大小写
|
||||||
|
if (enums.map { it.name.toLowerCase() }.hasDuplicates()) {
|
||||||
|
({ java.lang.Enum.valueOf(type, it) })
|
||||||
|
} else { // step 2: 分析是否能使用小驼峰命名
|
||||||
|
val lowerCaseEnumDirection = enums.map { it.name.toLowerCase() to it }.toList().toMap()
|
||||||
|
|
||||||
|
val camelCase = enums.mapNotNull { elm ->
|
||||||
|
val name = elm.name.split('_')
|
||||||
|
if (name.size == 1) { // No splitter
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
buildString {
|
||||||
|
val iterator = name.iterator()
|
||||||
|
append(iterator.next().toLowerCase())
|
||||||
|
for (v in iterator) {
|
||||||
|
if (v.isEmpty()) continue
|
||||||
|
append(v[0].toUpperCase())
|
||||||
|
append(v.substring(1, v.length).toLowerCase())
|
||||||
|
}
|
||||||
|
} to elm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val camelCaseDirection = if ((
|
||||||
|
enums.map { it.name.toLowerCase() } + camelCase.map { it.first.toLowerCase() }
|
||||||
|
).hasDuplicates()
|
||||||
|
) { // 确认驼峰命名与源没有冲突
|
||||||
|
emptyMap()
|
||||||
|
} else {
|
||||||
|
camelCase.toList().toMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
({
|
||||||
|
camelCaseDirection[it]
|
||||||
|
?: lowerCaseEnumDirection[it.toLowerCase()]
|
||||||
|
?: noConstant()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun parse(raw: String, sender: CommandSender): T {
|
||||||
|
return try {
|
||||||
|
delegate(raw)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
illegalArgument("无法解析 $raw 为 ${type.simpleName}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal abstract class InternalCommandValueArgumentParserExtensions<T : Any> : AbstractCommandValueArgumentParser<T>() {
|
internal abstract class InternalCommandValueArgumentParserExtensions<T : Any> : AbstractCommandValueArgumentParser<T>() {
|
||||||
private fun String.parseToLongOrFail(): Long = toLongOrNull() ?: illegalArgument("无法解析 $this 为整数")
|
private fun String.parseToLongOrFail(): Long = toLongOrNull() ?: illegalArgument("无法解析 $this 为整数")
|
||||||
|
|
||||||
|
@ -64,9 +64,38 @@ object TestSimpleCommand : RawCommand(owner, "testSimple", "tsS") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("EnumEntryName")
|
||||||
|
object TestEnumArgCommand : CompositeCommand(owner, "testenum") {
|
||||||
|
enum class TestEnum {
|
||||||
|
V1, V2, V3
|
||||||
|
}
|
||||||
|
enum class TestCase {
|
||||||
|
A, a
|
||||||
|
}
|
||||||
|
enum class TestCamelCase {
|
||||||
|
A, B, A_B
|
||||||
|
}
|
||||||
|
|
||||||
|
@SubCommand("tcc")
|
||||||
|
fun CommandSender.testCamelCase(enum: TestCamelCase) {
|
||||||
|
Testing.ok(enum)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SubCommand("tc")
|
||||||
|
fun CommandSender.testCase(enum: TestCase) {
|
||||||
|
Testing.ok(enum)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SubCommand
|
||||||
|
fun CommandSender.e1(enum: TestEnum) {
|
||||||
|
Testing.ok(enum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal val sender by lazy { ConsoleCommandSender }
|
internal val sender by lazy { ConsoleCommandSender }
|
||||||
|
|
||||||
internal object TestUnitCommandOwner : CommandOwner by ConsoleCommandOwner
|
internal object TestUnitCommandOwner : CommandOwner by ConsoleCommandOwner
|
||||||
|
|
||||||
internal val owner by lazy { TestUnitCommandOwner }
|
internal val owner by lazy { TestUnitCommandOwner }
|
||||||
|
|
||||||
|
|
||||||
@ -137,6 +166,75 @@ internal class TestCommand {
|
|||||||
assertEquals(2, result.size)
|
assertEquals(2, result.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test enum argument`() = runBlocking {
|
||||||
|
TestEnumArgCommand.withRegistration {
|
||||||
|
|
||||||
|
assertEquals(TestEnumArgCommand.TestEnum.V1, withTesting {
|
||||||
|
assertSuccess(TestEnumArgCommand.execute(sender, PlainText("e1"), PlainText("V1")))
|
||||||
|
})
|
||||||
|
assertEquals(TestEnumArgCommand.TestEnum.V2, withTesting {
|
||||||
|
assertSuccess(TestEnumArgCommand.execute(sender, PlainText("e1"), PlainText("V2")))
|
||||||
|
})
|
||||||
|
assertEquals(TestEnumArgCommand.TestEnum.V3, withTesting {
|
||||||
|
assertSuccess(TestEnumArgCommand.execute(sender, PlainText("e1"), PlainText("V3")))
|
||||||
|
})
|
||||||
|
withTesting<Unit> {
|
||||||
|
assertFailure(TestEnumArgCommand.execute(sender, PlainText("e1"), PlainText("ENUM_NOT_FOUND")))
|
||||||
|
Testing.ok(Unit)
|
||||||
|
}
|
||||||
|
assertEquals(TestEnumArgCommand.TestEnum.V1, withTesting {
|
||||||
|
assertSuccess(TestEnumArgCommand.execute(sender, PlainText("e1"), PlainText("v1")))
|
||||||
|
})
|
||||||
|
assertEquals(TestEnumArgCommand.TestEnum.V2, withTesting {
|
||||||
|
assertSuccess(TestEnumArgCommand.execute(sender, PlainText("e1"), PlainText("v2")))
|
||||||
|
})
|
||||||
|
assertEquals(TestEnumArgCommand.TestEnum.V3, withTesting {
|
||||||
|
assertSuccess(TestEnumArgCommand.execute(sender, PlainText("e1"), PlainText("v3")))
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
assertEquals(TestEnumArgCommand.TestCase.A, withTesting {
|
||||||
|
assertSuccess(TestEnumArgCommand.execute(sender, PlainText("tc"), PlainText("A")))
|
||||||
|
})
|
||||||
|
assertEquals(TestEnumArgCommand.TestCase.a, withTesting {
|
||||||
|
assertSuccess(TestEnumArgCommand.execute(sender, PlainText("tc"), PlainText("a")))
|
||||||
|
})
|
||||||
|
withTesting<Unit> {
|
||||||
|
assertFailure(TestEnumArgCommand.execute(sender, PlainText("tc"), PlainText("ENUM_NOT_FOUND")))
|
||||||
|
Testing.ok(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
assertEquals(TestEnumArgCommand.TestCamelCase.A, withTesting {
|
||||||
|
assertSuccess(TestEnumArgCommand.execute(sender, PlainText("tcc"), PlainText("A")))
|
||||||
|
})
|
||||||
|
assertEquals(TestEnumArgCommand.TestCamelCase.A, withTesting {
|
||||||
|
assertSuccess(TestEnumArgCommand.execute(sender, PlainText("tcc"), PlainText("a")))
|
||||||
|
})
|
||||||
|
assertEquals(TestEnumArgCommand.TestCamelCase.B, withTesting {
|
||||||
|
assertSuccess(TestEnumArgCommand.execute(sender, PlainText("tcc"), PlainText("B")))
|
||||||
|
})
|
||||||
|
assertEquals(TestEnumArgCommand.TestCamelCase.B, withTesting {
|
||||||
|
assertSuccess(TestEnumArgCommand.execute(sender, PlainText("tcc"), PlainText("b")))
|
||||||
|
})
|
||||||
|
assertEquals(TestEnumArgCommand.TestCamelCase.A_B, withTesting {
|
||||||
|
assertSuccess(TestEnumArgCommand.execute(sender, PlainText("tcc"), PlainText("A_B")))
|
||||||
|
})
|
||||||
|
assertEquals(TestEnumArgCommand.TestCamelCase.A_B, withTesting {
|
||||||
|
assertSuccess(TestEnumArgCommand.execute(sender, PlainText("tcc"), PlainText("a_b")))
|
||||||
|
})
|
||||||
|
assertEquals(TestEnumArgCommand.TestCamelCase.A_B, withTesting {
|
||||||
|
assertSuccess(TestEnumArgCommand.execute(sender, PlainText("tcc"), PlainText("aB")))
|
||||||
|
})
|
||||||
|
withTesting<Unit> {
|
||||||
|
assertFailure(TestEnumArgCommand.execute(sender, PlainText("tc"), PlainText("ENUM_NOT_FOUND")))
|
||||||
|
Testing.ok(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testSimpleArgsSplitting() = runBlocking {
|
fun testSimpleArgsSplitting() = runBlocking {
|
||||||
TestSimpleCommand.withRegistration {
|
TestSimpleCommand.withRegistration {
|
||||||
@ -362,3 +460,10 @@ internal fun assertSuccess(result: CommandExecuteResult) {
|
|||||||
throw result.exception ?: AssertionError(result.toString())
|
throw result.exception ?: AssertionError(result.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCommandDescriptors::class)
|
||||||
|
internal fun assertFailure(result: CommandExecuteResult) {
|
||||||
|
if (!result.isFailure()) {
|
||||||
|
throw AssertionError("$result not a failure")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user