diff --git a/src/main/java/com/mojang/brigadier/CommandDispatcher.java b/src/main/java/com/mojang/brigadier/CommandDispatcher.java index a4f27fe..5158b46 100644 --- a/src/main/java/com/mojang/brigadier/CommandDispatcher.java +++ b/src/main/java/com/mojang/brigadier/CommandDispatcher.java @@ -1,14 +1,31 @@ package com.mojang.brigadier; +import com.google.common.base.Predicate; +import com.google.common.collect.ComparisonChain; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.context.CommandContextBuilder; import com.mojang.brigadier.exceptions.CommandException; import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; import com.mojang.brigadier.tree.CommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; import com.mojang.brigadier.tree.RootCommandNode; +import javax.annotation.Nullable; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + public class CommandDispatcher { + private static final Predicate HAS_COMMAND = new Predicate() { + @Override + public boolean apply(@Nullable CommandNode input) { + return input != null && (input.getCommand() != null || Iterables.any(input.getChildren(), HAS_COMMAND)); + } + }; + public static final SimpleCommandExceptionType ERROR_UNKNOWN_COMMAND = new SimpleCommandExceptionType("unknown_command", "Unknown command"); public static final String ARGUMENT_SEPARATOR = " "; @@ -39,13 +56,57 @@ public class CommandDispatcher { } } - if (exception != null) { - throw exception; - } if (command.length() > 0) { + if (exception != null) { + throw exception; + } throw ERROR_UNKNOWN_COMMAND.create(); } return contextBuilder.build(); } + + public String getUsage(String command, T source) throws CommandException { + CommandContext context = parseNodes(root, command, new CommandContextBuilder(source)); + CommandNode base = Iterables.getLast(context.getNodes().keySet()); + List children = Lists.newArrayList(Iterables.filter(base.getChildren(), HAS_COMMAND)); + boolean optional = base.getCommand() != null; + + if (children.isEmpty()) { + return context.getInput(); + } + + Collections.sort(children, new Comparator() { + @Override + public int compare(CommandNode o1, CommandNode o2) { + return ComparisonChain.start() + .compareTrueFirst(o1 instanceof LiteralCommandNode, o2 instanceof LiteralCommandNode) + .result(); + } + }); + + StringBuilder result = new StringBuilder(context.getInput()); + result.append(ARGUMENT_SEPARATOR); + if (optional) { + result.append("["); + } else if (children.size() > 1) { + result.append("("); + } + + for (int i = 0; i < children.size(); i++) { + result.append(children.get(i).getUsageText()); + + if (i < children.size() - 1) { + result.append("|"); + } + } + + if (optional) { + result.append("]"); + } else if (children.size() > 1) { + result.append(")"); + } + + return result.toString(); + } } diff --git a/src/main/java/com/mojang/brigadier/context/CommandContext.java b/src/main/java/com/mojang/brigadier/context/CommandContext.java index 09d76a1..06e2471 100644 --- a/src/main/java/com/mojang/brigadier/context/CommandContext.java +++ b/src/main/java/com/mojang/brigadier/context/CommandContext.java @@ -1,19 +1,27 @@ package com.mojang.brigadier.context; +import com.google.common.base.Joiner; +import com.google.common.collect.Iterables; import com.google.common.primitives.Primitives; import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.tree.CommandNode; import java.util.Map; public class CommandContext { + private final Joiner JOINER = Joiner.on(CommandDispatcher.ARGUMENT_SEPARATOR); + private final T source; private final Map> arguments; private final Command command; + private final Map nodes; - public CommandContext(T source, Map> arguments, Command command) { + public CommandContext(T source, Map> arguments, Command command, Map nodes) { this.source = source; this.arguments = arguments; this.command = command; + this.nodes = nodes; } public Command getCommand() { @@ -47,6 +55,7 @@ public class CommandContext { CommandContext that = (CommandContext) o; if (!arguments.equals(that.arguments)) return false; + if (!Iterables.elementsEqual(nodes.entrySet(), that.nodes.entrySet())) return false; if (command != null ? !command.equals(that.command) : that.command != null) return false; if (!source.equals(that.source)) return false; @@ -58,6 +67,15 @@ public class CommandContext { int result = source.hashCode(); result = 31 * result + arguments.hashCode(); result = 31 * result + (command != null ? command.hashCode() : 0); + result = 31 * result + nodes.hashCode(); return result; } + + public String getInput() { + return JOINER.join(nodes.values()); + } + + public Map getNodes() { + return nodes; + } } diff --git a/src/main/java/com/mojang/brigadier/context/CommandContextBuilder.java b/src/main/java/com/mojang/brigadier/context/CommandContextBuilder.java index 0ca2ab2..7fafada 100644 --- a/src/main/java/com/mojang/brigadier/context/CommandContextBuilder.java +++ b/src/main/java/com/mojang/brigadier/context/CommandContextBuilder.java @@ -2,11 +2,13 @@ package com.mojang.brigadier.context; import com.google.common.collect.Maps; import com.mojang.brigadier.Command; +import com.mojang.brigadier.tree.CommandNode; import java.util.Map; public class CommandContextBuilder { private final Map> arguments = Maps.newHashMap(); + private final Map nodes = Maps.newLinkedHashMap(); private final T source; private Command command; @@ -28,14 +30,20 @@ public class CommandContextBuilder { return this; } + public CommandContextBuilder withNode(CommandNode node, String raw) { + this.nodes.put(node, raw); + return this; + } + public CommandContextBuilder copy() { CommandContextBuilder copy = new CommandContextBuilder(source); copy.command = this.command; copy.arguments.putAll(this.arguments); + copy.nodes.putAll(this.nodes); return copy; } public CommandContext build() { - return new CommandContext(source, arguments, command); + return new CommandContext(source, arguments, command, nodes); } } diff --git a/src/main/java/com/mojang/brigadier/tree/ArgumentCommandNode.java b/src/main/java/com/mojang/brigadier/tree/ArgumentCommandNode.java index 33808ba..540e8bc 100644 --- a/src/main/java/com/mojang/brigadier/tree/ArgumentCommandNode.java +++ b/src/main/java/com/mojang/brigadier/tree/ArgumentCommandNode.java @@ -29,12 +29,18 @@ public class ArgumentCommandNode extends CommandNode { return name; } + @Override + public String getUsageText() { + return "<" + name + ">"; + } + @Override public String parse(String command, CommandContextBuilder contextBuilder) throws CommandException { ParsedArgument parsed = type.parse(command); int start = parsed.getRaw().length(); contextBuilder.withArgument(name, parsed); + contextBuilder.withNode(this, parsed.getRaw()); if (command.length() > start) { return command.substring(start + 1); diff --git a/src/main/java/com/mojang/brigadier/tree/CommandNode.java b/src/main/java/com/mojang/brigadier/tree/CommandNode.java index 54ccc74..1802bb4 100644 --- a/src/main/java/com/mojang/brigadier/tree/CommandNode.java +++ b/src/main/java/com/mojang/brigadier/tree/CommandNode.java @@ -59,5 +59,7 @@ public abstract class CommandNode { protected abstract Object getMergeKey(); + public abstract String getUsageText(); + public abstract String parse(String command, CommandContextBuilder contextBuilder) throws CommandException; } diff --git a/src/main/java/com/mojang/brigadier/tree/LiteralCommandNode.java b/src/main/java/com/mojang/brigadier/tree/LiteralCommandNode.java index fcfe465..1e40c0e 100644 --- a/src/main/java/com/mojang/brigadier/tree/LiteralCommandNode.java +++ b/src/main/java/com/mojang/brigadier/tree/LiteralCommandNode.java @@ -33,6 +33,7 @@ public class LiteralCommandNode extends CommandNode { throw ERROR_INCORRECT_LITERAL.create(literal); } + contextBuilder.withNode(this, literal); int start = expected.length(); return command.substring(start); } @@ -48,6 +49,11 @@ public class LiteralCommandNode extends CommandNode { return super.equals(o); } + @Override + public String getUsageText() { + return literal; + } + @Override public int hashCode() { int result = literal.hashCode(); diff --git a/src/main/java/com/mojang/brigadier/tree/RootCommandNode.java b/src/main/java/com/mojang/brigadier/tree/RootCommandNode.java index 140a674..f692eef 100644 --- a/src/main/java/com/mojang/brigadier/tree/RootCommandNode.java +++ b/src/main/java/com/mojang/brigadier/tree/RootCommandNode.java @@ -13,6 +13,11 @@ public class RootCommandNode extends CommandNode { throw new UnsupportedOperationException("Cannot add a RootCommandNode as a child to any other CommandNode"); } + @Override + public String getUsageText() { + return ""; + } + @Override public String parse(String command, CommandContextBuilder contextBuilder) throws CommandException { return command; diff --git a/src/test/java/com/mojang/brigadier/CommandDispatcherUsagesTest.java b/src/test/java/com/mojang/brigadier/CommandDispatcherUsagesTest.java new file mode 100644 index 0000000..7205581 --- /dev/null +++ b/src/test/java/com/mojang/brigadier/CommandDispatcherUsagesTest.java @@ -0,0 +1,200 @@ +package com.mojang.brigadier; + +import com.mojang.brigadier.exceptions.CommandException; +import com.mojang.brigadier.exceptions.CommandExceptionType; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.util.Collections; + +import static com.mojang.brigadier.arguments.IntegerArgumentType.integer; +import static com.mojang.brigadier.builder.LiteralArgumentBuilder.literal; +import static com.mojang.brigadier.builder.RequiredArgumentBuilder.argument; +import static org.hamcrest.Matchers.hasToString; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +@RunWith(MockitoJUnitRunner.class) +public class CommandDispatcherUsagesTest { + CommandDispatcher subject; + @Mock Object source; + @Mock Command command; + + @Before + public void setUp() throws Exception { + subject = new CommandDispatcher(); + } + + @Test + public void testUnknownCommand() throws Exception { + try { + subject.getUsage("foo", source); + fail(); + } catch (CommandException ex) { + assertThat(ex.getType(), is((CommandExceptionType) CommandDispatcher.ERROR_UNKNOWN_COMMAND)); + assertThat(ex.getData(), is(Collections.emptyMap())); + } + } + + @Test + public void testSubcommandUsage() throws Exception { + subject.register( + literal("base").then( + literal("foo").executes(command) + ).then( + literal("bar").then( + literal("baz").executes(command) + ).then( + literal("qux").then( + literal("not_runnable") + ) + ).then( + literal("quux").then( + literal("corge").executes(command) + ) + ).executes(command) + ).executes(command) + ); + + assertThat(subject.getUsage("base bar", source), hasToString("base bar [baz|quux]")); + } + + @Test + public void testOptionalSingleLiteral() throws Exception { + subject.register( + literal("base").then( + literal("foo").executes(command) + ).executes(command) + ); + + assertThat(subject.getUsage("base", source), hasToString("base [foo]")); + } + + @Test + public void testNoArguments() throws Exception { + subject.register( + literal("base").executes(command) + ); + + assertThat(subject.getUsage("base", source), hasToString("base")); + } + + @Test + public void testRequiredSingleLiteral() throws Exception { + subject.register( + literal("base").then( + literal("foo").executes(command) + ) + ); + + assertThat(subject.getUsage("base", source), hasToString("base foo")); + } + + @Test + public void testOptionalTwoLiterals() throws Exception { + subject.register( + literal("base").then( + literal("foo").executes(command) + ).then( + literal("bar").executes(command) + ).executes(command) + ); + + assertThat(subject.getUsage("base", source), hasToString("base [foo|bar]")); + } + + @Test + public void testRequiredTwoLiterals() throws Exception { + subject.register( + literal("base").then( + literal("foo").executes(command) + ).then( + literal("bar").executes(command) + ) + ); + + assertThat(subject.getUsage("base", source), hasToString("base (foo|bar)")); + } + + @Test + public void testOptionalOneArgument() throws Exception { + subject.register( + literal("base").then( + argument("foo", integer()).executes(command) + ).executes(command) + ); + + assertThat(subject.getUsage("base", source), hasToString("base []")); + } + + @Test + public void testRequiredOneArgument() throws Exception { + subject.register( + literal("base").then( + argument("foo", integer()).executes(command) + ) + ); + + assertThat(subject.getUsage("base", source), hasToString("base ")); + } + + @Test + public void testOptionalTwoArguments() throws Exception { + subject.register( + literal("base").then( + argument("foo", integer()).executes(command) + ).then( + argument("bar", integer()).executes(command) + ).executes(command) + ); + + assertThat(subject.getUsage("base", source), hasToString("base [|]")); + } + + @Test + public void testRequiredTwoArguments() throws Exception { + subject.register( + literal("base").then( + argument("foo", integer()).executes(command) + ).then( + argument("bar", integer()).executes(command) + ) + ); + + assertThat(subject.getUsage("base", source), hasToString("base (|)")); + } + + @Test + public void testOptionalLiteralOrArgument() throws Exception { + subject.register( + literal("base").then( + literal("foo").executes(command) + ).then( + argument("bar", integer()).executes(command) + ).then( + literal("baz").executes(command) + ).executes(command) + ); + + assertThat(subject.getUsage("base", source), hasToString("base [foo|baz|]")); + } + + @Test + public void testRequiredLiteralOrArgument() throws Exception { + subject.register( + literal("base").then( + literal("foo").executes(command) + ).then( + argument("bar", integer()).executes(command) + ).then( + literal("baz").executes(command) + ) + ); + + assertThat(subject.getUsage("base", source), hasToString("base (foo|baz|)")); + } +} \ No newline at end of file diff --git a/src/test/java/com/mojang/brigadier/context/CommandContextTest.java b/src/test/java/com/mojang/brigadier/context/CommandContextTest.java index 892a8b3..2858e59 100644 --- a/src/test/java/com/mojang/brigadier/context/CommandContextTest.java +++ b/src/test/java/com/mojang/brigadier/context/CommandContextTest.java @@ -2,6 +2,7 @@ package com.mojang.brigadier.context; import com.google.common.testing.EqualsTester; import com.mojang.brigadier.Command; +import com.mojang.brigadier.tree.CommandNode; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -9,6 +10,8 @@ import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import static com.mojang.brigadier.arguments.IntegerArgumentType.integer; +import static com.mojang.brigadier.builder.LiteralArgumentBuilder.literal; +import static com.mojang.brigadier.builder.RequiredArgumentBuilder.argument; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.mock; @@ -50,12 +53,23 @@ public class CommandContextTest { Object otherSource = new Object(); Command command = mock(Command.class); Command otherCommand = mock(Command.class); + CommandNode node = mock(CommandNode.class); + CommandNode otherNode = mock(CommandNode.class); new EqualsTester() .addEqualityGroup(new CommandContextBuilder(source).build(), new CommandContextBuilder(source).build()) .addEqualityGroup(new CommandContextBuilder(otherSource).build(), new CommandContextBuilder(otherSource).build()) .addEqualityGroup(new CommandContextBuilder(source).withCommand(command).build(), new CommandContextBuilder(source).withCommand(command).build()) .addEqualityGroup(new CommandContextBuilder(source).withCommand(otherCommand).build(), new CommandContextBuilder(source).withCommand(otherCommand).build()) .addEqualityGroup(new CommandContextBuilder(source).withArgument("foo", integer().parse("123")).build(), new CommandContextBuilder(source).withArgument("foo", integer().parse("123")).build()) + .addEqualityGroup(new CommandContextBuilder(source).withNode(node, "foo").withNode(otherNode, "bar").build(), new CommandContextBuilder(source).withNode(node, "foo").withNode(otherNode, "bar").build()) + .addEqualityGroup(new CommandContextBuilder(source).withNode(otherNode, "bar").withNode(node, "foo").build(), new CommandContextBuilder(source).withNode(otherNode, "bar").withNode(node, "foo").build()) .testEquals(); } + + @Test + public void testGetInput() throws Exception { + CommandContext context = builder.withNode(literal("foo").build(), "foo").withNode(argument("bar", integer()).build(), "100").withNode(literal("baz").build(), "baz").build(); + + assertThat(context.getInput(), is("foo 100 baz")); + } } \ No newline at end of file diff --git a/src/test/java/com/mojang/brigadier/tree/ArgumentCommandNodeTest.java b/src/test/java/com/mojang/brigadier/tree/ArgumentCommandNodeTest.java index cb59f9b..9f4b0ba 100644 --- a/src/test/java/com/mojang/brigadier/tree/ArgumentCommandNodeTest.java +++ b/src/test/java/com/mojang/brigadier/tree/ArgumentCommandNodeTest.java @@ -61,6 +61,11 @@ public class ArgumentCommandNodeTest extends AbstractCommandNodeTest { } } + @Test + public void testUsage() throws Exception { + assertThat(node.getUsageText(), is("")); + } + @Test public void testEquals() throws Exception { Command command = mock(Command.class); diff --git a/src/test/java/com/mojang/brigadier/tree/LiteralCommandNodeTest.java b/src/test/java/com/mojang/brigadier/tree/LiteralCommandNodeTest.java index e90db21..43dcbc2 100644 --- a/src/test/java/com/mojang/brigadier/tree/LiteralCommandNodeTest.java +++ b/src/test/java/com/mojang/brigadier/tree/LiteralCommandNodeTest.java @@ -64,6 +64,11 @@ public class LiteralCommandNodeTest extends AbstractCommandNodeTest { } } + @Test + public void testUsage() throws Exception { + assertThat(node.getUsageText(), is("foo")); + } + @Test public void testEquals() throws Exception { Command command = mock(Command.class); diff --git a/src/test/java/com/mojang/brigadier/tree/RootCommandNodeTest.java b/src/test/java/com/mojang/brigadier/tree/RootCommandNodeTest.java index b5054f9..4b99de1 100644 --- a/src/test/java/com/mojang/brigadier/tree/RootCommandNodeTest.java +++ b/src/test/java/com/mojang/brigadier/tree/RootCommandNodeTest.java @@ -32,6 +32,11 @@ public class RootCommandNodeTest extends AbstractCommandNodeTest { node.addChild(new RootCommandNode()); } + @Test + public void testUsage() throws Exception { + assertThat(node.getUsageText(), is("")); + } + @Test public void testEquals() throws Exception { new EqualsTester()