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.permission.PermitteeId
import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.contact.* import net.mamoe.mirai.contact.*
import net.mamoe.mirai.message.data.MessageContent
import kotlin.internal.LowPriorityInOverloadResolution import kotlin.internal.LowPriorityInOverloadResolution
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.full.isSubclassOf import kotlin.reflect.full.isSubclassOf
@ -84,6 +85,8 @@ public interface CommandArgumentContext {
PermissionId::class with PermissionIdArgumentParser PermissionId::class with PermissionIdArgumentParser
PermitteeId::class with PermitteeIdArgumentParser 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.contact.*
import net.mamoe.mirai.getFriendOrNull import net.mamoe.mirai.getFriendOrNull
import net.mamoe.mirai.getGroupOrNull import net.mamoe.mirai.getGroupOrNull
import net.mamoe.mirai.message.data.At import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.message.data.MessageContent
import net.mamoe.mirai.message.data.SingleMessage
import net.mamoe.mirai.message.data.content
/** /**
@ -86,9 +83,9 @@ public object StringArgumentParser : InternalCommandArgumentParserExtensions<Str
public object BooleanArgumentParser : InternalCommandArgumentParserExtensions<Boolean> { public object BooleanArgumentParser : InternalCommandArgumentParserExtensions<Boolean> {
public override fun parse(raw: String, sender: CommandSender): Boolean = raw.trim().let { str -> public override fun parse(raw: String, sender: CommandSender): Boolean = raw.trim().let { str ->
str.equals("true", ignoreCase = true) str.equals("true", ignoreCase = true)
|| str.equals("yes", ignoreCase = true) || str.equals("yes", ignoreCase = true)
|| str.equals("enabled", ignoreCase = true) || str.equals("enabled", ignoreCase = true)
|| str.equals("on", ignoreCase = true) || str.equals("on", ignoreCase = true)
} }
} }
@ -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> { internal interface InternalCommandArgumentParserExtensions<T : Any> : CommandArgumentParser<T> {
fun String.parseToLongOrFail(): Long = toLongOrNull() ?: illegalArgument("无法解析 $this 为整数") fun String.parseToLongOrFail(): Long = toLongOrNull() ?: illegalArgument("无法解析 $this 为整数")
@ -365,10 +368,10 @@ internal interface InternalCommandArgumentParserExtensions<T : Any> : CommandArg
} else { } else {
var index = 1 var index = 1
illegalArgument("无法找到成员 $idOrCard。 多个成员满足搜索结果或匹配度不足: \n\n" + illegalArgument("无法找到成员 $idOrCard。 多个成员满足搜索结果或匹配度不足: \n\n" +
candidates.joinToString("\n", limit = 6) { candidates.joinToString("\n", limit = 6) {
val percentage = (it.second * 100).toDecimalPlace(0) val percentage = (it.second * 100).toDecimalPlace(0)
"#${index++}(${percentage}%)${it.first.nameCardOrNick.truncate(10)}(${it.first.id})" // #1 15.4% "#${index++}(${percentage}%)${it.first.nameCardOrNick.truncate(10)}(${it.first.id})" // #1 15.4%
} }
) )
} }
} }

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

View File

@ -21,7 +21,8 @@ import net.mamoe.mirai.message.data.*
import kotlin.reflect.KAnnotatedElement import kotlin.reflect.KAnnotatedElement
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KFunction 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.declaredFunctions
import kotlin.reflect.full.findAnnotation import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.isSubclassOf import kotlin.reflect.full.isSubclassOf
@ -131,8 +132,9 @@ internal abstract class AbstractReflectionCommand
val params: Array<CommandParameter<*>>, val params: Array<CommandParameter<*>>,
val description: String, val description: String,
val permission: Permission, 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 context: CommandArgumentContext,
val argumentBuilder: (sender: CommandSender) -> MutableMap<KParameter, Any?>,
) { ) {
val usage: String = createUsage(this@AbstractReflectionCommand) val usage: String = createUsage(this@AbstractReflectionCommand)
@ -151,21 +153,40 @@ internal abstract class AbstractReflectionCommand
} }
} }
private fun KParameter.isOptional(): Boolean {
return isOptional || this.type.isMarkedNullable
}
val minimalArgumentsSize = params.count {
!it.parameter.isOptional()
}
@JvmField @JvmField
internal val bakedSubNames: Array<Array<String>> = names.map { it.bakeSubName() }.toTypedArray() internal val bakedSubNames: Array<Array<String>> = names.map { it.bakeSubName() }.toTypedArray()
private fun parseArgs(sender: CommandSender, rawArgs: MessageChain, offset: Int): Array<out Any>? { private fun parseArgs(sender: CommandSender, rawArgs: MessageChain, offset: Int): MutableMap<KParameter, Any?>? {
if (rawArgs.size < offset + this.params.size) if (rawArgs.size < offset + minimalArgumentsSize)
return null return null
//require(rawArgs.size >= offset + this.params.size) { "No enough args. Required ${params.size}, but given ${rawArgs.size - offset}" } //require(rawArgs.size >= offset + this.params.size) { "No enough args. Required ${params.size}, but given ${rawArgs.size - offset}" }
return argumentBuilder(sender).also { result ->
return Array(this.params.size) { index -> params.forEachIndexed { index, parameter ->
val param = params[index] val rawArg = rawArgs.getOrNull(offset + index)
val rawArg = rawArgs[offset + index] result[parameter.parameter] = when (rawArg) {
when (rawArg) { null -> {
is PlainText -> context[param.type]?.parse(rawArg.content, sender) val p = parameter.parameter
is MessageContent -> context[param.type]?.parse(rawArg, sender) when {
else -> throw IllegalArgumentException("Illegal Message kind: ${rawArg.kClassQualifiedNameOrTip}") p.isOptional -> return@forEachIndexed
} ?: error("Cannot find a parser for $rawArg") 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")
}
} }
} }
} }
@ -250,6 +271,10 @@ internal fun AbstractReflectionCommand.SubCommandDescriptor.createUsage(baseComm
appendLine() appendLine()
}.trimEnd() }.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( internal fun AbstractReflectionCommand.createSubCommand(
function: KFunction<*>, function: KFunction<*>,
context: CommandArgumentContext, context: CommandArgumentContext,
@ -273,12 +298,17 @@ internal fun AbstractReflectionCommand.createSubCommand(
check(!function.returnType.isMarkedNullable) { 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})") 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() 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()) { 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})" "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 -> (parameters.first()).let { receiver ->
if ((receiver.type.classifier as? KClass<*>)?.isSubclassOf(CommandSender::class) == true) { if ((receiver.type.classifier as? KClass<*>)?.isSubclassOf(CommandSender::class) == true) {
hasSenderParam = true val senderType = parameters.removeAt(0)
parameters.removeAt(0) argumentBuilder = argumentBuilder.then { sender, map ->
map[senderType] = sender
map
}
} }
} }
@ -313,37 +346,32 @@ internal fun AbstractReflectionCommand.createSubCommand(
//map parameter //map parameter
val params = parameters.map { param -> 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" val paramName = param.findAnnotation<CompositeCommand.Name>()?.value ?: param.name ?: "unknown"
CommandParameter( CommandParameter(
paramName, paramName,
(param.type.classifier as? KClass<*>) (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() }.toTypedArray()
// TODO: 2020/09/19 检查 optional/nullable 是否都在最后
return SubCommandDescriptor( return SubCommandDescriptor(
commandName, commandName,
params, params,
subDescription, // overridePermission?.value subDescription, // overridePermission?.value
permission,//overridePermission?.value?.let { PermissionService.INSTANCE[PermissionId.parseFromString(it)] } ?: permission, permission,//overridePermission?.value?.let { PermissionService.INSTANCE[PermissionId.parseFromString(it)] } ?: permission,
onCommand = { sender: CommandSender, args: Array<out Any> -> onCommand = { sender: CommandSender, args: Map<KParameter, Any?> ->
val result = if (notStatic) { val result = function.callSuspendBy(args)
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)
}
checkNotNull(result) { "sub command return value is null (at ${this::class.qualifiedName}.${function.name})" } checkNotNull(result) { "sub command return value is null (at ${this::class.qualifiedName}.${function.name})" }
result as? Boolean ?: true // Unit, void is considered as true. 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) { internal fun assertSuccess(result: CommandExecuteResult) {
assertTrue(result.isSuccess(), result.toString()) if (result.isFailure()) {
throw result.exception ?: AssertionError(result.toString())
}
} }