From 7c322707baf40ca79f4e8a84d4ed3f7255411732 Mon Sep 17 00:00:00 2001 From: kento2 Date: Tue, 25 Aug 2026 15:37:39 +0200 Subject: [PATCH] initial commit --- .bazelrc | 7 ++ .bazelversion | 1 + .forgejo/workflows/publish-artifact.yml | 31 +++++ .gitignore | 6 + BUILD | 0 MODULE.bazel | 39 +++++++ bukkit/BUILD.bazel | 23 ++++ .../commands/bukkit/BukkitCommandContext.java | 24 ++++ .../scrow/commands/bukkit/BukkitPlatform.java | 16 +++ .../commands/bukkit/BukkitScrowCommand.java | 36 ++++++ .../bukkit/CommandContextFactory.java | 9 ++ .../scrow/commands/bukkit/ScrowCommands.java | 50 ++++++++ .../commands/bukkit/TabCompleteListener.java | 46 ++++++++ common/BUILD.bazel | 12 ++ common/deps.bzl | 14 +++ .../scrow/commands/CommandHandler.java | 67 +++++++++++ .../scrow/commands/IScrowCommands.java | 12 ++ .../scrow/commands/PlatformAdapter.java | 10 ++ .../model/AbstractCommandContext.java | 23 ++++ .../scrow/commands/model/CommandContext.java | 27 +++++ .../scrow/commands/model/CommandExecutor.java | 22 ++++ .../lab0x13/scrow/commands/model/Node.java | 6 + .../commands/model/SuggestionProvider.java | 16 +++ .../commands/model/argument/Argument.java | 41 +++++++ .../model/argument/ArgumentBuilder.java | 55 +++++++++ .../model/argument/ArgumentPredicate.java | 12 ++ .../model/argument/ArgumentRequirement.java | 13 +++ .../commands/model/argument/ArgumentType.java | 28 +++++ .../model/argument/DefaultValueProvider.java | 14 +++ .../model/argument/ParsedArgument.java | 14 +++ .../argument/types/BooleanArgumentType.java | 23 ++++ .../argument/types/DoubleArgumentType.java | 20 ++++ .../argument/types/IntegerArgumentType.java | 21 ++++ .../argument/types/StringArgumentType.java | 25 ++++ .../model/literal/DynamicLiteral.java | 46 ++++++++ .../model/literal/DynamicLiteralBuilder.java | 55 +++++++++ .../scrow/commands/model/literal/Literal.java | 44 +++++++ .../model/literal/LiteralBuilder.java | 53 +++++++++ .../commands/model/literal/MessageStyle.java | 32 ++++++ .../model/literal/PlainMessageStyle.java | 20 ++++ .../commands/model/literal/RootLiteral.java | 8 ++ .../model/literal/RootLiteralWrapper.java | 53 +++++++++ .../commands/model/literal/StaticLiteral.java | 38 ++++++ .../model/literal/StaticLiteralBuilder.java | 37 ++++++ .../scrow/commands/parser/NodeTreeWalker.java | 100 ++++++++++++++++ .../scrow/commands/parser/WalkResult.java | 80 +++++++++++++ common/tests/BUILD.bazel | 16 +++ .../scrow/commands/CommandContextImpl.java | 6 + .../scrow/commands/CommandHandlerTest.java | 108 ++++++++++++++++++ .../lab0x13/scrow/commands/TestPlatform.java | 27 +++++ defs.bzl | 2 + 51 files changed, 1488 insertions(+) create mode 100644 .bazelrc create mode 100644 .bazelversion create mode 100644 .forgejo/workflows/publish-artifact.yml create mode 100644 .gitignore create mode 100644 BUILD create mode 100644 MODULE.bazel create mode 100644 bukkit/BUILD.bazel create mode 100644 bukkit/site/lab0x13/scrow/commands/bukkit/BukkitCommandContext.java create mode 100644 bukkit/site/lab0x13/scrow/commands/bukkit/BukkitPlatform.java create mode 100644 bukkit/site/lab0x13/scrow/commands/bukkit/BukkitScrowCommand.java create mode 100644 bukkit/site/lab0x13/scrow/commands/bukkit/CommandContextFactory.java create mode 100644 bukkit/site/lab0x13/scrow/commands/bukkit/ScrowCommands.java create mode 100644 bukkit/site/lab0x13/scrow/commands/bukkit/TabCompleteListener.java create mode 100644 common/BUILD.bazel create mode 100644 common/deps.bzl create mode 100644 common/site/lab0x13/scrow/commands/CommandHandler.java create mode 100644 common/site/lab0x13/scrow/commands/IScrowCommands.java create mode 100644 common/site/lab0x13/scrow/commands/PlatformAdapter.java create mode 100644 common/site/lab0x13/scrow/commands/model/AbstractCommandContext.java create mode 100644 common/site/lab0x13/scrow/commands/model/CommandContext.java create mode 100644 common/site/lab0x13/scrow/commands/model/CommandExecutor.java create mode 100644 common/site/lab0x13/scrow/commands/model/Node.java create mode 100644 common/site/lab0x13/scrow/commands/model/SuggestionProvider.java create mode 100644 common/site/lab0x13/scrow/commands/model/argument/Argument.java create mode 100644 common/site/lab0x13/scrow/commands/model/argument/ArgumentBuilder.java create mode 100644 common/site/lab0x13/scrow/commands/model/argument/ArgumentPredicate.java create mode 100644 common/site/lab0x13/scrow/commands/model/argument/ArgumentRequirement.java create mode 100644 common/site/lab0x13/scrow/commands/model/argument/ArgumentType.java create mode 100644 common/site/lab0x13/scrow/commands/model/argument/DefaultValueProvider.java create mode 100644 common/site/lab0x13/scrow/commands/model/argument/ParsedArgument.java create mode 100644 common/site/lab0x13/scrow/commands/model/argument/types/BooleanArgumentType.java create mode 100644 common/site/lab0x13/scrow/commands/model/argument/types/DoubleArgumentType.java create mode 100644 common/site/lab0x13/scrow/commands/model/argument/types/IntegerArgumentType.java create mode 100644 common/site/lab0x13/scrow/commands/model/argument/types/StringArgumentType.java create mode 100644 common/site/lab0x13/scrow/commands/model/literal/DynamicLiteral.java create mode 100644 common/site/lab0x13/scrow/commands/model/literal/DynamicLiteralBuilder.java create mode 100644 common/site/lab0x13/scrow/commands/model/literal/Literal.java create mode 100644 common/site/lab0x13/scrow/commands/model/literal/LiteralBuilder.java create mode 100644 common/site/lab0x13/scrow/commands/model/literal/MessageStyle.java create mode 100644 common/site/lab0x13/scrow/commands/model/literal/PlainMessageStyle.java create mode 100644 common/site/lab0x13/scrow/commands/model/literal/RootLiteral.java create mode 100644 common/site/lab0x13/scrow/commands/model/literal/RootLiteralWrapper.java create mode 100644 common/site/lab0x13/scrow/commands/model/literal/StaticLiteral.java create mode 100644 common/site/lab0x13/scrow/commands/model/literal/StaticLiteralBuilder.java create mode 100644 common/site/lab0x13/scrow/commands/parser/NodeTreeWalker.java create mode 100644 common/site/lab0x13/scrow/commands/parser/WalkResult.java create mode 100644 common/tests/BUILD.bazel create mode 100644 common/tests/site/lab0x13/scrow/commands/CommandContextImpl.java create mode 100644 common/tests/site/lab0x13/scrow/commands/CommandHandlerTest.java create mode 100644 common/tests/site/lab0x13/scrow/commands/TestPlatform.java create mode 100644 defs.bzl diff --git a/.bazelrc b/.bazelrc new file mode 100644 index 0000000..8359bd6 --- /dev/null +++ b/.bazelrc @@ -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 diff --git a/.bazelversion b/.bazelversion new file mode 100644 index 0000000..85f864f --- /dev/null +++ b/.bazelversion @@ -0,0 +1 @@ +9.2.0 \ No newline at end of file diff --git a/.forgejo/workflows/publish-artifact.yml b/.forgejo/workflows/publish-artifact.yml new file mode 100644 index 0000000..96bade7 --- /dev/null +++ b/.forgejo/workflows/publish-artifact.yml @@ -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 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9bc6fb2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.bazelbsp/ +.idea/ +bazel-bin +bazel-genfiles +bazel-out +bazel-testlogs \ No newline at end of file diff --git a/BUILD b/BUILD new file mode 100644 index 0000000..e69de29 diff --git a/MODULE.bazel b/MODULE.bazel new file mode 100644 index 0000000..e6cad17 --- /dev/null +++ b/MODULE.bazel @@ -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") diff --git a/bukkit/BUILD.bazel b/bukkit/BUILD.bazel new file mode 100644 index 0000000..bedabe9 --- /dev/null +++ b/bukkit/BUILD.bazel @@ -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"), + ] +) diff --git a/bukkit/site/lab0x13/scrow/commands/bukkit/BukkitCommandContext.java b/bukkit/site/lab0x13/scrow/commands/bukkit/BukkitCommandContext.java new file mode 100644 index 0000000..807b733 --- /dev/null +++ b/bukkit/site/lab0x13/scrow/commands/bukkit/BukkitCommandContext.java @@ -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> extends AbstractCommandContext { + + private final CommandSender sender; + + public BukkitCommandContext(CommandSender sender) { + this.sender = sender; + } + + public CommandSender sender() { + return sender; + } + + public Player player() { + return (Player) sender(); + } + +} diff --git a/bukkit/site/lab0x13/scrow/commands/bukkit/BukkitPlatform.java b/bukkit/site/lab0x13/scrow/commands/bukkit/BukkitPlatform.java new file mode 100644 index 0000000..1f9b120 --- /dev/null +++ b/bukkit/site/lab0x13/scrow/commands/bukkit/BukkitPlatform.java @@ -0,0 +1,16 @@ +package site.lab0x13.scrow.commands.bukkit; + +import net.kyori.adventure.text.Component; +import site.lab0x13.scrow.commands.PlatformAdapter; + +class BukkitPlatform> implements PlatformAdapter { + @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); + } +} diff --git a/bukkit/site/lab0x13/scrow/commands/bukkit/BukkitScrowCommand.java b/bukkit/site/lab0x13/scrow/commands/bukkit/BukkitScrowCommand.java new file mode 100644 index 0000000..1c8bfe0 --- /dev/null +++ b/bukkit/site/lab0x13/scrow/commands/bukkit/BukkitScrowCommand.java @@ -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> extends Command { + + private final RootLiteral rootLiteral; + private final CommandContextFactory commandContextFactory; + private final CommandHandler commandHandler; + + protected BukkitScrowCommand(CommandHandler commandHandler, CommandContextFactory commandContextFactory, RootLiteral 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; + } +} diff --git a/bukkit/site/lab0x13/scrow/commands/bukkit/CommandContextFactory.java b/bukkit/site/lab0x13/scrow/commands/bukkit/CommandContextFactory.java new file mode 100644 index 0000000..f254c84 --- /dev/null +++ b/bukkit/site/lab0x13/scrow/commands/bukkit/CommandContextFactory.java @@ -0,0 +1,9 @@ +package site.lab0x13.scrow.commands.bukkit; + +import org.bukkit.command.CommandSender; + +@FunctionalInterface +public interface CommandContextFactory> { + + C createContext(CommandSender sender); +} diff --git a/bukkit/site/lab0x13/scrow/commands/bukkit/ScrowCommands.java b/bukkit/site/lab0x13/scrow/commands/bukkit/ScrowCommands.java new file mode 100644 index 0000000..b439ee5 --- /dev/null +++ b/bukkit/site/lab0x13/scrow/commands/bukkit/ScrowCommands.java @@ -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> implements IScrowCommands { + + private final CommandContextFactory commandContextFactory; + private final List> rootLiterals = new ArrayList<>(); + private final SimpleCommandMap commandMap; + private final CommandHandler commandHandler; + + public ScrowCommands(Plugin plugin, CommandContextFactory 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(new BukkitPlatform<>()); + Bukkit.getPluginManager().registerEvents(new TabCompleteListener<>(this, commandHandler, commandContextFactory), plugin); + } + + @Override + public void register(RootLiteral rootLiteral) { + rootLiterals.add(rootLiteral); + commandMap.register(rootLiteral.names().getFirst(), + new BukkitScrowCommand<>(commandHandler, commandContextFactory, rootLiteral)); + } + + @Override + public @Nullable RootLiteral getRootLiteral(String name) { + return rootLiterals.stream() + .filter(literal -> literal.names().contains(name)) + .findAny() + .orElse(null); + } +} diff --git a/bukkit/site/lab0x13/scrow/commands/bukkit/TabCompleteListener.java b/bukkit/site/lab0x13/scrow/commands/bukkit/TabCompleteListener.java new file mode 100644 index 0000000..0f559eb --- /dev/null +++ b/bukkit/site/lab0x13/scrow/commands/bukkit/TabCompleteListener.java @@ -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> implements Listener { + + private final ScrowCommands commandAPI; + private final CommandHandler commandHandler; + private final CommandContextFactory commandContextFactory; + + public TabCompleteListener(ScrowCommands commandAPI, CommandHandler commandHandler, CommandContextFactory 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 ':' 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(" ") + )); + } +} diff --git a/common/BUILD.bazel b/common/BUILD.bazel new file mode 100644 index 0000000..582bce1 --- /dev/null +++ b/common/BUILD.bazel @@ -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, +) diff --git a/common/deps.bzl b/common/deps.bzl new file mode 100644 index 0000000..713fe14 --- /dev/null +++ b/common/deps.bzl @@ -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"), +] \ No newline at end of file diff --git a/common/site/lab0x13/scrow/commands/CommandHandler.java b/common/site/lab0x13/scrow/commands/CommandHandler.java new file mode 100644 index 0000000..70c1dbc --- /dev/null +++ b/common/site/lab0x13/scrow/commands/CommandHandler.java @@ -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> { + + private final NodeTreeWalker nodeTreeWalker; + private final PlatformAdapter platformAdapter; + + public CommandHandler(PlatformAdapter platformAdapter) { + this.nodeTreeWalker = new NodeTreeWalker<>(platformAdapter); + this.platformAdapter = platformAdapter; + } + + public void invoke(RootLiteral rootLiteral, C ctx, Stream 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 suggest(RootLiteral rootLiteral, C ctx, String[] args, boolean trailingSpace) { + var walkResult = nodeTreeWalker.walk(ctx, rootLiteral, Arrays.stream(args).iterator()); + Stream stream; + String input = null; + switch (walkResult) { + case WalkResult.LiteralUnknown res -> { + stream = res.lastLiteral().subLiteralNames().stream(); + input = res.input(); + } + case WalkResult.ArgumentIllegal res -> { + stream = res.arg().suggestionProvider().suggest(ctx, res.input()).stream(); + input = res.input(); + } + case WalkResult.LiteralExpected res when trailingSpace -> + stream = res.lastLiteral().subLiteralNames().stream(); + case WalkResult.ArgumentExpected 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(); + } +} \ No newline at end of file diff --git a/common/site/lab0x13/scrow/commands/IScrowCommands.java b/common/site/lab0x13/scrow/commands/IScrowCommands.java new file mode 100644 index 0000000..9a65f2c --- /dev/null +++ b/common/site/lab0x13/scrow/commands/IScrowCommands.java @@ -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> { + + void register(RootLiteral rootLiteral); + + @Nullable RootLiteral getRootLiteral(String name); +} diff --git a/common/site/lab0x13/scrow/commands/PlatformAdapter.java b/common/site/lab0x13/scrow/commands/PlatformAdapter.java new file mode 100644 index 0000000..7718f02 --- /dev/null +++ b/common/site/lab0x13/scrow/commands/PlatformAdapter.java @@ -0,0 +1,10 @@ +package site.lab0x13.scrow.commands; + +import net.kyori.adventure.text.Component; + +public interface PlatformAdapter { + + boolean checkPermission(C ctx, String permission); + + void sendMessage(C ctx, Component message); +} diff --git a/common/site/lab0x13/scrow/commands/model/AbstractCommandContext.java b/common/site/lab0x13/scrow/commands/model/AbstractCommandContext.java new file mode 100644 index 0000000..43a489e --- /dev/null +++ b/common/site/lab0x13/scrow/commands/model/AbstractCommandContext.java @@ -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> implements CommandContext { + + private final Map> parsedArguments = new HashMap<>(); + + @SuppressWarnings("unchecked") + @Override + public ParsedArgument getArgument(String id) { + return (ParsedArgument) parsedArguments.get(id); + } + + @Override + public void addParsedArgument(Argument argument, String rawValue, V value) { + parsedArguments.put(argument.id(), new ParsedArgument<>(argument, rawValue, value)); + } +} diff --git a/common/site/lab0x13/scrow/commands/model/CommandContext.java b/common/site/lab0x13/scrow/commands/model/CommandContext.java new file mode 100644 index 0000000..e6d18d2 --- /dev/null +++ b/common/site/lab0x13/scrow/commands/model/CommandContext.java @@ -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 argument of this + */ +public interface CommandContext> { + + ParsedArgument getArgument(String id); + + default V getArg(String id) { + ParsedArgument arg = getArgument(id); + return arg.value(); + } + + default V getArg(Argument arg) { + return getArg(arg.id()); + } + + default ParsedArgument getArgument(Argument arg) { + return getArgument(arg.id()); + } + + void addParsedArgument(Argument argument, String rawValue, V value); +} diff --git a/common/site/lab0x13/scrow/commands/model/CommandExecutor.java b/common/site/lab0x13/scrow/commands/model/CommandExecutor.java new file mode 100644 index 0000000..e509f0c --- /dev/null +++ b/common/site/lab0x13/scrow/commands/model/CommandExecutor.java @@ -0,0 +1,22 @@ +package site.lab0x13.scrow.commands.model; + +import java.util.concurrent.CompletableFuture; + +/** + * @param argument of command context + */ +public interface CommandExecutor> { + + CompletableFuture execute(C ctx); + + interface Sync> extends CommandExecutor { + + void executeSync(C ctx); + + @Override + default CompletableFuture execute(C ctx) { + executeSync(ctx); + return CompletableFuture.completedFuture(Void.TYPE); + } + } +} diff --git a/common/site/lab0x13/scrow/commands/model/Node.java b/common/site/lab0x13/scrow/commands/model/Node.java new file mode 100644 index 0000000..84ccbe4 --- /dev/null +++ b/common/site/lab0x13/scrow/commands/model/Node.java @@ -0,0 +1,6 @@ +package site.lab0x13.scrow.commands.model; + +public interface Node> { + + SuggestionProvider suggestionProvider(); +} diff --git a/common/site/lab0x13/scrow/commands/model/SuggestionProvider.java b/common/site/lab0x13/scrow/commands/model/SuggestionProvider.java new file mode 100644 index 0000000..1af962c --- /dev/null +++ b/common/site/lab0x13/scrow/commands/model/SuggestionProvider.java @@ -0,0 +1,16 @@ +package site.lab0x13.scrow.commands.model; + +import java.util.Collections; +import java.util.List; + +/** + * @param argument of command context + */ +public interface SuggestionProvider> { + + static > SuggestionProvider none() { + return (_, _) -> Collections.emptyList(); + } + + List suggest(C ctx, String input); +} diff --git a/common/site/lab0x13/scrow/commands/model/argument/Argument.java b/common/site/lab0x13/scrow/commands/model/argument/Argument.java new file mode 100644 index 0000000..094b7b1 --- /dev/null +++ b/common/site/lab0x13/scrow/commands/model/argument/Argument.java @@ -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 argument of command context + * @param argument of argument value + */ +public record Argument, V>( + String id, + ArgumentType type, + List> requirements, + SuggestionProvider suggestionProvider, + DefaultValueProvider defaultValueProvider +) implements Node { + + public static , V> ArgumentBuilder builder(String id, ArgumentType type) { + return new ArgumentBuilder<>(id, type); + } + + public Result, 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)); + } +} diff --git a/common/site/lab0x13/scrow/commands/model/argument/ArgumentBuilder.java b/common/site/lab0x13/scrow/commands/model/argument/ArgumentBuilder.java new file mode 100644 index 0000000..c975c64 --- /dev/null +++ b/common/site/lab0x13/scrow/commands/model/argument/ArgumentBuilder.java @@ -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 argument of command context + * @param argument of argument value + */ +public final class ArgumentBuilder, V> { + + private final String id; + private final ArgumentType type; + private DefaultValueProvider defaultValueProvider = DefaultValueProvider.none(); + private @Nullable SuggestionProvider suggestionProvider = null; + + private final List> requirements = new ArrayList<>(); + + ArgumentBuilder(String id, ArgumentType type) { + this.id = id; + this.type = type; + } + + public ArgumentBuilder withRequirement(ArgumentPredicate predicate, String errorMessage) { + this.requirements.add(new ArgumentRequirement<>(predicate, errorMessage)); + return this; + } + + public ArgumentBuilder withDefaultValue(@Nullable DefaultValueProvider defaultValue) { + this.defaultValueProvider = defaultValue; + return this; + } + + /** + * @param suggestionProvider if null, use the argument's suggestion provider + */ + public ArgumentBuilder withSuggestionProvider(@Nullable SuggestionProvider suggestionProvider) { + this.suggestionProvider = suggestionProvider; + return this; + } + + public Argument build() { + return new Argument<>( + id, + type, + new ArrayList<>(requirements), + suggestionProvider != null ? suggestionProvider : type.defaultSuggestionProvider(), + defaultValueProvider + ); + } +} diff --git a/common/site/lab0x13/scrow/commands/model/argument/ArgumentPredicate.java b/common/site/lab0x13/scrow/commands/model/argument/ArgumentPredicate.java new file mode 100644 index 0000000..68fd33d --- /dev/null +++ b/common/site/lab0x13/scrow/commands/model/argument/ArgumentPredicate.java @@ -0,0 +1,12 @@ +package site.lab0x13.scrow.commands.model.argument; + +import site.lab0x13.scrow.commands.model.CommandContext; + +/** + * @param argument of command context + * @param argument of argument value + */ +@FunctionalInterface +public interface ArgumentPredicate, V> { + boolean check(C ctx, V value); +} diff --git a/common/site/lab0x13/scrow/commands/model/argument/ArgumentRequirement.java b/common/site/lab0x13/scrow/commands/model/argument/ArgumentRequirement.java new file mode 100644 index 0000000..237f964 --- /dev/null +++ b/common/site/lab0x13/scrow/commands/model/argument/ArgumentRequirement.java @@ -0,0 +1,13 @@ +package site.lab0x13.scrow.commands.model.argument; + +import site.lab0x13.scrow.commands.model.CommandContext; + +/** + * @param argument of command context + * @param argument of argument value + */ +public record ArgumentRequirement, V>( + ArgumentPredicate predicate, + String errorMessage +) { +} diff --git a/common/site/lab0x13/scrow/commands/model/argument/ArgumentType.java b/common/site/lab0x13/scrow/commands/model/argument/ArgumentType.java new file mode 100644 index 0000000..5f37223 --- /dev/null +++ b/common/site/lab0x13/scrow/commands/model/argument/ArgumentType.java @@ -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 argument of command context + * @param argument of parsed argument value + */ +public interface ArgumentType, V> { + + Result parseInput(String input); + + /** + * @return default suggestion provider for the argument, if not overwritten by the argument + */ + SuggestionProvider 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; + } +} diff --git a/common/site/lab0x13/scrow/commands/model/argument/DefaultValueProvider.java b/common/site/lab0x13/scrow/commands/model/argument/DefaultValueProvider.java new file mode 100644 index 0000000..093c021 --- /dev/null +++ b/common/site/lab0x13/scrow/commands/model/argument/DefaultValueProvider.java @@ -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, V> { + + static > DefaultValueProvider none() { + return _ -> null; + } + + @Nullable V getDefault(C ctx); +} diff --git a/common/site/lab0x13/scrow/commands/model/argument/ParsedArgument.java b/common/site/lab0x13/scrow/commands/model/argument/ParsedArgument.java new file mode 100644 index 0000000..9460666 --- /dev/null +++ b/common/site/lab0x13/scrow/commands/model/argument/ParsedArgument.java @@ -0,0 +1,14 @@ +package site.lab0x13.scrow.commands.model.argument; + +import site.lab0x13.scrow.commands.model.CommandContext; + +/** + * @param argument of command context + * @param argument of argument value + */ +public record ParsedArgument, V>( + Argument argument, + String rawValue, + V value +) { +} diff --git a/common/site/lab0x13/scrow/commands/model/argument/types/BooleanArgumentType.java b/common/site/lab0x13/scrow/commands/model/argument/types/BooleanArgumentType.java new file mode 100644 index 0000000..7d2eb85 --- /dev/null +++ b/common/site/lab0x13/scrow/commands/model/argument/types/BooleanArgumentType.java @@ -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> implements ArgumentType { + + @Override + public Result parseInput(String input) { + return Results.ofCallable(() -> Boolean.parseBoolean(input)) + .mapFailure(_ -> "Must be true or false."); + } + + @Override + public SuggestionProvider defaultSuggestionProvider() { + return (_, _) -> List.of("true", "false"); + } +} diff --git a/common/site/lab0x13/scrow/commands/model/argument/types/DoubleArgumentType.java b/common/site/lab0x13/scrow/commands/model/argument/types/DoubleArgumentType.java new file mode 100644 index 0000000..d37cdc6 --- /dev/null +++ b/common/site/lab0x13/scrow/commands/model/argument/types/DoubleArgumentType.java @@ -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> implements ArgumentType { + @Override + public Result parseInput(String input) { + return Results.ofCallable(() -> Double.parseDouble(input)) + .mapFailure(_ -> "Not a valid double."); + } + + @Override + public SuggestionProvider defaultSuggestionProvider() { + return SuggestionProvider.none(); + } +} diff --git a/common/site/lab0x13/scrow/commands/model/argument/types/IntegerArgumentType.java b/common/site/lab0x13/scrow/commands/model/argument/types/IntegerArgumentType.java new file mode 100644 index 0000000..0a01a5d --- /dev/null +++ b/common/site/lab0x13/scrow/commands/model/argument/types/IntegerArgumentType.java @@ -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> implements ArgumentType { + + @Override + public Result parseInput(String string) { + return Results.ofCallable(() -> Integer.parseInt(string)) + .mapFailure(_ -> "Not a number."); + } + + @Override + public SuggestionProvider defaultSuggestionProvider() { + return SuggestionProvider.none(); + } +} diff --git a/common/site/lab0x13/scrow/commands/model/argument/types/StringArgumentType.java b/common/site/lab0x13/scrow/commands/model/argument/types/StringArgumentType.java new file mode 100644 index 0000000..ecfb637 --- /dev/null +++ b/common/site/lab0x13/scrow/commands/model/argument/types/StringArgumentType.java @@ -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>(boolean greedy) + implements ArgumentType { + + public StringArgumentType() { + this(false); + } + + @Override + public Result parseInput(String string) { + return Results.success(string); + } + + @Override + public SuggestionProvider defaultSuggestionProvider() { + return SuggestionProvider.none(); + } +} diff --git a/common/site/lab0x13/scrow/commands/model/literal/DynamicLiteral.java b/common/site/lab0x13/scrow/commands/model/literal/DynamicLiteral.java new file mode 100644 index 0000000..e67e7af --- /dev/null +++ b/common/site/lab0x13/scrow/commands/model/literal/DynamicLiteral.java @@ -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> implements Literal{ + + private final List names; + private final @Nullable CommandExecutor executor; + private final List> arguments; + private final String permission; + + public DynamicLiteral(List names, @Nullable CommandExecutor executor, List> 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 names() { + return names; + } + + @Override + public @Nullable CommandExecutor executor() { + return executor; + } + + @Override + public List> arguments() { + return arguments; + } + + @Override + public String permission() { + return permission; + } +} diff --git a/common/site/lab0x13/scrow/commands/model/literal/DynamicLiteralBuilder.java b/common/site/lab0x13/scrow/commands/model/literal/DynamicLiteralBuilder.java new file mode 100644 index 0000000..46b3bd0 --- /dev/null +++ b/common/site/lab0x13/scrow/commands/model/literal/DynamicLiteralBuilder.java @@ -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> extends LiteralBuilder> { + + private Supplier> subLiteralNamesSupplier = Collections::emptyList; + private Function> subLiteralProvider = _ -> null; + + DynamicLiteralBuilder(String... names) { + super(names); + } + + @Override + protected DynamicLiteralBuilder getThis() { + return this; + } + + @Override + public Literal build() { + return new DynamicLiteral<>( + Arrays.asList(this.names), + this.executor, + this.arguments, + this.permission + ) { + @Override + public @Nullable Literal getSubLiteral(String name) { + return subLiteralProvider.apply(name); + } + + @Override + public List subLiteralNames() { + return subLiteralNamesSupplier.get(); + } + }; + } + + public DynamicLiteralBuilder withSubLiteralNames(Supplier> names) { + subLiteralNamesSupplier = names; + return this; + } + + public DynamicLiteralBuilder withSubLiteralProvider(Function> literalProvider) { + this.subLiteralProvider = literalProvider; + return this; + } +} diff --git a/common/site/lab0x13/scrow/commands/model/literal/Literal.java b/common/site/lab0x13/scrow/commands/model/literal/Literal.java new file mode 100644 index 0000000..a8107ef --- /dev/null +++ b/common/site/lab0x13/scrow/commands/model/literal/Literal.java @@ -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 argument of the command context + */ +public interface Literal> extends Node { + + static > StaticLiteralBuilder builder(String... names) { + return new StaticLiteralBuilder<>(names); + } + + static > DynamicLiteralBuilder dynamic(String... names) { + return new DynamicLiteralBuilder<>(names); + } + + List names(); + + @Nullable CommandExecutor executor(); + + @Nullable Literal getSubLiteral(String name); + + /** + * @return aliases/names of literals + */ + List subLiteralNames(); + + List> arguments(); + + String permission(); + + @Override + default SuggestionProvider suggestionProvider() { + return (_, _) -> subLiteralNames(); + } +} \ No newline at end of file diff --git a/common/site/lab0x13/scrow/commands/model/literal/LiteralBuilder.java b/common/site/lab0x13/scrow/commands/model/literal/LiteralBuilder.java new file mode 100644 index 0000000..025c221 --- /dev/null +++ b/common/site/lab0x13/scrow/commands/model/literal/LiteralBuilder.java @@ -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 argument of command context + * @param argument of this/builder + */ +public abstract class LiteralBuilder,B extends LiteralBuilder> { + + protected final String[] names; + protected @Nullable CommandExecutor executor; + protected @Nullable String permission; + protected final List> arguments = new ArrayList<>(); + + protected abstract B getThis(); + + public abstract Literal build(); + + protected LiteralBuilder(String... names) { + this.names = names; + } + + public final RootLiteral asRootLiteral(MessageStyle style) { + return new RootLiteralWrapper<>(build(), style); + } + + public final B withExecutor(@Nullable CommandExecutor executor) { + this.executor = executor; + return getThis(); + } + + public final B withSyncExecutor(@Nullable CommandExecutor.Sync executor) { + this.executor = executor; + return getThis(); + } + + public final B withPermission(@Nullable String permission) { + this.permission = permission; + return getThis(); + } + + public final B withArgument(Argument argument) { + arguments.add(argument); + return getThis(); + } +} diff --git a/common/site/lab0x13/scrow/commands/model/literal/MessageStyle.java b/common/site/lab0x13/scrow/commands/model/literal/MessageStyle.java new file mode 100644 index 0000000..b1e754c --- /dev/null +++ b/common/site/lab0x13/scrow/commands/model/literal/MessageStyle.java @@ -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()); + } +} diff --git a/common/site/lab0x13/scrow/commands/model/literal/PlainMessageStyle.java b/common/site/lab0x13/scrow/commands/model/literal/PlainMessageStyle.java new file mode 100644 index 0000000..82440ec --- /dev/null +++ b/common/site/lab0x13/scrow/commands/model/literal/PlainMessageStyle.java @@ -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); + } +} diff --git a/common/site/lab0x13/scrow/commands/model/literal/RootLiteral.java b/common/site/lab0x13/scrow/commands/model/literal/RootLiteral.java new file mode 100644 index 0000000..2472815 --- /dev/null +++ b/common/site/lab0x13/scrow/commands/model/literal/RootLiteral.java @@ -0,0 +1,8 @@ +package site.lab0x13.scrow.commands.model.literal; + +import site.lab0x13.scrow.commands.model.CommandContext; + +public interface RootLiteral> extends Literal{ + + MessageStyle style(); +} diff --git a/common/site/lab0x13/scrow/commands/model/literal/RootLiteralWrapper.java b/common/site/lab0x13/scrow/commands/model/literal/RootLiteralWrapper.java new file mode 100644 index 0000000..02a07e8 --- /dev/null +++ b/common/site/lab0x13/scrow/commands/model/literal/RootLiteralWrapper.java @@ -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> implements RootLiteral { + private final Literal literal; + private final MessageStyle messageStyle; + + public RootLiteralWrapper(Literal literal, MessageStyle messageStyle) { + this.literal = literal; + this.messageStyle = messageStyle; + } + + @Override + public MessageStyle style() { + return messageStyle; + } + + @Override + public List names() { + return literal.names(); + } + + @Override + public @Nullable CommandExecutor executor() { + return literal.executor(); + } + + @Override + public @Nullable Literal getSubLiteral(String name) { + return literal.getSubLiteral(name); + } + + @Override + public List subLiteralNames() { + return literal.subLiteralNames(); + } + + @Override + public List> arguments() { + return literal.arguments(); + } + + @Override + public String permission() { + return literal.permission(); + } +} diff --git a/common/site/lab0x13/scrow/commands/model/literal/StaticLiteral.java b/common/site/lab0x13/scrow/commands/model/literal/StaticLiteral.java new file mode 100644 index 0000000..83f9d03 --- /dev/null +++ b/common/site/lab0x13/scrow/commands/model/literal/StaticLiteral.java @@ -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> extends DynamicLiteral { + + private final List> subLiterals; + + public StaticLiteral( + List names, + @Nullable CommandExecutor executor, + List> arguments, + String permission, List> subLiterals + ) { + super(names, executor, arguments, permission); + this.subLiterals = subLiterals; + } + + @Override + public @Nullable Literal getSubLiteral(String name) { + return subLiterals.stream() + .filter(literal -> literal.names().contains(name)) + .findAny() + .orElse(null); + } + + @Override + public List subLiteralNames() { + return subLiterals.stream() + .flatMap(literal -> literal.names().stream()) + .toList(); + } +} diff --git a/common/site/lab0x13/scrow/commands/model/literal/StaticLiteralBuilder.java b/common/site/lab0x13/scrow/commands/model/literal/StaticLiteralBuilder.java new file mode 100644 index 0000000..47fa348 --- /dev/null +++ b/common/site/lab0x13/scrow/commands/model/literal/StaticLiteralBuilder.java @@ -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> extends LiteralBuilder> { + + private final List> subLiterals = new ArrayList<>(); + + StaticLiteralBuilder(String... names) { + super(names); + } + + @Override + protected StaticLiteralBuilder getThis() { + return this; + } + + @Override + public Literal build() { + return new StaticLiteral<>( + Arrays.asList(this.names), + this.executor, + this.arguments, + this.permission, + this.subLiterals + ); + } + + public StaticLiteralBuilder withSubLiteral(Literal subLiteral) { + subLiterals.add(subLiteral); + return this; + } +} diff --git a/common/site/lab0x13/scrow/commands/parser/NodeTreeWalker.java b/common/site/lab0x13/scrow/commands/parser/NodeTreeWalker.java new file mode 100644 index 0000000..f58c678 --- /dev/null +++ b/common/site/lab0x13/scrow/commands/parser/NodeTreeWalker.java @@ -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> { + + private final PlatformAdapter platformAdapter; + + public NodeTreeWalker(PlatformAdapter platformAdapter) { + this.platformAdapter = platformAdapter; + } + + public WalkResult walk( + C ctx, + RootLiteral rootLiteral, + Iterator iter + ) { + var cursor = new Cursor(iter); + Literal literal = rootLiteral; + String token; + + while (true) { + for (var arg : literal.arguments()) { + //noinspection unchecked + var res = walkArgument(ctx, cursor, literal, (Argument) 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 walkArgument( + C ctx, + Cursor cursor, + Literal literal, + Argument 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 tokenIter; + + public Cursor(Iterator 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(); + } + } +} diff --git a/common/site/lab0x13/scrow/commands/parser/WalkResult.java b/common/site/lab0x13/scrow/commands/parser/WalkResult.java new file mode 100644 index 0000000..87a0223 --- /dev/null +++ b/common/site/lab0x13/scrow/commands/parser/WalkResult.java @@ -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 ctx(); + + Literal lastLiteral(); + + boolean isValidUsage(); + + @Nullable String message(); + + record ArgumentIllegal>( + C ctx, + Literal lastLiteral, + boolean isValidUsage, + String message, + Argument arg, + String input + ) implements WalkResult { + } + + record ArgumentExpected>( + C ctx, + Literal lastLiteral, + boolean isValidUsage, + Argument arg + ) implements WalkResult { + @Override + public String message() { + return "Argument expected."; + } + } + + record LiteralExpected>( + C ctx, + Literal lastLiteral, + boolean isValidUsage + ) implements WalkResult { + @Override + public String message() { + return "Literal expected."; + } + } + + record LiteralUnknown>( + C ctx, + Literal lastLiteral, + String input + ) implements WalkResult { + @Override + public boolean isValidUsage() { + return false; + } + + @Override + public String message() { + return "Unknown literal \"" + input + "\"."; + } + } + + record Complete>( + C ctx, + Literal lastLiteral + ) implements WalkResult { + @Override + public boolean isValidUsage() { + return true; + } + + @Override + public @Nullable String message() { + return null; + } + } +} diff --git a/common/tests/BUILD.bazel b/common/tests/BUILD.bazel new file mode 100644 index 0000000..80c84da --- /dev/null +++ b/common/tests/BUILD.bazel @@ -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, +) \ No newline at end of file diff --git a/common/tests/site/lab0x13/scrow/commands/CommandContextImpl.java b/common/tests/site/lab0x13/scrow/commands/CommandContextImpl.java new file mode 100644 index 0000000..c84cd61 --- /dev/null +++ b/common/tests/site/lab0x13/scrow/commands/CommandContextImpl.java @@ -0,0 +1,6 @@ +package site.lab0x13.scrow.commands; + +import site.lab0x13.scrow.commands.model.AbstractCommandContext; + +public class CommandContextImpl extends AbstractCommandContext { +} diff --git a/common/tests/site/lab0x13/scrow/commands/CommandHandlerTest.java b/common/tests/site/lab0x13/scrow/commands/CommandHandlerTest.java new file mode 100644 index 0000000..e41e2a2 --- /dev/null +++ b/common/tests/site/lab0x13/scrow/commands/CommandHandlerTest.java @@ -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 commandHandler = new CommandHandler<>(new TestPlatform(Collections.emptyList())); + private final Argument playerArg = + Argument.builder("player", new StringArgumentType()) + .build(); + private final Argument amountArg = + Argument.builder("amount", new IntegerArgumentType()) + .build(); + private final RootLiteral rootLiteral = Literal.builder("coins") + .withSyncExecutor(_ -> { + System.out.println("default executor"); + }) + .withSubLiteral(Literal.builder("get") + .withArgument(playerArg) + .withSyncExecutor(ctx -> { + var player = ctx.getArg(playerArg); + System.out.println("get executor: player=" + player); + }) + .build() + ) + .withSubLiteral(Literal.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()); + } +} diff --git a/common/tests/site/lab0x13/scrow/commands/TestPlatform.java b/common/tests/site/lab0x13/scrow/commands/TestPlatform.java new file mode 100644 index 0000000..0fc7ebf --- /dev/null +++ b/common/tests/site/lab0x13/scrow/commands/TestPlatform.java @@ -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 { + + private final PlainTextComponentSerializer plainText = PlainTextComponentSerializer.plainText(); + + private final List permissions; + + public TestPlatform(List 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)); + } +} diff --git a/defs.bzl b/defs.bzl new file mode 100644 index 0000000..c84a8a9 --- /dev/null +++ b/defs.bzl @@ -0,0 +1,2 @@ +GROUP = "site.lab0x13.scrow" +VERSION = "0.1-SNAPSHOT" \ No newline at end of file