**Refactor command usage printing into CommandUsageFormatter**
Extracted command usage formatting logic from `CommandContainer` into a dedicated `CommandUsageFormatter` class, improving separation of concerns and maintainability. Updated `CommandManager` initialization and added comprehensive unit tests to validate the formatter's functionality.
This commit is contained in:
1 parent
f96c2677b6
commit
7443cbfb4b
5 files changed
+289
-139
No files matched your search
@@ -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,25 +12,47 @@ 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<OptionData> options;
|
||||
private List<SubcommandData> subcommands;
|
||||
private List<SubcommandGroupData> subcommandGroups;
|
||||
private final CommandDataImpl commandData;
|
||||
private final List<OptionData> options;
|
||||
private final List<SubcommandData> subcommands;
|
||||
private final List<SubcommandGroupData> subcommandGroups;
|
||||
|
||||
public CommandContainer(Command command) {
|
||||
this.command = command;
|
||||
|
||||
if (command != null) {
|
||||
CommandOptionsBuilder commandOptionsBuilder = new CommandOptionsBuilder(command.getType(), command);
|
||||
command.registerCommandOptions(commandOptionsBuilder);
|
||||
initContainer(commandOptionsBuilder);
|
||||
this(command, build(command));
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -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<OptionData> 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<net.dv8tion.jda.api.interactions.commands.Command.Choice> 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<ChannelType> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<Command> 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());
|
||||
|
||||
@@ -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<OptionData> 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<SubcommandData> 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<SubcommandGroupData> 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<OptionData> options) {
|
||||
for (OptionData option : options) {
|
||||
usage.append(" <").append(option.getName()).append(">");
|
||||
}
|
||||
}
|
||||
|
||||
private String describeOptions(List<OptionData> 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<net.dv8tion.jda.api.interactions.commands.Command.Choice> 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<ChannelType> 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("]");
|
||||
}
|
||||
}
|
||||
@@ -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 <name> (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 <name> (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 <mode> <n> <ch>\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"));
|
||||
|
||||
@@ -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 <text>\n - *text: Text\n\n-# *: required argument",
|
||||
formatter.format(new CommandContainer(command)));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user