clean up command parsing
Some checks failed
/ test (push) Successful in 1m10s
/ deploy (push) Failing after 56s

This commit is contained in:
kento2 2026-08-30 12:10:14 +02:00
parent c22f707556
commit 2c3307e59e
10 changed files with 89 additions and 183 deletions

View file

@ -1,14 +1,10 @@
package site.lab0x13.scrow.commands;
import site.lab0x13.scrow.commands.model.CommandContext;
import site.lab0x13.scrow.commands.model.literal.LiteralMeta;
import site.lab0x13.scrow.commands.model.literal.RootLiteral;
import site.lab0x13.scrow.commands.parser.NodeTreeWalker;
import site.lab0x13.scrow.commands.parser.WalkResult;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;
@ -25,13 +21,16 @@ public class CommandHandler<C extends CommandContext<C>> {
}
public void invoke(RootLiteral<C> rootLiteral, C ctx, Stream<String> args) {
var walkResult = nodeTreeWalker.walk(ctx, rootLiteral, args.iterator());
if (!walkResult.isValidUsage()) {
platformAdapter.sendMessage(ctx, rootLiteral.style().err(walkResult.message()));
var walkResult = nodeTreeWalker.walk(ctx, rootLiteral, args.iterator(), false);
var error = walkResult.executor().getFailure();
if (error.isPresent()) {
platformAdapter.sendMessage(ctx, rootLiteral.style().err(error.get()));
return;
}
requireNonNull(walkResult.lastLiteral().executor())
.execute(walkResult.ctx())
var executor = walkResult.executor().getSuccess().orElseThrow();
requireNonNull(executor)
.execute(ctx)
.exceptionally(ex -> {
platformAdapter.sendMessage(ctx, rootLiteral.style().exception(ex));
return null;
@ -39,39 +38,10 @@ public class CommandHandler<C extends CommandContext<C>> {
}
public List<String> suggest(RootLiteral<C> rootLiteral, C ctx, String[] args, boolean trailingSpace) {
var walkResult = nodeTreeWalker.walk(ctx, rootLiteral, Arrays.stream(args).iterator());
Stream<String> stream;
String input = null;
switch (walkResult) {
case WalkResult.LiteralUnknown<C> res -> {
stream = filterMetas(ctx, res.lastLiteral().subLiteralMetas());
input = res.input();
}
case WalkResult.LiteralExpected<C> res when trailingSpace ->
stream = filterMetas(ctx, res.lastLiteral().subLiteralMetas());
case WalkResult.ArgumentIllegal<C> res -> {
stream = res.arg().suggestionProvider().suggest(ctx, res.input()).stream();
input = res.input();
}
case WalkResult.ArgumentExpected<C> res when trailingSpace ->
stream = res.arg().suggestionProvider().suggest(ctx, "").stream();
case WalkResult.Complete<C> res when !trailingSpace -> stream = filterMetas(ctx, res.alternativeLiterals());
default -> {
return Collections.emptyList();
}
}
if (input == null) return stream.toList();
final var finalInput = input;
return stream
.filter(s -> s.startsWith(finalInput))
// FIXME trailing space
var walkResult = nodeTreeWalker.walk(ctx, rootLiteral, Arrays.stream(args).iterator(), trailingSpace);
return walkResult.suggestionProvider().suggest(ctx, walkResult.input()).stream()
.filter(s -> s.startsWith(walkResult.input()))
.toList();
}
private Stream<String> filterMetas(C ctx, List<? extends LiteralMeta> metas) {
return metas.stream()
.filter(literalMeta -> platformAdapter.checkPermission(ctx, literalMeta.permission()))
.map(LiteralMeta::names)
.flatMap(Collection::stream);
}
}

View file

@ -1,6 +0,0 @@
package site.lab0x13.scrow.commands.model;
public interface Node<C extends CommandContext<C>> {
SuggestionProvider<C> suggestionProvider();
}

View file

@ -3,7 +3,6 @@ package site.lab0x13.scrow.commands.model.argument;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import site.lab0x13.scrow.commands.model.CommandContext;
import site.lab0x13.scrow.commands.model.Node;
import site.lab0x13.scrow.commands.model.SuggestionProvider;
import java.util.List;
@ -18,8 +17,7 @@ public record Argument<C extends CommandContext<C>, V>(
List<ArgumentRequirement<C, V>> requirements,
SuggestionProvider<C> suggestionProvider,
DefaultValueProvider<C, V> defaultValueProvider
) implements Node<C> {
) {
public static <C extends CommandContext<C>, V> ArgumentBuilder<C, V> builder(String id, ArgumentType<C, V> type) {
return new ArgumentBuilder<>(id, type);
}

View file

@ -3,18 +3,14 @@ package site.lab0x13.scrow.commands.model.literal;
import org.jetbrains.annotations.Nullable;
import site.lab0x13.scrow.commands.model.CommandContext;
import site.lab0x13.scrow.commands.model.CommandExecutor;
import site.lab0x13.scrow.commands.model.Node;
import site.lab0x13.scrow.commands.model.SuggestionProvider;
import site.lab0x13.scrow.commands.model.argument.Argument;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
/**
* @param <C> argument of the command context
*/
public interface Literal<C extends CommandContext<C>> extends Node<C>, LiteralMeta {
public interface Literal<C extends CommandContext<C>> extends LiteralMeta {
static <C extends CommandContext<C>> StaticLiteralBuilder<C> builder(String... names) {
return new StaticLiteralBuilder<>(names);
@ -34,12 +30,4 @@ public interface Literal<C extends CommandContext<C>> extends Node<C>, LiteralMe
List<? extends LiteralMeta> subLiteralMetas();
List<Argument<C, ?>> arguments();
@Override
default SuggestionProvider<C> suggestionProvider() {
return (_, _) -> subLiteralMetas().stream()
.map(LiteralMeta::names)
.flatMap(Collection::stream)
.toList();
}
}

View file

@ -0,0 +1,29 @@
package site.lab0x13.scrow.commands.parser;
import org.jetbrains.annotations.Nullable;
import java.util.Iterator;
public class Cursor {
private final Iterator<String> tokenIter;
public Cursor(Iterator<String> tokenIter) {
this.tokenIter = tokenIter;
}
public boolean hasNext() {
return tokenIter.hasNext();
}
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

@ -1,14 +1,18 @@
package site.lab0x13.scrow.commands.parser;
import com.leakyabstractions.result.core.Results;
import org.jetbrains.annotations.Nullable;
import site.lab0x13.scrow.commands.PlatformAdapter;
import site.lab0x13.scrow.commands.model.CommandContext;
import site.lab0x13.scrow.commands.model.CommandExecutor;
import site.lab0x13.scrow.commands.model.SuggestionProvider;
import site.lab0x13.scrow.commands.model.argument.Argument;
import site.lab0x13.scrow.commands.model.literal.Literal;
import site.lab0x13.scrow.commands.model.literal.LiteralMeta;
import site.lab0x13.scrow.commands.model.literal.RootLiteral;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import static java.util.Objects.requireNonNull;
@ -23,7 +27,8 @@ public class NodeTreeWalker<C extends CommandContext<C>> {
public WalkResult<C> walk(
C ctx,
RootLiteral<C> rootLiteral,
Iterator<String> iter
Iterator<String> iter,
boolean suggestNext
) {
var cursor = new Cursor(iter);
Literal<C> previousLiteral = null;
@ -32,7 +37,7 @@ public class NodeTreeWalker<C extends CommandContext<C>> {
while (true) {
for (var arg : literal.arguments()) {
//noinspection unchecked
var res = walkArgument(ctx, cursor, literal, (Argument<C, Object>) arg);
var res = walkArgument(ctx, cursor, literal.executor(), (Argument<C, Object>) arg);
if (res != null)
return res;
}
@ -42,57 +47,47 @@ public class NodeTreeWalker<C extends CommandContext<C>> {
break;
var nextLiteral = literal.getSubLiteral(literalName);
if (nextLiteral == null || !platformAdapter.checkPermission(ctx, nextLiteral.permission()))
return new WalkResult.LiteralUnknown<>(ctx, literal, literalName);
return new WalkResult<>(Results.failure("Unknown literal."), suggestionProvider(literal), literalName);
previousLiteral = literal;
literal = nextLiteral;
}
if (!literal.subLiteralMetas().isEmpty()) {
var isOptional = literal.executor() != null;
return new WalkResult.LiteralExpected<>(ctx, literal, isOptional);
}
return new WalkResult.Complete<>(ctx, literal, previousLiteral == null ? Collections.emptyList() : previousLiteral.subLiteralMetas());
var suggestionProvider = suggestionProvider(suggestNext ? literal : previousLiteral);
if (literal.executor() == null)
return new WalkResult<>(Results.failure("Literal expected."), suggestionProvider);
return new WalkResult<>(Results.success(requireNonNull(literal.executor())), suggestionProvider);
}
private @Nullable WalkResult<C> walkArgument(
C ctx,
Cursor cursor,
Literal<C> literal,
CommandExecutor<C> executor,
Argument<C, Object> arg
) {
var rawInput = arg.type().greedy() ? cursor.remainingTokens() : cursor.nextToken();
if (rawInput == null) {
var defaultValue = requireNonNull(arg.defaultValueProvider()).getDefault(ctx);
ctx.addParsedArgument(arg, "", defaultValue);
return new WalkResult.ArgumentExpected<>(ctx, literal, defaultValue != null, arg);
}
if (defaultValue == null)
return new WalkResult<>(Results.failure("Argument expected."), arg.suggestionProvider());
// default value used. if this is the last, we probably want to suggest this.
if (!cursor.hasNext())
return new WalkResult<>(Results.success(executor), arg.suggestionProvider());
} else {
var parseResult = arg.parse(ctx, rawInput);
if (parseResult.hasFailure())
return new WalkResult.ArgumentIllegal<>(ctx, literal, false,
parseResult.getFailure().orElseThrow(), arg, rawInput);
return new WalkResult<>(parseResult.mapSuccess(_ -> null), arg.suggestionProvider(), rawInput);
ctx.addParsedArgument(arg, rawInput, parseResult.getSuccess().orElseThrow().value());
}
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();
}
private SuggestionProvider<C> suggestionProvider(Literal<C> literal) {
if (literal == null) return SuggestionProvider.none();
return (ctx, _) -> literal.subLiteralMetas().stream()
.filter(meta -> platformAdapter.checkPermission(ctx, meta.permission()))
.map(LiteralMeta::names)
.flatMap(List::stream)
.toList();
}
}

View file

@ -1,84 +1,16 @@
package site.lab0x13.scrow.commands.parser;
import org.jetbrains.annotations.Nullable;
import com.leakyabstractions.result.api.Result;
import site.lab0x13.scrow.commands.model.CommandContext;
import site.lab0x13.scrow.commands.model.argument.Argument;
import site.lab0x13.scrow.commands.model.literal.Literal;
import site.lab0x13.scrow.commands.model.literal.LiteralMeta;
import site.lab0x13.scrow.commands.model.CommandExecutor;
import site.lab0x13.scrow.commands.model.SuggestionProvider;
import java.util.List;
public sealed interface WalkResult<C extends CommandContext<C>> {
C ctx();
Literal<C> lastLiteral();
boolean isValidUsage();
@Nullable String message();
record ArgumentIllegal<C extends CommandContext<C>>(
C ctx,
Literal<C> lastLiteral,
boolean isValidUsage,
String message,
Argument<C, ?> arg,
public record WalkResult<C extends CommandContext<C>>(
Result<CommandExecutor<C>, String> executor,
SuggestionProvider<C> suggestionProvider,
String input
) implements WalkResult<C> {
}
record ArgumentExpected<C extends CommandContext<C>>(
C ctx,
Literal<C> lastLiteral,
boolean isValidUsage,
Argument<C, ?> arg
) implements WalkResult<C> {
@Override
public String message() {
return "Argument expected.";
}
}
record LiteralExpected<C extends CommandContext<C>>(
C ctx,
Literal<C> lastLiteral,
boolean isValidUsage
) implements WalkResult<C> {
@Override
public String message() {
return "Literal expected.";
}
}
record LiteralUnknown<C extends CommandContext<C>>(
C ctx,
Literal<C> lastLiteral,
String input
) implements WalkResult<C> {
@Override
public boolean isValidUsage() {
return false;
}
@Override
public String message() {
return "Unknown literal \"" + input + "\".";
}
}
record Complete<C extends CommandContext<C>>(
C ctx,
Literal<C> lastLiteral,
List<? extends LiteralMeta> alternativeLiterals
) implements WalkResult<C> {
@Override
public boolean isValidUsage() {
return true;
}
@Override
public @Nullable String message() {
return null;
}
) {
public WalkResult(Result<CommandExecutor<C>, String> executor, SuggestionProvider<C> suggestionProvider) {
this(executor, suggestionProvider, "");
}
}

View file

@ -93,7 +93,7 @@ class CommandHandlerTest {
@Test
void testFailOnBadLiteral() {
commandHandler.invoke(rootLiteral, new CommandContextImpl(), Stream.of("BAD_LITERAL"));
assertEquals("Unknown literal \"BAD_LITERAL\".\n", outContent.toString(StandardCharsets.UTF_8));
assertEquals("Unknown literal.\n", outContent.toString(StandardCharsets.UTF_8));
}
@Test

View file

@ -1,2 +1,2 @@
GROUP = "site.lab0x13.scrow"
VERSION = "1.3.5"
VERSION = "1.4.0"

View file

@ -12,10 +12,10 @@ public class VelocityCommand<C extends VelocityCommandContext<C>> implements Raw
private final CommandHandler<C> commandHandler;
private final CommandContextFactory<C> commandContextFactory;
private final PlatformAdapter platformAdapter;
private final PlatformAdapter<C> platformAdapter;
private final RootLiteral<C> rootLiteral;
public VelocityCommand(CommandHandler<C> commandHandler, CommandContextFactory<C> commandContextFactory, PlatformAdapter platformAdapter, RootLiteral<C> rootLiteral) {
public VelocityCommand(CommandHandler<C> commandHandler, CommandContextFactory<C> commandContextFactory, PlatformAdapter<C> platformAdapter, RootLiteral<C> rootLiteral) {
this.commandHandler = commandHandler;
this.commandContextFactory = commandContextFactory;
this.platformAdapter = platformAdapter;
@ -32,7 +32,7 @@ public class VelocityCommand<C extends VelocityCommandContext<C>> implements Raw
@Override
public boolean hasPermission(Invocation ctx) {
return platformAdapter.checkPermission(ctx, rootLiteral.permission());
return platformAdapter.checkPermission(commandContextFactory.createContext(rootLiteral, ctx.source()), rootLiteral.permission());
}
@Override