initial commit
Some checks failed
Build and Deploy artifact / build (push) Failing after 11s

This commit is contained in:
kento2 2026-08-25 15:37:39 +02:00
commit 7c322707ba
51 changed files with 1488 additions and 0 deletions

7
.bazelrc Normal file
View file

@ -0,0 +1,7 @@
build --java_language_version=25
build --java_runtime_version=remotejdk_25
build --tool_java_language_version=25
build --tool_java_runtime_version=remotejdk_25
build --define=maven_repo=https://git.lab0x13.site/api/packages/scrow/maven
#build --remote_cache=https://cache.lab0x13.site:443
#build --remote_upload_local_results=false

1
.bazelversion Normal file
View file

@ -0,0 +1 @@
9.2.0

View file

@ -0,0 +1,31 @@
name: "Build and Deploy artifact"
on:
push:
branches: [master, main]
jobs:
build:
runs-on: docker
container:
image: "git.lab0x13.site/scrow/bazel-java:0.12"
steps:
- uses: actions/checkout@v7
- uses: actions/cache@v6
with:
path: /var/cache/bazel
key: bazel-${{ env.FORGEJO_REPOSITORY }}
- id: deploy
env:
CACHE_USERNAME: ${{ secrets.CACHE_USERNAME }}
CACHE_PASSWORD: ${{ secrets.CACHE_PASSWORD }}
MAVEN_USER: ${{ secrets.MAVENREPO_USERNAME }}
MAVEN_PASSWORD: ${{ secrets.MAVENREPO_PASSWORD }}
run: |
for target in //bukkit:bukkit.publish //core:core.publish
do
bazel \
--output_user_root=/var/cache/bazel \
run "$target" \
--remote_cache="https://${CACHE_USERNAME}:${CACHE_PASSWORD}@cache.lab0x13.site:443" \
--remote_upload_local_results \
done

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
.bazelbsp/
.idea/
bazel-bin
bazel-genfiles
bazel-out
bazel-testlogs

0
BUILD Normal file
View file

39
MODULE.bazel Normal file
View file

@ -0,0 +1,39 @@
module(name = "scrow_commands")
bazel_dep(name = "rules_java", version = "9.7.0")
bazel_dep(name = "rules_jvm_external", version = "7.1")
bazel_dep(name = "contrib_rules_jvm", version = "0.34.0")
bazel_dep(name = "apple_rules_lint", version = "0.4.0")
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
maven.install(
name = "maven",
artifacts = [
"net.kyori:adventure-api:5.2.0",
"org.jetbrains:annotations:26.0.2-1",
"com.google.guava:guava:33.5.0-jre",
"com.leakyabstractions:result-api:1.0.1.0",
"com.leakyabstractions:result:1.0.2.0",
"io.papermc.paper:paper-api:[26.2.build,)",
# TESTS
"net.kyori:adventure-text-serializer-plain:5.2.0",
"org.junit.jupiter:junit-jupiter-engine",
"org.junit.jupiter:junit-jupiter-api",
"org.junit.platform:junit-platform-launcher",
"org.junit.platform:junit-platform-reporting",
],
boms = [
"org.junit:junit-bom:5.14.4",
],
repositories = [
"https://repo1.maven.org/maven2",
"https://repo.papermc.io/repository/maven-public/",
],
known_contributing_modules = ["protobuf"],
)
use_repo(maven, "maven")
linter = use_extension("@apple_rules_lint//lint:extensions.bzl", "linter")
linter.configure(name = "java-spotbugs", config = "@contrib_rules_jvm//java:spotbugs-default-config")
use_repo(linter, "apple_linters")

23
bukkit/BUILD.bazel Normal file
View file

@ -0,0 +1,23 @@
load("@rules_jvm_external//:defs.bzl", "artifact", "java_export")
load("//:defs.bzl", "GROUP", "VERSION")
java_export(
name = "bukkit",
maven_coordinates = GROUP + ":commands-bukkit:" + VERSION,
srcs = glob(["site/lab0x13/scrow/commands/**/*.java"]),
visibility = ["//visibility:public"],
deps = [
"//common:common-lib",
artifact("org.jetbrains:annotations"),
artifact("com.google.guava:guava"),
artifact("net.kyori:adventure-api"),
artifact("com.leakyabstractions:result-api"),
artifact("com.leakyabstractions:result"),
artifact("io.papermc.paper:paper-api"),
],
exports = [
"//common:common-lib",
artifact("com.leakyabstractions:result-api"),
artifact("com.leakyabstractions:result"),
]
)

View file

@ -0,0 +1,24 @@
package site.lab0x13.scrow.commands.bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import site.lab0x13.scrow.commands.model.AbstractCommandContext;
import site.lab0x13.scrow.commands.model.CommandContext;
public class BukkitCommandContext<C extends CommandContext<C>> extends AbstractCommandContext<C> {
private final CommandSender sender;
public BukkitCommandContext(CommandSender sender) {
this.sender = sender;
}
public CommandSender sender() {
return sender;
}
public Player player() {
return (Player) sender();
}
}

View file

@ -0,0 +1,16 @@
package site.lab0x13.scrow.commands.bukkit;
import net.kyori.adventure.text.Component;
import site.lab0x13.scrow.commands.PlatformAdapter;
class BukkitPlatform<C extends BukkitCommandContext<C>> implements PlatformAdapter<C> {
@Override
public boolean checkPermission(C ctx, String permission) {
return ctx.sender().hasPermission(permission);
}
@Override
public void sendMessage(C ctx, Component message) {
ctx.sender().sendMessage(message);
}
}

View file

@ -0,0 +1,36 @@
package site.lab0x13.scrow.commands.bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import site.lab0x13.scrow.commands.CommandHandler;
import site.lab0x13.scrow.commands.model.literal.RootLiteral;
import java.util.stream.Stream;
public class BukkitScrowCommand<C extends BukkitCommandContext<C>> extends Command {
private final RootLiteral<C> rootLiteral;
private final CommandContextFactory<C> commandContextFactory;
private final CommandHandler<C> commandHandler;
protected BukkitScrowCommand(CommandHandler<C> commandHandler, CommandContextFactory<C> commandContextFactory, RootLiteral<C> rootLiteral) {
super(rootLiteral.names().getFirst());
this.commandHandler = commandHandler;
this.commandContextFactory = commandContextFactory;
this.rootLiteral = rootLiteral;
this.setAliases(rootLiteral.names());
this.setPermission(rootLiteral.permission());
}
@Override
public boolean execute(@NotNull CommandSender commandSender, @NotNull String commandLabel, String @NotNull [] args) {
commandHandler.invoke(
rootLiteral,
commandContextFactory.createContext(commandSender),
Stream.of(args).filter(str -> !str.isEmpty())
);
return true;
}
}

View file

@ -0,0 +1,9 @@
package site.lab0x13.scrow.commands.bukkit;
import org.bukkit.command.CommandSender;
@FunctionalInterface
public interface CommandContextFactory<C extends BukkitCommandContext<C>> {
C createContext(CommandSender sender);
}

View file

@ -0,0 +1,50 @@
package site.lab0x13.scrow.commands.bukkit;
import org.bukkit.Bukkit;
import org.bukkit.command.SimpleCommandMap;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.Nullable;
import site.lab0x13.scrow.commands.CommandHandler;
import site.lab0x13.scrow.commands.IScrowCommands;
import site.lab0x13.scrow.commands.model.literal.RootLiteral;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
public class ScrowCommands<C extends BukkitCommandContext<C>> implements IScrowCommands<C> {
private final CommandContextFactory<C> commandContextFactory;
private final List<RootLiteral<C>> rootLiterals = new ArrayList<>();
private final SimpleCommandMap commandMap;
private final CommandHandler<C> commandHandler;
public ScrowCommands(Plugin plugin, CommandContextFactory<C> commandContextFactory) {
try {
Field field = Bukkit.getPluginManager().getClass().getDeclaredField("commandMap");
field.setAccessible(true);
commandMap = (SimpleCommandMap) field.get(Bukkit.getPluginManager());
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new IllegalStateException("failed getting command map", e);
}
this.commandContextFactory = commandContextFactory;
this.commandHandler = new CommandHandler<C>(new BukkitPlatform<>());
Bukkit.getPluginManager().registerEvents(new TabCompleteListener<>(this, commandHandler, commandContextFactory), plugin);
}
@Override
public void register(RootLiteral<C> rootLiteral) {
rootLiterals.add(rootLiteral);
commandMap.register(rootLiteral.names().getFirst(),
new BukkitScrowCommand<>(commandHandler, commandContextFactory, rootLiteral));
}
@Override
public @Nullable RootLiteral<C> getRootLiteral(String name) {
return rootLiterals.stream()
.filter(literal -> literal.names().contains(name))
.findAny()
.orElse(null);
}
}

View file

@ -0,0 +1,46 @@
package site.lab0x13.scrow.commands.bukkit;
import com.destroystokyo.paper.event.server.AsyncTabCompleteEvent;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import site.lab0x13.scrow.commands.CommandHandler;
import java.util.Arrays;
public class TabCompleteListener<C extends BukkitCommandContext<C>> implements Listener {
private final ScrowCommands<C> commandAPI;
private final CommandHandler<C> commandHandler;
private final CommandContextFactory<C> commandContextFactory;
public TabCompleteListener(ScrowCommands<C> commandAPI, CommandHandler<C> commandHandler, CommandContextFactory<C> commandContextFactory) {
this.commandAPI = commandAPI;
this.commandHandler = commandHandler;
this.commandContextFactory = commandContextFactory;
}
@EventHandler(ignoreCancelled = true)
public void onAsyncTabComplete(AsyncTabCompleteEvent ev) {
if (!ev.isCommand()) return;
var args = ev.getBuffer().substring(1).split(" ");
if (args.length == 0) return;
var label = args[0];
{
// commands can be called with a '<plugin name>:' prefix. strip it.
int l = label.indexOf(':');
if (l >= 0)
label = label.substring(l + 1);
}
var literal = commandAPI.getRootLiteral(label);
if (literal == null) return;
ev.setHandled(true);
ev.setCompletions(commandHandler.suggest(
literal,
commandContextFactory.createContext(ev.getSender()),
Arrays.copyOfRange(args, 1, args.length),
ev.getBuffer().endsWith(" ")
));
}
}

12
common/BUILD.bazel Normal file
View file

@ -0,0 +1,12 @@
load("@rules_jvm_external//:defs.bzl", "java_export")
load("//common:deps.bzl", "COMMON_DEPS", "COMMON_DEPS_EXPORT")
load("//:defs.bzl", "GROUP", "VERSION")
java_export(
name = "common",
maven_coordinates = GROUP + ":commands-common:" + VERSION,
srcs = glob(["site/lab0x13/scrow/**/*.java"]),
visibility = ["//visibility:public"],
deps = COMMON_DEPS,
exports = COMMON_DEPS_EXPORT,
)

14
common/deps.bzl Normal file
View file

@ -0,0 +1,14 @@
load("@rules_jvm_external//:defs.bzl", "artifact")
COMMON_DEPS = [
artifact("org.jetbrains:annotations"),
artifact("com.google.guava:guava"),
artifact("net.kyori:adventure-api"),
artifact("com.leakyabstractions:result-api"),
artifact("com.leakyabstractions:result"),
]
COMMON_DEPS_EXPORT = [
artifact("com.leakyabstractions:result-api"),
artifact("com.leakyabstractions:result"),
]

View file

@ -0,0 +1,67 @@
package site.lab0x13.scrow.commands;
import site.lab0x13.scrow.commands.model.CommandContext;
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.Collections;
import java.util.List;
import java.util.stream.Stream;
import static java.util.Objects.requireNonNull;
public class CommandHandler<C extends CommandContext<C>> {
private final NodeTreeWalker<C> nodeTreeWalker;
private final PlatformAdapter<C> platformAdapter;
public CommandHandler(PlatformAdapter<C> platformAdapter) {
this.nodeTreeWalker = new NodeTreeWalker<>(platformAdapter);
this.platformAdapter = platformAdapter;
}
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()));
return;
}
requireNonNull(walkResult.lastLiteral().executor())
.execute(walkResult.ctx())
.exceptionally(ex -> {
platformAdapter.sendMessage(ctx, rootLiteral.style().exception(ex));
return null;
});
}
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 = res.lastLiteral().subLiteralNames().stream();
input = res.input();
}
case WalkResult.ArgumentIllegal<C> res -> {
stream = res.arg().suggestionProvider().suggest(ctx, res.input()).stream();
input = res.input();
}
case WalkResult.LiteralExpected<C> res when trailingSpace ->
stream = res.lastLiteral().subLiteralNames().stream();
case WalkResult.ArgumentExpected<C> res when trailingSpace ->
stream = res.arg().suggestionProvider().suggest(ctx, "").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

@ -0,0 +1,12 @@
package site.lab0x13.scrow.commands;
import org.jetbrains.annotations.Nullable;
import site.lab0x13.scrow.commands.model.CommandContext;
import site.lab0x13.scrow.commands.model.literal.RootLiteral;
public interface IScrowCommands<C extends CommandContext<C>> {
void register(RootLiteral<C> rootLiteral);
@Nullable RootLiteral<C> getRootLiteral(String name);
}

View file

@ -0,0 +1,10 @@
package site.lab0x13.scrow.commands;
import net.kyori.adventure.text.Component;
public interface PlatformAdapter<C> {
boolean checkPermission(C ctx, String permission);
void sendMessage(C ctx, Component message);
}

View file

@ -0,0 +1,23 @@
package site.lab0x13.scrow.commands.model;
import site.lab0x13.scrow.commands.model.argument.Argument;
import site.lab0x13.scrow.commands.model.argument.ParsedArgument;
import java.util.HashMap;
import java.util.Map;
public abstract class AbstractCommandContext<C extends CommandContext<C>> implements CommandContext<C> {
private final Map<String, ParsedArgument<?, ?>> parsedArguments = new HashMap<>();
@SuppressWarnings("unchecked")
@Override
public <V> ParsedArgument<C, V> getArgument(String id) {
return (ParsedArgument<C, V>) parsedArguments.get(id);
}
@Override
public <V> void addParsedArgument(Argument<?, V> argument, String rawValue, V value) {
parsedArguments.put(argument.id(), new ParsedArgument<>(argument, rawValue, value));
}
}

