diff --git a/src/main/java/me/bobert/netneutrality/core/command/CommandContainer.java b/src/main/java/me/bobert/netneutrality/core/command/CommandContainer.java index f44b126..73e9cd7 100644 --- a/src/main/java/me/bobert/netneutrality/core/command/CommandContainer.java +++ b/src/main/java/me/bobert/netneutrality/core/command/CommandContainer.java @@ -5,7 +5,6 @@ package me.bobert.netneutrality.core.command; * https://discord.gg/qUa6zNFZsd */ -import net.dv8tion.jda.api.entities.channel.ChannelType; import net.dv8tion.jda.api.interactions.commands.build.OptionData; import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; @@ -13,27 +12,49 @@ import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData; import net.dv8tion.jda.internal.interactions.CommandDataImpl; import java.util.List; -import java.util.Set; -import java.util.StringJoiner; public class CommandContainer { public static final CommandContainer NULL_COMMAND = new CommandContainer(null); + private static final CommandUsageFormatter USAGE_FORMATTER = new CommandUsageFormatter(); private final Command command; - private CommandDataImpl commandData; - private List options; - private List subcommands; - private List subcommandGroups; + private final CommandDataImpl commandData; + private final List options; + private final List subcommands; + private final List subcommandGroups; public CommandContainer(Command command) { - this.command = command; + this(command, build(command)); + } - if (command != null) { - CommandOptionsBuilder commandOptionsBuilder = new CommandOptionsBuilder(command.getType(), command); - command.registerCommandOptions(commandOptionsBuilder); - initContainer(commandOptionsBuilder); + private CommandContainer(Command command, CommandDataImpl commandData) { + this.command = command; + this.commandData = commandData; + + if (commandData != null) { + this.options = commandData.getOptions(); + this.subcommands = commandData.getSubcommands(); + this.subcommandGroups = commandData.getSubcommandGroups(); + } else { + this.options = List.of(); + this.subcommands = List.of(); + this.subcommandGroups = List.of(); } } + public static CommandContainer of(Command command) { + return new CommandContainer(command); + } + + private static CommandDataImpl build(Command command) { + if (command == null) { + return null; + } + + CommandOptionsBuilder builder = new CommandOptionsBuilder(command.getType(), command); + command.registerCommandOptions(builder); + return builder.build(); + } + public Command getCommand() { return command; } @@ -54,129 +75,11 @@ public class CommandContainer { return subcommandGroups; } - private void initContainer(CommandOptionsBuilder builder) { - this.commandData = builder.build(); - this.options = commandData.getOptions(); - this.subcommands = commandData.getSubcommands(); - this.subcommandGroups = commandData.getSubcommandGroups(); - } - public boolean isEmpty() { return options.isEmpty() && subcommands.isEmpty() && subcommandGroups.isEmpty(); } public String printUsage() { - StringBuilder usage = new StringBuilder(); - if (!options.isEmpty()) { - usage.append("- Usage: /").append(command.getCommandName()); - for (OptionData option : options) { - usage.append(" <").append(option.getName()).append(">"); - } - - usage.append(printOptions(options, null, false)); - } - - if (!subcommands.isEmpty()) { - for (SubcommandData subcommand : subcommands) { - usage.append("- Usage: /").append(command.getCommandName()).append(" ").append(subcommand.getName()); - - for (OptionData option : subcommand.getOptions()) { - usage.append(" <").append(option.getName()).append(">"); - } - - usage.append(" (").append(subcommand.getDescription()).append(")"); - usage.append(printOptions(subcommand.getOptions(), null, false)); - - usage.append("\n"); - } - } - - if (!subcommandGroups.isEmpty()) { - for (SubcommandGroupData subcommandGroup : subcommandGroups) { - usage.append("- Subcommand: /").append(command.getCommandName()) - .append(" ").append(subcommandGroup.getName()) - .append(" (").append(subcommandGroup.getDescription()).append(")") - .append("\n"); - - for (SubcommandData subcommand : subcommandGroup.getSubcommands()) { - usage.append(" - Usage: /").append(command.getCommandName()) - .append(" ").append(subcommandGroup.getName()) - .append(" ").append(subcommand.getName()); - - for (OptionData option : subcommand.getOptions()) { - usage.append(" <").append(option.getName()).append(">"); - } - - usage.append(printOptions(subcommand.getOptions(), subcommand.getDescription(), true)); - - usage.append("\n"); - } - } - } - - if (!isEmpty()) { - usage.append("\n\n-# *: required argument"); - } - - return usage.toString(); - } - - private String printOptions(List options, String description, boolean group) { - StringBuilder usage = new StringBuilder(); - - if (description != null) { - usage.append(" (").append(description).append(")"); - } - - for (OptionData option : options) { - usage.append("\n"); - if (group) { - usage.append(" "); - } - - usage.append(" - "); - if (option.isRequired()) { - usage.append("*"); - } - usage.append(option.getName()).append(": ").append(option.getDescription()); - - if (option.getMinValue() != null) { - usage.append(" [Min value: ").append(option.getMinValue().longValue()).append("]"); - } - - if (option.getMaxValue() != null) { - usage.append(" [Max value: ").append(option.getMaxValue().longValue()).append("]"); - } - - if (option.getMinLength() != null) { - usage.append(" [Min length: ").append(option.getMinLength().longValue()).append("]"); - } - - if (option.getMaxLength() != null) { - usage.append(" [Max length: ").append(option.getMaxLength().longValue()).append("]"); - } - - List choices = option.getChoices(); - if (!choices.isEmpty()) { - StringJoiner joiner = new StringJoiner(", "); - for (net.dv8tion.jda.api.interactions.commands.Command.Choice choice : choices) { - joiner.add(choice.getAsString()); - } - - usage.append(" [Available choices: ").append(joiner).append("]"); - } - - Set requiredChannelTypes = option.getChannelTypes(); - if (!requiredChannelTypes.isEmpty()) { - StringJoiner joiner = new StringJoiner(", "); - for (ChannelType type : requiredChannelTypes) { - joiner.add(type.name()); - } - - usage.append(" [Required channel types: ").append(joiner).append("]"); - } - } - - return usage.toString(); + return USAGE_FORMATTER.format(this); } } diff --git a/src/main/java/me/bobert/netneutrality/core/command/CommandManager.java b/src/main/java/me/bobert/netneutrality/core/command/CommandManager.java index d6010b2..29bf604 100644 --- a/src/main/java/me/bobert/netneutrality/core/command/CommandManager.java +++ b/src/main/java/me/bobert/netneutrality/core/command/CommandManager.java @@ -57,7 +57,7 @@ public class CommandManager implements CommandRegistry { public void init() { logger.info("Loading commands.."); - initCommands(); + ReflectionHelper.loadCommands(Command.class, context, this::onCommandLoaded); logger.info("Loaded {} commands!", commands.size()); } @@ -84,12 +84,6 @@ public class CommandManager implements CommandRegistry { return commands; } - public void initCommands() { - Consumer onLoaded = this::onCommandLoaded; - - ReflectionHelper.loadCommands(Command.class, context, onLoaded); - } - void onCommandLoaded(Command command) { if (!command.isEnabled()) { logger.info("Skipping command {} as it is disabled", command.getClass().getName()); diff --git a/src/main/java/me/bobert/netneutrality/core/command/CommandUsageFormatter.java b/src/main/java/me/bobert/netneutrality/core/command/CommandUsageFormatter.java new file mode 100644 index 0000000..563cfea --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/command/CommandUsageFormatter.java @@ -0,0 +1,151 @@ +package me.bobert.netneutrality.core.command; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; +import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; +import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData; + +import java.util.List; +import java.util.Set; +import java.util.StringJoiner; + +final class CommandUsageFormatter { + private static final String REQUIRED_LEGEND = "\n\n-# *: required argument"; + + String format(CommandContainer container) { + if (container.isEmpty()) { + return ""; + } + + String commandName = container.getCommand().getCommandName(); + StringBuilder usage = new StringBuilder(); + appendOptions(usage, commandName, container.getOptions()); + appendSubcommands(usage, commandName, container.getSubcommands()); + appendGroups(usage, commandName, container.getSubcommandGroups()); + usage.append(REQUIRED_LEGEND); + + return usage.toString(); + } + + private void appendOptions(StringBuilder usage, String commandName, List options) { + if (options.isEmpty()) { + return; + } + + usage.append("- Usage: /").append(commandName); + appendArgs(usage, options); + usage.append(describeOptions(options, null, false)); + } + + private void appendSubcommands(StringBuilder usage, String commandName, List subcommands) { + for (SubcommandData subcommand : subcommands) { + usage.append("- Usage: /").append(commandName).append(" ").append(subcommand.getName()); + appendArgs(usage, subcommand.getOptions()); + usage.append(" (").append(subcommand.getDescription()).append(")"); + usage.append(describeOptions(subcommand.getOptions(), null, false)); + usage.append("\n"); + } + } + + private void appendGroups(StringBuilder usage, String commandName, List groups) { + for (SubcommandGroupData group : groups) { + usage.append("- Subcommand: /").append(commandName) + .append(" ").append(group.getName()) + .append(" (").append(group.getDescription()).append(")") + .append("\n"); + + for (SubcommandData subcommand : group.getSubcommands()) { + usage.append(" - Usage: /").append(commandName) + .append(" ").append(group.getName()) + .append(" ").append(subcommand.getName()); + appendArgs(usage, subcommand.getOptions()); + usage.append(describeOptions(subcommand.getOptions(), subcommand.getDescription(), true)); + usage.append("\n"); + } + } + } + + private void appendArgs(StringBuilder usage, List options) { + for (OptionData option : options) { + usage.append(" <").append(option.getName()).append(">"); + } + } + + private String describeOptions(List options, String description, boolean group) { + StringBuilder usage = new StringBuilder(); + + if (description != null) { + usage.append(" (").append(description).append(")"); + } + + for (OptionData option : options) { + usage.append("\n"); + if (group) { + usage.append(" "); + } + + usage.append(" - "); + if (option.isRequired()) { + usage.append("*"); + } + usage.append(option.getName()).append(": ").append(option.getDescription()); + + appendConstraints(usage, option); + appendChoices(usage, option); + appendChannelTypes(usage, option); + } + + return usage.toString(); + } + + private void appendConstraints(StringBuilder usage, OptionData option) { + if (option.getMinValue() != null) { + usage.append(" [Min value: ").append(option.getMinValue().longValue()).append("]"); + } + + if (option.getMaxValue() != null) { + usage.append(" [Max value: ").append(option.getMaxValue().longValue()).append("]"); + } + + if (option.getMinLength() != null) { + usage.append(" [Min length: ").append(option.getMinLength().longValue()).append("]"); + } + + if (option.getMaxLength() != null) { + usage.append(" [Max length: ").append(option.getMaxLength().longValue()).append("]"); + } + } + + private void appendChoices(StringBuilder usage, OptionData option) { + List choices = option.getChoices(); + if (choices.isEmpty()) { + return; + } + + StringJoiner joiner = new StringJoiner(", "); + for (net.dv8tion.jda.api.interactions.commands.Command.Choice choice : choices) { + joiner.add(choice.getAsString()); + } + + usage.append(" [Available choices: ").append(joiner).append("]"); + } + + private void appendChannelTypes(StringBuilder usage, OptionData option) { + Set types = option.getChannelTypes(); + if (types.isEmpty()) { + return; + } + + StringJoiner joiner = new StringJoiner(", "); + for (ChannelType type : types) { + joiner.add(type.name()); + } + + usage.append(" [Required channel types: ").append(joiner).append("]"); + } +} diff --git a/src/test/java/me/bobert/netneutrality/core/command/CommandContainerTest.java b/src/test/java/me/bobert/netneutrality/core/command/CommandContainerTest.java index 7ed2da1..4556ff1 100644 --- a/src/test/java/me/bobert/netneutrality/core/command/CommandContainerTest.java +++ b/src/test/java/me/bobert/netneutrality/core/command/CommandContainerTest.java @@ -60,6 +60,26 @@ class CommandContainerTest { assertNull(CommandContainer.NULL_COMMAND.getCommand()); } + @Test + void nullCommandShouldBeEmptyAndPrintNothing() { + assertTrue(CommandContainer.NULL_COMMAND.isEmpty()); + assertEquals("", CommandContainer.NULL_COMMAND.printUsage()); + } + + @Test + void ofShouldBuildTheSameAsTheConstructor() { + doAnswer(invocation -> { + ((CommandOptionsBuilder) invocation.getArgument(0)) + .addOptions(new OptionData(OptionType.STRING, "text", "Text")); + return null; + }).when(command).registerCommandOptions(org.mockito.ArgumentMatchers.any()); + + CommandContainer container = CommandContainer.of(command); + + assertSame(command, container.getCommand()); + assertEquals(1, container.getOptions().size()); + } + @Test void shouldExposeTheCommand() { assertSame(command, container(_ -> { }).getCommand()); @@ -192,6 +212,41 @@ class CommandContainerTest { assertTrue(usage.endsWith("-# *: required argument"), usage); } + @Test + void shouldPrintExactSubcommandUsage() { + CommandContainer container = container(builder -> builder.addSubcommands( + new SubcommandData("add", "Adds").addOptions(new OptionData(OptionType.STRING, "name", "Name", true)))); + + assertEquals("- Usage: /test add (Adds)\n - *name: Name\n\n\n-# *: required argument", + container.printUsage()); + } + + @Test + void shouldPrintExactSubcommandGroupUsage() { + CommandContainer container = container(builder -> builder.addSubcommandGroups( + new SubcommandGroupData("grp", "Group").addSubcommands( + new SubcommandData("add", "Adds").addOptions(new OptionData(OptionType.STRING, "name", "Name", true))))); + + assertEquals("- Subcommand: /test grp (Group)\n" + + " - Usage: /test grp add (Adds)\n" + + " - *name: Name\n\n\n-# *: required argument", container.printUsage()); + } + + @Test + void shouldPrintExactAnnotations() { + CommandContainer container = container(builder -> builder.addOptions( + new OptionData(OptionType.STRING, "mode", "Mode").setMinLength(1).setMaxLength(5) + .addChoice("One", "one").addChoice("Two", "two"), + new OptionData(OptionType.INTEGER, "n", "N").setMinValue(1).setMaxValue(9), + new OptionData(OptionType.CHANNEL, "ch", "Ch").setChannelTypes(ChannelType.TEXT))); + + assertEquals("- Usage: /test \n" + + " - mode: Mode [Min length: 1] [Max length: 5] [Available choices: one, two]\n" + + " - n: N [Min value: 1] [Max value: 9]\n" + + " - ch: Ch [Required channel types: TEXT]\n\n" + + "-# *: required argument", container.printUsage()); + } + @Test void shouldNotPrintTheRequiredLegendForUsagelessCommands() { assertFalse(container(_ -> { }).printUsage().contains("required argument")); diff --git a/src/test/java/me/bobert/netneutrality/core/command/CommandUsageFormatterTest.java b/src/test/java/me/bobert/netneutrality/core/command/CommandUsageFormatterTest.java new file mode 100644 index 0000000..97efe6c --- /dev/null +++ b/src/test/java/me/bobert/netneutrality/core/command/CommandUsageFormatterTest.java @@ -0,0 +1,47 @@ +package me.bobert.netneutrality.core.command; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class CommandUsageFormatterTest { + + private final CommandUsageFormatter formatter = new CommandUsageFormatter(); + + private Command command() { + Command command = mock(Command.class); + when(command.getCommandName()).thenReturn("test"); + when(command.getDescription()).thenReturn("A test command"); + when(command.getType()).thenReturn(Command.Type.CHAT); + return command; + } + + @Test + void shouldFormatEmptyContainerAsEmptyString() { + assertEquals("", formatter.format(new CommandContainer(command()))); + } + + @Test + void shouldFormatOptionsWithRequiredMarkerAndLegend() { + Command command = command(); + doAnswer(invocation -> { + CommandOptionsBuilder builder = invocation.getArgument(0); + builder.addOptions(new OptionData(OptionType.STRING, "text", "Text", true)); + return null; + }).when(command).registerCommandOptions(org.mockito.ArgumentMatchers.any()); + + assertEquals("- Usage: /test \n - *text: Text\n\n-# *: required argument", + formatter.format(new CommandContainer(command))); + } +}