Add flag support to the annotation system.

This commit is contained in:
Alexander Söderberg 2020-10-02 20:52:35 +02:00
parent 782f3023fc
commit c67619e5da
No known key found for this signature in database
GPG key ID: C0207FF7EA146678
9 changed files with 214 additions and 2 deletions

View file

@ -27,6 +27,7 @@ import cloud.commandframework.Command;
import cloud.commandframework.CommandManager;
import cloud.commandframework.Description;
import cloud.commandframework.arguments.CommandArgument;
import cloud.commandframework.arguments.flags.CommandFlag;
import cloud.commandframework.arguments.parser.ArgumentParser;
import cloud.commandframework.arguments.parser.ParserParameter;
import cloud.commandframework.arguments.parser.StandardParameters;
@ -64,6 +65,7 @@ public final class AnnotationParser<C> {
private final Map<Class<? extends Annotation>, Function<? extends Annotation, ParserParameters>> annotationMappers;
private final Class<C> commandSenderClass;
private final MetaFactory metaFactory;
private final FlagExtractor flagExtractor;
/**
* Construct a new annotation parser
@ -82,6 +84,7 @@ public final class AnnotationParser<C> {
this.manager = manager;
this.metaFactory = new MetaFactory(this, metaMapper);
this.annotationMappers = new HashMap<>();
this.flagExtractor = new FlagExtractor(manager);
this.registerAnnotationMapper(CommandDescription.class, d ->
ParserParameters.single(StandardParameters.DESCRIPTION, d.value()));
}
@ -156,6 +159,7 @@ public final class AnnotationParser<C> {
tokens.get(commandToken).getMinor(),
metaBuilder.build());
final Collection<ArgumentParameterPair> arguments = this.argumentExtractor.apply(method);
final Collection<CommandFlag<?>> flags = this.flagExtractor.apply(method);
final Map<String, CommandArgument<C, ?>> commandArguments = new HashMap<>();
final Map<CommandArgument<C, ?>, String> argumentDescriptions = new HashMap<>();
/* Go through all annotated parameters and build up the argument tree */
@ -218,6 +222,10 @@ public final class AnnotationParser<C> {
if (method.isAnnotationPresent(Hidden.class)) {
builder = builder.hidden();
}
/* Apply flags */
for (final CommandFlag<?> flag : flags) {
builder = builder.flag(flag);
}
/* Construct and register the command */
final Command<C> builtCommand = builder.build();
commands.add(builtCommand);

View file

@ -35,7 +35,7 @@ import java.util.function.Function;
* Utility that extract {@link Argument arguments} from
* {@link java.lang.reflect.Method method} {@link java.lang.reflect.Parameter parameters}
*/
class ArgumentExtractor implements Function<@NonNull Method, Collection<@NonNull ArgumentParameterPair>> {
class ArgumentExtractor implements Function<@NonNull Method, @NonNull Collection<@NonNull ArgumentParameterPair>> {
@Override
public @NonNull Collection<@NonNull ArgumentParameterPair> apply(@NonNull final Method method) {

View file

@ -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;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Indicates that the parameter should be treated like a {@link cloud.commandframework.arguments.flags.CommandFlag}.
* If the parameter is a {@code boolean} then a presence flag will be created, else a value flag will be created
* and the parser will be resolved the same way as it would for a {@link Argument}
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Flag {
/**
* The flag name
*
* @return Flag name
*/
String value();
/**
* Flag aliases
*
* @return Aliases
*/
String[] aliases() default "";
/**
* Name of the parser. Leave empty to use
* the default parser for the parameter type
*
* @return Parser name
*/
String parserName() default "";
/**
* The argument description
*
* @return Argument description
*/
String description() default "";
}

View file

@ -0,0 +1,91 @@
//
// 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;
import cloud.commandframework.CommandManager;
import cloud.commandframework.Description;
import cloud.commandframework.arguments.CommandArgument;
import cloud.commandframework.arguments.flags.CommandFlag;
import cloud.commandframework.arguments.parser.ArgumentParser;
import cloud.commandframework.arguments.parser.ParserParameters;
import cloud.commandframework.arguments.parser.ParserRegistry;
import io.leangen.geantyref.TypeToken;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Collection;
import java.util.LinkedList;
import java.util.function.Function;
final class FlagExtractor implements Function<@NonNull Method, Collection<@NonNull CommandFlag<?>>> {
private final CommandManager<?> commandManager;
FlagExtractor(@NonNull final CommandManager<?> commandManager) {
this.commandManager = commandManager;
}
@Override
public @NonNull Collection<@NonNull CommandFlag<?>> apply(@NonNull final Method method) {
final Collection<CommandFlag<?>> flags = new LinkedList<>();
for (final Parameter parameter : method.getParameters()) {
if (!parameter.isAnnotationPresent(Flag.class)) {
continue;
}
final Flag flag = parameter.getAnnotation(Flag.class);
final CommandFlag.Builder<Void> builder = this.commandManager.flagBuilder(flag.value())
.withDescription(Description.of(flag.description())).withAliases(flag.aliases());
if (parameter.getType().equals(boolean.class)) {
flags.add(builder.build());
} else {
final ParserRegistry<?> registry = this.commandManager.getParserRegistry();
final ArgumentParser<?, ?> parser;
if (flag.parserName().isEmpty()) {
parser = registry.createParser(TypeToken.get(parameter.getType()), ParserParameters.empty())
.orElse(null);
} else {
parser = registry.createParser(flag.parserName(), ParserParameters.empty())
.orElse(null);
}
if (parser == null) {
throw new IllegalArgumentException(
String.format("Cannot find parser for type '%s' for flag '%s' in method '%s'",
parameter.getType().getCanonicalName(), flag.value(), method.getName()));
}
@SuppressWarnings("ALL")
final CommandArgument.Builder argumentBuilder = CommandArgument.ofType(parameter.getType(), flag.value());
@SuppressWarnings("ALL")
final CommandArgument argument = argumentBuilder.asRequired()
.manager(this.commandManager)
.withParser(parser)
.build();
// noinspection unchecked
flags.add(builder.withArgument(argument).build());
}
}
return flags;
}
}

View file

@ -24,6 +24,7 @@
package cloud.commandframework.annotations;
import cloud.commandframework.arguments.CommandArgument;
import cloud.commandframework.arguments.flags.FlagContext;
import cloud.commandframework.context.CommandContext;
import cloud.commandframework.execution.CommandExecutionHandler;
import org.checkerframework.checker.nullness.qual.NonNull;
@ -55,6 +56,7 @@ class MethodCommandExecutionHandler<C> implements CommandExecutionHandler<C> {
@Override
public void execute(@NonNull final CommandContext<C> commandContext) {
final List<Object> arguments = new ArrayList<>(this.parameters.length);
final FlagContext flagContext = commandContext.flags();
/* Bind parameters to context */
for (final Parameter parameter : this.parameters) {
@ -67,6 +69,13 @@ class MethodCommandExecutionHandler<C> implements CommandExecutionHandler<C> {
final Object optional = commandContext.getOptional(argument.value()).orElse(null);
arguments.add(optional);
}
} else if (parameter.isAnnotationPresent(Flag.class)) {
final Flag flag = parameter.getAnnotation(Flag.class);
if (parameter.getType() == boolean.class) {
arguments.add(flagContext.isPresent(flag.value()));
} else {
arguments.add(flagContext.getValue(flag.value(), null));
}
} else {
if (parameter.getType().isAssignableFrom(commandContext.getSender().getClass())) {
arguments.add(commandContext.getSender());

View file

@ -58,6 +58,8 @@ class AnnotationParserTest {
manager.executeCommand(new TestCommandSender(), "proxycommand 10").join();
Assertions.assertThrows(CompletionException.class, () ->
manager.executeCommand(new TestCommandSender(), "test 101").join());
manager.executeCommand(new TestCommandSender(), "flagcommand -p").join();
manager.executeCommand(new TestCommandSender(), "flagcommand --print --word peanut").join();
}
@ProxiedBy("proxycommand")
@ -69,4 +71,13 @@ class AnnotationParserTest {
System.out.printf("Received int: %d and string '%s'\n", argument, string);
}
@CommandMethod("flagcommand")
public void testFlags(final TestCommandSender sender,
@Flag(value = "print", aliases = "p") boolean print,
@Flag(value = "word", aliases = "w") String word) {
if (print) {
System.out.println(word);
}
}
}

View file

@ -680,6 +680,17 @@ public class Command<C> {
Collections.unmodifiableList(flags));
}
/**
* Register a new command flag
*
* @param builder Flag builder. {@link CommandFlag.Builder#build()} will be invoked.
* @param <T> Flag value type
* @return New builder instance that uses the provided flag
*/
public @NonNull <T> Builder<C> flag(final CommandFlag.@NonNull Builder<T> builder) {
return this.flag(builder.build());
}
/**
* Build a command using the builder instance
*

View file

@ -89,6 +89,7 @@ public final class CommandFlag<T> {
/**
* Get the flag description
* <p>
*
* @return Flag description
*/
public @NonNull Description getDescription() {
@ -179,6 +180,17 @@ public final class CommandFlag<T> {
return new Builder<>(this.name, this.aliases, this.description, argument);
}
/**
* Create a new builder instance using the given command argument
*
* @param builder Command argument builder. {@link CommandArgument.Builder#build()} will be invoked.
* @param <N> New argument type
* @return New builder instance
*/
public <N> Builder<N> withArgument(final CommandArgument.@NonNull Builder<?, N> builder) {
return this.withArgument(builder.build());
}
/**
* Build a new command flag instance
*

View file

@ -24,6 +24,7 @@
package cloud.commandframework.arguments.flags;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.HashMap;
import java.util.Map;
@ -94,7 +95,7 @@ public final class FlagContext {
* @param <T> Value type
* @return Stored value, or the supplied default value
*/
public <T> T getValue(@NonNull final String name, @NonNull final T defaultValue) {
public <T> @Nullable T getValue(@NonNull final String name, @Nullable final T defaultValue) {
final Object value = this.flagValues.get(name);
if (value == null) {
return defaultValue;