From 0abcc9f0108d1fea3a508f884ff6e0e53fd0a4c6 Mon Sep 17 00:00:00 2001 From: TheMode Date: Mon, 18 Jul 2022 04:29:44 +0200 Subject: [PATCH] Argument API --- .../java/net/minestom/server/command/Arg.java | 96 +++++ .../net/minestom/server/command/ArgImpl.java | 144 ++++++++ .../server/command/CommandParser.java | 3 +- .../server/command/CommandParserImpl.java | 208 ++++------- .../net/minestom/server/command/Graph.java | 15 +- .../server/command/GraphConverter.java | 301 +++++++++++----- .../minestom/server/command/GraphImpl.java | 42 ++- .../net/minestom/server/command/Parser.java | 142 ++++++++ .../minestom/server/command/ParserImpl.java | 196 ++++++++++ .../minestom/server/command/ParserSpec.java | 114 ++++++ .../server/command/ParserSpecImpl.java | 127 +++++++ .../server/command/ParserSpecTypes.java | 304 ++++++++++++++++ .../server/utils/StringReaderUtils.java | 57 +++ .../server/command/ArgParserTest.java | 133 +++++++ .../minestom/server/command/ArgSpecTest.java | 339 ++++++++++++++++++ .../net/minestom/server/command/ArgTest.java | 70 ++++ .../server/command/CommandCallbackTest.java | 35 ++ .../server/command/CommandPacketTest.java | 69 ++-- .../server/command/CommandParseTest.java | 45 +-- .../minestom/server/command/CommandTest.java | 25 ++ .../server/command/GraphConversionTest.java | 80 ++--- .../server/command/GraphMergeTest.java | 28 +- .../minestom/server/command/GraphTest.java | 19 +- 23 files changed, 2221 insertions(+), 371 deletions(-) create mode 100644 src/main/java/net/minestom/server/command/Arg.java create mode 100644 src/main/java/net/minestom/server/command/ArgImpl.java create mode 100644 src/main/java/net/minestom/server/command/Parser.java create mode 100644 src/main/java/net/minestom/server/command/ParserImpl.java create mode 100644 src/main/java/net/minestom/server/command/ParserSpec.java create mode 100644 src/main/java/net/minestom/server/command/ParserSpecImpl.java create mode 100644 src/main/java/net/minestom/server/command/ParserSpecTypes.java create mode 100644 src/main/java/net/minestom/server/utils/StringReaderUtils.java create mode 100644 src/test/java/net/minestom/server/command/ArgParserTest.java create mode 100644 src/test/java/net/minestom/server/command/ArgSpecTest.java create mode 100644 src/test/java/net/minestom/server/command/ArgTest.java create mode 100644 src/test/java/net/minestom/server/command/CommandCallbackTest.java diff --git a/src/main/java/net/minestom/server/command/Arg.java b/src/main/java/net/minestom/server/command/Arg.java new file mode 100644 index 000000000..312e38303 --- /dev/null +++ b/src/main/java/net/minestom/server/command/Arg.java @@ -0,0 +1,96 @@ +package net.minestom.server.command; + +import net.kyori.adventure.text.Component; +import net.minestom.server.command.builder.ArgumentCallback; +import net.minestom.server.command.builder.CommandContext; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnknownNullability; + +import java.util.List; +import java.util.function.Supplier; + +interface Arg { + static @NotNull Arg arg(@NotNull String id, @NotNull Parser parser, @Nullable Suggestion.Type suggestionType) { + return new ArgImpl<>(id, parser, suggestionType, null, null); + } + + static @NotNull Arg arg(@NotNull String id, @NotNull Parser parser) { + return arg(id, parser, null); + } + + static @NotNull Arg literalArg(@NotNull String id) { + return arg(id, Parser.Literal(id), null); + } + + @NotNull String id(); + + @NotNull Parser parser(); + + Suggestion.@UnknownNullability Type suggestionType(); + + @Nullable Supplier<@NotNull T> defaultValue(); + + @NotNull Arg defaultValue(@Nullable Supplier<@NotNull T> defaultValue); + + default @NotNull Arg defaultValue(@NotNull T defaultValue) { + return defaultValue(() -> defaultValue); + } + + @ApiStatus.Experimental + @Nullable ArgumentCallback callback(); + + @ApiStatus.Experimental + @NotNull Arg callback(@Nullable ArgumentCallback callback); + + interface Suggestion { + sealed interface Type + permits ArgImpl.SuggestionTypeImpl { + @NotNull String name(); + + @NotNull Entry suggest(@NotNull CommandSender sender, @NotNull CommandContext context); + + static @NotNull Type recipes() { + return ArgImpl.SuggestionTypeImpl.RECIPES; + } + + static @NotNull Type sounds() { + return ArgImpl.SuggestionTypeImpl.SOUNDS; + } + + static @NotNull Type entities() { + return ArgImpl.SuggestionTypeImpl.ENTITIES; + } + + static @NotNull Type askServer(@NotNull Callback callback) { + return ArgImpl.SuggestionTypeImpl.askServer(callback); + } + } + + sealed interface Entry + permits ArgImpl.SuggestionEntryImpl { + static @NotNull Entry of(int start, int length, @NotNull List matches) { + return new ArgImpl.SuggestionEntryImpl(start, length, matches); + } + + int start(); + + int length(); + + @NotNull List<@NotNull Match> matches(); + + sealed interface Match + permits ArgImpl.MatchImpl { + @NotNull String text(); + + @Nullable Component tooltip(); + } + } + + @FunctionalInterface + interface Callback { + @NotNull Entry apply(@NotNull CommandSender sender, @NotNull CommandContext context); + } + } +} diff --git a/src/main/java/net/minestom/server/command/ArgImpl.java b/src/main/java/net/minestom/server/command/ArgImpl.java new file mode 100644 index 000000000..675ed3764 --- /dev/null +++ b/src/main/java/net/minestom/server/command/ArgImpl.java @@ -0,0 +1,144 @@ +package net.minestom.server.command; + +import net.kyori.adventure.text.Component; +import net.minestom.server.command.builder.ArgumentCallback; +import net.minestom.server.command.builder.CommandContext; +import net.minestom.server.command.builder.arguments.*; +import net.minestom.server.command.builder.arguments.number.ArgumentDouble; +import net.minestom.server.command.builder.arguments.number.ArgumentFloat; +import net.minestom.server.command.builder.arguments.number.ArgumentInteger; +import net.minestom.server.command.builder.arguments.number.ArgumentLong; +import net.minestom.server.command.builder.suggestion.SuggestionCallback; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; + +record ArgImpl(String id, Parser parser, Suggestion.Type suggestionType, + Supplier defaultValue, ArgumentCallback callback) implements Arg { + static ArgImpl fromLegacy(Argument argument) { + return new ArgImpl<>(argument.getId(), retrieveParser(argument), + retrieveSuggestion(argument), argument.getDefaultValue(), retrieveCallback(argument)); + } + + private static Parser retrieveParser(Argument argument) { + var parserFun = ConversionMap.PARSERS.get(argument.getClass()); + final Parser parser; + if (parserFun != null) { + parser = parserFun.apply(argument); + } else { + // TODO remove legacy conversion + parser = Parser.custom(ParserSpec.legacy(argument)); + } + assert parser != null; + return parser; + } + + private static Suggestion.Type retrieveSuggestion(Argument argument) { + final var type = argument.suggestionType(); + if (type == null) return null; + return switch (type) { + case ALL_RECIPES -> Suggestion.Type.recipes(); + case AVAILABLE_SOUNDS -> Suggestion.Type.sounds(); + case SUMMONABLE_ENTITIES -> Suggestion.Type.entities(); + case ASK_SERVER -> Suggestion.Type.askServer((sender, context) -> { + final SuggestionCallback suggestionCallback = argument.getSuggestionCallback(); + assert suggestionCallback != null; + final String input = context.getInput(); + + final int lastSpace = input.lastIndexOf(" "); + + final int start = lastSpace + 2; + final int length = input.length() - lastSpace - 1; + + final var sug = new net.minestom.server.command.builder.suggestion.Suggestion(input, start, length); + suggestionCallback.apply(sender, context, sug); + + return new SuggestionEntryImpl(sug.getStart(), sug.getLength(), + sug.getEntries().stream().map(entry -> (Suggestion.Entry.Match) new MatchImpl(entry.getEntry(), entry.getTooltip())).toList()); + }); + }; + } + + private static ArgumentCallback retrieveCallback(Argument argument) { + final ArgumentCallback callback = argument.getCallback(); + if (callback == null) return null; + return (sender, context) -> { + callback.apply(sender, context); + }; + } + + @Override + public @NotNull Arg defaultValue(@Nullable Supplier<@NotNull T> defaultValue) { + return new ArgImpl<>(id, parser, suggestionType, defaultValue, callback); + } + + @Override + public @NotNull Arg callback(@Nullable ArgumentCallback callback) { + return new ArgImpl<>(id, parser, suggestionType, defaultValue, callback); + } + + record SuggestionTypeImpl(String name, Suggestion.Callback callback) implements Suggestion.Type { + static final Suggestion.Type RECIPES = new SuggestionTypeImpl("minecraft:all_recipes", null); + static final Suggestion.Type SOUNDS = new SuggestionTypeImpl("minecraft:available_sounds", null); + static final Suggestion.Type ENTITIES = new SuggestionTypeImpl("minecraft:summonable_entities", null); + + static Suggestion.Type askServer(Suggestion.Callback callback) { + return new SuggestionTypeImpl("minecraft:ask_server", callback); + } + + @Override + public @NotNull Suggestion.Entry suggest(@NotNull CommandSender sender, @NotNull CommandContext context) { + final Suggestion.Callback callback = this.callback; + if (callback == null) { + throw new IllegalStateException("Suggestion type is not supported"); + } + return callback.apply(sender, context); + } + } + + record SuggestionEntryImpl(int start, int length, List matches) implements Suggestion.Entry { + SuggestionEntryImpl { + matches = List.copyOf(matches); + } + } + + record MatchImpl(String text, Component tooltip) implements Suggestion.Entry.Match { + } + + static final class ConversionMap { + private static final Map, Function> PARSERS = new ConversionMap() + .append(ArgumentLiteral.class, arg -> Parser.Literal(arg.getId())) + .append(ArgumentBoolean.class, arg -> Parser.Boolean()) + .append(ArgumentFloat.class, arg -> Parser.Float().min(arg.getMin()).max(arg.getMax())) + .append(ArgumentDouble.class, arg -> Parser.Double().min(arg.getMin()).max(arg.getMax())) + .append(ArgumentInteger.class, arg -> Parser.Integer().min(arg.getMin()).max(arg.getMax())) + .append(ArgumentLong.class, arg -> Parser.Long().min(arg.getMin()).max(arg.getMax())) + .append(ArgumentWord.class, arg -> { + final String[] restrictions = arg.getRestrictions(); + if (restrictions != null && restrictions.length > 0) { + return Parser.Literals(restrictions); + } else { + return Parser.String(); + } + }) + .append(ArgumentString.class, arg -> Parser.String().type(Parser.StringParser.Type.QUOTED)) + .append(ArgumentStringArray.class, arg -> Parser.String().type(Parser.StringParser.Type.GREEDY)) + .toMap(); + + private final Map, Function> parsers = new HashMap<>(); + + > ConversionMap append(Class legacyType, Function> converter) { + this.parsers.put(legacyType, arg -> converter.apply((A) arg)); + return this; + } + + Map, Function> toMap() { + return Map.copyOf(parsers); + } + } +} diff --git a/src/main/java/net/minestom/server/command/CommandParser.java b/src/main/java/net/minestom/server/command/CommandParser.java index 78f8e5ac6..f1760df5c 100644 --- a/src/main/java/net/minestom/server/command/CommandParser.java +++ b/src/main/java/net/minestom/server/command/CommandParser.java @@ -1,6 +1,5 @@ package net.minestom.server.command; -import net.minestom.server.command.builder.arguments.Argument; import net.minestom.server.command.builder.suggestion.Suggestion; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Contract; @@ -33,7 +32,7 @@ public interface CommandParser { @Nullable Suggestion suggestion(CommandSender sender); @ApiStatus.Internal - List> args(); + List> args(); sealed interface UnknownCommand extends Result permits CommandParserImpl.UnknownCommandResult { diff --git a/src/main/java/net/minestom/server/command/CommandParserImpl.java b/src/main/java/net/minestom/server/command/CommandParserImpl.java index 007ebd65d..da0c6eeee 100644 --- a/src/main/java/net/minestom/server/command/CommandParserImpl.java +++ b/src/main/java/net/minestom/server/command/CommandParserImpl.java @@ -5,12 +5,10 @@ import net.minestom.server.command.builder.ArgumentCallback; import net.minestom.server.command.builder.CommandContext; import net.minestom.server.command.builder.CommandData; import net.minestom.server.command.builder.CommandExecutor; -import net.minestom.server.command.builder.arguments.Argument; import net.minestom.server.command.builder.condition.CommandCondition; import net.minestom.server.command.builder.exception.ArgumentSyntaxException; import net.minestom.server.command.builder.suggestion.Suggestion; -import net.minestom.server.command.builder.suggestion.SuggestionCallback; -import org.jetbrains.annotations.Contract; +import net.minestom.server.command.builder.suggestion.SuggestionEntry; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -20,7 +18,6 @@ import java.util.ArrayDeque; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -63,17 +60,17 @@ final class CommandParserImpl implements CommandParser { return (sender, context) -> globalListeners.forEach(x -> x.apply(sender, context)); } - SuggestionCallback extractSuggestionCallback() { - return nodeResults.peekLast().callback; + Arg.Suggestion.Type extractSuggestion() { + return nodeResults.peekLast().suggestionType; } - Map> collectArguments() { + Map> collectArguments() { return nodeResults.stream() .skip(1) // skip root .collect(Collectors.toUnmodifiableMap(NodeResult::name, NodeResult::argumentResult)); } - List> getArgs() { + List> getArgs() { return nodeResults.stream().map(x -> x.node.argument()).collect(Collectors.toList()); } } @@ -87,30 +84,31 @@ final class CommandParserImpl implements CommandParser { Node parent = graph.root(); while ((result = parseChild(parent, reader)) != null) { chain.append(result); - if (result.argumentResult instanceof ArgumentResult.SyntaxError e) { + final Node node = result.node; + if (result.argumentResult instanceof ParserSpec.Result.SyntaxError e) { // Syntax error stop at this arg - final ArgumentCallback argumentCallback = parent.argument().getCallback(); + final ArgumentCallback argumentCallback = node.argument().callback(); if (argumentCallback == null && chain.defaultExecutor != null) { return ValidCommand.defaultExecutor(input, chain); } else { return new InvalidCommand(input, chain.mergedConditions(), argumentCallback, e, chain.collectArguments(), chain.mergedGlobalExecutors(), - chain.extractSuggestionCallback(), chain.getArgs()); + chain.extractSuggestion(), chain.getArgs()); } } - parent = result.node; + parent = node; } // Check children for arguments with default values do { Node tmp = parent; parent = null; for (Node child : tmp.next()) { - final Argument argument = child.argument(); - final Supplier defaultSupplier = argument.getDefaultValue(); + final Arg argument = child.argument(); + final Supplier defaultSupplier = argument.defaultValue(); if (defaultSupplier != null) { final Object value = defaultSupplier.get(); - final ArgumentResult argumentResult = new ArgumentResult.Success<>(value, ""); - chain.append(new NodeResult(child, argumentResult, argument.getSuggestionCallback())); + final ParserSpec.Result argumentResult = ParserSpec.Result.success("", -1, value); + chain.append(new NodeResult(child, argumentResult, null)); parent = child; break; } @@ -120,7 +118,8 @@ final class CommandParserImpl implements CommandParser { final NodeResult lastNode = chain.nodeResults.peekLast(); if (lastNode == null) return UnknownCommandResult.INSTANCE; // Verify syntax(s) - final CommandExecutor executor = nullSafeGetter(lastNode.node().execution(), Graph.Execution::executor); + final Graph.Execution execution = lastNode.node.execution(); + final CommandExecutor executor = execution != null ? execution.executor() : null; if (executor == null) { // Syntax error if (chain.defaultExecutor != null) { @@ -140,34 +139,27 @@ final class CommandParserImpl implements CommandParser { return ValidCommand.executor(input, chain, executor); } - @Contract("null, _ -> null; !null, null -> fail; !null, !null -> _") - private static @Nullable R nullSafeGetter(@Nullable T obj, Function getter) { - return obj == null ? null : getter.apply(obj); - } - private static NodeResult parseChild(Node parent, CommandStringReader reader) { if (!reader.hasRemaining()) return null; - for (Node child : parent.next()) { - final Argument argument = child.argument(); - final int start = reader.cursor(); - final ArgumentResult parse = parse(argument, reader); - if (parse instanceof ArgumentResult.Success success) { - return new NodeResult(child, (ArgumentResult) success, - argument.getSuggestionCallback()); - } else if (parse instanceof ArgumentResult.SyntaxError syntaxError) { - return new NodeResult(child, (ArgumentResult) syntaxError, - argument.getSuggestionCallback()); - } else { - // Reset cursor & try next - reader.cursor(start); + final List children = parent.next(); + for (Node child : children) { + final ParserSpec spec = child.argument().parser().spec(); + final ParserSpec.Result parse = parse(spec, reader); + if (parse instanceof ParserSpec.Result.Success success) { + return new NodeResult(child, (ParserSpec.Result) success, + null); + } else if (parse instanceof ParserSpec.Result.SyntaxError syntaxError) { + return new NodeResult(child, (ParserSpec.Result) syntaxError, + null); } } - for (Node node : parent.next()) { - final SuggestionCallback suggestionCallback = node.argument().getSuggestionCallback(); - if (suggestionCallback != null) { + // No argument found, find syntax error from suggestion type + for (Node node : children) { + final Arg.Suggestion.Type suggestionType = node.argument().suggestionType(); + if (suggestionType != null) { return new NodeResult(parent, - new ArgumentResult.SyntaxError<>("None of the arguments were compatible, but a suggestion callback was found.", "", -1), - suggestionCallback); + ParserSpec.Result.error("", "None of the arguments were compatible, but a suggestion callback was found.", -1), + suggestionType); } } return null; @@ -187,7 +179,7 @@ final class CommandParserImpl implements CommandParser { } @Override - public List> args() { + public List> args() { return null; } } @@ -197,35 +189,38 @@ final class CommandParserImpl implements CommandParser { @Nullable CommandCondition condition(); - @NotNull Map> arguments(); + @NotNull Map> arguments(); CommandExecutor globalListener(); - @Nullable SuggestionCallback suggestionCallback(); + @Nullable Arg.Suggestion.Type suggestionType(); @Override default @Nullable Suggestion suggestion(CommandSender sender) { - final SuggestionCallback callback = suggestionCallback(); - if (callback == null) return null; - final int lastSpace = input().lastIndexOf(" "); - final Suggestion suggestion = new Suggestion(input(), lastSpace + 2, input().length() - lastSpace - 1); + final Arg.Suggestion.Type suggestionType = suggestionType(); + if (suggestionType == null) return null; final CommandContext context = createCommandContext(input(), arguments()); - callback.apply(sender, context, suggestion); + final Arg.Suggestion.Entry result = suggestionType.suggest(sender, context); + + Suggestion suggestion = new Suggestion(input(), result.start(), result.length()); + for (var match : result.matches()) { + suggestion.addEntry(new SuggestionEntry(match.text(), match.tooltip())); + } return suggestion; } } record InvalidCommand(String input, CommandCondition condition, ArgumentCallback callback, - ArgumentResult.SyntaxError error, - @NotNull Map> arguments, CommandExecutor globalListener, - @Nullable SuggestionCallback suggestionCallback, List> args) + ParserSpec.Result.SyntaxError error, + @NotNull Map> arguments, CommandExecutor globalListener, + @Nullable Arg.Suggestion.Type suggestionType, List> args) implements InternalKnownCommand, Result.KnownCommand.Invalid { static InvalidCommand invalid(String input, Chain chain) { return new InvalidCommand(input, chain.mergedConditions(), null/*todo command syntax callback*/, - new ArgumentResult.SyntaxError<>("Command has trailing data.", null, -1), - chain.collectArguments(), chain.mergedGlobalExecutors(), chain.extractSuggestionCallback(), chain.getArgs()); + ParserSpec.Result.error("", "Command has trailing data.", -1), + chain.collectArguments(), chain.mergedGlobalExecutors(), chain.extractSuggestion(), chain.getArgs()); } @Override @@ -235,18 +230,19 @@ final class CommandParserImpl implements CommandParser { } record ValidCommand(String input, CommandCondition condition, CommandExecutor executor, - @NotNull Map> arguments, - CommandExecutor globalListener, @Nullable SuggestionCallback suggestionCallback, List> args) + @NotNull Map> arguments, + CommandExecutor globalListener, @Nullable Arg.Suggestion.Type suggestionType, + List> args) implements InternalKnownCommand, Result.KnownCommand.Valid { static ValidCommand defaultExecutor(String input, Chain chain) { return new ValidCommand(input, chain.mergedConditions(), chain.defaultExecutor, chain.collectArguments(), - chain.mergedGlobalExecutors(), chain.extractSuggestionCallback(), chain.getArgs()); + chain.mergedGlobalExecutors(), chain.extractSuggestion(), chain.getArgs()); } static ValidCommand executor(String input, Chain chain, CommandExecutor executor) { return new ValidCommand(input, chain.mergedConditions(), executor, chain.collectArguments(), chain.mergedGlobalExecutors(), - chain.extractSuggestionCallback(), chain.getArgs()); + chain.extractSuggestion(), chain.getArgs()); } @Override @@ -266,7 +262,7 @@ final class CommandParserImpl implements CommandParser { record ValidExecutableCmd(CommandCondition condition, CommandExecutor globalListener, CommandExecutor executor, String input, - Map> arguments) implements ExecutableCommand { + Map> arguments) implements ExecutableCommand { @Override public @NotNull Result execute(@NotNull CommandSender sender) { final CommandContext context = createCommandContext(input, arguments); @@ -287,8 +283,8 @@ final class CommandParserImpl implements CommandParser { } record InvalidExecutableCmd(CommandCondition condition, CommandExecutor globalListener, ArgumentCallback callback, - ArgumentResult.SyntaxError error, String input, - Map> arguments) implements ExecutableCommand { + ParserSpec.Result.SyntaxError error, String input, + Map> arguments) implements ExecutableCommand { @Override public @NotNull Result execute(@NotNull CommandSender sender) { globalListener().apply(sender, createCommandContext(input, arguments)); @@ -297,19 +293,19 @@ final class CommandParserImpl implements CommandParser { return ExecutionResultImpl.PRECONDITION_FAILED; } if (callback != null) - callback.apply(sender, new ArgumentSyntaxException(error.message(), error.input(), error.code())); + callback.apply(sender, new ArgumentSyntaxException(error.message(), error.input(), error.error())); return ExecutionResultImpl.INVALID_SYNTAX; } } - private static CommandContext createCommandContext(String input, Map> arguments) { + private static CommandContext createCommandContext(String input, Map> arguments) { final CommandContext context = new CommandContext(input); for (var entry : arguments.entrySet()) { final String identifier = entry.getKey(); - final ArgumentResult value = entry.getValue(); + final ParserSpec.Result value = entry.getValue(); - final Object argOutput = value instanceof ArgumentResult.Success success ? success.value() : null; - final String argInput = value instanceof ArgumentResult.Success success ? success.input() : ""; + final Object argOutput = value instanceof ParserSpec.Result.Success success ? success.value() : null; + final String argInput = value instanceof ParserSpec.Result.Success success ? success.input() : ""; context.setArg(identifier, argOutput, argInput); } @@ -324,9 +320,9 @@ final class CommandParserImpl implements CommandParser { static final ExecutableCommand.Result INVALID_SYNTAX = new ExecutionResultImpl(Type.INVALID_SYNTAX, null); } - private record NodeResult(Node node, ArgumentResult argumentResult, SuggestionCallback callback) { + private record NodeResult(Node node, ParserSpec.Result argumentResult, Arg.Suggestion.Type suggestionType) { public String name() { - return node.argument().getId(); + return node.argument().id(); } } @@ -342,31 +338,6 @@ final class CommandParserImpl implements CommandParser { return cursor < input.length(); } - String readWord() { - final String input = this.input; - final int cursor = this.cursor; - - final int i = input.indexOf(' ', cursor); - if (i == -1) { - this.cursor = input.length() + 1; - return input.substring(cursor); - } - final String read = input.substring(cursor, i); - this.cursor += read.length() + 1; - return read; - } - - String readRemaining() { - final String input = this.input; - final String result = input.substring(cursor); - this.cursor = input.length(); - return result; - } - - int cursor() { - return cursor; - } - void cursor(int cursor) { assert cursor >= 0 && cursor <= input.length(); this.cursor = cursor; @@ -375,49 +346,16 @@ final class CommandParserImpl implements CommandParser { // ARGUMENT - private static ArgumentResult parse(Argument argument, CommandStringReader reader) { - // Handle specific type without loop - try { - // Single word argument - if (!argument.allowSpace()) { - final String word = reader.readWord(); - return new ArgumentResult.Success<>(argument.parse(word), word); - } - // Complete input argument - if (argument.useRemaining()) { - final String remaining = reader.readRemaining(); - return new ArgumentResult.Success<>(argument.parse(remaining), remaining); - } - } catch (ArgumentSyntaxException ignored) { - return new ArgumentResult.IncompatibleType<>(); - } - // Bruteforce - assert argument.allowSpace() && !argument.useRemaining(); - StringBuilder current = new StringBuilder(reader.readWord()); - while (true) { - try { - final String input = current.toString(); - return new ArgumentResult.Success<>(argument.parse(input), input); - } catch (ArgumentSyntaxException ignored) { - if (!reader.hasRemaining()) break; - current.append(" "); - current.append(reader.readWord()); - } - } - return new ArgumentResult.IncompatibleType<>(); - } - - private sealed interface ArgumentResult { - record Success(T value, String input) - implements ArgumentResult { - } - - record IncompatibleType() - implements ArgumentResult { - } - - record SyntaxError(String message, String input, int code) - implements ArgumentResult { + private static ParserSpec.Result parse(ParserSpec spec, CommandStringReader reader) { + final String input = reader.input; + final ParserSpec.Result result = spec.read(input, reader.cursor); + if (result instanceof ParserSpec.Result.Success success) { + // Increment index by 1 to be at next word + int index = success.index(); + if (index < input.length()) index++; + assert index >= 0 && index <= input.length() : "index out of bounds: " + index + " > " + input.length() + " for " + input; + reader.cursor(index); } + return result; } } diff --git a/src/main/java/net/minestom/server/command/Graph.java b/src/main/java/net/minestom/server/command/Graph.java index 4ef1fb2ef..d28254e9c 100644 --- a/src/main/java/net/minestom/server/command/Graph.java +++ b/src/main/java/net/minestom/server/command/Graph.java @@ -2,7 +2,6 @@ package net.minestom.server.command; import net.minestom.server.command.builder.Command; import net.minestom.server.command.builder.CommandExecutor; -import net.minestom.server.command.builder.arguments.Argument; import net.minestom.server.command.builder.condition.CommandCondition; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -14,11 +13,11 @@ import java.util.function.Consumer; import java.util.function.Predicate; sealed interface Graph permits GraphImpl { - static @NotNull Builder builder(@NotNull Argument argument, @Nullable Execution execution) { + static @NotNull Builder builder(@NotNull Arg argument, @Nullable Execution execution) { return new GraphImpl.BuilderImpl(argument, execution); } - static @NotNull Builder builder(@NotNull Argument argument) { + static @NotNull Builder builder(@NotNull Arg argument) { return new GraphImpl.BuilderImpl(argument, null); } @@ -43,7 +42,7 @@ sealed interface Graph permits GraphImpl { boolean compare(@NotNull Graph graph, @NotNull Comparator comparator); sealed interface Node permits GraphImpl.NodeImpl { - @NotNull Argument argument(); + @NotNull Arg argument(); @UnknownNullability Execution execution(); @@ -69,15 +68,15 @@ sealed interface Graph permits GraphImpl { } sealed interface Builder permits GraphImpl.BuilderImpl { - @NotNull Builder append(@NotNull Argument argument, @Nullable Execution execution, @NotNull Consumer consumer); + @NotNull Builder append(@NotNull Arg argument, @Nullable Execution execution, @NotNull Consumer consumer); - @NotNull Builder append(@NotNull Argument argument, @Nullable Execution execution); + @NotNull Builder append(@NotNull Arg argument, @Nullable Execution execution); - default @NotNull Builder append(@NotNull Argument argument, @NotNull Consumer consumer) { + default @NotNull Builder append(@NotNull Arg argument, @NotNull Consumer consumer) { return append(argument, null, consumer); } - default @NotNull Builder append(@NotNull Argument argument) { + default @NotNull Builder append(@NotNull Arg argument) { return append(argument, (Execution) null); } diff --git a/src/main/java/net/minestom/server/command/GraphConverter.java b/src/main/java/net/minestom/server/command/GraphConverter.java index 2578c62ee..d998309a9 100644 --- a/src/main/java/net/minestom/server/command/GraphConverter.java +++ b/src/main/java/net/minestom/server/command/GraphConverter.java @@ -3,14 +3,62 @@ package net.minestom.server.command; import net.minestom.server.command.builder.arguments.*; import net.minestom.server.entity.Player; import net.minestom.server.network.packet.server.play.DeclareCommandsPacket; +import net.minestom.server.utils.binary.BinaryWriter; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Nullable; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; +import java.util.function.Function; final class GraphConverter { + private static final Map>, String> parserNames = Map.of( + Parser.BooleanParser.class, "brigadier:bool", + Parser.DoubleParser.class, "brigadier:double", + Parser.FloatParser.class, "brigadier:float", + Parser.IntegerParser.class, "brigadier:integer", + Parser.LongParser.class, "brigadier:long", + Parser.StringParser.class, "brigadier:string" + ); + + private static final Map>, Function, byte[]>> propertiesFunctions = Map.ofEntries( + numberProps(Parser.DoubleParser.class, BinaryWriter::writeDouble, Parser.DoubleParser::min, Parser.DoubleParser::max), + numberProps(Parser.FloatParser.class, BinaryWriter::writeFloat, Parser.FloatParser::min, Parser.FloatParser::max), + numberProps(Parser.IntegerParser.class, BinaryWriter::writeInt, Parser.IntegerParser::min, Parser.IntegerParser::max), + numberProps(Parser.LongParser.class, BinaryWriter::writeLong, Parser.LongParser::min, Parser.LongParser::max), + propEntry(Parser.StringParser.class, p -> BinaryWriter.makeArray(w -> w.writeVarInt(switch (p.type()) { + case WORD -> 0; + case QUOTED -> 1; + case GREEDY -> 2; + }))) + ); + + private static > Map.Entry>, Function, byte[]>> propEntry(Class parser, Function func) { + return Map.entry(parser, p -> func.apply(parser.cast(p))); + } + + private static > Map.Entry>, Function, byte[]>> + numberProps(Class

parserClass, BiConsumer numWriter, Function minGetter, Function maxGetter) { + return propEntry(parserClass, p -> BinaryWriter.makeArray(w -> { + final T min = minGetter.apply(p); + final T max = maxGetter.apply(p); + if (min != null && max != null) { + w.write(0x03); + numWriter.accept(w, min); + numWriter.accept(w, max); + } else if (min != null) { + w.write(0x01); + numWriter.accept(w, min); + } else if (max != null) { + w.write(0x02); + numWriter.accept(w, max); + } else { + w.write(0x00); + } + })); + } + private GraphConverter() { //no instance } @@ -19,7 +67,7 @@ final class GraphConverter { public static DeclareCommandsPacket createPacket(Graph graph, @Nullable Player player) { List nodes = new ArrayList<>(); List> redirects = new ArrayList<>(); - Map, Integer> argToPacketId = new HashMap<>(); + Map, Integer> argToPacketId = new HashMap<>(); final AtomicInteger idSource = new AtomicInteger(0); final int rootId = append(graph.root(), nodes, redirects, idSource, null, player, argToPacketId)[0]; for (var r : redirects) { @@ -30,13 +78,14 @@ final class GraphConverter { private static int[] append(Graph.Node graphNode, List to, List> redirects, AtomicInteger id, @Nullable AtomicInteger redirect, - @Nullable Player player, Map, Integer> argToPacketId) { + @Nullable Player player, Map, Integer> argToPacketId) { final Graph.Execution execution = graphNode.execution(); if (player != null && execution != null) { if (!execution.test(player)) return new int[0]; } - final Argument argument = graphNode.argument(); + final Arg arg = graphNode.argument(); + final Parser parser = arg.parser(); final List children = graphNode.next(); final DeclareCommandsPacket.Node node = new DeclareCommandsPacket.Node(); @@ -55,12 +104,12 @@ final class GraphConverter { } } node.children = packetNodeChildren; - if (argument instanceof ArgumentLiteral literal) { - if (literal.getId().isEmpty()) { + if (parser instanceof Parser.LiteralParser literal) { + if (literal.literal().isEmpty()) { node.flags = 0; //root } else { node.flags = literal(false, false); - node.name = argument.getId(); + node.name = arg.id(); if (redirect != null) { node.flags |= 0x8; redirects.add((graph, root) -> node.redirectedNode = redirect.get()); @@ -68,108 +117,121 @@ final class GraphConverter { } to.add(node); return new int[]{id.getAndIncrement()}; + } else if (parser instanceof Parser.LiteralsParser literalsArg) { + return spreadLiteral(to, redirects, redirect, node, literalsArg.literals(), id); } else { - if (argument instanceof ArgumentCommand argCmd) { - node.flags = literal(false, true); - node.name = argument.getId(); - final String shortcut = argCmd.getShortcut(); - if (shortcut.isEmpty()) { - redirects.add((graph, root) -> node.redirectedNode = root); - } else { - redirects.add((graph, root) -> { - final List> args = CommandParser.parser().parse(graph, shortcut).args(); - final Argument last = args.get(args.size() - 1); - if (last.allowSpace()) { - node.redirectedNode = argToPacketId.get(args.get(args.size()-2)); - } else { - node.redirectedNode = argToPacketId.get(last); + if (parser.spec() instanceof ParserSpecImpl.Legacy legacyArg) { + final Argument argument = legacyArg.argument(); + if (argument instanceof ArgumentLiteral literal) { + if (literal.getId().isEmpty()) { + node.flags = 0; //root + } else { + node.flags = literal(false, false); + node.name = argument.getId(); + if (redirect != null) { + node.flags |= 0x8; + redirects.add((graph, root) -> node.redirectedNode = redirect.get()); } - }); - } - to.add(node); + } + to.add(node); + return new int[]{id.getAndIncrement()}; + } else if (argument instanceof ArgumentCommand argCmd) { + node.flags = literal(false, true); + node.name = argument.getId(); + final String shortcut = argCmd.getShortcut(); + if (shortcut.isEmpty()) { + redirects.add((graph, root) -> node.redirectedNode = root); + } else { + redirects.add((graph, root) -> { + node.redirectedNode = argToPacketId.get(findRedirectTargetForArgCmdShortcut(graph, argCmd.getShortcut())); + }); + } + to.add(node); - return new int[]{id.getAndIncrement()}; - } else if (argument instanceof ArgumentEnum || (argument instanceof ArgumentWord word && word.hasRestrictions())) { - List entries = argument instanceof ArgumentEnum ? - ((ArgumentEnum) argument).entries() : - Arrays.stream(((ArgumentWord) argument).getRestrictions()).toList(); - final int[] res = new int[entries.size()]; - for (int i = 0; i < res.length; i++) { - String entry = entries.get(i); - final DeclareCommandsPacket.Node subNode = new DeclareCommandsPacket.Node(); - subNode.children = node.children; - subNode.flags = literal(false, false); - subNode.name = entry; + return new int[]{id.getAndIncrement()}; + } else if (argument instanceof ArgumentEnum || (argument instanceof ArgumentWord word && word.hasRestrictions())) { + return spreadLiteral(to, redirects, redirect, node, argument instanceof ArgumentEnum ? + ((ArgumentEnum) argument).entries() : + Arrays.stream(((ArgumentWord) argument).getRestrictions()).toList(), id); + } else if (argument instanceof ArgumentGroup special) { + List> entries = special.group(); + int[] res = null; + int[] last = new int[0]; + for (int i = 0; i < entries.size(); i++) { + Arg entry = ArgImpl.fromLegacy(entries.get(i)); + if (i == entries.size() - 1) { + // Last will be the parent of next args + final int[] l = append(new GraphImpl.NodeImpl(entry, null, List.of()), to, redirects, + id, redirect, player, argToPacketId); + for (int n : l) { + to.get(n).children = node.children; + } + for (int n : last) { + to.get(n).children = l; + } + return res == null ? l : res; + } else if (i == 0) { + // First will be the children & parent of following + res = append(new GraphImpl.NodeImpl(entry, null, List.of()), to, redirects, id, + null, player, argToPacketId); + last = res; + } else { + final int[] l = append(new GraphImpl.NodeImpl(entry, null, List.of()), to, redirects, + id, null, player, argToPacketId); + for (int n : last) { + to.get(n).children = l; + } + last = l; + } + } + throw new RuntimeException("Arg group must have child args."); + } else if (argument instanceof ArgumentLoop special) { + AtomicInteger r = new AtomicInteger(); + int[] res = new int[special.arguments().size()]; + List> arguments = special.arguments(); + for (int i = 0, appendIndex = 0; i < arguments.size(); i++) { + final int[] append = append(new GraphImpl.NodeImpl(ArgImpl.fromLegacy(arguments.get(i)), null, List.of()), to, + redirects, id, r, player, argToPacketId); + if (append.length == 1) { + res[appendIndex++] = append[0]; + } else { + res = Arrays.copyOf(res, res.length + append.length - 1); + System.arraycopy(append, 0, res, appendIndex, append.length); + appendIndex += append.length; + } + } + r.set(id.get()); + return res; + } else { + // Normal legacy arg + final boolean hasSuggestion = argument.hasSuggestion(); + node.flags = arg(false, hasSuggestion); + node.name = argument.getId(); + node.parser = argument.parser(); + node.properties = argument.nodeProperties(); if (redirect != null) { - subNode.flags |= 0x8; - redirects.add((graph, root) -> subNode.redirectedNode = redirect.get()); + node.flags |= 0x8; + redirects.add((graph, root) -> node.redirectedNode = redirect.get()); } - to.add(subNode); - res[i] = id.getAndIncrement(); - } - return res; - } else if (argument instanceof ArgumentGroup special) { - List> entries = special.group(); - int[] res = null; - int[] last = new int[0]; - for (int i = 0; i < entries.size(); i++) { - Argument entry = entries.get(i); - if (i == entries.size() - 1) { - // Last will be the parent of next args - final int[] l = append(new GraphImpl.NodeImpl(entry, null, List.of()), to, redirects, - id, redirect, player, argToPacketId); - for (int n : l) { - to.get(n).children = node.children; - } - for (int n : last) { - to.get(n).children = l; - } - return res == null ? l : res; - } else if (i == 0) { - // First will be the children & parent of following - res = append(new GraphImpl.NodeImpl(entry, null, List.of()), to, redirects, id, - null, player, argToPacketId); - last = res; - } else { - final int[] l = append(new GraphImpl.NodeImpl(entry, null, List.of()), to, redirects, - id, null, player, argToPacketId); - for (int n : last) { - to.get(n).children = l; - } - last = l; + if (hasSuggestion) { + node.suggestionsType = argument.suggestionType().getIdentifier(); } + to.add(node); + return new int[]{id.getAndIncrement()}; } - throw new RuntimeException("Arg group must have child args."); - } else if (argument instanceof ArgumentLoop special) { - AtomicInteger r = new AtomicInteger(); - int[] res = new int[special.arguments().size()]; - List arguments = special.arguments(); - for (int i = 0, appendIndex = 0; i < arguments.size(); i++) { - Object arg = arguments.get(i); - final int[] append = append(new GraphImpl.NodeImpl((Argument) arg, null, List.of()), to, - redirects, id, r, player, argToPacketId); - if (append.length == 1) { - res[appendIndex++] = append[0]; - } else { - res = Arrays.copyOf(res, res.length + append.length - 1); - System.arraycopy(append, 0, res, appendIndex, append.length); - appendIndex += append.length; - } - } - r.set(id.get()); - return res; } else { - final boolean hasSuggestion = argument.hasSuggestion(); + // Normal arg + final boolean hasSuggestion = arg.suggestionType() != null; node.flags = arg(false, hasSuggestion); - node.name = argument.getId(); - node.parser = argument.parser(); - node.properties = argument.nodeProperties(); + node.name = arg.id(); + node.parser = getParserName(arg); + node.properties = getProperties(arg); if (redirect != null) { node.flags |= 0x8; redirects.add((graph, root) -> node.redirectedNode = redirect.get()); } if (hasSuggestion) { - node.suggestionsType = argument.suggestionType().getIdentifier(); + node.suggestionsType = arg.suggestionType().name(); } to.add(node); return new int[]{id.getAndIncrement()}; @@ -177,6 +239,29 @@ final class GraphConverter { } } + private static int[] spreadLiteral(List nodeList, + List> redirects, + @Nullable AtomicInteger redirect, + DeclareCommandsPacket.Node node, + Collection entries, + AtomicInteger id) { + final int[] res = new int[entries.size()]; + int i = 0; + for (String entry : entries) { + final DeclareCommandsPacket.Node subNode = new DeclareCommandsPacket.Node(); + subNode.children = node.children; + subNode.flags = literal(false, false); + subNode.name = entry; + if (redirect != null) { + subNode.flags |= 0x8; + redirects.add((graph, root) -> subNode.redirectedNode = redirect.get()); + } + nodeList.add(subNode); + res[i++] = id.getAndIncrement(); + } + return res; + } + private static byte literal(boolean executable, boolean hasRedirect) { return DeclareCommandsPacket.getFlag(DeclareCommandsPacket.NodeType.LITERAL, executable, hasRedirect, false); } @@ -184,4 +269,32 @@ final class GraphConverter { private static byte arg(boolean executable, boolean hasSuggestion) { return DeclareCommandsPacket.getFlag(DeclareCommandsPacket.NodeType.ARGUMENT, executable, false, hasSuggestion); } + + private static byte @Nullable [] getProperties(Arg arg) { + final Parser parser = arg.parser(); + if (parser.spec() instanceof ParserSpecImpl.Legacy legacy) { + return legacy.argument().nodeProperties(); + } else { + final Function, byte[]> parserFunction = propertiesFunctions.get(parser.getClass().getInterfaces()[0]); + if (parserFunction == null) return null; + return parserFunction.apply(parser); + } + } + + private static String getParserName(Arg arg) { + if (arg.parser().spec() instanceof ParserSpecImpl.Legacy legacy) { + return legacy.argument().parser(); + } else { + final Class parserClass = arg.parser().getClass().getInterfaces()[0]; + final String s = parserNames.get(parserClass); + if (s == null) throw new RuntimeException("Unsupported parser type: " + parserClass.getSimpleName()); + return s; + } + } + + private static Arg findRedirectTargetForArgCmdShortcut(Graph graph, String shortcut) { + // TODO verify if this works as intended in every case + final List> args = CommandParser.parser().parse(graph, shortcut).args(); + return args.get(args.size() - 1); + } } diff --git a/src/main/java/net/minestom/server/command/GraphImpl.java b/src/main/java/net/minestom/server/command/GraphImpl.java index df1733126..f3c5a8614 100644 --- a/src/main/java/net/minestom/server/command/GraphImpl.java +++ b/src/main/java/net/minestom/server/command/GraphImpl.java @@ -12,8 +12,9 @@ import java.util.*; import java.util.function.Consumer; import java.util.function.Predicate; -import static net.minestom.server.command.builder.arguments.ArgumentType.Literal; -import static net.minestom.server.command.builder.arguments.ArgumentType.Word; +import static net.minestom.server.command.Arg.arg; +import static net.minestom.server.command.Arg.literalArg; +import static net.minestom.server.command.Parser.Literals; record GraphImpl(NodeImpl root) implements Graph { static GraphImpl fromCommand(Command command) { @@ -26,7 +27,7 @@ record GraphImpl(NodeImpl root) implements Graph { static GraphImpl merge(List graphs) { final List children = graphs.stream().map(Graph::root).toList(); - final NodeImpl root = new NodeImpl(Literal(""), null, children); + final NodeImpl root = new NodeImpl(literalArg(""), null, children); return new GraphImpl(root); } @@ -35,13 +36,13 @@ record GraphImpl(NodeImpl root) implements Graph { return compare(root, graph.root(), comparator); } - record BuilderImpl(Argument argument, List children, Execution execution) implements Graph.Builder { - public BuilderImpl(Argument argument, Execution execution) { + record BuilderImpl(Arg argument, List children, Execution execution) implements Graph.Builder { + public BuilderImpl(Arg argument, Execution execution) { this(argument, new ArrayList<>(), execution); } @Override - public Graph.@NotNull Builder append(@NotNull Argument argument, @Nullable Execution execution, + public Graph.@NotNull Builder append(@NotNull Arg argument, @Nullable Execution execution, @NotNull Consumer consumer) { BuilderImpl builder = new BuilderImpl(argument, execution); consumer.accept(builder); @@ -50,7 +51,7 @@ record GraphImpl(NodeImpl root) implements Graph { } @Override - public Graph.@NotNull Builder append(@NotNull Argument argument, @Nullable Execution execution) { + public Graph.@NotNull Builder append(@NotNull Arg argument, @Nullable Execution execution) { this.children.add(new BuilderImpl(argument, List.of(), execution)); return this; } @@ -61,7 +62,7 @@ record GraphImpl(NodeImpl root) implements Graph { } } - record NodeImpl(Argument argument, ExecutionImpl execution, List next) implements Graph.Node { + record NodeImpl(Arg argument, ExecutionImpl execution, List next) implements Graph.Node { static NodeImpl fromBuilder(BuilderImpl builder) { final List children = builder.children; Node[] nodes = new NodeImpl[children.size()]; @@ -113,9 +114,9 @@ record GraphImpl(NodeImpl root) implements Graph { } } - private record ConversionNode(Argument argument, ExecutionImpl execution, - Map, ConversionNode> nextMap) { - ConversionNode(Argument argument, ExecutionImpl execution) { + private record ConversionNode(Arg argument, ExecutionImpl execution, + Map, ConversionNode> nextMap) { + ConversionNode(Arg argument, ExecutionImpl execution) { this(argument, execution, new LinkedHashMap<>()); } @@ -133,7 +134,8 @@ record GraphImpl(NodeImpl root) implements Graph { ConversionNode syntaxNode = root; for (Argument arg : syntax.getArguments()) { boolean last = arg == syntax.getArguments()[syntax.getArguments().length - 1]; - syntaxNode = syntaxNode.nextMap.computeIfAbsent(arg, argument -> { + final Arg convertedArgument = ArgImpl.fromLegacy(arg); + syntaxNode = syntaxNode.nextMap.computeIfAbsent(convertedArgument, argument -> { var ex = last ? ExecutionImpl.fromSyntax(syntax) : null; return new ConversionNode(argument, ex); }); @@ -147,19 +149,23 @@ record GraphImpl(NodeImpl root) implements Graph { } static ConversionNode rootConv(Collection commands) { - Map, ConversionNode> next = new LinkedHashMap<>(commands.size()); + Map, ConversionNode> next = new LinkedHashMap<>(commands.size()); for (Command command : commands) { final ConversionNode conv = fromCommand(command); next.put(conv.argument, conv); } - return new ConversionNode(Literal(""), null, next); + return new ConversionNode(literalArg(""), null, next); } } - static Argument commandToArgument(Command command) { - final String[] aliases = command.getNames(); - if (aliases.length == 1) return Literal(aliases[0]); - return Word(command.getName()).from(command.getNames()); + static Arg commandToArgument(Command command) { + final String commandName = command.getName(); + final String[] names = command.getNames(); + if (names.length == 1) { + return literalArg(commandName); + } else { + return arg(commandName, Literals(names)); + } } static boolean compare(@NotNull Node first, Node second, @NotNull Comparator comparator) { diff --git a/src/main/java/net/minestom/server/command/Parser.java b/src/main/java/net/minestom/server/command/Parser.java new file mode 100644 index 000000000..fb47b2ac8 --- /dev/null +++ b/src/main/java/net/minestom/server/command/Parser.java @@ -0,0 +1,142 @@ +package net.minestom.server.command; + +import net.minestom.server.command.builder.arguments.Argument; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.Set; + +sealed interface Parser { + static @NotNull LiteralParser Literal(@NotNull String literal) { + return ParserImpl.LITERAL.literal(literal); + } + + static @NotNull LiteralsParser Literals(@NotNull Set literals) { + return ParserImpl.LITERALS.literals(literals); + } + + static @NotNull LiteralsParser Literals(@NotNull String @NotNull ... literals) { + return ParserImpl.LITERALS.literals(literals); + } + + static @NotNull BooleanParser Boolean() { + return ParserImpl.BOOLEAN; + } + + static @NotNull FloatParser Float() { + return ParserImpl.FLOAT; + } + + static @NotNull DoubleParser Double() { + return ParserImpl.DOUBLE; + } + + static @NotNull IntegerParser Integer() { + return ParserImpl.INTEGER; + } + + static @NotNull LongParser Long() { + return ParserImpl.LONG; + } + + static @NotNull StringParser String() { + return ParserImpl.STRING; + } + + static @NotNull Custom custom(@NotNull ParserSpec spec) { + return new ParserImpl.CustomImpl<>(spec); + } + + @ApiStatus.Internal + static @NotNull Custom legacy(@NotNull Argument argument) { + return custom(ParserSpec.legacy(argument)); + } + + @NotNull ParserSpec spec(); + + sealed interface LiteralParser extends Parser + permits ParserImpl.LiteralParserImpl { + @NotNull String literal(); + + @NotNull LiteralParser literal(@NotNull String literal); + } + + sealed interface LiteralsParser extends Parser + permits ParserImpl.LiteralsParserImpl { + @Unmodifiable + @NotNull Set literals(); + + @NotNull LiteralsParser literals(@NotNull Set literals); + + default @NotNull LiteralsParser literals(@NotNull String @NotNull ... literals) { + return literals(Set.of(literals)); + } + } + + sealed interface BooleanParser extends Parser + permits ParserImpl.BooleanParserImpl { + } + + sealed interface FloatParser extends Parser + permits ParserImpl.FloatParserImpl { + @Nullable Float max(); + + @Nullable Float min(); + + @NotNull FloatParser max(@Nullable Float max); + + @NotNull FloatParser min(@Nullable Float min); + } + + sealed interface DoubleParser extends Parser + permits ParserImpl.DoubleParserImpl { + @Nullable Double max(); + + @Nullable Double min(); + + @NotNull DoubleParser max(@Nullable Double max); + + @NotNull DoubleParser min(@Nullable Double min); + } + + sealed interface IntegerParser extends Parser + permits ParserImpl.IntegerParserImpl { + @Nullable Integer max(); + + @Nullable Integer min(); + + @NotNull IntegerParser max(@Nullable Integer max); + + @NotNull IntegerParser min(@Nullable Integer min); + } + + sealed interface LongParser extends Parser + permits ParserImpl.LongParserImpl { + @Nullable Long max(); + + @Nullable Long min(); + + @NotNull LongParser max(@Nullable Long max); + + @NotNull LongParser min(@Nullable Long min); + } + + sealed interface StringParser extends Parser + permits ParserImpl.StringParserImpl { + @NotNull Type type(); + + @NotNull StringParser type(@NotNull Type type); + + enum Type { + WORD, + QUOTED, + GREEDY + } + } + + sealed interface Custom extends Parser + permits ParserImpl.CustomImpl { + } +} diff --git a/src/main/java/net/minestom/server/command/ParserImpl.java b/src/main/java/net/minestom/server/command/ParserImpl.java new file mode 100644 index 000000000..e91458950 --- /dev/null +++ b/src/main/java/net/minestom/server/command/ParserImpl.java @@ -0,0 +1,196 @@ +package net.minestom.server.command; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Set; + +final class ParserImpl { + static final LiteralParserImpl LITERAL = new LiteralParserImpl(""); + static final LiteralsParserImpl LITERALS = new LiteralsParserImpl(Set.of()); + static final BooleanParserImpl BOOLEAN = new BooleanParserImpl(); + static final FloatParserImpl FLOAT = new FloatParserImpl(null, null); + static final DoubleParserImpl DOUBLE = new DoubleParserImpl(null, null); + static final IntegerParserImpl INTEGER = new IntegerParserImpl(null, null); + static final LongParserImpl LONG = new LongParserImpl(null, null); + + static final StringParserImpl STRING = new StringParserImpl(Parser.StringParser.Type.WORD); + + record LiteralParserImpl(String literal) implements Parser.LiteralParser { + @Override + public @NotNull LiteralParser literal(@NotNull String literal) { + return new LiteralParserImpl(literal); + } + + @Override + public @NotNull ParserSpec spec() { + return ParserSpec.constant(ParserSpec.Type.WORD, literal); + } + } + + record LiteralsParserImpl(Set literals) implements Parser.LiteralsParser { + LiteralsParserImpl { + literals = Set.copyOf(literals); + } + + @Override + public @NotNull LiteralsParser literals(@NotNull Set literals) { + return new LiteralsParserImpl(literals); + } + + @Override + public @NotNull ParserSpec spec() { + return ParserSpec.constants(ParserSpec.Type.WORD, literals); + } + } + + record BooleanParserImpl() implements Parser.BooleanParser { + @Override + public @NotNull ParserSpec spec() { + return ParserSpec.Type.BOOLEAN; + } + } + + record FloatParserImpl(Float min, Float max) implements Parser.FloatParser { + private static final ParserSpec DEFAULT_SPEC = ParserSpec.Type.FLOAT; + + @Override + public @NotNull FloatParser max(@Nullable Float max) { + return new FloatParserImpl(min, max); + } + + @Override + public @NotNull FloatParser min(@Nullable Float min) { + return new FloatParserImpl(min, max); + } + + @Override + public @NotNull ParserSpec spec() { + if (min == null && max == null) { + return ParserSpec.Type.FLOAT; + } else { + return ParserSpec.specialized(DEFAULT_SPEC, + result -> { + final Float value = result.value(); + if (min != null && value < min) + return ParserSpec.Result.error(result.input(), "value is too low", 2); + if (max != null && value > max) + return ParserSpec.Result.error(result.input(), "value is too high", 3); + return result; + }); + } + } + } + + record DoubleParserImpl(Double min, Double max) implements Parser.DoubleParser { + private static final ParserSpec DEFAULT_SPEC = ParserSpec.Type.DOUBLE; + + @Override + public @NotNull DoubleParser max(@Nullable Double max) { + return new DoubleParserImpl(min, max); + } + + @Override + public @NotNull DoubleParser min(@Nullable Double min) { + return new DoubleParserImpl(min, max); + } + + @Override + public @NotNull ParserSpec spec() { + if (min == null && max == null) { + return DEFAULT_SPEC; + } else { + return ParserSpec.specialized(DEFAULT_SPEC, + result -> { + final Double value = result.value(); + if (min != null && value < min) + return ParserSpec.Result.error(result.input(), "value is too low", 2); + if (max != null && value > max) + return ParserSpec.Result.error(result.input(), "value is too high", 3); + return result; + }); + } + } + } + + record IntegerParserImpl(Integer min, Integer max) implements Parser.IntegerParser { + private static final ParserSpec DEFAULT_SPEC = ParserSpec.Type.INTEGER; + + @Override + public @NotNull IntegerParser max(@Nullable Integer max) { + return new IntegerParserImpl(min, max); + } + + @Override + public @NotNull IntegerParser min(@Nullable Integer min) { + return new IntegerParserImpl(min, max); + } + + @Override + public @NotNull ParserSpec spec() { + if (min == null && max == null) { + return DEFAULT_SPEC; + } else { + return ParserSpec.specialized(DEFAULT_SPEC, + result -> { + final Integer value = result.value(); + if (min != null && value < min) + return ParserSpec.Result.error(result.input(), "value is too low", 2); + if (max != null && value > max) + return ParserSpec.Result.error(result.input(), "value is too high", 3); + return result; + }); + } + } + } + + record LongParserImpl(Long min, Long max) implements Parser.LongParser { + private static final ParserSpec DEFAULT_SPEC = ParserSpec.Type.LONG; + + @Override + public @NotNull LongParser max(@Nullable Long max) { + return new LongParserImpl(min, max); + } + + @Override + public @NotNull LongParser min(@Nullable Long min) { + return new LongParserImpl(min, max); + } + + @Override + public @NotNull ParserSpec spec() { + if (min == null && max == null) { + return DEFAULT_SPEC; + } else { + return ParserSpec.specialized(DEFAULT_SPEC, + result -> { + final Long value = result.value(); + if (min != null && value < min) + return ParserSpec.Result.error(result.input(), "value is too low", 2); + if (max != null && value > max) + return ParserSpec.Result.error(result.input(), "value is too high", 3); + return result; + }); + } + } + } + + record StringParserImpl(StringParser.Type type) implements Parser.StringParser { + @Override + public @NotNull StringParser type(@NotNull Type type) { + return new StringParserImpl(type); + } + + @Override + public @NotNull ParserSpec spec() { + return switch (type) { + case WORD -> ParserSpec.Type.WORD; + case QUOTED -> ParserSpec.Type.QUOTED_PHRASE; + case GREEDY -> ParserSpec.Type.GREEDY_PHRASE; + }; + } + } + + record CustomImpl(ParserSpec spec) implements Parser.Custom { + } +} diff --git a/src/main/java/net/minestom/server/command/ParserSpec.java b/src/main/java/net/minestom/server/command/ParserSpec.java new file mode 100644 index 000000000..56726f43a --- /dev/null +++ b/src/main/java/net/minestom/server/command/ParserSpec.java @@ -0,0 +1,114 @@ +package net.minestom.server.command; + +import net.minestom.server.command.builder.arguments.Argument; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; + +sealed interface ParserSpec + permits ParserSpec.Type, ParserSpecImpl.Constant1, ParserSpecImpl.ConstantN, + ParserSpecImpl.Legacy, ParserSpecImpl.Reader, ParserSpecImpl.Specialized { + + static @NotNull ParserSpec constant(@NotNull Type type, @NotNull T constant) { + return new ParserSpecImpl.Constant1<>(type, constant); + } + + static @NotNull ParserSpec constants(@NotNull Type type, @NotNull Set<@NotNull T> constants) { + return new ParserSpecImpl.ConstantN<>(type, constants); + } + + static @NotNull ParserSpec reader(@NotNull BiFunction<@NotNull String, @NotNull Integer, @Nullable Result> reader) { + return new ParserSpecImpl.Reader<>(reader); + } + + static @NotNull ParserSpec specialized(@NotNull ParserSpec spec, + @NotNull Function, @NotNull Result> filter) { + return new ParserSpecImpl.Specialized<>(spec, filter); + } + + @ApiStatus.Internal + static @NotNull ParserSpec legacy(@NotNull Argument argument) { + return new ParserSpecImpl.Legacy<>(argument); + } + + @NotNull Result read(@NotNull String input, int startIndex); + + default @NotNull Result read(@NotNull String input) { + return read(input, 0); + } + + default @Nullable T readExact(@NotNull String input) { + final Result result = read(input); + return result instanceof Result.Success success && success.index() == input.length() ? + success.value() : null; + } + + sealed interface Type extends ParserSpec + permits ParserSpecTypes.TypeImpl { + Type BOOLEAN = ParserSpecTypes.BOOLEAN; + Type FLOAT = ParserSpecTypes.FLOAT; + Type DOUBLE = ParserSpecTypes.DOUBLE; + Type INTEGER = ParserSpecTypes.INTEGER; + Type LONG = ParserSpecTypes.LONG; + + Type WORD = ParserSpecTypes.WORD; + Type QUOTED_PHRASE = ParserSpecTypes.QUOTED_PHRASE; + Type GREEDY_PHRASE = ParserSpecTypes.GREEDY_PHRASE; + + @NotNull ParserSpec.Result equals(@NotNull String input, int startIndex, @NotNull T constant); + + @NotNull ParserSpec.Result find(@NotNull String input, int startIndex, @NotNull Set<@NotNull T> constants); + + @Nullable T equalsExact(@NotNull String input, @NotNull T constant); + + @Nullable T findExact(@NotNull String input, @NotNull Set<@NotNull T> constants); + } + + + sealed interface Result { + static Result.@NotNull Success success(@NotNull String input, int index, @NotNull T value) { + return new ParserSpecTypes.ResultSuccessImpl<>(input, index, value); + } + + static Result.@NotNull SyntaxError error(@NotNull String input, @NotNull String message, int error) { + return new ParserSpecTypes.ResultErrorImpl<>(input, message, error); + } + + static Result.@NotNull IncompatibleType incompatible() { + return new ParserSpecTypes.ResultIncompatibleImpl<>(); + } + + sealed interface Success extends Result + permits ParserSpecTypes.ResultSuccessImpl { + + @NotNull String input(); + + /** + * Indicates how much data was read from the input + * + * @return the index of the next unread character + */ + int index(); + + @NotNull T value(); + } + + sealed interface IncompatibleType extends Result + permits ParserSpecTypes.ResultIncompatibleImpl { + } + + sealed interface SyntaxError extends Result + permits ParserSpecTypes.ResultErrorImpl { + + @NotNull String input(); + + @NotNull String message(); + + int error(); + } + } +} diff --git a/src/main/java/net/minestom/server/command/ParserSpecImpl.java b/src/main/java/net/minestom/server/command/ParserSpecImpl.java new file mode 100644 index 000000000..ca369f6c5 --- /dev/null +++ b/src/main/java/net/minestom/server/command/ParserSpecImpl.java @@ -0,0 +1,127 @@ +package net.minestom.server.command; + +import net.minestom.server.command.builder.arguments.Argument; +import net.minestom.server.command.builder.exception.ArgumentSyntaxException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; + +import static net.minestom.server.command.ParserSpec.Result.*; + +final class ParserSpecImpl { + + /** + * Reads from a trusted type, and compare to the constant. + *

+ * Reading can be optimized using a raw string comparison to avoid parsing altogether. + */ + record Constant1(Type type, T constant) implements ParserSpec { + @Override + public @NotNull Result read(@NotNull String input, int startIndex) { + return type.equals(input, startIndex, constant); + } + + @Override + public @Nullable T readExact(@NotNull String input) { + return type.equalsExact(input, constant); + } + } + + /** + * Reads from a trusted type, and compare to a set of constants. + *

+ * Reading can be optimized using map lookups. + * + * @see Constant1 for raw string comparison, also relevant here + */ + record ConstantN(Type type, Set constants) implements ParserSpec { + ConstantN { + constants = Set.copyOf(constants); + } + + @Override + public @NotNull Result read(@NotNull String input, int startIndex) { + return type.find(input, startIndex, constants); + } + + @Override + public @Nullable T readExact(@NotNull String input) { + return type.findExact(input, constants); + } + } + + /** + * Reads from arbitrary code. + *

+ * Cannot be optimized at all, but more flexible. + */ + record Reader(BiFunction> reader) implements ParserSpec { + @Override + public @NotNull Result read(@NotNull String input, int startIndex) { + return reader.apply(input, startIndex); + } + } + + /** + * Reuses an existing spec but with an additional filter. + *

+ * The filter means that the parsec input has to pass through the arbitrary function, limiting potential optimizations. + */ + record Specialized(ParserSpec spec, Function, Result> filter) implements ParserSpec { + @Override + public @NotNull Result read(@NotNull String input, int startIndex) { + final Result result = spec.read(input); + if (!(result instanceof Result.Success success)) return result; + return filter.apply(success); + } + } + + record Legacy(Argument argument) implements ParserSpec { + @Override + public @NotNull Result read(@NotNull String input, int startIndex) { + final String sub = input.substring(startIndex); + final String[] split = sub.split(" "); + // Handle specific type without loop + try { + // Single word argument + if (!argument.allowSpace()) { + final String word = split[0]; + final int index = startIndex + word.length(); + final T value = argument.parse(word); + return success(input, index, value); + } + // Complete input argument + if (argument.useRemaining()) { + final T value = argument.parse(sub); + return success(input, input.length(), value); + } + } catch (ArgumentSyntaxException exception) { + return error(exception.getInput(), exception.getMessage(), exception.getErrorCode()); + } + // Bruteforce + assert argument.allowSpace() && !argument.useRemaining(); + StringBuilder current = new StringBuilder(); + + ArgumentSyntaxException lastException = null; + for (String word : split) { + if (!current.isEmpty()) current.append(' '); + current.append(word); + try { + final String result = current.toString(); + final T value = argument.parse(result); + final int index = result.length() + startIndex; + return success(result, index, value); + } catch (ArgumentSyntaxException exception) { + lastException = exception; + } + } + if (lastException != null) { + return error(lastException.getInput(), lastException.getMessage(), lastException.getErrorCode()); + } + return incompatible(); + } + } +} diff --git a/src/main/java/net/minestom/server/command/ParserSpecTypes.java b/src/main/java/net/minestom/server/command/ParserSpecTypes.java new file mode 100644 index 000000000..ad5eda6c1 --- /dev/null +++ b/src/main/java/net/minestom/server/command/ParserSpecTypes.java @@ -0,0 +1,304 @@ +package net.minestom.server.command; + +import net.minestom.server.utils.StringReaderUtils; +import net.minestom.server.utils.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; +import java.util.Set; + +import static net.minestom.server.command.ParserSpec.Result.*; + +final class ParserSpecTypes { + static final ParserSpec.Type BOOLEAN = ParserSpecTypes.builder((input, startIndex) -> { + final int index = input.indexOf(' ', startIndex); + if (index == -1) { + // Whole input is a float + final String word = input.substring(startIndex); + final Boolean value = word.equals("true") ? Boolean.TRUE : word.equals("false") ? Boolean.FALSE : null; + if (value == null) return incompatible(); + return success(word, input.length(), value); + } else { + // Part of input is a float + final String word = input.substring(startIndex, index); + final Boolean value = word.equals("true") ? Boolean.TRUE : word.equals("false") ? Boolean.FALSE : null; + if (value == null) return incompatible(); + return success(word, index, value); + } + }) + .build(); + static final ParserSpec.Type FLOAT = ParserSpecTypes.builder((input, startIndex) -> { + final int index = input.indexOf(' ', startIndex); + final String word = index == -1 ? input.substring(startIndex) : input.substring(startIndex, index); + final int resultIndex = index == -1 ? input.length() : index; + try { + final float value = Float.parseFloat(word); + return success(word, resultIndex, value); + } catch (NumberFormatException e) { + return incompatible(); + } + }) + .build(); + static final ParserSpec.Type DOUBLE = ParserSpecTypes.builder((input, startIndex) -> { + final int index = input.indexOf(' ', startIndex); + final String word = index == -1 ? input.substring(startIndex) : input.substring(startIndex, index); + final int resultIndex = index == -1 ? input.length() : index; + try { + final double value = Double.parseDouble(word); + return success(word, resultIndex, value); + } catch (NumberFormatException e) { + return incompatible(); + } + }) + .build(); + static final ParserSpec.Type INTEGER = ParserSpecTypes.builder((input, startIndex) -> { + final int index = input.indexOf(' ', startIndex); + final String word = index == -1 ? input.substring(startIndex) : input.substring(startIndex, index); + final int resultIndex = index == -1 ? input.length() : index; + try { + final int value = Integer.parseInt(input, startIndex, resultIndex, 10); + return success(word, resultIndex, value); + } catch (NumberFormatException e) { + return incompatible(); + } + }) + .build(); + static final ParserSpec.Type LONG = ParserSpecTypes.builder((input, startIndex) -> { + final int index = input.indexOf(' ', startIndex); + final String word = index == -1 ? input.substring(startIndex) : input.substring(startIndex, index); + final int resultIndex = index == -1 ? input.length() : index; + try { + final long value = Long.parseLong(input, startIndex, resultIndex, 10); + return success(word, resultIndex, value); + } catch (NumberFormatException e) { + return incompatible(); + } + }) + .build(); + static final ParserSpec.Type WORD = ParserSpecTypes.builder((input, startIndex) -> { + final int index = input.indexOf(' ', startIndex); + if (index == -1) { + // No space found, so it's a word + final String word = input.substring(startIndex); + return success(word, input.length(), word); + } else { + // Space found, substring the word + final String word = input.substring(startIndex, index); + return success(word, index, word); + } + }) + .equals((input, startIndex, constant) -> { + final int length = constant.length(); + if (input.regionMatches(startIndex, constant, 0, length)) { + final int index = startIndex + length; + return success(constant, index, constant); + } else { + return incompatible(); + } + }) + .find((input, startIndex, constants) -> { + for (String constant : constants) { + final int length = constant.length(); + if (input.regionMatches(startIndex, constant, 0, length)) { + final int index = startIndex + length; + return success(constant, index, constant); + } + } + return incompatible(); + }) + .equalsExact((input, constant) -> input.equals(constant) ? constant : null) + .findExact((input, constants) -> constants.contains(input) ? input : null) + .build(); + static final ParserSpec.Type QUOTED_PHRASE = ParserSpecTypes.builder((input, startIndex) -> { + final int inclusiveEnd = StringReaderUtils.endIndexOfQuotableString(input, startIndex); + if (inclusiveEnd == -1) { + return incompatible(); + } else { + final char type = input.charAt(startIndex); + final int exclusiveEnd = inclusiveEnd + 1; + if (type == '"' || type == '\'') { + // Quoted + return success(input.substring(startIndex, exclusiveEnd), exclusiveEnd, + StringUtils.unescapeJavaString(input.substring(startIndex + 1, inclusiveEnd))); + } else { + // Unquoted + final String substring = input.substring(startIndex, exclusiveEnd); + return success(substring, exclusiveEnd, substring); + } + } + }) + .build(); + static final ParserSpec.Type GREEDY_PHRASE = ParserSpecTypes.builder((input, startIndex) -> { + final String result = input.substring(startIndex); + return success(result, input.length(), result); + }) + .build(); + + static Builder builder(Functions.Read read) { + return new Builder<>(read); + } + + private interface Functions { + @FunctionalInterface + interface Read { + ParserSpec.Result read(String input, int startIndex); + } + + @FunctionalInterface + interface Find { + ParserSpec.Result find(String input, int startIndex, Set constants); + } + + @FunctionalInterface + interface Equals { + ParserSpec.Result equals(String input, int startIndex, T constant); + } + + @FunctionalInterface + interface ReadExact { + T readExact(String input); + } + + @FunctionalInterface + interface FindExact { + T findExact(String input, Set constants); + } + + @FunctionalInterface + interface EqualsExact { + T equalsExact(String input, T constant); + } + } + + static final class Builder { + final Functions.Read read; + Functions.Equals equals; + Functions.Find find; + + Functions.ReadExact readExact; + Functions.EqualsExact equalsExact; + Functions.FindExact findExact; + + Builder(Functions.Read read) { + this.read = read; + } + + public Builder equals(Functions.Equals equals) { + this.equals = equals; + return this; + } + + public Builder find(Functions.Find find) { + this.find = find; + return this; + } + + Builder readExact(Functions.ReadExact exact) { + this.readExact = exact; + return this; + } + + Builder equalsExact(Functions.EqualsExact equalsExact) { + this.equalsExact = equalsExact; + return this; + } + + Builder findExact(Functions.FindExact findExact) { + this.findExact = findExact; + return this; + } + + ParserSpec.Type build() { + return new TypeImpl<>(read, equals, find, readExact, equalsExact, findExact); + } + } + + record TypeImpl(Functions.Read read, + Functions.Equals equals, Functions.Find find, + Functions.ReadExact readExact, + Functions.EqualsExact equalsExact, Functions.FindExact findExact) + implements ParserSpec.Type { + + TypeImpl { + // Create fallback if no specialized function is provided + equals = Objects.requireNonNullElse(equals, (input, startIndex, constant) -> { + final ParserSpec.Result result = read(input, startIndex); + assertInput(result, input); + if (result instanceof Result.Success success && !constant.equals(success.value())) { + return error(success.input(), "Expected constant '" + constant + "' but found '" + success.value() + "'", 0); + } + return result; + }); + find = Objects.requireNonNullElse(find, (input, startIndex, constants) -> { + final ParserSpec.Result result = read(input, startIndex); + assertInput(result, input); + if (result instanceof Result.Success success && !constants.contains(success.value())) { + return error(success.input(), "Expected constants '" + constants + "' but found '" + success.value() + "'", 0); + } + return result; + }); + readExact = Objects.requireNonNullElse(readExact, (input) -> { + final ParserSpec.Result result = read(input, 0); + if (result instanceof Result.Success success && input.length() == success.index()) { + assertInput(result, input); + return success.value(); + } + return null; + }); + equalsExact = Objects.requireNonNullElse(equalsExact, (input, constant) -> { + final T value = readExact(input); + return Objects.equals(value, constant) ? constant : null; + }); + findExact = Objects.requireNonNullElse(findExact, (input, constants) -> { + final T value = readExact(input); + return constants.contains(value) ? value : null; + }); + } + + @Override + public ParserSpec.@NotNull Result read(@NotNull String input, int startIndex) { + return read.read(input, startIndex); + } + + @Override + public ParserSpec.@NotNull Result equals(@NotNull String input, int startIndex, @NotNull T constant) { + return equals.equals(input, startIndex, constant); + } + + @Override + public ParserSpec.@NotNull Result find(@NotNull String input, int startIndex, @NotNull Set<@NotNull T> constants) { + return find.find(input, startIndex, constants); + } + + @Override + public @Nullable T readExact(@NotNull String input) { + return readExact.readExact(input); + } + + @Override + public @Nullable T equalsExact(@NotNull String input, @NotNull T constant) { + return equalsExact.equalsExact(input, constant); + } + + @Override + public @Nullable T findExact(@NotNull String input, @NotNull Set<@NotNull T> constants) { + return findExact.findExact(input, constants); + } + } + + record ResultSuccessImpl(String input, int index, T value) implements ParserSpec.Result.Success { + } + + record ResultIncompatibleImpl() implements ParserSpec.Result.IncompatibleType { + } + + record ResultErrorImpl(String input, String message, int error) implements ParserSpec.Result.SyntaxError { + } + + static void assertInput(ParserSpec.Result result, String input) { + assert result != null : "Result must not be null"; + assert !(result instanceof ParserSpec.Result.Success su) + || su.input().equals(input) : "input mismatch: " + result + " != " + input; + } +} diff --git a/src/main/java/net/minestom/server/utils/StringReaderUtils.java b/src/main/java/net/minestom/server/utils/StringReaderUtils.java new file mode 100644 index 000000000..0c61de96a --- /dev/null +++ b/src/main/java/net/minestom/server/utils/StringReaderUtils.java @@ -0,0 +1,57 @@ +package net.minestom.server.utils; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public final class StringReaderUtils { + private StringReaderUtils() { + //no instance + } + + /** + * Locate the first unescaped escapable character + * + * @param charSequence the sequence to start + * @param start inclusive start position + * @param escapable the escapable character to find + * @param escape escape character + * @return the index of the first unescaped escapable character or -1 if it doesn't have an end + */ + public static int nextIndexOfEscapable(CharSequence charSequence, int start, char escapable, char escape) { + boolean wasEscape = false; + for (int i = start; i < charSequence.length(); i++) { + if (wasEscape) { + wasEscape = false; + } else { + final char charAt = charSequence.charAt(i); + if (charAt == escapable) return i; + if (charAt == escape) wasEscape = true; + } + } + return -1; + } + + public static int nextIndexOf(CharSequence charSequence, int start, char c) { + for (int i = start; i < charSequence.length(); i++) { + if (charSequence.charAt(i) == c) return i; + } + return -1; + } + + public static int endIndexOfQuotableString(CharSequence charSequence, int start) { + final char type = charSequence.charAt(start); + final int offsetStart = start + 1; + if (type == '\'') { + return nextIndexOfEscapable(charSequence, offsetStart, '\'', '\\'); + } else if (type == '"') { + return nextIndexOfEscapable(charSequence, offsetStart, '"', '\\'); + } else { + int res = nextIndexOf(charSequence, offsetStart, ' '); + res = res == -1 ? charSequence.length() - 1 : res - 1; + final int a, b; + if (((a = nextIndexOf(charSequence, offsetStart, '"')) > -1 && a <= res) || + ((b = nextIndexOf(charSequence, offsetStart, '\'')) > -1 && b <= res)) return -1; + return res; + } + } +} diff --git a/src/test/java/net/minestom/server/command/ArgParserTest.java b/src/test/java/net/minestom/server/command/ArgParserTest.java new file mode 100644 index 000000000..38192b961 --- /dev/null +++ b/src/test/java/net/minestom/server/command/ArgParserTest.java @@ -0,0 +1,133 @@ +package net.minestom.server.command; + +import org.junit.jupiter.api.Test; + +import java.lang.Double; +import java.lang.Float; +import java.lang.Integer; +import java.lang.Long; +import java.util.Set; + +import static net.minestom.server.command.Parser.Boolean; +import static net.minestom.server.command.Parser.Double; +import static net.minestom.server.command.Parser.Float; +import static net.minestom.server.command.Parser.Integer; +import static net.minestom.server.command.Parser.Long; +import static net.minestom.server.command.Parser.String; +import static net.minestom.server.command.Parser.*; +import static org.junit.jupiter.api.Assertions.*; + +public class ArgParserTest { + + @Test + public void literal() { + LiteralParser parser = Literal("test"); + assertNotNull(parser); + assertEquals("test", parser.literal()); + + parser = parser.literal("test2"); + assertNotNull(parser); + assertEquals("test2", parser.literal()); + } + + @Test + public void literals() { + LiteralsParser parser = Literals("test"); + assertNotNull(parser); + assertEquals(Set.of("test"), parser.literals()); + + parser = parser.literals("first", "second"); + assertNotNull(parser); + assertEquals(Set.of("first", "second"), parser.literals()); + + parser = parser.literals("third"); + assertNotNull(parser); + assertEquals(Set.of("third"), parser.literals()); + } + + @Test + public void booleanTest() { + BooleanParser parser = Boolean(); + assertNotNull(parser); + } + + @Test + public void floatTest() { + FloatParser parser = Float(); + Float min = parser.min(); + Float max = parser.max(); + assertNull(min); + assertNull(max); + + parser = parser.min(1f); + assertEquals(1f, parser.min()); + assertNull(parser.max()); + + parser = parser.max(2f); + assertEquals(1f, parser.min()); + assertEquals(2f, parser.max()); + } + + @Test + public void doubleTest() { + DoubleParser parser = Double(); + Double min = parser.min(); + Double max = parser.max(); + assertNull(min); + assertNull(max); + + parser = parser.min(1d); + assertEquals(1d, parser.min()); + assertNull(parser.max()); + + parser = parser.max(2d); + assertEquals(1d, parser.min()); + assertEquals(2d, parser.max()); + } + + @Test + public void integerTest() { + IntegerParser parser = Integer(); + Integer min = parser.min(); + Integer max = parser.max(); + assertNull(min); + assertNull(max); + + parser = parser.min(1); + assertEquals(1, parser.min()); + assertNull(parser.max()); + + parser = parser.max(2); + assertEquals(1, parser.min()); + assertEquals(2, parser.max()); + } + + @Test + public void longTest() { + LongParser parser = Long(); + Long min = parser.min(); + Long max = parser.max(); + assertNull(min); + assertNull(max); + + parser = parser.min(1L); + assertEquals(1L, parser.min()); + assertNull(parser.max()); + + parser = parser.max(2L); + assertEquals(1L, parser.min()); + assertEquals(2L, parser.max()); + } + + @Test + public void stringTest() { + StringParser parser = String(); + assertEquals(StringParser.Type.WORD, parser.type()); + + parser = parser.type(StringParser.Type.QUOTED); + assertEquals(StringParser.Type.QUOTED, parser.type()); + + parser = parser.type(StringParser.Type.GREEDY); + assertEquals(StringParser.Type.GREEDY, parser.type()); + } +} diff --git a/src/test/java/net/minestom/server/command/ArgSpecTest.java b/src/test/java/net/minestom/server/command/ArgSpecTest.java new file mode 100644 index 000000000..e34861d57 --- /dev/null +++ b/src/test/java/net/minestom/server/command/ArgSpecTest.java @@ -0,0 +1,339 @@ +package net.minestom.server.command; + +import net.minestom.server.command.builder.arguments.ArgumentType; +import org.junit.jupiter.api.Test; + +import java.lang.Integer; +import java.lang.String; +import java.util.Set; + +import static net.minestom.server.command.Parser.Boolean; +import static net.minestom.server.command.Parser.Double; +import static net.minestom.server.command.Parser.Float; +import static net.minestom.server.command.Parser.Integer; +import static net.minestom.server.command.Parser.Long; +import static net.minestom.server.command.Parser.String; +import static net.minestom.server.command.Parser.*; +import static org.junit.jupiter.api.Assertions.*; + +public class ArgSpecTest { + + @Test + public void literalParse() { + // Exact parsing + assertValidSpecExact(Literal("test"), "test", "test"); + + assertInvalidSpecExact(Literal("test"), "text"); + + // Sequence parsing + assertValidSpec(Literal("test"), "test", 4, "test"); + assertValidSpec(Literal("test"), "test", 4, "test 5"); + + assertInvalidSpec(Literal("test"), "text"); + assertInvalidSpec(Literal("test"), "5"); + assertInvalidSpec(Literal("test"), ""); + } + + @Test + public void literalsParse() { + // Exact parsing + assertValidSpecExact(Literals("test"), "test", "test"); + assertValidSpecExact(Literals("first", "second"), "first", "first"); + assertValidSpecExact(Literals(Set.of("first", "second")), "first", "first"); + assertValidSpecExact(Literals("first", "second"), "second", "second"); + + assertInvalidSpecExact(Literals("test"), "text"); + assertInvalidSpecExact(Literals("first", "second"), "first second"); + assertInvalidSpecExact(Literals("first", "second"), "second first"); + + // Sequence parsing + assertValidSpec(Literals("test"), "test", 4, "test"); + assertValidSpec(Literals("test"), "test", 4, "test 5"); + assertValidSpec(Literals("first", "second"), "first", 5, "first"); + assertValidSpec(Literals("first", "second"), "first", 5, "first second"); + assertValidSpec(Literals("first", "second"), "second", 6, "second"); + assertValidSpec(Literals("first", "second"), "second", 6, "second first"); + + assertInvalidSpec(Literals("test"), "text"); + assertInvalidSpec(Literals("test"), "5"); + assertInvalidSpec(Literals("test"), ""); + assertInvalidSpec(Literals("first", "second"), "text"); + } + + @Test + public void booleanParse() { + // Exact parsing + assertValidSpecExact(Boolean(), true, "true"); + assertValidSpecExact(Boolean(), false, "false"); + + assertInvalidSpecExact(Boolean(), "truee"); + assertInvalidSpecExact(Boolean(), "falsee"); + assertInvalidSpecExact(Boolean(), "TRuE"); + assertInvalidSpecExact(Boolean(), "ttrue"); + assertInvalidSpecExact(Boolean(), "t true"); + assertInvalidSpecExact(Boolean(), " false"); + assertInvalidSpecExact(Boolean(), " true"); + + // Sequence parsing + assertValidSpec(Boolean(), true, 4, "true"); + assertValidSpec(Boolean(), false, 5, "false"); + assertValidSpec(Boolean(), true, 4, "true test"); + assertValidSpec(Boolean(), false, 5, "false test"); + + assertInvalidSpec(Boolean(), "text"); + assertInvalidSpec(Boolean(), "text text"); + assertInvalidSpec(Boolean(), "text 55"); + } + + @Test + public void floatParse() { + // Exact parsing + assertValidSpecExact(Float(), 1f, "1"); + assertValidSpecExact(Float(), 1.5f, "1.5"); + assertValidSpecExact(Float(), -1.5f, "-1.5"); + assertValidSpecExact(Float(), -99f, "-99"); + assertValidSpecExact(Float().min(5f).max(10f), 5f, "5"); + assertValidSpecExact(Float().min(5f), 5f, "5"); + assertValidSpecExact(Float().max(10f), -99f, "-99"); + assertValidSpecExact(Float().min(5f).max(10f), 10f, "10"); + + assertInvalidSpecExact(Float(), "text"); + assertInvalidSpecExact(Float(), "text text"); + assertInvalidSpecExact(Float(), "1 1"); + assertInvalidSpecExact(Float().min(5f), "-5"); + assertInvalidSpecExact(Float().min(5f), "4"); + assertInvalidSpecExact(Float().max(10f), "11"); + + // Sequence parsing + assertValidSpec(Float(), 1f, 1, "1"); + assertValidSpec(Float(), 11f, 2, "11"); + assertValidSpec(Float(), 5f, 3, "5.0 1"); + assertValidSpec(Float(), 55f, 2, "55 1"); + assertValidSpec(Float(), 55f, 2, "55 text"); + + assertInvalidSpec(Float(), "text"); + assertInvalidSpec(Float(), "text text"); + assertInvalidSpec(Float(), "text 55"); + } + + @Test + public void doubleParse() { + // Exact parsing + assertValidSpecExact(Double(), 1d, "1"); + assertValidSpecExact(Double(), 1.5d, "1.5"); + assertValidSpecExact(Double(), -1.5d, "-1.5"); + assertValidSpecExact(Double(), -99d, "-99"); + assertValidSpecExact(Double().min(5d).max(10d), 5d, "5"); + assertValidSpecExact(Double().min(5d), 5d, "5"); + assertValidSpecExact(Double().max(10d), -99d, "-99"); + assertValidSpecExact(Double().min(5d).max(10d), 10d, "10"); + + assertInvalidSpecExact(Double(), "text"); + assertInvalidSpecExact(Double(), "text text"); + assertInvalidSpecExact(Double(), "1 1"); + assertInvalidSpecExact(Double().min(5d), "-5"); + assertInvalidSpecExact(Double().min(5d), "4"); + assertInvalidSpecExact(Double().max(10d), "11"); + + // Sequence parsing + assertValidSpec(Double(), 1d, 1, "1"); + assertValidSpec(Double(), 11d, 2, "11"); + assertValidSpec(Double(), 5d, 3, "5.0 1"); + assertValidSpec(Double(), 55d, 2, "55 1"); + assertValidSpec(Double(), 55d, 2, "55 text"); + + assertInvalidSpec(Double(), "text"); + assertInvalidSpec(Double(), "text text"); + assertInvalidSpec(Double(), "text 55"); + } + + @Test + public void integerParse() { + // Exact parsing + assertValidSpecExact(Integer(), 1, "1"); + assertValidSpecExact(Integer(), -99, "-99"); + assertValidSpecExact(Integer().min(5).max(10), 5, "5"); + assertValidSpecExact(Integer().min(5), 5, "5"); + assertValidSpecExact(Integer().max(10), -99, "-99"); + assertValidSpecExact(Integer().min(5).max(10), 10, "10"); + + assertInvalidSpecExact(Integer(), "text"); + assertInvalidSpecExact(Integer(), "text text"); + assertInvalidSpecExact(Integer(), "1 1"); + assertInvalidSpecExact(Integer().min(5), "-5"); + assertInvalidSpecExact(Integer().min(5), "4"); + assertInvalidSpecExact(Integer().max(10), "11"); + + // Sequence parsing + assertValidSpec(Integer(), 1, 1, "1"); + assertValidSpec(Integer(), 11, 2, "11"); + assertValidSpec(Integer(), 5, 1, "5 1"); + assertValidSpec(Integer(), 55, 2, "55 1"); + assertValidSpec(Integer(), 55, 2, "55 text"); + + assertInvalidSpec(Integer(), "text"); + assertInvalidSpec(Integer(), "text text"); + assertInvalidSpec(Integer(), "text 55"); + } + + @Test + public void longParse() { + // Exact parsing + assertValidSpecExact(Long(), 1L, "1"); + assertValidSpecExact(Long(), -99L, "-99"); + assertValidSpecExact(Long().min(5L).max(10L), 5L, "5"); + assertValidSpecExact(Long().min(5L), 5L, "5"); + assertValidSpecExact(Long().max(10L), -99L, "-99"); + assertValidSpecExact(Long().min(5L).max(10L), 10L, "10"); + + assertInvalidSpecExact(Long(), "text"); + assertInvalidSpecExact(Long(), "text text"); + assertInvalidSpecExact(Long(), "1 1"); + assertInvalidSpecExact(Long().min(5L), "-5"); + assertInvalidSpecExact(Long().min(5L), "4"); + assertInvalidSpecExact(Long().max(10L), "11"); + + // Sequence parsing + assertValidSpec(Long(), 1L, 1, "1"); + assertValidSpec(Long(), 11L, 2, "11"); + assertValidSpec(Long(), 5L, 1, "5 1"); + assertValidSpec(Long(), 55L, 2, "55 1"); + assertValidSpec(Long(), 55L, 2, "55 text"); + + assertInvalidSpec(Long(), "text"); + assertInvalidSpec(Long(), "text text"); + assertInvalidSpec(Long(), "text 55"); + } + + @Test + public void stringParse() { + // Exact parsing + assertValidSpecExact(String(), "test", "test"); + assertValidSpecExact(String().type(StringParser.Type.GREEDY), "test 1 2 3", "test 1 2 3"); + assertValidSpecExact(String().type(StringParser.Type.QUOTED), "test", "test"); + assertValidSpecExact(String().type(StringParser.Type.QUOTED), "Hey there", """ + "Hey there"\ + """); + assertValidSpecExact(String().type(StringParser.Type.QUOTED), "Hey there", """ + "Hey there"\ + """); + assertValidSpecExact(String().type(StringParser.Type.QUOTED), "text", "text"); + + assertInvalidSpecExact(String().type(StringParser.Type.QUOTED), """ + "Hey\ + """); + assertInvalidSpecExact(String().type(StringParser.Type.QUOTED), """ + there"\ + """); + + // Sequence parsing + assertValidSpec(String(), "test", 4, "test"); + assertValidSpec(String(), "test", 4, "test a"); + assertValidSpec(String().type(StringParser.Type.GREEDY), "test 1 2 3", 10, "test 1 2 3"); + assertValidSpec(String().type(StringParser.Type.QUOTED), "Hey there", 11, """ + "Hey there"\ + """); + assertValidSpec(String().type(StringParser.Type.QUOTED), "Hey there", 12, """ + "Hey there"\ + """); + assertValidSpec(String().type(StringParser.Type.QUOTED), "Hey there", 11, """ + "Hey there" test\ + """); + + assertValidSpec(String().type(StringParser.Type.QUOTED), "text", 4, "text"); + assertValidSpec(String().type(StringParser.Type.QUOTED), "text", 4, "text test"); + } + + @Test + public void customSingleParse() { + Custom parser = custom(ParserSpec.Type.INTEGER); + assertValidSpec(parser, 1, 1, "1"); + assertValidSpec(parser, 11, 2, "11"); + assertValidSpec(parser, 5, 1, "5 1"); + assertValidSpec(parser, 55, 2, "55 1"); + assertValidSpec(parser, 55, 2, "55 text"); + } + + @Test + public void customReaderParse() { + Custom parser = custom(ParserSpec.reader((s, startIndex) -> { + final String input = s.substring(startIndex); + if (!input.startsWith("1")) return null; + return ParserSpec.Result.success("1", startIndex + 1, 1); + })); + assertValidSpec(parser, 1, 1, "1"); + assertInvalidSpec(parser, "5 1"); + assertValidSpec(parser, 1, 1, "1 55"); + } + + @Test + public void customLegacyParse() { + final ParserSpec spec = ParserSpec.legacy(ArgumentType.String("test")); + final Custom parser = Parser.custom(spec); + assertValidSpecExact(parser, "test", "test"); + assertValidSpecExact(parser, "Hey there", """ + "Hey there"\ + """); + assertValidSpecExact(parser, "Hey there", """ + "Hey there"\ + """); + assertValidSpecExact(parser, "text", "text"); + + assertInvalidSpecExact(parser, """ + "Hey\ + """); + assertInvalidSpecExact(parser, """ + there"\ + """); + + // Sequence parsing + assertValidSpec(parser, "Hey there", 11, """ + "Hey there"\ + """); + assertValidSpec(parser, "Hey there", 12, """ + "Hey there"\ + """); + assertValidSpec(parser, "Hey there", 11, """ + "Hey there" test\ + """); + + assertValidSpec(parser, "text", 4, "text"); + } + + static void assertValidSpec(Parser parser, T expectedValue, int expectedIndex, String input) { + final ParserSpec spec = parser.spec(); + final ParserSpec.Result.Success result = (ParserSpec.Result.Success) spec.read(input); + assertNotNull(result); + assertEquals(input.substring(0, expectedIndex), result.input(), "Invalid input(" + expectedIndex + ") for '" + input + "'"); + assertEquals(expectedValue, result.value(), "Invalid value"); + assertEquals(expectedIndex, result.index(), "Invalid index"); + + // Assert read with non-zero initial index + input = "1 " + input; + expectedIndex += 2; + final ParserSpec.Result.Success result2 = (ParserSpec.Result.Success) spec.read(input, 2); + assertNotNull(result2); + assertEquals(input.substring(2, expectedIndex), result2.input(), "Invalid input(" + expectedIndex + ") for '" + input + "'"); + assertEquals(expectedValue, result2.value(), "Invalid value"); + assertEquals(expectedIndex, result2.index(), "Invalid index"); + } + + static void assertInvalidSpec(Parser parser, String input) { + final ParserSpec spec = parser.spec(); + final ParserSpec.Result result = spec.read(input); + if (result instanceof ParserSpec.Result.Success) { + fail("Expected failure for '" + input + "'"); + } + } + + static void assertValidSpecExact(Parser parser, T expected, String input) { + final ParserSpec spec = parser.spec(); + final T result = spec.readExact(input); + assertEquals(expected, result); + } + + static void assertInvalidSpecExact(Parser parser, String input) { + final ParserSpec spec = parser.spec(); + assertNull(spec.readExact(input)); + } +} diff --git a/src/test/java/net/minestom/server/command/ArgTest.java b/src/test/java/net/minestom/server/command/ArgTest.java new file mode 100644 index 000000000..a2d89a6cd --- /dev/null +++ b/src/test/java/net/minestom/server/command/ArgTest.java @@ -0,0 +1,70 @@ +package net.minestom.server.command; + +import net.minestom.server.command.builder.arguments.Argument; +import net.minestom.server.command.builder.arguments.ArgumentType; +import org.junit.jupiter.api.Test; + +import static net.minestom.server.command.Arg.arg; +import static net.minestom.server.command.Arg.literalArg; +import static net.minestom.server.command.Parser.Boolean; +import static net.minestom.server.command.Parser.Double; +import static net.minestom.server.command.Parser.Float; +import static net.minestom.server.command.Parser.Integer; +import static net.minestom.server.command.Parser.Long; +import static net.minestom.server.command.Parser.String; +import static net.minestom.server.command.Parser.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class ArgTest { + @Test + public void basic() { + var arg = arg("id", Integer()); + assertEquals("id", arg.id()); + assertEquals(Integer(), arg.parser()); + assertNull(arg.suggestionType()); + } + + @Test + public void equality() { + assertEquals(literalArg("test"), literalArg("test")); + assertEquals(arg("id", Integer()), arg("id", Integer())); + } + + @Test + public void conversionParser() { + // Boolean + assertParserConversion(Boolean(), ArgumentType.Boolean("id")); + // Float + assertParserConversion(Float(), ArgumentType.Float("id")); + assertParserConversion(Float().min(5f), ArgumentType.Float("id").min(5f)); + assertParserConversion(Float().max(5f), ArgumentType.Float("id").max(5f)); + assertParserConversion(Float().min(5f).max(5f), ArgumentType.Float("id").min(5f).max(5f)); + // Double + assertParserConversion(Double(), ArgumentType.Double("id")); + assertParserConversion(Double().min(5d), ArgumentType.Double("id").min(5d)); + assertParserConversion(Double().max(5d), ArgumentType.Double("id").max(5d)); + assertParserConversion(Double().min(5d).max(5d), ArgumentType.Double("id").min(5d).max(5d)); + // Integer + assertParserConversion(Integer(), ArgumentType.Integer("id")); + assertParserConversion(Integer().min(5), ArgumentType.Integer("id").min(5)); + assertParserConversion(Integer().max(5), ArgumentType.Integer("id").max(5)); + assertParserConversion(Integer().min(5).max(5), ArgumentType.Integer("id").min(5).max(5)); + // Long + assertParserConversion(Long(), ArgumentType.Long("id")); + assertParserConversion(Long().min(5L), ArgumentType.Long("id").min(5L)); + assertParserConversion(Long().max(5L), ArgumentType.Long("id").max(5L)); + assertParserConversion(Long().min(5L).max(5L), ArgumentType.Long("id").min(5L).max(5L)); + // Word + assertParserConversion(String(), ArgumentType.Word("id")); + assertParserConversion(String().type(StringParser.Type.QUOTED), ArgumentType.String("id")); + assertParserConversion(String().type(StringParser.Type.GREEDY), ArgumentType.StringArray("id")); + assertParserConversion(Literals("first", "second"), ArgumentType.Word("id").from("first", "second")); + } + + static void assertParserConversion(Parser expected, Argument argument) { + final Arg converted = ArgImpl.fromLegacy(argument); + final Parser convertedParser = converted.parser(); + assertEquals(expected, convertedParser); + } +} diff --git a/src/test/java/net/minestom/server/command/CommandCallbackTest.java b/src/test/java/net/minestom/server/command/CommandCallbackTest.java new file mode 100644 index 000000000..f6e495765 --- /dev/null +++ b/src/test/java/net/minestom/server/command/CommandCallbackTest.java @@ -0,0 +1,35 @@ +package net.minestom.server.command; + +import net.minestom.server.command.builder.Command; +import net.minestom.server.command.builder.arguments.ArgumentType; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class CommandCallbackTest { + + @Test + public void argCallback() { + var command = new Command("name"); + var arg = ArgumentType.Integer("number"); + + AtomicInteger callback = new AtomicInteger(-1); + + command.setDefaultExecutor((sender, context) -> callback.set(0)); + + arg.setCallback((sender, exception) -> callback.set(1)); + command.addSyntax((sender, context) -> callback.set(2), arg); + + var manager = new CommandManager(); + manager.register(command); + + manager.executeServerCommand("name a"); + assertEquals(1, callback.get()); + + callback.set(-1); + manager.executeServerCommand("name 1"); + assertEquals(2, callback.get()); + } +} diff --git a/src/test/java/net/minestom/server/command/CommandPacketTest.java b/src/test/java/net/minestom/server/command/CommandPacketTest.java index 512aadd11..bdc1b5e07 100644 --- a/src/test/java/net/minestom/server/command/CommandPacketTest.java +++ b/src/test/java/net/minestom/server/command/CommandPacketTest.java @@ -6,6 +6,14 @@ import net.minestom.server.command.builder.arguments.ArgumentType; import net.minestom.server.network.packet.server.play.DeclareCommandsPacket; import org.junit.jupiter.api.Test; +import java.lang.String; + +import static net.minestom.server.command.Arg.arg; +import static net.minestom.server.command.Arg.literalArg; +import static net.minestom.server.command.ArgImpl.fromLegacy; +import static net.minestom.server.command.Parser.Integer; +import static net.minestom.server.command.Parser.String; +import static net.minestom.server.command.Parser.*; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -63,8 +71,8 @@ public class CommandPacketTest { @Test public void singleCommandTwoEnum() { - var graph = Graph.builder(ArgumentType.Literal("foo")) - .append(ArgumentType.Enum("bar", A.class), b -> b.append(ArgumentType.Enum("baz", B.class))) + var graph = Graph.builder(literalArg("foo")) + .append(fromLegacy(ArgumentType.Enum("bar", A.class)), b -> b.append(fromLegacy(ArgumentType.Enum("baz", B.class)))) .build(); assertPacketGraph(""" foo=% @@ -77,8 +85,8 @@ public class CommandPacketTest { @Test public void singleCommandRestrictedWord() { - var graph = Graph.builder(ArgumentType.Literal("foo")) - .append(ArgumentType.Word("bar").from("A", "B", "C")) + var graph = Graph.builder(literalArg("foo")) + .append(arg("bar", Literals("A", "B", "C"))) .build(); assertPacketGraph(""" foo=% @@ -90,8 +98,8 @@ public class CommandPacketTest { @Test public void singleCommandWord() { - var graph = Graph.builder(ArgumentType.Literal("foo")) - .append(ArgumentType.Word("bar")) + var graph = Graph.builder(literalArg("foo")) + .append(arg("bar", String())) .build(); assertPacketGraph(""" foo=% @@ -103,8 +111,8 @@ public class CommandPacketTest { @Test public void singleCommandCommandAfterEnum() { - var graph = Graph.builder(ArgumentType.Literal("foo")) - .append(ArgumentType.Enum("bar", A.class), b -> b.append(ArgumentType.Command("baz"))) + var graph = Graph.builder(literalArg("foo")) + .append(fromLegacy(ArgumentType.Enum("bar", A.class)), b -> b.append(fromLegacy(ArgumentType.Command("baz")))) .build(); assertPacketGraph(""" foo baz=% @@ -118,11 +126,11 @@ public class CommandPacketTest { @Test public void twoCommandIntEnumInt() { - var graph = Graph.builder(ArgumentType.Literal("foo")) - .append(ArgumentType.Integer("int1"), b -> b.append(ArgumentType.Enum("test", A.class), c -> c.append(ArgumentType.Integer("int2")))) + var graph = Graph.builder(literalArg("foo")) + .append(arg("int1", Integer()), b -> b.append(fromLegacy(ArgumentType.Enum("test", A.class)), c -> c.append(arg("int2", Integer())))) .build(); - var graph2 = Graph.builder(ArgumentType.Literal("bar")) - .append(ArgumentType.Integer("int3"), b -> b.append(ArgumentType.Enum("test", B.class), c -> c.append(ArgumentType.Integer("int4")))) + var graph2 = Graph.builder(literalArg("bar")) + .append(arg("int3", Integer()), b -> b.append(fromLegacy(ArgumentType.Enum("test", B.class)), c -> c.append(arg("int4", Integer())))) .build(); assertPacketGraph(""" foo bar=% @@ -140,9 +148,9 @@ public class CommandPacketTest { @Test public void singleCommandTwoGroupOfIntInt() { - var graph = Graph.builder(ArgumentType.Literal("foo")) - .append(ArgumentType.Group("1", ArgumentType.Integer("int1"), ArgumentType.Integer("int2")), - b -> b.append(ArgumentType.Group("2", ArgumentType.Integer("int3"), ArgumentType.Integer("int4")))) + var graph = Graph.builder(literalArg("foo")) + .append(fromLegacy(ArgumentType.Group("1", ArgumentType.Integer("int1"), ArgumentType.Integer("int2"))), + b -> b.append(fromLegacy(ArgumentType.Group("2", ArgumentType.Integer("int3"), ArgumentType.Integer("int4"))))) .build(); assertPacketGraph(""" foo=% @@ -154,12 +162,13 @@ public class CommandPacketTest { int3->int4 """, graph); } + @Test public void twoEnumAndOneLiteralChild() { - var graph = Graph.builder(ArgumentType.Literal("foo")) - .append(ArgumentType.Enum("a", A.class)) - .append(ArgumentType.Literal("l")) - .append(ArgumentType.Enum("b", B.class)) + var graph = Graph.builder(literalArg("foo")) + .append(fromLegacy(ArgumentType.Enum("a", A.class))) + .append(literalArg("l")) + .append(fromLegacy(ArgumentType.Enum("b", B.class))) .build(); assertPacketGraph(""" foo l=% @@ -171,7 +180,7 @@ public class CommandPacketTest { @Test public void commandAliasWithoutArg() { - var graph = Graph.builder(ArgumentType.Word("foo").from("foo", "bar")) + var graph = Graph.builder(arg("foo", Literals("foo", "bar"))) .build(); assertPacketGraph(""" foo bar=% @@ -181,8 +190,8 @@ public class CommandPacketTest { @Test public void commandAliasWithArg() { - var graph = Graph.builder(ArgumentType.Word("foo").from("foo", "bar")) - .append(ArgumentType.Literal("l")) + var graph = Graph.builder(arg("foo", Literals("foo", "bar"))) + .append(literalArg("l")) .build(); assertPacketGraph(""" foo bar l=% @@ -193,11 +202,11 @@ public class CommandPacketTest { @Test public void cmdArgShortcut() { - var foo = Graph.builder(ArgumentType.Literal("foo")) - .append(ArgumentType.String("msg")) + var foo = Graph.builder(literalArg("foo")) + .append(arg("msg", String().type(StringParser.Type.QUOTED))) .build(); - var bar = Graph.builder(ArgumentType.Literal("bar")) - .append(ArgumentType.Command("cmd").setShortcut("foo")) + var bar = Graph.builder(literalArg("bar")) + .append(fromLegacy(ArgumentType.Command("cmd").setShortcut("foo"))) .build(); assertPacketGraph(""" foo bar cmd=% @@ -211,11 +220,11 @@ public class CommandPacketTest { @Test public void cmdArgShortcutWithPartialArg() { - var foo = Graph.builder(ArgumentType.Literal("foo")) - .append(ArgumentType.String("msg")) + var foo = Graph.builder(literalArg("foo")) + .append(arg("msg", String().type(StringParser.Type.QUOTED))) .build(); - var bar = Graph.builder(ArgumentType.Literal("bar")) - .append(ArgumentType.Command("cmd").setShortcut("foo \"prefix ")) + var bar = Graph.builder(literalArg("bar")) + .append(fromLegacy(ArgumentType.Command("cmd").setShortcut("foo \"prefix "))) .build(); assertPacketGraph(""" foo bar cmd=% diff --git a/src/test/java/net/minestom/server/command/CommandParseTest.java b/src/test/java/net/minestom/server/command/CommandParseTest.java index fb689bcae..749dd0277 100644 --- a/src/test/java/net/minestom/server/command/CommandParseTest.java +++ b/src/test/java/net/minestom/server/command/CommandParseTest.java @@ -4,11 +4,16 @@ import net.minestom.server.command.builder.arguments.ArgumentType; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; +import java.lang.String; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; -import static net.minestom.server.command.builder.arguments.ArgumentType.Literal; -import static net.minestom.server.command.builder.arguments.ArgumentType.Word; +import static net.minestom.server.command.Arg.arg; +import static net.minestom.server.command.Arg.literalArg; +import static net.minestom.server.command.ArgImpl.fromLegacy; +import static net.minestom.server.command.Parser.Integer; +import static net.minestom.server.command.Parser.String; +import static net.minestom.server.command.Parser.*; import static org.junit.jupiter.api.Assertions.*; public class CommandParseTest { @@ -16,7 +21,7 @@ public class CommandParseTest { @Test public void singleParameterlessCommand() { final AtomicBoolean b = new AtomicBoolean(); - var foo = Graph.merge(Graph.builder(Literal("foo"), createExecutor(b)).build()); + var foo = Graph.merge(Graph.builder(literalArg("foo"), createExecutor(b)).build()); assertValid(foo, "foo", b); assertUnknown(foo, "bar"); assertSyntaxError(foo, "foo bar baz"); @@ -27,8 +32,8 @@ public class CommandParseTest { final AtomicBoolean b = new AtomicBoolean(); final AtomicBoolean b1 = new AtomicBoolean(); var graph = Graph.merge( - Graph.builder(Literal("foo"), createExecutor(b)).build(), - Graph.builder(Literal("bar"), createExecutor(b1)).build() + Graph.builder(literalArg("foo"), createExecutor(b)).build(), + Graph.builder(literalArg("bar"), createExecutor(b1)).build() ); assertValid(graph, "foo", b); assertValid(graph, "bar", b1); @@ -41,11 +46,11 @@ public class CommandParseTest { public void singleCommandWithMultipleSyntax() { final AtomicBoolean add = new AtomicBoolean(); final AtomicBoolean action = new AtomicBoolean(); - var foo = Graph.merge(Graph.builder(Literal("foo")) - .append(Literal("add"), - x -> x.append(Word("name"), createExecutor(add))) - .append(Word("action").from("inc", "dec"), - x -> x.append(ArgumentType.Integer("num"), createExecutor(action))) + var foo = Graph.merge(Graph.builder(literalArg("foo")) + .append(literalArg("add"), + x -> x.append(arg("name", String()), createExecutor(add))) + .append(arg("action", Literals("inc", "dec")), + x -> x.append(arg("num", Integer()), createExecutor(action))) .build()); assertValid(foo, "foo add test", add); assertValid(foo, "foo add inc", add); @@ -66,11 +71,11 @@ public class CommandParseTest { public void singleCommandOptionalArgs() { final AtomicBoolean b = new AtomicBoolean(); final AtomicReference expectedFirstArg = new AtomicReference<>("T"); - var foo = Graph.merge(Graph.builder(Literal("foo")) - .append(Word("a").setDefaultValue("A"), - x -> x.append(Word("b").setDefaultValue("B"), - x1 -> x1.append(Word("c").setDefaultValue("C"), - x2 -> x2.append(Word("d").setDefaultValue("D"), + var foo = Graph.merge(Graph.builder(literalArg("foo")) + .append(arg("a", String()).defaultValue("A"), + x -> x.append(arg("b", String()).defaultValue("B"), + x1 -> x1.append(arg("c", String()).defaultValue("C"), + x2 -> x2.append(arg("d", String()).defaultValue("D"), new GraphImpl.ExecutionImpl(null, null, null, (sender, context) -> { b.set(true); @@ -89,8 +94,8 @@ public class CommandParseTest { public void singleCommandSingleEnumArg() { enum A {a, b} final AtomicBoolean b = new AtomicBoolean(); - var foo = Graph.merge(Graph.builder(Literal("foo")) - .append(ArgumentType.Enum("test", A.class), createExecutor(b)) + var foo = Graph.merge(Graph.builder(literalArg("foo")) + .append(fromLegacy(ArgumentType.Enum("test", A.class)), createExecutor(b)) .build()); assertValid(foo, "foo a", b); assertValid(foo, "foo b", b); @@ -101,7 +106,7 @@ public class CommandParseTest { @Test public void aliasWithoutArgs() { final AtomicBoolean b = new AtomicBoolean(); - var foo = Graph.merge(Graph.builder(Word("").from("foo", "bar"), createExecutor(b)) + var foo = Graph.merge(Graph.builder(arg("", Literals("foo", "bar")), createExecutor(b)) .build()); assertValid(foo, "foo", b); assertValid(foo, "bar", b); @@ -111,8 +116,8 @@ public class CommandParseTest { @Test public void aliasWithArgs() { final AtomicBoolean b = new AtomicBoolean(); - var foo = Graph.merge(Graph.builder(Word("").from("foo", "bar")) - .append(ArgumentType.Integer("test"), createExecutor(b)) + var foo = Graph.merge(Graph.builder(arg("", Literals("foo", "bar"))) + .append(arg("test", Integer()), createExecutor(b)) .build()); assertValid(foo, "foo 1", b); assertValid(foo, "bar 1", b); diff --git a/src/test/java/net/minestom/server/command/CommandTest.java b/src/test/java/net/minestom/server/command/CommandTest.java index 420530aa4..5f9294f3d 100644 --- a/src/test/java/net/minestom/server/command/CommandTest.java +++ b/src/test/java/net/minestom/server/command/CommandTest.java @@ -2,6 +2,7 @@ package net.minestom.server.command; import net.minestom.server.command.builder.Command; import net.minestom.server.command.builder.CommandContext; +import net.minestom.server.command.builder.arguments.ArgumentType; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; @@ -51,4 +52,28 @@ public class CommandTest { assertTrue(checkSet.get()); } + + @Test + public void testConflictingSyntaxAndSubcommand() { + final CommandManager manager = new CommandManager(); + + final AtomicBoolean subcommandRun = new AtomicBoolean(); + final AtomicBoolean syntaxRun = new AtomicBoolean(); + + final Command command = new Command("command"); + command.addSubcommand(new Command("subcommand") { + { + addSyntax((sender, ctx) -> subcommandRun.set(true)); + } + }); + var argument = ArgumentType.String("id"); + command.addSyntax((sender, ctx) -> syntaxRun.set(true), argument); + + manager.register(command); + + manager.executeServerCommand("command subcommand"); + + assertTrue(subcommandRun.get()); + assertFalse(syntaxRun.get()); + } } diff --git a/src/test/java/net/minestom/server/command/GraphConversionTest.java b/src/test/java/net/minestom/server/command/GraphConversionTest.java index 2bea24622..ed35b0366 100644 --- a/src/test/java/net/minestom/server/command/GraphConversionTest.java +++ b/src/test/java/net/minestom/server/command/GraphConversionTest.java @@ -2,42 +2,42 @@ package net.minestom.server.command; import net.minestom.server.command.builder.Command; import net.minestom.server.command.builder.CommandContext; +import net.minestom.server.command.builder.arguments.ArgumentType; import org.junit.jupiter.api.Test; -import static net.minestom.server.command.builder.arguments.ArgumentType.Enum; -import static net.minestom.server.command.builder.arguments.ArgumentType.Integer; -import static net.minestom.server.command.builder.arguments.ArgumentType.*; +import static net.minestom.server.command.Arg.arg; +import static net.minestom.server.command.Arg.literalArg; +import static net.minestom.server.command.Parser.Integer; +import static net.minestom.server.command.Parser.*; import static org.junit.jupiter.api.Assertions.assertTrue; public class GraphConversionTest { @Test public void empty() { final Command foo = new Command("foo"); - var graph = Graph.builder(Literal("foo")).build(); + var graph = Graph.builder(literalArg("foo")).build(); assertEqualsGraph(graph, foo); } @Test public void singleLiteral() { final Command foo = new Command("foo"); - var first = Literal("first"); - foo.addSyntax(GraphConversionTest::dummyExecutor, first); - var graph = Graph.builder(Literal("foo")) - .append(first).build(); + foo.addSyntax(GraphConversionTest::dummyExecutor, ArgumentType.Literal("first")); + var graph = Graph.builder(literalArg("foo")) + .append(literalArg("first")).build(); assertEqualsGraph(graph, foo); } @Test public void literalsPath() { final Command foo = new Command("foo"); - var first = Literal("first"); - var second = Literal("second"); - foo.addSyntax(GraphConversionTest::dummyExecutor, first); - foo.addSyntax(GraphConversionTest::dummyExecutor, second); + foo.addSyntax(GraphConversionTest::dummyExecutor, ArgumentType.Literal("first")); + foo.addSyntax(GraphConversionTest::dummyExecutor, ArgumentType.Literal("second")); - var graph = Graph.builder(Literal("foo")) - .append(first).append(second) + var graph = Graph.builder(literalArg("foo")) + .append(literalArg("first")) + .append(literalArg("second")) .build(); assertEqualsGraph(graph, foo); } @@ -47,18 +47,17 @@ public class GraphConversionTest { enum A {A, B, C, D, E} final Command foo = new Command("foo"); - var bar = Literal("bar"); + var a = ArgumentType.Enum("a", A.class); - var baz = Literal("baz"); - var a = Enum("a", A.class); + foo.addSyntax(GraphConversionTest::dummyExecutor, + ArgumentType.Literal("bar")); + foo.addSyntax(GraphConversionTest::dummyExecutor, + ArgumentType.Literal("baz"), a); - foo.addSyntax(GraphConversionTest::dummyExecutor, bar); - foo.addSyntax(GraphConversionTest::dummyExecutor, baz, a); - - var graph = Graph.builder(Literal("foo")) - .append(bar) - .append(baz, builder -> - builder.append(a)) + var graph = Graph.builder(literalArg("foo")) + .append(literalArg("bar")) + .append(literalArg("baz"), builder -> + builder.append(arg("a", legacy(a)))) .build(); assertEqualsGraph(graph, foo); } @@ -67,15 +66,14 @@ public class GraphConversionTest { public void doubleSyntaxMerge() { final Command foo = new Command("foo"); - var bar = Literal("bar"); - var number = Integer("number"); - - foo.addSyntax(GraphConversionTest::dummyExecutor, bar); - foo.addSyntax(GraphConversionTest::dummyExecutor, bar, number); + foo.addSyntax(GraphConversionTest::dummyExecutor, + ArgumentType.Literal("bar")); + foo.addSyntax(GraphConversionTest::dummyExecutor, + ArgumentType.Literal("bar"), ArgumentType.Integer("number")); // The two syntax shall start from the same node - var graph = Graph.builder(Literal("foo")) - .append(bar, builder -> builder.append(number)) + var graph = Graph.builder(literalArg("foo")) + .append(literalArg("bar"), builder -> builder.append(arg("number", Integer()))) .build(); assertEqualsGraph(graph, foo); } @@ -85,18 +83,18 @@ public class GraphConversionTest { final Command main = new Command("main"); final Command sub = new Command("sub"); - var bar = Literal("bar"); - var number = Integer("number"); - - sub.addSyntax(GraphConversionTest::dummyExecutor, bar); - sub.addSyntax(GraphConversionTest::dummyExecutor, bar, number); + sub.addSyntax(GraphConversionTest::dummyExecutor, + ArgumentType.Literal("bar")); + sub.addSyntax(GraphConversionTest::dummyExecutor, + ArgumentType.Literal("bar"), ArgumentType.Integer("number")); main.addSubcommand(sub); // The two syntax shall start from the same node - var graph = Graph.builder(Literal("main")) - .append(Literal("sub"), builder -> - builder.append(bar, builder1 -> builder1.append(number))) + var graph = Graph.builder(literalArg("main")) + .append(literalArg("sub"), builder -> + builder.append(literalArg("bar"), + builder1 -> builder1.append(arg("number", Integer())))) .build(); assertEqualsGraph(graph, main); } @@ -104,14 +102,14 @@ public class GraphConversionTest { @Test public void alias() { final Command main = new Command("main", "alias"); - var graph = Graph.builder(Word("main").from("main", "alias")).build(); + var graph = Graph.builder(arg("main", Literals("main", "alias"))).build(); assertEqualsGraph(graph, main); } @Test public void aliases() { final Command main = new Command("main", "first", "second"); - var graph = Graph.builder(Word("main").from("main", "first", "second")).build(); + var graph = Graph.builder(arg("main", Literals("main", "first", "second"))).build(); assertEqualsGraph(graph, main); } diff --git a/src/test/java/net/minestom/server/command/GraphMergeTest.java b/src/test/java/net/minestom/server/command/GraphMergeTest.java index d257c0bd8..f5ed069d3 100644 --- a/src/test/java/net/minestom/server/command/GraphMergeTest.java +++ b/src/test/java/net/minestom/server/command/GraphMergeTest.java @@ -5,7 +5,7 @@ import org.junit.jupiter.api.Test; import java.util.List; -import static net.minestom.server.command.builder.arguments.ArgumentType.Literal; +import static net.minestom.server.command.Arg.literalArg; import static org.junit.jupiter.api.Assertions.assertTrue; public class GraphMergeTest { @@ -14,31 +14,31 @@ public class GraphMergeTest { public void commands() { var foo = new Command("foo"); var bar = new Command("bar"); - var result = Graph.builder(Literal("")) - .append(Literal("foo")) - .append(Literal("bar")) + var result = Graph.builder(literalArg("")) + .append(literalArg("foo")) + .append(literalArg("bar")) .build(); assertEqualsGraph(result, Graph.merge(List.of(foo, bar))); } @Test public void empty() { - var graph1 = Graph.builder(Literal("foo")).build(); - var graph2 = Graph.builder(Literal("bar")).build(); - var result = Graph.builder(Literal("")) - .append(Literal("foo")) - .append(Literal("bar")) + var graph1 = Graph.builder(literalArg("foo")).build(); + var graph2 = Graph.builder(literalArg("bar")).build(); + var result = Graph.builder(literalArg("")) + .append(literalArg("foo")) + .append(literalArg("bar")) .build(); assertEqualsGraph(result, Graph.merge(graph1, graph2)); } @Test public void literals() { - var graph1 = Graph.builder(Literal("foo")).append(Literal("1")).build(); - var graph2 = Graph.builder(Literal("bar")).append(Literal("2")).build(); - var result = Graph.builder(Literal("")) - .append(Literal("foo"), builder -> builder.append(Literal("1"))) - .append(Literal("bar"), builder -> builder.append(Literal("2"))) + var graph1 = Graph.builder(literalArg("foo")).append(literalArg("1")).build(); + var graph2 = Graph.builder(literalArg("bar")).append(literalArg("2")).build(); + var result = Graph.builder(literalArg("")) + .append(literalArg("foo"), builder -> builder.append(literalArg("1"))) + .append(literalArg("bar"), builder -> builder.append(literalArg("2"))) .build(); assertEqualsGraph(result, Graph.merge(graph1, graph2)); } diff --git a/src/test/java/net/minestom/server/command/GraphTest.java b/src/test/java/net/minestom/server/command/GraphTest.java index 388768e3b..e931b9099 100644 --- a/src/test/java/net/minestom/server/command/GraphTest.java +++ b/src/test/java/net/minestom/server/command/GraphTest.java @@ -6,35 +6,36 @@ import org.junit.jupiter.api.Test; import java.util.List; +import static net.minestom.server.command.Arg.literalArg; import static net.minestom.server.command.builder.arguments.ArgumentType.Literal; import static org.junit.jupiter.api.Assertions.*; public class GraphTest { @Test public void empty() { - var result = Graph.builder(Literal("")) + var result = Graph.builder(literalArg("")) .build(); var node = result.root(); - assertEquals(Literal(""), node.argument()); + assertEquals(literalArg(""), node.argument()); assertTrue(node.next().isEmpty()); } @Test public void next() { - var result = Graph.builder(Literal("")) - .append(Literal("foo")) + var result = Graph.builder(literalArg("")) + .append(literalArg("foo")) .build(); var node = result.root(); - assertEquals(Literal(""), node.argument()); + assertEquals(literalArg(""), node.argument()); assertEquals(1, node.next().size()); - assertEquals(Literal("foo"), node.next().get(0).argument()); + assertEquals(literalArg("foo"), node.next().get(0).argument()); } @Test public void immutableNextBuilder() { - var result = Graph.builder(Literal("")) - .append(Literal("foo")) - .append(Literal("bar")) + var result = Graph.builder(literalArg("")) + .append(literalArg("foo")) + .append(literalArg("bar")) .build(); var node = result.root(); assertThrows(Exception.class, () -> result.root().next().add(node));