currently refactoring a lot.

This commit is contained in:
kento2 2026-07-04 01:04:01 +02:00
parent fe0b04c7d1
commit 9aa138058c
32 changed files with 484 additions and 254 deletions

View file

@ -6,6 +6,14 @@ plugins {
id("de.kentoj.scrow.java-library")
}
repositories {
mavenCentral()
}
dependencies {
api("net.kyori:adventure-api:5.2.0")
}
publishing {
publications {
create<MavenPublication>("KenCommandAPI") {

View file

@ -0,0 +1,69 @@
package de.kentoj.kencommandapi.api;
import de.kentoj.kencommandapi.api.invocation.CommandContext;
import de.kentoj.kencommandapi.api.node.CommandNode;
import de.kentoj.kencommandapi.api.node.RootCommandNode;
import de.kentoj.kencommandapi.api.parser.CommandParser;
import de.kentoj.kencommandapi.api.parser.ParseResult;
import de.kentoj.kencommandapi.api.platform.HasPermissionMethod;
import de.kentoj.kencommandapi.api.platform.SendMessageMethod;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@RequiredArgsConstructor
public class CommandHandler<T> {
private final CommandParser<T> commandParser;
private final SendMessageMethod<T> sendMessageMethod;
private final HasPermissionMethod<T> hasPermissionMethod;
public void invoke(RootCommandNode<T> rootNode, T sender, String[] args) {
var parseResult = commandParser.parseLiteral(rootNode, sender, args, true);
if (!(parseResult instanceof ParseResult.Success<T>)) {
sendMessageMethod.send(sender, rootNode.getMessageStyle().err(parseResult.getMessage()));
return;
}
var ctx = new CommandContext<>(sender, ((ParseResult.Success<T>) parseResult).getParsedArguments());
assert parseResult.getLiteral().getExecutor() != null;
parseResult.getLiteral().getExecutor().execute(ctx).whenComplete((result, ex) -> {
if (ex != null) {
sendMessageMethod.send(sender, rootNode.getMessageStyle().exception(ex));
}
result.ifFailure(failure ->
sendMessageMethod.send(sender, rootNode.getMessageStyle().err(failure))
);
});
}
public CompletableFuture<List<String>> getSuggestions(RootCommandNode<T> rootNode, T sender, String[] args) {
var parseResult = commandParser.parseLiteral(rootNode, sender, args, false);
if (parseResult instanceof ParseResult.ArgumentExpected<T>) {
var arg = ((ParseResult.ArgumentExpected<T>) parseResult).getArgument();
return arg.getSuggestionProvider().suggest(sender);
}
String filter = null;
if (parseResult instanceof ParseResult.UnknownLiteral<T>)
filter = ((ParseResult.UnknownLiteral<T>) parseResult).getName();
else if (parseResult instanceof ParseResult.NoPermission<T>)
filter = ((ParseResult.NoPermission<T>) parseResult).getName();
return getSuggestions(sender, parseResult.getLiteral(), filter);
}
private CompletableFuture<List<String>> getSuggestions(T sender, CommandNode<T> literal, @Nullable String filter) {
var stream = literal.getLiterals().stream()
.filter(lit -> lit.getPermission() != null && hasPermissionMethod.hasPermission(sender, lit.getPermission()))
.map(CommandNode::getNames)
.flatMap(Arrays::stream);
if (filter != null)
stream = stream.filter(f -> f.toLowerCase().contains(filter.toLowerCase()));
return CompletableFuture.completedFuture(stream.toList());
}
}

View file

@ -1,6 +1,10 @@
package de.kentoj.kencommandapi.api.invocation;
import com.leakyabstractions.result.api.Result;
import java.util.concurrent.CompletableFuture;
public interface CommandExecutor<T> {
Result execute(CommandContext<T> ctx);
CompletableFuture<Result<Void, String>> execute(CommandContext<T> ctx);
}

View file

@ -1,24 +0,0 @@
package de.kentoj.kencommandapi.api.invocation;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.Nullable;
@RequiredArgsConstructor
@Getter
public class Result {
public static Result success() {
return new Result(null);
}
public static Result error(String error) {
return new Result(error);
}
private final @Nullable String error;
public boolean isError() {
return error != null;
}
}

View file

@ -1,8 +1,10 @@
package de.kentoj.kencommandapi.api;
package de.kentoj.kencommandapi.api.node;
import de.kentoj.kencommandapi.api.platform.MessageStyle;
import de.kentoj.kencommandapi.api.argument.CommandArgument;
import de.kentoj.kencommandapi.api.invocation.CommandExecutor;
import de.kentoj.kencommandapi.internal.CommandNodeImpl;
import de.kentoj.kencommandapi.internal.RootCommandNodeImpl;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -14,6 +16,10 @@ public interface CommandNode<T> {
return new CommandNodeImpl<>(name);
}
static <T> RootCommandNode<T> rootNode(MessageStyle messageStyle, String...name) {
return new RootCommandNodeImpl<>(messageStyle, name);
}
@NotNull String[] getNames();
void setDescription(@Nullable String description);

View file

@ -0,0 +1,10 @@
package de.kentoj.kencommandapi.api.node;
import de.kentoj.kencommandapi.api.platform.MessageStyle;
public interface RootCommandNode<T> extends CommandNode<T> {
MessageStyle getMessageStyle();
void setMessageStyle(MessageStyle messageStyle);
}

View file

@ -1,8 +1,12 @@
package de.kentoj.kencommandapi.internal.parser;
package de.kentoj.kencommandapi.api.parser;
import de.kentoj.kencommandapi.api.CommandNode;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import de.kentoj.kencommandapi.api.argument.CommandArgument;
import de.kentoj.kencommandapi.api.invocation.ParsedArgument;
import de.kentoj.kencommandapi.api.platform.HasPermissionMethod;
import de.kentoj.kencommandapi.api.node.CommandNode;
import de.kentoj.kencommandapi.api.node.RootCommandNode;
import de.kentoj.kencommandapi.internal.ParsedArgumentImpl;
import lombok.RequiredArgsConstructor;
@ -14,28 +18,25 @@ import java.util.Map;
@RequiredArgsConstructor
public class CommandParser<T> {
private final PermissionChecker<T> hasPermissionMethod;
private final HasPermissionMethod<T> hasPermissionMethod;
public ParseResult<T> parseLiteral(CommandNode<T> rootNode, T sender, String[] args) {
var cur = rootNode;
public ParseResult<T> parseLiteral(RootCommandNode<T> rootNode, T sender, String[] args, boolean parseArgs) {
CommandNode<T> cur = rootNode;
final Iterator<String> argStrIter = Arrays.stream(args).iterator();
final Map<String, ParsedArgument<T, ?>> parsedArguments = new HashMap<>();
while (argStrIter.hasNext()) {
for (var nodeArg : cur.getArguments()) {
ParsedArgument<T, Object> parsed;
try {
parsed = parseArgument(argStrIter, sender, nodeArg);
} catch (IllegalArgumentException ex) {
return new ParseResult.IllegalArgument<>(cur, ex.getMessage());
}
if (parsed == null) return new ParseResult.IncompleteArgument<>(cur, nodeArg);
parsedArguments.put(nodeArg.getId(), parsed);
var parsed = parseArgument(rootNode, nodeArg, argStrIter, sender, !parseArgs);
if (parsed.hasFailure())
return parsed.getFailure().orElseThrow();
if (parseArgs)
parsedArguments.put(nodeArg.getId(), parsed.getSuccess().orElseThrow());
}
if (!argStrIter.hasNext()) {
if (cur.getExecutor() == null)
return new ParseResult.IncompleteCommand<>(cur);
return new ParseResult.LiteralExpected<>(cur);
return new ParseResult.Success<>(cur, parsedArguments);
}
@ -48,23 +49,40 @@ public class CommandParser<T> {
cur = nextNode;
}
throw new RuntimeException("code reached unexpected line");
if (cur.getExecutor() == null)
return new ParseResult.LiteralExpected<>(cur);
return new ParseResult.Success<>(cur, parsedArguments);
}
@SuppressWarnings("unchecked")
private ParsedArgument<T, Object> parseArgument(Iterator<String> args, T sender, CommandArgument<T, ?> nodeArg) {
private Result<ParsedArgument<T, Object>, ParseResult<T>> parseArgument(
CommandNode<T> node,
CommandArgument<T, ?> nodeArg,
Iterator<String> args,
T sender,
boolean fakeParse
) {
Object value;
if (args.hasNext()) {
var argStr = getRawArgument(args, nodeArg);
value = nodeArg.getType().parseInput(argStr);
if (fakeParse) {
value = null;
} else {
try {
value = nodeArg.getType().parseInput(argStr);
} catch (IllegalArgumentException ex) {
return Results.failure(new ParseResult.IllegalArgument<>(node, nodeArg, ex.getMessage(), argStr));
}
}
} else {
if (nodeArg.getDefaultValueProvider() == null)
return null;
return Results.failure(new ParseResult.ArgumentExpected<>(node, nodeArg));
value = nodeArg.getDefaultValueProvider().apply(sender);
}
return new ParsedArgumentImpl<>(value, (CommandArgument<T, Object>) nodeArg);
var parsedArg = new ParsedArgumentImpl<>(value, (CommandArgument<T, Object>) nodeArg);
return Results.success(parsedArg);
}
private String getRawArgument(Iterator<String> args, CommandArgument<T, ?> arg) {
@ -80,4 +98,4 @@ public class CommandParser<T> {
return rawArgument.toString();
}
}
}

View file

@ -1,17 +1,17 @@
package de.kentoj.kencommandapi.internal.parser;
package de.kentoj.kencommandapi.api.parser;
import de.kentoj.kencommandapi.api.CommandNode;
import de.kentoj.kencommandapi.api.argument.CommandArgument;
import de.kentoj.kencommandapi.api.invocation.ParsedArgument;
import de.kentoj.kencommandapi.api.node.CommandNode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Map;
public abstract sealed class ParseResult<T> permits ParseResult.IllegalArgument, ParseResult.IncompleteArgument,
ParseResult.IncompleteCommand, ParseResult.NoPermission, ParseResult.Success, ParseResult.UnknownLiteral {
public abstract sealed class ParseResult<T> permits ParseResult.IllegalArgument, ParseResult.ArgumentExpected,
ParseResult.LiteralExpected, ParseResult.NoPermission, ParseResult.Success, ParseResult.UnknownLiteral {
/**
/**
* @return last successfully literal
*/
public abstract CommandNode<T> getLiteral();
@ -19,25 +19,27 @@ public abstract sealed class ParseResult<T> permits ParseResult.IllegalArgument,
public abstract String getMessage();
@RequiredArgsConstructor
public static final class IncompleteArgument<T> extends ParseResult<T> {
public static final class ArgumentExpected<T> extends ParseResult<T> {
@Getter
private final CommandNode<T> literal;
@Getter
private final CommandArgument<T, ?> argument;
@Override
public String getMessage() {
return "Missing value for argument: " + argument.getId();
return "Argument expected: " + argument.getId();
}
}
@RequiredArgsConstructor
public static final class UnknownLiteral<T> extends ParseResult<T> {
private final CommandNode<T> previousLiteral;
@Getter
private final String name;
@Override
public String getMessage() {
return "Unknown literal: " + name;
return "Unknown subcommand: " + name;
}
@Override
@ -47,13 +49,13 @@ public abstract sealed class ParseResult<T> permits ParseResult.IllegalArgument,
}
@RequiredArgsConstructor
public static final class IncompleteCommand<T> extends ParseResult<T> {
public static final class LiteralExpected<T> extends ParseResult<T> {
@Getter
private final CommandNode<T> literal;
@Override
public String getMessage() {
return "Incomplete command";
return "Subcommand expected";
}
}
@ -74,11 +76,12 @@ public abstract sealed class ParseResult<T> permits ParseResult.IllegalArgument,
public static final class NoPermission<T> extends ParseResult<T> {
@Getter
private final CommandNode<T> literal;
@Getter
private final String name;
@Override
public String getMessage() {
return "Unknown literal: " + name;
return "Unknown subcommand: " + name;
}
}
@ -86,7 +89,26 @@ public abstract sealed class ParseResult<T> permits ParseResult.IllegalArgument,
public static final class IllegalArgument<T> extends ParseResult<T> {
@Getter
private final CommandNode<T> literal;
@Getter
private final CommandArgument<T, ?> argument;
private final String message;
/**
* Input passed to the parser
*/
@Getter
private final String argStr;
public IllegalArgument(CommandNode<T> literal, CommandArgument<T, ?> argument, Throwable throwable, String argStr) {
this.literal = literal;
this.argument = argument;
this.argStr = argStr;
while (throwable.getMessage() == null && throwable.getCause() != null)
throwable = throwable.getCause();
message = throwable.getMessage();
}
@Override
public String getMessage() {
return "Illegal argument '" + argStr + "' for " + argument.getId() + ":" + message;
}
}
}

View file

@ -0,0 +1,6 @@
package de.kentoj.kencommandapi.api.platform;
@FunctionalInterface
public interface HasPermissionMethod<T> {
boolean hasPermission(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;
public 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

@ -1,15 +1,13 @@
package de.kentoj.kencommandapi.api.suggestion;
import de.kentoj.kencommandapi.api.invocation.CommandContext;
import java.util.Collections;
import java.util.Set;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class NoSuggestionProvider<T> implements SuggestionProvider<T> {
@Override
public CompletableFuture<Set<String>> suggest(CommandContext<T> __) {
return CompletableFuture.completedFuture(Collections.emptySet());
public CompletableFuture<List<String>> suggest(T __) {
return CompletableFuture.completedFuture(Collections.emptyList());
}
}

View file

@ -1,11 +1,12 @@
package de.kentoj.kencommandapi.api.suggestion;
import de.kentoj.kencommandapi.api.invocation.CommandContext;
import java.util.Set;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public interface SuggestionProvider<T> {
CompletableFuture<Set<String>> suggest(CommandContext<T> context);
/**
* @return an unfiltered(no permission checks or filtering by input) list of strings that may be used for the argument
*/
CompletableFuture<List<String>> suggest(T sender);
}

View file

@ -6,6 +6,7 @@ import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.function.Function;
@ -22,7 +23,13 @@ public class CommandArgumentImpl<T, V> implements CommandArgument<T, V> {
@Setter
private @Nullable Function<T, V> defaultValueProvider;
@Getter
@Setter
private @Nullable SuggestionProvider<T> suggestionProvider;
@Override
public @NotNull SuggestionProvider<T> getSuggestionProvider() {
if (suggestionProvider == null)
return getType().defaultSuggestionProvider();
return this.suggestionProvider;
}
}

View file

@ -1,6 +1,6 @@
package de.kentoj.kencommandapi.internal;
import de.kentoj.kencommandapi.api.CommandNode;
import de.kentoj.kencommandapi.api.node.CommandNode;
import de.kentoj.kencommandapi.api.argument.CommandArgument;
import de.kentoj.kencommandapi.api.invocation.CommandExecutor;
import lombok.Getter;
@ -65,6 +65,7 @@ public class CommandNodeImpl<T> implements CommandNode<T> {
}
private boolean isGreedy() {
if (arguments.isEmpty()) return false;
var arg = arguments.getLast();
return arg != null && arg.getType().isGreedy();
}

View file

@ -0,0 +1,17 @@
package de.kentoj.kencommandapi.internal;
import de.kentoj.kencommandapi.api.platform.MessageStyle;
import de.kentoj.kencommandapi.api.node.RootCommandNode;
import lombok.Getter;
import lombok.Setter;
public class RootCommandNodeImpl<T> extends CommandNodeImpl<T> implements RootCommandNode<T> {
@Setter
@Getter
private MessageStyle messageStyle;
public RootCommandNodeImpl(MessageStyle messageStyle, String[] names) {
super(names);
this.messageStyle = messageStyle;
}
}

View file

@ -1,6 +0,0 @@
package de.kentoj.kencommandapi.internal.parser;
@FunctionalInterface
public interface PermissionChecker<T> {
boolean hasPermission(T sender, String permission);
}