From 3c7bd63f07203335d1b26a4195add3b08ef60307 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20S=C3=B6derberg?= Date: Thu, 22 Oct 2020 04:30:27 +0200 Subject: [PATCH] :sparkles: Add named suggestion providers This allows for pre-registration of command suggestion providers, that can then be used in annotated command methods. --- .../annotations/AnnotationParser.java | 26 +++++++++++++---- .../annotations/Argument.java | 17 +++++++++++ .../annotations/AnnotationParserTest.java | 24 ++++++++++++++-- .../cloud/commandframework/CommandTree.java | 2 +- .../arguments/parser/ParserRegistry.java | 28 +++++++++++++++++++ .../parser/StandardParserRegistry.java | 23 +++++++++++++++ 6 files changed, 112 insertions(+), 8 deletions(-) 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 5c684f20..f489d59d 100644 --- a/cloud-annotations/src/main/java/cloud/commandframework/annotations/AnnotationParser.java +++ b/cloud-annotations/src/main/java/cloud/commandframework/annotations/AnnotationParser.java @@ -52,7 +52,9 @@ import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Queue; import java.util.function.BiFunction; import java.util.function.Function; @@ -300,7 +302,7 @@ public final class AnnotationParser { final TypeToken token = TypeToken.get(parameter.getParameterizedType()); final ParserParameters parameters = this.manager.getParserRegistry() .parseAnnotations(token, annotations); - + /* Create the argument parser */ final ArgumentParser parser; if (argumentPair.getArgument().parserName().isEmpty()) { parser = this.manager.getParserRegistry() @@ -323,17 +325,19 @@ public final class AnnotationParser { token.toString() ))); } - + /* Check whether or not the corresponding method parameter actually exists */ if (syntaxFragment == null || syntaxFragment.getArgumentMode() == ArgumentMode.LITERAL) { throw new IllegalArgumentException(String.format( "Invalid command argument '%s' in method '%s': " + "Missing syntax mapping", argumentPair.getArgument().value(), method.getName())); } final Argument argument = argumentPair.getArgument(); + /* Create the argument builder */ @SuppressWarnings("ALL") final CommandArgument.Builder argumentBuilder = CommandArgument.ofType( parameter.getType(), argument.value() ); + /* Set the argument requirement status */ if (syntaxFragment.getArgumentMode() == ArgumentMode.OPTIONAL) { if (argument.defaultValue().isEmpty()) { argumentBuilder.asOptional(); @@ -343,9 +347,21 @@ public final class AnnotationParser { } else { argumentBuilder.asRequired(); } - + /* Check whether or not a suggestion provider should be set */ + if (!argument.suggestions().isEmpty()) { + final String suggestionProviderName = argument.suggestions(); + final Optional, String, List>> suggestionsFunction = + this.manager.getParserRegistry().getSuggestionProvider(suggestionProviderName); + argumentBuilder.withSuggestionsProvider( + suggestionsFunction.orElseThrow(() -> + new IllegalArgumentException(String.format( + "There is no suggestion provider with name '%s'. Did you forget to register it?", + suggestionProviderName + ))) + ); + } + /* Build the argument */ final CommandArgument builtArgument = argumentBuilder.manager(this.manager).withParser(parser).build(); - /* Add preprocessors */ for (final Annotation annotation : annotations) { @SuppressWarnings("ALL") final Function preprocessorMapper = @@ -357,7 +373,7 @@ public final class AnnotationParser { builtArgument.addPreprocessor(preprocessor); } } - + /* Yay, we're done */ return builtArgument; } diff --git a/cloud-annotations/src/main/java/cloud/commandframework/annotations/Argument.java b/cloud-annotations/src/main/java/cloud/commandframework/annotations/Argument.java index 4e42a7f1..e9fc1aef 100644 --- a/cloud-annotations/src/main/java/cloud/commandframework/annotations/Argument.java +++ b/cloud-annotations/src/main/java/cloud/commandframework/annotations/Argument.java @@ -27,6 +27,7 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.function.BiFunction; /** * Annotation used to indicate that a method parameter is a command argument @@ -49,6 +50,22 @@ public @interface Argument { */ String parserName() default ""; + /** + * Name of the suggestions provider to use. If the string is left empty, the default + * provider for the argument parser will be used. Otherwise, + * the {@link cloud.commandframework.arguments.parser.ParserRegistry} instance in the + * {@link cloud.commandframework.CommandManager} will be queried for a matching suggestion provider. + *

+ * For this to work, the suggestion needs to be registered in the parser registry. To do this, use + * {@link cloud.commandframework.arguments.parser.ParserRegistry#registerSuggestionProvider(String, BiFunction)}. + * The registry instance can be retrieved using {@link cloud.commandframework.CommandManager#getParserRegistry()}. + * + * @return The name of the suggestion provider, or {@code ""} if the default suggestion provider for the argument parser + * should be used instead + * @since 1.1.0 + */ + String suggestions() default ""; + /** * Get the default value * 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 f151dff7..519b7bec 100644 --- a/cloud-annotations/src/test/java/cloud/commandframework/annotations/AnnotationParserTest.java +++ b/cloud-annotations/src/test/java/cloud/commandframework/annotations/AnnotationParserTest.java @@ -32,12 +32,16 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.concurrent.CompletionException; class AnnotationParserTest { + private static final List NAMED_SUGGESTIONS = Arrays.asList("Dancing-Queen", "Gimme!-Gimme!-Gimme!", "Waterloo"); + private static CommandManager manager; private static AnnotationParser annotationParser; @@ -47,6 +51,11 @@ class AnnotationParserTest { annotationParser = new AnnotationParser<>(manager, TestCommandSender.class, p -> SimpleCommandMeta.empty()); manager.getParserRegistry().registerNamedParserSupplier("potato", p -> new StringArgument.StringParser<>( StringArgument.StringMode.SINGLE, (c, s) -> Collections.singletonList("potato"))); + /* Register a suggestion provider */ + manager.getParserRegistry().registerSuggestionProvider( + "some-name", + (context, input) -> NAMED_SUGGESTIONS + ); } @Test @@ -62,6 +71,11 @@ class AnnotationParserTest { manager.executeCommand(new TestCommandSender(), "flagcommand --print --word peanut").join(); } + @Test + void testNamedSuggestionProvider() { + Assertions.assertEquals(NAMED_SUGGESTIONS, manager.suggest(new TestCommandSender(), "namedsuggestions ")); + } + @ProxiedBy("proxycommand") @CommandMethod("test|t literal [string]") public void testCommand( @@ -75,12 +89,18 @@ class AnnotationParserTest { @CommandMethod("flagcommand") public void testFlags( final TestCommandSender sender, - @Flag(value = "print", aliases = "p") boolean print, - @Flag(value = "word", aliases = "w") String word + @Flag(value = "print", aliases = "p") final boolean print, + @Flag(value = "word", aliases = "w") final String word ) { if (print) { System.out.println(word); } } + @CommandMethod("namedsuggestions ") + public void testNamedSuggestionProviders( + @Argument(value = "input", suggestions = "some-name") final String argument + ) { + } + } diff --git a/cloud-core/src/main/java/cloud/commandframework/CommandTree.java b/cloud-core/src/main/java/cloud/commandframework/CommandTree.java index f4f06395..80a41843 100644 --- a/cloud-core/src/main/java/cloud/commandframework/CommandTree.java +++ b/cloud-core/src/main/java/cloud/commandframework/CommandTree.java @@ -621,7 +621,7 @@ public final class CommandTree { Objects.requireNonNull( Objects.requireNonNull( node.value, - "node.value" + "node.value: " ).getOwningCommand(), "owning command" ).getCommandPermission() diff --git a/cloud-core/src/main/java/cloud/commandframework/arguments/parser/ParserRegistry.java b/cloud-core/src/main/java/cloud/commandframework/arguments/parser/ParserRegistry.java index 74edfc69..411418d5 100644 --- a/cloud-core/src/main/java/cloud/commandframework/arguments/parser/ParserRegistry.java +++ b/cloud-core/src/main/java/cloud/commandframework/arguments/parser/ParserRegistry.java @@ -23,11 +23,13 @@ // package cloud.commandframework.arguments.parser; +import cloud.commandframework.context.CommandContext; import io.leangen.geantyref.TypeToken; import org.checkerframework.checker.nullness.qual.NonNull; import java.lang.annotation.Annotation; import java.util.Collection; +import java.util.List; import java.util.Optional; import java.util.function.BiFunction; import java.util.function.Function; @@ -121,4 +123,30 @@ public interface ParserRegistry { @NonNull ParserParameters parserParameters ); + /** + * Register a new named suggestion provider + * + * @param name Name of the suggestions provider. The name is case independent. + * @param suggestionsProvider The suggestions provider + * @see #getSuggestionProvider(String) Get a suggestion provider + * @since 1.1.0 + */ + void registerSuggestionProvider( + @NonNull String name, + @NonNull BiFunction<@NonNull CommandContext, @NonNull String, @NonNull List> suggestionsProvider + ); + + /** + * Get a named suggestion provider, if a suggestion provider with the given name exists in the registry + * + * @param name Suggestion provider name. The name is case independent. + * @return Optional that either contains the suggestion provider name, or nothing ({@link Optional#empty()}) if no + * suggestion provider is registered with the given name + * @see #registerSuggestionProvider(String, BiFunction) Register a suggestion provider + * @since 1.1.0 + */ + @NonNull Optional, @NonNull String, @NonNull List>> getSuggestionProvider( + @NonNull String name + ); + } diff --git a/cloud-core/src/main/java/cloud/commandframework/arguments/parser/StandardParserRegistry.java b/cloud-core/src/main/java/cloud/commandframework/arguments/parser/StandardParserRegistry.java index d8c2fe27..d1426ae3 100644 --- a/cloud-core/src/main/java/cloud/commandframework/arguments/parser/StandardParserRegistry.java +++ b/cloud-core/src/main/java/cloud/commandframework/arguments/parser/StandardParserRegistry.java @@ -37,6 +37,7 @@ import cloud.commandframework.arguments.standard.ShortArgument; import cloud.commandframework.arguments.standard.StringArgument; import cloud.commandframework.arguments.standard.StringArrayArgument; import cloud.commandframework.arguments.standard.UUIDArgument; +import cloud.commandframework.context.CommandContext; import io.leangen.geantyref.GenericTypeReflector; import io.leangen.geantyref.TypeToken; import org.checkerframework.checker.nullness.qual.NonNull; @@ -45,6 +46,8 @@ import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; +import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -75,6 +78,8 @@ public final class StandardParserRegistry implements ParserRegistry { private final Map, Function>> parserSuppliers = new HashMap<>(); private final Map, BiFunction, ParserParameters>> annotationMappers = new HashMap<>(); + private final Map, @NonNull String, @NonNull List>> + namedSuggestionProviders = new HashMap<>(); /** * Construct a new {@link StandardParserRegistry} instance. This will also @@ -221,6 +226,24 @@ public final class StandardParserRegistry implements ParserRegistry { return Optional.of(parser); } + @Override + public void registerSuggestionProvider( + @NonNull final String name, + @NonNull final BiFunction<@NonNull CommandContext, @NonNull String, @NonNull List> suggestionsProvider + ) { + this.namedSuggestionProviders.put(name.toLowerCase(Locale.ENGLISH), suggestionsProvider); + } + + @Override + public @NonNull Optional, @NonNull String, @NonNull List>> getSuggestionProvider( + @NonNull final String name + ) { + final BiFunction<@NonNull CommandContext, @NonNull String, @NonNull List> suggestionProvider = + this.namedSuggestionProviders.get(name.toLowerCase(Locale.ENGLISH)); + return Optional.ofNullable(suggestionProvider); + } + + private static final class RangeMapper implements BiFunction<@NonNull Range, @NonNull TypeToken, @NonNull ParserParameters> {