View file

@ -0,0 +1,27 @@
package site.lab0x13.scrow.commands.model;
import site.lab0x13.scrow.commands.model.argument.Argument;
import site.lab0x13.scrow.commands.model.argument.ParsedArgument;
/**
* @param <C> argument of this
*/
public interface CommandContext<C extends CommandContext<C>> {
<V> ParsedArgument<C, V> getArgument(String id);
default <V> V getArg(String id) {
ParsedArgument<C, V> arg = getArgument(id);
return arg.value();
}
default <V> V getArg(Argument<C, V> arg) {
return getArg(arg.id());
}
default <V> ParsedArgument<C, V> getArgument(Argument<C, V> arg) {
return getArgument(arg.id());
}
<V> void addParsedArgument(Argument<?, V> argument, String rawValue, V value);
}

View file

@ -0,0 +1,22 @@
package site.lab0x13.scrow.commands.model;
import java.util.concurrent.CompletableFuture;
/**
* @param <C> argument of command context
*/
public interface CommandExecutor<C extends CommandContext<C>> {
CompletableFuture<?> execute(C ctx);
interface Sync<C extends CommandContext<C>> extends CommandExecutor<C> {
void executeSync(C ctx);
@Override
default CompletableFuture<?> execute(C ctx) {
executeSync(ctx);
return CompletableFuture.completedFuture(Void.TYPE);
}
}
}

View file

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

View file

@ -0,0 +1,16 @@
package site.lab0x13.scrow.commands.model;
import java.util.Collections;
import java.util.List;
/**
* @param <C> argument of command context
*/
public interface SuggestionProvider<C extends CommandContext<C>> {
static <C extends CommandContext<C>> SuggestionProvider<C> none() {
return (_, _) -> Collections.emptyList();
}
List<String> suggest(C ctx, String input);
}

View file

@ -0,0 +1,41 @@
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;
/**
* @param <C> argument of command context
* @param <V> argument of argument value
*/
public record Argument<C extends CommandContext<C>, V>(
String id,
ArgumentType<C, V> type,
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);
}
public Result<ParsedArgument<C, V>, String> parse(C ctx, String rawInput) {
var parseResult = type.parseInput(rawInput);
if (parseResult.hasFailure())
return parseResult.mapSuccess(_ -> null);
var value = parseResult.getSuccess().orElseThrow();
for (var requirement : requirements) {
var isAllowed = requirement.predicate().check(ctx, value);
if (!isAllowed)
return Results.failure(requirement.errorMessage());
}
return Results.success(new ParsedArgument<>(this, rawInput, value));
}
}

View file

@ -0,0 +1,55 @@
package site.lab0x13.scrow.commands.model.argument;
import org.jetbrains.annotations.Nullable;
import site.lab0x13.scrow.commands.model.CommandContext;
import site.lab0x13.scrow.commands.model.SuggestionProvider;
import java.util.ArrayList;
import java.util.List;
/**
* @param <C> argument of command context
* @param <V> argument of argument value
*/
public final class ArgumentBuilder<C extends CommandContext<C>, V> {
private final String id;
private final ArgumentType<C, V> type;
private DefaultValueProvider<C, V> defaultValueProvider = DefaultValueProvider.none();
private @Nullable SuggestionProvider<C> suggestionProvider = null;
private final List<ArgumentRequirement<C, V>> requirements = new ArrayList<>();
ArgumentBuilder(String id, ArgumentType<C, V> type) {
this.id = id;
this.type = type;
}
public ArgumentBuilder<C, V> withRequirement(ArgumentPredicate<C, V> predicate, String errorMessage) {
this.requirements.add(new ArgumentRequirement<>(predicate, errorMessage));
return this;
}
public ArgumentBuilder<C, V> withDefaultValue(@Nullable DefaultValueProvider<C, V> defaultValue) {
this.defaultValueProvider = defaultValue;
return this;
}
/**
* @param suggestionProvider if null, use the argument's suggestion provider
*/
public ArgumentBuilder<C, V> withSuggestionProvider(@Nullable SuggestionProvider<C> suggestionProvider) {
this.suggestionProvider = suggestionProvider;
return this;
}
public Argument<C, V> build() {
return new Argument<>(
id,
type,
new ArrayList<>(requirements),
suggestionProvider != null ? suggestionProvider : type.defaultSuggestionProvider(),
defaultValueProvider
);
}
}

View file

@ -0,0 +1,12 @@
package site.lab0x13.scrow.commands.model.argument;
import site.lab0x13.scrow.commands.model.CommandContext;
/**
* @param <C> argument of command context
* @param <V> argument of argument value
*/
@FunctionalInterface
public interface ArgumentPredicate<C extends CommandContext<C>, V> {
boolean check(C ctx, V value);
}

View file

