Argument API

This commit is contained in:
TheMode 2022-07-18 04:29:44 +02:00
parent 9dab3183e5
commit 0abcc9f010
23 changed files with 2221 additions and 371 deletions

View File

@ -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<T> {
static <T> @NotNull Arg<T> arg(@NotNull String id, @NotNull Parser<T> parser, @Nullable Suggestion.Type suggestionType) {
return new ArgImpl<>(id, parser, suggestionType, null, null);
}
static <T> @NotNull Arg<T> arg(@NotNull String id, @NotNull Parser<T> parser) {
return arg(id, parser, null);
}
static @NotNull Arg<String> literalArg(@NotNull String id) {
return arg(id, Parser.Literal(id), null);
}
@NotNull String id();
@NotNull Parser<T> parser();
Suggestion.@UnknownNullability Type suggestionType();
@Nullable Supplier<@NotNull T> defaultValue();
@NotNull Arg<T> defaultValue(@Nullable Supplier<@NotNull T> defaultValue);
default @NotNull Arg<T> defaultValue(@NotNull T defaultValue) {
return defaultValue(() -> defaultValue);
}
@ApiStatus.Experimental
@Nullable ArgumentCallback callback();
@ApiStatus.Experimental
@NotNull Arg<T> 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<Match> 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);
}
}
}

View File

@ -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<T>(String id, Parser<T> parser, Suggestion.Type suggestionType,
Supplier<T> defaultValue, ArgumentCallback callback) implements Arg<T> {
static <T> ArgImpl<T> fromLegacy(Argument<T> argument) {
return new ArgImpl<>(argument.getId(), retrieveParser(argument),
retrieveSuggestion(argument), argument.getDefaultValue(), retrieveCallback(argument));
}
private static <T> Parser<T> retrieveParser(Argument<T> argument) {
var parserFun = ConversionMap.PARSERS.get(argument.getClass());
final Parser<T> 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<T> defaultValue(@Nullable Supplier<@NotNull T> defaultValue) {
return new ArgImpl<>(id, parser, suggestionType, defaultValue, callback);
}
@Override
public @NotNull Arg<T> 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<Match> 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<Class<? extends Argument>, Function<Argument, Parser>> 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<Class<? extends Argument>, Function<Argument, Parser>> parsers = new HashMap<>();
<T, A extends Argument<T>> ConversionMap append(Class<A> legacyType, Function<A, Parser<?>> converter) {
this.parsers.put(legacyType, arg -> converter.apply((A) arg));
return this;
}
Map<Class<? extends Argument>, Function<Argument, Parser>> toMap() {
return Map.copyOf(parsers);
}
}
}

View File

