Add DurationArgument for parsing java.time.Duration (#330)
Co-authored-by: Frank van der Heijden <frank.boekanier@gmail.com>
This commit is contained in:
parent
52f3e2c679
commit
c2b3145d4d
5 changed files with 486 additions and 0 deletions
|
|
@ -0,0 +1,287 @@
|
|||
//
|
||||
// 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.ArgumentDescription;
|
||||
import cloud.commandframework.arguments.CommandArgument;
|
||||
import cloud.commandframework.arguments.parser.ArgumentParseResult;
|
||||
import cloud.commandframework.arguments.parser.ArgumentParser;
|
||||
import cloud.commandframework.captions.CaptionVariable;
|
||||
import cloud.commandframework.captions.StandardCaptionKeys;
|
||||
import cloud.commandframework.context.CommandContext;
|
||||
import cloud.commandframework.exceptions.parsing.NoInputProvidedException;
|
||||
import cloud.commandframework.exceptions.parsing.ParserException;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Queue;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Stream;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
|
||||
/**
|
||||
* Parses <code>java.time.Duration</code> from a <code>1d2h3m4s</code> format.
|
||||
* @param <C> Command sender type
|
||||
* @since 1.7.0
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public final class DurationArgument<C> extends CommandArgument<C, Duration> {
|
||||
|
||||
/**
|
||||
* Matches durations in the format of: <code>2d15h7m12s</code>
|
||||
*/
|
||||
private static final Pattern DURATION_PATTERN = Pattern.compile("(([1-9][0-9]+|[1-9])[dhms])");
|
||||
|
||||
private DurationArgument(
|
||||
final boolean required,
|
||||
final @NonNull String name,
|
||||
final @NonNull String defaultValue,
|
||||
final @Nullable BiFunction<@NonNull CommandContext<C>, @NonNull String,
|
||||
@NonNull List<@NonNull String>> suggestionsProvider,
|
||||
final @NonNull ArgumentDescription defaultDescription
|
||||
) {
|
||||
super(
|
||||
required,
|
||||
name,
|
||||
new DurationArgument.DurationParser<>(),
|
||||
defaultValue,
|
||||
Duration.class,
|
||||
suggestionsProvider,
|
||||
defaultDescription
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new builder
|
||||
*
|
||||
* @param name Name of the component
|
||||
* @param <C> Command sender type
|
||||
* @return Created builder
|
||||
* @since 1.7.0
|
||||
*/
|
||||
public static <C> @NonNull Builder<C> newBuilder(final @NonNull String name) {
|
||||
return new Builder<>(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new required command component
|
||||
*
|
||||
* @param name Component name
|
||||
* @param <C> Command sender type
|
||||
* @return Created component
|
||||
* @since 1.7.0
|
||||
*/
|
||||
public static <C> @NonNull CommandArgument<C, Duration> of(final @NonNull String name) {
|
||||
return DurationArgument.<C>newBuilder(name).asRequired().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new optional command component
|
||||
*
|
||||
* @param name Component name
|
||||
* @param <C> Command sender type
|
||||
* @return Created component
|
||||
* @since 1.7.0
|
||||
*/
|
||||
public static <C> @NonNull CommandArgument<C, Duration> optional(final @NonNull String name) {
|
||||
return DurationArgument.<C>newBuilder(name).asOptional().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new required command component with a default value
|
||||
*
|
||||
* @param name Component name
|
||||
* @param defaultDuration Default duration
|
||||
* @param <C> Command sender type
|
||||
* @return Created component
|
||||
* @since 1.7.0
|
||||
*/
|
||||
public static <C> @NonNull CommandArgument<C, Duration> optional(
|
||||
final @NonNull String name,
|
||||
final @NonNull String defaultDuration
|
||||
) {
|
||||
return DurationArgument.<C>newBuilder(name).asOptionalWithDefault(defaultDuration).build();
|
||||
}
|
||||
|
||||
|
||||
public static final class Builder<C> extends CommandArgument.Builder<C, Duration> {
|
||||
|
||||
private Builder(final @NonNull String name) {
|
||||
super(Duration.class, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder a new boolean component
|
||||
*
|
||||
* @return Constructed component
|
||||
* @since 1.7.0
|
||||
*/
|
||||
@Override
|
||||
public @NonNull DurationArgument<C> build() {
|
||||
return new DurationArgument<>(this.isRequired(), this.getName(), this.getDefaultValue(),
|
||||
this.getSuggestionsProvider(), this.getDefaultDescription()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Represents a duration parser.
|
||||
* @since 1.7.0
|
||||
* @param <C> Command sender type
|
||||
*/
|
||||
public static final class DurationParser<C> implements ArgumentParser<C, Duration> {
|
||||
|
||||
@Override
|
||||
public @NonNull ArgumentParseResult<Duration> parse(
|
||||
final @NonNull CommandContext<C> commandContext,
|
||||
final @NonNull Queue<String> inputQueue
|
||||
) {
|
||||
final String input = inputQueue.peek();
|
||||
if (input == null) {
|
||||
return ArgumentParseResult.failure(new NoInputProvidedException(
|
||||
DurationArgument.DurationParseException.class,
|
||||
commandContext
|
||||
));
|
||||
}
|
||||
|
||||
final Matcher matcher = DURATION_PATTERN.matcher(input);
|
||||
|
||||
Duration duration = Duration.ofNanos(0);
|
||||
|
||||
while (matcher.find()) {
|
||||
String group = matcher.group();
|
||||
String timeUnit = String.valueOf(group.charAt(group.length() - 1));
|
||||
int timeValue = Integer.parseInt(group.substring(0, group.length() - 1));
|
||||
switch (timeUnit) {
|
||||
case "d":
|
||||
duration = duration.plusDays(timeValue);
|
||||
break;
|
||||
case "h":
|
||||
duration = duration.plusHours(timeValue);
|
||||
break;
|
||||
case "m":
|
||||
duration = duration.plusMinutes(timeValue);
|
||||
break;
|
||||
case "s":
|
||||
duration = duration.plusSeconds(timeValue);
|
||||
break;
|
||||
default:
|
||||
return ArgumentParseResult.failure(new DurationArgument.DurationParseException(input, commandContext));
|
||||
}
|
||||
}
|
||||
|
||||
if (duration.isZero()) {
|
||||
return ArgumentParseResult.failure(new DurationArgument.DurationParseException(input, commandContext));
|
||||
}
|
||||
|
||||
inputQueue.remove();
|
||||
return ArgumentParseResult.success(duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides suggestions for Durations.
|
||||
* @since 1.7.0
|
||||
*/
|
||||
@Override
|
||||
@SuppressWarnings("MixedMutabilityReturnType")
|
||||
public @NonNull List<@NonNull String> suggestions(
|
||||
final @NonNull CommandContext<C> commandContext,
|
||||
final @NonNull String input
|
||||
) {
|
||||
char[] chars = input.toLowerCase(Locale.ROOT).toCharArray();
|
||||
|
||||
if (chars.length == 0) {
|
||||
return IntStream.range(1, 10).boxed()
|
||||
.sorted()
|
||||
.map(String::valueOf)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
char last = chars[chars.length - 1];
|
||||
|
||||
// 1d_, 5d4m_, etc
|
||||
if (Character.isLetter(last)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 1d5_, 5d4m2_, etc
|
||||
return Stream.of("d", "h", "m", "s")
|
||||
.filter(unit -> !input.contains(unit))
|
||||
.map(unit -> input + unit)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a duration parse exception.
|
||||
* @since 1.7.0
|
||||
*/
|
||||
public static final class DurationParseException extends ParserException {
|
||||
|
||||
private static final long serialVersionUID = 7632293268451349508L;
|
||||
private final String input;
|
||||
|
||||
/**
|
||||
* Construct a new Duration parse exception
|
||||
*
|
||||
* @param input String input
|
||||
* @param context Command context
|
||||
* @since 1.7.0
|
||||
*/
|
||||
public DurationParseException(
|
||||
final @NonNull String input,
|
||||
final @NonNull CommandContext<?> context
|
||||
) {
|
||||
super(
|
||||
DurationArgument.DurationParser.class,
|
||||
context,
|
||||
StandardCaptionKeys.ARGUMENT_PARSE_FAILURE_DURATION,
|
||||
CaptionVariable.of("input", input)
|
||||
);
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the supplied input
|
||||
*
|
||||
* @return String value
|
||||
* @since 1.7.0
|
||||
*/
|
||||
public @NonNull String getInput() {
|
||||
return this.input;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -87,6 +87,10 @@ public class SimpleCaptionRegistry<C> implements FactoryDelegatingCaptionRegistr
|
|||
* Default caption for {@link StandardCaptionKeys#ARGUMENT_PARSE_FAILURE_COLOR}
|
||||
*/
|
||||
public static final String ARGUMENT_PARSE_FAILURE_COLOR = "'{input}' is not a valid color";
|
||||
/**
|
||||
* Default caption for {@link StandardCaptionKeys#ARGUMENT_PARSE_FAILURE_DURATION}
|
||||
*/
|
||||
public static final String ARGUMENT_PARSE_FAILURE_DURATION = "'{input}' is not a duration format";
|
||||
|
||||
private final Map<Caption, BiFunction<Caption, C, String>> messageFactories = new HashMap<>();
|
||||
|
||||
|
|
@ -143,6 +147,10 @@ public class SimpleCaptionRegistry<C> implements FactoryDelegatingCaptionRegistr
|
|||
StandardCaptionKeys.ARGUMENT_PARSE_FAILURE_COLOR,
|
||||
(caption, sender) -> ARGUMENT_PARSE_FAILURE_COLOR
|
||||
);
|
||||
this.registerMessageFactory(
|
||||
StandardCaptionKeys.ARGUMENT_PARSE_FAILURE_DURATION,
|
||||
(caption, sender) -> ARGUMENT_PARSE_FAILURE_DURATION
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -91,6 +91,10 @@ public final class StandardCaptionKeys {
|
|||
* Variables: {input}
|
||||
*/
|
||||
public static final Caption ARGUMENT_PARSE_FAILURE_COLOR = of("argument.parse.failure.color");
|
||||
/**
|
||||
* Variables: {input}
|
||||
*/
|
||||
public static final Caption ARGUMENT_PARSE_FAILURE_DURATION = of("argument.parse.failure.duration");
|
||||
|
||||
private StandardCaptionKeys() {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
//
|
||||
// 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.CommandManager;
|
||||
import cloud.commandframework.TestCommandSender;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static cloud.commandframework.util.TestUtils.createManager;
|
||||
|
||||
public class DurationArgumentSuggestionsTest {
|
||||
|
||||
private static CommandManager<TestCommandSender> manager;
|
||||
|
||||
@BeforeAll
|
||||
static void setupManager() {
|
||||
manager = createManager();
|
||||
manager.command(manager.commandBuilder("duration")
|
||||
.argument(DurationArgument.of("duration")));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testDurationSuggestions() {
|
||||
final String input = "duration ";
|
||||
final List<String> suggestions = manager.suggest(new TestCommandSender(), input);
|
||||
Assertions.assertEquals(Arrays.asList("1", "2", "3", "4", "5", "6", "7", "8", "9"), suggestions);
|
||||
|
||||
final String input2 = "duration 1";
|
||||
final List<String> suggestions2 = manager.suggest(new TestCommandSender(), input2);
|
||||
Assertions.assertEquals(Arrays.asList("1d", "1h", "1m", "1s"), suggestions2);
|
||||
|
||||
final String input3 = "duration 1d";
|
||||
final List<String> suggestions3 = manager.suggest(new TestCommandSender(), input3);
|
||||
Assertions.assertEquals(Collections.emptyList(), suggestions3);
|
||||
|
||||
final String input4 = "duration 1d2";
|
||||
final List<String> suggestions4 = manager.suggest(new TestCommandSender(), input4);
|
||||
Assertions.assertTrue(suggestions4.containsAll(Arrays.asList("1d2h", "1d2m", "1d2s")));
|
||||
Assertions.assertFalse(suggestions4.contains("1d2d"));
|
||||
|
||||
final String input9 = "duration 1d22";
|
||||
final List<String> suggestions9 = manager.suggest(new TestCommandSender(), input9);
|
||||
Assertions.assertTrue(suggestions9.containsAll(Arrays.asList("1d22h", "1d22m", "1d22s")));
|
||||
Assertions.assertFalse(suggestions9.contains("1d22d"));
|
||||
|
||||
final String input5 = "duration d";
|
||||
final List<String> suggestions5 = manager.suggest(new TestCommandSender(), input5);
|
||||
Assertions.assertEquals(Collections.emptyList(), suggestions5);
|
||||
|
||||
final String input6 = "duration 1d2d";
|
||||
final List<String> suggestions6 = manager.suggest(new TestCommandSender(), input6);
|
||||
Assertions.assertEquals(Collections.emptyList(), suggestions6);
|
||||
|
||||
final String input7 = "duration 1d2h3m4s";
|
||||
final List<String> suggestions7 = manager.suggest(new TestCommandSender(), input7);
|
||||
Assertions.assertEquals(Collections.emptyList(), suggestions7);
|
||||
|
||||
final String input8 = "duration dd";
|
||||
final List<String> suggestions8 = manager.suggest(new TestCommandSender(), input8);
|
||||
Assertions.assertEquals(Collections.emptyList(), suggestions8);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
//
|
||||
// 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.CommandManager;
|
||||
import cloud.commandframework.TestCommandSender;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.CompletionException;
|
||||
|
||||
import static cloud.commandframework.util.TestUtils.createManager;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
public class DurationArgumentTest {
|
||||
|
||||
private static final Duration[] storage = new Duration[1];
|
||||
private static CommandManager<TestCommandSender> manager;
|
||||
|
||||
@BeforeAll
|
||||
static void setup() {
|
||||
manager = createManager();
|
||||
manager.command(manager.commandBuilder("duration")
|
||||
.argument(DurationArgument.of("duration"))
|
||||
.handler(c -> {
|
||||
final Duration duration = c.get("duration");
|
||||
storage[0] = duration;
|
||||
})
|
||||
.build());
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void reset() {
|
||||
storage[0] = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
void single_single_unit() {
|
||||
manager.executeCommand(new TestCommandSender(), "duration 2d").join();
|
||||
|
||||
assertThat(storage[0]).isEqualTo(Duration.ofDays(2));
|
||||
|
||||
manager.executeCommand(new TestCommandSender(), "duration 999s").join();
|
||||
|
||||
assertThat(storage[0]).isEqualTo(Duration.ofSeconds(999));
|
||||
}
|
||||
|
||||
@Test
|
||||
void single_multiple_units() {
|
||||
manager.executeCommand(new TestCommandSender(), "duration 2d12h7m34s").join();
|
||||
|
||||
assertThat(storage[0]).isEqualTo(Duration.ofDays(2).plusHours(12).plusMinutes(7).plusSeconds(34));
|
||||
|
||||
manager.executeCommand(new TestCommandSender(), "duration 700h75m1d999s").join();
|
||||
|
||||
assertThat(storage[0]).isEqualTo(Duration.ofDays(1).plusHours(700).plusMinutes(75).plusSeconds(999));
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalid_format_failing() {
|
||||
Assertions.assertThrows(CompletionException.class, () -> manager.executeCommand(
|
||||
new TestCommandSender(),
|
||||
"duration d"
|
||||
).join());
|
||||
|
||||
Assertions.assertThrows(CompletionException.class, () -> manager.executeCommand(
|
||||
new TestCommandSender(),
|
||||
"duration 1x"
|
||||
).join());
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue