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; package site.lab0x13.scrow.commands;
import site.lab0x13.scrow.commands.model.CommandContext; 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.model.literal.RootLiteral;
import site.lab0x13.scrow.commands.parser.NodeTreeWalker; import site.lab0x13.scrow.commands.parser.NodeTreeWalker;
import site.lab0x13.scrow.commands.parser.WalkResult;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Stream; 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) { public void invoke(RootLiteral<C> rootLiteral, C ctx, Stream<String> args) {
var walkResult = nodeTreeWalker.walk(ctx, rootLiteral, args.iterator()); var walkResult = nodeTreeWalker.walk(ctx, rootLiteral, args.iterator(), false);
if (!walkResult.isValidUsage()) { var error = walkResult.executor().getFailure();
platformAdapter.sendMessage(ctx, rootLiteral.style().err(walkResult.message())); if (error.isPresent()) {
platformAdapter.sendMessage(ctx, rootLiteral.style().err(error.get()));
return; return;
} }
requireNonNull(walkResult.lastLiteral().executor())
.execute(walkResult.ctx()) var executor = walkResult.executor().getSuccess().orElseThrow();
requireNonNull(executor)
.execute(ctx)
.exceptionally(ex -> { .exceptionally(ex -> {
platformAdapter.sendMessage(ctx, rootLiteral.style().exception(ex)); platformAdapter.sendMessage(ctx, rootLiteral.style().exception(ex));
return null; 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) { public List<String> suggest(RootLiteral<C> rootLiteral, C ctx, String[] args, boolean trailingSpace) {
var walkResult = nodeTreeWalker.walk(ctx, rootLiteral, Arrays.stream(args).iterator()); // FIXME trailing space
Stream<String> stream; var walkResult = nodeTreeWalker.walk(ctx, rootLiteral, Arrays.stream(args).iterator(), trailingSpace);
String input = null; return walkResult.suggestionProvider().suggest(ctx, walkResult.input()).stream()
switch (walkResult) { .filter(s -> s.startsWith(walkResult.input()))
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))
.toList(); .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.api.Result;
import com.leakyabstractions.result.core.Results; import com.leakyabstractions.result.core.Results;
import site.lab0x13.scrow.commands.model.CommandContext; import site.lab0x13.scrow.commands.model.CommandContext;
import site.lab0x13.scrow.commands.model.Node;
import site.lab0x13.scrow.commands.model.SuggestionProvider; import site.lab0x13.scrow.commands.model.SuggestionProvider;
import java.util.List; import java.util.List;
@ -18,8 +17,7 @@ public record Argument<C extends CommandContext<C>, V>(
List<ArgumentRequirement<C, V>> requirements, List<ArgumentRequirement<C, V>> requirements,
SuggestionProvider<C> suggestionProvider, SuggestionProvider<C> suggestionProvider,
DefaultValueProvider<C, V> defaultValueProvider DefaultValueProvider<C, V> defaultValueProvider
) implements Node<C> { ) {
public static <C extends CommandContext<C>, V> ArgumentBuilder<C, V> builder(String id, ArgumentType<C, V> type) { public static <C extends CommandContext<C>, V> ArgumentBuilder<C, V> builder(String id, ArgumentType<C, V> type) {
return new ArgumentBuilder<>(id, 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 org.jetbrains.annotations.Nullable;
import site.lab0x13.scrow.commands.model.CommandContext; import site.lab0x13.scrow.commands.model.CommandContext;
import site.lab0x13.scrow.commands.model.CommandExecutor; 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 site.lab0x13.scrow.commands.model.argument.Argument;
import java.util.Arrays;
import java.util.Collection;
import java.util.List; import java.util.List;
/** /**
* @param <C> argument of the command context * @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) { static <C extends CommandContext<C>> StaticLiteralBuilder<C> builder(String... names) {
return new StaticLiteralBuilder<>(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<? extends LiteralMeta> subLiteralMetas();
List<Argument<C, ?>> arguments(); 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; package site.lab0x13.scrow.commands.parser;
import com.leakyabstractions.result.core.Results;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import site.lab0x13.scrow.commands.PlatformAdapter; import site.lab0x13.scrow.commands.PlatformAdapter;
import site.lab0x13.scrow.commands.model.CommandContext; 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.argument.Argument;
import site.lab0x13.scrow.commands.model.literal.Literal; 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 site.lab0x13.scrow.commands.model.literal.RootLiteral;
import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
import java.util.List;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
@ -23,7 +27,8 @@ public class NodeTreeWalker<C extends CommandContext<C>> {
public WalkResult<C> walk( public WalkResult<C> walk(
C ctx, C ctx,
RootLiteral<C> rootLiteral, RootLiteral<C> rootLiteral,
Iterator<String> iter Iterator<String> iter,
boolean suggestNext
) { ) {
var cursor = new Cursor(iter); var cursor = new Cursor(iter);
Literal<C> previousLiteral = null; Literal<C> previousLiteral = null;
@ -32,7 +37,7 @@ public class NodeTreeWalker<C extends CommandContext<C>> {
while (true) { while (true) {
for (var arg : literal.arguments()) { for (var arg : literal.arguments()) {
//noinspection unchecked //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) if (res != null)
return res; return res;
} }
@ -42,57 +47,47 @@ public class NodeTreeWalker<C extends CommandContext<C>> {
break; break;
var nextLiteral = literal.getSubLiteral(literalName); var nextLiteral = literal.getSubLiteral(literalName);
if (nextLiteral == null || !platformAdapter.checkPermission(ctx, nextLiteral.permission())) 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; previousLiteral = literal;
literal = nextLiteral; literal = nextLiteral;
} }
if (!literal.subLiteralMetas().isEmpty()) { var suggestionProvider = suggestionProvider(suggestNext ? literal : previousLiteral);
var isOptional = literal.executor() != null; if (literal.executor() == null)
return new WalkResult.LiteralExpected<>(ctx, literal, isOptional); return new WalkResult<>(Results.failure("Literal expected."), suggestionProvider);
} return new WalkResult<>(Results.success(requireNonNull(literal.executor())), suggestionProvider);
return new WalkResult.Complete<>(ctx, literal, previousLiteral == null ? Collections.emptyList() : previousLiteral.subLiteralMetas());
} }
private @Nullable WalkResult<C> walkArgument( private @Nullable WalkResult<C> walkArgument(
C ctx, C ctx,
Cursor cursor, Cursor cursor,
Literal<C> literal, CommandExecutor<C> executor,
Argument<C, Object> arg Argument<C, Object> arg
) { ) {
var rawInput = arg.type().greedy() ? cursor.remainingTokens() : cursor.nextToken(); var rawInput = arg.type().greedy() ? cursor.remainingTokens() : cursor.nextToken();
if (rawInput == null) { if (rawInput == null) {
var defaultValue = requireNonNull(arg.defaultValueProvider()).getDefault(ctx); var defaultValue = requireNonNull(arg.defaultValueProvider()).getDefault(ctx);
ctx.addParsedArgument(arg, "", defaultValue); 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<>(parseResult.mapSuccess(_ -> null), arg.suggestionProvider(), rawInput);
ctx.addParsedArgument(arg, rawInput, parseResult.getSuccess().orElseThrow().value());
} }
var parseResult = arg.parse(ctx, rawInput);
if (parseResult.hasFailure())
return new WalkResult.ArgumentIllegal<>(ctx, literal, false,
parseResult.getFailure().orElseThrow(), arg, rawInput);
ctx.addParsedArgument(arg, rawInput, parseResult.getSuccess().orElseThrow().value());
return null; return null;
} }
private static class Cursor { private SuggestionProvider<C> suggestionProvider(Literal<C> literal) {
private final Iterator<String> tokenIter; if (literal == null) return SuggestionProvider.none();
return (ctx, _) -> literal.subLiteralMetas().stream()
public Cursor(Iterator<String> tokenIter) { .filter(meta -> platformAdapter.checkPermission(ctx, meta.permission()))
this.tokenIter = tokenIter; .map(LiteralMeta::names)
} .flatMap(List::stream)
.toList();
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,84 +1,16 @@
package site.lab0x13.scrow.commands.parser; 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.CommandContext;
import site.lab0x13.scrow.commands.model.argument.Argument; import site.lab0x13.scrow.commands.model.CommandExecutor;
import site.lab0x13.scrow.commands.model.literal.Literal; import site.lab0x13.scrow.commands.model.SuggestionProvider;
import site.lab0x13.scrow.commands.model.literal.LiteralMeta;
import java.util.List; public record WalkResult<C extends CommandContext<C>>(
Result<CommandExecutor<C>, String> executor,
public sealed interface WalkResult<C extends CommandContext<C>> { SuggestionProvider<C> suggestionProvider,
C ctx(); String input
) {
Literal<C> lastLiteral(); public WalkResult(Result<CommandExecutor<C>, String> executor, SuggestionProvider<C> suggestionProvider) {
this(executor, suggestionProvider, "");
boolean isValidUsage();
@Nullable String message();
record ArgumentIllegal<C extends CommandContext<C>>(
C ctx,
Literal<C> lastLiteral,
boolean isValidUsage,
String message,
Argument<C, ?> arg,
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;
}
} }
} }

View file

@ -93,7 +93,7 @@ class CommandHandlerTest {
@Test @Test
void testFailOnBadLiteral() { void testFailOnBadLiteral() {
commandHandler.invoke(rootLiteral, new CommandContextImpl(), Stream.of("BAD_LITERAL")); 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 @Test

View file

@ -1,2 +1,2 @@
GROUP = "site.lab0x13.scrow" 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 CommandHandler<C> commandHandler;
private final CommandContextFactory<C> commandContextFactory; private final CommandContextFactory<C> commandContextFactory;
private final PlatformAdapter platformAdapter; private final PlatformAdapter<C> platformAdapter;
private final RootLiteral<C> rootLiteral; 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.commandHandler = commandHandler;
this.commandContextFactory = commandContextFactory; this.commandContextFactory = commandContextFactory;
this.platformAdapter = platformAdapter; this.platformAdapter = platformAdapter;
@ -32,7 +32,7 @@ public class VelocityCommand<C extends VelocityCommandContext<C>> implements Raw
@Override @Override
public boolean hasPermission(Invocation ctx) { public boolean hasPermission(Invocation ctx) {
return platformAdapter.checkPermission(ctx, rootLiteral.permission()); return platformAdapter.checkPermission(commandContextFactory.createContext(rootLiteral, ctx.source()), rootLiteral.permission());
} }
@Override @Override