@ -0,0 +1,13 @@
package site.lab0x13.scrow.commands.model.argument;
import site.lab0x13.scrow.commands.model.CommandContext;
/**
* @param <C> argument of command context
* @param <V> argument of argument value
*/
public record ArgumentRequirement<C extends CommandContext<C>, V>(
ArgumentPredicate<C, V> predicate,
String errorMessage
) {
}

View file

@ -0,0 +1,28 @@
package site.lab0x13.scrow.commands.model.argument;
import com.leakyabstractions.result.api.Result;
import site.lab0x13.scrow.commands.model.CommandContext;
import site.lab0x13.scrow.commands.model.SuggestionProvider;
/**
* @param <C> argument of command context
* @param <V> argument of parsed argument value
*/
public interface ArgumentType<C extends CommandContext<C>, V> {
Result<V, String> parseInput(String input);
/**
* @return default suggestion provider for the argument, if not overwritten by the argument
*/
SuggestionProvider<C> defaultSuggestionProvider();
/**
* @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,14 @@
package site.lab0x13.scrow.commands.model.argument;
import org.jetbrains.annotations.Nullable;
import site.lab0x13.scrow.commands.model.CommandContext;
@FunctionalInterface
public interface DefaultValueProvider<C extends CommandContext<C>, V> {
static <V, C extends CommandContext<C>> DefaultValueProvider<C, V> none() {
return _ -> null;
}
@Nullable V getDefault(C ctx);
}

View file

@ -0,0 +1,14 @@
package site.lab0x13.scrow.commands.model.argument;
import site.lab0x13.scrow.commands.model.CommandContext;
/**
* @param <C> argument of command context
* @param <V> argument of argument value
*/
public record ParsedArgument<C extends CommandContext<C>, V>(
Argument<C, V> argument,
String rawValue,
V value
) {
}

View file

@ -0,0 +1,23 @@
package site.lab0x13.scrow.commands.model.argument.types;
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.SuggestionProvider;
import site.lab0x13.scrow.commands.model.argument.ArgumentType;
import java.util.List;
public class BooleanArgumentType<C extends CommandContext<C>> implements ArgumentType<C, Boolean> {
@Override
public Result<Boolean, String> parseInput(String input) {
return Results.ofCallable(() -> Boolean.parseBoolean(input))
.mapFailure(_ -> "Must be true or false.");
}
@Override
public SuggestionProvider<C> defaultSuggestionProvider() {
return (_, _) -> List.of("true", "false");
}
}

View file

@ -0,0 +1,20 @@
package site.lab0x13.scrow.commands.model.argument.types;
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.SuggestionProvider;
import site.lab0x13.scrow.commands.model.argument.ArgumentType;
public class DoubleArgumentType<C extends CommandContext<C>> implements ArgumentType<C, Double> {
@Override
public Result<Double, String> parseInput(String input) {
return Results.ofCallable(() -> Double.parseDouble(input))
.mapFailure(_ -> "Not a valid double.");
}
@Override
public SuggestionProvider<C> defaultSuggestionProvider() {
return SuggestionProvider.none();
}
}

View file

@ -0,0 +1,21 @@
package site.lab0x13.scrow.commands.model.argument.types;
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.SuggestionProvider;
import site.lab0x13.scrow.commands.model.argument.ArgumentType;
public class IntegerArgumentType<C extends CommandContext<C>> implements ArgumentType<C, Integer> {
@Override
public Result<Integer, String> parseInput(String string) {
return Results.ofCallable(() -> Integer.parseInt(string))
.mapFailure(_ -> "Not a number.");
}
@Override
public SuggestionProvider<C> defaultSuggestionProvider() {
return SuggestionProvider.none();
}
}

View file

@ -0,0 +1,25 @@
package site.lab0x13.scrow.commands.model.argument.types;
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.SuggestionProvider;
import site.lab0x13.scrow.commands.model.argument.ArgumentType;
public record StringArgumentType<C extends CommandContext<C>>(boolean greedy)
implements ArgumentType<C, String> {
public StringArgumentType() {
this(false);
}
@Override
public Result<String, String> parseInput(String string) {
return Results.success(string);
}
@Override
public SuggestionProvider<C> defaultSuggestionProvider() {
return SuggestionProvider.none();
}
}

View file

@ -0,0 +1,46 @@
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.argument.Argument;
import java.util.List;
import static com.google.common.base.Preconditions.checkArgument;
public abstract class DynamicLiteral<C extends CommandContext<C>> implements Literal<C>{
private final List<String> names;
private final @Nullable CommandExecutor<C> executor;
private final List<Argument<C, ?>> arguments;
private final String permission;
public DynamicLiteral(List<String> names, @Nullable CommandExecutor<C> executor, List<Argument<C, ?>> arguments, String permission) {
checkArgument(!names.isEmpty(), "at least one name expected");
this.names = names;
this.executor = executor;
this.arguments = arguments;
this.permission = permission;
}
@Override
public List<String> names() {
return names;
}
@Override
public @Nullable CommandExecutor<C> executor() {
return executor;
}
@Override
public List<Argument<C, ?>> arguments() {
return arguments;
}
@Override
public String permission() {
return permission;
}
}

View file

@ -0,0 +1,55 @@
package site.lab0x13.scrow.commands.model.literal;
import org.jetbrains.annotations.Nullable;
import site.lab0x13.scrow.commands.model.CommandContext;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;
public final class DynamicLiteralBuilder<C extends CommandContext<C>> extends LiteralBuilder<C, DynamicLiteralBuilder<C>> {
private Supplier<List<String>> subLiteralNamesSupplier = Collections::emptyList;
private Function<String, @Nullable Literal<C>> subLiteralProvider = _ -> null;
DynamicLiteralBuilder(String... names) {
super(names);
}
@Override
protected DynamicLiteralBuilder<C> getThis() {
return this;
}
@Override
public Literal<C> build() {
return new DynamicLiteral<>(
Arrays.asList(this.names),
this.executor,
this.arguments,
this.permission
) {
@Override
public @Nullable Literal<C> getSubLiteral(String name) {
return subLiteralProvider.apply(name);
}
@Override
public List<String> subLiteralNames() {
return subLiteralNamesSupplier.get();
}
};
}
public DynamicLiteralBuilder<C> withSubLiteralNames(Supplier<List<String>> names) {
subLiteralNamesSupplier = names;
return this;
}
public DynamicLiteralBuilder<C> withSubLiteralProvider(Function<String, @Nullable Literal<C>> literalProvider) {
this.subLiteralProvider = literalProvider;
return this;
}
}

View file

@ -0,0 +1,44 @@
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.List;
/**
* @param <C> argument of the command context
*/
public interface Literal<C extends CommandContext<C>> extends Node<C> {
static <C extends CommandContext<C>> StaticLiteralBuilder<C> builder(String... names) {
return new StaticLiteralBuilder<>(names);
}
static <C extends CommandContext<C>> DynamicLiteralBuilder<C> dynamic(String... names) {
return new DynamicLiteralBuilder<>(names);
}
List<String> names();
@Nullable CommandExecutor<C> executor();
@Nullable Literal<C> getSubLiteral(String name);
/**
* @return aliases/names of literals
*/
List<String> subLiteralNames();
List<Argument<C, ?>> arguments();
String permission();
@Override
default SuggestionProvider<C> suggestionProvider() {
return (_, _) -> subLiteralNames();
}
}

View file

@ -0,0 +1,53 @@
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.argument.Argument;
import java.util.ArrayList;
import java.util.List;
/**
* @param <C> argument of command context
* @param <B> argument of this/builder
*/
public abstract class LiteralBuilder<C extends CommandContext<C>,B extends LiteralBuilder<C, B>> {
protected final String[] names;
protected @Nullable CommandExecutor<C> executor;
protected @Nullable String permission;
protected final List<Argument<C, ?>> arguments = new ArrayList<>();
protected abstract B getThis();
public abstract Literal<C> build();
protected LiteralBuilder(String... names) {
this.names = names;
}
public final RootLiteral<C> asRootLiteral(MessageStyle style) {
return new RootLiteralWrapper<>(build(), style);
}
public final B withExecutor(@Nullable CommandExecutor<C> executor) {
this.executor = executor;
return getThis();
}
public final B withSyncExecutor(@Nullable CommandExecutor.Sync<C> executor) {
this.executor = executor;
return getThis();
}
public final B withPermission(@Nullable String permission) {
this.permission = permission;
return getThis();
}
public final B withArgument(Argument<C, ?> argument) {
arguments.add(argument);
return getThis();
}
}

View file

@ -0,0 +1,32 @@
package site.lab0x13.scrow.commands.model.literal;
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 site.lab0x13.scrow.commands.model.literal;
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 site.lab0x13.scrow.commands.model.literal;
import site.lab0x13.scrow.commands.model.CommandContext;
public interface RootLiteral<C extends CommandContext<C>> extends Literal<C>{
MessageStyle style();
}

View file

@ -0,0 +1,53 @@
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.argument.Argument;
import java.util.List;
public class RootLiteralWrapper<C extends CommandContext<C>> implements RootLiteral<C> {
private final Literal<C> literal;
private final MessageStyle messageStyle;
public RootLiteralWrapper(Literal<C> literal, MessageStyle messageStyle) {
this.literal = literal;
this.messageStyle = messageStyle;
}
@Override
public MessageStyle style() {
return messageStyle;
}
@Override
public List<String> names() {
return literal.names();
}
@Override
public @Nullable CommandExecutor<C> executor() {
return literal.executor();
}
@Override
public @Nullable Literal<C> getSubLiteral(String name) {
return literal.getSubLiteral(name);
}
@Override
public List<String> subLiteralNames() {
return literal.subLiteralNames();
}
@Override
public List<Argument<C, ?>> arguments() {
return literal.arguments();
}
@Override
public String permission() {
return literal.permission();
}
}

View file

@ -0,0 +1,38 @@
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.argument.Argument;
import java.util.List;
public class StaticLiteral<C extends CommandContext<C>> extends DynamicLiteral<C> {
private final List<Literal<C>> subLiterals;
public StaticLiteral(
List<String> names,
@Nullable CommandExecutor<C> executor,
List<Argument<C, ?>> arguments,
String permission, List<Literal<C>> subLiterals
) {
super(names, executor, arguments, permission);
this.subLiterals = subLiterals;
}
@Override
public @Nullable Literal<C> getSubLiteral(String name) {
return subLiterals.stream()
.filter(literal -> literal.names().contains(name))
.findAny()
.orElse(null);
}
@Override
public List<String> subLiteralNames() {
return subLiterals.stream()
.flatMap(literal -> literal.names().stream())
.toList();
}
}

View file

@ -0,0 +1,37 @@
package site.lab0x13.scrow.commands.model.literal;
import site.lab0x13.scrow.commands.model.CommandContext;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public final class StaticLiteralBuilder<C extends CommandContext<C>> extends LiteralBuilder<C, StaticLiteralBuilder<C>> {
private final List<Literal<C>> subLiterals = new ArrayList<>();
StaticLiteralBuilder(String... names) {
super(names);
}
@Override
protected StaticLiteralBuilder<C> getThis() {
return this;
}
@Override
public Literal<C> build() {
return new StaticLiteral<>(
Arrays.asList(this.names),
this.executor,
this.arguments,
this.permission,
this.subLiterals
);
}
public StaticLiteralBuilder<C> withSubLiteral(Literal<C> subLiteral) {
subLiterals.add(subLiteral);
return this;
}
}

View file

@ -0,0 +1,100 @@
package site.lab0x13.scrow.commands.parser;
import org.jetbrains.annotations.Nullable;
import site.lab0x13.scrow.commands.PlatformAdapter;
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.RootLiteral;
import java.util.Iterator;
import static java.util.Objects.requireNonNull;
public class NodeTreeWalker<C extends CommandContext<C>> {
private final PlatformAdapter<C> platformAdapter;
public NodeTreeWalker(PlatformAdapter<C> platformAdapter) {
this.platformAdapter = platformAdapter;
}
public WalkResult<C> walk(
C ctx,
RootLiteral<C> rootLiteral,
Iterator<String> iter
) {
var cursor = new Cursor(iter);
Literal<C> literal = rootLiteral;
String token;
while (true) {
for (var arg : literal.arguments()) {
//noinspection unchecked
var res = walkArgument(ctx, cursor, literal, (Argument<C, Object>) arg);
if (res != null)
return res;
}
token = cursor.nextToken();
if (token == null)
break;
var nextLiteral = literal.getSubLiteral(token);
if (nextLiteral == null)
return new WalkResult.LiteralUnknown<>(ctx, literal, token);
if (nextLiteral.permission() != null && platformAdapter.checkPermission(ctx, nextLiteral.permission()))
return new WalkResult.LiteralUnknown<>(ctx, literal, token);
literal = nextLiteral;
}
if (!literal.subLiteralNames().isEmpty()) {
var isOptional = literal.executor() != null;
return new WalkResult.LiteralExpected<>(ctx, literal, isOptional);
}
return new WalkResult.Complete<>(ctx, literal);
}
private @Nullable WalkResult<C> walkArgument(
C ctx,
Cursor cursor,
Literal<C> literal,
Argument<C, Object> arg
) {
var rawInput = arg.type().greedy() ? cursor.remainingTokens() : cursor.nextToken();
if (rawInput == null) {
var value = requireNonNull(arg.defaultValueProvider()).getDefault(ctx);
if (value == null)
return new WalkResult.ArgumentExpected<>(ctx, literal, false, arg);
ctx.addParsedArgument(arg, "", value);
return null;
}
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;
}
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 site.lab0x13.scrow.commands.parser;
import org.jetbrains.annotations.Nullable;
import site.lab0x13.scrow.commands.model.CommandContext;
import site.lab0x13.scrow.commands.model.argument.Argument;
import site.lab0x13.scrow.commands.model.literal.Literal;
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,
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
) implements WalkResult<C> {
@Override
public boolean isValidUsage() {
return true;
}
@Override
public @Nullable String message() {
return null;
}
}
}

