feat(core): support root command deletion & standardize capabilities (#369)
This commit is contained in:
parent
08a97b2c4f
commit
28ff5d3003
14 changed files with 416 additions and 16 deletions
|
|
@ -0,0 +1,85 @@
|
|||
//
|
||||
// MIT License
|
||||
//
|
||||
// Copyright (c) 2021 Alexander Söderberg & Contributors
|
||||
//
|
||||
// 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 cloud.commandframework;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
/**
|
||||
* Represents a capability that a cloud implementation may have.
|
||||
*
|
||||
* @since 1.7.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.7.0")
|
||||
public interface CloudCapability {
|
||||
|
||||
/**
|
||||
* Returns the friendly name of this capability.
|
||||
*
|
||||
* @return the name of the capability
|
||||
*/
|
||||
@Override @NonNull String toString();
|
||||
|
||||
|
||||
/**
|
||||
* Standard {@link CloudCapability capabilities}.
|
||||
*
|
||||
* @since 1.7.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.7.0")
|
||||
enum StandardCapabilities implements CloudCapability {
|
||||
/**
|
||||
* The capability to delete root commands using {@link CommandManager#deleteRootCommand(String)}.
|
||||
*/
|
||||
ROOT_COMMAND_DELETION;
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return name();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Exception thrown when a {@link CloudCapability} is missing, when a method that requires the presence of that
|
||||
* capability is invoked.
|
||||
*
|
||||
* @since 1.7.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.7.0")
|
||||
@SuppressWarnings("serial")
|
||||
final class CloudCapabilityMissingException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 8961652857372971486L;
|
||||
|
||||
/**
|
||||
* Create a new cloud capability missing exception instance.
|
||||
*
|
||||
* @param capability the missing capability
|
||||
*/
|
||||
public CloudCapabilityMissingException(final @NonNull CloudCapability capability) {
|
||||
super(String.format("Missing capability '%s'", capability));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ import cloud.commandframework.arguments.CommandSuggestionEngine;
|
|||
import cloud.commandframework.arguments.CommandSyntaxFormatter;
|
||||
import cloud.commandframework.arguments.DelegatingCommandSuggestionEngineFactory;
|
||||
import cloud.commandframework.arguments.StandardCommandSyntaxFormatter;
|
||||
import cloud.commandframework.arguments.StaticArgument;
|
||||
import cloud.commandframework.arguments.flags.CommandFlag;
|
||||
import cloud.commandframework.arguments.parser.ArgumentParser;
|
||||
import cloud.commandframework.arguments.parser.ParserParameter;
|
||||
|
|
@ -66,15 +67,18 @@ import java.util.Collection;
|
|||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apiguardian.api.API;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
|
@ -100,6 +104,7 @@ public abstract class CommandManager<C> {
|
|||
private final CommandExecutionCoordinator<C> commandExecutionCoordinator;
|
||||
private final CommandTree<C> commandTree;
|
||||
private final CommandSuggestionEngine<C> commandSuggestionEngine;
|
||||
private final Set<CloudCapability> capabilities = new HashSet<>();
|
||||
|
||||
private CaptionVariableReplacementHandler captionVariableReplacementHandler = new SimpleCaptionVariableReplacementHandler();
|
||||
private CommandSyntaxFormatter<C> commandSyntaxFormatter = new StandardCommandSyntaxFormatter<>();
|
||||
|
|
@ -297,6 +302,40 @@ public abstract class CommandManager<C> {
|
|||
return this.commandRegistrationHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the given {@code capability}.
|
||||
*
|
||||
* @param capability the capability
|
||||
* @since 1.7.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.7.0")
|
||||
protected final void registerCapability(final @NonNull CloudCapability capability) {
|
||||
this.capabilities.add(capability);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the cloud implementation has the given {@code capability}.
|
||||
*
|
||||
* @param capability the capability
|
||||
* @return {@code true} if the implementation has the {@code capability}, {@code false} if not
|
||||
* @since 1.7.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.7.0")
|
||||
public boolean hasCapability(final @NonNull CloudCapability capability) {
|
||||
return this.capabilities.contains(capability);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an unmodifiable snapshot of the currently registered {@link CloudCapability capabilities}.
|
||||
*
|
||||
* @return the currently registered capabilities
|
||||
* @since 1.7.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.7.0")
|
||||
public @NonNull Collection<@NonNull CloudCapability> capabilities() {
|
||||
return Collections.unmodifiableSet(new HashSet<>(this.capabilities));
|
||||
}
|
||||
|
||||
protected final void setCommandRegistrationHandler(final @NonNull CommandRegistrationHandler commandRegistrationHandler) {
|
||||
this.requireState(RegistrationState.BEFORE_REGISTRATION);
|
||||
this.commandRegistrationHandler = commandRegistrationHandler;
|
||||
|
|
@ -382,6 +421,55 @@ public abstract class CommandManager<C> {
|
|||
*/
|
||||
public abstract boolean hasPermission(@NonNull C sender, @NonNull String permission);
|
||||
|
||||
/**
|
||||
* Deletes the given {@code rootCommand}.
|
||||
* <p>
|
||||
* This will delete all chains that originate at the root command.
|
||||
*
|
||||
* @param rootCommand The root command to delete
|
||||
* @throws CloudCapability.CloudCapabilityMissingException If {@link CloudCapability.StandardCapabilities#ROOT_COMMAND_DELETION} is missing
|
||||
* @since 1.7.0
|
||||
*/
|
||||
@API(status = API.Status.EXPERIMENTAL, since = "1.7.0")
|
||||
public void deleteRootCommand(final @NonNull String rootCommand) throws CloudCapability.CloudCapabilityMissingException {
|
||||
if (!this.hasCapability(CloudCapability.StandardCapabilities.ROOT_COMMAND_DELETION)) {
|
||||
throw new CloudCapability.CloudCapabilityMissingException(CloudCapability.StandardCapabilities.ROOT_COMMAND_DELETION);
|
||||
}
|
||||
|
||||
// Mark the command for deletion.
|
||||
final CommandTree.Node<@Nullable CommandArgument<C, ?>> node = this.commandTree.getNamedNode(rootCommand);
|
||||
if (node == null) {
|
||||
throw new IllegalArgumentException(String.format("No root command named '%s' exists", rootCommand));
|
||||
}
|
||||
|
||||
// The registration handler gets to act before we destruct the command.
|
||||
this.commandRegistrationHandler.unregisterRootCommand((StaticArgument<?>) node.getValue());
|
||||
|
||||
// We then delete it from the tree.
|
||||
this.commandTree.deleteRecursively(node);
|
||||
|
||||
// And lastly we re-build the entire tree.
|
||||
this.commandTree.verifyAndRegister();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all root command names.
|
||||
*
|
||||
* @return Root command names.
|
||||
* @since 1.7.0
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@API(status = API.Status.STABLE, since = "1.7.0")
|
||||
public @NonNull Collection<@NonNull String> rootCommands() {
|
||||
return this.commandTree.getRootNodes()
|
||||
.stream()
|
||||
.map(CommandTree.Node::getValue)
|
||||
.filter(arg -> arg instanceof StaticArgument)
|
||||
.map(arg -> (StaticArgument<C>) arg)
|
||||
.map(StaticArgument::getName)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new command builder. This will also register the creating manager in the command
|
||||
* builder using {@link Command.Builder#manager(CommandManager)}, so that the command
|
||||
|
|
|
|||
|
|
@ -953,6 +953,29 @@ public final class CommandTree<C> {
|
|||
return null;
|
||||
}
|
||||
|
||||
void deleteRecursively(final @NonNull Node<@Nullable CommandArgument<C, ?>> node) {
|
||||
for (final Node<@Nullable CommandArgument<C, ?>> child : new ArrayList<>(node.children)) {
|
||||
this.deleteRecursively(child);
|
||||
}
|
||||
|
||||
// We need to remove it from the tree.
|
||||
this.removeNode(node);
|
||||
}
|
||||
|
||||
private boolean removeNode(final @NonNull Node<@Nullable CommandArgument<C, ?>> node) {
|
||||
if (node.isLeaf()) {
|
||||
if (this.getRootNodes().contains(node)) {
|
||||
this.internalTree.removeChild(node);
|
||||
} else {
|
||||
return node.getParent().removeChild(node);
|
||||
}
|
||||
} else {
|
||||
throw new IllegalStateException(String.format("Cannot delete intermediate node '%s'", node));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the command manager
|
||||
*
|
||||
|
|
@ -971,7 +994,7 @@ public final class CommandTree<C> {
|
|||
|
||||
private final Map<String, Object> nodeMeta = new HashMap<>();
|
||||
private final List<Node<T>> children = new LinkedList<>();
|
||||
private final T value;
|
||||
private T value;
|
||||
private Node<T> parent;
|
||||
|
||||
private Node(final @Nullable T value) {
|
||||
|
|
@ -1002,6 +1025,10 @@ public final class CommandTree<C> {
|
|||
return null;
|
||||
}
|
||||
|
||||
private boolean removeChild(final @NonNull Node<T> child) {
|
||||
return this.children.remove(child);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the node is a leaf node
|
||||
*
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
package cloud.commandframework.internal;
|
||||
|
||||
import cloud.commandframework.Command;
|
||||
import cloud.commandframework.arguments.StaticArgument;
|
||||
import org.apiguardian.api.API;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
|
|
@ -54,6 +55,15 @@ public interface CommandRegistrationHandler {
|
|||
*/
|
||||
boolean registerCommand(@NonNull Command<?> command);
|
||||
|
||||
/**
|
||||
* Requests that the given {@code rootCommand} should be unregistered.
|
||||
*
|
||||
* @param rootCommand The command to delete
|
||||
* @since 1.7.0
|
||||
*/
|
||||
default void unregisterRootCommand(final @NonNull StaticArgument<?> rootCommand) {
|
||||
}
|
||||
|
||||
|
||||
@API(status = API.Status.INTERNAL, consumers = "cloud.commandframework.*")
|
||||
final class NullCommandRegistrationHandler implements CommandRegistrationHandler {
|
||||
|
|
@ -66,6 +76,9 @@ public interface CommandRegistrationHandler {
|
|||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterRootCommand(final @NonNull StaticArgument<?> rootCommand) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,149 @@
|
|||
//
|
||||
// MIT License
|
||||
//
|
||||
// Copyright (c) 2021 Alexander Söderberg & Contributors
|
||||
//
|
||||
// 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 cloud.commandframework;
|
||||
|
||||
import cloud.commandframework.arguments.standard.StringArgument;
|
||||
import cloud.commandframework.exceptions.NoSuchCommandException;
|
||||
import cloud.commandframework.execution.CommandExecutionCoordinator;
|
||||
import cloud.commandframework.execution.CommandExecutionHandler;
|
||||
import cloud.commandframework.internal.CommandRegistrationHandler;
|
||||
import cloud.commandframework.meta.CommandMeta;
|
||||
import cloud.commandframework.meta.SimpleCommandMeta;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
|
||||
class CommandDeletionTest {
|
||||
|
||||
private CommandManager<TestCommandSender> commandManager;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
this.commandManager = new CommandManager<TestCommandSender>(
|
||||
CommandExecutionCoordinator.simpleCoordinator(),
|
||||
CommandRegistrationHandler.nullCommandRegistrationHandler()
|
||||
) {
|
||||
{
|
||||
this.registerCapability(CloudCapability.StandardCapabilities.ROOT_COMMAND_DELETION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(
|
||||
final @NonNull TestCommandSender sender,
|
||||
final @NonNull String permission
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull CommandMeta createDefaultCommandMeta() {
|
||||
return SimpleCommandMeta.empty();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteSimpleCommand() {
|
||||
// Arrange
|
||||
this.commandManager.command(this.commandManager.commandBuilder("test").build());
|
||||
// Pre-assert.
|
||||
this.commandManager.executeCommand(new TestCommandSender(), "test").join();
|
||||
|
||||
// Act
|
||||
this.commandManager.deleteRootCommand("test");
|
||||
|
||||
// Assert
|
||||
final CompletionException completionException = assertThrows(
|
||||
CompletionException.class,
|
||||
() -> this.commandManager.executeCommand(new TestCommandSender(), "test").join()
|
||||
);
|
||||
assertThat(completionException).hasCauseThat().isInstanceOf(NoSuchCommandException.class);
|
||||
|
||||
assertThat(this.commandManager.suggest(new TestCommandSender(), "")).isEmpty();
|
||||
assertThat(this.commandManager.getCommandTree().getRootNodes()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void deleteIntermediateCommand() {
|
||||
// Arrange
|
||||
final CommandExecutionHandler<TestCommandSender> handler1 = mock(CommandExecutionHandler.class);
|
||||
final Command<TestCommandSender> command1 = this.commandManager
|
||||
.commandBuilder("test")
|
||||
.handler(handler1)
|
||||
.build();
|
||||
this.commandManager.command(command1);
|
||||
|
||||
final CommandExecutionHandler<TestCommandSender> handler2 = mock(CommandExecutionHandler.class);
|
||||
final Command<TestCommandSender> command2 = this.commandManager
|
||||
.commandBuilder("test")
|
||||
.literal("literal")
|
||||
.handler(handler2)
|
||||
.build();
|
||||
this.commandManager.command(command2);
|
||||
|
||||
final CommandExecutionHandler<TestCommandSender> handler3 = mock(CommandExecutionHandler.class);
|
||||
final Command<TestCommandSender> command3 = this.commandManager
|
||||
.commandBuilder("test")
|
||||
.literal("literal")
|
||||
.argument(StringArgument.of("string"))
|
||||
.handler(handler3)
|
||||
.build();
|
||||
this.commandManager.command(command3);
|
||||
|
||||
// Act
|
||||
this.commandManager.deleteRootCommand("test");
|
||||
|
||||
// Assert
|
||||
final CompletionException completionException = assertThrows(
|
||||
CompletionException.class,
|
||||
() -> this.commandManager.executeCommand(new TestCommandSender(), "test").join()
|
||||
);
|
||||
assertThat(completionException).hasCauseThat().isInstanceOf(NoSuchCommandException.class);
|
||||
|
||||
final CompletionException completionException2 = assertThrows(
|
||||
CompletionException.class,
|
||||
() -> this.commandManager.executeCommand(new TestCommandSender(), "test literal").join()
|
||||
);
|
||||
assertThat(completionException2).hasCauseThat().isInstanceOf(NoSuchCommandException.class);
|
||||
|
||||
final CompletionException completionException3 = assertThrows(
|
||||
CompletionException.class,
|
||||
() -> this.commandManager.executeCommand(new TestCommandSender(), "test literal hm").join()
|
||||
);
|
||||
assertThat(completionException3).hasCauseThat().isInstanceOf(NoSuchCommandException.class);
|
||||
|
||||
verifyNoMoreInteractions(handler1);
|
||||
verifyNoMoreInteractions(handler2);
|
||||
verifyNoMoreInteractions(handler3);
|
||||
|
||||
assertThat(this.commandManager.getCommandTree().getRootNodes()).isEmpty();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue