From ec535dad7f802088117c7f7de7bede13b9aa161d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20S=C3=B6derberg?= <4096670+Citymonstret@users.noreply.github.com> Date: Tue, 14 Jun 2022 17:21:51 +0200 Subject: [PATCH] feat(core): add repeatable flags (#378) implements #209. --- CHANGELOG.md | 1 + .../commandframework/annotations/Flag.java | 10 ++ .../annotations/FlagExtractor.java | 24 +++- .../MethodCommandExecutionHandler.java | 4 +- .../feature/RepeatableFlagTest.java | 79 +++++++++++ .../arguments/compound/FlagArgument.java | 91 ++++++------ .../arguments/flags/CommandFlag.java | 85 +++++++++-- .../arguments/flags/FlagContext.java | 134 ++++++++++++++++-- .../feature/RepeatableFlagTest.java | 111 +++++++++++++++ 9 files changed, 459 insertions(+), 80 deletions(-) create mode 100644 cloud-annotations/src/test/java/cloud/commandframework/annotations/feature/RepeatableFlagTest.java create mode 100644 cloud-core/src/test/java/cloud/commandframework/feature/RepeatableFlagTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ed70fbc..27b0a66a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Core: Add flag yielding modes to `StringArgument` and `StringArrayArgument` ([#367](https://github.com/Incendo/cloud/pull/367)) - Core: Add [apiguardian](https://github.com/apiguardian-team/apiguardian) `@API` annotations ([#368](https://github.com/Incendo/cloud/pull/368)) - Core: Deprecate prefixed getters/setters in `CommandManager` ([#377](https://github.com/Incendo/cloud/pull/377)) +- Core: Add repeatable flags ([#378](https://github.com/Incendo/cloud/pull/378)) - Annotations: Annotation string processors ([#353](https://github.com/Incendo/cloud/pull/353)) - Annotations: `@CommandContainer` annotation processing ([#364](https://github.com/Incendo/cloud/pull/364)) - Annotations: `@CommandMethod` annotation processing for compile-time validation ([#365](https://github.com/Incendo/cloud/pull/365)) diff --git a/cloud-annotations/src/main/java/cloud/commandframework/annotations/Flag.java b/cloud-annotations/src/main/java/cloud/commandframework/annotations/Flag.java index a89dbfc3..03756099 100644 --- a/cloud-annotations/src/main/java/cloud/commandframework/annotations/Flag.java +++ b/cloud-annotations/src/main/java/cloud/commandframework/annotations/Flag.java @@ -28,6 +28,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.function.BiFunction; +import org.apiguardian.api.API; import org.checkerframework.checker.nullness.qual.NonNull; /** @@ -94,4 +95,13 @@ public @interface Flag { * @since 1.6.0 */ @NonNull String permission() default ""; + + /** + * Whether the flag can be repeated. + * + * @return whether the flag can be repeated + * @since 1.7.0 + */ + @API(status = API.Status.STABLE, since = "1.7.0") + boolean repeatable() default false; } diff --git a/cloud-annotations/src/main/java/cloud/commandframework/annotations/FlagExtractor.java b/cloud-annotations/src/main/java/cloud/commandframework/annotations/FlagExtractor.java index f76da662..455f0f53 100644 --- a/cloud-annotations/src/main/java/cloud/commandframework/annotations/FlagExtractor.java +++ b/cloud-annotations/src/main/java/cloud/commandframework/annotations/FlagExtractor.java @@ -30,6 +30,7 @@ import cloud.commandframework.arguments.flags.CommandFlag; import cloud.commandframework.arguments.parser.ArgumentParser; import cloud.commandframework.arguments.parser.ParserRegistry; import cloud.commandframework.permission.Permission; +import io.leangen.geantyref.GenericTypeReflector; import io.leangen.geantyref.TypeToken; import java.lang.annotation.Annotation; import java.lang.reflect.Method; @@ -65,15 +66,34 @@ final class FlagExtractor implements Function<@NonNull Method, Collection<@NonNu } final Flag flag = parameter.getAnnotation(Flag.class); final String flagName = this.annotationParser.processString(flag.value()); - final CommandFlag.Builder builder = this.commandManager + + CommandFlag.Builder builder = this.commandManager .flagBuilder(this.annotationParser.processString(flagName)) .withDescription(ArgumentDescription.of(this.annotationParser.processString(flag.description()))) .withAliases(this.annotationParser.processStrings(flag.aliases())) .withPermission(Permission.of(this.annotationParser.processString(flag.permission()))); + if (flag.repeatable()) { + builder = builder.asRepeatable(); + } + if (parameter.getType().equals(boolean.class)) { flags.add(builder.build()); } else { - final TypeToken token = TypeToken.get(parameter.getType()); + final TypeToken token; + if (flag.repeatable() && Collection.class.isAssignableFrom(parameter.getType())) { + token = TypeToken.get(GenericTypeReflector.getTypeParameter( + parameter.getParameterizedType(), + Collection.class.getTypeParameters()[0] + )); + } else { + token = TypeToken.get(parameter.getType()); + } + + if (token.equals(TypeToken.get(boolean.class))) { + flags.add(builder.build()); + continue; + } + final Collection annotations = Arrays.asList(parameter.getAnnotations()); final ParserRegistry registry = this.commandManager.parserRegistry(); final ArgumentParser parser; diff --git a/cloud-annotations/src/main/java/cloud/commandframework/annotations/MethodCommandExecutionHandler.java b/cloud-annotations/src/main/java/cloud/commandframework/annotations/MethodCommandExecutionHandler.java index 92143e9e..0d1e2976 100644 --- a/cloud-annotations/src/main/java/cloud/commandframework/annotations/MethodCommandExecutionHandler.java +++ b/cloud-annotations/src/main/java/cloud/commandframework/annotations/MethodCommandExecutionHandler.java @@ -126,8 +126,10 @@ public class MethodCommandExecutionHandler implements CommandExecutionHandler } else if (parameter.isAnnotationPresent(Flag.class)) { final Flag flag = parameter.getAnnotation(Flag.class); final String flagName = this.annotationParser.processString(flag.value()); - if (parameter.getType() == boolean.class) { + if (parameter.getType().equals(boolean.class)) { arguments.add(flagContext.isPresent(flagName)); + } else if (flag.repeatable() && parameter.getType().isAssignableFrom(List.class)) { + arguments.add(flagContext.getAll(flagName)); } else { arguments.add(flagContext.getValue(flagName, null)); } diff --git a/cloud-annotations/src/test/java/cloud/commandframework/annotations/feature/RepeatableFlagTest.java b/cloud-annotations/src/test/java/cloud/commandframework/annotations/feature/RepeatableFlagTest.java new file mode 100644 index 00000000..e9150ae5 --- /dev/null +++ b/cloud-annotations/src/test/java/cloud/commandframework/annotations/feature/RepeatableFlagTest.java @@ -0,0 +1,79 @@ +// +// MIT License +// +// Copyright (c) 2021 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.feature; + +import cloud.commandframework.CommandManager; +import cloud.commandframework.annotations.AnnotationParser; +import cloud.commandframework.annotations.CommandMethod; +import cloud.commandframework.annotations.Flag; +import cloud.commandframework.annotations.TestCommandManager; +import cloud.commandframework.annotations.TestCommandSender; +import cloud.commandframework.arguments.parser.StandardParameters; +import cloud.commandframework.execution.CommandExecutionCoordinator; +import cloud.commandframework.execution.CommandResult; +import cloud.commandframework.internal.CommandRegistrationHandler; +import cloud.commandframework.meta.CommandMeta; +import cloud.commandframework.meta.SimpleCommandMeta; +import io.leangen.geantyref.TypeToken; +import java.util.Collection; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static com.google.common.truth.Truth.assertThat; + +class RepeatableFlagTest { + + private CommandManager commandManager; + + @BeforeEach + void setup() { + this.commandManager = new TestCommandManager(); + final AnnotationParser annotationParser = new AnnotationParser<>( + this.commandManager, + TestCommandSender.class, + p -> SimpleCommandMeta.empty() + ); + annotationParser.parse(new TestClassA()); + } + + @Test + void testRepeatableFlagParsing() { + // Act + final CommandResult result = this.commandManager.executeCommand( + new TestCommandSender(), + "test --flag one --flag two --flag three" + ).join(); + + // Assert + assertThat(result.getCommandContext().flags().getAll("flag")).containsExactly("one", "two", "three"); + } + + + public static final class TestClassA { + + @CommandMethod("test") + public void command(@Flag(value = "flag", repeatable = true) Collection flags) { + } + } +} diff --git a/cloud-core/src/main/java/cloud/commandframework/arguments/compound/FlagArgument.java b/cloud-core/src/main/java/cloud/commandframework/arguments/compound/FlagArgument.java index 9cc312f9..fe18e821 100644 --- a/cloud-core/src/main/java/cloud/commandframework/arguments/compound/FlagArgument.java +++ b/cloud-core/src/main/java/cloud/commandframework/arguments/compound/FlagArgument.java @@ -41,6 +41,7 @@ import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; +import java.util.Objects; import java.util.Optional; import java.util.Queue; import java.util.Set; @@ -186,8 +187,8 @@ public final class FlagArgument extends CommandArgument { final @NonNull String input ) { /* Check if we have a last flag stored */ - final String lastArg = commandContext.getOrDefault(FLAG_META_KEY, ""); - if (lastArg.isEmpty() || !lastArg.startsWith("-")) { + final String lastArg = Objects.requireNonNull(commandContext.getOrDefault(FLAG_META_KEY, "")); + if (!lastArg.startsWith("-")) { final String rawInput = commandContext.getRawInputJoined(); /* Collection containing all used flags */ final List> usedFlags = new LinkedList<>(); @@ -220,38 +221,30 @@ public final class FlagArgument extends CommandArgument { final List strings = new LinkedList<>(); /* Recommend "primary" flags */ for (final CommandFlag flag : this.flags) { - if (usedFlags.contains(flag) || !commandContext.hasPermission(flag.permission())) { + if (usedFlags.contains(flag) && flag.mode() != CommandFlag.FlagMode.REPEATABLE) { continue; } - strings.add( - String.format( - "--%s", - flag.getName() - ) - ); + if (!commandContext.hasPermission(flag.permission())) { + continue; + } + + strings.add(String.format("--%s", flag.getName())); } /* Recommend aliases */ final boolean suggestCombined = input.length() > 1 && input.charAt(0) == '-' && input.charAt(1) != '-'; for (final CommandFlag flag : this.flags) { - if (usedFlags.contains(flag) || !commandContext.hasPermission(flag.permission())) { + if (usedFlags.contains(flag) && flag.mode() != CommandFlag.FlagMode.REPEATABLE) { continue; } + if (!commandContext.hasPermission(flag.permission())) { + continue; + } + for (final String alias : flag.getAliases()) { if (suggestCombined && flag.getCommandArgument() == null) { - strings.add( - String.format( - "%s%s", - input, - alias - ) - ); + strings.add(String.format("%s%s", input, alias)); } else { - strings.add( - String.format( - "-%s", - alias - ) - ); + strings.add(String.format("-%s", alias)); } } } @@ -348,31 +341,33 @@ public final class FlagArgument extends CommandArgument { final String flagName = string.substring(1); if (flagName.length() > 1) { boolean oneAdded = false; - /* This is a multi-alias flag, find all flags that apply */ - for (final CommandFlag flag : FlagArgumentParser.this.flags) { - if (flag.getCommandArgument() != null) { - continue; - } - for (final String alias : flag.getAliases()) { - if (flagName.toLowerCase(Locale.ENGLISH).contains(alias.toLowerCase(Locale.ENGLISH))) { - if (parsedFlags.contains(flag)) { - return ArgumentParseResult.failure(new FlagParseException( - string, - FailureReason.DUPLICATE_FLAG, - commandContext - )); - } else if (!commandContext.hasPermission(flag.permission())) { - return ArgumentParseResult.failure(new FlagParseException( - string, - FailureReason.NO_PERMISSION, - commandContext - )); - } - parsedFlags.add(flag); - commandContext.flags().addPresenceFlag(flag); - oneAdded = true; - break; + for (int i = 0; i < flagName.length(); i++) { + final String parsedFlag = Character.toString(flagName.charAt(i)) + .toLowerCase(Locale.ENGLISH); + for (final CommandFlag candidateFlag : FlagArgumentParser.this.flags) { + if (candidateFlag.getCommandArgument() != null) { + continue; } + + if (candidateFlag.getAliases().contains(parsedFlag)) { + if (parsedFlags.contains(candidateFlag) + && candidateFlag.mode() != CommandFlag.FlagMode.REPEATABLE) { + return ArgumentParseResult.failure(new FlagParseException( + string, + FailureReason.DUPLICATE_FLAG, + commandContext + )); + } else if (!commandContext.hasPermission(candidateFlag.permission())) { + return ArgumentParseResult.failure(new FlagParseException( + string, + FailureReason.NO_PERMISSION, + commandContext + )); + } + parsedFlags.add(candidateFlag); + commandContext.flags().addPresenceFlag(candidateFlag); + oneAdded = true; + } } } /* We need to parse at least one flag */ @@ -402,7 +397,7 @@ public final class FlagArgument extends CommandArgument { FailureReason.UNKNOWN_FLAG, commandContext )); - } else if (parsedFlags.contains(currentFlag)) { + } else if (parsedFlags.contains(currentFlag) && currentFlag.mode() != CommandFlag.FlagMode.REPEATABLE) { return ArgumentParseResult.failure(new FlagParseException( string, FailureReason.DUPLICATE_FLAG, diff --git a/cloud-core/src/main/java/cloud/commandframework/arguments/flags/CommandFlag.java b/cloud-core/src/main/java/cloud/commandframework/arguments/flags/CommandFlag.java index b08a04c2..adcf4376 100644 --- a/cloud-core/src/main/java/cloud/commandframework/arguments/flags/CommandFlag.java +++ b/cloud-core/src/main/java/cloud/commandframework/arguments/flags/CommandFlag.java @@ -51,6 +51,7 @@ public final class CommandFlag { private final @NonNull String @NonNull [] aliases; private final @NonNull ArgumentDescription description; private final @NonNull CommandPermission permission; + private final @NonNull FlagMode mode; private final @Nullable CommandArgument commandArgument; @@ -59,13 +60,15 @@ public final class CommandFlag { final @NonNull String @NonNull [] aliases, final @NonNull ArgumentDescription description, final @NonNull CommandPermission permission, - final @Nullable CommandArgument commandArgument + final @Nullable CommandArgument commandArgument, + final @NonNull FlagMode mode ) { this.name = Objects.requireNonNull(name, "name cannot be null"); this.aliases = Objects.requireNonNull(aliases, "aliases cannot be null"); this.description = Objects.requireNonNull(description, "description cannot be null"); this.permission = Objects.requireNonNull(permission, "permission cannot be null"); this.commandArgument = commandArgument; + this.mode = Objects.requireNonNull(mode, "mode cannot be null"); } /** @@ -96,6 +99,16 @@ public final class CommandFlag { return Arrays.asList(this.aliases); } + /** + * Returns the {@link FlagMode mode} of this flag. + * + * @return the flag mode + */ + @API(status = API.Status.STABLE, since = "1.7.0") + public @NonNull FlagMode mode() { + return this.mode; + } + /** * Get the flag description * @@ -174,23 +187,26 @@ public final class CommandFlag { private final ArgumentDescription description; private final CommandPermission permission; private final CommandArgument commandArgument; + private final FlagMode mode; private Builder( final @NonNull String name, final @NonNull String[] aliases, final @NonNull ArgumentDescription description, final @NonNull CommandPermission permission, - final @Nullable CommandArgument commandArgument + final @Nullable CommandArgument commandArgument, + final @NonNull FlagMode mode ) { this.name = name; this.aliases = aliases; this.description = description; this.permission = permission; this.commandArgument = commandArgument; + this.mode = mode; } private Builder(final @NonNull String name) { - this(name, new String[0], ArgumentDescription.empty(), Permission.empty(), null); + this(name, new String[0], ArgumentDescription.empty(), Permission.empty(), null, FlagMode.SINGLE); } /** @@ -200,7 +216,7 @@ public final class CommandFlag { * @param aliases Flag aliases * @return New builder instance */ - public Builder withAliases(final @NonNull String... aliases) { + public @NonNull Builder withAliases(final @NonNull String... aliases) { final Set filteredAliases = new HashSet<>(); for (final String alias : aliases) { if (alias.isEmpty()) { @@ -221,7 +237,8 @@ public final class CommandFlag { filteredAliases.toArray(new String[0]), this.description, this.permission, - this.commandArgument + this.commandArgument, + this.mode ); } @@ -234,7 +251,7 @@ public final class CommandFlag { */ @Deprecated @API(status = API.Status.DEPRECATED, since = "1.4.0") - public Builder withDescription(final cloud.commandframework.@NonNull Description description) { + public @NonNull Builder withDescription(final cloud.commandframework.@NonNull Description description) { return this.withDescription((ArgumentDescription) description); } @@ -246,8 +263,8 @@ public final class CommandFlag { * @since 1.4.0 */ @API(status = API.Status.STABLE, since = "1.4.0") - public Builder withDescription(final @NonNull ArgumentDescription description) { - return new Builder<>(this.name, this.aliases, description, this.permission, this.commandArgument); + public @NonNull Builder withDescription(final @NonNull ArgumentDescription description) { + return new Builder<>(this.name, this.aliases, description, this.permission, this.commandArgument, this.mode); } /** @@ -257,8 +274,8 @@ public final class CommandFlag { * @param New argument type * @return New builder instance */ - public Builder withArgument(final @NonNull CommandArgument argument) { - return new Builder<>(this.name, this.aliases, this.description, this.permission, argument); + public @NonNull Builder withArgument(final @NonNull CommandArgument argument) { + return new Builder<>(this.name, this.aliases, this.description, this.permission, argument, this.mode); } /** @@ -268,7 +285,7 @@ public final class CommandFlag { * @param New argument type * @return New builder instance */ - public Builder withArgument(final CommandArgument.@NonNull Builder builder) { + public @NonNull Builder withArgument(final CommandArgument.@NonNull Builder builder) { return this.withArgument(builder.build()); } @@ -280,8 +297,26 @@ public final class CommandFlag { * @since 1.6.0 */ @API(status = API.Status.STABLE, since = "1.6.0") - public Builder withPermission(final @NonNull CommandPermission permission) { - return new Builder<>(this.name, this.aliases, this.description, permission, this.commandArgument); + public @NonNull Builder withPermission(final @NonNull CommandPermission permission) { + return new Builder<>(this.name, this.aliases, this.description, permission, this.commandArgument, this.mode); + } + + /** + * Marks the flag as {@link FlagMode#REPEATABLE}. + * + * @return new builder instance + * @since 1.7.0 + */ + @API(status = API.Status.STABLE, since = "1.7.0") + public @NonNull Builder asRepeatable() { + return new Builder<>( + this.name, + this.aliases, + this.description, + this.permission, + this.commandArgument, + FlagMode.REPEATABLE + ); } /** @@ -290,7 +325,29 @@ public final class CommandFlag { * @return Constructed instance */ public @NonNull CommandFlag build() { - return new CommandFlag<>(this.name, this.aliases, this.description, this.permission, this.commandArgument); + return new CommandFlag<>( + this.name, + this.aliases, + this.description, + this.permission, + this.commandArgument, + this.mode + ); } } + + + @API(status = API.Status.STABLE, since = "1.7.0") + public enum FlagMode { + /** + * Only a single value can be provided for the flag, and should be extracted + * using {@link FlagContext#get(CommandFlag)}. + */ + SINGLE, + /** + * Multiple values can be provided for the flag, and sdhould be extracted + * using {@link FlagContext#getAll(CommandFlag)}. + */ + REPEATABLE + } } diff --git a/cloud-core/src/main/java/cloud/commandframework/arguments/flags/FlagContext.java b/cloud-core/src/main/java/cloud/commandframework/arguments/flags/FlagContext.java index ecaf4514..409a9fa3 100644 --- a/cloud-core/src/main/java/cloud/commandframework/arguments/flags/FlagContext.java +++ b/cloud-core/src/main/java/cloud/commandframework/arguments/flags/FlagContext.java @@ -23,7 +23,11 @@ // package cloud.commandframework.arguments.flags; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import org.apiguardian.api.API; @@ -34,6 +38,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; * Flag value mappings */ @API(status = API.Status.STABLE) +@SuppressWarnings({"rawtypes", "unchecked"}) public final class FlagContext { /** @@ -41,7 +46,7 @@ public final class FlagContext { */ public static final Object FLAG_PRESENCE_VALUE = new Object(); - private final Map flagValues; + private final Map flagValues; private FlagContext() { this.flagValues = new HashMap<>(); @@ -62,7 +67,10 @@ public final class FlagContext { * @param flag Flag instance */ public void addPresenceFlag(final @NonNull CommandFlag flag) { - this.flagValues.put(flag.getName(), FLAG_PRESENCE_VALUE); + ((List) this.flagValues.computeIfAbsent( + flag.getName(), + $ -> new ArrayList<>() + )).add(FLAG_PRESENCE_VALUE); } /** @@ -76,7 +84,36 @@ public final class FlagContext { final @NonNull CommandFlag flag, final @NonNull T value ) { - this.flagValues.put(flag.getName(), value); + ((List) this.flagValues.computeIfAbsent( + flag.getName(), + $ -> new ArrayList<>() + )).add(value); + } + + /** + * Returns the number of values associated with the given {@code flag}. + * + * @param flag the flag + * @param the flag value type + * @return the number of values associated with the flag + * @since 1.7.0 + */ + @API(status = API.Status.STABLE, since = "1.7.0") + public int count(final @NonNull CommandFlag flag) { + return this.getAll(flag).size(); + } + + /** + * Returns the number of values associated with the given {@code flag}. + * + * @param flag the flag + * @param the flag value type + * @return the number of values associated with the flag + * @since 1.7.0 + */ + @API(status = API.Status.STABLE, since = "1.7.0") + public int count(final @NonNull String flag) { + return this.getAll(flag).size(); } /** @@ -88,8 +125,8 @@ public final class FlagContext { * else {@code false} */ public boolean isPresent(final @NonNull String flag) { - final Object value = this.flagValues.get(flag); - return FLAG_PRESENCE_VALUE.equals(value); + final List value = this.flagValues.get(flag); + return value != null && !value.isEmpty(); } /** @@ -107,7 +144,12 @@ public final class FlagContext { } /** - * Get a flag value as an optional. Will be empty if the value is not present. + * Returns a flag value. + *

+ * If using {@link cloud.commandframework.arguments.flags.CommandFlag.FlagMode#SINGLE} + * then this returns the only value, if it has been specified. If using + * {@link cloud.commandframework.arguments.flags.CommandFlag.FlagMode#REPEATABLE} then + * it'll return the first value. * * @param name Flag name * @param Value type @@ -118,16 +160,20 @@ public final class FlagContext { public @NonNull Optional getValue( final @NonNull String name ) { - final Object value = this.flagValues.get(name); - if (value == null) { + final List value = this.flagValues.get(name); + if (value == null || value.isEmpty()) { return Optional.empty(); } - @SuppressWarnings("unchecked") final T casted = (T) value; - return Optional.of(casted); + return Optional.of((T) value.get(0)); } /** - * Get a flag value as an optional. Will be empty if the value is not present. + * Returns a flag value. + *

+ * If using {@link cloud.commandframework.arguments.flags.CommandFlag.FlagMode#SINGLE} + * then this returns the only value, if it has been specified. If using + * {@link cloud.commandframework.arguments.flags.CommandFlag.FlagMode#REPEATABLE} then + * it'll return the first value. * * @param flag Flag type * @param Value type @@ -142,7 +188,12 @@ public final class FlagContext { } /** - * Get a flag value + * Returns a flag value. + *

+ * If using {@link cloud.commandframework.arguments.flags.CommandFlag.FlagMode#SINGLE} + * then this returns the only value, if it has been specified. If using + * {@link cloud.commandframework.arguments.flags.CommandFlag.FlagMode#REPEATABLE} then + * it'll return the first value. * * @param name Flag name * @param defaultValue Default value @@ -157,7 +208,12 @@ public final class FlagContext { } /** - * Get a flag value + * Returns a flag value. + *

+ * If using {@link cloud.commandframework.arguments.flags.CommandFlag.FlagMode#SINGLE} + * then this returns the only value, if it has been specified. If using + * {@link cloud.commandframework.arguments.flags.CommandFlag.FlagMode#REPEATABLE} then + * it'll return the first value. * * @param name Flag value * @param defaultValue Default value @@ -234,7 +290,12 @@ public final class FlagContext { } /** - * Get a flag value + * Returns a flag value. + *

+ * If using {@link cloud.commandframework.arguments.flags.CommandFlag.FlagMode#SINGLE} + * then this returns the only value, if it has been specified. If using + * {@link cloud.commandframework.arguments.flags.CommandFlag.FlagMode#REPEATABLE} then + * it'll return the first value. * * @param name Flag name * @param Value type @@ -250,7 +311,12 @@ public final class FlagContext { } /** - * Get a flag value + * Returns a flag value. + *

+ * If using {@link cloud.commandframework.arguments.flags.CommandFlag.FlagMode#SINGLE} + * then this returns the only value, if it has been specified. If using + * {@link cloud.commandframework.arguments.flags.CommandFlag.FlagMode#REPEATABLE} then + * it'll return the first value. * * @param flag Flag name * @param Value type @@ -263,4 +329,42 @@ public final class FlagContext { ) { return this.getValue(flag).orElse(null); } + + /** + * Returns all supplied flag values for the given {@code flag}. + * + * @param flag the flag + * @param the flag value type + * @return unmodifiable view of all stored flag values, or {@link Collections#emptyList()}. + * @since 1.7.0 + */ + @API(status = API.Status.STABLE, since = "1.7.0") + public @NonNull Collection getAll( + final @NonNull CommandFlag flag + ) { + final List values = this.flagValues.get(flag.getName()); + if (values != null) { + return Collections.unmodifiableList((List) values); + } + return Collections.emptyList(); + } + + /** + * Returns all supplied flag values for the given {@code flag}. + * + * @param flag the flag + * @param the flag value type + * @return unmodifiable view of all stored flag values, or {@link Collections#emptyList()}. + * @since 1.7.0 + */ + @API(status = API.Status.STABLE, since = "1.7.0") + public @NonNull Collection getAll( + final @NonNull String flag + ) { + final List values = this.flagValues.get(flag); + if (values != null) { + return Collections.unmodifiableList((List) values); + } + return Collections.emptyList(); + } } diff --git a/cloud-core/src/test/java/cloud/commandframework/feature/RepeatableFlagTest.java b/cloud-core/src/test/java/cloud/commandframework/feature/RepeatableFlagTest.java new file mode 100644 index 00000000..8199516d --- /dev/null +++ b/cloud-core/src/test/java/cloud/commandframework/feature/RepeatableFlagTest.java @@ -0,0 +1,111 @@ +// +// MIT License +// +// Copyright (c) 2021 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.feature; + +import cloud.commandframework.CommandManager; +import cloud.commandframework.TestCommandSender; +import cloud.commandframework.arguments.standard.StringArgument; +import cloud.commandframework.execution.CommandResult; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static cloud.commandframework.util.TestUtils.createManager; +import static com.google.common.truth.Truth.assertThat; + +class RepeatableFlagTest { + + private CommandManager commandManager; + + @BeforeEach + void setup() { + this.commandManager = createManager(); + } + + @Test + void testParsingRepeatingValueFlags() { + // Arrange + this.commandManager.command( + this.commandManager.commandBuilder("test") + .flag( + this.commandManager.flagBuilder("flag") + .asRepeatable() + .withArgument(StringArgument.single("string")) + ) + ); + + // Act + final CommandResult result = this.commandManager.executeCommand( + new TestCommandSender(), + "test --flag one --flag two --flag three" + ).join(); + + // Assert + assertThat(result.getCommandContext().flags().getAll("flag")).containsExactly("one", "two", "three"); + } + + @Test + void testParsingRepeatingPresenceFlags() { + // Arrange + this.commandManager.command( + this.commandManager.commandBuilder("test") + .flag( + this.commandManager.flagBuilder("flag") + .withAliases("f") + .asRepeatable() + ) + ); + + // Act + final CommandResult result = this.commandManager.executeCommand( + new TestCommandSender(), + "test --flag -fff" + ).join(); + + // Assert + assertThat(result.getCommandContext().flags().count("flag")).isEqualTo(4); + } + + @Test + void testSuggestingRepeatableFlags() { + // Arrange + this.commandManager.command( + this.commandManager.commandBuilder("test") + .flag( + this.commandManager.flagBuilder("flag") + .withAliases("f") + .asRepeatable() + ) + ); + + // Act + final List suggestions = this.commandManager.suggest( + new TestCommandSender(), + "test --flag --" + ); + + // Assert + assertThat(suggestions).containsExactly("--flag"); + } +}