Finalize command flags

They're currently quite ugly in the help menu. this should probably be fixed, but it's not a priority issue.
This commit is contained in:
Alexander Söderberg 2020-10-01 23:41:17 +02:00
parent 549fbd1d1d
commit 782f3023fc
No known key found for this signature in database
GPG key ID: C0207FF7EA146678
14 changed files with 453 additions and 76 deletions

View file

@ -27,6 +27,8 @@ import cloud.commandframework.arguments.CommandArgument;
import cloud.commandframework.arguments.StaticArgument;
import cloud.commandframework.arguments.compound.ArgumentPair;
import cloud.commandframework.arguments.compound.ArgumentTriplet;
import cloud.commandframework.arguments.compound.FlagArgument;
import cloud.commandframework.arguments.flags.CommandFlag;
import cloud.commandframework.execution.CommandExecutionHandler;
import cloud.commandframework.meta.CommandMeta;
import cloud.commandframework.meta.SimpleCommandMeta;
@ -39,6 +41,7 @@ import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
@ -146,8 +149,13 @@ public class Command<C> {
@NonNull final String... aliases) {
final Map<@NonNull CommandArgument<C, ?>, @NonNull Description> map = new LinkedHashMap<>();
map.put(StaticArgument.of(commandName, aliases), description);
return new Builder<>(null, commandMeta, null, map,
new CommandExecutionHandler.NullCommandExecutionHandler<>(), Permission.empty());
return new Builder<>(null,
commandMeta,
null,
map,
new CommandExecutionHandler.NullCommandExecutionHandler<>(),
Permission.empty(),
Collections.emptyList());
}
/**
@ -165,8 +173,13 @@ public class Command<C> {
@NonNull final String... aliases) {
final Map<CommandArgument<C, ?>, Description> map = new LinkedHashMap<>();
map.put(StaticArgument.of(commandName, aliases), Description.empty());
return new Builder<>(null, commandMeta, null, map,
new CommandExecutionHandler.NullCommandExecutionHandler<>(), Permission.empty());
return new Builder<>(null,
commandMeta,
null,
map,
new CommandExecutionHandler.NullCommandExecutionHandler<>(),
Permission.empty(),
Collections.emptyList());
}
/**
@ -258,19 +271,22 @@ public class Command<C> {
private final Class<? extends C> senderType;
private final CommandPermission commandPermission;
private final CommandManager<C> commandManager;
private final Collection<CommandFlag<?>> flags;
private Builder(@Nullable final CommandManager<C> commandManager,
@NonNull final CommandMeta commandMeta,
@Nullable final Class<? extends C> senderType,
@NonNull final Map<@NonNull CommandArgument<C, ?>, @NonNull Description> commandArguments,
@NonNull final CommandExecutionHandler<@NonNull C> commandExecutionHandler,
@NonNull final CommandPermission commandPermission) {
@NonNull final CommandPermission commandPermission,
@NonNull final Collection<CommandFlag<?>> flags) {
this.commandManager = commandManager;
this.senderType = senderType;
this.commandArguments = Objects.requireNonNull(commandArguments, "Arguments may not be null");
this.commandExecutionHandler = Objects.requireNonNull(commandExecutionHandler, "Execution handler may not be null");
this.commandPermission = Objects.requireNonNull(commandPermission, "Permission may not be null");
this.commandMeta = Objects.requireNonNull(commandMeta, "Meta may not be null");
this.flags = Objects.requireNonNull(flags, "Flags may not be null");
}
/**
@ -282,8 +298,13 @@ public class Command<C> {
*/
public @NonNull Builder<C> meta(@NonNull final String key, @NonNull final String value) {
final CommandMeta commandMeta = SimpleCommandMeta.builder().with(this.commandMeta).with(key, value).build();
return new Builder<>(this.commandManager, commandMeta, this.senderType, this.commandArguments,
this.commandExecutionHandler, this.commandPermission);
return new Builder<>(this.commandManager,
commandMeta,
this.senderType,
this.commandArguments,
this.commandExecutionHandler,
this.commandPermission,
this.flags);
}
/**
@ -295,8 +316,13 @@ public class Command<C> {
* @return New builder instance using the provided command manager
*/
public @NonNull Builder<C> manager(@Nullable final CommandManager<C> commandManager) {
return new Builder<>(commandManager, this.commandMeta, this.senderType, this.commandArguments,
this.commandExecutionHandler, this.commandPermission);
return new Builder<>(commandManager,
this.commandMeta,
this.senderType,
this.commandArguments,
this.commandExecutionHandler,
this.commandPermission,
this.flags);
}
/**
@ -360,8 +386,13 @@ public class Command<C> {
@NonNull final Description description) {
final Map<CommandArgument<C, ?>, Description> commandArgumentMap = new LinkedHashMap<>(this.commandArguments);
commandArgumentMap.put(argument, description);
return new Builder<>(this.commandManager, this.commandMeta, this.senderType, commandArgumentMap,
this.commandExecutionHandler, this.commandPermission);
return new Builder<>(this.commandManager,
this.commandMeta,
this.senderType,
commandArgumentMap,
this.commandExecutionHandler,
this.commandPermission,
this.flags);
}
/**
@ -537,8 +568,13 @@ public class Command<C> {
* @return New builder instance using the command execution handler
*/
public @NonNull Builder<C> handler(@NonNull final CommandExecutionHandler<C> commandExecutionHandler) {
return new Builder<>(this.commandManager, this.commandMeta, this.senderType, this.commandArguments,
commandExecutionHandler, this.commandPermission);
return new Builder<>(this.commandManager,
this.commandMeta,
this.senderType,
this.commandArguments,
commandExecutionHandler,
this.commandPermission,
this.flags);
}
/**
@ -548,8 +584,13 @@ public class Command<C> {
* @return New builder instance using the command execution handler
*/
public @NonNull Builder<C> withSenderType(@NonNull final Class<? extends C> senderType) {
return new Builder<>(this.commandManager, this.commandMeta, senderType, this.commandArguments,
this.commandExecutionHandler, this.commandPermission);
return new Builder<>(this.commandManager,
this.commandMeta,
senderType,
this.commandArguments,
this.commandExecutionHandler,
this.commandPermission,
this.flags);
}
/**
@ -559,8 +600,13 @@ public class Command<C> {
* @return New builder instance using the command permission
*/
public @NonNull Builder<C> withPermission(@NonNull final CommandPermission permission) {
return new Builder<>(this.commandManager, this.commandMeta, this.senderType, this.commandArguments,
this.commandExecutionHandler, permission);
return new Builder<>(this.commandManager,
this.commandMeta,
this.senderType,
this.commandArguments,
this.commandExecutionHandler,
permission,
this.flags);
}
/**
@ -570,8 +616,13 @@ public class Command<C> {
* @return New builder instance using the command permission
*/
public @NonNull Builder<C> withPermission(@NonNull final String permission) {
return new Builder<>(this.commandManager, this.commandMeta, this.senderType, this.commandArguments,
this.commandExecutionHandler, Permission.of(permission));
return new Builder<>(this.commandManager,
this.commandMeta,
this.senderType,
this.commandArguments,
this.commandExecutionHandler,
Permission.of(permission),
this.flags);
}
/**
@ -610,13 +661,38 @@ public class Command<C> {
return this.meta("hidden", "true");
}
/**
* Register a new command flag
*
* @param flag Flag
* @param <T> Flag value type
* @return New builder instance that uses the provided flag
*/
public @NonNull <T> Builder<C> flag(@NonNull final CommandFlag<T> flag) {
final List<CommandFlag<?>> flags = new ArrayList<>(this.flags);
flags.add(flag);
return new Builder<>(this.commandManager,
this.commandMeta,
this.senderType,
this.commandArguments,
this.commandExecutionHandler,
this.commandPermission,
Collections.unmodifiableList(flags));
}
/**
* Build a command using the builder instance
*
* @return Built command
*/
public @NonNull Command<C> build() {
return new Command<>(Collections.unmodifiableMap(this.commandArguments),
final LinkedHashMap<CommandArgument<C, ?>, Description> commandArguments = new LinkedHashMap<>(this.commandArguments);
/* Construct flag node */
if (!flags.isEmpty()) {
final FlagArgument<C> flagArgument = new FlagArgument<>(this.flags);
commandArguments.put(flagArgument, Description.of("Command flags"));
}
return new Command<>(Collections.unmodifiableMap(commandArguments),
this.commandExecutionHandler,
this.senderType,
this.commandPermission,

View file

@ -26,6 +26,7 @@ package cloud.commandframework;
import cloud.commandframework.arguments.CommandArgument;
import cloud.commandframework.arguments.CommandSyntaxFormatter;
import cloud.commandframework.arguments.StandardCommandSyntaxFormatter;
import cloud.commandframework.arguments.flags.CommandFlag;
import cloud.commandframework.arguments.parser.ArgumentParser;
import cloud.commandframework.arguments.parser.ParserParameter;
import cloud.commandframework.arguments.parser.ParserRegistry;
@ -363,6 +364,16 @@ public abstract class CommandManager<C> {
return CommandArgument.<C, T>ofType(type, name).manager(this);
}
/**
* Create a new command flag builder
*
* @param name Flag name
* @return Flag builder
*/
public CommandFlag.@NonNull Builder<Void> flagBuilder(@NonNull final String name) {
return CommandFlag.<C>newBuilder(name);
}
/**
* Get the internal command tree. This should not be accessed unless you know what you
* are doing

View file

@ -26,6 +26,7 @@ package cloud.commandframework;
import cloud.commandframework.arguments.CommandArgument;
import cloud.commandframework.arguments.StaticArgument;
import cloud.commandframework.arguments.compound.CompoundArgument;
import cloud.commandframework.arguments.compound.FlagArgument;
import cloud.commandframework.arguments.parser.ArgumentParseResult;
import cloud.commandframework.context.CommandContext;
import cloud.commandframework.exceptions.AmbiguousNodeException;
@ -378,6 +379,15 @@ public final class CommandTree<C> {
}
// END: Compound arguments
// START: Flags
if (child.getValue() instanceof FlagArgument) {
/* Remove all but last */
while (commandQueue.size() > 1) {
commandContext.store(FlagArgument.FLAG_META, commandQueue.remove());
}
}
// END: Flags
if (child.getValue() != null) {
if (commandQueue.isEmpty()) {
return Collections.emptyList();

View file

@ -23,12 +23,21 @@
//
package cloud.commandframework.arguments.compound;
import cloud.commandframework.types.tuples.DynamicTuple;
import cloud.commandframework.types.tuples.Tuple;
import io.leangen.geantyref.TypeToken;
import cloud.commandframework.arguments.CommandArgument;
import cloud.commandframework.arguments.flags.CommandFlag;
import cloud.commandframework.arguments.parser.ArgumentParseResult;
import cloud.commandframework.arguments.parser.ArgumentParser;
import cloud.commandframework.context.CommandContext;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.function.Function;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.function.BiFunction;
/**
* Container for flag parsing logic. This should not be be used directly.
@ -36,21 +45,160 @@ import java.util.function.Function;
*
* @param <C> Command sender type
*/
public class FlagArgument<C> extends CompoundArgument<DynamicTuple, C, DynamicTuple> {
public class FlagArgument<C> extends CommandArgument<C, Object> {
FlagArgument(final @NonNull Tuple names,
final @NonNull Tuple parserTuple,
final @NonNull Tuple types,
final @NonNull Function<@NonNull DynamicTuple, @NonNull DynamicTuple> mapper,
final @NonNull TypeToken<DynamicTuple> valueType) {
/**
* Dummy object that indicates that flags were parsed successfully
*/
public static final Object FLAG_PARSE_RESULT_OBJECT = new Object();
/**
* Meta data for the last argument that was suggested
*/
public static final String FLAG_META = "__last_flag__";
private static final String FLAG_ARGUMENT_NAME = "flags";
/**
* Construct a new flag argument
*
* @param flags Flags
*/
public FlagArgument(final Collection<CommandFlag<?>> flags) {
super(false,
"flags",
names,
parserTuple,
types,
mapper,
DynamicTuple::of,
valueType);
FLAG_ARGUMENT_NAME,
new FlagArgumentParser<>(flags.toArray(new CommandFlag<?>[0])),
Object.class);
}
public static final class FlagArgumentParser<C> implements ArgumentParser<C, Object> {
private final CommandFlag<?>[] flags;
private FlagArgumentParser(@NonNull final CommandFlag<?>[] flags) {
this.flags = flags;
}
@Override
public @NonNull ArgumentParseResult<@NonNull Object> parse(@NonNull final CommandContext<@NonNull C> commandContext,
@NonNull final Queue<@NonNull String> inputQueue) {
/*
This argument must necessarily be the last so we can just consume all remaining input. This argument type
is similar to a greedy string in that sense. But, we need to keep all flag logic contained to the parser
*/
final Set<CommandFlag<?>> parsedFlags = new HashSet<>();
CommandFlag<?> currentFlag = null;
for (@NonNull final String string : inputQueue) {
if (string.startsWith("-")) {
if (currentFlag != null && currentFlag.getCommandArgument() != null) {
return ArgumentParseResult.failure(
new IllegalArgumentException(String.format("Missing argument for '%s'", currentFlag.getName())));
}
if (string.startsWith("--")) {
final String flagName = string.substring(2);
for (final CommandFlag<?> flag : this.flags) {
if (flagName.equalsIgnoreCase(flag.getName())) {
currentFlag = flag;
break;
}
}
} else {
final String flagName = string.substring(1);
for (final CommandFlag<?> flag : this.flags) {
for (final String alias : flag.getAliases()) {
if (alias.equalsIgnoreCase(flagName)) {
currentFlag = flag;
break;
}
}
}
}
if (currentFlag == null) {
return ArgumentParseResult.failure(
new IllegalArgumentException(String.format("Unknown flag '%s'", string)));
} else if (parsedFlags.contains(currentFlag)) {
return ArgumentParseResult.failure(
new IllegalArgumentException(String.format("Duplicate flag '%s'", string)));
}
parsedFlags.add(currentFlag);
if (currentFlag.getCommandArgument() == null) {
/* It's a presence flag */
commandContext.flags().addPresenceFlag(currentFlag);
/* We don't want to parse a value for this flag */
currentFlag = null;
}
} else {
if (currentFlag == null) {
return ArgumentParseResult.failure(
new IllegalArgumentException(String.format("No flag started. Don't"
+ " know what to do with '%s'", string)));
} else {
final ArgumentParseResult<?> result =
((CommandArgument) currentFlag.getCommandArgument())
.getParser()
.parse(commandContext,
new LinkedList<>(Collections.singletonList(string)));
if (result.getFailure().isPresent()) {
return ArgumentParseResult.failure(result.getFailure().get());
} else {
final CommandFlag erasedFlag = currentFlag;
final Object value = result.getParsedValue().get();
commandContext.flags().addValueFlag(erasedFlag, value);
}
}
}
}
/* We've consumed everything */
inputQueue.clear();
return ArgumentParseResult.success(FLAG_PARSE_RESULT_OBJECT);
}
@Override
public @NonNull List<@NonNull String> suggestions(final @NonNull CommandContext<C> commandContext,
final @NonNull String input) {
/* Check if we have a last flag stored */
final String lastArg = commandContext.getOrDefault(FLAG_META, "");
if (lastArg.isEmpty() || !lastArg.startsWith("-")) {
/* We don't care about the last value and so we expect a flag */
final List<String> strings = new LinkedList<>();
for (final CommandFlag<?> flag : this.flags) {
strings.add(String.format("--%s", flag.getName()));
for (final String alias : flag.getAliases()) {
strings.add(String.format("-%s", alias));
}
}
return strings;
} else {
CommandFlag<?> currentFlag = null;
if (lastArg.startsWith("--")) {
final String flagName = lastArg.substring(2);
for (final CommandFlag<?> flag : this.flags) {
if (flagName.equalsIgnoreCase(flag.getName())) {
currentFlag = flag;
break;
}
}
} else if (lastArg.startsWith("-")) {
final String flagName = lastArg.substring(1);
for (final CommandFlag<?> flag : this.flags) {
for (final String alias : flag.getAliases()) {
if (alias.equalsIgnoreCase(flagName)) {
currentFlag = flag;
break;
}
}
}
}
if (currentFlag != null && currentFlag.getCommandArgument() != null) {
// noinspection all
return (List<String>) ((BiFunction) currentFlag.getCommandArgument().getSuggestionsProvider())
.apply(commandContext, input);
}
}
commandContext.store(FLAG_META, "");
return suggestions(commandContext, input);
}
}
}

View file

@ -30,6 +30,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.Arrays;
import java.util.Collection;
import java.util.Objects;
/**
* A flag is an optional command argument that may have an associated parser,
@ -88,7 +89,7 @@ public final class CommandFlag<T> {
/**
* Get the flag description
* <p>
* Flag description
* @return Flag description
*/
public @NonNull Description getDescription() {
return this.description;
@ -108,6 +109,23 @@ public final class CommandFlag<T> {
return String.format("--%s", this.name);
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final CommandFlag<?> that = (CommandFlag<?>) o;
return getName().equals(that.getName());
}
@Override
public int hashCode() {
return Objects.hash(getName());
}
public static final class Builder<T> {

View file

@ -31,7 +31,7 @@ import java.util.Map;
/**
* Flag value mappings
*/
public class FlagContext {
public final class FlagContext {
/**
* Dummy object stored as a flag value when the flag has no associated parser

View file

@ -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.
//
/**
* Cloud flag system
*/
package cloud.commandframework.arguments.flags;

View file

@ -26,6 +26,7 @@ package cloud.commandframework.context;
import cloud.commandframework.arguments.CommandArgument;
import cloud.commandframework.arguments.flags.FlagContext;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.Collections;
import java.util.HashMap;
@ -114,6 +115,15 @@ public final class CommandContext<C> {
}
}
/**
* Remove a stored value from the context
*
* @param key Key to remove
*/
public void remove(@NonNull final String key) {
this.internalStorage.remove(key);
}
/**
* Get a required argument from the context. This will thrown an exception
* if there's no value associated with the given key
@ -140,8 +150,8 @@ public final class CommandContext<C> {
* @param <T> Argument type
* @return Argument, or supplied default value
*/
public <T> @NonNull T getOrDefault(@NonNull final String key,
@NonNull final T defaultValue) {
public <T> @Nullable T getOrDefault(@NonNull final String key,
@Nullable final T defaultValue) {
return this.<T>getOptional(key).orElse(defaultValue);
}

View file

@ -47,7 +47,7 @@ public final class DynamicTuple implements Tuple {
}
@Override
public final int getSize() {
public int getSize() {
return this.internalArray.length;
}

View file

@ -68,6 +68,15 @@ public class CommandSuggestionsTest {
manager.command(manager.commandBuilder("com2")
.argumentPair("com", Pair.of("x", "enum"),
Pair.of(Integer.class, TestEnum.class), Description.empty()));
manager.command(manager.commandBuilder("flags")
.argument(IntegerArgument.of("num"))
.flag(manager.flagBuilder("enum")
.withArgument(EnumArgument.of(TestEnum.class, "enum"))
.build())
.flag(manager.flagBuilder("static")
.build())
.build());
}
@Test
@ -142,6 +151,20 @@ public class CommandSuggestionsTest {
Assertions.assertEquals(Arrays.asList("foo", "bar"), suggestions4);
}
@Test
void testFlags() {
final String input = "flags 10 ";
final List<String> suggestions = manager.suggest(new TestCommandSender(), input);
Assertions.assertEquals(Arrays.asList("--enum", "--static"), suggestions);
final String input2 = "flags 10 --enum ";
final List<String> suggestions2 = manager.suggest(new TestCommandSender(), input2);
Assertions.assertEquals(Arrays.asList("foo", "bar"), suggestions2);
final String input3 = "flags 10 --enum foo ";
final List<String> suggestions3 = manager.suggest(new TestCommandSender(), input3);
Assertions.assertEquals(Arrays.asList("--enum", "--static"), suggestions3);
}
public enum TestEnum {
FOO,
BAR

View file

@ -101,6 +101,21 @@ class CommandTreeTest {
final Vector2 vector2 = c.get("vec");
System.out.printf("X: %f | Y: %f\n", vector2.getX(), vector2.getY());
}));
/* Build command for testing flags */
manager.command(manager.commandBuilder("flags")
.flag(manager.flagBuilder("test")
.withAliases("t")
.build())
.flag(manager.flagBuilder("test2")
.build())
.flag(manager.flagBuilder("num")
.withArgument(IntegerArgument.of("num")).build())
.handler(c -> {
System.out.println("Flag present? " + c.flags().isPresent("test"));
System.out.println("Numerical flag: " + c.flags().getValue("num", -10));
})
.build());
}
@Test
@ -182,6 +197,20 @@ class CommandTreeTest {
manager.executeCommand(new TestCommandSender(), "vec 1 1").join();
}
@Test
void testFlags() {
manager.executeCommand(new TestCommandSender(), "flags").join();
manager.executeCommand(new TestCommandSender(), "flags --test").join();
manager.executeCommand(new TestCommandSender(), "flags -t").join();
Assertions.assertThrows(CompletionException.class, () ->
manager.executeCommand(new TestCommandSender(), "flags --test --nonexistant").join());
Assertions.assertThrows(CompletionException.class, () ->
manager.executeCommand(new TestCommandSender(), "flags --test --duplicate").join());
manager.executeCommand(new TestCommandSender(), "flags --test --test2").join();
Assertions.assertThrows(CompletionException.class, () ->
manager.executeCommand(new TestCommandSender(), "flags --test test2").join());
manager.executeCommand(new TestCommandSender(), "flags --num 500");
}
public static final class SpecificCommandSender extends TestCommandSender {
}