diff --git a/cloud-annotations/src/main/java/cloud/commandframework/annotations/AnnotationParser.java b/cloud-annotations/src/main/java/cloud/commandframework/annotations/AnnotationParser.java index 144621e2..fff758ea 100644 --- a/cloud-annotations/src/main/java/cloud/commandframework/annotations/AnnotationParser.java +++ b/cloud-annotations/src/main/java/cloud/commandframework/annotations/AnnotationParser.java @@ -48,6 +48,9 @@ import cloud.commandframework.extra.confirmation.CommandConfirmationManager; import cloud.commandframework.meta.CommandMeta; import cloud.commandframework.meta.SimpleCommandMeta; import io.leangen.geantyref.TypeToken; + +import java.util.function.Predicate; + import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -85,6 +88,8 @@ public final class AnnotationParser { @NonNull Queue<@NonNull String>, @NonNull ArgumentParseResult>>> preprocessorMappers; private final Map, BiFunction, Command.Builder>> builderModifiers; + private final Map, Function, + MethodCommandExecutionHandler>> commandMethodFactories; private final Class commandSenderClass; private final MetaFactory metaFactory; private final FlagExtractor flagExtractor; @@ -110,6 +115,7 @@ public final class AnnotationParser { this.annotationMappers = new HashMap<>(); this.preprocessorMappers = new HashMap<>(); this.builderModifiers = new HashMap<>(); + this.commandMethodFactories = new HashMap<>(); this.flagExtractor = new FlagExtractor(manager); this.registerAnnotationMapper(CommandDescription.class, d -> ParserParameters.single(StandardParameters.DESCRIPTION, d.value())); @@ -180,6 +186,29 @@ public final class AnnotationParser { return getMethodOrClassAnnotation(method, clazz) != null; } + /** + * Returns the command manager that was used to create this parser + * + * @return Command manager + */ + public @NonNull CommandManager manager() { + return this.manager; + } + + /** + * Registers a new command execution method factory. This allows for the registration of + * custom command method execution strategies. + * + * @param predicate The predicate that decides whether or not to apply the custom execution handler to the given method + * @param function The function that produces the command execution handler + */ + public void registerCommandExecutionMethodFactory( + final @NonNull Predicate<@NonNull Method> predicate, + final @NonNull Function, MethodCommandExecutionHandler> function + ) { + this.commandMethodFactories.put(predicate, function); + } + /** * Register a builder modifier for a specific annotation. The builder modifiers are * allowed to act on a {@link Command.Builder} after all arguments have been added @@ -265,12 +294,6 @@ public final class AnnotationParser { if (!method.isAccessible()) { method.setAccessible(true); } - if (method.getReturnType() != Void.TYPE) { - throw new IllegalArgumentException(String.format( - "@CommandMethod annotated method '%s' has non-void return type", - method.getName() - )); - } if (Modifier.isStatic(method.getModifiers())) { throw new IllegalArgumentException(String.format( "@CommandMethod annotated method '%s' is static! @CommandMethod annotated methods should not be static.", @@ -460,13 +483,25 @@ public final class AnnotationParser { builder = builder.senderType(senderType); } try { - /* Construct the handler */ - final CommandExecutionHandler commandExecutionHandler = new MethodCommandExecutionHandler<>( + final MethodCommandExecutionHandler.CommandMethodContext context = new MethodCommandExecutionHandler.CommandMethodContext<>( instance, commandArguments, method, this.getParameterInjectorRegistry() ); + + /* Create the command execution handler */ + CommandExecutionHandler commandExecutionHandler = new MethodCommandExecutionHandler<>(context); + for (final Map.Entry, Function, MethodCommandExecutionHandler>> entry : + commandMethodFactories.entrySet()) { + if (entry.getKey().test(method)) { + commandExecutionHandler = entry.getValue().apply(context); + + /* Once we have our custom handler, we stop */ + break; + } + } + builder = builder.handler(commandExecutionHandler); } catch (final Exception e) { throw new RuntimeException("Failed to construct command execution handler", e); diff --git a/cloud-annotations/src/main/java/cloud/commandframework/annotations/MethodCommandExecutionHandler.java b/cloud-annotations/src/main/java/cloud/commandframework/annotations/MethodCommandExecutionHandler.java index c6489718..84683f2c 100644 --- a/cloud-annotations/src/main/java/cloud/commandframework/annotations/MethodCommandExecutionHandler.java +++ b/cloud-annotations/src/main/java/cloud/commandframework/annotations/MethodCommandExecutionHandler.java @@ -40,38 +40,61 @@ import java.util.List; import java.util.Map; import java.util.Optional; -class MethodCommandExecutionHandler implements CommandExecutionHandler { +/** + * A command execution handler that invokes a method. + * + * @param Command sender type. + */ +public class MethodCommandExecutionHandler implements CommandExecutionHandler { + private final CommandMethodContext context; private final Parameter[] parameters; private final MethodHandle methodHandle; - private final Map> commandArguments; - private final ParameterInjectorRegistry injectorRegistry; private final AnnotationAccessor annotationAccessor; - MethodCommandExecutionHandler( - final @NonNull Object instance, - final @NonNull Map<@NonNull String, @NonNull CommandArgument<@NonNull C, @NonNull ?>> commandArguments, - final @NonNull Method method, - final @NonNull ParameterInjectorRegistry injectorRegistry + /** + * Constructs a new method command execution handler + * + * @param context The context + */ + public MethodCommandExecutionHandler( + final @NonNull CommandMethodContext context ) throws Exception { - this.commandArguments = commandArguments; - method.setAccessible(true); - this.methodHandle = MethodHandles.lookup().unreflect(method).bindTo(instance); - this.parameters = method.getParameters(); - this.injectorRegistry = injectorRegistry; - this.annotationAccessor = AnnotationAccessor.of(method); + this.context = context; + this.methodHandle = MethodHandles.lookup().unreflect(context.method).bindTo(context.instance); + this.parameters = context.method.getParameters(); + this.annotationAccessor = AnnotationAccessor.of(context.method); } + /** + * {@inheritDoc} + */ @Override public void execute(final @NonNull CommandContext commandContext) { - final List arguments = new ArrayList<>(this.parameters.length); - final FlagContext flagContext = commandContext.flags(); + /* Invoke the command method */ + try { + this.methodHandle.invokeWithArguments(createParameterValues( + commandContext, + commandContext.flags(), + true) + ); + } catch (final Error e) { + throw e; + } catch (final Throwable throwable) { + throw new CommandExecutionException(throwable, commandContext); + } + } - /* Bind parameters to context */ + protected List createParameterValues( + final CommandContext commandContext, + final FlagContext flagContext, + final boolean throwOnMissing + ) { + final List arguments = new ArrayList<>(this.parameters.length); for (final Parameter parameter : this.parameters) { if (parameter.isAnnotationPresent(Argument.class)) { final Argument argument = parameter.getAnnotation(Argument.class); - final CommandArgument commandArgument = this.commandArguments.get(argument.value()); + final CommandArgument commandArgument = this.context.commandArguments.get(argument.value()); if (commandArgument.isRequired()) { arguments.add(commandContext.get(argument.value())); } else { @@ -89,14 +112,14 @@ class MethodCommandExecutionHandler implements CommandExecutionHandler { if (parameter.getType().isAssignableFrom(commandContext.getSender().getClass())) { arguments.add(commandContext.getSender()); } else { - final Optional value = this.injectorRegistry.getInjectable( + final Optional value = this.context.injectorRegistry.getInjectable( parameter.getType(), commandContext, AnnotationAccessor.of(AnnotationAccessor.of(parameter), this.annotationAccessor) ); if (value.isPresent()) { arguments.add(value.get()); - } else { + } else if (throwOnMissing) { throw new IllegalArgumentException(String.format( "Unknown command parameter '%s' in method '%s'", parameter.getName(), @@ -106,15 +129,106 @@ class MethodCommandExecutionHandler implements CommandExecutionHandler { } } } + return arguments; + } - /* Invoke the command method */ - try { - this.methodHandle.invokeWithArguments(arguments); - } catch (final Error e) { - throw e; - } catch (final Throwable throwable) { - throw new CommandExecutionException(throwable, commandContext); + /** + * Returns the command method context + * + * @return The context + */ + public @NonNull CommandMethodContext context() { + return this.context; + } + + /** + * Returns all parameters passed to the method + * + * @return The parameters + */ + public final @NonNull Parameter @NonNull [] parameters() { + return this.parameters; + } + + /** + * Returns the compiled method handle for the command method. + * + * @return The method handle + */ + public final @NonNull MethodHandle methodHandle() { + return this.methodHandle; + } + + /** + * The annotation accessor for the command method + * + * @return Annotation accessor + */ + public final AnnotationAccessor annotationAccessor() { + return this.annotationAccessor; + } + + /** + * Context for command methods + * + * @param Command sender type + */ + public static class CommandMethodContext { + + private final Object instance; + private final Map> commandArguments; + private final Method method; + private final ParameterInjectorRegistry injectorRegistry; + + CommandMethodContext( + final @NonNull Object instance, + final @NonNull Map<@NonNull String, @NonNull CommandArgument<@NonNull C, @NonNull ?>> commandArguments, + final @NonNull Method method, + final @NonNull ParameterInjectorRegistry injectorRegistry + ) { + this.instance = instance; + this.commandArguments = commandArguments; + this.method = method; + this.method.setAccessible(true); + this.injectorRegistry = injectorRegistry; } + + /** + * The instance that owns the command method + * + * @return The instance + */ + public @NonNull Object instance() { + return this.instance; + } + + /** + * The command method + * + * @return The method + */ + public final @NonNull Method method() { + return this.method; + } + + /** + * The compiled command arguments + * + * @return Compiled command arguments + */ + public final @NonNull Map<@NonNull String, @NonNull CommandArgument> commandArguments() { + return this.commandArguments; + } + + /** + * The injector registry + * + * @return Injector registry + */ + public final @NonNull ParameterInjectorRegistry injectorRegistry() { + return this.injectorRegistry; + } + } } diff --git a/cloud-core/src/main/java/cloud/commandframework/CommandManager.java b/cloud-core/src/main/java/cloud/commandframework/CommandManager.java index 5a51d3f2..afd8e576 100644 --- a/cloud-core/src/main/java/cloud/commandframework/CommandManager.java +++ b/cloud-core/src/main/java/cloud/commandframework/CommandManager.java @@ -910,6 +910,15 @@ public abstract class CommandManager { } } + /** + * Returns the command execution coordinator used in this manager + * + * @return Command execution coordinator + */ + public @NonNull CommandExecutionCoordinator commandExecutionCoordinator() { + return this.commandExecutionCoordinator; + } + /** * Transition from the {@code in} state to the {@code out} state, if the manager is not already in that state. * diff --git a/cloud-core/src/main/java/cloud/commandframework/execution/AsynchronousCommandExecutionCoordinator.java b/cloud-core/src/main/java/cloud/commandframework/execution/AsynchronousCommandExecutionCoordinator.java index d7932833..ee9e6117 100644 --- a/cloud-core/src/main/java/cloud/commandframework/execution/AsynchronousCommandExecutionCoordinator.java +++ b/cloud-core/src/main/java/cloud/commandframework/execution/AsynchronousCommandExecutionCoordinator.java @@ -81,13 +81,15 @@ public final class AsynchronousCommandExecutionCoordinator extends CommandExe final Consumer> commandConsumer = command -> { if (this.commandManager.postprocessContext(commandContext, command) == State.ACCEPTED) { - try { - command.getCommandExecutionHandler().execute(commandContext); - } catch (final CommandExecutionException exception) { - resultFuture.completeExceptionally(exception); - } catch (final Exception exception) { - resultFuture.completeExceptionally(new CommandExecutionException(exception, commandContext)); - } + command.getCommandExecutionHandler().executeFuture(commandContext).whenComplete((result, throwable) -> { + if (throwable != null) { + if (throwable instanceof CommandExecutionException) { + resultFuture.completeExceptionally(throwable); + } else { + resultFuture.completeExceptionally(new CommandExecutionException(throwable, commandContext)); + } + } + }); } }; diff --git a/cloud-core/src/main/java/cloud/commandframework/execution/CommandExecutionCoordinator.java b/cloud-core/src/main/java/cloud/commandframework/execution/CommandExecutionCoordinator.java index 0d27859d..dfe36fd0 100644 --- a/cloud-core/src/main/java/cloud/commandframework/execution/CommandExecutionCoordinator.java +++ b/cloud-core/src/main/java/cloud/commandframework/execution/CommandExecutionCoordinator.java @@ -118,7 +118,7 @@ public abstract class CommandExecutionCoordinator { final Command command = Objects.requireNonNull(pair.getFirst()); if (this.getCommandTree().getCommandManager().postprocessContext(commandContext, command) == State.ACCEPTED) { try { - command.getCommandExecutionHandler().execute(commandContext); + command.getCommandExecutionHandler().executeFuture(commandContext).get(); } catch (final CommandExecutionException exception) { completableFuture.completeExceptionally(exception); } catch (final Exception exception) { diff --git a/cloud-core/src/main/java/cloud/commandframework/execution/CommandExecutionHandler.java b/cloud-core/src/main/java/cloud/commandframework/execution/CommandExecutionHandler.java index f800aedf..04eb4a17 100644 --- a/cloud-core/src/main/java/cloud/commandframework/execution/CommandExecutionHandler.java +++ b/cloud-core/src/main/java/cloud/commandframework/execution/CommandExecutionHandler.java @@ -25,7 +25,11 @@ package cloud.commandframework.execution; import cloud.commandframework.Command; import cloud.commandframework.context.CommandContext; + +import java.util.concurrent.CompletableFuture; + import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; /** * Handler that is invoked whenever a {@link Command} is executed @@ -43,6 +47,23 @@ public interface CommandExecutionHandler { */ void execute(@NonNull CommandContext commandContext); + /** + * Handle command execution + * + * @param commandContext Command context + * @return future that completes when the command has finished execution + */ + default CompletableFuture<@Nullable Object> executeFuture(@NonNull CommandContext commandContext) { + final CompletableFuture future = new CompletableFuture<>(); + try { + execute(commandContext); + /* The command executed successfully */ + future.complete(null); + } catch (final Throwable throwable) { + future.completeExceptionally(throwable); + } + return future; + } /** * Command execution handler that does nothing @@ -57,4 +78,27 @@ public interface CommandExecutionHandler { } + /** + * Handler that is invoked whenever a {@link Command} is executed + * by a command sender + * + * @param Command sender type + */ + @FunctionalInterface + interface FutureCommandExecutionHandler extends CommandExecutionHandler { + + @Override + @SuppressWarnings("FunctionalInterfaceMethodChanged") + default void execute( + @NonNull CommandContext commandContext + ) { + } + + @Override + CompletableFuture<@Nullable Object> executeFuture( + @NonNull CommandContext commandContext + ); + + } + } diff --git a/cloud-core/src/main/java/cloud/commandframework/extra/confirmation/CommandConfirmationManager.java b/cloud-core/src/main/java/cloud/commandframework/extra/confirmation/CommandConfirmationManager.java index 472da044..023a039b 100644 --- a/cloud-core/src/main/java/cloud/commandframework/extra/confirmation/CommandConfirmationManager.java +++ b/cloud-core/src/main/java/cloud/commandframework/extra/confirmation/CommandConfirmationManager.java @@ -24,6 +24,7 @@ package cloud.commandframework.extra.confirmation; import cloud.commandframework.CommandManager; +import cloud.commandframework.context.CommandContext; import cloud.commandframework.execution.CommandExecutionHandler; import cloud.commandframework.execution.postprocessor.CommandPostprocessingContext; import cloud.commandframework.execution.postprocessor.CommandPostprocessor; @@ -31,6 +32,9 @@ import cloud.commandframework.meta.CommandMeta; import cloud.commandframework.meta.SimpleCommandMeta; import cloud.commandframework.services.types.ConsumerService; import cloud.commandframework.types.tuples.Pair; + +import java.util.concurrent.CompletableFuture; + import org.checkerframework.checker.nullness.qual.NonNull; import java.util.LinkedHashMap; @@ -158,20 +162,20 @@ public class CommandConfirmationManager { * @return Handler for a confirmation command */ public @NonNull CommandExecutionHandler createConfirmationExecutionHandler() { - return context -> { + return (CommandExecutionHandler.FutureCommandExecutionHandler) context -> { final Optional> pending = this.getPending(context.getSender()); if (pending.isPresent()) { final CommandPostprocessingContext postprocessingContext = pending.get(); - postprocessingContext.getCommand() + return postprocessingContext.getCommand() .getCommandExecutionHandler() - .execute(postprocessingContext.getCommandContext()); + .executeFuture(postprocessingContext.getCommandContext()); } else { this.errorNotifier.accept(context.getSender()); } + return CompletableFuture.completedFuture(null); }; } - private final class CommandConfirmationPostProcessor implements CommandPostprocessor { @Override diff --git a/cloud-kotlin-extensions/build.gradle.kts b/cloud-kotlin-extensions/build.gradle.kts index cb234268..560b01e8 100644 --- a/cloud-kotlin-extensions/build.gradle.kts +++ b/cloud-kotlin-extensions/build.gradle.kts @@ -4,6 +4,7 @@ import java.net.URL plugins { kotlin("jvm") version "1.4.31" id("org.jetbrains.dokka") version "1.4.20" + id("com.ncorti.ktfmt.gradle") version "0.6.0" } configurations.all { @@ -13,7 +14,14 @@ configurations.all { dependencies { api(project(":cloud-core")) implementation(kotlin("stdlib-jdk8")) + + implementation(project(":cloud-annotations")) + implementation("org.jetbrains.kotlin:kotlin-reflect:1.4.31") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.4.3") + testImplementation("org.jetbrains.kotlin", "kotlin-test-junit5") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.4.3") } tasks { @@ -42,3 +50,7 @@ tasks { kotlin { explicitApi() } + +ktfmt { + dropboxStyle() +} diff --git a/cloud-kotlin-extensions/src/main/kotlin/cloud/commandframework/kotlin/MutableCommandBuilder.kt b/cloud-kotlin-extensions/src/main/kotlin/cloud/commandframework/kotlin/MutableCommandBuilder.kt index 857945fe..ad953d6b 100644 --- a/cloud-kotlin-extensions/src/main/kotlin/cloud/commandframework/kotlin/MutableCommandBuilder.kt +++ b/cloud-kotlin-extensions/src/main/kotlin/cloud/commandframework/kotlin/MutableCommandBuilder.kt @@ -1,7 +1,7 @@ // // MIT License // -// Copyright (c) 2021 Alexander Söderberg & Contributors +// Copyright (c) 2021 Alexander Söderberg & Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -36,7 +36,8 @@ import cloud.commandframework.permission.CommandPermission import kotlin.reflect.KClass /** - * A mutable [Command.Builder] wrapper, providing functions to assist in creating commands using the Kotlin builder DSL style + * A mutable [Command.Builder] wrapper, providing functions to assist in creating commands using the + * Kotlin builder DSL style * * @since 1.3.0 */ @@ -54,7 +55,9 @@ public class MutableCommandBuilder { * @since 1.3.0 */ @Suppress("DEPRECATION") - @Deprecated(message = "ArgumentDescription should be used over Description", level = DeprecationLevel.HIDDEN) + @Deprecated( + message = "ArgumentDescription should be used over Description", + level = DeprecationLevel.HIDDEN) public constructor( name: String, description: Description = Description.empty(), @@ -75,10 +78,10 @@ public class MutableCommandBuilder { * @since 1.4.0 */ public constructor( - name: String, - description: ArgumentDescription = ArgumentDescription.empty(), - aliases: Array = emptyArray(), - commandManager: CommandManager + name: String, + description: ArgumentDescription = ArgumentDescription.empty(), + aliases: Array = emptyArray(), + commandManager: CommandManager ) { this.commandManager = commandManager this.commandBuilder = commandManager.commandBuilder(name, description, *aliases) @@ -95,7 +98,9 @@ public class MutableCommandBuilder { * @since 1.3.0 */ @Suppress("DEPRECATION") - @Deprecated(message = "ArgumentDescription should be used over Description", level = DeprecationLevel.HIDDEN) + @Deprecated( + message = "ArgumentDescription should be used over Description", + level = DeprecationLevel.HIDDEN) public constructor( name: String, description: Description = Description.empty(), @@ -117,19 +122,16 @@ public class MutableCommandBuilder { * @since 1.4.0 */ public constructor( - name: String, - description: ArgumentDescription = ArgumentDescription.empty(), - aliases: Array = emptyArray(), - commandManager: CommandManager, - lambda: MutableCommandBuilder.() -> Unit + name: String, + description: ArgumentDescription = ArgumentDescription.empty(), + aliases: Array = emptyArray(), + commandManager: CommandManager, + lambda: MutableCommandBuilder.() -> Unit ) : this(name, description, aliases, commandManager) { lambda(this) } - private constructor( - commandManager: CommandManager, - commandBuilder: Command.Builder - ) { + private constructor(commandManager: CommandManager, commandBuilder: Command.Builder) { this.commandManager = commandManager this.commandBuilder = commandBuilder } @@ -140,19 +142,17 @@ public class MutableCommandBuilder { * @return built command * @since 1.3.0 */ - public fun build(): Command = - this.commandBuilder.build() + public fun build(): Command = this.commandBuilder.build() /** - * Invoke the provided receiver lambda on this builder, then build a [Command] from the resulting state + * Invoke the provided receiver lambda on this builder, then build a [Command] from the + * resulting state * * @param lambda receiver lambda which will be invoked on builder before building * @return built command * @since 1.3.0 */ - public fun build( - lambda: MutableCommandBuilder.() -> Unit - ): Command { + public fun build(lambda: MutableCommandBuilder.() -> Unit): Command { lambda(this) return this.commandBuilder.build() } @@ -171,9 +171,7 @@ public class MutableCommandBuilder { return this } - private fun onlyMutate( - mutator: (Command.Builder) -> Command.Builder - ): Unit { + private fun onlyMutate(mutator: (Command.Builder) -> Command.Builder): Unit { mutate(mutator) } @@ -193,15 +191,12 @@ public class MutableCommandBuilder { * @return a copy of this mutable builder * @since 1.3.0 */ - public fun copy( - lambda: MutableCommandBuilder.() -> Unit - ): MutableCommandBuilder = - copy().apply { - lambda(this) - } + public fun copy(lambda: MutableCommandBuilder.() -> Unit): MutableCommandBuilder = + copy().apply { lambda(this) } /** - * Make a new copy of this [MutableCommandBuilder], append a literal, and invoke the provided receiver lambda on it + * Make a new copy of this [MutableCommandBuilder], append a literal, and invoke the provided + * receiver lambda on it * * @param literal name for the literal * @param description description for the literal @@ -210,7 +205,9 @@ public class MutableCommandBuilder { * @since 1.3.0 */ @Suppress("DEPRECATION") - @Deprecated(message = "ArgumentDescription should be used over Description", level = DeprecationLevel.HIDDEN) + @Deprecated( + message = "ArgumentDescription should be used over Description", + level = DeprecationLevel.HIDDEN) public fun copy( literal: String, description: Description, @@ -222,7 +219,8 @@ public class MutableCommandBuilder { } /** - * Make a new copy of this [MutableCommandBuilder], append a literal, and invoke the provided receiver lambda on it + * Make a new copy of this [MutableCommandBuilder], append a literal, and invoke the provided + * receiver lambda on it * * @param literal name for the literal * @param description description for the literal @@ -231,17 +229,18 @@ public class MutableCommandBuilder { * @since 1.4.0 */ public fun copy( - literal: String, - description: ArgumentDescription, - lambda: MutableCommandBuilder.() -> Unit + literal: String, + description: ArgumentDescription, + lambda: MutableCommandBuilder.() -> Unit ): MutableCommandBuilder = - copy().apply { - literal(literal, description) - lambda(this) - } + copy().apply { + literal(literal, description) + lambda(this) + } /** - * Make a new copy of this [MutableCommandBuilder], append a literal, and invoke the provided receiver lambda on it + * Make a new copy of this [MutableCommandBuilder], append a literal, and invoke the provided + * receiver lambda on it * * @param literal name for the literal * @param lambda receiver lambda which will be invoked on the new builder @@ -264,29 +263,23 @@ public class MutableCommandBuilder { * @see [CommandManager.command] * @since 1.3.0 */ - public fun register(): MutableCommandBuilder = - apply { - this.commandManager.command(this) - } + public fun register(): MutableCommandBuilder = apply { this.commandManager.command(this) } /** - * Create a new copy of this mutable builder, act on it with a receiver lambda, and then register it with the owning - * command manager + * Create a new copy of this mutable builder, act on it with a receiver lambda, and then + * register it with the owning command manager * * @param lambda receiver lambda which will be invoked on the new builder * @return the new mutable builder * @see [CommandManager.command] * @since 1.3.0 */ - public fun registerCopy( - lambda: MutableCommandBuilder.() -> Unit - ): MutableCommandBuilder = + public fun registerCopy(lambda: MutableCommandBuilder.() -> Unit): MutableCommandBuilder = copy(lambda).register() /** - * Create a new copy of this mutable builder, append a literal, act on it with a receiver lambda, and then register it with - * the owning - * command manager + * Create a new copy of this mutable builder, append a literal, act on it with a receiver + * lambda, and then register it with the owning command manager * * @param literal name for the literal * @param lambda receiver lambda which will be invoked on the new builder @@ -297,13 +290,11 @@ public class MutableCommandBuilder { public fun registerCopy( literal: String, lambda: MutableCommandBuilder.() -> Unit - ): MutableCommandBuilder = - copy(literal, lambda).register() + ): MutableCommandBuilder = copy(literal, lambda).register() /** - * Create a new copy of this mutable builder, append a literal, act on it with a receiver lambda, and then register it with - * the owning - * command manager + * Create a new copy of this mutable builder, append a literal, act on it with a receiver + * lambda, and then register it with the owning command manager * * @param literal name for the literal * @param description description for the literal @@ -313,18 +304,18 @@ public class MutableCommandBuilder { * @since 1.3.0 */ @Suppress("DEPRECATION") - @Deprecated(message = "ArgumentDescription should be used over Description", level = DeprecationLevel.HIDDEN) + @Deprecated( + message = "ArgumentDescription should be used over Description", + level = DeprecationLevel.HIDDEN) public fun registerCopy( literal: String, description: Description, lambda: MutableCommandBuilder.() -> Unit - ): MutableCommandBuilder = - copy(literal, description, lambda).register() + ): MutableCommandBuilder = copy(literal, description, lambda).register() /** - * Create a new copy of this mutable builder, append a literal, act on it with a receiver lambda, and then register it with - * the owning - * command manager + * Create a new copy of this mutable builder, append a literal, act on it with a receiver + * lambda, and then register it with the owning command manager * * @param literal name for the literal * @param description description for the literal @@ -334,11 +325,10 @@ public class MutableCommandBuilder { * @since 1.4.0 */ public fun registerCopy( - literal: String, - description: ArgumentDescription, - lambda: MutableCommandBuilder.() -> Unit - ): MutableCommandBuilder = - copy(literal, description, lambda).register() + literal: String, + description: ArgumentDescription, + lambda: MutableCommandBuilder.() -> Unit + ): MutableCommandBuilder = copy(literal, description, lambda).register() /** * Set the value for a certain [CommandMeta.Key] in the command meta storage for this builder @@ -349,11 +339,10 @@ public class MutableCommandBuilder { * @return this mutable builder * @since 1.3.0 */ - public fun meta( - key: CommandMeta.Key, - value: T - ): MutableCommandBuilder = - mutate { it.meta(key, value) } + public fun meta(key: CommandMeta.Key, value: T): MutableCommandBuilder = + mutate { + it.meta(key, value) + } /** * Set the value for a certain [CommandMeta.Key] in the command meta storage for this builder @@ -363,9 +352,7 @@ public class MutableCommandBuilder { * @return this mutable builder * @since 1.3.0 */ - public infix fun CommandMeta.Key.to( - value: T - ): MutableCommandBuilder = + public infix fun CommandMeta.Key.to(value: T): MutableCommandBuilder = meta(this, value) /** @@ -375,9 +362,7 @@ public class MutableCommandBuilder { * @return this mutable builder * @since 1.3.0 */ - public fun commandDescription( - description: String - ): MutableCommandBuilder = + public fun commandDescription(description: String): MutableCommandBuilder = meta(CommandMeta.DESCRIPTION, description) /** @@ -387,9 +372,7 @@ public class MutableCommandBuilder { * @return this mutable builder * @since 1.3.0 */ - public fun longCommandDescription( - description: String - ): MutableCommandBuilder = + public fun longCommandDescription(description: String): MutableCommandBuilder = meta(CommandMeta.LONG_DESCRIPTION, description) /** @@ -399,9 +382,7 @@ public class MutableCommandBuilder { * @return this mutable builder * @since 1.3.0 */ - public fun hidden( - hidden: Boolean = true - ): MutableCommandBuilder = + public fun hidden(hidden: Boolean = true): MutableCommandBuilder = meta(CommandMeta.HIDDEN, hidden) /** @@ -411,8 +392,9 @@ public class MutableCommandBuilder { * @return this mutable builder * @since 1.3.0 */ - public inline fun senderType(): MutableCommandBuilder = - mutate { it.senderType(T::class) } + public inline fun senderType(): MutableCommandBuilder = mutate { + it.senderType(T::class) + } /** * Specify a required sender type @@ -421,10 +403,9 @@ public class MutableCommandBuilder { * @return this mutable builder * @since 1.3.0 */ - public fun senderType( - type: KClass - ): MutableCommandBuilder = - mutate { it.senderType(type) } + public fun senderType(type: KClass): MutableCommandBuilder = mutate { + it.senderType(type) + } /** * Field to get and set the required sender type for this command builder @@ -445,10 +426,9 @@ public class MutableCommandBuilder { * @return this mutable builder * @since 1.3.0 */ - public fun senderType( - type: Class - ): MutableCommandBuilder = - mutate { it.senderType(type) } + public fun senderType(type: Class): MutableCommandBuilder = mutate { + it.senderType(type) + } /** * Specify a permission required to execute this command @@ -457,10 +437,9 @@ public class MutableCommandBuilder { * @return this mutable builder * @since 1.3.0 */ - public fun permission( - permission: String - ): MutableCommandBuilder = - mutate { it.permission(permission) } + public fun permission(permission: String): MutableCommandBuilder = mutate { + it.permission(permission) + } /** * Specify a permission required to execute this command @@ -469,10 +448,9 @@ public class MutableCommandBuilder { * @return this mutable builder * @since 1.3.0 */ - public fun permission( - permission: CommandPermission - ): MutableCommandBuilder = - mutate { it.permission(permission) } + public fun permission(permission: CommandPermission): MutableCommandBuilder = mutate { + it.permission(permission) + } /** * Field to get and set the required permission for this command builder @@ -501,12 +479,13 @@ public class MutableCommandBuilder { * @since 1.3.0 */ @Suppress("DEPRECATION") - @Deprecated(message = "ArgumentDescription should be used over Description", level = DeprecationLevel.HIDDEN) + @Deprecated( + message = "ArgumentDescription should be used over Description", + level = DeprecationLevel.HIDDEN) public fun argument( argument: CommandArgument, description: Description = Description.empty() - ): MutableCommandBuilder = - mutate { it.argument(argument, description) } + ): MutableCommandBuilder = mutate { it.argument(argument, description) } /** * Add a new argument to this command @@ -517,10 +496,9 @@ public class MutableCommandBuilder { * @since 1.4.0 */ public fun argument( - argument: CommandArgument, - description: ArgumentDescription = ArgumentDescription.empty() - ): MutableCommandBuilder = - mutate { it.argument(argument, description) } + argument: CommandArgument, + description: ArgumentDescription = ArgumentDescription.empty() + ): MutableCommandBuilder = mutate { it.argument(argument, description) } /** * Add a new argument to this command @@ -531,12 +509,13 @@ public class MutableCommandBuilder { * @since 1.3.0 */ @Suppress("DEPRECATION") - @Deprecated(message = "ArgumentDescription should be used over Description", level = DeprecationLevel.HIDDEN) + @Deprecated( + message = "ArgumentDescription should be used over Description", + level = DeprecationLevel.HIDDEN) public fun argument( argument: CommandArgument.Builder, description: Description = Description.empty() - ): MutableCommandBuilder = - mutate { it.argument(argument, description) } + ): MutableCommandBuilder = mutate { it.argument(argument, description) } /** * Add a new argument to this command @@ -547,10 +526,9 @@ public class MutableCommandBuilder { * @since 1.4.0 */ public fun argument( - argument: CommandArgument.Builder, - description: ArgumentDescription = ArgumentDescription.empty() - ): MutableCommandBuilder = - mutate { it.argument(argument, description) } + argument: CommandArgument.Builder, + description: ArgumentDescription = ArgumentDescription.empty() + ): MutableCommandBuilder = mutate { it.argument(argument, description) } /** * Add a new argument to this command @@ -561,12 +539,13 @@ public class MutableCommandBuilder { * @since 1.3.0 */ @Suppress("DEPRECATION") - @Deprecated(message = "ArgumentDescription should be used over Description", level = DeprecationLevel.HIDDEN) + @Deprecated( + message = "ArgumentDescription should be used over Description", + level = DeprecationLevel.HIDDEN) public fun argument( description: Description = Description.empty(), argumentSupplier: () -> CommandArgument - ): MutableCommandBuilder = - mutate { it.argument(argumentSupplier(), description) } + ): MutableCommandBuilder = mutate { it.argument(argumentSupplier(), description) } /** * Add a new argument to this command @@ -577,10 +556,9 @@ public class MutableCommandBuilder { * @since 1.4.0 */ public fun argument( - description: ArgumentDescription = ArgumentDescription.empty(), - argumentSupplier: () -> CommandArgument - ): MutableCommandBuilder = - mutate { it.argument(argumentSupplier(), description) } + description: ArgumentDescription = ArgumentDescription.empty(), + argumentSupplier: () -> CommandArgument + ): MutableCommandBuilder = mutate { it.argument(argumentSupplier(), description) } /** * Add a new literal argument to this command @@ -592,13 +570,14 @@ public class MutableCommandBuilder { * @since 1.3.0 */ @Suppress("DEPRECATION") - @Deprecated(message = "ArgumentDescription should be used over Description", level = DeprecationLevel.HIDDEN) + @Deprecated( + message = "ArgumentDescription should be used over Description", + level = DeprecationLevel.HIDDEN) public fun literal( name: String, description: Description = Description.empty(), vararg aliases: String - ): MutableCommandBuilder = - mutate { it.literal(name, description, *aliases) } + ): MutableCommandBuilder = mutate { it.literal(name, description, *aliases) } /** * Add a new literal argument to this command @@ -610,11 +589,10 @@ public class MutableCommandBuilder { * @since 1.4.0 */ public fun literal( - name: String, - description: ArgumentDescription = ArgumentDescription.empty(), - vararg aliases: String - ): MutableCommandBuilder = - mutate { it.literal(name, description, *aliases) } + name: String, + description: ArgumentDescription = ArgumentDescription.empty(), + vararg aliases: String + ): MutableCommandBuilder = mutate { it.literal(name, description, *aliases) } /** * Set the [CommandExecutionHandler] for this builder @@ -623,10 +601,9 @@ public class MutableCommandBuilder { * @return this mutable builder * @since 1.3.0 */ - public fun handler( - handler: CommandExecutionHandler - ): MutableCommandBuilder = - mutate { it.handler(handler) } + public fun handler(handler: CommandExecutionHandler): MutableCommandBuilder = mutate { + it.handler(handler) + } /** * Add a new flag argument to this command @@ -645,12 +622,12 @@ public class MutableCommandBuilder { argumentSupplier: () -> CommandArgument ): MutableCommandBuilder = mutate { it.flag( - this.commandManager.flagBuilder(name) + this.commandManager + .flagBuilder(name) .withAliases(*aliases) .withDescription(description) .withArgument(argumentSupplier()) - .build() - ) + .build()) } /** @@ -670,12 +647,12 @@ public class MutableCommandBuilder { argument: CommandArgument ): MutableCommandBuilder = mutate { it.flag( - this.commandManager.flagBuilder(name) + this.commandManager + .flagBuilder(name) .withAliases(*aliases) .withDescription(description) .withArgument(argument) - .build() - ) + .build()) } /** @@ -695,12 +672,12 @@ public class MutableCommandBuilder { argumentBuilder: CommandArgument.Builder ): MutableCommandBuilder = mutate { it.flag( - this.commandManager.flagBuilder(name) + this.commandManager + .flagBuilder(name) .withAliases(*aliases) .withDescription(description) .withArgument(argumentBuilder) - .build() - ) + .build()) } /** @@ -718,10 +695,10 @@ public class MutableCommandBuilder { description: ArgumentDescription = ArgumentDescription.empty(), ): MutableCommandBuilder = mutate { it.flag( - this.commandManager.flagBuilder(name) + this.commandManager + .flagBuilder(name) .withAliases(*aliases) .withDescription(description) - .build() - ) + .build()) } } diff --git a/cloud-kotlin-extensions/src/main/kotlin/cloud/commandframework/kotlin/coroutines/KotlinAnnotatedMethods.kt b/cloud-kotlin-extensions/src/main/kotlin/cloud/commandframework/kotlin/coroutines/KotlinAnnotatedMethods.kt new file mode 100644 index 00000000..e7f80320 --- /dev/null +++ b/cloud-kotlin-extensions/src/main/kotlin/cloud/commandframework/kotlin/coroutines/KotlinAnnotatedMethods.kt @@ -0,0 +1,51 @@ +package cloud.commandframework.kotlin.coroutines + +import cloud.commandframework.annotations.AnnotationParser +import cloud.commandframework.annotations.MethodCommandExecutionHandler +import cloud.commandframework.context.CommandContext +import cloud.commandframework.execution.CommandExecutionCoordinator +import java.lang.reflect.Method +import java.util.concurrent.CompletableFuture +import java.util.function.Predicate +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.reflect.full.callSuspend +import kotlin.reflect.jvm.kotlinFunction +import kotlinx.coroutines.* +import kotlinx.coroutines.future.asCompletableFuture + +/** Adds coroutine support to the [AnnotationParser]. */ +public fun AnnotationParser.installCoroutineSupport( + scope: CoroutineScope = GlobalScope, + context: CoroutineContext = EmptyCoroutineContext +) { + if (manager().commandExecutionCoordinator() is CommandExecutionCoordinator.SimpleCoordinator) { + RuntimeException( + """You are highly advised to not use the simple command execution coordinator together + with coroutine support. Consider using the asynchronous command execution coordinator instead.""") + .printStackTrace() + } + + val predicate = Predicate { it.kotlinFunction?.isSuspend == true } + registerCommandExecutionMethodFactory(predicate) { + KotlinMethodCommandExecutionHandler(scope, context, it) + } +} + +private class KotlinMethodCommandExecutionHandler( + private val coroutineScope: CoroutineScope, + private val coroutineContext: CoroutineContext, + context: CommandMethodContext +) : MethodCommandExecutionHandler(context) { + + override fun executeFuture(commandContext: CommandContext): CompletableFuture { + val instance = context().instance() + val params = createParameterValues(commandContext, commandContext.flags(), false) + // We need to propagate exceptions to the caller. + return coroutineScope + .async(this@KotlinMethodCommandExecutionHandler.coroutineContext) { + context().method().kotlinFunction?.callSuspend(instance, *params.toTypedArray()) + } + .asCompletableFuture() + } +} diff --git a/cloud-kotlin-extensions/src/main/kotlin/cloud/commandframework/kotlin/extension/CommandBuildingExtensions.kt b/cloud-kotlin-extensions/src/main/kotlin/cloud/commandframework/kotlin/extension/CommandBuildingExtensions.kt index 76d3a2b3..864252ee 100644 --- a/cloud-kotlin-extensions/src/main/kotlin/cloud/commandframework/kotlin/extension/CommandBuildingExtensions.kt +++ b/cloud-kotlin-extensions/src/main/kotlin/cloud/commandframework/kotlin/extension/CommandBuildingExtensions.kt @@ -1,7 +1,7 @@ // // MIT License // -// Copyright (c) 2021 Alexander Söderberg & Contributors +// Copyright (c) 2021 Alexander Söderberg & Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -40,14 +40,15 @@ import kotlin.reflect.KClass * @since 1.3.0 */ @Suppress("DEPRECATION") -@Deprecated(message = "ArgumentDescription should be used over Description", level = DeprecationLevel.HIDDEN) +@Deprecated( + message = "ArgumentDescription should be used over Description", + level = DeprecationLevel.HIDDEN) public fun CommandManager.commandBuilder( - name: String, - description: Description = Description.empty(), - aliases: Array = emptyArray(), - lambda: MutableCommandBuilder.() -> Unit -): MutableCommandBuilder = - MutableCommandBuilder(name, description, aliases, this, lambda) + name: String, + description: Description = Description.empty(), + aliases: Array = emptyArray(), + lambda: MutableCommandBuilder.() -> Unit +): MutableCommandBuilder = MutableCommandBuilder(name, description, aliases, this, lambda) /** * Create a new [MutableCommandBuilder] and invoke the provided receiver lambda on it @@ -59,16 +60,15 @@ public fun CommandManager.commandBuilder( * @since 1.4.0 */ public fun CommandManager.commandBuilder( - name: String, - description: ArgumentDescription = ArgumentDescription.empty(), - aliases: Array = emptyArray(), - lambda: MutableCommandBuilder.() -> Unit -): MutableCommandBuilder = - MutableCommandBuilder(name, description, aliases, this, lambda) + name: String, + description: ArgumentDescription = ArgumentDescription.empty(), + aliases: Array = emptyArray(), + lambda: MutableCommandBuilder.() -> Unit +): MutableCommandBuilder = MutableCommandBuilder(name, description, aliases, this, lambda) /** - * Create a new [MutableCommandBuilder] which will invoke the provided receiver lambda, and then register itself with the - * owning [CommandManager] + * Create a new [MutableCommandBuilder] which will invoke the provided receiver lambda, and then + * register itself with the owning [CommandManager] * * @param name name for the root command node * @param description description for the root command node @@ -77,18 +77,19 @@ public fun CommandManager.commandBuilder( * @since 1.3.0 */ @Suppress("DEPRECATION") -@Deprecated(message = "ArgumentDescription should be used over Description", level = DeprecationLevel.HIDDEN) +@Deprecated( + message = "ArgumentDescription should be used over Description", + level = DeprecationLevel.HIDDEN) public fun CommandManager.buildAndRegister( - name: String, - description: Description = Description.empty(), - aliases: Array = emptyArray(), - lambda: MutableCommandBuilder.() -> Unit -): MutableCommandBuilder = - commandBuilder(name, description, aliases, lambda).register() + name: String, + description: Description = Description.empty(), + aliases: Array = emptyArray(), + lambda: MutableCommandBuilder.() -> Unit +): MutableCommandBuilder = commandBuilder(name, description, aliases, lambda).register() /** - * Create a new [MutableCommandBuilder] which will invoke the provided receiver lambda, and then register itself with the - * owning [CommandManager] + * Create a new [MutableCommandBuilder] which will invoke the provided receiver lambda, and then + * register itself with the owning [CommandManager] * * @param name name for the root command node * @param description description for the root command node @@ -97,15 +98,15 @@ public fun CommandManager.buildAndRegister( * @since 1.4.0 */ public fun CommandManager.buildAndRegister( - name: String, - description: ArgumentDescription = ArgumentDescription.empty(), - aliases: Array = emptyArray(), - lambda: MutableCommandBuilder.() -> Unit -): MutableCommandBuilder = - commandBuilder(name, description, aliases, lambda).register() + name: String, + description: ArgumentDescription = ArgumentDescription.empty(), + aliases: Array = emptyArray(), + lambda: MutableCommandBuilder.() -> Unit +): MutableCommandBuilder = commandBuilder(name, description, aliases, lambda).register() /** - * Build the provided [MutableCommandBuilder]s into [Command]s, and then register them with the command manager + * Build the provided [MutableCommandBuilder]s into [Command]s, and then register them with the + * command manager * * @param commands mutable command builder(s) to register * @return the command manager @@ -113,13 +114,8 @@ public fun CommandManager.buildAndRegister( * @since 1.3.0 */ public fun CommandManager.command( - vararg commands: MutableCommandBuilder -): CommandManager = - apply { - commands.forEach { command -> - this.command(command.build()) - } - } + vararg commands: MutableCommandBuilder +): CommandManager = apply { commands.forEach { command -> this.command(command.build()) } } /** * Specify a required sender type @@ -129,7 +125,7 @@ public fun CommandManager.command( * @since 1.3.0 */ public fun Command.Builder.senderType(type: KClass): Command.Builder = - senderType(type.java) + senderType(type.java) /** * Get a [Description], defaulting to [Description.empty] @@ -140,13 +136,10 @@ public fun Command.Builder.senderType(type: KClass): Command */ @Suppress("DEPRECATION") @Deprecated( - message = "Use interface variant that allows for rich text", - replaceWith = ReplaceWith("argumentDescription(description)") -) -public fun description( - description: String = "" -): Description = - if (description.isEmpty()) Description.empty() else Description.of(description) + message = "Use interface variant that allows for rich text", + replaceWith = ReplaceWith("argumentDescription(description)")) +public fun description(description: String = ""): Description = + if (description.isEmpty()) Description.empty() else Description.of(description) /** * Get a [ArgumentDescription], defaulting to [ArgumentDescription.empty] @@ -155,7 +148,5 @@ public fun description( * @return the description * @since 1.4.0 */ -public fun argumentDescription( - description: String = "" -): ArgumentDescription = - if (description.isEmpty()) ArgumentDescription.empty() else ArgumentDescription.of(description) +public fun argumentDescription(description: String = ""): ArgumentDescription = + if (description.isEmpty()) ArgumentDescription.empty() else ArgumentDescription.of(description) diff --git a/cloud-kotlin-extensions/src/test/kotlin/cloud/commandframework/kotlin/coroutines/KotlinAnnotatedMethodsTest.kt b/cloud-kotlin-extensions/src/test/kotlin/cloud/commandframework/kotlin/coroutines/KotlinAnnotatedMethodsTest.kt new file mode 100644 index 00000000..2baa8476 --- /dev/null +++ b/cloud-kotlin-extensions/src/test/kotlin/cloud/commandframework/kotlin/coroutines/KotlinAnnotatedMethodsTest.kt @@ -0,0 +1,68 @@ +package cloud.commandframework.kotlin.coroutines + +import cloud.commandframework.CommandManager +import cloud.commandframework.annotations.AnnotationParser +import cloud.commandframework.annotations.CommandMethod +import cloud.commandframework.execution.AsynchronousCommandExecutionCoordinator +import cloud.commandframework.internal.CommandRegistrationHandler +import cloud.commandframework.meta.CommandMeta +import cloud.commandframework.meta.SimpleCommandMeta +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.future.await +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +class KotlinAnnotatedMethodsTest { + + companion object { + val executorService = Executors.newSingleThreadExecutor() + } + + private lateinit var commandManager: CommandManager + + @BeforeEach + fun setUp() { + commandManager = TestCommandManager() + } + + private fun awaitCommands() { + executorService.shutdown() + executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS) + } + + @Test + fun `test suspending command methods`(): Unit = runBlocking { + AnnotationParser(commandManager, TestCommandSender::class.java) { + SimpleCommandMeta.empty() + }.also { it.installCoroutineSupport() }.parse(CommandMethods()) + + commandManager.executeCommand(TestCommandSender(), "test").await() + } + + private class TestCommandSender {} + + private class TestCommandManager : CommandManager( + AsynchronousCommandExecutionCoordinator.newBuilder().withExecutor(executorService).build(), + CommandRegistrationHandler.nullCommandRegistrationHandler() + ) { + + override fun hasPermission(sender: TestCommandSender, permission: String): Boolean = true + + override fun createDefaultCommandMeta(): CommandMeta = SimpleCommandMeta.empty() + + } + + public class CommandMethods { + + @CommandMethod("test") + public suspend fun suspendingCommand(): Unit = withContext(Dispatchers.Default) { + println("called from thread: ${Thread.currentThread().name}") + } + + } + +}