235 lines
8.0 KiB
Kotlin
235 lines
8.0 KiB
Kotlin
package codes.som.hibiscus.api.command
|
|
|
|
import codes.som.hibiscus.api.command.ExecutableCommand.CommandBranch
|
|
import codes.som.hibiscus.api.command.context.CommandExecutionContext
|
|
import codes.som.hibiscus.api.command.exceptions.*
|
|
import codes.som.hibiscus.api.command.parser.ArgumentParser
|
|
import codes.som.hibiscus.api.command.utils.PeekableIterator
|
|
import codes.som.hibiscus.api.command.utils.splitExceptingQuotes
|
|
import java.util.*
|
|
|
|
class CommandManager(private val registerDefaultParsers: Boolean = true) {
|
|
private val parserRegistry = mutableMapOf<Class<*>, ArgumentParser<*>>()
|
|
val commands = mutableListOf<ExecutableCommand>()
|
|
|
|
var context: CommandContext = CommandContext.OTHER
|
|
|
|
init {
|
|
registerDefaultParsersIfApplicable()
|
|
}
|
|
|
|
private fun registerDefaultParsersIfApplicable() {
|
|
if (registerDefaultParsers) {
|
|
addDefaultParsers(parserRegistry)
|
|
}
|
|
}
|
|
|
|
fun <T> registerParser(type: Class<T>, parser: ArgumentParser<T>) {
|
|
parserRegistry[type] = parser
|
|
}
|
|
|
|
private fun verifyCommand(command: ExecutableCommand) {
|
|
// TODO: Prevent name collisions for commands / aliases
|
|
|
|
command.branches.forEach { branch ->
|
|
branch.parameterTypes
|
|
.filter { it !in parserRegistry }
|
|
.forEach { throw MissingParserException(it) }
|
|
}
|
|
}
|
|
|
|
@Throws(CommandRegistrationException::class)
|
|
fun register(declaration: Command) {
|
|
try {
|
|
val command = declaration.buildCommand()
|
|
|
|
verifyCommand(command)
|
|
commands += command
|
|
} catch (e: Exception) {
|
|
throw CommandRegistrationException(e)
|
|
}
|
|
}
|
|
|
|
@Throws(CommandExecutionException::class)
|
|
fun executeCommand(fullCommand: String) {
|
|
try {
|
|
val args = splitExceptingQuotes(fullCommand, true).toList()
|
|
if (args.isEmpty())
|
|
throw CommandNotFoundException("")
|
|
|
|
val commandName = args[0]
|
|
|
|
val matchingCommands = commands.filter { commandMatches(commandName, it) }
|
|
when {
|
|
matchingCommands.isEmpty() -> throw CommandNotFoundException(commandName)
|
|
matchingCommands.size > 1 -> throw CommandIsAmbiguousException(commandName)
|
|
}
|
|
|
|
val command = matchingCommands.first()
|
|
|
|
fun isBranchViable(branch: CommandBranch, args: List<String>): Boolean {
|
|
try {
|
|
val argObjects = parseArgumentsForBranch(branch, args)
|
|
if (argObjects.size == branch.parameterTypes.size) {
|
|
return true
|
|
}
|
|
} catch (e: Exception) {
|
|
return false
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
val viableBranches = command.branches.filter { branchMatches(args, it) }.filter { isBranchViable(it, args) }
|
|
|
|
val viableNamedBranches = viableBranches.filter { it.name != null }
|
|
if (viableNamedBranches.size > 1)
|
|
throw BranchesAreAmbiguousException(commandName, args[1])
|
|
|
|
// Loop through named branches first, as branch names have priority over arguments.
|
|
for (branch in viableNamedBranches) {
|
|
executeBranch(branch, args)
|
|
return
|
|
}
|
|
|
|
for (branch in viableBranches.filter { it.name == null }) {
|
|
executeBranch(branch, args)
|
|
return
|
|
}
|
|
|
|
throw NoMatchingBranchesException()
|
|
} catch (e: Exception) {
|
|
throw CommandExecutionException(e)
|
|
}
|
|
}
|
|
|
|
fun completeCommand(fullCommand: String): Array<String> {
|
|
return completeCommandDuplicatesSpaces(fullCommand)
|
|
.map { s ->
|
|
if (s.toCharArray().any { it.isWhitespace() }) "\"$s\"" else s
|
|
} // Wrap suggestions containing spaces in quotes
|
|
.toSet() // Remove duplicates
|
|
.toTypedArray()
|
|
}
|
|
|
|
private fun completeCommandDuplicatesSpaces(fullCommand: String): List<String> {
|
|
val args = splitExceptingQuotes(fullCommand, true).toList()
|
|
|
|
if (args.isEmpty())
|
|
return commands.map { it.name }
|
|
|
|
if (args.size == 1) {
|
|
val namesAndAliasesOfCommands = mutableListOf<String>().apply {
|
|
addAll(commands.map { it.name })
|
|
commands.forEach { addAll(it.aliases) }
|
|
}
|
|
|
|
return namesAndAliasesOfCommands.filter {
|
|
it.lowercase(Locale.getDefault()).startsWith(fullCommand.lowercase(Locale.getDefault()))
|
|
}
|
|
}
|
|
|
|
val commandName = args[0]
|
|
val matchingCommands = commands.filter { commandMatches(commandName, it) }
|
|
when {
|
|
matchingCommands.isEmpty() -> return emptyList()
|
|
matchingCommands.size > 1 -> throw CommandIsAmbiguousException(commandName)
|
|
}
|
|
|
|
val command = matchingCommands.first()
|
|
|
|
if (args[1].isBlank())
|
|
return command.branches.mapNotNull { it.name }
|
|
|
|
val matchingBranches = mutableListOf<String>().apply {
|
|
addAll(command.branches.mapNotNull { it.name })
|
|
command.branches.forEach { addAll(it.aliases) }
|
|
}.filter { it.startsWith(args[1]) }
|
|
|
|
if (args.size == 2)
|
|
return matchingBranches
|
|
|
|
// TODO: Tab completion for parameters
|
|
// - Solve ambiguity issues
|
|
// ????
|
|
|
|
return emptyList()
|
|
}
|
|
|
|
private fun parseArgumentsForBranch(branch: CommandBranch, args: List<String>): Array<Any> {
|
|
val parsers = mutableListOf<ArgumentParser<*>>()
|
|
for ((index, parameterType) in branch.parameterTypes.withIndex()) {
|
|
if (index in branch.typeHints) {
|
|
parsers.add(branch.typeHints[index]!!)
|
|
} else {
|
|
if (parameterType !in parserRegistry)
|
|
throw MissingParserException(parameterType)
|
|
|
|
parsers.add(parserRegistry[parameterType]!!)
|
|
}
|
|
}
|
|
|
|
val startIndex = if (branch.name == null) 1 else 2
|
|
|
|
if (parsers.isEmpty() && args.size > startIndex) {
|
|
throw ParsingException(
|
|
InvalidArgumentCount(
|
|
branch.name
|
|
?: "<blank>"
|
|
)
|
|
)
|
|
}
|
|
|
|
val argsIterator = PeekableIterator(args.listIterator(startIndex))
|
|
|
|
val context = CommandExecutionContext(argsIterator, branch.parameterTypes, 0)
|
|
|
|
val argumentObjects = mutableListOf<Any>()
|
|
|
|
val minArgs = parsers.sumOf { it.minimumAcceptedArguments() }
|
|
|
|
if (args.size - startIndex >= minArgs && args.size - startIndex >= parsers.size) {
|
|
for ((index, parser) in parsers.withIndex()) {
|
|
context.currentParameter = index
|
|
|
|
try {
|
|
argumentObjects += parser.parse(context)!!
|
|
} catch (e: Exception) {
|
|
throw ParsingException(e)
|
|
}
|
|
}
|
|
} else {
|
|
throw ParsingException(
|
|
InvalidArgumentCount(
|
|
branch.name
|
|
?: "<blank>"
|
|
)
|
|
)
|
|
}
|
|
|
|
return argumentObjects.toTypedArray()
|
|
}
|
|
|
|
private fun executeBranch(branch: CommandBranch, args: List<String>) {
|
|
val argumentObjects = parseArgumentsForBranch(branch, args)
|
|
branch.execute(*argumentObjects)
|
|
}
|
|
|
|
private fun commandMatches(commandName: String, command: ExecutableCommand): Boolean {
|
|
return command.name.equals(commandName, ignoreCase = true) ||
|
|
command.aliases.any { it.equals(commandName, ignoreCase = true) }
|
|
}
|
|
|
|
private fun branchMatches(args: List<String>, branch: CommandBranch): Boolean {
|
|
return branch.name == null ||
|
|
(args.size > 1 && (branch.name.equals(args[1], ignoreCase = true) ||
|
|
branch.aliases.any { it.equals(args[1], ignoreCase = true) }))
|
|
}
|
|
|
|
fun reset() {
|
|
commands.clear()
|
|
parserRegistry.clear()
|
|
registerDefaultParsersIfApplicable()
|
|
}
|
|
}
|