Change SubCommandDescriptor calling from callSuspend to callSuspendBy

- Support optional argument now.
This commit is contained in:
Karlatemp 2020-09-19 15:56:57 +08:00
parent 48f5c947b6
commit 5f6873e347
No known key found for this signature in database
GPG Key ID: 21FBDDF664FF06F8
5 changed files with 116 additions and 48 deletions

View File

@ -20,6 +20,7 @@ import net.mamoe.mirai.console.permission.PermissionId
import net.mamoe.mirai.console.permission.PermitteeId
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.message.data.MessageContent
import kotlin.internal.LowPriorityInOverloadResolution
import kotlin.reflect.KClass
import kotlin.reflect.full.isSubclassOf
@ -84,6 +85,8 @@ public interface CommandArgumentContext {
PermissionId::class with PermissionIdArgumentParser
PermitteeId::class with PermitteeIdArgumentParser
MessageContent::class with RawContentArgumentParser
})
}

View File

@ -19,10 +19,7 @@ import net.mamoe.mirai.console.permission.PermitteeId
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.getFriendOrNull
import net.mamoe.mirai.getGroupOrNull
import net.mamoe.mirai.message.data.At
import net.mamoe.mirai.message.data.MessageContent
import net.mamoe.mirai.message.data.SingleMessage
import net.mamoe.mirai.message.data.content
import net.mamoe.mirai.message.data.*
/**
@ -331,6 +328,12 @@ public object PermitteeIdArgumentParser : CommandArgumentParser<PermitteeId> {
}
}
/** 直接返回原始参数 [MessageContent] */
public object RawContentArgumentParser : CommandArgumentParser<MessageContent> {
override fun parse(raw: String, sender: CommandSender): MessageContent = PlainText(raw)
override fun parse(raw: MessageContent, sender: CommandSender): MessageContent = raw
}
internal interface InternalCommandArgumentParserExtensions<T : Any> : CommandArgumentParser<T> {
fun String.parseToLongOrFail(): Long = toLongOrNull() ?: illegalArgument("无法解析 $this 为整数")

View File

@ -14,20 +14,25 @@ package net.mamoe.mirai.console.internal.command
import net.mamoe.mirai.console.command.CompositeCommand
import net.mamoe.mirai.console.command.description.CommandArgumentParser
import java.lang.reflect.Parameter
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
import kotlin.reflect.KClass
import kotlin.reflect.KParameter
/*
internal fun Parameter.toCommandParam(): CommandParameter<*> {
val name = getAnnotation(CompositeCommand.Name::class.java)
return CommandParameter(
name?.value ?: this.name
?: throw IllegalArgumentException("Cannot construct CommandParam from a unnamed param"),
this.type.kotlin
this.type.kotlin,
null
)
}
*/
/**
* 指令形式参数.
* @see toCommandParam
*/
internal data class CommandParameter<T : Any>(
/**
@ -37,9 +42,12 @@ internal data class CommandParameter<T : Any>(
/**
* 参数类型. 将从 [CompositeCommand.context] 中寻找 [CommandArgumentParser] 解析.
*/
val type: KClass<T> // exact type
val type: KClass<T>, // exact type
val parameter: KParameter, // source parameter
) {
constructor(name: String, type: KClass<T>, parameter: KParameter, parser: CommandArgumentParser<T>) : this(
name, type, parameter
) {
constructor(name: String, type: KClass<T>, parser: CommandArgumentParser<T>) : this(name, type) {
this._overrideParser = parser
}

View File

@ -21,7 +21,8 @@ import net.mamoe.mirai.message.data.*
import kotlin.reflect.KAnnotatedElement
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.full.callSuspend
import kotlin.reflect.KParameter
import kotlin.reflect.full.callSuspendBy
import kotlin.reflect.full.declaredFunctions
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.isSubclassOf
@ -131,8 +132,9 @@ internal abstract class AbstractReflectionCommand
val params: Array<CommandParameter<*>>,
val description: String,
val permission: Permission,
val onCommand: suspend (sender: CommandSender, parsedArgs: Array<out Any>) -> Boolean,
val onCommand: suspend (sender: CommandSender, parsedArgs: Map<KParameter, Any?>) -> Boolean,
val context: CommandArgumentContext,
val argumentBuilder: (sender: CommandSender) -> MutableMap<KParameter, Any?>,
) {
val usage: String = createUsage(this@AbstractReflectionCommand)
@ -151,24 +153,43 @@ internal abstract class AbstractReflectionCommand
}
}
private fun KParameter.isOptional(): Boolean {
return isOptional || this.type.isMarkedNullable
}
val minimalArgumentsSize = params.count {
!it.parameter.isOptional()
}
@JvmField
internal val bakedSubNames: Array<Array<String>> = names.map { it.bakeSubName() }.toTypedArray()
private fun parseArgs(sender: CommandSender, rawArgs: MessageChain, offset: Int): Array<out Any>? {
if (rawArgs.size < offset + this.params.size)
private fun parseArgs(sender: CommandSender, rawArgs: MessageChain, offset: Int): MutableMap<KParameter, Any?>? {
if (rawArgs.size < offset + minimalArgumentsSize)
return null
//require(rawArgs.size >= offset + this.params.size) { "No enough args. Required ${params.size}, but given ${rawArgs.size - offset}" }
return Array(this.params.size) { index ->
val param = params[index]
val rawArg = rawArgs[offset + index]
when (rawArg) {
is PlainText -> context[param.type]?.parse(rawArg.content, sender)
is MessageContent -> context[param.type]?.parse(rawArg, sender)
return argumentBuilder(sender).also { result ->
params.forEachIndexed { index, parameter ->
val rawArg = rawArgs.getOrNull(offset + index)
result[parameter.parameter] = when (rawArg) {
null -> {
val p = parameter.parameter
when {
p.isOptional -> return@forEachIndexed
p.type.isMarkedNullable -> {
result[parameter.parameter] = null
return@forEachIndexed
}
else -> null
}
}
is PlainText -> context[parameter.type]?.parse(rawArg.content, sender)
is MessageContent -> context[parameter.type]?.parse(rawArg, sender)
else -> throw IllegalArgumentException("Illegal Message kind: ${rawArg.kClassQualifiedNameOrTip}")
} ?: error("Cannot find a parser for $rawArg")
}
}
}
}
/**
* @param rawArgs 元素类型必须为 [SingleMessage] [String], 且已经经过扁平化处理. 否则抛出异常 [IllegalArgumentException]
@ -250,6 +271,10 @@ internal fun AbstractReflectionCommand.SubCommandDescriptor.createUsage(baseComm
appendLine()
}.trimEnd()
internal fun <T1, R1, R2> ((T1) -> R1).then(then: (T1, R1) -> R2): ((T1) -> R2) {
return { a -> then.invoke(a, (this@then(a))) }
}
internal fun AbstractReflectionCommand.createSubCommand(
function: KFunction<*>,
context: CommandArgumentContext,
@ -273,12 +298,17 @@ internal fun AbstractReflectionCommand.createSubCommand(
check(!function.returnType.isMarkedNullable) {
error("Return type of sub command ${function.name} must not be marked nullable in Kotlin, and must be marked with @NotNull or @NonNull explicitly in Java. (at ${this::class.qualifiedNameOrTip}.${function.name})")
}
var argumentBuilder: (sender: CommandSender) -> MutableMap<KParameter, Any?> = { HashMap() }
val parameters = function.parameters.toMutableList()
if (notStatic) parameters.removeAt(0) // instance
if (notStatic) {
val type = parameters.removeAt(0) // instance
argumentBuilder = argumentBuilder.then { _, map ->
map[type] = this@createSubCommand
map
}
}
var hasSenderParam = false
check(parameters.isNotEmpty()) {
"Parameters of sub command ${function.name} must not be empty. (Must have CommandSender as its receiver or first parameter or absent, followed by naturally typed params) (at ${this::class.qualifiedNameOrTip}.${function.name})"
}
@ -291,8 +321,11 @@ internal fun AbstractReflectionCommand.createSubCommand(
(parameters.first()).let { receiver ->
if ((receiver.type.classifier as? KClass<*>)?.isSubclassOf(CommandSender::class) == true) {
hasSenderParam = true
parameters.removeAt(0)
val senderType = parameters.removeAt(0)
argumentBuilder = argumentBuilder.then { sender, map ->
map[senderType] = sender
map
}
}
}
@ -313,37 +346,32 @@ internal fun AbstractReflectionCommand.createSubCommand(
//map parameter
val params = parameters.map { param ->
if (param.isOptional) error("optional parameters are not yet supported. (at ${this::class.qualifiedNameOrTip}.${function.name}.$param)")
// if (param.isOptional) error("optional parameters are not yet supported. (at ${this::class.qualifiedNameOrTip}.${function.name}.$param)")
val paramName = param.findAnnotation<CompositeCommand.Name>()?.value ?: param.name ?: "unknown"
CommandParameter(
paramName,
(param.type.classifier as? KClass<*>)
?: throw IllegalArgumentException("unsolved type reference from param " + param.name + ". (at ${this::class.qualifiedNameOrTip}.${function.name}.$param)")
?: throw IllegalArgumentException("unsolved type reference from param " + param.name + ". (at ${this::class.qualifiedNameOrTip}.${function.name}.$param)"),
param
)
}.toTypedArray()
// TODO: 2020/09/19 检查 optional/nullable 是否都在最后
return SubCommandDescriptor(
commandName,
params,
subDescription, // overridePermission?.value
permission,//overridePermission?.value?.let { PermissionService.INSTANCE[PermissionId.parseFromString(it)] } ?: permission,
onCommand = { sender: CommandSender, args: Array<out Any> ->
val result = if (notStatic) {
if (hasSenderParam) {
function.isSuspend
function.callSuspend(this, sender, *args)
} else function.callSuspend(this, *args)
} else {
if (hasSenderParam) {
function.callSuspend(sender, *args)
} else function.callSuspend(*args)
}
onCommand = { sender: CommandSender, args: Map<KParameter, Any?> ->
val result = function.callSuspendBy(args)
checkNotNull(result) { "sub command return value is null (at ${this::class.qualifiedName}.${function.name})" }
result as? Boolean ?: true // Unit, void is considered as true.
},
context = context
context = context,
argumentBuilder = argumentBuilder
)
}

View File

@ -238,8 +238,34 @@ internal class TestCommand {
}
}
}
@Test
fun `test optional argument command`() {
runBlocking {
val optionCommand = object : CompositeCommand(
ConsoleCommandOwner,
"testOptional"
) {
@SubCommand
fun optional(arg1: String, arg2: String = "Here is optional", arg3: String?) {
println(arg1)
println(arg2)
println(arg3)
// println(arg3)
Testing.ok(Unit)
}
}
optionCommand.withRegistration {
withTesting<Unit> {
assertSuccess(sender.executeCommand("/testOptional optional 1"))
}
}
}
}
}
internal fun assertSuccess(result: CommandExecuteResult) {
assertTrue(result.isSuccess(), result.toString())
if (result.isFailure()) {
throw result.exception ?: AssertionError(result.toString())
}
}