diff --git a/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/ArgumentUtilities.java b/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/ArgumentUtilities.java index 715b04d80..dd8a4861d 100644 --- a/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/ArgumentUtilities.java +++ b/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/ArgumentUtilities.java @@ -4,14 +4,12 @@ import com.mojang.brigadier.StringReader; import com.mojang.brigadier.exceptions.BuiltInExceptionProvider; import com.mojang.brigadier.exceptions.CommandSyntaxException; -import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; import dev.jorel.commandapi.BukkitTooltip; -import dev.jorel.commandapi.arguments.parser.Parser; +import dev.jorel.commandapi.arguments.parser.ParserArgument; +import dev.jorel.commandapi.arguments.parser.ParserLiteral; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; -import java.util.function.Function; - /** * Utilities for creating mock argument parsers */ @@ -53,42 +51,17 @@ public static Message translatedMessage(String key, Object... args) { } // Parser utilities - /** - * A placeholder {@link CommandSyntaxException} that a parser can throw if it doesn't match the input. - * Typically, this exception will be caught immediately using {@link Parser.ExceptionHandler#neverThrowException()}, - * and parsing will continue to the next branch in a {@link Parser#tryParse(Parser.NonTerminal)} chain. - */ - public static final CommandSyntaxException NEXT_BRANCH = new SimpleCommandExceptionType( - () -> "This branch did not match" - ).create(); - - /** - * Returns a new {@link Parser.Literal}. When the returned parser is invoked, if {@link StringReader#canRead()} - * returns {@code false}, then a {@link CommandSyntaxException} will be thrown according to the given {@code exception} - * {@link Function}. If {@link StringReader#canRead()} returns {@code true}, then the returned parser succeeds. - * - * @param exception A {@link Function} that creates a {@link CommandSyntaxException} when the input - * {@link StringReader} does not have any more characters to read. - * @return A {@link Parser.Literal} that checks if the input {@link StringReader} has characters to read. - */ - public static Parser.Literal assertCanRead(Function exception) { - return reader -> { - if (!reader.canRead()) { - throw exception.apply(reader); - } - }; - } /** - * Returns a new {@link Parser.Literal}. When the returned parser is invoked, it tries to read the given + * Returns a new {@link ParserLiteral}. When the returned parser is invoked, it tries to read the given * {@code literal} String from the input {@link StringReader}. If the {@code literal} is present, this parser * succeeds and moves {@link StringReader#getCursor()} to the end of the {@code literal}. Otherwise, this parser * will fail and throw a {@link CommandSyntaxException} with type {@link BuiltInExceptionProvider#literalIncorrect()}. * * @param literal The exact String that is expected to be at the start of the input {@link StringReader}. - * @return A {@link Parser.Literal} that checks if the {@code literal} String can be read from the input {@link StringReader}. + * @return A {@link ParserLiteral} that checks if the {@code literal} String can be read from the input {@link StringReader}. */ - public static Parser.Literal literal(String literal) { + public static ParserLiteral literal(String literal) { return reader -> { if (reader.canRead(literal.length())) { int start = reader.getCursor(); @@ -104,7 +77,7 @@ public static Parser.Literal literal(String literal) { } /** - * Returns a new {@link Parser.Argument} that reads characters from the input {@link StringReader} until it reaches + * Returns a new {@link ParserArgument} that reads characters from the input {@link StringReader} until it reaches * the given terminator character. If the terminator character is not found, the entire * {@link StringReader#getRemaining()} String will be read. *

