Introduce executeCommandDetailed;

Enhance CommandExecuteResult, add type-safe classification with contracts;
Introduce CommandExecutionException providing information about the command on execution failure.
This commit is contained in:
Him188 2020-06-23 17:49:09 +08:00
parent f1752db580
commit a117e73186
5 changed files with 324 additions and 89 deletions

View File

@ -1,6 +1,7 @@
package net.mamoe.mirai.console.command;
import kotlin.NotImplementedError;
import kotlin.coroutines.Continuation;
import kotlin.coroutines.EmptyCoroutineContext;
import kotlinx.coroutines.BuildersKt;
import kotlinx.coroutines.CoroutineScope;
@ -18,7 +19,7 @@ import java.util.concurrent.CompletableFuture;
/**
* Java 适配的 {@link CommandManagerKt}
*/
@SuppressWarnings("unused")
@SuppressWarnings({"unused", "RedundantSuppression"})
public final class JCommandManager {
private JCommandManager() {
throw new NotImplementedError();
@ -101,14 +102,17 @@ public final class JCommandManager {
CommandManagerKt.unregisterAllCommands(owner);
}
/**
* 解析并执行一个指令
*
* @param args 接受 {@link String} {@link Message} , 其他对象将会被 {@link Object#toString()}
* @see CommandExecuteResult
* @return 成功执行的指令, 在无匹配指令时返回 <code>null</code>
* @throws CommandExecutionException {@link Command#onCommand(CommandSender, Object[], Continuation)} 抛出异常时包装并附带相关指令信息抛出
* @see #executeCommandAsync(CoroutineScope, CommandSender, Object...)
*/
public static CommandExecuteResult executeCommand(final @NotNull CommandSender sender, final @NotNull Object... args) throws InterruptedException {
@Nullable
public static Command executeCommand(final @NotNull CommandSender sender, final @NotNull Object... args) throws CommandExecutionException, InterruptedException {
Objects.requireNonNull(sender, "sender");
Objects.requireNonNull(args, "args");
for (Object arg : args) {
@ -123,10 +127,11 @@ public final class JCommandManager {
*
* @param scope 协程作用域 (用于管理协程生命周期). 一般填入 {@link JavaPlugin} 实例.
* @param args 接受 {@link String} {@link Message} , 其他对象将会被 {@link Object#toString()}
* @see CommandExecuteResult
* @return 成功执行的指令, 在无匹配指令时返回 <code>null</code>
* @see #executeCommand(CommandSender, Object...)
*/
public static CompletableFuture<CommandExecuteResult> executeCommandAsync(final @NotNull CoroutineScope scope, final @NotNull CommandSender sender, final @NotNull Object... args) {
@NotNull
public static CompletableFuture<@Nullable Command> executeCommandAsync(final @NotNull CoroutineScope scope, final @NotNull CommandSender sender, final @NotNull Object... args) {
Objects.requireNonNull(sender, "sender");
Objects.requireNonNull(args, "args");
Objects.requireNonNull(scope, "scope");
@ -136,4 +141,46 @@ public final class JCommandManager {
return FutureKt.future(scope, EmptyCoroutineContext.INSTANCE, CoroutineStart.DEFAULT, (sc, completion) -> CommandManagerKt.executeCommand(sender, args, completion));
}
/**
* 解析并执行一个指令, 获取详细的指令参数等信息.
* <br />
* 执行过程中产生的异常将不会直接抛出, 而会包装为 {@link CommandExecuteResult.ExecutionException}
*
* @param args 接受 {@link String} {@link Message} , 其他对象将会被 {@link Object#toString()}
* @return 执行结果
* @see #executeCommandDetailedAsync(CoroutineScope, CommandSender, Object...)
*/
@NotNull
public static CommandExecuteResult executeCommandDetailed(final @NotNull CommandSender sender, final @NotNull Object... args) throws InterruptedException {
Objects.requireNonNull(sender, "sender");
Objects.requireNonNull(args, "args");
for (Object arg : args) {
Objects.requireNonNull(arg, "element of args");
}
return BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE, (scope, completion) -> CommandManagerKt.executeCommandDetailed(sender, args, completion));
}
/**
* 异步 ( Kotlin 协程线程池) 解析并执行一个指令, 获取详细的指令参数等信息
*
* @param scope 协程作用域 (用于管理协程生命周期). 一般填入 {@link JavaPlugin} 实例.
* @param args 接受 {@link String} {@link Message} , 其他对象将会被 {@link Object#toString()}
* @return 执行结果
* @see #executeCommandDetailed(CommandSender, Object...)
*/
@NotNull
public static CompletableFuture<@NotNull CommandExecuteResult>
executeCommandDetailedAsync(final @NotNull CoroutineScope scope, final @NotNull CommandSender sender, final @NotNull Object... args) {
Objects.requireNonNull(sender, "sender");
Objects.requireNonNull(args, "args");
Objects.requireNonNull(scope, "scope");
for (Object arg : args) {
Objects.requireNonNull(arg, "element of args");
}
return FutureKt.future(scope, EmptyCoroutineContext.INSTANCE, CoroutineStart.DEFAULT, (sc, completion) -> CommandManagerKt.executeCommandDetailed(sender, args, completion));
}
}

View File

@ -0,0 +1,146 @@
@file:Suppress("unused")
package net.mamoe.mirai.console.command
import net.mamoe.mirai.console.command.CommandExecuteResult.CommandExecuteStatus
import net.mamoe.mirai.message.data.Message
import kotlin.contracts.contract
/**
* 指令的执行返回
*
* @see CommandExecuteStatus
*/
sealed class CommandExecuteResult {
/** 指令最终执行状态 */
abstract val status: CommandExecuteStatus
/** 指令执行时发生的错误 (如果有) */
abstract val exception: Throwable?
/** 尝试执行的指令 (如果匹配到) */
abstract val command: Command?
/** 尝试执行的指令名 (如果匹配到) */
abstract val commandName: String?
/** 基础分割后的实际参数列表, 元素类型可能为 [Message] 或 [String] */
abstract val args: Array<out Any>?
// abstract val to allow smart casting
/** 指令执行成功 */
class Success(
/** 尝试执行的指令 */
override val command: Command,
/** 尝试执行的指令名 */
override val commandName: String,
/** 基础分割后的实际参数列表, 元素类型可能为 [Message] 或 [String] */
override val args: Array<out Any>
) : CommandExecuteResult() {
/** 指令执行时发生的错误, 总是 `null` */
override val exception: Nothing? get() = null
/** 指令最终执行状态, 总是 [CommandExecuteStatus.SUCCESSFUL] */
override val status: CommandExecuteStatus get() = CommandExecuteStatus.SUCCESSFUL
}
/** 指令执行过程出现了错误 */
class ExecutionException(
/** 指令执行时发生的错误 */
override val exception: Throwable,
/** 尝试执行的指令 */
override val command: Command,
/** 尝试执行的指令名 */
override val commandName: String,
/** 基础分割后的实际参数列表, 元素类型可能为 [Message] 或 [String] */
override val args: Array<out Any>
) : CommandExecuteResult() {
/** 指令最终执行状态, 总是 [CommandExecuteStatus.EXECUTION_EXCEPTION] */
override val status: CommandExecuteStatus get() = CommandExecuteStatus.EXECUTION_EXCEPTION
}
/** 没有匹配的指令 */
class CommandNotFound(
/** 尝试执行的指令名 */
override val commandName: String
) : CommandExecuteResult() {
/** 指令执行时发生的错误, 总是 `null` */
override val exception: Nothing? get() = null
/** 尝试执行的指令, 总是 `null` */
override val command: Nothing? get() = null
/** 基础分割后的实际参数列表, 元素类型可能为 [Message] 或 [String] */
override val args: Nothing? get() = null
/** 指令最终执行状态, 总是 [CommandExecuteStatus.COMMAND_NOT_FOUND] */
override val status: CommandExecuteStatus get() = CommandExecuteStatus.COMMAND_NOT_FOUND
}
/**
* 指令的执行状态
*/
enum class CommandExecuteStatus {
/** 指令执行成功 */
SUCCESSFUL,
/** 指令执行过程出现了错误 */
EXECUTION_EXCEPTION,
/** 没有匹配的指令 */
COMMAND_NOT_FOUND
}
}
@Suppress("RemoveRedundantQualifierName")
typealias CommandExecuteStatus = CommandExecuteResult.CommandExecuteStatus
/**
* [this] [CommandExecuteResult.Success] 时返回 `true`
*/
@JvmSynthetic
fun CommandExecuteResult.isSuccess(): Boolean {
contract {
returns(true) implies (this@isSuccess is CommandExecuteResult.Success)
returns(false) implies (this@isSuccess !is CommandExecuteResult.Success)
}
return this is CommandExecuteResult.Success
}
/**
* [this] [CommandExecuteResult.ExecutionException] 时返回 `true`
*/
@JvmSynthetic
fun CommandExecuteResult.isExecutionException(): Boolean {
contract {
returns(true) implies (this@isExecutionException is CommandExecuteResult.ExecutionException)
returns(false) implies (this@isExecutionException !is CommandExecuteResult.ExecutionException)
}
return this is CommandExecuteResult.ExecutionException
}
/**
* [this] [CommandExecuteResult.ExecutionException] 时返回 `true`
*/
@JvmSynthetic
fun CommandExecuteResult.isCommandNotFound(): Boolean {
contract {
returns(true) implies (this@isCommandNotFound is CommandExecuteResult.CommandNotFound)
returns(false) implies (this@isCommandNotFound !is CommandExecuteResult.CommandNotFound)
}
return this is CommandExecuteResult.CommandNotFound
}
/**
* [this] [CommandExecuteResult.ExecutionException] [CommandExecuteResult.CommandNotFound] 时返回 `true`
*/
@JvmSynthetic
fun CommandExecuteResult.isFailure(): Boolean {
contract {
returns(true) implies (this@isFailure !is CommandExecuteResult.Success)
returns(false) implies (this@isFailure is CommandExecuteResult.Success)
}
return this !is CommandExecuteResult.Success
}

View File

@ -7,7 +7,10 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("NOTHING_TO_INLINE", "unused")
@file:Suppress(
"NOTHING_TO_INLINE", "unused", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE",
"MemberVisibilityCanBePrivate"
)
@file:JvmName("CommandManagerKt")
package net.mamoe.mirai.console.command
@ -18,7 +21,6 @@ import kotlinx.coroutines.Job
import net.mamoe.mirai.console.plugin.Plugin
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.SingleMessage
import net.mamoe.mirai.utils.MiraiInternalAPI
/**
@ -126,104 +128,119 @@ fun Command.unregister(): Boolean = InternalCommandManager.modifyLock.withLock {
InternalCommandManager.registeredCommands.remove(this)
}
//// executing
//// executing without detailed result (faster)
/**
* 解析并执行一个指令
*
* Java 调用方式: `<static> CommandManager.executeCommand(Command)`
*
* @param messages 接受 [String] [Message], 其他对象将会被 [Any.toString]
* @see CommandExecuteResult
*
* @return 成功执行的指令, 在无匹配指令时返回 `null`
* @throws CommandExecutionException [Command.onCommand] 抛出异常时包装并附带相关指令信息抛出
*
* @see JCommandManager.executeCommand Java 方法
*/
suspend fun CommandSender.executeCommand(vararg messages: Any): CommandExecuteResult {
if (messages.isEmpty()) return CommandExecuteResult(
status = CommandExecuteStatus.EMPTY_COMMAND
)
return executeCommandInternal(
messages,
messages[0].let { if (it is SingleMessage) it.toString() else it.toString().substringBefore(' ') })
suspend fun CommandSender.executeCommand(vararg messages: Any): Command? {
if (messages.isEmpty()) return null
return executeCommandInternal(messages, messages[0].toString().substringBefore(' '))
}
@JvmSynthetic
internal inline fun <reified T> List<T>.dropToTypedArray(n: Int): Array<T> = Array(size - n) { this[n + it] }
/**
* 解析并执行一个指令
* @see CommandExecuteResult
*
* @return 成功执行的指令, 在无匹配指令时返回 `null`
* @throws CommandExecutionException [Command.onCommand] 抛出异常时包装并附带相关指令信息抛出
*
* @see JCommandManager.executeCommand Java 方法
*/
suspend fun CommandSender.executeCommand(message: MessageChain): CommandExecuteResult {
if (message.isEmpty()) return CommandExecuteResult(
status = CommandExecuteStatus.EMPTY_COMMAND
)
@Throws(CommandExecutionException::class)
suspend fun CommandSender.executeCommand(message: MessageChain): Command? {
if (message.isEmpty()) return null
return executeCommandInternal(message, message[0].toString())
}
@JvmSynthetic
internal suspend inline fun CommandSender.executeCommandInternal(
messages: Any,
commandName: String
): CommandExecuteResult {
val command = InternalCommandManager.matchCommand(commandName) ?: return CommandExecuteResult(
status = CommandExecuteStatus.COMMAND_NOT_FOUND,
commandName = commandName
)
val rawInput = messages.flattenCommandComponents()
kotlin.runCatching {
command.onCommand(this, rawInput.dropToTypedArray(1))
}.onFailure {
return CommandExecuteResult(
status = CommandExecuteStatus.FAILED,
commandName = commandName,
command = command,
exception = it
)
}
return CommandExecuteResult(
status = CommandExecuteStatus.SUCCESSFUL,
commandName = commandName,
command = command
)
/**
* [executeCommand] , [Command.onCommand] 抛出异常时包装的异常.
*/
class CommandExecutionException(
/**
* 执行过程发生异常的指令
*/
val command: Command,
/**
* 匹配到的指令名
*/
val name: String,
/**
* 基础分割后的实际参数列表, 元素类型可能为 [Message] [String]
*/
val args: Array<out Any>,
cause: Throwable
) : RuntimeException(
"Exception while executing command '${command.primaryName}' with args ${args.joinToString { "'$it'" }}",
cause
) {
override fun toString(): String =
"CommandExecutionException(command=$command, name='$name', args=${args.contentToString()})"
}
//// execution with detailed result
/**
* 解析并执行一个指令, 获取详细的指令参数等信息
*
* @param messages 接受 [String] [Message], 其他对象将会被 [Any.toString]
*
* @return 执行结果
*
* @see JCommandManager.executeCommandDetailed Java 方法
*/
suspend fun CommandSender.executeCommandDetailed(vararg messages: Any): CommandExecuteResult {
if (messages.isEmpty()) return CommandExecuteResult.CommandNotFound("")
return executeCommandDetailedInternal(messages, messages[0].toString().substringBefore(' '))
}
/**
* 命令的执行返回
* 解析并执行一个指令, 获取详细的指令参数等信息
*
* @param status 命令最终执行状态
* @param exception 命令执行时发生的错误(如果有)
* @param command 尝试执行的命令 (status = SUCCESSFUL | FAILED)
* @param commandName 尝试执行的命令的名字 (status != EMPTY_COMMAND)
* 执行过程中产生的异常将不会直接抛出, 而会包装为 [CommandExecuteResult.ExecutionException]
*
* @return 执行结果
*
* @see CommandExecuteStatus
* @see JCommandManager.executeCommandDetailed Java 方法
*/
class CommandExecuteResult(
val status: CommandExecuteStatus,
val exception: Throwable? = null,
val command: Command? = null,
val commandName: String? = null
) {
/**
* 命令的执行状态
*
* 当为 [SUCCESSFUL] 的时候代表命令执行成功
*
* 当为 [FAILED] 的时候, 代表命令执行出现了错误
*
* 当为 [COMMAND_NOT_FOUND] 的时候代表没有匹配的命令
*
* 当为 [EMPTY_COMMAND] 的时候, 代表尝试执行 ""
*
*/
enum class CommandExecuteStatus {
SUCCESSFUL, FAILED, COMMAND_NOT_FOUND, EMPTY_COMMAND
}
suspend fun CommandSender.executeCommandDetailed(messages: MessageChain): CommandExecuteResult {
if (messages.isEmpty()) return CommandExecuteResult.CommandNotFound("")
return executeCommandDetailedInternal(messages, messages[0].toString())
}
@Suppress("RemoveRedundantQualifierName")
typealias CommandExecuteStatus = CommandExecuteResult.CommandExecuteStatus
@JvmSynthetic
internal suspend inline fun CommandSender.executeCommandDetailedInternal(
messages: Any,
commandName: String
): CommandExecuteResult {
val command =
InternalCommandManager.matchCommand(commandName) ?: return CommandExecuteResult.CommandNotFound(commandName)
val args = messages.flattenCommandComponents().dropToTypedArray(1)
kotlin.runCatching {
command.onCommand(this, args)
}.fold(
onSuccess = {
return CommandExecuteResult.Success(
commandName = commandName,
command = command,
args = args
)
},
onFailure = {
return CommandExecuteResult.ExecutionException(
commandName = commandName,
command = command,
exception = it,
args = args
)
}
)
}

View File

@ -156,4 +156,33 @@ internal fun Group.fuzzySearchMember(nameCardTarget: String): Member? {
return this.members.fuzzySearchOnly(nameCardTarget) {
it.nameCard
}
}
}
//// internal
@JvmSynthetic
internal inline fun <reified T> List<T>.dropToTypedArray(n: Int): Array<T> = Array(size - n) { this[n + it] }
@JvmSynthetic
@Throws(CommandExecutionException::class)
internal suspend inline fun CommandSender.executeCommandInternal(
messages: Any,
commandName: String
): Command? {
val command = InternalCommandManager.matchCommand(commandName) ?: return null
val rawInput = messages.flattenCommandComponents()
val loweredArgs = rawInput.dropToTypedArray(1)
kotlin.runCatching {
command.onCommand(this, loweredArgs)
}.fold(
onSuccess = {
return command
},
onFailure = {
throw CommandExecutionException(command, commandName, loweredArgs, it)
}
)
}

View File

@ -22,9 +22,8 @@ package net.mamoe.mirai.console.pure
import net.mamoe.mirai.console.MiraiConsoleInitializer
import net.mamoe.mirai.console.command.CommandExecuteStatus
import net.mamoe.mirai.console.command.ConsoleCommandSender
import net.mamoe.mirai.console.command.executeCommand
import net.mamoe.mirai.console.command.executeCommandDetailed
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.PlainText
import net.mamoe.mirai.utils.DefaultLogger
import kotlin.concurrent.thread
@ -48,14 +47,11 @@ internal fun startConsoleThread() {
while (true) {
val next = MiraiConsoleFrontEndPure.requestInput("")
consoleLogger.debug("INPUT> $next")
val result = ConsoleCS.executeCommand(PlainText(next))
val result = ConsoleCS.executeCommandDetailed(next)
when (result.status) {
CommandExecuteStatus.SUCCESSFUL -> {
}
CommandExecuteStatus.EMPTY_COMMAND -> {
}
CommandExecuteStatus.FAILED -> {
consoleLogger.error("An error occurred while executing the command: $next", result.exception)
CommandExecuteStatus.EXECUTION_EXCEPTION -> {
}
CommandExecuteStatus.COMMAND_NOT_FOUND -> {
consoleLogger.warning("Unknown command: ${result.commandName}")