16
common/tests/BUILD.bazel Normal file
View file

@ -0,0 +1,16 @@
load("@contrib_rules_jvm//java:defs.bzl", "java_test_suite", "JUNIT5_DEPS")
load("@rules_jvm_external//:defs.bzl", "artifact")
load("//common:deps.bzl", "COMMON_DEPS")
java_test_suite(
name = "all_tests",
srcs = glob(["**/*.java"]),
test_suffixes = ["Test.java"],
runner = "junit5",
size = "small",
deps = [
"//common",
artifact("net.kyori:adventure-text-serializer-plain"),
artifact("org.junit.jupiter:junit-jupiter-api"),
] + JUNIT5_DEPS + COMMON_DEPS,
)

View file

@ -0,0 +1,6 @@
package site.lab0x13.scrow.commands;
import site.lab0x13.scrow.commands.model.AbstractCommandContext;
public class CommandContextImpl extends AbstractCommandContext<CommandContextImpl> {
}

View file

@ -0,0 +1,108 @@
package site.lab0x13.scrow.commands;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import site.lab0x13.scrow.commands.model.argument.Argument;
import site.lab0x13.scrow.commands.model.argument.types.IntegerArgumentType;
import site.lab0x13.scrow.commands.model.argument.types.StringArgumentType;
import site.lab0x13.scrow.commands.model.literal.Literal;
import site.lab0x13.scrow.commands.model.literal.MessageStyle;
import site.lab0x13.scrow.commands.model.literal.RootLiteral;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.Collections;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
class CommandHandlerTest {
private static final ByteArrayOutputStream outContent = new ByteArrayOutputStream();
private static final PrintStream originalOut = System.out;
private final CommandHandler<CommandContextImpl> commandHandler = new CommandHandler<>(new TestPlatform(Collections.emptyList()));
private final Argument<CommandContextImpl, String> playerArg =
Argument.builder("player", new StringArgumentType<CommandContextImpl>())
.build();
private final Argument<CommandContextImpl, Integer> amountArg =
Argument.builder("amount", new IntegerArgumentType<CommandContextImpl>())
.build();
private final RootLiteral<CommandContextImpl> rootLiteral = Literal.<CommandContextImpl>builder("coins")
.withSyncExecutor(_ -> {
System.out.println("default executor");
})
.withSubLiteral(Literal.<CommandContextImpl>builder("get")
.withArgument(playerArg)
.withSyncExecutor(ctx -> {
var player = ctx.getArg(playerArg);
System.out.println("get executor: player=" + player);
})
.build()
)
.withSubLiteral(Literal.<CommandContextImpl>builder("set")
.withArgument(playerArg)
.withArgument(amountArg)
.withSyncExecutor(ctx -> {
var player = ctx.getArg(playerArg);
var amount = ctx.getArg(amountArg);
System.out.println("set executor: player=" + player + " amount=" + amount);
})
.build()
)
.asRootLiteral(MessageStyle.PLAIN);
@BeforeAll
static void init() {
System.setOut(new PrintStream(outContent));
}
@AfterAll
static void cleanup() {
System.setOut(originalOut);
}
@AfterEach
void resetOut() {
outContent.reset();
}
@Test
void testDefaultExecutor() {
commandHandler.invoke(rootLiteral, new CommandContextImpl(), Stream.of());
assertEquals("default executor\n", outContent.toString());
}
@Test
void testSubLiteralNoArgs() {
commandHandler.invoke(rootLiteral, new CommandContextImpl(), Stream.of("get", "foo"));
assertEquals("get executor: player=foo\n", outContent.toString());
}
@Test
void testSubLiteralWithArgs() {
commandHandler.invoke(rootLiteral, new CommandContextImpl(), Stream.of("set", "foo", "10"));
assertEquals("set executor: player=foo amount=10\n", outContent.toString());
outContent.reset();
}
@Test
void testFailOnBadLiteral() {
commandHandler.invoke(rootLiteral, new CommandContextImpl(), Stream.of("BAD_LITERAL"));
assertEquals("Unknown literal \"BAD_LITERAL\".\n", outContent.toString());
}
@Test
void testFailOnBadArgument() {
commandHandler.invoke(rootLiteral, new CommandContextImpl(), Stream.of("set", "foo", "bar"));
assertEquals("Not a number.\n", outContent.toString());
}
@Test
void testFailOnMissingArgument() {
commandHandler.invoke(rootLiteral, new CommandContextImpl(), Stream.of("set"));
assertEquals("Argument expected.\n", outContent.toString());
}
}

View file

@ -0,0 +1,27 @@
package site.lab0x13.scrow.commands;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import java.util.List;
public class TestPlatform implements PlatformAdapter<CommandContextImpl> {
private final PlainTextComponentSerializer plainText = PlainTextComponentSerializer.plainText();
private final List<String> permissions;
public TestPlatform(List<String> permissions) {
this.permissions = permissions;
}
@Override
public boolean checkPermission(CommandContextImpl ctx, String permission) {
return permissions.contains(permission);
}
@Override
public void sendMessage(CommandContextImpl ctx, Component message) {
System.out.println(plainText.serialize(message));
}
}

2
defs.bzl Normal file
View file

@ -0,0 +1,2 @@
GROUP = "site.lab0x13.scrow"
VERSION = "0.1-SNAPSHOT"