@ -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<Argument<?>> args();
List<Arg<?>> args();
sealed interface UnknownCommand extends Result
permits CommandParserImpl.UnknownCommandResult {

View File

@ -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<String, ArgumentResult<Object>> collectArguments() {
Map<String, ParserSpec.Result<Object>> collectArguments() {
return nodeResults.stream()
.skip(1) // skip root
.collect(Collectors.toUnmodifiableMap(NodeResult::name, NodeResult::argumentResult));
}
List<Argument<?>> getArgs() {
List<Arg<?>> 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<Object> argumentResult = new ArgumentResult.Success<>(value, "");
chain.append(new NodeResult(child, argumentResult, argument.getSuggestionCallback()));
final ParserSpec.Result<Object> 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 <R, T> @Nullable R nullSafeGetter(@Nullable T obj, Function<T, R> 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<Object>) success,
argument.getSuggestionCallback());
} else if (parse instanceof ArgumentResult.SyntaxError<?> syntaxError) {
return new NodeResult(child, (ArgumentResult<Object>) syntaxError,
argument.getSuggestionCallback());
} else {
// Reset cursor & try next
reader.cursor(start);
final List<Node> 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<Object>) success,
null);
} else if (parse instanceof ParserSpec.Result.SyntaxError<?> syntaxError) {
return new NodeResult(child, (ParserSpec.Result<Object>) 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<Argument<?>> args() {
public List<Arg<?>> args() {
return null;
}
}
@ -197,35 +189,38 @@ final class CommandParserImpl implements CommandParser {
@Nullable CommandCondition condition();
@NotNull Map<String, ArgumentResult<Object>> arguments();
@NotNull Map<String, ParserSpec.Result<Object>> 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<String, ArgumentResult<Object>> arguments, CommandExecutor globalListener,
@Nullable SuggestionCallback suggestionCallback, List<Argument<?>> args)
ParserSpec.Result.SyntaxError<?> error,
@NotNull Map<String, ParserSpec.Result<Object>> arguments, CommandExecutor globalListener,
@Nullable Arg.Suggestion.Type suggestionType, List<Arg<?>> 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<String, ArgumentResult<Object>> arguments,
CommandExecutor globalListener, @Nullable SuggestionCallback suggestionCallback, List<Argument<?>> args)
@NotNull Map<String, ParserSpec.Result<Object>> arguments,
CommandExecutor globalListener, @Nullable Arg.Suggestion.Type suggestionType,
List<Arg<?>> 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<String, ArgumentResult<Object>> arguments) implements ExecutableCommand {
Map<String, ParserSpec.Result<Object>> 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<String, ArgumentResult<Object>> arguments) implements ExecutableCommand {
ParserSpec.Result.SyntaxError<?> error, String input,
Map<String, ParserSpec.Result<Object>> 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<String, ArgumentResult<Object>> arguments) {
private static CommandContext createCommandContext(String input, Map<String, ParserSpec.Result<Object>> arguments) {
final CommandContext context = new CommandContext(input);
for (var entry : arguments.entrySet()) {
final String identifier = entry.getKey();
final ArgumentResult<Object> value = entry.getValue();
final ParserSpec.Result<Object> value = entry.getValue();
final Object argOutput = value instanceof ArgumentResult.Success<Object> success ? success.value() : null;
final String argInput = value instanceof ArgumentResult.Success<Object> success ? success.input() : "";
final Object argOutput = value instanceof ParserSpec.Result.Success<Object> success ? success.value() : null;
final String argInput = value instanceof ParserSpec.Result.Success<Object> 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<Object> argumentResult, SuggestionCallback callback) {
private record NodeResult(Node node, ParserSpec.Result<Object> 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 <T> ArgumentResult<T> parse(Argument<T> 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<R> {
record Success<T>(T value, String input)
implements ArgumentResult<T> {
}
record IncompatibleType<T>()
implements ArgumentResult<T> {
}
record SyntaxError<T>(String message, String input, int code)
implements ArgumentResult<T> {
private static <T> ParserSpec.Result<T> parse(ParserSpec<T> spec, CommandStringReader reader) {
final String input = reader.input;
final ParserSpec.Result<T> result = spec.read(input, reader.cursor);
if (result instanceof ParserSpec.Result.Success<T> 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;
}
}

View File

@ -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<Builder> consumer);
@NotNull Builder append(@NotNull Arg<?> argument, @Nullable Execution execution, @NotNull Consumer<Builder> 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<Builder> consumer) {
default @NotNull Builder append(@NotNull Arg<?> argument, @NotNull Consumer<Builder> 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);
}

View File

@ -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<Class<? extends Parser<?>>, 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<Class<? extends Parser<?>>, Function<Parser<?>, 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 <T extends Parser<?>> Map.Entry<Class<? extends Parser<?>>, Function<Parser<?>, byte[]>> propEntry(Class<T> parser, Function<T, byte[]> func) {
return Map.entry(parser, p -> func.apply(parser.cast(p)));
}
private static <T extends Number, P extends Parser<T>> Map.Entry<Class<? extends Parser<?>>, Function<Parser<?>, byte[]>>
numberProps(Class<P> parserClass, BiConsumer<BinaryWriter, T> numWriter, Function<P, T> minGetter, Function<P, T> 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<DeclareCommandsPacket.Node> nodes = new ArrayList<>();
List<BiConsumer<Graph, Integer>> redirects = new ArrayList<>();
Map<Argument<?>, Integer> argToPacketId = new HashMap<>();
Map<Arg<?>, 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<DeclareCommandsPacket.Node> to,
List<BiConsumer<Graph, Integer>> redirects, AtomicInteger id, @Nullable AtomicInteger redirect,
@Nullable Player player, Map<Argument<?>, Integer> argToPacketId) {
@Nullable Player player, Map<Arg<?>, 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<Graph.Node> 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<Argument<?>> 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<String> 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<Argument<?>> 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<? extends Argument<?>> 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<Argument<?>> 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<DeclareCommandsPacket.Node> nodeList,
List<BiConsumer<Graph, Integer>> redirects,
@Nullable AtomicInteger redirect,
DeclareCommandsPacket.Node node,
Collection<String> 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<Parser<?>, 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<Arg<?>> args = CommandParser.parser().parse(graph, shortcut).args();
return args.get(args.size() - 1);
}
}

View File

@ -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<Graph> graphs) {
final List<Node> 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<BuilderImpl> children, Execution execution) implements Graph.Builder {
public BuilderImpl(Argument<?> argument, Execution execution) {
record BuilderImpl(Arg<?> argument, List<BuilderImpl> 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<Graph.Builder> 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<Graph.Node> next) implements Graph.Node {
record NodeImpl(Arg<?> argument, ExecutionImpl execution, List<Graph.Node> next) implements Graph.Node {
static NodeImpl fromBuilder(BuilderImpl builder) {
final List<BuilderImpl> 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<Argument<?>, ConversionNode> nextMap) {
ConversionNode(Argument<?> argument, ExecutionImpl execution) {
private record ConversionNode(Arg<?> argument, ExecutionImpl execution,
Map<Arg<?>, 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<Command> commands) {
Map<Argument<?>, ConversionNode> next = new LinkedHashMap<>(commands.size());
Map<Arg<?>, 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<String> 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<String> 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) {

View File

@ -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<T> {
static @NotNull LiteralParser Literal(@NotNull String literal) {
return ParserImpl.LITERAL.literal(literal);
}
static @NotNull LiteralsParser Literals(@NotNull Set<String> 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 <T> @NotNull Custom<T> custom(@NotNull ParserSpec<T> spec) {
return new ParserImpl.CustomImpl<>(spec);
}
@ApiStatus.Internal
static <T> @NotNull Custom<T> legacy(@NotNull Argument<T> argument) {
return custom(ParserSpec.legacy(argument));
}
@NotNull ParserSpec<T> spec();
sealed interface LiteralParser extends Parser<String>
permits ParserImpl.LiteralParserImpl {
@NotNull String literal();
@NotNull LiteralParser literal(@NotNull String literal);
}
sealed interface LiteralsParser extends Parser<String>
permits ParserImpl.LiteralsParserImpl {
@Unmodifiable
@NotNull Set<String> literals();
@NotNull LiteralsParser literals(@NotNull Set<String> literals);
default @NotNull LiteralsParser literals(@NotNull String @NotNull ... literals) {
return literals(Set.of(literals));
}
}
sealed interface BooleanParser extends Parser<Boolean>
permits ParserImpl.BooleanParserImpl {
}
sealed interface FloatParser extends Parser<Float>
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<Double>
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<Integer>
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<Long>
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<String>
permits ParserImpl.StringParserImpl {
@NotNull Type type();
@NotNull StringParser type(@NotNull Type type);
enum Type {
WORD,
QUOTED,
GREEDY
}
}
sealed interface Custom<T> extends Parser<T>
permits ParserImpl.CustomImpl {
}
}

View File

@ -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<String> spec() {
return ParserSpec.constant(ParserSpec.Type.WORD, literal);
}
}
record LiteralsParserImpl(Set<String> literals) implements Parser.LiteralsParser {
LiteralsParserImpl {
literals = Set.copyOf(literals);
}
@Override
public @NotNull LiteralsParser literals(@NotNull Set<String> literals) {
return new LiteralsParserImpl(literals);
}
@Override
public @NotNull ParserSpec<String> spec() {
return ParserSpec.constants(ParserSpec.Type.WORD, literals);
}
}
record BooleanParserImpl() implements Parser.BooleanParser {
@Override
public @NotNull ParserSpec<Boolean> spec() {
return ParserSpec.Type.BOOLEAN;
}
}
record FloatParserImpl(Float min, Float max) implements Parser.FloatParser {
private static final ParserSpec<Float> 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<Float> 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<Double> 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<Double> 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<Integer> 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<Integer> 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<Long> 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<Long> 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<String> spec() {
return switch (type) {
case WORD -> ParserSpec.Type.WORD;
case QUOTED -> ParserSpec.Type.QUOTED_PHRASE;
case GREEDY -> ParserSpec.Type.GREEDY_PHRASE;
};
}
}
record CustomImpl<T>(ParserSpec<T> spec) implements Parser.Custom<T> {
}
}

View File

@ -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<T>
permits ParserSpec.Type, ParserSpecImpl.Constant1, ParserSpecImpl.ConstantN,
ParserSpecImpl.Legacy, ParserSpecImpl.Reader, ParserSpecImpl.Specialized {
static <T> @NotNull ParserSpec<T> constant(@NotNull Type<T> type, @NotNull T constant) {
return new ParserSpecImpl.Constant1<>(type, constant);
}
static <T> @NotNull ParserSpec<T> constants(@NotNull Type<T> type, @NotNull Set<@NotNull T> constants) {
return new ParserSpecImpl.ConstantN<>(type, constants);
}
static <T> @NotNull ParserSpec<T> reader(@NotNull BiFunction<@NotNull String, @NotNull Integer, @Nullable Result<T>> reader) {
return new ParserSpecImpl.Reader<>(reader);
}
static <T> @NotNull ParserSpec<T> specialized(@NotNull ParserSpec<T> spec,
@NotNull Function<Result.@NotNull Success<T>, @NotNull Result<T>> filter) {
return new ParserSpecImpl.Specialized<>(spec, filter);
}
@ApiStatus.Internal
static <T> @NotNull ParserSpec<T> legacy(@NotNull Argument<T> argument) {
return new ParserSpecImpl.Legacy<>(argument);
}
@NotNull Result<T> read(@NotNull String input, int startIndex);
default @NotNull Result<T> read(@NotNull String input) {
return read(input, 0);
}
default @Nullable T readExact(@NotNull String input) {
final Result<T> result = read(input);
return result instanceof Result.Success<T> success && success.index() == input.length() ?
success.value() : null;
}
sealed interface Type<T> extends ParserSpec<T>
permits ParserSpecTypes.TypeImpl {
Type<Boolean> BOOLEAN = ParserSpecTypes.BOOLEAN;
Type<Float> FLOAT = ParserSpecTypes.FLOAT;
Type<Double> DOUBLE = ParserSpecTypes.DOUBLE;
Type<Integer> INTEGER = ParserSpecTypes.INTEGER;
Type<Long> LONG = ParserSpecTypes.LONG;
Type<String> WORD = ParserSpecTypes.WORD;
Type<String> QUOTED_PHRASE = ParserSpecTypes.QUOTED_PHRASE;
Type<String> GREEDY_PHRASE = ParserSpecTypes.GREEDY_PHRASE;
@NotNull ParserSpec.Result<T> equals(@NotNull String input, int startIndex, @NotNull T constant);
@NotNull ParserSpec.Result<T> 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<T> {
static <T> Result.@NotNull Success<T> success(@NotNull String input, int index, @NotNull T value) {
return new ParserSpecTypes.ResultSuccessImpl<>(input, index, value);
}
static <T> Result.@NotNull SyntaxError<T> error(@NotNull String input, @NotNull String message, int error) {
return new ParserSpecTypes.ResultErrorImpl<>(input, message, error);
}
static <T> Result.@NotNull IncompatibleType<T> incompatible() {
return new ParserSpecTypes.ResultIncompatibleImpl<>();
}
sealed interface Success<T> extends Result<T>
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<T> extends Result<T>
permits ParserSpecTypes.ResultIncompatibleImpl {
}
sealed interface SyntaxError<T> extends Result<T>
permits ParserSpecTypes.ResultErrorImpl {
@NotNull String input();
@NotNull String message();
int error();
}
}
}

View File

@ -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.
* <p>
* Reading can be optimized using a raw string comparison to avoid parsing altogether.
*/
record Constant1<T>(Type<T> type, T constant) implements ParserSpec<T> {
@Override
public @NotNull Result<T> 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.
* <p>
* Reading can be optimized using map lookups.
*
* @see Constant1 for raw string comparison, also relevant here
*/
record ConstantN<T>(Type<T> type, Set<T> constants) implements ParserSpec<T> {
ConstantN {
constants = Set.copyOf(constants);
}
@Override
public @NotNull Result<T> 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.
* <p>
* Cannot be optimized at all, but more flexible.
*/
record Reader<T>(BiFunction<String, Integer, Result<T>> reader) implements ParserSpec<T> {
@Override
public @NotNull Result<T> read(@NotNull String input, int startIndex) {
return reader.apply(input, startIndex);
}
}
/**
* Reuses an existing spec but with an additional filter.
* <p>
* The filter means that the parsec input has to pass through the arbitrary function, limiting potential optimizations.
*/
record Specialized<T>(ParserSpec<T> spec, Function<Success<T>, Result<T>> filter) implements ParserSpec<T> {
@Override
public @NotNull Result<T> read(@NotNull String input, int startIndex) {
final Result<T> result = spec.read(input);
if (!(result instanceof Result.Success<T> success)) return result;
return filter.apply(success);
}
}
record Legacy<T>(Argument<T> argument) implements ParserSpec<T> {
@Override
public @NotNull Result<T> 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();
}
}
}

View File

@ -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> 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> 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> 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> 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> 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<String> 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<String> 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<String> GREEDY_PHRASE = ParserSpecTypes.builder((input, startIndex) -> {
final String result = input.substring(startIndex);
return success(result, input.length(), result);
})
.build();
static <T> Builder<T> builder(Functions.Read<T> read) {
return new Builder<>(read);
}
private interface Functions {
@FunctionalInterface
interface Read<T> {
ParserSpec.Result<T> read(String input, int startIndex);
}
@FunctionalInterface
interface Find<T> {
ParserSpec.Result<T> find(String input, int startIndex, Set<T> constants);
}
@FunctionalInterface
interface Equals<T> {
ParserSpec.Result<T> equals(String input, int startIndex, T constant);
}
@FunctionalInterface
interface ReadExact<T> {
T readExact(String input);
}
@FunctionalInterface
interface FindExact<T> {
T findExact(String input, Set<T> constants);
}
@FunctionalInterface
interface EqualsExact<T> {
T equalsExact(String input, T constant);
}
}
static final class Builder<T> {
final Functions.Read<T> read;
Functions.Equals<T> equals;
Functions.Find<T> find;
Functions.ReadExact<T> readExact;
Functions.EqualsExact<T> equalsExact;
Functions.FindExact<T> findExact;
Builder(Functions.Read<T> read) {
this.read = read;
}
public Builder<T> equals(Functions.Equals<T> equals) {
this.equals = equals;
return this;
}
public Builder<T> find(Functions.Find<T> find) {
this.find = find;
return this;
}
Builder<T> readExact(Functions.ReadExact<T> exact) {
this.readExact = exact;
return this;
}
Builder<T> equalsExact(Functions.EqualsExact<T> equalsExact) {
this.equalsExact = equalsExact;
return this;
}
Builder<T> findExact(Functions.FindExact<T> findExact) {
this.findExact = findExact;
return this;
}
ParserSpec.Type<T> build() {
return new TypeImpl<>(read, equals, find, readExact, equalsExact, findExact);
}
}
record TypeImpl<T>(Functions.Read<T> read,
Functions.Equals<T> equals, Functions.Find<T> find,
Functions.ReadExact<T> readExact,
Functions.EqualsExact<T> equalsExact, Functions.FindExact<T> findExact)
implements ParserSpec.Type<T> {
TypeImpl {
// Create fallback if no specialized function is provided
equals = Objects.requireNonNullElse(equals, (input, startIndex, constant) -> {
final ParserSpec.Result<T> result = read(input, startIndex);
assertInput(result, input);
if (result instanceof Result.Success<T> 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<T> result = read(input, startIndex);
assertInput(result, input);
if (result instanceof Result.Success<T> 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<T> result = read(input, 0);
if (result instanceof Result.Success<T> 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<T> read(@NotNull String input, int startIndex) {
return read.read(input, startIndex);
}
@Override
public ParserSpec.@NotNull Result<T> equals(@NotNull String input, int startIndex, @NotNull T constant) {
return equals.equals(input, startIndex, constant);
}
@Override
public ParserSpec.@NotNull Result<T> 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<T>(String input, int index, T value) implements ParserSpec.Result.Success<T> {
}
record ResultIncompatibleImpl<T>() implements ParserSpec.Result.IncompatibleType<T> {
}
record ResultErrorImpl<T>(String input, String message, int error) implements ParserSpec.Result.SyntaxError<T> {
}
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;
}
}

View File

@ -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;
}
}
}

View File

@ -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());
}
}

View File

@ -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<Integer> 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<Integer> 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<String> spec = ParserSpec.legacy(ArgumentType.String("test"));
final Custom<String> 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 <T> void assertValidSpec(Parser<T> parser, T expectedValue, int expectedIndex, String input) {
final ParserSpec<T> spec = parser.spec();
final ParserSpec.Result.Success<T> result = (ParserSpec.Result.Success<T>) 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<T> result2 = (ParserSpec.Result.Success<T>) 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 <T> void assertInvalidSpec(Parser<T> 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 <T> void assertValidSpecExact(Parser<T> parser, T expected, String input) {
final ParserSpec<T> 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));
}
}

View File

@ -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 <T> void assertParserConversion(Parser<?> expected, Argument<?> argument) {
final Arg<?> converted = ArgImpl.fromLegacy(argument);
final Parser<?> convertedParser = converted.parser();
assertEquals(expected, convertedParser);
}
}

View File

@ -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());
}
}

View File

@ -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=%

View File

@ -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<String> 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);

View File

@ -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());
}
}

View File

@ -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);
}

View File

@ -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));
}

View File

@ -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));