Skip to content

Commit

Permalink
Merge pull request #112 from mfnalex/argument-parsing
Browse files Browse the repository at this point in the history
Parsed Commands!!!
  • Loading branch information
mfnalex authored Feb 12, 2024
2 parents 9c515df + 231e203 commit 74ac4ce
Show file tree
Hide file tree
Showing 28 changed files with 751 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,26 @@ import java.util.TreeMap
* @param T The type of the values in the map.
* @constructor Create empty Dictionary
*/
class Dictionary<T> : MutableMap<String, T>, TreeMap<String, T>(String.CASE_INSENSITIVE_ORDER)
class Dictionary<T> : MutableMap<String, T>, TreeMap<String, T>(String.CASE_INSENSITIVE_ORDER) {
companion object {
fun fromEnum(
enumClass: Class<out Enum<*>>,
lowercase: Boolean = false,
): Dictionary<Enum<*>> {
val dictionary = Dictionary<Enum<*>>()
for (enumConstant in enumClass.enumConstants) {
val name = if (lowercase) enumConstant.name.lowercase() else enumConstant.name
dictionary[name] = enumConstant
}
return dictionary
}

fun <T> from(entries: List<Pair<String, T>>): Dictionary<T> {
val dictionary = Dictionary<T>()
for ((key, value) in entries) {
dictionary[key] = value
}
return dictionary
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,18 @@ class DictionaryTest {
assertNull(map["test2"])
assertNull(map["TEST2"])
}

@Test
fun replacesProperly() {
val map = Dictionary<Any>()
map["aaa"] = object { }
map["AAA"] = object { }
map["aAa"] = object { }
assertEquals(1, map.size)

map["aab"] = object { }
map["AAB"] = object { }
map["aAb"] = object { }
assertEquals(2, map.size)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package com.github.spigotbasics.core.command.parsed

import com.github.spigotbasics.common.Either
import com.github.spigotbasics.core.Basics
import com.github.spigotbasics.core.extensions.lastOrEmpty
import com.github.spigotbasics.core.messages.Message
import org.bukkit.command.CommandSender
import org.bukkit.permissions.Permission

class ArgumentPath<T : CommandContext>(
val senderArgument: SenderType<*>,
val arguments: List<CommandArgument<*>>,
// TODO: Check permission for specific paths!
val permission: List<Permission> = emptyList(),
private val contextBuilder: (CommandSender, List<Any?>) -> T,
) {
// fun matches(args: List<String>): Boolean {
// if (args.size > arguments.size) return false
//
// for ((index, arg) in args.withIndex()) {
// if (arguments[index].parse(arg) == null) return false
// }
//
// return true
// }

fun matches(
sender: CommandSender,
args: List<String>,
): Either<PathMatchResult, List<Message>> {
// Exact match for the number of arguments
if (args.size > arguments.size) {
return Either.Left(
PathMatchResult.NO,
) // Maybe use != ? > allows to show "missing item", != wouldn't
}
// TODO: Keep a list of non-matches where size is too little, and if no other errors occur, say "missing item", only otherwise
// fallback to CommandResult.USAGE

// Each provided arg must be parseable by its corresponding CommandArgument
val errors = mutableListOf<Message>()
// val matches = // used to be all(...)
args.indices.forEach { index ->
val parsed = arguments[index].parse(args[index])

if (parsed == null) {
val error = arguments[index].errorMessage(args[index])
errors.add(error)
}

// true
}

if (errors.isNotEmpty()) return Either.Right(errors)

if (!senderArgument.requiredType.isInstance(sender)) return Either.Left(PathMatchResult.YES_BUT_NOT_FROM_CONSOLE)
if (!hasPermission(sender)) return Either.Left(PathMatchResult.YES_BUT_NO_PERMISSION)
return Either.Left(PathMatchResult.YES)
}

// fun parse(args: List<String>): T? {
// if (!matches(args)) return null
// val parsedArgs =
// arguments.zip(args).mapNotNull { (arg, value) ->
// arg.parse(value)
// }
// return contextBuilder(parsedArgs)
// }

fun parse(
sender: CommandSender,
args: List<String>,
): ParseResult<T> {
if (!senderArgument.requiredType.isInstance(sender)) {
return ParseResult.Failure(listOf(Basics.messages.commandNotFromConsole))
}

val parsedArgs = mutableListOf<Any?>()
val errors = mutableListOf<Message>()

for ((index, arg) in arguments.withIndex()) {
if (index >= args.size) {
// errors.add("Missing argument for ${arg.name}")
errors.add(Basics.messages.missingArgument(arg.name))
break
}

val parsed = arg.parse(args[index])
if (parsed == null) {
errors.add(arg.errorMessage(args[index]))
break
} else {
parsedArgs.add(parsed)
}
}

if (errors.isEmpty() && parsedArgs.size == arguments.size) {
return ParseResult.Success(contextBuilder(sender, parsedArgs))
} else {
return ParseResult.Failure(errors)
}
}

fun tabComplete(args: List<String>): List<String> {
if (args.isEmpty() || args.size > arguments.size) return emptyList()

val currentArgIndex = args.size - 1
return arguments[currentArgIndex].tabComplete(args.lastOrEmpty())
}

fun isCorrectSender(sender: CommandSender): Boolean {
return senderArgument.requiredType.isInstance(sender)
}

fun hasPermission(sender: CommandSender): Boolean {
return permission.all { sender.hasPermission(it) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.github.spigotbasics.core.command.parsed

import com.github.spigotbasics.core.Basics
import com.github.spigotbasics.core.messages.Message

abstract class CommandArgument<T>(val name: String) {
abstract fun parse(value: String): T?

open fun tabComplete(typing: String): List<String> = emptyList()

// TODO: This is using the static Singleton :/
open fun errorMessage(value: String? = null): Message = Basics.messages.invalidValueForArgument(name, value ?: "null")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.github.spigotbasics.core.command.parsed

interface CommandContext
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.github.spigotbasics.core.command.parsed

import org.bukkit.command.CommandSender

interface CommandExecutor<T : CommandContext> {
fun execute(
sender: CommandSender,
context: T,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.github.spigotbasics.core.command.parsed

import com.github.spigotbasics.core.messages.Message

sealed class ParseResult<out T> {
data class Success<T>(val context: T) : ParseResult<T>()

data class Failure(val errors: List<Message>) : ParseResult<Nothing>()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.github.spigotbasics.core.command

import com.github.spigotbasics.common.Either
import com.github.spigotbasics.core.command.parsed.ArgumentPath
import com.github.spigotbasics.core.command.parsed.CommandContext
import com.github.spigotbasics.core.command.parsed.CommandExecutor
import com.github.spigotbasics.core.command.parsed.ParsedCommandExecutor
import com.github.spigotbasics.core.messages.Message
import com.github.spigotbasics.core.module.BasicsModule
import org.bukkit.permissions.Permission

class ParsedCommandBuilder<T : CommandContext>(
private val module: BasicsModule,
private val name: String,
private val permission: Permission,
) {
private var permissionMessage: Message = module.plugin.messages.noPermission
private var description: String? = null
private var usage: String = ""
private var aliases: List<String> = emptyList()
private var executor: BasicsCommandExecutor? = null
private var tabCompleter: BasicsTabCompleter? = null
private var parsedExecutor: CommandExecutor<T>? = null
private var argumentPaths: List<ArgumentPath<T>>? = null

fun description(description: String) = apply { this.description = description }

fun usage(usage: String) =
apply {
if (usage.startsWith("/")) error("Usage should not start with /<command> - only pass the arguments.")
this.usage = usage
}

fun paths(argumentPaths: List<ArgumentPath<T>>) = apply { this.argumentPaths = argumentPaths }

fun paths(vararg argumentPaths: ArgumentPath<T>) = apply { this.argumentPaths = argumentPaths.toList() }

fun executor(executor: CommandExecutor<T>) = apply { this.parsedExecutor = executor }

private fun executor(executor: BasicsCommandExecutor) = apply { this.executor = executor }

private fun executor(command: ParsedCommandExecutor<T>) =
apply {
this.executor =
object : BasicsCommandExecutor(module) {
override fun execute(context: BasicsCommandContext): CommandResult? {
val result = command.execute(context.sender, context.args)

if (result is Either.Left) {
return result.value
}

if (result is Either.Right) {
val failure = result.value
// TODO: Proper messages
failure.errors.forEach { it.sendToSender(context.sender) }
}

return null
}

override fun tabComplete(context: BasicsCommandContext): MutableList<String> {
return command.tabComplete(context.sender, context.args).toMutableList()
}
}
}

fun register(): BasicsCommand {
val command = build()
module.commandManager.registerCommand(command)
return command
}

private fun build(): BasicsCommand {
val command =
ParsedCommandExecutor(
parsedExecutor ?: error("parsedExecutor must be set"),
argumentPaths ?: error("Argument paths must be set"),
)
executor(command)
val info =
CommandInfo(
name = name,
permission = permission,
permissionMessage = permissionMessage,
description = description,
usage = usage,
aliases = aliases,
)
return BasicsCommand(
info = info,
executor = executor ?: error("Executor must be set"),
tabCompleter = tabCompleter ?: executor,
coreMessages = module.plugin.messages,
messageFactory = module.plugin.messageFactory,
)
}
}
Loading

0 comments on commit 74ac4ce

Please sign in to comment.