@@ -116,11 +89,11 @@ public static Parser.Literal literal(String literal) { * if this happens. * * @param terminator The character to stop reading at. - * @return A {@link Parser.Argument} that reads until it finds the given terminator. Note that the returned String will + * @return A {@link ParserArgument} that reads until it finds the given terminator. Note that the returned String will * include the terminator at the end, unless the end of the input {@link StringReader} is reached without finding the * terminator. */ - public static Parser.Argument readUntilWithoutEscapeCharacter(char terminator) { + public static ParserArgument readUntilWithoutEscapeCharacter(char terminator) { return reader -> { int start = reader.getCursor(); diff --git a/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/EntitySelectorArgumentType.java b/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/EntitySelectorArgumentType.java index f8870ca06..72f046994 100644 --- a/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/EntitySelectorArgumentType.java +++ b/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/EntitySelectorArgumentType.java @@ -51,7 +51,7 @@ public static EntitySelectorArgumentType player() { @Override public EntitySelector parse(StringReader reader) throws CommandSyntaxException { - EntitySelector entityselector = EntitySelectorParser.PARSER.parse(reader); + EntitySelector entityselector = EntitySelectorParser.parser.parse(reader); // I don't know why Minecraft does `reader.setCursor(0)` here before throwing exceptions, but it does ¯\_(ツ)_/¯ // That has the goofy result of underlining the whole command when it should really only underline the selector // This is easily fixed, just store `reader.getCursor()` before parsing the selector @@ -73,7 +73,7 @@ public EntitySelector parse(StringReader reader) throws CommandSyntaxException { @Override public CompletableFuture listSuggestions(CommandContext context, SuggestionsBuilder builder) { - return EntitySelectorParser.PARSER.listSuggestions(context, builder); + return EntitySelectorParser.parser.listSuggestions(context, builder); } public static List findManyEntities(CommandContext cmdCtx, String key, boolean allowEmpty) throws CommandSyntaxException { diff --git a/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/EntitySelectorParser.java b/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/EntitySelectorParser.java index 60f1d4b67..c70800c6b 100644 --- a/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/EntitySelectorParser.java +++ b/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/EntitySelectorParser.java @@ -3,14 +3,16 @@ import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; import dev.jorel.commandapi.UnimplementedMethodException; -import dev.jorel.commandapi.arguments.parser.ParameterGetter; import dev.jorel.commandapi.arguments.parser.Parser; +import dev.jorel.commandapi.arguments.parser.ParserLiteral; +import dev.jorel.commandapi.arguments.parser.Result; import dev.jorel.commandapi.arguments.parser.SuggestionProvider; import org.bukkit.Bukkit; import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import java.util.UUID; +import java.util.function.Function; import java.util.function.Predicate; public class EntitySelectorParser { @@ -48,16 +50,12 @@ private EntitySelector build() { } // Parsing - private static final Parser.Literal isSelectorStart = reader -> { - if (!(reader.canRead() && reader.peek() == '@')) throw ArgumentUtilities.NEXT_BRANCH; - }; - - private static Parser.Literal parseSelector(ParameterGetter selectorBuilderGetter) { - return reader -> { + private static ParserLiteral parseSelector(EntitySelectorParser selectorBuilder) { + return Parser.read(reader -> { reader.skip(); // skip @ if (!reader.canRead()) throw ERROR_MISSING_SELECTOR_TYPE.createWithContext(reader); char selectorCode = reader.read(); - EntitySelectorParser selectorBuilder = selectorBuilderGetter.get(); + switch (selectorCode) { case 'p' -> { selectorBuilder.maxResults = 1; @@ -101,14 +99,10 @@ private static Parser.Literal parseSelector(ParameterGetter { - if (!(reader.canRead() && reader.peek() == '[')) throw ArgumentUtilities.NEXT_BRANCH; - }; - - private static Parser.Literal parseSelectorOptions(ParameterGetter selectorBuilderGetter) { + private static ParserLiteral parseSelectorOptions(EntitySelectorParser selectorBuilder) { return reader -> { // TODO: Implement looping to parse these selector options // I'm pretty sure it would basically reuse many other object parsers as well, so maybe do those first @@ -116,14 +110,8 @@ private static Parser.Literal parseSelectorOptions(ParameterGetter { - if (!(reader.canRead() && reader.peek() != ' ')) throw ERROR_INVALID_NAME_OR_UUID.createWithContext(reader); - }; - - private static Parser.Literal parseNameOrUUID(ParameterGetter selectorBuilderGetter) { - return reader -> { - EntitySelectorParser selectorBuilder = selectorBuilderGetter.get(); - + private static ParserLiteral parseNameOrUUID(EntitySelectorParser selectorBuilder) { + return Parser.read(reader -> { int start = reader.getCursor(); String input = reader.readString(); try { @@ -132,7 +120,7 @@ private static Parser.Literal parseNameOrUUID(ParameterGetter 16) { + if (input.isEmpty() || input.length() > 16) { // Also not a valid player name reader.setCursor(start); throw ERROR_INVALID_NAME_OR_UUID.createWithContext(reader); @@ -143,11 +131,7 @@ private static Parser.Literal parseNameOrUUID(ParameterGetter conclude(ParameterGetter selectorBuilderGetter) { - return reader -> selectorBuilderGetter.get().build(); + }).suggests(suggestName); } private static final SuggestionProvider suggestName = (context, builder) -> { @@ -173,44 +157,43 @@ private static Parser.Argument conclude(ParameterGetter builder.suggest("["); - private static final SuggestionProvider suggestOptionsKeyOrClose = (context, builder) -> { - throw new UnimplementedMethodException("Entity selectors with options are not supported"); - }; - public static final Parser PARSER = Parser - .parse(reader -> new EntitySelectorParser()) - .suggests(suggestNameOrSelector) - .alwaysThrowException() - .continueWith(selectorBuilder -> - Parser.tryParse(Parser.read(isSelectorStart) - .neverThrowException() - .continueWith( - Parser.read(parseSelector(selectorBuilder)) - .suggests(suggestSelector) - .alwaysThrowException() - .continueWith( - Parser.tryParse(Parser.read(isSelectorOptionsStart) - .suggests(suggestOpenOptions) - .neverThrowException() - .continueWith( - Parser.read(parseSelectorOptions(selectorBuilder)) - .suggests(suggestOptionsKeyOrClose) - .alwaysThrowException() - // Input @?[???] - .continueWith(conclude(selectorBuilder)) - ) - ).then(conclude(selectorBuilder)) // Input @? - ) - ) - ).then(Parser.read(isNameStart) - .alwaysThrowException() - .continueWith( - Parser.read(parseNameOrUUID(selectorBuilder)) - .suggests(suggestName) - .alwaysThrowException() - // Input name or uuid - .continueWith(conclude(selectorBuilder)) - ) - ) + public static final Parser parser = reader -> { + if (!reader.canRead()) { + // Empty input + return Result.withExceptionAndSuggestions(ERROR_INVALID_NAME_OR_UUID.createWithContext(reader), reader.getCursor(), suggestNameOrSelector); + } + + // Build our selector + EntitySelectorParser selectorBuilder = new EntitySelectorParser(); + Function> conclude = Result.wrapFunctionResult(success -> selectorBuilder.build()); + + if (reader.peek() == '@') { + // Looks like selector + return parseSelector(selectorBuilder).getResult(reader).continueWith( + // Successfully read selector + success -> { + if (reader.canRead() && reader.peek() == '[') { + // Looks like includes selector options + return parseSelectorOptions(selectorBuilder).getResult(reader).continueWith( + // If successful, build the final selector + conclude + // Otherwise, pass original exception + ); + } + + // Otherwise, valid selector, but suggest opening options + return Result.withValueAndSuggestions(selectorBuilder.build(), reader.getCursor(), suggestOpenOptions); + } + // Otherwise pass original exception + ); + } + + // Looks like name/uuid + return parseNameOrUUID(selectorBuilder).getResult(reader).continueWith( + // If successful, build the final selector + conclude + // Otherwise pass original exception ); + }; } diff --git a/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/IntegerRangeArgumentType.java b/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/IntegerRangeArgumentType.java index 364e6dc71..6ff331711 100644 --- a/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/IntegerRangeArgumentType.java +++ b/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/IntegerRangeArgumentType.java @@ -7,8 +7,12 @@ import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; import dev.jorel.commandapi.MockCommandSource; import dev.jorel.commandapi.arguments.parser.Parser; +import dev.jorel.commandapi.arguments.parser.ParserArgument; +import dev.jorel.commandapi.arguments.parser.ParserLiteral; +import dev.jorel.commandapi.arguments.parser.Result; import dev.jorel.commandapi.wrappers.IntegerRange; +import java.util.function.Function; import java.util.function.Predicate; public class IntegerRangeArgumentType implements ArgumentType { @@ -27,8 +31,15 @@ private IntegerRangeArgumentType() { ArgumentUtilities.translatedMessage("argument.range.swapped") ); - private static final Parser.Argument READ_INT_BEFORE_RANGE = reader -> { - // Custom parser avoids reading `..` indicator for range as part of a number + private static final Predicate throwInvalidIntExceptions = exception -> + exception.getType().equals(CommandSyntaxException.BUILT_IN_EXCEPTIONS.readerInvalidInt()); + + private static final ParserLiteral rangeIndicator = ArgumentUtilities.literal(".."); + + private static final ParserArgument readHigh = StringReader::readInt; + + private static final ParserArgument readLow = reader -> { + // Acts like `StringReader#readInt`, but avoids reading `..` indicator for range as part of a number int start = reader.getCursor(); while (reader.canRead()) { @@ -52,87 +63,80 @@ private IntegerRangeArgumentType() { throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.readerInvalidInt().createWithContext(reader, number); } }; - private static final Predicate THROW_INVALID_INT_EXCEPTIONS = exception -> - exception.getType().equals(CommandSyntaxException.BUILT_IN_EXCEPTIONS.readerInvalidInt()); - private static final Parser PARSER = ArgumentUtilities - .assertCanRead(EMPTY_INPUT::createWithContext) - .alwaysThrowException() - .continueWith( - Parser.tryParse(ArgumentUtilities.literal("..") - .neverThrowException() - // Input .. - .continueWith( - Parser.tryParse(Parser.parse(StringReader::readInt) - // It looks like they tried to enter ..high, but high was not a valid int - .throwExceptionIfTrue(THROW_INVALID_INT_EXCEPTIONS) - // Input ..high - .continueWith(high -> Parser.parse( - reader -> IntegerRange.integerRangeLessThanOrEq(high.get()) - )) - ).then(Parser.parse( - // Input just .. - reader -> { - // Move cursor to start of .. - reader.setCursor(reader.getCursor() - 2); - throw EMPTY_INPUT.createWithContext(reader); - } - )) - ) - ).then(Parser.parse(StringReader::getCursor) - .alwaysThrowException() - .continueWith(start -> - Parser.parse(READ_INT_BEFORE_RANGE) - .alwaysMapException(exception -> { - if (exception.getType().equals(CommandSyntaxException.BUILT_IN_EXCEPTIONS.readerExpectedInt())) { - // If we didn't find any int to input, this range is empty - StringReader context = new StringReader(exception.getInput()); - context.setCursor(exception.getCursor()); - return EMPTY_INPUT.createWithContext(context); + public static final Parser parser = reader -> { + if (!reader.canRead()) { + return Result.withException(EMPTY_INPUT.createWithContext(reader)); + } + + Function> handleNumberReadFailure = Result.wrapFunctionResult(exception -> { + if (throwInvalidIntExceptions.test(exception)) { + // Tried to input a number, but it was not a valid int + throw exception; + } + + // Nothing looking like a number was found, empty input + throw EMPTY_INPUT.createWithContext(reader); + }); + + int start = reader.getCursor(); + return rangeIndicator.getResult(reader).continueWith( + // Input .. + // Try to read ..high + success -> readHigh.getResult(reader).continueWith( + // Successfully input ..high + Result.wrapFunctionResult(IntegerRange::integerRangeLessThanOrEq), + // Either input a high that was not an int, or just an empty .. input + handleNumberReadFailure + ), + // No range indicator yet + // Try to read low + failure -> readLow.getResult(reader).continueWith( + // Successfully read low + // Try to read low.. + low -> rangeIndicator.getResult(reader).continueWith( + // Successfully read low.. + // Try to read low..high + success -> readHigh.getResult(reader).continueWith( + // Successfully read low..high + Result.wrapFunctionResult(high -> { + if (low > high) { + throw RANGE_SWAPPED.createWithContext(reader); } - // Otherwise throw original exception (invalid integer) - return exception; + return new IntegerRange(low, high); + }), + // Either input a high that was not an int, or just low.. + Result.wrapFunctionResult(exception -> { + if (throwInvalidIntExceptions.test(exception)) { + // Tried to input low..high, but high was not an int + throw exception; + } + + // Input low.. + return IntegerRange.integerRangeGreaterThanOrEq(low); }) - // Input low - .continueWith(getLow -> - Parser.tryParse(ArgumentUtilities.literal("..") - .neverThrowException() - // Input low.. - .continueWith( - Parser.tryParse(Parser.parse(StringReader::readInt) - .throwExceptionIfTrue(THROW_INVALID_INT_EXCEPTIONS) - .continueWith(getHigh -> Parser.parse( - reader -> { - int low = getLow.get(); - int high = getHigh.get(); - if (low > high) { - // Reset to start of input - reader.setCursor(start.get()); - throw RANGE_SWAPPED.createWithContext(reader); - } - return new IntegerRange(low, high); - } - )) - ).then(Parser.parse( - // Input low.. - reader -> IntegerRange.integerRangeGreaterThanOrEq(getLow.get()) - )) - ) - ).then(Parser.parse( - // Input exact - reader -> { - int exact = getLow.get(); - return new IntegerRange(exact, exact); - } - )) - ) - ) + ), + // Didn't find the range indicator + // Input is just low + Result.wrapFunctionResult(failure2 -> new IntegerRange(low, low)) + ), + // Either input a low that was not an int, or just an empty input + handleNumberReadFailure ) + ).continueWith( + // If we return an IntegerRange, keep that + Result.wrapFunctionResult(success -> success), + // For some reason, Minecraft explicitly maps all exceptions to underline the start of the reader input + // Even something like `..1.0`, which by my intuition should underline `1.0` to show it is not an integer + Result.wrapFunctionResult(exception -> { + throw new CommandSyntaxException(exception.getType(), exception.getRawMessage(), exception.getInput(), start); + }) ); + }; @Override public IntegerRange parse(StringReader reader) throws CommandSyntaxException { - return PARSER.parse(reader); + return parser.parse(reader); } public static IntegerRange getRange(CommandContext cmdCtx, String key) { diff --git a/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/ProfileArgumentType.java b/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/ProfileArgumentType.java index c3995c04c..4df0f2833 100644 --- a/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/ProfileArgumentType.java +++ b/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/ProfileArgumentType.java @@ -9,13 +9,15 @@ import com.mojang.brigadier.suggestion.SuggestionsBuilder; import dev.jorel.commandapi.MockCommandSource; import dev.jorel.commandapi.arguments.parser.Parser; +import dev.jorel.commandapi.arguments.parser.ParserArgument; +import dev.jorel.commandapi.arguments.parser.Result; import org.bukkit.Bukkit; import org.bukkit.entity.Player; import java.util.*; import java.util.concurrent.CompletableFuture; -public class ProfileArgumentType implements ArgumentType { +public class ProfileArgumentType implements ArgumentType { // No internal state is necessary public static final ProfileArgumentType INSTANCE = new ProfileArgumentType(); @@ -25,7 +27,7 @@ private ProfileArgumentType() { // ArgumentType implementation @FunctionalInterface - public interface Result { + public interface ProfileSelector { Collection getProfiles(MockCommandSource source) throws CommandSyntaxException; } @@ -33,65 +35,60 @@ public interface Result { ArgumentUtilities.translatedMessage("argument.player.unknown") ); - private static final Parser.Literal isSelectorStart = reader -> { - if (!(reader.canRead() && reader.peek() == '@')) throw ArgumentUtilities.NEXT_BRANCH; - }; - - private static final Parser PARSER = Parser - .tryParse(Parser.read(isSelectorStart) - .neverThrowException() - .continueWith( - Parser.parse(EntitySelectorParser.PARSER) - .alwaysThrowException() - .continueWith(selectorGetter -> Parser.parse( - reader -> { - EntitySelector selector = selectorGetter.get(); - if (selector.includesEntities()) { - throw EntitySelectorArgumentType.ERROR_ONLY_PLAYERS_ALLOWED.create(); - } - return (Result) source -> { - List players = selector.findPlayers(source); - if (players.isEmpty()) { - throw EntitySelectorArgumentType.NO_PLAYERS_FOUND.create(); - } + private static final ParserArgument readName = ArgumentUtilities.readUntilWithoutEscapeCharacter(' '); - List profiles = new ArrayList<>(players.size()); - for (Player player : players) { - profiles.add(player.getUniqueId()); - } - return profiles; - }; + public static final Parser parser = reader -> { + if (reader.canRead() && reader.peek() == '@') { + // Looks like reading an entity selector + return EntitySelectorParser.parser.getResult(reader).continueWith( + // successfully read an entity selector, adapt it to our profile selector + Result.wrapFunctionResult(entitySelector -> { + if (entitySelector.includesEntities()) { + throw EntitySelectorArgumentType.ERROR_ONLY_PLAYERS_ALLOWED.create(); + } + return (ProfileSelector) source -> { + List players = entitySelector.findPlayers(source); + if (players.isEmpty()) { + throw EntitySelectorArgumentType.NO_PLAYERS_FOUND.create(); } - )) - ) - ).then(ArgumentUtilities.readUntilWithoutEscapeCharacter(' ') - .alwaysThrowException() - .continueWith(nameGetter -> Parser.parse( - reader -> { - String name = nameGetter.get(); - return source -> { - // TODO: I'm not sure if or how this should check if offline player profiles exist - Player player = Bukkit.getPlayerExact(name); - if (player == null) { - throw ERROR_UNKNOWN_PLAYER.create(); + + List profiles = new ArrayList<>(players.size()); + for (Player player : players) { + profiles.add(player.getUniqueId()); } - return Collections.singleton(player.getUniqueId()); + return profiles; }; + }) + // entity selector could not be parsed, pass error unchanged + ); + } + + // Looks like reading a name + return readName.getResult(reader).continueWith( + // Successfully read name, convert to profile selector + Result.wrapFunctionResult(name -> source -> { + // TODO: I'm not sure if or how this should check if offline player profiles exist + Player player = Bukkit.getPlayerExact(name); + if (player == null) { + throw ERROR_UNKNOWN_PLAYER.create(); } - )) + return Collections.singleton(player.getUniqueId()); + }) + // Name was not parsed, pass error unchanged ); + }; @Override - public Result parse(StringReader reader) throws CommandSyntaxException { - return PARSER.parse(reader); + public ProfileSelector parse(StringReader reader) throws CommandSyntaxException { + return parser.parse(reader); } @Override public CompletableFuture listSuggestions(CommandContext context, SuggestionsBuilder builder) { - return EntitySelectorParser.PARSER.listSuggestions(context, builder); + return EntitySelectorParser.parser.listSuggestions(context, builder); } public static Collection getProfiles(CommandContext cmdCtx, String key) throws CommandSyntaxException { - return cmdCtx.getArgument(key, Result.class).getProfiles(cmdCtx.getSource()); + return cmdCtx.getArgument(key, ProfileSelector.class).getProfiles(cmdCtx.getSource()); } } diff --git a/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/parser/ParameterGetter.java b/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/parser/ParameterGetter.java deleted file mode 100644 index 72e2d4174..000000000 --- a/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/parser/ParameterGetter.java +++ /dev/null @@ -1,23 +0,0 @@ -package dev.jorel.commandapi.arguments.parser; - -/** - * A class used to track previous context while parsing. The creator of this object promises to update the stored - * value, which can be retrieved using the {@link #get()} method. - * - * @param The type of object held. - */ -public class ParameterGetter { - // Idea for type safe parameter retrieval from https://github.com/JorelAli/CommandAPI/issues/544 - private T value; - - protected void set(T value) { - this.value = value; - } - - /** - * @return The currently stored value. This will be automatically updated when appropriate by this object's creator. - */ - public T get() { - return value; - } -} diff --git a/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/parser/Parser.java b/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/parser/Parser.java index 516825fa5..e93c5bfa7 100644 --- a/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/parser/Parser.java +++ b/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/parser/Parser.java @@ -6,25 +6,22 @@ import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.SuggestionsBuilder; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; import java.util.concurrent.CompletableFuture; -import java.util.function.Function; -import java.util.function.Predicate; /** * A {@link FunctionalInterface} that reads input from a {@link StringReader} and interprets it as an object. *

* Use {@link #parse(StringReader)} or {@link #listSuggestions(CommandContext, SuggestionsBuilder)} to process input. *

- * Parsers may be directly defined by implementing the abstract method {@link #getResult(StringReader)} method. More - * complex parsers can be created with a builder syntax that starts with one of the following methods: + * Parsers can be directly defined by implementing the abstract method {@link #getResult(StringReader)} method. They may + * also be defined with a slightly different interface by implementing {@link ParserArgument#parse(StringReader)} + * or {@link ParserLiteral#parse(StringReader)}. If you are trying to define one of these using a lambda and Java needs + * extra hints to infer the lambda signature, you can use one of the following static methods: + * *

* * @param The type of object that {@link #parse(StringReader)} returns. @@ -52,10 +49,7 @@ public interface Parser { */ default T parse(StringReader reader) throws CommandSyntaxException { Result result = getResult(reader); - if (result.exception != null) { - throw result.exception; - } - return result.value; + return result.throwOrReturn(); } /** @@ -71,693 +65,48 @@ default CompletableFuture listSuggestions(CommandContext con reader.setCursor(builder.getStart()); Result result = getResult(reader); - if (result.suggestions == null) { + if (result.suggestions() == null) { return builder.buildFuture(); } - builder = builder.createOffset(result.suggestionsStart); - result.suggestions.addSuggestions(context, builder); + builder = builder.createOffset(result.suggestionsStart()); + result.suggestions().addSuggestions(context, builder); return builder.buildFuture(); } - /////////////////// - // Build parsers // - /////////////////// - - // Start builder by reading from input - /** - * Starts building a {@link Parser}. The first action of the built parser will be to read an object or throw - * an exception according to the given {@link Parser}. - * - * @param parser A {@link Parser} object representing the first parse action that should be taken by the final built parser. - * @return A {@link Argument} object to continue the building process with. - * @param The type of object returned by the first parse action. - */ - static Argument parse(Parser parser) { - return parser::parse; - } - - /** - * Starts building a {@link Parser}. The first action of the built parser will be to read an object or throw - * an exception according to the given {@link Argument}. This will typically be defined as a lambda of the form - * {@code (StringReader reader) -> new T()}. - *

- * Note that this method returns exactly the object passed as its parameter. If you already have an {@link Argument} - * object, you don't need to call this method. However, if you are defining a new {@link Argument} object using a - * lambda, this method can be used to help Java understand the intended signature of the lambda and properly interpret - * it as an {@link Argument} object. - * - * @param parser A {@link Argument} object representing the first parse action that should be taken by the final built parser. - * @return A {@link Argument} object to continue the building process with. - * @param The type of object returned by the first parse action. - */ - static Argument parse(Argument parser) { - return parser; - } - - /** - * Starts building a {@link Parser}. The first action of the built parser will be to read from the input or throw - * an exception according to the given {@link Literal}. This will typically be defined as a lambda of the form - * {@code (StringReader reader) -> {}}. - *

- * Note that this method returns exactly the object passed as its parameter. If you already have a {@link Literal} - * object, you don't need to call this method. However, if you are defining a new {@link Literal} object using a - * lambda, this method can be used to help Java understand the intended signature of the lambda and properly interpret - * it as a {@link Literal} object. - * - * @param reader A {@link Literal} object representing the first parse action that should be taken by the final built parser. - * @return A {@link Literal} object to continue the building process with. - */ - static Literal read(Literal reader) { - return reader; - } - - /** - * A {@link FunctionalInterface} used for building a {@link Parser}. You can create an object for this by implementing - * the abstract {@link #parse(StringReader)} method. This interface extends {@code Parser}, so it can be directly - * used as a {@link Parser}. It also provides methods to continue the building process. - *

- * The build process continues with the methods detailed in {@link ExceptionHandler}. - * You can optionally add suggestions for this step of the parsing process using {@link #suggests(SuggestionProvider)}. - * - * @param The type of object that {@link #parse(StringReader)} returns. - */ - @FunctionalInterface - interface Argument extends ExceptionHandler, NonTerminal.Argument> { - // Implement parsing logic - @Override - T parse(StringReader reader) throws CommandSyntaxException; - - @Override - default Result getResult(StringReader reader) { - try { - return Result.withValue(parse(reader)); - } catch (CommandSyntaxException exception) { - return Result.withException(exception); - } - } - - // Optionally continue build - - /** - * An {@link Argument} that also places suggestions at the cursor of the {@link StringReader} when invoked. This - * object can be used as a {@link Parser}, or the building process can continue with the methods detailed in - * {@link ExceptionHandler}. - * - * @param base The {@link Argument} that defines the parsing behavior. - * @param suggestions The {@link SuggestionProvider} that will generate the suggestions. - * @param The type of object that {@link #parse(StringReader)} returns. - */ - record WithSuggestions(Argument base, SuggestionProvider suggestions) implements Argument { - @Override - public T parse(StringReader reader) throws CommandSyntaxException { - return base.parse(reader); - } - - @Override - public Result getResult(StringReader reader) { - int suggestionsStart = reader.getCursor(); - return Argument.super.getResult(reader).withSuggestions(suggestionsStart, suggestions); - } - - @Override - public WithSuggestions suggests(SuggestionProvider suggestions) { - return new WithSuggestions<>(base, suggestions); - } - } - - /** - * Adds suggestions to this step of the parsing process. The suggestions will be placed where the cursor - * of the {@link StringReader} was when the parser was invoked. - * - * @param suggestions The {@link SuggestionProvider} object that will generate the suggestions. - * @return A {@link WithSuggestions} object to continue the building process with. - */ - default WithSuggestions suggests(SuggestionProvider suggestions) { - return new WithSuggestions<>(this, suggestions); - } - - @Override - default TerminalArgument alwaysMapException(Function map) { - return new TerminalArgument<>(this, map); - } - - @Override - default NonTerminal.Argument mapExceptions(Function> map) { - return new NonTerminal.Argument<>(this, map); - } - } - - /** - * A {@link FunctionalInterface} used for building a {@link Parser}. You can create an object for this by implementing - * the abstract {@link #read(StringReader)} method. This interface extends {@code Parser}, so it can be directly - * used as a {@link Parser}. It also provides methods to continue the building process. - *

- * The build process continues with the methods detailed in {@link ExceptionHandler}. - * You can optionally add suggestions for this step of the parsing process using {@link #suggests(SuggestionProvider)}. - */ - @FunctionalInterface - interface Literal extends ExceptionHandler { - // Implement parsing logic - /** - * Reads from the given input and either returns successfully or throws - * an exception explaining why the input could not be interpreted. - * - * @param reader The {@link StringReader} that holds the input to parse. - * @throws CommandSyntaxException If the input is malformed. - */ - void read(StringReader reader) throws CommandSyntaxException; - - @Override - default Result getResult(StringReader reader) { - try { - read(reader); - return Result.withValue(null); - } catch (CommandSyntaxException exception) { - return Result.withException(exception); - } - } - - // Optionally continue build - /** - * An {@link Literal} that also places suggestions at the cursor of the {@link StringReader} when invoked. This - * object can be used as a {@link Parser}, or the building process can continue with the methods detailed in - * {@link ExceptionHandler}. - * - * @param base The {@link Literal} that defines the parsing behavior. - * @param suggestions The {@link SuggestionProvider} that will generate the suggestions. - */ - record WithSuggestions(Literal base, SuggestionProvider suggestions) implements Literal { - @Override - public void read(StringReader reader) throws CommandSyntaxException { - base.read(reader); - } - - @Override - public Result getResult(StringReader reader) { - int suggestionsStart = reader.getCursor(); - return Literal.super.getResult(reader).withSuggestions(suggestionsStart, suggestions); - } - - @Override - public WithSuggestions suggests(SuggestionProvider suggestions) { - return new WithSuggestions(base, suggestions); - } - } - - /** - * Adds suggestions to this step of the parsing process. The suggestions will be placed where the cursor - * of the {@link StringReader} was when the parser was invoked. - * - * @param suggestions The {@link SuggestionProvider} object that will generate the suggestions. - * @return A {@link WithSuggestions} object to continue the building process with. - */ - default WithSuggestions suggests(SuggestionProvider suggestions) { - return new WithSuggestions(this, suggestions); - } - - @Override - default TerminalLiteral alwaysMapException(Function map) { - return new TerminalLiteral(this, map); - } - - @Override - default NonTerminal.Literal mapExceptions(Function> map) { - return new NonTerminal.Literal(this, map); - } - } - - // Define special handling with an exception + // Helper methods for letting Java infer the signature of a lambda /** - * An interface used for building a {@link Parser}. A {@link Parser} usually either returns a value or throws a - * {@link CommandSyntaxException}. The methods in this class can be used to define more specific behavior when an - * exception is thrown. - *

- * These methods will return a {@link Parser} since the resulting object still always either returns a value or - * throws a {@link CommandSyntaxException}: - *

    - *
  • {@link #alwaysMapException(Function)}
  • - *
  • {@link #alwaysThrowException()}
  • - *
- * These objects will always terminate the parsing process with a return value or an exception that will bubble up - * and appear in the final result. - *

- * These methods will return a {@link NonTerminal} because the resulting object may sometimes not return a value - * or throw an exception, and so does not match the interface of a {@link Parser}: - *

    - *
  • {@link #mapExceptions(Function)}
  • - *
  • {@link #throwExceptionIfTrue(Predicate)}
  • - *
  • {@link #neverThrowException()}
  • - *
- * {@link NonTerminal} is intended for use in branching structures (See {@link #tryParse(NonTerminal)}). If a - * {@link NonTerminal} ends up not returning a value or throwing an exception, then the next branch will be - * evaluated. + * Directly returns the {@link Parser} given. This method can help Java infer + * the lambda signature of {@code (StringReader) -> Result}. * + * @param parser The {@link Parser} lambda. + * @return The {@link Parser} object. * @param The type of object that {@link #parse(StringReader)} returns. - * @param The specific type of {@link Parser} returned by the terminal methods to continue the building process. - * @param The specific type of {@link NonTerminal} returned by the non-terminal methods to continue the building process. */ - interface ExceptionHandler, NextNonTerminal extends NonTerminal> extends Parser { - // Either throw it (or substitute with another exception) to match Parser interface - - /** - * Returns a new {@link Parser} that always throws an exception when this {@link Parser} throws - * an exception. The {@link CommandSyntaxException} thrown is determined by the given mapping {@link Function}. - * - * @param map A {@link Function} that takes the original {@link CommandSyntaxException} and returns the - * {@link CommandSyntaxException} that should be thrown by the returned {@link Parser}. - * @return A new {@link Parser} to continue the building process with. - */ - NextTerminal alwaysMapException(Function map); - - /** - * Returns a new {@link Parser} that always throws the exception this {@link Parser} throws. - * - * @return A new {@link Parser} to continue the building process with. - */ - default NextTerminal alwaysThrowException() { - return alwaysMapException(Function.identity()); - } - - // Or catch it to leave the path unresolved - - /** - * Returns a new {@link NonTerminal} that may not throw an exception when this {@link Parser} throws - * an exception. The {@link CommandSyntaxException} thrown is determined by the given mapping {@link Function}. - * - * @param map A {@link Function} that takes the original {@link CommandSyntaxException} and returns a {@link Optional} - * containing the {@link CommandSyntaxException} that should be thrown by the returned {@link Parser}. - * If the {@link Optional} is empty, then no exception is thrown. - * @return A new {@link NonTerminal} to continue the building process with. - */ - NextNonTerminal mapExceptions(Function> map); - - /** - * Returns a new {@link NonTerminal} that may not throw an exception when this {@link Parser} throws - * an exception. Whether a {@link CommandSyntaxException} is thrown is determined by the given {@link Predicate}. - * - * @param test A {@link Predicate} that takes the original {@link CommandSyntaxException} and returns true - * if the exception should be thrown and false is the exception should be caught. - * @return A new {@link NonTerminal} to continue the building process with. - */ - default NextNonTerminal throwExceptionIfTrue(Predicate test) { - return mapExceptions(exception -> Optional.ofNullable(test.test(exception) ? exception : null)); - } - - /** - * Returns a new {@link NonTerminal} that never throws an exception when this {@link Parser} throws - * a {@link CommandSyntaxException}. - * - * @return A new {@link NonTerminal} to continue the building process with. - */ - default NextNonTerminal neverThrowException() { - return mapExceptions(exception -> Optional.empty()); - } - } - - private static Result mergeResultSuggestions(Result newResult, Result oldResult) { - return newResult.suggestions == null ? - newResult.withSuggestions(oldResult.suggestionsStart, oldResult.suggestions) : - newResult; + static Parser parse(Parser parser) { + return parser; } /** - * A class used for building a {@link Parser}. This class implements {@code Parser}, so it can be used - * as a {@link Parser} that always either returns an object or throws a {@link CommandSyntaxException}. - *

- * The build process may also be continued using the {@link #continueWith(Function)} method. + * Directly returns the {@link ParserArgument} given. This method can help Java infer + * the lambda signature of {@code (StringReader) -> T throws CommandSyntaxException}. * - * @param parser The {@link Argument} that defines the parsing behavior. - * @param map A mapping {@link Function} to apply to the exceptions thrown by the given parser. + * @param parser The {@link ParserArgument} lambda. + * @return The {@link ParserArgument} object. * @param The type of object that {@link #parse(StringReader)} returns. */ - record TerminalArgument( - Argument parser, - Function map - ) implements Parser { - // Implement parsing logic - @Override - public Result getResult(StringReader reader) { - Result originalResult = parser.getResult(reader); - if (originalResult.exception != null) { - // If an exception was thrown, apply our mapping function - CommandSyntaxException toThrow = map.apply(originalResult.exception); - // Keep the original suggestions - return Result.withExceptionAndSuggestions(toThrow, originalResult.suggestionsStart, originalResult.suggestions); - } - return originalResult; - } - - // Optionally continue build - - /** - * Returns a new {@link Parser} that continues parsing with the result of this {@link Parser} as context. - *

- * If this {@link Parser} fails, the resulting {@link CommandSyntaxException} will be passed up to be the - * final result. If this {@link Parser} succeeds, its value will be accessible through the provided - * {@link ParameterGetter}, and the {@link Parser} returned by the {@code continueBuild} {@link Function} - * will be invoked to continue parsing the input {@link StringReader}. - *

- * This method is intended for combining the results of {@link Parser}s into more complex composite objects. - * For example: - *

-		 * {@code
-		 * Parser aParser;
-		 * Parser bParser;
-		 *
-		 * // Expected input to create C: AB
-		 * Parser cParser = Parser
-		 * 	// Parse A object from the input
-		 * 	.parse(aParser)
-		 * 	.alwaysThrowException()
-		 * 	.continueWith(aGetter -> Parser
-		 * 		// Parse B object from the input
-		 * 		.parse(bParser)
-		 * 		.alwaysThrowException()
-		 * 		.continueWith(bGetter -> Parser
-		 * 			// Get A and B to create C
-		 * 			.parse(reader -> {
-		 * 				return new C(aGetter.get(), bGetter.get());
-		 * 			})
-		 * 		)
-		 * 	);
-		 * }
-		 * 
- * - * @param continueBuild A {@link Function} that takes a {@link ParameterGetter} and returns a {@link Parser} - * that will perform the next step in the parsing process. - * @return A new {@link Parser} that attempts to invoke this {@link Parser}, and if successful invokes the - * {@link Parser} returned by the {@code continueBuild} {@link Function}. - * @param

The type of object returned by the new {@link Parser}. - */ - public

Parser

continueWith(Function, Parser

> continueBuild) { - ParameterGetter getParameter = new ParameterGetter<>(); - - Parser

continueParser = continueBuild.apply(getParameter); - - return reader -> { - Result parameterResult = this.getResult(reader); - - // If the parser failed, bubble that up as our result - if (parameterResult.exception != null) { - // Convert Result to Result

with same exception and suggestions - return Result.withExceptionAndSuggestions( - parameterResult.exception, - parameterResult.suggestionsStart, parameterResult.suggestions - ); - } - // Otherwise, provide the parsed value as context and keep parsing - getParameter.set(parameterResult.value); - - return mergeResultSuggestions(continueParser.getResult(reader), parameterResult); - }; - } - } - - /** - * A class used for building a {@link Parser}. This class implements {@code Parser}, so it can be used - * as a {@link Parser} that always either succeeds or throws a {@link CommandSyntaxException}. - *

- * The build process may also be continued using the {@link #continueWith(Parser)} method. - * - * @param parser The {@link Literal} that defines the parsing behavior. - * @param map A mapping {@link Function} to apply to the exceptions thrown by the given parser. - */ - record TerminalLiteral( - Literal parser, - Function map - ) implements Parser { - // Implement parsing logic - @Override - public Result getResult(StringReader reader) { - Result originalResult = parser.getResult(reader); - if (originalResult.exception != null) { - // If an exception was thrown, apply our mapping function - CommandSyntaxException toThrow = map.apply(originalResult.exception); - // Keep the original suggestions - return Result.withExceptionAndSuggestions(toThrow, originalResult.suggestionsStart, originalResult.suggestions); - } - return originalResult; - } - - // Optionally continue build - // In the case where the read does not throw an exception, keep parsing - - /** - * Returns a new {@link Parser} that invokes the given {@link Parser} after running this {@link Parser}. - *

- * If this {@link Parser} fails, the resulting {@link CommandSyntaxException} will be passed up to be the - * final result. If this {@link Parser} succeeds, the final result is determined by the given {@link Parser}. - * - * @param continueParser The {@link Parser} to continue with if this {@link Parser} succeeds. - * @return A new {@link Parser} that attempts to invoke this {@link Parser}, and if successful invokes the given {@link Parser}. - * @param

The type of object returned by the new {@link Parser}. - */ - public

Parser

continueWith(Parser

continueParser) { - return reader -> { - Result parameterResult = this.getResult(reader); - - // If the parser failed, bubble that up as our result - if (parameterResult.exception != null) { - // Convert Result to Result

with same exception and suggestions - return Result.withExceptionAndSuggestions( - parameterResult.exception, - parameterResult.suggestionsStart, parameterResult.suggestions - ); - } - - // Otherwise, keep parsing - return mergeResultSuggestions(continueParser.getResult(reader), parameterResult); - }; - } - } - - // Not a Parser since it may not finish with a definite value or exception - /** - * An interface used for building a {@link Parser}. Its abstract method, {@link #getResult(StringReader)} is - * almost identical to {@link Parser#getResult(StringReader)}, except that the {@link Result} may not have a - * final value or exception. This is intended for use in branching {@link Parser} structures (See - * {@link Parser#tryParse(NonTerminal)}), where an inconclusive result means that parsing continues to the next branch. - * - * @param The type of object returned as the value of {@link #getResult(StringReader)}. - */ - interface NonTerminal { - /** - * Parses the given input. The returned {@link Result} may not define a value or exception. - * - * @param reader The {@link StringReader} that holds the input to parse. - * @return A {@link Result} object holding information about the parse. - */ - Result getResult(StringReader reader); - - /** - * A class used for building a {@link Parser}. This class implements {@code NonTerminal}, so it can be used - * as a {@link NonTerminal}. - *

- * The build process may also be continued using the {@link #continueWith(Function)} method. - * - * @param parser The {@link Parser.Argument} that defines the parsing behavior. - * @param map A mapping {@link Function} to apply to the exceptions thrown by the given parser, which may - * choose to not throw an exception and resolve this {@link NonTerminal} with no value or exception. - * @param The type of object returned as the value of {@link #getResult(StringReader)}. - */ - record Argument( - Parser.Argument parser, - Function> map - ) implements NonTerminal { - // Implement parsing logic - @Override - public Result getResult(StringReader reader) { - Result originalResult = parser.getResult(reader); - if (originalResult.exception != null) { - // If an exception was thrown, apply our mapping function - // We may choose to resolve with no final result, in which case value and exception are both null - CommandSyntaxException toThrow = map.apply(originalResult.exception).orElse(null); - // Keep the original suggestions - return Result.withExceptionAndSuggestions(toThrow, originalResult.suggestionsStart, originalResult.suggestions); - } - return originalResult; - } - - // Optionally continue build - // In the case where this returns a value, provide that value as context and try new branches - - /** - * Returns a new {@link NonTerminal} that continues parsing with the result of this {@link NonTerminal} as context. - *

- * If this {@link NonTerminal} fails, the resulting {@link CommandSyntaxException} will be passed up to be - * the final result. If this {@link NonTerminal} resolves without a value or exception, that is the final - * result. If this {@link NonTerminal} succeeds, its value will be accessible through the provided - * {@link ParameterGetter}, and the {@link Parser} returned by the {@code continueBuild} {@link Function} - * will be invoked to continue parsing the input {@link StringReader}. - *

- * See also {@link TerminalArgument#continueWith(Function)}. - * - * @param continueBuild A {@link Function} that takes a {@link ParameterGetter} and returns a {@link Parser} - * that will perform the next step in the parsing process. - * @return A new {@link NonTerminal} that attempts to invoke this {@link NonTerminal}, and if successful - * invokes the {@link Parser} returned by the {@code continueBuild} {@link Function}. - * @param

The type of object returned by the new {@link NonTerminal}. - */ - public

NonTerminal

continueWith(Function, Parser

> continueBuild) { - ParameterGetter getParameter = new ParameterGetter<>(); - - Parser

continueParser = continueBuild.apply(getParameter); - - return reader -> { - Result parameterResult = parser.getResult(reader); - - if (parameterResult.exception != null) { - // If an exception was thrown, apply our mapping function - // We may choose to resolve with no final result, in which case value and exception are both null - CommandSyntaxException toThrow = map.apply(parameterResult.exception).orElse(null); - // Keep the original suggestions - return Result.withExceptionAndSuggestions(toThrow, parameterResult.suggestionsStart, parameterResult.suggestions); - } - // Otherwise, provide the parsed value as context and keep parsing - getParameter.set(parameterResult.value); - - return mergeResultSuggestions(continueParser.getResult(reader), parameterResult); - }; - } - } - - /** - * A class used for building a {@link Parser}. This class implements {@code NonTerminal}, so it can be used - * as a {@link NonTerminal}. - *

- * The build process may also be continued using the {@link #continueWith(Parser)} method. - * - * @param parser The {@link Parser.Literal} that defines the parsing behavior. - * @param map A mapping {@link Function} to apply to the exceptions thrown by the given parser, which may - * choose to not throw an exception and resolve this {@link NonTerminal} with no value or exception. - */ - record Literal( - Parser.Literal parser, - Function> map - ) implements NonTerminal { - // Implement parsing logic - @Override - public Result getResult(StringReader reader) { - Result originalResult = parser.getResult(reader); - if (originalResult.exception != null) { - // If an exception was thrown, apply our mapping function - // We may choose to resolve with no final result, in which case value and exception are both null - CommandSyntaxException toThrow = map.apply(originalResult.exception).orElse(null); - // Keep the original suggestions - return Result.withExceptionAndSuggestions(toThrow, originalResult.suggestionsStart, originalResult.suggestions); - } - return originalResult; - } - - // Optionally continue build - // In the case where the read does not throw an exception, try new branches - - /** - * Returns a new {@link NonTerminal} that invokes the given {@link Parser} after running this {@link NonTerminal}. - *

- * If this {@link NonTerminal} fails, the resulting {@link CommandSyntaxException} will be passed up to be - * the final result. If this {@link NonTerminal} resolves without a value or exception, that is the final - * result. If this {@link NonTerminal} succeeds, the final result is determined by the given {@link Parser}. - * - * @param continueParser The {@link Parser} to continue with if this {@link NonTerminal} succeeds. - * @return A new {@link NonTerminal} that attempts to invoke this {@link NonTerminal}, and if successful invokes the given {@link Parser}. - * @param

The type of object returned by the new {@link Parser}. - */ - public

NonTerminal

continueWith(Parser

continueParser) { - return reader -> { - Result parameterResult = parser.getResult(reader); - - if (parameterResult.exception != null) { - // If an exception was thrown, apply our mapping function - // We may choose to resolve with no final result, in which case value and exception are both null - CommandSyntaxException toThrow = map.apply(parameterResult.exception).orElse(null); - return Result.withExceptionAndSuggestions(toThrow, parameterResult.suggestionsStart, parameterResult.suggestions); - } - - // Otherwise, keep parsing - return mergeResultSuggestions(continueParser.getResult(reader), parameterResult); - }; - } - } - } - - // Try different possibilities - /** - * Starts building a {@link Parser}. The first action of the built parser will be to read an object or throw - * an exception according to the given {@link NonTerminal}. If the {@link NonTerminal} does not return a value or - * throw an exception, then the next branch given will be evaluated. - * - * @param branch A {@link NonTerminal} object representing the first parse action that should be taken by the final built parser. - * @return A new {@link Branches} object to continue the building process with. - * @param The type of object returned by the final built {@link Parser}. - */ - static Branches tryParse(NonTerminal branch) { - return new Branches().thenTryParse(branch); + static ParserArgument argument(ParserArgument parser) { + return parser; } /** - * A class used for building a {@link Parser}. The {@link #thenTryParse(NonTerminal)} method adds {@link NonTerminal} - * steps that should be attempted. The first {@link NonTerminal} that throws a {@link CommandSyntaxException} or - * returns a value determines the final {@link Result} for the final built {@link Parser}. If none of the {@link NonTerminal} - * return a final result, then the {@link Parser} given by {@link #then(Parser)} will determine the final result. + * Directly returns the {@link ParserLiteral} given. This method can help Java infer + * the lambda signature of {@code (StringReader) -> void throws CommandSyntaxException}. * - * @param The type of object returned by the final build {@link Parser}. + * @param reader The {@link ParserLiteral} lambda. + * @return The {@link ParserLiteral} object. */ - class Branches { - private final List> possibilities = new ArrayList<>(); - - // Check through possibly non-terminal results - - /** - * Adds a {@link NonTerminal} branch at this step in the parsing process. If the given {@link NonTerminal} returns - * a final value or exception, that will be the final result of the final built {@link Parser}. If the given - * {@link NonTerminal} does not give a final result, then the next branch will be evaluated. - * - * @param branch The {@link NonTerminal} branch to test. - * @return This {@link Branches} object to continue the building process with. - */ - public Branches thenTryParse(NonTerminal branch) { - this.possibilities.add(branch); - - return this; - } - - // Conclude with a parser that always resolves - - /** - * Finishes building the branching {@link Parser}. The returned {@link Parser} will check each {@link NonTerminal} - * given by {@link #thenTryParse(NonTerminal)}. If none of those give a final result, the {@link Parser} given here - * will be invoked to determine the final {@link Result}. - * - * @param parser The {@link Parser} that concludes this branching step. - * @return A new {@link Parser} that evaluates different branches. - */ - public Parser then(Parser parser) { - // Putting the list into a local variable means that the lambda can hold onto the - // list directly, rather than tracking the `this` object as well. - List> possibilities = this.possibilities; - - return reader -> { - Result suggestionsResult = Result.withValue(null); - - int start = reader.getCursor(); - for (NonTerminal potential : possibilities) { - Result result = potential.getResult(reader); - - if (result.value != null || result.exception != null) { - return result; - } - if (result.suggestions != null) { - suggestionsResult = result; - } - - // Reset cursor for next try - reader.setCursor(start); - } - - return mergeResultSuggestions(parser.getResult(reader), suggestionsResult); - }; - } + static ParserLiteral read(ParserLiteral reader) { + return reader; } } diff --git a/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/parser/ParserArgument.java b/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/parser/ParserArgument.java new file mode 100644 index 000000000..d10a4073c --- /dev/null +++ b/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/parser/ParserArgument.java @@ -0,0 +1,64 @@ +package dev.jorel.commandapi.arguments.parser; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.exceptions.CommandSyntaxException; + +/** + * A {@link FunctionalInterface} with abstract method {@link #parse(StringReader)}. + * This extends {@link Parser}{@code }. + * + * @param The type of object that {@link #parse(StringReader)} returns. + */ +@FunctionalInterface +public interface ParserArgument extends Parser { + // Implement parsing logic + @Override + T parse(StringReader reader) throws CommandSyntaxException; + + @Override + default Result getResult(StringReader reader) { + try { + return Result.withValue(parse(reader)); + } catch (CommandSyntaxException exception) { + return Result.withException(exception); + } + } + + // Add suggestions + + /** + * A {@link ParserArgument} that also places suggestions at the cursor of the {@link StringReader} when invoked. + * + * @param base The {@link ParserArgument} that defines the parsing behavior. + * @param suggestions The {@link SuggestionProvider} that will generate the suggestions. + * @param The type of object that {@link #parse(StringReader)} returns. + */ + record WithSuggestions(ParserArgument base, SuggestionProvider suggestions) implements ParserArgument { + @Override + public T parse(StringReader reader) throws CommandSyntaxException { + return base.parse(reader); + } + + @Override + public Result getResult(StringReader reader) { + int suggestionsStart = reader.getCursor(); + return ParserArgument.super.getResult(reader).withSuggestions(suggestionsStart, suggestions); + } + + @Override + public WithSuggestions suggests(SuggestionProvider suggestions) { + return new WithSuggestions<>(base, suggestions); + } + } + + /** + * Adds suggestions to the {@link Result} of this {@link ParserArgument}. The suggestions will be placed where the + * cursor of the {@link StringReader} was when the parser was invoked. + * + * @param suggestions The {@link SuggestionProvider} object that will generate the suggestions. + * @return A {@link WithSuggestions} object to continue the building process with. + */ + default WithSuggestions suggests(SuggestionProvider suggestions) { + return new WithSuggestions<>(this, suggestions); + } +} diff --git a/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/parser/ParserLiteral.java b/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/parser/ParserLiteral.java new file mode 100644 index 000000000..6584671c4 --- /dev/null +++ b/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/parser/ParserLiteral.java @@ -0,0 +1,71 @@ +package dev.jorel.commandapi.arguments.parser; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.exceptions.CommandSyntaxException; + +/** + * A {@link FunctionalInterface} with abstract method {@link #read(StringReader)}. + * This extends {@link Parser}{@code <}{@link Result.Void}{@code >}, so it only + * reads from the input {@link StringReader} and doesn't return any specific object + * when successful. + */ +@FunctionalInterface +public interface ParserLiteral extends Parser { + // Implement parsing logic + + /** + * Reads from the given input and either returns successfully or throws + * an exception explaining why the input could not be interpreted. + * + * @param reader The {@link StringReader} that holds the input to parse. + * @throws CommandSyntaxException If the input is malformed. + */ + void read(StringReader reader) throws CommandSyntaxException; + + @Override + default Result getResult(StringReader reader) { + try { + read(reader); + return Result.withVoidValue(); + } catch (CommandSyntaxException exception) { + return Result.withException(exception); + } + } + + // Add suggestions + + /** + * A {@link ParserLiteral} that also places suggestions at the cursor of the {@link StringReader} when invoked. + * + * @param base The {@link ParserLiteral} that defines the parsing behavior. + * @param suggestions The {@link SuggestionProvider} that will generate the suggestions. + */ + record WithSuggestions(ParserLiteral base, SuggestionProvider suggestions) implements ParserLiteral { + @Override + public void read(StringReader reader) throws CommandSyntaxException { + base.read(reader); + } + + @Override + public Result getResult(StringReader reader) { + int suggestionsStart = reader.getCursor(); + return ParserLiteral.super.getResult(reader).withSuggestions(suggestionsStart, suggestions); + } + + @Override + public WithSuggestions suggests(SuggestionProvider suggestions) { + return new WithSuggestions(base, suggestions); + } + } + + /** + * Adds suggestions to the {@link Result} of this {@link ParserLiteral}. The suggestions will be placed where the + * cursor of the {@link StringReader} was when the parser was invoked. + * + * @param suggestions The {@link SuggestionProvider} object that will generate the suggestions. + * @return A {@link WithSuggestions} object to continue the building process with. + */ + default WithSuggestions suggests(SuggestionProvider suggestions) { + return new WithSuggestions(this, suggestions); + } +} diff --git a/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/parser/Result.java b/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/parser/Result.java index b6ac180da..0b1634edc 100644 --- a/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/parser/Result.java +++ b/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/parser/Result.java @@ -1,6 +1,10 @@ package dev.jorel.commandapi.arguments.parser; +import com.mojang.brigadier.StringReader; import com.mojang.brigadier.exceptions.CommandSyntaxException; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Function; /** * Holds the information that results from a {@link Parser} parsing input. A parse either creates an object @@ -12,25 +16,55 @@ *

    *
  • {@link #withValue(Object)}
  • *
  • {@link #withValueAndSuggestions(Object, int, SuggestionProvider)}
  • + * + *
  • {@link #withVoidValue()}
  • + *
  • {@link #withVoidValueAndSuggestions(int, SuggestionProvider)}
  • + * *
  • {@link #withException(CommandSyntaxException)}
  • *
  • {@link #withExceptionAndSuggestions(CommandSyntaxException, int, SuggestionProvider)}
  • *
* Additionally, a {@link Result} object with the same value but different * suggestions can be created using {@link #withSuggestions(int, SuggestionProvider)}. + *

+ * The information of a {@link Result} can be accessed directly using {@link #value()}, {@link #exception()}, + * {@link #suggestionsStart()}, or {@link #suggestions()}. Additionally, the information can be interacted with + * using {@link #throwOrReturn()} or {@link #continueWith(Function, Function)}. * - * @param The type of object held as a return result. + * @param The type of object held as a return result. + * @param value The result of parsing input, if the parse was successful. + * @param exception The {@link CommandSyntaxException} thrown, if the parse failed. + * @param suggestionsStart The point in the input {@link StringReader} where the suggestions should be placed. + * @param suggestions The {@link SuggestionProvider} object that will generate the suggestions. */ -public class Result { - protected final T value; - protected final CommandSyntaxException exception; - protected final int suggestionsStart; - protected final SuggestionProvider suggestions; - - private Result(T value, CommandSyntaxException exception, int suggestionsStart, SuggestionProvider suggestions) { - this.value = value; - this.exception = exception; - this.suggestionsStart = suggestionsStart; - this.suggestions = suggestions; +public record Result( + /** + * @param The result of parsing input, if the parse was successful. + */ + T value, + /** + * @param The {@link CommandSyntaxException} thrown, if the parse failed. + */ + CommandSyntaxException exception, + /** + * @param The point in the input {@link StringReader} where the suggestions should be placed. + */ + int suggestionsStart, + /** + * @param The {@link SuggestionProvider} object that will generate the suggestions. + */ + SuggestionProvider suggestions +) { + /** + * A placeholder object that represents a successful parse that didn't return a specific object. + * This allows a null {@link Result} value to indicate an unsuccessful parse. + */ + public static class Void { + // Not using `java.lang.Void` since that class can never be instantiated. + private static final Void INSTANCE = new Void(); + + private Void() { + + } } /** @@ -38,16 +72,23 @@ private Result(T value, CommandSyntaxException exception, int suggestionsStart, * @param The type of object held as a return value. * @return A new {@link Result} object with the given information. */ - public static Result withValue(T value) { + public static Result withValue(@NotNull T value) { return new Result<>(value, null, 0, null); } + /** + * @return A new {@link Result} with a {@link Void} value. + */ + public static Result withVoidValue() { + return withValue(Void.INSTANCE); + } + /** * @param exception The {@link CommandSyntaxException} explaining why a value object could not be created from the given input. * @param The type of object held as a return value. * @return A new {@link Result} object with the given information. */ - public static Result withException(CommandSyntaxException exception) { + public static Result withException(@NotNull CommandSyntaxException exception) { return new Result<>(null, exception, 0, null); } @@ -58,10 +99,19 @@ public static Result withException(CommandSyntaxException exception) { * @param The type of object held as a return value. * @return A new {@link Result} object with the given information. */ - public static Result withValueAndSuggestions(T value, int suggestionsStart, SuggestionProvider suggestions) { + public static Result withValueAndSuggestions(@NotNull T value, int suggestionsStart, SuggestionProvider suggestions) { return new Result<>(value, null, suggestionsStart, suggestions); } + /** + * @param suggestionsStart The point in the input where suggestions should be placed. + * @param suggestions The {@link SuggestionProvider} object that will generate the suggestions. + * @return A new {@link Result} object with a {@link Void} value and the given suggestions. + */ + public static Result withVoidValueAndSuggestions(int suggestionsStart, SuggestionProvider suggestions) { + return withValueAndSuggestions(Void.INSTANCE, suggestionsStart, suggestions); + } + /** * @param exception The {@link CommandSyntaxException} explaining why a value object could not be created from the given input. * @param suggestionsStart The point in the input where suggestions should be placed. @@ -69,7 +119,7 @@ public static Result withValueAndSuggestions(T value, int suggestionsStar * @param The type of object held as a return value. * @return A new {@link Result} object with the given information. */ - public static Result withExceptionAndSuggestions(CommandSyntaxException exception, int suggestionsStart, SuggestionProvider suggestions) { + public static Result withExceptionAndSuggestions(@NotNull CommandSyntaxException exception, int suggestionsStart, SuggestionProvider suggestions) { return new Result<>(null, exception, suggestionsStart, suggestions); } @@ -83,4 +133,110 @@ public static Result withExceptionAndSuggestions(CommandSyntaxException e public Result withSuggestions(int suggestionsStart, SuggestionProvider suggestions) { return new Result<>(this.value, this.exception, suggestionsStart, suggestions); } + + /** + * @return {@link #value()} if the parse was successful. + * @throws CommandSyntaxException {@link #exception()} if the parse failed. + */ + public T throwOrReturn() throws CommandSyntaxException { + if (this.value == null) { + // No value, parsing failed + throw this.exception; + } + + // Parsing succeeded + return this.value; + } + + /** + * Uses this {@link Result} to generate a final {@link Result} with a value of type {@code R}. + * If this parse was successful, then the given {@code success} {@link Function} is used to generate the new {@link Result}. + * If this parse failed, then the given {@code failure} {@link Function} is used to generate the new {@link Result}. + *

+ * If the new {@link Result} does not have any {@link #suggestions()}, then the final {@link Result} returned by + * this method will have the {@link #suggestionsStart()} and {@link #suggestions()} of this {@link Result}. + *

+ * Note that the {@link #wrapFunctionResult(ThrowableFunction)} method may be useful for defining the {@code success} + * and {@code failure} {@link Function}s. + * + * @param success The {@link Function} to use to generate the new {@link Result} when this parse was successful. + * The argument passed to {@link Function#apply(Object)} will be the {@link #value()} of this {@link Result}. + * @param failure The {@link Function} to use to generate the new {@link Result} when this parse failed. + * The argument passed to {@link Function#apply(Object)} will be the {@link #exception()} of this {@link Result}. + * @return The final {@link Result} object. + * @param The type of object held as the {@link #value()} of the final {@link Result}. + */ + public Result continueWith(Function> success, Function> failure) { + Result result; + if (this.value == null) { + // No value, parsing failed + result = failure.apply(this.exception); + } else { + // Continue with value of parse + result = success.apply(this.value); + } + + // Merge suggestions + if (result.suggestions == null) { + return result.withSuggestions(suggestionsStart, suggestions); + } + return result; + } + + /** + * This method works the same as {@link #continueWith(Function, Function)}, but the {@code failure} {@link Function} + * is given as {@code Result::withException}. This has the effect of passing the exception and suggestions of this + * {@link Result} without changes if this parse failed. If this parse succeeded, the {@code success} {@link Function} + * is applied as usual. + * + * @param success The {@link Function} to use to generate the new {@link Result} when this parse was successful. + * The argument passed to {@link Function#apply(Object)} will be the {@link #value()} of this {@link Result}. + * @return The final {@link Result} object. + * @param The type of object held as the {@link #value()} of the final {@link Result}. + */ + public Result continueWith(Function> success) { + return continueWith(success, Result::withException); + } + + /** + * A {@link FunctionalInterface} with abstract method {@link #apply(Object)}. Implementations of this interface + * can be wrapped into a {@link Function}{@code >} using {@link #wrapFunctionResult(ThrowableFunction)}. + * + * @param The type of the function parameter. + * @param The function return type. + */ + @FunctionalInterface + public interface ThrowableFunction { + /** + * Applies this function to the given argument. + * + * @param arg The function argument. + * @return The function result. + * @throws CommandSyntaxException Some exception that caused this to fail. + */ + R apply(T arg) throws CommandSyntaxException; + } + + /** + * Converts a {@link ThrowableFunction}{@code } into a {@link Function}{@code >}. + * The returned {@link Function} will pass its argument unchanged to the given {@link ThrowableFunction}. The outcome + * of the {@link ThrowableFunction} - either returning an object of type {@code R} or throwing a {@link CommandSyntaxException} + * - will be wrapped into a {@link Result}{@code } as appropriate. + * + * @param original The {@link ThrowableFunction} being wrapped. + * @return A {@link Function} that wraps the outcome of the given {@link ThrowableFunction} in a {@link Result}. + * @param The type of the function parameter. + * @param The function return type. + */ + public static Function> wrapFunctionResult(ThrowableFunction original) { + return arg -> { + R result; + try { + result = original.apply(arg); + } catch (CommandSyntaxException exception) { + return Result.withException(exception); + } + return Result.withValue(result); + }; + } } diff --git a/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/test/java/dev/jorel/commandapi/CommandTestBase.java b/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/test/java/dev/jorel/commandapi/CommandTestBase.java index 8223a53e9..2c292a3cd 100644 --- a/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/test/java/dev/jorel/commandapi/CommandTestBase.java +++ b/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/test/java/dev/jorel/commandapi/CommandTestBase.java @@ -10,6 +10,7 @@ import com.mojang.brigadier.context.ParsedArgument; import com.mojang.brigadier.exceptions.CommandSyntaxException; import dev.jorel.commandapi.arguments.parser.Parser; +import dev.jorel.commandapi.arguments.parser.ParserArgument; import dev.jorel.commandapi.executors.CommandExecutionInfo; import org.bukkit.command.CommandSender; import org.junit.jupiter.api.function.Executable; @@ -72,7 +73,7 @@ public CommandContext createContextWithParser( } public CommandContext createContextWithParser( - CommandSender source, String key, Parser.Argument parser, String input + CommandSender source, String key, ParserArgument parser, String input ) throws CommandSyntaxException { return createContextWithParser(source, key, (Parser) parser, input); } diff --git a/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/test/java/dev/jorel/commandapi/arguments/EntitySelectorArgumentTests.java b/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/test/java/dev/jorel/commandapi/arguments/EntitySelectorArgumentTests.java index 95d4dc37d..73aa47c52 100644 --- a/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/test/java/dev/jorel/commandapi/arguments/EntitySelectorArgumentTests.java +++ b/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/test/java/dev/jorel/commandapi/arguments/EntitySelectorArgumentTests.java @@ -72,7 +72,7 @@ void testEmptyInput() { assertThrowsWithMessage( CommandSyntaxException.class, - () -> createContextWithParser(player, "entities", EntitySelectorParser.PARSER, ""), + () -> createContextWithParser(player, "entities", EntitySelectorParser.parser, ""), "Invalid name or UUID at position 0: <--[HERE]" ); } @@ -195,7 +195,7 @@ void test_EntitySelectorArgumentType_tooManyEntitiesFound(boolean twoEntities) t // Entities CommandContext entitiesContext = createContextWithParser( - player1, "entities", EntitySelectorParser.PARSER, "@e" + player1, "entities", EntitySelectorParser.parser, "@e" ); if (twoEntities) { @@ -210,7 +210,7 @@ void test_EntitySelectorArgumentType_tooManyEntitiesFound(boolean twoEntities) t // Players CommandContext playersContext = createContextWithParser( - player1, "players", EntitySelectorParser.PARSER, "@a" + player1, "players", EntitySelectorParser.parser, "@a" ); if (twoEntities) { @@ -587,7 +587,7 @@ void testSelectorUUID() throws CommandSyntaxException { // Note that a command like `test multiple player ` will fail b/c uuid selector includes entities, // but you can still ask an uuid selector to find players directly CommandContext playerUUIDContext = createContextWithParser( - console, "players", EntitySelectorParser.PARSER, playerUUID.toString() + console, "players", EntitySelectorParser.parser, playerUUID.toString() ); assertEquals( List.of(player), @@ -595,7 +595,7 @@ void testSelectorUUID() throws CommandSyntaxException { ); CommandContext entityUUIDContext = createContextWithParser( - console, "players", EntitySelectorParser.PARSER, entityUUID.toString() + console, "players", EntitySelectorParser.parser, entityUUID.toString() ); assertEquals( List.of(), @@ -603,7 +603,7 @@ void testSelectorUUID() throws CommandSyntaxException { ); CommandContext otherUUIDContext = createContextWithParser( - console, "players", EntitySelectorParser.PARSER, otherUUID.toString() + console, "players", EntitySelectorParser.parser, otherUUID.toString() ); assertEquals( List.of(), diff --git a/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/test/java/dev/jorel/commandapi/arguments/IntegerRangeArgumentTests.java b/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/test/java/dev/jorel/commandapi/arguments/IntegerRangeArgumentTests.java index f4a0c04b3..e5aac0e17 100644 --- a/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/test/java/dev/jorel/commandapi/arguments/IntegerRangeArgumentTests.java +++ b/commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/test/java/dev/jorel/commandapi/arguments/IntegerRangeArgumentTests.java @@ -79,13 +79,14 @@ void testFloatInput() { player, "test 1.0..", "Invalid integer '1.0' at position 5: test <--[HERE]" ); + // Yes, Minecraft underlines the whole range when complaining about the last number assertCommandFails( player, "test 0..1.0", - "Invalid integer '1.0' at position 8: test 0..<--[HERE]" + "Invalid integer '1.0' at position 5: test <--[HERE]" ); assertCommandFails( player, "test ..1.0", - "Invalid integer '1.0' at position 7: test ..<--[HERE]" + "Invalid integer '1.0' at position 5: test <--[HERE]" ); }