From d4ab593460af8b88a5b79be822fd0dca150c66db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20S=C3=B6derberg?= <4096670+Citymonstret@users.noreply.github.com> Date: Mon, 6 Jun 2022 07:49:23 +0200 Subject: [PATCH] feat(core): flag yielding arguments (#367) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements https://github.com/Incendo/cloud/issues/218 Also confirmed to fix https://github.com/Incendo/cloud/issues/321. Both `StringArgument` and `StringArrayArgument` now have flag-yielding modes. For annotated command method users, this can be activated using ´@FlagYielding` --- CHANGELOG.md | 1 + .../annotations/specifier/FlagYielding.java | 43 +++++++ .../arguments/compound/FlagArgument.java | 6 +- .../arguments/parser/StandardParameters.java | 10 ++ .../parser/StandardParserRegistry.java | 12 +- .../arguments/standard/StringArgument.java | 41 ++++++- .../standard/StringArrayArgument.java | 111 ++++++++++++++++-- .../CommandSuggestionsTest.java | 103 ++++++++++++++++ .../standard/StringArrayParserTest.java | 57 +++++++++ .../arguments/standard/StringParserTest.java | 106 +++++++++++++++++ .../commandframework/issue/Issue321.java | 83 +++++++++++++ 11 files changed, 558 insertions(+), 15 deletions(-) create mode 100644 cloud-core/src/main/java/cloud/commandframework/annotations/specifier/FlagYielding.java create mode 100644 cloud-core/src/test/java/cloud/commandframework/arguments/standard/StringParserTest.java create mode 100644 cloud-core/src/test/java/cloud/commandframework/issue/Issue321.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 608975ba..c97ee3ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Core: Add `DurationArgument` for parsing `java.time.Duration` ([#330](https://github.com/Incendo/cloud/pull/330)) - Core: Add delegating command execution handlers ([#363](https://github.com/Incendo/cloud/pull/363)) - Core: Add `builder()` getter to `Command.Builder` ([#363](https://github.com/Incendo/cloud/pull/363)) +- Core: Add flag yielding modes to `StringArgument` and `StringArrayArgument` ([#367](https://github.com/Incendo/cloud/pull/367)) - 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-core/src/main/java/cloud/commandframework/annotations/specifier/FlagYielding.java b/cloud-core/src/main/java/cloud/commandframework/annotations/specifier/FlagYielding.java new file mode 100644 index 00000000..64da0fbc --- /dev/null +++ b/cloud-core/src/main/java/cloud/commandframework/annotations/specifier/FlagYielding.java @@ -0,0 +1,43 @@ +// +// 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.specifier; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that the argument should stop parsing when encountering what + * could potentially be a flag. + *

+ * This only has an effect on greedy arguments that consume all remaining input. + * + * @since 1.7.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface FlagYielding { + +} 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 021a1293..a5bd60b3 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 @@ -57,8 +57,8 @@ import org.checkerframework.checker.nullness.qual.NonNull; */ public final class FlagArgument extends CommandArgument { - private static final Pattern FLAG_ALIAS_PATTERN = Pattern.compile(" -(?([A-Za-z]+))"); private static final Pattern FLAG_PRIMARY_PATTERN = Pattern.compile(" --(?([A-Za-z]+))"); + private static final Pattern FLAG_ALIAS_PATTERN = Pattern.compile(" -(?([A-Za-z]+))"); /** * Dummy object that indicates that flags were parsed successfully @@ -125,13 +125,13 @@ public final class FlagArgument extends CommandArgument { /** * Parse command input to figure out what flag is currently being * typed at the end of the input queue. If no flag value is being - * inputed, returns {@link Optional#empty()}.
+ * inputted, returns {@link Optional#empty()}.
*
* Will consume all but the last element from the input queue. * * @param commandContext Command context * @param inputQueue The input queue of arguments - * @return current flag being typed, or empty() if none is + * @return current flag being typed, or {@code empty()} if none is */ public @NonNull Optional parseCurrentFlag( final @NonNull CommandContext<@NonNull C> commandContext, diff --git a/cloud-core/src/main/java/cloud/commandframework/arguments/parser/StandardParameters.java b/cloud-core/src/main/java/cloud/commandframework/arguments/parser/StandardParameters.java index 5d7eb178..170cec8a 100644 --- a/cloud-core/src/main/java/cloud/commandframework/arguments/parser/StandardParameters.java +++ b/cloud-core/src/main/java/cloud/commandframework/arguments/parser/StandardParameters.java @@ -59,6 +59,16 @@ public final class StandardParameters { * Indicates that a string argument should be greedy */ public static final ParserParameter GREEDY = create("greedy", TypeToken.get(Boolean.class)); + /** + * Indicates that an argument should stop parsing when encountering a potential flag. + * + * @since 1.7.0 + * @see cloud.commandframework.annotations.specifier.FlagYielding + */ + public static final ParserParameter FLAG_YIELDING = create( + "flag_yielding", + TypeToken.get(Boolean.class) + ); /** * Indicates that a string argument should be quoted. * 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 89ccc3a6..cdf48ec8 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 @@ -23,6 +23,7 @@ // package cloud.commandframework.arguments.parser; +import cloud.commandframework.annotations.specifier.FlagYielding; import cloud.commandframework.annotations.specifier.Greedy; import cloud.commandframework.annotations.specifier.Liberal; import cloud.commandframework.annotations.specifier.Quoted; @@ -104,6 +105,10 @@ public final class StandardParserRegistry implements ParserRegistry { Liberal.class, (liberal, typeToken) -> ParserParameters.single(StandardParameters.LIBERAL, true) ); + this.registerAnnotationMapper( + FlagYielding.class, + (flagYielding, typeToken) -> ParserParameters.single(StandardParameters.FLAG_YIELDING, true) + ); /* Register standard types */ this.registerParserSupplier(TypeToken.get(Byte.class), options -> @@ -137,10 +142,13 @@ public final class StandardParserRegistry implements ParserRegistry { (double) options.get(StandardParameters.RANGE_MAX, Double.POSITIVE_INFINITY) )); this.registerParserSupplier(TypeToken.get(Character.class), options -> new CharArgument.CharacterParser<>()); - this.registerParserSupplier(TypeToken.get(String[].class), options -> new StringArrayArgument.StringArrayParser<>()); + this.registerParserSupplier(TypeToken.get(String[].class), options -> + new StringArrayArgument.StringArrayParser<>(options.get(StandardParameters.FLAG_YIELDING, false)) + ); /* Make this one less awful */ this.registerParserSupplier(TypeToken.get(String.class), options -> { final boolean greedy = options.get(StandardParameters.GREEDY, false); + final boolean greedyFlagAware = options.get(StandardParameters.FLAG_YIELDING, false); final boolean quoted = options.get(StandardParameters.QUOTED, false); if (greedy && quoted) { throw new IllegalArgumentException( @@ -150,6 +158,8 @@ public final class StandardParserRegistry implements ParserRegistry { final StringArgument.StringMode stringMode; if (greedy) { stringMode = StringArgument.StringMode.GREEDY; + } else if (greedyFlagAware) { + stringMode = StringArgument.StringMode.GREEDY_FLAG_YIELDING; } else if (quoted) { stringMode = StringArgument.StringMode.QUOTED; } else { diff --git a/cloud-core/src/main/java/cloud/commandframework/arguments/standard/StringArgument.java b/cloud-core/src/main/java/cloud/commandframework/arguments/standard/StringArgument.java index 023bb5ab..0574f46c 100644 --- a/cloud-core/src/main/java/cloud/commandframework/arguments/standard/StringArgument.java +++ b/cloud-core/src/main/java/cloud/commandframework/arguments/standard/StringArgument.java @@ -166,6 +166,18 @@ public final class StringArgument extends CommandArgument { return of(name, StringMode.GREEDY); } + /** + * Create a new required command argument with the 'greedy flag yielding' parsing mode + * + * @param name Argument name + * @param Command sender type + * @return Created argument + * @since 1.7.0 + */ + public static @NonNull CommandArgument greedyFlagYielding(final @NonNull String name) { + return of(name, StringMode.GREEDY_FLAG_YIELDING); + } + /** * Create a new required command argument with the 'quoted' parsing mode * @@ -189,8 +201,14 @@ public final class StringArgument extends CommandArgument { public enum StringMode { SINGLE, + QUOTED, GREEDY, - QUOTED + /** + * Greedy string that will consume the input until a flag is present. + * + * @since 1.7.0 + */ + GREEDY_FLAG_YIELDING } @@ -224,6 +242,17 @@ public final class StringArgument extends CommandArgument { return this; } + /** + * Greedy string that will consume the input until a flag is present. + * + * @return Builder instance + * @since 1.7.0 + */ + public @NonNull @This Builder greedyFlagYielding() { + this.stringMode = StringMode.GREEDY_FLAG_YIELDING; + return this; + } + /** * Set the string mode to single * @@ -274,8 +303,11 @@ public final class StringArgument extends CommandArgument { } + @SuppressWarnings("UnnecessaryLambda") public static final class StringParser implements ArgumentParser { + private static final Pattern FLAG_PATTERN = Pattern.compile("(-[A-Za-z_\\-0-9])|(--[A-Za-z_\\-0-9]*)"); + private final StringMode stringMode; private final BiFunction, String, List> suggestionsProvider; @@ -390,6 +422,13 @@ public final class StringArgument extends CommandArgument { break; } + if (this.stringMode == StringMode.GREEDY_FLAG_YIELDING) { + // The pattern requires a leading space. + if (FLAG_PATTERN.matcher(string).matches()) { + break; + } + } + sj.add(string); inputQueue.remove(); } diff --git a/cloud-core/src/main/java/cloud/commandframework/arguments/standard/StringArrayArgument.java b/cloud-core/src/main/java/cloud/commandframework/arguments/standard/StringArrayArgument.java index caf326a7..968d965b 100644 --- a/cloud-core/src/main/java/cloud/commandframework/arguments/standard/StringArrayArgument.java +++ b/cloud-core/src/main/java/cloud/commandframework/arguments/standard/StringArrayArgument.java @@ -29,9 +29,11 @@ import cloud.commandframework.arguments.parser.ArgumentParseResult; import cloud.commandframework.arguments.parser.ArgumentParser; import cloud.commandframework.context.CommandContext; import io.leangen.geantyref.TypeToken; +import java.util.LinkedList; import java.util.List; import java.util.Queue; import java.util.function.BiFunction; +import java.util.regex.Pattern; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -48,12 +50,13 @@ public final class StringArrayArgument extends CommandArgument { final boolean required, final @NonNull String name, final @Nullable BiFunction, String, List> suggestionsProvider, - final @NonNull ArgumentDescription defaultDescription + final @NonNull ArgumentDescription defaultDescription, + final boolean flagYielding ) { super( required, name, - new StringArrayParser<>(), + new StringArrayParser<>(flagYielding), "", TypeToken.get(String[].class), suggestionsProvider, @@ -74,10 +77,35 @@ public final class StringArrayArgument extends CommandArgument { final @NonNull BiFunction, String, List> suggestionsProvider ) { return new StringArrayArgument<>( - true, + true /* required */, name, suggestionsProvider, - ArgumentDescription.empty() + ArgumentDescription.empty(), + false /* flagYielding */ + ); + } + + /** + * Create a new required string array argument + * + * @param name Argument name + * @param flagYielding Whether the parser should stop parsing when encountering a potential flag + * @param suggestionsProvider Suggestions provider + * @param Command sender type + * @return Created argument + * @since 1.7.0 + */ + public static @NonNull StringArrayArgument of( + final @NonNull String name, + final boolean flagYielding, + final @NonNull BiFunction, String, List> suggestionsProvider + ) { + return new StringArrayArgument<>( + true /* required */, + name, + suggestionsProvider, + ArgumentDescription.empty(), + flagYielding ); } @@ -94,10 +122,35 @@ public final class StringArrayArgument extends CommandArgument { final @NonNull BiFunction, String, List> suggestionsProvider ) { return new StringArrayArgument<>( - false, + false /* required */, name, suggestionsProvider, - ArgumentDescription.empty() + ArgumentDescription.empty(), + false /* flagYielding */ + ); + } + + /** + * Create a new optional string array argument + * + * @param name Argument name + * @param flagYielding Whether the parser should stop parsing when encountering a potential flag + * @param suggestionsProvider Suggestions provider + * @param Command sender type + * @return Created argument + * @since 1.7.0 + */ + public static @NonNull StringArrayArgument optional( + final @NonNull String name, + final boolean flagYielding, + final @NonNull BiFunction, String, List> suggestionsProvider + ) { + return new StringArrayArgument<>( + false /* required */, + name, + suggestionsProvider, + ArgumentDescription.empty(), + flagYielding ); } @@ -109,16 +162,54 @@ public final class StringArrayArgument extends CommandArgument { */ public static final class StringArrayParser implements ArgumentParser { + private static final Pattern FLAG_PATTERN = Pattern.compile("(-[A-Za-z_\\-0-9])|(--[A-Za-z_\\-0-9]*)"); + + private final boolean flagYielding; + + /** + * Construct a new string array parser. + */ + public StringArrayParser() { + this.flagYielding = false; + } + + /** + * Construct a new string array parser. + * + * @param flagYielding Whether the parser should stop parsing when encountering a potential flag + * @since 1.7.0 + */ + public StringArrayParser(final boolean flagYielding) { + this.flagYielding = flagYielding; + } + @Override public @NonNull ArgumentParseResult parse( final @NonNull CommandContext<@NonNull C> commandContext, final @NonNull Queue<@NonNull String> inputQueue ) { - final String[] result = new String[inputQueue.size()]; - for (int i = 0; i < result.length; i++) { - result[i] = inputQueue.remove(); + if (this.flagYielding) { + final List result = new LinkedList<>(); + final int size = inputQueue.size(); + + for (int i = 0; i < size; i++) { + final String string = inputQueue.peek(); + if (string == null || FLAG_PATTERN.matcher(string).matches()) { + break; + } + inputQueue.remove(); + + result.add(string); + } + + return ArgumentParseResult.success(result.toArray(new String[0])); + } else { + final String[] result = new String[inputQueue.size()]; + for (int i = 0; i < result.length; i++) { + result[i] = inputQueue.remove(); + } + return ArgumentParseResult.success(result); } - return ArgumentParseResult.success(result); } } diff --git a/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java b/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java index 5a0d09d8..bd492669 100644 --- a/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java +++ b/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java @@ -28,6 +28,7 @@ import cloud.commandframework.arguments.standard.BooleanArgument; import cloud.commandframework.arguments.standard.EnumArgument; import cloud.commandframework.arguments.standard.IntegerArgument; import cloud.commandframework.arguments.standard.StringArgument; +import cloud.commandframework.arguments.standard.StringArrayArgument; import cloud.commandframework.types.tuples.Pair; import cloud.commandframework.types.tuples.Triplet; import java.util.Arrays; @@ -38,6 +39,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import static cloud.commandframework.util.TestUtils.createManager; +import static com.google.common.truth.Truth.assertThat; public class CommandSuggestionsTest { @@ -394,6 +396,107 @@ public class CommandSuggestionsTest { Assertions.assertEquals(Collections.singletonList("later"), suggestions6); } + @Test + void testFlagYieldingGreedyStringFollowedByFlagArgument() { + // Arrange + final CommandManager manager = createManager(); + manager.command( + manager.commandBuilder("command") + .argument( + StringArgument.newBuilder("string") + .greedyFlagYielding() + .withSuggestionsProvider((context, input) -> Collections.singletonList("hello")) + .build() + ).flag(manager.flagBuilder("flag").withAliases("f").build()) + .flag(manager.flagBuilder("flag2").build()) + ); + + // Act + final List suggestions1 = manager.suggest( + new TestCommandSender(), + "command " + ); + final List suggestions2 = manager.suggest( + new TestCommandSender(), + "command hel" + ); + final List suggestions3 = manager.suggest( + new TestCommandSender(), + "command hello --" + ); + final List suggestions4 = manager.suggest( + new TestCommandSender(), + "command hello --f" + ); + final List suggestions5 = manager.suggest( + new TestCommandSender(), + "command hello -f" + ); + final List suggestions6 = manager.suggest( + new TestCommandSender(), + "command hello -" + ); + + // Assert + assertThat(suggestions1).containsExactly("hello"); + assertThat(suggestions2).containsExactly("hello"); + assertThat(suggestions3).containsExactly("--flag", "--flag2"); + assertThat(suggestions4).containsExactly("--flag", "--flag2"); + assertThat(suggestions5).containsExactly("-f"); + assertThat(suggestions6).containsExactly("hello"); + } + + @Test + void testFlagYieldingStringArrayFollowedByFlagArgument() { + // Arrange + final CommandManager manager = createManager(); + manager.command( + manager.commandBuilder("command") + .argument( + StringArrayArgument.of( + "array", + true, + (context, input) -> Collections.emptyList() + ) + ).flag(manager.flagBuilder("flag").withAliases("f").build()) + .flag(manager.flagBuilder("flag2").build()) + ); + + // Act + final List suggestions1 = manager.suggest( + new TestCommandSender(), + "command " + ); + final List suggestions2 = manager.suggest( + new TestCommandSender(), + "command hello" + ); + final List suggestions3 = manager.suggest( + new TestCommandSender(), + "command hello --" + ); + final List suggestions4 = manager.suggest( + new TestCommandSender(), + "command hello --f" + ); + final List suggestions5 = manager.suggest( + new TestCommandSender(), + "command hello -f" + ); + final List suggestions6 = manager.suggest( + new TestCommandSender(), + "command hello -" + ); + + // Assert + assertThat(suggestions1).isEmpty(); + assertThat(suggestions2).isEmpty(); + assertThat(suggestions3).containsExactly("--flag", "--flag2"); + assertThat(suggestions4).containsExactly("--flag", "--flag2"); + assertThat(suggestions5).containsExactly("-f"); + assertThat(suggestions6).isEmpty(); + } + public enum TestEnum { FOO, BAR diff --git a/cloud-core/src/test/java/cloud/commandframework/arguments/standard/StringArrayParserTest.java b/cloud-core/src/test/java/cloud/commandframework/arguments/standard/StringArrayParserTest.java index 27dfd0c9..ab7c9c21 100644 --- a/cloud-core/src/test/java/cloud/commandframework/arguments/standard/StringArrayParserTest.java +++ b/cloud-core/src/test/java/cloud/commandframework/arguments/standard/StringArrayParserTest.java @@ -26,6 +26,7 @@ package cloud.commandframework.arguments.standard; import cloud.commandframework.TestCommandSender; import cloud.commandframework.arguments.parser.ArgumentParseResult; import cloud.commandframework.context.CommandContext; +import java.util.Collections; import java.util.LinkedList; import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Collectors; @@ -78,4 +79,60 @@ class StringArrayParserTest { assertThat(input).isEmpty(); } + + @Test + void Parse_GreedyFlagAwareLongFormFlag_EndsAfterFlag() { + // Arrange + final StringArrayArgument.StringArrayParser parser = new StringArrayArgument.StringArrayParser<>(true); + final LinkedList input = ArgumentTestHelper.linkedListOf( + "this", + "is", + "a", + "string", + "--flag", + "more", + "flag", + "content" + ); + + // Act + final ArgumentParseResult result = parser.parse( + this.context, + input + ); + + // Assert + assertThat(result.getFailure()).isEmpty(); + assertThat(result.getParsedValue()).hasValue(new String[] {"this", "is", "a", "string"}); + + assertThat(input).containsExactly("--flag", "more", "flag", "content"); + } + + @Test + void Parse_GreedyFlagAwareShortFormFlag_EndsAfterFlag() { + // Arrange + final StringArrayArgument.StringArrayParser parser = new StringArrayArgument.StringArrayParser<>(true); + final LinkedList input = ArgumentTestHelper.linkedListOf( + "this", + "is", + "a", + "string", + "-f", + "-l", + "-a", + "-g" + ); + + // Act + final ArgumentParseResult result = parser.parse( + this.context, + input + ); + + // Assert + assertThat(result.getFailure()).isEmpty(); + assertThat(result.getParsedValue()).hasValue(new String[] {"this", "is", "a", "string"}); + + assertThat(input).containsExactly("-f", "-l", "-a", "-g"); + } } diff --git a/cloud-core/src/test/java/cloud/commandframework/arguments/standard/StringParserTest.java b/cloud-core/src/test/java/cloud/commandframework/arguments/standard/StringParserTest.java new file mode 100644 index 00000000..5b3df330 --- /dev/null +++ b/cloud-core/src/test/java/cloud/commandframework/arguments/standard/StringParserTest.java @@ -0,0 +1,106 @@ +// +// 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.arguments.standard; + +import cloud.commandframework.TestCommandSender; +import cloud.commandframework.arguments.parser.ArgumentParseResult; +import cloud.commandframework.context.CommandContext; +import java.util.Collections; +import java.util.LinkedList; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; + +@ExtendWith(MockitoExtension.class) +class StringParserTest { + + @Mock + private CommandContext context; + + @Test + void Parse_GreedyFlagAwareLongFormFlag_EndsAfterFlag() { + // Arrange + final StringArgument.StringParser parser = new StringArgument.StringParser<>( + StringArgument.StringMode.GREEDY_FLAG_YIELDING, + (context, input) -> Collections.emptyList() + ); + final LinkedList input = ArgumentTestHelper.linkedListOf( + "this", + "is", + "a", + "string", + "--flag", + "more", + "flag", + "content" + ); + + // Act + final ArgumentParseResult result = parser.parse( + this.context, + input + ); + + // Assert + assertThat(result.getFailure()).isEmpty(); + assertThat(result.getParsedValue()).hasValue("this is a string"); + + assertThat(input).containsExactly("--flag", "more", "flag", "content"); + } + + @Test + void Parse_GreedyFlagAwareShortFormFlag_EndsAfterFlag() { + // Arrange + final StringArgument.StringParser parser = new StringArgument.StringParser<>( + StringArgument.StringMode.GREEDY_FLAG_YIELDING, + (context, input) -> Collections.emptyList() + ); + final LinkedList input = ArgumentTestHelper.linkedListOf( + "this", + "is", + "a", + "string", + "-f", + "-l", + "-a", + "-g" + ); + + // Act + final ArgumentParseResult result = parser.parse( + this.context, + input + ); + + // Assert + assertThat(result.getFailure()).isEmpty(); + assertThat(result.getParsedValue()).hasValue("this is a string"); + + assertThat(input).containsExactly("-f", "-l", "-a", "-g"); + } +} diff --git a/cloud-core/src/test/java/cloud/commandframework/issue/Issue321.java b/cloud-core/src/test/java/cloud/commandframework/issue/Issue321.java new file mode 100644 index 00000000..459cb191 --- /dev/null +++ b/cloud-core/src/test/java/cloud/commandframework/issue/Issue321.java @@ -0,0 +1,83 @@ +// +// 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.issue; + +import cloud.commandframework.CommandManager; +import cloud.commandframework.TestCommandSender; +import cloud.commandframework.arguments.flags.FlagContext; +import cloud.commandframework.arguments.standard.StringArrayArgument; +import cloud.commandframework.context.CommandContext; +import cloud.commandframework.execution.CommandResult; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +import static cloud.commandframework.util.TestUtils.createManager; +import static com.google.common.truth.Truth8.assertThat; + +/** + * Test for https://github.com/Incendo/cloud/issues/321. + */ +class Issue321 { + + @Test + void flagSucceedingFlagWithStringArrayArgument() { + // Arrange + final CommandManager commandManager = createManager(); + commandManager.command( + commandManager.commandBuilder("command") + .flag( + commandManager.flagBuilder("flag1") + .withArgument( + StringArrayArgument.of( + "array", + true /* flagYielding */, + (context, input) -> Collections.emptyList() + ) + ) + ).flag( + commandManager.flagBuilder("flag2") + .withArgument( + StringArrayArgument.of( + "array", + true /* flagYielding */, + (context, input) -> Collections.emptyList() + ) + ) + ) + ); + + // Act + final CommandResult result = commandManager.executeCommand( + new TestCommandSender(), + "command --flag1 one two three --flag2 1 2 3" + ).join(); + + // Assert + final CommandContext context = result.getCommandContext(); + final FlagContext flags = context.flags(); + + assertThat(flags.getValue("flag1")).hasValue(new String[] {"one", "two", "three"}); + assertThat(flags.getValue("flag2")).hasValue(new String[] {"1", "2", "3"}); + } +}