diff --git a/cloud-annotations/src/main/java/cloud/commandframework/annotations/assembler/ArgumentAssemblerImpl.java b/cloud-annotations/src/main/java/cloud/commandframework/annotations/assembler/ArgumentAssemblerImpl.java index e562cabc1..917115ac5 100644 --- a/cloud-annotations/src/main/java/cloud/commandframework/annotations/assembler/ArgumentAssemblerImpl.java +++ b/cloud-annotations/src/main/java/cloud/commandframework/annotations/assembler/ArgumentAssemblerImpl.java @@ -33,9 +33,13 @@ import cloud.commandframework.arguments.ComponentPreprocessor; import cloud.commandframework.arguments.DefaultValue; import cloud.commandframework.arguments.parser.ArgumentParser; +import cloud.commandframework.arguments.parser.EitherParser; +import cloud.commandframework.arguments.parser.ParserDescriptor; import cloud.commandframework.arguments.parser.ParserParameters; import cloud.commandframework.arguments.suggestion.Suggestion; import cloud.commandframework.arguments.suggestion.SuggestionProvider; +import cloud.commandframework.types.Either; +import io.leangen.geantyref.GenericTypeReflector; import io.leangen.geantyref.TypeToken; import java.lang.annotation.Annotation; import java.lang.reflect.Parameter; @@ -74,7 +78,41 @@ public ArgumentAssemblerImpl(final @NonNull AnnotationParser annotationParser .parseAnnotations(token, annotations); /* Create the argument parser */ final ArgumentParser parser; - if (descriptor.parserName() == null) { + if (GenericTypeReflector.isSuperType(Either.class, token.getType())) { + final TypeToken primaryType = TypeToken.get(GenericTypeReflector.getTypeParameter( + parameter.getParameterizedType(), + Either.class.getTypeParameters()[0] + )); + final TypeToken fallbackType = TypeToken.get(GenericTypeReflector.getTypeParameter( + parameter.getParameterizedType(), + Either.class.getTypeParameters()[1] + )); + final ParserDescriptor primary = this.annotationParser.manager() + .parserRegistry() + .createParser(primaryType, parameters) + .map(primaryParser -> ParserDescriptor.of(primaryParser, (TypeToken) primaryType)) + .orElseThrow(() -> new IllegalArgumentException( + String.format( + "Parameter '%s' " + + "has parser 'Either<%s, ?>' but no parser exists " + + "for that type", + parameter.getName(), + token.getType().getTypeName() + ))); + final ParserDescriptor fallback = this.annotationParser.manager() + .parserRegistry() + .createParser(fallbackType, parameters) + .map(fallbackParser -> ParserDescriptor.of(fallbackParser, (TypeToken) fallbackType)) + .orElseThrow(() -> new IllegalArgumentException( + String.format( + "Parameter '%s' " + + "has parser 'Either' but no parser exists " + + "for that type", + parameter.getName(), + token.getType().getTypeName() + ))); + parser = EitherParser.eitherParser(primary, fallback).parser(); + } else if (descriptor.parserName() == null) { parser = this.annotationParser.manager().parserRegistry() .createParser(token, parameters) .orElseThrow(() -> new IllegalArgumentException( diff --git a/cloud-annotations/src/test/java/cloud/commandframework/annotations/feature/EitherParserAnnotationsTest.java b/cloud-annotations/src/test/java/cloud/commandframework/annotations/feature/EitherParserAnnotationsTest.java new file mode 100644 index 000000000..05d538c77 --- /dev/null +++ b/cloud-annotations/src/test/java/cloud/commandframework/annotations/feature/EitherParserAnnotationsTest.java @@ -0,0 +1,77 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// 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.CommandComponent; +import cloud.commandframework.CommandManager; +import cloud.commandframework.annotations.AnnotationParser; +import cloud.commandframework.annotations.Command; +import cloud.commandframework.annotations.TestCommandManager; +import cloud.commandframework.annotations.TestCommandSender; +import cloud.commandframework.arguments.parser.EitherParser; +import cloud.commandframework.arguments.standard.BooleanParser; +import cloud.commandframework.arguments.standard.IntegerParser; +import cloud.commandframework.types.Either; +import java.util.Collection; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static com.google.common.truth.Truth.assertThat; + +class EitherParserAnnotationsTest { + + private CommandManager commandManager; + private AnnotationParser annotationParser; + + @BeforeEach + void setup() { + this.commandManager = new TestCommandManager(); + this.annotationParser = new AnnotationParser<>( + this.commandManager, + TestCommandSender.class + ); + } + + @Test + @SuppressWarnings("unchecked") + void test() { + // Act + final Collection> result = this.annotationParser.parse( + new EitherParserAnnotationsTest() + ); + + // Assert + final cloud.commandframework.Command command = result.stream().findFirst().get(); + final CommandComponent eitherComponent = command.components().get(1); + assertThat(eitherComponent.parser()).isInstanceOf(EitherParser.class); + final EitherParser eitherParser = (EitherParser) eitherComponent.parser(); + assertThat(eitherParser.primary().parser()).isInstanceOf(IntegerParser.class); + assertThat(eitherParser.fallback().parser()).isInstanceOf(BooleanParser.class); + } + + @Command("command ") + public void command(@NonNull Either arg) { + } +} diff --git a/cloud-core/src/main/java/cloud/commandframework/arguments/parser/ArgumentParser.java b/cloud-core/src/main/java/cloud/commandframework/arguments/parser/ArgumentParser.java index 154763d1a..40c81d5d3 100644 --- a/cloud-core/src/main/java/cloud/commandframework/arguments/parser/ArgumentParser.java +++ b/cloud-core/src/main/java/cloud/commandframework/arguments/parser/ArgumentParser.java @@ -28,6 +28,7 @@ import cloud.commandframework.arguments.suggestion.SuggestionProviderHolder; import cloud.commandframework.context.CommandContext; import cloud.commandframework.context.CommandInput; +import cloud.commandframework.types.Either; import java.util.concurrent.CompletableFuture; import java.util.function.BiFunction; import org.apiguardian.api.API; @@ -175,6 +176,24 @@ public interface ArgumentParser extends SuggestionProviderHolder { return SuggestionProvider.noSuggestions(); } + /** + * Creates a new parser which attempts to use the {@code primary} parser and falls back on + * the {@code fallback} parser if that fails. + * + * @param command sender type + * @param primary value type + * @param fallback value type + * @param primary primary parser + * @param fallback fallback parser which gets invoked if the primary parser fails to parse the input + * @return the descriptor of the parser + */ + static @NonNull ParserDescriptor> firstOf( + final @NonNull ParserDescriptor primary, + final @NonNull ParserDescriptor fallback + ) { + return EitherParser.eitherParser(primary, fallback); + } + /** * Utility interface extending {@link ArgumentParser} to make it easier to implement diff --git a/cloud-core/src/main/java/cloud/commandframework/arguments/parser/EitherParser.java b/cloud-core/src/main/java/cloud/commandframework/arguments/parser/EitherParser.java new file mode 100644 index 000000000..a9640ddc7 --- /dev/null +++ b/cloud-core/src/main/java/cloud/commandframework/arguments/parser/EitherParser.java @@ -0,0 +1,239 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// 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.parser; + +import cloud.commandframework.arguments.suggestion.Suggestion; +import cloud.commandframework.arguments.suggestion.SuggestionProvider; +import cloud.commandframework.captions.CaptionVariable; +import cloud.commandframework.captions.StandardCaptionKeys; +import cloud.commandframework.context.CommandContext; +import cloud.commandframework.context.CommandInput; +import cloud.commandframework.exceptions.parsing.ParserException; +import cloud.commandframework.types.Either; +import io.leangen.geantyref.GenericTypeReflector; +import io.leangen.geantyref.TypeToken; +import java.util.Collections; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * A parser which attempts to use the {@link #primary()} parser and falls back on the {@link #fallback()} parser if that fails. + * + * @param command sender type + * @param primary value type + * @param fallback value type + * @since 2.0.0 + */ +@API(status = API.Status.STABLE, since = "2.0.0") +public final class EitherParser implements ArgumentParser.FutureArgumentParser>, SuggestionProvider { + + /** + * Creates a new parser which attempts to use the {@code primary} parser and falls back on + * the {@code fallback} parser if that fails. + * + * @param command sender type + * @param primary value type + * @param fallback value type + * @param primary primary parser + * @param fallback fallback parser which gets invoked if the primary parser fails to parse the input + * @return the descriptor of the parser + */ + public static ParserDescriptor> eitherParser( + final @NonNull ParserDescriptor primary, + final @NonNull ParserDescriptor fallback + ) { + return ParserDescriptor.of(new EitherParser<>(primary, fallback), new TypeToken>() { + }); + } + + private final ParserDescriptor primary; + private final ParserDescriptor fallback; + + /** + * Creates a new either parser. + * + * @param primary primary parser + * @param fallback fallback parser which gets invoked if the primary parser fails to parse the input + */ + public EitherParser(final @NonNull ParserDescriptor primary, final @NonNull ParserDescriptor fallback) { + this.primary = Objects.requireNonNull(primary, "primary"); + this.fallback = Objects.requireNonNull(fallback, "fallback"); + } + + /** + * Returns the primary parser. + * + * @return the primary parser + */ + public @NonNull ParserDescriptor primary() { + return this.primary; + } + + /** + * Returns the fallback parser. + * + * @return the fallback parser + */ + public @NonNull ParserDescriptor fallback() { + return this.fallback; + } + + @Override + public @NonNull CompletableFuture<@NonNull ArgumentParseResult>> parseFuture( + final @NonNull CommandContext<@NonNull C> commandContext, + final @NonNull CommandInput commandInput + ) { + final String input = commandInput.peekString(); + final int originalCursor = commandInput.cursor(); + + return this.primary.parser().parseFuture(commandContext, commandInput).thenCompose(primaryResult -> { + if (primaryResult.parsedValue().isPresent()) { + return ArgumentParseResult.successFuture(Either.ofPrimary(primaryResult.parsedValue().get())); + } + + // We need to restore the input if the first parser fails, so that the fallback parser gets access + // to the unspoiled input. + commandInput.cursor(originalCursor); + + return this.fallback.parser() + .parseFuture(commandContext, commandInput) + .thenApply(fallbackResult -> { + if (fallbackResult.parsedValue().isPresent()) { + return ArgumentParseResult.success(Either.ofFallback(fallbackResult.parsedValue().get())); + } + return ArgumentParseResult.failure(new EitherParseException( + primaryResult.failure().get(), + fallbackResult.failure().get(), + this.primary.valueType(), + this.fallback.valueType(), + commandContext, + input + )); + }); + }); + } + + @Override + @SuppressWarnings({"unchecked", "rawtypes"}) + public @NonNull CompletableFuture<@NonNull Iterable<@NonNull Suggestion>> suggestionsFuture( + final @NonNull CommandContext context, + final @NonNull CommandInput input + ) { + if (!(this.primary.parser() instanceof SuggestionProvider)) { + if (!(this.fallback.parser() instanceof SuggestionProvider)) { + return CompletableFuture.completedFuture(Collections.emptyList()); + } + return ((SuggestionProvider) this.fallback.parser()).suggestionsFuture(context, input); + } + if (!(this.fallback.parser() instanceof SuggestionProvider)) { + return ((SuggestionProvider) this.primary.parser()).suggestionsFuture(context, input); + } + final CompletableFuture>[] suggestionFutures = new CompletableFuture[] { + ((SuggestionProvider) this.primary.parser()).suggestionsFuture(context, input.copy()), + ((SuggestionProvider) this.fallback.parser()).suggestionsFuture(context, input) + }; + return CompletableFuture.allOf(suggestionFutures).thenApply(ignored -> + Stream.concat( + StreamSupport.stream(suggestionFutures[0].getNow(Collections.emptyList()).spliterator(), false), + StreamSupport.stream(suggestionFutures[1].getNow(Collections.emptyList()).spliterator(), false) + ).collect(Collectors.toList()) + ); + } + + + /** + * Exception thrown when both the primary and fallback parsers fail to parse the input. + */ + public static final class EitherParseException extends ParserException { + + private final Throwable primaryFailure; + private final Throwable fallbackFailure; + private final TypeToken primaryType; + private final TypeToken fallbackType; + + private EitherParseException( + final @NonNull Throwable primaryFailure, + final @NonNull Throwable fallbackFailure, + final @NonNull TypeToken primaryType, + final @NonNull TypeToken fallbackType, + final @NonNull CommandContext context, + final @NonNull String input + ) { + super( + fallbackFailure, + EitherParser.class, + context, + StandardCaptionKeys.ARGUMENT_PARSE_FAILURE_EITHER, + CaptionVariable.of("input", input), + CaptionVariable.of("primary", GenericTypeReflector.erase(primaryType.getType()).getSimpleName()), + CaptionVariable.of("fallback", GenericTypeReflector.erase(fallbackType.getType()).getSimpleName()) + ); + this.primaryFailure = primaryFailure; + this.fallbackFailure = fallbackFailure; + this.primaryType = primaryType; + this.fallbackType = fallbackType; + } + + /** + * Returns the throwable thrown by the primary parser. + * + * @return primary failure + */ + public @NonNull Throwable primaryFailure() { + return this.primaryFailure; + } + + /** + * Returns the throwable thrown by the fallback parser. + * + * @return fallback failure + */ + public @NonNull Throwable fallbackFailure() { + return this.fallbackFailure; + } + + /** + * Returns the type produced by the primary parser. + * + * @return primary type + */ + public @NonNull TypeToken primaryType() { + return this.primaryType; + } + + /** + * Returns the type produced by the fallback parser. + * + * @return fallback type + */ + public @NonNull TypeToken fallbackType() { + return this.fallbackType; + } + } +} diff --git a/cloud-core/src/main/java/cloud/commandframework/captions/StandardCaptionKeys.java b/cloud-core/src/main/java/cloud/commandframework/captions/StandardCaptionKeys.java index ab26f8230..59d15f006 100644 --- a/cloud-core/src/main/java/cloud/commandframework/captions/StandardCaptionKeys.java +++ b/cloud-core/src/main/java/cloud/commandframework/captions/StandardCaptionKeys.java @@ -106,6 +106,10 @@ public final class StandardCaptionKeys { */ public static final Caption ARGUMENT_PARSE_FAILURE_AGGREGATE_COMPONENT_FAILURE = of( "argument.parse.failure.aggregate.failure"); + /** + * Variables: {@code }, {@code }, {@code } + */ + public static final Caption ARGUMENT_PARSE_FAILURE_EITHER = of("argument.parse.failure.either"); private StandardCaptionKeys() { } diff --git a/cloud-core/src/main/java/cloud/commandframework/captions/StandardCaptionRegistry.java b/cloud-core/src/main/java/cloud/commandframework/captions/StandardCaptionRegistry.java index bbfcd16d9..8f70865d1 100644 --- a/cloud-core/src/main/java/cloud/commandframework/captions/StandardCaptionRegistry.java +++ b/cloud-core/src/main/java/cloud/commandframework/captions/StandardCaptionRegistry.java @@ -104,6 +104,10 @@ public class StandardCaptionRegistry implements CaptionRegistry { * Default caption for {@link StandardCaptionKeys#ARGUMENT_PARSE_FAILURE_AGGREGATE_COMPONENT_FAILURE} */ public static final String ARGUMENT_PARSE_FAILURE_AGGREGATE_COMPONENT_FAILURE = "Invalid component '': "; + /** + * Default caption for {@link StandardCaptionKeys#ARGUMENT_PARSE_FAILURE_EITHER} + */ + public static final String ARGUMENT_PARSE_FAILURE_EITHER = "Could not resolve or from ''"; private final LinkedList<@NonNull CaptionProvider> providers = new LinkedList<>(); @@ -161,6 +165,9 @@ protected StandardCaptionRegistry() { ).putCaptions( StandardCaptionKeys.ARGUMENT_PARSE_FAILURE_AGGREGATE_COMPONENT_FAILURE, ARGUMENT_PARSE_FAILURE_AGGREGATE_COMPONENT_FAILURE + ).putCaptions( + StandardCaptionKeys.ARGUMENT_PARSE_FAILURE_EITHER, + ARGUMENT_PARSE_FAILURE_EITHER ).build() ); } diff --git a/cloud-core/src/main/java/cloud/commandframework/types/Either.java b/cloud-core/src/main/java/cloud/commandframework/types/Either.java new file mode 100644 index 000000000..4d2cc25ae --- /dev/null +++ b/cloud-core/src/main/java/cloud/commandframework/types/Either.java @@ -0,0 +1,83 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// 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.types; + +import cloud.commandframework.internal.ImmutableImpl; +import java.util.Optional; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.immutables.value.Value; + +import static java.util.Objects.requireNonNull; + +/** + * An object that contains either a value of type {@link U} or type {@link V}. + * + * @param primary value type + * @param fallback value type + * @since 2.0.0 + */ +@ImmutableImpl +@Value.Immutable +@API(status = API.Status.STABLE, since = "2.0.0") +public interface Either { + + /** + * Creates an instance with a {@code value} of the primary type {@link U}. + * + * @param primary value type + * @param secondary value type + * @param value primary value + * @return the instance + */ + static @NonNull Either ofPrimary(final @NonNull U value) { + return EitherImpl.of(requireNonNull(value, "value"), null); + } + + /** + * Creates an instance with a {@code value} of the fallback type {@link V}. + * + * @param primary value type + * @param secondary value type + * @param value primary value + * @return the instance + */ + static @NonNull Either ofFallback(final @NonNull V value) { + return EitherImpl.of(null, requireNonNull(value, "value")); + } + + /** + * Returns an optional containing the value of type {@link U}, if it exists. + * + * @return the first value + */ + @NonNull Optional primary(); + + /** + * Returns an optional containing the value of type {@link V}, if it exists. + * + * @return the second value + */ + @NonNull Optional fallback(); +} diff --git a/cloud-core/src/main/java/cloud/commandframework/types/package-info.java b/cloud-core/src/main/java/cloud/commandframework/types/package-info.java new file mode 100644 index 000000000..fe288947d --- /dev/null +++ b/cloud-core/src/main/java/cloud/commandframework/types/package-info.java @@ -0,0 +1,4 @@ +/** + * Types used throughout Cloud. + */ +package cloud.commandframework.types; diff --git a/cloud-core/src/test/java/cloud/commandframework/arguments/parser/EitherParserTest.java b/cloud-core/src/test/java/cloud/commandframework/arguments/parser/EitherParserTest.java new file mode 100644 index 000000000..e6736d2bc --- /dev/null +++ b/cloud-core/src/test/java/cloud/commandframework/arguments/parser/EitherParserTest.java @@ -0,0 +1,108 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// 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.parser; + +import cloud.commandframework.TestCommandSender; +import cloud.commandframework.arguments.standard.BooleanParser; +import cloud.commandframework.arguments.standard.IntegerParser; +import cloud.commandframework.arguments.suggestion.Suggestion; +import cloud.commandframework.context.CommandContext; +import cloud.commandframework.context.CommandInput; +import cloud.commandframework.types.Either; +import org.junit.jupiter.api.BeforeEach; +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 cloud.commandframework.truth.ArgumentParseResultSubject.assertThat; +import static com.google.common.truth.Truth.assertThat; + +@ExtendWith(MockitoExtension.class) +class EitherParserTest { + + @Mock + private CommandContext context; + + private EitherParser parser; + + @BeforeEach + void setup() { + this.parser = new EitherParser<>( + IntegerParser.integerParser(1, 3), + BooleanParser.booleanParser(false) + ); + } + + @Test + void testParsingSuccessfulPrimary() { + // Arrange + final CommandInput input = CommandInput.of("1"); + + // Act + final ArgumentParseResult> result = this.parser.parseFuture(this.context, input).join(); + + // Assert + assertThat(result).hasParsedValue(Either.ofPrimary(1)); + } + + @Test + void testParsingSuccessfulFallback() { + // Arrange + final CommandInput input = CommandInput.of("false"); + + // Act + final ArgumentParseResult> result = this.parser.parseFuture(this.context, input).join(); + + // Assert + assertThat(result).hasParsedValue(Either.ofFallback(false)); + } + + @Test + void testParsingFailing() { + // Arrange + final CommandInput input = CommandInput.of("sausage"); + + // Act + final ArgumentParseResult> result = this.parser.parseFuture(this.context, input).join(); + + // Assert + assertThat(result).hasFailureThat().isInstanceOf(EitherParser.EitherParseException.class); + } + + @Test + void testSuggestions() { + // Act + final Iterable suggestions = this.parser.suggestionsFuture(this.context, CommandInput.empty()).join(); + + // Assert + assertThat(suggestions).containsExactly( + Suggestion.simple("1"), + Suggestion.simple("2"), + Suggestion.simple("3"), + Suggestion.simple("true"), + Suggestion.simple("false") + ); + } +}