Add named suggestion providers

This allows for pre-registration of command suggestion providers, that can then be used in annotated command methods.
This commit is contained in:
Alexander Söderberg 2020-10-22 04:30:27 +02:00 committed by Alexander Söderberg
parent 37e0b4e91b
commit 3c7bd63f07
6 changed files with 112 additions and 8 deletions

View file

@ -52,7 +52,9 @@ import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.Queue; import java.util.Queue;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.Function; import java.util.function.Function;
@ -300,7 +302,7 @@ public final class AnnotationParser<C> {
final TypeToken<?> token = TypeToken.get(parameter.getParameterizedType()); final TypeToken<?> token = TypeToken.get(parameter.getParameterizedType());
final ParserParameters parameters = this.manager.getParserRegistry() final ParserParameters parameters = this.manager.getParserRegistry()
.parseAnnotations(token, annotations); .parseAnnotations(token, annotations);
/* Create the argument parser */
final ArgumentParser<C, ?> parser; final ArgumentParser<C, ?> parser;
if (argumentPair.getArgument().parserName().isEmpty()) { if (argumentPair.getArgument().parserName().isEmpty()) {
parser = this.manager.getParserRegistry() parser = this.manager.getParserRegistry()
@ -323,17 +325,19 @@ public final class AnnotationParser<C> {
token.toString() token.toString()
))); )));
} }
/* Check whether or not the corresponding method parameter actually exists */
if (syntaxFragment == null || syntaxFragment.getArgumentMode() == ArgumentMode.LITERAL) { if (syntaxFragment == null || syntaxFragment.getArgumentMode() == ArgumentMode.LITERAL) {
throw new IllegalArgumentException(String.format( throw new IllegalArgumentException(String.format(
"Invalid command argument '%s' in method '%s': " "Invalid command argument '%s' in method '%s': "
+ "Missing syntax mapping", argumentPair.getArgument().value(), method.getName())); + "Missing syntax mapping", argumentPair.getArgument().value(), method.getName()));
} }
final Argument argument = argumentPair.getArgument(); final Argument argument = argumentPair.getArgument();
/* Create the argument builder */
@SuppressWarnings("ALL") final CommandArgument.Builder argumentBuilder = CommandArgument.ofType( @SuppressWarnings("ALL") final CommandArgument.Builder argumentBuilder = CommandArgument.ofType(
parameter.getType(), parameter.getType(),
argument.value() argument.value()
); );
/* Set the argument requirement status */
if (syntaxFragment.getArgumentMode() == ArgumentMode.OPTIONAL) { if (syntaxFragment.getArgumentMode() == ArgumentMode.OPTIONAL) {
if (argument.defaultValue().isEmpty()) { if (argument.defaultValue().isEmpty()) {
argumentBuilder.asOptional(); argumentBuilder.asOptional();
@ -343,9 +347,21 @@ public final class AnnotationParser<C> {
} else { } else {
argumentBuilder.asRequired(); argumentBuilder.asRequired();
} }
/* Check whether or not a suggestion provider should be set */
if (!argument.suggestions().isEmpty()) {
final String suggestionProviderName = argument.suggestions();
final Optional<BiFunction<CommandContext<C>, String, List<String>>> 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<C, ?> builtArgument = argumentBuilder.manager(this.manager).withParser(parser).build(); final CommandArgument<C, ?> builtArgument = argumentBuilder.manager(this.manager).withParser(parser).build();
/* Add preprocessors */ /* Add preprocessors */
for (final Annotation annotation : annotations) { for (final Annotation annotation : annotations) {
@SuppressWarnings("ALL") final Function preprocessorMapper = @SuppressWarnings("ALL") final Function preprocessorMapper =
@ -357,7 +373,7 @@ public final class AnnotationParser<C> {
builtArgument.addPreprocessor(preprocessor); builtArgument.addPreprocessor(preprocessor);
} }
} }
/* Yay, we're done */
return builtArgument; return builtArgument;
} }

View file

@ -27,6 +27,7 @@ import java.lang.annotation.ElementType;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import java.util.function.BiFunction;
/** /**
* Annotation used to indicate that a method parameter is a command argument * Annotation used to indicate that a method parameter is a command argument
@ -49,6 +50,22 @@ public @interface Argument {
*/ */
String parserName() default ""; 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.
* <p>
* 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 * Get the default value
* *

View file

@ -32,12 +32,16 @@ import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionException;
class AnnotationParserTest { class AnnotationParserTest {
private static final List<String> NAMED_SUGGESTIONS = Arrays.asList("Dancing-Queen", "Gimme!-Gimme!-Gimme!", "Waterloo");
private static CommandManager<TestCommandSender> manager; private static CommandManager<TestCommandSender> manager;
private static AnnotationParser<TestCommandSender> annotationParser; private static AnnotationParser<TestCommandSender> annotationParser;
@ -47,6 +51,11 @@ class AnnotationParserTest {
annotationParser = new AnnotationParser<>(manager, TestCommandSender.class, p -> SimpleCommandMeta.empty()); annotationParser = new AnnotationParser<>(manager, TestCommandSender.class, p -> SimpleCommandMeta.empty());
manager.getParserRegistry().registerNamedParserSupplier("potato", p -> new StringArgument.StringParser<>( manager.getParserRegistry().registerNamedParserSupplier("potato", p -> new StringArgument.StringParser<>(
StringArgument.StringMode.SINGLE, (c, s) -> Collections.singletonList("potato"))); StringArgument.StringMode.SINGLE, (c, s) -> Collections.singletonList("potato")));
/* Register a suggestion provider */
manager.getParserRegistry().registerSuggestionProvider(
"some-name",
(context, input) -> NAMED_SUGGESTIONS
);
} }
@Test @Test
@ -62,6 +71,11 @@ class AnnotationParserTest {
manager.executeCommand(new TestCommandSender(), "flagcommand --print --word peanut").join(); manager.executeCommand(new TestCommandSender(), "flagcommand --print --word peanut").join();
} }
@Test
void testNamedSuggestionProvider() {
Assertions.assertEquals(NAMED_SUGGESTIONS, manager.suggest(new TestCommandSender(), "namedsuggestions "));
}
@ProxiedBy("proxycommand") @ProxiedBy("proxycommand")
@CommandMethod("test|t literal <int> [string]") @CommandMethod("test|t literal <int> [string]")
public void testCommand( public void testCommand(
@ -75,12 +89,18 @@ class AnnotationParserTest {
@CommandMethod("flagcommand") @CommandMethod("flagcommand")
public void testFlags( public void testFlags(
final TestCommandSender sender, final TestCommandSender sender,
@Flag(value = "print", aliases = "p") boolean print, @Flag(value = "print", aliases = "p") final boolean print,
@Flag(value = "word", aliases = "w") String word @Flag(value = "word", aliases = "w") final String word
) { ) {
if (print) { if (print) {
System.out.println(word); System.out.println(word);
} }
} }
@CommandMethod("namedsuggestions <input>")
public void testNamedSuggestionProviders(
@Argument(value = "input", suggestions = "some-name") final String argument
) {
}
} }

View file

@ -621,7 +621,7 @@ public final class CommandTree<C> {
Objects.requireNonNull( Objects.requireNonNull(
Objects.requireNonNull( Objects.requireNonNull(
node.value, node.value,
"node.value" "node.value: "
).getOwningCommand(), ).getOwningCommand(),
"owning command" "owning command"
).getCommandPermission() ).getCommandPermission()

View file

@ -23,11 +23,13 @@
// //
package cloud.commandframework.arguments.parser; package cloud.commandframework.arguments.parser;
import cloud.commandframework.context.CommandContext;
import io.leangen.geantyref.TypeToken; import io.leangen.geantyref.TypeToken;
import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.NonNull;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.util.Collection; import java.util.Collection;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.Function; import java.util.function.Function;
@ -121,4 +123,30 @@ public interface ParserRegistry<C> {
@NonNull ParserParameters parserParameters @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<C>, @NonNull String, @NonNull List<String>> 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<BiFunction<@NonNull CommandContext<C>, @NonNull String, @NonNull List<String>>> getSuggestionProvider(
@NonNull String name
);
} }

View file

@ -37,6 +37,7 @@ import cloud.commandframework.arguments.standard.ShortArgument;
import cloud.commandframework.arguments.standard.StringArgument; import cloud.commandframework.arguments.standard.StringArgument;
import cloud.commandframework.arguments.standard.StringArrayArgument; import cloud.commandframework.arguments.standard.StringArrayArgument;
import cloud.commandframework.arguments.standard.UUIDArgument; import cloud.commandframework.arguments.standard.UUIDArgument;
import cloud.commandframework.context.CommandContext;
import io.leangen.geantyref.GenericTypeReflector; import io.leangen.geantyref.GenericTypeReflector;
import io.leangen.geantyref.TypeToken; import io.leangen.geantyref.TypeToken;
import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.NonNull;
@ -45,6 +46,8 @@ import java.lang.annotation.Annotation;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@ -75,6 +78,8 @@ public final class StandardParserRegistry<C> implements ParserRegistry<C> {
private final Map<TypeToken<?>, Function<ParserParameters, ArgumentParser<C, ?>>> parserSuppliers = new HashMap<>(); private final Map<TypeToken<?>, Function<ParserParameters, ArgumentParser<C, ?>>> parserSuppliers = new HashMap<>();
private final Map<Class<? extends Annotation>, BiFunction<? extends Annotation, TypeToken<?>, ParserParameters>> private final Map<Class<? extends Annotation>, BiFunction<? extends Annotation, TypeToken<?>, ParserParameters>>
annotationMappers = new HashMap<>(); annotationMappers = new HashMap<>();
private final Map<String, BiFunction<@NonNull CommandContext<C>, @NonNull String, @NonNull List<String>>>
namedSuggestionProviders = new HashMap<>();
/** /**
* Construct a new {@link StandardParserRegistry} instance. This will also * Construct a new {@link StandardParserRegistry} instance. This will also
@ -221,6 +226,24 @@ public final class StandardParserRegistry<C> implements ParserRegistry<C> {
return Optional.of(parser); return Optional.of(parser);
} }
@Override
public void registerSuggestionProvider(
@NonNull final String name,
@NonNull final BiFunction<@NonNull CommandContext<C>, @NonNull String, @NonNull List<String>> suggestionsProvider
) {
this.namedSuggestionProviders.put(name.toLowerCase(Locale.ENGLISH), suggestionsProvider);
}
@Override
public @NonNull Optional<BiFunction<@NonNull CommandContext<C>, @NonNull String, @NonNull List<String>>> getSuggestionProvider(
@NonNull final String name
) {
final BiFunction<@NonNull CommandContext<C>, @NonNull String, @NonNull List<String>> suggestionProvider =
this.namedSuggestionProviders.get(name.toLowerCase(Locale.ENGLISH));
return Optional.ofNullable(suggestionProvider);
}
private static final class RangeMapper<T> implements BiFunction<@NonNull Range, @NonNull TypeToken<?>, private static final class RangeMapper<T> implements BiFunction<@NonNull Range, @NonNull TypeToken<?>,
@NonNull ParserParameters> { @NonNull ParserParameters> {