refactor ig
This commit is contained in:
parent
ef32f9dbc4
commit
336ac4cad5
54 changed files with 528 additions and 444 deletions
16
core/BUILD
Normal file
16
core/BUILD
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
load("@rules_jvm_external//:defs.bzl", "artifact", "java_export")
|
||||
|
||||
java_export(
|
||||
name = "core",
|
||||
maven_coordinates = "de.kentoj.scrow:kencommandapi-core:0.34-SNAPSHOT",
|
||||
srcs = glob(["src/main/**/*.java"]),
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
artifact("com.leakyabstractions:result-api"),
|
||||
artifact("com.leakyabstractions:result"),
|
||||
artifact("org.jetbrains:annotations"),
|
||||
artifact("com.google.guava:guava"),
|
||||
artifact("org.slf4j:slf4j-api"),
|
||||
artifact("net.kyori:adventure-api"),
|
||||
],
|
||||
)
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package de.kentoj.kencommandapi.api;
|
||||
|
||||
import de.kentoj.kencommandapi.api.literal.RootLiteral;
|
||||
import de.kentoj.kencommandapi.api.platform.HasPermissionMethod;
|
||||
import de.kentoj.kencommandapi.api.platform.SendMessageMethod;
|
||||
import de.kentoj.kencommandapi.internal.parser.CommandParseHelper;
|
||||
import de.kentoj.kencommandapi.internal.parser.InvocationCommandParser;
|
||||
import de.kentoj.kencommandapi.internal.parser.SuggestionCommandParser;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class CommandHandler<T> {
|
||||
|
||||
private final SendMessageMethod<T> sendMessageMethod;
|
||||
private final InvocationCommandParser<T> invocationParser;
|
||||
private final SuggestionCommandParser<T> suggestionParser;
|
||||
|
||||
public CommandHandler(SendMessageMethod<T> sendMessageMethod, HasPermissionMethod<T> hasPermissionMethod) {
|
||||
this.sendMessageMethod = sendMessageMethod;
|
||||
var parseHelper = new CommandParseHelper<>(hasPermissionMethod);
|
||||
invocationParser = new InvocationCommandParser<>(parseHelper);
|
||||
suggestionParser = new SuggestionCommandParser<>(parseHelper);
|
||||
}
|
||||
|
||||
public void invoke(RootLiteral<T> rootLiteral, T sender, Stream<String> args) {
|
||||
invocationParser.parse(rootLiteral, sender, args.toList().iterator())
|
||||
.ifSuccessOrElse(data -> exec(rootLiteral, data),
|
||||
err -> sendMessageMethod.send(sender, rootLiteral.messageStyle().err(err)));
|
||||
}
|
||||
|
||||
private void exec(RootLiteral<T> rootLiteral, InvocationCommandParser<T>.ExecutionData data) {
|
||||
var ctx = data.context();
|
||||
data.executor().execute(ctx)
|
||||
.whenComplete((result, ex) -> {
|
||||
if (ex != null)
|
||||
sendMessageMethod.send(ctx.sender(), rootLiteral.messageStyle().exception(ex));
|
||||
result.ifFailure(err ->
|
||||
sendMessageMethod.send(ctx.sender(), rootLiteral.messageStyle().err(err)));
|
||||
});
|
||||
}
|
||||
|
||||
public List<String> getSuggestions(RootLiteral<T> rootNode, T sender, String[] args, boolean trailingSpace) {
|
||||
return suggestionParser.suggest(rootNode, sender, Arrays.stream(args).iterator(), trailingSpace);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package de.kentoj.kencommandapi.api.argument;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* @param <V> type of the parsed argument value
|
||||
*/
|
||||
public record ArgumentRequirement<V>(
|
||||
Predicate<V> predicate,
|
||||
String message
|
||||
) {
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package de.kentoj.kencommandapi.api.argument;
|
||||
|
||||
import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public interface ArgumentType<T, V> {
|
||||
|
||||
/**
|
||||
* parses the given input and returns it.
|
||||
* Unlike {@link CommandArgumentSpec#parseInput(String)}, this does not check any additional requirements.
|
||||
*
|
||||
* @throws IllegalArgumentException if string invalid
|
||||
* @see CommandArgumentSpec#parseInput(String)
|
||||
* @see CommandArgumentSpecBuilder#withRequirement(Predicate, String)
|
||||
*/
|
||||
V parseInputUnchecked(String input) throws IllegalArgumentException;
|
||||
|
||||
/**
|
||||
* @see CommandArgumentSpec#suggestionProvider()
|
||||
*/
|
||||
SuggestionProvider<T> getDefaultSuggestionProvider();
|
||||
|
||||
/**
|
||||
* @return whether this argument is greedy. Greedy arguments consume all remaining input instead of a single word.
|
||||
* For example, given the 2 words "hello" "world" as input, a greedy argument parses "hello world" as a single value.
|
||||
* Greedy arguments may not be followed by another argument or a literal
|
||||
*/
|
||||
default boolean greedy() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package de.kentoj.kencommandapi.api.argument;
|
||||
|
||||
public interface CommandArgument<T, V> {
|
||||
|
||||
V value();
|
||||
|
||||
CommandArgumentSpec<T, V> argument();
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package de.kentoj.kencommandapi.api.argument;
|
||||
|
||||
public record CommandArgumentImpl<T, V>(
|
||||
V value,
|
||||
CommandArgumentSpec<T, V> argument
|
||||
) implements CommandArgument<T, V> {
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package de.kentoj.kencommandapi.api.argument;
|
||||
|
||||
import de.kentoj.kencommandapi.api.invocation.CommandContext;
|
||||
import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
public sealed interface CommandArgumentSpec<T, V> permits CommandArgumentSpecImpl {
|
||||
|
||||
static <T, V> CommandArgumentSpecBuilder<T, V> builder(String id, ArgumentType<T, V> type) {
|
||||
return new CommandArgumentSpecBuilder<>(id, type);
|
||||
}
|
||||
|
||||
String id();
|
||||
|
||||
ArgumentType<T, V> type();
|
||||
|
||||
/**
|
||||
* @throws IllegalArgumentException if string invalid or if a requirement is not met
|
||||
*/
|
||||
V parseInput(String rawInput);
|
||||
|
||||
@Nullable Function<CommandContext<T>, V> defaultValueProvider();
|
||||
|
||||
/**
|
||||
* @return the explicitly set SuggestionProvider or the default suggestions for the type if unset/null
|
||||
*/
|
||||
/* explicit @NotNull to avoid confusion */
|
||||
@NotNull SuggestionProvider<T> suggestionProvider();
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package de.kentoj.kencommandapi.api.argument;
|
||||
|
||||
import de.kentoj.kencommandapi.api.invocation.CommandContext;
|
||||
import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* @param <T> type of the command sender
|
||||
* @param <V> type of the value
|
||||
*/
|
||||
public class CommandArgumentSpecBuilder<T, V> {
|
||||
|
||||
private final String id;
|
||||
private final ArgumentType<T, V> type;
|
||||
private @Nullable Function<CommandContext<T>, V> defaultValueProvider;
|
||||
private @Nullable SuggestionProvider<T> suggestionProvider;
|
||||
private final List<ArgumentRequirement<V>> requirements = new ArrayList<>();
|
||||
|
||||
public CommandArgumentSpecBuilder(String id, ArgumentType<T, V> type) {
|
||||
this.id = id;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public CommandArgumentSpecBuilder<T, V> withRequirement(Predicate<V> predicate, String message) {
|
||||
requirements.add(new ArgumentRequirement<>(predicate, message));
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandArgumentSpecBuilder<T, V> withDefaultValue(@Nullable Function<CommandContext<T>, V> defaultValueProvider) {
|
||||
this.defaultValueProvider = defaultValueProvider;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandArgumentSpecBuilder<T, V> withSuggestionProvider(@Nullable SuggestionProvider<T> suggestionProvider) {
|
||||
this.suggestionProvider = suggestionProvider;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandArgumentSpec<T, V> build() {
|
||||
return new CommandArgumentSpecImpl<>(id, type, requirements, defaultValueProvider, suggestionProvider);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package de.kentoj.kencommandapi.api.argument;
|
||||
|
||||
import de.kentoj.kencommandapi.api.invocation.CommandContext;
|
||||
import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
public record CommandArgumentSpecImpl<T, V>(
|
||||
String id,
|
||||
ArgumentType<T, V> type,
|
||||
List<ArgumentRequirement<V>> requirements,
|
||||
Function<CommandContext<T>, V> defaultValueProvider,
|
||||
@Nullable SuggestionProvider<T> suggestionProvider
|
||||
) implements CommandArgumentSpec<T, V> {
|
||||
|
||||
@Override
|
||||
public V parseInput(String rawInput) {
|
||||
var parsed = type.parseInputUnchecked(rawInput);
|
||||
for (var requirement : requirements) {
|
||||
if (!requirement.predicate().test(parsed))
|
||||
throw new IllegalArgumentException(requirement.message());
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Function<CommandContext<T>, V> defaultValueProvider() {
|
||||
return defaultValueProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull SuggestionProvider<T> suggestionProvider() {
|
||||
return suggestionProvider != null ? suggestionProvider : type.getDefaultSuggestionProvider();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package de.kentoj.kencommandapi.api.argument.types;
|
||||
|
||||
import de.kentoj.kencommandapi.api.argument.ArgumentType;
|
||||
import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider;
|
||||
|
||||
public class IntegerArgumentType<T> implements ArgumentType<T, Integer> {
|
||||
|
||||
@Override
|
||||
public Integer parseInputUnchecked(String string) {
|
||||
return Integer.parseInt(string);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SuggestionProvider<T> getDefaultSuggestionProvider() {
|
||||
return SuggestionProvider.none();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package de.kentoj.kencommandapi.api.argument.types;
|
||||
|
||||
import de.kentoj.kencommandapi.api.argument.ArgumentType;
|
||||
import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider;
|
||||
|
||||
public record StringArgumentType<T>(boolean greedy) implements ArgumentType<T, String> {
|
||||
|
||||
@Override
|
||||
public String parseInputUnchecked(String string) {
|
||||
return string;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SuggestionProvider<T> getDefaultSuggestionProvider() {
|
||||
return SuggestionProvider.none();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package de.kentoj.kencommandapi.api.invocation;
|
||||
|
||||
import de.kentoj.kencommandapi.api.argument.CommandArgument;
|
||||
import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface CommandContext<T> {
|
||||
|
||||
T sender();
|
||||
|
||||
<V> CommandArgument<T, V> getParsedArgument(String id);
|
||||
|
||||
/**
|
||||
* @return the value of the argument with the specified id
|
||||
* @param <V> the type of the value of the argument with the specified id
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
default <V> V getArg(String id) {
|
||||
return (V) getParsedArgument(id).value();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param arg the argument whose value to return
|
||||
* @return the value of the argument
|
||||
* @param <V> the type of the value of the argument
|
||||
*/
|
||||
default <V> V getArg(CommandArgumentSpec<T, V> arg) {
|
||||
return getArg(arg.id());
|
||||
}
|
||||
|
||||
default <V> CommandArgument<T, V> getParsedArgument(CommandArgumentSpec<T, V> arg) {
|
||||
return getParsedArgument(arg.id());
|
||||
}
|
||||
|
||||
Map<String, CommandArgument<T, ?>> parsedArguments();
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package de.kentoj.kencommandapi.api.invocation;
|
||||
|
||||
import com.leakyabstractions.result.api.Result;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public interface CommandExecutor<T> {
|
||||
|
||||
CompletableFuture<Result<Object, String>> execute(CommandContext<T> ctx);
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package de.kentoj.kencommandapi.api.invocation;
|
||||
|
||||
import com.leakyabstractions.result.api.Result;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public interface SyncCommandExecutor<T> extends CommandExecutor<T> {
|
||||
|
||||
Result<Object, String> executeSync(CommandContext<T> ctx);
|
||||
|
||||
default CompletableFuture<Result<Object, String>> execute(CommandContext<T> ctx) {
|
||||
return CompletableFuture.completedFuture(this.executeSync(ctx));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package de.kentoj.kencommandapi.api.literal;
|
||||
|
||||
import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec;
|
||||
import de.kentoj.kencommandapi.api.invocation.CommandExecutor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface Literal<T> {
|
||||
static <T> LiteralBuilder<T> builder(String... name) {
|
||||
return new LiteralBuilder<>(name);
|
||||
}
|
||||
|
||||
@NotNull String[] names();
|
||||
|
||||
/**
|
||||
* @return what the literal does
|
||||
*/
|
||||
@Nullable String description();
|
||||
|
||||
List<CommandArgumentSpec<T, ?>> arguments();
|
||||
|
||||
@Nullable CommandExecutor<T> executor();
|
||||
|
||||
@Nullable Literal<T> getLiteral(@NotNull String name);
|
||||
|
||||
@NotNull List<Literal<T>> literals();
|
||||
|
||||
@Nullable String permission();
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
package de.kentoj.kencommandapi.api.literal;
|
||||
|
||||
import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec;
|
||||
import de.kentoj.kencommandapi.api.invocation.CommandExecutor;
|
||||
import de.kentoj.kencommandapi.api.invocation.SyncCommandExecutor;
|
||||
import de.kentoj.kencommandapi.api.platform.MessageStyle;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
|
||||
public final class LiteralBuilder<T> {
|
||||
|
||||
private final String[] names;
|
||||
private @Nullable String description;
|
||||
private @Nullable CommandExecutor<T> executor;
|
||||
private @Nullable String permission;
|
||||
private final List<CommandArgumentSpec<T, ?>> arguments = new ArrayList<>();
|
||||
private final List<Literal<T>> literals = new ArrayList<>();
|
||||
|
||||
LiteralBuilder(String... names) {
|
||||
checkArgument(names.length > 0, "at least one name is required");
|
||||
this.names = names;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param description what the literal does
|
||||
*/
|
||||
public LiteralBuilder<T> withDescription(@Nullable String description) {
|
||||
this.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
public LiteralBuilder<T> withExecutor(@Nullable CommandExecutor<T> executor) {
|
||||
this.executor = executor;
|
||||
return this;
|
||||
}
|
||||
|
||||
public LiteralBuilder<T> withSyncExecutor(@Nullable SyncCommandExecutor<T> executor) {
|
||||
return withExecutor(executor);
|
||||
}
|
||||
|
||||
public LiteralBuilder<T> withPermission(@Nullable String permission) {
|
||||
this.permission = permission;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws IllegalArgumentException if there is already an argument with the same id
|
||||
* @throws IllegalStateException if a greedy argument was added to the literal previously
|
||||
*/
|
||||
public LiteralBuilder<T> withArgument(CommandArgumentSpec<T, ?> argument) {
|
||||
checkArgument(!hasGreedyArg(), "cannot add an argument after a greedy argument");
|
||||
if (arguments.stream().anyMatch(arg -> arg.id().equals(argument.id())))
|
||||
throw new IllegalArgumentException("cannot add multiple arguments with the same id");
|
||||
|
||||
this.arguments.add(argument);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws IllegalArgumentException if there is already a literal with the same name
|
||||
* @throws IllegalStateException if a greedy argument was added to the literal previously
|
||||
*/
|
||||
public LiteralBuilder<T> withLiteral(Literal<T> literal) {
|
||||
checkArgument(!hasGreedyArg(), "cannot add a literal after a greedy argument");
|
||||
for (String name : literal.names())
|
||||
for (var l : literals)
|
||||
if (l.getLiteral(name) != null)
|
||||
throw new IllegalArgumentException("literal with same name already exists");
|
||||
|
||||
this.literals.add(literal);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RootLiteral<T> asRootLiteral(MessageStyle messageStyle) {
|
||||
return new RootLiteralImpl<>(names, description, literals, arguments, executor, permission, messageStyle);
|
||||
}
|
||||
|
||||
public Literal<T> build() {
|
||||
return new LiteralImpl<>(names, description, literals, arguments, executor, permission);
|
||||
}
|
||||
|
||||
private boolean hasGreedyArg() {
|
||||
if (arguments.isEmpty()) return false;
|
||||
var arg = arguments.getLast();
|
||||
return arg != null && arg.type().greedy();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package de.kentoj.kencommandapi.api.literal;
|
||||
|
||||
import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec;
|
||||
import de.kentoj.kencommandapi.api.invocation.CommandExecutor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @param description what the literal does
|
||||
*/
|
||||
record LiteralImpl<T>(
|
||||
String[] names,
|
||||
String description,
|
||||
List<Literal<T>> literals,
|
||||
List<CommandArgumentSpec<T, ?>> arguments,
|
||||
CommandExecutor<T> executor,
|
||||
@Nullable String permission
|
||||
) implements Literal<T> {
|
||||
|
||||
@Override
|
||||
public @Nullable Literal<T> getLiteral(@NotNull String name) {
|
||||
for (Literal<T> literal : literals) {
|
||||
for (var literalName : literal.names()) {
|
||||
if (literalName.equalsIgnoreCase(name))
|
||||
return literal;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package de.kentoj.kencommandapi.api.literal;
|
||||
|
||||
import de.kentoj.kencommandapi.api.platform.MessageStyle;
|
||||
|
||||
public sealed interface RootLiteral<T> extends Literal<T> permits RootLiteralImpl {
|
||||
|
||||
MessageStyle messageStyle();
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package de.kentoj.kencommandapi.api.literal;
|
||||
|
||||
import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec;
|
||||
import de.kentoj.kencommandapi.api.invocation.CommandExecutor;
|
||||
import de.kentoj.kencommandapi.api.platform.MessageStyle;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
record RootLiteralImpl<T>(
|
||||
String[] names,
|
||||
String description,
|
||||
List<Literal<T>> literals,
|
||||
List<CommandArgumentSpec<T, ?>> arguments,
|
||||
CommandExecutor<T> executor,
|
||||
@Nullable String permission,
|
||||
MessageStyle messageStyle
|
||||
) implements RootLiteral<T> {
|
||||
@Override
|
||||
public @Nullable Literal<T> getLiteral(@NotNull String name) {
|
||||
for (var l : literals) {
|
||||
for (var lName : l.names())
|
||||
if (lName.equalsIgnoreCase(name))
|
||||
return l;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package de.kentoj.kencommandapi.api.platform;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface HasPermissionMethod<T> {
|
||||
boolean test(T sender, String permission);
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package de.kentoj.kencommandapi.api.platform;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
|
||||
public interface MessageStyle {
|
||||
|
||||
MessageStyle PLAIN = new PlainMessageStyle();
|
||||
|
||||
Component ok(Component msg);
|
||||
|
||||
Component err(Component msg);
|
||||
|
||||
Component exception(Component msg);
|
||||
|
||||
default Component ok(String msg) {
|
||||
return ok(Component.text(msg));
|
||||
}
|
||||
|
||||
default Component err(String msg) {
|
||||
return err(Component.text(msg));
|
||||
}
|
||||
|
||||
default Component exception(String msg) {
|
||||
return exception(Component.text(msg));
|
||||
}
|
||||
|
||||
default Component exception(Throwable ex) {
|
||||
while (ex.getMessage() == null && ex.getCause() != null)
|
||||
ex = ex.getCause();
|
||||
return exception(ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package de.kentoj.kencommandapi.api.platform;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
|
||||
class PlainMessageStyle implements MessageStyle {
|
||||
@Override
|
||||
public Component ok(Component msg) {
|
||||
return msg;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component err(Component msg) {
|
||||
return msg;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component exception(Component msg) {
|
||||
return Component.text("ERROR: ").append(msg);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package de.kentoj.kencommandapi.api.platform;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface SendMessageMethod<T> {
|
||||
void send(T to, Component msg);
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package de.kentoj.kencommandapi.api.suggestion;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
class NoSuggestionProvider<T> implements SuggestionProvider<T> {
|
||||
@Override
|
||||
public List<String> suggest(T __) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package de.kentoj.kencommandapi.api.suggestion;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface SuggestionProvider<T> {
|
||||
|
||||
static <T> SuggestionProvider<T> none() {
|
||||
return new NoSuggestionProvider<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return an unfiltered(no permission checks or filtering by input) list of strings that may be used for the argument/literal
|
||||
*/
|
||||
List<String> suggest(T sender);
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package de.kentoj.kencommandapi.internal;
|
||||
|
||||
import de.kentoj.kencommandapi.api.argument.CommandArgument;
|
||||
import de.kentoj.kencommandapi.api.invocation.CommandContext;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public record CommandContextImpl<T>(
|
||||
T sender,
|
||||
Map<String, CommandArgument<T, ?>> parsedArguments
|
||||
) implements CommandContext<T> {
|
||||
|
||||
@Override
|
||||
public <V> CommandArgument<T, V> getParsedArgument(String id) {
|
||||
try {
|
||||
//noinspection unchecked
|
||||
return (CommandArgument<T, V>) parsedArguments.get(id);
|
||||
} catch (ClassCastException e) {
|
||||
throw new RuntimeException("requested argument with wrong type", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package de.kentoj.kencommandapi.internal.parser;
|
||||
|
||||
import de.kentoj.kencommandapi.api.literal.Literal;
|
||||
import de.kentoj.kencommandapi.api.platform.HasPermissionMethod;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
public final class CommandParseHelper<T> {
|
||||
private final HasPermissionMethod<T> hasPermission;
|
||||
|
||||
public CommandParseHelper(HasPermissionMethod<T> hasPermission) {
|
||||
this.hasPermission = hasPermission;
|
||||
}
|
||||
|
||||
public boolean hasPermission(T sender, Literal<T> literal) {
|
||||
if (literal.permission() == null) return true;
|
||||
return hasPermission.test(sender, literal.permission());
|
||||
}
|
||||
|
||||
public String parseInput(Iterator<String> tokenIter, boolean isGreedy) {
|
||||
if (!isGreedy)
|
||||
return tokenIter.next();
|
||||
var builder = new StringBuilder();
|
||||
tokenIter.forEachRemaining(tok -> builder.append(" ").append(tok));
|
||||
builder.deleteCharAt(0);
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
package de.kentoj.kencommandapi.internal.parser;
|
||||
|
||||
import com.leakyabstractions.result.api.Result;
|
||||
import com.leakyabstractions.result.core.Results;
|
||||
import de.kentoj.kencommandapi.api.argument.CommandArgument;
|
||||
import de.kentoj.kencommandapi.api.argument.CommandArgumentImpl;
|
||||
import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec;
|
||||
import de.kentoj.kencommandapi.api.invocation.CommandContext;
|
||||
import de.kentoj.kencommandapi.api.invocation.CommandExecutor;
|
||||
import de.kentoj.kencommandapi.api.literal.Literal;
|
||||
import de.kentoj.kencommandapi.internal.CommandContextImpl;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
public class InvocationCommandParser<T> {
|
||||
|
||||
private final CommandParseHelper<T> helper;
|
||||
|
||||
public InvocationCommandParser(CommandParseHelper<T> helper) {
|
||||
this.helper = helper;
|
||||
}
|
||||
|
||||
public Result<ExecutionData, String> parse(
|
||||
Literal<T> rootLiteral,
|
||||
T sender,
|
||||
Iterator<String> tokenIter
|
||||
) {
|
||||
Literal<T> currentLiteral = rootLiteral;
|
||||
var ctx = new CommandContextImpl<>(sender, new HashMap<>());
|
||||
|
||||
while (true) {
|
||||
{
|
||||
var result = processArguments(currentLiteral, ctx, tokenIter)
|
||||
.ifSuccess(ctx.parsedArguments()::putAll);
|
||||
if (result.hasFailure())
|
||||
return result.mapSuccess(_ -> null);
|
||||
}
|
||||
|
||||
if (!tokenIter.hasNext()) {
|
||||
if (currentLiteral.executor() == null)
|
||||
return Results.failure("Literal expected");
|
||||
break;
|
||||
}
|
||||
|
||||
var literalName = tokenIter.next();
|
||||
var nextLiteral = currentLiteral.getLiteral(literalName);
|
||||
var isIncompleteOrUnknown = nextLiteral == null || !helper.hasPermission(sender, nextLiteral);
|
||||
if (isIncompleteOrUnknown)
|
||||
return Results.failure("Unknown literal: " + literalName);
|
||||
currentLiteral = nextLiteral;
|
||||
}
|
||||
|
||||
return Results.success(new ExecutionData(currentLiteral.executor(), ctx));
|
||||
}
|
||||
|
||||
private Result<Map<String, CommandArgument<T, ?>>, String> processArguments(
|
||||
Literal<T> literal,
|
||||
CommandContext<T> ctx,
|
||||
Iterator<String> tokenIter
|
||||
) {
|
||||
Map<String, CommandArgument<T, ?>> parsedArguments = new HashMap<>();
|
||||
|
||||
for (var _argument : literal.arguments()) {
|
||||
@SuppressWarnings("unchecked")
|
||||
var argument = ((CommandArgumentSpec<T, Object>) _argument);
|
||||
|
||||
Object value;
|
||||
if (!tokenIter.hasNext()) {
|
||||
if (argument.defaultValueProvider() == null)
|
||||
return Results.failure("Argument expected: " + argument.id());
|
||||
value = requireNonNull(argument.defaultValueProvider()).apply(ctx);
|
||||
} else {
|
||||
var input = helper.parseInput(tokenIter, argument.type().greedy());
|
||||
try {
|
||||
value = argument.parseInput(input);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
return Results.failure("Illegal argument '" + input + "' for " + argument.id() + ": " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
parsedArguments.put(argument.id(), new CommandArgumentImpl<>(value, argument));
|
||||
}
|
||||
|
||||
return Results.success(parsedArguments);
|
||||
}
|
||||
|
||||
public class ExecutionData {
|
||||
private final CommandExecutor<T> executor;
|
||||
private final CommandContext<T> context;
|
||||
|
||||
public ExecutionData(CommandExecutor<T> executor, CommandContext<T> context) {
|
||||
this.executor = executor;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public CommandExecutor<T> executor() {
|
||||
return executor;
|
||||
}
|
||||
|
||||
public CommandContext<T> context() {
|
||||
return context;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
package de.kentoj.kencommandapi.internal.parser;
|
||||
|
||||
import de.kentoj.kencommandapi.api.literal.Literal;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class SuggestionCommandParser<T> {
|
||||
|
||||
private final CommandParseHelper<T> helper;
|
||||
|
||||
public SuggestionCommandParser(CommandParseHelper<T> helper) {
|
||||
this.helper = helper;
|
||||
}
|
||||
|
||||
public List<String> suggest(
|
||||
Literal<T> rootLiteral,
|
||||
T sender,
|
||||
Iterator<String> tokenIter,
|
||||
boolean suggestNext
|
||||
) {
|
||||
Literal<T> currentLiteral = rootLiteral;
|
||||
Literal<T> previousLiteral = null;
|
||||
|
||||
while (true) {
|
||||
var argumentSuggestions = processArguments(currentLiteral, tokenIter, sender, suggestNext);
|
||||
if (argumentSuggestions != null)
|
||||
return argumentSuggestions;
|
||||
|
||||
if (!tokenIter.hasNext()) {
|
||||
if (suggestNext || previousLiteral == null)
|
||||
return suggestions(currentLiteral, sender).toList();
|
||||
return suggestions(previousLiteral, sender).toList();
|
||||
}
|
||||
|
||||
var input = tokenIter.next();
|
||||
var nextLiteral = currentLiteral.getLiteral(input);
|
||||
var isIncompleteOrUnknown = nextLiteral == null || !helper.hasPermission(sender, nextLiteral);
|
||||
if (isIncompleteOrUnknown) {
|
||||
var finalInput = input.toLowerCase();
|
||||
return suggestions(currentLiteral, sender)
|
||||
.filter(name -> name.toLowerCase().startsWith(finalInput))
|
||||
.toList();
|
||||
}
|
||||
|
||||
previousLiteral = currentLiteral;
|
||||
currentLiteral = nextLiteral;
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable List<String> processArguments(
|
||||
Literal<T> literal,
|
||||
Iterator<String> tokenIter,
|
||||
T sender,
|
||||
boolean suggestNext
|
||||
) {
|
||||
var argsIter = literal.arguments().iterator();
|
||||
|
||||
/*
|
||||
for each argument:
|
||||
if no next token: suggest current
|
||||
parse token
|
||||
if not last token: skip
|
||||
if nextToken:
|
||||
if has next argument: suggest next argument
|
||||
else: return null
|
||||
return current argument's suggestions filtered
|
||||
|
||||
*/
|
||||
|
||||
while (argsIter.hasNext()) {
|
||||
var argument = argsIter.next();
|
||||
if (!tokenIter.hasNext())
|
||||
return argument.suggestionProvider().suggest(sender);
|
||||
var input = helper.parseInput(tokenIter, argument.type().greedy()).toLowerCase();
|
||||
if (tokenIter.hasNext())
|
||||
continue;
|
||||
if (suggestNext) {
|
||||
if (argsIter.hasNext())
|
||||
return argsIter.next().suggestionProvider().suggest(sender);
|
||||
return null;
|
||||
}
|
||||
return argument.suggestionProvider().suggest(sender).stream()
|
||||
.filter(suggestion -> suggestion.toLowerCase().startsWith(input))
|
||||
.toList();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Stream<String> suggestions(Literal<T> literal, T sender) {
|
||||
return literal.literals().stream()
|
||||
.filter(lit -> helper.hasPermission(sender, lit))
|
||||
.map(Literal::names)
|
||||
.flatMap(Arrays::stream);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package de.kentoj.kencommandapi.api.parser;
|
||||
|
||||
import com.leakyabstractions.result.core.Results;
|
||||
import de.kentoj.kencommandapi.api.CommandHandler;
|
||||
import de.kentoj.kencommandapi.api.invocation.CommandExecutor;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class ENV {
|
||||
public static final CommandHandler<Object> PARSER =
|
||||
new CommandHandler<>((__, ___) -> {}, (__, ___) -> true);
|
||||
public static final Object SENDER = new Object();
|
||||
public static final CommandExecutor<Object> EMPTY_EXECUTOR = __ ->
|
||||
CompletableFuture.completedFuture(Results.success(new Object()));
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
package de.kentoj.kencommandapi.api.parser;
|
||||
|
||||
import de.kentoj.kencommandapi.api.argument.CommandArgument;
|
||||
import de.kentoj.kencommandapi.api.argument.types.StringArgumentType;
|
||||
import de.kentoj.kencommandapi.api.literal.Literal;
|
||||
import de.kentoj.kencommandapi.api.literal.Literal;
|
||||
import de.kentoj.kencommandapi.api.platform.MessageStyle;
|
||||
import de.kentoj.kencommandapi.internal.parser.SuggestionCommandParser;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public class SuggestionCommandParserTest {
|
||||
|
||||
private final SuggestionCommandParser<Object> suggestionParser;
|
||||
private final Literal<Object> rootLiteral;
|
||||
|
||||
public SuggestionCommandParserTest() {
|
||||
rootLiteral = Literal.rootLiteral(MessageStyle.PLAIN, "someRootNode");
|
||||
suggestionParser = new SuggestionCommandParser<>((__, ___) -> true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void suggestNextArgument() {
|
||||
var arg = CommandArgument.arg("someArg", new StringArgumentType<>());
|
||||
arg.setSuggestionProvider(__ -> List.of("foo", "bar"));
|
||||
rootLiteral.addArgument(arg);
|
||||
|
||||
var suggestions = suggestionParser.getSuggestions(rootLiteral, ENV.SENDER, args(""));
|
||||
assertEquals(Stream.of("foo", "bar").sorted().toList(), suggestions.stream().sorted().toList());
|
||||
}
|
||||
|
||||
@Test
|
||||
void suggestNextArgumentAtSpace() {
|
||||
var uselessArg = CommandArgument.arg("someUselessArg", new StringArgumentType<>());
|
||||
var arg = CommandArgument.arg("someArg", new StringArgumentType<>());
|
||||
arg.setSuggestionProvider(__ -> List.of("foo", "bar"));
|
||||
rootLiteral.addArgument(uselessArg);
|
||||
rootLiteral.addArgument(arg);
|
||||
|
||||
var suggestions = suggestionParser.getSuggestions(rootLiteral, ENV.SENDER, args("hi "));
|
||||
assertEquals(Stream.of("foo", "bar").sorted().toList(), suggestions.stream().sorted().toList());
|
||||
}
|
||||
|
||||
@Test
|
||||
void suggestCurrentArgument() {
|
||||
var arg = CommandArgument.arg("someArg", new StringArgumentType<>());
|
||||
arg.setSuggestionProvider(__ -> List.of("foo", "bar"));
|
||||
rootLiteral.addArgument(arg);
|
||||
|
||||
var suggestions = suggestionParser.getSuggestions(rootLiteral, ENV.SENDER, args("f"));
|
||||
assertEquals(List.of("foo"), suggestions);
|
||||
}
|
||||
|
||||
@Test
|
||||
void suggestNextLiteral() {
|
||||
var literalFoo = Literal.literal("foo");
|
||||
var literalBar = Literal.literal("bar");
|
||||
rootLiteral.addLiteral(literalFoo);
|
||||
rootLiteral.addLiteral(literalBar);
|
||||
|
||||
var suggestions = suggestionParser.getSuggestions(rootLiteral, ENV.SENDER, args(""));
|
||||
assertEquals(Stream.of("foo", "bar").sorted().toList(), suggestions.stream().sorted().toList());
|
||||
}
|
||||
|
||||
@Test
|
||||
void suggestCurrentLiteral() {
|
||||
var literalFoo = Literal.literal("foo");
|
||||
var literalBar = Literal.literal("bar");
|
||||
rootLiteral.addLiteral(literalFoo);
|
||||
rootLiteral.addLiteral(literalBar);
|
||||
|
||||
var suggestions = suggestionParser.getSuggestions(rootLiteral, ENV.SENDER, args("f"));
|
||||
assertEquals(List.of("foo"), suggestions);
|
||||
}
|
||||
|
||||
private String[] args(String input) {
|
||||
List<String> args = new ArrayList<>(Arrays.asList(input.split(" ")));
|
||||
if (input.endsWith(" "))
|
||||
args.add("");
|
||||
return args.toArray(new String[0]);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue