bump version to 0.38-SNAPSHOT + pass input to SuggestionProvider + rewrite command parsing + ...
All checks were successful
Build and Deploy artifact / build (push) Successful in 1m31s

This commit is contained in:
kento2 2026-08-09 18:14:24 +02:00
parent 997906cd02
commit 23f7d1a550
26 changed files with 382 additions and 302 deletions

View file

@ -1,8 +1,6 @@
bazel_dep(name = "rules_java", version = "9.7.0") bazel_dep(name = "rules_java", version = "9.7.0")
bazel_dep(name = "rules_jvm_external", version = "7.1") bazel_dep(name = "rules_jvm_external", version = "7.1")
VERSION = "0.36-SNAPSHOT"
git_override( git_override(
module_name = "rules_jvm_external", module_name = "rules_jvm_external",
remote = "http://forge.kentoj.de/scrow/rules_jvm_external.git", remote = "http://forge.kentoj.de/scrow/rules_jvm_external.git",

View file

@ -8,7 +8,7 @@ import java.util.List;
public final class PlayerSuggestionProvider<T> implements SuggestionProvider<T> { public final class PlayerSuggestionProvider<T> implements SuggestionProvider<T> {
@Override @Override
public List<String> suggest(T __) { public List<String> suggest(T __, String ___) {
return Bukkit.getOnlinePlayers().stream() return Bukkit.getOnlinePlayers().stream()
.map(Player::getName) .map(Player::getName)
.toList(); .toList();

View file

@ -1,5 +1,7 @@
package de.kentoj.kencommandapi.type; package de.kentoj.kencommandapi.type;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import de.kentoj.kencommandapi.api.argument.ArgumentType; import de.kentoj.kencommandapi.api.argument.ArgumentType;
import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider; import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider;
import de.kentoj.kencommandapi.suggestionprovider.PlayerSuggestionProvider; import de.kentoj.kencommandapi.suggestionprovider.PlayerSuggestionProvider;
@ -11,20 +13,16 @@ import java.util.UUID;
public class OfflinePlayerArgumentType<T> implements ArgumentType<T, OfflinePlayer> { public class OfflinePlayerArgumentType<T> implements ArgumentType<T, OfflinePlayer> {
@Override @Override
public OfflinePlayer parseInputUnchecked(String string) { public Result<OfflinePlayer, String> parseInputUnchecked(String string) {
// UUIDs are always 36 chars // UUIDs are always 36 chars
if (string.length() == 36) { if (string.length() == 36)
try { return Results.ofCallable(() -> Bukkit.getOfflinePlayer(UUID.fromString(string)))
return Bukkit.getOfflinePlayer(UUID.fromString(string)); .mapFailure(_ -> "Invalid UUID.");
} catch (IllegalArgumentException ex) {
throw new IllegalArgumentException("Invalid UUID");
}
}
OfflinePlayer player = Bukkit.getPlayerExact(string); var player = Bukkit.getPlayerExact(string);
if (player == null || (!player.hasPlayedBefore() && !player.isOnline())) if (player == null || (!player.hasPlayedBefore() && !player.isOnline()))
throw new IllegalArgumentException("Player not found"); return Results.failure("Player not found.");
return player; return Results.success(player);
} }
@Override @Override

View file

@ -1,18 +1,32 @@
package de.kentoj.kencommandapi.type; package de.kentoj.kencommandapi.type;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import de.kentoj.kencommandapi.api.argument.ArgumentType; import de.kentoj.kencommandapi.api.argument.ArgumentType;
import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider; import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider;
import de.kentoj.kencommandapi.suggestionprovider.PlayerSuggestionProvider; import de.kentoj.kencommandapi.suggestionprovider.PlayerSuggestionProvider;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import java.util.UUID;
public class PlayerArgumentType<T> implements ArgumentType<T, Player> { public class PlayerArgumentType<T> implements ArgumentType<T, Player> {
@Override @Override
public Player parseInputUnchecked(String string) { public Result<Player, String> parseInputUnchecked(String input) {
var player = Bukkit.getPlayer(string); // UUIDs are always 36 chars
if (player == null) throw new IllegalArgumentException("player not online"); Player player;
return player; if (input.length() == 36) {
try {
player = Bukkit.getPlayer(UUID.fromString(input));
} catch (IllegalArgumentException _) {
return Results.failure("Invalid UUID.");
}
} else {
player = Bukkit.getPlayer(input);
}
return Results.ofNullable(player, "Player not online.");
} }
@Override @Override

View file

@ -1,5 +1,7 @@
package de.kentoj.kencommandapi.type; package de.kentoj.kencommandapi.type;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import de.kentoj.kencommandapi.api.argument.ArgumentType; import de.kentoj.kencommandapi.api.argument.ArgumentType;
import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider; import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
@ -10,21 +12,24 @@ import java.util.stream.Stream;
public class WorldArgumentType<T> implements ArgumentType<T, World> { public class WorldArgumentType<T> implements ArgumentType<T, World> {
@Override @Override
public World parseInputUnchecked(String input) throws IllegalArgumentException { public Result<World, String> parseInputUnchecked(String input) throws IllegalArgumentException {
var world = Bukkit.getWorld(input); World world;
if (world == null && input.length() == 36) { if (input.length() == 36) {
try { try {
return Bukkit.getWorld(UUID.fromString(input)); world = Bukkit.getWorld(UUID.fromString(input));
} catch (IllegalArgumentException _) { } catch (IllegalArgumentException _) {
throw new IllegalArgumentException("World not found"); return Results.failure("Invalid UUID.");
} }
} else {
world = Bukkit.getWorld(input);
} }
return world;
return Results.ofNullable(world, "World not found");
} }
@Override @Override
public SuggestionProvider<T> getDefaultSuggestionProvider() { public SuggestionProvider<T> getDefaultSuggestionProvider() {
return _ -> Stream.concat( return (_, _) -> Stream.concat(
Bukkit.getWorlds().stream().map(World::getName), Bukkit.getWorlds().stream().map(World::getName),
Bukkit.getWorlds().stream().map(w -> w.getUID().toString()) Bukkit.getWorlds().stream().map(w -> w.getUID().toString())
).toList(); ).toList();

View file

@ -14,4 +14,8 @@ java_export(
artifact("org.slf4j:slf4j-api"), artifact("org.slf4j:slf4j-api"),
artifact("net.kyori:adventure-api"), artifact("net.kyori:adventure-api"),
], ],
exports = [
artifact("com.leakyabstractions:result-api"),
artifact("com.leakyabstractions:result"),
],
) )

View file

@ -1,47 +1,72 @@
package de.kentoj.kencommandapi.api; package de.kentoj.kencommandapi.api;
import de.kentoj.kencommandapi.api.literal.Literal;
import de.kentoj.kencommandapi.api.literal.RootLiteral; import de.kentoj.kencommandapi.api.literal.RootLiteral;
import de.kentoj.kencommandapi.api.platform.HasPermissionMethod; import de.kentoj.kencommandapi.api.platform.HasPermissionMethod;
import de.kentoj.kencommandapi.api.platform.SendMessageMethod; import de.kentoj.kencommandapi.api.platform.SendMessageMethod;
import de.kentoj.kencommandapi.internal.parser.CommandParseHelper; import de.kentoj.kencommandapi.internal.NodeTreeWalker;
import de.kentoj.kencommandapi.internal.parser.InvocationCommandParser; import de.kentoj.kencommandapi.internal.WalkResult;
import de.kentoj.kencommandapi.internal.parser.SuggestionCommandParser;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Stream; import java.util.stream.Stream;
import static java.util.Objects.requireNonNull;
public class CommandHandler<T> { public class CommandHandler<T> {
private final SendMessageMethod<T> sendMessageMethod; private final SendMessageMethod<T> sendMessageMethod;
private final InvocationCommandParser<T> invocationParser; private final NodeTreeWalker<T> nodeTreeWalker;
private final SuggestionCommandParser<T> suggestionParser;
public CommandHandler(SendMessageMethod<T> sendMessageMethod, HasPermissionMethod<T> hasPermissionMethod) { public CommandHandler(SendMessageMethod<T> sendMessageMethod, HasPermissionMethod<T> hasPermissionMethod) {
this.sendMessageMethod = sendMessageMethod; this.sendMessageMethod = sendMessageMethod;
var parseHelper = new CommandParseHelper<>(hasPermissionMethod); nodeTreeWalker = new NodeTreeWalker<>(hasPermissionMethod);
invocationParser = new InvocationCommandParser<>(parseHelper);
suggestionParser = new SuggestionCommandParser<>(parseHelper);
} }
public void invoke(RootLiteral<T> rootLiteral, T sender, Stream<String> args) { public void invoke(RootLiteral<T> rootLiteral, T sender, Stream<String> args) {
invocationParser.parse(rootLiteral, sender, args.toList().iterator()) var walkResult = nodeTreeWalker.walk(rootLiteral, sender, args.iterator());
.ifSuccessOrElse(data -> exec(rootLiteral, data), if (!walkResult.isValidUsage()) {
err -> sendMessageMethod.send(sender, rootLiteral.messageStyle().err(err))); sendMessageMethod.send(sender, rootLiteral.messageStyle().err(walkResult.message()));
} return;
}
private void exec(RootLiteral<T> rootLiteral, InvocationCommandParser<T>.ExecutionData data) { requireNonNull(walkResult.lastLiteral().executor())
var ctx = data.context(); .execute(walkResult.ctx())
data.executor().execute(ctx)
.whenComplete((result, ex) -> { .whenComplete((result, ex) -> {
if (ex != null) if (ex != null)
sendMessageMethod.send(ctx.sender(), rootLiteral.messageStyle().exception(ex)); sendMessageMethod.send(sender, rootLiteral.messageStyle().exception(ex));
result.ifFailure(err -> result.ifFailure(err ->
sendMessageMethod.send(ctx.sender(), rootLiteral.messageStyle().err(err))); sendMessageMethod.send(sender, rootLiteral.messageStyle().err(err)));
}); });
} }
public List<String> getSuggestions(RootLiteral<T> rootNode, T sender, String[] args, boolean trailingSpace) { public List<String> getSuggestions(RootLiteral<T> rootLiteral, T sender, String[] args, boolean trailingSpace) {
return suggestionParser.suggest(rootNode, sender, Arrays.stream(args).iterator(), trailingSpace); var walkResult = nodeTreeWalker.walk(rootLiteral, sender, Arrays.stream(args).iterator());
Stream<String> stream;
String input = null;
switch (walkResult) {
case WalkResult.LiteralUnknown<T> res -> {
stream = res.lastLiteral().literals().stream().map(Literal::names).flatMap(Arrays::stream);
input = res.input();
}
case WalkResult.ArgumentIllegal<T> res -> {
stream = res.arg().suggestionProvider().suggest(sender, res.input()).stream();
input = res.input();
}
case WalkResult.LiteralExpected<T> res when trailingSpace ->
stream = res.lastLiteral().literals().stream().map(Literal::names).flatMap(Arrays::stream);
case WalkResult.ArgumentExpected<T> res when trailingSpace ->
stream = res.arg().suggestionProvider().suggest(sender, "").stream();
default -> {
return Collections.emptyList();
}
}
if (input == null) return stream.toList();
final var finalInput = input;
return stream
.filter(s -> s.startsWith(finalInput))
.toList();
} }
} }

View file

@ -1,5 +1,6 @@
package de.kentoj.kencommandapi.api.argument; package de.kentoj.kencommandapi.api.argument;
import com.leakyabstractions.result.api.Result;
import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider; import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider;
import java.util.function.Predicate; import java.util.function.Predicate;
@ -10,11 +11,10 @@ public interface ArgumentType<T, V> {
* parses the given input and returns it. * parses the given input and returns it.
* Unlike {@link CommandArgumentSpec#parseInput(String)}, this does not check any additional requirements. * Unlike {@link CommandArgumentSpec#parseInput(String)}, this does not check any additional requirements.
* *
* @throws IllegalArgumentException if string invalid
* @see CommandArgumentSpec#parseInput(String) * @see CommandArgumentSpec#parseInput(String)
* @see CommandArgumentSpecBuilder#withRequirement(Predicate, String) * @see CommandArgumentSpecBuilder#withRequirement(Predicate, String)
*/ */
V parseInputUnchecked(String input) throws IllegalArgumentException; Result<V, String> parseInputUnchecked(String input);
/** /**
* @see CommandArgumentSpec#suggestionProvider() * @see CommandArgumentSpec#suggestionProvider()

View file

@ -1,5 +1,6 @@
package de.kentoj.kencommandapi.api.argument; package de.kentoj.kencommandapi.api.argument;
import com.leakyabstractions.result.api.Result;
import de.kentoj.kencommandapi.api.invocation.CommandContext; import de.kentoj.kencommandapi.api.invocation.CommandContext;
import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider; import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -18,9 +19,9 @@ public sealed interface CommandArgumentSpec<T, V> permits CommandArgumentSpecImp
ArgumentType<T, V> type(); ArgumentType<T, V> type();
/** /**
* @throws IllegalArgumentException if string invalid or if a requirement is not met * @return Result of either the parsed value or a message describing the error
*/ */
V parseInput(String rawInput); Result<V, String> parseInput(String rawInput);
@Nullable Function<CommandContext<T>, V> defaultValueProvider(); @Nullable Function<CommandContext<T>, V> defaultValueProvider();

View file

@ -1,5 +1,7 @@
package de.kentoj.kencommandapi.api.argument; package de.kentoj.kencommandapi.api.argument;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import de.kentoj.kencommandapi.api.invocation.CommandContext; import de.kentoj.kencommandapi.api.invocation.CommandContext;
import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider; import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -17,13 +19,15 @@ public record CommandArgumentSpecImpl<T, V>(
) implements CommandArgumentSpec<T, V> { ) implements CommandArgumentSpec<T, V> {
@Override @Override
public V parseInput(String rawInput) { public Result<V, String> parseInput(String rawInput) {
var parsed = type.parseInputUnchecked(rawInput); var parseResult = type.parseInputUnchecked(rawInput);
if (parseResult.hasFailure())
return parseResult.mapSuccess(_ -> null);
for (var requirement : requirements) { for (var requirement : requirements) {
if (!requirement.predicate().test(parsed)) if (!requirement.predicate().test(parseResult.getSuccess().orElseThrow()))
throw new IllegalArgumentException(requirement.message()); return Results.failure(requirement.message());
} }
return parsed; return parseResult;
} }
@Override @Override

View file

@ -0,0 +1,22 @@
package de.kentoj.kencommandapi.api.argument.types;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import de.kentoj.kencommandapi.api.argument.ArgumentType;
import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider;
import java.util.List;
public class BooleanArgumentType<T> implements ArgumentType<T, Boolean> {
@Override
public Result<Boolean, String> parseInputUnchecked(String input) {
return Results.ofCallable(() -> Boolean.parseBoolean(input))
.mapFailure(_ -> "Must be true or false.");
}
@Override
public SuggestionProvider<T> getDefaultSuggestionProvider() {
return (_, _) -> List.of("true", "false");
}
}

View file

@ -0,0 +1,19 @@
package de.kentoj.kencommandapi.api.argument.types;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import de.kentoj.kencommandapi.api.argument.ArgumentType;
import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider;
public class DoubleArgumentType<T> implements ArgumentType<T, Double> {
@Override
public Result<Double, String> parseInputUnchecked(String input) {
return Results.ofCallable(() -> Double.parseDouble(input))
.mapFailure(_ -> "Not a valid double.");
}
@Override
public SuggestionProvider<T> getDefaultSuggestionProvider() {
return SuggestionProvider.none();
}
}

View file

@ -1,13 +1,16 @@
package de.kentoj.kencommandapi.api.argument.types; package de.kentoj.kencommandapi.api.argument.types;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import de.kentoj.kencommandapi.api.argument.ArgumentType; import de.kentoj.kencommandapi.api.argument.ArgumentType;
import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider; import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider;
public class IntegerArgumentType<T> implements ArgumentType<T, Integer> { public class IntegerArgumentType<T> implements ArgumentType<T, Integer> {
@Override @Override
public Integer parseInputUnchecked(String string) { public Result<Integer, String> parseInputUnchecked(String string) {
return Integer.parseInt(string); return Results.ofCallable(() -> Integer.parseInt(string))
.mapFailure(_ -> "Not a number.");
} }
@Override @Override

View file

@ -1,5 +1,7 @@
package de.kentoj.kencommandapi.api.argument.types; package de.kentoj.kencommandapi.api.argument.types;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import de.kentoj.kencommandapi.api.argument.ArgumentType; import de.kentoj.kencommandapi.api.argument.ArgumentType;
import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider; import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider;
@ -10,8 +12,8 @@ public record StringArgumentType<T>(boolean greedy) implements ArgumentType<T, S
} }
@Override @Override
public String parseInputUnchecked(String string) { public Result<String, String> parseInputUnchecked(String string) {
return string; return Results.success(string);
} }
@Override @Override

View file

@ -5,7 +5,7 @@ import java.util.List;
class NoSuggestionProvider<T> implements SuggestionProvider<T> { class NoSuggestionProvider<T> implements SuggestionProvider<T> {
@Override @Override
public List<String> suggest(T __) { public List<String> suggest(T __, String ___) {
return Collections.emptyList(); return Collections.emptyList();
} }
} }

View file

@ -11,5 +11,5 @@ public interface SuggestionProvider<T> {
/** /**
* @return an unfiltered(no permission checks or filtering by input) list of strings that may be used for the argument/literal * @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); List<String> suggest(T sender, String input);
} }

View file

@ -0,0 +1,26 @@
package de.kentoj.kencommandapi.internal;
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 java.util.Map;
public class CommandContextBuilder<T> {
private final T sender;
private final Map<String, CommandArgument<T, ?>> parsedArgument;
public CommandContextBuilder(T sender, Map<String, CommandArgument<T, ?>> parsedArgument) {
this.sender = sender;
this.parsedArgument = parsedArgument;
}
public <S> void addParsed(CommandArgumentSpec<T, S> arg, S value) {
parsedArgument.put(arg.id(), new CommandArgumentImpl<>(value, arg));
}
public CommandContext<T> build() {
return new CommandContextImpl<>(sender, parsedArgument);
}
}

View file

@ -5,11 +5,10 @@ import de.kentoj.kencommandapi.api.invocation.CommandContext;
import java.util.Map; import java.util.Map;
public record CommandContextImpl<T>( record CommandContextImpl<T>(
T sender, T sender,
Map<String, CommandArgument<T, ?>> parsedArguments Map<String, CommandArgument<T, ?>> parsedArguments
) implements CommandContext<T> { ) implements CommandContext<T> {
@Override @Override
public <V> CommandArgument<T, V> getParsedArgument(String id) { public <V> CommandArgument<T, V> getParsedArgument(String id) {
try { try {

View file

@ -0,0 +1,101 @@
package de.kentoj.kencommandapi.internal;
import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec;
import de.kentoj.kencommandapi.api.literal.Literal;
import de.kentoj.kencommandapi.api.literal.RootLiteral;
import de.kentoj.kencommandapi.api.platform.HasPermissionMethod;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Iterator;
import static java.util.Objects.requireNonNull;
public class NodeTreeWalker<T> {
private final HasPermissionMethod<T> hasPermission;
public NodeTreeWalker(HasPermissionMethod<T> hasPermission) {
this.hasPermission = hasPermission;
}
public WalkResult<T> walk(
RootLiteral<T> rootLiteral,
T sender,
Iterator<String> iter
) {
var cursor = new Cursor(iter);
var ctxBuilder = new CommandContextBuilder<>(sender, new HashMap<>());
Literal<T> literal = rootLiteral;
String token;
while (true) {
for (var arg : literal.arguments()) {
@SuppressWarnings("unchecked")
var res = walkArgument(cursor, literal, ctxBuilder, (CommandArgumentSpec<T, Object>) arg);
if (res != null)
return res;
}
token = cursor.nextToken();
if (token == null)
break;
var nextLiteral = literal.getLiteral(token);
if (nextLiteral == null)
return new WalkResult.LiteralUnknown<>(ctxBuilder.build(), literal, token);
if (nextLiteral.permission() != null && hasPermission.test(sender, nextLiteral.permission()))
return new WalkResult.LiteralUnknown<>(ctxBuilder.build(), literal, token);
literal = nextLiteral;
}
if (!literal.literals().isEmpty()) {
var isRequired = literal.executor() == null;
return new WalkResult.LiteralExpected<>(ctxBuilder.build(), literal, isRequired);
}
return new WalkResult.Complete<>(ctxBuilder.build(), literal);
}
private @Nullable WalkResult<T> walkArgument(
Cursor cursor,
Literal<T> literal,
CommandContextBuilder<T> ctxBuilder,
CommandArgumentSpec<T, Object> arg
) {
var rawInput = arg.type().greedy() ? cursor.remainingTokens() : cursor.nextToken();
if (rawInput == null) {
if (arg.defaultValueProvider() == null)
return new WalkResult.ArgumentExpected<>(ctxBuilder.build(), literal, false, arg);
var value = requireNonNull(arg.defaultValueProvider()).apply(ctxBuilder.build());
ctxBuilder.addParsed(arg, value);
return null;
}
var parseResult = arg.parseInput(rawInput);
if (parseResult.hasFailure())
return new WalkResult.ArgumentIllegal<>(ctxBuilder.build(), literal,
false, arg, rawInput, parseResult.getFailure().orElseThrow());
ctxBuilder.addParsed(arg, parseResult.getSuccess().orElseThrow());
return null;
}
private static class Cursor {
private final Iterator<String> tokenIter;
public Cursor(Iterator<String> tokenIter) {
this.tokenIter = tokenIter;
}
public @Nullable String nextToken() {
if (!tokenIter.hasNext())
return null;
return tokenIter.next();
}
public @Nullable String remainingTokens() {
var builder = new StringBuilder();
tokenIter.forEachRemaining(s -> builder.append(' ').append(s));
return builder.toString();
}
}
}

View file

@ -0,0 +1,80 @@
package de.kentoj.kencommandapi.internal;
import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec;
import de.kentoj.kencommandapi.api.invocation.CommandContext;
import de.kentoj.kencommandapi.api.literal.Literal;
import org.jetbrains.annotations.Nullable;
public sealed interface WalkResult<T> {
CommandContext<T> ctx();
Literal<T> lastLiteral();
boolean isValidUsage();
@Nullable String message();
record ArgumentIllegal<T>(
CommandContext<T> ctx,
Literal<T> lastLiteral,
boolean isValidUsage,
CommandArgumentSpec<T, ?> arg,
String input,
String message
) implements WalkResult<T> {
}
record ArgumentExpected<T>(
CommandContext<T> ctx,
Literal<T> lastLiteral,
boolean isValidUsage,
CommandArgumentSpec<T, ?> arg
) implements WalkResult<T> {
@Override
public String message() {
return "Argument expected.";
}
}
record LiteralExpected<T>(
CommandContext<T> ctx,
Literal<T> lastLiteral,
boolean isValidUsage
) implements WalkResult<T> {
@Override
public String message() {
return "Literal expected.";
}
}
record LiteralUnknown<T>(
CommandContext<T> ctx,
Literal<T> lastLiteral,
String input
) implements WalkResult<T> {
@Override
public boolean isValidUsage() {
return false;
}
@Override
public String message() {
return "Unknown literal \"" + input + "\".";
}
}
record Complete<T>(
CommandContext<T> ctx,
Literal<T> lastLiteral
) implements WalkResult<T> {
@Override
public boolean isValidUsage() {
return true;
}
@Override
public @Nullable String message() {
return null;
}
}
}

View file

@ -1,28 +0,0 @@
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

@ -1,108 +0,0 @@
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

@ -1,99 +0,0 @@
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

@ -1,2 +1,2 @@
GROUP = "de.kentoj.scrow" GROUP = "de.kentoj.scrow"
VERSION = "0.37-SNAPSHOT" VERSION = "0.38-SNAPSHOT"

View file

@ -16,7 +16,7 @@ public final class PlayerSuggestionProvider<T extends CommandSource> implements
} }
@Override @Override
public List<String> suggest(T __) { public List<String> suggest(T __, String ___) {
return proxyServer.getAllPlayers().stream() return proxyServer.getAllPlayers().stream()
.map(Player::getUsername) .map(Player::getUsername)
.toList(); .toList();

View file

@ -1,5 +1,7 @@
package de.kentoj.kencommandapi.type; package de.kentoj.kencommandapi.type;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ProxyServer; import com.velocitypowered.api.proxy.ProxyServer;
@ -7,6 +9,8 @@ import de.kentoj.kencommandapi.api.argument.ArgumentType;
import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider; import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider;
import de.kentoj.kencommandapi.suggestionprovider.PlayerSuggestionProvider; import de.kentoj.kencommandapi.suggestionprovider.PlayerSuggestionProvider;
import java.util.UUID;
public class PlayerArgumentType<T extends CommandSource> implements ArgumentType<T, Player> { public class PlayerArgumentType<T extends CommandSource> implements ArgumentType<T, Player> {
private final ProxyServer server; private final ProxyServer server;
@ -16,10 +20,20 @@ public class PlayerArgumentType<T extends CommandSource> implements ArgumentType
} }
@Override @Override
public Player parseInputUnchecked(String input) { public Result<Player, String> parseInputUnchecked(String input) {
var player = server.getPlayer(input).orElse(null); // TODO UUID support
if (player == null) throw new IllegalArgumentException("Player not online"); Player player;
return player; if (input.length() == 36) {
try {
player = server.getPlayer(UUID.fromString(input)).orElse(null);
} catch (IllegalArgumentException _) {
return Results.failure("Invalid UUID.");
}
} else {
player = server.getPlayer(input).orElse(null);
}
return Results.ofNullable(player, "Player not online.");
} }
@Override @Override