refactor ig

This commit is contained in:
kento2 2026-08-04 02:30:41 +02:00
parent ef32f9dbc4
commit 336ac4cad5
54 changed files with 528 additions and 444 deletions

16
core/BUILD Normal file
View 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"),
],
)

View file

@ -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);
}
}

View file

@ -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
) {
}

View file

@ -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;
}
}

View file

@ -0,0 +1,8 @@
package de.kentoj.kencommandapi.api.argument;
public interface CommandArgument<T, V> {
V value();
CommandArgumentSpec<T, V> argument();
}

View file

@ -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> {
}

View file

@ -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();
}

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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();
}

View file

@ -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);
}

View file

@ -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));
}
}

View file

@ -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();
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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();
}

View file

@ -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;
}
}

View file

@ -0,0 +1,6 @@
package de.kentoj.kencommandapi.api.platform;
@FunctionalInterface
public interface HasPermissionMethod<T> {
boolean test(T sender, String permission);
}

View file

@ -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());
}
}

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -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();
}
}

View file

@ -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);
}

View file

@ -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);
}
}
}

View file

@ -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();
}
}

View file

@ -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;
}
}
}

View file

@ -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);
}
}

View file

@ -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()));
}

View file

@ -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]);
}
}