From 6294e8d90f2c846151fa9249d80890f0966c2fee Mon Sep 17 00:00:00 2001 From: Adam Spofford Date: Sun, 14 Apr 2019 22:19:15 -0700 Subject: [PATCH] Continue to clean up mess --- .../frontier/skc/CommandMappingParser.kt | 220 ------------------ .../kotlin/frontier/skc/ParameterMapping.kt | 11 +- src/main/kotlin/frontier/skc/SKCCommand.kt | 209 ++++++++++++++++- 3 files changed, 217 insertions(+), 223 deletions(-) delete mode 100644 src/main/kotlin/frontier/skc/CommandMappingParser.kt diff --git a/src/main/kotlin/frontier/skc/CommandMappingParser.kt b/src/main/kotlin/frontier/skc/CommandMappingParser.kt deleted file mode 100644 index 4aec0f4..0000000 --- a/src/main/kotlin/frontier/skc/CommandMappingParser.kt +++ /dev/null @@ -1,220 +0,0 @@ -package frontier.skc - -import com.google.inject.Injector -import frontier.skc.annotation.Command -import frontier.skc.annotation.Description -import frontier.skc.annotation.Executor -import frontier.skc.annotation.Flag -import frontier.skc.annotation.Permission -import frontier.skc.annotation.Weak -import frontier.skc.util.ConstantNoUsageCommandElement -import frontier.skc.util.effectiveName -import frontier.skc.util.isSubtypeOf -import frontier.skc.util.isType -import frontier.ske.text.not -import frontier.ske.text.unaryPlus -import org.spongepowered.api.command.CommandException -import org.spongepowered.api.command.CommandResult -import org.spongepowered.api.command.args.CommandContext -import org.spongepowered.api.command.args.CommandElement -import org.spongepowered.api.command.args.GenericArguments -import org.spongepowered.api.command.spec.CommandExecutor -import org.spongepowered.api.command.spec.CommandSpec -import org.spongepowered.api.text.Text -import java.lang.reflect.InvocationTargetException -import java.util.LinkedList -import kotlin.reflect.KAnnotatedElement -import kotlin.reflect.KClass -import kotlin.reflect.KFunction -import kotlin.reflect.KParameter -import kotlin.reflect.full.findAnnotation -import kotlin.reflect.full.functions -import kotlin.reflect.jvm.isAccessible - -fun KClass<*>.newSpec(mappings: List, injector: Injector? = null): CommandSpec.Builder { - val spec = CommandSpec.builder() - - val instance = this.objectInstance ?: requireNotNull(injector?.getInstance(this.java)) { - "Could not instantiate Command for ${this.simpleName}: must be an object, or provide an injector if class" - } - - val finalMappings = mappings + ObjectInstanceParameterMapping(this, instance) - - this.checkCommand() - this.applyAnnotations(spec) - - // Register child objects/classes. - for (childClass in this.nestedClasses) { - val aliases = childClass.findAnnotation()?.aliases ?: continue - val childSpec = childClass.newSpec(finalMappings).build() - spec.child(childSpec, *aliases) - } - - var hasDefault = false - - // Register child functions and the default executor, if available. - for (childFunction in this.functions) { - if (childFunction.findAnnotation() != null) { - // Found a default executor - require(!hasDefault) { "${this.simpleName} already has a default executor." } - - spec.arguments(childFunction.mapParameters(finalMappings)) - spec.executor(childFunction.createExecutor()) - - hasDefault = true - } - - val aliases = childFunction.findAnnotation()?.aliases ?: continue - val childSpec = childFunction.newSpec(finalMappings).build() - spec.child(childSpec, *aliases) - } - - return spec -} - -fun KClass<*>.checkCommand() { - require(!this.isAbstract) { "Unsupported class modifier: abstract" } - require(!this.isSealed) { "Unsupported class modifier: sealed" } -} - -fun KFunction<*>.newSpec(mappings: List): CommandSpec.Builder { - val spec = CommandSpec.builder() - - this.checkCommand() - this.applyAnnotations(spec) - - spec.arguments(this.mapParameters(mappings)) - spec.executor(this.createExecutor()) - - return spec -} - -fun KFunction<*>.checkCommand() { - require(this.returnType.isSubtypeOf() || this.returnType.isSubtypeOf()) { - "Unsupported return type (${this.returnType}), must be CommandResult or Unit" - } - - require(!this.isAbstract) { "Unsupported function modifier: abstract" } - require(!this.isExternal) { "Unsupported function modifier: external" } - require(!this.isInfix) { "Unsupported function modifier: infix" } - require(!this.isInline) { "Unsupported function modifier: inline" } - require(!this.isOperator) { "Unsupported function modifier: operator" } - require(!this.isSuspend) { "Unsupported function modifier: suspend" } - - this.isAccessible = true -} - -fun KFunction<*>.mapParameters(mappings: List): CommandElement { - val flags = GenericArguments.flags() - val elements = LinkedList() - - for (parameter in this.parameters) { - val flag = parameter.findAnnotation() - - if (flag != null) { - // Flag element. - if (parameter.type.isType()) { - // Boolean flag. - val permission = parameter.findAnnotation() - - when (permission) { - null -> flags.flag(*flag.specs) - else -> flags.permissionFlag(permission.value, *flag.specs) - } - } else { - // Value flag. - flags.valueFlag(mappings.match(parameter), *flag.specs) - } - } else { - // Non-flag element. - elements += mappings.match(parameter) - } - } - - return flags.buildWith(GenericArguments.seq(*elements.toTypedArray())) -} - -fun KFunction<*>.createExecutor(): CommandExecutor = CommandExecutor { _, ctx -> - try { - val result = this.callBy(this.parameters.buildCallingArguments(ctx)) - - if (result is CommandResult) { - result - } else { - CommandResult.success() - } - } catch (e: InvocationTargetException) { - val cause = e.cause - - when (cause) { - is CommandException -> throw cause - null -> { - e.printStackTrace() - throw CommandException(!"An error occurred while executing that command.", e) - } - else -> { - e.printStackTrace() - throw CommandException(!"An error occurred while executing that command.", cause) - } - } - } -} - -fun KAnnotatedElement.applyAnnotations(spec: CommandSpec.Builder) { - this.findAnnotation()?.let { spec.permission(it.value) } - this.findAnnotation()?.let { spec.description(+it.value) } -} - -fun List.buildCallingArguments(ctx: CommandContext): Map { - val values = hashMapOf() - - for (parameter in this) { - if (ctx.hasAny(parameter.effectiveName)) { - // A value is available, use it. - values[parameter] = ctx.requireOne(parameter.effectiveName) - } else if (parameter.type.isMarkedNullable) { - // No value is available, but the type is nullable, so set it to null. - values[parameter] = null - } else if (!parameter.isOptional) { - // There is no available value to use! What do we do?! - throw CommandException(!"No value found for parameter '${parameter.effectiveName}'") - } - } - - return values -} - -fun List.match(parameter: KParameter): CommandElement { - var element: CommandElement? = null - - for (mapper in this) { - element = mapper(parameter)?.invoke(!parameter.effectiveName) - - if (element != null) break - } - - var result = requireNotNull(element) { - "Could not find a ParameterMapping that matches ${parameter.type}" - } - - parameter.findAnnotation()?.let { - result = GenericArguments.requiringPermission(result, it.value) - } - - if (parameter.isOptional || parameter.type.isMarkedNullable) { - result = when { - parameter.findAnnotation() != null -> GenericArguments.optionalWeak(result) - else -> GenericArguments.optional(result) - } - } - - return result -} - - -class ObjectInstanceParameterMapping(private val clazz: KClass<*>, private val instance: Any) : ParameterMapping { - override fun invoke(parameter: KParameter): ((Text) -> CommandElement)? = when (parameter.type.classifier) { - clazz -> { key -> ConstantNoUsageCommandElement(key, instance) } - else -> null - } -} diff --git a/src/main/kotlin/frontier/skc/ParameterMapping.kt b/src/main/kotlin/frontier/skc/ParameterMapping.kt index 4e02183..0a369c0 100644 --- a/src/main/kotlin/frontier/skc/ParameterMapping.kt +++ b/src/main/kotlin/frontier/skc/ParameterMapping.kt @@ -1,7 +1,16 @@ package frontier.skc +import frontier.skc.util.ConstantNoUsageCommandElement import org.spongepowered.api.command.args.CommandElement import org.spongepowered.api.text.Text +import kotlin.reflect.KClass import kotlin.reflect.KParameter -typealias ParameterMapping = (KParameter) -> ((Text) -> CommandElement)? \ No newline at end of file +typealias ParameterMapping = (KParameter) -> ((Text) -> CommandElement)? + +class ObjectInstanceParameterMapping(private val clazz: KClass<*>, private val instance: Any) : ParameterMapping { + override fun invoke(parameter: KParameter): ((Text) -> CommandElement)? = when (parameter.type.classifier) { + clazz -> { key -> ConstantNoUsageCommandElement(key, instance) } + else -> null + } +} diff --git a/src/main/kotlin/frontier/skc/SKCCommand.kt b/src/main/kotlin/frontier/skc/SKCCommand.kt index 330f244..b697d28 100644 --- a/src/main/kotlin/frontier/skc/SKCCommand.kt +++ b/src/main/kotlin/frontier/skc/SKCCommand.kt @@ -2,10 +2,35 @@ package frontier.skc import com.google.inject.Injector import frontier.skc.annotation.Command +import frontier.skc.annotation.Description +import frontier.skc.annotation.Executor +import frontier.skc.annotation.Flag +import frontier.skc.annotation.Permission +import frontier.skc.annotation.Weak +import frontier.skc.util.ConstantNoUsageCommandElement +import frontier.skc.util.effectiveName +import frontier.skc.util.isSubtypeOf +import frontier.skc.util.isType import frontier.ske.commandManager +import frontier.ske.text.not +import frontier.ske.text.unaryPlus +import org.spongepowered.api.command.CommandException +import org.spongepowered.api.command.CommandResult +import org.spongepowered.api.command.args.CommandContext +import org.spongepowered.api.command.args.CommandElement +import org.spongepowered.api.command.args.GenericArguments +import org.spongepowered.api.command.spec.CommandExecutor +import org.spongepowered.api.command.spec.CommandSpec +import org.spongepowered.api.text.Text +import java.lang.reflect.InvocationTargetException +import java.util.LinkedList +import kotlin.reflect.KAnnotatedElement import kotlin.reflect.KClass import kotlin.reflect.KFunction +import kotlin.reflect.KParameter import kotlin.reflect.full.findAnnotation +import kotlin.reflect.full.functions +import kotlin.reflect.jvm.isAccessible class SKCCommand( private val mappings: List = ParameterMappings.DEFAULT, @@ -19,7 +44,7 @@ class SKCCommand( "${clazz.simpleName} must be annotated with @Command" } - val spec = clazz.newSpec(mappings, injector).build() + val spec = newSpec(clazz, mappings, injector).build() commandManager.register(plugin, spec, *command.aliases) } @@ -29,8 +54,188 @@ class SKCCommand( "${function.name} must be annotated with @Command" } - val spec = function.newSpec(mappings).build() + val spec = newSpec(function, mappings).build() commandManager.register(plugin, spec, *command.aliases) } + + fun newSpec(clazz: KClass<*>, mappings: List, injector: Injector? = null): CommandSpec.Builder { + val spec = CommandSpec.builder() + + val instance = clazz.objectInstance ?: requireNotNull(injector?.getInstance(clazz.java)) { + "Could not instantiate Command for ${clazz.simpleName}: must be an object, or provide an injector if class" + } + + val finalMappings = mappings + ObjectInstanceParameterMapping(clazz, instance) + + checkCommand(clazz) + applyAnnotations(clazz, spec) + + // Register child objects/classes. + for (childClass in clazz.nestedClasses) { + val aliases = childClass.findAnnotation()?.aliases ?: continue + val childSpec = newSpec(childClass, finalMappings).build() + spec.child(childSpec, *aliases) + } + + var hasDefault = false + + // Register child functions and the default executor, if available. + for (childFunction in clazz.functions) { + if (childFunction.findAnnotation() != null) { + // Found a default executor + require(!hasDefault) { "${clazz.simpleName} already has a default executor." } + + spec.arguments(mapParameters(childFunction, finalMappings)) + spec.executor(createExecutor(childFunction)) + + hasDefault = true + } + + val aliases = childFunction.findAnnotation()?.aliases ?: continue + val childSpec = newSpec(childFunction, finalMappings).build() + spec.child(childSpec, *aliases) + } + + return spec + } + + fun checkCommand(clazz: KClass<*>) { + require(!clazz.isAbstract) { "Unsupported class modifier: abstract" } + require(!clazz.isSealed) { "Unsupported class modifier: sealed" } + } + + fun newSpec(function: KFunction<*>, mappings: List): CommandSpec.Builder { + val spec = CommandSpec.builder() + + checkCommand(function) + applyAnnotations(function, spec) + + spec.arguments(mapParameters(function, mappings)) + spec.executor(createExecutor(function)) + + return spec + } + + fun checkCommand(function: KFunction<*>) { + require(function.returnType.isSubtypeOf() || function.returnType.isSubtypeOf()) { + "Unsupported return type (${function.returnType}), must be CommandResult or Unit" + } + + require(!function.isAbstract) { "Unsupported function modifier: abstract" } + require(!function.isExternal) { "Unsupported function modifier: external" } + require(!function.isInfix) { "Unsupported function modifier: infix" } + require(!function.isInline) { "Unsupported function modifier: inline" } + require(!function.isOperator) { "Unsupported function modifier: operator" } + require(!function.isSuspend) { "Unsupported function modifier: suspend" } + + function.isAccessible = true + } + + fun mapParameters(function: KFunction<*>, mappings: List): CommandElement { + val flags = GenericArguments.flags() + val elements = LinkedList() + + for (parameter in function.parameters) { + val flag = parameter.findAnnotation() + + if (flag != null) { + // Flag element. + if (parameter.type.isType()) { + // Boolean flag. + val permission = parameter.findAnnotation() + + when (permission) { + null -> flags.flag(*flag.specs) + else -> flags.permissionFlag(permission.value, *flag.specs) + } + } else { + // Value flag. + flags.valueFlag(match(mappings, parameter), *flag.specs) + } + } else { + // Non-flag element. + elements += match(mappings, parameter) + } + } + + return flags.buildWith(GenericArguments.seq(*elements.toTypedArray())) + } + + fun createExecutor(function: KFunction<*>): CommandExecutor = CommandExecutor { _, ctx -> + try { + val result = function.callBy(buildCallingArguments(function.parameters, ctx)) + + if (result is CommandResult) { + result + } else { + CommandResult.success() + } + } catch (e: InvocationTargetException) { + val cause = e.cause + + when (cause) { + is CommandException -> throw cause + null -> { + e.printStackTrace() + throw CommandException(!"An error occurred while executing that command.", e) + } + else -> { + e.printStackTrace() + throw CommandException(!"An error occurred while executing that command.", cause) + } + } + } + } + + fun applyAnnotations(element: KAnnotatedElement, spec: CommandSpec.Builder) { + element.findAnnotation()?.let { spec.permission(it.value) } + element.findAnnotation()?.let { spec.description(+it.value) } + } + + fun buildCallingArguments(parameters: List, ctx: CommandContext): Map { + val values = hashMapOf() + + for (parameter in parameters) { + if (ctx.hasAny(parameter.effectiveName)) { + // A value is available, use it. + values[parameter] = ctx.requireOne(parameter.effectiveName) + } else if (parameter.type.isMarkedNullable) { + // No value is available, but the type is nullable, so set it to null. + values[parameter] = null + } else if (!parameter.isOptional) { + // There is no available value to use! What do we do?! + throw CommandException(!"No value found for parameter '${parameter.effectiveName}'") + } + } + + return values + } + + fun match(mappings: List, parameter: KParameter): CommandElement { + var element: CommandElement? = null + + for (mapper in mappings) { + element = mapper(parameter)?.invoke(!parameter.effectiveName) + + if (element != null) break + } + + var result = requireNotNull(element) { + "Could not find a ParameterMapping that matches ${parameter.type}" + } + + parameter.findAnnotation()?.let { + result = GenericArguments.requiringPermission(result, it.value) + } + + if (parameter.isOptional || parameter.type.isMarkedNullable) { + result = when { + parameter.findAnnotation() != null -> GenericArguments.optionalWeak(result) + else -> GenericArguments.optional(result) + } + } + + return result + } } \ No newline at end of file