diff --git a/CHANGELOG.md b/CHANGELOG.md index cf36e71d..107c43c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + - Added `@Suggestions` annotated methods + ### Changed - Moved the parser injector registry into CommandManager and added injection to CommandContext 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 45aff3eb..e07a9ca3 100644 --- a/cloud-annotations/src/main/java/cloud/commandframework/annotations/AnnotationParser.java +++ b/cloud-annotations/src/main/java/cloud/commandframework/annotations/AnnotationParser.java @@ -29,6 +29,8 @@ import cloud.commandframework.Description; import cloud.commandframework.annotations.injection.ParameterInjectorRegistry; import cloud.commandframework.annotations.injection.RawArgs; import cloud.commandframework.annotations.specifier.Completions; +import cloud.commandframework.annotations.suggestions.MethodSuggestionsProvider; +import cloud.commandframework.annotations.suggestions.Suggestions; import cloud.commandframework.arguments.CommandArgument; import cloud.commandframework.arguments.flags.CommandFlag; import cloud.commandframework.arguments.parser.ArgumentParseResult; @@ -223,6 +225,9 @@ public final class AnnotationParser { */ @SuppressWarnings({"deprecation", "unchecked", "rawtypes"}) public @NonNull Collection<@NonNull Command> parse(final @NonNull T instance) { + /* Start by registering all @Suggestion annotated methods */ + this.parseSuggestions(instance); + /* Then construct commands from @CommandMethod annotated classes */ final Method[] methods = instance.getClass().getDeclaredMethods(); final Collection commandMethodPairs = new ArrayList<>(); for (final Method method : methods) { @@ -254,6 +259,38 @@ public final class AnnotationParser { return commands; } + @SuppressWarnings("deprecation") + private void parseSuggestions(final @NonNull T instance) { + for (final Method method : instance.getClass().getMethods()) { + final Suggestions suggestions = method.getAnnotation(Suggestions.class); + if (suggestions == null) { + continue; + } + if (!method.isAccessible()) { + method.setAccessible(true); + } + if (method.getParameterCount() != 2 + || !method.getReturnType().equals(List.class) + || !method.getParameters()[0].getType().equals(CommandContext.class) + || !method.getParameters()[1].getType().equals(String.class) + ) { + throw new IllegalArgumentException(String.format( + "@Suggestions annotated method '%s' in class '%s' does not have the correct signature", + method.getName(), + instance.getClass().getCanonicalName() + )); + } + try { + this.manager.getParserRegistry().registerSuggestionProvider( + suggestions.value(), + new MethodSuggestionsProvider<>(instance, method) + ); + } catch (final Exception e) { + throw new RuntimeException(e); + } + } + } + @SuppressWarnings("unchecked") private @NonNull Collection<@NonNull Command> construct( final @NonNull Object instance, diff --git a/cloud-annotations/src/main/java/cloud/commandframework/annotations/suggestions/MethodSuggestionsProvider.java b/cloud-annotations/src/main/java/cloud/commandframework/annotations/suggestions/MethodSuggestionsProvider.java new file mode 100644 index 00000000..e43836a9 --- /dev/null +++ b/cloud-annotations/src/main/java/cloud/commandframework/annotations/suggestions/MethodSuggestionsProvider.java @@ -0,0 +1,69 @@ +// +// MIT License +// +// Copyright (c) 2020 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 +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package cloud.commandframework.annotations.suggestions; + +import cloud.commandframework.context.CommandContext; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Method; +import java.util.List; +import java.util.function.BiFunction; + +/** + * Represents a method annotated with {@link Suggestions} + * + * @param Command sender type + * @since 1.3.0 + */ +public final class MethodSuggestionsProvider implements BiFunction, String, List> { + + private final MethodHandle methodHandle; + + /** + * Create a new provider + * + * @param instance Instance that owns the method + * @param method the annotated method + * @throws Exception If the method lookup fails + */ + public MethodSuggestionsProvider( + final @NonNull Object instance, + final @NonNull Method method + ) throws Exception { + this.methodHandle = MethodHandles.lookup().unreflect(method).bindTo(instance); + } + + @Override + @SuppressWarnings("unchecked") + public List apply(final CommandContext context, final String s) { + try { + return (List) this.methodHandle.invokeWithArguments(context, s); + } catch (final Throwable t) { + throw new RuntimeException(t); + } + } + +} diff --git a/cloud-annotations/src/main/java/cloud/commandframework/annotations/suggestions/Suggestions.java b/cloud-annotations/src/main/java/cloud/commandframework/annotations/suggestions/Suggestions.java new file mode 100644 index 00000000..ebf647dc --- /dev/null +++ b/cloud-annotations/src/main/java/cloud/commandframework/annotations/suggestions/Suggestions.java @@ -0,0 +1,53 @@ +// +// MIT License +// +// Copyright (c) 2020 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 +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package cloud.commandframework.annotations.suggestions; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation allows you to create annotated methods that behave like suggestions provider. + * The method must have this exact signature:
{@code
+ * ﹫Suggestions("name")
+ * public List methodName(CommandContext sender, String input) {
+ * }}
+ * + * @since 1.3.0 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Suggestions { + + /** + * Name of the suggestions provider. This should be the same as the name specified in your command arguments + * + * @return Suggestions provider name + */ + @NonNull String value(); + +} diff --git a/cloud-annotations/src/main/java/cloud/commandframework/annotations/suggestions/package-info.java b/cloud-annotations/src/main/java/cloud/commandframework/annotations/suggestions/package-info.java new file mode 100644 index 00000000..b1f5be6d --- /dev/null +++ b/cloud-annotations/src/main/java/cloud/commandframework/annotations/suggestions/package-info.java @@ -0,0 +1,28 @@ +// +// MIT License +// +// Copyright (c) 2020 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 +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +/** + * Classes related to {@link cloud.commandframework.annotations.suggestions.Suggestions} annotated methods + */ +package cloud.commandframework.annotations.suggestions; diff --git a/cloud-annotations/src/test/java/cloud/commandframework/annotations/AnnotationParserTest.java b/cloud-annotations/src/test/java/cloud/commandframework/annotations/AnnotationParserTest.java index 51dbe63a..744b439e 100644 --- a/cloud-annotations/src/test/java/cloud/commandframework/annotations/AnnotationParserTest.java +++ b/cloud-annotations/src/test/java/cloud/commandframework/annotations/AnnotationParserTest.java @@ -26,7 +26,9 @@ package cloud.commandframework.annotations; import cloud.commandframework.Command; import cloud.commandframework.CommandManager; import cloud.commandframework.annotations.specifier.Range; +import cloud.commandframework.annotations.suggestions.Suggestions; import cloud.commandframework.arguments.standard.StringArgument; +import cloud.commandframework.context.CommandContext; import cloud.commandframework.meta.SimpleCommandMeta; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; @@ -43,6 +45,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.concurrent.CompletionException; +import java.util.function.BiFunction; @TestInstance(TestInstance.Lifecycle.PER_CLASS) class AnnotationParserTest { @@ -129,6 +132,20 @@ class AnnotationParserTest { manager.executeCommand(new TestCommandSender(), "inject").join(); } + @Test + void testAnnotatedSuggestionsProviders() { + final BiFunction, String, List> suggestionsProvider = + this.manager.getParserRegistry().getSuggestionProvider("cows").orElse(null); + Assertions.assertNotNull(suggestionsProvider); + Assertions.assertTrue(suggestionsProvider.apply(new CommandContext<>(new TestCommandSender(), manager), "") + .contains("Stella")); + } + + @Suggestions("cows") + public List cowSuggestions(final CommandContext context, final String input) { + return Arrays.asList("Stella", "Bella", "Agda"); + } + @ProxiedBy("proxycommand") @CommandMethod("test|t literal [string]") public void testCommand(