From 94710c5174b18a3bdcedbe6ea4d953901045265d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20S=C3=B6derberg?= Date: Sun, 27 Sep 2020 22:39:56 +0200 Subject: [PATCH] :sparkles: Initial support for compound arguments This allows for grouping and mappings of multiple command arguments by using product types. --- checkstyle.xml | 2 - .../java/cloud/commandframework/Command.java | 134 +++++++++++++- .../cloud/commandframework/CommandTree.java | 23 +++ .../arguments/CommandArgument.java | 53 +++++- .../DelegatingSuggestionsProvider.java | 4 +- .../StandardCommandSyntaxFormatter.java | 37 +++- .../commandframework/types/tuples/Pair.java | 16 +- .../types/tuples/Quartet.java | 18 +- .../types/tuples/Quintet.java | 19 +- .../commandframework/types/tuples/Sextet.java | 20 ++- .../types/tuples/Triplet.java | 17 +- .../commandframework/types/tuples/Tuples.java | 6 + .../arguments/compound/ArgumentPair.java | 155 ++++++++++++++++ .../arguments/compound/ArgumentTriplet.java | 166 ++++++++++++++++++ .../arguments/compound/CompoundArgument.java | 153 ++++++++++++++++ .../arguments/compound/package-info.java | 28 +++ .../commands/types/tuples/Tuple.java | 48 +++++ .../CommandHelpHandlerTest.java | 11 +- .../CommandSuggestionsTest.java | 26 +++ .../commandframework/CommandTreeTest.java | 49 ++++++ .../brigadier/CloudBrigadierManager.java | 50 +++++- .../cloud/commandframework/BukkitTest.java | 25 ++- 22 files changed, 1032 insertions(+), 28 deletions(-) create mode 100644 cloud-core/src/main/java/com/intellectualsites/commands/arguments/compound/ArgumentPair.java create mode 100644 cloud-core/src/main/java/com/intellectualsites/commands/arguments/compound/ArgumentTriplet.java create mode 100644 cloud-core/src/main/java/com/intellectualsites/commands/arguments/compound/CompoundArgument.java create mode 100644 cloud-core/src/main/java/com/intellectualsites/commands/arguments/compound/package-info.java create mode 100644 cloud-core/src/main/java/com/intellectualsites/commands/types/tuples/Tuple.java diff --git a/checkstyle.xml b/checkstyle.xml index c0a7dff1..76fc3e41 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -133,7 +133,6 @@ - @@ -166,7 +165,6 @@ - diff --git a/cloud-core/src/main/java/cloud/commandframework/Command.java b/cloud-core/src/main/java/cloud/commandframework/Command.java index f4724382..4889825f 100644 --- a/cloud-core/src/main/java/cloud/commandframework/Command.java +++ b/cloud-core/src/main/java/cloud/commandframework/Command.java @@ -41,6 +41,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; +import java.util.function.Function; /** * A command consists out of a chain of {@link CommandArgument command arguments}. @@ -359,7 +360,6 @@ public class Command { this.commandExecutionHandler, this.commandPermission); } - /** * Add a new command argument by interacting with a constructed command argument builder * @@ -381,6 +381,138 @@ public class Command { return this.argument(builder.build()); } + // Compound helper methods + + /** + * Create a new argument pair that maps to {@link Pair} + *

+ * For this to work, there must be a {@link CommandManager} + * attached to the command builder. To guarantee this, it is recommended to get the command builder instance + * using {@link CommandManager#commandBuilder(String, String...)} + * + * @param name Name of the argument + * @param names Pair containing the names of the sub-arguments + * @param parserPair Pair containing the types of the sub-arguments. There must be parsers for these types registered + * in the {@link com.intellectualsites.commands.arguments.parser.ParserRegistry} used by the + * {@link CommandManager} attached to this command + * @param description Description of the argument + * @param First type + * @param Second type + * @return Builder instance with the argument inserted + */ + @Nonnull + public Builder argumentPair(@Nonnull final String name, + @Nonnull final Pair names, + @Nonnull final Pair, Class> parserPair, + @Nonnull final Description description) { + if (this.commandManager == null) { + throw new IllegalStateException("This cannot be called from a command that has no command manager attached"); + } + return this.argument(ArgumentPair.required(this.commandManager, name, names, parserPair).simple(), description); + } + + /** + * Create a new argument pair that maps to a custom type. + *

+ * For this to work, there must be a {@link CommandManager} + * attached to the command builder. To guarantee this, it is recommended to get the command builder instance + * using {@link CommandManager#commandBuilder(String, String...)} + * + * @param name Name of the argument + * @param outputType The output type + * @param names Pair containing the names of the sub-arguments + * @param parserPair Pair containing the types of the sub-arguments. There must be parsers for these types registered + * in the {@link com.intellectualsites.commands.arguments.parser.ParserRegistry} used by the + * {@link CommandManager} attached to this command + * @param mapper Mapper that maps from {@link Pair} to the custom type + * @param description Description of the argument + * @param First type + * @param Second type + * @param Output type + * @return Builder instance with the argument inserted + */ + @Nonnull + public Builder argumentPair(@Nonnull final String name, + @Nonnull final TypeToken outputType, + @Nonnull final Pair names, + @Nonnull final Pair, Class> parserPair, + @Nonnull final Function, O> mapper, + @Nonnull final Description description) { + if (this.commandManager == null) { + throw new IllegalStateException("This cannot be called from a command that has no command manager attached"); + } + return this.argument( + ArgumentPair.required(this.commandManager, name, names, parserPair).withMapper(outputType, mapper), + description); + } + + /** + * Create a new argument pair that maps to {@link com.intellectualsites.commands.types.tuples.Triplet} + *

+ * For this to work, there must be a {@link CommandManager} + * attached to the command builder. To guarantee this, it is recommended to get the command builder instance + * using {@link CommandManager#commandBuilder(String, String...)} + * + * @param name Name of the argument + * @param names Triplet containing the names of the sub-arguments + * @param parserTriplet Triplet containing the types of the sub-arguments. There must be parsers for these types + * registered in the {@link com.intellectualsites.commands.arguments.parser.ParserRegistry} + * used by the {@link CommandManager} attached to this command + * @param description Description of the argument + * @param First type + * @param Second type + * @param Third type + * @return Builder instance with the argument inserted + */ + @Nonnull + public Builder argumentTriplet(@Nonnull final String name, + @Nonnull final Triplet names, + @Nonnull final Triplet, Class, Class> parserTriplet, + @Nonnull final Description description) { + if (this.commandManager == null) { + throw new IllegalStateException("This cannot be called from a command that has no command manager attached"); + } + return this.argument(ArgumentTriplet.required(this.commandManager, name, names, parserTriplet).simple(), description); + } + + /** + * Create a new argument triplet that maps to a custom type. + *

+ * For this to work, there must be a {@link CommandManager} + * attached to the command builder. To guarantee this, it is recommended to get the command builder instance + * using {@link CommandManager#commandBuilder(String, String...)} + * + * @param name Name of the argument + * @param outputType The output type + * @param names Triplet containing the names of the sub-arguments + * @param parserTriplet Triplet containing the types of the sub-arguments. There must be parsers for these types + * registered in the {@link com.intellectualsites.commands.arguments.parser.ParserRegistry} used by + * the {@link CommandManager} attached to this command + * @param mapper Mapper that maps from {@link Triplet} to the custom type + * @param description Description of the argument + * @param First type + * @param Second type + * @param Third type + * @param Output type + * @return Builder instance with the argument inserted + */ + @Nonnull + public Builder argumentTriplet(@Nonnull final String name, + @Nonnull final TypeToken outputType, + @Nonnull final Triplet names, + @Nonnull final Triplet, Class, Class> parserTriplet, + @Nonnull final Function, O> mapper, + @Nonnull final Description description) { + if (this.commandManager == null) { + throw new IllegalStateException("This cannot be called from a command that has no command manager attached"); + } + return this.argument( + ArgumentTriplet.required(this.commandManager, name, names, parserTriplet).withMapper(outputType, mapper), + description); + } + + // End of compound helper methods + /** * Specify the command execution handler * diff --git a/cloud-core/src/main/java/cloud/commandframework/CommandTree.java b/cloud-core/src/main/java/cloud/commandframework/CommandTree.java index 423da3c2..491ab2e2 100644 --- a/cloud-core/src/main/java/cloud/commandframework/CommandTree.java +++ b/cloud-core/src/main/java/cloud/commandframework/CommandTree.java @@ -360,6 +360,25 @@ public final class CommandTree { if (children.size() == 1 && !(children.get(0).getValue() instanceof StaticArgument)) { // The value has to be a variable final Node> child = children.get(0); + + // START: Compound arguments + /* When we get in here, we need to treat compound arguments a little differently */ + if (child.getValue() instanceof CompoundArgument) { + @SuppressWarnings("unchecked") + final CompoundArgument compoundArgument = (CompoundArgument) child.getValue(); + /* See how many arguments it requires */ + final int requiredArguments = compoundArgument.getParserTuple().getSize(); + /* Figure out whether we even need to care about this */ + if (commandQueue.size() <= requiredArguments) { + /* Attempt to pop as many arguments from the stack as possible */ + for (int i = 0; i < requiredArguments - 1 && commandQueue.size() > 1; i++) { + commandQueue.remove(); + commandContext.store("__parsing_argument__", i + 2); + } + } + } + // END: Compound arguments + if (child.getValue() != null) { if (commandQueue.isEmpty()) { return Collections.emptyList(); @@ -367,6 +386,10 @@ public final class CommandTree { } else if (child.isLeaf() && commandQueue.size() < 2) { return child.getValue().getSuggestionsProvider().apply(commandContext, commandQueue.peek()); } else if (child.isLeaf()) { + if (child.getValue() instanceof CompoundArgument) { + final String last = ((LinkedList) commandQueue).getLast(); + return child.getValue().getSuggestionsProvider().apply(commandContext, last); + } return Collections.emptyList(); } else if (commandQueue.peek().isEmpty()) { return child.getValue().getSuggestionsProvider().apply(commandContext, commandQueue.remove()); diff --git a/cloud-core/src/main/java/cloud/commandframework/arguments/CommandArgument.java b/cloud-core/src/main/java/cloud/commandframework/arguments/CommandArgument.java index 9255e1c3..a1a627ad 100644 --- a/cloud-core/src/main/java/cloud/commandframework/arguments/CommandArgument.java +++ b/cloud-core/src/main/java/cloud/commandframework/arguments/CommandArgument.java @@ -77,7 +77,7 @@ public class CommandArgument implements Comparable> /** * The type that is produces by the argument's parser */ - private final Class valueType; + private final TypeToken valueType; /** * Suggestion provider */ @@ -99,7 +99,7 @@ public class CommandArgument implements Comparable> @Nonnull final String name, @Nonnull final ArgumentParser parser, @Nonnull final String defaultValue, - @Nonnull final Class valueType, + @Nonnull final TypeToken valueType, @Nullable final BiFunction, String, List> suggestionsProvider) { this.required = required; this.name = Objects.requireNonNull(name, "Name may not be null"); @@ -114,6 +114,25 @@ public class CommandArgument implements Comparable> : suggestionsProvider; } + /** + * Construct a new command argument + * + * @param required Whether or not the argument is required + * @param name The argument name + * @param parser The argument parser + * @param defaultValue Default value used when no value is provided by the command sender + * @param valueType Type produced by the parser + * @param suggestionsProvider Suggestions provider + */ + public CommandArgument(final boolean required, + @Nonnull final String name, + @Nonnull final ArgumentParser parser, + @Nonnull final String defaultValue, + @Nonnull final Class valueType, + @Nullable final BiFunction, String, List> suggestionsProvider) { + this(required, name, parser, defaultValue, TypeToken.of(valueType), suggestionsProvider); + } + /** * Construct a new command argument * @@ -144,11 +163,26 @@ public class CommandArgument implements Comparable> * @return Argument builder */ @Nonnull - public static CommandArgument.Builder ofType(@Nonnull final Class clazz, + public static CommandArgument.Builder ofType(@Nonnull final TypeToken clazz, @Nonnull final String name) { return new Builder<>(clazz, name); } + /** + * Create a new command argument + * + * @param clazz Argument class + * @param name Argument name + * @param Command sender type + * @param Argument Type. Used to make the compiler happy. + * @return Argument builder + */ + @Nonnull + public static CommandArgument.Builder ofType(@Nonnull final Class clazz, + @Nonnull final String name) { + return new Builder<>(TypeToken.of(clazz), name); + } + /** * Check whether or not the command argument is required * @@ -277,7 +311,7 @@ public class CommandArgument implements Comparable> * @return Value type */ @Nonnull - public Class getValueType() { + public TypeToken getValueType() { return this.valueType; } @@ -310,7 +344,7 @@ public class CommandArgument implements Comparable> */ public static class Builder { - private final Class valueType; + private final TypeToken valueType; private final String name; private CommandManager manager; @@ -319,12 +353,17 @@ public class CommandArgument implements Comparable> private String defaultValue = ""; private BiFunction, String, List> suggestionsProvider; - protected Builder(@Nonnull final Class valueType, + protected Builder(@Nonnull final TypeToken valueType, @Nonnull final String name) { this.valueType = valueType; this.name = name; } + protected Builder(@Nonnull final Class valueType, + @Nonnull final String name) { + this(TypeToken.of(valueType), name); + } + /** * Set the command manager. Will be used to create a default parser * if none was provided @@ -418,7 +457,7 @@ public class CommandArgument implements Comparable> @Nonnull public CommandArgument build() { if (this.parser == null && this.manager != null) { - this.parser = this.manager.getParserRegistry().createParser(TypeToken.of(valueType), ParserParameters.empty()) + this.parser = this.manager.getParserRegistry().createParser(valueType, ParserParameters.empty()) .orElse(null); } if (this.parser == null) { diff --git a/cloud-core/src/main/java/cloud/commandframework/arguments/DelegatingSuggestionsProvider.java b/cloud-core/src/main/java/cloud/commandframework/arguments/DelegatingSuggestionsProvider.java index 9f5f65f4..9af8860e 100644 --- a/cloud-core/src/main/java/cloud/commandframework/arguments/DelegatingSuggestionsProvider.java +++ b/cloud-core/src/main/java/cloud/commandframework/arguments/DelegatingSuggestionsProvider.java @@ -41,8 +41,8 @@ final class DelegatingSuggestionsProvider implements BiFunction apply(final CommandContext context, final String s) { - return this.parser.suggestions(context, s); + public List apply(final CommandContext context, final String string) { + return this.parser.suggestions(context, string); } @Override diff --git a/cloud-core/src/main/java/cloud/commandframework/arguments/StandardCommandSyntaxFormatter.java b/cloud-core/src/main/java/cloud/commandframework/arguments/StandardCommandSyntaxFormatter.java index d5f1525a..cc886111 100644 --- a/cloud-core/src/main/java/cloud/commandframework/arguments/StandardCommandSyntaxFormatter.java +++ b/cloud-core/src/main/java/cloud/commandframework/arguments/StandardCommandSyntaxFormatter.java @@ -53,7 +53,20 @@ public class StandardCommandSyntaxFormatter implements CommandSyntaxFormatter if (commandArgument instanceof StaticArgument) { stringBuilder.append(commandArgument.getName()); } else { - if (commandArgument.isRequired()) { + if (commandArgument instanceof CompoundArgument) { + final char prefix = commandArgument.isRequired() ? '<' : '['; + final char suffix = commandArgument.isRequired() ? '>' : ']'; + stringBuilder.append(prefix); + // noinspection all + final Object[] names = ((CompoundArgument) commandArgument).getNames().toArray(); + for (int i = 0; i < names.length; i++) { + stringBuilder.append(prefix).append(names[i]).append(suffix); + if ((i + 1) < names.length) { + stringBuilder.append(' '); + } + } + stringBuilder.append(suffix); + } else if (commandArgument.isRequired()) { stringBuilder.append("<").append(commandArgument.getName()).append(">"); } else { stringBuilder.append("[").append(commandArgument.getName()).append("]"); @@ -90,10 +103,24 @@ public class StandardCommandSyntaxFormatter implements CommandSyntaxFormatter prefix = "["; suffix = "]"; } - stringBuilder.append(" ") - .append(prefix) - .append(argument.getName()) - .append(suffix); + + if (argument instanceof CompoundArgument) { + stringBuilder.append(" ").append(prefix); + // noinspection all + final Object[] names = ((CompoundArgument) argument).getNames().toArray(); + for (int i = 0; i < names.length; i++) { + stringBuilder.append(prefix).append(names[i]).append(suffix); + if ((i + 1) < names.length) { + stringBuilder.append(' '); + } + } + stringBuilder.append(suffix); + } else { + stringBuilder.append(" ") + .append(prefix) + .append(argument.getName()) + .append(suffix); + } tail = tail.getChildren().get(0); } return stringBuilder.toString(); diff --git a/cloud-core/src/main/java/cloud/commandframework/types/tuples/Pair.java b/cloud-core/src/main/java/cloud/commandframework/types/tuples/Pair.java index 9703ad57..80d37096 100644 --- a/cloud-core/src/main/java/cloud/commandframework/types/tuples/Pair.java +++ b/cloud-core/src/main/java/cloud/commandframework/types/tuples/Pair.java @@ -33,7 +33,7 @@ import javax.annotation.Nonnull; * @param First type * @param Second type */ -public class Pair { +public class Pair implements Tuple { @Nonnull private final U first; @@ -104,4 +104,18 @@ public class Pair { return String.format("(%s, %s)", this.first, this.second); } + @Override + public final int getSize() { + return Tuples.SIZE_PAIR; + } + + @Nonnull + @Override + public final Object[] toArray() { + final Object[] array = new Object[2]; + array[0] = this.first; + array[1] = this.second; + return array; + } + } diff --git a/cloud-core/src/main/java/cloud/commandframework/types/tuples/Quartet.java b/cloud-core/src/main/java/cloud/commandframework/types/tuples/Quartet.java index 27a49b67..31a8be07 100644 --- a/cloud-core/src/main/java/cloud/commandframework/types/tuples/Quartet.java +++ b/cloud-core/src/main/java/cloud/commandframework/types/tuples/Quartet.java @@ -35,7 +35,7 @@ import javax.annotation.Nonnull; * @param Third type * @param Fourth type */ -public class Quartet { +public class Quartet implements Tuple { @Nonnull private final U first; @@ -142,4 +142,20 @@ public class Quartet { return String.format("(%s, %s, %s, %s)", this.first, this.second, this.third, this.fourth); } + @Override + public final int getSize() { + return Tuples.SIZE_QUARTET; + } + + @Nonnull + @Override + public final Object[] toArray() { + final Object[] array = new Object[4]; + array[0] = this.first; + array[1] = this.second; + array[3] = this.third; + array[4] = this.fourth; + return array; + } + } diff --git a/cloud-core/src/main/java/cloud/commandframework/types/tuples/Quintet.java b/cloud-core/src/main/java/cloud/commandframework/types/tuples/Quintet.java index 96ebfaf9..59c751e2 100644 --- a/cloud-core/src/main/java/cloud/commandframework/types/tuples/Quintet.java +++ b/cloud-core/src/main/java/cloud/commandframework/types/tuples/Quintet.java @@ -36,7 +36,7 @@ import javax.annotation.Nonnull; * @param Fourth type * @param Fifth type */ -public class Quintet { +public class Quintet implements Tuple { @Nonnull private final U first; @@ -161,4 +161,21 @@ public class Quintet { return String.format("(%s, %s, %s, %s, %s)", this.first, this.second, this.third, this.fourth, this.fifth); } + @Override + public final int getSize() { + return Tuples.SIZE_QUINTET; + } + + @Nonnull + @Override + public final Object[] toArray() { + final Object[] array = new Object[5]; + array[0] = this.first; + array[1] = this.second; + array[3] = this.third; + array[4] = this.fourth; + array[5] = this.fifth; + return array; + } + } diff --git a/cloud-core/src/main/java/cloud/commandframework/types/tuples/Sextet.java b/cloud-core/src/main/java/cloud/commandframework/types/tuples/Sextet.java index f15ae081..24d4150d 100644 --- a/cloud-core/src/main/java/cloud/commandframework/types/tuples/Sextet.java +++ b/cloud-core/src/main/java/cloud/commandframework/types/tuples/Sextet.java @@ -37,7 +37,7 @@ import javax.annotation.Nonnull; * @param Fifth type * @param Sixth type */ -public class Sextet { +public class Sextet implements Tuple { @Nonnull private final U first; @@ -181,4 +181,22 @@ public class Sextet { this.fourth, this.fifth, this.sixth); } + @Override + public final int getSize() { + return Tuples.SIZE_SEXTET; + } + + @Nonnull + @Override + public final Object[] toArray() { + final Object[] array = new Object[6]; + array[0] = this.first; + array[1] = this.second; + array[3] = this.third; + array[4] = this.fourth; + array[5] = this.fifth; + array[6] = this.sixth; + return array; + } + } diff --git a/cloud-core/src/main/java/cloud/commandframework/types/tuples/Triplet.java b/cloud-core/src/main/java/cloud/commandframework/types/tuples/Triplet.java index bd74fa67..7d0f6e23 100644 --- a/cloud-core/src/main/java/cloud/commandframework/types/tuples/Triplet.java +++ b/cloud-core/src/main/java/cloud/commandframework/types/tuples/Triplet.java @@ -34,7 +34,7 @@ import javax.annotation.Nonnull; * @param Second type * @param Third type */ -public class Triplet { +public class Triplet implements Tuple { @Nonnull private final U first; @@ -123,4 +123,19 @@ public class Triplet { return String.format("(%s, %s, %s)", this.first, this.second, this.third); } + @Override + public final int getSize() { + return Tuples.SIZE_TRIPLET; + } + + @Nonnull + @Override + public final Object[] toArray() { + final Object[] array = new Object[3]; + array[0] = this.first; + array[1] = this.second; + array[2] = this.third; + return array; + } + } diff --git a/cloud-core/src/main/java/cloud/commandframework/types/tuples/Tuples.java b/cloud-core/src/main/java/cloud/commandframework/types/tuples/Tuples.java index a7557bd9..4428f52e 100644 --- a/cloud-core/src/main/java/cloud/commandframework/types/tuples/Tuples.java +++ b/cloud-core/src/main/java/cloud/commandframework/types/tuples/Tuples.java @@ -30,6 +30,12 @@ import javax.annotation.Nonnull; */ public final class Tuples { + static final int SIZE_PAIR = 2; + static final int SIZE_TRIPLET = 3; + static final int SIZE_QUARTET = 4; + static final int SIZE_QUINTET = 5; + static final int SIZE_SEXTET = 6; + private Tuples() { } diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/arguments/compound/ArgumentPair.java b/cloud-core/src/main/java/com/intellectualsites/commands/arguments/compound/ArgumentPair.java new file mode 100644 index 00000000..cee39180 --- /dev/null +++ b/cloud-core/src/main/java/com/intellectualsites/commands/arguments/compound/ArgumentPair.java @@ -0,0 +1,155 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package com.intellectualsites.commands.arguments.compound; + +import com.google.common.reflect.TypeToken; +import com.intellectualsites.commands.CommandManager; +import com.intellectualsites.commands.arguments.parser.ArgumentParser; +import com.intellectualsites.commands.arguments.parser.ParserParameters; +import com.intellectualsites.commands.arguments.parser.ParserRegistry; +import com.intellectualsites.commands.types.tuples.Pair; + +import javax.annotation.Nonnull; +import java.util.function.Function; + +/** + * A compound argument consisting of two inner arguments + * + * @param Command sender type + * @param First argument type + * @param Second argument type + * @param Output type + */ +public final class ArgumentPair extends CompoundArgument, C, O> { + + private ArgumentPair(final boolean required, + @Nonnull final String name, + @Nonnull final Pair names, + @Nonnull final Pair, Class> types, + @Nonnull final Pair, ArgumentParser> parserPair, + @Nonnull final Function, O> mapper, + @Nonnull final TypeToken valueType) { + super(required, name, names, parserPair, types, mapper, o -> Pair.of((U) o[0], (V) o[1]), valueType); + } + + /** + * Construct a builder for an argument pair + * + * @param manager Command manager + * @param name Argument name + * @param names Sub-argument names + * @param types Pair containing the types of the sub-arguments. There must be parsers for these types registered + * in the {@link com.intellectualsites.commands.arguments.parser.ParserRegistry} used by the + * {@link CommandManager} + * @param Command sender type + * @param First parsed type + * @param Second parsed type + * @return Intermediary builder + */ + @Nonnull + public static ArgumentPairIntermediaryBuilder required(@Nonnull final CommandManager manager, + @Nonnull final String name, + @Nonnull final Pair names, + @Nonnull final Pair, Class> types) { + final ParserRegistry parserRegistry = manager.getParserRegistry(); + final ArgumentParser firstParser = parserRegistry.createParser(TypeToken.of(types.getFirst()), + ParserParameters.empty()).orElseThrow(() -> + new IllegalArgumentException( + "Could not create parser for primary type")); + final ArgumentParser secondaryParser = parserRegistry.createParser(TypeToken.of(types.getSecond()), + ParserParameters.empty()).orElseThrow(() -> + new IllegalArgumentException( + "Could not create parser for secondary type")); + return new ArgumentPairIntermediaryBuilder<>(true, name, names, Pair.of(firstParser, secondaryParser), types); + } + + @SuppressWarnings("ALL") + public static final class ArgumentPairIntermediaryBuilder { + + private final boolean required; + private final String name; + private final Pair, ArgumentParser> parserPair; + private final Pair names; + private final Pair, Class> types; + + private ArgumentPairIntermediaryBuilder(final boolean required, + @Nonnull final String name, + @Nonnull final Pair names, + @Nonnull final Pair, ArgumentParser> parserPair, + @Nonnull final Pair, Class> types) { + this.required = required; + this.name = name; + this.names = names; + this.parserPair = parserPair; + this.types = types; + } + + /** + * Create a simple argument pair that maps to a pair + * + * @return Argument pair + */ + @Nonnull + public ArgumentPair> simple() { + return new ArgumentPair>(this.required, + this.name, + this.names, + this.types, + this.parserPair, + Function.identity(), + new TypeToken>() { + }); + } + + /** + * Create an argument pair that maps to a specific type + * + * @param clazz Output class + * @param mapper Output mapper + * @param Output type + * @return Created pair + */ + @Nonnull + public ArgumentPair withMapper(@Nonnull final TypeToken clazz, + @Nonnull final Function, O> mapper) { + return new ArgumentPair(this.required, this.name, this.names, this.types, this.parserPair, mapper, clazz); + } + + /** + * Create an argument pair that maps to a specific type + * + * @param clazz Output class + * @param mapper Output mapper + * @param Output type + * @return Created pair + */ + @Nonnull + public ArgumentPair withMapper(@Nonnull final Class clazz, + @Nonnull final Function, O> mapper) { + return this.withMapper(TypeToken.of(clazz), mapper); + } + + } + +} diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/arguments/compound/ArgumentTriplet.java b/cloud-core/src/main/java/com/intellectualsites/commands/arguments/compound/ArgumentTriplet.java new file mode 100644 index 00000000..248b2835 --- /dev/null +++ b/cloud-core/src/main/java/com/intellectualsites/commands/arguments/compound/ArgumentTriplet.java @@ -0,0 +1,166 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package com.intellectualsites.commands.arguments.compound; + +import com.google.common.reflect.TypeToken; +import com.intellectualsites.commands.CommandManager; +import com.intellectualsites.commands.arguments.parser.ArgumentParser; +import com.intellectualsites.commands.arguments.parser.ParserParameters; +import com.intellectualsites.commands.arguments.parser.ParserRegistry; +import com.intellectualsites.commands.types.tuples.Triplet; + +import javax.annotation.Nonnull; +import java.util.function.Function; + +/** + * A compound argument consisting of three inner arguments + * + * @param Command sender type + * @param First argument type + * @param Second argument type + * @param Third argument type + * @param Output type + */ +public final class ArgumentTriplet extends CompoundArgument, C, O> { + + private ArgumentTriplet(final boolean required, + @Nonnull final String name, + @Nonnull final Triplet names, + @Nonnull final Triplet, Class, Class> types, + @Nonnull final Triplet, ArgumentParser, + ArgumentParser> parserTriplet, + @Nonnull final Function, O> mapper, + @Nonnull final TypeToken valueType) { + super(required, name, names, parserTriplet, types, mapper, o -> Triplet.of((U) o[0], (V) o[1], (W) o[2]), valueType); + } + + /** + * Construct a builder for an argument triplet + * + * @param manager Command manager + * @param name Argument name + * @param names Sub-argument names + * @param types Triplet containing the types of the sub-arguments. There must be parsers for these types registered + * in the {@link com.intellectualsites.commands.arguments.parser.ParserRegistry} used by the + * {@link CommandManager} + * @param Command sender type + * @param First parsed type + * @param Second parsed type + * @param Third type + * @return Intermediary builder + */ + @Nonnull + public static ArgumentTripletIntermediaryBuilder + required(@Nonnull final CommandManager manager, + @Nonnull final String name, + @Nonnull final Triplet names, + @Nonnull final Triplet, Class, Class> types) { + final ParserRegistry parserRegistry = manager.getParserRegistry(); + final ArgumentParser firstParser = parserRegistry.createParser(TypeToken.of(types.getFirst()), + ParserParameters.empty()).orElseThrow(() -> + new IllegalArgumentException( + "Could not create parser for primary type")); + final ArgumentParser secondaryParser = parserRegistry.createParser(TypeToken.of(types.getSecond()), + ParserParameters.empty()).orElseThrow(() -> + new IllegalArgumentException( + "Could not create parser for secondary type")); + final ArgumentParser tertiaryParser = parserRegistry.createParser(TypeToken.of(types.getThird()), + ParserParameters.empty()).orElseThrow(() -> + new IllegalArgumentException( + "Could not create parser for tertiary type")); + return new ArgumentTripletIntermediaryBuilder<>(true, name, names, + Triplet.of(firstParser, secondaryParser, tertiaryParser), types); + } + + @SuppressWarnings("ALL") + public static final class ArgumentTripletIntermediaryBuilder { + + private final boolean required; + private final String name; + private final Triplet, ArgumentParser, ArgumentParser> parserTriplet; + private final Triplet names; + private final Triplet, Class, Class> types; + + private ArgumentTripletIntermediaryBuilder(final boolean required, + @Nonnull final String name, + @Nonnull final Triplet names, + @Nonnull final Triplet, + ArgumentParser, ArgumentParser> parserTriplet, + @Nonnull final Triplet, Class, Class> types) { + this.required = required; + this.name = name; + this.names = names; + this.parserTriplet = parserTriplet; + this.types = types; + } + + /** + * Create a simple argument triplet that maps to a triplet + * + * @return Argument triplet + */ + @Nonnull + public ArgumentTriplet> simple() { + return new ArgumentTriplet<>(this.required, + this.name, + this.names, + this.types, + this.parserTriplet, + Function.identity(), + new TypeToken>() { + }); + } + + /** + * Create an argument triplet that maps to a specific type + * + * @param clazz Output class + * @param mapper Output mapper + * @param Output type + * @return Created triplet + */ + @Nonnull + public ArgumentTriplet withMapper(@Nonnull final TypeToken clazz, + @Nonnull final Function, O> mapper) { + return new ArgumentTriplet<>(this.required, this.name, this.names, this.types, this.parserTriplet, mapper, clazz); + } + + /** + * Create an argument triplet that maps to a specific type + * + * @param clazz Output class + * @param mapper Output mapper + * @param Output type + * @return Created triplet + */ + @Nonnull + public ArgumentTriplet withMapper(@Nonnull final Class clazz, + @Nonnull final Function, O> mapper) { + return new ArgumentTriplet<>(this.required, this.name, this.names, this.types, + this.parserTriplet, mapper, TypeToken.of(clazz)); + } + + } + +} diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/arguments/compound/CompoundArgument.java b/cloud-core/src/main/java/com/intellectualsites/commands/arguments/compound/CompoundArgument.java new file mode 100644 index 00000000..09035107 --- /dev/null +++ b/cloud-core/src/main/java/com/intellectualsites/commands/arguments/compound/CompoundArgument.java @@ -0,0 +1,153 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package com.intellectualsites.commands.arguments.compound; + +import com.google.common.reflect.TypeToken; +import com.intellectualsites.commands.arguments.CommandArgument; +import com.intellectualsites.commands.arguments.parser.ArgumentParseResult; +import com.intellectualsites.commands.arguments.parser.ArgumentParser; +import com.intellectualsites.commands.context.CommandContext; +import com.intellectualsites.commands.types.tuples.Tuple; + +import javax.annotation.Nonnull; +import java.util.List; +import java.util.Queue; +import java.util.function.Function; + +/** + * Compound argument + * + * @param Tuple type + * @param Command sender type + * @param Output type + */ +public class CompoundArgument extends CommandArgument { + + private final Tuple types; + private final Tuple names; + private final Tuple parserTuple; + + CompoundArgument(final boolean required, + @Nonnull final String name, + @Nonnull final Tuple names, + @Nonnull final Tuple parserTuple, + @Nonnull final Tuple types, + @Nonnull final Function mapper, + @Nonnull final Function tupleFactory, + @Nonnull final TypeToken valueType) { + super(required, + name, + new CompoundParser<>(parserTuple, mapper, tupleFactory), + "", + valueType, + null); + this.parserTuple = parserTuple; + this.names = names; + this.types = types; + } + + /** + * Get the tuple containing the internal parsers + * + * @return Internal parsers + */ + @Nonnull + public Tuple getParserTuple() { + return this.parserTuple; + } + + /** + * Get the argument names + * + * @return Argument names + */ + @Nonnull + public Tuple getNames() { + return this.names; + } + + /** + * Get the parser types + * + * @return Parser types + */ + @Nonnull + public Tuple getTypes() { + return this.types; + } + + + private static final class CompoundParser implements ArgumentParser { + + private final Object[] parsers; + private final Function mapper; + private final Function tupleFactory; + + private CompoundParser(@Nonnull final Tuple parserTuple, + @Nonnull final Function mapper, + @Nonnull final Function tupleFactory) { + this.parsers = parserTuple.toArray(); + this.mapper = mapper; + this.tupleFactory = tupleFactory; + } + + @Nonnull + @Override + public ArgumentParseResult parse(@Nonnull final CommandContext commandContext, + @Nonnull final Queue inputQueue) { + final Object[] output = new Object[this.parsers.length]; + for (int i = 0; i < this.parsers.length; i++) { + @SuppressWarnings("unchecked") + final ArgumentParser parser = (ArgumentParser) this.parsers[i]; + final ArgumentParseResult result = parser.parse(commandContext, inputQueue); + if (result.getFailure().isPresent()) { + /* Return the failure */ + return ArgumentParseResult.failure(result.getFailure().get()); + } + /* Store the parsed value */ + output[i] = result.getParsedValue().orElse(null); + } + /* We now know that we have complete output, as none of the parsers returned a failure */ + return ArgumentParseResult.success(this.mapper.apply(this.tupleFactory.apply(output))); + } + + @Nonnull + @Override + public List suggestions(@Nonnull final CommandContext commandContext, + @Nonnull final String input) { + /* + This method will be called n times, each time for each of the internal types. + The problem is that we need to then know which of the parsers to forward the + suggestion request to. This is done by storing the number of parsed subtypes + in the context, so we can then extract that number and forward the request + */ + final int argument = commandContext.getOrDefault("__parsing_argument__", 1) - 1; + System.out.printf("Compound argument suggestions: %d | %s\n", argument, input); + + //noinspection all + return ((ArgumentParser) this.parsers[argument]).suggestions(commandContext, input); + } + } + +} diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/arguments/compound/package-info.java b/cloud-core/src/main/java/com/intellectualsites/commands/arguments/compound/package-info.java new file mode 100644 index 00000000..9d3ebc98 --- /dev/null +++ b/cloud-core/src/main/java/com/intellectualsites/commands/arguments/compound/package-info.java @@ -0,0 +1,28 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +/** + * Argument types that consists of 2 or more sub-types + */ +package com.intellectualsites.commands.arguments.compound; diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/types/tuples/Tuple.java b/cloud-core/src/main/java/com/intellectualsites/commands/types/tuples/Tuple.java new file mode 100644 index 00000000..043b7988 --- /dev/null +++ b/cloud-core/src/main/java/com/intellectualsites/commands/types/tuples/Tuple.java @@ -0,0 +1,48 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package com.intellectualsites.commands.types.tuples; + +import javax.annotation.Nonnull; + +/** + * Tuple type + */ +public interface Tuple { + + /** + * Get the tuple size + * + * @return Tuple size + */ + int getSize(); + + /** + * Turn the tuple into a type erased array + * + * @return Created array + */ + @Nonnull + Object[] toArray(); + +} diff --git a/cloud-core/src/test/java/cloud/commandframework/CommandHelpHandlerTest.java b/cloud-core/src/test/java/cloud/commandframework/CommandHelpHandlerTest.java index dbeafacb..2354ccb2 100644 --- a/cloud-core/src/test/java/cloud/commandframework/CommandHelpHandlerTest.java +++ b/cloud-core/src/test/java/cloud/commandframework/CommandHelpHandlerTest.java @@ -48,6 +48,12 @@ class CommandHelpHandlerTest { final SimpleCommandMeta meta2 = SimpleCommandMeta.builder().with("description", "Command with variables").build(); manager.command(manager.commandBuilder("test", meta2).literal("int"). argument(IntegerArgument.required("int"), Description.of("A number")).build()); + + manager.command(manager.commandBuilder("vec") + .meta("description", "Takes in a vector") + .argumentPair("vec", Pair.of("x", "y"), + Pair.of(Double.class, Double.class), Description.of("Vector")) + .build()); } @Test @@ -63,7 +69,7 @@ class CommandHelpHandlerTest { @Test void testLongestChains() { final List longestChains = manager.getCommandHelpHandler().getLongestSharedChains(); - Assertions.assertEquals(Arrays.asList("test int|this"), longestChains); + Assertions.assertEquals(Arrays.asList("test int|this", "vec < >"), longestChains); } @Test @@ -77,6 +83,9 @@ class CommandHelpHandlerTest { final CommandHelpHandler.HelpTopic query3 = manager.getCommandHelpHandler().queryHelp("test int"); Assertions.assertTrue(query3 instanceof CommandHelpHandler.VerboseHelpTopic); this.printTopic("test int", query3); + final CommandHelpHandler.HelpTopic query4 = manager.getCommandHelpHandler().queryHelp("vec"); + Assertions.assertTrue(query4 instanceof CommandHelpHandler.VerboseHelpTopic); + this.printTopic("vec", query4); } private void printTopic(@Nonnull final String query, diff --git a/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java b/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java index 0fac10c3..4e18dbe7 100644 --- a/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java +++ b/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java @@ -63,6 +63,17 @@ public class CommandSuggestionsTest { .argument(IntegerArgument.newBuilder("num") .withSuggestionsProvider((c, s) -> Arrays.asList("3", "33", "333")).build()) .build()); + + manager.command(manager.commandBuilder("com") + .argumentPair("com", Pair.of("x", "y"), Pair.of(Integer.class, TestEnum.class), + Description.empty()) + .argument(IntegerArgument.required("int")) + .build()); + + manager.command(manager.commandBuilder("com2") + .argumentPair("com", Pair.of("x", "enum"), + Pair.of(Integer.class, TestEnum.class), Description.empty()) + .build()); } @Test @@ -121,6 +132,21 @@ public class CommandSuggestionsTest { Assertions.assertEquals(Arrays.asList("3", "33", "333"), suggestions); } + @Test + void testCompound() { + final String input = "com "; + final List suggestions = manager.suggest(new TestCommandSender(), input); + Assertions.assertEquals(Arrays.asList("0", "1", "2", "3", "4", "5", "6", "7", "8", "9"), suggestions); + final String input2 = "com 1 "; + final List suggestions2 = manager.suggest(new TestCommandSender(), input2); + Assertions.assertEquals(Arrays.asList("foo", "bar"), suggestions2); + final String input3 = "com 1 foo "; + final List suggestions3 = manager.suggest(new TestCommandSender(), input3); + Assertions.assertEquals(Arrays.asList("0", "1", "2", "3", "4", "5", "6", "7", "8", "9"), suggestions3); + final String input4 = "com2 1 "; + final List suggestions4 = manager.suggest(new TestCommandSender(), input4); + Assertions.assertEquals(Arrays.asList("foo", "bar"), suggestions4); + } public enum TestEnum { FOO, diff --git a/cloud-core/src/test/java/cloud/commandframework/CommandTreeTest.java b/cloud-core/src/test/java/cloud/commandframework/CommandTreeTest.java index 73ab1e7a..1bd64241 100644 --- a/cloud-core/src/test/java/cloud/commandframework/CommandTreeTest.java +++ b/cloud-core/src/test/java/cloud/commandframework/CommandTreeTest.java @@ -81,6 +81,28 @@ class CommandTreeTest { .withPermission("command.outer") .handler(c -> System.out.println("Using outer command")) .build()); + + /* Build command for testing compound types */ + manager.command(manager.commandBuilder("pos") + .argument(ArgumentPair.required(manager, "pos", Pair.of("x", "y"), + Pair.of(Integer.class, Integer.class)) + .simple()) + .handler(c -> { + final Pair pair = c.getRequired("pos"); + System.out.printf("X: %d | Y: %d\n", pair.getFirst(), pair.getSecond()); + }) + .build()); + manager.command(manager.commandBuilder("vec") + .argument(ArgumentPair.required(manager, "vec", Pair.of("x", "y"), + Pair.of(Double.class, Double.class)) + .withMapper(Vector2.class, + pair -> new Vector2(pair.getFirst(), pair.getSecond())) + ) + .handler(c -> { + final Vector2 vector2 = c.getRequired("vec"); + System.out.printf("X: %f | Y: %f\n", vector2.getX(), vector2.getY()); + }) + .build()); } @Test @@ -157,8 +179,35 @@ class CommandTreeTest { manager.executeCommand(new TestCommandSender(), "command").join(); } + @Test + void testCompound() { + manager.executeCommand(new TestCommandSender(), "pos -3 2").join(); + manager.executeCommand(new TestCommandSender(), "vec 1 1").join(); + } + public static final class SpecificCommandSender extends TestCommandSender { } + + public static final class Vector2 { + + private final double x; + private final double y; + + private Vector2(final double x, final double y) { + this.x = x; + this.y = y; + } + + private double getX() { + return this.x; + } + + private double getY() { + return this.y; + } + + } + } diff --git a/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/CloudBrigadierManager.java b/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/CloudBrigadierManager.java index 78e9ab13..9a8eae6e 100644 --- a/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/CloudBrigadierManager.java +++ b/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/CloudBrigadierManager.java @@ -212,7 +212,7 @@ public final class CloudBrigadierManager { @Nullable @SuppressWarnings("all") private > Pair, Boolean> getArgument( - @Nonnull final Class valueType, + @Nonnull final TypeToken valueType, @Nonnull final TypeToken argumentType, @Nonnull final K argument) { final ArgumentParser commandArgument = (ArgumentParser) argument; @@ -225,9 +225,9 @@ public final class CloudBrigadierManager { @Nonnull private > Pair, Boolean> createDefaultMapper( - @Nonnull final Class clazz, + @Nonnull final TypeToken clazz, @Nonnull final ArgumentParser argument) { - final Supplier> argumentTypeSupplier = this.defaultArgumentTypeSuppliers.get(clazz); + final Supplier> argumentTypeSupplier = this.defaultArgumentTypeSuppliers.get(clazz.getRawType()); if (argumentTypeSupplier != null) { return new Pair<>(argumentTypeSupplier.get(), true); } @@ -298,12 +298,54 @@ public final class CloudBrigadierManager { @Nonnull final BiPredicate permissionChecker, @Nonnull final com.mojang.brigadier.Command executor, @Nonnull final SuggestionProvider suggestionProvider) { + if (root.getValue() instanceof CompoundArgument) { + @SuppressWarnings("unchecked") + final CompoundArgument compoundArgument = (CompoundArgument) root.getValue(); + final Object[] parsers = compoundArgument.getParserTuple().toArray(); + final Object[] types = compoundArgument.getTypes().toArray(); + final Object[] names = compoundArgument.getNames().toArray(); + /* Build nodes backwards */ + final ArgumentBuilder[] argumentBuilders = new ArgumentBuilder[parsers.length]; + + for (int i = parsers.length - 1; i >= 0; i--) { + @SuppressWarnings("unchecked") + final ArgumentParser parser = (ArgumentParser) parsers[i]; + final Pair, Boolean> pair = this.getArgument(TypeToken.of((Class) types[i]), + TypeToken.of(parser.getClass()), + parser); + final SuggestionProvider provider = pair.getRight() ? null : suggestionProvider; + final ArgumentBuilder fragmentBuilder = RequiredArgumentBuilder + .argument((String) names[i], (ArgumentType) pair.getLeft()) + .suggests(provider) + .requires(sender -> permissionChecker.test(sender, + (CommandPermission) root.getNodeMeta() + .getOrDefault("permission", Permission.empty()))); + argumentBuilders[i] = fragmentBuilder; + + if (forceExecutor || (i == parsers.length - 1) && (root.isLeaf() || !root.getValue().isRequired())) { + fragmentBuilder.executes(executor); + } + + /* Link all previous builder to this one */ + if ((i + 1) < parsers.length) { + fragmentBuilder.then(argumentBuilders[i + 1]); + } + } + + for (final CommandTree.Node> node : root.getChildren()) { + argumentBuilders[parsers.length - 1] + .then(constructCommandNode(forceExecutor, node, permissionChecker, executor, suggestionProvider)); + } + + return argumentBuilders[0]; + } ArgumentBuilder argumentBuilder; if (root.getValue() instanceof StaticArgument) { argumentBuilder = LiteralArgumentBuilder.literal(root.getValue().getName()) .requires(sender -> permissionChecker.test(sender, (CommandPermission) root.getNodeMeta() - .getOrDefault("permission", Permission.empty()))) + .getOrDefault("permission", + Permission.empty()))) .executes(executor); } else { final Pair, Boolean> pair = this.getArgument(root.getValue().getValueType(), diff --git a/cloud-minecraft/cloud-bukkit-test/src/main/java/cloud/commandframework/BukkitTest.java b/cloud-minecraft/cloud-bukkit-test/src/main/java/cloud/commandframework/BukkitTest.java index 7ae4bd08..1973dd87 100644 --- a/cloud-minecraft/cloud-bukkit-test/src/main/java/cloud/commandframework/BukkitTest.java +++ b/cloud-minecraft/cloud-bukkit-test/src/main/java/cloud/commandframework/BukkitTest.java @@ -58,6 +58,7 @@ import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.util.Vector; import javax.annotation.Nonnull; import java.util.ArrayList; @@ -210,13 +211,35 @@ public final class BukkitTest extends JavaPlugin { .build(), Description.of("Help Query")) .handler(c -> minecraftHelp.queryCommands(c.get("query").orElse(""), c.getSender())).build()); - + this.registerTeleportCommand(mgr); mgr.registerExceptionHandler(InvalidSyntaxException.class, (c, e) -> e.printStackTrace()); } catch (final Exception e) { e.printStackTrace(); } } +private void registerTeleportCommand(@Nonnull final BukkitCommandManager manager) { + manager.command(mgr.commandBuilder("teleport") + .meta("description", "Takes in a location and teleports the player there") + .withSenderType(Player.class) + .argument(WorldArgument.required("world"), Description.of("World name")) + .argumentTriplet("coords", + TypeToken.of(Vector.class), + Triplet.of("x", "y", "z"), + Triplet.of(Double.class, Double.class, Double.class), + triplet -> new Vector(triplet.getFirst(), triplet.getSecond(), triplet.getThird()), + Description.of("Coordinates")) + .handler(context -> { + context.getSender().sendMessage(ChatColor.GOLD + "Teleporting!"); + Bukkit.getScheduler().runTask(this, () -> { + final World world = context.getRequired("world"); + final Vector vector = context.getRequired("coords"); + ((Player) context.getSender()).teleport(vector.toLocation(world)); + }); + }) + .build()); +} + @CommandDescription("Test cloud command using @CommandMethod") @CommandMethod(value = "annotation|a [number]", permission = "some.permission.node") private void annotatedCommand(@Nonnull final Player player,